summaryrefslogtreecommitdiff
path: root/1.1.x/test/etap
diff options
context:
space:
mode:
authorRobert Newson <rnewson@apache.org>2011-05-17 11:15:14 +0000
committerRobert Newson <rnewson@apache.org>2011-05-17 11:15:14 +0000
commite8e4b0d293021fe90326a85828f3cfb087bf18b7 (patch)
tree986f544eac623ec23b769b36828894f93a173aa3 /1.1.x/test/etap
parentda6a5322b0b8084f434752060caa8be214c6f4fa (diff)
tagging 1.1.0
git-svn-id: https://svn.apache.org/repos/asf/couchdb/tags/1.1.0@1104149 13f79535-47bb-0310-9956-ffa450edef68
Diffstat (limited to '1.1.x/test/etap')
-rwxr-xr-x1.1.x/test/etap/001-load.t68
-rw-r--r--1.1.x/test/etap/002-icu-driver.t33
-rwxr-xr-x1.1.x/test/etap/010-file-basics.t108
-rwxr-xr-x1.1.x/test/etap/011-file-headers.t145
-rwxr-xr-x1.1.x/test/etap/020-btree-basics.t205
-rwxr-xr-x1.1.x/test/etap/021-btree-reductions.t141
-rwxr-xr-x1.1.x/test/etap/030-doc-from-json.t236
-rwxr-xr-x1.1.x/test/etap/031-doc-to-json.t197
-rwxr-xr-x1.1.x/test/etap/040-util.t80
-rw-r--r--1.1.x/test/etap/041-uuid-gen-seq.ini19
-rw-r--r--1.1.x/test/etap/041-uuid-gen-utc.ini19
-rwxr-xr-x1.1.x/test/etap/041-uuid-gen.t118
-rwxr-xr-x1.1.x/test/etap/050-stream.t87
-rwxr-xr-x1.1.x/test/etap/060-kt-merging.t115
-rwxr-xr-x1.1.x/test/etap/061-kt-missing-leaves.t65
-rwxr-xr-x1.1.x/test/etap/062-kt-remove-leaves.t69
-rwxr-xr-x1.1.x/test/etap/063-kt-get-leaves.t98
-rwxr-xr-x1.1.x/test/etap/064-kt-counting.t46
-rwxr-xr-x1.1.x/test/etap/065-kt-stemming.t42
-rwxr-xr-x1.1.x/test/etap/070-couch-db.t73
-rwxr-xr-x1.1.x/test/etap/080-config-get-set.t128
-rw-r--r--1.1.x/test/etap/081-config-override.1.ini22
-rw-r--r--1.1.x/test/etap/081-config-override.2.ini22
-rwxr-xr-x1.1.x/test/etap/081-config-override.t212
-rwxr-xr-x1.1.x/test/etap/082-config-register.t94
-rwxr-xr-x1.1.x/test/etap/083-config-no-files.t55
-rwxr-xr-x1.1.x/test/etap/090-task-status.t209
-rwxr-xr-x1.1.x/test/etap/100-ref-counter.t114
-rwxr-xr-x1.1.x/test/etap/110-replication-httpc.t132
-rwxr-xr-x1.1.x/test/etap/111-replication-changes-feed.t253
-rwxr-xr-x1.1.x/test/etap/112-replication-missing-revs.t207
-rwxr-xr-x1.1.x/test/etap/113-replication-attachment-comp.t314
-rwxr-xr-x1.1.x/test/etap/120-stats-collect.t150
-rw-r--r--1.1.x/test/etap/121-stats-aggregates.cfg19
-rw-r--r--1.1.x/test/etap/121-stats-aggregates.ini20
-rwxr-xr-x1.1.x/test/etap/121-stats-aggregates.t171
-rwxr-xr-x1.1.x/test/etap/130-attachments-md5.t248
-rwxr-xr-x1.1.x/test/etap/140-attachment-comp.t762
-rwxr-xr-x1.1.x/test/etap/150-invalid-view-seq.t190
-rwxr-xr-x1.1.x/test/etap/160-vhosts.t291
-rwxr-xr-x1.1.x/test/etap/170-os-daemons.es26
-rwxr-xr-x1.1.x/test/etap/170-os-daemons.t114
-rwxr-xr-x1.1.x/test/etap/171-os-daemons-config.es83
-rwxr-xr-x1.1.x/test/etap/171-os-daemons-config.t74
-rw-r--r--1.1.x/test/etap/172-os-daemon-errors.1.es22
-rwxr-xr-x1.1.x/test/etap/172-os-daemon-errors.2.es16
-rwxr-xr-x1.1.x/test/etap/172-os-daemon-errors.3.es17
-rwxr-xr-x1.1.x/test/etap/172-os-daemon-errors.4.es17
-rwxr-xr-x1.1.x/test/etap/172-os-daemon-errors.t126
-rwxr-xr-x1.1.x/test/etap/173-os-daemon-cfg-register.t93
-rw-r--r--1.1.x/test/etap/180-http-proxy.ini20
-rwxr-xr-x1.1.x/test/etap/180-http-proxy.t378
-rwxr-xr-x1.1.x/test/etap/200-view-group-no-db-leaks.t262
-rw-r--r--1.1.x/test/etap/Makefile.am88
-rw-r--r--1.1.x/test/etap/random_port.ini19
-rw-r--r--1.1.x/test/etap/run.tpl27
-rw-r--r--1.1.x/test/etap/test_cfg_register.c30
-rw-r--r--1.1.x/test/etap/test_util.erl.in42
-rw-r--r--1.1.x/test/etap/test_web.erl99
59 files changed, 7130 insertions, 0 deletions
diff --git a/1.1.x/test/etap/001-load.t b/1.1.x/test/etap/001-load.t
new file mode 100755
index 00000000..6f49e1ba
--- /dev/null
+++ b/1.1.x/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_can:loaded_ok(
+ Module,
+ lists:concat(["Loaded: ", Module])
+ )
+ end, Modules),
+ etap:end_tests().
diff --git a/1.1.x/test/etap/002-icu-driver.t b/1.1.x/test/etap/002-icu-driver.t
new file mode 100644
index 00000000..d70f3303
--- /dev/null
+++ b/1.1.x/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(
+ couch_util:start_driver("src/couchdb/priv/.libs"),
+ 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/1.1.x/test/etap/010-file-basics.t b/1.1.x/test/etap/010-file-basics.t
new file mode 100755
index 00000000..ed71f5e8
--- /dev/null
+++ b/1.1.x/test/etap/010-file-basics.t
@@ -0,0 +1,108 @@
+#!/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">>]),
+ {ok, IoList} = couch_file:pread_iolist(Fd, IOLPos),
+ etap:is(<<"foombam">>, iolist_to_binary(IoList),
+ "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/1.1.x/test/etap/011-file-headers.t b/1.1.x/test/etap/011-file-headers.t
new file mode 100755
index 00000000..4705f629
--- /dev/null
+++ b/1.1.x/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/1.1.x/test/etap/020-btree-basics.t b/1.1.x/test/etap/020-btree-basics.t
new file mode 100755
index 00000000..18c4a836
--- /dev/null
+++ b/1.1.x/test/etap/020-btree-basics.t
@@ -0,0 +1,205 @@
+#!/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(),
+ etap:plan(48),
+ 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."),
+ 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.
+
+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/1.1.x/test/etap/021-btree-reductions.t b/1.1.x/test/etap/021-btree-reductions.t
new file mode 100755
index 00000000..331e49af
--- /dev/null
+++ b/1.1.x/test/etap/021-btree-reductions.t
@@ -0,0 +1,141 @@
+#!/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/etap/temp.021".
+rows() -> 1000.
+
+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()
+ 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 forward 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 forward 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/1.1.x/test/etap/030-doc-from-json.t b/1.1.x/test/etap/030-doc-from-json.t
new file mode 100755
index 00000000..b0c393ef
--- /dev/null
+++ b/1.1.x/test/etap/030-doc-from-json.t
@@ -0,0 +1,236 @@
+#!/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() ->
+ couch_config:start_link(test_util:config_files()),
+ couch_config:set("attachments", "compression_level", "0", false),
+ 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/1.1.x/test/etap/031-doc-to-json.t b/1.1.x/test/etap/031-doc-to-json.t
new file mode 100755
index 00000000..ce950f95
--- /dev/null
+++ b/1.1.x/test/etap/031-doc-to-json.t
@@ -0,0 +1,197 @@
+#!/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() ->
+ couch_config:start_link(test_util:config_files()),
+ couch_config:set("attachments", "compression_level", "0", false),
+ 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/1.1.x/test/etap/040-util.t b/1.1.x/test/etap/040-util.t
new file mode 100755
index 00000000..8f80db87
--- /dev/null
+++ b/1.1.x/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/1.1.x/test/etap/041-uuid-gen-seq.ini b/1.1.x/test/etap/041-uuid-gen-seq.ini
new file mode 100644
index 00000000..94cebc6f
--- /dev/null
+++ b/1.1.x/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/1.1.x/test/etap/041-uuid-gen-utc.ini b/1.1.x/test/etap/041-uuid-gen-utc.ini
new file mode 100644
index 00000000..c2b83831
--- /dev/null
+++ b/1.1.x/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/1.1.x/test/etap/041-uuid-gen.t b/1.1.x/test/etap/041-uuid-gen.t
new file mode 100755
index 00000000..1e6aa9ee
--- /dev/null
+++ b/1.1.x/test/etap/041-uuid-gen.t
@@ -0,0 +1,118 @@
+#!/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() ->
+ test_util:build_file("etc/couchdb/default_dev.ini").
+
+seq_alg_config() ->
+ test_util:source_file("test/etap/041-uuid-gen-seq.ini").
+
+utc_alg_config() ->
+ test_util:source_file("test/etap/041-uuid-gen-utc.ini").
+
+% Run tests and wait for the gen_servers to shutdown
+run_test(IniFiles, Test) ->
+ {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(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() ->
+
+ TestUnique = fun() ->
+ etap:is(
+ test_unique(10000, couch_uuids:new()),
+ true,
+ "Can generate 10K unique IDs"
+ )
+ end,
+ run_test([default_config()], TestUnique),
+ run_test([default_config(), seq_alg_config()], TestUnique),
+ run_test([default_config(), utc_alg_config()], TestUnique),
+
+ TestMonotonic = fun () ->
+ etap:is(
+ couch_uuids:new() < couch_uuids:new(),
+ true,
+ "should produce monotonically increasing ids"
+ )
+ end,
+ run_test([default_config(), seq_alg_config()], TestMonotonic),
+ run_test([default_config(), 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([default_config(), 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/1.1.x/test/etap/050-stream.t b/1.1.x/test/etap/050-stream.t
new file mode 100755
index 00000000..d30b524a
--- /dev/null
+++ b/1.1.x/test/etap/050-stream.t
@@ -0,0 +1,87 @@
+#!/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("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, 8}], "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, 20}], "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, 4100}, {SecondPtr, 1020}], "Pointers every 4K bytes."),
+ etap:is(Length3, 5120, "Wrote the expected 5K bytes."),
+
+ couch_file:close(Fd),
+ ok.
diff --git a/1.1.x/test/etap/060-kt-merging.t b/1.1.x/test/etap/060-kt-merging.t
new file mode 100755
index 00000000..0e481a52
--- /dev/null
+++ b/1.1.x/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/1.1.x/test/etap/061-kt-missing-leaves.t b/1.1.x/test/etap/061-kt-missing-leaves.t
new file mode 100755
index 00000000..d60b4db8
--- /dev/null
+++ b/1.1.x/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/1.1.x/test/etap/062-kt-remove-leaves.t b/1.1.x/test/etap/062-kt-remove-leaves.t
new file mode 100755
index 00000000..745a00be
--- /dev/null
+++ b/1.1.x/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/1.1.x/test/etap/063-kt-get-leaves.t b/1.1.x/test/etap/063-kt-get-leaves.t
new file mode 100755
index 00000000..6d4e8007
--- /dev/null
+++ b/1.1.x/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/1.1.x/test/etap/064-kt-counting.t b/1.1.x/test/etap/064-kt-counting.t
new file mode 100755
index 00000000..f182d287
--- /dev/null
+++ b/1.1.x/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/1.1.x/test/etap/065-kt-stemming.t b/1.1.x/test/etap/065-kt-stemming.t
new file mode 100755
index 00000000..6e781c1d
--- /dev/null
+++ b/1.1.x/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/1.1.x/test/etap/070-couch-db.t b/1.1.x/test/etap/070-couch-db.t
new file mode 100755
index 00000000..787d6c6a
--- /dev/null
+++ b/1.1.x/test/etap/070-couch-db.t
@@ -0,0 +1,73 @@
+#!/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() ->
+
+ couch_server_sup:start_link(test_util:config_files()),
+
+ couch_db:create(<<"etap-test-db">>, []),
+ {ok, AllDbs} = couch_server:all_databases(),
+ etap:ok(lists:member(<<"etap-test-db">>, AllDbs), "Database was created."),
+
+ couch_server:delete(<<"etap-test-db">>, []),
+ {ok, AllDbs2} = couch_server:all_databases(),
+ etap:ok(not lists:member(<<"etap-test-db">>, AllDbs2),
+ "Database was deleted."),
+
+ gen_server:call(couch_server, {set_max_dbs_open, 3}),
+ MkDbName = fun(Int) -> list_to_binary("lru-" ++ integer_to_list(Int)) end,
+
+ lists:foreach(fun(Int) ->
+ {ok, TestDbs} = couch_server:all_databases(),
+ ok = case lists:member(MkDbName(Int), TestDbs) of
+ true -> couch_server:delete(MkDbName(Int), []);
+ _ -> ok
+ end,
+ {ok, Db} = couch_db:create(MkDbName(Int), []),
+ ok = couch_db:close(Db)
+ end, lists:seq(1, 6)),
+
+ {ok, AllDbs3} = couch_server:all_databases(),
+ NumCreated = lists:foldl(fun(Int, Acc) ->
+ true = lists:member(MkDbName(Int), AllDbs3),
+ Acc+1
+ end, 0, lists:seq(1, 6)),
+ etap:is(6, NumCreated, "Created all databases."),
+
+ lists:foreach(fun(Int) ->
+ ok = couch_server:delete(MkDbName(Int), [])
+ end, lists:seq(1, 6)),
+
+ {ok, AllDbs4} = couch_server:all_databases(),
+ NumDeleted = lists:foldl(fun(Int, Acc) ->
+ false = lists:member(MkDbName(Int), AllDbs4),
+ Acc+1
+ end, 0, lists:seq(1, 6)),
+ etap:is(6, NumDeleted, "Deleted all databases."),
+
+ ok.
diff --git a/1.1.x/test/etap/080-config-get-set.t b/1.1.x/test/etap/080-config-get-set.t
new file mode 100755
index 00000000..a4a8577a
--- /dev/null
+++ b/1.1.x/test/etap/080-config-get-set.t
@@ -0,0 +1,128 @@
+#!/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() ->
+ test_util:build_file("etc/couchdb/default_dev.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: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/1.1.x/test/etap/081-config-override.1.ini b/1.1.x/test/etap/081-config-override.1.ini
new file mode 100644
index 00000000..55451dad
--- /dev/null
+++ b/1.1.x/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/1.1.x/test/etap/081-config-override.2.ini b/1.1.x/test/etap/081-config-override.2.ini
new file mode 100644
index 00000000..5f46357f
--- /dev/null
+++ b/1.1.x/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/1.1.x/test/etap/081-config-override.t b/1.1.x/test/etap/081-config-override.t
new file mode 100755
index 00000000..01f8b4c2
--- /dev/null
+++ b/1.1.x/test/etap/081-config-override.t
@@ -0,0 +1,212 @@
+#!/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() ->
+ test_util:build_file("etc/couchdb/default_dev.ini").
+
+local_config_1() ->
+ test_util:source_file("test/etap/081-config-override.1.ini").
+
+local_config_2() ->
+ test_util:source_file("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) ->
+ {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/1.1.x/test/etap/082-config-register.t b/1.1.x/test/etap/082-config-register.t
new file mode 100755
index 00000000..191ba8f8
--- /dev/null
+++ b/1.1.x/test/etap/082-config-register.t
@@ -0,0 +1,94 @@
+#!/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() ->
+ test_util:build_file("etc/couchdb/default_dev.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: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/1.1.x/test/etap/083-config-no-files.t b/1.1.x/test/etap/083-config-no-files.t
new file mode 100755
index 00000000..bc40ec9d
--- /dev/null
+++ b/1.1.x/test/etap/083-config-no-files.t
@@ -0,0 +1,55 @@
+#!/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() ->
+ test_util:build_file("etc/couchdb/default_dev.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: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", false),
+ 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/1.1.x/test/etap/090-task-status.t b/1.1.x/test/etap/090-task-status.t
new file mode 100755
index 00000000..b278de7f
--- /dev/null
+++ b/1.1.x/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/1.1.x/test/etap/100-ref-counter.t b/1.1.x/test/etap/100-ref-counter.t
new file mode 100755
index 00000000..8f996d04
--- /dev/null
+++ b/1.1.x/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/1.1.x/test/etap/110-replication-httpc.t b/1.1.x/test/etap/110-replication-httpc.t
new file mode 100755
index 00000000..39b0755e
--- /dev/null
+++ b/1.1.x/test/etap/110-replication-httpc.t
@@ -0,0 +1,132 @@
+#!/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.
+
+%% XXX: Figure out how to -include("couch_rep.hrl")
+-record(http_db, {
+ url,
+ auth = [],
+ resource = "",
+ headers = [
+ {"User-Agent", "CouchDB/"++couch_server:get_version()},
+ {"Accept", "application/json"},
+ {"Accept-Encoding", "gzip"}
+ ],
+ qs = [],
+ method = get,
+ body = nil,
+ options = [
+ {response_format,binary},
+ {inactivity_timeout, 30000}
+ ],
+ retries = 10,
+ pause = 1,
+ conn = nil
+}).
+
+server() ->
+ lists:concat([
+ "http://127.0.0.1:", mochiweb_socket_server:get(couch_httpd, port), "/"
+ ]).
+
+dbname() -> "etap-test-db".
+
+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() ->
+ couch_server_sup:start_link(test_util:config_files()),
+ ibrowse:start(),
+ crypto:start(),
+
+ couch_server:delete(list_to_binary(dbname()), []),
+ {ok, Db} = couch_db:create(list_to_binary(dbname()), []),
+
+ test_welcome(),
+ test_binary_url(),
+ test_put(),
+ test_qs(),
+ test_db_exists(),
+
+ couch_db:close(Db),
+ couch_server:delete(list_to_binary(dbname()), []),
+ ok.
+
+test_welcome() ->
+ WelcomeReq = #http_db{url=server()},
+ Expect = {[
+ {<<"couchdb">>, <<"Welcome">>},
+ {<<"version">>, list_to_binary(couch_server:get_version())}
+ ]},
+ etap:is(
+ couch_rep_httpc:request(WelcomeReq),
+ Expect,
+ "welcome request with url-as-list"
+ ).
+
+test_binary_url() ->
+ Req = #http_db{url=list_to_binary(server())},
+ Expect = {[
+ {<<"couchdb">>, <<"Welcome">>},
+ {<<"version">>, list_to_binary(couch_server:get_version())}
+ ]},
+ etap:is(
+ couch_rep_httpc:request(Req),
+ Expect,
+ "welcome request with url-as-binary"
+ ).
+
+test_put() ->
+ Req = #http_db{
+ url = server() ++ dbname() ++ "/",
+ resource = "test_put",
+ body = {[{<<"foo">>, <<"bar">>}]},
+ method = put
+ },
+ {Resp} = couch_rep_httpc:request(Req),
+ etap:ok(couch_util:get_value(<<"ok">>, Resp), "ok:true on upload"),
+ etap:is(<<"test_put">>, couch_util:get_value(<<"id">>, Resp), "id is correct").
+
+test_qs() ->
+ Req = #http_db{
+ url = server() ++ dbname() ++ "/",
+ resource = "foo",
+ qs = [
+ {bar, true},
+ {baz, 1.03},
+ {bif, mochijson2:encode(<<"1-23456">>)}
+ ]
+ },
+ Expect = server() ++ dbname() ++ "/foo?bar=true&baz=1.03&bif=\"1-23456\"",
+ etap:is(
+ couch_rep_httpc:full_url(Req),
+ Expect,
+ "query-string proplist encoding ok"
+ ).
+
+test_db_exists() ->
+ Req1 = #http_db{url=server() ++ dbname() ++ "/"},
+ Req2 = #http_db{url=server() ++ dbname() ++ "_foo/"},
+ etap:is(couch_rep_httpc:db_exists(Req1), Req1, "db_exists true check").
+ % etap:is(couch_rep_httpc:db_exists(Req2), false, "db_exists false check").
diff --git a/1.1.x/test/etap/111-replication-changes-feed.t b/1.1.x/test/etap/111-replication-changes-feed.t
new file mode 100755
index 00000000..358bf1e2
--- /dev/null
+++ b/1.1.x/test/etap/111-replication-changes-feed.t
@@ -0,0 +1,253 @@
+#!/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.
+
+%% XXX: Figure out how to -include("couch_db.hrl")
+-record(doc, {id= <<"">>, revs={0, []}, body={[]},
+ attachments=[], deleted=false, meta=[]}).
+
+-record(http_db, {
+ url,
+ auth = [],
+ resource = "",
+ headers = [
+ {"User-Agent", "CouchDB/"++couch_server:get_version()},
+ {"Accept", "application/json"},
+ {"Accept-Encoding", "gzip"}
+ ],
+ qs = [],
+ method = get,
+ body = nil,
+ options = [
+ {response_format,binary},
+ {inactivity_timeout, 30000}
+ ],
+ retries = 10,
+ pause = 1,
+ conn = nil
+}).
+
+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.
+
+test() ->
+ couch_server_sup:start_link(test_util:config_files()),
+ ibrowse:start(),
+ crypto:start(),
+
+ couch_server:delete(<<"etap-test-db">>, []),
+ {ok, Db1} = couch_db:create(<<"etap-test-db">>, []),
+ test_all(local),
+ couch_db:close(Db1),
+ couch_server:delete(<<"etap-test-db">>, []),
+
+ couch_server:delete(<<"etap-test-db">>, []),
+ {ok, Db2} = couch_db:create(<<"etap-test-db">>, []),
+ test_all(remote),
+ test_remote_only(),
+ couch_db:close(Db2),
+ couch_server:delete(<<"etap-test-db">>, []),
+
+ ok.
+
+test_all(Type) ->
+ test_unchanged_db(Type),
+ test_simple_change(Type),
+ test_since_parameter(Type),
+ test_continuous_parameter(Type),
+ test_conflicts(Type),
+ test_deleted_conflicts(Type).
+
+test_remote_only() ->
+ test_chunk_reassembly(remote).
+
+test_unchanged_db(Type) ->
+ {ok, Pid} = start_changes_feed(Type, 0, false),
+ etap:is(
+ couch_rep_changes_feed:next(Pid),
+ complete,
+ io_lib:format(
+ "(~p) changes feed for unchanged DB is automatically complete",
+ [Type])
+ ).
+
+test_simple_change(Type) ->
+ Expect = generate_change(),
+ {ok, Pid} = start_changes_feed(Type, 0, false),
+ etap:is(
+ {couch_rep_changes_feed:next(Pid), couch_rep_changes_feed:next(Pid)},
+ {[Expect], complete},
+ io_lib:format("(~p) change one document, get one row", [Type])
+ ).
+
+test_since_parameter(Type) ->
+ {ok, Pid} = start_changes_feed(Type, get_update_seq(), false),
+ etap:is(
+ couch_rep_changes_feed:next(Pid),
+ complete,
+ io_lib:format(
+ "(~p) since query-string parameter allows us to skip changes",
+ [Type])
+ ).
+
+test_continuous_parameter(Type) ->
+ {ok, Pid} = start_changes_feed(Type, get_update_seq(), true),
+
+ % make the changes_feed request before the next update
+ Self = self(),
+ spawn(fun() ->
+ Change = couch_rep_changes_feed:next(Pid),
+ Self ! {actual, Change}
+ end),
+
+ Expect = generate_change(),
+ etap:is(
+ receive {actual, Actual} -> Actual end,
+ [Expect],
+ io_lib:format(
+ "(~p) feed=continuous query-string parameter picks up new changes",
+ [Type])
+ ),
+
+ ok = couch_rep_changes_feed:stop(Pid).
+
+test_conflicts(Type) ->
+ Since = get_update_seq(),
+ Expect = generate_conflict(),
+ {ok, Pid} = start_changes_feed(Type, Since, false),
+ etap:is(
+ {couch_rep_changes_feed:next(Pid), couch_rep_changes_feed:next(Pid)},
+ {[Expect], complete},
+ io_lib:format("(~p) conflict revisions show up in feed", [Type])
+ ).
+
+test_deleted_conflicts(Type) ->
+ Since = get_update_seq(),
+ {ExpectProps} = generate_conflict(),
+
+ %% delete the conflict revision
+ Id = couch_util:get_value(<<"id">>, ExpectProps),
+ [Win, {[{<<"rev">>, Lose}]}] = couch_util:get_value(<<"changes">>, ExpectProps),
+ Doc = couch_doc:from_json_obj({[
+ {<<"_id">>, Id},
+ {<<"_rev">>, Lose},
+ {<<"_deleted">>, true}
+ ]}),
+ Db = get_db(),
+ {ok, Rev} = couch_db:update_doc(Db, Doc, [full_commit]),
+ couch_db:close(Db),
+
+ Expect = {[
+ {<<"seq">>, get_update_seq()},
+ {<<"id">>, Id},
+ {<<"changes">>, [Win, {[{<<"rev">>, couch_doc:rev_to_str(Rev)}]}]}
+ ]},
+
+ {ok, Pid} = start_changes_feed(Type, Since, false),
+ etap:is(
+ {couch_rep_changes_feed:next(Pid), couch_rep_changes_feed:next(Pid)},
+ {[Expect], complete},
+ io_lib:format("(~p) deleted conflict revisions show up in feed", [Type])
+ ).
+
+test_chunk_reassembly(Type) ->
+ Since = get_update_seq(),
+ Expect = [generate_change() || _I <- lists:seq(1,30)],
+ {ok, Pid} = start_changes_feed(Type, Since, false),
+ etap:is(
+ get_all_changes(Pid, []),
+ Expect,
+ io_lib:format("(~p) reassembles chunks split across TCP frames",
+ [Type])
+ ).
+
+get_all_changes(Pid, Acc) ->
+ case couch_rep_changes_feed:next(Pid) of
+ complete ->
+ lists:flatten(lists:reverse(Acc));
+ Else ->
+ get_all_changes(Pid, [Else|Acc])
+ end.
+
+generate_change() ->
+ generate_change(couch_uuids:random()).
+
+generate_change(Id) ->
+ generate_change(Id, {[]}).
+
+generate_change(Id, EJson) ->
+ Doc = couch_doc:from_json_obj(EJson),
+ Db = get_db(),
+ {ok, Rev} = couch_db:update_doc(Db, Doc#doc{id = Id}, [full_commit]),
+ couch_db:close(Db),
+ {[
+ {<<"seq">>, get_update_seq()},
+ {<<"id">>, Id},
+ {<<"changes">>, [{[{<<"rev">>, couch_doc:rev_to_str(Rev)}]}]}
+ ]}.
+
+generate_conflict() ->
+ Id = couch_uuids:random(),
+ Db = get_db(),
+ Doc1 = (couch_doc:from_json_obj({[<<"foo">>, <<"bar">>]}))#doc{id = Id},
+ Doc2 = (couch_doc:from_json_obj({[<<"foo">>, <<"baz">>]}))#doc{id = Id},
+ {ok, Rev1} = couch_db:update_doc(Db, Doc1, [full_commit]),
+ {ok, Rev2} = couch_db:update_doc(Db, Doc2, [full_commit, all_or_nothing]),
+
+ %% relies on undocumented CouchDB conflict winner algo and revision sorting!
+ RevList = [{[{<<"rev">>, couch_doc:rev_to_str(R)}]} || R
+ <- lists:sort(fun(A,B) -> B<A end, [Rev1,Rev2])],
+ {[
+ {<<"seq">>, get_update_seq()},
+ {<<"id">>, Id},
+ {<<"changes">>, RevList}
+ ]}.
+
+get_db() ->
+ {ok, Db} = couch_db:open(<<"etap-test-db">>, []),
+ Db.
+
+get_dbname(local) ->
+ "etap-test-db";
+get_dbname(remote) ->
+ server() ++ "etap-test-db/".
+
+server() ->
+ lists:concat([
+ "http://127.0.0.1:", mochiweb_socket_server:get(couch_httpd, port), "/"
+ ]).
+
+get_update_seq() ->
+ Db = get_db(),
+ Seq = couch_db:get_update_seq(Db),
+ couch_db:close(Db),
+ Seq.
+
+start_changes_feed(local, Since, Continuous) ->
+ Props = [{<<"continuous">>, Continuous}],
+ couch_rep_changes_feed:start_link(self(), get_db(), Since, Props);
+start_changes_feed(remote, Since, Continuous) ->
+ Props = [{<<"continuous">>, Continuous}],
+ Db = #http_db{url = get_dbname(remote)},
+ couch_rep_changes_feed:start_link(self(), Db, Since, Props).
diff --git a/1.1.x/test/etap/112-replication-missing-revs.t b/1.1.x/test/etap/112-replication-missing-revs.t
new file mode 100755
index 00000000..39280aee
--- /dev/null
+++ b/1.1.x/test/etap/112-replication-missing-revs.t
@@ -0,0 +1,207 @@
+#!/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.
+
+%% XXX: Figure out how to -include("couch_db.hrl")
+
+-record(doc, {id= <<"">>, revs={0, []}, body={[]},
+ attachments=[], deleted=false, meta=[]}).
+
+-record(http_db, {
+ url,
+ auth = [],
+ resource = "",
+ headers = [
+ {"User-Agent", "CouchDB/"++couch_server:get_version()},
+ {"Accept", "application/json"},
+ {"Accept-Encoding", "gzip"}
+ ],
+ qs = [],
+ method = get,
+ body = nil,
+ options = [
+ {response_format,binary},
+ {inactivity_timeout, 30000}
+ ],
+ retries = 10,
+ pause = 1,
+ conn = nil
+}).
+
+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() ->
+ couch_server_sup:start_link(test_util:config_files()),
+ ibrowse:start(),
+ crypto:start(),
+
+ couch_server:delete(<<"etap-test-source">>, []),
+ couch_server:delete(<<"etap-test-target">>, []),
+
+ Dbs1 = setup(),
+ test_all(local, local),
+ ok = teardown(Dbs1),
+
+ Dbs2 = setup(),
+ test_all(local, remote),
+ ok = teardown(Dbs2),
+
+ Dbs3 = setup(),
+ test_all(remote, local),
+ ok = teardown(Dbs3),
+
+ Dbs4 = setup(),
+ test_all(remote, remote),
+ ok = teardown(Dbs4),
+
+ ok.
+
+test_all(SrcType, TgtType) ->
+ test_unchanged_db(SrcType, TgtType),
+ test_multiple_changes(SrcType, TgtType),
+ test_changes_not_missing(SrcType, TgtType).
+
+test_unchanged_db(SrcType, TgtType) ->
+ {ok, Pid1} = start_changes_feed(SrcType, 0, false),
+ {ok, Pid2} = start_missing_revs(TgtType, Pid1),
+ etap:is(
+ couch_rep_missing_revs:next(Pid2),
+ complete,
+ io_lib:format(
+ "(~p, ~p) no missing revs if source is unchanged",
+ [SrcType, TgtType])
+ ).
+
+test_multiple_changes(SrcType, TgtType) ->
+ Expect = {2, [generate_change(), generate_change()]},
+ {ok, Pid1} = start_changes_feed(SrcType, 0, false),
+ {ok, Pid2} = start_missing_revs(TgtType, Pid1),
+ etap:is(
+ get_all_missing_revs(Pid2, {0, []}),
+ Expect,
+ io_lib:format("(~p, ~p) add src docs, get missing tgt revs + high seq",
+ [SrcType, TgtType])
+ ).
+
+test_changes_not_missing(SrcType, TgtType) ->
+ %% put identical changes on source and target
+ Id = couch_uuids:random(),
+ {Id, _Seq, [Rev]} = Expect = generate_change(Id, {[]}, get_db(source)),
+ {Id, _, [Rev]} = generate_change(Id, {[]}, get_db(target)),
+
+ %% confirm that this change is not in missing revs feed
+ {ok, Pid1} = start_changes_feed(SrcType, 0, false),
+ {ok, Pid2} = start_missing_revs(TgtType, Pid1),
+ {HighSeq, AllRevs} = get_all_missing_revs(Pid2, {0, []}),
+
+ %% etap:none/3 has a bug, so just define it correctly here
+ etap:is(
+ lists:member(Expect, AllRevs),
+ false,
+ io_lib:format(
+ "(~p, ~p) skip revs that already exist on target",
+ [SrcType, TgtType])
+ ).
+
+generate_change() ->
+ generate_change(couch_uuids:random()).
+
+generate_change(Id) ->
+ generate_change(Id, {[]}).
+
+generate_change(Id, EJson) ->
+ generate_change(Id, EJson, get_db(source)).
+
+generate_change(Id, EJson, Db) ->
+ Doc = couch_doc:from_json_obj(EJson),
+ Seq = get_update_seq(),
+ {ok, Rev} = couch_db:update_doc(Db, Doc#doc{id = Id}, [full_commit]),
+ couch_db:close(Db),
+ {Id, Seq+1, [Rev]}.
+
+get_all_missing_revs(Pid, {HighSeq, Revs}) ->
+ case couch_rep_missing_revs:next(Pid) of
+ complete ->
+ {HighSeq, lists:flatten(lists:reverse(Revs))};
+ {Seq, More} ->
+ get_all_missing_revs(Pid, {Seq, [More|Revs]})
+ end.
+
+get_db(source) ->
+ {ok, Db} = couch_db:open(<<"etap-test-source">>, []),
+ Db;
+get_db(target) ->
+ {ok, Db} = couch_db:open(<<"etap-test-target">>, []),
+ Db.
+
+get_update_seq() ->
+ Db = get_db(source),
+ Seq = couch_db:get_update_seq(Db),
+ couch_db:close(Db),
+ Seq.
+
+setup() ->
+ {ok, DbA} = couch_db:create(<<"etap-test-source">>, []),
+ {ok, DbB} = couch_db:create(<<"etap-test-target">>, []),
+ [DbA, DbB].
+
+teardown([DbA, DbB]) ->
+ couch_db:close(DbA),
+ couch_db:close(DbB),
+ couch_server:delete(<<"etap-test-source">>, []),
+ couch_server:delete(<<"etap-test-target">>, []),
+ ok.
+
+start_changes_feed(local, Since, Continuous) ->
+ Props = [{<<"continuous">>, Continuous}],
+ couch_rep_changes_feed:start_link(self(), get_db(source), Since, Props);
+start_changes_feed(remote, Since, Continuous) ->
+ Props = [{<<"continuous">>, Continuous}],
+ Db = #http_db{url = server() ++ "etap-test-source/"},
+ couch_rep_changes_feed:start_link(self(), Db, Since, Props).
+
+server() ->
+ lists:concat([
+ "http://127.0.0.1:", mochiweb_socket_server:get(couch_httpd, port), "/"
+ ]).
+
+couch_rep_pid(Db) ->
+ spawn(fun() -> couch_rep_pid_loop(Db) end).
+
+couch_rep_pid_loop(Db) ->
+ receive
+ {'$gen_call', From, get_target_db} ->
+ gen_server:reply(From, {ok, Db})
+ end,
+ couch_rep_pid_loop(Db).
+
+start_missing_revs(local, Changes) ->
+ TargetDb = get_db(target),
+ MainPid = couch_rep_pid(TargetDb),
+ couch_rep_missing_revs:start_link(MainPid, TargetDb, Changes, []);
+start_missing_revs(remote, Changes) ->
+ TargetDb = #http_db{url = server() ++ "etap-test-target/"},
+ MainPid = couch_rep_pid(TargetDb),
+ couch_rep_missing_revs:start_link(MainPid, TargetDb, Changes, []).
diff --git a/1.1.x/test/etap/113-replication-attachment-comp.t b/1.1.x/test/etap/113-replication-attachment-comp.t
new file mode 100755
index 00000000..e30a96bc
--- /dev/null
+++ b/1.1.x/test/etap/113-replication-attachment-comp.t
@@ -0,0 +1,314 @@
+#!/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.
+
+-record(user_ctx, {
+ name = null,
+ roles = [],
+ handler
+}).
+
+test_db_a_name() ->
+ <<"couch_test_rep_att_comp_a">>.
+
+test_db_b_name() ->
+ <<"couch_test_rep_att_comp_b">>.
+
+main(_) ->
+ test_util:init_code_path(),
+ etap:plan(45),
+ 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_server_sup:start_link(test_util:config_files()),
+ put(addr, couch_config:get("httpd", "bind_address", "127.0.0.1")),
+ put(port, integer_to_list(mochiweb_socket_server:get(couch_httpd, port))),
+ application:start(inets),
+ ibrowse:start(),
+ timer:sleep(1000),
+
+ %
+ % test pull replication
+ %
+
+ delete_db(test_db_a_name()),
+ delete_db(test_db_b_name()),
+ create_db(test_db_a_name()),
+ create_db(test_db_b_name()),
+
+ % enable compression
+ couch_config:set("attachments", "compression_level", "8", false),
+ couch_config:set("attachments", "compressible_types", "text/*", false),
+
+ % store doc with text attachment in DB A
+ put_text_att(test_db_a_name()),
+
+ % disable attachment compression
+ couch_config:set("attachments", "compression_level", "0", false),
+
+ % do pull replication
+ do_pull_replication(test_db_a_name(), test_db_b_name()),
+
+ % verify that DB B has the attachment stored in compressed form
+ check_att_is_compressed(test_db_b_name()),
+ check_server_can_decompress_att(test_db_b_name()),
+ check_att_stubs(test_db_a_name(), test_db_b_name()),
+
+ %
+ % test push replication
+ %
+
+ delete_db(test_db_a_name()),
+ delete_db(test_db_b_name()),
+ create_db(test_db_a_name()),
+ create_db(test_db_b_name()),
+
+ % enable compression
+ couch_config:set("attachments", "compression_level", "8", false),
+ couch_config:set("attachments", "compressible_types", "text/*", false),
+
+ % store doc with text attachment in DB A
+ put_text_att(test_db_a_name()),
+
+ % disable attachment compression
+ couch_config:set("attachments", "compression_level", "0", false),
+
+ % do push replication
+ do_push_replication(test_db_a_name(), test_db_b_name()),
+
+ % verify that DB B has the attachment stored in compressed form
+ check_att_is_compressed(test_db_b_name()),
+ check_server_can_decompress_att(test_db_b_name()),
+ check_att_stubs(test_db_a_name(), test_db_b_name()),
+
+ %
+ % test local replication
+ %
+
+ delete_db(test_db_a_name()),
+ delete_db(test_db_b_name()),
+ create_db(test_db_a_name()),
+ create_db(test_db_b_name()),
+
+ % enable compression
+ couch_config:set("attachments", "compression_level", "8", false),
+ couch_config:set("attachments", "compressible_types", "text/*", false),
+
+ % store doc with text attachment in DB A
+ put_text_att(test_db_a_name()),
+
+ % disable attachment compression
+ couch_config:set("attachments", "compression_level", "0", false),
+
+ % do local-local replication
+ do_local_replication(test_db_a_name(), test_db_b_name()),
+
+ % verify that DB B has the attachment stored in compressed form
+ check_att_is_compressed(test_db_b_name()),
+ check_server_can_decompress_att(test_db_b_name()),
+ check_att_stubs(test_db_a_name(), test_db_b_name()),
+
+ timer:sleep(3000), % to avoid mochiweb socket closed exceptions
+ delete_db(test_db_a_name()),
+ delete_db(test_db_b_name()),
+ couch_server_sup:stop(),
+ ok.
+
+put_text_att(DbName) ->
+ {ok, {{_, Code, _}, _Headers, _Body}} = http:request(
+ put,
+ {db_url(DbName) ++ "/testdoc1/readme.txt", [],
+ "text/plain", test_text_data()},
+ [],
+ [{sync, true}]),
+ etap:is(Code, 201, "Created text attachment"),
+ ok.
+
+do_pull_replication(SourceDbName, TargetDbName) ->
+ RepObj = {[
+ {<<"source">>, list_to_binary(db_url(SourceDbName))},
+ {<<"target">>, TargetDbName}
+ ]},
+ {ok, {{_, Code, _}, _Headers, Body}} = http:request(
+ post,
+ {rep_url(), [],
+ "application/json", list_to_binary(couch_util:json_encode(RepObj))},
+ [],
+ [{sync, true}]),
+ etap:is(Code, 200, "Pull replication successfully triggered"),
+ Json = couch_util:json_decode(Body),
+ RepOk = couch_util:get_nested_json_value(Json, [<<"ok">>]),
+ etap:is(RepOk, true, "Pull replication completed with success"),
+ ok.
+
+do_push_replication(SourceDbName, TargetDbName) ->
+ RepObj = {[
+ {<<"source">>, SourceDbName},
+ {<<"target">>, list_to_binary(db_url(TargetDbName))}
+ ]},
+ {ok, {{_, Code, _}, _Headers, Body}} = http:request(
+ post,
+ {rep_url(), [],
+ "application/json", list_to_binary(couch_util:json_encode(RepObj))},
+ [],
+ [{sync, true}]),
+ etap:is(Code, 200, "Push replication successfully triggered"),
+ Json = couch_util:json_decode(Body),
+ RepOk = couch_util:get_nested_json_value(Json, [<<"ok">>]),
+ etap:is(RepOk, true, "Push replication completed with success"),
+ ok.
+
+do_local_replication(SourceDbName, TargetDbName) ->
+ RepObj = {[
+ {<<"source">>, SourceDbName},
+ {<<"target">>, TargetDbName}
+ ]},
+ {ok, {{_, Code, _}, _Headers, Body}} = http:request(
+ post,
+ {rep_url(), [],
+ "application/json", list_to_binary(couch_util:json_encode(RepObj))},
+ [],
+ [{sync, true}]),
+ etap:is(Code, 200, "Local replication successfully triggered"),
+ Json = couch_util:json_decode(Body),
+ RepOk = couch_util:get_nested_json_value(Json, [<<"ok">>]),
+ etap:is(RepOk, true, "Local replication completed with success"),
+ ok.
+
+check_att_is_compressed(DbName) ->
+ {ok, {{_, Code, _}, Headers, Body}} = http:request(
+ get,
+ {db_url(DbName) ++ "/testdoc1/readme.txt",
+ [{"Accept-Encoding", "gzip"}]},
+ [],
+ [{sync, true}]),
+ etap:is(Code, 200, "HTTP response code for the attachment request is 200"),
+ Gziped = lists:member({"content-encoding", "gzip"}, Headers),
+ etap:is(Gziped, true, "The attachment was received in compressed form"),
+ Uncompressed = binary_to_list(zlib:gunzip(list_to_binary(Body))),
+ etap:is(
+ Uncompressed,
+ test_text_data(),
+ "The attachment content is valid after decompression at the client side"
+ ),
+ ok.
+
+check_server_can_decompress_att(DbName) ->
+ {ok, {{_, Code, _}, Headers, Body}} = http:request(
+ get,
+ {db_url(DbName) ++ "/testdoc1/readme.txt", []},
+ [],
+ [{sync, true}]),
+ etap:is(Code, 200, "HTTP response code for the attachment request is 200"),
+ Gziped = lists:member({"content-encoding", "gzip"}, Headers),
+ etap:is(
+ Gziped, false, "The attachment was not received in compressed form"
+ ),
+ etap:is(
+ Body,
+ test_text_data(),
+ "The attachment content is valid after server decompression"
+ ),
+ ok.
+
+check_att_stubs(SourceDbName, TargetDbName) ->
+ {ok, {{_, Code1, _}, _Headers1, Body1}} = http:request(
+ get,
+ {db_url(SourceDbName) ++ "/testdoc1?att_encoding_info=true", []},
+ [],
+ [{sync, true}]),
+ etap:is(
+ Code1,
+ 200,
+ "HTTP response code is 200 for the source DB doc request"
+ ),
+ Json1 = couch_util:json_decode(Body1),
+ SourceAttStub = couch_util:get_nested_json_value(
+ Json1,
+ [<<"_attachments">>, <<"readme.txt">>]
+ ),
+ {ok, {{_, Code2, _}, _Headers2, Body2}} = http:request(
+ get,
+ {db_url(TargetDbName) ++ "/testdoc1?att_encoding_info=true", []},
+ [],
+ [{sync, true}]),
+ etap:is(
+ Code2,
+ 200,
+ "HTTP response code is 200 for the target DB doc request"
+ ),
+ Json2 = couch_util:json_decode(Body2),
+ TargetAttStub = couch_util:get_nested_json_value(
+ Json2,
+ [<<"_attachments">>, <<"readme.txt">>]
+ ),
+ IdenticalStubs = (SourceAttStub =:= TargetAttStub),
+ etap:is(IdenticalStubs, true, "Attachment stubs are identical"),
+ TargetAttStubLength = couch_util:get_nested_json_value(
+ TargetAttStub,
+ [<<"length">>]
+ ),
+ TargetAttStubEnc = couch_util:get_nested_json_value(
+ TargetAttStub,
+ [<<"encoding">>]
+ ),
+ etap:is(
+ TargetAttStubEnc,
+ <<"gzip">>,
+ "Attachment stub has encoding property set to gzip"
+ ),
+ TargetAttStubEncLength = couch_util:get_nested_json_value(
+ TargetAttStub,
+ [<<"encoded_length">>]
+ ),
+ EncLengthDefined = is_integer(TargetAttStubEncLength),
+ etap:is(
+ EncLengthDefined,
+ true,
+ "Stubs have the encoded_length field properly defined"
+ ),
+ EncLengthSmaller = (TargetAttStubEncLength < TargetAttStubLength),
+ etap:is(
+ EncLengthSmaller,
+ true,
+ "Stubs have the encoded_length field smaller than their length field"
+ ),
+ ok.
+
+admin_user_ctx() ->
+ {user_ctx, #user_ctx{roles=[<<"_admin">>]}}.
+
+create_db(DbName) ->
+ {ok, _} = couch_db:create(DbName, [admin_user_ctx()]).
+
+delete_db(DbName) ->
+ couch_server:delete(DbName, [admin_user_ctx()]).
+
+db_url(DbName) ->
+ "http://" ++ get(addr) ++ ":" ++ get(port) ++ "/" ++
+ binary_to_list(DbName).
+
+rep_url() ->
+ "http://" ++ get(addr) ++ ":" ++ get(port) ++ "/_replicate".
+
+test_text_data() ->
+ {ok, Data} = file:read_file(test_util:source_file("README")),
+ binary_to_list(Data).
diff --git a/1.1.x/test/etap/120-stats-collect.t b/1.1.x/test/etap/120-stats-collect.t
new file mode 100755
index 00000000..dee88765
--- /dev/null
+++ b/1.1.x/test/etap/120-stats-collect.t
@@ -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/1.1.x/test/etap/121-stats-aggregates.cfg b/1.1.x/test/etap/121-stats-aggregates.cfg
new file mode 100644
index 00000000..30e475da
--- /dev/null
+++ b/1.1.x/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/1.1.x/test/etap/121-stats-aggregates.ini b/1.1.x/test/etap/121-stats-aggregates.ini
new file mode 100644
index 00000000..cc5cd218
--- /dev/null
+++ b/1.1.x/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/1.1.x/test/etap/121-stats-aggregates.t b/1.1.x/test/etap/121-stats-aggregates.t
new file mode 100755
index 00000000..d678aa9d
--- /dev/null
+++ b/1.1.x/test/etap/121-stats-aggregates.t
@@ -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/1.1.x/test/etap/130-attachments-md5.t b/1.1.x/test/etap/130-attachments-md5.t
new file mode 100755
index 00000000..6296e08a
--- /dev/null
+++ b/1.1.x/test/etap/130-attachments-md5.t
@@ -0,0 +1,248 @@
+#!/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.
+
+test_db_name() ->
+ <<"etap-test-db">>.
+
+docid() ->
+ case get(docid) of
+ undefined ->
+ put(docid, 1),
+ "1";
+ Count ->
+ put(docid, Count+1),
+ integer_to_list(Count+1)
+ end.
+
+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.
+
+test() ->
+ couch_server_sup:start_link(test_util:config_files()),
+ Addr = couch_config:get("httpd", "bind_address", any),
+ put(addr, Addr),
+ put(port, mochiweb_socket_server:get(couch_httpd, port)),
+ timer:sleep(1000),
+
+ couch_server:delete(test_db_name(), []),
+ couch_db:create(test_db_name(), []),
+
+ test_identity_without_md5(),
+ test_chunked_without_md5(),
+
+ test_identity_with_valid_md5(),
+ test_chunked_with_valid_md5_header(),
+ test_chunked_with_valid_md5_trailer(),
+
+ test_identity_with_invalid_md5(),
+ test_chunked_with_invalid_md5_header(),
+ test_chunked_with_invalid_md5_trailer(),
+
+ couch_server:delete(test_db_name(), []),
+ couch_server_sup:stop(),
+ ok.
+
+test_identity_without_md5() ->
+ Data = [
+ "PUT /", test_db_name(), "/", docid(), "/readme.txt HTTP/1.1\r\n",
+ "Content-Type: text/plain\r\n",
+ "Content-Length: 34\r\n",
+ "\r\n",
+ "We all live in a yellow submarine!"],
+
+ {Code, Json} = do_request(Data),
+ etap:is(Code, 201, "Stored with identity encoding and no MD5"),
+ etap:is(get_json(Json, [<<"ok">>]), true, "Body indicates success.").
+
+test_chunked_without_md5() ->
+ AttData = <<"We all live in a yellow submarine!">>,
+ <<Part1:21/binary, Part2:13/binary>> = AttData,
+ Data = [
+ "PUT /", test_db_name(), "/", docid(), "/readme.txt HTTP/1.1\r\n",
+ "Content-Type: text/plain\r\n",
+ "Transfer-Encoding: chunked\r\n",
+ "\r\n",
+ to_hex(size(Part1)), "\r\n",
+ Part1, "\r\n",
+ to_hex(size(Part2)), "\r\n",
+ Part2, "\r\n"
+ "0\r\n"
+ "\r\n"],
+
+ {Code, Json} = do_request(Data),
+ etap:is(Code, 201, "Stored with chunked encoding and no MD5"),
+ etap:is(get_json(Json, [<<"ok">>]), true, "Body indicates success.").
+
+test_identity_with_valid_md5() ->
+ AttData = "We all live in a yellow submarine!",
+ Data = [
+ "PUT /", test_db_name(), "/", docid(), "/readme.txt HTTP/1.1\r\n",
+ "Content-Type: text/plain\r\n",
+ "Content-Length: 34\r\n",
+ "Content-MD5: ", base64:encode(couch_util:md5(AttData)), "\r\n",
+ "\r\n",
+ AttData],
+
+ {Code, Json} = do_request(Data),
+ etap:is(Code, 201, "Stored with identity encoding and valid MD5"),
+ etap:is(get_json(Json, [<<"ok">>]), true, "Body indicates success.").
+
+test_chunked_with_valid_md5_header() ->
+ AttData = <<"We all live in a yellow submarine!">>,
+ <<Part1:21/binary, Part2:13/binary>> = AttData,
+ Data = [
+ "PUT /", test_db_name(), "/", docid(), "/readme.txt HTTP/1.1\r\n",
+ "Content-Type: text/plain\r\n",
+ "Transfer-Encoding: chunked\r\n",
+ "Content-MD5: ", base64:encode(couch_util:md5(AttData)), "\r\n",
+ "\r\n",
+ to_hex(size(Part1)), "\r\n",
+ Part1, "\r\n",
+ to_hex(size(Part2)), "\r\n",
+ Part2, "\r\n",
+ "0\r\n",
+ "\r\n"],
+
+ {Code, Json} = do_request(Data),
+ etap:is(Code, 201, "Stored with chunked encoding and valid MD5 header."),
+ etap:is(get_json(Json, [<<"ok">>]), true, "Body indicates success.").
+
+test_chunked_with_valid_md5_trailer() ->
+ AttData = <<"We all live in a yellow submarine!">>,
+ <<Part1:21/binary, Part2:13/binary>> = AttData,
+ Data = [
+ "PUT /", test_db_name(), "/", docid(), "/readme.txt HTTP/1.1\r\n",
+ "Content-Type: text/plain\r\n",
+ "Transfer-Encoding: chunked\r\n",
+ "Trailer: Content-MD5\r\n",
+ "\r\n",
+ to_hex(size(Part1)), "\r\n",
+ Part1, "\r\n",
+ to_hex(size(Part2)), "\r\n",
+ Part2, "\r\n",
+ "0\r\n",
+ "Content-MD5: ", base64:encode(couch_util:md5(AttData)), "\r\n",
+ "\r\n"],
+
+ {Code, Json} = do_request(Data),
+ etap:is(Code, 201, "Stored with chunked encoding and valid MD5 trailer."),
+ etap:is(get_json(Json, [<<"ok">>]), true, "Body indicates success.").
+
+test_identity_with_invalid_md5() ->
+ Data = [
+ "PUT /", test_db_name(), "/", docid(), "/readme.txt HTTP/1.1\r\n",
+ "Content-Type: text/plain\r\n",
+ "Content-Length: 34\r\n",
+ "Content-MD5: ", base64:encode(<<"foobar!">>), "\r\n",
+ "\r\n",
+ "We all live in a yellow submarine!"],
+
+ {Code, Json} = do_request(Data),
+ etap:is(Code, 400, "Invalid MD5 header causes an error: identity"),
+ etap:is(
+ get_json(Json, [<<"error">>]),
+ <<"content_md5_mismatch">>,
+ "Body indicates reason for failure."
+ ).
+
+test_chunked_with_invalid_md5_header() ->
+ AttData = <<"We all live in a yellow submarine!">>,
+ <<Part1:21/binary, Part2:13/binary>> = AttData,
+ Data = [
+ "PUT /", test_db_name(), "/", docid(), "/readme.txt HTTP/1.1\r\n",
+ "Content-Type: text/plain\r\n",
+ "Transfer-Encoding: chunked\r\n",
+ "Content-MD5: ", base64:encode(<<"so sneaky...">>), "\r\n",
+ "\r\n",
+ to_hex(size(Part1)), "\r\n",
+ Part1, "\r\n",
+ to_hex(size(Part2)), "\r\n",
+ Part2, "\r\n",
+ "0\r\n",
+ "\r\n"],
+
+ {Code, Json} = do_request(Data),
+ etap:is(Code, 400, "Invalid MD5 header causes an error: chunked"),
+ etap:is(
+ get_json(Json, [<<"error">>]),
+ <<"content_md5_mismatch">>,
+ "Body indicates reason for failure."
+ ).
+
+test_chunked_with_invalid_md5_trailer() ->
+ AttData = <<"We all live in a yellow submarine!">>,
+ <<Part1:21/binary, Part2:13/binary>> = AttData,
+ Data = [
+ "PUT /", test_db_name(), "/", docid(), "/readme.txt HTTP/1.1\r\n",
+ "Content-Type: text/plain\r\n",
+ "Transfer-Encoding: chunked\r\n",
+ "Trailer: Content-MD5\r\n",
+ "\r\n",
+ to_hex(size(Part1)), "\r\n",
+ Part1, "\r\n",
+ to_hex(size(Part2)), "\r\n",
+ Part2, "\r\n",
+ "0\r\n",
+ "Content-MD5: ", base64:encode(<<"Kool-Aid Fountain!">>), "\r\n",
+ "\r\n"],
+
+ {Code, Json} = do_request(Data),
+ etap:is(Code, 400, "Invalid MD5 Trailer causes an error"),
+ etap:is(
+ get_json(Json, [<<"error">>]),
+ <<"content_md5_mismatch">>,
+ "Body indicates reason for failure."
+ ).
+
+
+get_socket() ->
+ Options = [binary, {packet, 0}, {active, false}],
+ {ok, Sock} = gen_tcp:connect(get(addr), get(port), Options),
+ Sock.
+
+do_request(Request) ->
+ Sock = get_socket(),
+ gen_tcp:send(Sock, list_to_binary(lists:flatten(Request))),
+ timer:sleep(1000),
+ {ok, R} = gen_tcp:recv(Sock, 0),
+ gen_tcp:close(Sock),
+ [Header, Body] = re:split(R, "\r\n\r\n", [{return, binary}]),
+ {ok, {http_response, _, Code, _}, _} =
+ erlang:decode_packet(http, Header, []),
+ Json = couch_util:json_decode(Body),
+ {Code, Json}.
+
+get_json(Json, Path) ->
+ couch_util:get_nested_json_value(Json, Path).
+
+to_hex(Val) ->
+ to_hex(Val, []).
+
+to_hex(0, Acc) ->
+ Acc;
+to_hex(Val, Acc) ->
+ to_hex(Val div 16, [hex_char(Val rem 16) | Acc]).
+
+hex_char(V) when V < 10 -> $0 + V;
+hex_char(V) -> $A + V - 10.
+
diff --git a/1.1.x/test/etap/140-attachment-comp.t b/1.1.x/test/etap/140-attachment-comp.t
new file mode 100755
index 00000000..475f4fb0
--- /dev/null
+++ b/1.1.x/test/etap/140-attachment-comp.t
@@ -0,0 +1,762 @@
+#!/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_db_name() ->
+ <<"couch_test_atts_compression">>.
+
+main(_) ->
+ test_util:init_code_path(),
+
+ etap:plan(85),
+ 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_server_sup:start_link(test_util:config_files()),
+ put(addr, couch_config:get("httpd", "bind_address", "127.0.0.1")),
+ put(port, integer_to_list(mochiweb_socket_server:get(couch_httpd, port))),
+ application:start(inets),
+ timer:sleep(1000),
+ couch_server:delete(test_db_name(), []),
+ couch_db:create(test_db_name(), []),
+
+ couch_config:set("attachments", "compression_level", "8", false),
+ couch_config:set("attachments", "compressible_types", "text/*", false),
+
+ create_1st_text_att(),
+ create_1st_png_att(),
+ create_2nd_text_att(),
+ create_2nd_png_att(),
+
+ tests_for_1st_text_att(),
+ tests_for_1st_png_att(),
+ tests_for_2nd_text_att(),
+ tests_for_2nd_png_att(),
+
+ create_already_compressed_att(db_url() ++ "/doc_comp_att", "readme.txt"),
+ test_already_compressed_att(db_url() ++ "/doc_comp_att", "readme.txt"),
+
+ test_create_already_compressed_att_with_invalid_content_encoding(
+ db_url() ++ "/doc_att_deflate",
+ "readme.txt",
+ zlib:compress(test_text_data()),
+ "deflate"
+ ),
+
+ test_create_already_compressed_att_with_invalid_content_encoding(
+ db_url() ++ "/doc_att_compress",
+ "readme.txt",
+ % Note: As of OTP R13B04, it seems there's no LZW compression
+ % (i.e. UNIX compress utility implementation) lib in OTP.
+ % However there's a simple working Erlang implementation at:
+ % http://scienceblogs.com/goodmath/2008/01/simple_lempelziv_compression_i.php
+ test_text_data(),
+ "compress"
+ ),
+
+ test_compressible_type_with_parameters(),
+
+ timer:sleep(3000), % to avoid mochiweb socket closed exceptions
+ couch_server:delete(test_db_name(), []),
+ couch_server_sup:stop(),
+ ok.
+
+db_url() ->
+ "http://" ++ get(addr) ++ ":" ++ get(port) ++ "/" ++
+ binary_to_list(test_db_name()).
+
+create_1st_text_att() ->
+ {ok, {{_, Code, _}, _Headers, _Body}} = http:request(
+ put,
+ {db_url() ++ "/testdoc1/readme.txt", [],
+ "text/plain", test_text_data()},
+ [],
+ [{sync, true}]),
+ etap:is(Code, 201, "Created text attachment using the standalone api"),
+ ok.
+
+create_1st_png_att() ->
+ {ok, {{_, Code, _}, _Headers, _Body}} = http:request(
+ put,
+ {db_url() ++ "/testdoc2/icon.png", [],
+ "image/png", test_png_data()},
+ [],
+ [{sync, true}]),
+ etap:is(Code, 201, "Created png attachment using the standalone api"),
+ ok.
+
+% create a text attachment using the non-standalone attachment api
+create_2nd_text_att() ->
+ DocJson = {[
+ {<<"_attachments">>, {[
+ {<<"readme.txt">>, {[
+ {<<"content_type">>, <<"text/plain">>},
+ {<<"data">>, base64:encode(test_text_data())}
+ ]}
+ }]}}
+ ]},
+ {ok, {{_, Code, _}, _Headers, _Body}} = http:request(
+ put,
+ {db_url() ++ "/testdoc3", [],
+ "application/json", list_to_binary(couch_util:json_encode(DocJson))},
+ [],
+ [{sync, true}]),
+ etap:is(Code, 201, "Created text attachment using the non-standalone api"),
+ ok.
+
+% create a png attachment using the non-standalone attachment api
+create_2nd_png_att() ->
+ DocJson = {[
+ {<<"_attachments">>, {[
+ {<<"icon.png">>, {[
+ {<<"content_type">>, <<"image/png">>},
+ {<<"data">>, base64:encode(test_png_data())}
+ ]}
+ }]}}
+ ]},
+ {ok, {{_, Code, _}, _Headers, _Body}} = http:request(
+ put,
+ {db_url() ++ "/testdoc4", [],
+ "application/json", list_to_binary(couch_util:json_encode(DocJson))},
+ [],
+ [{sync, true}]),
+ etap:is(Code, 201, "Created png attachment using the non-standalone api"),
+ ok.
+
+create_already_compressed_att(DocUri, AttName) ->
+ {ok, {{_, Code, _}, _Headers, _Body}} = http:request(
+ put,
+ {DocUri ++ "/" ++ AttName, [{"Content-Encoding", "gzip"}],
+ "text/plain", zlib:gzip(test_text_data())},
+ [],
+ [{sync, true}]),
+ etap:is(
+ Code,
+ 201,
+ "Created already compressed attachment using the standalone api"
+ ),
+ ok.
+
+tests_for_1st_text_att() ->
+ test_get_1st_text_att_with_accept_encoding_gzip(),
+ test_get_1st_text_att_without_accept_encoding_header(),
+ test_get_1st_text_att_with_accept_encoding_deflate(),
+ test_get_1st_text_att_with_accept_encoding_deflate_only(),
+ test_get_doc_with_1st_text_att(),
+ test_1st_text_att_stub().
+
+tests_for_1st_png_att() ->
+ test_get_1st_png_att_without_accept_encoding_header(),
+ test_get_1st_png_att_with_accept_encoding_gzip(),
+ test_get_1st_png_att_with_accept_encoding_deflate(),
+ test_get_doc_with_1st_png_att(),
+ test_1st_png_att_stub().
+
+tests_for_2nd_text_att() ->
+ test_get_2nd_text_att_with_accept_encoding_gzip(),
+ test_get_2nd_text_att_without_accept_encoding_header(),
+ test_get_doc_with_2nd_text_att(),
+ test_2nd_text_att_stub().
+
+tests_for_2nd_png_att() ->
+ test_get_2nd_png_att_without_accept_encoding_header(),
+ test_get_2nd_png_att_with_accept_encoding_gzip(),
+ test_get_doc_with_2nd_png_att(),
+ test_2nd_png_att_stub().
+
+test_get_1st_text_att_with_accept_encoding_gzip() ->
+ {ok, {{_, Code, _}, Headers, Body}} = http:request(
+ get,
+ {db_url() ++ "/testdoc1/readme.txt", [{"Accept-Encoding", "gzip"}]},
+ [],
+ [{sync, true}]),
+ etap:is(Code, 200, "HTTP response code is 200"),
+ Gziped = lists:member({"content-encoding", "gzip"}, Headers),
+ etap:is(Gziped, true, "received body is gziped"),
+ Uncompressed = binary_to_list(zlib:gunzip(list_to_binary(Body))),
+ etap:is(
+ Uncompressed,
+ test_text_data(),
+ "received data for the 1st text attachment is ok"
+ ),
+ ok.
+
+test_get_1st_text_att_without_accept_encoding_header() ->
+ {ok, {{_, Code, _}, Headers, Body}} = http:request(
+ get,
+ {db_url() ++ "/testdoc1/readme.txt", []},
+ [],
+ [{sync, true}]),
+ etap:is(Code, 200, "HTTP response code is 200"),
+ Gziped = lists:member({"content-encoding", "gzip"}, Headers),
+ etap:is(Gziped, false, "received body is not gziped"),
+ etap:is(
+ Body,
+ test_text_data(),
+ "received data for the 1st text attachment is ok"
+ ),
+ ok.
+
+test_get_1st_text_att_with_accept_encoding_deflate() ->
+ {ok, {{_, Code, _}, Headers, Body}} = http:request(
+ get,
+ {db_url() ++ "/testdoc1/readme.txt", [{"Accept-Encoding", "deflate"}]},
+ [],
+ [{sync, true}]),
+ etap:is(Code, 200, "HTTP response code is 200"),
+ Gziped = lists:member({"content-encoding", "gzip"}, Headers),
+ etap:is(Gziped, false, "received body is not gziped"),
+ Deflated = lists:member({"content-encoding", "deflate"}, Headers),
+ etap:is(Deflated, false, "received body is not deflated"),
+ etap:is(
+ Body,
+ test_text_data(),
+ "received data for the 1st text attachment is ok"
+ ),
+ ok.
+
+test_get_1st_text_att_with_accept_encoding_deflate_only() ->
+ {ok, {{_, Code, _}, _Headers, _Body}} = http:request(
+ get,
+ {db_url() ++ "/testdoc1/readme.txt",
+ [{"Accept-Encoding", "deflate, *;q=0"}]},
+ [],
+ [{sync, true}]),
+ etap:is(
+ Code,
+ 406,
+ "HTTP response code is 406 for an unsupported content encoding request"
+ ),
+ ok.
+
+test_get_1st_png_att_without_accept_encoding_header() ->
+ {ok, {{_, Code, _}, Headers, Body}} = http:request(
+ get,
+ {db_url() ++ "/testdoc2/icon.png", []},
+ [],
+ [{sync, true}]),
+ etap:is(Code, 200, "HTTP response code is 200"),
+ Encoding = couch_util:get_value("content-encoding", Headers),
+ etap:is(Encoding, undefined, "received body is not gziped"),
+ etap:is(
+ Body,
+ test_png_data(),
+ "received data for the 1st png attachment is ok"
+ ),
+ ok.
+
+test_get_1st_png_att_with_accept_encoding_gzip() ->
+ {ok, {{_, Code, _}, Headers, Body}} = http:request(
+ get,
+ {db_url() ++ "/testdoc2/icon.png", [{"Accept-Encoding", "gzip"}]},
+ [],
+ [{sync, true}]),
+ etap:is(Code, 200, "HTTP response code is 200"),
+ Encoding = couch_util:get_value("content-encoding", Headers),
+ etap:is(Encoding, undefined, "received body is not gziped"),
+ etap:is(
+ Body,
+ test_png_data(),
+ "received data for the 1st png attachment is ok"
+ ),
+ ok.
+
+test_get_1st_png_att_with_accept_encoding_deflate() ->
+ {ok, {{_, Code, _}, Headers, Body}} = http:request(
+ get,
+ {db_url() ++ "/testdoc2/icon.png", [{"Accept-Encoding", "deflate"}]},
+ [],
+ [{sync, true}]),
+ etap:is(Code, 200, "HTTP response code is 200"),
+ Encoding = couch_util:get_value("content-encoding", Headers),
+ etap:is(Encoding, undefined, "received body is in identity form"),
+ etap:is(
+ Body,
+ test_png_data(),
+ "received data for the 1st png attachment is ok"
+ ),
+ ok.
+
+test_get_doc_with_1st_text_att() ->
+ {ok, {{_, Code, _}, _Headers, Body}} = http:request(
+ get,
+ {db_url() ++ "/testdoc1?attachments=true",
+ [{"Accept", "application/json"}]},
+ [],
+ [{sync, true}]),
+ etap:is(Code, 200, "HTTP response code is 200"),
+ Json = couch_util:json_decode(Body),
+ TextAttJson = couch_util:get_nested_json_value(
+ Json,
+ [<<"_attachments">>, <<"readme.txt">>]
+ ),
+ TextAttType = couch_util:get_nested_json_value(
+ TextAttJson,
+ [<<"content_type">>]
+ ),
+ TextAttData = couch_util:get_nested_json_value(
+ TextAttJson,
+ [<<"data">>]
+ ),
+ etap:is(
+ TextAttType,
+ <<"text/plain">>,
+ "1st text attachment has type text/plain"
+ ),
+ %% check the attachment's data is the base64 encoding of the plain text
+ %% and not the base64 encoding of the gziped plain text
+ etap:is(
+ TextAttData,
+ base64:encode(test_text_data()),
+ "1st text attachment data is properly base64 encoded"
+ ),
+ ok.
+
+test_1st_text_att_stub() ->
+ {ok, {{_, Code, _}, _Headers, Body}} = http:request(
+ get,
+ {db_url() ++ "/testdoc1?att_encoding_info=true", []},
+ [],
+ [{sync, true}]),
+ etap:is(Code, 200, "HTTP response code is 200"),
+ Json = couch_util:json_decode(Body),
+ {TextAttJson} = couch_util:get_nested_json_value(
+ Json,
+ [<<"_attachments">>, <<"readme.txt">>]
+ ),
+ TextAttLength = couch_util:get_value(<<"length">>, TextAttJson),
+ etap:is(
+ TextAttLength,
+ length(test_text_data()),
+ "1st text attachment stub length matches the uncompressed length"
+ ),
+ TextAttEncoding = couch_util:get_value(<<"encoding">>, TextAttJson),
+ etap:is(
+ TextAttEncoding,
+ <<"gzip">>,
+ "1st text attachment stub has the encoding field set to gzip"
+ ),
+ TextAttEncLength = couch_util:get_value(<<"encoded_length">>, TextAttJson),
+ etap:is(
+ TextAttEncLength,
+ iolist_size(zlib:gzip(test_text_data())),
+ "1st text attachment stub encoded_length matches the compressed length"
+ ),
+ ok.
+
+test_get_doc_with_1st_png_att() ->
+ {ok, {{_, Code, _}, _Headers, Body}} = http:request(
+ get,
+ {db_url() ++ "/testdoc2?attachments=true",
+ [{"Accept", "application/json"}]},
+ [],
+ [{sync, true}]),
+ etap:is(Code, 200, "HTTP response code is 200"),
+ Json = couch_util:json_decode(Body),
+ PngAttJson = couch_util:get_nested_json_value(
+ Json,
+ [<<"_attachments">>, <<"icon.png">>]
+ ),
+ PngAttType = couch_util:get_nested_json_value(
+ PngAttJson,
+ [<<"content_type">>]
+ ),
+ PngAttData = couch_util:get_nested_json_value(
+ PngAttJson,
+ [<<"data">>]
+ ),
+ etap:is(PngAttType, <<"image/png">>, "attachment has type image/png"),
+ etap:is(
+ PngAttData,
+ base64:encode(test_png_data()),
+ "1st png attachment data is properly base64 encoded"
+ ),
+ ok.
+
+test_1st_png_att_stub() ->
+ {ok, {{_, Code, _}, _Headers, Body}} = http:request(
+ get,
+ {db_url() ++ "/testdoc2?att_encoding_info=true", []},
+ [],
+ [{sync, true}]),
+ etap:is(Code, 200, "HTTP response code is 200"),
+ Json = couch_util:json_decode(Body),
+ {PngAttJson} = couch_util:get_nested_json_value(
+ Json,
+ [<<"_attachments">>, <<"icon.png">>]
+ ),
+ PngAttLength = couch_util:get_value(<<"length">>, PngAttJson),
+ etap:is(
+ PngAttLength,
+ length(test_png_data()),
+ "1st png attachment stub length matches the uncompressed length"
+ ),
+ PngEncoding = couch_util:get_value(<<"encoding">>, PngAttJson),
+ etap:is(
+ PngEncoding,
+ undefined,
+ "1st png attachment stub doesn't have an encoding field"
+ ),
+ PngEncLength = couch_util:get_value(<<"encoded_length">>, PngAttJson),
+ etap:is(
+ PngEncLength,
+ undefined,
+ "1st png attachment stub doesn't have an encoded_length field"
+ ),
+ ok.
+
+test_get_2nd_text_att_with_accept_encoding_gzip() ->
+ {ok, {{_, Code, _}, Headers, Body}} = http:request(
+ get,
+ {db_url() ++ "/testdoc3/readme.txt", [{"Accept-Encoding", "gzip"}]},
+ [],
+ [{sync, true}]),
+ etap:is(Code, 200, "HTTP response code is 200"),
+ Gziped = lists:member({"content-encoding", "gzip"}, Headers),
+ etap:is(Gziped, true, "received body is gziped"),
+ Uncompressed = binary_to_list(zlib:gunzip(list_to_binary(Body))),
+ etap:is(
+ Uncompressed,
+ test_text_data(),
+ "received data for the 2nd text attachment is ok"
+ ),
+ ok.
+
+test_get_2nd_text_att_without_accept_encoding_header() ->
+ {ok, {{_, Code, _}, Headers, Body}} = http:request(
+ get,
+ {db_url() ++ "/testdoc3/readme.txt", []},
+ [],
+ [{sync, true}]),
+ etap:is(Code, 200, "HTTP response code is 200"),
+ Gziped = lists:member({"content-encoding", "gzip"}, Headers),
+ etap:is(Gziped, false, "received body is not gziped"),
+ etap:is(
+ Body,
+ test_text_data(),
+ "received data for the 2nd text attachment is ok"
+ ),
+ ok.
+
+test_get_2nd_png_att_without_accept_encoding_header() ->
+ {ok, {{_, Code, _}, Headers, Body}} = http:request(
+ get,
+ {db_url() ++ "/testdoc4/icon.png", []},
+ [],
+ [{sync, true}]),
+ etap:is(Code, 200, "HTTP response code is 200"),
+ Gziped = lists:member({"content-encoding", "gzip"}, Headers),
+ etap:is(Gziped, false, "received body is not gziped"),
+ etap:is(
+ Body,
+ test_png_data(),
+ "received data for the 2nd png attachment is ok"
+ ),
+ ok.
+
+test_get_2nd_png_att_with_accept_encoding_gzip() ->
+ {ok, {{_, Code, _}, Headers, Body}} = http:request(
+ get,
+ {db_url() ++ "/testdoc4/icon.png", [{"Accept-Encoding", "gzip"}]},
+ [],
+ [{sync, true}]),
+ etap:is(Code, 200, "HTTP response code is 200"),
+ Gziped = lists:member({"content-encoding", "gzip"}, Headers),
+ etap:is(Gziped, false, "received body is not gziped"),
+ etap:is(
+ Body,
+ test_png_data(),
+ "received data for the 2nd png attachment is ok"
+ ),
+ ok.
+
+test_get_doc_with_2nd_text_att() ->
+ {ok, {{_, Code, _}, _Headers, Body}} = http:request(
+ get,
+ {db_url() ++ "/testdoc3?attachments=true",
+ [{"Accept", "application/json"}]},
+ [],
+ [{sync, true}]),
+ etap:is(Code, 200, "HTTP response code is 200"),
+ Json = couch_util:json_decode(Body),
+ TextAttJson = couch_util:get_nested_json_value(
+ Json,
+ [<<"_attachments">>, <<"readme.txt">>]
+ ),
+ TextAttType = couch_util:get_nested_json_value(
+ TextAttJson,
+ [<<"content_type">>]
+ ),
+ TextAttData = couch_util:get_nested_json_value(
+ TextAttJson,
+ [<<"data">>]
+ ),
+ etap:is(TextAttType, <<"text/plain">>, "attachment has type text/plain"),
+ %% check the attachment's data is the base64 encoding of the plain text
+ %% and not the base64 encoding of the gziped plain text
+ etap:is(
+ TextAttData,
+ base64:encode(test_text_data()),
+ "2nd text attachment data is properly base64 encoded"
+ ),
+ ok.
+
+test_2nd_text_att_stub() ->
+ {ok, {{_, Code, _}, _Headers, Body}} = http:request(
+ get,
+ {db_url() ++ "/testdoc3?att_encoding_info=true", []},
+ [],
+ [{sync, true}]),
+ etap:is(Code, 200, "HTTP response code is 200"),
+ Json = couch_util:json_decode(Body),
+ {TextAttJson} = couch_util:get_nested_json_value(
+ Json,
+ [<<"_attachments">>, <<"readme.txt">>]
+ ),
+ TextAttLength = couch_util:get_value(<<"length">>, TextAttJson),
+ etap:is(
+ TextAttLength,
+ length(test_text_data()),
+ "2nd text attachment stub length matches the uncompressed length"
+ ),
+ TextAttEncoding = couch_util:get_value(<<"encoding">>, TextAttJson),
+ etap:is(
+ TextAttEncoding,
+ <<"gzip">>,
+ "2nd text attachment stub has the encoding field set to gzip"
+ ),
+ TextAttEncLength = couch_util:get_value(<<"encoded_length">>, TextAttJson),
+ etap:is(
+ TextAttEncLength,
+ iolist_size(zlib:gzip(test_text_data())),
+ "2nd text attachment stub encoded_length matches the compressed length"
+ ),
+ ok.
+
+test_get_doc_with_2nd_png_att() ->
+ {ok, {{_, Code, _}, _Headers, Body}} = http:request(
+ get,
+ {db_url() ++ "/testdoc4?attachments=true",
+ [{"Accept", "application/json"}]},
+ [],
+ [{sync, true}]),
+ etap:is(Code, 200, "HTTP response code is 200"),
+ Json = couch_util:json_decode(Body),
+ PngAttJson = couch_util:get_nested_json_value(
+ Json,
+ [<<"_attachments">>, <<"icon.png">>]
+ ),
+ PngAttType = couch_util:get_nested_json_value(
+ PngAttJson,
+ [<<"content_type">>]
+ ),
+ PngAttData = couch_util:get_nested_json_value(
+ PngAttJson,
+ [<<"data">>]
+ ),
+ etap:is(PngAttType, <<"image/png">>, "attachment has type image/png"),
+ etap:is(
+ PngAttData,
+ base64:encode(test_png_data()),
+ "2nd png attachment data is properly base64 encoded"
+ ),
+ ok.
+
+test_2nd_png_att_stub() ->
+ {ok, {{_, Code, _}, _Headers, Body}} = http:request(
+ get,
+ {db_url() ++ "/testdoc4?att_encoding_info=true", []},
+ [],
+ [{sync, true}]),
+ etap:is(Code, 200, "HTTP response code is 200"),
+ Json = couch_util:json_decode(Body),
+ {PngAttJson} = couch_util:get_nested_json_value(
+ Json,
+ [<<"_attachments">>, <<"icon.png">>]
+ ),
+ PngAttLength = couch_util:get_value(<<"length">>, PngAttJson),
+ etap:is(
+ PngAttLength,
+ length(test_png_data()),
+ "2nd png attachment stub length matches the uncompressed length"
+ ),
+ PngEncoding = couch_util:get_value(<<"encoding">>, PngAttJson),
+ etap:is(
+ PngEncoding,
+ undefined,
+ "2nd png attachment stub doesn't have an encoding field"
+ ),
+ PngEncLength = couch_util:get_value(<<"encoded_length">>, PngAttJson),
+ etap:is(
+ PngEncLength,
+ undefined,
+ "2nd png attachment stub doesn't have an encoded_length field"
+ ),
+ ok.
+
+test_already_compressed_att(DocUri, AttName) ->
+ test_get_already_compressed_att_with_accept_gzip(DocUri, AttName),
+ test_get_already_compressed_att_without_accept(DocUri, AttName),
+ test_get_already_compressed_att_stub(DocUri, AttName).
+
+test_get_already_compressed_att_with_accept_gzip(DocUri, AttName) ->
+ {ok, {{_, Code, _}, Headers, Body}} = http:request(
+ get,
+ {DocUri ++ "/" ++ AttName, [{"Accept-Encoding", "gzip"}]},
+ [],
+ [{sync, true}]),
+ etap:is(Code, 200, "HTTP response code is 200"),
+ Gziped = lists:member({"content-encoding", "gzip"}, Headers),
+ etap:is(Gziped, true, "received body is gziped"),
+ etap:is(
+ iolist_to_binary(Body),
+ iolist_to_binary(zlib:gzip(test_text_data())),
+ "received data for the already compressed attachment is ok"
+ ),
+ ok.
+
+test_get_already_compressed_att_without_accept(DocUri, AttName) ->
+ {ok, {{_, Code, _}, Headers, Body}} = http:request(
+ get,
+ {DocUri ++ "/" ++ AttName, []},
+ [],
+ [{sync, true}]),
+ etap:is(Code, 200, "HTTP response code is 200"),
+ Gziped = lists:member({"content-encoding", "gzip"}, Headers),
+ etap:is(Gziped, false, "received body is not gziped"),
+ etap:is(
+ iolist_to_binary(Body),
+ iolist_to_binary(test_text_data()),
+ "received data for the already compressed attachment is ok"
+ ),
+ ok.
+
+test_get_already_compressed_att_stub(DocUri, AttName) ->
+ {ok, {{_, Code, _}, _Headers, Body}} = http:request(
+ get,
+ {DocUri ++ "?att_encoding_info=true", []},
+ [],
+ [{sync, true}]),
+ etap:is(Code, 200, "HTTP response code is 200"),
+ Json = couch_util:json_decode(Body),
+ {AttJson} = couch_util:get_nested_json_value(
+ Json,
+ [<<"_attachments">>, iolist_to_binary(AttName)]
+ ),
+ AttLength = couch_util:get_value(<<"length">>, AttJson),
+ etap:is(
+ AttLength,
+ iolist_size((zlib:gzip(test_text_data()))),
+ "Already compressed attachment stub length matches the "
+ "compressed length"
+ ),
+ Encoding = couch_util:get_value(<<"encoding">>, AttJson),
+ etap:is(
+ Encoding,
+ <<"gzip">>,
+ "Already compressed attachment stub has the encoding field set to gzip"
+ ),
+ EncLength = couch_util:get_value(<<"encoded_length">>, AttJson),
+ etap:is(
+ EncLength,
+ AttLength,
+ "Already compressed attachment stub encoded_length matches the "
+ "length field value"
+ ),
+ ok.
+
+test_create_already_compressed_att_with_invalid_content_encoding(
+ DocUri, AttName, AttData, Encoding) ->
+ {ok, {{_, Code, _}, _Headers, _Body}} = http:request(
+ put,
+ {DocUri ++ "/" ++ AttName, [{"Content-Encoding", Encoding}],
+ "text/plain", AttData},
+ [],
+ [{sync, true}]),
+ etap:is(
+ Code,
+ 415,
+ "Couldn't create an already compressed attachment using the "
+ "unsupported encoding '" ++ Encoding ++ "'"
+ ),
+ ok.
+
+test_compressible_type_with_parameters() ->
+ {ok, {{_, Code, _}, _Headers, _Body}} = http:request(
+ put,
+ {db_url() ++ "/testdoc5/readme.txt", [],
+ "text/plain; charset=UTF-8", test_text_data()},
+ [],
+ [{sync, true}]),
+ etap:is(Code, 201, "Created text attachment with MIME type "
+ "'text/plain; charset=UTF-8' using the standalone api"),
+ {ok, {{_, Code2, _}, Headers2, Body}} = http:request(
+ get,
+ {db_url() ++ "/testdoc5/readme.txt", [{"Accept-Encoding", "gzip"}]},
+ [],
+ [{sync, true}]),
+ etap:is(Code2, 200, "HTTP response code is 200"),
+ Gziped = lists:member({"content-encoding", "gzip"}, Headers2),
+ etap:is(Gziped, true, "received body is gziped"),
+ Uncompressed = binary_to_list(zlib:gunzip(list_to_binary(Body))),
+ etap:is(Uncompressed, test_text_data(), "received data is gzipped"),
+ {ok, {{_, Code3, _}, _Headers3, Body3}} = http:request(
+ get,
+ {db_url() ++ "/testdoc5?att_encoding_info=true", []},
+ [],
+ [{sync, true}]),
+ etap:is(Code3, 200, "HTTP response code is 200"),
+ Json = couch_util:json_decode(Body3),
+ {TextAttJson} = couch_util:get_nested_json_value(
+ Json,
+ [<<"_attachments">>, <<"readme.txt">>]
+ ),
+ TextAttLength = couch_util:get_value(<<"length">>, TextAttJson),
+ etap:is(
+ TextAttLength,
+ length(test_text_data()),
+ "text attachment stub length matches the uncompressed length"
+ ),
+ TextAttEncoding = couch_util:get_value(<<"encoding">>, TextAttJson),
+ etap:is(
+ TextAttEncoding,
+ <<"gzip">>,
+ "text attachment stub has the encoding field set to gzip"
+ ),
+ TextAttEncLength = couch_util:get_value(<<"encoded_length">>, TextAttJson),
+ etap:is(
+ TextAttEncLength,
+ iolist_size(zlib:gzip(test_text_data())),
+ "text attachment stub encoded_length matches the compressed length"
+ ),
+ ok.
+
+test_png_data() ->
+ {ok, Data} = file:read_file(
+ test_util:source_file("share/www/image/logo.png")
+ ),
+ binary_to_list(Data).
+
+test_text_data() ->
+ {ok, Data} = file:read_file(
+ test_util:source_file("README")
+ ),
+ binary_to_list(Data).
diff --git a/1.1.x/test/etap/150-invalid-view-seq.t b/1.1.x/test/etap/150-invalid-view-seq.t
new file mode 100755
index 00000000..594d3416
--- /dev/null
+++ b/1.1.x/test/etap/150-invalid-view-seq.t
@@ -0,0 +1,190 @@
+#!/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.
+
+-record(user_ctx, {
+ name = null,
+ roles = [],
+ handler
+}).
+
+test_db_name() ->
+ <<"couch_test_invalid_view_seq">>.
+
+main(_) ->
+ test_util:init_code_path(),
+
+ etap:plan(10),
+ case (catch test()) of
+ ok ->
+ etap:end_tests();
+ Other ->
+ etap:diag(io_lib:format("Test died abnormally: ~p", [Other])),
+ etap:bail(Other)
+ end,
+ ok.
+
+%% NOTE: since during the test we stop the server,
+%% a huge and ugly but harmless stack trace is sent to stderr
+%%
+test() ->
+ couch_server_sup:start_link(test_util:config_files()),
+ timer:sleep(1000),
+ delete_db(),
+ create_db(),
+
+ create_docs(),
+ create_design_doc(),
+
+ % make DB file backup
+ backup_db_file(),
+
+ put(addr, couch_config:get("httpd", "bind_address", "127.0.0.1")),
+ put(port, integer_to_list(mochiweb_socket_server:get(couch_httpd, port))),
+ application:start(inets),
+
+ create_new_doc(),
+ query_view_before_restore_backup(),
+
+ % restore DB file backup after querying view
+ restore_backup_db_file(),
+
+ query_view_after_restore_backup(),
+
+ delete_db(),
+ couch_server_sup:stop(),
+ ok.
+
+admin_user_ctx() ->
+ {user_ctx, #user_ctx{roles=[<<"_admin">>]}}.
+
+create_db() ->
+ {ok, _} = couch_db:create(test_db_name(), [admin_user_ctx()]).
+
+delete_db() ->
+ couch_server:delete(test_db_name(), [admin_user_ctx()]).
+
+create_docs() ->
+ {ok, Db} = couch_db:open(test_db_name(), [admin_user_ctx()]),
+ Doc1 = couch_doc:from_json_obj({[
+ {<<"_id">>, <<"doc1">>},
+ {<<"value">>, 1}
+
+ ]}),
+ Doc2 = couch_doc:from_json_obj({[
+ {<<"_id">>, <<"doc2">>},
+ {<<"value">>, 2}
+
+ ]}),
+ Doc3 = couch_doc:from_json_obj({[
+ {<<"_id">>, <<"doc3">>},
+ {<<"value">>, 3}
+
+ ]}),
+ {ok, _} = couch_db:update_docs(Db, [Doc1, Doc2, Doc3]),
+ couch_db:ensure_full_commit(Db),
+ couch_db:close(Db).
+
+create_design_doc() ->
+ {ok, Db} = couch_db:open(test_db_name(), [admin_user_ctx()]),
+ DDoc = couch_doc:from_json_obj({[
+ {<<"_id">>, <<"_design/foo">>},
+ {<<"language">>, <<"javascript">>},
+ {<<"views">>, {[
+ {<<"bar">>, {[
+ {<<"map">>, <<"function(doc) { emit(doc.value, 1); }">>}
+ ]}}
+ ]}}
+ ]}),
+ {ok, _} = couch_db:update_docs(Db, [DDoc]),
+ couch_db:ensure_full_commit(Db),
+ couch_db:close(Db).
+
+backup_db_file() ->
+ DbFile = test_util:build_file("tmp/lib/" ++
+ binary_to_list(test_db_name()) ++ ".couch"),
+ {ok, _} = file:copy(DbFile, DbFile ++ ".backup"),
+ ok.
+
+create_new_doc() ->
+ {ok, Db} = couch_db:open(test_db_name(), [admin_user_ctx()]),
+ Doc666 = couch_doc:from_json_obj({[
+ {<<"_id">>, <<"doc666">>},
+ {<<"value">>, 999}
+
+ ]}),
+ {ok, _} = couch_db:update_docs(Db, [Doc666]),
+ couch_db:ensure_full_commit(Db),
+ couch_db:close(Db).
+
+db_url() ->
+ "http://" ++ get(addr) ++ ":" ++ get(port) ++ "/" ++
+ binary_to_list(test_db_name()).
+
+query_view_before_restore_backup() ->
+ {ok, {{_, Code, _}, _Headers, Body}} = http:request(
+ get,
+ {db_url() ++ "/_design/foo/_view/bar", []},
+ [],
+ [{sync, true}]),
+ etap:is(Code, 200, "Got view response before restoring backup."),
+ ViewJson = couch_util:json_decode(Body),
+ Rows = couch_util:get_nested_json_value(ViewJson, [<<"rows">>]),
+ HasDoc1 = has_doc("doc1", Rows),
+ HasDoc2 = has_doc("doc2", Rows),
+ HasDoc3 = has_doc("doc3", Rows),
+ HasDoc666 = has_doc("doc666", Rows),
+ etap:is(HasDoc1, true, "Before backup restore, view has doc1"),
+ etap:is(HasDoc2, true, "Before backup restore, view has doc2"),
+ etap:is(HasDoc3, true, "Before backup restore, view has doc3"),
+ etap:is(HasDoc666, true, "Before backup restore, view has doc666"),
+ ok.
+
+has_doc(DocId1, Rows) ->
+ DocId = iolist_to_binary(DocId1),
+ lists:any(
+ fun({R}) -> lists:member({<<"id">>, DocId}, R) end,
+ Rows
+ ).
+
+restore_backup_db_file() ->
+ couch_server_sup:stop(),
+ timer:sleep(3000),
+ DbFile = test_util:build_file("tmp/lib/" ++
+ binary_to_list(test_db_name()) ++ ".couch"),
+ ok = file:delete(DbFile),
+ ok = file:rename(DbFile ++ ".backup", DbFile),
+ couch_server_sup:start_link(test_util:config_files()),
+ timer:sleep(1000),
+ put(port, integer_to_list(mochiweb_socket_server:get(couch_httpd, port))),
+ ok.
+
+query_view_after_restore_backup() ->
+ {ok, {{_, Code, _}, _Headers, Body}} = http:request(
+ get,
+ {db_url() ++ "/_design/foo/_view/bar", []},
+ [],
+ [{sync, true}]),
+ etap:is(Code, 200, "Got view response after restoring backup."),
+ ViewJson = couch_util:json_decode(Body),
+ Rows = couch_util:get_nested_json_value(ViewJson, [<<"rows">>]),
+ HasDoc1 = has_doc("doc1", Rows),
+ HasDoc2 = has_doc("doc2", Rows),
+ HasDoc3 = has_doc("doc3", Rows),
+ HasDoc666 = has_doc("doc666", Rows),
+ etap:is(HasDoc1, true, "After backup restore, view has doc1"),
+ etap:is(HasDoc2, true, "After backup restore, view has doc2"),
+ etap:is(HasDoc3, true, "After backup restore, view has doc3"),
+ etap:is(HasDoc666, false, "After backup restore, view does not have doc666"),
+ ok.
diff --git a/1.1.x/test/etap/160-vhosts.t b/1.1.x/test/etap/160-vhosts.t
new file mode 100755
index 00000000..8dac53e5
--- /dev/null
+++ b/1.1.x/test/etap/160-vhosts.t
@@ -0,0 +1,291 @@
+#!/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.
+
+%% XXX: Figure out how to -include("couch_rep.hrl")
+-record(http_db, {
+ url,
+ auth = [],
+ resource = "",
+ headers = [
+ {"User-Agent", "CouchDB/"++couch_server:get_version()},
+ {"Accept", "application/json"},
+ {"Accept-Encoding", "gzip"}
+ ],
+ qs = [],
+ method = get,
+ body = nil,
+ options = [
+ {response_format,binary},
+ {inactivity_timeout, 30000}
+ ],
+ retries = 10,
+ pause = 1,
+ conn = nil
+}).
+
+-record(user_ctx, {
+ name = null,
+ roles = [],
+ handler
+}).
+
+server() ->
+ lists:concat([
+ "http://127.0.0.1:", mochiweb_socket_server:get(couch_httpd, port), "/"
+ ]).
+
+dbname() -> "etap-test-db".
+admin_user_ctx() -> {user_ctx, #user_ctx{roles=[<<"_admin">>]}}.
+
+main(_) ->
+ test_util:init_code_path(),
+
+ 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() ->
+ couch_server_sup:start_link(test_util:config_files()),
+ ibrowse:start(),
+ crypto:start(),
+
+ timer:sleep(1000),
+ couch_server:delete(list_to_binary(dbname()), [admin_user_ctx()]),
+ {ok, Db} = couch_db:create(list_to_binary(dbname()), [admin_user_ctx()]),
+
+ Doc = couch_doc:from_json_obj({[
+ {<<"_id">>, <<"doc1">>},
+ {<<"value">>, 666}
+ ]}),
+
+ Doc1 = couch_doc:from_json_obj({[
+ {<<"_id">>, <<"_design/doc1">>},
+ {<<"shows">>, {[
+ {<<"test">>, <<"function(doc, req) {
+ return { json: {
+ requested_path: '/' + req.requested_path.join('/'),
+ path: '/' + req.path.join('/')
+ }};
+}">>}
+ ]}},
+ {<<"rewrites">>, [
+ {[
+ {<<"from">>, <<"/">>},
+ {<<"to">>, <<"_show/test">>}
+ ]}
+ ]}
+ ]}),
+
+ {ok, _} = couch_db:update_docs(Db, [Doc, Doc1]),
+
+ couch_db:ensure_full_commit(Db),
+
+ %% end boilerplate, start test
+
+ ok = couch_config:set("vhosts", "example.com", "/etap-test-db", false),
+ ok = couch_config:set("vhosts", "*.example.com",
+ "/etap-test-db/_design/doc1/_rewrite", false),
+ ok = couch_config:set("vhosts", "example.com/test", "/etap-test-db", false),
+ ok = couch_config:set("vhosts", "example1.com",
+ "/etap-test-db/_design/doc1/_rewrite/", false),
+ ok = couch_config:set("vhosts",":appname.:dbname.example1.com",
+ "/:dbname/_design/:appname/_rewrite/", false),
+ ok = couch_config:set("vhosts", ":dbname.example1.com", "/:dbname", false),
+
+ ok = couch_config:set("vhosts", "*.example2.com", "/*", false),
+ ok = couch_config:set("vhosts", "*/test", "/etap-test-db", false),
+ ok = couch_config:set("vhosts", "*.example2.com/test", "/*", false),
+ ok = couch_config:set("vhosts", "*/test1",
+ "/etap-test-db/_design/doc1/_show/test", false),
+
+ % let couch_httpd restart
+ timer:sleep(100),
+
+ test_regular_request(),
+ test_vhost_request(),
+ test_vhost_request_with_qs(),
+ test_vhost_request_with_global(),
+ test_vhost_requested_path(),
+ test_vhost_requested_path_path(),
+ test_vhost_request_wildcard(),
+ test_vhost_request_replace_var(),
+ test_vhost_request_replace_var1(),
+ test_vhost_request_replace_wildcard(),
+ test_vhost_request_path(),
+ test_vhost_request_path1(),
+ test_vhost_request_path2(),
+ test_vhost_request_path3(),
+
+ %% restart boilerplate
+ couch_db:close(Db),
+ timer:sleep(3000),
+ couch_server_sup:stop(),
+
+ ok.
+
+test_regular_request() ->
+ Result = case ibrowse:send_req(server(), [], get, []) of
+ {ok, _, _, Body} ->
+ {[{<<"couchdb">>, <<"Welcome">>},
+ {<<"version">>,_}
+ ]} = couch_util:json_decode(Body),
+ etap:is(true, true, "should return server info");
+ _Else ->
+ etap:is(false, true, <<"ibrowse fail">>)
+ end.
+
+test_vhost_request() ->
+ case ibrowse:send_req(server(), [], get, [], [{host_header, "example.com"}]) of
+ {ok, _, _, Body} ->
+ {JsonBody} = couch_util:json_decode(Body),
+ HasDbNameInfo = proplists:is_defined(<<"db_name">>, JsonBody),
+ etap:is(HasDbNameInfo, true, "should return database info");
+ _Else ->
+ etap:is(false, true, <<"ibrowse fail">>)
+ end.
+
+test_vhost_request_with_qs() ->
+ Url = server() ++ "doc1?revs_info=true",
+ case ibrowse:send_req(Url, [], get, [], [{host_header, "example.com"}]) of
+ {ok, _, _, Body} ->
+ {JsonProps} = couch_util:json_decode(Body),
+ HasRevsInfo = proplists:is_defined(<<"_revs_info">>, JsonProps),
+ etap:is(HasRevsInfo, true, "should return _revs_info");
+ _Else ->
+ etap:is(false, true, <<"ibrowse fail">>)
+ end.
+
+test_vhost_request_with_global() ->
+ Url2 = server() ++ "_utils/index.html",
+ case ibrowse:send_req(Url2, [], get, [], [{host_header, "example.com"}]) of
+ {ok, _, _, Body2} ->
+ "<!DOCTYPE" ++ _Foo = Body2,
+ etap:is(true, true, "should serve /_utils even inside vhosts");
+ _Else ->
+ etap:is(false, true, <<"ibrowse fail">>)
+ end.
+
+test_vhost_requested_path() ->
+ case ibrowse:send_req(server(), [], get, [], [{host_header, "example1.com"}]) of
+ {ok, _, _, Body} ->
+ {Json} = couch_util:json_decode(Body),
+ etap:is(case proplists:get_value(<<"requested_path">>, Json) of
+ <<"/">> -> true;
+ _ -> false
+ end, true, <<"requested path in req ok">>);
+ _Else ->
+ etap:is(false, true, <<"ibrowse fail">>)
+ end.
+
+test_vhost_requested_path_path() ->
+ case ibrowse:send_req(server(), [], get, [], [{host_header, "example1.com"}]) of
+ {ok, _, _, Body} ->
+ {Json} = couch_util:json_decode(Body),
+ etap:is(case proplists:get_value(<<"path">>, Json) of
+ <<"/etap-test-db/_design/doc1/_show/test">> -> true;
+ _ -> false
+ end, true, <<"path in req ok">>);
+ _Else ->
+ etap:is(false, true, <<"ibrowse fail">>)
+ end.
+
+test_vhost_request_wildcard()->
+ case ibrowse:send_req(server(), [], get, [], [{host_header, "test.example.com"}]) of
+ {ok, _, _, Body} ->
+ {Json} = couch_util:json_decode(Body),
+ etap:is(case proplists:get_value(<<"path">>, Json) of
+ <<"/etap-test-db/_design/doc1/_show/test">> -> true;
+ _ -> false
+ end, true, <<"wildcard ok">>);
+ _Else -> etap:is(false, true, <<"ibrowse fail">>)
+ end.
+
+
+test_vhost_request_replace_var() ->
+ case ibrowse:send_req(server(), [], get, [], [{host_header,"etap-test-db.example1.com"}]) of
+ {ok, _, _, Body} ->
+ {JsonBody} = couch_util:json_decode(Body),
+ HasDbNameInfo = proplists:is_defined(<<"db_name">>, JsonBody),
+ etap:is(HasDbNameInfo, true, "should return database info");
+ _Else -> etap:is(false, true, <<"ibrowse fail">>)
+ end.
+
+test_vhost_request_replace_var1() ->
+ case ibrowse:send_req(server(), [], get, [], [{host_header, "doc1.etap-test-db.example1.com"}]) of
+ {ok, _, _, Body} ->
+ {Json} = couch_util:json_decode(Body),
+ etap:is(case proplists:get_value(<<"path">>, Json) of
+ <<"/etap-test-db/_design/doc1/_show/test">> -> true;
+ _ -> false
+ end, true, <<"wildcard ok">>);
+ _Else -> etap:is(false, true, <<"ibrowse fail">>)
+ end.
+
+test_vhost_request_replace_wildcard() ->
+ case ibrowse:send_req(server(), [], get, [], [{host_header,"etap-test-db.example2.com"}]) of
+ {ok, _, _, Body} ->
+ {JsonBody} = couch_util:json_decode(Body),
+ HasDbNameInfo = proplists:is_defined(<<"db_name">>, JsonBody),
+ etap:is(HasDbNameInfo, true, "should return database info");
+ _Else -> etap:is(false, true, <<"ibrowse fail">>)
+ end.
+
+test_vhost_request_path() ->
+ Uri = server() ++ "test",
+ case ibrowse:send_req(Uri, [], get, [], [{host_header, "example.com"}]) of
+ {ok, _, _, Body} ->
+ {JsonBody} = couch_util:json_decode(Body),
+ HasDbNameInfo = proplists:is_defined(<<"db_name">>, JsonBody),
+ etap:is(HasDbNameInfo, true, "should return database info");
+ _Else -> etap:is(false, true, <<"ibrowse fail">>)
+ end.
+
+test_vhost_request_path1() ->
+ Url = server() ++ "test/doc1?revs_info=true",
+ case ibrowse:send_req(Url, [], get, [], []) of
+ {ok, _, _, Body} ->
+ {JsonProps} = couch_util:json_decode(Body),
+ HasRevsInfo = proplists:is_defined(<<"_revs_info">>, JsonProps),
+ etap:is(HasRevsInfo, true, "should return _revs_info");
+ _Else -> etap:is(false, true, <<"ibrowse fail">>)
+ end.
+
+test_vhost_request_path2() ->
+ Uri = server() ++ "test",
+ case ibrowse:send_req(Uri, [], get, [], [{host_header,"etap-test-db.example2.com"}]) of
+ {ok, _, _, Body} ->
+ {JsonBody} = couch_util:json_decode(Body),
+ HasDbNameInfo = proplists:is_defined(<<"db_name">>, JsonBody),
+ etap:is(HasDbNameInfo, true, "should return database info");
+ _Else -> etap:is(false, true, <<"ibrowse fail">>)
+ end.
+
+test_vhost_request_path3() ->
+ Uri = server() ++ "test1",
+ case ibrowse:send_req(Uri, [], get, [], []) of
+ {ok, _, _, Body} ->
+ {Json} = couch_util:json_decode(Body),
+ etap:is(case proplists:get_value(<<"path">>, Json) of
+ <<"/etap-test-db/_design/doc1/_show/test">> -> true;
+ _ -> false
+ end, true, <<"path in req ok">>);
+ _Else -> etap:is(false, true, <<"ibrowse fail">>)
+ end.
diff --git a/1.1.x/test/etap/170-os-daemons.es b/1.1.x/test/etap/170-os-daemons.es
new file mode 100755
index 00000000..73974e90
--- /dev/null
+++ b/1.1.x/test/etap/170-os-daemons.es
@@ -0,0 +1,26 @@
+#! /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.
+
+loop() ->
+ loop(io:read("")).
+
+loop({ok, _}) ->
+ loop(io:read(""));
+loop(eof) ->
+ stop;
+loop({error, Reason}) ->
+ throw({error, Reason}).
+
+main([]) ->
+ loop().
diff --git a/1.1.x/test/etap/170-os-daemons.t b/1.1.x/test/etap/170-os-daemons.t
new file mode 100755
index 00000000..6feaa1bf
--- /dev/null
+++ b/1.1.x/test/etap/170-os-daemons.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.
+
+-record(daemon, {
+ port,
+ name,
+ cmd,
+ kill,
+ status=running,
+ cfg_patterns=[],
+ errors=[],
+ buf=[]
+}).
+
+config_files() ->
+ lists:map(fun test_util:build_file/1, [
+ "etc/couchdb/default_dev.ini"
+ ]).
+
+daemon_cmd() ->
+ test_util:source_file("test/etap/170-os-daemons.es").
+
+main(_) ->
+ test_util:init_code_path(),
+
+ etap:plan(49),
+ 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:start_link(config_files()),
+ couch_os_daemons:start_link(),
+
+ etap:diag("Daemons boot after configuration added."),
+ couch_config:set("os_daemons", "foo", daemon_cmd(), false),
+ timer:sleep(1000),
+
+ {ok, [D1]} = couch_os_daemons:info([table]),
+ check_daemon(D1, "foo"),
+
+ % Check table form
+ {ok, Tab1} = couch_os_daemons:info(),
+ [T1] = ets:tab2list(Tab1),
+ check_daemon(T1, "foo"),
+
+ etap:diag("Daemons stop after configuration removed."),
+ couch_config:delete("os_daemons", "foo", false),
+ timer:sleep(500),
+
+ {ok, []} = couch_os_daemons:info([table]),
+ {ok, Tab2} = couch_os_daemons:info(),
+ etap:is(ets:tab2list(Tab2), [], "As table returns empty table."),
+
+ etap:diag("Adding multiple daemons causes both to boot."),
+ couch_config:set("os_daemons", "bar", daemon_cmd(), false),
+ couch_config:set("os_daemons", "baz", daemon_cmd(), false),
+ timer:sleep(500),
+ {ok, Daemons} = couch_os_daemons:info([table]),
+ lists:foreach(fun(D) ->
+ check_daemon(D)
+ end, Daemons),
+
+ {ok, Tab3} = couch_os_daemons:info(),
+ lists:foreach(fun(D) ->
+ check_daemon(D)
+ end, ets:tab2list(Tab3)),
+
+ etap:diag("Removing one daemon leaves the other alive."),
+ couch_config:delete("os_daemons", "bar", false),
+ timer:sleep(500),
+
+ {ok, [D2]} = couch_os_daemons:info([table]),
+ check_daemon(D2, "baz"),
+
+ % Check table version
+ {ok, Tab4} = couch_os_daemons:info(),
+ [T4] = ets:tab2list(Tab4),
+ check_daemon(T4, "baz"),
+
+ ok.
+
+check_daemon(D) ->
+ check_daemon(D, D#daemon.name).
+
+check_daemon(D, Name) ->
+ BaseName = "170-os-daemons.es",
+ BaseLen = length(BaseName),
+ CmdLen = length(D#daemon.cmd),
+ CmdName = lists:sublist(D#daemon.cmd, CmdLen-BaseLen+1, BaseLen),
+
+ etap:is(is_port(D#daemon.port), true, "Daemon port is a port."),
+ etap:is(D#daemon.name, Name, "Daemon name was set correctly."),
+ etap:is(CmdName, BaseName, "Command name was set correctly."),
+ etap:isnt(D#daemon.kill, undefined, "Kill command was set."),
+ etap:is(D#daemon.errors, [], "No errors occurred while booting."),
+ etap:is(D#daemon.buf, [], "No extra data left in the buffer.").
diff --git a/1.1.x/test/etap/171-os-daemons-config.es b/1.1.x/test/etap/171-os-daemons-config.es
new file mode 100755
index 00000000..1f68ddc6
--- /dev/null
+++ b/1.1.x/test/etap/171-os-daemons-config.es
@@ -0,0 +1,83 @@
+#! /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.
+
+filename() ->
+ list_to_binary(test_util:source_file("test/etap/171-os-daemons-config.es")).
+
+read() ->
+ case io:get_line('') of
+ eof ->
+ stop;
+ Data ->
+ couch_util:json_decode(Data)
+ end.
+
+write(Mesg) ->
+ Data = iolist_to_binary(couch_util:json_encode(Mesg)),
+ io:format(binary_to_list(Data) ++ "\n", []).
+
+get_cfg(Section) ->
+ write([<<"get">>, Section]),
+ read().
+
+get_cfg(Section, Name) ->
+ write([<<"get">>, Section, Name]),
+ read().
+
+log(Mesg) ->
+ write([<<"log">>, Mesg]).
+
+log(Mesg, Level) ->
+ write([<<"log">>, Mesg, {[{<<"level">>, Level}]}]).
+
+test_get_cfg1() ->
+ FileName = filename(),
+ {[{<<"foo">>, FileName}]} = get_cfg(<<"os_daemons">>).
+
+test_get_cfg2() ->
+ FileName = filename(),
+ FileName = get_cfg(<<"os_daemons">>, <<"foo">>),
+ <<"sequential">> = get_cfg(<<"uuids">>, <<"algorithm">>).
+
+test_get_unknown_cfg() ->
+ {[]} = get_cfg(<<"aal;3p4">>),
+ null = get_cfg(<<"aal;3p4">>, <<"313234kjhsdfl">>).
+
+test_log() ->
+ log(<<"foobar!">>),
+ log(<<"some stuff!">>, <<"debug">>),
+ log(2),
+ log(true),
+ write([<<"log">>, <<"stuff">>, 2]),
+ write([<<"log">>, 3, null]),
+ write([<<"log">>, [1, 2], {[{<<"level">>, <<"debug">>}]}]),
+ write([<<"log">>, <<"true">>, {[]}]).
+
+do_tests() ->
+ test_get_cfg1(),
+ test_get_cfg2(),
+ test_get_unknown_cfg(),
+ test_log(),
+ loop(io:read("")).
+
+loop({ok, _}) ->
+ loop(io:read(""));
+loop(eof) ->
+ init:stop();
+loop({error, _Reason}) ->
+ init:stop().
+
+main([]) ->
+ test_util:init_code_path(),
+ do_tests().
diff --git a/1.1.x/test/etap/171-os-daemons-config.t b/1.1.x/test/etap/171-os-daemons-config.t
new file mode 100755
index 00000000..e9dc3f32
--- /dev/null
+++ b/1.1.x/test/etap/171-os-daemons-config.t
@@ -0,0 +1,74 @@
+#!/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.
+
+-record(daemon, {
+ port,
+ name,
+ cmd,
+ kill,
+ status=running,
+ cfg_patterns=[],
+ errors=[],
+ buf=[]
+}).
+
+config_files() ->
+ lists:map(fun test_util:build_file/1, [
+ "etc/couchdb/default_dev.ini"
+ ]).
+
+daemon_cmd() ->
+ test_util:source_file("test/etap/171-os-daemons-config.es").
+
+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() ->
+ couch_config:start_link(config_files()),
+ couch_config:set("log", "level", "debug", false),
+ couch_log:start_link(),
+ couch_os_daemons:start_link(),
+
+ % "foo" is a required name by this test.
+ couch_config:set("os_daemons", "foo", daemon_cmd(), false),
+ timer:sleep(1000),
+
+ {ok, [D1]} = couch_os_daemons:info([table]),
+ check_daemon(D1, "foo"),
+
+ ok.
+
+check_daemon(D, Name) ->
+ BaseName = "171-os-daemons-config.es",
+ BaseLen = length(BaseName),
+ CmdLen = length(D#daemon.cmd),
+ CmdName = lists:sublist(D#daemon.cmd, CmdLen-BaseLen+1, BaseLen),
+
+ etap:is(is_port(D#daemon.port), true, "Daemon port is a port."),
+ etap:is(D#daemon.name, Name, "Daemon name was set correctly."),
+ etap:is(CmdName, BaseName, "Command name was set correctly."),
+ etap:isnt(D#daemon.kill, undefined, "Kill command was set."),
+ etap:is(D#daemon.errors, [], "No errors occurred while booting."),
+ etap:is(D#daemon.buf, [], "No extra data left in the buffer.").
diff --git a/1.1.x/test/etap/172-os-daemon-errors.1.es b/1.1.x/test/etap/172-os-daemon-errors.1.es
new file mode 100644
index 00000000..a9defba1
--- /dev/null
+++ b/1.1.x/test/etap/172-os-daemon-errors.1.es
@@ -0,0 +1,22 @@
+#! /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.
+
+% Please do not make this file executable as that's the error being tested.
+
+loop() ->
+ timer:sleep(5000),
+ loop().
+
+main([]) ->
+ loop().
diff --git a/1.1.x/test/etap/172-os-daemon-errors.2.es b/1.1.x/test/etap/172-os-daemon-errors.2.es
new file mode 100755
index 00000000..52de0401
--- /dev/null
+++ b/1.1.x/test/etap/172-os-daemon-errors.2.es
@@ -0,0 +1,16 @@
+#! /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([]) ->
+ init:stop().
diff --git a/1.1.x/test/etap/172-os-daemon-errors.3.es b/1.1.x/test/etap/172-os-daemon-errors.3.es
new file mode 100755
index 00000000..64229800
--- /dev/null
+++ b/1.1.x/test/etap/172-os-daemon-errors.3.es
@@ -0,0 +1,17 @@
+#! /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([]) ->
+ timer:sleep(1000),
+ init:stop().
diff --git a/1.1.x/test/etap/172-os-daemon-errors.4.es b/1.1.x/test/etap/172-os-daemon-errors.4.es
new file mode 100755
index 00000000..577f3410
--- /dev/null
+++ b/1.1.x/test/etap/172-os-daemon-errors.4.es
@@ -0,0 +1,17 @@
+#! /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([]) ->
+ timer:sleep(2000),
+ init:stop().
diff --git a/1.1.x/test/etap/172-os-daemon-errors.t b/1.1.x/test/etap/172-os-daemon-errors.t
new file mode 100755
index 00000000..287a0812
--- /dev/null
+++ b/1.1.x/test/etap/172-os-daemon-errors.t
@@ -0,0 +1,126 @@
+#!/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.
+
+-record(daemon, {
+ port,
+ name,
+ cmd,
+ kill,
+ status=running,
+ cfg_patterns=[],
+ errors=[],
+ buf=[]
+}).
+
+config_files() ->
+ lists:map(fun test_util:build_file/1, [
+ "etc/couchdb/default_dev.ini"
+ ]).
+
+bad_perms() ->
+ test_util:source_file("test/etap/172-os-daemon-errors.1.es").
+
+die_on_boot() ->
+ test_util:source_file("test/etap/172-os-daemon-errors.2.es").
+
+die_quickly() ->
+ test_util:source_file("test/etap/172-os-daemon-errors.3.es").
+
+can_reboot() ->
+ test_util:source_file("test/etap/172-os-daemon-errors.4.es").
+
+main(_) ->
+ test_util:init_code_path(),
+
+ etap:plan(36),
+ 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:start_link(config_files()),
+ couch_os_daemons:start_link(),
+
+ etap:diag("Daemon not executable."),
+ test_halts("foo", bad_perms(), 1000),
+
+ etap:diag("Daemon dies on boot."),
+ test_halts("bar", die_on_boot(), 1000),
+
+ etap:diag("Daemon dies quickly after boot."),
+ test_halts("baz", die_quickly(), 4000),
+
+ etap:diag("Daemon dies, but not quickly enough to be halted."),
+ test_runs("bam", can_reboot()),
+
+ ok.
+
+test_halts(Name, Cmd, Time) ->
+ couch_config:set("os_daemons", Name, Cmd ++ " 2> /dev/null", false),
+ timer:sleep(Time),
+ {ok, [D]} = couch_os_daemons:info([table]),
+ check_dead(D, Name, Cmd),
+ couch_config:delete("os_daemons", Name, false).
+
+test_runs(Name, Cmd) ->
+ couch_config:set("os_daemons", Name, Cmd, false),
+
+ timer:sleep(1000),
+ {ok, [D1]} = couch_os_daemons:info([table]),
+ check_daemon(D1, Name, Cmd, 0),
+
+ % Should reboot every two seconds. We're at 1s, so wait
+ % utnil 3s to be in the middle of the next invocation's
+ % life span.
+ timer:sleep(2000),
+ {ok, [D2]} = couch_os_daemons:info([table]),
+ check_daemon(D2, Name, Cmd, 1),
+
+ % If the kill command changed, that means we rebooted the process.
+ etap:isnt(D1#daemon.kill, D2#daemon.kill, "Kill command changed.").
+
+check_dead(D, Name, Cmd) ->
+ BaseName = filename:basename(Cmd) ++ " 2> /dev/null",
+ BaseLen = length(BaseName),
+ CmdLen = length(D#daemon.cmd),
+ CmdName = lists:sublist(D#daemon.cmd, CmdLen-BaseLen+1, BaseLen),
+
+ etap:is(is_port(D#daemon.port), true, "Daemon port is a port."),
+ etap:is(D#daemon.name, Name, "Daemon name was set correctly."),
+ etap:is(CmdName, BaseName, "Command name was set correctly."),
+ etap:isnt(D#daemon.kill, undefined, "Kill command was set."),
+ etap:is(D#daemon.status, halted, "Daemon has been halted."),
+ etap:is(D#daemon.errors, nil, "Errors have been disabled."),
+ etap:is(D#daemon.buf, nil, "Buffer has been switched off.").
+
+check_daemon(D, Name, Cmd, Errs) ->
+ BaseName = filename:basename(Cmd),
+ BaseLen = length(BaseName),
+ CmdLen = length(D#daemon.cmd),
+ CmdName = lists:sublist(D#daemon.cmd, CmdLen-BaseLen+1, BaseLen),
+
+ etap:is(is_port(D#daemon.port), true, "Daemon port is a port."),
+ etap:is(D#daemon.name, Name, "Daemon name was set correctly."),
+ etap:is(CmdName, BaseName, "Command name was set correctly."),
+ etap:isnt(D#daemon.kill, undefined, "Kill command was set."),
+ etap:is(D#daemon.status, running, "Daemon still running."),
+ etap:is(length(D#daemon.errors), Errs, "Found expected number of errors."),
+ etap:is(D#daemon.buf, [], "No extra data left in the buffer.").
+
diff --git a/1.1.x/test/etap/173-os-daemon-cfg-register.t b/1.1.x/test/etap/173-os-daemon-cfg-register.t
new file mode 100755
index 00000000..71181aa2
--- /dev/null
+++ b/1.1.x/test/etap/173-os-daemon-cfg-register.t
@@ -0,0 +1,93 @@
+#!/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.
+
+-record(daemon, {
+ port,
+ name,
+ cmd,
+ kill,
+ status=running,
+ cfg_patterns=[],
+ errors=[],
+ buf=[]
+}).
+
+daemon_name() ->
+ "wheee".
+
+daemon_cmd() ->
+ test_util:build_file("test/etap/test_cfg_register").
+
+main(_) ->
+ test_util:init_code_path(),
+
+ etap:plan(27),
+ 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:start_link(test_util:config_files()),
+ couch_os_daemons:start_link(),
+
+ DaemonCmd = daemon_cmd() ++ " 2> /dev/null",
+
+ etap:diag("Booting the daemon"),
+ couch_config:set("os_daemons", daemon_name(), DaemonCmd, false),
+ timer:sleep(1000),
+ {ok, [D1]} = couch_os_daemons:info([table]),
+ check_daemon(D1, running),
+
+ etap:diag("Daemon restarts when section changes."),
+ couch_config:set("s1", "k", "foo", false),
+ timer:sleep(1000),
+ {ok, [D2]} = couch_os_daemons:info([table]),
+ check_daemon(D2, running),
+ etap:isnt(D2#daemon.kill, D1#daemon.kill, "Kill command shows restart."),
+
+ etap:diag("Daemon doesn't restart for ignored section key."),
+ couch_config:set("s2", "k2", "baz", false),
+ timer:sleep(1000),
+ {ok, [D3]} = couch_os_daemons:info([table]),
+ etap:is(D3, D2, "Same daemon info after ignored config change."),
+
+ etap:diag("Daemon restarts for specific section/key pairs."),
+ couch_config:set("s2", "k", "bingo", false),
+ timer:sleep(1000),
+ {ok, [D4]} = couch_os_daemons:info([table]),
+ check_daemon(D4, running),
+ etap:isnt(D4#daemon.kill, D3#daemon.kill, "Kill command changed again."),
+
+ ok.
+
+check_daemon(D, Status) ->
+ BaseName = filename:basename(daemon_cmd()) ++ " 2> /dev/null",
+ BaseLen = length(BaseName),
+ CmdLen = length(D#daemon.cmd),
+ CmdName = lists:sublist(D#daemon.cmd, CmdLen-BaseLen+1, BaseLen),
+
+ etap:is(is_port(D#daemon.port), true, "Daemon port is a port."),
+ etap:is(D#daemon.name, daemon_name(), "Daemon name was set correctly."),
+ etap:is(CmdName, BaseName, "Command name was set correctly."),
+ etap:isnt(D#daemon.kill, undefined, "Kill command was set."),
+ etap:is(D#daemon.status, Status, "Daemon status is correct."),
+ etap:is(D#daemon.cfg_patterns, [{"s1"}, {"s2", "k"}], "Cfg patterns set"),
+ etap:is(D#daemon.errors, [], "No errors have occurred."),
+ etap:isnt(D#daemon.buf, nil, "Buffer is active.").
diff --git a/1.1.x/test/etap/180-http-proxy.ini b/1.1.x/test/etap/180-http-proxy.ini
new file mode 100644
index 00000000..3e2ba137
--- /dev/null
+++ b/1.1.x/test/etap/180-http-proxy.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.
+
+; 49151 is IANA Reserved, let's assume no one is listening there
+[httpd_global_handlers]
+_error = {couch_httpd_proxy, handle_proxy_req, <<"http://127.0.0.1:49151/">>}
diff --git a/1.1.x/test/etap/180-http-proxy.t b/1.1.x/test/etap/180-http-proxy.t
new file mode 100755
index 00000000..41c77631
--- /dev/null
+++ b/1.1.x/test/etap/180-http-proxy.t
@@ -0,0 +1,378 @@
+#!/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.
+
+-record(req, {method=get, path="", headers=[], body="", opts=[]}).
+
+server() ->
+ lists:concat([
+ "http://127.0.0.1:",
+ mochiweb_socket_server:get(couch_httpd, port),
+ "/_test/"
+ ]).
+
+proxy() ->
+ "http://127.0.0.1:" ++ integer_to_list(test_web:get_port()) ++ "/".
+
+external() -> "https://www.google.com/".
+
+main(_) ->
+ test_util:init_code_path(),
+
+ etap:plan(61),
+ case (catch test()) of
+ ok ->
+ etap:end_tests();
+ Other ->
+ etap:diag("Test died abnormally: ~p", [Other]),
+ etap:bail("Bad return value.")
+ end,
+ ok.
+
+check_request(Name, Req, Remote, Local) ->
+ case Remote of
+ no_remote -> ok;
+ _ -> test_web:set_assert(Remote)
+ end,
+ Url = case proplists:lookup(url, Req#req.opts) of
+ none -> server() ++ Req#req.path;
+ {url, DestUrl} -> DestUrl
+ end,
+ Opts = [{headers_as_is, true} | Req#req.opts],
+ Resp =ibrowse:send_req(
+ Url, Req#req.headers, Req#req.method, Req#req.body, Opts
+ ),
+ %etap:diag("ibrowse response: ~p", [Resp]),
+ case Local of
+ no_local -> ok;
+ _ -> etap:fun_is(Local, Resp, Name)
+ end,
+ case {Remote, Local} of
+ {no_remote, _} ->
+ ok;
+ {_, no_local} ->
+ ok;
+ _ ->
+ etap:is(test_web:check_last(), was_ok, Name ++ " - request handled")
+ end,
+ Resp.
+
+test() ->
+ couch_server_sup:start_link([
+ test_util:source_file("test/etap/180-http-proxy.ini") |
+ test_util:config_files()
+ ]),
+ ibrowse:start(),
+ crypto:start(),
+
+ % start the test_web server on a random port
+ test_web:start_link(),
+ Url = lists:concat([
+ "{couch_httpd_proxy, handle_proxy_req, <<\"http://127.0.0.1:",
+ test_web:get_port(),
+ "/\">>}"
+ ]),
+ couch_config:set("httpd_global_handlers", "_test", Url, false),
+
+ % let couch_httpd restart
+ timer:sleep(100),
+
+ test_basic(),
+ test_alternate_status(),
+ test_trailing_slash(),
+ test_passes_header(),
+ test_passes_host_header(),
+ test_passes_header_back(),
+ test_rewrites_location_headers(),
+ test_doesnt_rewrite_external_locations(),
+ test_rewrites_relative_location(),
+ test_uses_same_version(),
+ test_passes_body(),
+ test_passes_eof_body_back(),
+ test_passes_chunked_body(),
+ test_passes_chunked_body_back(),
+
+ test_connect_error(),
+
+ ok.
+
+test_basic() ->
+ Remote = fun(Req) ->
+ 'GET' = Req:get(method),
+ "/" = Req:get(path),
+ 0 = Req:get(body_length),
+ <<>> = Req:recv_body(),
+ {ok, {200, [{"Content-Type", "text/plain"}], "ok"}}
+ end,
+ Local = fun({ok, "200", _, "ok"}) -> true; (_) -> false end,
+ check_request("Basic proxy test", #req{}, Remote, Local).
+
+test_alternate_status() ->
+ Remote = fun(Req) ->
+ "/alternate_status" = Req:get(path),
+ {ok, {201, [], "ok"}}
+ end,
+ Local = fun({ok, "201", _, "ok"}) -> true; (_) -> false end,
+ Req = #req{path="alternate_status"},
+ check_request("Alternate status", Req, Remote, Local).
+
+test_trailing_slash() ->
+ Remote = fun(Req) ->
+ "/trailing_slash/" = Req:get(path),
+ {ok, {200, [], "ok"}}
+ end,
+ Local = fun({ok, "200", _, "ok"}) -> true; (_) -> false end,
+ Req = #req{path="trailing_slash/"},
+ check_request("Trailing slash", Req, Remote, Local).
+
+test_passes_header() ->
+ Remote = fun(Req) ->
+ "/passes_header" = Req:get(path),
+ "plankton" = Req:get_header_value("X-CouchDB-Ralph"),
+ {ok, {200, [], "ok"}}
+ end,
+ Local = fun({ok, "200", _, "ok"}) -> true; (_) -> false end,
+ Req = #req{
+ path="passes_header",
+ headers=[{"X-CouchDB-Ralph", "plankton"}]
+ },
+ check_request("Passes header", Req, Remote, Local).
+
+test_passes_host_header() ->
+ Remote = fun(Req) ->
+ "/passes_host_header" = Req:get(path),
+ "www.google.com" = Req:get_header_value("Host"),
+ {ok, {200, [], "ok"}}
+ end,
+ Local = fun({ok, "200", _, "ok"}) -> true; (_) -> false end,
+ Req = #req{
+ path="passes_host_header",
+ headers=[{"Host", "www.google.com"}]
+ },
+ check_request("Passes host header", Req, Remote, Local).
+
+test_passes_header_back() ->
+ Remote = fun(Req) ->
+ "/passes_header_back" = Req:get(path),
+ {ok, {200, [{"X-CouchDB-Plankton", "ralph"}], "ok"}}
+ end,
+ Local = fun
+ ({ok, "200", Headers, "ok"}) ->
+ lists:member({"X-CouchDB-Plankton", "ralph"}, Headers);
+ (_) ->
+ false
+ end,
+ Req = #req{path="passes_header_back"},
+ check_request("Passes header back", Req, Remote, Local).
+
+test_rewrites_location_headers() ->
+ etap:diag("Testing location header rewrites."),
+ do_rewrite_tests([
+ {"Location", proxy() ++ "foo/bar", server() ++ "foo/bar"},
+ {"Content-Location", proxy() ++ "bing?q=2", server() ++ "bing?q=2"},
+ {"Uri", proxy() ++ "zip#frag", server() ++ "zip#frag"},
+ {"Destination", proxy(), server()}
+ ]).
+
+test_doesnt_rewrite_external_locations() ->
+ etap:diag("Testing no rewrite of external locations."),
+ do_rewrite_tests([
+ {"Location", external() ++ "search", external() ++ "search"},
+ {"Content-Location", external() ++ "s?q=2", external() ++ "s?q=2"},
+ {"Uri", external() ++ "f#f", external() ++ "f#f"},
+ {"Destination", external() ++ "f?q=2#f", external() ++ "f?q=2#f"}
+ ]).
+
+test_rewrites_relative_location() ->
+ etap:diag("Testing relative rewrites."),
+ do_rewrite_tests([
+ {"Location", "/foo", server() ++ "foo"},
+ {"Content-Location", "bar", server() ++ "bar"},
+ {"Uri", "/zing?q=3", server() ++ "zing?q=3"},
+ {"Destination", "bing?q=stuff#yay", server() ++ "bing?q=stuff#yay"}
+ ]).
+
+do_rewrite_tests(Tests) ->
+ lists:foreach(fun({Header, Location, Url}) ->
+ do_rewrite_test(Header, Location, Url)
+ end, Tests).
+
+do_rewrite_test(Header, Location, Url) ->
+ Remote = fun(Req) ->
+ "/rewrite_test" = Req:get(path),
+ {ok, {302, [{Header, Location}], "ok"}}
+ end,
+ Local = fun
+ ({ok, "302", Headers, "ok"}) ->
+ etap:is(
+ couch_util:get_value(Header, Headers),
+ Url,
+ "Header rewritten correctly."
+ ),
+ true;
+ (_) ->
+ false
+ end,
+ Req = #req{path="rewrite_test"},
+ Label = "Rewrite test for ",
+ check_request(Label ++ Header, Req, Remote, Local).
+
+test_uses_same_version() ->
+ Remote = fun(Req) ->
+ "/uses_same_version" = Req:get(path),
+ {1, 0} = Req:get(version),
+ {ok, {200, [], "ok"}}
+ end,
+ Local = fun({ok, "200", _, "ok"}) -> true; (_) -> false end,
+ Req = #req{
+ path="uses_same_version",
+ opts=[{http_vsn, {1, 0}}]
+ },
+ check_request("Uses same version", Req, Remote, Local).
+
+test_passes_body() ->
+ Remote = fun(Req) ->
+ 'PUT' = Req:get(method),
+ "/passes_body" = Req:get(path),
+ <<"Hooray!">> = Req:recv_body(),
+ {ok, {201, [], "ok"}}
+ end,
+ Local = fun({ok, "201", _, "ok"}) -> true; (_) -> false end,
+ Req = #req{
+ method=put,
+ path="passes_body",
+ body="Hooray!"
+ },
+ check_request("Passes body", Req, Remote, Local).
+
+test_passes_eof_body_back() ->
+ BodyChunks = [<<"foo">>, <<"bar">>, <<"bazinga">>],
+ Remote = fun(Req) ->
+ 'GET' = Req:get(method),
+ "/passes_eof_body" = Req:get(path),
+ {raw, {200, [{"Connection", "close"}], BodyChunks}}
+ end,
+ Local = fun({ok, "200", _, "foobarbazinga"}) -> true; (_) -> false end,
+ Req = #req{path="passes_eof_body"},
+ check_request("Passes eof body", Req, Remote, Local).
+
+test_passes_chunked_body() ->
+ BodyChunks = [<<"foo">>, <<"bar">>, <<"bazinga">>],
+ Remote = fun(Req) ->
+ 'POST' = Req:get(method),
+ "/passes_chunked_body" = Req:get(path),
+ RecvBody = fun
+ ({Length, Chunk}, [Chunk | Rest]) ->
+ Length = size(Chunk),
+ Rest;
+ ({0, []}, []) ->
+ ok
+ end,
+ ok = Req:stream_body(1024*1024, RecvBody, BodyChunks),
+ {ok, {201, [], "ok"}}
+ end,
+ Local = fun({ok, "201", _, "ok"}) -> true; (_) -> false end,
+ Req = #req{
+ method=post,
+ path="passes_chunked_body",
+ headers=[{"Transfer-Encoding", "chunked"}],
+ body=mk_chunked_body(BodyChunks)
+ },
+ check_request("Passes chunked body", Req, Remote, Local).
+
+test_passes_chunked_body_back() ->
+ Name = "Passes chunked body back",
+ Remote = fun(Req) ->
+ 'GET' = Req:get(method),
+ "/passes_chunked_body_back" = Req:get(path),
+ BodyChunks = [<<"foo">>, <<"bar">>, <<"bazinga">>],
+ {chunked, {200, [{"Transfer-Encoding", "chunked"}], BodyChunks}}
+ end,
+ Req = #req{
+ path="passes_chunked_body_back",
+ opts=[{stream_to, self()}]
+ },
+
+ Resp = check_request(Name, Req, Remote, no_local),
+
+ etap:fun_is(
+ fun({ibrowse_req_id, _}) -> true; (_) -> false end,
+ Resp,
+ "Received an ibrowse request id."
+ ),
+ {_, ReqId} = Resp,
+
+ % Grab headers from response
+ receive
+ {ibrowse_async_headers, ReqId, "200", Headers} ->
+ etap:is(
+ proplists:get_value("Transfer-Encoding", Headers),
+ "chunked",
+ "Response included the Transfer-Encoding: chunked header"
+ ),
+ ibrowse:stream_next(ReqId)
+ after 1000 ->
+ throw({error, timeout})
+ end,
+
+ % Check body received
+ % TODO: When we upgrade to ibrowse >= 2.0.0 this check needs to
+ % check that the chunks returned are what we sent from the
+ % Remote test.
+ etap:diag("TODO: UPGRADE IBROWSE"),
+ etap:is(recv_body(ReqId, []), <<"foobarbazinga">>, "Decoded chunked body."),
+
+ % Check test_web server.
+ etap:is(test_web:check_last(), was_ok, Name ++ " - request handled").
+
+test_connect_error() ->
+ Local = fun({ok, "500", _Headers, _Body}) -> true; (_) -> false end,
+ Url = lists:concat([
+ "http://127.0.0.1:",
+ mochiweb_socket_server:get(couch_httpd, port),
+ "/_error"
+ ]),
+ Req = #req{opts=[{url, Url}]},
+ check_request("Connect error", Req, no_remote, Local).
+
+
+mk_chunked_body(Chunks) ->
+ mk_chunked_body(Chunks, []).
+
+mk_chunked_body([], Acc) ->
+ iolist_to_binary(lists:reverse(Acc, "0\r\n\r\n"));
+mk_chunked_body([Chunk | Rest], Acc) ->
+ Size = to_hex(size(Chunk)),
+ mk_chunked_body(Rest, ["\r\n", Chunk, "\r\n", Size | Acc]).
+
+to_hex(Val) ->
+ to_hex(Val, []).
+
+to_hex(0, Acc) ->
+ Acc;
+to_hex(Val, Acc) ->
+ to_hex(Val div 16, [hex_char(Val rem 16) | Acc]).
+
+hex_char(V) when V < 10 -> $0 + V;
+hex_char(V) -> $A + V - 10.
+
+recv_body(ReqId, Acc) ->
+ receive
+ {ibrowse_async_response, ReqId, Data} ->
+ recv_body(ReqId, [Data | Acc]);
+ {ibrowse_async_response_end, ReqId} ->
+ iolist_to_binary(lists:reverse(Acc));
+ Else ->
+ throw({error, unexpected_mesg, Else})
+ after 5000 ->
+ throw({error, timeout})
+ end.
diff --git a/1.1.x/test/etap/200-view-group-no-db-leaks.t b/1.1.x/test/etap/200-view-group-no-db-leaks.t
new file mode 100755
index 00000000..9c77f1a8
--- /dev/null
+++ b/1.1.x/test/etap/200-view-group-no-db-leaks.t
@@ -0,0 +1,262 @@
+#!/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.
+
+-record(user_ctx, {
+ name = null,
+ roles = [],
+ handler
+}).
+
+-define(LATEST_DISK_VERSION, 5).
+
+-record(db_header,
+ {disk_version = ?LATEST_DISK_VERSION,
+ update_seq = 0,
+ unused = 0,
+ fulldocinfo_by_id_btree_state = nil,
+ docinfo_by_seq_btree_state = nil,
+ local_docs_btree_state = nil,
+ purge_seq = 0,
+ purged_docs = nil,
+ security_ptr = nil,
+ revs_limit = 1000
+}).
+
+-record(db, {
+ main_pid = nil,
+ update_pid = nil,
+ compactor_pid = nil,
+ instance_start_time, % number of microsecs since jan 1 1970 as a binary string
+ fd,
+ fd_ref_counter,
+ header = #db_header{},
+ committed_update_seq,
+ fulldocinfo_by_id_btree,
+ docinfo_by_seq_btree,
+ local_docs_btree,
+ update_seq,
+ name,
+ filepath,
+ validate_doc_funs = [],
+ security = [],
+ security_ptr = nil,
+ user_ctx = #user_ctx{},
+ waiting_delayed_commit = nil,
+ revs_limit = 1000,
+ fsync_options = [],
+ is_sys_db = false
+}).
+
+test_db_name() -> <<"couch_test_view_group_db_leaks">>.
+ddoc_name() -> <<"foo">>.
+
+main(_) ->
+ test_util:init_code_path(),
+
+ etap:plan(18),
+ 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_server_sup:start_link(test_util:config_files()),
+ timer:sleep(1000),
+ put(addr, couch_config:get("httpd", "bind_address", "127.0.0.1")),
+ put(port, integer_to_list(mochiweb_socket_server:get(couch_httpd, port))),
+ application:start(inets),
+
+ delete_db(),
+ create_db(),
+
+ create_docs(),
+ create_design_doc(),
+
+ ViewGroup = couch_view:get_group_server(
+ test_db_name(), <<"_design/", (ddoc_name())/binary>>),
+ etap:is(is_pid(ViewGroup), true, "got view group pid"),
+ etap:is(is_process_alive(ViewGroup), true, "view group pid is alive"),
+
+ query_view(),
+ check_db_ref_count(),
+ etap:is(is_process_alive(ViewGroup), true, "view group pid is alive"),
+
+ create_new_doc(<<"doc1000">>),
+ query_view(),
+ check_db_ref_count(),
+ etap:is(is_process_alive(ViewGroup), true, "view group pid is alive"),
+
+ Ref1 = get_db_ref_counter(),
+ compact_db(),
+ check_db_ref_count(),
+ Ref2 = get_db_ref_counter(),
+ etap:isnt(Ref1, Ref2, "DB ref counter changed"),
+ etap:is(false, is_process_alive(Ref1), "old DB ref counter is not alive"),
+ etap:is(is_process_alive(ViewGroup), true, "view group pid is alive"),
+
+ compact_view_group(),
+ check_db_ref_count(),
+ Ref3 = get_db_ref_counter(),
+ etap:is(Ref3, Ref2, "DB ref counter didn't change"),
+ etap:is(is_process_alive(ViewGroup), true, "view group pid is alive"),
+
+ create_new_doc(<<"doc1001">>),
+ query_view(),
+ check_db_ref_count(),
+ etap:is(is_process_alive(ViewGroup), true, "view group pid is alive"),
+
+ MonRef = erlang:monitor(process, ViewGroup),
+ ok = couch_server:delete(test_db_name(), []),
+ receive
+ {'DOWN', MonRef, _, _, _} ->
+ etap:diag("view group is dead after DB deletion")
+ after 5000 ->
+ etap:bail("view group did not die after DB deletion")
+ end,
+
+ ok = timer:sleep(1000),
+ delete_db(),
+ couch_server_sup:stop(),
+ ok.
+
+admin_user_ctx() ->
+ {user_ctx, #user_ctx{roles=[<<"_admin">>]}}.
+
+create_db() ->
+ {ok, Db} = couch_db:create(test_db_name(), [admin_user_ctx()]),
+ ok = couch_db:close(Db).
+
+delete_db() ->
+ couch_server:delete(test_db_name(), [admin_user_ctx()]).
+
+compact_db() ->
+ {ok, Db} = couch_db:open_int(test_db_name(), []),
+ ok = couch_db:start_compact(Db),
+ ok = couch_db:close(Db),
+ wait_db_compact_done(10).
+
+wait_db_compact_done(0) ->
+ etap:bail("DB compaction failed to finish.");
+wait_db_compact_done(N) ->
+ {ok, Db} = couch_db:open_int(test_db_name(), []),
+ ok = couch_db:close(Db),
+ case is_pid(Db#db.compactor_pid) of
+ false ->
+ ok;
+ true ->
+ ok = timer:sleep(500),
+ wait_db_compact_done(N - 1)
+ end.
+
+compact_view_group() ->
+ ok = couch_view_compactor:start_compact(test_db_name(), ddoc_name()),
+ wait_view_compact_done(10).
+
+wait_view_compact_done(0) ->
+ etap:bail("View group compaction failed to finish.");
+wait_view_compact_done(N) ->
+ {ok, {{_, Code, _}, _Headers, Body}} = http:request(
+ get,
+ {db_url() ++ "/_design/" ++ binary_to_list(ddoc_name()) ++ "/_info", []},
+ [],
+ [{sync, true}]),
+ case Code of
+ 200 -> ok;
+ _ -> etap:bail("Invalid view group info.")
+ end,
+ {Info} = couch_util:json_decode(Body),
+ {IndexInfo} = couch_util:get_value(<<"view_index">>, Info),
+ CompactRunning = couch_util:get_value(<<"compact_running">>, IndexInfo),
+ case CompactRunning of
+ false ->
+ ok;
+ true ->
+ ok = timer:sleep(500),
+ wait_view_compact_done(N - 1)
+ end.
+
+get_db_ref_counter() ->
+ {ok, #db{fd_ref_counter = Ref} = Db} = couch_db:open_int(test_db_name(), []),
+ ok = couch_db:close(Db),
+ Ref.
+
+check_db_ref_count() ->
+ {ok, #db{fd_ref_counter = Ref} = Db} = couch_db:open_int(test_db_name(), []),
+ ok = couch_db:close(Db),
+ etap:is(couch_ref_counter:count(Ref), 2,
+ "DB ref counter is only held by couch_db and couch_db_updater"),
+ ok.
+
+create_docs() ->
+ {ok, Db} = couch_db:open(test_db_name(), [admin_user_ctx()]),
+ Doc1 = couch_doc:from_json_obj({[
+ {<<"_id">>, <<"doc1">>},
+ {<<"value">>, 1}
+ ]}),
+ Doc2 = couch_doc:from_json_obj({[
+ {<<"_id">>, <<"doc2">>},
+ {<<"value">>, 2}
+
+ ]}),
+ Doc3 = couch_doc:from_json_obj({[
+ {<<"_id">>, <<"doc3">>},
+ {<<"value">>, 3}
+ ]}),
+ {ok, _} = couch_db:update_docs(Db, [Doc1, Doc2, Doc3]),
+ couch_db:ensure_full_commit(Db),
+ couch_db:close(Db).
+
+create_design_doc() ->
+ {ok, Db} = couch_db:open(test_db_name(), [admin_user_ctx()]),
+ DDoc = couch_doc:from_json_obj({[
+ {<<"_id">>, <<"_design/", (ddoc_name())/binary>>},
+ {<<"language">>, <<"javascript">>},
+ {<<"views">>, {[
+ {<<"bar">>, {[
+ {<<"map">>, <<"function(doc) { emit(doc._id, null); }">>}
+ ]}}
+ ]}}
+ ]}),
+ {ok, _} = couch_db:update_docs(Db, [DDoc]),
+ couch_db:ensure_full_commit(Db),
+ couch_db:close(Db).
+
+create_new_doc(Id) ->
+ {ok, Db} = couch_db:open(test_db_name(), [admin_user_ctx()]),
+ Doc666 = couch_doc:from_json_obj({[
+ {<<"_id">>, Id},
+ {<<"value">>, 999}
+ ]}),
+ {ok, _} = couch_db:update_docs(Db, [Doc666]),
+ couch_db:ensure_full_commit(Db),
+ couch_db:close(Db).
+
+db_url() ->
+ "http://" ++ get(addr) ++ ":" ++ get(port) ++ "/" ++
+ binary_to_list(test_db_name()).
+
+query_view() ->
+ {ok, {{_, Code, _}, _Headers, _Body}} = http:request(
+ get,
+ {db_url() ++ "/_design/" ++ binary_to_list(ddoc_name()) ++
+ "/_view/bar", []},
+ [],
+ [{sync, true}]),
+ etap:is(Code, 200, "got view response"),
+ ok.
diff --git a/1.1.x/test/etap/Makefile.am b/1.1.x/test/etap/Makefile.am
new file mode 100644
index 00000000..9ba3fcfa
--- /dev/null
+++ b/1.1.x/test/etap/Makefile.am
@@ -0,0 +1,88 @@
+## 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.
+
+noinst_SCRIPTS = run
+noinst_DATA = test_util.beam test_web.beam
+
+noinst_PROGRAMS = test_cfg_register
+test_cfg_register_SOURCES = test_cfg_register.c
+test_cfg_register_CFLAGS = -D_BSD_SOURCE
+
+%.beam: %.erl
+ $(ERLC) $<
+
+run: run.tpl
+ sed -e "s|%abs_top_srcdir%|@abs_top_srcdir@|g" \
+ -e "s|%abs_top_builddir%|@abs_top_builddir@|g" > \
+ $@ < $<
+ chmod +x $@
+
+CLEANFILES = run *.beam
+DISTCLEANFILES = temp.*
+
+EXTRA_DIST = \
+ run.tpl \
+ test_web.erl \
+ random_port.ini \
+ 001-load.t \
+ 002-icu-driver.t \
+ 010-file-basics.t \
+ 011-file-headers.t \
+ 020-btree-basics.t \
+ 021-btree-reductions.t \
+ 030-doc-from-json.t \
+ 031-doc-to-json.t \
+ 040-util.t \
+ 041-uuid-gen-seq.ini \
+ 041-uuid-gen-utc.ini \
+ 041-uuid-gen.t \
+ 050-stream.t \
+ 060-kt-merging.t \
+ 061-kt-missing-leaves.t \
+ 062-kt-remove-leaves.t \
+ 063-kt-get-leaves.t \
+ 064-kt-counting.t \
+ 065-kt-stemming.t \
+ 070-couch-db.t \
+ 080-config-get-set.t \
+ 081-config-override.1.ini \
+ 081-config-override.2.ini \
+ 081-config-override.t \
+ 082-config-register.t \
+ 083-config-no-files.t \
+ 090-task-status.t \
+ 100-ref-counter.t \
+ 110-replication-httpc.t \
+ 111-replication-changes-feed.t \
+ 112-replication-missing-revs.t \
+ 113-replication-attachment-comp.t \
+ 120-stats-collect.t \
+ 121-stats-aggregates.cfg \
+ 121-stats-aggregates.ini \
+ 121-stats-aggregates.t \
+ 130-attachments-md5.t \
+ 140-attachment-comp.t \
+ 150-invalid-view-seq.t \
+ 160-vhosts.t \
+ 170-os-daemons.es \
+ 170-os-daemons.t \
+ 171-os-daemons-config.es \
+ 171-os-daemons-config.t \
+ 172-os-daemon-errors.1.es \
+ 172-os-daemon-errors.2.es \
+ 172-os-daemon-errors.3.es \
+ 172-os-daemon-errors.4.es \
+ 172-os-daemon-errors.t \
+ 173-os-daemon-cfg-register.t \
+ 180-http-proxy.ini \
+ 180-http-proxy.t \
+ 200-view-group-no-db-leaks.t
diff --git a/1.1.x/test/etap/random_port.ini b/1.1.x/test/etap/random_port.ini
new file mode 100644
index 00000000..ada3c13d
--- /dev/null
+++ b/1.1.x/test/etap/random_port.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.
+
+[httpd]
+port = 0
diff --git a/1.1.x/test/etap/run.tpl b/1.1.x/test/etap/run.tpl
new file mode 100644
index 00000000..faf0f456
--- /dev/null
+++ b/1.1.x/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
diff --git a/1.1.x/test/etap/test_cfg_register.c b/1.1.x/test/etap/test_cfg_register.c
new file mode 100644
index 00000000..7161eb55
--- /dev/null
+++ b/1.1.x/test/etap/test_cfg_register.c
@@ -0,0 +1,30 @@
+// 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.
+
+#include <stdio.h>
+
+int
+main(int argc, const char * argv[])
+{
+ char c = '\0';
+ size_t num = 1;
+
+ fprintf(stdout, "[\"register\", \"s1\"]\n");
+ fprintf(stdout, "[\"register\", \"s2\", \"k\"]\n");
+ fflush(stdout);
+
+ while(c != '\n' && num > 0) {
+ num = fread(&c, 1, 1, stdin);
+ }
+
+ exit(0);
+}
diff --git a/1.1.x/test/etap/test_util.erl.in b/1.1.x/test/etap/test_util.erl.in
new file mode 100644
index 00000000..460b0293
--- /dev/null
+++ b/1.1.x/test/etap/test_util.erl.in
@@ -0,0 +1,42 @@
+% Licensed under the Apache License, Version 2.0 (the "License"); you may not
+% use this file except in compliance with the License. You may obtain a copy of
+% the License at
+%
+% http://www.apache.org/licenses/LICENSE-2.0
+%
+% Unless required by applicable law or agreed to in writing, software
+% distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+% WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+% License for the specific language governing permissions and limitations under
+% the License.
+
+-module(test_util).
+
+-export([init_code_path/0]).
+-export([source_file/1, build_file/1, config_files/0]).
+
+srcdir() ->
+ "@abs_top_srcdir@".
+
+builddir() ->
+ "@abs_top_builddir@".
+
+init_code_path() ->
+ Paths = ["etap", "couchdb", "erlang-oauth", "ibrowse", "mochiweb"],
+ lists:foreach(fun(Name) ->
+ code:add_patha(filename:join([builddir(), "src", Name]))
+ end, Paths).
+
+source_file(Name) ->
+ filename:join([srcdir(), Name]).
+
+build_file(Name) ->
+ filename:join([builddir(), Name]).
+
+config_files() ->
+ [
+ build_file("etc/couchdb/default_dev.ini"),
+ build_file("etc/couchdb/local_dev.ini"),
+ source_file("test/etap/random_port.ini")
+ ].
+
diff --git a/1.1.x/test/etap/test_web.erl b/1.1.x/test/etap/test_web.erl
new file mode 100644
index 00000000..ed78651f
--- /dev/null
+++ b/1.1.x/test/etap/test_web.erl
@@ -0,0 +1,99 @@
+% Licensed under the Apache License, Version 2.0 (the "License"); you may not
+% use this file except in compliance with the License. You may obtain a copy of
+% the License at
+%
+% http://www.apache.org/licenses/LICENSE-2.0
+%
+% Unless required by applicable law or agreed to in writing, software
+% distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
+% WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
+% License for the specific language governing permissions and limitations under
+% the License.
+
+-module(test_web).
+-behaviour(gen_server).
+
+-export([start_link/0, loop/1, get_port/0, set_assert/1, check_last/0]).
+-export([init/1, terminate/2, code_change/3]).
+-export([handle_call/3, handle_cast/2, handle_info/2]).
+
+-define(SERVER, test_web_server).
+-define(HANDLER, test_web_handler).
+
+start_link() ->
+ gen_server:start({local, ?HANDLER}, ?MODULE, [], []),
+ mochiweb_http:start([
+ {name, ?SERVER},
+ {loop, {?MODULE, loop}},
+ {port, 0}
+ ]).
+
+loop(Req) ->
+ %etap:diag("Handling request: ~p", [Req]),
+ case gen_server:call(?HANDLER, {check_request, Req}) of
+ {ok, RespInfo} ->
+ {ok, Req:respond(RespInfo)};
+ {raw, {Status, Headers, BodyChunks}} ->
+ Resp = Req:start_response({Status, Headers}),
+ lists:foreach(fun(C) -> Resp:send(C) end, BodyChunks),
+ erlang:put(mochiweb_request_force_close, true),
+ {ok, Resp};
+ {chunked, {Status, Headers, BodyChunks}} ->
+ Resp = Req:respond({Status, Headers, chunked}),
+ timer:sleep(500),
+ lists:foreach(fun(C) -> Resp:write_chunk(C) end, BodyChunks),
+ Resp:write_chunk([]),
+ {ok, Resp};
+ {error, Reason} ->
+ etap:diag("Error: ~p", [Reason]),
+ Body = lists:flatten(io_lib:format("Error: ~p", [Reason])),
+ {ok, Req:respond({200, [], Body})}
+ end.
+
+get_port() ->
+ mochiweb_socket_server:get(?SERVER, port).
+
+set_assert(Fun) ->
+ ok = gen_server:call(?HANDLER, {set_assert, Fun}).
+
+check_last() ->
+ gen_server:call(?HANDLER, last_status).
+
+init(_) ->
+ {ok, nil}.
+
+terminate(_Reason, _State) ->
+ ok.
+
+handle_call({check_request, Req}, _From, State) when is_function(State, 1) ->
+ Resp2 = case (catch State(Req)) of
+ {ok, Resp} -> {reply, {ok, Resp}, was_ok};
+ {raw, Resp} -> {reply, {raw, Resp}, was_ok};
+ {chunked, Resp} -> {reply, {chunked, Resp}, was_ok};
+ Error -> {reply, {error, Error}, not_ok}
+ end,
+ Req:cleanup(),
+ Resp2;
+handle_call({check_request, _Req}, _From, _State) ->
+ {reply, {error, no_assert_function}, not_ok};
+handle_call(last_status, _From, State) when is_atom(State) ->
+ {reply, State, nil};
+handle_call(last_status, _From, State) ->
+ {reply, {error, not_checked}, State};
+handle_call({set_assert, Fun}, _From, nil) ->
+ {reply, ok, Fun};
+handle_call({set_assert, _}, _From, State) ->
+ {reply, {error, assert_function_set}, State};
+handle_call(Msg, _From, State) ->
+ {reply, {ignored, Msg}, State}.
+
+handle_cast(Msg, State) ->
+ etap:diag("Ignoring cast message: ~p", [Msg]),
+ {noreply, State}.
+
+handle_info(Msg, State) ->
+ etap:diag("Ignoring info message: ~p", [Msg]),
+ {noreply, State}.
+
+code_change(_OldVsn, State, _Extra) ->
+ {ok, State}.