From 500bc564df316c7e7d081cecdf93dbc834b51a6f Mon Sep 17 00:00:00 2001 From: Till Maas Date: Oct 28 2015 20:32:18 +0000 Subject: 2015-10-28: Retired because it depends on erlang-mochiweb, which was retired, because it was orphaned for more than six weeks. --- diff --git a/.gitignore b/.gitignore deleted file mode 100644 index 386bb0c..0000000 --- a/.gitignore +++ /dev/null @@ -1,16 +0,0 @@ -apache-couchdb-0.11.1.tar.gz -apache-couchdb-1.0.0.tar.gz -apache-couchdb-1.0.1.tar.gz -/apache-couchdb-1.0.1.tar.gz -/apache-couchdb-1.0.2.tar.gz -/apache-couchdb-1.0.3-0-ga5f5eab.tar.gz -/apache-couchdb-1.0.3.tar.gz -/apache-couchdb-1.1.1.tar.gz -/apache-couchdb-1.2.0.tar.gz -/apache-couchdb-1.2.1.tar.gz -/apache-couchdb-1.2.2.tar.gz -/apache-couchdb-1.3.1.tar.gz -/apache-couchdb-1.5.0.tar.gz -/apache-couchdb-1.6.0.tar.gz -/couchdb-tests-blobs.tar -/apache-couchdb-1.6.1.tar.gz diff --git a/apache-couchdb-1.6.1.tar.gz.asc b/apache-couchdb-1.6.1.tar.gz.asc deleted file mode 100644 index 1a492af..0000000 --- a/apache-couchdb-1.6.1.tar.gz.asc +++ /dev/null @@ -1,17 +0,0 @@ ------BEGIN PGP SIGNATURE----- -Version: GnuPG/MacGPG2 v2.0 - -iQIcBAABCgAGBQJT9oijAAoJEM2wwPkE9O6baNYP/Aio0owVh5c1X1IQA99VmsDy -pYwKV7JS1mPMAzJr5DmRIYp4/L6y5zYYGy/0LoIhIBBzzpei4D3ou1Du3ILMe8CH -unkCCaETHkd7npdm522NpiOy/t4FHKMWeTyOfCcbS+82e3Ff28rpr8Mwa0L5y+u9 -p8NCaIPgQT3+QgK2L8ryeFJWAgzWair92jpkD+nR918lQZc6ZLlakP0Xc4+m22ax -QUDnSj8wPlDlvALdIpQVGSQR9+kzHFRGQwHSjNrGARomCXs0B1OpVNz3xCxMwB+K -pIlZutOl5WRF6TncZxxuFVrjciA6yVcn5lAfJwVtHnDHlCHngdt3HPiiMxTwfQYZ -EoZ6LS9ZkPBUd5VaJPq3DfJ0AIfXwENoiGWmaOmvgK+QmlfQQQpx8k3bUoph46xJ -6c7eBDplSOilvG9cpedAlSpkUtlVfiaXREaMPp20+ulmeyrtNmbpgaHzOwzIJ9Tw -BwGg0J4pjppKAnwcEwFbP4yd7GLAo6YLCUmt79BrwxrKtVjHXLH+dz/7PKOJ/tQO -xgvXSOvYMAx5m1eGJIb2MP1k/FuCK3BmVhg3LoKjyKmKTq3vzZZi/S6gpevb6cGl -jqqw0DPzJUBrss00Blk65ue1IUwrX7Mf8Uvu2qSt5ATjBwltLdMnaU9xkCGyHIYX -kfQmlBB1d5FamR/fCflt -=eMY1 ------END PGP SIGNATURE----- diff --git a/couchdb-0001-Do-not-gzip-doc-files-and-do-not-install-installatio.patch b/couchdb-0001-Do-not-gzip-doc-files-and-do-not-install-installatio.patch deleted file mode 100644 index 93b383c..0000000 --- a/couchdb-0001-Do-not-gzip-doc-files-and-do-not-install-installatio.patch +++ /dev/null @@ -1,75 +0,0 @@ -From: Peter Lemenkov -Date: Sun, 13 Feb 2011 13:52:38 +0300 -Subject: [PATCH] Do not gzip doc-files and do not install installation - instructions - -Signed-off-by: Peter Lemenkov - -diff --git a/Makefile.am b/Makefile.am -index 22809f8..661ba58 100644 ---- a/Makefile.am -+++ b/Makefile.am -@@ -15,16 +15,16 @@ SUBDIRS = bin etc src share test var utils - ACLOCAL_AMFLAGS = -I m4 - - localdoc_DATA = \ -- AUTHORS.gz \ -- BUGS.gz \ -- DEVELOPERS.gz \ -- INSTALL.gz \ -- INSTALL.Unix.gz \ -- INSTALL.Windows.gz \ -- LICENSE.gz \ -- NOTICE.gz \ -- README.gz \ -- THANKS.gz -+ AUTHORS \ -+ BUGS \ -+ DEVELOPERS \ -+ INSTALL \ -+ INSTALL.Unix \ -+ INSTALL.Windows \ -+ LICENSE \ -+ NOTICE \ -+ README \ -+ THANKS - - DISTCLEANFILES = $(localdoc_DATA) - -@@ -45,36 +45,6 @@ EXTRA_DIST = \ - license.skip \ - Vagrantfile - --AUTHORS.gz: AUTHORS -- gzip -9 < $< > $@ -- --BUGS.gz: BUGS -- gzip -9 < $< > $@ -- --DEVELOPERS.gz: DEVELOPERS -- gzip -9 < $< > $@ -- --INSTALL.gz: INSTALL -- gzip -9 < $< > $@ -- --INSTALL.Unix.gz: INSTALL.Unix -- gzip -9 < $< > $@ -- --INSTALL.Windows.gz: INSTALL.Windows -- gzip -9 < $< > $@ -- --LICENSE.gz: LICENSE -- gzip -9 < $< > $@ -- --NOTICE.gz: NOTICE -- gzip -9 < $< > $@ -- --README.gz: README.rst -- gzip -9 < $< > $@ -- --THANKS.gz: THANKS -- gzip -9 < $< > $@ -- - check: dev check-js - if TESTS - $(top_builddir)/test/etap/run $(top_srcdir)/test/etap diff --git a/couchdb-0002-More-directories-to-search-for-place-for-init-script.patch b/couchdb-0002-More-directories-to-search-for-place-for-init-script.patch deleted file mode 100644 index dec7f14..0000000 --- a/couchdb-0002-More-directories-to-search-for-place-for-init-script.patch +++ /dev/null @@ -1,39 +0,0 @@ -From: Peter Lemenkov -Date: Sun, 13 Feb 2011 14:21:20 +0300 -Subject: [PATCH] More directories to search for place for init-script - -Signed-off-by: Peter Lemenkov - -diff --git a/configure.ac b/configure.ac -index 103f029..0c36065 100644 ---- a/configure.ac -+++ b/configure.ac -@@ -528,17 +528,23 @@ launchd_enabled=false - - if test "$use_init" = "yes"; then - AC_MSG_CHECKING(location of init directory) -- if test -d /etc/rc.d; then -+ if test -d /etc/rc.d/init.d; then - init_enabled=true -- AC_SUBST([initdir], ['${sysconfdir}/rc.d']) -+ AC_SUBST([initdir], ['${sysconfdir}/rc.d/init.d']) - AC_MSG_RESULT(${initdir}) - else -- if test -d /etc/init.d; then -+ if test -d /etc/rc.d; then - init_enabled=true -- AC_SUBST([initdir], ['${sysconfdir}/init.d']) -+ AC_SUBST([initdir], ['${sysconfdir}/rc.d']) - AC_MSG_RESULT(${initdir}) - else -- AC_MSG_RESULT(not found) -+ if test -d /etc/init.d; then -+ init_enabled=true -+ AC_SUBST([initdir], ['${sysconfdir}/init.d']) -+ AC_MSG_RESULT(${initdir}) -+ else -+ AC_MSG_RESULT(not found) -+ fi - fi - fi - fi diff --git a/couchdb-0003-Install-into-erllibdir-by-default.patch b/couchdb-0003-Install-into-erllibdir-by-default.patch deleted file mode 100644 index 85a9731..0000000 --- a/couchdb-0003-Install-into-erllibdir-by-default.patch +++ /dev/null @@ -1,28 +0,0 @@ -From: Peter Lemenkov -Date: Sun, 13 Feb 2011 14:36:36 +0300 -Subject: [PATCH] Install into erllibdir by default - -Signed-off-by: Peter Lemenkov - -diff --git a/configure.ac b/configure.ac -index 0c36065..967e3b1 100644 ---- a/configure.ac -+++ b/configure.ac -@@ -698,7 +698,7 @@ AC_SUBST([bug_uri], ["LOCAL_BUG_URI"]) - AC_SUBST([localconfdir], [${sysconfdir}/${package_identifier}]) - AC_SUBST([localdatadir], [${datadir}/${package_identifier}]) - AC_SUBST([localdocdir], [${datadir}/doc/${package_identifier}]) --AC_SUBST([locallibdir], [${libdir}/${package_identifier}]) -+AC_SUBST([locallibdir], [${libdir}]) - AC_SUBST([localstatelibdir], [${localstatedir}/lib/${package_identifier}]) - AC_SUBST([localstatelogdir], [${localstatedir}/log/${package_identifier}]) - AC_SUBST([localstaterundir], [${localstatedir}/run/${package_identifier}]) -@@ -711,7 +711,7 @@ if test x${IS_WINDOWS} = xTRUE; then - AC_SUBST([locallibbindir], [${prefix}/bin]) - AC_SUBST([localerlanglibdir], [${libdir}]) - else -- AC_SUBST([locallibbindir], [${locallibdir}/bin]) -+ AC_SUBST([locallibbindir], [${locallibdir}/erlang/lib/couch-${version}/priv]) - AC_SUBST([localerlanglibdir], [${locallibdir}/erlang/lib]) - fi - diff --git a/couchdb-0004-Don-t-use-bundled-libraries.patch b/couchdb-0004-Don-t-use-bundled-libraries.patch deleted file mode 100644 index c1410ac..0000000 --- a/couchdb-0004-Don-t-use-bundled-libraries.patch +++ /dev/null @@ -1,89 +0,0 @@ -From: Peter Lemenkov -Date: Sun, 15 May 2011 18:47:41 +0400 -Subject: [PATCH] Don't use bundled libraries - -Signed-off-by: Peter Lemenkov - -diff --git a/configure.ac b/configure.ac -index 967e3b1..7a84f4f 100644 ---- a/configure.ac -+++ b/configure.ac -@@ -22,7 +22,6 @@ AC_CONFIG_AUX_DIR([build-aux]) - AC_CONFIG_MACRO_DIR([m4]) - - AC_CONFIG_HEADERS([config.h]) --AC_CONFIG_HEADERS([src/snappy/google-snappy/config.h]) - - AM_INIT_AUTOMAKE([1.6.3 foreign tar-ustar]) - -@@ -750,12 +749,7 @@ AC_CONFIG_FILES([src/couchjs-node/Makefile]) - AC_CONFIG_FILES([src/couchdb/couch.app.tpl]) - AC_CONFIG_FILES([src/couchdb/Makefile]) - AC_CONFIG_FILES([src/couchdb/priv/Makefile]) --AC_CONFIG_FILES([src/erlang-oauth/Makefile]) - AC_CONFIG_FILES([src/etap/Makefile]) --AC_CONFIG_FILES([src/ibrowse/Makefile]) --AC_CONFIG_FILES([src/mochiweb/Makefile]) --AC_CONFIG_FILES([src/snappy/Makefile]) --AC_CONFIG_FILES([src/snappy/google-snappy/snappy-stubs-public.h]) - AC_CONFIG_FILES([src/ejson/Makefile]) - AC_CONFIG_FILES([test/Makefile]) - AC_CONFIG_FILES([test/bench/Makefile]) -diff --git a/src/Makefile.am b/src/Makefile.am -index a17674c..dde8b52 100644 ---- a/src/Makefile.am -+++ b/src/Makefile.am -@@ -18,11 +18,7 @@ SUBDIRS = \ - couch_replicator \ - couchdb \ - ejson \ -- erlang-oauth \ - etap \ -- ibrowse \ -- mochiweb \ -- snappy \ - couchjs-node - - EXTRA_DIST = \ -diff --git a/src/ejson/Makefile.am b/src/ejson/Makefile.am -index 60dacc2..2d4e539 100644 ---- a/src/ejson/Makefile.am -+++ b/src/ejson/Makefile.am -@@ -46,15 +46,11 @@ EJSON_C_HDRS = \ - - ejson_file_collection = \ - ejson.app.in \ -- ejson.erl \ -- mochijson2.erl \ -- mochinum.erl -+ ejson.erl - - ejsonebin_make_generated_file_list = \ - ejson.app \ -- ejson.beam \ -- mochijson2.beam \ -- mochinum.beam -+ ejson.beam - - EXTRA_DIST = \ - $(EJSON_C_HDRS) \ -diff --git a/test/etap/test_util.erl.in b/test/etap/test_util.erl.in -index 352714e..c0292a7 100644 ---- a/test/etap/test_util.erl.in -+++ b/test/etap/test_util.erl.in -@@ -25,13 +25,9 @@ builddir() -> - - init_code_path() -> - Paths = [ -- "etap", -+ "etap", - "couchdb", -- "ejson", -- "erlang-oauth", -- "ibrowse", -- "mochiweb", -- "snappy" -+ "ejson" - ], - lists:foreach(fun(Name) -> - code:add_patha(filename:join([builddir(), "src", Name])) diff --git a/couchdb-0005-Fixes-for-system-wide-ibrowse.patch b/couchdb-0005-Fixes-for-system-wide-ibrowse.patch deleted file mode 100644 index 1dda0a3..0000000 --- a/couchdb-0005-Fixes-for-system-wide-ibrowse.patch +++ /dev/null @@ -1,45 +0,0 @@ -From: Peter Lemenkov -Date: Sun, 13 Feb 2011 14:52:57 +0300 -Subject: [PATCH] Fixes for system-wide ibrowse - -Signed-off-by: Peter Lemenkov - -diff --git a/src/couch_replicator/src/couch_replicator_httpc.erl b/src/couch_replicator/src/couch_replicator_httpc.erl -index b8fb31b..44f308b 100644 ---- a/src/couch_replicator/src/couch_replicator_httpc.erl -+++ b/src/couch_replicator/src/couch_replicator_httpc.erl -@@ -14,7 +14,7 @@ - - -include("couch_db.hrl"). - -include("couch_replicator_api_wrap.hrl"). ---include("../ibrowse/ibrowse.hrl"). -+-include_lib("ibrowse/include/ibrowse.hrl"). - - -export([setup/1]). - -export([send_req/3]). -diff --git a/src/couch_replicator/src/couch_replicator_utils.erl b/src/couch_replicator/src/couch_replicator_utils.erl -index 99ddebf..1948b7d 100644 ---- a/src/couch_replicator/src/couch_replicator_utils.erl -+++ b/src/couch_replicator/src/couch_replicator_utils.erl -@@ -21,7 +21,7 @@ - -include("couch_db.hrl"). - -include("couch_replicator_api_wrap.hrl"). - -include("couch_replicator.hrl"). ---include("../ibrowse/ibrowse.hrl"). -+-include_lib("ibrowse/include/ibrowse.hrl"). - - -import(couch_util, [ - get_value/2, -diff --git a/src/couchdb/couch_httpd_proxy.erl b/src/couchdb/couch_httpd_proxy.erl -index dec3f55..eb037b4 100644 ---- a/src/couchdb/couch_httpd_proxy.erl -+++ b/src/couchdb/couch_httpd_proxy.erl -@@ -14,7 +14,7 @@ - -export([handle_proxy_req/2]). - - -include("couch_db.hrl"). ---include("../ibrowse/ibrowse.hrl"). -+-include_lib("ibrowse/include/ibrowse.hrl"). - - -define(TIMEOUT, infinity). - -define(PKT_SIZE, 4096). diff --git a/couchdb-0006-Remove-pid-file-after-stop.patch b/couchdb-0006-Remove-pid-file-after-stop.patch deleted file mode 100644 index 7452376..0000000 --- a/couchdb-0006-Remove-pid-file-after-stop.patch +++ /dev/null @@ -1,17 +0,0 @@ -From: Peter Lemenkov -Date: Mon, 7 Jun 2010 15:08:42 +0400 -Subject: [PATCH] Remove pid-file after stop - - -diff --git a/bin/couchdb.tpl.in b/bin/couchdb.tpl.in -index f2027f1..ba034cc 100644 ---- a/bin/couchdb.tpl.in -+++ b/bin/couchdb.tpl.in -@@ -305,6 +305,7 @@ EOF - - stop_couchdb () { - PID=`_get_pid` -+ rm -f $PID_FILE - STOP_TIMEOUT=60 - if test -n "$PID"; then - if test "$1" = "false"; then diff --git a/couchdb-0007-Change-respawn-timeout-to-0.patch b/couchdb-0007-Change-respawn-timeout-to-0.patch deleted file mode 100644 index d499c12..0000000 --- a/couchdb-0007-Change-respawn-timeout-to-0.patch +++ /dev/null @@ -1,17 +0,0 @@ -From: Peter Lemenkov -Date: Thu, 26 Aug 2010 13:22:56 +0400 -Subject: [PATCH] Change respawn timeout to 0. - -Signed-off-by: Peter Lemenkov - -diff --git a/etc/default/couchdb b/etc/default/couchdb -index c2a3f2a..f589c0a 100644 ---- a/etc/default/couchdb -+++ b/etc/default/couchdb -@@ -3,5 +3,5 @@ - COUCHDB_USER=couchdb - COUCHDB_STDOUT_FILE=/dev/null - COUCHDB_STDERR_FILE=/dev/null --COUCHDB_RESPAWN_TIMEOUT=5 -+COUCHDB_RESPAWN_TIMEOUT=0 - COUCHDB_OPTIONS= diff --git a/couchdb-0008-Fix-for-Erlang-R16B01.patch b/couchdb-0008-Fix-for-Erlang-R16B01.patch deleted file mode 100644 index b60c859..0000000 --- a/couchdb-0008-Fix-for-Erlang-R16B01.patch +++ /dev/null @@ -1,19 +0,0 @@ -From: Peter Lemenkov -Date: Fri, 21 Jun 2013 11:56:54 +0400 -Subject: [PATCH] Fix for Erlang R16B01 - -Signed-off-by: Peter Lemenkov - -diff --git a/src/couchdb/couch_app.erl b/src/couchdb/couch_app.erl -index 9644877..42411a8 100644 ---- a/src/couchdb/couch_app.erl -+++ b/src/couchdb/couch_app.erl -@@ -20,7 +20,7 @@ - - start(_Type, DefaultIniFiles) -> - IniFiles = get_ini_files(DefaultIniFiles), -- case start_apps([crypto, asn1, public_key, sasl, inets, oauth, ssl, ibrowse, syntax_tools, compiler, xmerl, mochiweb, os_mon]) of -+ case start_apps([crypto, asn1, public_key, sasl, inets, oauth, ssl, ibrowse, xmerl, compiler, syntax_tools, mochiweb, os_mon]) of - ok -> - couch_server_sup:start_link(IniFiles); - {error, Reason} -> diff --git a/couchdb-0009-README-was-renamed.patch b/couchdb-0009-README-was-renamed.patch deleted file mode 100644 index 8f55d64..0000000 --- a/couchdb-0009-README-was-renamed.patch +++ /dev/null @@ -1,19 +0,0 @@ -From: Peter Lemenkov -Date: Thu, 15 Aug 2013 19:06:31 +0400 -Subject: [PATCH] README was renamed - -Signed-off-by: Peter Lemenkov - -diff --git a/Makefile.am b/Makefile.am -index 661ba58..1eca5ab 100644 ---- a/Makefile.am -+++ b/Makefile.am -@@ -23,7 +23,7 @@ localdoc_DATA = \ - INSTALL.Windows \ - LICENSE \ - NOTICE \ -- README \ -+ README.rst \ - THANKS - - DISTCLEANFILES = $(localdoc_DATA) diff --git a/couchdb-0010-Use-_DEFAULT_SOURCE-instead-of-obsolete-_BSD_SOURCE.patch b/couchdb-0010-Use-_DEFAULT_SOURCE-instead-of-obsolete-_BSD_SOURCE.patch deleted file mode 100644 index 4f3ff28..0000000 --- a/couchdb-0010-Use-_DEFAULT_SOURCE-instead-of-obsolete-_BSD_SOURCE.patch +++ /dev/null @@ -1,41 +0,0 @@ -From: Peter Lemenkov -Date: Sun, 22 Jun 2014 23:30:32 +0400 -Subject: [PATCH] Use _DEFAULT_SOURCE instead of obsolete _BSD_SOURCE - -Signed-off-by: Peter Lemenkov - -diff --git a/src/couchdb/priv/Makefile.am b/src/couchdb/priv/Makefile.am -index a7973ba..5c8620b 100644 ---- a/src/couchdb/priv/Makefile.am -+++ b/src/couchdb/priv/Makefile.am -@@ -44,7 +44,7 @@ couchprivlib_LTLIBRARIES = couch_icu_driver.la - if USE_EJSON_COMPARE_NIF - couchprivlib_LTLIBRARIES += couch_ejson_compare.la - couch_ejson_compare_la_SOURCES = couch_ejson_compare/couch_ejson_compare.c --couch_ejson_compare_la_CPPFLAGS = -D_BSD_SOURCE $(ICU_CPPFLAGS) $(ERLANG_FLAGS) -+couch_ejson_compare_la_CPPFLAGS = -D_DEFAULT_SOURCE $(ICU_CPPFLAGS) $(ERLANG_FLAGS) - couch_ejson_compare_la_LDFLAGS = -module -avoid-version - couch_ejson_compare_la_LIBADD = $(ICU_LIBS) - if WINDOWS -@@ -72,7 +72,7 @@ COUCHJS_SRCS = \ - - locallibbin_PROGRAMS = couchjs - couchjs_SOURCES = $(COUCHJS_SRCS) --couchjs_CFLAGS = -g -Wall -Werror -D_BSD_SOURCE $(CURL_CFLAGS) $(JS_CFLAGS) -+couchjs_CFLAGS = -g -Wall -Werror -D_DEFAULT_SOURCE $(CURL_CFLAGS) $(JS_CFLAGS) - couchjs_LDADD = $(CURL_LIBS) $(JS_LIBS) - - couchpriv_DATA = stat_descriptions.cfg -diff --git a/test/etap/Makefile.am b/test/etap/Makefile.am -index c9778ca..0b3bbe9 100644 ---- a/test/etap/Makefile.am -+++ b/test/etap/Makefile.am -@@ -15,7 +15,7 @@ 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 -+test_cfg_register_CFLAGS = -D_DEFAULT_SOURCE - - %.beam: %.erl - $(ERLC) $< diff --git a/couchdb-0011-Silence-redundant-logging-to-stdout-stderr.patch b/couchdb-0011-Silence-redundant-logging-to-stdout-stderr.patch deleted file mode 100644 index ab6c802..0000000 --- a/couchdb-0011-Silence-redundant-logging-to-stdout-stderr.patch +++ /dev/null @@ -1,44 +0,0 @@ -From: Warren Togami -Date: Wed, 2 Jul 2014 22:54:38 -1000 -Subject: [PATCH] Silence redundant logging to stdout/stderr - -Instead print log filename to stdout during startup. - -CouchDB already logs everything to /var/log/couchdb/couch.log. -The stdout/stderr redundantly floods /var/log/messages. -This temporary hack was suggested by rnewson in #couchdb. -https://issues.apache.org/jira/browse/COUCHDB-2264 -Related issue - -diff --git a/src/couchdb/couch_log.erl b/src/couchdb/couch_log.erl -index cd4bbbb..db483a7 100644 ---- a/src/couchdb/couch_log.erl -+++ b/src/couchdb/couch_log.erl -@@ -204,7 +204,7 @@ terminate(_Arg, #state{fd = Fd}) -> - file:close(Fd). - - log(#state{fd = Fd}, ConsoleMsg, FileMsg) -> -- ok = io:put_chars(ConsoleMsg), -+ %ok = io:put_chars(ConsoleMsg), - ok = io:put_chars(Fd, FileMsg). - - get_log_messages(Pid, Level, Format, Args) -> -diff --git a/src/couchdb/couch_server_sup.erl b/src/couchdb/couch_server_sup.erl -index be3c3a3..39a5568 100644 ---- a/src/couchdb/couch_server_sup.erl -+++ b/src/couchdb/couch_server_sup.erl -@@ -56,10 +56,12 @@ start_server(IniFiles) -> - {ok, ConfigPid} = couch_config:start_link(IniFiles), - - LogLevel = couch_config:get("log", "level", "info"), -+ LogFileName = couch_config:get("log", "file"), - % announce startup -- io:format("Apache CouchDB ~s (LogLevel=~s) is starting.~n", [ -+ io:format("Apache CouchDB ~s (LogLevel=~s) is logging to ~s.~n", [ - couch_server:get_version(), -- LogLevel -+ LogLevel, -+ LogFileName - ]), - case LogLevel of - "debug" -> diff --git a/couchdb-0012-Expand-.d-directories-in-erlang.patch b/couchdb-0012-Expand-.d-directories-in-erlang.patch deleted file mode 100644 index 0b8b4d3..0000000 --- a/couchdb-0012-Expand-.d-directories-in-erlang.patch +++ /dev/null @@ -1,104 +0,0 @@ -From: Robert Newson -Date: Sun, 6 Jul 2014 23:47:23 +0100 -Subject: [PATCH] Expand .d directories in erlang - - -diff --git a/bin/couchdb.tpl.in b/bin/couchdb.tpl.in -index ba034cc..ffdbb17 100644 ---- a/bin/couchdb.tpl.in -+++ b/bin/couchdb.tpl.in -@@ -120,7 +120,7 @@ _get_pid () { - echo $PID - } - --_add_config_file () { -+_add_config_path () { - if test -z "$print_arguments"; then - print_arguments="$1" - else -@@ -134,14 +134,6 @@ EOF - background_start_arguments="$background_start_arguments -a $1" - } - --_add_config_dir () { -- for file in "$1"/*.ini; do -- if [ -r "$file" ]; then -- _add_config_file "$file" -- fi -- done --} -- - _add_erlang_config () { - if [ -r "$1" ]; then - ERL_START_OPTIONS="$ERL_START_OPTIONS -config '$1'" -@@ -149,15 +141,15 @@ _add_erlang_config () { - } - - _load_config () { -- _add_config_file "$DEFAULT_CONFIG_FILE" -- _add_config_dir "$DEFAULT_CONFIG_DIR" -+ _add_config_path "$DEFAULT_CONFIG_FILE" -+ _add_config_path "$DEFAULT_CONFIG_DIR" - # We initialize plugins here to get the desired default config load order - _find_plugins -- _add_config_file "$LOCAL_CONFIG_FILE" -- _add_config_dir "$LOCAL_CONFIG_DIR" -+ _add_config_path "$LOCAL_CONFIG_DIR" -+ _add_config_path "$LOCAL_CONFIG_FILE" - if [ "$COUCHDB_ADDITIONAL_CONFIG_FILE" != '' ] - then -- _add_config_file "$COUCHDB_ADDITIONAL_CONFIG_FILE" -+ _add_config_path "$COUCHDB_ADDITIONAL_CONFIG_FILE" - fi - } - -@@ -238,7 +230,7 @@ _find_plugins () { - else - ERL_ZFLAGS="$ERL_ZFLAGS -pz '$plugin/ebin'" - fi -- _add_config_dir "$plugin/priv/default.d" -+ _add_config_path "$plugin/priv/default.d" - _add_erlang_config "$plugin/priv/couch_plugin.config" - fi - done -@@ -358,8 +350,8 @@ parse_script_option_list () { - case "$1" in - -h) shift; display_help; exit;; - -V) shift; display_version; exit;; -- -a) shift; _add_config_file "$1"; shift;; -- -A) shift; _add_config_dir "$1"; shift;; -+ -a) shift; _add_config_path "$1"; shift;; -+ -A) shift; _add_config_path "$1"; shift;; - -n) shift; _reset_config;; - -c) shift; _print_config; exit;; - -i) shift; INTERACTIVE=true;; -diff --git a/src/couchdb/couch_app.erl b/src/couchdb/couch_app.erl -index 42411a8..d6d8c0c 100644 ---- a/src/couchdb/couch_app.erl -+++ b/src/couchdb/couch_app.erl -@@ -15,6 +15,7 @@ - -behaviour(application). - - -include("couch_db.hrl"). -+-include_lib("kernel/include/file.hrl"). - - -export([start/2, stop/1]). - -@@ -37,7 +38,16 @@ get_ini_files(Default) -> - {ok, [[]]} -> - Default; - {ok, [Values]} -> -- Values -+ lists:flatmap(fun(V) -> -+ case file:read_file_info(V) of -+ {ok, #file_info{type = regular}} -> -+ [V]; -+ {ok, #file_info{type = directory}} -> -+ lists:sort(filelib:wildcard(filename:join([V, "*.ini"]))); -+ {error, enoent} -> -+ [] -+ end -+ end, Values) - end. - - start_apps([]) -> diff --git a/couchdb-0013-Add-systemd-notification-support.patch b/couchdb-0013-Add-systemd-notification-support.patch deleted file mode 100644 index 26802e6..0000000 --- a/couchdb-0013-Add-systemd-notification-support.patch +++ /dev/null @@ -1,27 +0,0 @@ -From: Peter Lemenkov -Date: Mon, 7 Jul 2014 21:32:43 +0400 -Subject: [PATCH] Add systemd notification support - -Signed-off-by: Peter Lemenkov - -Load module first - -Function erlang:function_exported/3 looks only for modules already -loaded. So we have to load module first. We can do it either implicitly -by calling any function from the module (and catch for possible -exceptions if no such module available) or explicitly. - -Signed-off-by: Peter Lemenkov - -diff --git a/src/couchdb/couch_server_sup.erl b/src/couchdb/couch_server_sup.erl -index 39a5568..2d25220 100644 ---- a/src/couchdb/couch_server_sup.erl -+++ b/src/couchdb/couch_server_sup.erl -@@ -108,6 +108,7 @@ start_server(IniFiles) -> - - Ip = couch_config:get("httpd", "bind_address"), - io:format("Apache CouchDB has started. Time to relax.~n"), -+ {module, sd_notify} == code:load_file(sd_notify) andalso sd_notify:sd_notify(0, "READY=1"), - Uris = [get_uri(Name, Ip) || Name <- [couch_httpd, https]], - [begin - case Uri of diff --git a/couchdb-0014-Add-run-script-to-execute-eunit-tests.patch b/couchdb-0014-Add-run-script-to-execute-eunit-tests.patch deleted file mode 100644 index 8469be9..0000000 --- a/couchdb-0014-Add-run-script-to-execute-eunit-tests.patch +++ /dev/null @@ -1,15346 +0,0 @@ -From: Alexander Shorin -Date: Fri, 16 May 2014 00:08:36 +0400 -Subject: [PATCH] Add run script to execute eunit tests - -Usage is the same as for test/etap/run: - - ./test/couchdb/run -v ${PATH} - --v runs in verbose mode, as etap does. Also, you can use make for that: - - make check-eunit - -which will run tests everywhere in project where Makefile contains -check-eunit subcommand definition. - -The ${PATH} thing could be single file or directory. The latter should -contains *_tests.erl files which would be compiled and executed by -eunit. - -The *_tests.erl - -The reason of compiling on run instead of using autoconf for that is -to simplify tests developing and avoid situations, when you'd fixed -the test or add new one, but forgot to remove/compile beam file. - -All test_*.beam files are been stored in test/couchdb/ebin directory, -the temporary test files will be places to test/couchdb/temp one. -Both directories will be removed by make clean/distclean command. - -Add common header for eunit test files - -Port 001-load.t etap test suite to eunit - -Port 002-icu-driver.t etap test suite to eunit - -See setup/0 comment for specific info about loading -couch_icu_driver with eunit. - -Port 010-file-basics.t and 011-file-headers.t etap test suites to eunit - -Both merged into single suite since they tests single target and shares -common bits. - -Port 020-btree-basics.t and 021-btree-reductions.t etap suites to eunit - -Both merged into single suite since they tests single target and shares -common bits. - -Port 030-doc-from-json.t and 031-doc-to-json.t etap suites to eunit - -Both merged into single suite since they tests single target and shares -common bits. - -Port 040-util.t etap test suite to eunit - -Port 041-uuid.t etap test suite to eunit - -Config files are removed in favor of using couch_config API instead. - -Port 042-work-queue.t etap test suite to eunit - -Etap tests were made in flow style, testing the same things multiple -times without real need. For eunit they are split into small test cases -to focus on testing goals. - -Timeout on receive is decreased from 3000 to 100. - -Port 043-find-in-binary.t etap test suite to eunit - -It been merged into couch_util_tests suite. - -Port 050-stream.t etap test suite to eunit - -Port 06[0-5]-kt-*.t etap test suites to eunit - -All merged into single suite since they're test the same module. - -Port 070-couch-db.t etap test suite to eunit - -Fix ERL_LIB environment variable setting. Add ?tempdb macros for -unique temporary database name generation. - -Port 072-cleanup.t etap test suite to eunit - -requests functions from test_util were moved to test_request module -with nicer and simpler API. - -Port 073-changes.t etap test suite to eunit - -For heartbeats test they don't being counted anymore since their -amount is heavy depends from the overall system performance and some -assertions may fail or not because of that. Instead of this, we just -ensure that their amount is going to increase over the time. - -Port 074-doc-update-conflicts.t etap test suite to eunit - -Timeout decreased, added 10K clients case - -Port 075-auth-cache.t etap test suite to eunit - -Timeouts are removed. - -Port 076-file-compression.t etap test suite to eunit - -The original test suite was decoupled into compaction and comparison -cases. - -Port 077-couch-db-fast-db-delete-create.t etap test suite to eunit - -Merged into couch_db_tests suite. - -Port 080-config-get-set.t etap test suite to eunit - -Port 081-config-override.t etap test suite to eunit - -Merged into couch_config_tests suite. -Setup fixtures. - -Port 082-config-register.t etap test suite to eunit - -Merged into couch_config_tests suite. - -Port 083-config-no-files.t etap test suite to eunit - -Merged into couch_config_tests suite. - -Port 090-task-status.t etap test suite to eunit - -Split huge test case into multiple ones. Fix issue with get_task_prop -when Acc may be reset if searched task isn't last in the list. - -Port 100-ref-counter.t etap test suite to eunit - -Port 120-stats-collect.t etap test suite to eunit - -Port 121-stats-aggregates.t etap test suite to eunit - -Merged into couch_stats_tests suite. - -Port 130-attachments-md5.t etap test suite to eunit - -Add random document id generator macros. -Have to use handmade http client instead of ibrowse since it makes too -complicated sending chunked requests. - -Port 140-attachments-comp.t etap test suite to eunit - -- Merge into couchdb_attachments_tests suite; -- Add PUT requests to test_request util; -- Remove dependency from files outside fixtures directory; -- Group test cases to reduce amount of duplicate code; -- Fix hidden issue with gzip encoding: for encoding_length stub info - check using zlib:gzip on 2KiB+ files leads to mismatch by 2-4 bytes - and this difference grows with file size. Using gzip fun code from - couch_stream solves the issue. - -Port 150-invalid-view-seq.t etap test suite to eunit - -Merged into couchdb_views_tests suite. - -Port 160-vhosts.t etap test suite to eunit - -Split Rewrite and OAuth tests. - -Port 170-os-daemons.t etap test suite to eunit - -Port 171-os-daemons-config.t etap test suite to eunit - -Merged into couchdb_os_daemons_tests suite. - -Port 172-os-daemons-errors.t etap test suite to eunit - -Merged into couchdb_os_daemons_tests suite. -Removed errors redirection to /dev/null to explicitly signal that -permissions are set correctly. - -Port 173-os-daemons-cfg-register.t etap test suite to eunit - -Merged into couchdb_os_daemons_tests suite. - -Port 180-http-proxy.t etap test suite to eunit - -Port 190-json-stream-parse.t etap test suite to eunit - -Port 200-view-group-no-db-leaks.t etap test suite to eunit - -Merged into couchdb_views_tests suite. Apply minor refactor changes. - -Port 201-view-group-shutdown.t etap test suite to eunit - -Merged into couchdb_views_tests suite. Database population reduced -to speedup test and removed second view index call which leaded to -race condition when compaction becomes completed in time of view index -update call and before assertion check for {error, all_dbs_active}. - -Port 210-os-proc-pool.t etap test suite to eunit - -Port 220-compaction-daemon.t etap test suite to eunit - -Port 230-pbkfd2.t etap test suite to eunit - -Port 231-cors.t etap test suite to eunit - -Extend vhost and subresource testing for more generic code. - -Port 232-csp.t etap test suite to eunit - -Port 250-upgrade-legacy-view-files.t etap test suite to eunit - -Run couchdb tests with eunit - -Port couch_mrview/01-load.t etap test suite to eunit - -Port couch_mrview/02-map-views.t etap test suite to eunit - -Port couch_mrview/03-red-views.t etap test suite to eunit - -Port couch_mrview/04-index-info.t etap test suite to eunit - -Port couch_mrview/05-collation.t etap test suite to eunit - -Port couch_mrview/06-all-docs.t etap test suite to eunit - -Port couch_mrview/07-compact-swap.t etap test suite to eunit - -Run couch_mrview tests with eunit - -Port couch_replicator/01-load.t etap test suite to eunit - -Port couch_replicator/02-httpc-pool.t etap test suite to eunit - -Test test_worker_dead_pool_full removed as it's redundant. - -Port couch_replicator/03-replication-compact.t etap test suite to eunit - -Split big test fun into smaller steps, optimize timeouts. - -Port couch_replicator/04-replication-large_atts.t etap test to eunit - -Port couch_replicator/05-replication-many-leaves.t etap test to eunit - -Port couch_replicator/06-doc-missing-stubs.t etap test suite to eunit - -Port couch_replicator/07-use-checkpoints.t etap test to eunit - -Run couch_replicator tests with eunit - -Goodbye etap! - -Add uuid for test server - -Prevents it generation during tests running and failure for -make distcheck when config file is read only. - -Fix tests temp directory path - -Fix relative include path for couch_eunit.hrl - -Move couch_eunit.hrl.in to include directory - -Because include path "." has higher priority than any other custom -paths, generated during `configure` phase couch_eunit.hrl contains -build and source paths which aren't used for `make distcheck` causing -various failures. - -Ensures that .test_design directory becomes created - -Handle test_cfg_register daemon path in special way - -During make distcheck test_cfg_register binary become separated from -others fixtures and becomes available by ?BUILDDIR path, not ?SOURCEDIR -as others are. - -Simplify os_daemon_configer.escript - -There is no need to start couch_config, init code paths and include -anything, just need to make ejson beams available. - -More etap cleanup - -Fix Makefile rules for `make distcheck` - -Conflicts: - configure.ac - src/Makefile.am - test/etap/Makefile.am - test/etap/test_util.erl.in - -diff --git a/LICENSE b/LICENSE -index 4c58f19..0fbc123 100644 ---- a/LICENSE -+++ b/LICENSE -@@ -474,31 +474,6 @@ For the src/erlang-oauth component: - FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR - OTHER DEALINGS IN THE SOFTWARE. - --For the src/etap component: -- -- Copyright (c) 2008-2009 Nick Gerakines -- -- Permission is hereby granted, free of charge, to any person -- obtaining a copy of this software and associated documentation -- files (the "Software"), to deal in the Software without -- restriction, including without limitation the rights to use, -- copy, modify, merge, publish, distribute, sublicense, and/or sell -- copies of the Software, and to permit persons to whom the -- Software is furnished to do so, subject to the following -- conditions: -- -- The above copyright notice and this permission notice shall be -- included in all copies or substantial portions of the Software. -- -- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -- EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES -- OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -- NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT -- HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, -- WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -- FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR -- OTHER DEALINGS IN THE SOFTWARE. -- - For the src/ejson/yajl component - - Copyright 2010, Lloyd Hilaiel. -diff --git a/Makefile.am b/Makefile.am -index 1eca5ab..d6b2afe 100644 ---- a/Makefile.am -+++ b/Makefile.am -@@ -47,7 +47,7 @@ EXTRA_DIST = \ - - check: dev check-js - if TESTS -- $(top_builddir)/test/etap/run $(top_srcdir)/test/etap -+ $(top_builddir)/test/couchdb/run -v $(top_srcdir)/test/couchdb - endif - - check-js: dev -@@ -57,20 +57,9 @@ if USE_CURL - endif - endif - --check-etap: dev -+check-eunit: dev - if TESTS -- $(top_builddir)/test/etap/run $(top_srcdir)/test/etap --endif -- --cover: dev --if TESTS -- rm -f cover/*.coverdata -- COVER=1 COVER_BIN=./src/couchdb/ $(top_builddir)/test/etap/run -- SRC=./src/couchdb/ \ -- $(ERL) -noshell \ -- -pa src/etap \ -- -eval 'etap_report:create()' \ -- -s init stop > /dev/null 2>&1 -+ $(top_builddir)/test/couchdb/run -v $(top_srcdir)/test/couchdb - endif - - dev: all -@@ -107,7 +96,6 @@ local-clean: maintainer-clean - rm -f $(top_srcdir)/aclocal.m4 - rm -f $(top_srcdir)/config.h.in - rm -f $(top_srcdir)/configure -- rm -f $(top_srcdir)/test/etap/temp.* - rm -f $(top_srcdir)/*.tar.gz - rm -f $(top_srcdir)/*.tar.gz.* - find $(top_srcdir) -name Makefile.in -exec rm -f {} \; -diff --git a/NOTICE b/NOTICE -index 08e3b82..be5ed49 100644 ---- a/NOTICE -+++ b/NOTICE -@@ -42,10 +42,6 @@ This product also includes the following third-party components: - - Copyright 2012, the authors and contributors - -- * ETap (http://github.com/ngerakines/etap/) -- -- Copyright 2009, Nick Gerakines -- - * mimeparse.js (http://code.google.com/p/mimeparse/) - - Copyright 2009, Chris Anderson -diff --git a/configure.ac b/configure.ac -index 7a84f4f..8d1a64f 100644 ---- a/configure.ac -+++ b/configure.ac -@@ -749,12 +749,14 @@ AC_CONFIG_FILES([src/couchjs-node/Makefile]) - AC_CONFIG_FILES([src/couchdb/couch.app.tpl]) - AC_CONFIG_FILES([src/couchdb/Makefile]) - AC_CONFIG_FILES([src/couchdb/priv/Makefile]) --AC_CONFIG_FILES([src/etap/Makefile]) - AC_CONFIG_FILES([src/ejson/Makefile]) - AC_CONFIG_FILES([test/Makefile]) - AC_CONFIG_FILES([test/bench/Makefile]) --AC_CONFIG_FILES([test/etap/Makefile]) --AC_CONFIG_FILES([test/etap/test_util.erl]) -+AC_CONFIG_FILES([test/couchdb/run]) -+AC_CONFIG_FILES([test/couchdb/Makefile]) -+AC_CONFIG_FILES([test/couchdb/include/couch_eunit.hrl]) -+AC_CONFIG_FILES([test/couchdb/fixtures/Makefile]) -+AC_CONFIG_FILES([test/couchdb/fixtures/os_daemon_configer.escript]) - AC_CONFIG_FILES([test/javascript/Makefile]) - AC_CONFIG_FILES([test/view_server/Makefile]) - AC_CONFIG_FILES([utils/Makefile]) -diff --git a/license.skip b/license.skip -index 45558d1..12eaa5e 100644 ---- a/license.skip -+++ b/license.skip -@@ -166,14 +166,16 @@ - ^test/Makefile.in - ^test/bench/Makefile - ^test/bench/Makefile.in --^test/etap/.*.beam --^test/etap/.*.o --^test/etap/.deps/.* --^test/etap/test_cfg_register --^test/etap/Makefile --^test/etap/Makefile.in --^test/etap/temp..* --^test/etap/fixtures/* -+^test/couchdb/Makefile -+^test/couchdb/Makefile.in -+^test/couchdb/fixtures/logo.png -+^test/couchdb/fixtures/3b835456c235b1827e012e25666152f3.view -+^test/couchdb/fixtures/Makefile -+^test/couchdb/fixtures/Makefile.in -+^test/couchdb/fixtures/test.couch -+^test/couchdb/fixtures/.deps/test_cfg_register-test_cfg_register.Po -+^test/couchdb/fixtures/test_cfg_register -+^test/couchdb/fixtures/test_cfg_register.o - ^test/javascript/Makefile - ^test/javascript/Makefile.in - ^test/local.ini -diff --git a/src/Makefile.am b/src/Makefile.am -index dde8b52..66efc11 100644 ---- a/src/Makefile.am -+++ b/src/Makefile.am -@@ -18,7 +18,6 @@ SUBDIRS = \ - couch_replicator \ - couchdb \ - ejson \ -- etap \ - couchjs-node - - EXTRA_DIST = \ -diff --git a/src/couch_mrview/Makefile.am b/src/couch_mrview/Makefile.am -index 2b9ef86..b9abe28 100644 ---- a/src/couch_mrview/Makefile.am -+++ b/src/couch_mrview/Makefile.am -@@ -33,13 +33,13 @@ source_files = \ - src/couch_mrview_util.erl - - test_files = \ -- test/01-load.t \ -- test/02-map-views.t \ -- test/03-red-views.t \ -- test/04-index-info.t \ -- test/05-collation.t \ -- test/06-all-docs.t \ -- test/07-compact-swap.t -+ test/couch_mrview_all_docs_tests.erl \ -+ test/couch_mrview_collation_tests.erl \ -+ test/couch_mrview_compact_tests.erl \ -+ test/couch_mrview_index_info_tests.erl \ -+ test/couch_mrview_map_views_tests.erl \ -+ test/couch_mrview_modules_load_tests.erl \ -+ test/couch_mrview_red_views_tests.erl - - compiled_files = \ - ebin/couch_mrview.app \ -@@ -58,7 +58,7 @@ CLEANFILES = $(compiled_files) - - check: - if TESTS -- $(abs_top_builddir)/test/etap/run $(abs_top_srcdir)/src/couch_mrview/test -+ $(abs_top_builddir)/test/couchdb/run -v $(abs_top_srcdir)/src/couch_mrview/test - endif - - ebin/%.app: src/%.app.src -diff --git a/src/couch_mrview/test/01-load.t b/src/couch_mrview/test/01-load.t -deleted file mode 100644 -index a57c1a7..0000000 ---- a/src/couch_mrview/test/01-load.t -+++ /dev/null -@@ -1,34 +0,0 @@ --#!/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(), -- Modules = [ -- couch_mrview, -- couch_mrview_compactor, -- couch_mrview_http, -- couch_mrview_index, -- couch_mrview_updater, -- couch_mrview_util -- ], -- -- etap:plan(length(Modules)), -- lists:foreach( -- fun(Module) -> -- etap:loaded_ok(Module, lists:concat(["Loaded: ", Module])) -- end, Modules), -- etap:end_tests(). -diff --git a/src/couch_mrview/test/02-map-views.t b/src/couch_mrview/test/02-map-views.t -deleted file mode 100644 -index 7e1ca0c..0000000 ---- a/src/couch_mrview/test/02-map-views.t -+++ /dev/null -@@ -1,131 +0,0 @@ --#!/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, -- timer:sleep(300), -- ok. -- --test() -> -- couch_server_sup:start_link(test_util:config_files()), -- -- {ok, Db} = couch_mrview_test_util:init_db(<<"foo">>, map), -- -- test_basic(Db), -- test_range(Db), -- test_rev_range(Db), -- test_limit_and_skip(Db), -- test_include_docs(Db), -- test_empty_view(Db), -- -- ok. -- -- --test_basic(Db) -> -- Result = run_query(Db, []), -- Expect = {ok, [ -- {meta, [{total, 10}, {offset, 0}]}, -- {row, [{id, <<"1">>}, {key, 1}, {value, 1}]}, -- {row, [{id, <<"2">>}, {key, 2}, {value, 2}]}, -- {row, [{id, <<"3">>}, {key, 3}, {value, 3}]}, -- {row, [{id, <<"4">>}, {key, 4}, {value, 4}]}, -- {row, [{id, <<"5">>}, {key, 5}, {value, 5}]}, -- {row, [{id, <<"6">>}, {key, 6}, {value, 6}]}, -- {row, [{id, <<"7">>}, {key, 7}, {value, 7}]}, -- {row, [{id, <<"8">>}, {key, 8}, {value, 8}]}, -- {row, [{id, <<"9">>}, {key, 9}, {value, 9}]}, -- {row, [{id, <<"10">>}, {key, 10}, {value, 10}]} -- ]}, -- etap:is(Result, Expect, "Simple view query worked."). -- -- --test_range(Db) -> -- Result = run_query(Db, [{start_key, 3}, {end_key, 5}]), -- Expect = {ok, [ -- {meta, [{total, 10}, {offset, 2}]}, -- {row, [{id, <<"3">>}, {key, 3}, {value, 3}]}, -- {row, [{id, <<"4">>}, {key, 4}, {value, 4}]}, -- {row, [{id, <<"5">>}, {key, 5}, {value, 5}]} -- ]}, -- etap:is(Result, Expect, "Query with range works."). -- -- --test_rev_range(Db) -> -- Result = run_query(Db, [ -- {direction, rev}, -- {start_key, 5}, {end_key, 3}, -- {inclusive_end, true} -- ]), -- Expect = {ok, [ -- {meta, [{total, 10}, {offset, 5}]}, -- {row, [{id, <<"5">>}, {key, 5}, {value, 5}]}, -- {row, [{id, <<"4">>}, {key, 4}, {value, 4}]}, -- {row, [{id, <<"3">>}, {key, 3}, {value, 3}]} -- ]}, -- etap:is(Result, Expect, "Query with reversed range works."). -- -- --test_limit_and_skip(Db) -> -- Result = run_query(Db, [ -- {start_key, 2}, -- {limit, 3}, -- {skip, 3} -- ]), -- Expect = {ok, [ -- {meta, [{total, 10}, {offset, 4}]}, -- {row, [{id, <<"5">>}, {key, 5}, {value, 5}]}, -- {row, [{id, <<"6">>}, {key, 6}, {value, 6}]}, -- {row, [{id, <<"7">>}, {key, 7}, {value, 7}]} -- ]}, -- etap:is(Result, Expect, "Query with limit and skip works."). -- -- --test_include_docs(Db) -> -- Result = run_query(Db, [ -- {start_key, 8}, -- {end_key, 8}, -- {include_docs, true} -- ]), -- Doc = {[ -- {<<"_id">>,<<"8">>}, -- {<<"_rev">>, <<"1-55b9a29311341e07ec0a7ca13bc1b59f">>}, -- {<<"val">>,8} -- ]}, -- Expect = {ok, [ -- {meta, [{total, 10}, {offset, 7}]}, -- {row, [{id, <<"8">>}, {key, 8}, {value, 8}, {doc, Doc}]} -- ]}, -- etap:is(Result, Expect, "Query with include docs works."). -- -- --test_empty_view(Db) -> -- Result = couch_mrview:query_view(Db, <<"_design/bar">>, <<"bing">>), -- Expect = {ok, [ -- {meta, [{total, 0}, {offset, 0}]} -- ]}, -- etap:is(Result, Expect, "Empty views are correct."). -- -- --run_query(Db, Opts) -> -- couch_mrview:query_view(Db, <<"_design/bar">>, <<"baz">>, Opts). -diff --git a/src/couch_mrview/test/03-red-views.t b/src/couch_mrview/test/03-red-views.t -deleted file mode 100644 -index 6ad341b..0000000 ---- a/src/couch_mrview/test/03-red-views.t -+++ /dev/null -@@ -1,78 +0,0 @@ --#!/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:run(4, fun() -> test() end). -- --test() -> -- couch_server_sup:start_link(test_util:config_files()), -- -- {ok, Db} = couch_mrview_test_util:init_db(<<"foo">>, red), -- -- test_basic(Db), -- test_key_range(Db), -- test_group_level(Db), -- test_group_exact(Db), -- -- ok. -- -- --test_basic(Db) -> -- Result = run_query(Db, []), -- Expect = {ok, [ -- {meta, []}, -- {row, [{key, null}, {value, 55}]} -- ]}, -- etap:is(Result, Expect, "Simple reduce view works."). -- -- --test_key_range(Db) -> -- Result = run_query(Db, [{start_key, [0, 2]}, {end_key, [0, 4]}]), -- Expect = {ok, [ -- {meta, []}, -- {row, [{key, null}, {value, 6}]} -- ]}, -- etap:is(Result, Expect, "Reduce with key range works."). -- -- --test_group_level(Db) -> -- Result = run_query(Db, [{group_level, 1}]), -- Expect = {ok, [ -- {meta, []}, -- {row, [{key, [0]}, {value, 30}]}, -- {row, [{key, [1]}, {value, 25}]} -- ]}, -- etap:is(Result, Expect, "Group level works."). -- --test_group_exact(Db) -> -- Result = run_query(Db, [{group_level, exact}]), -- Expect = {ok, [ -- {meta, []}, -- {row, [{key, [0, 2]}, {value, 2}]}, -- {row, [{key, [0, 4]}, {value, 4}]}, -- {row, [{key, [0, 6]}, {value, 6}]}, -- {row, [{key, [0, 8]}, {value, 8}]}, -- {row, [{key, [0, 10]}, {value, 10}]}, -- {row, [{key, [1, 1]}, {value, 1}]}, -- {row, [{key, [1, 3]}, {value, 3}]}, -- {row, [{key, [1, 5]}, {value, 5}]}, -- {row, [{key, [1, 7]}, {value, 7}]}, -- {row, [{key, [1, 9]}, {value, 9}]} -- ]}, -- etap:is(Result, Expect, "Group exact works."). -- -- --run_query(Db, Opts) -> -- couch_mrview:query_view(Db, <<"_design/bar">>, <<"baz">>, Opts). -diff --git a/src/couch_mrview/test/04-index-info.t b/src/couch_mrview/test/04-index-info.t -deleted file mode 100644 -index 6b67b56..0000000 ---- a/src/couch_mrview/test/04-index-info.t -+++ /dev/null -@@ -1,54 +0,0 @@ --#!/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(9), -- case (catch test()) of -- ok -> -- etap:end_tests(); -- Other -> -- etap:diag(io_lib:format("Test died abnormally: ~p", [Other])), -- etap:bail(Other) -- end, -- timer:sleep(300), -- ok. -- --sig() -> <<"276df562b152b3c4e5d34024f62672ed">>. -- --test() -> -- couch_server_sup:start_link(test_util:config_files()), -- -- {ok, Db} = couch_mrview_test_util:init_db(<<"foo">>, map), -- couch_mrview:query_view(Db, <<"_design/bar">>, <<"baz">>), -- -- {ok, Info} = couch_mrview:get_info(Db, <<"_design/bar">>), -- -- etap:is(getval(signature, Info), sig(), "Signature is ok."), -- etap:is(getval(language, Info), <<"javascript">>, "Language is ok."), -- etap:is_greater(getval(disk_size, Info), 0, "Disk size is ok."), -- etap:is_greater(getval(data_size, Info), 0, "Data size is ok."), -- etap:is(getval(update_seq, Info), 11, "Update seq is ok."), -- etap:is(getval(purge_seq, Info), 0, "Purge seq is ok."), -- etap:is(getval(updater_running, Info), false, "No updater running."), -- etap:is(getval(compact_running, Info), false, "No compaction running."), -- etap:is(getval(waiting_clients, Info), 0, "No waiting clients."), -- -- ok. -- --getval(Key, PL) -> -- {value, {Key, Val}} = lists:keysearch(Key, 1, PL), -- Val. -diff --git a/src/couch_mrview/test/05-collation.t b/src/couch_mrview/test/05-collation.t -deleted file mode 100644 -index ac8f8bc..0000000 ---- a/src/couch_mrview/test/05-collation.t -+++ /dev/null -@@ -1,163 +0,0 @@ --#!/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:run(9, fun() -> test() end). -- -- --test() -> -- couch_server_sup:start_link(test_util:config_files()), -- {ok, Db0} = couch_mrview_test_util:new_db(<<"foo">>, map), -- {ok, Db1} = couch_mrview_test_util:save_docs(Db0, docs()), -- -- test_collated_fwd(Db1), -- test_collated_rev(Db1), -- test_range_collation(Db1), -- test_inclusive_end(Db1), -- test_uninclusive_end(Db1), -- test_with_endkey_docid(Db1), -- -- ok. -- --test_collated_fwd(Db) -> -- {ok, Results} = run_query(Db, []), -- Expect = [{meta, [{total, 26}, {offset, 0}]}] ++ rows(), -- etap:is(Results, Expect, "Values were collated correctly."). -- -- --test_collated_rev(Db) -> -- {ok, Results} = run_query(Db, [{direction, rev}]), -- Expect = [{meta, [{total, 26}, {offset, 0}]}] ++ lists:reverse(rows()), -- etap:is(Results, Expect, "Values were collated correctly descending."). -- -- --test_range_collation(Db) -> -- {_, Error} = lists:foldl(fun(V, {Count, Error}) -> -- {ok, Results} = run_query(Db, [{start_key, V}, {end_key, V}]), -- Id = list_to_binary(integer_to_list(Count)), -- Expect = [ -- {meta, [{total, 26}, {offset, Count}]}, -- {row, [{id, Id}, {key, V}, {value, 0}]} -- ], -- case Results == Expect of -- true -> {Count+1, Error}; -- _ -> {Count+1, true} -- end -- end, {0, false}, vals()), -- etap:is(Error, false, "Found each individual key correctly."). -- -- --test_inclusive_end(Db) -> -- Opts = [{end_key, <<"b">>}, {inclusive_end, true}], -- {ok, Rows0} = run_query(Db, Opts), -- LastRow0 = lists:last(Rows0), -- Expect0 = {row, [{id,<<"10">>}, {key,<<"b">>}, {value,0}]}, -- etap:is(LastRow0, Expect0, "Inclusive end is correct."), -- -- {ok, Rows1} = run_query(Db, Opts ++ [{direction, rev}]), -- LastRow1 = lists:last(Rows1), -- Expect1 = {row, [{id,<<"10">>}, {key,<<"b">>}, {value,0}]}, -- etap:is(LastRow1, Expect1, -- "Inclusive end is correct with descending=true"). -- --test_uninclusive_end(Db) -> -- Opts = [{end_key, <<"b">>}, {inclusive_end, false}], -- {ok, Rows0} = run_query(Db, Opts), -- LastRow0 = lists:last(Rows0), -- Expect0 = {row, [{id,<<"9">>}, {key,<<"aa">>}, {value,0}]}, -- etap:is(LastRow0, Expect0, "Uninclusive end is correct."), -- -- {ok, Rows1} = run_query(Db, Opts ++ [{direction, rev}]), -- LastRow1 = lists:last(Rows1), -- Expect1 = {row, [{id,<<"11">>}, {key,<<"B">>}, {value,0}]}, -- etap:is(LastRow1, Expect1, -- "Uninclusive end is correct with descending=true"). -- -- --test_with_endkey_docid(Db) -> -- {ok, Rows0} = run_query(Db, [ -- {end_key, <<"b">>}, {end_key_docid, <<"10">>}, -- {inclusive_end, false} -- ]), -- Result0 = lists:last(Rows0), -- Expect0 = {row, [{id,<<"9">>}, {key,<<"aa">>}, {value,0}]}, -- etap:is(Result0, Expect0, "Uninclsuive end with endkey_docid set is ok."), -- -- {ok, Rows1} = run_query(Db, [ -- {end_key, <<"b">>}, {end_key_docid, <<"11">>}, -- {inclusive_end, false} -- ]), -- Result1 = lists:last(Rows1), -- Expect1 = {row, [{id,<<"10">>}, {key,<<"b">>}, {value,0}]}, -- etap:is(Result1, Expect1, "Uninclsuive end with endkey_docid set is ok."). -- -- --run_query(Db, Opts) -> -- couch_mrview:query_view(Db, <<"_design/bar">>, <<"zing">>, Opts). -- -- --docs() -> -- {Docs, _} = lists:foldl(fun(V, {Docs0, Count}) -> -- Doc = couch_doc:from_json_obj({[ -- {<<"_id">>, list_to_binary(integer_to_list(Count))}, -- {<<"foo">>, V} -- ]}), -- {[Doc | Docs0], Count+1} -- end, {[], 0}, vals()), -- Docs. -- -- --rows() -> -- {Rows, _} = lists:foldl(fun(V, {Rows0, Count}) -> -- Id = list_to_binary(integer_to_list(Count)), -- Row = {row, [{id, Id}, {key, V}, {value, 0}]}, -- {[Row | Rows0], Count+1} -- end, {[], 0}, vals()), -- lists:reverse(Rows). -- -- --vals() -> -- [ -- null, -- false, -- true, -- -- 1, -- 2, -- 3.0, -- 4, -- -- <<"a">>, -- <<"A">>, -- <<"aa">>, -- <<"b">>, -- <<"B">>, -- <<"ba">>, -- <<"bb">>, -- -- [<<"a">>], -- [<<"b">>], -- [<<"b">>, <<"c">>], -- [<<"b">>, <<"c">>, <<"a">>], -- [<<"b">>, <<"d">>], -- [<<"b">>, <<"d">>, <<"e">>], -- -- {[{<<"a">>, 1}]}, -- {[{<<"a">>, 2}]}, -- {[{<<"b">>, 1}]}, -- {[{<<"b">>, 2}]}, -- {[{<<"b">>, 2}, {<<"a">>, 1}]}, -- {[{<<"b">>, 2}, {<<"c">>, 2}]} -- ]. -diff --git a/src/couch_mrview/test/06-all-docs.t b/src/couch_mrview/test/06-all-docs.t -deleted file mode 100644 -index 4501aa5..0000000 ---- a/src/couch_mrview/test/06-all-docs.t -+++ /dev/null -@@ -1,127 +0,0 @@ --#!/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:run(6, fun() -> test() end). -- -- --test() -> -- couch_server_sup:start_link(test_util:config_files()), -- -- {ok, Db} = couch_mrview_test_util:init_db(<<"foo">>, map), -- -- test_basic(Db), -- test_range(Db), -- test_rev_range(Db), -- test_limit_and_skip(Db), -- test_include_docs(Db), -- test_empty_view(Db), -- -- ok. -- -- --test_basic(Db) -> -- Result = run_query(Db, []), -- Expect = {ok, [ -- {meta, [{total, 11}, {offset, 0}]}, -- mk_row(<<"1">>, <<"1-08d53a5760b95fce6df2e2c5b008be39">>), -- mk_row(<<"10">>, <<"1-a05b6ea2bc0243949f103d5b4f15f71e">>), -- mk_row(<<"2">>, <<"1-b57c77a9e6f7574ca6469f0d6dcd78bb">>), -- mk_row(<<"3">>, <<"1-7fbf84d56f8017880974402d60f5acd6">>), -- mk_row(<<"4">>, <<"1-fcaf5852c08ffb239ac8ce16c409f253">>), -- mk_row(<<"5">>, <<"1-aaac5d460fd40f9286e57b9bf12e23d2">>), -- mk_row(<<"6">>, <<"1-aca21c2e7bc5f8951424fcfc5d1209d8">>), -- mk_row(<<"7">>, <<"1-4374aeec17590d82f16e70f318116ad9">>), -- mk_row(<<"8">>, <<"1-55b9a29311341e07ec0a7ca13bc1b59f">>), -- mk_row(<<"9">>, <<"1-558c8487d9aee25399a91b5d31d90fe2">>), -- mk_row(<<"_design/bar">>, <<"1-a44e1dd1994a7717bf89c894ebd1f081">>) -- ]}, -- etap:is(Result, Expect, "Simple view query worked."). -- -- --test_range(Db) -> -- Result = run_query(Db, [{start_key, <<"3">>}, {end_key, <<"5">>}]), -- Expect = {ok, [ -- {meta, [{total, 11}, {offset, 3}]}, -- mk_row(<<"3">>, <<"1-7fbf84d56f8017880974402d60f5acd6">>), -- mk_row(<<"4">>, <<"1-fcaf5852c08ffb239ac8ce16c409f253">>), -- mk_row(<<"5">>, <<"1-aaac5d460fd40f9286e57b9bf12e23d2">>) -- ]}, -- etap:is(Result, Expect, "Query with range works."). -- -- --test_rev_range(Db) -> -- Result = run_query(Db, [ -- {direction, rev}, -- {start_key, <<"5">>}, {end_key, <<"3">>}, -- {inclusive_end, true} -- ]), -- Expect = {ok, [ -- {meta, [{total, 11}, {offset, 5}]}, -- mk_row(<<"5">>, <<"1-aaac5d460fd40f9286e57b9bf12e23d2">>), -- mk_row(<<"4">>, <<"1-fcaf5852c08ffb239ac8ce16c409f253">>), -- mk_row(<<"3">>, <<"1-7fbf84d56f8017880974402d60f5acd6">>) -- ]}, -- etap:is(Result, Expect, "Query with reversed range works."). -- -- --test_limit_and_skip(Db) -> -- Result = run_query(Db, [ -- {start_key, <<"2">>}, -- {limit, 3}, -- {skip, 3} -- ]), -- Expect = {ok, [ -- {meta, [{total, 11}, {offset, 5}]}, -- mk_row(<<"5">>, <<"1-aaac5d460fd40f9286e57b9bf12e23d2">>), -- mk_row(<<"6">>, <<"1-aca21c2e7bc5f8951424fcfc5d1209d8">>), -- mk_row(<<"7">>, <<"1-4374aeec17590d82f16e70f318116ad9">>) -- ]}, -- etap:is(Result, Expect, "Query with limit and skip works."). -- -- --test_include_docs(Db) -> -- Result = run_query(Db, [ -- {start_key, <<"8">>}, -- {end_key, <<"8">>}, -- {include_docs, true} -- ]), -- Doc = {[ -- {<<"_id">>,<<"8">>}, -- {<<"_rev">>, <<"1-55b9a29311341e07ec0a7ca13bc1b59f">>}, -- {<<"val">>, 8} -- ]}, -- Val = {[{rev, <<"1-55b9a29311341e07ec0a7ca13bc1b59f">>}]}, -- Expect = {ok, [ -- {meta, [{total, 11}, {offset, 8}]}, -- {row, [{id, <<"8">>}, {key, <<"8">>}, {value, Val}, {doc, Doc}]} -- ]}, -- etap:is(Result, Expect, "Query with include docs works."). -- -- --test_empty_view(Db) -> -- Result = couch_mrview:query_view(Db, <<"_design/bar">>, <<"bing">>), -- Expect = {ok, [ -- {meta, [{total, 0}, {offset, 0}]} -- ]}, -- etap:is(Result, Expect, "Empty views are correct."). -- -- --mk_row(Id, Rev) -> -- {row, [{id, Id}, {key, Id}, {value, {[{rev, Rev}]}}]}. -- -- --run_query(Db, Opts) -> -- couch_mrview:query_all_docs(Db, Opts). -diff --git a/src/couch_mrview/test/07-compact-swap.t b/src/couch_mrview/test/07-compact-swap.t -deleted file mode 100644 -index 4bfe124..0000000 ---- a/src/couch_mrview/test/07-compact-swap.t -+++ /dev/null -@@ -1,57 +0,0 @@ --#!/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:run(1, fun() -> test() end). -- -- --test() -> -- couch_server_sup:start_link(test_util:config_files()), -- {ok, Db} = couch_mrview_test_util:init_db(<<"foo">>, map, 1000), -- couch_mrview:query_view(Db, <<"_design/bar">>, <<"baz">>), -- test_swap(Db), -- ok. -- -- --test_swap(Db) -> -- {ok, QPid} = start_query(Db), -- {ok, MonRef} = couch_mrview:compact(Db, <<"_design/bar">>, [monitor]), -- receive -- {'DOWN', MonRef, process, _, _} -> ok -- after 1000 -> -- throw(compaction_failed) -- end, -- QPid ! {self(), continue}, -- receive -- {QPid, Count} -> -- etap:is(Count, 1000, "View finished successfully.") -- after 1000 -> -- throw("query failed") -- end. -- -- --start_query(Db) -> -- Self = self(), -- Pid = spawn(fun() -> -- CB = fun -- (_, wait) -> receive {Self, continue} -> {ok, 0} end; -- ({row, _}, Count) -> {ok, Count+1}; -- (_, Count) -> {ok, Count} -- end, -- {ok, Result} = -- couch_mrview:query_view(Db, <<"_design/bar">>, <<"baz">>, [], CB, wait), -- Self ! {self(), Result} -- end), -- {ok, Pid}. -diff --git a/src/couch_mrview/test/couch_mrview_all_docs_tests.erl b/src/couch_mrview/test/couch_mrview_all_docs_tests.erl -new file mode 100644 -index 0000000..4e098ff ---- /dev/null -+++ b/src/couch_mrview/test/couch_mrview_all_docs_tests.erl -@@ -0,0 +1,154 @@ -+% Licensed under the Apache License, Version 2.0 (the "License"); you may not -+% use this file except in compliance with the License. You may obtain a copy of -+% the License at -+% -+% http://www.apache.org/licenses/LICENSE-2.0 -+% -+% Unless required by applicable law or agreed to in writing, software -+% distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -+% WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -+% License for the specific language governing permissions and limitations under -+% the License. -+ -+-module(couch_mrview_all_docs_tests). -+ -+-include("couch_eunit.hrl"). -+-include_lib("couchdb/couch_db.hrl"). -+ -+-define(ADMIN_USER, {user_ctx, #user_ctx{roles=[<<"_admin">>]}}). -+-define(TIMEOUT, 1000). -+ -+ -+start() -> -+ {ok, Pid} = couch_server_sup:start_link(?CONFIG_CHAIN), -+ Pid. -+ -+stop(Pid) -> -+ erlang:monitor(process, Pid), -+ couch_server_sup:stop(), -+ receive -+ {'DOWN', _, _, Pid, _} -> -+ ok -+ after ?TIMEOUT -> -+ throw({timeout, server_stop}) -+ end. -+ -+setup() -> -+ {ok, Db} = couch_mrview_test_util:init_db(?tempdb(), map), -+ Db. -+ -+teardown(Db) -> -+ couch_db:close(Db), -+ couch_server:delete(Db#db.name, [?ADMIN_USER]), -+ ok. -+ -+ -+all_docs_test_() -> -+ { -+ "_all_docs view tests", -+ { -+ setup, -+ fun start/0, fun stop/1, -+ { -+ foreach, -+ fun setup/0, fun teardown/1, -+ [ -+ fun should_query/1, -+ fun should_query_with_range/1, -+ fun should_query_with_range_rev/1, -+ fun should_query_with_limit_and_skip/1, -+ fun should_query_with_include_docs/1, -+ fun should_query_empty_views/1 -+ ] -+ } -+ } -+ }. -+ -+ -+should_query(Db) -> -+ Result = run_query(Db, []), -+ Expect = {ok, [ -+ {meta, [{total, 11}, {offset, 0}]}, -+ mk_row(<<"1">>, <<"1-08d53a5760b95fce6df2e2c5b008be39">>), -+ mk_row(<<"10">>, <<"1-a05b6ea2bc0243949f103d5b4f15f71e">>), -+ mk_row(<<"2">>, <<"1-b57c77a9e6f7574ca6469f0d6dcd78bb">>), -+ mk_row(<<"3">>, <<"1-7fbf84d56f8017880974402d60f5acd6">>), -+ mk_row(<<"4">>, <<"1-fcaf5852c08ffb239ac8ce16c409f253">>), -+ mk_row(<<"5">>, <<"1-aaac5d460fd40f9286e57b9bf12e23d2">>), -+ mk_row(<<"6">>, <<"1-aca21c2e7bc5f8951424fcfc5d1209d8">>), -+ mk_row(<<"7">>, <<"1-4374aeec17590d82f16e70f318116ad9">>), -+ mk_row(<<"8">>, <<"1-55b9a29311341e07ec0a7ca13bc1b59f">>), -+ mk_row(<<"9">>, <<"1-558c8487d9aee25399a91b5d31d90fe2">>), -+ mk_row(<<"_design/bar">>, <<"1-a44e1dd1994a7717bf89c894ebd1f081">>) -+ ]}, -+ ?_assertEqual(Expect, Result). -+ -+should_query_with_range(Db) -> -+ Result = run_query(Db, [{start_key, <<"3">>}, {end_key, <<"5">>}]), -+ Expect = {ok, [ -+ {meta, [{total, 11}, {offset, 3}]}, -+ mk_row(<<"3">>, <<"1-7fbf84d56f8017880974402d60f5acd6">>), -+ mk_row(<<"4">>, <<"1-fcaf5852c08ffb239ac8ce16c409f253">>), -+ mk_row(<<"5">>, <<"1-aaac5d460fd40f9286e57b9bf12e23d2">>) -+ ]}, -+ ?_assertEqual(Expect, Result). -+ -+should_query_with_range_rev(Db) -> -+ Result = run_query(Db, [ -+ {direction, rev}, -+ {start_key, <<"5">>}, {end_key, <<"3">>}, -+ {inclusive_end, true} -+ ]), -+ Expect = {ok, [ -+ {meta, [{total, 11}, {offset, 5}]}, -+ mk_row(<<"5">>, <<"1-aaac5d460fd40f9286e57b9bf12e23d2">>), -+ mk_row(<<"4">>, <<"1-fcaf5852c08ffb239ac8ce16c409f253">>), -+ mk_row(<<"3">>, <<"1-7fbf84d56f8017880974402d60f5acd6">>) -+ ]}, -+ ?_assertEqual(Expect, Result). -+ -+should_query_with_limit_and_skip(Db) -> -+ Result = run_query(Db, [ -+ {start_key, <<"2">>}, -+ {limit, 3}, -+ {skip, 3} -+ ]), -+ Expect = {ok, [ -+ {meta, [{total, 11}, {offset, 5}]}, -+ mk_row(<<"5">>, <<"1-aaac5d460fd40f9286e57b9bf12e23d2">>), -+ mk_row(<<"6">>, <<"1-aca21c2e7bc5f8951424fcfc5d1209d8">>), -+ mk_row(<<"7">>, <<"1-4374aeec17590d82f16e70f318116ad9">>) -+ ]}, -+ ?_assertEqual(Expect, Result). -+ -+should_query_with_include_docs(Db) -> -+ Result = run_query(Db, [ -+ {start_key, <<"8">>}, -+ {end_key, <<"8">>}, -+ {include_docs, true} -+ ]), -+ Doc = {[ -+ {<<"_id">>,<<"8">>}, -+ {<<"_rev">>, <<"1-55b9a29311341e07ec0a7ca13bc1b59f">>}, -+ {<<"val">>, 8} -+ ]}, -+ Val = {[{rev, <<"1-55b9a29311341e07ec0a7ca13bc1b59f">>}]}, -+ Expect = {ok, [ -+ {meta, [{total, 11}, {offset, 8}]}, -+ {row, [{id, <<"8">>}, {key, <<"8">>}, {value, Val}, {doc, Doc}]} -+ ]}, -+ ?_assertEqual(Expect, Result). -+ -+should_query_empty_views(Db) -> -+ Result = couch_mrview:query_view(Db, <<"_design/bar">>, <<"bing">>), -+ Expect = {ok, [ -+ {meta, [{total, 0}, {offset, 0}]} -+ ]}, -+ ?_assertEqual(Expect, Result). -+ -+ -+mk_row(Id, Rev) -> -+ {row, [{id, Id}, {key, Id}, {value, {[{rev, Rev}]}}]}. -+ -+run_query(Db, Opts) -> -+ couch_mrview:query_all_docs(Db, Opts). -diff --git a/src/couch_mrview/test/couch_mrview_collation_tests.erl b/src/couch_mrview/test/couch_mrview_collation_tests.erl -new file mode 100644 -index 0000000..2e0b75b ---- /dev/null -+++ b/src/couch_mrview/test/couch_mrview_collation_tests.erl -@@ -0,0 +1,202 @@ -+% Licensed under the Apache License, Version 2.0 (the "License"); you may not -+% use this file except in compliance with the License. You may obtain a copy of -+% the License at -+% -+% http://www.apache.org/licenses/LICENSE-2.0 -+% -+% Unless required by applicable law or agreed to in writing, software -+% distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -+% WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -+% License for the specific language governing permissions and limitations under -+% the License. -+ -+-module(couch_mrview_collation_tests). -+ -+-include("couch_eunit.hrl"). -+-include_lib("couchdb/couch_db.hrl"). -+ -+-define(ADMIN_USER, {user_ctx, #user_ctx{roles=[<<"_admin">>]}}). -+-define(TIMEOUT, 1000). -+-define(VALUES, [ -+ null, -+ false, -+ true, -+ -+ 1, -+ 2, -+ 3.0, -+ 4, -+ -+ <<"a">>, -+ <<"A">>, -+ <<"aa">>, -+ <<"b">>, -+ <<"B">>, -+ <<"ba">>, -+ <<"bb">>, -+ -+ [<<"a">>], -+ [<<"b">>], -+ [<<"b">>, <<"c">>], -+ [<<"b">>, <<"c">>, <<"a">>], -+ [<<"b">>, <<"d">>], -+ [<<"b">>, <<"d">>, <<"e">>], -+ -+ {[{<<"a">>, 1}]}, -+ {[{<<"a">>, 2}]}, -+ {[{<<"b">>, 1}]}, -+ {[{<<"b">>, 2}]}, -+ {[{<<"b">>, 2}, {<<"a">>, 1}]}, -+ {[{<<"b">>, 2}, {<<"c">>, 2}]} -+]). -+ -+ -+start() -> -+ {ok, Pid} = couch_server_sup:start_link(?CONFIG_CHAIN), -+ Pid. -+ -+stop(Pid) -> -+ erlang:monitor(process, Pid), -+ couch_server_sup:stop(), -+ receive -+ {'DOWN', _, _, Pid, _} -> -+ ok -+ after ?TIMEOUT -> -+ throw({timeout, server_stop}) -+ end. -+ -+setup() -> -+ {ok, Db1} = couch_mrview_test_util:new_db(?tempdb(), map), -+ {ok, Db2} = couch_mrview_test_util:save_docs(Db1, make_docs()), -+ Db2. -+ -+teardown(Db) -> -+ couch_db:close(Db), -+ couch_server:delete(Db#db.name, [?ADMIN_USER]), -+ ok. -+ -+ -+collation_test_() -> -+ { -+ "Collation tests", -+ { -+ setup, -+ fun start/0, fun stop/1, -+ { -+ foreach, -+ fun setup/0, fun teardown/1, -+ [ -+ fun should_collate_fwd/1, -+ fun should_collate_rev/1, -+ fun should_collate_range/1, -+ fun should_collate_with_inclusive_end_fwd/1, -+ fun should_collate_with_inclusive_end_rev/1, -+ fun should_collate_without_inclusive_end_fwd/1, -+ fun should_collate_without_inclusive_end_rev/1, -+ fun should_collate_with_endkey_docid/1 -+ ] -+ } -+ } -+ }. -+ -+ -+should_collate_fwd(Db) -> -+ {ok, Results} = run_query(Db, []), -+ Expect = [{meta, [{total, 26}, {offset, 0}]}] ++ rows(), -+ %% cannot use _assertEqual since mrview converts -+ %% value 3.0 to 3 making assertion fail -+ ?_assert(Expect == Results). -+ -+should_collate_rev(Db) -> -+ {ok, Results} = run_query(Db, [{direction, rev}]), -+ Expect = [{meta, [{total, 26}, {offset, 0}]}] ++ lists:reverse(rows()), -+ %% cannot use _assertEqual since mrview converts -+ %% value 3.0 to 3 making assertion fail -+ ?_assert(Expect == Results). -+ -+should_collate_range(Db) -> -+ ?_assertNot( -+ begin -+ {_, Error} = lists:foldl(fun(V, {Count, Error}) -> -+ {ok, Results} = run_query(Db, [{start_key, V}, {end_key, V}]), -+ Id = list_to_binary(integer_to_list(Count)), -+ Expect = [ -+ {meta, [{total, 26}, {offset, Count}]}, -+ {row, [{id, Id}, {key, V}, {value, 0}]} -+ ], -+ case Results == Expect of -+ true -> {Count+1, Error}; -+ _ -> {Count+1, true} -+ end -+ end, {0, false}, ?VALUES), -+ Error -+ end). -+ -+should_collate_with_inclusive_end_fwd(Db) -> -+ Opts = [{end_key, <<"b">>}, {inclusive_end, true}], -+ {ok, Rows0} = run_query(Db, Opts), -+ LastRow = lists:last(Rows0), -+ Expect = {row, [{id,<<"10">>}, {key,<<"b">>}, {value,0}]}, -+ ?_assertEqual(Expect, LastRow). -+ -+should_collate_with_inclusive_end_rev(Db) -> -+ Opts = [{end_key, <<"b">>}, {inclusive_end, true}, {direction, rev}], -+ {ok, Rows} = run_query(Db, Opts), -+ LastRow = lists:last(Rows), -+ Expect = {row, [{id,<<"10">>}, {key,<<"b">>}, {value,0}]}, -+ ?_assertEqual(Expect, LastRow). -+ -+should_collate_without_inclusive_end_fwd(Db) -> -+ Opts = [{end_key, <<"b">>}, {inclusive_end, false}], -+ {ok, Rows0} = run_query(Db, Opts), -+ LastRow = lists:last(Rows0), -+ Expect = {row, [{id,<<"9">>}, {key,<<"aa">>}, {value,0}]}, -+ ?_assertEqual(Expect, LastRow). -+ -+should_collate_without_inclusive_end_rev(Db) -> -+ Opts = [{end_key, <<"b">>}, {inclusive_end, false}, {direction, rev}], -+ {ok, Rows} = run_query(Db, Opts), -+ LastRow = lists:last(Rows), -+ Expect = {row, [{id,<<"11">>}, {key,<<"B">>}, {value,0}]}, -+ ?_assertEqual(Expect, LastRow). -+ -+should_collate_with_endkey_docid(Db) -> -+ ?_test(begin -+ {ok, Rows0} = run_query(Db, [ -+ {end_key, <<"b">>}, {end_key_docid, <<"10">>}, -+ {inclusive_end, false} -+ ]), -+ Result0 = lists:last(Rows0), -+ Expect0 = {row, [{id,<<"9">>}, {key,<<"aa">>}, {value,0}]}, -+ ?assertEqual(Expect0, Result0), -+ -+ {ok, Rows1} = run_query(Db, [ -+ {end_key, <<"b">>}, {end_key_docid, <<"11">>}, -+ {inclusive_end, false} -+ ]), -+ Result1 = lists:last(Rows1), -+ Expect1 = {row, [{id,<<"10">>}, {key,<<"b">>}, {value,0}]}, -+ ?assertEqual(Expect1, Result1) -+ end). -+ -+ -+make_docs() -> -+ {Docs, _} = lists:foldl(fun(V, {Docs0, Count}) -> -+ Doc = couch_doc:from_json_obj({[ -+ {<<"_id">>, list_to_binary(integer_to_list(Count))}, -+ {<<"foo">>, V} -+ ]}), -+ {[Doc | Docs0], Count+1} -+ end, {[], 0}, ?VALUES), -+ Docs. -+ -+rows() -> -+ {Rows, _} = lists:foldl(fun(V, {Rows0, Count}) -> -+ Id = list_to_binary(integer_to_list(Count)), -+ Row = {row, [{id, Id}, {key, V}, {value, 0}]}, -+ {[Row | Rows0], Count+1} -+ end, {[], 0}, ?VALUES), -+ lists:reverse(Rows). -+ -+run_query(Db, Opts) -> -+ couch_mrview:query_view(Db, <<"_design/bar">>, <<"zing">>, Opts). -diff --git a/src/couch_mrview/test/couch_mrview_compact_tests.erl b/src/couch_mrview/test/couch_mrview_compact_tests.erl -new file mode 100644 -index 0000000..4cb7daf ---- /dev/null -+++ b/src/couch_mrview/test/couch_mrview_compact_tests.erl -@@ -0,0 +1,101 @@ -+% Licensed under the Apache License, Version 2.0 (the "License"); you may not -+% use this file except in compliance with the License. You may obtain a copy of -+% the License at -+% -+% http://www.apache.org/licenses/LICENSE-2.0 -+% -+% Unless required by applicable law or agreed to in writing, software -+% distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -+% WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -+% License for the specific language governing permissions and limitations under -+% the License. -+ -+-module(couch_mrview_compact_tests). -+ -+-include("couch_eunit.hrl"). -+-include_lib("couchdb/couch_db.hrl"). -+ -+-define(ADMIN_USER, {user_ctx, #user_ctx{roles=[<<"_admin">>]}}). -+-define(TIMEOUT, 1000). -+ -+ -+start() -> -+ {ok, Pid} = couch_server_sup:start_link(?CONFIG_CHAIN), -+ Pid. -+ -+stop(Pid) -> -+ erlang:monitor(process, Pid), -+ couch_server_sup:stop(), -+ receive -+ {'DOWN', _, _, Pid, _} -> -+ ok -+ after ?TIMEOUT -> -+ throw({timeout, server_stop}) -+ end. -+ -+setup() -> -+ {ok, Db} = couch_mrview_test_util:init_db(?tempdb(), map, 1000), -+ Db. -+ -+teardown(Db) -> -+ couch_db:close(Db), -+ couch_server:delete(Db#db.name, [?ADMIN_USER]), -+ ok. -+ -+ -+compaction_test_() -> -+ { -+ "Compaction tests", -+ { -+ setup, -+ fun start/0, fun stop/1, -+ { -+ foreach, -+ fun setup/0, fun teardown/1, -+ [ -+ fun should_swap/1 -+ ] -+ } -+ } -+ }. -+ -+ -+should_swap(Db) -> -+ ?_test(begin -+ couch_mrview:query_view(Db, <<"_design/bar">>, <<"baz">>), -+ {ok, QPid} = start_query(Db), -+ {ok, MonRef} = couch_mrview:compact(Db, <<"_design/bar">>, [monitor]), -+ receive -+ {'DOWN', MonRef, process, _, _} -> ok -+ after ?TIMEOUT -> -+ erlang:error( -+ {assertion_failed, -+ [{module, ?MODULE}, {line, ?LINE}, -+ {reason, "compaction failed"}]}) -+ end, -+ QPid ! {self(), continue}, -+ receive -+ {QPid, Count} -> -+ ?assertEqual(1000, Count) -+ after ?TIMEOUT -> -+ erlang:error( -+ {assertion_failed, -+ [{module, ?MODULE}, {line, ?LINE}, -+ {reason, "query failed"}]}) -+ end -+ end). -+ -+ -+start_query(Db) -> -+ Self = self(), -+ Pid = spawn(fun() -> -+ CB = fun -+ (_, wait) -> receive {Self, continue} -> {ok, 0} end; -+ ({row, _}, Count) -> {ok, Count+1}; -+ (_, Count) -> {ok, Count} -+ end, -+ {ok, Result} = -+ couch_mrview:query_view(Db, <<"_design/bar">>, <<"baz">>, [], CB, wait), -+ Self ! {self(), Result} -+ end), -+ {ok, Pid}. -diff --git a/src/couch_mrview/test/couch_mrview_index_info_tests.erl b/src/couch_mrview/test/couch_mrview_index_info_tests.erl -new file mode 100644 -index 0000000..6c30da8 ---- /dev/null -+++ b/src/couch_mrview/test/couch_mrview_index_info_tests.erl -@@ -0,0 +1,87 @@ -+% Licensed under the Apache License, Version 2.0 (the "License"); you may not -+% use this file except in compliance with the License. You may obtain a copy of -+% the License at -+% -+% http://www.apache.org/licenses/LICENSE-2.0 -+% -+% Unless required by applicable law or agreed to in writing, software -+% distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -+% WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -+% License for the specific language governing permissions and limitations under -+% the License. -+ -+-module(couch_mrview_index_info_tests). -+ -+-include("couch_eunit.hrl"). -+-include_lib("couchdb/couch_db.hrl"). -+ -+-define(ADMIN_USER, {user_ctx, #user_ctx{roles=[<<"_admin">>]}}). -+-define(TIMEOUT, 1000). -+ -+ -+start() -> -+ {ok, Pid} = couch_server_sup:start_link(?CONFIG_CHAIN), -+ Pid. -+ -+stop(Pid) -> -+ erlang:monitor(process, Pid), -+ couch_server_sup:stop(), -+ receive -+ {'DOWN', _, _, Pid, _} -> -+ ok -+ after ?TIMEOUT -> -+ throw({timeout, server_stop}) -+ end. -+ -+setup() -> -+ {ok, Db} = couch_mrview_test_util:init_db(?tempdb(), map), -+ couch_mrview:query_view(Db, <<"_design/bar">>, <<"baz">>), -+ {ok, Info} = couch_mrview:get_info(Db, <<"_design/bar">>), -+ {Db, Info}. -+ -+teardown({Db, _}) -> -+ couch_db:close(Db), -+ couch_server:delete(Db#db.name, [?ADMIN_USER]), -+ ok. -+ -+ -+view_info_test_() -> -+ { -+ "Views index tests", -+ { -+ setup, -+ fun start/0, fun stop/1, -+ { -+ foreach, -+ fun setup/0, fun teardown/1, -+ [ -+ fun should_get_property/1 -+ ] -+ } -+ } -+ }. -+ -+ -+should_get_property({_, Info}) -> -+ InfoProps = [ -+ {signature, <<"276df562b152b3c4e5d34024f62672ed">>}, -+ {language, <<"javascript">>}, -+ {disk_size, 314}, -+ {data_size, 263}, -+ {update_seq, 11}, -+ {purge_seq, 0}, -+ {updater_running, false}, -+ {compact_running, false}, -+ {waiting_clients, 0} -+ ], -+ [ -+ {atom_to_list(Key), ?_assertEqual(Val, getval(Key, Info))} -+ || {Key, Val} <- InfoProps -+ ]. -+ -+ -+getval(Key, PL) -> -+ {value, {Key, Val}} = lists:keysearch(Key, 1, PL), -+ Val. -+ -+ -diff --git a/src/couch_mrview/test/couch_mrview_map_views_tests.erl b/src/couch_mrview/test/couch_mrview_map_views_tests.erl -new file mode 100644 -index 0000000..b364b77 ---- /dev/null -+++ b/src/couch_mrview/test/couch_mrview_map_views_tests.erl -@@ -0,0 +1,138 @@ -+% Licensed under the Apache License, Version 2.0 (the "License"); you may not -+% use this file except in compliance with the License. You may obtain a copy of -+% the License at -+% -+% http://www.apache.org/licenses/LICENSE-2.0 -+% -+% Unless required by applicable law or agreed to in writing, software -+% distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -+% WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -+% License for the specific language governing permissions and limitations under -+% the License. -+ -+-module(couch_mrview_map_views_tests). -+ -+-include("couch_eunit.hrl"). -+-include_lib("couchdb/couch_db.hrl"). -+ -+-define(TIMEOUT, 1000). -+-define(ADMIN_USER, {user_ctx, #user_ctx{roles=[<<"_admin">>]}}). -+ -+ -+start() -> -+ {ok, Pid} = couch_server_sup:start_link(?CONFIG_CHAIN), -+ Pid. -+ -+stop(Pid) -> -+ erlang:monitor(process, Pid), -+ couch_server_sup:stop(), -+ receive -+ {'DOWN', _, _, Pid, _} -> -+ ok -+ after ?TIMEOUT -> -+ throw({timeout, server_stop}) -+ end. -+ -+setup() -> -+ {ok, Db} = couch_mrview_test_util:init_db(?tempdb(), map), -+ Db. -+ -+teardown(Db) -> -+ couch_db:close(Db), -+ couch_server:delete(Db#db.name, [?ADMIN_USER]), -+ ok. -+ -+ -+map_views_test_() -> -+ { -+ "Map views", -+ { -+ setup, -+ fun start/0, fun stop/1, -+ { -+ foreach, -+ fun setup/0, fun teardown/1, -+ [ -+ fun should_map/1, -+ fun should_map_with_range/1, -+ fun should_map_with_limit_and_skip/1, -+ fun should_map_with_include_docs/1, -+ fun should_map_empty_views/1 -+ ] -+ } -+ } -+ }. -+ -+ -+should_map(Db) -> -+ Result = run_query(Db, []), -+ Expect = {ok, [ -+ {meta, [{total, 10}, {offset, 0}]}, -+ {row, [{id, <<"1">>}, {key, 1}, {value, 1}]}, -+ {row, [{id, <<"2">>}, {key, 2}, {value, 2}]}, -+ {row, [{id, <<"3">>}, {key, 3}, {value, 3}]}, -+ {row, [{id, <<"4">>}, {key, 4}, {value, 4}]}, -+ {row, [{id, <<"5">>}, {key, 5}, {value, 5}]}, -+ {row, [{id, <<"6">>}, {key, 6}, {value, 6}]}, -+ {row, [{id, <<"7">>}, {key, 7}, {value, 7}]}, -+ {row, [{id, <<"8">>}, {key, 8}, {value, 8}]}, -+ {row, [{id, <<"9">>}, {key, 9}, {value, 9}]}, -+ {row, [{id, <<"10">>}, {key, 10}, {value, 10}]} -+ ]}, -+ ?_assertEqual(Expect, Result). -+ -+should_map_with_range(Db) -> -+ Result = run_query(Db, [ -+ {direction, rev}, -+ {start_key, 5}, {end_key, 3}, -+ {inclusive_end, true} -+ ]), -+ Expect = {ok, [ -+ {meta, [{total, 10}, {offset, 5}]}, -+ {row, [{id, <<"5">>}, {key, 5}, {value, 5}]}, -+ {row, [{id, <<"4">>}, {key, 4}, {value, 4}]}, -+ {row, [{id, <<"3">>}, {key, 3}, {value, 3}]} -+ ]}, -+ ?_assertEqual(Expect, Result). -+ -+should_map_with_limit_and_skip(Db) -> -+ Result = run_query(Db, [ -+ {start_key, 2}, -+ {limit, 3}, -+ {skip, 3} -+ ]), -+ Expect = {ok, [ -+ {meta, [{total, 10}, {offset, 4}]}, -+ {row, [{id, <<"5">>}, {key, 5}, {value, 5}]}, -+ {row, [{id, <<"6">>}, {key, 6}, {value, 6}]}, -+ {row, [{id, <<"7">>}, {key, 7}, {value, 7}]} -+ ]}, -+ ?_assertEqual(Expect, Result). -+ -+should_map_with_include_docs(Db) -> -+ Result = run_query(Db, [ -+ {start_key, 8}, -+ {end_key, 8}, -+ {include_docs, true} -+ ]), -+ Doc = {[ -+ {<<"_id">>,<<"8">>}, -+ {<<"_rev">>, <<"1-55b9a29311341e07ec0a7ca13bc1b59f">>}, -+ {<<"val">>,8} -+ ]}, -+ Expect = {ok, [ -+ {meta, [{total, 10}, {offset, 7}]}, -+ {row, [{id, <<"8">>}, {key, 8}, {value, 8}, {doc, Doc}]} -+ ]}, -+ ?_assertEqual(Expect, Result). -+ -+should_map_empty_views(Db) -> -+ Result = couch_mrview:query_view(Db, <<"_design/bar">>, <<"bing">>), -+ Expect = {ok, [ -+ {meta, [{total, 0}, {offset, 0}]} -+ ]}, -+ ?_assertEqual(Expect, Result). -+ -+ -+run_query(Db, Opts) -> -+ couch_mrview:query_view(Db, <<"_design/bar">>, <<"baz">>, Opts). -diff --git a/src/couch_mrview/test/couch_mrview_modules_load_tests.erl b/src/couch_mrview/test/couch_mrview_modules_load_tests.erl -new file mode 100644 -index 0000000..bfab646 ---- /dev/null -+++ b/src/couch_mrview/test/couch_mrview_modules_load_tests.erl -@@ -0,0 +1,37 @@ -+% Licensed under the Apache License, Version 2.0 (the "License"); you may not -+% use this file except in compliance with the License. You may obtain a copy of -+% the License at -+% -+% http://www.apache.org/licenses/LICENSE-2.0 -+% -+% Unless required by applicable law or agreed to in writing, software -+% distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -+% WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -+% License for the specific language governing permissions and limitations under -+% the License. -+ -+-module(couch_mrview_modules_load_tests). -+ -+-include("couch_eunit.hrl"). -+ -+ -+modules_load_test_() -> -+ { -+ "Verify that all modules loads", -+ should_load_modules() -+ }. -+ -+ -+should_load_modules() -> -+ Modules = [ -+ couch_mrview, -+ couch_mrview_compactor, -+ couch_mrview_http, -+ couch_mrview_index, -+ couch_mrview_updater, -+ couch_mrview_util -+ ], -+ [should_load_module(Mod) || Mod <- Modules]. -+ -+should_load_module(Mod) -> -+ {atom_to_list(Mod), ?_assertMatch({module, _}, code:load_file(Mod))}. -diff --git a/src/couch_mrview/test/couch_mrview_red_views_tests.erl b/src/couch_mrview/test/couch_mrview_red_views_tests.erl -new file mode 100644 -index 0000000..ed6018b ---- /dev/null -+++ b/src/couch_mrview/test/couch_mrview_red_views_tests.erl -@@ -0,0 +1,110 @@ -+% Licensed under the Apache License, Version 2.0 (the "License"); you may not -+% use this file except in compliance with the License. You may obtain a copy of -+% the License at -+% -+% http://www.apache.org/licenses/LICENSE-2.0 -+% -+% Unless required by applicable law or agreed to in writing, software -+% distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -+% WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -+% License for the specific language governing permissions and limitations under -+% the License. -+ -+-module(couch_mrview_red_views_tests). -+ -+-include("couch_eunit.hrl"). -+-include_lib("couchdb/couch_db.hrl"). -+ -+-define(TIMEOUT, 1000). -+-define(ADMIN_USER, {user_ctx, #user_ctx{roles=[<<"_admin">>]}}). -+ -+ -+start() -> -+ {ok, Pid} = couch_server_sup:start_link(?CONFIG_CHAIN), -+ Pid. -+ -+stop(Pid) -> -+ erlang:monitor(process, Pid), -+ couch_server_sup:stop(), -+ receive -+ {'DOWN', _, _, Pid, _} -> -+ ok -+ after ?TIMEOUT -> -+ throw({timeout, server_stop}) -+ end. -+ -+setup() -> -+ {ok, Db} = couch_mrview_test_util:init_db(?tempdb(), red), -+ Db. -+ -+teardown(Db) -> -+ couch_db:close(Db), -+ couch_server:delete(Db#db.name, [?ADMIN_USER]), -+ ok. -+ -+ -+reduce_views_test_() -> -+ { -+ "Reduce views", -+ { -+ setup, -+ fun start/0, fun stop/1, -+ { -+ foreach, -+ fun setup/0, fun teardown/1, -+ [ -+ fun should_reduce_basic/1, -+ fun should_reduce_key_range/1, -+ fun should_reduce_with_group_level/1, -+ fun should_reduce_with_group_exact/1 -+ ] -+ } -+ } -+ }. -+ -+ -+should_reduce_basic(Db) -> -+ Result = run_query(Db, []), -+ Expect = {ok, [ -+ {meta, []}, -+ {row, [{key, null}, {value, 55}]} -+ ]}, -+ ?_assertEqual(Expect, Result). -+ -+should_reduce_key_range(Db) -> -+ Result = run_query(Db, [{start_key, [0, 2]}, {end_key, [0, 4]}]), -+ Expect = {ok, [ -+ {meta, []}, -+ {row, [{key, null}, {value, 6}]} -+ ]}, -+ ?_assertEqual(Expect, Result). -+ -+should_reduce_with_group_level(Db) -> -+ Result = run_query(Db, [{group_level, 1}]), -+ Expect = {ok, [ -+ {meta, []}, -+ {row, [{key, [0]}, {value, 30}]}, -+ {row, [{key, [1]}, {value, 25}]} -+ ]}, -+ ?_assertEqual(Expect, Result). -+ -+should_reduce_with_group_exact(Db) -> -+ Result = run_query(Db, [{group_level, exact}]), -+ Expect = {ok, [ -+ {meta, []}, -+ {row, [{key, [0, 2]}, {value, 2}]}, -+ {row, [{key, [0, 4]}, {value, 4}]}, -+ {row, [{key, [0, 6]}, {value, 6}]}, -+ {row, [{key, [0, 8]}, {value, 8}]}, -+ {row, [{key, [0, 10]}, {value, 10}]}, -+ {row, [{key, [1, 1]}, {value, 1}]}, -+ {row, [{key, [1, 3]}, {value, 3}]}, -+ {row, [{key, [1, 5]}, {value, 5}]}, -+ {row, [{key, [1, 7]}, {value, 7}]}, -+ {row, [{key, [1, 9]}, {value, 9}]} -+ ]}, -+ ?_assertEqual(Expect, Result). -+ -+ -+run_query(Db, Opts) -> -+ couch_mrview:query_view(Db, <<"_design/bar">>, <<"baz">>, Opts). -diff --git a/src/couch_replicator/Makefile.am b/src/couch_replicator/Makefile.am -index 2dcd47d..67c9872 100644 ---- a/src/couch_replicator/Makefile.am -+++ b/src/couch_replicator/Makefile.am -@@ -36,13 +36,13 @@ source_files = \ - src/couch_replicator.erl - - test_files = \ -- test/01-load.t \ -- test/02-httpc-pool.t \ -- test/03-replication-compact.t \ -- test/04-replication-large-atts.t \ -- test/05-replication-many-leaves.t \ -- test/06-doc-missing-stubs.t \ -- test/07-use-checkpoints.t -+ test/couch_replicator_compact_tests.erl \ -+ test/couch_replicator_httpc_pool_tests.erl \ -+ test/couch_replicator_large_atts_tests.erl \ -+ test/couch_replicator_many_leaves_tests.erl \ -+ test/couch_replicator_missing_stubs_tests.erl \ -+ test/couch_replicator_modules_load_tests.erl \ -+ test/couch_replicator_use_checkpoints_tests.erl - - compiled_files = \ - ebin/couch_replicator_api_wrap.beam \ -@@ -62,7 +62,7 @@ CLEANFILES = $(compiled_files) - - check: - if TESTS -- $(abs_top_builddir)/test/etap/run $(abs_top_srcdir)/src/couch_replicator/test -+ $(abs_top_builddir)/test/couchdb/run -v $(abs_top_srcdir)/src/couch_replicator/test - endif - - ebin/%.app: src/%.app.src -diff --git a/src/couch_replicator/test/01-load.t b/src/couch_replicator/test/01-load.t -deleted file mode 100644 -index 8bd82dd..0000000 ---- a/src/couch_replicator/test/01-load.t -+++ /dev/null -@@ -1,37 +0,0 @@ --#!/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(), -- Modules = [ -- couch_replicator_api_wrap, -- couch_replicator_httpc, -- couch_replicator_httpd, -- couch_replicator_manager, -- couch_replicator_notifier, -- couch_replicator, -- couch_replicator_worker, -- couch_replicator_utils, -- couch_replicator_job_sup -- ], -- -- etap:plan(length(Modules)), -- lists:foreach( -- fun(Module) -> -- etap:loaded_ok(Module, lists:concat(["Loaded: ", Module])) -- end, Modules), -- etap:end_tests(). -diff --git a/src/couch_replicator/test/02-httpc-pool.t b/src/couch_replicator/test/02-httpc-pool.t -deleted file mode 100755 -index a7bde6c..0000000 ---- a/src/couch_replicator/test/02-httpc-pool.t -+++ /dev/null -@@ -1,250 +0,0 @@ --#!/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(55), -- 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(), -- -- test_pool_full(), -- test_worker_dead_pool_non_full(), -- test_worker_dead_pool_full(), -- -- couch_server_sup:stop(), -- ok. -- -- --test_pool_full() -> -- Pool = spawn_pool(), -- Client1 = spawn_client(Pool), -- Client2 = spawn_client(Pool), -- Client3 = spawn_client(Pool), -- -- etap:diag("Check that we can spawn the max number of connections."), -- etap:is(ping_client(Client1), ok, "Client 1 started ok."), -- etap:is(ping_client(Client2), ok, "Client 2 started ok."), -- etap:is(ping_client(Client3), ok, "Client 3 started ok."), -- -- Worker1 = get_client_worker(Client1, "1"), -- Worker2 = get_client_worker(Client2, "2"), -- Worker3 = get_client_worker(Client3, "3"), -- etap:is(is_process_alive(Worker1), true, "Client's 1 worker is alive."), -- etap:is(is_process_alive(Worker2), true, "Client's 2 worker is alive."), -- etap:is(is_process_alive(Worker3), true, "Client's 3 worker is alive."), -- -- etap:isnt(Worker1, Worker2, "Clients 1 and 2 got different workers."), -- etap:isnt(Worker2, Worker3, "Clients 2 and 3 got different workers."), -- etap:isnt(Worker1, Worker3, "Clients 1 and 3 got different workers."), -- -- etap:diag("Check that client 4 blocks waiting for a worker."), -- Client4 = spawn_client(Pool), -- etap:is(ping_client(Client4), timeout, "Client 4 blocked while waiting."), -- -- etap:diag("Check that stopping a client gives up its worker."), -- etap:is(stop_client(Client1), ok, "First client stopped."), -- -- etap:diag("And check that our blocked client has been unblocked."), -- etap:is(ping_client(Client4), ok, "Client 4 was unblocked."), -- -- Worker4 = get_client_worker(Client4, "4"), -- etap:is(is_process_alive(Worker4), true, "Client's 4 worker is alive."), -- etap:is(Worker4, Worker1, "Client 4 got worker that client 1 got before."), -- -- lists:foreach(fun(C) -> ok = stop_client(C) end, [Client2, Client3, Client4]), -- stop_pool(Pool). -- -- --test_worker_dead_pool_non_full() -> -- Pool = spawn_pool(), -- Client1 = spawn_client(Pool), -- -- etap:is(ping_client(Client1), ok, "Client 1 started ok."), -- Worker1 = get_client_worker(Client1, "1"), -- etap:is(is_process_alive(Worker1), true, "Client's 1 worker is alive."), -- -- etap:diag("Kill client's 1 worker."), -- etap:is(kill_client_worker(Client1), ok, "Killed client's 1 worker."), -- etap:is(is_process_alive(Worker1), false, "Client's 1 worker process is dead."), -- -- etap:is(stop_client(Client1), ok, "First client stopped and released its worker."), -- -- Client2 = spawn_client(Pool), -- etap:is(ping_client(Client2), ok, "Client 2 started ok."), -- Worker2 = get_client_worker(Client2, "2"), -- etap:isnt(Worker2, Worker1, "Client 2 got a different worker from client 1"), -- etap:is(is_process_alive(Worker2), true, "Client's 2 worker is alive."), -- -- etap:is(stop_client(Client2), ok, "Second client stopped."), -- stop_pool(Pool). -- -- --test_worker_dead_pool_full() -> -- Pool = spawn_pool(), -- Client1 = spawn_client(Pool), -- Client2 = spawn_client(Pool), -- Client3 = spawn_client(Pool), -- -- etap:diag("Check that we can spawn the max number of connections."), -- etap:is(ping_client(Client1), ok, "Client 1 started ok."), -- etap:is(ping_client(Client2), ok, "Client 2 started ok."), -- etap:is(ping_client(Client3), ok, "Client 3 started ok."), -- -- Worker1 = get_client_worker(Client1, "1"), -- Worker2 = get_client_worker(Client2, "2"), -- Worker3 = get_client_worker(Client3, "3"), -- etap:is(is_process_alive(Worker1), true, "Client's 1 worker is alive."), -- etap:is(is_process_alive(Worker2), true, "Client's 2 worker is alive."), -- etap:is(is_process_alive(Worker3), true, "Client's 3 worker is alive."), -- -- etap:isnt(Worker1, Worker2, "Clients 1 and 2 got different workers."), -- etap:isnt(Worker2, Worker3, "Clients 2 and 3 got different workers."), -- etap:isnt(Worker1, Worker3, "Clients 1 and 3 got different workers."), -- -- etap:diag("Check that client 4 blocks waiting for a worker."), -- Client4 = spawn_client(Pool), -- etap:is(ping_client(Client4), timeout, "Client 4 blocked while waiting."), -- -- etap:diag("Kill client's 1 worker."), -- etap:is(kill_client_worker(Client1), ok, "Killed client's 1 worker."), -- etap:is(is_process_alive(Worker1), false, "Client's 1 worker process is dead."), -- -- etap:diag("Check client 4 got unblocked after first worker's death"), -- etap:is(ping_client(Client4), ok, "Client 4 not blocked anymore."), -- -- Worker4 = get_client_worker(Client4, "4"), -- etap:is(is_process_alive(Worker4), true, "Client's 4 worker is alive."), -- etap:isnt(Worker4, Worker1, "Client 4 got a worker different from client 1."), -- etap:isnt(Worker4, Worker2, "Client 4 got a worker different from client 2."), -- etap:isnt(Worker4, Worker3, "Client 4 got a worker different from client 3."), -- -- etap:diag("Check that stopping client 1 is a noop."), -- etap:is(stop_client(Client1), ok, "First client stopped."), -- -- etap:is(is_process_alive(Worker2), true, "Client's 2 worker still alive."), -- etap:is(is_process_alive(Worker3), true, "Client's 3 worker still alive."), -- etap:is(is_process_alive(Worker4), true, "Client's 4 worker still alive."), -- -- etap:diag("Check that client 5 blocks waiting for a worker."), -- Client5 = spawn_client(Pool), -- etap:is(ping_client(Client5), timeout, "Client 5 blocked while waiting."), -- -- etap:diag("Check that stopping client 2 gives up its worker."), -- etap:is(stop_client(Client2), ok, "Second client stopped."), -- -- etap:diag("Now check that client 5 has been unblocked."), -- etap:is(ping_client(Client5), ok, "Client 5 was unblocked."), -- -- Worker5 = get_client_worker(Client5, "5"), -- etap:is(is_process_alive(Worker5), true, "Client's 5 worker is alive."), -- etap:isnt(Worker5, Worker1, "Client 5 got a worker different from client 1."), -- etap:is(Worker5, Worker2, "Client 5 got same worker as client 2."), -- etap:isnt(Worker5, Worker3, "Client 5 got a worker different from client 3."), -- etap:isnt(Worker5, Worker4, "Client 5 got a worker different from client 4."), -- -- etap:is(is_process_alive(Worker3), true, "Client's 3 worker still alive."), -- etap:is(is_process_alive(Worker4), true, "Client's 4 worker still alive."), -- etap:is(is_process_alive(Worker5), true, "Client's 5 worker still alive."), -- -- lists:foreach(fun(C) -> ok = stop_client(C) end, [Client3, Client4, Client5]), -- stop_pool(Pool). -- -- --spawn_client(Pool) -> -- Parent = self(), -- Ref = make_ref(), -- Pid = spawn(fun() -> -- {ok, Worker} = couch_replicator_httpc_pool:get_worker(Pool), -- loop(Parent, Ref, Worker, Pool) -- end), -- {Pid, Ref}. -- -- --ping_client({Pid, Ref}) -> -- Pid ! ping, -- receive -- {pong, Ref} -> -- ok -- after 3000 -> -- timeout -- end. -- -- --get_client_worker({Pid, Ref}, ClientName) -> -- Pid ! get_worker, -- receive -- {worker, Ref, Worker} -> -- Worker -- after 3000 -> -- etap:bail("Timeout getting client " ++ ClientName ++ " worker.") -- end. -- -- --stop_client({Pid, Ref}) -> -- Pid ! stop, -- receive -- {stop, Ref} -> -- ok -- after 3000 -> -- timeout -- end. -- -- --kill_client_worker({Pid, Ref}) -> -- Pid ! get_worker, -- receive -- {worker, Ref, Worker} -> -- exit(Worker, kill), -- ok -- after 3000 -> -- timeout -- end. -- -- --loop(Parent, Ref, Worker, Pool) -> -- receive -- ping -> -- Parent ! {pong, Ref}, -- loop(Parent, Ref, Worker, Pool); -- get_worker -> -- Parent ! {worker, Ref, Worker}, -- loop(Parent, Ref, Worker, Pool); -- stop -> -- couch_replicator_httpc_pool:release_worker(Pool, Worker), -- Parent ! {stop, Ref} -- end. -- -- --spawn_pool() -> -- Host = couch_config:get("httpd", "bind_address", "127.0.0.1"), -- Port = couch_config:get("httpd", "port", "5984"), -- {ok, Pool} = couch_replicator_httpc_pool:start_link( -- "http://" ++ Host ++ ":5984", [{max_connections, 3}]), -- Pool. -- -- --stop_pool(Pool) -> -- ok = couch_replicator_httpc_pool:stop(Pool). -diff --git a/src/couch_replicator/test/03-replication-compact.t b/src/couch_replicator/test/03-replication-compact.t -deleted file mode 100755 -index 7c4d38c..0000000 ---- a/src/couch_replicator/test/03-replication-compact.t -+++ /dev/null -@@ -1,488 +0,0 @@ --#!/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. -- --% Verify that compacting databases that are being used as the source or --% target of a replication doesn't affect the replication and that the --% replication doesn't hold their reference counters forever. -- ---define(b2l(B), binary_to_list(B)). -- ---record(user_ctx, { -- name = null, -- roles = [], -- handler --}). -- ---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, -- updater_fd, -- fd_ref_counter, -- header = nil, -- 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 = [], -- options = [], -- compression, -- before_doc_update, -- after_doc_read --}). -- ---record(rep, { -- id, -- source, -- target, -- options, -- user_ctx, -- doc_id --}). -- -- --source_db_name() -> <<"couch_test_rep_db_a">>. --target_db_name() -> <<"couch_test_rep_db_b">>. -- -- --main(_) -> -- test_util:init_code_path(), -- -- etap:plan(376), -- 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(), -- -- Pairs = [ -- {source_db_name(), target_db_name()}, -- {{remote, source_db_name()}, target_db_name()}, -- {source_db_name(), {remote, target_db_name()}}, -- {{remote, source_db_name()}, {remote, (target_db_name())}} -- ], -- -- lists:foreach( -- fun({Source, Target}) -> -- {ok, SourceDb} = create_db(source_db_name()), -- etap:is(couch_db:is_idle(SourceDb), true, -- "Source database is idle before starting replication"), -- -- {ok, TargetDb} = create_db(target_db_name()), -- etap:is(couch_db:is_idle(TargetDb), true, -- "Target database is idle before starting replication"), -- -- {ok, RepPid, RepId} = replicate(Source, Target), -- check_active_tasks(RepPid, RepId, Source, Target), -- {ok, DocsWritten} = populate_and_compact_test( -- RepPid, SourceDb, TargetDb), -- -- wait_target_in_sync(DocsWritten, TargetDb), -- check_active_tasks(RepPid, RepId, Source, Target), -- cancel_replication(RepId, RepPid), -- compare_dbs(SourceDb, TargetDb), -- -- delete_db(SourceDb), -- delete_db(TargetDb), -- couch_server_sup:stop(), -- ok = timer:sleep(1000), -- couch_server_sup:start_link(test_util:config_files()) -- end, -- Pairs), -- -- couch_server_sup:stop(), -- ok. -- -- --populate_and_compact_test(RepPid, SourceDb0, TargetDb0) -> -- etap:is(is_process_alive(RepPid), true, "Replication process is alive"), -- check_db_alive("source", SourceDb0), -- check_db_alive("target", TargetDb0), -- -- Writer = spawn_writer(SourceDb0), -- -- lists:foldl( -- fun(_, {SourceDb, TargetDb, DocCount}) -> -- pause_writer(Writer), -- -- compact_db("source", SourceDb), -- etap:is(is_process_alive(RepPid), true, -- "Replication process is alive after source database compaction"), -- check_db_alive("source", SourceDb), -- check_ref_counter("source", SourceDb), -- -- compact_db("target", TargetDb), -- etap:is(is_process_alive(RepPid), true, -- "Replication process is alive after target database compaction"), -- check_db_alive("target", TargetDb), -- check_ref_counter("target", TargetDb), -- -- {ok, SourceDb2} = reopen_db(SourceDb), -- {ok, TargetDb2} = reopen_db(TargetDb), -- -- resume_writer(Writer), -- wait_writer(Writer, DocCount), -- -- compact_db("source", SourceDb2), -- etap:is(is_process_alive(RepPid), true, -- "Replication process is alive after source database compaction"), -- check_db_alive("source", SourceDb2), -- pause_writer(Writer), -- check_ref_counter("source", SourceDb2), -- resume_writer(Writer), -- -- compact_db("target", TargetDb2), -- etap:is(is_process_alive(RepPid), true, -- "Replication process is alive after target database compaction"), -- check_db_alive("target", TargetDb2), -- pause_writer(Writer), -- check_ref_counter("target", TargetDb2), -- resume_writer(Writer), -- -- {ok, SourceDb3} = reopen_db(SourceDb2), -- {ok, TargetDb3} = reopen_db(TargetDb2), -- {SourceDb3, TargetDb3, DocCount + 50} -- end, -- {SourceDb0, TargetDb0, 50}, lists:seq(1, 5)), -- -- DocsWritten = stop_writer(Writer), -- {ok, DocsWritten}. -- -- --check_db_alive(Type, #db{main_pid = Pid}) -> -- etap:is(is_process_alive(Pid), true, -- "Local " ++ Type ++ " database main pid is alive"). -- -- --compact_db(Type, #db{name = Name}) -> -- {ok, Db} = couch_db:open_int(Name, []), -- {ok, CompactPid} = couch_db:start_compact(Db), -- MonRef = erlang:monitor(process, CompactPid), -- receive -- {'DOWN', MonRef, process, CompactPid, normal} -> -- ok; -- {'DOWN', MonRef, process, CompactPid, Reason} -> -- etap:bail("Error compacting " ++ Type ++ " database " ++ ?b2l(Name) ++ -- ": " ++ couch_util:to_list(Reason)) -- after 30000 -> -- etap:bail("Compaction for " ++ Type ++ " database " ++ ?b2l(Name) ++ -- " didn't finish") -- end, -- ok = couch_db:close(Db). -- -- --check_ref_counter(Type, #db{name = Name, fd_ref_counter = OldRefCounter}) -> -- MonRef = erlang:monitor(process, OldRefCounter), -- receive -- {'DOWN', MonRef, process, OldRefCounter, _} -> -- etap:diag("Old " ++ Type ++ " database ref counter terminated") -- after 30000 -> -- etap:bail("Old " ++ Type ++ " database ref counter didn't terminate") -- end, -- {ok, #db{fd_ref_counter = NewRefCounter} = Db} = couch_db:open_int(Name, []), -- ok = couch_db:close(Db), -- etap:isnt( -- NewRefCounter, OldRefCounter, Type ++ " database has new ref counter"). -- -- --reopen_db(#db{name = Name}) -> -- {ok, Db} = couch_db:open_int(Name, []), -- ok = couch_db:close(Db), -- {ok, Db}. -- -- --wait_target_in_sync(DocCount, #db{name = TargetName}) -> -- wait_target_in_sync_loop(DocCount, TargetName, 300). -- -- --wait_target_in_sync_loop(_DocCount, _TargetName, 0) -> -- etap:bail("Could not get source and target databases in sync"); --wait_target_in_sync_loop(DocCount, TargetName, RetriesLeft) -> -- {ok, Target} = couch_db:open_int(TargetName, []), -- {ok, TargetInfo} = couch_db:get_db_info(Target), -- ok = couch_db:close(Target), -- TargetDocCount = couch_util:get_value(doc_count, TargetInfo), -- case TargetDocCount == DocCount of -- true -> -- etap:diag("Source and target databases are in sync"); -- false -> -- ok = timer:sleep(100), -- wait_target_in_sync_loop(DocCount, TargetName, RetriesLeft - 1) -- end. -- -- --compare_dbs(#db{name = SourceName}, #db{name = TargetName}) -> -- {ok, SourceDb} = couch_db:open_int(SourceName, []), -- {ok, TargetDb} = couch_db:open_int(TargetName, []), -- Fun = fun(FullDocInfo, _, Acc) -> -- {ok, Doc} = couch_db:open_doc(SourceDb, FullDocInfo), -- {Props} = DocJson = couch_doc:to_json_obj(Doc, [attachments]), -- DocId = couch_util:get_value(<<"_id">>, Props), -- DocTarget = case couch_db:open_doc(TargetDb, DocId) of -- {ok, DocT} -> -- DocT; -- Error -> -- etap:bail("Error opening document '" ++ ?b2l(DocId) ++ -- "' from target: " ++ couch_util:to_list(Error)) -- end, -- DocTargetJson = couch_doc:to_json_obj(DocTarget, [attachments]), -- case DocTargetJson of -- DocJson -> -- ok; -- _ -> -- etap:bail("Content from document '" ++ ?b2l(DocId) ++ -- "' differs in target database") -- end, -- {ok, Acc} -- end, -- {ok, _, _} = couch_db:enum_docs(SourceDb, Fun, [], []), -- etap:diag("Target database has the same documents as the source database"), -- ok = couch_db:close(SourceDb), -- ok = couch_db:close(TargetDb). -- -- --check_active_tasks(RepPid, {BaseId, Ext} = _RepId, Src, Tgt) -> -- Source = case Src of -- {remote, NameSrc} -> -- <<(db_url(NameSrc))/binary, $/>>; -- _ -> -- Src -- end, -- Target = case Tgt of -- {remote, NameTgt} -> -- <<(db_url(NameTgt))/binary, $/>>; -- _ -> -- Tgt -- end, -- FullRepId = list_to_binary(BaseId ++ Ext), -- Pid = list_to_binary(pid_to_list(RepPid)), -- [RepTask] = couch_task_status:all(), -- etap:is(couch_util:get_value(pid, RepTask), Pid, -- "_active_tasks entry has correct pid property"), -- etap:is(couch_util:get_value(replication_id, RepTask), FullRepId, -- "_active_tasks entry has right replication id"), -- etap:is(couch_util:get_value(continuous, RepTask), true, -- "_active_tasks entry has continuous property set to true"), -- etap:is(couch_util:get_value(source, RepTask), Source, -- "_active_tasks entry has correct source property"), -- etap:is(couch_util:get_value(target, RepTask), Target, -- "_active_tasks entry has correct target property"), -- etap:is(is_integer(couch_util:get_value(docs_read, RepTask)), true, -- "_active_tasks entry has integer docs_read property"), -- etap:is(is_integer(couch_util:get_value(docs_written, RepTask)), true, -- "_active_tasks entry has integer docs_written property"), -- etap:is(is_integer(couch_util:get_value(doc_write_failures, RepTask)), true, -- "_active_tasks entry has integer doc_write_failures property"), -- etap:is(is_integer(couch_util:get_value(revisions_checked, RepTask)), true, -- "_active_tasks entry has integer revisions_checked property"), -- etap:is(is_integer(couch_util:get_value(missing_revisions_found, RepTask)), true, -- "_active_tasks entry has integer missing_revisions_found property"), -- etap:is(is_integer(couch_util:get_value(checkpointed_source_seq, RepTask)), true, -- "_active_tasks entry has integer checkpointed_source_seq property"), -- etap:is(is_integer(couch_util:get_value(source_seq, RepTask)), true, -- "_active_tasks entry has integer source_seq property"), -- Progress = couch_util:get_value(progress, RepTask), -- etap:is(is_integer(Progress), true, -- "_active_tasks entry has an integer progress property"), -- etap:is(Progress =< 100, true, "Progress is not greater than 100%"). -- -- --wait_writer(Pid, NumDocs) -> -- case get_writer_num_docs_written(Pid) of -- N when N >= NumDocs -> -- ok; -- _ -> -- wait_writer(Pid, NumDocs) -- end. -- -- --spawn_writer(Db) -> -- Parent = self(), -- Pid = spawn(fun() -> writer_loop(Db, Parent, 0) end), -- etap:diag("Started source database writer"), -- Pid. -- -- --pause_writer(Pid) -> -- Ref = make_ref(), -- Pid ! {pause, Ref}, -- receive -- {paused, Ref} -> -- ok -- after 30000 -> -- etap:bail("Failed to pause source database writer") -- end. -- -- --resume_writer(Pid) -> -- Ref = make_ref(), -- Pid ! {continue, Ref}, -- receive -- {ok, Ref} -> -- ok -- after 30000 -> -- etap:bail("Failed to unpause source database writer") -- end. -- -- --get_writer_num_docs_written(Pid) -> -- Ref = make_ref(), -- Pid ! {get_count, Ref}, -- receive -- {count, Ref, Count} -> -- Count -- after 30000 -> -- etap:bail("Timeout getting number of documents written from " -- "source database writer") -- end. -- -- --stop_writer(Pid) -> -- Ref = make_ref(), -- Pid ! {stop, Ref}, -- receive -- {stopped, Ref, DocsWritten} -> -- MonRef = erlang:monitor(process, Pid), -- receive -- {'DOWN', MonRef, process, Pid, _Reason} -> -- etap:diag("Stopped source database writer"), -- DocsWritten -- after 30000 -> -- etap:bail("Timeout stopping source database writer") -- end -- after 30000 -> -- etap:bail("Timeout stopping source database writer") -- end. -- -- --writer_loop(#db{name = DbName}, Parent, Counter) -> -- maybe_pause(Parent, Counter), -- Doc = couch_doc:from_json_obj({[ -- {<<"_id">>, list_to_binary(integer_to_list(Counter + 1))}, -- {<<"value">>, Counter + 1}, -- {<<"_attachments">>, {[ -- {<<"icon1.png">>, {[ -- {<<"data">>, base64:encode(att_data())}, -- {<<"content_type">>, <<"image/png">>} -- ]}}, -- {<<"icon2.png">>, {[ -- {<<"data">>, base64:encode(iolist_to_binary( -- [att_data(), att_data()]))}, -- {<<"content_type">>, <<"image/png">>} -- ]}} -- ]}} -- ]}), -- maybe_pause(Parent, Counter), -- {ok, Db} = couch_db:open_int(DbName, []), -- {ok, _} = couch_db:update_doc(Db, Doc, []), -- ok = couch_db:close(Db), -- receive -- {get_count, Ref} -> -- Parent ! {count, Ref, Counter + 1}, -- writer_loop(Db, Parent, Counter + 1); -- {stop, Ref} -> -- Parent ! {stopped, Ref, Counter + 1} -- after 0 -> -- ok = timer:sleep(500), -- writer_loop(Db, Parent, Counter + 1) -- end. -- -- --maybe_pause(Parent, Counter) -> -- receive -- {get_count, Ref} -> -- Parent ! {count, Ref, Counter}; -- {pause, Ref} -> -- Parent ! {paused, Ref}, -- receive {continue, Ref2} -> Parent ! {ok, Ref2} end -- after 0 -> -- ok -- end. -- -- --db_url(DbName) -> -- iolist_to_binary([ -- "http://", couch_config:get("httpd", "bind_address", "127.0.0.1"), -- ":", integer_to_list(mochiweb_socket_server:get(couch_httpd, port)), -- "/", DbName -- ]). -- -- --create_db(DbName) -> -- {ok, Db} = couch_db:create( -- DbName, -- [{user_ctx, #user_ctx{roles = [<<"_admin">>]}}, overwrite]), -- couch_db:close(Db), -- {ok, Db}. -- -- --delete_db(#db{name = DbName, main_pid = Pid}) -> -- ok = couch_server:delete( -- DbName, [{user_ctx, #user_ctx{roles = [<<"_admin">>]}}]), -- MonRef = erlang:monitor(process, Pid), -- receive -- {'DOWN', MonRef, process, Pid, _Reason} -> -- ok -- after 30000 -> -- etap:bail("Timeout deleting database") -- end. -- -- --replicate({remote, Db}, Target) -> -- replicate(db_url(Db), Target); -- --replicate(Source, {remote, Db}) -> -- replicate(Source, db_url(Db)); -- --replicate(Source, Target) -> -- RepObject = {[ -- {<<"source">>, Source}, -- {<<"target">>, Target}, -- {<<"continuous">>, true} -- ]}, -- {ok, Rep} = couch_replicator_utils:parse_rep_doc( -- RepObject, #user_ctx{roles = [<<"_admin">>]}), -- {ok, Pid} = couch_replicator:async_replicate(Rep), -- {ok, Pid, Rep#rep.id}. -- -- --cancel_replication(RepId, RepPid) -> -- {ok, _} = couch_replicator:cancel_replication(RepId), -- etap:is(is_process_alive(RepPid), false, -- "Replication process is no longer alive after cancel"). -- -- --att_data() -> -- {ok, Data} = file:read_file( -- test_util:source_file("share/www/image/logo.png")), -- Data. -diff --git a/src/couch_replicator/test/04-replication-large-atts.t b/src/couch_replicator/test/04-replication-large-atts.t -deleted file mode 100755 -index a7063c7..0000000 ---- a/src/couch_replicator/test/04-replication-large-atts.t -+++ /dev/null -@@ -1,267 +0,0 @@ --#!/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 replication of large attachments. Verify that both source and --% target have the same attachment data and metadata. -- ---define(b2l(Bin), binary_to_list(Bin)). -- ---record(user_ctx, { -- name = null, -- roles = [], -- handler --}). -- ---record(doc, { -- id = <<"">>, -- revs = {0, []}, -- body = {[]}, -- atts = [], -- deleted = false, -- meta = [] --}). -- ---record(att, { -- name, -- type, -- att_len, -- disk_len, -- md5= <<>>, -- revpos=0, -- data, -- encoding=identity --}). -- -- --source_db_name() -> <<"couch_test_rep_db_a">>. --target_db_name() -> <<"couch_test_rep_db_b">>. -- -- --main(_) -> -- test_util:init_code_path(), -- -- etap:plan(1192), -- 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()), -- application:start(ibrowse), -- application:start(crypto), -- couch_config:set("attachments", "compressible_types", "text/*", false), -- -- Pairs = [ -- {source_db_name(), target_db_name()}, -- {{remote, source_db_name()}, target_db_name()}, -- {source_db_name(), {remote, target_db_name()}}, -- {{remote, source_db_name()}, {remote, (target_db_name())}} -- ], -- -- {ok, SourceDb} = create_db(source_db_name()), -- etap:diag("Populating source database"), -- populate_db(SourceDb, 11), -- ok = couch_db:close(SourceDb), -- -- lists:foreach( -- fun({Source, Target}) -> -- etap:diag("Creating target database"), -- {ok, TargetDb} = create_db(target_db_name()), -- -- ok = couch_db:close(TargetDb), -- etap:diag("Triggering replication"), -- replicate(Source, Target), -- etap:diag("Replication finished, comparing source and target databases"), -- compare_dbs(SourceDb, TargetDb), -- -- etap:diag("Deleting target database"), -- delete_db(TargetDb), -- ok = timer:sleep(1000) -- end, -- Pairs), -- -- delete_db(SourceDb), -- couch_server_sup:stop(), -- ok. -- -- --populate_db(Db, DocCount) -> -- Docs = lists:foldl( -- fun(DocIdCounter, Acc) -> -- Doc = #doc{ -- id = iolist_to_binary(["doc", integer_to_list(DocIdCounter)]), -- body = {[]}, -- atts = [ -- att(<<"att1">>, 2 * 1024 * 1024, <<"text/plain">>), -- att(<<"att2">>, round(6.6 * 1024 * 1024), <<"app/binary">>) -- ] -- }, -- [Doc | Acc] -- end, -- [], lists:seq(1, DocCount)), -- {ok, _} = couch_db:update_docs(Db, Docs, []). -- -- --att(Name, Size, Type) -> -- #att{ -- name = Name, -- type = Type, -- att_len = Size, -- data = fun(Count) -> crypto:rand_bytes(Count) end -- }. -- -- --compare_dbs(Source, Target) -> -- {ok, SourceDb} = couch_db:open_int(couch_db:name(Source), []), -- {ok, TargetDb} = couch_db:open_int(couch_db:name(Target), []), -- -- Fun = fun(FullDocInfo, _, Acc) -> -- {ok, DocSource} = couch_db:open_doc(SourceDb, FullDocInfo), -- Id = DocSource#doc.id, -- -- etap:diag("Verifying document " ++ ?b2l(Id)), -- -- {ok, DocTarget} = couch_db:open_doc(TargetDb, Id), -- etap:is(DocTarget#doc.body, DocSource#doc.body, -- "Same body in source and target databases"), -- -- #doc{atts = SourceAtts} = DocSource, -- #doc{atts = TargetAtts} = DocTarget, -- etap:is( -- lists:sort([N || #att{name = N} <- SourceAtts]), -- lists:sort([N || #att{name = N} <- TargetAtts]), -- "Document has same number (and names) of attachments in " -- "source and target databases"), -- -- lists:foreach( -- fun(#att{name = AttName} = Att) -> -- etap:diag("Verifying attachment " ++ ?b2l(AttName)), -- -- {ok, AttTarget} = find_att(TargetAtts, AttName), -- SourceMd5 = att_md5(Att), -- TargetMd5 = att_md5(AttTarget), -- case AttName of -- <<"att1">> -> -- etap:is(Att#att.encoding, gzip, -- "Attachment is gzip encoded in source database"), -- etap:is(AttTarget#att.encoding, gzip, -- "Attachment is gzip encoded in target database"), -- DecSourceMd5 = att_decoded_md5(Att), -- DecTargetMd5 = att_decoded_md5(AttTarget), -- etap:is(DecTargetMd5, DecSourceMd5, -- "Same identity content in source and target databases"); -- _ -> -- etap:is(Att#att.encoding, identity, -- "Attachment is not encoded in source database"), -- etap:is(AttTarget#att.encoding, identity, -- "Attachment is not encoded in target database") -- end, -- etap:is(TargetMd5, SourceMd5, -- "Same content in source and target databases"), -- etap:is(is_integer(Att#att.disk_len), true, -- "#att.disk_len is an integer in source database"), -- etap:is(is_integer(Att#att.att_len), true, -- "#att.att_len is an integer in source database"), -- etap:is(is_integer(AttTarget#att.disk_len), true, -- "#att.disk_len is an integer in target database"), -- etap:is(is_integer(AttTarget#att.att_len), true, -- "#att.att_len is an integer in target database"), -- etap:is(Att#att.disk_len, AttTarget#att.disk_len, -- "Same identity length in source and target databases"), -- etap:is(Att#att.att_len, AttTarget#att.att_len, -- "Same encoded length in source and target databases"), -- etap:is(Att#att.type, AttTarget#att.type, -- "Same type in source and target databases"), -- etap:is(Att#att.md5, SourceMd5, "Correct MD5 in source database"), -- etap:is(AttTarget#att.md5, SourceMd5, "Correct MD5 in target database") -- end, -- SourceAtts), -- -- {ok, Acc} -- end, -- -- {ok, _, _} = couch_db:enum_docs(SourceDb, Fun, [], []), -- ok = couch_db:close(SourceDb), -- ok = couch_db:close(TargetDb). -- -- --find_att([], _Name) -> -- nil; --find_att([#att{name = Name} = Att | _], Name) -> -- {ok, Att}; --find_att([_ | Rest], Name) -> -- find_att(Rest, Name). -- -- --att_md5(Att) -> -- Md50 = couch_doc:att_foldl( -- Att, -- fun(Chunk, Acc) -> couch_util:md5_update(Acc, Chunk) end, -- couch_util:md5_init()), -- couch_util:md5_final(Md50). -- --att_decoded_md5(Att) -> -- Md50 = couch_doc:att_foldl_decode( -- Att, -- fun(Chunk, Acc) -> couch_util:md5_update(Acc, Chunk) end, -- couch_util:md5_init()), -- couch_util:md5_final(Md50). -- -- --db_url(DbName) -> -- iolist_to_binary([ -- "http://", couch_config:get("httpd", "bind_address", "127.0.0.1"), -- ":", integer_to_list(mochiweb_socket_server:get(couch_httpd, port)), -- "/", DbName -- ]). -- -- --create_db(DbName) -> -- couch_db:create( -- DbName, -- [{user_ctx, #user_ctx{roles = [<<"_admin">>]}}, overwrite]). -- -- --delete_db(Db) -> -- ok = couch_server:delete( -- couch_db:name(Db), [{user_ctx, #user_ctx{roles = [<<"_admin">>]}}]). -- -- --replicate({remote, Db}, Target) -> -- replicate(db_url(Db), Target); -- --replicate(Source, {remote, Db}) -> -- replicate(Source, db_url(Db)); -- --replicate(Source, Target) -> -- RepObject = {[ -- {<<"source">>, Source}, -- {<<"target">>, Target} -- ]}, -- {ok, Rep} = couch_replicator_utils:parse_rep_doc( -- RepObject, #user_ctx{roles = [<<"_admin">>]}), -- {ok, Pid} = couch_replicator:async_replicate(Rep), -- MonRef = erlang:monitor(process, Pid), -- receive -- {'DOWN', MonRef, process, Pid, Reason} -> -- etap:is(Reason, normal, "Replication finished successfully") -- after 300000 -> -- etap:bail("Timeout waiting for replication to finish") -- end. -diff --git a/src/couch_replicator/test/05-replication-many-leaves.t b/src/couch_replicator/test/05-replication-many-leaves.t -deleted file mode 100755 -index 212ee99..0000000 ---- a/src/couch_replicator/test/05-replication-many-leaves.t -+++ /dev/null -@@ -1,294 +0,0 @@ --#!/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 replication of documents with many leaf revisions. --% Motivated by COUCHDB-1340 and other similar issues where a document --% GET with a too long ?open_revs revision list doesn't work due to --% maximum web server limits for the HTTP request path. -- ---record(user_ctx, { -- name = null, -- roles = [], -- handler --}). -- ---record(doc, { -- id = <<"">>, -- revs = {0, []}, -- body = {[]}, -- atts = [], -- deleted = false, -- meta = [] --}). -- ---record(att, { -- name, -- type, -- att_len, -- disk_len, -- md5= <<>>, -- revpos=0, -- data, -- encoding=identity --}). -- ---define(b2l(B), binary_to_list(B)). ---define(l2b(L), list_to_binary(L)). ---define(i2l(I), integer_to_list(I)). -- -- --source_db_name() -> <<"couch_test_rep_db_a">>. --target_db_name() -> <<"couch_test_rep_db_b">>. -- --doc_ids() -> -- [<<"doc1">>, <<"doc2">>, <<"doc3">>]. -- --doc_num_conflicts(<<"doc1">>) -> 10; --doc_num_conflicts(<<"doc2">>) -> 100; --% a number > MaxURLlength (7000) / length(DocRevisionString) --doc_num_conflicts(<<"doc3">>) -> 210. -- -- --main(_) -> -- test_util:init_code_path(), -- -- etap:plan(56), -- 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_config:set("replicator", "connection_timeout", "90000", false), -- -- Pairs = [ -- {source_db_name(), target_db_name()}, -- {{remote, source_db_name()}, target_db_name()}, -- {source_db_name(), {remote, target_db_name()}}, -- {{remote, source_db_name()}, {remote, (target_db_name())}} -- ], -- -- lists:foreach( -- fun({Source, Target}) -> -- {ok, SourceDb} = create_db(source_db_name()), -- etap:diag("Populating source database"), -- {ok, DocRevs} = populate_db(SourceDb), -- ok = couch_db:close(SourceDb), -- etap:diag("Creating target database"), -- {ok, TargetDb} = create_db(target_db_name()), -- -- ok = couch_db:close(TargetDb), -- etap:diag("Triggering replication"), -- replicate(Source, Target), -- etap:diag("Replication finished, comparing source and target databases"), -- {ok, SourceDb2} = couch_db:open_int(source_db_name(), []), -- {ok, TargetDb2} = couch_db:open_int(target_db_name(), []), -- verify_target(SourceDb2, TargetDb2, DocRevs), -- ok = couch_db:close(SourceDb2), -- ok = couch_db:close(TargetDb2), -- -- {ok, SourceDb3} = couch_db:open_int(source_db_name(), []), -- {ok, DocRevs2} = add_attachments(SourceDb3, DocRevs, 2), -- ok = couch_db:close(SourceDb3), -- etap:diag("Triggering replication again"), -- replicate(Source, Target), -- etap:diag("Replication finished, comparing source and target databases"), -- {ok, SourceDb4} = couch_db:open_int(source_db_name(), []), -- {ok, TargetDb4} = couch_db:open_int(target_db_name(), []), -- verify_target(SourceDb4, TargetDb4, DocRevs2), -- ok = couch_db:close(SourceDb4), -- ok = couch_db:close(TargetDb4), -- -- etap:diag("Deleting source and target databases"), -- delete_db(TargetDb), -- delete_db(SourceDb), -- ok = timer:sleep(1000) -- end, -- Pairs), -- -- couch_server_sup:stop(), -- ok. -- -- --populate_db(Db) -> -- DocRevsDict = lists:foldl( -- fun(DocId, Acc) -> -- Value = <<"0">>, -- Doc = #doc{ -- id = DocId, -- body = {[ {<<"value">>, Value} ]} -- }, -- {ok, Rev} = couch_db:update_doc(Db, Doc, []), -- {ok, DocRevs} = add_doc_siblings(Db, DocId, doc_num_conflicts(DocId)), -- dict:store(DocId, [Rev | DocRevs], Acc) -- end, -- dict:new(), doc_ids()), -- {ok, dict:to_list(DocRevsDict)}. -- -- --add_doc_siblings(Db, DocId, NumLeaves) when NumLeaves > 0 -> -- add_doc_siblings(Db, DocId, NumLeaves, [], []). -- -- --add_doc_siblings(Db, _DocId, 0, AccDocs, AccRevs) -> -- {ok, []} = couch_db:update_docs(Db, AccDocs, [], replicated_changes), -- {ok, AccRevs}; -- --add_doc_siblings(Db, DocId, NumLeaves, AccDocs, AccRevs) -> -- Value = list_to_binary(integer_to_list(NumLeaves)), -- Rev = couch_util:md5(Value), -- Doc = #doc{ -- id = DocId, -- revs = {1, [Rev]}, -- body = {[ {<<"value">>, Value} ]} -- }, -- add_doc_siblings(Db, DocId, NumLeaves - 1, [Doc | AccDocs], [{1, Rev} | AccRevs]). -- -- --verify_target(_SourceDb, _TargetDb, []) -> -- ok; -- --verify_target(SourceDb, TargetDb, [{DocId, RevList} | Rest]) -> -- {ok, Lookups} = couch_db:open_doc_revs( -- TargetDb, -- DocId, -- RevList, -- [conflicts, deleted_conflicts]), -- Docs = [Doc || {ok, Doc} <- Lookups], -- {ok, SourceLookups} = couch_db:open_doc_revs( -- SourceDb, -- DocId, -- RevList, -- [conflicts, deleted_conflicts]), -- SourceDocs = [Doc || {ok, Doc} <- SourceLookups], -- Total = doc_num_conflicts(DocId) + 1, -- etap:is( -- length(Docs), -- Total, -- "Target has " ++ ?i2l(Total) ++ " leaf revisions of document " ++ ?b2l(DocId)), -- etap:diag("Verifying all revisions of document " ++ ?b2l(DocId)), -- lists:foreach( -- fun({#doc{id = Id, revs = Revs} = TgtDoc, #doc{id = Id, revs = Revs} = SrcDoc}) -> -- SourceJson = couch_doc:to_json_obj(SrcDoc, [attachments]), -- TargetJson = couch_doc:to_json_obj(TgtDoc, [attachments]), -- case TargetJson of -- SourceJson -> -- ok; -- _ -> -- {Pos, [Rev | _]} = Revs, -- etap:bail("Wrong value for revision " ++ -- ?b2l(couch_doc:rev_to_str({Pos, Rev})) ++ -- " of document " ++ ?b2l(DocId)) -- end -- end, -- lists:zip(Docs, SourceDocs)), -- verify_target(SourceDb, TargetDb, Rest). -- -- --add_attachments(Source, DocIdRevs, NumAtts) -> -- add_attachments(Source, DocIdRevs, NumAtts, []). -- --add_attachments(_SourceDb, [], _NumAtts, Acc) -> -- {ok, Acc}; -- --add_attachments(SourceDb, [{DocId, RevList} | Rest], NumAtts, IdRevsAcc) -> -- {ok, SourceLookups} = couch_db:open_doc_revs( -- SourceDb, -- DocId, -- RevList, -- []), -- SourceDocs = [Doc || {ok, Doc} <- SourceLookups], -- Total = doc_num_conflicts(DocId) + 1, -- etap:is( -- length(SourceDocs), -- Total, -- "Source still has " ++ ?i2l(Total) ++ -- " leaf revisions of document " ++ ?b2l(DocId)), -- etap:diag("Adding " ++ ?i2l(NumAtts) ++ -- " attachments to each revision of the document " ++ ?b2l(DocId)), -- NewDocs = lists:foldl( -- fun(#doc{atts = Atts, revs = {Pos, [Rev | _]}} = Doc, Acc) -> -- NewAtts = lists:foldl( -- fun(I, AttAcc) -> -- AttData = crypto:rand_bytes(100), -- NewAtt = #att{ -- name = iolist_to_binary( -- ["att_", ?i2l(I), "_", couch_doc:rev_to_str({Pos, Rev})]), -- type = <<"application/foobar">>, -- att_len = byte_size(AttData), -- data = AttData -- }, -- [NewAtt | AttAcc] -- end, -- [], lists:seq(1, NumAtts)), -- [Doc#doc{atts = Atts ++ NewAtts} | Acc] -- end, -- [], SourceDocs), -- {ok, UpdateResults} = couch_db:update_docs(SourceDb, NewDocs, []), -- NewRevs = [R || {ok, R} <- UpdateResults], -- etap:is( -- length(NewRevs), -- length(NewDocs), -- "Document revisions updated with " ++ ?i2l(NumAtts) ++ " attachments"), -- add_attachments(SourceDb, Rest, NumAtts, [{DocId, NewRevs} | IdRevsAcc]). -- -- --db_url(DbName) -> -- iolist_to_binary([ -- "http://", couch_config:get("httpd", "bind_address", "127.0.0.1"), -- ":", integer_to_list(mochiweb_socket_server:get(couch_httpd, port)), -- "/", DbName -- ]). -- -- --create_db(DbName) -> -- couch_db:create( -- DbName, -- [{user_ctx, #user_ctx{roles = [<<"_admin">>]}}, overwrite]). -- -- --delete_db(Db) -> -- ok = couch_server:delete( -- couch_db:name(Db), [{user_ctx, #user_ctx{roles = [<<"_admin">>]}}]). -- -- --replicate({remote, Db}, Target) -> -- replicate(db_url(Db), Target); -- --replicate(Source, {remote, Db}) -> -- replicate(Source, db_url(Db)); -- --replicate(Source, Target) -> -- RepObject = {[ -- {<<"source">>, Source}, -- {<<"target">>, Target} -- ]}, -- {ok, Rep} = couch_replicator_utils:parse_rep_doc( -- RepObject, #user_ctx{roles = [<<"_admin">>]}), -- {ok, Pid} = couch_replicator:async_replicate(Rep), -- MonRef = erlang:monitor(process, Pid), -- receive -- {'DOWN', MonRef, process, Pid, Reason} -> -- etap:is(Reason, normal, "Replication finished successfully") -- after 900000 -> -- etap:bail("Timeout waiting for replication to finish") -- end. -diff --git a/src/couch_replicator/test/06-doc-missing-stubs.t b/src/couch_replicator/test/06-doc-missing-stubs.t -deleted file mode 100755 -index e17efc9..0000000 ---- a/src/couch_replicator/test/06-doc-missing-stubs.t -+++ /dev/null -@@ -1,304 +0,0 @@ --#!/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 replication of documents with many leaf revisions. --% Motivated by COUCHDB-1340 and other similar issues where a document --% GET with a too long ?open_revs revision list doesn't work due to --% maximum web server limits for the HTTP request path. -- ---record(user_ctx, { -- name = null, -- roles = [], -- handler --}). -- ---record(doc, { -- id = <<"">>, -- revs = {0, []}, -- body = {[]}, -- atts = [], -- deleted = false, -- meta = [] --}). -- ---record(att, { -- name, -- type, -- att_len, -- disk_len, -- md5= <<>>, -- revpos=0, -- data, -- encoding=identity --}). -- ---define(b2l(B), binary_to_list(B)). -- --source_db_name() -> <<"couch_test_rep_db_a">>. --target_db_name() -> <<"couch_test_rep_db_b">>. -- --target_revs_limit() -> 3. -- -- --main(_) -> -- test_util:init_code_path(), -- -- etap:plan(128), -- 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 motivated by COUCHDB-1365. --test() -> -- couch_server_sup:start_link(test_util:config_files()), -- ibrowse:start(), -- -- Pairs = [ -- {source_db_name(), target_db_name()}, -- {{remote, source_db_name()}, target_db_name()}, -- {source_db_name(), {remote, target_db_name()}}, -- {{remote, source_db_name()}, {remote, (target_db_name())}} -- ], -- -- lists:foreach( -- fun({Source, Target}) -> -- {ok, SourceDb} = create_db(source_db_name()), -- etap:diag("Populating source database"), -- populate_db(SourceDb), -- ok = couch_db:close(SourceDb), -- -- etap:diag("Creating target database"), -- {ok, TargetDb} = create_db(target_db_name()), -- ok = couch_db:set_revs_limit(TargetDb, target_revs_limit()), -- ok = couch_db:close(TargetDb), -- -- etap:diag("Triggering replication"), -- replicate(Source, Target), -- etap:diag("Replication finished, comparing source and target databases"), -- compare_dbs(SourceDb, TargetDb), -- -- etap:diag("Updating source database docs"), -- update_db_docs(couch_db:name(SourceDb), target_revs_limit() + 2), -- -- etap:diag("Triggering replication again"), -- replicate(Source, Target), -- etap:diag("Replication finished, comparing source and target databases"), -- compare_dbs(SourceDb, TargetDb), -- -- etap:diag("Deleting databases"), -- delete_db(TargetDb), -- delete_db(SourceDb), -- ok = timer:sleep(1000) -- end, -- Pairs), -- -- couch_server_sup:stop(), -- ok. -- -- --populate_db(Db) -> -- AttData = crypto:rand_bytes(6000), -- Doc1 = #doc{ -- id = <<"doc1">>, -- atts = [ -- #att{ -- name = <<"doc1_att1">>, -- type = <<"application/foobar">>, -- att_len = byte_size(AttData), -- data = AttData -- } -- ] -- }, -- {ok, _} = couch_db:update_doc(Db, Doc1, []). -- -- --update_db_docs(DbName, Times) -> -- {ok, Db} = couch_db:open_int(DbName, []), -- {ok, _, _} = couch_db:enum_docs( -- Db, -- fun(FDI, _, Acc) -> db_fold_fun(FDI, Acc) end, -- {DbName, Times}, -- []), -- ok = couch_db:close(Db). -- -- --db_fold_fun(FullDocInfo, {DbName, Times}) -> -- {ok, Db} = couch_db:open_int(DbName, []), -- {ok, Doc} = couch_db:open_doc(Db, FullDocInfo), -- lists:foldl( -- fun(_, {Pos, RevId}) -> -- {ok, Db2} = couch_db:reopen(Db), -- NewDocVersion = Doc#doc{ -- revs = {Pos, [RevId]}, -- body = {[{<<"value">>, base64:encode(crypto:rand_bytes(100))}]} -- }, -- {ok, NewRev} = couch_db:update_doc(Db2, NewDocVersion, []), -- NewRev -- end, -- {element(1, Doc#doc.revs), hd(element(2, Doc#doc.revs))}, -- lists:seq(1, Times)), -- ok = couch_db:close(Db), -- {ok, {DbName, Times}}. -- -- --compare_dbs(Source, Target) -> -- {ok, SourceDb} = couch_db:open_int(couch_db:name(Source), []), -- {ok, TargetDb} = couch_db:open_int(couch_db:name(Target), []), -- -- Fun = fun(FullDocInfo, _, Acc) -> -- {ok, DocSource} = couch_db:open_doc( -- SourceDb, FullDocInfo, [conflicts, deleted_conflicts]), -- Id = DocSource#doc.id, -- -- etap:diag("Verifying document " ++ ?b2l(Id)), -- -- {ok, DocTarget} = couch_db:open_doc( -- TargetDb, Id, [conflicts, deleted_conflicts]), -- etap:is(DocTarget#doc.body, DocSource#doc.body, -- "Same body in source and target databases"), -- -- etap:is( -- couch_doc:to_json_obj(DocTarget, []), -- couch_doc:to_json_obj(DocSource, []), -- "Same doc body in source and target databases"), -- -- #doc{atts = SourceAtts} = DocSource, -- #doc{atts = TargetAtts} = DocTarget, -- etap:is( -- lists:sort([N || #att{name = N} <- SourceAtts]), -- lists:sort([N || #att{name = N} <- TargetAtts]), -- "Document has same number (and names) of attachments in " -- "source and target databases"), -- -- lists:foreach( -- fun(#att{name = AttName} = Att) -> -- etap:diag("Verifying attachment " ++ ?b2l(AttName)), -- -- {ok, AttTarget} = find_att(TargetAtts, AttName), -- SourceMd5 = att_md5(Att), -- TargetMd5 = att_md5(AttTarget), -- case AttName of -- <<"att1">> -> -- etap:is(Att#att.encoding, gzip, -- "Attachment is gzip encoded in source database"), -- etap:is(AttTarget#att.encoding, gzip, -- "Attachment is gzip encoded in target database"), -- DecSourceMd5 = att_decoded_md5(Att), -- DecTargetMd5 = att_decoded_md5(AttTarget), -- etap:is(DecTargetMd5, DecSourceMd5, -- "Same identity content in source and target databases"); -- _ -> -- etap:is(Att#att.encoding, identity, -- "Attachment is not encoded in source database"), -- etap:is(AttTarget#att.encoding, identity, -- "Attachment is not encoded in target database") -- end, -- etap:is(TargetMd5, SourceMd5, -- "Same content in source and target databases"), -- etap:is(is_integer(Att#att.disk_len), true, -- "#att.disk_len is an integer in source database"), -- etap:is(is_integer(Att#att.att_len), true, -- "#att.att_len is an integer in source database"), -- etap:is(is_integer(AttTarget#att.disk_len), true, -- "#att.disk_len is an integer in target database"), -- etap:is(is_integer(AttTarget#att.att_len), true, -- "#att.att_len is an integer in target database"), -- etap:is(Att#att.disk_len, AttTarget#att.disk_len, -- "Same identity length in source and target databases"), -- etap:is(Att#att.att_len, AttTarget#att.att_len, -- "Same encoded length in source and target databases"), -- etap:is(Att#att.type, AttTarget#att.type, -- "Same type in source and target databases"), -- etap:is(Att#att.md5, SourceMd5, "Correct MD5 in source database"), -- etap:is(AttTarget#att.md5, SourceMd5, "Correct MD5 in target database") -- end, -- SourceAtts), -- -- {ok, Acc} -- end, -- -- {ok, _, _} = couch_db:enum_docs(SourceDb, Fun, [], []), -- ok = couch_db:close(SourceDb), -- ok = couch_db:close(TargetDb). -- -- --find_att([], _Name) -> -- nil; --find_att([#att{name = Name} = Att | _], Name) -> -- {ok, Att}; --find_att([_ | Rest], Name) -> -- find_att(Rest, Name). -- -- --att_md5(Att) -> -- Md50 = couch_doc:att_foldl( -- Att, -- fun(Chunk, Acc) -> couch_util:md5_update(Acc, Chunk) end, -- couch_util:md5_init()), -- couch_util:md5_final(Md50). -- --att_decoded_md5(Att) -> -- Md50 = couch_doc:att_foldl_decode( -- Att, -- fun(Chunk, Acc) -> couch_util:md5_update(Acc, Chunk) end, -- couch_util:md5_init()), -- couch_util:md5_final(Md50). -- -- --db_url(DbName) -> -- iolist_to_binary([ -- "http://", couch_config:get("httpd", "bind_address", "127.0.0.1"), -- ":", integer_to_list(mochiweb_socket_server:get(couch_httpd, port)), -- "/", DbName -- ]). -- -- --create_db(DbName) -> -- couch_db:create( -- DbName, -- [{user_ctx, #user_ctx{roles = [<<"_admin">>]}}, overwrite]). -- -- --delete_db(Db) -> -- ok = couch_server:delete( -- couch_db:name(Db), [{user_ctx, #user_ctx{roles = [<<"_admin">>]}}]). -- -- --replicate({remote, Db}, Target) -> -- replicate(db_url(Db), Target); -- --replicate(Source, {remote, Db}) -> -- replicate(Source, db_url(Db)); -- --replicate(Source, Target) -> -- RepObject = {[ -- {<<"source">>, Source}, -- {<<"target">>, Target} -- ]}, -- {ok, Rep} = couch_replicator_utils:parse_rep_doc( -- RepObject, #user_ctx{roles = [<<"_admin">>]}), -- {ok, Pid} = couch_replicator:async_replicate(Rep), -- MonRef = erlang:monitor(process, Pid), -- receive -- {'DOWN', MonRef, process, Pid, Reason} -> -- etap:is(Reason, normal, "Replication finished successfully") -- after 300000 -> -- etap:bail("Timeout waiting for replication to finish") -- end. -diff --git a/src/couch_replicator/test/07-use-checkpoints.t b/src/couch_replicator/test/07-use-checkpoints.t -deleted file mode 100755 -index a3295c7..0000000 ---- a/src/couch_replicator/test/07-use-checkpoints.t -+++ /dev/null -@@ -1,273 +0,0 @@ --#!/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. -- --% Verify that compacting databases that are being used as the source or --% target of a replication doesn't affect the replication and that the --% replication doesn't hold their reference counters forever. -- ---define(b2l(B), binary_to_list(B)). -- ---record(user_ctx, { -- name = null, -- roles = [], -- handler --}). -- ---record(doc, { -- id = <<"">>, -- revs = {0, []}, -- body = {[]}, -- atts = [], -- deleted = false, -- meta = [] --}). -- ---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, -- updater_fd, -- fd_ref_counter, -- header = nil, -- 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 = [], -- options = [], -- compression, -- before_doc_update, -- after_doc_read --}). -- ---record(rep, { -- id, -- source, -- target, -- options, -- user_ctx, -- doc_id --}). -- -- --source_db_name() -> <<"couch_test_rep_db_a">>. --target_db_name() -> <<"couch_test_rep_db_b">>. -- -- --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()), -- ibrowse:start(), -- -- % order matters -- test_use_checkpoints(false), -- test_use_checkpoints(true), -- -- couch_server_sup:stop(), -- ok. -- -- --test_use_checkpoints(UseCheckpoints) -> -- Pairs = [ -- {source_db_name(), target_db_name()}, -- {{remote, source_db_name()}, target_db_name()}, -- {source_db_name(), {remote, target_db_name()}}, -- {{remote, source_db_name()}, {remote, (target_db_name())}} -- ], -- -- ListenerFun = case UseCheckpoints of -- false -> -- fun({finished, _, {CheckpointHistory}}) -> -- etap:is(CheckpointHistory, -- [{<<"use_checkpoints">>,false}], -- "No checkpoints found"); -- (_) -> -- ok -- end; -- true -> -- fun({finished, _, {CheckpointHistory}}) -> -- SessionId = lists:keyfind( -- <<"session_id">>, 1, CheckpointHistory), -- case SessionId of -- false -> -- OtpRel = erlang:system_info(otp_release), -- case OtpRel >= "R14B01" orelse OtpRel < "R14B03" of -- false -> -- etap:bail("Checkpoint expected, but not found"); -- true -> -- etap:ok(true, -- " Checkpoint expected, but wan't found." -- " Your Erlang " ++ OtpRel ++ " version is" -- " affected to OTP-9167 issue which causes" -- " failure of this test. Try to upgrade Erlang" -- " and if this failure repeats file the bug.") -- end; -- _ -> -- etap:ok(true, "There's a checkpoint") -- end; -- (_) -> -- ok -- end -- end, -- {ok, Listener} = couch_replicator_notifier:start_link(ListenerFun), -- -- lists:foreach( -- fun({Source, Target}) -> -- {ok, SourceDb} = create_db(source_db_name()), -- etap:diag("Populating source database"), -- populate_db(SourceDb, 100), -- ok = couch_db:close(SourceDb), -- -- etap:diag("Creating target database"), -- {ok, TargetDb} = create_db(target_db_name()), -- ok = couch_db:close(TargetDb), -- -- etap:diag("Setup replicator notifier listener"), -- -- etap:diag("Triggering replication"), -- replicate(Source, Target, UseCheckpoints), -- -- etap:diag("Replication finished, comparing source and target databases"), -- compare_dbs(SourceDb, TargetDb), -- -- etap:diag("Deleting databases"), -- delete_db(TargetDb), -- delete_db(SourceDb), -- -- ok = timer:sleep(1000) -- end, -- Pairs), -- -- couch_replicator_notifier:stop(Listener). -- -- --populate_db(Db, DocCount) -> -- Docs = lists:foldl( -- fun(DocIdCounter, Acc) -> -- Id = iolist_to_binary(["doc", integer_to_list(DocIdCounter)]), -- Value = iolist_to_binary(["val", integer_to_list(DocIdCounter)]), -- Doc = #doc{ -- id = Id, -- body = {[ {<<"value">>, Value} ]} -- }, -- [Doc | Acc] -- end, -- [], lists:seq(1, DocCount)), -- {ok, _} = couch_db:update_docs(Db, Docs, []). -- -- --compare_dbs(#db{name = SourceName}, #db{name = TargetName}) -> -- {ok, SourceDb} = couch_db:open_int(SourceName, []), -- {ok, TargetDb} = couch_db:open_int(TargetName, []), -- Fun = fun(FullDocInfo, _, Acc) -> -- {ok, Doc} = couch_db:open_doc(SourceDb, FullDocInfo), -- {Props} = DocJson = couch_doc:to_json_obj(Doc, [attachments]), -- DocId = couch_util:get_value(<<"_id">>, Props), -- DocTarget = case couch_db:open_doc(TargetDb, DocId) of -- {ok, DocT} -> -- DocT; -- Error -> -- etap:bail("Error opening document '" ++ ?b2l(DocId) ++ -- "' from target: " ++ couch_util:to_list(Error)) -- end, -- DocTargetJson = couch_doc:to_json_obj(DocTarget, [attachments]), -- case DocTargetJson of -- DocJson -> -- ok; -- _ -> -- etap:bail("Content from document '" ++ ?b2l(DocId) ++ -- "' differs in target database") -- end, -- {ok, Acc} -- end, -- {ok, _, _} = couch_db:enum_docs(SourceDb, Fun, [], []), -- etap:diag("Target database has the same documents as the source database"), -- ok = couch_db:close(SourceDb), -- ok = couch_db:close(TargetDb). -- -- --db_url(DbName) -> -- iolist_to_binary([ -- "http://", couch_config:get("httpd", "bind_address", "127.0.0.1"), -- ":", integer_to_list(mochiweb_socket_server:get(couch_httpd, port)), -- "/", DbName -- ]). -- -- --create_db(DbName) -> -- {ok, Db} = couch_db:create( -- DbName, -- [{user_ctx, #user_ctx{roles = [<<"_admin">>]}}, overwrite]), -- couch_db:close(Db), -- {ok, Db}. -- -- --delete_db(#db{name = DbName, main_pid = Pid}) -> -- ok = couch_server:delete( -- DbName, [{user_ctx, #user_ctx{roles = [<<"_admin">>]}}]), -- MonRef = erlang:monitor(process, Pid), -- receive -- {'DOWN', MonRef, process, Pid, _Reason} -> -- ok -- after 30000 -> -- etap:bail("Timeout deleting database") -- end. -- -- --replicate({remote, Db}, Target, UseCheckpoints) -> -- replicate(db_url(Db), Target, UseCheckpoints); -- --replicate(Source, {remote, Db}, UseCheckpoints) -> -- replicate(Source, db_url(Db), UseCheckpoints); -- --replicate(Source, Target, UseCheckpoints) -> -- RepObject = {[ -- {<<"source">>, Source}, -- {<<"target">>, Target}, -- {<<"use_checkpoints">>, UseCheckpoints} -- ]}, -- {ok, Rep} = couch_replicator_utils:parse_rep_doc( -- RepObject, #user_ctx{roles = [<<"_admin">>]}), -- {ok, Pid} = couch_replicator:async_replicate(Rep), -- MonRef = erlang:monitor(process, Pid), -- receive -- {'DOWN', MonRef, process, Pid, Reason} -> -- etap:is(Reason, normal, "Replication finished successfully") -- after 300000 -> -- etap:bail("Timeout waiting for replication to finish") -- end. -diff --git a/src/couch_replicator/test/couch_replicator_compact_tests.erl b/src/couch_replicator/test/couch_replicator_compact_tests.erl -new file mode 100644 -index 0000000..05b368e ---- /dev/null -+++ b/src/couch_replicator/test/couch_replicator_compact_tests.erl -@@ -0,0 +1,448 @@ -+% Licensed under the Apache License, Version 2.0 (the "License"); you may not -+% use this file except in compliance with the License. You may obtain a copy of -+% the License at -+% -+% http://www.apache.org/licenses/LICENSE-2.0 -+% -+% Unless required by applicable law or agreed to in writing, software -+% distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -+% WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -+% License for the specific language governing permissions and limitations under -+% the License. -+ -+-module(couch_replicator_compact_tests). -+ -+-include("couch_eunit.hrl"). -+-include_lib("couchdb/couch_db.hrl"). -+-include_lib("couch_replicator/src/couch_replicator.hrl"). -+ -+-define(ADMIN_ROLE, #user_ctx{roles=[<<"_admin">>]}). -+-define(ADMIN_USER, {user_ctx, ?ADMIN_ROLE}). -+-define(ATTFILE, filename:join([?FIXTURESDIR, "logo.png"])). -+-define(DELAY, 100). -+-define(TIMEOUT, 30000). -+-define(TIMEOUT_STOP, 1000). -+-define(TIMEOUT_WRITER, 3000). -+-define(TIMEOUT_EUNIT, ?TIMEOUT div 1000 + 5). -+ -+setup() -> -+ DbName = ?tempdb(), -+ {ok, Db} = couch_db:create(DbName, [?ADMIN_USER]), -+ ok = couch_db:close(Db), -+ DbName. -+ -+setup(local) -> -+ setup(); -+setup(remote) -> -+ {remote, setup()}; -+setup({A, B}) -> -+ {ok, _} = couch_server_sup:start_link(?CONFIG_CHAIN), -+ Source = setup(A), -+ Target = setup(B), -+ {Source, Target}. -+ -+teardown({remote, DbName}) -> -+ teardown(DbName); -+teardown(DbName) -> -+ ok = couch_server:delete(DbName, [?ADMIN_USER]), -+ ok. -+ -+teardown(_, {Source, Target}) -> -+ teardown(Source), -+ teardown(Target), -+ -+ Pid = whereis(couch_server_sup), -+ erlang:monitor(process, Pid), -+ couch_server_sup:stop(), -+ receive -+ {'DOWN', _, _, Pid, _} -> -+ ok -+ after ?TIMEOUT_STOP -> -+ throw({timeout, server_stop}) -+ end. -+ -+ -+compact_test_() -> -+ Pairs = [{local, local}, {local, remote}, -+ {remote, local}, {remote, remote}], -+ { -+ "Compaction during replication tests", -+ { -+ foreachx, -+ fun setup/1, fun teardown/2, -+ [{Pair, fun should_populate_replicate_compact/2} -+ || Pair <- Pairs] -+ } -+ }. -+ -+ -+should_populate_replicate_compact({From, To}, {Source, Target}) -> -+ {ok, RepPid, RepId} = replicate(Source, Target), -+ {lists:flatten(io_lib:format("~p -> ~p", [From, To])), -+ {inorder, [ -+ should_run_replication(RepPid, RepId, Source, Target), -+ should_all_processes_be_alive(RepPid, Source, Target), -+ should_populate_and_compact(RepPid, Source, Target, 50, 5), -+ should_wait_target_in_sync(Source, Target), -+ should_ensure_replication_still_running(RepPid, RepId, Source, Target), -+ should_cancel_replication(RepId, RepPid), -+ should_compare_databases(Source, Target) -+ ]}}. -+ -+should_all_processes_be_alive(RepPid, Source, Target) -> -+ ?_test(begin -+ {ok, SourceDb} = reopen_db(Source), -+ {ok, TargetDb} = reopen_db(Target), -+ ?assert(is_process_alive(RepPid)), -+ ?assert(is_process_alive(SourceDb#db.main_pid)), -+ ?assert(is_process_alive(TargetDb#db.main_pid)) -+ end). -+ -+should_run_replication(RepPid, RepId, Source, Target) -> -+ ?_test(check_active_tasks(RepPid, RepId, Source, Target)). -+ -+should_ensure_replication_still_running(RepPid, RepId, Source, Target) -> -+ ?_test(check_active_tasks(RepPid, RepId, Source, Target)). -+ -+check_active_tasks(RepPid, {BaseId, Ext} = _RepId, Src, Tgt) -> -+ Source = case Src of -+ {remote, NameSrc} -> -+ <<(db_url(NameSrc))/binary, $/>>; -+ _ -> -+ Src -+ end, -+ Target = case Tgt of -+ {remote, NameTgt} -> -+ <<(db_url(NameTgt))/binary, $/>>; -+ _ -> -+ Tgt -+ end, -+ FullRepId = ?l2b(BaseId ++ Ext), -+ Pid = ?l2b(pid_to_list(RepPid)), -+ [RepTask] = couch_task_status:all(), -+ ?assertEqual(Pid, couch_util:get_value(pid, RepTask)), -+ ?assertEqual(FullRepId, couch_util:get_value(replication_id, RepTask)), -+ ?assertEqual(true, couch_util:get_value(continuous, RepTask)), -+ ?assertEqual(Source, couch_util:get_value(source, RepTask)), -+ ?assertEqual(Target, couch_util:get_value(target, RepTask)), -+ ?assert(is_integer(couch_util:get_value(docs_read, RepTask))), -+ ?assert(is_integer(couch_util:get_value(docs_written, RepTask))), -+ ?assert(is_integer(couch_util:get_value(doc_write_failures, RepTask))), -+ ?assert(is_integer(couch_util:get_value(revisions_checked, RepTask))), -+ ?assert(is_integer(couch_util:get_value(missing_revisions_found, RepTask))), -+ ?assert(is_integer(couch_util:get_value(checkpointed_source_seq, RepTask))), -+ ?assert(is_integer(couch_util:get_value(source_seq, RepTask))), -+ Progress = couch_util:get_value(progress, RepTask), -+ ?assert(is_integer(Progress)), -+ ?assert(Progress =< 100). -+ -+should_cancel_replication(RepId, RepPid) -> -+ ?_assertNot(begin -+ {ok, _} = couch_replicator:cancel_replication(RepId), -+ is_process_alive(RepPid) -+ end). -+ -+should_populate_and_compact(RepPid, Source, Target, BatchSize, Rounds) -> -+ {timeout, ?TIMEOUT_EUNIT, ?_test(begin -+ {ok, SourceDb0} = reopen_db(Source), -+ Writer = spawn_writer(SourceDb0), -+ lists:foreach( -+ fun(N) -> -+ {ok, SourceDb} = reopen_db(Source), -+ {ok, TargetDb} = reopen_db(Target), -+ pause_writer(Writer), -+ -+ compact_db("source", SourceDb), -+ ?assert(is_process_alive(RepPid)), -+ ?assert(is_process_alive(SourceDb#db.main_pid)), -+ check_ref_counter("source", SourceDb), -+ -+ compact_db("target", TargetDb), -+ ?assert(is_process_alive(RepPid)), -+ ?assert(is_process_alive(TargetDb#db.main_pid)), -+ check_ref_counter("target", TargetDb), -+ -+ {ok, SourceDb2} = reopen_db(SourceDb), -+ {ok, TargetDb2} = reopen_db(TargetDb), -+ -+ resume_writer(Writer), -+ wait_writer(Writer, BatchSize * N), -+ -+ compact_db("source", SourceDb2), -+ ?assert(is_process_alive(RepPid)), -+ ?assert(is_process_alive(SourceDb2#db.main_pid)), -+ pause_writer(Writer), -+ check_ref_counter("source", SourceDb2), -+ resume_writer(Writer), -+ -+ compact_db("target", TargetDb2), -+ ?assert(is_process_alive(RepPid)), -+ ?assert(is_process_alive(TargetDb2#db.main_pid)), -+ pause_writer(Writer), -+ check_ref_counter("target", TargetDb2), -+ resume_writer(Writer) -+ end, lists:seq(1, Rounds)), -+ stop_writer(Writer) -+ end)}. -+ -+should_wait_target_in_sync({remote, Source}, Target) -> -+ should_wait_target_in_sync(Source, Target); -+should_wait_target_in_sync(Source, {remote, Target}) -> -+ should_wait_target_in_sync(Source, Target); -+should_wait_target_in_sync(Source, Target) -> -+ {timeout, ?TIMEOUT_EUNIT, ?_assert(begin -+ {ok, SourceDb} = couch_db:open_int(Source, []), -+ {ok, SourceInfo} = couch_db:get_db_info(SourceDb), -+ ok = couch_db:close(SourceDb), -+ SourceDocCount = couch_util:get_value(doc_count, SourceInfo), -+ wait_target_in_sync_loop(SourceDocCount, Target, 300) -+ end)}. -+ -+wait_target_in_sync_loop(_DocCount, _TargetName, 0) -> -+ erlang:error( -+ {assertion_failed, -+ [{module, ?MODULE}, {line, ?LINE}, -+ {reason, "Could not get source and target databases in sync"}]}); -+wait_target_in_sync_loop(DocCount, {remote, TargetName}, RetriesLeft) -> -+ wait_target_in_sync_loop(DocCount, TargetName, RetriesLeft); -+wait_target_in_sync_loop(DocCount, TargetName, RetriesLeft) -> -+ {ok, Target} = couch_db:open_int(TargetName, []), -+ {ok, TargetInfo} = couch_db:get_db_info(Target), -+ ok = couch_db:close(Target), -+ TargetDocCount = couch_util:get_value(doc_count, TargetInfo), -+ case TargetDocCount == DocCount of -+ true -> -+ true; -+ false -> -+ ok = timer:sleep(?DELAY), -+ wait_target_in_sync_loop(DocCount, TargetName, RetriesLeft - 1) -+ end. -+ -+should_compare_databases({remote, Source}, Target) -> -+ should_compare_databases(Source, Target); -+should_compare_databases(Source, {remote, Target}) -> -+ should_compare_databases(Source, Target); -+should_compare_databases(Source, Target) -> -+ {timeout, 35, ?_test(begin -+ {ok, SourceDb} = couch_db:open_int(Source, []), -+ {ok, TargetDb} = couch_db:open_int(Target, []), -+ Fun = fun(FullDocInfo, _, Acc) -> -+ {ok, Doc} = couch_db:open_doc(SourceDb, FullDocInfo), -+ {Props} = DocJson = couch_doc:to_json_obj(Doc, [attachments]), -+ DocId = couch_util:get_value(<<"_id">>, Props), -+ DocTarget = case couch_db:open_doc(TargetDb, DocId) of -+ {ok, DocT} -> -+ DocT; -+ Error -> -+ erlang:error( -+ {assertion_failed, -+ [{module, ?MODULE}, {line, ?LINE}, -+ {reason, lists:concat(["Error opening document '", -+ ?b2l(DocId), "' from target: ", -+ couch_util:to_list(Error)])}]}) -+ end, -+ DocTargetJson = couch_doc:to_json_obj(DocTarget, [attachments]), -+ ?assertEqual(DocJson, DocTargetJson), -+ {ok, Acc} -+ end, -+ {ok, _, _} = couch_db:enum_docs(SourceDb, Fun, [], []), -+ ok = couch_db:close(SourceDb), -+ ok = couch_db:close(TargetDb) -+ end)}. -+ -+ -+reopen_db({remote, Db}) -> -+ reopen_db(Db); -+reopen_db(#db{name=DbName}) -> -+ reopen_db(DbName); -+reopen_db(DbName) -> -+ {ok, Db} = couch_db:open_int(DbName, []), -+ ok = couch_db:close(Db), -+ {ok, Db}. -+ -+compact_db(Type, #db{name = Name}) -> -+ {ok, Db} = couch_db:open_int(Name, []), -+ {ok, CompactPid} = couch_db:start_compact(Db), -+ MonRef = erlang:monitor(process, CompactPid), -+ receive -+ {'DOWN', MonRef, process, CompactPid, normal} -> -+ ok; -+ {'DOWN', MonRef, process, CompactPid, Reason} -> -+ erlang:error( -+ {assertion_failed, -+ [{module, ?MODULE}, {line, ?LINE}, -+ {reason, -+ lists:concat(["Error compacting ", Type, " database ", -+ ?b2l(Name), ": ", -+ couch_util:to_list(Reason)])}]}) -+ after ?TIMEOUT -> -+ erlang:error( -+ {assertion_failed, -+ [{module, ?MODULE}, {line, ?LINE}, -+ {reason, lists:concat(["Compaction for ", Type, " database ", -+ ?b2l(Name), " didn't finish"])}]}) -+ end, -+ ok = couch_db:close(Db). -+ -+check_ref_counter(Type, #db{name = Name, fd_ref_counter = OldRefCounter}) -> -+ MonRef = erlang:monitor(process, OldRefCounter), -+ receive -+ {'DOWN', MonRef, process, OldRefCounter, _} -> -+ ok -+ after ?TIMEOUT -> -+ erlang:error( -+ {assertion_failed, -+ [{module, ?MODULE}, {line, ?LINE}, -+ {reason, lists:concat(["Old ", Type, -+ " database ref counter didn't" -+ " terminate"])}]}) -+ end, -+ {ok, #db{fd_ref_counter = NewRefCounter} = Db} = couch_db:open_int(Name, []), -+ ok = couch_db:close(Db), -+ ?assertNotEqual(OldRefCounter, NewRefCounter). -+ -+db_url(DbName) -> -+ iolist_to_binary([ -+ "http://", couch_config:get("httpd", "bind_address", "127.0.0.1"), -+ ":", integer_to_list(mochiweb_socket_server:get(couch_httpd, port)), -+ "/", DbName -+ ]). -+ -+replicate({remote, Db}, Target) -> -+ replicate(db_url(Db), Target); -+ -+replicate(Source, {remote, Db}) -> -+ replicate(Source, db_url(Db)); -+ -+replicate(Source, Target) -> -+ RepObject = {[ -+ {<<"source">>, Source}, -+ {<<"target">>, Target}, -+ {<<"continuous">>, true} -+ ]}, -+ {ok, Rep} = couch_replicator_utils:parse_rep_doc(RepObject, ?ADMIN_ROLE), -+ {ok, Pid} = couch_replicator:async_replicate(Rep), -+ {ok, Pid, Rep#rep.id}. -+ -+ -+wait_writer(Pid, NumDocs) -> -+ case get_writer_num_docs_written(Pid) of -+ N when N >= NumDocs -> -+ ok; -+ _ -> -+ wait_writer(Pid, NumDocs) -+ end. -+ -+spawn_writer(Db) -> -+ Parent = self(), -+ Pid = spawn(fun() -> writer_loop(Db, Parent, 0) end), -+ Pid. -+ -+ -+pause_writer(Pid) -> -+ Ref = make_ref(), -+ Pid ! {pause, Ref}, -+ receive -+ {paused, Ref} -> -+ ok -+ after ?TIMEOUT_WRITER -> -+ erlang:error({assertion_failed, -+ [{module, ?MODULE}, -+ {line, ?LINE}, -+ {reason, "Failed to pause source database writer"}]}) -+ end. -+ -+resume_writer(Pid) -> -+ Ref = make_ref(), -+ Pid ! {continue, Ref}, -+ receive -+ {ok, Ref} -> -+ ok -+ after ?TIMEOUT_WRITER -> -+ erlang:error({assertion_failed, -+ [{module, ?MODULE}, -+ {line, ?LINE}, -+ {reason, "Failed to pause source database writer"}]}) -+ end. -+ -+get_writer_num_docs_written(Pid) -> -+ Ref = make_ref(), -+ Pid ! {get_count, Ref}, -+ receive -+ {count, Ref, Count} -> -+ Count -+ after ?TIMEOUT_WRITER -> -+ erlang:error({assertion_failed, -+ [{module, ?MODULE}, -+ {line, ?LINE}, -+ {reason, "Timeout getting number of documents written" -+ " from source database writer"}]}) -+ end. -+ -+stop_writer(Pid) -> -+ Ref = make_ref(), -+ Pid ! {stop, Ref}, -+ receive -+ {stopped, Ref, DocsWritten} -> -+ MonRef = erlang:monitor(process, Pid), -+ receive -+ {'DOWN', MonRef, process, Pid, _Reason} -> -+ DocsWritten -+ after ?TIMEOUT -> -+ erlang:error({assertion_failed, -+ [{module, ?MODULE}, -+ {line, ?LINE}, -+ {reason, "Timeout stopping source database writer"}]}) -+ end -+ after ?TIMEOUT_WRITER -> -+ erlang:error({assertion_failed, -+ [{module, ?MODULE}, -+ {line, ?LINE}, -+ {reason, "Timeout stopping source database writer"}]}) -+ end. -+ -+writer_loop(#db{name = DbName}, Parent, Counter) -> -+ {ok, Data} = file:read_file(?ATTFILE), -+ maybe_pause(Parent, Counter), -+ Doc = couch_doc:from_json_obj({[ -+ {<<"_id">>, ?l2b(integer_to_list(Counter + 1))}, -+ {<<"value">>, Counter + 1}, -+ {<<"_attachments">>, {[ -+ {<<"icon1.png">>, {[ -+ {<<"data">>, base64:encode(Data)}, -+ {<<"content_type">>, <<"image/png">>} -+ ]}}, -+ {<<"icon2.png">>, {[ -+ {<<"data">>, base64:encode(iolist_to_binary([Data, Data]))}, -+ {<<"content_type">>, <<"image/png">>} -+ ]}} -+ ]}} -+ ]}), -+ maybe_pause(Parent, Counter), -+ {ok, Db} = couch_db:open_int(DbName, []), -+ {ok, _} = couch_db:update_doc(Db, Doc, []), -+ ok = couch_db:close(Db), -+ receive -+ {get_count, Ref} -> -+ Parent ! {count, Ref, Counter + 1}, -+ writer_loop(Db, Parent, Counter + 1); -+ {stop, Ref} -> -+ Parent ! {stopped, Ref, Counter + 1} -+ after 0 -> -+ timer:sleep(?DELAY), -+ writer_loop(Db, Parent, Counter + 1) -+ end. -+ -+maybe_pause(Parent, Counter) -> -+ receive -+ {get_count, Ref} -> -+ Parent ! {count, Ref, Counter}; -+ {pause, Ref} -> -+ Parent ! {paused, Ref}, -+ receive -+ {continue, Ref2} -> -+ Parent ! {ok, Ref2} -+ end -+ after 0 -> -+ ok -+ end. -diff --git a/src/couch_replicator/test/couch_replicator_httpc_pool_tests.erl b/src/couch_replicator/test/couch_replicator_httpc_pool_tests.erl -new file mode 100644 -index 0000000..88534ed ---- /dev/null -+++ b/src/couch_replicator/test/couch_replicator_httpc_pool_tests.erl -@@ -0,0 +1,189 @@ -+% Licensed under the Apache License, Version 2.0 (the "License"); you may not -+% use this file except in compliance with the License. You may obtain a copy of -+% the License at -+% -+% http://www.apache.org/licenses/LICENSE-2.0 -+% -+% Unless required by applicable law or agreed to in writing, software -+% distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -+% WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -+% License for the specific language governing permissions and limitations under -+% the License. -+ -+-module(couch_replicator_httpc_pool_tests). -+ -+-include("couch_eunit.hrl"). -+-include_lib("couchdb/couch_db.hrl"). -+ -+-define(ADMIN_USER, {user_ctx, #user_ctx{roles=[<<"_admin">>]}}). -+-define(TIMEOUT, 1000). -+ -+ -+start() -> -+ {ok, Pid} = couch_server_sup:start_link(?CONFIG_CHAIN), -+ Pid. -+ -+stop(Pid) -> -+ erlang:monitor(process, Pid), -+ couch_server_sup:stop(), -+ receive -+ {'DOWN', _, _, Pid, _} -> -+ ok -+ after ?TIMEOUT -> -+ throw({timeout, server_stop}) -+ end. -+ -+setup() -> -+ spawn_pool(). -+ -+teardown(Pool) -> -+ stop_pool(Pool). -+ -+ -+httpc_pool_test_() -> -+ { -+ "httpc pool tests", -+ { -+ setup, -+ fun start/0, fun stop/1, -+ { -+ foreach, -+ fun setup/0, fun teardown/1, -+ [ -+ fun should_block_new_clients_when_full/1, -+ fun should_replace_worker_on_death/1 -+ ] -+ } -+ } -+ }. -+ -+ -+should_block_new_clients_when_full(Pool) -> -+ ?_test(begin -+ Client1 = spawn_client(Pool), -+ Client2 = spawn_client(Pool), -+ Client3 = spawn_client(Pool), -+ -+ ?assertEqual(ok, ping_client(Client1)), -+ ?assertEqual(ok, ping_client(Client2)), -+ ?assertEqual(ok, ping_client(Client3)), -+ -+ Worker1 = get_client_worker(Client1, "1"), -+ Worker2 = get_client_worker(Client2, "2"), -+ Worker3 = get_client_worker(Client3, "3"), -+ -+ ?assert(is_process_alive(Worker1)), -+ ?assert(is_process_alive(Worker2)), -+ ?assert(is_process_alive(Worker3)), -+ -+ ?assertNotEqual(Worker1, Worker2), -+ ?assertNotEqual(Worker2, Worker3), -+ ?assertNotEqual(Worker3, Worker1), -+ -+ Client4 = spawn_client(Pool), -+ ?assertEqual(timeout, ping_client(Client4)), -+ -+ ?assertEqual(ok, stop_client(Client1)), -+ ?assertEqual(ok, ping_client(Client4)), -+ -+ Worker4 = get_client_worker(Client4, "4"), -+ ?assertEqual(Worker1, Worker4), -+ -+ lists:foreach( -+ fun(C) -> -+ ?assertEqual(ok, stop_client(C)) -+ end, [Client2, Client3, Client4]) -+ end). -+ -+should_replace_worker_on_death(Pool) -> -+ ?_test(begin -+ Client1 = spawn_client(Pool), -+ ?assertEqual(ok, ping_client(Client1)), -+ Worker1 = get_client_worker(Client1, "1"), -+ ?assert(is_process_alive(Worker1)), -+ -+ ?assertEqual(ok, kill_client_worker(Client1)), -+ ?assertNot(is_process_alive(Worker1)), -+ ?assertEqual(ok, stop_client(Client1)), -+ -+ Client2 = spawn_client(Pool), -+ ?assertEqual(ok, ping_client(Client2)), -+ Worker2 = get_client_worker(Client2, "2"), -+ ?assert(is_process_alive(Worker2)), -+ -+ ?assertNotEqual(Worker1, Worker2), -+ ?assertEqual(ok, stop_client(Client2)) -+ end). -+ -+ -+spawn_client(Pool) -> -+ Parent = self(), -+ Ref = make_ref(), -+ Pid = spawn(fun() -> -+ {ok, Worker} = couch_replicator_httpc_pool:get_worker(Pool), -+ loop(Parent, Ref, Worker, Pool) -+ end), -+ {Pid, Ref}. -+ -+ping_client({Pid, Ref}) -> -+ Pid ! ping, -+ receive -+ {pong, Ref} -> -+ ok -+ after ?TIMEOUT -> -+ timeout -+ end. -+ -+get_client_worker({Pid, Ref}, ClientName) -> -+ Pid ! get_worker, -+ receive -+ {worker, Ref, Worker} -> -+ Worker -+ after ?TIMEOUT -> -+ erlang:error( -+ {assertion_failed, -+ [{module, ?MODULE}, {line, ?LINE}, -+ {reason, "Timeout getting client " ++ ClientName ++ " worker"}]}) -+ end. -+ -+stop_client({Pid, Ref}) -> -+ Pid ! stop, -+ receive -+ {stop, Ref} -> -+ ok -+ after ?TIMEOUT -> -+ timeout -+ end. -+ -+kill_client_worker({Pid, Ref}) -> -+ Pid ! get_worker, -+ receive -+ {worker, Ref, Worker} -> -+ exit(Worker, kill), -+ ok -+ after ?TIMEOUT -> -+ timeout -+ end. -+ -+loop(Parent, Ref, Worker, Pool) -> -+ receive -+ ping -> -+ Parent ! {pong, Ref}, -+ loop(Parent, Ref, Worker, Pool); -+ get_worker -> -+ Parent ! {worker, Ref, Worker}, -+ loop(Parent, Ref, Worker, Pool); -+ stop -> -+ couch_replicator_httpc_pool:release_worker(Pool, Worker), -+ Parent ! {stop, Ref} -+ end. -+ -+spawn_pool() -> -+ Host = couch_config:get("httpd", "bind_address", "127.0.0.1"), -+ Port = couch_config:get("httpd", "port", "5984"), -+ {ok, Pool} = couch_replicator_httpc_pool:start_link( -+ "http://" ++ Host ++ ":" ++ Port, [{max_connections, 3}]), -+ Pool. -+ -+stop_pool(Pool) -> -+ ok = couch_replicator_httpc_pool:stop(Pool). -diff --git a/src/couch_replicator/test/couch_replicator_large_atts_tests.erl b/src/couch_replicator/test/couch_replicator_large_atts_tests.erl -new file mode 100644 -index 0000000..7c4e334 ---- /dev/null -+++ b/src/couch_replicator/test/couch_replicator_large_atts_tests.erl -@@ -0,0 +1,218 @@ -+% Licensed under the Apache License, Version 2.0 (the "License"); you may not -+% use this file except in compliance with the License. You may obtain a copy of -+% the License at -+% -+% http://www.apache.org/licenses/LICENSE-2.0 -+% -+% Unless required by applicable law or agreed to in writing, software -+% distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -+% WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -+% License for the specific language governing permissions and limitations under -+% the License. -+ -+-module(couch_replicator_large_atts_tests). -+ -+-include("couch_eunit.hrl"). -+-include_lib("couchdb/couch_db.hrl"). -+ -+-define(ADMIN_ROLE, #user_ctx{roles=[<<"_admin">>]}). -+-define(ADMIN_USER, {user_ctx, ?ADMIN_ROLE}). -+-define(ATT_SIZE_1, 2 * 1024 * 1024). -+-define(ATT_SIZE_2, round(6.6 * 1024 * 1024)). -+-define(DOCS_COUNT, 11). -+-define(TIMEOUT_EUNIT, 30). -+-define(TIMEOUT_STOP, 1000). -+ -+ -+setup() -> -+ DbName = ?tempdb(), -+ {ok, Db} = couch_db:create(DbName, [?ADMIN_USER]), -+ ok = couch_db:close(Db), -+ DbName. -+ -+setup(local) -> -+ setup(); -+setup(remote) -> -+ {remote, setup()}; -+setup({A, B}) -> -+ {ok, _} = couch_server_sup:start_link(?CONFIG_CHAIN), -+ couch_config:set("attachments", "compressible_types", "text/*", false), -+ Source = setup(A), -+ Target = setup(B), -+ {Source, Target}. -+ -+teardown({remote, DbName}) -> -+ teardown(DbName); -+teardown(DbName) -> -+ ok = couch_server:delete(DbName, [?ADMIN_USER]), -+ ok. -+ -+teardown(_, {Source, Target}) -> -+ teardown(Source), -+ teardown(Target), -+ -+ Pid = whereis(couch_server_sup), -+ erlang:monitor(process, Pid), -+ couch_server_sup:stop(), -+ receive -+ {'DOWN', _, _, Pid, _} -> -+ ok -+ after ?TIMEOUT_STOP -> -+ throw({timeout, server_stop}) -+ end. -+ -+ -+large_atts_test_() -> -+ Pairs = [{local, local}, {local, remote}, -+ {remote, local}, {remote, remote}], -+ { -+ "Replicate docs with large attachments", -+ { -+ foreachx, -+ fun setup/1, fun teardown/2, -+ [{Pair, fun should_populate_replicate_compact/2} -+ || Pair <- Pairs] -+ } -+ }. -+ -+ -+should_populate_replicate_compact({From, To}, {Source, Target}) -> -+ {lists:flatten(io_lib:format("~p -> ~p", [From, To])), -+ {inorder, [should_populate_source(Source), -+ should_replicate(Source, Target), -+ should_compare_databases(Source, Target)]}}. -+ -+should_populate_source({remote, Source}) -> -+ should_populate_source(Source); -+should_populate_source(Source) -> -+ {timeout, ?TIMEOUT_EUNIT, ?_test(populate_db(Source, ?DOCS_COUNT))}. -+ -+should_replicate({remote, Source}, Target) -> -+ should_replicate(db_url(Source), Target); -+should_replicate(Source, {remote, Target}) -> -+ should_replicate(Source, db_url(Target)); -+should_replicate(Source, Target) -> -+ {timeout, ?TIMEOUT_EUNIT, ?_test(replicate(Source, Target))}. -+ -+should_compare_databases({remote, Source}, Target) -> -+ should_compare_databases(Source, Target); -+should_compare_databases(Source, {remote, Target}) -> -+ should_compare_databases(Source, Target); -+should_compare_databases(Source, Target) -> -+ {timeout, ?TIMEOUT_EUNIT, ?_test(compare_dbs(Source, Target))}. -+ -+ -+populate_db(DbName, DocCount) -> -+ {ok, Db} = couch_db:open_int(DbName, []), -+ Docs = lists:foldl( -+ fun(DocIdCounter, Acc) -> -+ Doc = #doc{ -+ id = iolist_to_binary(["doc", integer_to_list(DocIdCounter)]), -+ body = {[]}, -+ atts = [ -+ att(<<"att1">>, ?ATT_SIZE_1, <<"text/plain">>), -+ att(<<"att2">>, ?ATT_SIZE_2, <<"app/binary">>) -+ ] -+ }, -+ [Doc | Acc] -+ end, -+ [], lists:seq(1, DocCount)), -+ {ok, _} = couch_db:update_docs(Db, Docs, []), -+ couch_db:close(Db). -+ -+compare_dbs(Source, Target) -> -+ {ok, SourceDb} = couch_db:open_int(Source, []), -+ {ok, TargetDb} = couch_db:open_int(Target, []), -+ -+ Fun = fun(FullDocInfo, _, Acc) -> -+ {ok, DocSource} = couch_db:open_doc(SourceDb, FullDocInfo), -+ Id = DocSource#doc.id, -+ -+ {ok, DocTarget} = couch_db:open_doc(TargetDb, Id), -+ ?assertEqual(DocSource#doc.body, DocTarget#doc.body), -+ -+ #doc{atts = SourceAtts} = DocSource, -+ #doc{atts = TargetAtts} = DocTarget, -+ ?assertEqual(lists:sort([N || #att{name = N} <- SourceAtts]), -+ lists:sort([N || #att{name = N} <- TargetAtts])), -+ -+ FunCompareAtts = fun(#att{name = AttName} = Att) -> -+ {ok, AttTarget} = find_att(TargetAtts, AttName), -+ SourceMd5 = att_md5(Att), -+ TargetMd5 = att_md5(AttTarget), -+ case AttName of -+ <<"att1">> -> -+ ?assertEqual(gzip, Att#att.encoding), -+ ?assertEqual(gzip, AttTarget#att.encoding), -+ DecSourceMd5 = att_decoded_md5(Att), -+ DecTargetMd5 = att_decoded_md5(AttTarget), -+ ?assertEqual(DecSourceMd5, DecTargetMd5); -+ _ -> -+ ?assertEqual(identity, Att#att.encoding), -+ ?assertEqual(identity, AttTarget#att.encoding) -+ end, -+ ?assertEqual(SourceMd5, TargetMd5), -+ ?assert(is_integer(Att#att.disk_len)), -+ ?assert(is_integer(Att#att.att_len)), -+ ?assert(is_integer(AttTarget#att.disk_len)), -+ ?assert(is_integer(AttTarget#att.att_len)), -+ ?assertEqual(Att#att.disk_len, AttTarget#att.disk_len), -+ ?assertEqual(Att#att.att_len, AttTarget#att.att_len), -+ ?assertEqual(Att#att.type, AttTarget#att.type), -+ ?assertEqual(Att#att.md5, AttTarget#att.md5) -+ end, -+ -+ lists:foreach(FunCompareAtts, SourceAtts), -+ -+ {ok, Acc} -+ end, -+ -+ {ok, _, _} = couch_db:enum_docs(SourceDb, Fun, [], []), -+ ok = couch_db:close(SourceDb), -+ ok = couch_db:close(TargetDb). -+ -+att(Name, Size, Type) -> -+ #att{ -+ name = Name, -+ type = Type, -+ att_len = Size, -+ data = fun(Count) -> crypto:rand_bytes(Count) end -+ }. -+ -+find_att([], _Name) -> -+ nil; -+find_att([#att{name = Name} = Att | _], Name) -> -+ {ok, Att}; -+find_att([_ | Rest], Name) -> -+ find_att(Rest, Name). -+ -+att_md5(Att) -> -+ Md50 = couch_doc:att_foldl( -+ Att, -+ fun(Chunk, Acc) -> couch_util:md5_update(Acc, Chunk) end, -+ couch_util:md5_init()), -+ couch_util:md5_final(Md50). -+ -+att_decoded_md5(Att) -> -+ Md50 = couch_doc:att_foldl_decode( -+ Att, -+ fun(Chunk, Acc) -> couch_util:md5_update(Acc, Chunk) end, -+ couch_util:md5_init()), -+ couch_util:md5_final(Md50). -+ -+db_url(DbName) -> -+ iolist_to_binary([ -+ "http://", couch_config:get("httpd", "bind_address", "127.0.0.1"), -+ ":", integer_to_list(mochiweb_socket_server:get(couch_httpd, port)), -+ "/", DbName -+ ]). -+ -+replicate(Source, Target) -> -+ RepObject = {[{<<"source">>, Source}, {<<"target">>, Target}]}, -+ {ok, Rep} = couch_replicator_utils:parse_rep_doc(RepObject, ?ADMIN_ROLE), -+ {ok, Pid} = couch_replicator:async_replicate(Rep), -+ MonRef = erlang:monitor(process, Pid), -+ receive -+ {'DOWN', MonRef, process, Pid, _} -> -+ ok -+ end. -diff --git a/src/couch_replicator/test/couch_replicator_many_leaves_tests.erl b/src/couch_replicator/test/couch_replicator_many_leaves_tests.erl -new file mode 100644 -index 0000000..27d51db ---- /dev/null -+++ b/src/couch_replicator/test/couch_replicator_many_leaves_tests.erl -@@ -0,0 +1,232 @@ -+% Licensed under the Apache License, Version 2.0 (the "License"); you may not -+% use this file except in compliance with the License. You may obtain a copy of -+% the License at -+% -+% http://www.apache.org/licenses/LICENSE-2.0 -+% -+% Unless required by applicable law or agreed to in writing, software -+% distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -+% WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -+% License for the specific language governing permissions and limitations under -+% the License. -+ -+-module(couch_replicator_many_leaves_tests). -+ -+-include("couch_eunit.hrl"). -+-include_lib("couchdb/couch_db.hrl"). -+ -+-define(ADMIN_ROLE, #user_ctx{roles=[<<"_admin">>]}). -+-define(ADMIN_USER, {user_ctx, ?ADMIN_ROLE}). -+-define(DOCS_CONFLICTS, [ -+ {<<"doc1">>, 10}, -+ {<<"doc2">>, 100}, -+ % a number > MaxURLlength (7000) / length(DocRevisionString) -+ {<<"doc3">>, 210} -+]). -+-define(NUM_ATTS, 2). -+-define(TIMEOUT_STOP, 1000). -+-define(TIMEOUT_EUNIT, 60). -+-define(i2l(I), integer_to_list(I)). -+-define(io2b(Io), iolist_to_binary(Io)). -+ -+setup() -> -+ DbName = ?tempdb(), -+ {ok, Db} = couch_db:create(DbName, [?ADMIN_USER]), -+ ok = couch_db:close(Db), -+ DbName. -+ -+setup(local) -> -+ setup(); -+setup(remote) -> -+ {remote, setup()}; -+setup({A, B}) -> -+ {ok, _} = couch_server_sup:start_link(?CONFIG_CHAIN), -+ Source = setup(A), -+ Target = setup(B), -+ {Source, Target}. -+ -+teardown({remote, DbName}) -> -+ teardown(DbName); -+teardown(DbName) -> -+ ok = couch_server:delete(DbName, [?ADMIN_USER]), -+ ok. -+ -+teardown(_, {Source, Target}) -> -+ teardown(Source), -+ teardown(Target), -+ -+ Pid = whereis(couch_server_sup), -+ erlang:monitor(process, Pid), -+ couch_server_sup:stop(), -+ receive -+ {'DOWN', _, _, Pid, _} -> -+ ok -+ after ?TIMEOUT_STOP -> -+ throw({timeout, server_stop}) -+ end. -+ -+ -+docs_with_many_leaves_test_() -> -+ Pairs = [{local, local}, {local, remote}, -+ {remote, local}, {remote, remote}], -+ { -+ "Replicate documents with many leaves", -+ { -+ foreachx, -+ fun setup/1, fun teardown/2, -+ [{Pair, fun should_populate_replicate_compact/2} -+ || Pair <- Pairs] -+ } -+ }. -+ -+ -+should_populate_replicate_compact({From, To}, {Source, Target}) -> -+ {lists:flatten(io_lib:format("~p -> ~p", [From, To])), -+ {inorder, [ -+ should_populate_source(Source), -+ should_replicate(Source, Target), -+ should_verify_target(Source, Target), -+ should_add_attachments_to_source(Source), -+ should_replicate(Source, Target), -+ should_verify_target(Source, Target) -+ ]}}. -+ -+should_populate_source({remote, Source}) -> -+ should_populate_source(Source); -+should_populate_source(Source) -> -+ {timeout, ?TIMEOUT_EUNIT, ?_test(populate_db(Source))}. -+ -+should_replicate({remote, Source}, Target) -> -+ should_replicate(db_url(Source), Target); -+should_replicate(Source, {remote, Target}) -> -+ should_replicate(Source, db_url(Target)); -+should_replicate(Source, Target) -> -+ {timeout, ?TIMEOUT_EUNIT, ?_test(replicate(Source, Target))}. -+ -+should_verify_target({remote, Source}, Target) -> -+ should_verify_target(Source, Target); -+should_verify_target(Source, {remote, Target}) -> -+ should_verify_target(Source, Target); -+should_verify_target(Source, Target) -> -+ {timeout, ?TIMEOUT_EUNIT, ?_test(begin -+ {ok, SourceDb} = couch_db:open_int(Source, []), -+ {ok, TargetDb} = couch_db:open_int(Target, []), -+ verify_target(SourceDb, TargetDb, ?DOCS_CONFLICTS), -+ ok = couch_db:close(SourceDb), -+ ok = couch_db:close(TargetDb) -+ end)}. -+ -+should_add_attachments_to_source({remote, Source}) -> -+ should_add_attachments_to_source(Source); -+should_add_attachments_to_source(Source) -> -+ {timeout, ?TIMEOUT_EUNIT, ?_test(begin -+ {ok, SourceDb} = couch_db:open_int(Source, []), -+ add_attachments(SourceDb, ?NUM_ATTS, ?DOCS_CONFLICTS), -+ ok = couch_db:close(SourceDb) -+ end)}. -+ -+populate_db(DbName) -> -+ {ok, Db} = couch_db:open_int(DbName, []), -+ lists:foreach( -+ fun({DocId, NumConflicts}) -> -+ Value = <<"0">>, -+ Doc = #doc{ -+ id = DocId, -+ body = {[ {<<"value">>, Value} ]} -+ }, -+ {ok, _} = couch_db:update_doc(Db, Doc, []), -+ {ok, _} = add_doc_siblings(Db, DocId, NumConflicts) -+ end, ?DOCS_CONFLICTS), -+ couch_db:close(Db). -+ -+add_doc_siblings(Db, DocId, NumLeaves) when NumLeaves > 0 -> -+ add_doc_siblings(Db, DocId, NumLeaves, [], []). -+ -+add_doc_siblings(Db, _DocId, 0, AccDocs, AccRevs) -> -+ {ok, []} = couch_db:update_docs(Db, AccDocs, [], replicated_changes), -+ {ok, AccRevs}; -+ -+add_doc_siblings(Db, DocId, NumLeaves, AccDocs, AccRevs) -> -+ Value = ?l2b(?i2l(NumLeaves)), -+ Rev = couch_util:md5(Value), -+ Doc = #doc{ -+ id = DocId, -+ revs = {1, [Rev]}, -+ body = {[ {<<"value">>, Value} ]} -+ }, -+ add_doc_siblings(Db, DocId, NumLeaves - 1, -+ [Doc | AccDocs], [{1, Rev} | AccRevs]). -+ -+verify_target(_SourceDb, _TargetDb, []) -> -+ ok; -+verify_target(SourceDb, TargetDb, [{DocId, NumConflicts} | Rest]) -> -+ {ok, SourceLookups} = couch_db:open_doc_revs( -+ SourceDb, -+ DocId, -+ all, -+ [conflicts, deleted_conflicts]), -+ {ok, TargetLookups} = couch_db:open_doc_revs( -+ TargetDb, -+ DocId, -+ all, -+ [conflicts, deleted_conflicts]), -+ SourceDocs = [Doc || {ok, Doc} <- SourceLookups], -+ TargetDocs = [Doc || {ok, Doc} <- TargetLookups], -+ Total = NumConflicts + 1, -+ ?assertEqual(Total, length(TargetDocs)), -+ lists:foreach( -+ fun({SourceDoc, TargetDoc}) -> -+ SourceJson = couch_doc:to_json_obj(SourceDoc, [attachments]), -+ TargetJson = couch_doc:to_json_obj(TargetDoc, [attachments]), -+ ?assertEqual(SourceJson, TargetJson) -+ end, -+ lists:zip(SourceDocs, TargetDocs)), -+ verify_target(SourceDb, TargetDb, Rest). -+ -+add_attachments(_SourceDb, _NumAtts, []) -> -+ ok; -+add_attachments(SourceDb, NumAtts, [{DocId, NumConflicts} | Rest]) -> -+ {ok, SourceLookups} = couch_db:open_doc_revs(SourceDb, DocId, all, []), -+ SourceDocs = [Doc || {ok, Doc} <- SourceLookups], -+ Total = NumConflicts + 1, -+ ?assertEqual(Total, length(SourceDocs)), -+ NewDocs = lists:foldl( -+ fun(#doc{atts = Atts, revs = {Pos, [Rev | _]}} = Doc, Acc) -> -+ NewAtts = lists:foldl(fun(I, AttAcc) -> -+ AttData = crypto:rand_bytes(100), -+ NewAtt = #att{ -+ name = ?io2b(["att_", ?i2l(I), "_", -+ couch_doc:rev_to_str({Pos, Rev})]), -+ type = <<"application/foobar">>, -+ att_len = byte_size(AttData), -+ data = AttData -+ }, -+ [NewAtt | AttAcc] -+ end, [], lists:seq(1, NumAtts)), -+ [Doc#doc{atts = Atts ++ NewAtts} | Acc] -+ end, -+ [], SourceDocs), -+ {ok, UpdateResults} = couch_db:update_docs(SourceDb, NewDocs, []), -+ NewRevs = [R || {ok, R} <- UpdateResults], -+ ?assertEqual(length(NewDocs), length(NewRevs)), -+ add_attachments(SourceDb, NumAtts, Rest). -+ -+db_url(DbName) -> -+ iolist_to_binary([ -+ "http://", couch_config:get("httpd", "bind_address", "127.0.0.1"), -+ ":", integer_to_list(mochiweb_socket_server:get(couch_httpd, port)), -+ "/", DbName -+ ]). -+ -+replicate(Source, Target) -> -+ RepObject = {[ -+ {<<"source">>, Source}, -+ {<<"target">>, Target} -+ ]}, -+ {ok, Rep} = couch_replicator_utils:parse_rep_doc(RepObject, ?ADMIN_ROLE), -+ {ok, Pid} = couch_replicator:async_replicate(Rep), -+ MonRef = erlang:monitor(process, Pid), -+ receive -+ {'DOWN', MonRef, process, Pid, _} -> -+ ok -+ end. -diff --git a/src/couch_replicator/test/couch_replicator_missing_stubs_tests.erl b/src/couch_replicator/test/couch_replicator_missing_stubs_tests.erl -new file mode 100644 -index 0000000..8c64929 ---- /dev/null -+++ b/src/couch_replicator/test/couch_replicator_missing_stubs_tests.erl -@@ -0,0 +1,260 @@ -+% Licensed under the Apache License, Version 2.0 (the "License"); you may not -+% use this file except in compliance with the License. You may obtain a copy of -+% the License at -+% -+% http://www.apache.org/licenses/LICENSE-2.0 -+% -+% Unless required by applicable law or agreed to in writing, software -+% distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -+% WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -+% License for the specific language governing permissions and limitations under -+% the License. -+ -+-module(couch_replicator_missing_stubs_tests). -+ -+-include("couch_eunit.hrl"). -+-include_lib("couchdb/couch_db.hrl"). -+ -+-define(ADMIN_ROLE, #user_ctx{roles=[<<"_admin">>]}). -+-define(ADMIN_USER, {user_ctx, ?ADMIN_ROLE}). -+-define(REVS_LIMIT, 3). -+-define(TIMEOUT_STOP, 1000). -+-define(TIMEOUT_EUNIT, 30). -+ -+ -+setup() -> -+ DbName = ?tempdb(), -+ {ok, Db} = couch_db:create(DbName, [?ADMIN_USER]), -+ ok = couch_db:close(Db), -+ DbName. -+ -+setup(local) -> -+ setup(); -+setup(remote) -> -+ {remote, setup()}; -+setup({A, B}) -> -+ {ok, _} = couch_server_sup:start_link(?CONFIG_CHAIN), -+ Source = setup(A), -+ Target = setup(B), -+ {Source, Target}. -+ -+teardown({remote, DbName}) -> -+ teardown(DbName); -+teardown(DbName) -> -+ ok = couch_server:delete(DbName, [?ADMIN_USER]), -+ ok. -+ -+teardown(_, {Source, Target}) -> -+ teardown(Source), -+ teardown(Target), -+ -+ Pid = whereis(couch_server_sup), -+ erlang:monitor(process, Pid), -+ couch_server_sup:stop(), -+ receive -+ {'DOWN', _, _, Pid, _} -> -+ ok -+ after ?TIMEOUT_STOP -> -+ throw({timeout, server_stop}) -+ end. -+ -+ -+missing_stubs_test_() -> -+ Pairs = [{local, local}, {local, remote}, -+ {remote, local}, {remote, remote}], -+ { -+ "Replicate docs with missing stubs (COUCHDB-1365)", -+ { -+ foreachx, -+ fun setup/1, fun teardown/2, -+ [{Pair, fun should_replicate_docs_with_missed_att_stubs/2} -+ || Pair <- Pairs] -+ } -+ }. -+ -+ -+should_replicate_docs_with_missed_att_stubs({From, To}, {Source, Target}) -> -+ {lists:flatten(io_lib:format("~p -> ~p", [From, To])), -+ {inorder, [ -+ should_populate_source(Source), -+ should_set_target_revs_limit(Target, ?REVS_LIMIT), -+ should_replicate(Source, Target), -+ should_compare_databases(Source, Target), -+ should_update_source_docs(Source, ?REVS_LIMIT * 2), -+ should_replicate(Source, Target), -+ should_compare_databases(Source, Target) -+ ]}}. -+ -+should_populate_source({remote, Source}) -> -+ should_populate_source(Source); -+should_populate_source(Source) -> -+ {timeout, ?TIMEOUT_EUNIT, ?_test(populate_db(Source))}. -+ -+should_replicate({remote, Source}, Target) -> -+ should_replicate(db_url(Source), Target); -+should_replicate(Source, {remote, Target}) -> -+ should_replicate(Source, db_url(Target)); -+should_replicate(Source, Target) -> -+ {timeout, ?TIMEOUT_EUNIT, ?_test(replicate(Source, Target))}. -+ -+should_set_target_revs_limit({remote, Target}, RevsLimit) -> -+ should_set_target_revs_limit(Target, RevsLimit); -+should_set_target_revs_limit(Target, RevsLimit) -> -+ ?_test(begin -+ {ok, Db} = couch_db:open_int(Target, [?ADMIN_USER]), -+ ?assertEqual(ok, couch_db:set_revs_limit(Db, RevsLimit)), -+ ok = couch_db:close(Db) -+ end). -+ -+should_compare_databases({remote, Source}, Target) -> -+ should_compare_databases(Source, Target); -+should_compare_databases(Source, {remote, Target}) -> -+ should_compare_databases(Source, Target); -+should_compare_databases(Source, Target) -> -+ {timeout, ?TIMEOUT_EUNIT, ?_test(compare_dbs(Source, Target))}. -+ -+should_update_source_docs({remote, Source}, Times) -> -+ should_update_source_docs(Source, Times); -+should_update_source_docs(Source, Times) -> -+ {timeout, ?TIMEOUT_EUNIT, ?_test(update_db_docs(Source, Times))}. -+ -+ -+populate_db(DbName) -> -+ {ok, Db} = couch_db:open_int(DbName, []), -+ AttData = crypto:rand_bytes(6000), -+ Doc = #doc{ -+ id = <<"doc1">>, -+ atts = [ -+ #att{ -+ name = <<"doc1_att1">>, -+ type = <<"application/foobar">>, -+ att_len = byte_size(AttData), -+ data = AttData -+ } -+ ] -+ }, -+ {ok, _} = couch_db:update_doc(Db, Doc, []), -+ couch_db:close(Db). -+ -+update_db_docs(DbName, Times) -> -+ {ok, Db} = couch_db:open_int(DbName, []), -+ {ok, _, _} = couch_db:enum_docs( -+ Db, -+ fun(FDI, _, Acc) -> db_fold_fun(FDI, Acc) end, -+ {DbName, Times}, -+ []), -+ ok = couch_db:close(Db). -+ -+db_fold_fun(FullDocInfo, {DbName, Times}) -> -+ {ok, Db} = couch_db:open_int(DbName, []), -+ {ok, Doc} = couch_db:open_doc(Db, FullDocInfo), -+ lists:foldl( -+ fun(_, {Pos, RevId}) -> -+ {ok, Db2} = couch_db:reopen(Db), -+ NewDocVersion = Doc#doc{ -+ revs = {Pos, [RevId]}, -+ body = {[{<<"value">>, base64:encode(crypto:rand_bytes(100))}]} -+ }, -+ {ok, NewRev} = couch_db:update_doc(Db2, NewDocVersion, []), -+ NewRev -+ end, -+ {element(1, Doc#doc.revs), hd(element(2, Doc#doc.revs))}, -+ lists:seq(1, Times)), -+ ok = couch_db:close(Db), -+ {ok, {DbName, Times}}. -+ -+compare_dbs(Source, Target) -> -+ {ok, SourceDb} = couch_db:open_int(Source, []), -+ {ok, TargetDb} = couch_db:open_int(Target, []), -+ -+ Fun = fun(FullDocInfo, _, Acc) -> -+ {ok, DocSource} = couch_db:open_doc(SourceDb, FullDocInfo, -+ [conflicts, deleted_conflicts]), -+ Id = DocSource#doc.id, -+ -+ {ok, DocTarget} = couch_db:open_doc(TargetDb, Id, -+ [conflicts, deleted_conflicts]), -+ ?assertEqual(DocSource#doc.body, DocTarget#doc.body), -+ -+ ?assertEqual(couch_doc:to_json_obj(DocSource, []), -+ couch_doc:to_json_obj(DocTarget, [])), -+ -+ #doc{atts = SourceAtts} = DocSource, -+ #doc{atts = TargetAtts} = DocTarget, -+ ?assertEqual(lists:sort([N || #att{name = N} <- SourceAtts]), -+ lists:sort([N || #att{name = N} <- TargetAtts])), -+ -+ lists:foreach( -+ fun(#att{name = AttName} = Att) -> -+ {ok, AttTarget} = find_att(TargetAtts, AttName), -+ SourceMd5 = att_md5(Att), -+ TargetMd5 = att_md5(AttTarget), -+ case AttName of -+ <<"att1">> -> -+ ?assertEqual(gzip, Att#att.encoding), -+ ?assertEqual(gzip, AttTarget#att.encoding), -+ DecSourceMd5 = att_decoded_md5(Att), -+ DecTargetMd5 = att_decoded_md5(AttTarget), -+ ?assertEqual(DecSourceMd5, DecTargetMd5); -+ _ -> -+ ?assertEqual(identity, Att#att.encoding), -+ ?assertEqual(identity, AttTarget#att.encoding) -+ end, -+ ?assertEqual(SourceMd5, TargetMd5), -+ ?assert(is_integer(Att#att.disk_len)), -+ ?assert(is_integer(Att#att.att_len)), -+ ?assert(is_integer(AttTarget#att.disk_len)), -+ ?assert(is_integer(AttTarget#att.att_len)), -+ ?assertEqual(Att#att.disk_len, AttTarget#att.disk_len), -+ ?assertEqual(Att#att.att_len, AttTarget#att.att_len), -+ ?assertEqual(Att#att.type, AttTarget#att.type), -+ ?assertEqual(Att#att.md5, AttTarget#att.md5) -+ end, -+ SourceAtts), -+ {ok, Acc} -+ end, -+ -+ {ok, _, _} = couch_db:enum_docs(SourceDb, Fun, [], []), -+ ok = couch_db:close(SourceDb), -+ ok = couch_db:close(TargetDb). -+ -+find_att([], _Name) -> -+ nil; -+find_att([#att{name = Name} = Att | _], Name) -> -+ {ok, Att}; -+find_att([_ | Rest], Name) -> -+ find_att(Rest, Name). -+ -+att_md5(Att) -> -+ Md50 = couch_doc:att_foldl( -+ Att, -+ fun(Chunk, Acc) -> couch_util:md5_update(Acc, Chunk) end, -+ couch_util:md5_init()), -+ couch_util:md5_final(Md50). -+ -+att_decoded_md5(Att) -> -+ Md50 = couch_doc:att_foldl_decode( -+ Att, -+ fun(Chunk, Acc) -> couch_util:md5_update(Acc, Chunk) end, -+ couch_util:md5_init()), -+ couch_util:md5_final(Md50). -+ -+db_url(DbName) -> -+ iolist_to_binary([ -+ "http://", couch_config:get("httpd", "bind_address", "127.0.0.1"), -+ ":", integer_to_list(mochiweb_socket_server:get(couch_httpd, port)), -+ "/", DbName -+ ]). -+ -+replicate(Source, Target) -> -+ RepObject = {[ -+ {<<"source">>, Source}, -+ {<<"target">>, Target} -+ ]}, -+ {ok, Rep} = couch_replicator_utils:parse_rep_doc(RepObject, ?ADMIN_ROLE), -+ {ok, Pid} = couch_replicator:async_replicate(Rep), -+ MonRef = erlang:monitor(process, Pid), -+ receive -+ {'DOWN', MonRef, process, Pid, _} -> -+ ok -+ end. -diff --git a/src/couch_replicator/test/couch_replicator_modules_load_tests.erl b/src/couch_replicator/test/couch_replicator_modules_load_tests.erl -new file mode 100644 -index 0000000..7107b9e ---- /dev/null -+++ b/src/couch_replicator/test/couch_replicator_modules_load_tests.erl -@@ -0,0 +1,40 @@ -+% Licensed under the Apache License, Version 2.0 (the "License"); you may not -+% use this file except in compliance with the License. You may obtain a copy of -+% the License at -+% -+% http://www.apache.org/licenses/LICENSE-2.0 -+% -+% Unless required by applicable law or agreed to in writing, software -+% distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -+% WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -+% License for the specific language governing permissions and limitations under -+% the License. -+ -+-module(couch_replicator_modules_load_tests). -+ -+-include("couch_eunit.hrl"). -+ -+ -+modules_load_test_() -> -+ { -+ "Verify that all modules loads", -+ should_load_modules() -+ }. -+ -+ -+should_load_modules() -> -+ Modules = [ -+ couch_replicator_api_wrap, -+ couch_replicator_httpc, -+ couch_replicator_httpd, -+ couch_replicator_manager, -+ couch_replicator_notifier, -+ couch_replicator, -+ couch_replicator_worker, -+ couch_replicator_utils, -+ couch_replicator_job_sup -+ ], -+ [should_load_module(Mod) || Mod <- Modules]. -+ -+should_load_module(Mod) -> -+ {atom_to_list(Mod), ?_assertMatch({module, _}, code:load_file(Mod))}. -diff --git a/src/couch_replicator/test/couch_replicator_use_checkpoints_tests.erl b/src/couch_replicator/test/couch_replicator_use_checkpoints_tests.erl -new file mode 100644 -index 0000000..5356a37 ---- /dev/null -+++ b/src/couch_replicator/test/couch_replicator_use_checkpoints_tests.erl -@@ -0,0 +1,200 @@ -+% Licensed under the Apache License, Version 2.0 (the "License"); you may not -+% use this file except in compliance with the License. You may obtain a copy of -+% the License at -+% -+% http://www.apache.org/licenses/LICENSE-2.0 -+% -+% Unless required by applicable law or agreed to in writing, software -+% distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -+% WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -+% License for the specific language governing permissions and limitations under -+% the License. -+ -+-module(couch_replicator_use_checkpoints_tests). -+ -+-include("couch_eunit.hrl"). -+-include_lib("couchdb/couch_db.hrl"). -+ -+-define(ADMIN_ROLE, #user_ctx{roles=[<<"_admin">>]}). -+-define(ADMIN_USER, {user_ctx, ?ADMIN_ROLE}). -+-define(DOCS_COUNT, 100). -+-define(TIMEOUT_STOP, 1000). -+-define(TIMEOUT_EUNIT, 30). -+-define(i2l(I), integer_to_list(I)). -+-define(io2b(Io), iolist_to_binary(Io)). -+ -+ -+start(false) -> -+ fun -+ ({finished, _, {CheckpointHistory}}) -> -+ ?assertEqual([{<<"use_checkpoints">>,false}], CheckpointHistory); -+ (_) -> -+ ok -+ end; -+start(true) -> -+ fun -+ ({finished, _, {CheckpointHistory}}) -> -+ ?assertNotEqual(false, lists:keyfind(<<"session_id">>, -+ 1, CheckpointHistory)); -+ (_) -> -+ ok -+ end. -+ -+stop(_, _) -> -+ ok. -+ -+setup() -> -+ DbName = ?tempdb(), -+ {ok, Db} = couch_db:create(DbName, [?ADMIN_USER]), -+ ok = couch_db:close(Db), -+ DbName. -+ -+setup(local) -> -+ setup(); -+setup(remote) -> -+ {remote, setup()}; -+setup({_, Fun, {A, B}}) -> -+ {ok, _} = couch_server_sup:start_link(?CONFIG_CHAIN), -+ {ok, Listener} = couch_replicator_notifier:start_link(Fun), -+ Source = setup(A), -+ Target = setup(B), -+ {Source, Target, Listener}. -+ -+teardown({remote, DbName}) -> -+ teardown(DbName); -+teardown(DbName) -> -+ ok = couch_server:delete(DbName, [?ADMIN_USER]), -+ ok. -+ -+teardown(_, {Source, Target, Listener}) -> -+ teardown(Source), -+ teardown(Target), -+ -+ couch_replicator_notifier:stop(Listener), -+ Pid = whereis(couch_server_sup), -+ erlang:monitor(process, Pid), -+ couch_server_sup:stop(), -+ receive -+ {'DOWN', _, _, Pid, _} -> -+ ok -+ after ?TIMEOUT_STOP -> -+ throw({timeout, server_stop}) -+ end. -+ -+ -+use_checkpoints_test_() -> -+ { -+ "Replication use_checkpoints feature tests", -+ { -+ foreachx, -+ fun start/1, fun stop/2, -+ [{UseCheckpoints, fun use_checkpoints_tests/2} -+ || UseCheckpoints <- [false, true]] -+ } -+ }. -+ -+use_checkpoints_tests(UseCheckpoints, Fun) -> -+ Pairs = [{local, local}, {local, remote}, -+ {remote, local}, {remote, remote}], -+ { -+ "use_checkpoints: " ++ atom_to_list(UseCheckpoints), -+ { -+ foreachx, -+ fun setup/1, fun teardown/2, -+ [{{UseCheckpoints, Fun, Pair}, fun should_test_checkpoints/2} -+ || Pair <- Pairs] -+ } -+ }. -+ -+should_test_checkpoints({UseCheckpoints, _, {From, To}}, {Source, Target, _}) -> -+ should_test_checkpoints(UseCheckpoints, {From, To}, {Source, Target}). -+should_test_checkpoints(UseCheckpoints, {From, To}, {Source, Target}) -> -+ {lists:flatten(io_lib:format("~p -> ~p", [From, To])), -+ {inorder, [ -+ should_populate_source(Source, ?DOCS_COUNT), -+ should_replicate(Source, Target, UseCheckpoints), -+ should_compare_databases(Source, Target) -+ ]}}. -+ -+should_populate_source({remote, Source}, DocCount) -> -+ should_populate_source(Source, DocCount); -+should_populate_source(Source, DocCount) -> -+ {timeout, ?TIMEOUT_EUNIT, ?_test(populate_db(Source, DocCount))}. -+ -+should_replicate({remote, Source}, Target, UseCheckpoints) -> -+ should_replicate(db_url(Source), Target, UseCheckpoints); -+should_replicate(Source, {remote, Target}, UseCheckpoints) -> -+ should_replicate(Source, db_url(Target), UseCheckpoints); -+should_replicate(Source, Target, UseCheckpoints) -> -+ {timeout, ?TIMEOUT_EUNIT, ?_test(replicate(Source, Target, UseCheckpoints))}. -+ -+should_compare_databases({remote, Source}, Target) -> -+ should_compare_databases(Source, Target); -+should_compare_databases(Source, {remote, Target}) -> -+ should_compare_databases(Source, Target); -+should_compare_databases(Source, Target) -> -+ {timeout, ?TIMEOUT_EUNIT, ?_test(compare_dbs(Source, Target))}. -+ -+ -+populate_db(DbName, DocCount) -> -+ {ok, Db} = couch_db:open_int(DbName, []), -+ Docs = lists:foldl( -+ fun(DocIdCounter, Acc) -> -+ Id = ?io2b(["doc", ?i2l(DocIdCounter)]), -+ Value = ?io2b(["val", ?i2l(DocIdCounter)]), -+ Doc = #doc{ -+ id = Id, -+ body = {[ {<<"value">>, Value} ]} -+ }, -+ [Doc | Acc] -+ end, -+ [], lists:seq(1, DocCount)), -+ {ok, _} = couch_db:update_docs(Db, Docs, []), -+ ok = couch_db:close(Db). -+ -+compare_dbs(Source, Target) -> -+ {ok, SourceDb} = couch_db:open_int(Source, []), -+ {ok, TargetDb} = couch_db:open_int(Target, []), -+ Fun = fun(FullDocInfo, _, Acc) -> -+ {ok, Doc} = couch_db:open_doc(SourceDb, FullDocInfo), -+ {Props} = DocJson = couch_doc:to_json_obj(Doc, [attachments]), -+ DocId = couch_util:get_value(<<"_id">>, Props), -+ DocTarget = case couch_db:open_doc(TargetDb, DocId) of -+ {ok, DocT} -> -+ DocT; -+ Error -> -+ erlang:error( -+ {assertion_failed, -+ [{module, ?MODULE}, {line, ?LINE}, -+ {reason, lists:concat(["Error opening document '", -+ ?b2l(DocId), "' from target: ", -+ couch_util:to_list(Error)])}]}) -+ end, -+ DocTargetJson = couch_doc:to_json_obj(DocTarget, [attachments]), -+ ?assertEqual(DocJson, DocTargetJson), -+ {ok, Acc} -+ end, -+ {ok, _, _} = couch_db:enum_docs(SourceDb, Fun, [], []), -+ ok = couch_db:close(SourceDb), -+ ok = couch_db:close(TargetDb). -+ -+db_url(DbName) -> -+ iolist_to_binary([ -+ "http://", couch_config:get("httpd", "bind_address", "127.0.0.1"), -+ ":", integer_to_list(mochiweb_socket_server:get(couch_httpd, port)), -+ "/", DbName -+ ]). -+ -+replicate(Source, Target, UseCheckpoints) -> -+ RepObject = {[ -+ {<<"source">>, Source}, -+ {<<"target">>, Target}, -+ {<<"use_checkpoints">>, UseCheckpoints} -+ ]}, -+ {ok, Rep} = couch_replicator_utils:parse_rep_doc(RepObject, ?ADMIN_ROLE), -+ {ok, Pid} = couch_replicator:async_replicate(Rep), -+ MonRef = erlang:monitor(process, Pid), -+ receive -+ {'DOWN', MonRef, process, Pid, _} -> -+ ok -+ end. -diff --git a/src/couchdb/couch_key_tree.erl b/src/couchdb/couch_key_tree.erl -index ce45ab8..58204e2 100644 ---- a/src/couchdb/couch_key_tree.erl -+++ b/src/couchdb/couch_key_tree.erl -@@ -418,5 +418,5 @@ value_pref(Last, _) -> - Last. - - --% Tests moved to test/etap/06?-*.t -+% Tests moved to test/couchdb/couch_key_tree_tests.erl - -diff --git a/test/Makefile.am b/test/Makefile.am -index 7c70a5a..f93baae 100644 ---- a/test/Makefile.am -+++ b/test/Makefile.am -@@ -10,6 +10,6 @@ - ## License for the specific language governing permissions and limitations under - ## the License. - --SUBDIRS = bench etap javascript view_server -+SUBDIRS = bench couchdb javascript view_server - EXTRA_DIST = random_port.ini - -diff --git a/test/couchdb/Makefile.am b/test/couchdb/Makefile.am -new file mode 100644 -index 0000000..1d9406c ---- /dev/null -+++ b/test/couchdb/Makefile.am -@@ -0,0 +1,82 @@ -+## 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. -+ -+SUBDIRS = fixtures -+ -+noinst_SCRIPTS = run -+ -+eunit_files = \ -+ couch_auth_cache_tests.erl \ -+ couch_btree_tests.erl \ -+ couch_changes_tests.erl \ -+ couch_config_tests.erl \ -+ couch_db_tests.erl \ -+ couch_doc_json_tests.erl \ -+ couch_file_tests.erl \ -+ couch_key_tree_tests.erl \ -+ couch_passwords_tests.erl \ -+ couch_ref_counter_tests.erl \ -+ couch_stream_tests.erl \ -+ couch_stats_tests.erl \ -+ couch_task_status_tests.erl \ -+ couch_util_tests.erl \ -+ couch_uuids_tests.erl \ -+ couch_work_queue_tests.erl \ -+ couchdb_attachments_tests.erl \ -+ couchdb_compaction_daemon.erl \ -+ couchdb_cors_tests.erl \ -+ couchdb_file_compression_tests.erl \ -+ couchdb_http_proxy_tests.erl \ -+ couchdb_modules_load_tests.erl \ -+ couchdb_os_daemons_tests.erl \ -+ couchdb_os_proc_pool.erl \ -+ couchdb_update_conflicts_tests.erl \ -+ couchdb_vhosts_tests.erl \ -+ couchdb_views_tests.erl \ -+ json_stream_parse_tests.erl \ -+ test_request.erl \ -+ test_web.erl \ -+ include/couch_eunit.hrl -+ -+fixture_files = \ -+ fixtures/couch_config_tests_1.ini \ -+ fixtures/couch_config_tests_2.ini \ -+ fixtures/couch_stats_aggregates.cfg \ -+ fixtures/couch_stats_aggregates.ini \ -+ fixtures/os_daemon_looper.escript \ -+ fixtures/os_daemon_configer.escript \ -+ fixtures/os_daemon_bad_perm.sh \ -+ fixtures/os_daemon_can_reboot.sh \ -+ fixtures/os_daemon_die_on_boot.sh \ -+ fixtures/os_daemon_die_quickly.sh \ -+ fixtures/logo.png \ -+ fixtures/3b835456c235b1827e012e25666152f3.view \ -+ fixtures/test.couch -+ -+EXTRA_DIST = \ -+ run.in \ -+ eunit.ini \ -+ $(eunit_files) \ -+ $(fixture_files) -+ -+all: -+ @mkdir -p ebin/ -+ @mkdir -p temp/ -+ $(ERLC) -Wall -I$(top_srcdir)/src -I$(top_srcdir)/test/couchdb/include \ -+ -o $(top_builddir)/test/couchdb/ebin/ $(ERLC_FLAGS) ${TEST} \ -+ $(top_srcdir)/test/couchdb/test_request.erl \ -+ $(top_srcdir)/test/couchdb/test_web.erl -+ chmod +x run -+ -+clean-local: -+ rm -rf ebin/ -+ rm -rf temp/ -diff --git a/test/couchdb/couch_auth_cache_tests.erl b/test/couchdb/couch_auth_cache_tests.erl -new file mode 100644 -index 0000000..3b2321c ---- /dev/null -+++ b/test/couchdb/couch_auth_cache_tests.erl -@@ -0,0 +1,238 @@ -+% Licensed under the Apache License, Version 2.0 (the "License"); you may not -+% use this file except in compliance with the License. You may obtain a copy of -+% the License at -+% -+% http://www.apache.org/licenses/LICENSE-2.0 -+% -+% Unless required by applicable law or agreed to in writing, software -+% distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -+% WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -+% License for the specific language governing permissions and limitations under -+% the License. -+ -+-module(couch_auth_cache_tests). -+ -+-include("couch_eunit.hrl"). -+-include_lib("couchdb/couch_db.hrl"). -+ -+-define(ADMIN_USER, {user_ctx, #user_ctx{roles=[<<"_admin">>]}}). -+-define(SALT, <<"SALT">>). -+-define(TIMEOUT, 1000). -+ -+ -+start() -> -+ {ok, Pid} = couch_server_sup:start_link(?CONFIG_CHAIN), -+ Pid. -+ -+stop(Pid) -> -+ erlang:monitor(process, Pid), -+ couch_server_sup:stop(), -+ receive -+ {'DOWN', _, _, Pid, _} -> -+ ok -+ after ?TIMEOUT -> -+ throw({timeout, server_stop}) -+ end. -+ -+setup() -> -+ DbName = ?tempdb(), -+ couch_config:set("couch_httpd_auth", "authentication_db", -+ ?b2l(DbName), false), -+ DbName. -+ -+teardown(DbName) -> -+ ok = couch_server:delete(DbName, [?ADMIN_USER]), -+ ok. -+ -+ -+couch_auth_cache_test_() -> -+ { -+ "CouchDB auth cache tests", -+ { -+ setup, -+ fun start/0, fun stop/1, -+ { -+ foreach, -+ fun setup/0, fun teardown/1, -+ [ -+ fun should_get_nil_on_missed_cache/1, -+ fun should_get_right_password_hash/1, -+ fun should_ensure_doc_hash_equals_cached_one/1, -+ fun should_update_password/1, -+ fun should_cleanup_cache_after_userdoc_deletion/1, -+ fun should_restore_cache_after_userdoc_recreation/1, -+ fun should_drop_cache_on_auth_db_change/1, -+ fun should_restore_cache_on_auth_db_change/1, -+ fun should_recover_cache_after_shutdown/1 -+ ] -+ } -+ } -+ }. -+ -+ -+should_get_nil_on_missed_cache(_) -> -+ ?_assertEqual(nil, couch_auth_cache:get_user_creds("joe")). -+ -+should_get_right_password_hash(DbName) -> -+ ?_test(begin -+ PasswordHash = hash_password("pass1"), -+ {ok, _} = update_user_doc(DbName, "joe", "pass1"), -+ Creds = couch_auth_cache:get_user_creds("joe"), -+ ?assertEqual(PasswordHash, -+ couch_util:get_value(<<"password_sha">>, Creds)) -+ end). -+ -+should_ensure_doc_hash_equals_cached_one(DbName) -> -+ ?_test(begin -+ {ok, _} = update_user_doc(DbName, "joe", "pass1"), -+ Creds = couch_auth_cache:get_user_creds("joe"), -+ -+ CachedHash = couch_util:get_value(<<"password_sha">>, Creds), -+ StoredHash = get_user_doc_password_sha(DbName, "joe"), -+ ?assertEqual(StoredHash, CachedHash) -+ end). -+ -+should_update_password(DbName) -> -+ ?_test(begin -+ PasswordHash = hash_password("pass2"), -+ {ok, Rev} = update_user_doc(DbName, "joe", "pass1"), -+ {ok, _} = update_user_doc(DbName, "joe", "pass2", Rev), -+ Creds = couch_auth_cache:get_user_creds("joe"), -+ ?assertEqual(PasswordHash, -+ couch_util:get_value(<<"password_sha">>, Creds)) -+ end). -+ -+should_cleanup_cache_after_userdoc_deletion(DbName) -> -+ ?_test(begin -+ {ok, _} = update_user_doc(DbName, "joe", "pass1"), -+ delete_user_doc(DbName, "joe"), -+ ?assertEqual(nil, couch_auth_cache:get_user_creds("joe")) -+ end). -+ -+should_restore_cache_after_userdoc_recreation(DbName) -> -+ ?_test(begin -+ PasswordHash = hash_password("pass5"), -+ {ok, _} = update_user_doc(DbName, "joe", "pass1"), -+ delete_user_doc(DbName, "joe"), -+ ?assertEqual(nil, couch_auth_cache:get_user_creds("joe")), -+ -+ {ok, _} = update_user_doc(DbName, "joe", "pass5"), -+ Creds = couch_auth_cache:get_user_creds("joe"), -+ -+ ?assertEqual(PasswordHash, -+ couch_util:get_value(<<"password_sha">>, Creds)) -+ end). -+ -+should_drop_cache_on_auth_db_change(DbName) -> -+ ?_test(begin -+ {ok, _} = update_user_doc(DbName, "joe", "pass1"), -+ full_commit(DbName), -+ couch_config:set("couch_httpd_auth", "authentication_db", -+ ?b2l(?tempdb()), false), -+ ?assertEqual(nil, couch_auth_cache:get_user_creds("joe")) -+ end). -+ -+should_restore_cache_on_auth_db_change(DbName) -> -+ ?_test(begin -+ PasswordHash = hash_password("pass1"), -+ {ok, _} = update_user_doc(DbName, "joe", "pass1"), -+ Creds = couch_auth_cache:get_user_creds("joe"), -+ full_commit(DbName), -+ -+ DbName1 = ?tempdb(), -+ couch_config:set("couch_httpd_auth", "authentication_db", -+ ?b2l(DbName1), false), -+ -+ {ok, _} = update_user_doc(DbName1, "joe", "pass5"), -+ full_commit(DbName1), -+ -+ couch_config:set("couch_httpd_auth", "authentication_db", -+ ?b2l(DbName), false), -+ -+ Creds = couch_auth_cache:get_user_creds("joe"), -+ ?assertEqual(PasswordHash, -+ couch_util:get_value(<<"password_sha">>, Creds)) -+ end). -+ -+should_recover_cache_after_shutdown(DbName) -> -+ ?_test(begin -+ PasswordHash = hash_password("pass2"), -+ {ok, Rev0} = update_user_doc(DbName, "joe", "pass1"), -+ {ok, Rev1} = update_user_doc(DbName, "joe", "pass2", Rev0), -+ full_commit(DbName), -+ shutdown_db(DbName), -+ {ok, Rev1} = get_doc_rev(DbName, "joe"), -+ ?assertEqual(PasswordHash, get_user_doc_password_sha(DbName, "joe")) -+ end). -+ -+ -+update_user_doc(DbName, UserName, Password) -> -+ update_user_doc(DbName, UserName, Password, nil). -+ -+update_user_doc(DbName, UserName, Password, Rev) -> -+ User = iolist_to_binary(UserName), -+ Doc = couch_doc:from_json_obj({[ -+ {<<"_id">>, <<"org.couchdb.user:", User/binary>>}, -+ {<<"name">>, User}, -+ {<<"type">>, <<"user">>}, -+ {<<"salt">>, ?SALT}, -+ {<<"password_sha">>, hash_password(Password)}, -+ {<<"roles">>, []} -+ ] ++ case Rev of -+ nil -> []; -+ _ -> [{<<"_rev">>, Rev}] -+ end -+ }), -+ {ok, AuthDb} = couch_db:open_int(DbName, [?ADMIN_USER]), -+ {ok, NewRev} = couch_db:update_doc(AuthDb, Doc, []), -+ ok = couch_db:close(AuthDb), -+ {ok, couch_doc:rev_to_str(NewRev)}. -+ -+hash_password(Password) -> -+ ?l2b(couch_util:to_hex(crypto:sha(iolist_to_binary([Password, ?SALT])))). -+ -+shutdown_db(DbName) -> -+ {ok, AuthDb} = couch_db:open_int(DbName, [?ADMIN_USER]), -+ ok = couch_db:close(AuthDb), -+ couch_util:shutdown_sync(AuthDb#db.main_pid), -+ ok = timer:sleep(1000). -+ -+get_doc_rev(DbName, UserName) -> -+ DocId = iolist_to_binary([<<"org.couchdb.user:">>, UserName]), -+ {ok, AuthDb} = couch_db:open_int(DbName, [?ADMIN_USER]), -+ UpdateRev = -+ case couch_db:open_doc(AuthDb, DocId, []) of -+ {ok, Doc} -> -+ {Props} = couch_doc:to_json_obj(Doc, []), -+ couch_util:get_value(<<"_rev">>, Props); -+ {not_found, missing} -> -+ nil -+ end, -+ ok = couch_db:close(AuthDb), -+ {ok, UpdateRev}. -+ -+get_user_doc_password_sha(DbName, UserName) -> -+ DocId = iolist_to_binary([<<"org.couchdb.user:">>, UserName]), -+ {ok, AuthDb} = couch_db:open_int(DbName, [?ADMIN_USER]), -+ {ok, Doc} = couch_db:open_doc(AuthDb, DocId, []), -+ ok = couch_db:close(AuthDb), -+ {Props} = couch_doc:to_json_obj(Doc, []), -+ couch_util:get_value(<<"password_sha">>, Props). -+ -+delete_user_doc(DbName, UserName) -> -+ DocId = iolist_to_binary([<<"org.couchdb.user:">>, UserName]), -+ {ok, AuthDb} = couch_db:open_int(DbName, [?ADMIN_USER]), -+ {ok, Doc} = couch_db:open_doc(AuthDb, DocId, []), -+ {Props} = couch_doc:to_json_obj(Doc, []), -+ DeletedDoc = couch_doc:from_json_obj({[ -+ {<<"_id">>, DocId}, -+ {<<"_rev">>, couch_util:get_value(<<"_rev">>, Props)}, -+ {<<"_deleted">>, true} -+ ]}), -+ {ok, _} = couch_db:update_doc(AuthDb, DeletedDoc, []), -+ ok = couch_db:close(AuthDb). -+ -+full_commit(DbName) -> -+ {ok, AuthDb} = couch_db:open_int(DbName, [?ADMIN_USER]), -+ {ok, _} = couch_db:ensure_full_commit(AuthDb), -+ ok = couch_db:close(AuthDb). -diff --git a/test/couchdb/couch_btree_tests.erl b/test/couchdb/couch_btree_tests.erl -new file mode 100644 -index 0000000..911640f ---- /dev/null -+++ b/test/couchdb/couch_btree_tests.erl -@@ -0,0 +1,551 @@ -+% Licensed under the Apache License, Version 2.0 (the "License"); you may not -+% use this file except in compliance with the License. You may obtain a copy of -+% the License at -+% -+% http://www.apache.org/licenses/LICENSE-2.0 -+% -+% Unless required by applicable law or agreed to in writing, software -+% distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -+% WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -+% License for the specific language governing permissions and limitations under -+% the License. -+ -+-module(couch_btree_tests). -+ -+-include("couch_eunit.hrl"). -+-include_lib("couchdb/couch_db.hrl"). -+ -+-define(ROWS, 1000). -+ -+ -+setup() -> -+ {ok, Fd} = couch_file:open(?tempfile(), [create, overwrite]), -+ {ok, Btree} = couch_btree:open(nil, Fd, [{compression, none}, -+ {reduce, fun reduce_fun/2}]), -+ {Fd, Btree}. -+ -+setup_kvs(_) -> -+ setup(). -+ -+setup_red() -> -+ {_, 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)), -+ {Fd, Btree} = setup(), -+ {ok, Btree1} = couch_btree:add_remove(Btree, EvenOddKVs, []), -+ {Fd, Btree1}. -+setup_red(_) -> -+ setup_red(). -+ -+teardown(Fd) when is_pid(Fd) -> -+ ok = couch_file:close(Fd); -+teardown({Fd, _}) -> -+ teardown(Fd). -+teardown(_, {Fd, _}) -> -+ teardown(Fd). -+ -+ -+kvs_test_funs() -> -+ [ -+ fun should_set_fd_correctly/2, -+ fun should_set_root_correctly/2, -+ fun should_create_zero_sized_btree/2, -+ fun should_set_reduce_option/2, -+ fun should_fold_over_empty_btree/2, -+ fun should_add_all_keys/2, -+ fun should_continuously_add_new_kv/2, -+ fun should_continuously_remove_keys/2, -+ fun should_insert_keys_in_reversed_order/2, -+ fun should_add_every_odd_key_remove_every_even/2, -+ fun should_add_every_even_key_remove_every_old/2 -+ ]. -+ -+red_test_funs() -> -+ [ -+ fun should_reduce_whole_range/2, -+ fun should_reduce_first_half/2, -+ fun should_reduce_second_half/2 -+ ]. -+ -+ -+btree_open_test_() -> -+ {ok, Fd} = couch_file:open(?tempfile(), [create, overwrite]), -+ {ok, Btree} = couch_btree:open(nil, Fd, [{compression, none}]), -+ { -+ "Ensure that created btree is really a btree record", -+ ?_assert(is_record(Btree, btree)) -+ }. -+ -+sorted_kvs_test_() -> -+ Funs = kvs_test_funs(), -+ Sorted = [{Seq, random:uniform()} || Seq <- lists:seq(1, ?ROWS)], -+ { -+ "BTree with sorted keys", -+ { -+ foreachx, -+ fun setup_kvs/1, fun teardown/2, -+ [{Sorted, Fun} || Fun <- Funs] -+ } -+ }. -+ -+rsorted_kvs_test_() -> -+ Sorted = [{Seq, random:uniform()} || Seq <- lists:seq(1, ?ROWS)], -+ Funs = kvs_test_funs(), -+ Reversed = Sorted, -+ { -+ "BTree with backward sorted keys", -+ { -+ foreachx, -+ fun setup_kvs/1, fun teardown/2, -+ [{Reversed, Fun} || Fun <- Funs] -+ } -+ }. -+ -+shuffled_kvs_test_() -> -+ Funs = kvs_test_funs(), -+ Sorted = [{Seq, random:uniform()} || Seq <- lists:seq(1, ?ROWS)], -+ Shuffled = shuffle(Sorted), -+ { -+ "BTree with shuffled keys", -+ { -+ foreachx, -+ fun setup_kvs/1, fun teardown/2, -+ [{Shuffled, Fun} || Fun <- Funs] -+ } -+ }. -+ -+reductions_test_() -> -+ { -+ "BTree reductions", -+ [ -+ { -+ "Common tests", -+ { -+ foreach, -+ fun setup_red/0, fun teardown/1, -+ [ -+ fun should_reduce_without_specified_direction/1, -+ fun should_reduce_forward/1, -+ fun should_reduce_backward/1 -+ ] -+ } -+ }, -+ { -+ "Range requests", -+ [ -+ { -+ "Forward direction", -+ { -+ foreachx, -+ fun setup_red/1, fun teardown/2, -+ [{fwd, F} || F <- red_test_funs()] -+ } -+ }, -+ { -+ "Backward direction", -+ { -+ foreachx, -+ fun setup_red/1, fun teardown/2, -+ [{rev, F} || F <- red_test_funs()] -+ } -+ } -+ ] -+ } -+ ] -+ }. -+ -+ -+should_set_fd_correctly(_, {Fd, Btree}) -> -+ ?_assertMatch(Fd, Btree#btree.fd). -+ -+should_set_root_correctly(_, {_, Btree}) -> -+ ?_assertMatch(nil, Btree#btree.root). -+ -+should_create_zero_sized_btree(_, {_, Btree}) -> -+ ?_assertMatch(0, couch_btree:size(Btree)). -+ -+should_set_reduce_option(_, {_, Btree}) -> -+ ReduceFun = fun reduce_fun/2, -+ Btree1 = couch_btree:set_options(Btree, [{reduce, ReduceFun}]), -+ ?_assertMatch(ReduceFun, Btree1#btree.reduce). -+ -+should_fold_over_empty_btree(_, {_, Btree}) -> -+ {ok, _, EmptyRes} = couch_btree:foldl(Btree, fun(_, X) -> {ok, X+1} end, 0), -+ ?_assertEqual(EmptyRes, 0). -+ -+should_add_all_keys(KeyValues, {Fd, Btree}) -> -+ {ok, Btree1} = couch_btree:add_remove(Btree, KeyValues, []), -+ [ -+ should_return_complete_btree_on_adding_all_keys(KeyValues, Btree1), -+ should_have_non_zero_size(Btree1), -+ should_have_lesser_size_than_file(Fd, Btree1), -+ should_keep_root_pointer_to_kp_node(Fd, Btree1), -+ should_remove_all_keys(KeyValues, Btree1) -+ ]. -+ -+should_return_complete_btree_on_adding_all_keys(KeyValues, Btree) -> -+ ?_assert(test_btree(Btree, KeyValues)). -+ -+should_have_non_zero_size(Btree) -> -+ ?_assert(couch_btree:size(Btree) > 0). -+ -+should_have_lesser_size_than_file(Fd, Btree) -> -+ ?_assert((couch_btree:size(Btree) =< couch_file:bytes(Fd))). -+ -+should_keep_root_pointer_to_kp_node(Fd, Btree) -> -+ ?_assertMatch({ok, {kp_node, _}}, -+ couch_file:pread_term(Fd, element(1, Btree#btree.root))). -+ -+should_remove_all_keys(KeyValues, Btree) -> -+ Keys = keys(KeyValues), -+ {ok, Btree1} = couch_btree:add_remove(Btree, [], Keys), -+ { -+ "Should remove all the keys", -+ [ -+ should_produce_valid_btree(Btree1, []), -+ should_be_empty(Btree1) -+ ] -+ }. -+ -+should_continuously_add_new_kv(KeyValues, {_, Btree}) -> -+ {Btree1, _} = lists:foldl( -+ fun(KV, {BtAcc, PrevSize}) -> -+ {ok, BtAcc2} = couch_btree:add_remove(BtAcc, [KV], []), -+ ?assert(couch_btree:size(BtAcc2) > PrevSize), -+ {BtAcc2, couch_btree:size(BtAcc2)} -+ end, {Btree, couch_btree:size(Btree)}, KeyValues), -+ { -+ "Should continuously add key-values to btree", -+ [ -+ should_produce_valid_btree(Btree1, KeyValues), -+ should_not_be_empty(Btree1) -+ ] -+ }. -+ -+should_continuously_remove_keys(KeyValues, {_, Btree}) -> -+ {ok, Btree1} = couch_btree:add_remove(Btree, KeyValues, []), -+ {Btree2, _} = lists:foldl( -+ fun({K, _}, {BtAcc, PrevSize}) -> -+ {ok, BtAcc2} = couch_btree:add_remove(BtAcc, [], [K]), -+ ?assert(couch_btree:size(BtAcc2) < PrevSize), -+ {BtAcc2, couch_btree:size(BtAcc2)} -+ end, {Btree1, couch_btree:size(Btree1)}, KeyValues), -+ { -+ "Should continuously remove keys from btree", -+ [ -+ should_produce_valid_btree(Btree2, []), -+ should_be_empty(Btree2) -+ ] -+ }. -+ -+should_insert_keys_in_reversed_order(KeyValues, {_, Btree}) -> -+ KeyValuesRev = lists:reverse(KeyValues), -+ {Btree1, _} = lists:foldl( -+ fun(KV, {BtAcc, PrevSize}) -> -+ {ok, BtAcc2} = couch_btree:add_remove(BtAcc, [KV], []), -+ ?assert(couch_btree:size(BtAcc2) > PrevSize), -+ {BtAcc2, couch_btree:size(BtAcc2)} -+ end, {Btree, couch_btree:size(Btree)}, KeyValuesRev), -+ should_produce_valid_btree(Btree1, KeyValues). -+ -+should_add_every_odd_key_remove_every_even(KeyValues, {_, Btree}) -> -+ {ok, Btree1} = couch_btree:add_remove(Btree, KeyValues, []), -+ {_, 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), -+ ?_assert(test_add_remove(Btree1, Rem2Keys0, Rem2Keys1)). -+ -+should_add_every_even_key_remove_every_old(KeyValues, {_, Btree}) -> -+ {ok, Btree1} = couch_btree:add_remove(Btree, KeyValues, []), -+ {_, 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), -+ ?_assert(test_add_remove(Btree1, Rem2Keys1, Rem2Keys0)). -+ -+ -+should_reduce_without_specified_direction({_, Btree}) -> -+ ?_assertMatch( -+ {ok, [{{"odd", _}, ?ROWS div 2}, {{"even", _}, ?ROWS div 2}]}, -+ fold_reduce(Btree, [])). -+ -+should_reduce_forward({_, Btree}) -> -+ ?_assertMatch( -+ {ok, [{{"odd", _}, ?ROWS div 2}, {{"even", _}, ?ROWS div 2}]}, -+ fold_reduce(Btree, [{dir, fwd}])). -+ -+should_reduce_backward({_, Btree}) -> -+ ?_assertMatch( -+ {ok, [{{"even", _}, ?ROWS div 2}, {{"odd", _}, ?ROWS div 2}]}, -+ fold_reduce(Btree, [{dir, rev}])). -+ -+should_reduce_whole_range(fwd, {_, Btree}) -> -+ {SK, EK} = {{"even", 0}, {"odd", ?ROWS - 1}}, -+ [ -+ { -+ "include endkey", -+ ?_assertMatch( -+ {ok, [{{"odd", 1}, ?ROWS div 2}, -+ {{"even", 2}, ?ROWS div 2}]}, -+ fold_reduce(Btree, [{dir, fwd}, -+ {start_key, SK}, -+ {end_key, EK}])) -+ }, -+ { -+ "exclude endkey", -+ ?_assertMatch( -+ {ok, [{{"odd", 1}, (?ROWS div 2) - 1}, -+ {{"even", 2}, ?ROWS div 2}]}, -+ fold_reduce(Btree, [{dir, fwd}, -+ {start_key, SK}, -+ {end_key_gt, EK}])) -+ } -+ ]; -+should_reduce_whole_range(rev, {_, Btree}) -> -+ {SK, EK} = {{"odd", ?ROWS - 1}, {"even", 2}}, -+ [ -+ { -+ "include endkey", -+ ?_assertMatch( -+ {ok, [{{"even", ?ROWS}, ?ROWS div 2}, -+ {{"odd", ?ROWS - 1}, ?ROWS div 2}]}, -+ fold_reduce(Btree, [{dir, rev}, -+ {start_key, SK}, -+ {end_key, EK}])) -+ }, -+ { -+ "exclude endkey", -+ ?_assertMatch( -+ {ok, [{{"even", ?ROWS}, (?ROWS div 2) - 1}, -+ {{"odd", ?ROWS - 1}, ?ROWS div 2}]}, -+ fold_reduce(Btree, [{dir, rev}, -+ {start_key, SK}, -+ {end_key_gt, EK}])) -+ } -+ ]. -+ -+should_reduce_first_half(fwd, {_, Btree}) -> -+ {SK, EK} = {{"even", 0}, {"odd", (?ROWS div 2) - 1}}, -+ [ -+ { -+ "include endkey", -+ ?_assertMatch( -+ {ok, [{{"odd", 1}, ?ROWS div 4}, -+ {{"even", 2}, ?ROWS div 2}]}, -+ fold_reduce(Btree, [{dir, fwd}, -+ {start_key, SK}, {end_key, EK}])) -+ }, -+ { -+ "exclude endkey", -+ ?_assertMatch( -+ {ok, [{{"odd", 1}, (?ROWS div 4) - 1}, -+ {{"even", 2}, ?ROWS div 2}]}, -+ fold_reduce(Btree, [{dir, fwd}, -+ {start_key, SK}, -+ {end_key_gt, EK}])) -+ } -+ ]; -+should_reduce_first_half(rev, {_, Btree}) -> -+ {SK, EK} = {{"odd", ?ROWS - 1}, {"even", ?ROWS div 2}}, -+ [ -+ { -+ "include endkey", -+ ?_assertMatch( -+ {ok, [{{"even", ?ROWS}, (?ROWS div 4) + 1}, -+ {{"odd", ?ROWS - 1}, ?ROWS div 2}]}, -+ fold_reduce(Btree, [{dir, rev}, -+ {start_key, SK}, -+ {end_key, EK}])) -+ }, -+ { -+ "exclude endkey", -+ ?_assertMatch( -+ {ok, [{{"even", ?ROWS}, ?ROWS div 4}, -+ {{"odd", ?ROWS - 1}, ?ROWS div 2}]}, -+ fold_reduce(Btree, [{dir, rev}, -+ {start_key, SK}, -+ {end_key_gt, EK}])) -+ } -+ ]. -+ -+should_reduce_second_half(fwd, {_, Btree}) -> -+ {SK, EK} = {{"even", ?ROWS div 2}, {"odd", ?ROWS - 1}}, -+ [ -+ { -+ "include endkey", -+ ?_assertMatch( -+ {ok, [{{"odd", 1}, ?ROWS div 2}, -+ {{"even", ?ROWS div 2}, (?ROWS div 4) + 1}]}, -+ fold_reduce(Btree, [{dir, fwd}, -+ {start_key, SK}, -+ {end_key, EK}])) -+ }, -+ { -+ "exclude endkey", -+ ?_assertMatch( -+ {ok, [{{"odd", 1}, (?ROWS div 2) - 1}, -+ {{"even", ?ROWS div 2}, (?ROWS div 4) + 1}]}, -+ fold_reduce(Btree, [{dir, fwd}, -+ {start_key, SK}, -+ {end_key_gt, EK}])) -+ } -+ ]; -+should_reduce_second_half(rev, {_, Btree}) -> -+ {SK, EK} = {{"odd", (?ROWS div 2) + 1}, {"even", 2}}, -+ [ -+ { -+ "include endkey", -+ ?_assertMatch( -+ {ok, [{{"even", ?ROWS}, ?ROWS div 2}, -+ {{"odd", (?ROWS div 2) + 1}, (?ROWS div 4) + 1}]}, -+ fold_reduce(Btree, [{dir, rev}, -+ {start_key, SK}, -+ {end_key, EK}])) -+ }, -+ { -+ "exclude endkey", -+ ?_assertMatch( -+ {ok, [{{"even", ?ROWS}, (?ROWS div 2) - 1}, -+ {{"odd", (?ROWS div 2) + 1}, (?ROWS div 4) + 1}]}, -+ fold_reduce(Btree, [{dir, rev}, -+ {start_key, SK}, -+ {end_key_gt, EK}])) -+ } -+ ]. -+ -+should_produce_valid_btree(Btree, KeyValues) -> -+ ?_assert(test_btree(Btree, KeyValues)). -+ -+should_be_empty(Btree) -> -+ ?_assertEqual(couch_btree:size(Btree), 0). -+ -+should_not_be_empty(Btree) -> -+ ?_assert(couch_btree:size(Btree) > 0). -+ -+fold_reduce(Btree, Opts) -> -+ GroupFun = fun({K1, _}, {K2, _}) -> -+ K1 == K2 -+ end, -+ FoldFun = fun(GroupedKey, Unreduced, Acc) -> -+ {ok, [{GroupedKey, couch_btree:final_reduce(Btree, Unreduced)} | Acc]} -+ end, -+ couch_btree:fold_reduce(Btree, FoldFun, [], -+ [{key_group_fun, GroupFun}] ++ Opts). -+ -+ -+keys(KVs) -> -+ [K || {K, _} <- KVs]. -+ -+reduce_fun(reduce, KVs) -> -+ length(KVs); -+reduce_fun(rereduce, Reds) -> -+ lists:sum(Reds). -+ -+ -+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. -+ -+test_btree(Btree, KeyValues) -> -+ ok = test_key_access(Btree, KeyValues), -+ ok = test_lookup_access(Btree, KeyValues), -+ ok = test_final_reductions(Btree, KeyValues), -+ ok = test_traversal_callbacks(Btree, KeyValues), -+ true. -+ -+test_add_remove(Btree, OutKeyValues, RemainingKeyValues) -> -+ Btree2 = lists:foldl( -+ fun({K, _}, BtAcc) -> -+ {ok, BtAcc2} = couch_btree:add_remove(BtAcc, [], [K]), -+ BtAcc2 -+ end, Btree, OutKeyValues), -+ true = test_btree(Btree2, RemainingKeyValues), -+ -+ Btree3 = lists:foldl( -+ fun(KV, BtAcc) -> -+ {ok, BtAcc2} = couch_btree:add_remove(BtAcc, [KV], []), -+ BtAcc2 -+ end, Btree2, OutKeyValues), -+ true = test_btree(Btree3, OutKeyValues ++ RemainingKeyValues). -+ -+test_key_access(Btree, List) -> -+ FoldFun = fun(Element, {[HAcc|TAcc], Count}) -> -+ case Element == HAcc of -+ true -> {ok, {TAcc, Count + 1}}; -+ _ -> {ok, {TAcc, Count + 1}} -+ end -+ end, -+ Length = length(List), -+ Sorted = lists:sort(List), -+ {ok, _, {[], Length}} = couch_btree:foldl(Btree, FoldFun, {Sorted, 0}), -+ {ok, _, {[], Length}} = couch_btree:fold(Btree, FoldFun, -+ {Sorted, 0}, [{dir, rev}]), -+ ok. -+ -+test_lookup_access(Btree, KeyValues) -> -+ FoldFun = fun({Key, Value}, {Key, Value}) -> {stop, true} end, -+ lists:foreach( -+ fun({Key, Value}) -> -+ [{ok, {Key, Value}}] = couch_btree:lookup(Btree, [Key]), -+ {ok, _, true} = couch_btree:foldl(Btree, FoldFun, -+ {Key, Value}, [{start_key, Key}]) -+ end, KeyValues). -+ -+test_final_reductions(Btree, KeyValues) -> -+ KVLen = length(KeyValues), -+ FoldLFun = fun(_X, LeadingReds, Acc) -> -+ CountToStart = KVLen div 3 + Acc, -+ CountToStart = couch_btree:final_reduce(Btree, LeadingReds), -+ {ok, Acc + 1} -+ end, -+ FoldRFun = fun(_X, LeadingReds, Acc) -> -+ CountToEnd = KVLen - KVLen div 3 + Acc, -+ CountToEnd = couch_btree:final_reduce(Btree, LeadingReds), -+ {ok, Acc + 1} -+ end, -+ {LStartKey, _} = case KVLen of -+ 0 -> {nil, nil}; -+ _ -> lists:nth(KVLen div 3 + 1, lists:sort(KeyValues)) -+ end, -+ {RStartKey, _} = case KVLen of -+ 0 -> {nil, nil}; -+ _ -> lists:nth(KVLen div 3, lists:sort(KeyValues)) -+ end, -+ {ok, _, FoldLRed} = couch_btree:foldl(Btree, FoldLFun, 0, -+ [{start_key, LStartKey}]), -+ {ok, _, FoldRRed} = couch_btree:fold(Btree, FoldRFun, 0, -+ [{dir, rev}, {start_key, RStartKey}]), -+ KVLen = FoldLRed + FoldRRed, -+ ok. -+ -+test_traversal_callbacks(Btree, _KeyValues) -> -+ FoldFun = fun -+ (visit, _GroupedKey, _Unreduced, Acc) -> -+ {ok, Acc andalso false}; -+ (traverse, _LK, _Red, Acc) -> -+ {skip, Acc andalso true} -+ end, -+ % With 250 items the root is a kp. Always skipping should reduce to true. -+ {ok, _, true} = couch_btree:fold(Btree, FoldFun, true, [{dir, fwd}]), -+ ok. -diff --git a/test/couchdb/couch_changes_tests.erl b/test/couchdb/couch_changes_tests.erl -new file mode 100644 -index 0000000..a129ba2 ---- /dev/null -+++ b/test/couchdb/couch_changes_tests.erl -@@ -0,0 +1,612 @@ -+% Licensed under the Apache License, Version 2.0 (the "License"); you may not -+% use this file except in compliance with the License. You may obtain a copy of -+% the License at -+% -+% http://www.apache.org/licenses/LICENSE-2.0 -+% -+% Unless required by applicable law or agreed to in writing, software -+% distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -+% WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -+% License for the specific language governing permissions and limitations under -+% the License. -+ -+-module(couch_changes_tests). -+ -+-include("couch_eunit.hrl"). -+-include_lib("couchdb/couch_db.hrl"). -+ -+-define(ADMIN_USER, {user_ctx, #user_ctx{roles = [<<"_admin">>]}}). -+-define(TIMEOUT, 3000). -+-define(TEST_TIMEOUT, 10000). -+ -+-record(row, { -+ id, -+ seq, -+ deleted = false -+}). -+ -+ -+start() -> -+ {ok, Pid} = couch_server_sup:start_link(?CONFIG_CHAIN), -+ Pid. -+ -+stop(Pid) -> -+ erlang:monitor(process, Pid), -+ couch_server_sup:stop(), -+ receive -+ {'DOWN', _, _, Pid, _} -> -+ ok -+ after ?TIMEOUT -> -+ throw({timeout, server_stop}) -+ end. -+ -+setup() -> -+ DbName = ?tempdb(), -+ {ok, Db} = create_db(DbName), -+ Revs = [R || {ok, R} <- [ -+ save_doc(Db, {[{<<"_id">>, <<"doc1">>}]}), -+ save_doc(Db, {[{<<"_id">>, <<"doc2">>}]}), -+ save_doc(Db, {[{<<"_id">>, <<"doc3">>}]}), -+ save_doc(Db, {[{<<"_id">>, <<"doc4">>}]}), -+ save_doc(Db, {[{<<"_id">>, <<"doc5">>}]}) -+ ]], -+ Rev = lists:nth(3, Revs), -+ {ok, Rev1} = save_doc(Db, {[{<<"_id">>, <<"doc3">>}, {<<"_rev">>, Rev}]}), -+ Revs1 = Revs ++ [Rev1], -+ Revs2 = Revs1 ++ [R || {ok, R} <- [ -+ save_doc(Db, {[{<<"_id">>, <<"doc6">>}]}), -+ save_doc(Db, {[{<<"_id">>, <<"_design/foo">>}]}), -+ save_doc(Db, {[{<<"_id">>, <<"doc7">>}]}), -+ save_doc(Db, {[{<<"_id">>, <<"doc8">>}]}) -+ ]], -+ {DbName, list_to_tuple(Revs2)}. -+ -+teardown({DbName, _}) -> -+ delete_db(DbName), -+ ok. -+ -+ -+changes_test_() -> -+ { -+ "Changes feeed", -+ { -+ setup, -+ fun start/0, fun stop/1, -+ [ -+ filter_by_doc_id(), -+ filter_by_design(), -+ continuous_feed(), -+ filter_by_custom_function() -+ ] -+ } -+ }. -+ -+filter_by_doc_id() -> -+ { -+ "Filter _doc_id", -+ { -+ foreach, -+ fun setup/0, fun teardown/1, -+ [ -+ fun should_filter_by_specific_doc_ids/1, -+ fun should_filter_by_specific_doc_ids_descending/1, -+ fun should_filter_by_specific_doc_ids_with_since/1, -+ fun should_filter_by_specific_doc_ids_no_result/1, -+ fun should_handle_deleted_docs/1 -+ ] -+ } -+ }. -+ -+filter_by_design() -> -+ { -+ "Filter _design", -+ { -+ foreach, -+ fun setup/0, fun teardown/1, -+ [ -+ fun should_emit_only_design_documents/1 -+ ] -+ } -+ }. -+ -+filter_by_custom_function() -> -+ { -+ "Filter function", -+ { -+ foreach, -+ fun setup/0, fun teardown/1, -+ [ -+ fun should_receive_heartbeats/1 -+ ] -+ } -+ }. -+ -+continuous_feed() -> -+ { -+ "Continuous Feed", -+ { -+ foreach, -+ fun setup/0, fun teardown/1, -+ [ -+ fun should_filter_continuous_feed_by_specific_doc_ids/1 -+ ] -+ } -+ }. -+ -+ -+should_filter_by_specific_doc_ids({DbName, _}) -> -+ ?_test( -+ begin -+ ChangesArgs = #changes_args{ -+ filter = "_doc_ids" -+ }, -+ DocIds = [<<"doc3">>, <<"doc4">>, <<"doc9999">>], -+ Req = {json_req, {[{<<"doc_ids">>, DocIds}]}}, -+ Consumer = spawn_consumer(DbName, ChangesArgs, Req), -+ -+ {Rows, LastSeq} = wait_finished(Consumer), -+ {ok, Db} = couch_db:open_int(DbName, []), -+ UpSeq = couch_db:get_update_seq(Db), -+ couch_db:close(Db), -+ stop_consumer(Consumer), -+ -+ ?assertEqual(2, length(Rows)), -+ [#row{seq = Seq1, id = Id1}, #row{seq = Seq2, id = Id2}] = Rows, -+ ?assertEqual(<<"doc4">>, Id1), -+ ?assertEqual(4, Seq1), -+ ?assertEqual(<<"doc3">>, Id2), -+ ?assertEqual(6, Seq2), -+ ?assertEqual(UpSeq, LastSeq) -+ end). -+ -+should_filter_by_specific_doc_ids_descending({DbName, _}) -> -+ ?_test( -+ begin -+ ChangesArgs = #changes_args{ -+ filter = "_doc_ids", -+ dir = rev -+ }, -+ DocIds = [<<"doc3">>, <<"doc4">>, <<"doc9999">>], -+ Req = {json_req, {[{<<"doc_ids">>, DocIds}]}}, -+ Consumer = spawn_consumer(DbName, ChangesArgs, Req), -+ -+ {Rows, LastSeq} = wait_finished(Consumer), -+ {ok, Db} = couch_db:open_int(DbName, []), -+ couch_db:close(Db), -+ stop_consumer(Consumer), -+ -+ ?assertEqual(2, length(Rows)), -+ [#row{seq = Seq1, id = Id1}, #row{seq = Seq2, id = Id2}] = Rows, -+ ?assertEqual(<<"doc3">>, Id1), -+ ?assertEqual(6, Seq1), -+ ?assertEqual(<<"doc4">>, Id2), -+ ?assertEqual(4, Seq2), -+ ?assertEqual(4, LastSeq) -+ end). -+ -+should_filter_by_specific_doc_ids_with_since({DbName, _}) -> -+ ?_test( -+ begin -+ ChangesArgs = #changes_args{ -+ filter = "_doc_ids", -+ since = 5 -+ }, -+ DocIds = [<<"doc3">>, <<"doc4">>, <<"doc9999">>], -+ Req = {json_req, {[{<<"doc_ids">>, DocIds}]}}, -+ Consumer = spawn_consumer(DbName, ChangesArgs, Req), -+ -+ {Rows, LastSeq} = wait_finished(Consumer), -+ {ok, Db} = couch_db:open_int(DbName, []), -+ UpSeq = couch_db:get_update_seq(Db), -+ couch_db:close(Db), -+ stop_consumer(Consumer), -+ -+ ?assertEqual(1, length(Rows)), -+ [#row{seq = Seq1, id = Id1}] = Rows, -+ ?assertEqual(<<"doc3">>, Id1), -+ ?assertEqual(6, Seq1), -+ ?assertEqual(UpSeq, LastSeq) -+ end). -+ -+should_filter_by_specific_doc_ids_no_result({DbName, _}) -> -+ ?_test( -+ begin -+ ChangesArgs = #changes_args{ -+ filter = "_doc_ids", -+ since = 6 -+ }, -+ DocIds = [<<"doc3">>, <<"doc4">>, <<"doc9999">>], -+ Req = {json_req, {[{<<"doc_ids">>, DocIds}]}}, -+ Consumer = spawn_consumer(DbName, ChangesArgs, Req), -+ -+ {Rows, LastSeq} = wait_finished(Consumer), -+ {ok, Db} = couch_db:open_int(DbName, []), -+ UpSeq = couch_db:get_update_seq(Db), -+ couch_db:close(Db), -+ stop_consumer(Consumer), -+ -+ ?assertEqual(0, length(Rows)), -+ ?assertEqual(UpSeq, LastSeq) -+ end). -+ -+should_handle_deleted_docs({DbName, Revs}) -> -+ ?_test( -+ begin -+ Rev3_2 = element(6, Revs), -+ {ok, Db} = couch_db:open_int(DbName, []), -+ {ok, _} = save_doc( -+ Db, -+ {[{<<"_id">>, <<"doc3">>}, -+ {<<"_deleted">>, true}, -+ {<<"_rev">>, Rev3_2}]}), -+ -+ ChangesArgs = #changes_args{ -+ filter = "_doc_ids", -+ since = 9 -+ }, -+ DocIds = [<<"doc3">>, <<"doc4">>, <<"doc9999">>], -+ Req = {json_req, {[{<<"doc_ids">>, DocIds}]}}, -+ Consumer = spawn_consumer(DbName, ChangesArgs, Req), -+ -+ {Rows, LastSeq} = wait_finished(Consumer), -+ couch_db:close(Db), -+ stop_consumer(Consumer), -+ -+ ?assertEqual(1, length(Rows)), -+ ?assertMatch( -+ [#row{seq = LastSeq, id = <<"doc3">>, deleted = true}], -+ Rows -+ ), -+ ?assertEqual(11, LastSeq) -+ end). -+ -+should_filter_continuous_feed_by_specific_doc_ids({DbName, Revs}) -> -+ ?_test( -+ begin -+ {ok, Db} = couch_db:open_int(DbName, []), -+ ChangesArgs = #changes_args{ -+ filter = "_doc_ids", -+ feed = "continuous" -+ }, -+ DocIds = [<<"doc3">>, <<"doc4">>, <<"doc9999">>], -+ Req = {json_req, {[{<<"doc_ids">>, DocIds}]}}, -+ Consumer = spawn_consumer(DbName, ChangesArgs, Req), -+ pause(Consumer), -+ -+ Rows = get_rows(Consumer), -+ ?assertEqual(2, length(Rows)), -+ [#row{seq = Seq1, id = Id1}, #row{seq = Seq2, id = Id2}] = Rows, -+ ?assertEqual(<<"doc4">>, Id1), -+ ?assertEqual(4, Seq1), -+ ?assertEqual(<<"doc3">>, Id2), -+ ?assertEqual(6, Seq2), -+ -+ clear_rows(Consumer), -+ {ok, _Rev9} = save_doc(Db, {[{<<"_id">>, <<"doc9">>}]}), -+ {ok, _Rev10} = save_doc(Db, {[{<<"_id">>, <<"doc10">>}]}), -+ unpause(Consumer), -+ pause(Consumer), -+ ?assertEqual([], get_rows(Consumer)), -+ -+ Rev4 = element(4, Revs), -+ Rev3_2 = element(6, Revs), -+ {ok, Rev4_2} = save_doc(Db, {[{<<"_id">>, <<"doc4">>}, -+ {<<"_rev">>, Rev4}]}), -+ {ok, _} = save_doc(Db, {[{<<"_id">>, <<"doc11">>}]}), -+ {ok, _} = save_doc(Db, {[{<<"_id">>, <<"doc4">>}, -+ {<<"_rev">>, Rev4_2}]}), -+ {ok, _} = save_doc(Db, {[{<<"_id">>, <<"doc12">>}]}), -+ {ok, Rev3_3} = save_doc(Db, {[{<<"_id">>, <<"doc3">>}, -+ {<<"_rev">>, Rev3_2}]}), -+ unpause(Consumer), -+ pause(Consumer), -+ -+ NewRows = get_rows(Consumer), -+ ?assertEqual(2, length(NewRows)), -+ [Row14, Row16] = NewRows, -+ ?assertEqual(<<"doc4">>, Row14#row.id), -+ ?assertEqual(15, Row14#row.seq), -+ ?assertEqual(<<"doc3">>, Row16#row.id), -+ ?assertEqual(17, Row16#row.seq), -+ -+ clear_rows(Consumer), -+ {ok, _Rev3_4} = save_doc(Db, {[{<<"_id">>, <<"doc3">>}, -+ {<<"_rev">>, Rev3_3}]}), -+ unpause(Consumer), -+ pause(Consumer), -+ -+ FinalRows = get_rows(Consumer), -+ -+ unpause(Consumer), -+ stop_consumer(Consumer), -+ -+ ?assertMatch([#row{seq = 18, id = <<"doc3">>}], FinalRows) -+ end). -+ -+should_emit_only_design_documents({DbName, Revs}) -> -+ ?_test( -+ begin -+ ChangesArgs = #changes_args{ -+ filter = "_design" -+ }, -+ Consumer = spawn_consumer(DbName, ChangesArgs, {json_req, null}), -+ -+ {Rows, LastSeq} = wait_finished(Consumer), -+ {ok, Db} = couch_db:open_int(DbName, []), -+ UpSeq = couch_db:get_update_seq(Db), -+ couch_db:close(Db), -+ -+ ?assertEqual(1, length(Rows)), -+ ?assertEqual(UpSeq, LastSeq), -+ ?assertEqual([#row{seq = 8, id = <<"_design/foo">>}], Rows), -+ -+ stop_consumer(Consumer), -+ -+ {ok, Db2} = couch_db:open_int(DbName, [?ADMIN_USER]), -+ {ok, _} = save_doc(Db2, {[{<<"_id">>, <<"_design/foo">>}, -+ {<<"_rev">>, element(8, Revs)}, -+ {<<"_deleted">>, true}]}), -+ -+ Consumer2 = spawn_consumer(DbName, ChangesArgs, {json_req, null}), -+ -+ {Rows2, LastSeq2} = wait_finished(Consumer2), -+ UpSeq2 = UpSeq + 1, -+ couch_db:close(Db2), -+ -+ ?assertEqual(1, length(Rows2)), -+ ?assertEqual(UpSeq2, LastSeq2), -+ ?assertEqual([#row{seq = 11, -+ id = <<"_design/foo">>, -+ deleted = true}], -+ Rows2) -+ end). -+ -+should_receive_heartbeats(_) -> -+ {timeout, ?TEST_TIMEOUT div 1000, -+ ?_test( -+ begin -+ DbName = ?tempdb(), -+ Timeout = 100, -+ {ok, Db} = create_db(DbName), -+ -+ {ok, _} = save_doc(Db, {[ -+ {<<"_id">>, <<"_design/filtered">>}, -+ {<<"language">>, <<"javascript">>}, -+ {<<"filters">>, {[ -+ {<<"foo">>, <<"function(doc) { -+ return ['doc10', 'doc11', 'doc12'].indexOf(doc._id) != -1;}">> -+ }]}} -+ ]}), -+ -+ ChangesArgs = #changes_args{ -+ filter = "filtered/foo", -+ feed = "continuous", -+ timeout = 10000, -+ heartbeat = 1000 -+ }, -+ Consumer = spawn_consumer(DbName, ChangesArgs, {json_req, null}), -+ -+ {ok, _Rev1} = save_doc(Db, {[{<<"_id">>, <<"doc1">>}]}), -+ timer:sleep(Timeout), -+ {ok, _Rev2} = save_doc(Db, {[{<<"_id">>, <<"doc2">>}]}), -+ timer:sleep(Timeout), -+ {ok, _Rev3} = save_doc(Db, {[{<<"_id">>, <<"doc3">>}]}), -+ timer:sleep(Timeout), -+ {ok, _Rev4} = save_doc(Db, {[{<<"_id">>, <<"doc4">>}]}), -+ timer:sleep(Timeout), -+ {ok, _Rev5} = save_doc(Db, {[{<<"_id">>, <<"doc5">>}]}), -+ timer:sleep(Timeout), -+ {ok, _Rev6} = save_doc(Db, {[{<<"_id">>, <<"doc6">>}]}), -+ timer:sleep(Timeout), -+ {ok, _Rev7} = save_doc(Db, {[{<<"_id">>, <<"doc7">>}]}), -+ timer:sleep(Timeout), -+ {ok, _Rev8} = save_doc(Db, {[{<<"_id">>, <<"doc8">>}]}), -+ timer:sleep(Timeout), -+ {ok, _Rev9} = save_doc(Db, {[{<<"_id">>, <<"doc9">>}]}), -+ -+ Heartbeats = get_heartbeats(Consumer), -+ ?assert(Heartbeats > 0), -+ -+ {ok, _Rev10} = save_doc(Db, {[{<<"_id">>, <<"doc10">>}]}), -+ timer:sleep(Timeout), -+ {ok, _Rev11} = save_doc(Db, {[{<<"_id">>, <<"doc11">>}]}), -+ timer:sleep(Timeout), -+ {ok, _Rev12} = save_doc(Db, {[{<<"_id">>, <<"doc12">>}]}), -+ -+ Heartbeats2 = get_heartbeats(Consumer), -+ ?assert(Heartbeats2 > Heartbeats), -+ -+ Rows = get_rows(Consumer), -+ ?assertEqual(3, length(Rows)), -+ -+ {ok, _Rev13} = save_doc(Db, {[{<<"_id">>, <<"doc13">>}]}), -+ timer:sleep(Timeout), -+ {ok, _Rev14} = save_doc(Db, {[{<<"_id">>, <<"doc14">>}]}), -+ timer:sleep(Timeout), -+ -+ Heartbeats3 = get_heartbeats(Consumer), -+ ?assert(Heartbeats3 > Heartbeats2) -+ end)}. -+ -+ -+save_doc(Db, Json) -> -+ Doc = couch_doc:from_json_obj(Json), -+ {ok, Rev} = couch_db:update_doc(Db, Doc, []), -+ {ok, couch_doc:rev_to_str(Rev)}. -+ -+get_rows(Consumer) -> -+ Ref = make_ref(), -+ Consumer ! {get_rows, Ref}, -+ Resp = receive -+ {rows, Ref, Rows} -> -+ Rows -+ after ?TIMEOUT -> -+ timeout -+ end, -+ ?assertNotEqual(timeout, Resp), -+ Resp. -+ -+get_heartbeats(Consumer) -> -+ Ref = make_ref(), -+ Consumer ! {get_heartbeats, Ref}, -+ Resp = receive -+ {hearthbeats, Ref, HeartBeats} -> -+ HeartBeats -+ after ?TIMEOUT -> -+ timeout -+ end, -+ ?assertNotEqual(timeout, Resp), -+ Resp. -+ -+clear_rows(Consumer) -> -+ Ref = make_ref(), -+ Consumer ! {reset, Ref}, -+ Resp = receive -+ {ok, Ref} -> -+ ok -+ after ?TIMEOUT -> -+ timeout -+ end, -+ ?assertNotEqual(timeout, Resp), -+ Resp. -+ -+stop_consumer(Consumer) -> -+ Ref = make_ref(), -+ Consumer ! {stop, Ref}, -+ Resp = receive -+ {ok, Ref} -> -+ ok -+ after ?TIMEOUT -> -+ timeout -+ end, -+ ?assertNotEqual(timeout, Resp), -+ Resp. -+ -+pause(Consumer) -> -+ Ref = make_ref(), -+ Consumer ! {pause, Ref}, -+ Resp = receive -+ {paused, Ref} -> -+ ok -+ after ?TIMEOUT -> -+ timeout -+ end, -+ ?assertNotEqual(timeout, Resp), -+ Resp. -+ -+unpause(Consumer) -> -+ Ref = make_ref(), -+ Consumer ! {continue, Ref}, -+ Resp = receive -+ {ok, Ref} -> -+ ok -+ after ?TIMEOUT -> -+ timeout -+ end, -+ ?assertNotEqual(timeout, Resp), -+ Resp. -+ -+wait_finished(_Consumer) -> -+ Resp = receive -+ {consumer_finished, Rows, LastSeq} -> -+ {Rows, LastSeq} -+ after ?TIMEOUT -> -+ timeout -+ end, -+ ?assertNotEqual(timeout, Resp), -+ Resp. -+ -+spawn_consumer(DbName, ChangesArgs0, Req) -> -+ Parent = self(), -+ spawn(fun() -> -+ put(heartbeat_count, 0), -+ Callback = fun -+ ({change, {Change}, _}, _, Acc) -> -+ Id = couch_util:get_value(<<"id">>, Change), -+ Seq = couch_util:get_value(<<"seq">>, Change), -+ Del = couch_util:get_value(<<"deleted">>, Change, false), -+ [#row{id = Id, seq = Seq, deleted = Del} | Acc]; -+ ({stop, LastSeq}, _, Acc) -> -+ Parent ! {consumer_finished, lists:reverse(Acc), LastSeq}, -+ stop_loop(Parent, Acc); -+ (timeout, _, Acc) -> -+ put(heartbeat_count, get(heartbeat_count) + 1), -+ maybe_pause(Parent, Acc); -+ (_, _, Acc) -> -+ maybe_pause(Parent, Acc) -+ end, -+ {ok, Db} = couch_db:open_int(DbName, []), -+ ChangesArgs = case (ChangesArgs0#changes_args.timeout =:= undefined) -+ andalso (ChangesArgs0#changes_args.heartbeat =:= undefined) of -+ true -> -+ ChangesArgs0#changes_args{timeout = 10, heartbeat = 10}; -+ false -> -+ ChangesArgs0 -+ end, -+ FeedFun = couch_changes:handle_changes(ChangesArgs, Req, Db), -+ try -+ FeedFun({Callback, []}) -+ catch throw:{stop, _} -> -+ ok -+ end, -+ catch couch_db:close(Db) -+ end). -+ -+maybe_pause(Parent, Acc) -> -+ receive -+ {get_rows, Ref} -> -+ Parent ! {rows, Ref, lists:reverse(Acc)}, -+ maybe_pause(Parent, Acc); -+ {get_heartbeats, Ref} -> -+ Parent ! {hearthbeats, Ref, get(heartbeat_count)}, -+ maybe_pause(Parent, Acc); -+ {reset, Ref} -> -+ Parent ! {ok, Ref}, -+ maybe_pause(Parent, []); -+ {pause, Ref} -> -+ Parent ! {paused, Ref}, -+ pause_loop(Parent, Acc); -+ {stop, Ref} -> -+ Parent ! {ok, Ref}, -+ throw({stop, Acc}); -+ V -> -+ erlang:error({assertion_failed, -+ [{module, ?MODULE}, -+ {line, ?LINE}, -+ {value, V}, -+ {reason, "Received unexpected message"}]}) -+ after 0 -> -+ Acc -+ end. -+ -+pause_loop(Parent, Acc) -> -+ receive -+ {stop, Ref} -> -+ Parent ! {ok, Ref}, -+ throw({stop, Acc}); -+ {reset, Ref} -> -+ Parent ! {ok, Ref}, -+ pause_loop(Parent, []); -+ {continue, Ref} -> -+ Parent ! {ok, Ref}, -+ Acc; -+ {get_rows, Ref} -> -+ Parent ! {rows, Ref, lists:reverse(Acc)}, -+ pause_loop(Parent, Acc) -+ end. -+ -+stop_loop(Parent, Acc) -> -+ receive -+ {get_rows, Ref} -> -+ Parent ! {rows, Ref, lists:reverse(Acc)}, -+ stop_loop(Parent, Acc); -+ {stop, Ref} -> -+ Parent ! {ok, Ref}, -+ Acc -+ end. -+ -+create_db(DbName) -> -+ couch_db:create(DbName, [?ADMIN_USER, overwrite]). -+ -+delete_db(DbName) -> -+ ok = couch_server:delete(DbName, [?ADMIN_USER]). -diff --git a/test/couchdb/couch_config_tests.erl b/test/couchdb/couch_config_tests.erl -new file mode 100644 -index 0000000..9e9dfe7 ---- /dev/null -+++ b/test/couchdb/couch_config_tests.erl -@@ -0,0 +1,463 @@ -+% Licensed under the Apache License, Version 2.0 (the "License"); you may not -+% use this file except in compliance with the License. You may obtain a copy of -+% the License at -+% -+% http://www.apache.org/licenses/LICENSE-2.0 -+% -+% Unless required by applicable law or agreed to in writing, software -+% distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -+% WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -+% License for the specific language governing permissions and limitations under -+% the License. -+ -+-module(couch_config_tests). -+ -+-include("couch_eunit.hrl"). -+-include_lib("couchdb/couch_db.hrl"). -+ -+-define(SHORT_TIMEOUT, 100). -+-define(TIMEOUT, 1000). -+ -+-define(CONFIG_DEFAULT, -+ filename:join([?BUILDDIR, "etc", "couchdb", "default_dev.ini"])). -+-define(CONFIG_FIXTURE_1, -+ filename:join([?FIXTURESDIR, "couch_config_tests_1.ini"])). -+-define(CONFIG_FIXTURE_2, -+ filename:join([?FIXTURESDIR, "couch_config_tests_2.ini"])). -+-define(CONFIG_FIXTURE_TEMP, -+ begin -+ FileName = filename:join([?TEMPDIR, "couch_config_temp.ini"]), -+ {ok, Fd} = file:open(FileName, write), -+ ok = file:truncate(Fd), -+ ok = file:close(Fd), -+ FileName -+ end). -+ -+ -+setup() -> -+ setup(?CONFIG_CHAIN). -+setup({temporary, Chain}) -> -+ setup(Chain); -+setup({persistent, Chain}) -> -+ setup(lists:append(Chain, [?CONFIG_FIXTURE_TEMP])); -+setup(Chain) -> -+ {ok, Pid} = couch_config:start_link(Chain), -+ Pid. -+ -+setup_empty() -> -+ setup([]). -+ -+setup_register() -> -+ ConfigPid = setup(), -+ SentinelFunc = fun() -> -+ % Ping/Pong to make sure we wait for this -+ % process to die -+ receive -+ {ping, From} -> -+ From ! pong -+ end -+ end, -+ SentinelPid = spawn(SentinelFunc), -+ {ConfigPid, SentinelPid}. -+ -+teardown({ConfigPid, SentinelPid}) -> -+ teardown(ConfigPid), -+ case process_info(SentinelPid) of -+ undefined -> ok; -+ _ -> -+ SentinelPid ! {ping, self()}, -+ receive -+ pong -> -+ ok -+ after 100 -> -+ throw({timeout_error, registered_pid}) -+ end -+ end; -+teardown(Pid) -> -+ couch_config:stop(), -+ erlang:monitor(process, Pid), -+ receive -+ {'DOWN', _, _, Pid, _} -> -+ ok -+ after ?TIMEOUT -> -+ throw({timeout_error, config_stop}) -+ end. -+teardown(_, Pid) -> -+ teardown(Pid). -+ -+ -+couch_config_test_() -> -+ { -+ "CouchDB config tests", -+ [ -+ couch_config_get_tests(), -+ couch_config_set_tests(), -+ couch_config_del_tests(), -+ config_override_tests(), -+ config_persistent_changes_tests(), -+ config_register_tests(), -+ config_no_files_tests() -+ ] -+ }. -+ -+couch_config_get_tests() -> -+ { -+ "Config get tests", -+ { -+ foreach, -+ fun setup/0, fun teardown/1, -+ [ -+ should_load_all_configs(), -+ should_locate_daemons_section(), -+ should_locate_mrview_handler(), -+ should_return_undefined_atom_on_missed_section(), -+ should_return_undefined_atom_on_missed_option(), -+ should_return_custom_default_value_on_missed_option(), -+ should_only_return_default_on_missed_option(), -+ should_get_binary_option() -+ ] -+ } -+ }. -+ -+couch_config_set_tests() -> -+ { -+ "Config set tests", -+ { -+ foreach, -+ fun setup/0, fun teardown/1, -+ [ -+ should_update_option(), -+ should_create_new_section(), -+ should_set_binary_option() -+ ] -+ } -+ }. -+ -+couch_config_del_tests() -> -+ { -+ "Config deletion tests", -+ { -+ foreach, -+ fun setup/0, fun teardown/1, -+ [ -+ should_return_undefined_atom_after_option_deletion(), -+ should_be_ok_on_deleting_unknown_options(), -+ should_delete_binary_option() -+ ] -+ } -+ }. -+ -+config_override_tests() -> -+ { -+ "Configs overide tests", -+ { -+ foreachx, -+ fun setup/1, fun teardown/2, -+ [ -+ {{temporary, [?CONFIG_DEFAULT]}, -+ fun should_ensure_in_defaults/2}, -+ {{temporary, [?CONFIG_DEFAULT, ?CONFIG_FIXTURE_1]}, -+ fun should_override_options/2}, -+ {{temporary, [?CONFIG_DEFAULT, ?CONFIG_FIXTURE_2]}, -+ fun should_create_new_sections_on_override/2}, -+ {{temporary, [?CONFIG_DEFAULT, ?CONFIG_FIXTURE_1, -+ ?CONFIG_FIXTURE_2]}, -+ fun should_win_last_in_chain/2} -+ ] -+ } -+ }. -+ -+config_persistent_changes_tests() -> -+ { -+ "Config persistent changes", -+ { -+ foreachx, -+ fun setup/1, fun teardown/2, -+ [ -+ {{persistent, [?CONFIG_DEFAULT]}, -+ fun should_write_changes/2}, -+ {{temporary, [?CONFIG_DEFAULT]}, -+ fun should_ensure_that_default_wasnt_modified/2}, -+ {{temporary, [?CONFIG_FIXTURE_TEMP]}, -+ fun should_ensure_that_written_to_last_config_in_chain/2} -+ ] -+ } -+ }. -+ -+config_register_tests() -> -+ { -+ "Config changes subscriber", -+ { -+ foreach, -+ fun setup_register/0, fun teardown/1, -+ [ -+ fun should_handle_port_changes/1, -+ fun should_pass_persistent_flag/1, -+ fun should_not_trigger_handler_on_other_options_changes/1, -+ fun should_not_trigger_handler_after_related_process_death/1 -+ ] -+ } -+ }. -+ -+config_no_files_tests() -> -+ { -+ "Test couch_config with no files", -+ { -+ foreach, -+ fun setup_empty/0, fun teardown/1, -+ [ -+ should_ensure_that_no_ini_files_loaded(), -+ should_create_non_persistent_option(), -+ should_create_persistent_option() -+ ] -+ } -+ }. -+ -+ -+should_load_all_configs() -> -+ ?_assert(length(couch_config:all()) > 0). -+ -+should_locate_daemons_section() -> -+ ?_assert(length(couch_config:get("daemons")) > 0). -+ -+should_locate_mrview_handler() -> -+ ?_assertEqual("{couch_mrview_http, handle_view_req}", -+ couch_config:get("httpd_design_handlers", "_view")). -+ -+should_return_undefined_atom_on_missed_section() -> -+ ?_assertEqual(undefined, -+ couch_config:get("foo", "bar")). -+ -+should_return_undefined_atom_on_missed_option() -> -+ ?_assertEqual(undefined, -+ couch_config:get("httpd", "foo")). -+ -+should_return_custom_default_value_on_missed_option() -> -+ ?_assertEqual("bar", -+ couch_config:get("httpd", "foo", "bar")). -+ -+should_only_return_default_on_missed_option() -> -+ ?_assertEqual("0", -+ couch_config:get("httpd", "port", "bar")). -+ -+should_get_binary_option() -> -+ ?_assertEqual(<<"baz">>, -+ couch_config:get(<<"foo">>, <<"bar">>, <<"baz">>)). -+ -+should_update_option() -> -+ ?_assertEqual("severe", -+ begin -+ ok = couch_config:set("log", "level", "severe", false), -+ couch_config:get("log", "level") -+ end). -+ -+should_create_new_section() -> -+ ?_assertEqual("bang", -+ begin -+ undefined = couch_config:get("new_section", "bizzle"), -+ ok = couch_config:set("new_section", "bizzle", "bang", false), -+ couch_config:get("new_section", "bizzle") -+ end). -+ -+should_set_binary_option() -> -+ ?_assertEqual(<<"baz">>, -+ begin -+ ok = couch_config:set(<<"foo">>, <<"bar">>, <<"baz">>, false), -+ couch_config:get(<<"foo">>, <<"bar">>) -+ end). -+ -+should_return_undefined_atom_after_option_deletion() -> -+ ?_assertEqual(undefined, -+ begin -+ ok = couch_config:delete("log", "level", false), -+ couch_config:get("log", "level") -+ end). -+ -+should_be_ok_on_deleting_unknown_options() -> -+ ?_assertEqual(ok, couch_config:delete("zoo", "boo", false)). -+ -+should_delete_binary_option() -> -+ ?_assertEqual(undefined, -+ begin -+ ok = couch_config:set(<<"foo">>, <<"bar">>, <<"baz">>, false), -+ ok = couch_config:delete(<<"foo">>, <<"bar">>, false), -+ couch_config:get(<<"foo">>, <<"bar">>) -+ end). -+ -+should_ensure_in_defaults(_, _) -> -+ ?_test(begin -+ ?assertEqual("100", -+ couch_config:get("couchdb", "max_dbs_open")), -+ ?assertEqual("5984", -+ couch_config:get("httpd", "port")), -+ ?assertEqual(undefined, -+ couch_config:get("fizbang", "unicode")) -+ end). -+ -+should_override_options(_, _) -> -+ ?_test(begin -+ ?assertEqual("10", -+ couch_config:get("couchdb", "max_dbs_open")), -+ ?assertEqual("4895", -+ couch_config:get("httpd", "port")) -+ end). -+ -+should_create_new_sections_on_override(_, _) -> -+ ?_test(begin -+ ?assertEqual("80", -+ couch_config:get("httpd", "port")), -+ ?assertEqual("normalized", -+ couch_config:get("fizbang", "unicode")) -+ end). -+ -+should_win_last_in_chain(_, _) -> -+ ?_assertEqual("80", couch_config:get("httpd", "port")). -+ -+should_write_changes(_, _) -> -+ ?_test(begin -+ ?assertEqual("5984", -+ couch_config:get("httpd", "port")), -+ ?assertEqual(ok, -+ couch_config:set("httpd", "port", "8080")), -+ ?assertEqual("8080", -+ couch_config:get("httpd", "port")), -+ ?assertEqual(ok, -+ couch_config:delete("httpd", "bind_address", "8080")), -+ ?assertEqual(undefined, -+ couch_config:get("httpd", "bind_address")) -+ end). -+ -+should_ensure_that_default_wasnt_modified(_, _) -> -+ ?_test(begin -+ ?assertEqual("5984", -+ couch_config:get("httpd", "port")), -+ ?assertEqual("127.0.0.1", -+ couch_config:get("httpd", "bind_address")) -+ end). -+ -+should_ensure_that_written_to_last_config_in_chain(_, _) -> -+ ?_test(begin -+ ?assertEqual("8080", -+ couch_config:get("httpd", "port")), -+ ?assertEqual(undefined, -+ couch_config:get("httpd", "bind_address")) -+ end). -+ -+should_handle_port_changes({_, SentinelPid}) -> -+ ?_assert(begin -+ MainProc = self(), -+ Port = "8080", -+ -+ couch_config:register( -+ fun("httpd", "port", Value) -> -+ % couch_config catches every error raised from handler -+ % so it's not possible to just assert on wrong value. -+ % We have to return the result as message -+ MainProc ! (Value =:= Port) -+ end, -+ SentinelPid -+ ), -+ ok = couch_config:set("httpd", "port", Port, false), -+ -+ receive -+ R -> -+ R -+ after ?TIMEOUT -> -+ erlang:error({assertion_failed, -+ [{module, ?MODULE}, -+ {line, ?LINE}, -+ {reason, "Timeout"}]}) -+ end -+ end). -+ -+should_pass_persistent_flag({_, SentinelPid}) -> -+ ?_assert(begin -+ MainProc = self(), -+ -+ couch_config:register( -+ fun("httpd", "port", _, Persist) -> -+ % couch_config catches every error raised from handler -+ % so it's not possible to just assert on wrong value. -+ % We have to return the result as message -+ MainProc ! Persist -+ end, -+ SentinelPid -+ ), -+ ok = couch_config:set("httpd", "port", "8080", false), -+ -+ receive -+ false -> -+ true -+ after ?SHORT_TIMEOUT -> -+ false -+ end -+ end). -+ -+should_not_trigger_handler_on_other_options_changes({_, SentinelPid}) -> -+ ?_assert(begin -+ MainProc = self(), -+ -+ couch_config:register( -+ fun("httpd", "port", _) -> -+ MainProc ! ok -+ end, -+ SentinelPid -+ ), -+ ok = couch_config:set("httpd", "bind_address", "0.0.0.0", false), -+ -+ receive -+ ok -> -+ false -+ after ?SHORT_TIMEOUT -> -+ true -+ end -+ end). -+ -+should_not_trigger_handler_after_related_process_death({_, SentinelPid}) -> -+ ?_assert(begin -+ MainProc = self(), -+ -+ couch_config:register( -+ fun("httpd", "port", _) -> -+ MainProc ! ok -+ end, -+ SentinelPid -+ ), -+ -+ SentinelPid ! {ping, MainProc}, -+ receive -+ pong -> -+ ok -+ after ?SHORT_TIMEOUT -> -+ erlang:error({assertion_failed, -+ [{module, ?MODULE}, -+ {line, ?LINE}, -+ {reason, "Timeout"}]}) -+ end, -+ -+ ok = couch_config:set("httpd", "port", "12345", false), -+ -+ receive -+ ok -> -+ false -+ after ?SHORT_TIMEOUT -> -+ true -+ end -+ end). -+ -+should_ensure_that_no_ini_files_loaded() -> -+ ?_assertEqual(0, length(couch_config:all())). -+ -+should_create_non_persistent_option() -> -+ ?_assertEqual("80", -+ begin -+ ok = couch_config:set("httpd", "port", "80", false), -+ couch_config:get("httpd", "port") -+ end). -+ -+should_create_persistent_option() -> -+ ?_assertEqual("127.0.0.1", -+ begin -+ ok = couch_config:set("httpd", "bind_address", "127.0.0.1"), -+ couch_config:get("httpd", "bind_address") -+ end). -diff --git a/test/couchdb/couch_db_tests.erl b/test/couchdb/couch_db_tests.erl -new file mode 100644 -index 0000000..3089714 ---- /dev/null -+++ b/test/couchdb/couch_db_tests.erl -@@ -0,0 +1,114 @@ -+% Licensed under the Apache License, Version 2.0 (the "License"); you may not -+% use this file except in compliance with the License. You may obtain a copy of -+% the License at -+% -+% http://www.apache.org/licenses/LICENSE-2.0 -+% -+% Unless required by applicable law or agreed to in writing, software -+% distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -+% WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -+% License for the specific language governing permissions and limitations under -+% the License. -+ -+-module(couch_db_tests). -+ -+-include("couch_eunit.hrl"). -+ -+-define(TIMEOUT, 120). -+ -+ -+setup() -> -+ {ok, _} = couch_server_sup:start_link(?CONFIG_CHAIN), -+ couch_config:set("log", "include_sasl", "false", false), -+ ok. -+ -+teardown(_) -> -+ couch_server_sup:stop(). -+ -+ -+create_delete_db_test_()-> -+ { -+ "Database create/delete tests", -+ { -+ setup, -+ fun setup/0, fun teardown/1, -+ fun(_) -> -+ [should_create_db(), -+ should_delete_db(), -+ should_create_multiple_dbs(), -+ should_delete_multiple_dbs(), -+ should_create_delete_database_continuously()] -+ end -+ } -+ }. -+ -+ -+should_create_db() -> -+ DbName = ?tempdb(), -+ {ok, Db} = couch_db:create(DbName, []), -+ ok = couch_db:close(Db), -+ {ok, AllDbs} = couch_server:all_databases(), -+ ?_assert(lists:member(DbName, AllDbs)). -+ -+should_delete_db() -> -+ DbName = ?tempdb(), -+ couch_db:create(DbName, []), -+ couch_server:delete(DbName, []), -+ {ok, AllDbs} = couch_server:all_databases(), -+ ?_assertNot(lists:member(DbName, AllDbs)). -+ -+should_create_multiple_dbs() -> -+ gen_server:call(couch_server, {set_max_dbs_open, 3}), -+ -+ DbNames = [?tempdb() || _ <- lists:seq(1, 6)], -+ lists:foreach(fun(DbName) -> -+ {ok, Db} = couch_db:create(DbName, []), -+ ok = couch_db:close(Db) -+ end, DbNames), -+ -+ {ok, AllDbs} = couch_server:all_databases(), -+ NumCreated = lists:foldl(fun(DbName, Acc) -> -+ ?assert(lists:member(DbName, AllDbs)), -+ Acc+1 -+ end, 0, DbNames), -+ -+ ?_assertEqual(NumCreated, 6). -+ -+should_delete_multiple_dbs() -> -+ DbNames = [?tempdb() || _ <- lists:seq(1, 6)], -+ lists:foreach(fun(DbName) -> -+ {ok, Db} = couch_db:create(DbName, []), -+ ok = couch_db:close(Db) -+ end, DbNames), -+ -+ lists:foreach(fun(DbName) -> -+ ok = couch_server:delete(DbName, []) -+ end, DbNames), -+ -+ {ok, AllDbs} = couch_server:all_databases(), -+ NumDeleted = lists:foldl(fun(DbName, Acc) -> -+ ?assertNot(lists:member(DbName, AllDbs)), -+ Acc + 1 -+ end, 0, DbNames), -+ -+ ?_assertEqual(NumDeleted, 6). -+ -+should_create_delete_database_continuously() -> -+ DbName = ?tempdb(), -+ {ok, Db} = couch_db:create(DbName, []), -+ couch_db:close(Db), -+ [{timeout, ?TIMEOUT, {integer_to_list(N) ++ " times", -+ ?_assert(loop(DbName, N))}} -+ || N <- [10, 100, 1000]]. -+ -+loop(_, 0) -> -+ true; -+loop(DbName, N) -> -+ ok = cycle(DbName), -+ loop(DbName, N - 1). -+ -+cycle(DbName) -> -+ ok = couch_server:delete(DbName, []), -+ {ok, Db} = couch_db:create(DbName, []), -+ couch_db:close(Db), -+ ok. -diff --git a/test/couchdb/couch_doc_json_tests.erl b/test/couchdb/couch_doc_json_tests.erl -new file mode 100644 -index 0000000..1592b6b ---- /dev/null -+++ b/test/couchdb/couch_doc_json_tests.erl -@@ -0,0 +1,391 @@ -+% Licensed under the Apache License, Version 2.0 (the "License"); you may not -+% use this file except in compliance with the License. You may obtain a copy of -+% the License at -+% -+% http://www.apache.org/licenses/LICENSE-2.0 -+% -+% Unless required by applicable law or agreed to in writing, software -+% distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -+% WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -+% License for the specific language governing permissions and limitations under -+% the License. -+ -+-module(couch_doc_json_tests). -+ -+-include("couch_eunit.hrl"). -+-include_lib("couchdb/couch_db.hrl"). -+ -+ -+setup() -> -+ couch_config:start_link(?CONFIG_CHAIN), -+ couch_config:set("attachments", "compression_level", "0", false), -+ ok. -+ -+teardown(_) -> -+ couch_config:stop(). -+ -+ -+json_doc_test_() -> -+ { -+ setup, -+ fun setup/0, fun teardown/1, -+ [ -+ { -+ "Document from JSON", -+ [ -+ from_json_success_cases(), -+ from_json_error_cases() -+ ] -+ }, -+ { -+ "Document to JSON", -+ [ -+ to_json_success_cases() -+ ] -+ } -+ ] -+ }. -+ -+from_json_success_cases() -> -+ 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:map( -+ fun({EJson, Expect, Msg}) -> -+ {Msg, ?_assertMatch(Expect, couch_doc:from_json_obj(EJson))} -+ end, -+ Cases). -+ -+from_json_error_cases() -> -+ 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:map(fun -+ ({EJson, Expect, Msg}) -> -+ Error = (catch couch_doc:from_json_obj(EJson)), -+ {Msg, ?_assertMatch(Expect, Error)}; -+ ({EJson, Msg}) -> -+ try -+ couch_doc:from_json_obj(EJson), -+ {"Conversion failed to raise an exception", ?_assert(false)} -+ catch -+ _:_ -> {Msg, ?_assert(true)} -+ end -+ end, Cases). -+ -+to_json_success_cases() -> -+ 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:map(fun -+ ({Doc, EJson, Msg}) -> -+ {Msg, ?_assertMatch(EJson, couch_doc:to_json_obj(Doc, []))}; -+ ({Options, Doc, EJson, Msg}) -> -+ {Msg, ?_assertMatch(EJson, couch_doc:to_json_obj(Doc, Options))} -+ end, Cases). -diff --git a/test/couchdb/couch_file_tests.erl b/test/couchdb/couch_file_tests.erl -new file mode 100644 -index 0000000..ad13383 ---- /dev/null -+++ b/test/couchdb/couch_file_tests.erl -@@ -0,0 +1,265 @@ -+% Licensed under the Apache License, Version 2.0 (the "License"); you may not -+% use this file except in compliance with the License. You may obtain a copy of -+% the License at -+% -+% http://www.apache.org/licenses/LICENSE-2.0 -+% -+% Unless required by applicable law or agreed to in writing, software -+% distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -+% WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -+% License for the specific language governing permissions and limitations under -+% the License. -+ -+-module(couch_file_tests). -+ -+-include("couch_eunit.hrl"). -+ -+-define(BLOCK_SIZE, 4096). -+-define(setup(F), {setup, fun setup/0, fun teardown/1, F}). -+-define(foreach(Fs), {foreach, fun setup/0, fun teardown/1, Fs}). -+ -+ -+setup() -> -+ {ok, Fd} = couch_file:open(?tempfile(), [create, overwrite]), -+ Fd. -+ -+teardown(Fd) -> -+ ok = couch_file:close(Fd). -+ -+ -+open_close_test_() -> -+ { -+ "Test for proper file open and close", -+ [ -+ should_return_enoent_if_missed(), -+ should_ignore_invalid_flags_with_open(), -+ ?setup(fun should_return_pid_on_file_open/1), -+ should_close_file_properly(), -+ ?setup(fun should_create_empty_new_files/1) -+ ] -+ }. -+ -+should_return_enoent_if_missed() -> -+ ?_assertEqual({error, enoent}, couch_file:open("not a real file")). -+ -+should_ignore_invalid_flags_with_open() -> -+ ?_assertMatch({ok, _}, -+ couch_file:open(?tempfile(), [create, invalid_option])). -+ -+should_return_pid_on_file_open(Fd) -> -+ ?_assert(is_pid(Fd)). -+ -+should_close_file_properly() -> -+ {ok, Fd} = couch_file:open(?tempfile(), [create, overwrite]), -+ ok = couch_file:close(Fd), -+ ?_assert(true). -+ -+should_create_empty_new_files(Fd) -> -+ ?_assertMatch({ok, 0}, couch_file:bytes(Fd)). -+ -+ -+read_write_test_() -> -+ { -+ "Common file read/write tests", -+ ?foreach([ -+ fun should_increase_file_size_on_write/1, -+ fun should_return_current_file_size_on_write/1, -+ fun should_write_and_read_term/1, -+ fun should_write_and_read_binary/1, -+ fun should_write_and_read_large_binary/1, -+ fun should_return_term_as_binary_for_reading_binary/1, -+ fun should_read_term_written_as_binary/1, -+ fun should_read_iolist/1, -+ fun should_fsync/1, -+ fun should_not_read_beyond_eof/1, -+ fun should_truncate/1 -+ ]) -+ }. -+ -+ -+should_increase_file_size_on_write(Fd) -> -+ {ok, 0, _} = couch_file:append_term(Fd, foo), -+ {ok, Size} = couch_file:bytes(Fd), -+ ?_assert(Size > 0). -+ -+should_return_current_file_size_on_write(Fd) -> -+ {ok, 0, _} = couch_file:append_term(Fd, foo), -+ {ok, Size} = couch_file:bytes(Fd), -+ ?_assertMatch({ok, Size, _}, couch_file:append_term(Fd, bar)). -+ -+should_write_and_read_term(Fd) -> -+ {ok, Pos, _} = couch_file:append_term(Fd, foo), -+ ?_assertMatch({ok, foo}, couch_file:pread_term(Fd, Pos)). -+ -+should_write_and_read_binary(Fd) -> -+ {ok, Pos, _} = couch_file:append_binary(Fd, <<"fancy!">>), -+ ?_assertMatch({ok, <<"fancy!">>}, couch_file:pread_binary(Fd, Pos)). -+ -+should_return_term_as_binary_for_reading_binary(Fd) -> -+ {ok, Pos, _} = couch_file:append_term(Fd, foo), -+ Foo = couch_compress:compress(foo, snappy), -+ ?_assertMatch({ok, Foo}, couch_file:pread_binary(Fd, Pos)). -+ -+should_read_term_written_as_binary(Fd) -> -+ {ok, Pos, _} = couch_file:append_binary(Fd, <<131,100,0,3,102,111,111>>), -+ ?_assertMatch({ok, foo}, couch_file:pread_term(Fd, Pos)). -+ -+should_write_and_read_large_binary(Fd) -> -+ BigBin = list_to_binary(lists:duplicate(100000, 0)), -+ {ok, Pos, _} = couch_file:append_binary(Fd, BigBin), -+ ?_assertMatch({ok, BigBin}, couch_file:pread_binary(Fd, Pos)). -+ -+should_read_iolist(Fd) -> -+ %% append_binary == append_iolist? -+ %% Possible bug in pread_iolist or iolist() -> append_binary -+ {ok, Pos, _} = couch_file:append_binary(Fd, ["foo", $m, <<"bam">>]), -+ {ok, IoList} = couch_file:pread_iolist(Fd, Pos), -+ ?_assertMatch(<<"foombam">>, iolist_to_binary(IoList)). -+ -+should_fsync(Fd) -> -+ {"How does on test fsync?", ?_assertMatch(ok, couch_file:sync(Fd))}. -+ -+should_not_read_beyond_eof(_) -> -+ {"No idea how to test reading beyond EOF", ?_assert(true)}. -+ -+should_truncate(Fd) -> -+ {ok, 0, _} = couch_file:append_term(Fd, foo), -+ {ok, Size} = couch_file:bytes(Fd), -+ BigBin = list_to_binary(lists:duplicate(100000, 0)), -+ {ok, _, _} = couch_file:append_binary(Fd, BigBin), -+ ok = couch_file:truncate(Fd, Size), -+ ?_assertMatch({ok, foo}, couch_file:pread_term(Fd, 0)). -+ -+ -+header_test_() -> -+ { -+ "File header read/write tests", -+ [ -+ ?foreach([ -+ fun should_write_and_read_atom_header/1, -+ fun should_write_and_read_tuple_header/1, -+ fun should_write_and_read_second_header/1, -+ fun should_truncate_second_header/1, -+ fun should_produce_same_file_size_on_rewrite/1, -+ fun should_save_headers_larger_than_block_size/1 -+ ]), -+ should_recover_header_marker_corruption(), -+ should_recover_header_size_corruption(), -+ should_recover_header_md5sig_corruption(), -+ should_recover_header_data_corruption() -+ ] -+ }. -+ -+ -+should_write_and_read_atom_header(Fd) -> -+ ok = couch_file:write_header(Fd, hello), -+ ?_assertMatch({ok, hello}, couch_file:read_header(Fd)). -+ -+should_write_and_read_tuple_header(Fd) -> -+ ok = couch_file:write_header(Fd, {<<"some_data">>, 32}), -+ ?_assertMatch({ok, {<<"some_data">>, 32}}, couch_file:read_header(Fd)). -+ -+should_write_and_read_second_header(Fd) -> -+ ok = couch_file:write_header(Fd, {<<"some_data">>, 32}), -+ ok = couch_file:write_header(Fd, [foo, <<"more">>]), -+ ?_assertMatch({ok, [foo, <<"more">>]}, couch_file:read_header(Fd)). -+ -+should_truncate_second_header(Fd) -> -+ ok = couch_file:write_header(Fd, {<<"some_data">>, 32}), -+ {ok, Size} = couch_file:bytes(Fd), -+ ok = couch_file:write_header(Fd, [foo, <<"more">>]), -+ ok = couch_file:truncate(Fd, Size), -+ ?_assertMatch({ok, {<<"some_data">>, 32}}, couch_file:read_header(Fd)). -+ -+should_produce_same_file_size_on_rewrite(Fd) -> -+ ok = couch_file:write_header(Fd, {<<"some_data">>, 32}), -+ {ok, Size1} = couch_file:bytes(Fd), -+ ok = couch_file:write_header(Fd, [foo, <<"more">>]), -+ {ok, Size2} = couch_file:bytes(Fd), -+ ok = couch_file:truncate(Fd, Size1), -+ ok = couch_file:write_header(Fd, [foo, <<"more">>]), -+ ?_assertMatch({ok, Size2}, couch_file:bytes(Fd)). -+ -+should_save_headers_larger_than_block_size(Fd) -> -+ Header = erlang:make_tuple(5000, <<"CouchDB">>), -+ couch_file:write_header(Fd, Header), -+ {"COUCHDB-1319", ?_assertMatch({ok, Header}, couch_file:read_header(Fd))}. -+ -+ -+should_recover_header_marker_corruption() -> -+ ?_assertMatch( -+ ok, -+ check_header_recovery( -+ fun(CouchFd, RawFd, Expect, HeaderPos) -> -+ ?assertNotMatch(Expect, couch_file:read_header(CouchFd)), -+ file:pwrite(RawFd, HeaderPos, <<0>>), -+ ?assertMatch(Expect, couch_file:read_header(CouchFd)) -+ end) -+ ). -+ -+should_recover_header_size_corruption() -> -+ ?_assertMatch( -+ ok, -+ check_header_recovery( -+ fun(CouchFd, RawFd, Expect, HeaderPos) -> -+ ?assertNotMatch(Expect, couch_file:read_header(CouchFd)), -+ % +1 for 0x1 byte marker -+ file:pwrite(RawFd, HeaderPos + 1, <<10/integer>>), -+ ?assertMatch(Expect, couch_file:read_header(CouchFd)) -+ end) -+ ). -+ -+should_recover_header_md5sig_corruption() -> -+ ?_assertMatch( -+ ok, -+ check_header_recovery( -+ fun(CouchFd, RawFd, Expect, HeaderPos) -> -+ ?assertNotMatch(Expect, couch_file:read_header(CouchFd)), -+ % +5 = +1 for 0x1 byte and +4 for term size. -+ file:pwrite(RawFd, HeaderPos + 5, <<"F01034F88D320B22">>), -+ ?assertMatch(Expect, couch_file:read_header(CouchFd)) -+ end) -+ ). -+ -+should_recover_header_data_corruption() -> -+ ?_assertMatch( -+ ok, -+ check_header_recovery( -+ fun(CouchFd, RawFd, Expect, HeaderPos) -> -+ ?assertNotMatch(Expect, couch_file:read_header(CouchFd)), -+ % +21 = +1 for 0x1 byte, +4 for term size and +16 for MD5 sig -+ file:pwrite(RawFd, HeaderPos + 21, <<"some data goes here!">>), -+ ?assertMatch(Expect, couch_file:read_header(CouchFd)) -+ end) -+ ). -+ -+ -+check_header_recovery(CheckFun) -> -+ Path = ?tempfile(), -+ {ok, Fd} = couch_file:open(Path, [create, overwrite]), -+ {ok, RawFd} = file:open(Path, [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 ?BLOCK_SIZE) * ?BLOCK_SIZE}; -+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/test/couchdb/couch_key_tree_tests.erl b/test/couchdb/couch_key_tree_tests.erl -new file mode 100644 -index 0000000..753ecc4 ---- /dev/null -+++ b/test/couchdb/couch_key_tree_tests.erl -@@ -0,0 +1,380 @@ -+% Licensed under the Apache License, Version 2.0 (the "License"); you may not -+% use this file except in compliance with the License. You may obtain a copy of -+% the License at -+% -+% http://www.apache.org/licenses/LICENSE-2.0 -+% -+% Unless required by applicable law or agreed to in writing, software -+% distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -+% WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -+% License for the specific language governing permissions and limitations under -+% the License. -+ -+-module(couch_key_tree_tests). -+ -+-include("couch_eunit.hrl"). -+ -+-define(DEPTH, 10). -+ -+ -+key_tree_merge_test_()-> -+ { -+ "Key tree merge", -+ [ -+ should_merge_with_empty_tree(), -+ should_merge_reflexive(), -+ should_merge_prefix_of_a_tree_with_tree(), -+ should_produce_conflict_on_merge_with_unrelated_branch(), -+ should_merge_reflexive_for_child_nodes(), -+ should_merge_tree_to_itself(), -+ should_merge_tree_of_odd_length(), -+ should_merge_tree_with_stem(), -+ should_merge_with_stem_at_deeper_level(), -+ should_merge_with_stem_at_deeper_level_with_deeper_paths(), -+ should_merge_single_tree_with_deeper_stem(), -+ should_merge_tree_with_large_stem(), -+ should_merge_stems(), -+ should_create_conflicts_on_merge(), -+ should_create_no_conflicts_on_merge(), -+ should_ignore_conflicting_branch() -+ ] -+ }. -+ -+key_tree_missing_leaves_test_()-> -+ { -+ "Missing tree leaves", -+ [ -+ should_not_find_missing_leaves(), -+ should_find_missing_leaves() -+ ] -+ }. -+ -+key_tree_remove_leaves_test_()-> -+ { -+ "Remove tree leaves", -+ [ -+ should_have_no_effect_on_removing_no_leaves(), -+ should_have_no_effect_on_removing_non_existant_branch(), -+ should_remove_leaf(), -+ should_produce_empty_tree_on_removing_all_leaves(), -+ should_have_no_effect_on_removing_non_existant_node(), -+ should_produce_empty_tree_on_removing_last_leaf() -+ ] -+ }. -+ -+key_tree_get_leaves_test_()-> -+ { -+ "Leaves retrieving", -+ [ -+ should_extract_subtree(), -+ should_extract_subsubtree(), -+ should_gather_non_existant_leaf(), -+ should_gather_leaf(), -+ shoul_gather_multiple_leaves(), -+ should_retrieve_full_key_path(), -+ should_retrieve_full_key_path_for_node(), -+ should_retrieve_leaves_with_parent_node(), -+ should_retrieve_all_leaves() -+ ] -+ }. -+ -+key_tree_leaf_counting_test_()-> -+ { -+ "Leaf counting", -+ [ -+ should_have_no_leaves_for_empty_tree(), -+ should_have_single_leaf_for_tree_with_single_node(), -+ should_have_two_leaves_for_tree_with_chindler_siblings(), -+ should_not_affect_on_leaf_counting_for_stemmed_tree() -+ ] -+ }. -+ -+key_tree_stemming_test_()-> -+ { -+ "Stemming", -+ [ -+ should_have_no_effect_for_stemming_more_levels_than_exists(), -+ should_return_one_deepest_node(), -+ should_return_two_deepest_nodes() -+ ] -+ }. -+ -+ -+should_merge_with_empty_tree()-> -+ One = {1, {"1","foo",[]}}, -+ ?_assertEqual({[One], no_conflicts}, -+ couch_key_tree:merge([], One, ?DEPTH)). -+ -+should_merge_reflexive()-> -+ One = {1, {"1","foo",[]}}, -+ ?_assertEqual({[One], no_conflicts}, -+ couch_key_tree:merge([One], One, ?DEPTH)). -+ -+should_merge_prefix_of_a_tree_with_tree()-> -+ One = {1, {"1","foo",[]}}, -+ TwoSibs = [{1, {"1","foo",[]}}, -+ {1, {"2","foo",[]}}], -+ ?_assertEqual({TwoSibs, no_conflicts}, -+ couch_key_tree:merge(TwoSibs, One, ?DEPTH)). -+ -+should_produce_conflict_on_merge_with_unrelated_branch()-> -+ TwoSibs = [{1, {"1","foo",[]}}, -+ {1, {"2","foo",[]}}], -+ Three = {1, {"3","foo",[]}}, -+ ThreeSibs = [{1, {"1","foo",[]}}, -+ {1, {"2","foo",[]}}, -+ {1, {"3","foo",[]}}], -+ ?_assertEqual({ThreeSibs, conflicts}, -+ couch_key_tree:merge(TwoSibs, Three, ?DEPTH)). -+ -+should_merge_reflexive_for_child_nodes()-> -+ TwoChild = {1, {"1","foo", [{"1a", "bar", [{"1aa", "bar", []}]}]}}, -+ ?_assertEqual({[TwoChild], no_conflicts}, -+ couch_key_tree:merge([TwoChild], TwoChild, ?DEPTH)). -+ -+should_merge_tree_to_itself()-> -+ TwoChildSibs = {1, {"1","foo", [{"1a", "bar", []}, -+ {"1b", "bar", []}]}}, -+ ?_assertEqual({[TwoChildSibs], no_conflicts}, -+ couch_key_tree:merge([TwoChildSibs], TwoChildSibs, ?DEPTH)). -+ -+should_merge_tree_of_odd_length()-> -+ TwoChild = {1, {"1","foo", [{"1a", "bar", [{"1aa", "bar", []}]}]}}, -+ TwoChildSibs = {1, {"1","foo", [{"1a", "bar", []}, -+ {"1b", "bar", []}]}}, -+ TwoChildPlusSibs = {1, {"1","foo", [{"1a", "bar", [{"1aa", "bar", []}]}, -+ {"1b", "bar", []}]}}, -+ -+ ?_assertEqual({[TwoChildPlusSibs], no_conflicts}, -+ couch_key_tree:merge([TwoChild], TwoChildSibs, ?DEPTH)). -+ -+should_merge_tree_with_stem()-> -+ Stemmed = {2, {"1a", "bar", []}}, -+ TwoChildSibs = {1, {"1","foo", [{"1a", "bar", []}, -+ {"1b", "bar", []}]}}, -+ -+ ?_assertEqual({[TwoChildSibs], no_conflicts}, -+ couch_key_tree:merge([TwoChildSibs], Stemmed, ?DEPTH)). -+ -+should_merge_with_stem_at_deeper_level()-> -+ Stemmed = {3, {"1bb", "boo", []}}, -+ TwoChildSibs = {1, {"1","foo", [{"1a", "bar", []}, -+ {"1b", "bar", [{"1bb", "boo", []}]}]}}, -+ ?_assertEqual({[TwoChildSibs], no_conflicts}, -+ couch_key_tree:merge([TwoChildSibs], Stemmed, ?DEPTH)). -+ -+should_merge_with_stem_at_deeper_level_with_deeper_paths()-> -+ Stemmed = {3, {"1bb", "boo", []}}, -+ StemmedTwoChildSibs = [{2,{"1a", "bar", []}}, -+ {2,{"1b", "bar", [{"1bb", "boo", []}]}}], -+ ?_assertEqual({StemmedTwoChildSibs, no_conflicts}, -+ couch_key_tree:merge(StemmedTwoChildSibs, Stemmed, ?DEPTH)). -+ -+should_merge_single_tree_with_deeper_stem()-> -+ Stemmed = {3, {"1aa", "bar", []}}, -+ TwoChild = {1, {"1","foo", [{"1a", "bar", [{"1aa", "bar", []}]}]}}, -+ ?_assertEqual({[TwoChild], no_conflicts}, -+ couch_key_tree:merge([TwoChild], Stemmed, ?DEPTH)). -+ -+should_merge_tree_with_large_stem()-> -+ Stemmed = {2, {"1a", "bar", [{"1aa", "bar", []}]}}, -+ TwoChild = {1, {"1","foo", [{"1a", "bar", [{"1aa", "bar", []}]}]}}, -+ ?_assertEqual({[TwoChild], no_conflicts}, -+ couch_key_tree:merge([TwoChild], Stemmed, ?DEPTH)). -+ -+should_merge_stems()-> -+ StemmedA = {2, {"1a", "bar", [{"1aa", "bar", []}]}}, -+ StemmedB = {3, {"1aa", "bar", []}}, -+ ?_assertEqual({[StemmedA], no_conflicts}, -+ couch_key_tree:merge([StemmedA], StemmedB, ?DEPTH)). -+ -+should_create_conflicts_on_merge()-> -+ OneChild = {1, {"1","foo",[{"1a", "bar", []}]}}, -+ Stemmed = {3, {"1aa", "bar", []}}, -+ ?_assertEqual({[OneChild, Stemmed], conflicts}, -+ couch_key_tree:merge([OneChild], Stemmed, ?DEPTH)). -+ -+should_create_no_conflicts_on_merge()-> -+ OneChild = {1, {"1","foo",[{"1a", "bar", []}]}}, -+ Stemmed = {3, {"1aa", "bar", []}}, -+ TwoChild = {1, {"1","foo", [{"1a", "bar", [{"1aa", "bar", []}]}]}}, -+ ?_assertEqual({[TwoChild], no_conflicts}, -+ couch_key_tree:merge([OneChild, Stemmed], TwoChild, ?DEPTH)). -+ -+should_ignore_conflicting_branch()-> -+ %% this test is based on couch-902-test-case2.py -+ %% foo has conflicts from replication at depth two -+ %% foo3 is the current value -+ Foo = {1, {"foo", -+ "val1", -+ [{"foo2","val2",[]}, -+ {"foo3", "val3", []} -+ ]}}, -+ %% foo now has an attachment added, which leads to foo4 and val4 -+ %% off foo3 -+ Bar = {1, {"foo", -+ [], -+ [{"foo3", -+ [], -+ [{"foo4","val4",[]} -+ ]}]}}, -+ %% this is what the merge returns -+ %% note that it ignore the conflicting branch as there's no match -+ FooBar = {1, {"foo", -+ "val1", -+ [{"foo2","val2",[]}, -+ {"foo3", "val3", [{"foo4","val4",[]}]} -+ ]}}, -+ { -+ "COUCHDB-902", -+ ?_assertEqual({[FooBar], no_conflicts}, -+ couch_key_tree:merge([Foo], Bar, ?DEPTH)) -+ }. -+ -+should_not_find_missing_leaves()-> -+ TwoChildSibs = [{0, {"1","foo", [{"1a", "bar", []}, {"1b", "bar", []}]}}], -+ ?_assertEqual([], -+ couch_key_tree:find_missing(TwoChildSibs, -+ [{0,"1"}, {1,"1a"}])). -+ -+should_find_missing_leaves()-> -+ Stemmed1 = [{1, {"1a", "bar", [{"1aa", "bar", []}]}}], -+ Stemmed2 = [{2, {"1aa", "bar", []}}], -+ TwoChildSibs = [{0, {"1","foo", [{"1a", "bar", []}, {"1b", "bar", []}]}}], -+ [ -+ ?_assertEqual( -+ [{0, "10"}, {100, "x"}], -+ couch_key_tree:find_missing( -+ TwoChildSibs, -+ [{0,"1"}, {0, "10"}, {1,"1a"}, {100, "x"}])), -+ ?_assertEqual( -+ [{0, "1"}, {100, "x"}], -+ couch_key_tree:find_missing( -+ Stemmed1, -+ [{0,"1"}, {1,"1a"}, {100, "x"}])), -+ ?_assertEqual( -+ [{0, "1"}, {1,"1a"}, {100, "x"}], -+ couch_key_tree:find_missing( -+ Stemmed2, -+ [{0,"1"}, {1,"1a"}, {100, "x"}])) -+ ]. -+ -+should_have_no_effect_on_removing_no_leaves()-> -+ TwoChildSibs = [{0, {"1","foo", [{"1a", "bar", []}, {"1b", "bar", []}]}}], -+ ?_assertEqual({TwoChildSibs, []}, -+ couch_key_tree:remove_leafs(TwoChildSibs, -+ [])). -+ -+should_have_no_effect_on_removing_non_existant_branch()-> -+ TwoChildSibs = [{0, {"1","foo", [{"1a", "bar", []}, {"1b", "bar", []}]}}], -+ ?_assertEqual({TwoChildSibs, []}, -+ couch_key_tree:remove_leafs(TwoChildSibs, -+ [{0, "1"}])). -+ -+should_remove_leaf()-> -+ OneChild = [{0, {"1","foo",[{"1a", "bar", []}]}}], -+ TwoChildSibs = [{0, {"1","foo", [{"1a", "bar", []}, {"1b", "bar", []}]}}], -+ ?_assertEqual({OneChild, [{1, "1b"}]}, -+ couch_key_tree:remove_leafs(TwoChildSibs, -+ [{1, "1b"}])). -+ -+should_produce_empty_tree_on_removing_all_leaves()-> -+ TwoChildSibs = [{0, {"1","foo", [{"1a", "bar", []}, {"1b", "bar", []}]}}], -+ ?_assertEqual({[], [{1, "1b"}, {1, "1a"}]}, -+ couch_key_tree:remove_leafs(TwoChildSibs, -+ [{1, "1b"}, {1, "1a"}])). -+ -+should_have_no_effect_on_removing_non_existant_node()-> -+ Stemmed = [{1, {"1a", "bar", [{"1aa", "bar", []}]}}], -+ ?_assertEqual({Stemmed, []}, -+ couch_key_tree:remove_leafs(Stemmed, -+ [{1, "1a"}])). -+ -+should_produce_empty_tree_on_removing_last_leaf()-> -+ Stemmed = [{1, {"1a", "bar", [{"1aa", "bar", []}]}}], -+ ?_assertEqual({[], [{2, "1aa"}]}, -+ couch_key_tree:remove_leafs(Stemmed, -+ [{2, "1aa"}])). -+ -+should_extract_subtree()-> -+ TwoChildSibs = [{0, {"1","foo", [{"1a", "bar", []}, {"1b", "bar", []}]}}], -+ ?_assertEqual({[{"foo", {0, ["1"]}}],[]}, -+ couch_key_tree:get(TwoChildSibs, [{0, "1"}])). -+ -+should_extract_subsubtree()-> -+ TwoChildSibs = [{0, {"1","foo", [{"1a", "bar", []}, {"1b", "bar", []}]}}], -+ ?_assertEqual({[{"bar", {1, ["1a", "1"]}}],[]}, -+ couch_key_tree:get(TwoChildSibs, [{1, "1a"}])). -+ -+should_gather_non_existant_leaf()-> -+ TwoChildSibs = [{0, {"1","foo", [{"1a", "bar", []}, {"1b", "bar", []}]}}], -+ ?_assertEqual({[],[{0, "x"}]}, -+ couch_key_tree:get_key_leafs(TwoChildSibs, [{0, "x"}])). -+ -+should_gather_leaf()-> -+ TwoChildSibs = [{0, {"1","foo", [{"1a", "bar", []}, {"1b", "bar", []}]}}], -+ ?_assertEqual({[{"bar", {1, ["1a","1"]}}],[]}, -+ couch_key_tree:get_key_leafs(TwoChildSibs, [{1, "1a"}])). -+ -+shoul_gather_multiple_leaves()-> -+ TwoChildSibs = [{0, {"1","foo", [{"1a", "bar", []}, {"1b", "bar", []}]}}], -+ ?_assertEqual({[{"bar", {1, ["1a","1"]}},{"bar",{1, ["1b","1"]}}],[]}, -+ couch_key_tree:get_key_leafs(TwoChildSibs, [{0, "1"}])). -+ -+should_retrieve_full_key_path()-> -+ TwoChildSibs = [{0, {"1","foo", [{"1a", "bar", []}, {"1b", "bar", []}]}}], -+ ?_assertEqual({[{0,[{"1", "foo"}]}],[]}, -+ couch_key_tree:get_full_key_paths(TwoChildSibs, [{0, "1"}])). -+ -+should_retrieve_full_key_path_for_node()-> -+ TwoChildSibs = [{0, {"1","foo", [{"1a", "bar", []}, {"1b", "bar", []}]}}], -+ ?_assertEqual({[{1,[{"1a", "bar"},{"1", "foo"}]}],[]}, -+ couch_key_tree:get_full_key_paths(TwoChildSibs, [{1, "1a"}])). -+ -+should_retrieve_leaves_with_parent_node()-> -+ Stemmed = [{1, {"1a", "bar", [{"1aa", "bar", []}]}}], -+ TwoChildSibs = [{0, {"1","foo", [{"1a", "bar", []}, {"1b", "bar", []}]}}], -+ [ -+ ?_assertEqual([{2, [{"1aa", "bar"},{"1a", "bar"}]}], -+ couch_key_tree:get_all_leafs_full(Stemmed)), -+ ?_assertEqual([{1, [{"1a", "bar"},{"1", "foo"}]}, -+ {1, [{"1b", "bar"},{"1", "foo"}]}], -+ couch_key_tree:get_all_leafs_full(TwoChildSibs)) -+ ]. -+ -+should_retrieve_all_leaves()-> -+ Stemmed = [{1, {"1a", "bar", [{"1aa", "bar", []}]}}], -+ TwoChildSibs = [{0, {"1","foo", [{"1a", "bar", []}, {"1b", "bar", []}]}}], -+ [ -+ ?_assertEqual([{"bar", {2, ["1aa","1a"]}}], -+ couch_key_tree:get_all_leafs(Stemmed)), -+ ?_assertEqual([{"bar", {1, ["1a", "1"]}}, {"bar", {1, ["1b","1"]}}], -+ couch_key_tree:get_all_leafs(TwoChildSibs)) -+ ]. -+ -+should_have_no_leaves_for_empty_tree()-> -+ ?_assertEqual(0, couch_key_tree:count_leafs([])). -+ -+should_have_single_leaf_for_tree_with_single_node()-> -+ ?_assertEqual(1, couch_key_tree:count_leafs([{0, {"1","foo",[]}}])). -+ -+should_have_two_leaves_for_tree_with_chindler_siblings()-> -+ TwoChildSibs = [{0, {"1","foo", [{"1a", "bar", []}, {"1b", "bar", []}]}}], -+ ?_assertEqual(2, couch_key_tree:count_leafs(TwoChildSibs)). -+ -+should_not_affect_on_leaf_counting_for_stemmed_tree()-> -+ ?_assertEqual(1, couch_key_tree:count_leafs([{2, {"1bb", "boo", []}}])). -+ -+should_have_no_effect_for_stemming_more_levels_than_exists()-> -+ TwoChild = [{0, {"1","foo", [{"1a", "bar", [{"1aa", "bar", []}]}]}}], -+ ?_assertEqual(TwoChild, couch_key_tree:stem(TwoChild, 3)). -+ -+should_return_one_deepest_node()-> -+ TwoChild = [{0, {"1","foo", [{"1a", "bar", [{"1aa", "bar", []}]}]}}], -+ Stemmed = [{2, {"1aa", "bar", []}}], -+ ?_assertEqual(Stemmed, couch_key_tree:stem(TwoChild, 1)). -+ -+should_return_two_deepest_nodes()-> -+ TwoChild = [{0, {"1","foo", [{"1a", "bar", [{"1aa", "bar", []}]}]}}], -+ Stemmed = [{1, {"1a", "bar", [{"1aa", "bar", []}]}}], -+ ?_assertEqual(Stemmed, couch_key_tree:stem(TwoChild, 2)). -diff --git a/test/couchdb/couch_passwords_tests.erl b/test/couchdb/couch_passwords_tests.erl -new file mode 100644 -index 0000000..116265c ---- /dev/null -+++ b/test/couchdb/couch_passwords_tests.erl -@@ -0,0 +1,54 @@ -+% Licensed under the Apache License, Version 2.0 (the "License"); you may not -+% use this file except in compliance with the License. You may obtain a copy of -+% the License at -+% -+% http://www.apache.org/licenses/LICENSE-2.0 -+% -+% Unless required by applicable law or agreed to in writing, software -+% distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -+% WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -+% License for the specific language governing permissions and limitations under -+% the License. -+ -+-module(couch_passwords_tests). -+ -+-include("couch_eunit.hrl"). -+ -+ -+pbkdf2_test_()-> -+ {"PBKDF2", -+ [ -+ {"Iterations: 1, length: 20", -+ ?_assertEqual( -+ {ok, <<"0c60c80f961f0e71f3a9b524af6012062fe037a6">>}, -+ couch_passwords:pbkdf2(<<"password">>, <<"salt">>, 1, 20))}, -+ -+ {"Iterations: 2, length: 20", -+ ?_assertEqual( -+ {ok, <<"ea6c014dc72d6f8ccd1ed92ace1d41f0d8de8957">>}, -+ couch_passwords:pbkdf2(<<"password">>, <<"salt">>, 2, 20))}, -+ -+ {"Iterations: 4096, length: 20", -+ ?_assertEqual( -+ {ok, <<"4b007901b765489abead49d926f721d065a429c1">>}, -+ couch_passwords:pbkdf2(<<"password">>, <<"salt">>, 4096, 20))}, -+ -+ {"Iterations: 4096, length: 25", -+ ?_assertEqual( -+ {ok, <<"3d2eec4fe41c849b80c8d83662c0e44a8b291a964cf2f07038">>}, -+ couch_passwords:pbkdf2(<<"passwordPASSWORDpassword">>, -+ <<"saltSALTsaltSALTsaltSALTsaltSALTsalt">>, -+ 4096, 25))}, -+ {"Null byte", -+ ?_assertEqual( -+ {ok, <<"56fa6aa75548099dcc37d7f03425e0c3">>}, -+ couch_passwords:pbkdf2(<<"pass\0word">>, -+ <<"sa\0lt">>, -+ 4096, 16))}, -+ -+ {timeout, 180, %% this may runs too long on slow hosts -+ {"Iterations: 16777216 - this may take some time", -+ ?_assertEqual( -+ {ok, <<"eefe3d61cd4da4e4e9945b3d6ba2158c2634e984">>}, -+ couch_passwords:pbkdf2(<<"password">>, <<"salt">>, 16777216, 20) -+ )}}]}. -diff --git a/test/couchdb/couch_ref_counter_tests.erl b/test/couchdb/couch_ref_counter_tests.erl -new file mode 100644 -index 0000000..b7e97b4 ---- /dev/null -+++ b/test/couchdb/couch_ref_counter_tests.erl -@@ -0,0 +1,107 @@ -+% Licensed under the Apache License, Version 2.0 (the "License"); you may not -+% use this file except in compliance with the License. You may obtain a copy of -+% the License at -+% -+% http://www.apache.org/licenses/LICENSE-2.0 -+% -+% Unless required by applicable law or agreed to in writing, software -+% distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -+% WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -+% License for the specific language governing permissions and limitations under -+% the License. -+ -+-module(couch_ref_counter_tests). -+ -+-include("couch_eunit.hrl"). -+-include_lib("couchdb/couch_db.hrl"). -+ -+-define(TIMEOUT, 1000). -+ -+ -+setup() -> -+ {ok, RefCtr} = couch_ref_counter:start([]), -+ ChildPid = spawn(fun() -> loop() end), -+ {RefCtr, ChildPid}. -+ -+teardown({_, ChildPid}) -> -+ erlang:monitor(process, ChildPid), -+ ChildPid ! close, -+ wait(). -+ -+ -+couch_ref_counter_test_() -> -+ { -+ "CouchDB reference counter tests", -+ { -+ foreach, -+ fun setup/0, fun teardown/1, -+ [ -+ fun should_initialize_with_calling_process_as_referrer/1, -+ fun should_ignore_unknown_pid/1, -+ fun should_increment_counter_on_pid_add/1, -+ fun should_not_increase_counter_on_readding_same_pid/1, -+ fun should_drop_ref_for_double_added_pid/1, -+ fun should_decrement_counter_on_pid_drop/1, -+ fun should_add_after_drop/1, -+ fun should_decrement_counter_on_process_exit/1 -+ -+ ] -+ } -+ }. -+ -+ -+should_initialize_with_calling_process_as_referrer({RefCtr, _}) -> -+ ?_assertEqual(1, couch_ref_counter:count(RefCtr)). -+ -+should_ignore_unknown_pid({RefCtr, ChildPid}) -> -+ ?_assertEqual(ok, couch_ref_counter:drop(RefCtr, ChildPid)). -+ -+should_increment_counter_on_pid_add({RefCtr, ChildPid}) -> -+ couch_ref_counter:add(RefCtr, ChildPid), -+ ?_assertEqual(2, couch_ref_counter:count(RefCtr)). -+ -+should_not_increase_counter_on_readding_same_pid({RefCtr, ChildPid}) -> -+ couch_ref_counter:add(RefCtr, ChildPid), -+ couch_ref_counter:add(RefCtr, ChildPid), -+ ?_assertEqual(2, couch_ref_counter:count(RefCtr)). -+ -+should_drop_ref_for_double_added_pid({RefCtr, ChildPid}) -> -+ couch_ref_counter:add(RefCtr, ChildPid), -+ couch_ref_counter:add(RefCtr, ChildPid), -+ couch_ref_counter:drop(RefCtr, ChildPid), -+ ?_assertEqual(2, couch_ref_counter:count(RefCtr)). -+ -+should_decrement_counter_on_pid_drop({RefCtr, ChildPid}) -> -+ couch_ref_counter:add(RefCtr, ChildPid), -+ couch_ref_counter:drop(RefCtr, ChildPid), -+ ?_assertEqual(1, couch_ref_counter:count(RefCtr)). -+ -+should_add_after_drop({RefCtr, ChildPid}) -> -+ couch_ref_counter:add(RefCtr, ChildPid), -+ couch_ref_counter:drop(RefCtr, ChildPid), -+ couch_ref_counter:add(RefCtr, ChildPid), -+ ?_assertEqual(2, couch_ref_counter:count(RefCtr)). -+ -+should_decrement_counter_on_process_exit({RefCtr, ChildPid}) -> -+ ?_assertEqual(1, -+ begin -+ couch_ref_counter:add(RefCtr, ChildPid), -+ erlang:monitor(process, ChildPid), -+ ChildPid ! close, -+ wait(), -+ couch_ref_counter:count(RefCtr) -+ end). -+ -+ -+loop() -> -+ receive -+ close -> ok -+ end. -+ -+wait() -> -+ receive -+ {'DOWN', _, _, _, _} -> -+ ok -+ after ?TIMEOUT -> -+ throw(timeout_error) -+ end. -diff --git a/test/couchdb/couch_stats_tests.erl b/test/couchdb/couch_stats_tests.erl -new file mode 100644 -index 0000000..d156449 ---- /dev/null -+++ b/test/couchdb/couch_stats_tests.erl -@@ -0,0 +1,412 @@ -+% Licensed under the Apache License, Version 2.0 (the "License"); you may not -+% use this file except in compliance with the License. You may obtain a copy of -+% the License at -+% -+% http://www.apache.org/licenses/LICENSE-2.0 -+% -+% Unless required by applicable law or agreed to in writing, software -+% distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -+% WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -+% License for the specific language governing permissions and limitations under -+% the License. -+ -+-module(couch_stats_tests). -+ -+-include("couch_eunit.hrl"). -+-include_lib("couchdb/couch_db.hrl"). -+ -+-define(STATS_CFG_FIXTURE, -+ filename:join([?FIXTURESDIR, "couch_stats_aggregates.cfg"])). -+-define(STATS_INI_FIXTURE, -+ filename:join([?FIXTURESDIR, "couch_stats_aggregates.ini"])). -+-define(TIMEOUT, 1000). -+-define(TIMEWAIT, 500). -+ -+ -+setup_collector() -> -+ couch_stats_collector:start(), -+ ok. -+ -+setup_aggregator(_) -> -+ {ok, Pid} = couch_config:start_link([?STATS_INI_FIXTURE]), -+ {ok, _} = couch_stats_collector:start(), -+ {ok, _} = couch_stats_aggregator:start(?STATS_CFG_FIXTURE), -+ Pid. -+ -+teardown_collector(_) -> -+ couch_stats_collector:stop(), -+ ok. -+ -+teardown_aggregator(_, Pid) -> -+ couch_stats_aggregator:stop(), -+ couch_stats_collector:stop(), -+ erlang:monitor(process, Pid), -+ couch_config:stop(), -+ receive -+ {'DOWN', _, _, Pid, _} -> -+ ok -+ after ?TIMEOUT -> -+ throw({timeout, config_stop}) -+ end, -+ ok. -+ -+ -+couch_stats_collector_test_() -> -+ { -+ "CouchDB stats collector tests", -+ { -+ foreach, -+ fun setup_collector/0, fun teardown_collector/1, -+ [ -+ should_increment_counter(), -+ should_decrement_counter(), -+ should_increment_and_decrement_counter(), -+ should_record_absolute_values(), -+ should_clear_absolute_values(), -+ should_track_process_count(), -+ should_increment_counter_multiple_times_per_pid(), -+ should_decrement_counter_on_process_exit(), -+ should_decrement_for_each_track_process_count_call_on_exit(), -+ should_return_all_counters_and_absolute_values(), -+ should_return_incremental_counters(), -+ should_return_absolute_values() -+ ] -+ } -+ }. -+ -+couch_stats_aggregator_test_() -> -+ Funs = [ -+ fun should_init_empty_aggregate/2, -+ fun should_get_empty_aggregate/2, -+ fun should_change_stats_on_values_add/2, -+ fun should_change_stats_for_all_times_on_values_add/2, -+ fun should_change_stats_on_values_change/2, -+ fun should_change_stats_for_all_times_on_values_change/2, -+ fun should_not_remove_data_after_some_time_for_0_sample/2, -+ fun should_remove_data_after_some_time_for_other_samples/2 -+ ], -+ { -+ "CouchDB stats aggregator tests", -+ [ -+ { -+ "Absolute values", -+ { -+ foreachx, -+ fun setup_aggregator/1, fun teardown_aggregator/2, -+ [{absolute, Fun} || Fun <- Funs] -+ } -+ }, -+ { -+ "Counters", -+ { -+ foreachx, -+ fun setup_aggregator/1, fun teardown_aggregator/2, -+ [{counter, Fun} || Fun <- Funs] -+ } -+ } -+ ] -+ }. -+ -+ -+should_increment_counter() -> -+ ?_assertEqual(100, -+ begin -+ AddCount = fun() -> couch_stats_collector:increment(foo) end, -+ repeat(AddCount, 100), -+ couch_stats_collector:get(foo) -+ end). -+ -+should_decrement_counter() -> -+ ?_assertEqual(67, -+ begin -+ AddCount = fun() -> couch_stats_collector:increment(foo) end, -+ RemCount = fun() -> couch_stats_collector:decrement(foo) end, -+ repeat(AddCount, 100), -+ repeat(RemCount, 33), -+ couch_stats_collector:get(foo) -+ end). -+ -+should_increment_and_decrement_counter() -> -+ ?_assertEqual(0, -+ begin -+ 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), -+ repeat(RemCount, 80), -+ couch_stats_collector:get(foo) -+ end). -+ -+should_record_absolute_values() -> -+ ?_assertEqual(lists:seq(1, 15), -+ begin -+ lists:map(fun(Val) -> -+ couch_stats_collector:record(bar, Val) -+ end, lists:seq(1, 15)), -+ couch_stats_collector:get(bar) -+ end). -+ -+should_clear_absolute_values() -> -+ ?_assertEqual(nil, -+ begin -+ lists:map(fun(Val) -> -+ couch_stats_collector:record(bar, Val) -+ end, lists:seq(1, 15)), -+ couch_stats_collector:clear(bar), -+ couch_stats_collector:get(bar) -+ end). -+ -+should_track_process_count() -> -+ ?_assertMatch({_, 1}, spawn_and_count(1)). -+ -+should_increment_counter_multiple_times_per_pid() -> -+ ?_assertMatch({_, 3}, spawn_and_count(3)). -+ -+should_decrement_counter_on_process_exit() -> -+ ?_assertEqual(2, -+ begin -+ {Pid, 1} = spawn_and_count(1), -+ spawn_and_count(2), -+ RefMon = erlang:monitor(process, Pid), -+ Pid ! sepuku, -+ receive -+ {'DOWN', RefMon, _, _, _} -> ok -+ after ?TIMEOUT -> -+ throw(timeout) -+ end, -+ % sleep for awhile to let collector handle the updates -+ % suddenly, it couldn't notice process death instantly -+ timer:sleep(?TIMEWAIT), -+ couch_stats_collector:get(hoopla) -+ end). -+ -+should_decrement_for_each_track_process_count_call_on_exit() -> -+ ?_assertEqual(2, -+ begin -+ {_, 2} = spawn_and_count(2), -+ {Pid, 6} = spawn_and_count(4), -+ RefMon = erlang:monitor(process, Pid), -+ Pid ! sepuku, -+ receive -+ {'DOWN', RefMon, _, _, _} -> ok -+ after ?TIMEOUT -> -+ throw(timeout) -+ end, -+ timer:sleep(?TIMEWAIT), -+ couch_stats_collector:get(hoopla) -+ end). -+ -+should_return_all_counters_and_absolute_values() -> -+ ?_assertEqual([{bar,[1.0,0.0]}, {foo,1}], -+ begin -+ couch_stats_collector:record(bar, 0.0), -+ couch_stats_collector:record(bar, 1.0), -+ couch_stats_collector:increment(foo), -+ lists:sort(couch_stats_collector:all()) -+ end). -+ -+should_return_incremental_counters() -> -+ ?_assertEqual([{foo,1}], -+ begin -+ couch_stats_collector:record(bar, 0.0), -+ couch_stats_collector:record(bar, 1.0), -+ couch_stats_collector:increment(foo), -+ lists:sort(couch_stats_collector:all(incremental)) -+ end). -+ -+should_return_absolute_values() -> -+ ?_assertEqual([{bar,[1.0,0.0]}, {zing, "Z"}], -+ begin -+ couch_stats_collector:record(bar, 0.0), -+ couch_stats_collector:record(bar, 1.0), -+ couch_stats_collector:record(zing, 90), -+ couch_stats_collector:increment(foo), -+ lists:sort(couch_stats_collector:all(absolute)) -+ end). -+ -+should_init_empty_aggregate(absolute, _) -> -+ {Aggs} = couch_stats_aggregator:all(), -+ ?_assertEqual({[{'11', make_agg(<<"randomosity">>, -+ null, null, null, null, null)}]}, -+ couch_util:get_value(number, Aggs)); -+should_init_empty_aggregate(counter, _) -> -+ {Aggs} = couch_stats_aggregator:all(), -+ ?_assertEqual({[{stuff, make_agg(<<"yay description">>, -+ null, null, null, null, null)}]}, -+ couch_util:get_value(testing, Aggs)). -+ -+should_get_empty_aggregate(absolute, _) -> -+ ?_assertEqual(make_agg(<<"randomosity">>, null, null, null, null, null), -+ couch_stats_aggregator:get_json({number, '11'})); -+should_get_empty_aggregate(counter, _) -> -+ ?_assertEqual(make_agg(<<"yay description">>, null, null, null, null, null), -+ couch_stats_aggregator:get_json({testing, stuff})). -+ -+should_change_stats_on_values_add(absolute, _) -> -+ lists:foreach(fun(X) -> -+ couch_stats_collector:record({number, 11}, X) -+ end, lists:seq(0, 10)), -+ couch_stats_aggregator:collect_sample(), -+ ?_assertEqual(make_agg(<<"randomosity">>, 5.0, 5.0, null, 5.0, 5.0), -+ couch_stats_aggregator:get_json({number, 11})); -+should_change_stats_on_values_add(counter, _) -> -+ lists:foreach(fun(_) -> -+ couch_stats_collector:increment({testing, stuff}) -+ end, lists:seq(1, 100)), -+ couch_stats_aggregator:collect_sample(), -+ ?_assertEqual(make_agg(<<"yay description">>, 100.0, 100.0, null, 100, 100), -+ couch_stats_aggregator:get_json({testing, stuff})). -+ -+should_change_stats_for_all_times_on_values_add(absolute, _) -> -+ lists:foreach(fun(X) -> -+ couch_stats_collector:record({number, 11}, X) -+ end, lists:seq(0, 10)), -+ couch_stats_aggregator:collect_sample(), -+ ?_assertEqual(make_agg(<<"randomosity">>, 5.0, 5.0, null, 5.0, 5.0), -+ couch_stats_aggregator:get_json({number, 11}, 1)); -+should_change_stats_for_all_times_on_values_add(counter, _) -> -+ lists:foreach(fun(_) -> -+ couch_stats_collector:increment({testing, stuff}) -+ end, lists:seq(1, 100)), -+ couch_stats_aggregator:collect_sample(), -+ ?_assertEqual(make_agg(<<"yay description">>, 100.0, 100.0, null, 100, 100), -+ couch_stats_aggregator:get_json({testing, stuff}, 1)). -+ -+should_change_stats_on_values_change(absolute, _) -> -+ ?_assertEqual(make_agg(<<"randomosity">>, 20.0, 10.0, 7.071, 5.0, 15.0), -+ begin -+ lists:foreach(fun(X) -> -+ couch_stats_collector:record({number, 11}, X) -+ end, lists:seq(0, 10)), -+ couch_stats_aggregator:collect_sample(), -+ timer:sleep(?TIMEWAIT), -+ couch_stats_collector:record({number, 11}, 15), -+ couch_stats_aggregator:collect_sample(), -+ couch_stats_aggregator:get_json({number, 11}) -+ end); -+should_change_stats_on_values_change(counter, _) -> -+ ?_assertEqual(make_agg(<<"yay description">>, 100.0, 50.0, 70.711, 0, 100), -+ begin -+ lists:foreach(fun(_) -> -+ couch_stats_collector:increment({testing, stuff}) -+ end, lists:seq(1, 100)), -+ couch_stats_aggregator:collect_sample(), -+ timer:sleep(?TIMEWAIT), -+ couch_stats_aggregator:collect_sample(), -+ couch_stats_aggregator:get_json({testing, stuff}) -+ end). -+ -+should_change_stats_for_all_times_on_values_change(absolute, _) -> -+ ?_assertEqual(make_agg(<<"randomosity">>, 20.0, 10.0, 7.071, 5.0, 15.0), -+ begin -+ lists:foreach(fun(X) -> -+ couch_stats_collector:record({number, 11}, X) -+ end, lists:seq(0, 10)), -+ couch_stats_aggregator:collect_sample(), -+ timer:sleep(?TIMEWAIT), -+ couch_stats_collector:record({number, 11}, 15), -+ couch_stats_aggregator:collect_sample(), -+ couch_stats_aggregator:get_json({number, 11}, 1) -+ end); -+should_change_stats_for_all_times_on_values_change(counter, _) -> -+ ?_assertEqual(make_agg(<<"yay description">>, 100.0, 50.0, 70.711, 0, 100), -+ begin -+ lists:foreach(fun(_) -> -+ couch_stats_collector:increment({testing, stuff}) -+ end, lists:seq(1, 100)), -+ couch_stats_aggregator:collect_sample(), -+ timer:sleep(?TIMEWAIT), -+ couch_stats_aggregator:collect_sample(), -+ couch_stats_aggregator:get_json({testing, stuff}, 1) -+ end). -+ -+should_not_remove_data_after_some_time_for_0_sample(absolute, _) -> -+ ?_assertEqual(make_agg(<<"randomosity">>, 20.0, 10.0, 7.071, 5.0, 15.0), -+ begin -+ lists:foreach(fun(X) -> -+ couch_stats_collector:record({number, 11}, X) -+ end, lists:seq(0, 10)), -+ couch_stats_aggregator:collect_sample(), -+ timer:sleep(?TIMEWAIT), -+ couch_stats_collector:record({number, 11}, 15), -+ couch_stats_aggregator:collect_sample(), -+ timer:sleep(?TIMEWAIT), -+ couch_stats_aggregator:collect_sample(), -+ couch_stats_aggregator:get_json({number, 11}) -+ end); -+should_not_remove_data_after_some_time_for_0_sample(counter, _) -> -+ ?_assertEqual(make_agg(<<"yay description">>, 100.0, 33.333, 57.735, 0, 100), -+ begin -+ lists:foreach(fun(_) -> -+ couch_stats_collector:increment({testing, stuff}) -+ end, lists:seq(1, 100)), -+ couch_stats_aggregator:collect_sample(), -+ timer:sleep(?TIMEWAIT), -+ couch_stats_aggregator:collect_sample(), -+ timer:sleep(?TIMEWAIT), -+ couch_stats_aggregator:collect_sample(), -+ couch_stats_aggregator:get_json({testing, stuff}) -+ end). -+ -+should_remove_data_after_some_time_for_other_samples(absolute, _) -> -+ ?_assertEqual(make_agg(<<"randomosity">>, 15.0, 15.0, null, 15.0, 15.0), -+ begin -+ lists:foreach(fun(X) -> -+ couch_stats_collector:record({number, 11}, X) -+ end, lists:seq(0, 10)), -+ couch_stats_aggregator:collect_sample(), -+ timer:sleep(?TIMEWAIT), -+ couch_stats_collector:record({number, 11}, 15), -+ couch_stats_aggregator:collect_sample(), -+ timer:sleep(?TIMEWAIT), -+ couch_stats_aggregator:collect_sample(), -+ couch_stats_aggregator:get_json({number, 11}, 1) -+ end); -+should_remove_data_after_some_time_for_other_samples(counter, _) -> -+ ?_assertEqual(make_agg(<<"yay description">>, 0, 0.0, 0.0, 0, 0), -+ begin -+ lists:foreach(fun(_) -> -+ couch_stats_collector:increment({testing, stuff}) -+ end, lists:seq(1, 100)), -+ couch_stats_aggregator:collect_sample(), -+ timer:sleep(?TIMEWAIT), -+ couch_stats_aggregator:collect_sample(), -+ timer:sleep(?TIMEWAIT), -+ couch_stats_aggregator:collect_sample(), -+ couch_stats_aggregator:get_json({testing, stuff}, 1) -+ end). -+ -+ -+spawn_and_count(N) -> -+ Self = self(), -+ Pid = spawn(fun() -> -+ lists:foreach( -+ fun(_) -> -+ couch_stats_collector:track_process_count(hoopla) -+ end, lists:seq(1,N)), -+ Self ! reporting, -+ receive -+ sepuku -> ok -+ end -+ end), -+ receive reporting -> ok end, -+ {Pid, couch_stats_collector:get(hoopla)}. -+ -+repeat(_, 0) -> -+ ok; -+repeat(Fun, Count) -> -+ Fun(), -+ repeat(Fun, Count-1). -+ -+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/test/couchdb/couch_stream_tests.erl b/test/couchdb/couch_stream_tests.erl -new file mode 100644 -index 0000000..335a2fe ---- /dev/null -+++ b/test/couchdb/couch_stream_tests.erl -@@ -0,0 +1,100 @@ -+% Licensed under the Apache License, Version 2.0 (the "License"); you may not -+% use this file except in compliance with the License. You may obtain a copy of -+% the License at -+% -+% http://www.apache.org/licenses/LICENSE-2.0 -+% -+% Unless required by applicable law or agreed to in writing, software -+% distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -+% WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -+% License for the specific language governing permissions and limitations under -+% the License. -+ -+-module(couch_stream_tests). -+ -+-include("couch_eunit.hrl"). -+ -+ -+setup() -> -+ {ok, Fd} = couch_file:open(?tempfile(), [create, overwrite]), -+ {ok, Stream} = couch_stream:open(Fd), -+ {Fd, Stream}. -+ -+teardown({Fd, _}) -> -+ ok = couch_file:close(Fd). -+ -+ -+stream_test_() -> -+ { -+ "CouchDB stream tests", -+ { -+ foreach, -+ fun setup/0, fun teardown/1, -+ [ -+ fun should_write/1, -+ fun should_write_consecutive/1, -+ fun should_write_empty_binary/1, -+ fun should_return_file_pointers_on_close/1, -+ fun should_return_stream_size_on_close/1, -+ fun should_return_valid_pointers/1, -+ fun should_recall_last_pointer_position/1, -+ fun should_stream_more_with_4K_chunk_size/1 -+ ] -+ } -+ }. -+ -+ -+should_write({_, Stream}) -> -+ ?_assertEqual(ok, couch_stream:write(Stream, <<"food">>)). -+ -+should_write_consecutive({_, Stream}) -> -+ couch_stream:write(Stream, <<"food">>), -+ ?_assertEqual(ok, couch_stream:write(Stream, <<"foob">>)). -+ -+should_write_empty_binary({_, Stream}) -> -+ ?_assertEqual(ok, couch_stream:write(Stream, <<>>)). -+ -+should_return_file_pointers_on_close({_, Stream}) -> -+ couch_stream:write(Stream, <<"foodfoob">>), -+ {Ptrs, _, _, _, _} = couch_stream:close(Stream), -+ ?_assertEqual([{0, 8}], Ptrs). -+ -+should_return_stream_size_on_close({_, Stream}) -> -+ couch_stream:write(Stream, <<"foodfoob">>), -+ {_, Length, _, _, _} = couch_stream:close(Stream), -+ ?_assertEqual(8, Length). -+ -+should_return_valid_pointers({Fd, Stream}) -> -+ couch_stream:write(Stream, <<"foodfoob">>), -+ {Ptrs, _, _, _, _} = couch_stream:close(Stream), -+ ?_assertEqual(<<"foodfoob">>, read_all(Fd, Ptrs)). -+ -+should_recall_last_pointer_position({Fd, Stream}) -> -+ couch_stream:write(Stream, <<"foodfoob">>), -+ {_, _, _, _, _} = couch_stream:close(Stream), -+ {ok, ExpPtr} = couch_file:bytes(Fd), -+ {ok, Stream2} = couch_stream:open(Fd), -+ ZeroBits = <<0:(8 * 10)>>, -+ OneBits = <<1:(8 * 10)>>, -+ ok = couch_stream:write(Stream2, OneBits), -+ ok = couch_stream:write(Stream2, ZeroBits), -+ {Ptrs, 20, _, _, _} = couch_stream:close(Stream2), -+ [{ExpPtr, 20}] = Ptrs, -+ AllBits = iolist_to_binary([OneBits, ZeroBits]), -+ ?_assertEqual(AllBits, read_all(Fd, Ptrs)). -+ -+should_stream_more_with_4K_chunk_size({Fd, _}) -> -+ {ok, Stream} = couch_stream:open(Fd, [{buffer_size, 4096}]), -+ lists:foldl( -+ fun(_, Acc) -> -+ Data = <<"a1b2c">>, -+ couch_stream:write(Stream, Data), -+ [Data | Acc] -+ end, [], lists:seq(1, 1024)), -+ ?_assertMatch({[{0, 4100}, {4106, 1020}], 5120, _, _, _}, -+ couch_stream:close(Stream)). -+ -+ -+read_all(Fd, PosList) -> -+ Data = couch_stream:foldl(Fd, PosList, fun(Bin, Acc) -> [Bin, Acc] end, []), -+ iolist_to_binary(Data). -diff --git a/test/couchdb/couch_task_status_tests.erl b/test/couchdb/couch_task_status_tests.erl -new file mode 100644 -index 0000000..f71ad2b ---- /dev/null -+++ b/test/couchdb/couch_task_status_tests.erl -@@ -0,0 +1,225 @@ -+% Licensed under the Apache License, Version 2.0 (the "License"); you may not -+% use this file except in compliance with the License. You may obtain a copy of -+% the License at -+% -+% http://www.apache.org/licenses/LICENSE-2.0 -+% -+% Unless required by applicable law or agreed to in writing, software -+% distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -+% WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -+% License for the specific language governing permissions and limitations under -+% the License. -+ -+-module(couch_task_status_tests). -+ -+-include("couch_eunit.hrl"). -+-include_lib("couchdb/couch_db.hrl"). -+ -+-define(TIMEOUT, 1000). -+ -+ -+setup() -> -+ {ok, TaskStatusPid} = couch_task_status:start_link(), -+ TaskUpdaterPid = spawn(fun() -> loop() end), -+ {TaskStatusPid, TaskUpdaterPid}. -+ -+teardown({TaskStatusPid, _}) -> -+ erlang:monitor(process, TaskStatusPid), -+ couch_task_status:stop(), -+ receive -+ {'DOWN', _, _, TaskStatusPid, _} -> -+ ok -+ after ?TIMEOUT -> -+ throw(timeout_error) -+ end. -+ -+ -+couch_task_status_test_() -> -+ { -+ "CouchDB task status updates", -+ { -+ foreach, -+ fun setup/0, fun teardown/1, -+ [ -+ fun should_register_task/1, -+ fun should_set_task_startup_time/1, -+ fun should_have_update_time_as_startup_before_any_progress/1, -+ fun should_set_task_type/1, -+ fun should_not_register_multiple_tasks_for_same_pid/1, -+ fun should_set_task_progress/1, -+ fun should_update_task_progress/1, -+ fun should_update_time_changes_on_task_progress/1, -+ fun should_control_update_frequency/1, -+ fun should_reset_control_update_frequency/1, -+ fun should_track_multiple_tasks/1, -+ fun should_finish_task/1 -+ -+ ] -+ } -+ }. -+ -+ -+should_register_task({_, Pid}) -> -+ ok = call(Pid, add, [{type, replication}, {progress, 0}]), -+ ?_assertEqual(1, length(couch_task_status:all())). -+ -+should_set_task_startup_time({_, Pid}) -> -+ ok = call(Pid, add, [{type, replication}, {progress, 0}]), -+ ?_assert(is_integer(get_task_prop(Pid, started_on))). -+ -+should_have_update_time_as_startup_before_any_progress({_, Pid}) -> -+ ok = call(Pid, add, [{type, replication}, {progress, 0}]), -+ StartTime = get_task_prop(Pid, started_on), -+ ?_assertEqual(StartTime, get_task_prop(Pid, updated_on)). -+ -+should_set_task_type({_, Pid}) -> -+ ok = call(Pid, add, [{type, replication}, {progress, 0}]), -+ ?_assertEqual(replication, get_task_prop(Pid, type)). -+ -+should_not_register_multiple_tasks_for_same_pid({_, Pid}) -> -+ ok = call(Pid, add, [{type, replication}, {progress, 0}]), -+ ?_assertEqual({add_task_error, already_registered}, -+ call(Pid, add, [{type, compaction}, {progress, 0}])). -+ -+should_set_task_progress({_, Pid}) -> -+ ok = call(Pid, add, [{type, replication}, {progress, 0}]), -+ ?_assertEqual(0, get_task_prop(Pid, progress)). -+ -+should_update_task_progress({_, Pid}) -> -+ ok = call(Pid, add, [{type, replication}, {progress, 0}]), -+ call(Pid, update, [{progress, 25}]), -+ ?_assertEqual(25, get_task_prop(Pid, progress)). -+ -+should_update_time_changes_on_task_progress({_, Pid}) -> -+ ?_assert( -+ begin -+ ok = call(Pid, add, [{type, replication}, {progress, 0}]), -+ ok = timer:sleep(1000), % sleep awhile to customize update time -+ call(Pid, update, [{progress, 25}]), -+ get_task_prop(Pid, updated_on) > get_task_prop(Pid, started_on) -+ end). -+ -+should_control_update_frequency({_, Pid}) -> -+ ?_assertEqual(66, -+ begin -+ ok = call(Pid, add, [{type, replication}, {progress, 0}]), -+ call(Pid, update, [{progress, 50}]), -+ call(Pid, update_frequency, 500), -+ call(Pid, update, [{progress, 66}]), -+ call(Pid, update, [{progress, 77}]), -+ get_task_prop(Pid, progress) -+ end). -+ -+should_reset_control_update_frequency({_, Pid}) -> -+ ?_assertEqual(87, -+ begin -+ ok = call(Pid, add, [{type, replication}, {progress, 0}]), -+ call(Pid, update, [{progress, 50}]), -+ call(Pid, update_frequency, 500), -+ call(Pid, update, [{progress, 66}]), -+ call(Pid, update, [{progress, 77}]), -+ call(Pid, update_frequency, 0), -+ call(Pid, update, [{progress, 87}]), -+ get_task_prop(Pid, progress) -+ end). -+ -+should_track_multiple_tasks(_) -> -+ ?_assert(run_multiple_tasks()). -+ -+should_finish_task({_, Pid}) -> -+ ok = call(Pid, add, [{type, replication}, {progress, 0}]), -+ ?assertEqual(1, length(couch_task_status:all())), -+ ok = call(Pid, done), -+ ?_assertEqual(0, length(couch_task_status:all())). -+ -+ -+run_multiple_tasks() -> -+ Pid1 = spawn(fun() -> loop() end), -+ Pid2 = spawn(fun() -> loop() end), -+ Pid3 = spawn(fun() -> loop() end), -+ call(Pid1, add, [{type, replication}, {progress, 0}]), -+ call(Pid2, add, [{type, compaction}, {progress, 0}]), -+ call(Pid3, add, [{type, indexer}, {progress, 0}]), -+ -+ ?assertEqual(3, length(couch_task_status:all())), -+ ?assertEqual(replication, get_task_prop(Pid1, type)), -+ ?assertEqual(compaction, get_task_prop(Pid2, type)), -+ ?assertEqual(indexer, get_task_prop(Pid3, type)), -+ -+ call(Pid2, update, [{progress, 33}]), -+ call(Pid3, update, [{progress, 42}]), -+ call(Pid1, update, [{progress, 11}]), -+ ?assertEqual(42, get_task_prop(Pid3, progress)), -+ call(Pid1, update, [{progress, 72}]), -+ ?assertEqual(72, get_task_prop(Pid1, progress)), -+ ?assertEqual(33, get_task_prop(Pid2, progress)), -+ -+ call(Pid1, done), -+ ?assertEqual(2, length(couch_task_status:all())), -+ call(Pid3, done), -+ ?assertEqual(1, length(couch_task_status:all())), -+ call(Pid2, done), -+ ?assertEqual(0, length(couch_task_status:all())), -+ -+ true. -+ -+ -+loop() -> -+ receive -+ {add, Props, From} -> -+ Resp = couch_task_status:add_task(Props), -+ From ! {ok, self(), Resp}, -+ loop(); -+ {update, Props, From} -> -+ Resp = couch_task_status:update(Props), -+ 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 ?TIMEOUT -> -+ throw(timeout_error) -+ end. -+ -+get_task_prop(Pid, Prop) -> -+ 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]; -+ _ -> -+ Acc -+ end -+ end, -+ [], couch_task_status:all() -+ ), -+ case couch_util:get_value(Prop, hd(Element), nil) of -+ nil -> -+ erlang:error({assertion_failed, -+ [{module, ?MODULE}, -+ {line, ?LINE}, -+ {reason, "Could not get property '" -+ ++ couch_util:to_list(Prop) -+ ++ "' for task " -+ ++ pid_to_list(Pid)}]}); -+ Value -> -+ Value -+ end. -diff --git a/test/couchdb/couch_util_tests.erl b/test/couchdb/couch_util_tests.erl -new file mode 100644 -index 0000000..8e24e72 ---- /dev/null -+++ b/test/couchdb/couch_util_tests.erl -@@ -0,0 +1,136 @@ -+% Licensed under the Apache License, Version 2.0 (the "License"); you may not -+% use this file except in compliance with the License. You may obtain a copy of -+% the License at -+% -+% http://www.apache.org/licenses/LICENSE-2.0 -+% -+% Unless required by applicable law or agreed to in writing, software -+% distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -+% WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -+% License for the specific language governing permissions and limitations under -+% the License. -+ -+-module(couch_util_tests). -+ -+-include("couch_eunit.hrl"). -+ -+ -+setup() -> -+ %% We cannot start driver from here since it becomes bounded to eunit -+ %% master process and the next couch_server_sup:start_link call will -+ %% fail because server couldn't load driver since it already is. -+ %% -+ %% On other hand, we cannot unload driver here due to -+ %% {error, not_loaded_by_this_process} while it is. Any ideas is welcome. -+ %% -+ couch_server_sup:start_link(?CONFIG_CHAIN), -+ %% couch_config:start_link(?CONFIG_CHAIN), -+ %% {ok, _} = couch_drv:start_link(), -+ ok. -+ -+teardown(_) -> -+ couch_server_sup:stop(), -+ %% couch_config:stop(), -+ %% erl_ddll:unload_driver(couch_icu_driver), -+ ok. -+ -+ -+collation_test_() -> -+ { -+ "Collation tests", -+ [ -+ { -+ setup, -+ fun setup/0, fun teardown/1, -+ [ -+ should_collate_ascii(), -+ should_collate_non_ascii() -+ ] -+ } -+ ] -+ }. -+ -+should_collate_ascii() -> -+ ?_assertEqual(1, couch_util:collate(<<"foo">>, <<"bar">>)). -+ -+should_collate_non_ascii() -> -+ ?_assertEqual(-1, couch_util:collate(<<"A">>, <<"aa">>)). -+ -+to_existed_atom_test() -> -+ ?assert(couch_util:to_existing_atom(true)), -+ ?assertMatch(foo, couch_util:to_existing_atom(<<"foo">>)), -+ ?assertMatch(foobarbaz, couch_util:to_existing_atom("foobarbaz")). -+ -+implode_test() -> -+ ?assertEqual([1, 38, 2, 38, 3], couch_util:implode([1, 2, 3], "&")). -+ -+trim_test() -> -+ lists:map(fun(S) -> ?assertEqual("foo", couch_util:trim(S)) end, -+ [" foo", "foo ", "\tfoo", " foo ", "foo\t", "foo\n", "\nfoo"]). -+ -+abs_pathname_test() -> -+ {ok, Cwd} = file:get_cwd(), -+ ?assertEqual(Cwd ++ "/foo", couch_util:abs_pathname("./foo")). -+ -+flush_test() -> -+ ?assertNot(couch_util:should_flush()), -+ AcquireMem = fun() -> -+ _IntsToAGazillion = lists:seq(1, 200000), -+ _LotsOfData = lists:map(fun(_) -> <<"foobar">> end, -+ lists:seq(1, 500000)), -+ _BigBin = list_to_binary(_LotsOfData), -+ -+ %% Allocation 200K tuples puts us above the memory threshold -+ %% Originally, there should be: -+ %% ?assertNot(should_flush()) -+ %% however, unlike for etap test, GC collects all allocated bits -+ %% making this conditions fail. So we have to invert the condition -+ %% since GC works, cleans the memory and everything is fine. -+ ?assertNot(couch_util:should_flush()) -+ end, -+ AcquireMem(), -+ -+ %% Checking to flush invokes GC -+ ?assertNot(couch_util:should_flush()). -+ -+verify_test() -> -+ ?assert(couch_util:verify("It4Vooya", "It4Vooya")), -+ ?assertNot(couch_util:verify("It4VooyaX", "It4Vooya")), -+ ?assert(couch_util:verify(<<"ahBase3r">>, <<"ahBase3r">>)), -+ ?assertNot(couch_util:verify(<<"ahBase3rX">>, <<"ahBase3r">>)), -+ ?assertNot(couch_util:verify(nil, <<"ahBase3r">>)). -+ -+find_in_binary_test_() -> -+ Cases = [ -+ {<<"foo">>, <<"foobar">>, {exact, 0}}, -+ {<<"foo">>, <<"foofoo">>, {exact, 0}}, -+ {<<"foo">>, <<"barfoo">>, {exact, 3}}, -+ {<<"foo">>, <<"barfo">>, {partial, 3}}, -+ {<<"f">>, <<"fobarfff">>, {exact, 0}}, -+ {<<"f">>, <<"obarfff">>, {exact, 4}}, -+ {<<"f">>, <<"obarggf">>, {exact, 6}}, -+ {<<"f">>, <<"f">>, {exact, 0}}, -+ {<<"f">>, <<"g">>, not_found}, -+ {<<"foo">>, <<"f">>, {partial, 0}}, -+ {<<"foo">>, <<"g">>, not_found}, -+ {<<"foo">>, <<"">>, not_found}, -+ {<<"fofo">>, <<"foofo">>, {partial, 3}}, -+ {<<"foo">>, <<"gfobarfo">>, {partial, 6}}, -+ {<<"foo">>, <<"gfobarf">>, {partial, 6}}, -+ {<<"foo">>, <<"gfobar">>, not_found}, -+ {<<"fog">>, <<"gbarfogquiz">>, {exact, 4}}, -+ {<<"ggg">>, <<"ggg">>, {exact, 0}}, -+ {<<"ggg">>, <<"ggggg">>, {exact, 0}}, -+ {<<"ggg">>, <<"bggg">>, {exact, 1}}, -+ {<<"ggg">>, <<"bbgg">>, {partial, 2}}, -+ {<<"ggg">>, <<"bbbg">>, {partial, 3}}, -+ {<<"ggg">>, <<"bgbggbggg">>, {exact, 6}}, -+ {<<"ggg">>, <<"bgbggb">>, not_found} -+ ], -+ lists:map( -+ fun({Needle, Haystack, Result}) -> -+ Msg = lists:flatten(io_lib:format("Looking for ~s in ~s", -+ [Needle, Haystack])), -+ {Msg, ?_assertMatch(Result, -+ couch_util:find_in_binary(Needle, Haystack))} -+ end, Cases). -diff --git a/test/couchdb/couch_uuids_tests.erl b/test/couchdb/couch_uuids_tests.erl -new file mode 100644 -index 0000000..ea1d034 ---- /dev/null -+++ b/test/couchdb/couch_uuids_tests.erl -@@ -0,0 +1,161 @@ -+% Licensed under the Apache License, Version 2.0 (the "License"); you may not -+% use this file except in compliance with the License. You may obtain a copy of -+% the License at -+% -+% http://www.apache.org/licenses/LICENSE-2.0 -+% -+% Unless required by applicable law or agreed to in writing, software -+% distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -+% WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -+% License for the specific language governing permissions and limitations under -+% the License. -+ -+-module(couch_uuids_tests). -+ -+-include("couch_eunit.hrl"). -+ -+-define(TIMEOUT_S, 20). -+ -+ -+setup() -> -+ {ok, Pid} = couch_config:start_link(?CONFIG_CHAIN), -+ erlang:monitor(process, Pid), -+ couch_uuids:start(), -+ Pid. -+ -+setup(Opts) -> -+ Pid = setup(), -+ lists:foreach( -+ fun({Option, Value}) -> -+ couch_config:set("uuids", Option, Value, false) -+ end, Opts), -+ Pid. -+ -+teardown(Pid) -> -+ couch_uuids:stop(), -+ couch_config:stop(), -+ receive -+ {'DOWN', _, _, Pid, _} -> ok -+ after -+ 1000 -> throw({timeout_error, config_stop}) -+ end. -+ -+teardown(_, Pid) -> -+ teardown(Pid). -+ -+ -+default_test_() -> -+ { -+ "Default UUID algorithm", -+ { -+ setup, -+ fun setup/0, fun teardown/1, -+ fun should_be_unique/1 -+ } -+ }. -+ -+sequential_test_() -> -+ Opts = [{"algorithm", "sequential"}], -+ Cases = [ -+ fun should_be_unique/2, -+ fun should_increment_monotonically/2, -+ fun should_rollover/2 -+ ], -+ { -+ "UUID algorithm: sequential", -+ { -+ foreachx, -+ fun setup/1, fun teardown/2, -+ [{Opts, Fun} || Fun <- Cases] -+ } -+ }. -+ -+utc_test_() -> -+ Opts = [{"algorithm", "utc_random"}], -+ Cases = [ -+ fun should_be_unique/2, -+ fun should_increment_monotonically/2 -+ ], -+ { -+ "UUID algorithm: utc_random", -+ { -+ foreachx, -+ fun setup/1, fun teardown/2, -+ [{Opts, Fun} || Fun <- Cases] -+ } -+ }. -+ -+utc_id_suffix_test_() -> -+ Opts = [{"algorithm", "utc_id"}, {"utc_id_suffix", "bozo"}], -+ Cases = [ -+ fun should_be_unique/2, -+ fun should_increment_monotonically/2, -+ fun should_preserve_suffix/2 -+ ], -+ { -+ "UUID algorithm: utc_id", -+ { -+ foreachx, -+ fun setup/1, fun teardown/2, -+ [{Opts, Fun} || Fun <- Cases] -+ } -+ }. -+ -+ -+should_be_unique() -> -+ %% this one may really runs for too long on slow hosts -+ {timeout, ?TIMEOUT_S, ?_assert(test_unique(10000, [couch_uuids:new()]))}. -+should_be_unique(_) -> -+ should_be_unique(). -+should_be_unique(_, _) -> -+ should_be_unique(). -+ -+should_increment_monotonically(_, _) -> -+ ?_assert(couch_uuids:new() < couch_uuids:new()). -+ -+should_rollover(_, _) -> -+ ?_test(begin -+ UUID = binary_to_list(couch_uuids:new()), -+ Prefix = element(1, lists:split(26, UUID)), -+ N = gen_until_pref_change(Prefix, 0), -+ ?assert(N >= 5000 andalso N =< 11000) -+ end). -+ -+should_preserve_suffix(_, _) -> -+ ?_test(begin -+ UUID = binary_to_list(couch_uuids:new()), -+ Suffix = get_suffix(UUID), -+ ?assert(test_same_suffix(10000, Suffix)) -+ end). -+ -+ -+test_unique(0, _) -> -+ true; -+test_unique(N, UUIDs) -> -+ UUID = couch_uuids:new(), -+ ?assertNot(lists:member(UUID, UUIDs)), -+ test_unique(N - 1, [UUID| UUIDs]). -+ -+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. -+ -+get_suffix(UUID) when is_binary(UUID) -> -+ get_suffix(binary_to_list(UUID)); -+get_suffix(UUID) -> -+ element(2, lists:split(14, UUID)). -+ -+test_same_suffix(0, _) -> -+ true; -+test_same_suffix(N, Suffix) -> -+ case get_suffix(couch_uuids:new()) of -+ Suffix -> test_same_suffix(N - 1, Suffix); -+ _ -> false -+ end. -diff --git a/test/couchdb/couch_work_queue_tests.erl b/test/couchdb/couch_work_queue_tests.erl -new file mode 100644 -index 0000000..8a463b5 ---- /dev/null -+++ b/test/couchdb/couch_work_queue_tests.erl -@@ -0,0 +1,393 @@ -+% Licensed under the Apache License, Version 2.0 (the "License"); you may not -+% use this file except in compliance with the License. You may obtain a copy of -+% the License at -+% -+% http://www.apache.org/licenses/LICENSE-2.0 -+% -+% Unless required by applicable law or agreed to in writing, software -+% distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -+% WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -+% License for the specific language governing permissions and limitations under -+% the License. -+ -+-module(couch_work_queue_tests). -+ -+-include("couch_eunit.hrl"). -+ -+-define(TIMEOUT, 100). -+ -+ -+setup(Opts) -> -+ {ok, Q} = couch_work_queue:new(Opts), -+ Producer = spawn_producer(Q), -+ Consumer = spawn_consumer(Q), -+ {Q, Producer, Consumer}. -+ -+setup_max_items() -> -+ setup([{max_items, 3}]). -+ -+setup_max_size() -> -+ setup([{max_size, 160}]). -+ -+setup_max_items_and_size() -> -+ setup([{max_size, 160}, {max_items, 3}]). -+ -+setup_multi_workers() -> -+ {Q, Producer, Consumer1} = setup([{max_size, 160}, -+ {max_items, 3}, -+ {multi_workers, true}]), -+ Consumer2 = spawn_consumer(Q), -+ Consumer3 = spawn_consumer(Q), -+ {Q, Producer, [Consumer1, Consumer2, Consumer3]}. -+ -+teardown({Q, Producer, Consumers}) when is_list(Consumers) -> -+ % consume all to unblock and let producer/consumer stop without timeout -+ [consume(Consumer, all) || Consumer <- Consumers], -+ -+ ok = close_queue(Q), -+ ok = stop(Producer, "producer"), -+ R = [stop(Consumer, "consumer") || Consumer <- Consumers], -+ R = [ok || _ <- Consumers], -+ ok; -+teardown({Q, Producer, Consumer}) -> -+ teardown({Q, Producer, [Consumer]}). -+ -+ -+single_consumer_test_() -> -+ { -+ "Single producer and consumer", -+ [ -+ { -+ "Queue with 3 max items", -+ { -+ foreach, -+ fun setup_max_items/0, fun teardown/1, -+ single_consumer_max_item_count() ++ common_cases() -+ } -+ }, -+ { -+ "Queue with max size of 160 bytes", -+ { -+ foreach, -+ fun setup_max_size/0, fun teardown/1, -+ single_consumer_max_size() ++ common_cases() -+ } -+ }, -+ { -+ "Queue with max size of 160 bytes and 3 max items", -+ { -+ foreach, -+ fun setup_max_items_and_size/0, fun teardown/1, -+ single_consumer_max_items_and_size() ++ common_cases() -+ } -+ } -+ ] -+ }. -+ -+multiple_consumers_test_() -> -+ { -+ "Single producer and multiple consumers", -+ [ -+ { -+ "Queue with max size of 160 bytes and 3 max items", -+ { -+ foreach, -+ fun setup_multi_workers/0, fun teardown/1, -+ common_cases() ++ multiple_consumers() -+ } -+ -+ } -+ ] -+ }. -+ -+common_cases()-> -+ [ -+ fun should_block_consumer_on_dequeue_from_empty_queue/1, -+ fun should_consume_right_item/1, -+ fun should_timeout_on_close_non_empty_queue/1, -+ fun should_not_block_producer_for_non_empty_queue_after_close/1, -+ fun should_be_closed/1 -+ ]. -+ -+single_consumer_max_item_count()-> -+ [ -+ fun should_have_no_items_for_new_queue/1, -+ fun should_block_producer_on_full_queue_count/1, -+ fun should_receive_first_queued_item/1, -+ fun should_consume_multiple_items/1, -+ fun should_consume_all/1 -+ ]. -+ -+single_consumer_max_size()-> -+ [ -+ fun should_have_zero_size_for_new_queue/1, -+ fun should_block_producer_on_full_queue_size/1, -+ fun should_increase_queue_size_on_produce/1, -+ fun should_receive_first_queued_item/1, -+ fun should_consume_multiple_items/1, -+ fun should_consume_all/1 -+ ]. -+ -+single_consumer_max_items_and_size() -> -+ single_consumer_max_item_count() ++ single_consumer_max_size(). -+ -+multiple_consumers() -> -+ [ -+ fun should_have_zero_size_for_new_queue/1, -+ fun should_have_no_items_for_new_queue/1, -+ fun should_increase_queue_size_on_produce/1 -+ ]. -+ -+ -+should_have_no_items_for_new_queue({Q, _, _}) -> -+ ?_assertEqual(0, couch_work_queue:item_count(Q)). -+ -+should_have_zero_size_for_new_queue({Q, _, _}) -> -+ ?_assertEqual(0, couch_work_queue:size(Q)). -+ -+should_block_consumer_on_dequeue_from_empty_queue({_, _, Consumers}) when is_list(Consumers) -> -+ [consume(C, 2) || C <- Consumers], -+ Pongs = [ping(C) || C <- Consumers], -+ ?_assertEqual([timeout, timeout, timeout], Pongs); -+should_block_consumer_on_dequeue_from_empty_queue({_, _, Consumer}) -> -+ consume(Consumer, 1), -+ Pong = ping(Consumer), -+ ?_assertEqual(timeout, Pong). -+ -+should_consume_right_item({Q, Producer, Consumers}) when is_list(Consumers) -> -+ [consume(C, 3) || C <- Consumers], -+ -+ Item1 = produce(Producer, 10), -+ ok = ping(Producer), -+ ?assertEqual(0, couch_work_queue:item_count(Q)), -+ ?assertEqual(0, couch_work_queue:size(Q)), -+ -+ Item2 = produce(Producer, 10), -+ ok = ping(Producer), -+ ?assertEqual(0, couch_work_queue:item_count(Q)), -+ ?assertEqual(0, couch_work_queue:size(Q)), -+ -+ Item3 = produce(Producer, 10), -+ ok = ping(Producer), -+ ?assertEqual(0, couch_work_queue:item_count(Q)), -+ ?assertEqual(0, couch_work_queue:size(Q)), -+ -+ R = [{ping(C), Item} -+ || {C, Item} <- lists:zip(Consumers, [Item1, Item2, Item3])], -+ -+ ?_assertEqual([{ok, Item1}, {ok, Item2}, {ok, Item3}], R); -+should_consume_right_item({_, Producer, Consumer}) -> -+ consume(Consumer, 1), -+ Item = produce(Producer, 10), -+ produce(Producer, 20), -+ ok = ping(Producer), -+ ok = ping(Consumer), -+ {ok, Items} = last_consumer_items(Consumer), -+ ?_assertEqual([Item], Items). -+ -+should_increase_queue_size_on_produce({Q, Producer, _}) -> -+ produce(Producer, 50), -+ ok = ping(Producer), -+ Count1 = couch_work_queue:item_count(Q), -+ Size1 = couch_work_queue:size(Q), -+ -+ produce(Producer, 10), -+ Count2 = couch_work_queue:item_count(Q), -+ Size2 = couch_work_queue:size(Q), -+ -+ ?_assertEqual([{Count1, Size1}, {Count2, Size2}], [{1, 50}, {2, 60}]). -+ -+should_block_producer_on_full_queue_count({Q, Producer, _}) -> -+ produce(Producer, 10), -+ ?assertEqual(1, couch_work_queue:item_count(Q)), -+ ok = ping(Producer), -+ -+ produce(Producer, 15), -+ ?assertEqual(2, couch_work_queue:item_count(Q)), -+ ok = ping(Producer), -+ -+ produce(Producer, 20), -+ ?assertEqual(3, couch_work_queue:item_count(Q)), -+ Pong = ping(Producer), -+ -+ ?_assertEqual(timeout, Pong). -+ -+should_block_producer_on_full_queue_size({Q, Producer, _}) -> -+ produce(Producer, 100), -+ ok = ping(Producer), -+ ?assertEqual(1, couch_work_queue:item_count(Q)), -+ ?assertEqual(100, couch_work_queue:size(Q)), -+ -+ produce(Producer, 110), -+ Pong = ping(Producer), -+ ?assertEqual(2, couch_work_queue:item_count(Q)), -+ ?assertEqual(210, couch_work_queue:size(Q)), -+ -+ ?_assertEqual(timeout, Pong). -+ -+should_consume_multiple_items({_, Producer, Consumer}) -> -+ Item1 = produce(Producer, 10), -+ ok = ping(Producer), -+ -+ Item2 = produce(Producer, 15), -+ ok = ping(Producer), -+ -+ consume(Consumer, 2), -+ -+ {ok, Items} = last_consumer_items(Consumer), -+ ?_assertEqual([Item1, Item2], Items). -+ -+should_receive_first_queued_item({Q, Producer, Consumer}) -> -+ consume(Consumer, 100), -+ timeout = ping(Consumer), -+ -+ Item = produce(Producer, 11), -+ ok = ping(Producer), -+ -+ ok = ping(Consumer), -+ ?assertEqual(0, couch_work_queue:item_count(Q)), -+ -+ {ok, Items} = last_consumer_items(Consumer), -+ ?_assertEqual([Item], Items). -+ -+should_consume_all({_, Producer, Consumer}) -> -+ Item1 = produce(Producer, 10), -+ Item2 = produce(Producer, 15), -+ Item3 = produce(Producer, 20), -+ -+ consume(Consumer, all), -+ -+ {ok, Items} = last_consumer_items(Consumer), -+ ?_assertEqual([Item1, Item2, Item3], Items). -+ -+should_timeout_on_close_non_empty_queue({Q, Producer, _}) -> -+ produce(Producer, 1), -+ Status = close_queue(Q), -+ -+ ?_assertEqual(timeout, Status). -+ -+should_not_block_producer_for_non_empty_queue_after_close({Q, Producer, _}) -> -+ produce(Producer, 1), -+ close_queue(Q), -+ Pong = ping(Producer), -+ Size = couch_work_queue:size(Q), -+ Count = couch_work_queue:item_count(Q), -+ -+ ?_assertEqual({ok, 1, 1}, {Pong, Size, Count}). -+ -+should_be_closed({Q, _, Consumers}) when is_list(Consumers) -> -+ ok = close_queue(Q), -+ -+ [consume(C, 1) || C <- Consumers], -+ -+ LastConsumerItems = [last_consumer_items(C) || C <- Consumers], -+ ItemsCount = couch_work_queue:item_count(Q), -+ Size = couch_work_queue:size(Q), -+ -+ ?_assertEqual({[closed, closed, closed], closed, closed}, -+ {LastConsumerItems, ItemsCount, Size}); -+should_be_closed({Q, _, Consumer}) -> -+ ok = close_queue(Q), -+ -+ consume(Consumer, 1), -+ -+ LastConsumerItems = last_consumer_items(Consumer), -+ ItemsCount = couch_work_queue:item_count(Q), -+ Size = couch_work_queue:size(Q), -+ -+ ?_assertEqual({closed, closed, closed}, -+ {LastConsumerItems, ItemsCount, Size}). -+ -+ -+close_queue(Q) -> -+ ok = couch_work_queue:close(Q), -+ MonRef = erlang:monitor(process, Q), -+ receive -+ {'DOWN', MonRef, process, Q, _Reason} -> ok -+ after ?TIMEOUT -> -+ erlang:demonitor(MonRef), -+ timeout -+ end. -+ -+spawn_consumer(Q) -> -+ Parent = self(), -+ spawn(fun() -> consumer_loop(Parent, Q, nil) end). -+ -+consumer_loop(Parent, Q, PrevItem) -> -+ receive -+ {stop, Ref} -> -+ Parent ! {ok, Ref}; -+ {ping, Ref} -> -+ Parent ! {pong, Ref}, -+ consumer_loop(Parent, Q, PrevItem); -+ {last_item, Ref} -> -+ Parent ! {item, Ref, PrevItem}, -+ consumer_loop(Parent, Q, PrevItem); -+ {consume, N} -> -+ Result = couch_work_queue:dequeue(Q, N), -+ consumer_loop(Parent, Q, Result) -+ end. -+ -+spawn_producer(Q) -> -+ Parent = self(), -+ spawn(fun() -> producer_loop(Parent, Q) end). -+ -+producer_loop(Parent, Q) -> -+ receive -+ {stop, Ref} -> -+ Parent ! {ok, Ref}; -+ {ping, Ref} -> -+ Parent ! {pong, Ref}, -+ producer_loop(Parent, Q); -+ {produce, Ref, Size} -> -+ Item = crypto:rand_bytes(Size), -+ Parent ! {item, Ref, Item}, -+ ok = couch_work_queue:queue(Q, Item), -+ producer_loop(Parent, Q) -+ end. -+ -+consume(Consumer, N) -> -+ Consumer ! {consume, N}. -+ -+last_consumer_items(Consumer) -> -+ Ref = make_ref(), -+ Consumer ! {last_item, Ref}, -+ receive -+ {item, Ref, Items} -> -+ Items -+ after ?TIMEOUT -> -+ timeout -+ end. -+ -+produce(Producer, Size) -> -+ Ref = make_ref(), -+ Producer ! {produce, Ref, Size}, -+ receive -+ {item, Ref, Item} -> -+ Item -+ after ?TIMEOUT -> -+ erlang:error({assertion_failed, -+ [{module, ?MODULE}, -+ {line, ?LINE}, -+ {reason, "Timeout asking producer to produce an item"}]}) -+ end. -+ -+ping(Pid) -> -+ Ref = make_ref(), -+ Pid ! {ping, Ref}, -+ receive -+ {pong, Ref} -> -+ ok -+ after ?TIMEOUT -> -+ timeout -+ end. -+ -+stop(Pid, Name) -> -+ Ref = make_ref(), -+ Pid ! {stop, Ref}, -+ receive -+ {ok, Ref} -> ok -+ after ?TIMEOUT -> -+ ?debugMsg("Timeout stopping " ++ Name), -+ timeout -+ end. -diff --git a/test/couchdb/couchdb_attachments_tests.erl b/test/couchdb/couchdb_attachments_tests.erl -new file mode 100644 -index 0000000..cf59785 ---- /dev/null -+++ b/test/couchdb/couchdb_attachments_tests.erl -@@ -0,0 +1,638 @@ -+% 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(couchdb_attachments_tests). -+ -+-include("couch_eunit.hrl"). -+-include_lib("couchdb/couch_db.hrl"). -+ -+-define(COMPRESSION_LEVEL, 8). -+-define(ATT_BIN_NAME, <<"logo.png">>). -+-define(ATT_TXT_NAME, <<"file.erl">>). -+-define(FIXTURE_PNG, filename:join([?FIXTURESDIR, "logo.png"])). -+-define(FIXTURE_TXT, ?FILE). -+-define(TIMEOUT, 1000). -+-define(TIMEOUT_EUNIT, 10). -+-define(TIMEWAIT, 100). -+-define(i2l(I), integer_to_list(I)). -+ -+ -+start() -> -+ {ok, Pid} = couch_server_sup:start_link(?CONFIG_CHAIN), -+ % ensure in default compression settings for attachments_compression_tests -+ couch_config:set("attachments", "compression_level", -+ ?i2l(?COMPRESSION_LEVEL), false), -+ couch_config:set("attachments", "compressible_types", "text/*", false), -+ Pid. -+ -+stop(Pid) -> -+ erlang:monitor(process, Pid), -+ couch_server_sup:stop(), -+ receive -+ {'DOWN', _, _, Pid, _} -> -+ ok -+ after ?TIMEOUT -> -+ throw({timeout, server_stop}) -+ end. -+ -+setup() -> -+ DbName = ?tempdb(), -+ {ok, Db} = couch_db:create(DbName, []), -+ ok = couch_db:close(Db), -+ Addr = couch_config:get("httpd", "bind_address", any), -+ Port = mochiweb_socket_server:get(couch_httpd, port), -+ Host = Addr ++ ":" ++ ?i2l(Port), -+ {Host, ?b2l(DbName)}. -+ -+setup({binary, standalone}) -> -+ {Host, DbName} = setup(), -+ setup_att(fun create_standalone_png_att/2, Host, DbName, ?FIXTURE_PNG); -+setup({text, standalone}) -> -+ {Host, DbName} = setup(), -+ setup_att(fun create_standalone_text_att/2, Host, DbName, ?FIXTURE_TXT); -+setup({binary, inline}) -> -+ {Host, DbName} = setup(), -+ setup_att(fun create_inline_png_att/2, Host, DbName, ?FIXTURE_PNG); -+setup({text, inline}) -> -+ {Host, DbName} = setup(), -+ setup_att(fun create_inline_text_att/2, Host, DbName, ?FIXTURE_TXT); -+setup(compressed) -> -+ {Host, DbName} = setup(), -+ setup_att(fun create_already_compressed_att/2, Host, DbName, ?FIXTURE_TXT). -+setup_att(Fun, Host, DbName, File) -> -+ HttpHost = "http://" ++ Host, -+ AttUrl = Fun(HttpHost, DbName), -+ {ok, Data} = file:read_file(File), -+ DocUrl = string:join([HttpHost, DbName, "doc"], "/"), -+ Helpers = {DbName, DocUrl, AttUrl}, -+ {Data, Helpers}. -+ -+teardown(_, {_, {DbName, _, _}}) -> -+ teardown(DbName). -+ -+teardown({_, DbName}) -> -+ teardown(DbName); -+teardown(DbName) -> -+ ok = couch_server:delete(?l2b(DbName), []), -+ ok. -+ -+ -+attachments_test_() -> -+ { -+ "Attachments tests", -+ { -+ setup, -+ fun start/0, fun stop/1, -+ [ -+ attachments_md5_tests(), -+ attachments_compression_tests() -+ ] -+ } -+ }. -+ -+attachments_md5_tests() -> -+ { -+ "Attachments MD5 tests", -+ { -+ foreach, -+ fun setup/0, fun teardown/1, -+ [ -+ fun should_upload_attachment_without_md5/1, -+ fun should_upload_attachment_by_chunks_without_md5/1, -+ fun should_upload_attachment_with_valid_md5_header/1, -+ fun should_upload_attachment_by_chunks_with_valid_md5_header/1, -+ fun should_upload_attachment_by_chunks_with_valid_md5_trailer/1, -+ fun should_reject_attachment_with_invalid_md5/1, -+ fun should_reject_chunked_attachment_with_invalid_md5/1, -+ fun should_reject_chunked_attachment_with_invalid_md5_trailer/1 -+ ] -+ } -+ }. -+ -+attachments_compression_tests() -> -+ Funs = [ -+ fun should_get_att_without_accept_gzip_encoding/2, -+ fun should_get_att_with_accept_gzip_encoding/2, -+ fun should_get_att_with_accept_deflate_encoding/2, -+ fun should_return_406_response_on_unsupported_encoding/2, -+ fun should_get_doc_with_att_data/2, -+ fun should_get_doc_with_att_data_stub/2 -+ ], -+ { -+ "Attachments compression tests", -+ [ -+ { -+ "Created via Attachments API", -+ created_attachments_compression_tests(standalone, Funs) -+ }, -+ { -+ "Created inline via Document API", -+ created_attachments_compression_tests(inline, Funs) -+ }, -+ { -+ "Created already been compressed via Attachments API", -+ { -+ foreachx, -+ fun setup/1, fun teardown/2, -+ [{compressed, Fun} || Fun <- Funs] -+ } -+ }, -+ { -+ foreach, -+ fun setup/0, fun teardown/1, -+ [ -+ fun should_not_create_compressed_att_with_deflate_encoding/1, -+ fun should_not_create_compressed_att_with_compress_encoding/1, -+ fun should_create_compressible_att_with_ctype_params/1 -+ ] -+ } -+ ] -+ }. -+ -+created_attachments_compression_tests(Mod, Funs) -> -+ [ -+ { -+ "Compressiable attachments", -+ { -+ foreachx, -+ fun setup/1, fun teardown/2, -+ [{{text, Mod}, Fun} || Fun <- Funs] -+ } -+ }, -+ { -+ "Uncompressiable attachments", -+ { -+ foreachx, -+ fun setup/1, fun teardown/2, -+ [{{binary, Mod}, Fun} || Fun <- Funs] -+ } -+ } -+ ]. -+ -+ -+ -+should_upload_attachment_without_md5({Host, DbName}) -> -+ ?_test(begin -+ AttUrl = string:join(["", DbName, ?docid(), "readme.txt"], "/"), -+ Body = "We all live in a yellow submarine!", -+ Headers = [ -+ {"Content-Length", "34"}, -+ {"Content-Type", "text/plain"}, -+ {"Host", Host} -+ ], -+ {ok, Code, Json} = request("PUT", AttUrl, Headers, Body), -+ ?assertEqual(201, Code), -+ ?assertEqual(true, get_json(Json, [<<"ok">>])) -+ end). -+ -+should_upload_attachment_by_chunks_without_md5({Host, DbName}) -> -+ ?_test(begin -+ AttUrl = string:join(["", DbName, ?docid(), "readme.txt"], "/"), -+ AttData = <<"We all live in a yellow submarine!">>, -+ <> = AttData, -+ Body = chunked_body([Part1, Part2]), -+ Headers = [ -+ {"Content-Type", "text/plain"}, -+ {"Transfer-Encoding", "chunked"}, -+ {"Host", Host} -+ ], -+ {ok, Code, Json} = request("PUT", AttUrl, Headers, Body), -+ ?assertEqual(201, Code), -+ ?assertEqual(true, get_json(Json, [<<"ok">>])) -+ end). -+ -+should_upload_attachment_with_valid_md5_header({Host, DbName}) -> -+ ?_test(begin -+ AttUrl = string:join(["", DbName, ?docid(), "readme.txt"], "/"), -+ Body = "We all live in a yellow submarine!", -+ Headers = [ -+ {"Content-Length", "34"}, -+ {"Content-Type", "text/plain"}, -+ {"Content-MD5", ?b2l(base64:encode(couch_util:md5(Body)))}, -+ {"Host", Host} -+ ], -+ {ok, Code, Json} = request("PUT", AttUrl, Headers, Body), -+ ?assertEqual(201, Code), -+ ?assertEqual(true, get_json(Json, [<<"ok">>])) -+ end). -+ -+should_upload_attachment_by_chunks_with_valid_md5_header({Host, DbName}) -> -+ ?_test(begin -+ AttUrl = string:join(["", DbName, ?docid(), "readme.txt"], "/"), -+ AttData = <<"We all live in a yellow submarine!">>, -+ <> = AttData, -+ Body = chunked_body([Part1, Part2]), -+ Headers = [ -+ {"Content-Type", "text/plain"}, -+ {"Content-MD5", ?b2l(base64:encode(couch_util:md5(AttData)))}, -+ {"Host", Host}, -+ {"Transfer-Encoding", "chunked"} -+ ], -+ {ok, Code, Json} = request("PUT", AttUrl, Headers, Body), -+ ?assertEqual(201, Code), -+ ?assertEqual(true, get_json(Json, [<<"ok">>])) -+ end). -+ -+should_upload_attachment_by_chunks_with_valid_md5_trailer({Host, DbName}) -> -+ ?_test(begin -+ AttUrl = string:join(["", DbName, ?docid(), "readme.txt"], "/"), -+ AttData = <<"We all live in a yellow submarine!">>, -+ <> = AttData, -+ Body = [chunked_body([Part1, Part2]), -+ "Content-MD5: ", base64:encode(couch_util:md5(AttData)), -+ "\r\n"], -+ Headers = [ -+ {"Content-Type", "text/plain"}, -+ {"Host", Host}, -+ {"Trailer", "Content-MD5"}, -+ {"Transfer-Encoding", "chunked"} -+ ], -+ {ok, Code, Json} = request("PUT", AttUrl, Headers, Body), -+ ?assertEqual(201, Code), -+ ?assertEqual(true, get_json(Json, [<<"ok">>])) -+ end). -+ -+should_reject_attachment_with_invalid_md5({Host, DbName}) -> -+ ?_test(begin -+ AttUrl = string:join(["", DbName, ?docid(), "readme.txt"], "/"), -+ Body = "We all live in a yellow submarine!", -+ Headers = [ -+ {"Content-Length", "34"}, -+ {"Content-Type", "text/plain"}, -+ {"Content-MD5", ?b2l(base64:encode(<<"foobar!">>))}, -+ {"Host", Host} -+ ], -+ {ok, Code, Json} = request("PUT", AttUrl, Headers, Body), -+ ?assertEqual(400, Code), -+ ?assertEqual(<<"content_md5_mismatch">>, -+ get_json(Json, [<<"error">>])) -+ end). -+ -+ -+should_reject_chunked_attachment_with_invalid_md5({Host, DbName}) -> -+ ?_test(begin -+ AttUrl = string:join(["", DbName, ?docid(), "readme.txt"], "/"), -+ AttData = <<"We all live in a yellow submarine!">>, -+ <> = AttData, -+ Body = chunked_body([Part1, Part2]), -+ Headers = [ -+ {"Content-Type", "text/plain"}, -+ {"Content-MD5", ?b2l(base64:encode(<<"foobar!">>))}, -+ {"Host", Host}, -+ {"Transfer-Encoding", "chunked"} -+ ], -+ {ok, Code, Json} = request("PUT", AttUrl, Headers, Body), -+ ?assertEqual(400, Code), -+ ?assertEqual(<<"content_md5_mismatch">>, -+ get_json(Json, [<<"error">>])) -+ end). -+ -+should_reject_chunked_attachment_with_invalid_md5_trailer({Host, DbName}) -> -+ ?_test(begin -+ AttUrl = string:join(["", DbName, ?docid(), "readme.txt"], "/"), -+ AttData = <<"We all live in a yellow submarine!">>, -+ <> = AttData, -+ Body = [chunked_body([Part1, Part2]), -+ "Content-MD5: ", base64:encode(<<"foobar!">>), -+ "\r\n"], -+ Headers = [ -+ {"Content-Type", "text/plain"}, -+ {"Host", Host}, -+ {"Trailer", "Content-MD5"}, -+ {"Transfer-Encoding", "chunked"} -+ ], -+ {ok, Code, Json} = request("PUT", AttUrl, Headers, Body), -+ ?assertEqual(400, Code), -+ ?assertEqual(<<"content_md5_mismatch">>, get_json(Json, [<<"error">>])) -+ end). -+ -+should_get_att_without_accept_gzip_encoding(_, {Data, {_, _, AttUrl}}) -> -+ ?_test(begin -+ {ok, Code, Headers, Body} = test_request:get(AttUrl), -+ ?assertEqual(200, Code), -+ ?assertNot(lists:member({"Content-Encoding", "gzip"}, Headers)), -+ ?assertEqual(Data, iolist_to_binary(Body)) -+ end). -+ -+should_get_att_with_accept_gzip_encoding(compressed, {Data, {_, _, AttUrl}}) -> -+ ?_test(begin -+ {ok, Code, Headers, Body} = test_request:get( -+ AttUrl, [{"Accept-Encoding", "gzip"}]), -+ ?assertEqual(200, Code), -+ ?assert(lists:member({"Content-Encoding", "gzip"}, Headers)), -+ ?assertEqual(Data, zlib:gunzip(iolist_to_binary(Body))) -+ end); -+should_get_att_with_accept_gzip_encoding({text, _}, {Data, {_, _, AttUrl}}) -> -+ ?_test(begin -+ {ok, Code, Headers, Body} = test_request:get( -+ AttUrl, [{"Accept-Encoding", "gzip"}]), -+ ?assertEqual(200, Code), -+ ?assert(lists:member({"Content-Encoding", "gzip"}, Headers)), -+ ?assertEqual(Data, zlib:gunzip(iolist_to_binary(Body))) -+ end); -+should_get_att_with_accept_gzip_encoding({binary, _}, {Data, {_, _, AttUrl}}) -> -+ ?_test(begin -+ {ok, Code, Headers, Body} = test_request:get( -+ AttUrl, [{"Accept-Encoding", "gzip"}]), -+ ?assertEqual(200, Code), -+ ?assertEqual(undefined, -+ couch_util:get_value("Content-Encoding", Headers)), -+ ?assertEqual(Data, iolist_to_binary(Body)) -+ end). -+ -+should_get_att_with_accept_deflate_encoding(_, {Data, {_, _, AttUrl}}) -> -+ ?_test(begin -+ {ok, Code, Headers, Body} = test_request:get( -+ AttUrl, [{"Accept-Encoding", "deflate"}]), -+ ?assertEqual(200, Code), -+ ?assertEqual(undefined, -+ couch_util:get_value("Content-Encoding", Headers)), -+ ?assertEqual(Data, iolist_to_binary(Body)) -+ end). -+ -+should_return_406_response_on_unsupported_encoding(_, {_, {_, _, AttUrl}}) -> -+ ?_assertEqual(406, -+ begin -+ {ok, Code, _, _} = test_request:get( -+ AttUrl, [{"Accept-Encoding", "deflate, *;q=0"}]), -+ Code -+ end). -+ -+should_get_doc_with_att_data(compressed, {Data, {_, DocUrl, _}}) -> -+ ?_test(begin -+ Url = DocUrl ++ "?attachments=true", -+ {ok, Code, _, Body} = test_request:get( -+ Url, [{"Accept", "application/json"}]), -+ ?assertEqual(200, Code), -+ Json = ejson:decode(Body), -+ AttJson = couch_util:get_nested_json_value( -+ Json, [<<"_attachments">>, ?ATT_TXT_NAME]), -+ AttData = couch_util:get_nested_json_value( -+ AttJson, [<<"data">>]), -+ ?assertEqual( -+ <<"text/plain">>, -+ couch_util:get_nested_json_value(AttJson,[<<"content_type">>])), -+ ?assertEqual(Data, base64:decode(AttData)) -+ end); -+should_get_doc_with_att_data({text, _}, {Data, {_, DocUrl, _}}) -> -+ ?_test(begin -+ Url = DocUrl ++ "?attachments=true", -+ {ok, Code, _, Body} = test_request:get( -+ Url, [{"Accept", "application/json"}]), -+ ?assertEqual(200, Code), -+ Json = ejson:decode(Body), -+ AttJson = couch_util:get_nested_json_value( -+ Json, [<<"_attachments">>, ?ATT_TXT_NAME]), -+ AttData = couch_util:get_nested_json_value( -+ AttJson, [<<"data">>]), -+ ?assertEqual( -+ <<"text/plain">>, -+ couch_util:get_nested_json_value(AttJson,[<<"content_type">>])), -+ ?assertEqual(Data, base64:decode(AttData)) -+ end); -+should_get_doc_with_att_data({binary, _}, {Data, {_, DocUrl, _}}) -> -+ ?_test(begin -+ Url = DocUrl ++ "?attachments=true", -+ {ok, Code, _, Body} = test_request:get( -+ Url, [{"Accept", "application/json"}]), -+ ?assertEqual(200, Code), -+ Json = ejson:decode(Body), -+ AttJson = couch_util:get_nested_json_value( -+ Json, [<<"_attachments">>, ?ATT_BIN_NAME]), -+ AttData = couch_util:get_nested_json_value( -+ AttJson, [<<"data">>]), -+ ?assertEqual( -+ <<"image/png">>, -+ couch_util:get_nested_json_value(AttJson,[<<"content_type">>])), -+ ?assertEqual(Data, base64:decode(AttData)) -+ end). -+ -+should_get_doc_with_att_data_stub(compressed, {Data, {_, DocUrl, _}}) -> -+ ?_test(begin -+ Url = DocUrl ++ "?att_encoding_info=true", -+ {ok, Code, _, Body} = test_request:get( -+ Url, [{"Accept", "application/json"}]), -+ ?assertEqual(200, Code), -+ Json = ejson:decode(Body), -+ {AttJson} = couch_util:get_nested_json_value( -+ Json, [<<"_attachments">>, ?ATT_TXT_NAME]), -+ ?assertEqual(<<"gzip">>, -+ couch_util:get_value(<<"encoding">>, AttJson)), -+ AttLength = couch_util:get_value(<<"length">>, AttJson), -+ EncLength = couch_util:get_value(<<"encoded_length">>, AttJson), -+ ?assertEqual(AttLength, EncLength), -+ ?assertEqual(iolist_size(zlib:gzip(Data)), AttLength) -+ end); -+should_get_doc_with_att_data_stub({text, _}, {Data, {_, DocUrl, _}}) -> -+ ?_test(begin -+ Url = DocUrl ++ "?att_encoding_info=true", -+ {ok, Code, _, Body} = test_request:get( -+ Url, [{"Accept", "application/json"}]), -+ ?assertEqual(200, Code), -+ Json = ejson:decode(Body), -+ {AttJson} = couch_util:get_nested_json_value( -+ Json, [<<"_attachments">>, ?ATT_TXT_NAME]), -+ ?assertEqual(<<"gzip">>, -+ couch_util:get_value(<<"encoding">>, AttJson)), -+ AttEncLength = iolist_size(gzip(Data)), -+ ?assertEqual(AttEncLength, -+ couch_util:get_value(<<"encoded_length">>, AttJson)), -+ ?assertEqual(byte_size(Data), -+ couch_util:get_value(<<"length">>, AttJson)) -+ end); -+should_get_doc_with_att_data_stub({binary, _}, {Data, {_, DocUrl, _}}) -> -+ ?_test(begin -+ Url = DocUrl ++ "?att_encoding_info=true", -+ {ok, Code, _, Body} = test_request:get( -+ Url, [{"Accept", "application/json"}]), -+ ?assertEqual(200, Code), -+ Json = ejson:decode(Body), -+ {AttJson} = couch_util:get_nested_json_value( -+ Json, [<<"_attachments">>, ?ATT_BIN_NAME]), -+ ?assertEqual(undefined, -+ couch_util:get_value(<<"encoding">>, AttJson)), -+ ?assertEqual(undefined, -+ couch_util:get_value(<<"encoded_length">>, AttJson)), -+ ?assertEqual(byte_size(Data), -+ couch_util:get_value(<<"length">>, AttJson)) -+ end). -+ -+should_not_create_compressed_att_with_deflate_encoding({Host, DbName}) -> -+ ?_assertEqual(415, -+ begin -+ HttpHost = "http://" ++ Host, -+ AttUrl = string:join([HttpHost, DbName, ?docid(), "file.txt"], "/"), -+ {ok, Data} = file:read_file(?FIXTURE_TXT), -+ Body = zlib:compress(Data), -+ Headers = [ -+ {"Content-Encoding", "deflate"}, -+ {"Content-Type", "text/plain"} -+ ], -+ {ok, Code, _, _} = test_request:put(AttUrl, Headers, Body), -+ Code -+ end). -+ -+should_not_create_compressed_att_with_compress_encoding({Host, DbName}) -> -+ % 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 -+ ?_assertEqual(415, -+ begin -+ HttpHost = "http://" ++ Host, -+ AttUrl = string:join([HttpHost, DbName, ?docid(), "file.txt"], "/"), -+ {ok, Data} = file:read_file(?FIXTURE_TXT), -+ Headers = [ -+ {"Content-Encoding", "compress"}, -+ {"Content-Type", "text/plain"} -+ ], -+ {ok, Code, _, _} = test_request:put(AttUrl, Headers, Data), -+ Code -+ end). -+ -+should_create_compressible_att_with_ctype_params({Host, DbName}) -> -+ {timeout, ?TIMEOUT_EUNIT, ?_test(begin -+ HttpHost = "http://" ++ Host, -+ DocUrl = string:join([HttpHost, DbName, ?docid()], "/"), -+ AttUrl = string:join([DocUrl, ?b2l(?ATT_TXT_NAME)], "/"), -+ {ok, Data} = file:read_file(?FIXTURE_TXT), -+ Headers = [{"Content-Type", "text/plain; charset=UTF-8"}], -+ {ok, Code0, _, _} = test_request:put(AttUrl, Headers, Data), -+ ?assertEqual(201, Code0), -+ -+ {ok, Code1, _, Body} = test_request:get( -+ DocUrl ++ "?att_encoding_info=true"), -+ ?assertEqual(200, Code1), -+ Json = ejson:decode(Body), -+ {AttJson} = couch_util:get_nested_json_value( -+ Json, [<<"_attachments">>, ?ATT_TXT_NAME]), -+ ?assertEqual(<<"gzip">>, -+ couch_util:get_value(<<"encoding">>, AttJson)), -+ AttEncLength = iolist_size(gzip(Data)), -+ ?assertEqual(AttEncLength, -+ couch_util:get_value(<<"encoded_length">>, AttJson)), -+ ?assertEqual(byte_size(Data), -+ couch_util:get_value(<<"length">>, AttJson)) -+ end)}. -+ -+ -+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. -+ -+chunked_body(Chunks) -> -+ chunked_body(Chunks, []). -+ -+chunked_body([], Acc) -> -+ iolist_to_binary(lists:reverse(Acc, "0\r\n")); -+chunked_body([Chunk | Rest], Acc) -> -+ Size = to_hex(size(Chunk)), -+ chunked_body(Rest, ["\r\n", Chunk, "\r\n", Size | Acc]). -+ -+get_socket() -> -+ Options = [binary, {packet, 0}, {active, false}], -+ Addr = couch_config:get("httpd", "bind_address", any), -+ Port = mochiweb_socket_server:get(couch_httpd, port), -+ {ok, Sock} = gen_tcp:connect(Addr, Port, Options), -+ Sock. -+ -+request(Method, Url, Headers, Body) -> -+ RequestHead = [Method, " ", Url, " HTTP/1.1"], -+ RequestHeaders = [[string:join([Key, Value], ": "), "\r\n"] -+ || {Key, Value} <- Headers], -+ Request = [RequestHead, "\r\n", RequestHeaders, "\r\n", Body, "\r\n"], -+ Sock = get_socket(), -+ gen_tcp:send(Sock, list_to_binary(lists:flatten(Request))), -+ timer:sleep(?TIMEWAIT), % must wait to receive complete response -+ {ok, R} = gen_tcp:recv(Sock, 0), -+ gen_tcp:close(Sock), -+ [Header, Body1] = re:split(R, "\r\n\r\n", [{return, binary}]), -+ {ok, {http_response, _, Code, _}, _} = -+ erlang:decode_packet(http, Header, []), -+ Json = ejson:decode(Body1), -+ {ok, Code, Json}. -+ -+create_standalone_text_att(Host, DbName) -> -+ {ok, Data} = file:read_file(?FIXTURE_TXT), -+ Url = string:join([Host, DbName, "doc", ?b2l(?ATT_TXT_NAME)], "/"), -+ {ok, Code, _Headers, _Body} = test_request:put( -+ Url, [{"Content-Type", "text/plain"}], Data), -+ ?assertEqual(201, Code), -+ Url. -+ -+create_standalone_png_att(Host, DbName) -> -+ {ok, Data} = file:read_file(?FIXTURE_PNG), -+ Url = string:join([Host, DbName, "doc", ?b2l(?ATT_BIN_NAME)], "/"), -+ {ok, Code, _Headers, _Body} = test_request:put( -+ Url, [{"Content-Type", "image/png"}], Data), -+ ?assertEqual(201, Code), -+ Url. -+ -+create_inline_text_att(Host, DbName) -> -+ {ok, Data} = file:read_file(?FIXTURE_TXT), -+ Url = string:join([Host, DbName, "doc"], "/"), -+ Doc = {[ -+ {<<"_attachments">>, {[ -+ {?ATT_TXT_NAME, {[ -+ {<<"content_type">>, <<"text/plain">>}, -+ {<<"data">>, base64:encode(Data)} -+ ]} -+ }]}} -+ ]}, -+ {ok, Code, _Headers, _Body} = test_request:put( -+ Url, [{"Content-Type", "application/json"}], ejson:encode(Doc)), -+ ?assertEqual(201, Code), -+ string:join([Url, ?b2l(?ATT_TXT_NAME)], "/"). -+ -+create_inline_png_att(Host, DbName) -> -+ {ok, Data} = file:read_file(?FIXTURE_PNG), -+ Url = string:join([Host, DbName, "doc"], "/"), -+ Doc = {[ -+ {<<"_attachments">>, {[ -+ {?ATT_BIN_NAME, {[ -+ {<<"content_type">>, <<"image/png">>}, -+ {<<"data">>, base64:encode(Data)} -+ ]} -+ }]}} -+ ]}, -+ {ok, Code, _Headers, _Body} = test_request:put( -+ Url, [{"Content-Type", "application/json"}], ejson:encode(Doc)), -+ ?assertEqual(201, Code), -+ string:join([Url, ?b2l(?ATT_BIN_NAME)], "/"). -+ -+create_already_compressed_att(Host, DbName) -> -+ {ok, Data} = file:read_file(?FIXTURE_TXT), -+ Url = string:join([Host, DbName, "doc", ?b2l(?ATT_TXT_NAME)], "/"), -+ {ok, Code, _Headers, _Body} = test_request:put( -+ Url, [{"Content-Type", "text/plain"}, {"Content-Encoding", "gzip"}], -+ zlib:gzip(Data)), -+ ?assertEqual(201, Code), -+ Url. -+ -+gzip(Data) -> -+ Z = zlib:open(), -+ ok = zlib:deflateInit(Z, ?COMPRESSION_LEVEL, deflated, 16 + 15, 8, default), -+ zlib:deflate(Z, Data), -+ Last = zlib:deflate(Z, [], finish), -+ ok = zlib:deflateEnd(Z), -+ ok = zlib:close(Z), -+ Last. -diff --git a/test/couchdb/couchdb_compaction_daemon.erl b/test/couchdb/couchdb_compaction_daemon.erl -new file mode 100644 -index 0000000..725a97b ---- /dev/null -+++ b/test/couchdb/couchdb_compaction_daemon.erl -@@ -0,0 +1,231 @@ -+% 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(couchdb_compaction_daemon). -+ -+-include("couch_eunit.hrl"). -+-include_lib("couchdb/couch_db.hrl"). -+ -+-define(ADMIN_USER, {user_ctx, #user_ctx{roles=[<<"_admin">>]}}). -+-define(DELAY, 100). -+-define(TIMEOUT, 30000). -+-define(TIMEOUT_S, ?TIMEOUT div 1000). -+ -+ -+start() -> -+ {ok, Pid} = couch_server_sup:start_link(?CONFIG_CHAIN), -+ couch_config:set("compaction_daemon", "check_interval", "3", false), -+ couch_config:set("compaction_daemon", "min_file_size", "100000", false), -+ Pid. -+ -+stop(Pid) -> -+ erlang:monitor(process, Pid), -+ couch_server_sup:stop(), -+ receive -+ {'DOWN', _, _, Pid, _} -> -+ ok -+ after ?TIMEOUT -> -+ throw({timeout, server_stop}) -+ end. -+ -+setup() -> -+ DbName = ?tempdb(), -+ {ok, Db} = couch_db:create(DbName, [?ADMIN_USER]), -+ create_design_doc(Db), -+ ok = couch_db:close(Db), -+ DbName. -+ -+teardown(DbName) -> -+ Configs = couch_config:get("compactions"), -+ lists:foreach( -+ fun({Key, _}) -> -+ ok = couch_config:delete("compactions", Key, false) -+ end, -+ Configs), -+ couch_server:delete(DbName, [?ADMIN_USER]), -+ ok. -+ -+ -+compaction_daemon_test_() -> -+ { -+ "Compaction daemon tests", -+ { -+ setup, -+ fun start/0, fun stop/1, -+ { -+ foreach, -+ fun setup/0, fun teardown/1, -+ [ -+ fun should_compact_by_default_rule/1, -+ fun should_compact_by_dbname_rule/1 -+ ] -+ } -+ } -+ }. -+ -+ -+should_compact_by_default_rule(DbName) -> -+ {timeout, ?TIMEOUT_S, ?_test(begin -+ {ok, Db} = couch_db:open_int(DbName, []), -+ populate(DbName, 70, 70, 200 * 1024), -+ -+ {_, DbFileSize} = get_db_frag(DbName), -+ {_, ViewFileSize} = get_view_frag(DbName), -+ -+ ok = couch_config:set("compactions", "_default", -+ "[{db_fragmentation, \"70%\"}, {view_fragmentation, \"70%\"}]", -+ false), -+ -+ ok = timer:sleep(4000), % something >= check_interval -+ wait_compaction_finished(DbName), -+ ok = couch_config:delete("compactions", "_default", false), -+ -+ {DbFrag2, DbFileSize2} = get_db_frag(DbName), -+ {ViewFrag2, ViewFileSize2} = get_view_frag(DbName), -+ -+ ?assert(DbFrag2 < 70), -+ ?assert(ViewFrag2 < 70), -+ -+ ?assert(DbFileSize > DbFileSize2), -+ ?assert(ViewFileSize > ViewFileSize2), -+ -+ ?assert(couch_db:is_idle(Db)), -+ ok = couch_db:close(Db) -+ end)}. -+ -+should_compact_by_dbname_rule(DbName) -> -+ {timeout, ?TIMEOUT_S, ?_test(begin -+ {ok, Db} = couch_db:open_int(DbName, []), -+ populate(DbName, 70, 70, 200 * 1024), -+ -+ {_, DbFileSize} = get_db_frag(DbName), -+ {_, ViewFileSize} = get_view_frag(DbName), -+ -+ ok = couch_config:set("compactions", ?b2l(DbName), -+ "[{db_fragmentation, \"70%\"}, {view_fragmentation, \"70%\"}]", -+ false), -+ -+ ok = timer:sleep(4000), % something >= check_interval -+ wait_compaction_finished(DbName), -+ ok = couch_config:delete("compactions", ?b2l(DbName), false), -+ -+ {DbFrag2, DbFileSize2} = get_db_frag(DbName), -+ {ViewFrag2, ViewFileSize2} = get_view_frag(DbName), -+ -+ ?assert(DbFrag2 < 70), -+ ?assert(ViewFrag2 < 70), -+ -+ ?assert(DbFileSize > DbFileSize2), -+ ?assert(ViewFileSize > ViewFileSize2), -+ -+ ?assert(couch_db:is_idle(Db)), -+ ok = couch_db:close(Db) -+ end)}. -+ -+ -+create_design_doc(Db) -> -+ DDoc = couch_doc:from_json_obj({[ -+ {<<"_id">>, <<"_design/foo">>}, -+ {<<"language">>, <<"javascript">>}, -+ {<<"views">>, {[ -+ {<<"foo">>, {[ -+ {<<"map">>, <<"function(doc) { emit(doc._id, doc); }">>} -+ ]}}, -+ {<<"foo2">>, {[ -+ {<<"map">>, <<"function(doc) { emit(doc._id, doc); }">>} -+ ]}}, -+ {<<"foo3">>, {[ -+ {<<"map">>, <<"function(doc) { emit(doc._id, doc); }">>} -+ ]}} -+ ]}} -+ ]}), -+ {ok, _} = couch_db:update_docs(Db, [DDoc]), -+ {ok, _} = couch_db:ensure_full_commit(Db), -+ ok. -+ -+populate(DbName, DbFrag, ViewFrag, MinFileSize) -> -+ {CurDbFrag, DbFileSize} = get_db_frag(DbName), -+ {CurViewFrag, ViewFileSize} = get_view_frag(DbName), -+ populate(DbName, DbFrag, ViewFrag, MinFileSize, CurDbFrag, CurViewFrag, -+ lists:min([DbFileSize, ViewFileSize])). -+ -+populate(_Db, DbFrag, ViewFrag, MinFileSize, CurDbFrag, CurViewFrag, FileSize) -+ when CurDbFrag >= DbFrag, CurViewFrag >= ViewFrag, FileSize >= MinFileSize -> -+ ok; -+populate(DbName, DbFrag, ViewFrag, MinFileSize, _, _, _) -> -+ update(DbName), -+ {CurDbFrag, DbFileSize} = get_db_frag(DbName), -+ {CurViewFrag, ViewFileSize} = get_view_frag(DbName), -+ populate(DbName, DbFrag, ViewFrag, MinFileSize, CurDbFrag, CurViewFrag, -+ lists:min([DbFileSize, ViewFileSize])). -+ -+update(DbName) -> -+ {ok, Db} = couch_db:open_int(DbName, []), -+ lists:foreach(fun(_) -> -+ Doc = couch_doc:from_json_obj({[{<<"_id">>, couch_uuids:new()}]}), -+ {ok, _} = couch_db:update_docs(Db, [Doc]), -+ query_view(Db#db.name) -+ end, lists:seq(1, 200)), -+ couch_db:close(Db). -+ -+db_url(DbName) -> -+ Addr = couch_config:get("httpd", "bind_address", "127.0.0.1"), -+ Port = integer_to_list(mochiweb_socket_server:get(couch_httpd, port)), -+ "http://" ++ Addr ++ ":" ++ Port ++ "/" ++ ?b2l(DbName). -+ -+query_view(DbName) -> -+ {ok, Code, _Headers, _Body} = test_request:get( -+ db_url(DbName) ++ "/_design/foo/_view/foo"), -+ ?assertEqual(200, Code). -+ -+get_db_frag(DbName) -> -+ {ok, Db} = couch_db:open_int(DbName, []), -+ {ok, Info} = couch_db:get_db_info(Db), -+ couch_db:close(Db), -+ FileSize = couch_util:get_value(disk_size, Info), -+ DataSize = couch_util:get_value(data_size, Info), -+ {round((FileSize - DataSize) / FileSize * 100), FileSize}. -+ -+get_view_frag(DbName) -> -+ {ok, Db} = couch_db:open_int(DbName, []), -+ {ok, Info} = couch_mrview:get_info(Db, <<"_design/foo">>), -+ couch_db:close(Db), -+ FileSize = couch_util:get_value(disk_size, Info), -+ DataSize = couch_util:get_value(data_size, Info), -+ {round((FileSize - DataSize) / FileSize * 100), FileSize}. -+ -+wait_compaction_finished(DbName) -> -+ Parent = self(), -+ Loop = spawn_link(fun() -> wait_loop(DbName, Parent) end), -+ receive -+ {done, Loop} -> -+ ok -+ after ?TIMEOUT -> -+ erlang:error( -+ {assertion_failed, -+ [{module, ?MODULE}, {line, ?LINE}, -+ {reason, "Compaction timeout"}]}) -+ end. -+ -+wait_loop(DbName, Parent) -> -+ {ok, Db} = couch_db:open_int(DbName, []), -+ {ok, DbInfo} = couch_db:get_db_info(Db), -+ {ok, ViewInfo} = couch_mrview:get_info(Db, <<"_design/foo">>), -+ couch_db:close(Db), -+ case (couch_util:get_value(compact_running, ViewInfo) =:= true) orelse -+ (couch_util:get_value(compact_running, DbInfo) =:= true) of -+ false -> -+ Parent ! {done, self()}; -+ true -> -+ ok = timer:sleep(?DELAY), -+ wait_loop(DbName, Parent) -+ end. -diff --git a/test/couchdb/couchdb_cors_tests.erl b/test/couchdb/couchdb_cors_tests.erl -new file mode 100644 -index 0000000..4e88ae7 ---- /dev/null -+++ b/test/couchdb/couchdb_cors_tests.erl -@@ -0,0 +1,344 @@ -+% 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(couchdb_cors_tests). -+ -+-include("couch_eunit.hrl"). -+-include_lib("couchdb/couch_db.hrl"). -+ -+ -+-define(ADMIN_USER, {user_ctx, #user_ctx{roles=[<<"_admin">>]}}). -+-define(SUPPORTED_METHODS, -+ "GET, HEAD, POST, PUT, DELETE, TRACE, CONNECT, COPY, OPTIONS"). -+-define(TIMEOUT, 1000). -+ -+ -+start() -> -+ {ok, Pid} = couch_server_sup:start_link(?CONFIG_CHAIN), -+ ok = couch_config:set("httpd", "enable_cors", "true", false), -+ ok = couch_config:set("vhosts", "example.com", "/", false), -+ Pid. -+ -+stop(Pid) -> -+ couch_server_sup:stop(), -+ erlang:monitor(process, Pid), -+ receive -+ {'DOWN', _, _, Pid, _} -> -+ ok -+ after ?TIMEOUT -> -+ throw({timeout, server_stop}) -+ end. -+ -+setup() -> -+ DbName = ?tempdb(), -+ {ok, Db} = couch_db:create(DbName, [?ADMIN_USER]), -+ couch_db:close(Db), -+ -+ couch_config:set("cors", "credentials", "false", false), -+ couch_config:set("cors", "origins", "http://example.com", false), -+ -+ Addr = couch_config:get("httpd", "bind_address", "127.0.0.1"), -+ Port = integer_to_list(mochiweb_socket_server:get(couch_httpd, port)), -+ Host = "http://" ++ Addr ++ ":" ++ Port, -+ {Host, ?b2l(DbName)}. -+ -+setup({Mod, VHost}) -> -+ {Host, DbName} = setup(), -+ Url = case Mod of -+ server -> -+ Host; -+ db -> -+ Host ++ "/" ++ DbName -+ end, -+ DefaultHeaders = [{"Origin", "http://example.com"}] -+ ++ maybe_append_vhost(VHost), -+ {Host, DbName, Url, DefaultHeaders}. -+ -+teardown(DbName) when is_list(DbName) -> -+ ok = couch_server:delete(?l2b(DbName), [?ADMIN_USER]), -+ ok; -+teardown({_, DbName}) -> -+ teardown(DbName). -+ -+teardown(_, {_, DbName, _, _}) -> -+ teardown(DbName). -+ -+ -+cors_test_() -> -+ Funs = [ -+ fun should_not_allow_origin/2, -+ fun should_not_allow_origin_with_port_mismatch/2, -+ fun should_not_allow_origin_with_scheme_mismatch/2, -+ fun should_not_all_origin_due_case_mismatch/2, -+ fun should_make_simple_request/2, -+ fun should_make_preflight_request/2, -+ fun should_make_prefligh_request_with_port/2, -+ fun should_make_prefligh_request_with_scheme/2, -+ fun should_make_prefligh_request_with_wildcard_origin/2, -+ fun should_make_request_with_credentials/2, -+ fun should_make_origin_request_with_auth/2, -+ fun should_make_preflight_request_with_auth/2 -+ ], -+ { -+ "CORS (COUCHDB-431)", -+ { -+ setup, -+ fun start/0, fun stop/1, -+ [ -+ cors_tests(Funs), -+ vhost_cors_tests(Funs), -+ headers_tests() -+ ] -+ } -+ }. -+ -+headers_tests() -> -+ { -+ "Various headers tests", -+ { -+ foreach, -+ fun setup/0, fun teardown/1, -+ [ -+ fun should_not_return_cors_headers_for_invalid_origin/1, -+ fun should_not_return_cors_headers_for_invalid_origin_preflight/1, -+ fun should_make_request_against_attachment/1, -+ fun should_make_range_request_against_attachment/1, -+ fun should_make_request_with_if_none_match_header/1 -+ ] -+ } -+ }. -+ -+cors_tests(Funs) -> -+ { -+ "CORS tests", -+ [ -+ make_test_case(server, false, Funs), -+ make_test_case(db, false, Funs) -+ ] -+ }. -+ -+vhost_cors_tests(Funs) -> -+ { -+ "Virtual Host CORS", -+ [ -+ make_test_case(server, true, Funs), -+ make_test_case(db, true, Funs) -+ ] -+ }. -+ -+make_test_case(Mod, UseVhost, Funs) -> -+ { -+ case Mod of server -> "Server"; db -> "Database" end, -+ {foreachx, fun setup/1, fun teardown/2, [{{Mod, UseVhost}, Fun} -+ || Fun <- Funs]} -+ }. -+ -+ -+should_not_allow_origin(_, {_, _, Url, Headers0}) -> -+ ?_assertEqual(undefined, -+ begin -+ couch_config:delete("cors", "origins", false), -+ Headers1 = proplists:delete("Origin", Headers0), -+ Headers = [{"Origin", "http://127.0.0.1"}] -+ ++ Headers1, -+ {ok, _, Resp, _} = test_request:get(Url, Headers), -+ proplists:get_value("Access-Control-Allow-Origin", Resp) -+ end). -+ -+should_not_allow_origin_with_port_mismatch({_, VHost}, {_, _, Url, _}) -> -+ ?_assertEqual(undefined, -+ begin -+ Headers = [{"Origin", "http://example.com:5984"}, -+ {"Access-Control-Request-Method", "GET"}] -+ ++ maybe_append_vhost(VHost), -+ {ok, _, Resp, _} = test_request:options(Url, Headers), -+ proplists:get_value("Access-Control-Allow-Origin", Resp) -+ end). -+ -+should_not_allow_origin_with_scheme_mismatch({_, VHost}, {_, _, Url, _}) -> -+ ?_assertEqual(undefined, -+ begin -+ Headers = [{"Origin", "http://example.com:5984"}, -+ {"Access-Control-Request-Method", "GET"}] -+ ++ maybe_append_vhost(VHost), -+ {ok, _, Resp, _} = test_request:options(Url, Headers), -+ proplists:get_value("Access-Control-Allow-Origin", Resp) -+ end). -+ -+should_not_all_origin_due_case_mismatch({_, VHost}, {_, _, Url, _}) -> -+ ?_assertEqual(undefined, -+ begin -+ Headers = [{"Origin", "http://ExAmPlE.CoM"}, -+ {"Access-Control-Request-Method", "GET"}] -+ ++ maybe_append_vhost(VHost), -+ {ok, _, Resp, _} = test_request:options(Url, Headers), -+ proplists:get_value("Access-Control-Allow-Origin", Resp) -+ end). -+ -+should_make_simple_request(_, {_, _, Url, DefaultHeaders}) -> -+ ?_test(begin -+ {ok, _, Resp, _} = test_request:get(Url, DefaultHeaders), -+ ?assertEqual( -+ undefined, -+ proplists:get_value("Access-Control-Allow-Credentials", Resp)), -+ ?assertEqual( -+ "http://example.com", -+ proplists:get_value("Access-Control-Allow-Origin", Resp)), -+ ?assertEqual( -+ "Cache-Control, Content-Type, Server", -+ proplists:get_value("Access-Control-Expose-Headers", Resp)) -+ end). -+ -+should_make_preflight_request(_, {_, _, Url, DefaultHeaders}) -> -+ ?_assertEqual(?SUPPORTED_METHODS, -+ begin -+ Headers = DefaultHeaders -+ ++ [{"Access-Control-Request-Method", "GET"}], -+ {ok, _, Resp, _} = test_request:options(Url, Headers), -+ proplists:get_value("Access-Control-Allow-Methods", Resp) -+ end). -+ -+should_make_prefligh_request_with_port({_, VHost}, {_, _, Url, _}) -> -+ ?_assertEqual("http://example.com:5984", -+ begin -+ couch_config:set("cors", "origins", "http://example.com:5984", -+ false), -+ Headers = [{"Origin", "http://example.com:5984"}, -+ {"Access-Control-Request-Method", "GET"}] -+ ++ maybe_append_vhost(VHost), -+ {ok, _, Resp, _} = test_request:options(Url, Headers), -+ proplists:get_value("Access-Control-Allow-Origin", Resp) -+ end). -+ -+should_make_prefligh_request_with_scheme({_, VHost}, {_, _, Url, _}) -> -+ ?_assertEqual("https://example.com:5984", -+ begin -+ couch_config:set("cors", "origins", "https://example.com:5984", -+ false), -+ Headers = [{"Origin", "https://example.com:5984"}, -+ {"Access-Control-Request-Method", "GET"}] -+ ++ maybe_append_vhost(VHost), -+ {ok, _, Resp, _} = test_request:options(Url, Headers), -+ proplists:get_value("Access-Control-Allow-Origin", Resp) -+ end). -+ -+should_make_prefligh_request_with_wildcard_origin({_, VHost}, {_, _, Url, _}) -> -+ ?_assertEqual("https://example.com:5984", -+ begin -+ couch_config:set("cors", "origins", "*", false), -+ Headers = [{"Origin", "https://example.com:5984"}, -+ {"Access-Control-Request-Method", "GET"}] -+ ++ maybe_append_vhost(VHost), -+ {ok, _, Resp, _} = test_request:options(Url, Headers), -+ proplists:get_value("Access-Control-Allow-Origin", Resp) -+ end). -+ -+should_make_request_with_credentials(_, {_, _, Url, DefaultHeaders}) -> -+ ?_assertEqual("true", -+ begin -+ ok = couch_config:set("cors", "credentials", "true", false), -+ {ok, _, Resp, _} = test_request:options(Url, DefaultHeaders), -+ proplists:get_value("Access-Control-Allow-Credentials", Resp) -+ end). -+ -+should_make_origin_request_with_auth(_, {_, _, Url, DefaultHeaders}) -> -+ ?_assertEqual("http://example.com", -+ begin -+ Hashed = couch_passwords:hash_admin_password(<<"test">>), -+ couch_config:set("admins", "test", Hashed, false), -+ {ok, _, Resp, _} = test_request:get( -+ Url, DefaultHeaders, [{basic_auth, {"test", "test"}}]), -+ couch_config:delete("admins", "test", false), -+ proplists:get_value("Access-Control-Allow-Origin", Resp) -+ end). -+ -+should_make_preflight_request_with_auth(_, {_, _, Url, DefaultHeaders}) -> -+ ?_assertEqual(?SUPPORTED_METHODS, -+ begin -+ Hashed = couch_passwords:hash_admin_password(<<"test">>), -+ couch_config:set("admins", "test", Hashed, false), -+ Headers = DefaultHeaders -+ ++ [{"Access-Control-Request-Method", "GET"}], -+ {ok, _, Resp, _} = test_request:options( -+ Url, Headers, [{basic_auth, {"test", "test"}}]), -+ couch_config:delete("admins", "test", false), -+ proplists:get_value("Access-Control-Allow-Methods", Resp) -+ end). -+ -+should_not_return_cors_headers_for_invalid_origin({Host, _}) -> -+ ?_assertEqual(undefined, -+ begin -+ Headers = [{"Origin", "http://127.0.0.1"}], -+ {ok, _, Resp, _} = test_request:get(Host, Headers), -+ proplists:get_value("Access-Control-Allow-Origin", Resp) -+ end). -+ -+should_not_return_cors_headers_for_invalid_origin_preflight({Host, _}) -> -+ ?_assertEqual(undefined, -+ begin -+ Headers = [{"Origin", "http://127.0.0.1"}, -+ {"Access-Control-Request-Method", "GET"}], -+ {ok, _, Resp, _} = test_request:options(Host, Headers), -+ proplists:get_value("Access-Control-Allow-Origin", Resp) -+ end). -+ -+should_make_request_against_attachment({Host, DbName}) -> -+ {"COUCHDB-1689", -+ ?_assertEqual(200, -+ begin -+ Url = Host ++ "/" ++ DbName, -+ {ok, Code0, _, _} = test_request:put( -+ Url ++ "/doc/file.txt", [{"Content-Type", "text/plain"}], -+ "hello, couch!"), -+ ?assert(Code0 =:= 201), -+ {ok, Code, _, _} = test_request:get( -+ Url ++ "/doc?attachments=true", -+ [{"Origin", "http://example.com"}]), -+ Code -+ end)}. -+ -+should_make_range_request_against_attachment({Host, DbName}) -> -+ {"COUCHDB-1689", -+ ?_assertEqual(206, -+ begin -+ Url = Host ++ "/" ++ DbName, -+ {ok, Code0, _, _} = test_request:put( -+ Url ++ "/doc/file.txt", -+ [{"Content-Type", "application/octet-stream"}], -+ "hello, couch!"), -+ ?assert(Code0 =:= 201), -+ {ok, Code, _, _} = test_request:get( -+ Url ++ "/doc/file.txt", [{"Origin", "http://example.com"}, -+ {"Range", "bytes=0-6"}]), -+ Code -+ end)}. -+ -+should_make_request_with_if_none_match_header({Host, DbName}) -> -+ {"COUCHDB-1697", -+ ?_assertEqual(304, -+ begin -+ Url = Host ++ "/" ++ DbName, -+ {ok, Code0, Headers0, _} = test_request:put( -+ Url ++ "/doc", [{"Content-Type", "application/json"}], "{}"), -+ ?assert(Code0 =:= 201), -+ ETag = proplists:get_value("ETag", Headers0), -+ {ok, Code, _, _} = test_request:get( -+ Url ++ "/doc", [{"Origin", "http://example.com"}, -+ {"If-None-Match", ETag}]), -+ Code -+ end)}. -+ -+ -+maybe_append_vhost(true) -> -+ [{"Host", "http://example.com"}]; -+maybe_append_vhost(false) -> -+ []. -diff --git a/test/couchdb/couchdb_file_compression_tests.erl b/test/couchdb/couchdb_file_compression_tests.erl -new file mode 100644 -index 0000000..fd3f513 ---- /dev/null -+++ b/test/couchdb/couchdb_file_compression_tests.erl -@@ -0,0 +1,239 @@ -+% 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(couchdb_file_compression_tests). -+ -+-include("couch_eunit.hrl"). -+-include_lib("couchdb/couch_db.hrl"). -+ -+-define(ADMIN_USER, {user_ctx, #user_ctx{roles=[<<"_admin">>]}}). -+-define(DDOC_ID, <<"_design/test">>). -+-define(DOCS_COUNT, 5000). -+-define(TIMEOUT, 30000). -+ -+ -+start() -> -+ {ok, Pid} = couch_server_sup:start_link(?CONFIG_CHAIN), -+ Pid. -+ -+stop(Pid) -> -+ erlang:monitor(process, Pid), -+ couch_server_sup:stop(), -+ receive -+ {'DOWN', _, _, Pid, _} -> -+ ok -+ after ?TIMEOUT -> -+ throw({timeout, server_stop}) -+ end. -+ -+setup() -> -+ couch_config:set("couchdb", "file_compression", "none", false), -+ DbName = ?tempdb(), -+ {ok, Db} = couch_db:create(DbName, [?ADMIN_USER]), -+ ok = populate_db(Db, ?DOCS_COUNT), -+ DDoc = couch_doc:from_json_obj({[ -+ {<<"_id">>, ?DDOC_ID}, -+ {<<"language">>, <<"javascript">>}, -+ {<<"views">>, {[ -+ {<<"by_id">>, {[ -+ {<<"map">>, <<"function(doc){emit(doc._id, doc.string);}">>} -+ ]}} -+ ]} -+ } -+ ]}), -+ {ok, _} = couch_db:update_doc(Db, DDoc, []), -+ refresh_index(DbName), -+ ok = couch_db:close(Db), -+ DbName. -+ -+teardown(DbName) -> -+ ok = couch_server:delete(DbName, [?ADMIN_USER]), -+ ok. -+ -+ -+couch_auth_cache_test_() -> -+ { -+ "CouchDB file compression tests", -+ { -+ setup, -+ fun start/0, fun stop/1, -+ { -+ foreach, -+ fun setup/0, fun teardown/1, -+ [ -+ fun should_use_none/1, -+ fun should_use_deflate_1/1, -+ fun should_use_deflate_9/1, -+ fun should_use_snappy/1, -+ fun should_compare_compression_methods/1 -+ ] -+ } -+ } -+ }. -+ -+ -+should_use_none(DbName) -> -+ couch_config:set("couchdb", "file_compression", "none", false), -+ { -+ "Use no compression", -+ [ -+ {"compact database", ?_test(compact_db(DbName))}, -+ {"compact view", ?_test(compact_view(DbName))} -+ ] -+ }. -+ -+should_use_deflate_1(DbName) -> -+ couch_config:set("couchdb", "file_compression", "deflate_1", false), -+ { -+ "Use deflate compression at level 1", -+ [ -+ {"compact database", ?_test(compact_db(DbName))}, -+ {"compact view", ?_test(compact_view(DbName))} -+ ] -+ }. -+ -+should_use_deflate_9(DbName) -> -+ couch_config:set("couchdb", "file_compression", "deflate_9", false), -+ { -+ "Use deflate compression at level 9", -+ [ -+ {"compact database", ?_test(compact_db(DbName))}, -+ {"compact view", ?_test(compact_view(DbName))} -+ ] -+ }. -+ -+should_use_snappy(DbName) -> -+ couch_config:set("couchdb", "file_compression", "snappy", false), -+ { -+ "Use snappy compression", -+ [ -+ {"compact database", ?_test(compact_db(DbName))}, -+ {"compact view", ?_test(compact_view(DbName))} -+ ] -+ }. -+ -+should_compare_compression_methods(DbName) -> -+ {"none > snappy > deflate_1 > deflate_9", -+ {timeout, ?TIMEOUT div 1000, ?_test(compare_compression_methods(DbName))}}. -+ -+compare_compression_methods(DbName) -> -+ couch_config:set("couchdb", "file_compression", "none", false), -+ compact_db(DbName), -+ compact_view(DbName), -+ DbSizeNone = db_disk_size(DbName), -+ ViewSizeNone = view_disk_size(DbName), -+ -+ couch_config:set("couchdb", "file_compression", "snappy", false), -+ compact_db(DbName), -+ compact_view(DbName), -+ DbSizeSnappy = db_disk_size(DbName), -+ ViewSizeSnappy = view_disk_size(DbName), -+ -+ ?assert(DbSizeNone > DbSizeSnappy), -+ ?assert(ViewSizeNone > ViewSizeSnappy), -+ -+ couch_config:set("couchdb", "file_compression", "deflate_1", false), -+ compact_db(DbName), -+ compact_view(DbName), -+ DbSizeDeflate1 = db_disk_size(DbName), -+ ViewSizeDeflate1 = view_disk_size(DbName), -+ -+ ?assert(DbSizeSnappy > DbSizeDeflate1), -+ ?assert(ViewSizeSnappy > ViewSizeDeflate1), -+ -+ couch_config:set("couchdb", "file_compression", "deflate_9", false), -+ compact_db(DbName), -+ compact_view(DbName), -+ DbSizeDeflate9 = db_disk_size(DbName), -+ ViewSizeDeflate9 = view_disk_size(DbName), -+ -+ ?assert(DbSizeDeflate1 > DbSizeDeflate9), -+ ?assert(ViewSizeDeflate1 > ViewSizeDeflate9). -+ -+ -+populate_db(_Db, NumDocs) when NumDocs =< 0 -> -+ ok; -+populate_db(Db, NumDocs) -> -+ Docs = lists:map( -+ fun(_) -> -+ couch_doc:from_json_obj({[ -+ {<<"_id">>, couch_uuids:random()}, -+ {<<"string">>, ?l2b(lists:duplicate(1000, $X))} -+ ]}) -+ end, -+ lists:seq(1, 500)), -+ {ok, _} = couch_db:update_docs(Db, Docs, []), -+ populate_db(Db, NumDocs - 500). -+ -+refresh_index(DbName) -> -+ {ok, Db} = couch_db:open_int(DbName, []), -+ {ok, DDoc} = couch_db:open_doc(Db, ?DDOC_ID, [ejson_body]), -+ couch_mrview:query_view(Db, DDoc, <<"by_id">>, [{stale, false}]), -+ ok = couch_db:close(Db). -+ -+compact_db(DbName) -> -+ DiskSizeBefore = db_disk_size(DbName), -+ {ok, Db} = couch_db:open_int(DbName, []), -+ {ok, CompactPid} = couch_db:start_compact(Db), -+ MonRef = erlang:monitor(process, CompactPid), -+ receive -+ {'DOWN', MonRef, process, CompactPid, normal} -> -+ ok; -+ {'DOWN', MonRef, process, CompactPid, Reason} -> -+ erlang:error({assertion_failed, -+ [{module, ?MODULE}, -+ {line, ?LINE}, -+ {reason, "Error compacting database: " -+ ++ couch_util:to_list(Reason)}]}) -+ after ?TIMEOUT -> -+ erlang:error({assertion_failed, -+ [{module, ?MODULE}, -+ {line, ?LINE}, -+ {reason, "Timeout waiting for database compaction"}]}) -+ end, -+ ok = couch_db:close(Db), -+ DiskSizeAfter = db_disk_size(DbName), -+ ?assert(DiskSizeBefore > DiskSizeAfter). -+ -+compact_view(DbName) -> -+ DiskSizeBefore = view_disk_size(DbName), -+ {ok, MonRef} = couch_mrview:compact(DbName, ?DDOC_ID, [monitor]), -+ receive -+ {'DOWN', MonRef, process, _CompactPid, normal} -> -+ ok; -+ {'DOWN', MonRef, process, _CompactPid, Reason} -> -+ erlang:error({assertion_failed, -+ [{module, ?MODULE}, -+ {line, ?LINE}, -+ {reason, "Error compacting view group: " -+ ++ couch_util:to_list(Reason)}]}) -+ after ?TIMEOUT -> -+ erlang:error({assertion_failed, -+ [{module, ?MODULE}, -+ {line, ?LINE}, -+ {reason, "Timeout waiting for view group compaction"}]}) -+ end, -+ DiskSizeAfter = view_disk_size(DbName), -+ ?assert(DiskSizeBefore > DiskSizeAfter). -+ -+db_disk_size(DbName) -> -+ {ok, Db} = couch_db:open_int(DbName, []), -+ {ok, Info} = couch_db:get_db_info(Db), -+ ok = couch_db:close(Db), -+ couch_util:get_value(disk_size, Info). -+ -+view_disk_size(DbName) -> -+ {ok, Db} = couch_db:open_int(DbName, []), -+ {ok, DDoc} = couch_db:open_doc(Db, ?DDOC_ID, [ejson_body]), -+ {ok, Info} = couch_mrview:get_info(Db, DDoc), -+ ok = couch_db:close(Db), -+ couch_util:get_value(disk_size, Info). -diff --git a/test/couchdb/couchdb_http_proxy_tests.erl b/test/couchdb/couchdb_http_proxy_tests.erl -new file mode 100644 -index 0000000..acb1974 ---- /dev/null -+++ b/test/couchdb/couchdb_http_proxy_tests.erl -@@ -0,0 +1,462 @@ -+% 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(couchdb_http_proxy_tests). -+ -+-include("couch_eunit.hrl"). -+ -+-record(req, {method=get, path="", headers=[], body="", opts=[]}). -+ -+-define(CONFIG_FIXTURE_TEMP, -+ begin -+ FileName = filename:join([?TEMPDIR, ?tempfile() ++ ".ini"]), -+ {ok, Fd} = file:open(FileName, write), -+ ok = file:truncate(Fd), -+ ok = file:close(Fd), -+ FileName -+ end). -+-define(TIMEOUT, 5000). -+ -+ -+start() -> -+ % we have to write any config changes to temp ini file to not loose them -+ % when supervisor will kill all children due to reaching restart threshold -+ % (each httpd_global_handlers changes causes couch_httpd restart) -+ couch_server_sup:start_link(?CONFIG_CHAIN ++ [?CONFIG_FIXTURE_TEMP]), -+ % 49151 is IANA Reserved, let's assume no one is listening there -+ couch_config:set("httpd_global_handlers", "_error", -+ "{couch_httpd_proxy, handle_proxy_req, <<\"http://127.0.0.1:49151/\">>}" -+ ), -+ ok. -+ -+stop(_) -> -+ couch_server_sup:stop(), -+ ok. -+ -+setup() -> -+ {ok, Pid} = test_web:start_link(), -+ Value = lists:flatten(io_lib:format( -+ "{couch_httpd_proxy, handle_proxy_req, ~p}", -+ [list_to_binary(proxy_url())])), -+ couch_config:set("httpd_global_handlers", "_test", Value), -+ % let couch_httpd restart -+ timer:sleep(100), -+ Pid. -+ -+teardown(Pid) -> -+ erlang:monitor(process, Pid), -+ test_web:stop(), -+ receive -+ {'DOWN', _, _, Pid, _} -> -+ ok -+ after ?TIMEOUT -> -+ throw({timeout, test_web_stop}) -+ end. -+ -+ -+http_proxy_test_() -> -+ { -+ "HTTP Proxy handler tests", -+ { -+ setup, -+ fun start/0, fun stop/1, -+ { -+ foreach, -+ fun setup/0, fun teardown/1, -+ [ -+ fun should_proxy_basic_request/1, -+ fun should_return_alternative_status/1, -+ fun should_respect_trailing_slash/1, -+ fun should_proxy_headers/1, -+ fun should_proxy_host_header/1, -+ fun should_pass_headers_back/1, -+ fun should_use_same_protocol_version/1, -+ fun should_proxy_body/1, -+ fun should_proxy_body_back/1, -+ fun should_proxy_chunked_body/1, -+ fun should_proxy_chunked_body_back/1, -+ fun should_rewrite_location_header/1, -+ fun should_not_rewrite_external_locations/1, -+ fun should_rewrite_relative_location/1, -+ fun should_refuse_connection_to_backend/1 -+ ] -+ } -+ -+ } -+ }. -+ -+ -+should_proxy_basic_request(_) -> -+ 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, -+ ?_test(check_request(#req{}, Remote, Local)). -+ -+should_return_alternative_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"}, -+ ?_test(check_request(Req, Remote, Local)). -+ -+should_respect_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/"}, -+ ?_test(check_request(Req, Remote, Local)). -+ -+should_proxy_headers(_) -> -+ 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"}] -+ }, -+ ?_test(check_request(Req, Remote, Local)). -+ -+should_proxy_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"}] -+ }, -+ ?_test(check_request(Req, Remote, Local)). -+ -+should_pass_headers_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"}, -+ ?_test(check_request(Req, Remote, Local)). -+ -+should_use_same_protocol_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}}] -+ }, -+ ?_test(check_request(Req, Remote, Local)). -+ -+should_proxy_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!" -+ }, -+ ?_test(check_request(Req, Remote, Local)). -+ -+should_proxy_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"}, -+ ?_test(check_request(Req, Remote, Local)). -+ -+should_proxy_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=chunked_body(BodyChunks) -+ }, -+ ?_test(check_request(Req, Remote, Local)). -+ -+should_proxy_chunked_body_back(_) -> -+ ?_test(begin -+ 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(Req, Remote, no_local), -+ ?assertMatch({ibrowse_req_id, _}, Resp), -+ {_, ReqId} = Resp, -+ -+ % Grab headers from response -+ receive -+ {ibrowse_async_headers, ReqId, "200", Headers} -> -+ ?assertEqual("chunked", -+ proplists:get_value("Transfer-Encoding", Headers)), -+ ibrowse:stream_next(ReqId) -+ after 1000 -> -+ throw({error, timeout}) -+ end, -+ -+ ?assertEqual(<<"foobarbazinga">>, recv_body(ReqId, [])), -+ ?assertEqual(was_ok, test_web:check_last()) -+ end). -+ -+should_refuse_connection_to_backend(_) -> -+ Local = fun -+ ({ok, "500", _, _}) -> -+ true; -+ (_) -> -+ false -+ end, -+ Req = #req{opts=[{url, server_url("/_error")}]}, -+ ?_test(check_request(Req, no_remote, Local)). -+ -+should_rewrite_location_header(_) -> -+ { -+ "Testing location header rewrites", -+ do_rewrite_tests([ -+ {"Location", proxy_url() ++ "/foo/bar", -+ server_url() ++ "/foo/bar"}, -+ {"Content-Location", proxy_url() ++ "/bing?q=2", -+ server_url() ++ "/bing?q=2"}, -+ {"Uri", proxy_url() ++ "/zip#frag", -+ server_url() ++ "/zip#frag"}, -+ {"Destination", proxy_url(), -+ server_url() ++ "/"} -+ ]) -+ }. -+ -+should_not_rewrite_external_locations(_) -> -+ { -+ "Testing no rewrite of external locations", -+ do_rewrite_tests([ -+ {"Location", external_url() ++ "/search", -+ external_url() ++ "/search"}, -+ {"Content-Location", external_url() ++ "/s?q=2", -+ external_url() ++ "/s?q=2"}, -+ {"Uri", external_url() ++ "/f#f", -+ external_url() ++ "/f#f"}, -+ {"Destination", external_url() ++ "/f?q=2#f", -+ external_url() ++ "/f?q=2#f"} -+ ]) -+ }. -+ -+should_rewrite_relative_location(_) -> -+ { -+ "Testing relative rewrites", -+ do_rewrite_tests([ -+ {"Location", "/foo", -+ server_url() ++ "/foo"}, -+ {"Content-Location", "bar", -+ server_url() ++ "/bar"}, -+ {"Uri", "/zing?q=3", -+ server_url() ++ "/zing?q=3"}, -+ {"Destination", "bing?q=stuff#yay", -+ server_url() ++ "/bing?q=stuff#yay"} -+ ]) -+ }. -+ -+ -+do_rewrite_tests(Tests) -> -+ lists:map(fun({Header, Location, Url}) -> -+ should_rewrite_header(Header, Location, Url) -+ end, Tests). -+ -+should_rewrite_header(Header, Location, Url) -> -+ Remote = fun(Req) -> -+ "/rewrite_test" = Req:get(path), -+ {ok, {302, [{Header, Location}], "ok"}} -+ end, -+ Local = fun -+ ({ok, "302", Headers, "ok"}) -> -+ ?assertEqual(Url, couch_util:get_value(Header, Headers)), -+ true; -+ (E) -> -+ ?debugFmt("~p", [E]), -+ false -+ end, -+ Req = #req{path="/rewrite_test"}, -+ {Header, ?_test(check_request(Req, Remote, Local))}. -+ -+ -+server_url() -> -+ server_url("/_test"). -+ -+server_url(Resource) -> -+ Addr = couch_config:get("httpd", "bind_address"), -+ Port = integer_to_list(mochiweb_socket_server:get(couch_httpd, port)), -+ lists:concat(["http://", Addr, ":", Port, Resource]). -+ -+proxy_url() -> -+ "http://127.0.0.1:" ++ integer_to_list(test_web:get_port()). -+ -+external_url() -> -+ "https://google.com". -+ -+check_request(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_url() ++ 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 -+ ), -+ %?debugFmt("ibrowse response: ~p", [Resp]), -+ case Local of -+ no_local -> -+ ok; -+ _ -> -+ ?assert(Local(Resp)) -+ end, -+ case {Remote, Local} of -+ {no_remote, _} -> -+ ok; -+ {_, no_local} -> -+ ok; -+ _ -> -+ ?assertEqual(was_ok, test_web:check_last()) -+ end, -+ Resp. -+ -+chunked_body(Chunks) -> -+ chunked_body(Chunks, []). -+ -+chunked_body([], Acc) -> -+ iolist_to_binary(lists:reverse(Acc, "0\r\n\r\n")); -+chunked_body([Chunk | Rest], Acc) -> -+ Size = to_hex(size(Chunk)), -+ 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 ?TIMEOUT -> -+ throw({error, timeout}) -+ end. -diff --git a/test/couchdb/couchdb_modules_load_tests.erl b/test/couchdb/couchdb_modules_load_tests.erl -new file mode 100644 -index 0000000..4eaa42b ---- /dev/null -+++ b/test/couchdb/couchdb_modules_load_tests.erl -@@ -0,0 +1,68 @@ -+% 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(couchdb_modules_load_tests). -+ -+-include("couch_eunit.hrl"). -+ -+ -+modules_load_test_() -> -+ { -+ "Verify that all modules loads", -+ should_load_modules() -+ }. -+ -+ -+should_load_modules() -> -+ Modules = [ -+ couch_auth_cache, -+ couch_btree, -+ couch_changes, -+ couch_compress, -+ couch_config, -+ couch_config_writer, -+ couch_db, -+ couch_db_update_notifier, -+ couch_db_update_notifier_sup, -+ couch_db_updater, -+ couch_doc, -+ % Fails unless couch_config gen_server is started. -+ % couch_ejson_compare, -+ 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_rewrite, -+ couch_httpd_stats_handlers, -+ couch_key_tree, -+ couch_log, -+ couch_os_process, -+ couch_query_servers, -+ couch_ref_counter, -+ couch_server, -+ couch_server_sup, -+ couch_stats_aggregator, -+ couch_stats_collector, -+ couch_stream, -+ couch_task_status, -+ couch_util, -+ couch_work_queue, -+ json_stream_parse -+ ], -+ [should_load_module(Mod) || Mod <- Modules]. -+ -+should_load_module(Mod) -> -+ {atom_to_list(Mod), ?_assertMatch({module, _}, code:load_file(Mod))}. -diff --git a/test/couchdb/couchdb_os_daemons_tests.erl b/test/couchdb/couchdb_os_daemons_tests.erl -new file mode 100644 -index 0000000..ed9b6e8 ---- /dev/null -+++ b/test/couchdb/couchdb_os_daemons_tests.erl -@@ -0,0 +1,329 @@ -+% 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(couchdb_os_daemons_tests). -+ -+-include("couch_eunit.hrl"). -+ -+%% keep in sync with couchdb/couch_os_daemons.erl -+-record(daemon, { -+ port, -+ name, -+ cmd, -+ kill, -+ status=running, -+ cfg_patterns=[], -+ errors=[], -+ buf=[] -+}). -+ -+-define(DAEMON_CONFIGER, "os_daemon_configer.escript"). -+-define(DAEMON_LOOPER, "os_daemon_looper.escript"). -+-define(DAEMON_BAD_PERM, "os_daemon_bad_perm.sh"). -+-define(DAEMON_CAN_REBOOT, "os_daemon_can_reboot.sh"). -+-define(DAEMON_DIE_ON_BOOT, "os_daemon_die_on_boot.sh"). -+-define(DAEMON_DIE_QUICKLY, "os_daemon_die_quickly.sh"). -+-define(DAEMON_CFGREG, "test_cfg_register"). -+-define(DELAY, 100). -+-define(FIXTURES_BUILDDIR, -+ filename:join([?BUILDDIR, "test", "couchdb", "fixtures"])). -+-define(TIMEOUT, 1000). -+ -+ -+setup(DName) -> -+ {ok, CfgPid} = couch_config:start_link(?CONFIG_CHAIN), -+ {ok, OsDPid} = couch_os_daemons:start_link(), -+ Path = case DName of -+ ?DAEMON_CFGREG -> -+ filename:join([?FIXTURES_BUILDDIR, DName]); -+ ?DAEMON_CONFIGER -> -+ filename:join([?FIXTURES_BUILDDIR, DName]); -+ _ -> -+ filename:join([?FIXTURESDIR, DName]) -+ end, -+ couch_config:set("os_daemons", DName, Path, false), -+ timer:sleep(?DELAY), % sleep a bit to let daemon set kill flag -+ {CfgPid, OsDPid}. -+ -+teardown(_, {CfgPid, OsDPid}) -> -+ erlang:monitor(process, CfgPid), -+ couch_config:stop(), -+ receive -+ {'DOWN', _, _, CfgPid, _} -> -+ ok -+ after ?TIMEOUT -> -+ throw({timeout, config_stop}) -+ end, -+ -+ erlang:monitor(process, OsDPid), -+ exit(OsDPid, normal), -+ receive -+ {'DOWN', _, _, OsDPid, _} -> -+ ok -+ after ?TIMEOUT -> -+ throw({timeout, os_daemon_stop}) -+ end. -+ -+ -+os_daemons_test_() -> -+ { -+ "OS Daemons tests", -+ { -+ foreachx, -+ fun setup/1, fun teardown/2, -+ [{?DAEMON_LOOPER, Fun} || Fun <- [ -+ fun should_check_daemon/2, -+ fun should_check_daemon_table_form/2, -+ fun should_clean_tables_on_daemon_remove/2, -+ fun should_spawn_multiple_daemons/2, -+ fun should_keep_alive_one_daemon_on_killing_other/2 -+ ]] -+ } -+ }. -+ -+configuration_reader_test_() -> -+ { -+ "OS Daemon requests CouchDB configuration", -+ { -+ foreachx, -+ fun setup/1, fun teardown/2, -+ [{?DAEMON_CONFIGER, -+ fun should_read_write_config_settings_by_daemon/2}] -+ -+ } -+ }. -+ -+error_test_() -> -+ { -+ "OS Daemon process error tests", -+ { -+ foreachx, -+ fun setup/1, fun teardown/2, -+ [{?DAEMON_BAD_PERM, fun should_fail_due_to_lack_of_permissions/2}, -+ {?DAEMON_DIE_ON_BOOT, fun should_die_on_boot/2}, -+ {?DAEMON_DIE_QUICKLY, fun should_die_quickly/2}, -+ {?DAEMON_CAN_REBOOT, fun should_not_being_halted/2}] -+ } -+ }. -+ -+configuration_register_test_() -> -+ { -+ "OS daemon subscribed to config changes", -+ { -+ foreachx, -+ fun setup/1, fun teardown/2, -+ [{?DAEMON_CFGREG, Fun} || Fun <- [ -+ fun should_start_daemon/2, -+ fun should_restart_daemon_on_section_change/2, -+ fun should_not_restart_daemon_on_changing_ignored_section_key/2, -+ fun should_restart_daemon_on_section_key_change/2 -+ ]] -+ } -+ }. -+ -+ -+should_check_daemon(DName, _) -> -+ ?_test(begin -+ {ok, [D]} = couch_os_daemons:info([table]), -+ check_daemon(D, DName) -+ end). -+ -+should_check_daemon_table_form(DName, _) -> -+ ?_test(begin -+ {ok, Tab} = couch_os_daemons:info(), -+ [D] = ets:tab2list(Tab), -+ check_daemon(D, DName) -+ end). -+ -+should_clean_tables_on_daemon_remove(DName, _) -> -+ ?_test(begin -+ couch_config:delete("os_daemons", DName, false), -+ {ok, Tab2} = couch_os_daemons:info(), -+ ?_assertEqual([], ets:tab2list(Tab2)) -+ end). -+ -+should_spawn_multiple_daemons(DName, _) -> -+ ?_test(begin -+ couch_config:set("os_daemons", "bar", -+ filename:join([?FIXTURESDIR, DName]), false), -+ couch_config:set("os_daemons", "baz", -+ filename:join([?FIXTURESDIR, DName]), false), -+ timer:sleep(?DELAY), -+ {ok, Daemons} = couch_os_daemons:info([table]), -+ lists:foreach(fun(D) -> -+ check_daemon(D) -+ end, Daemons), -+ {ok, Tab} = couch_os_daemons:info(), -+ lists:foreach(fun(D) -> -+ check_daemon(D) -+ end, ets:tab2list(Tab)) -+ end). -+ -+should_keep_alive_one_daemon_on_killing_other(DName, _) -> -+ ?_test(begin -+ couch_config:set("os_daemons", "bar", -+ filename:join([?FIXTURESDIR, DName]), false), -+ timer:sleep(?DELAY), -+ {ok, Daemons} = couch_os_daemons:info([table]), -+ lists:foreach(fun(D) -> -+ check_daemon(D) -+ end, Daemons), -+ -+ couch_config:delete("os_daemons", "bar", false), -+ timer:sleep(?DELAY), -+ {ok, [D2]} = couch_os_daemons:info([table]), -+ check_daemon(D2, DName), -+ -+ {ok, Tab} = couch_os_daemons:info(), -+ [T] = ets:tab2list(Tab), -+ check_daemon(T, DName) -+ end). -+ -+should_read_write_config_settings_by_daemon(DName, _) -> -+ ?_test(begin -+ % have to wait till daemon run all his tests -+ % see daemon's script for more info -+ timer:sleep(?TIMEOUT), -+ {ok, [D]} = couch_os_daemons:info([table]), -+ check_daemon(D, DName) -+ end). -+ -+should_fail_due_to_lack_of_permissions(DName, _) -> -+ ?_test(should_halts(DName, 1000)). -+ -+should_die_on_boot(DName, _) -> -+ ?_test(should_halts(DName, 1000)). -+ -+should_die_quickly(DName, _) -> -+ ?_test(should_halts(DName, 4000)). -+ -+should_not_being_halted(DName, _) -> -+ ?_test(begin -+ timer:sleep(1000), -+ {ok, [D1]} = couch_os_daemons:info([table]), -+ check_daemon(D1, DName, running, 0), -+ -+ % Should reboot every two seconds. We're at 1s, so wait -+ % until 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, DName, running, 1), -+ -+ % If the kill command changed, that means we rebooted the process. -+ ?assertNotEqual(D1#daemon.kill, D2#daemon.kill) -+ end). -+ -+should_halts(DName, Time) -> -+ timer:sleep(Time), -+ {ok, [D]} = couch_os_daemons:info([table]), -+ check_dead(D, DName), -+ couch_config:delete("os_daemons", DName, false). -+ -+should_start_daemon(DName, _) -> -+ ?_test(begin -+ wait_for_start(10), -+ {ok, [D]} = couch_os_daemons:info([table]), -+ check_daemon(D, DName, running, 0, [{"s1"}, {"s2", "k"}]) -+ end). -+ -+should_restart_daemon_on_section_change(DName, _) -> -+ ?_test(begin -+ wait_for_start(10), -+ {ok, [D1]} = couch_os_daemons:info([table]), -+ couch_config:set("s1", "k", "foo", false), -+ wait_for_restart(10), -+ {ok, [D2]} = couch_os_daemons:info([table]), -+ check_daemon(D2, DName, running, 0, [{"s1"}, {"s2", "k"}]), -+ ?assertNotEqual(D1, D2) -+ end). -+ -+should_not_restart_daemon_on_changing_ignored_section_key(_, _) -> -+ ?_test(begin -+ wait_for_start(10), -+ {ok, [D1]} = couch_os_daemons:info([table]), -+ couch_config:set("s2", "k2", "baz", false), -+ timer:sleep(?DELAY), -+ {ok, [D2]} = couch_os_daemons:info([table]), -+ ?assertEqual(D1, D2) -+ end). -+ -+should_restart_daemon_on_section_key_change(DName, _) -> -+ ?_test(begin -+ wait_for_start(10), -+ {ok, [D1]} = couch_os_daemons:info([table]), -+ couch_config:set("s2", "k", "bingo", false), -+ wait_for_restart(10), -+ {ok, [D2]} = couch_os_daemons:info([table]), -+ check_daemon(D2, DName, running, 0, [{"s1"}, {"s2", "k"}]), -+ ?assertNotEqual(D1, D2) -+ end). -+ -+ -+wait_for_start(0) -> -+ erlang:error({assertion_failed, -+ [{module, ?MODULE}, -+ {line, ?LINE}, -+ {reason, "Timeout on waiting daemon for start"}]}); -+wait_for_start(N) -> -+ case couch_os_daemons:info([table]) of -+ {ok, []} -> -+ timer:sleep(?DELAY), -+ wait_for_start(N - 1); -+ _ -> -+ timer:sleep(?TIMEOUT) -+ end. -+ -+wait_for_restart(0) -> -+ erlang:error({assertion_failed, -+ [{module, ?MODULE}, -+ {line, ?LINE}, -+ {reason, "Timeout on waiting daemon for restart"}]}); -+wait_for_restart(N) -> -+ {ok, [D]} = couch_os_daemons:info([table]), -+ case D#daemon.status of -+ restarting -> -+ timer:sleep(?DELAY), -+ wait_for_restart(N - 1); -+ _ -> -+ timer:sleep(?TIMEOUT) -+ end. -+ -+check_daemon(D) -> -+ check_daemon(D, D#daemon.name). -+ -+check_daemon(D, Name) -> -+ check_daemon(D, Name, running). -+ -+check_daemon(D, Name, Status) -> -+ check_daemon(D, Name, Status, 0). -+ -+check_daemon(D, Name, Status, Errs) -> -+ check_daemon(D, Name, Status, Errs, []). -+ -+check_daemon(D, Name, Status, Errs, CfgPatterns) -> -+ ?assert(is_port(D#daemon.port)), -+ ?assertEqual(Name, D#daemon.name), -+ ?assertNotEqual(undefined, D#daemon.kill), -+ ?assertEqual(Status, D#daemon.status), -+ ?assertEqual(CfgPatterns, D#daemon.cfg_patterns), -+ ?assertEqual(Errs, length(D#daemon.errors)), -+ ?assertEqual([], D#daemon.buf). -+ -+check_dead(D, Name) -> -+ ?assert(is_port(D#daemon.port)), -+ ?assertEqual(Name, D#daemon.name), -+ ?assertNotEqual(undefined, D#daemon.kill), -+ ?assertEqual(halted, D#daemon.status), -+ ?assertEqual(nil, D#daemon.errors), -+ ?assertEqual(nil, D#daemon.buf). -diff --git a/test/couchdb/couchdb_os_proc_pool.erl b/test/couchdb/couchdb_os_proc_pool.erl -new file mode 100644 -index 0000000..1bb266e ---- /dev/null -+++ b/test/couchdb/couchdb_os_proc_pool.erl -@@ -0,0 +1,179 @@ -+% 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(couchdb_os_proc_pool). -+ -+-include("couch_eunit.hrl"). -+-include_lib("couchdb/couch_db.hrl"). -+ -+-define(TIMEOUT, 3000). -+ -+ -+start() -> -+ {ok, Pid} = couch_server_sup:start_link(?CONFIG_CHAIN), -+ couch_config:set("query_server_config", "os_process_limit", "3", false), -+ Pid. -+ -+stop(Pid) -> -+ couch_server_sup:stop(), -+ erlang:monitor(process, Pid), -+ receive -+ {'DOWN', _, _, Pid, _} -> -+ ok -+ after ?TIMEOUT -> -+ throw({timeout, server_stop}) -+ end. -+ -+ -+os_proc_pool_test_() -> -+ { -+ "OS processes pool tests", -+ { -+ setup, -+ fun start/0, fun stop/1, -+ [ -+ should_block_new_proc_on_full_pool(), -+ should_free_slot_on_proc_unexpected_exit() -+ ] -+ } -+ }. -+ -+ -+should_block_new_proc_on_full_pool() -> -+ ?_test(begin -+ Client1 = spawn_client(), -+ Client2 = spawn_client(), -+ Client3 = spawn_client(), -+ -+ ?assertEqual(ok, ping_client(Client1)), -+ ?assertEqual(ok, ping_client(Client2)), -+ ?assertEqual(ok, ping_client(Client3)), -+ -+ Proc1 = get_client_proc(Client1, "1"), -+ Proc2 = get_client_proc(Client2, "2"), -+ Proc3 = get_client_proc(Client3, "3"), -+ -+ ?assertNotEqual(Proc1, Proc2), -+ ?assertNotEqual(Proc2, Proc3), -+ ?assertNotEqual(Proc3, Proc1), -+ -+ Client4 = spawn_client(), -+ ?assertEqual(timeout, ping_client(Client4)), -+ -+ ?assertEqual(ok, stop_client(Client1)), -+ ?assertEqual(ok, ping_client(Client4)), -+ -+ Proc4 = get_client_proc(Client4, "4"), -+ ?assertEqual(Proc1, Proc4), -+ -+ lists:map(fun(C) -> -+ ?assertEqual(ok, stop_client(C)) -+ end, [Client2, Client3, Client4]) -+ end). -+ -+should_free_slot_on_proc_unexpected_exit() -> -+ ?_test(begin -+ Client1 = spawn_client(), -+ Client2 = spawn_client(), -+ Client3 = spawn_client(), -+ -+ ?assertEqual(ok, ping_client(Client1)), -+ ?assertEqual(ok, ping_client(Client2)), -+ ?assertEqual(ok, ping_client(Client3)), -+ -+ Proc1 = get_client_proc(Client1, "1"), -+ Proc2 = get_client_proc(Client2, "2"), -+ Proc3 = get_client_proc(Client3, "3"), -+ -+ ?assertNotEqual(Proc1, Proc2), -+ ?assertNotEqual(Proc2, Proc3), -+ ?assertNotEqual(Proc3, Proc1), -+ -+ ?assertEqual(ok, kill_client(Client1)), -+ -+ Client4 = spawn_client(), -+ ?assertEqual(ok, ping_client(Client4)), -+ -+ Proc4 = get_client_proc(Client4, "4"), -+ ?assertNotEqual(Proc4, Proc1), -+ ?assertNotEqual(Proc2, Proc4), -+ ?assertNotEqual(Proc3, Proc4), -+ -+ lists:map(fun(C) -> -+ ?assertEqual(ok, stop_client(C)) -+ end, [Client2, Client3, Client4]) -+ end). -+ -+ -+spawn_client() -> -+ Parent = self(), -+ Ref = make_ref(), -+ Pid = spawn(fun() -> -+ Proc = couch_query_servers:get_os_process(<<"javascript">>), -+ loop(Parent, Ref, Proc) -+ end), -+ {Pid, Ref}. -+ -+ping_client({Pid, Ref}) -> -+ Pid ! ping, -+ receive -+ {pong, Ref} -> -+ ok -+ after ?TIMEOUT -> -+ timeout -+ end. -+ -+get_client_proc({Pid, Ref}, ClientName) -> -+ Pid ! get_proc, -+ receive -+ {proc, Ref, Proc} -> Proc -+ after ?TIMEOUT -> -+ erlang:error({assertion_failed, -+ [{module, ?MODULE}, -+ {line, ?LINE}, -+ {reason, "Timeout getting client " -+ ++ ClientName ++ " proc"}]}) -+ end. -+ -+stop_client({Pid, Ref}) -> -+ Pid ! stop, -+ receive -+ {stop, Ref} -> -+ ok -+ after ?TIMEOUT -> -+ timeout -+ end. -+ -+kill_client({Pid, Ref}) -> -+ Pid ! die, -+ receive -+ {die, Ref} -> -+ ok -+ after ?TIMEOUT -> -+ timeout -+ end. -+ -+loop(Parent, Ref, Proc) -> -+ receive -+ ping -> -+ Parent ! {pong, Ref}, -+ loop(Parent, Ref, Proc); -+ get_proc -> -+ Parent ! {proc, Ref, Proc}, -+ loop(Parent, Ref, Proc); -+ stop -> -+ couch_query_servers:ret_os_process(Proc), -+ Parent ! {stop, Ref}; -+ die -> -+ Parent ! {die, Ref}, -+ exit(some_error) -+ end. -diff --git a/test/couchdb/couchdb_update_conflicts_tests.erl b/test/couchdb/couchdb_update_conflicts_tests.erl -new file mode 100644 -index 0000000..7226860 ---- /dev/null -+++ b/test/couchdb/couchdb_update_conflicts_tests.erl -@@ -0,0 +1,243 @@ -+% 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(couchdb_update_conflicts_tests). -+ -+-include("couch_eunit.hrl"). -+-include_lib("couchdb/couch_db.hrl"). -+ -+-define(i2l(I), integer_to_list(I)). -+-define(ADMIN_USER, {userctx, #user_ctx{roles=[<<"_admin">>]}}). -+-define(DOC_ID, <<"foobar">>). -+-define(NUM_CLIENTS, [100, 500, 1000, 2000, 5000, 10000]). -+-define(TIMEOUT, 10000). -+ -+ -+start() -> -+ {ok, Pid} = couch_server_sup:start_link(?CONFIG_CHAIN), -+ couch_config:set("couchdb", "delayed_commits", "true", false), -+ Pid. -+ -+stop(Pid) -> -+ erlang:monitor(process, Pid), -+ couch_server_sup:stop(), -+ receive -+ {'DOWN', _, _, Pid, _} -> -+ ok -+ after ?TIMEOUT -> -+ throw({timeout, server_stop}) -+ end. -+ -+setup() -> -+ DbName = ?tempdb(), -+ {ok, Db} = couch_db:create(DbName, [?ADMIN_USER, overwrite]), -+ Doc = couch_doc:from_json_obj({[{<<"_id">>, ?DOC_ID}, -+ {<<"value">>, 0}]}), -+ {ok, Rev} = couch_db:update_doc(Db, Doc, []), -+ ok = couch_db:close(Db), -+ RevStr = couch_doc:rev_to_str(Rev), -+ {DbName, RevStr}. -+setup(_) -> -+ setup(). -+ -+teardown({DbName, _}) -> -+ ok = couch_server:delete(DbName, []), -+ ok. -+teardown(_, {DbName, _RevStr}) -> -+ teardown({DbName, _RevStr}). -+ -+ -+view_indexes_cleanup_test_() -> -+ { -+ "Update conflicts", -+ { -+ setup, -+ fun start/0, fun stop/1, -+ [ -+ concurrent_updates(), -+ couchdb_188() -+ ] -+ } -+ }. -+ -+concurrent_updates()-> -+ { -+ "Concurrent updates", -+ { -+ foreachx, -+ fun setup/1, fun teardown/2, -+ [{NumClients, fun should_concurrently_update_doc/2} -+ || NumClients <- ?NUM_CLIENTS] -+ } -+ }. -+ -+couchdb_188()-> -+ { -+ "COUCHDB-188", -+ { -+ foreach, -+ fun setup/0, fun teardown/1, -+ [fun should_bulk_create_delete_doc/1] -+ } -+ }. -+ -+ -+should_concurrently_update_doc(NumClients, {DbName, InitRev})-> -+ {?i2l(NumClients) ++ " clients", -+ {inorder, -+ [{"update doc", -+ {timeout, ?TIMEOUT div 1000, -+ ?_test(concurrent_doc_update(NumClients, DbName, InitRev))}}, -+ {"ensure in single leaf", -+ ?_test(ensure_in_single_revision_leaf(DbName))}]}}. -+ -+should_bulk_create_delete_doc({DbName, InitRev})-> -+ ?_test(bulk_delete_create(DbName, InitRev)). -+ -+ -+concurrent_doc_update(NumClients, DbName, InitRev) -> -+ Clients = lists:map( -+ fun(Value) -> -+ ClientDoc = couch_doc:from_json_obj({[ -+ {<<"_id">>, ?DOC_ID}, -+ {<<"_rev">>, InitRev}, -+ {<<"value">>, Value} -+ ]}), -+ Pid = spawn_client(DbName, ClientDoc), -+ {Value, Pid, erlang:monitor(process, Pid)} -+ end, -+ lists:seq(1, NumClients)), -+ -+ lists:foreach(fun({_, Pid, _}) -> Pid ! go end, Clients), -+ -+ {NumConflicts, SavedValue} = lists:foldl( -+ fun({Value, Pid, MonRef}, {AccConflicts, AccValue}) -> -+ receive -+ {'DOWN', MonRef, process, Pid, {ok, _NewRev}} -> -+ {AccConflicts, Value}; -+ {'DOWN', MonRef, process, Pid, conflict} -> -+ {AccConflicts + 1, AccValue}; -+ {'DOWN', MonRef, process, Pid, Error} -> -+ erlang:error({assertion_failed, -+ [{module, ?MODULE}, -+ {line, ?LINE}, -+ {reason, "Client " ++ ?i2l(Value) -+ ++ " got update error: " -+ ++ couch_util:to_list(Error)}]}) -+ after ?TIMEOUT div 2 -> -+ erlang:error({assertion_failed, -+ [{module, ?MODULE}, -+ {line, ?LINE}, -+ {reason, "Timeout waiting for client " -+ ++ ?i2l(Value) ++ " to die"}]}) -+ end -+ end, {0, nil}, Clients), -+ ?assertEqual(NumClients - 1, NumConflicts), -+ -+ {ok, Db} = couch_db:open_int(DbName, []), -+ {ok, Leaves} = couch_db:open_doc_revs(Db, ?DOC_ID, all, []), -+ ok = couch_db:close(Db), -+ ?assertEqual(1, length(Leaves)), -+ -+ [{ok, Doc2}] = Leaves, -+ {JsonDoc} = couch_doc:to_json_obj(Doc2, []), -+ ?assertEqual(SavedValue, couch_util:get_value(<<"value">>, JsonDoc)). -+ -+ensure_in_single_revision_leaf(DbName) -> -+ {ok, Db} = couch_db:open_int(DbName, []), -+ {ok, Leaves} = couch_db:open_doc_revs(Db, ?DOC_ID, all, []), -+ ok = couch_db:close(Db), -+ [{ok, Doc}] = Leaves, -+ -+ %% FIXME: server restart won't work from test side -+ %% stop(ok), -+ %% start(), -+ -+ {ok, Db2} = couch_db:open_int(DbName, []), -+ {ok, Leaves2} = couch_db:open_doc_revs(Db2, ?DOC_ID, all, []), -+ ok = couch_db:close(Db2), -+ ?assertEqual(1, length(Leaves2)), -+ -+ [{ok, Doc2}] = Leaves, -+ ?assertEqual(Doc, Doc2). -+ -+bulk_delete_create(DbName, InitRev) -> -+ {ok, Db} = couch_db:open_int(DbName, []), -+ -+ DeletedDoc = couch_doc:from_json_obj({[ -+ {<<"_id">>, ?DOC_ID}, -+ {<<"_rev">>, InitRev}, -+ {<<"_deleted">>, true} -+ ]}), -+ NewDoc = couch_doc:from_json_obj({[ -+ {<<"_id">>, ?DOC_ID}, -+ {<<"value">>, 666} -+ ]}), -+ -+ {ok, Results} = couch_db:update_docs(Db, [DeletedDoc, NewDoc], []), -+ ok = couch_db:close(Db), -+ -+ ?assertEqual(2, length([ok || {ok, _} <- Results])), -+ [{ok, Rev1}, {ok, Rev2}] = Results, -+ -+ {ok, Db2} = couch_db:open_int(DbName, []), -+ {ok, [{ok, Doc1}]} = couch_db:open_doc_revs( -+ Db2, ?DOC_ID, [Rev1], [conflicts, deleted_conflicts]), -+ {ok, [{ok, Doc2}]} = couch_db:open_doc_revs( -+ Db2, ?DOC_ID, [Rev2], [conflicts, deleted_conflicts]), -+ ok = couch_db:close(Db2), -+ -+ {Doc1Props} = couch_doc:to_json_obj(Doc1, []), -+ {Doc2Props} = couch_doc:to_json_obj(Doc2, []), -+ -+ %% Document was deleted -+ ?assert(couch_util:get_value(<<"_deleted">>, Doc1Props)), -+ %% New document not flagged as deleted -+ ?assertEqual(undefined, couch_util:get_value(<<"_deleted">>, -+ Doc2Props)), -+ %% New leaf revision has the right value -+ ?assertEqual(666, couch_util:get_value(<<"value">>, -+ Doc2Props)), -+ %% Deleted document has no conflicts -+ ?assertEqual(undefined, couch_util:get_value(<<"_conflicts">>, -+ Doc1Props)), -+ %% Deleted document has no deleted conflicts -+ ?assertEqual(undefined, couch_util:get_value(<<"_deleted_conflicts">>, -+ Doc1Props)), -+ %% New leaf revision doesn't have conflicts -+ ?assertEqual(undefined, couch_util:get_value(<<"_conflicts">>, -+ Doc1Props)), -+ %% New leaf revision doesn't have deleted conflicts -+ ?assertEqual(undefined, couch_util:get_value(<<"_deleted_conflicts">>, -+ Doc1Props)), -+ -+ %% Deleted revision has position 2 -+ ?assertEqual(2, element(1, Rev1)), -+ %% New leaf revision has position 1 -+ ?assertEqual(1, element(1, Rev2)). -+ -+ -+spawn_client(DbName, Doc) -> -+ spawn(fun() -> -+ {ok, Db} = couch_db:open_int(DbName, []), -+ receive -+ go -> ok -+ end, -+ erlang:yield(), -+ Result = try -+ couch_db:update_doc(Db, Doc, []) -+ catch _:Error -> -+ Error -+ end, -+ ok = couch_db:close(Db), -+ exit(Result) -+ end). -diff --git a/test/couchdb/couchdb_vhosts_tests.erl b/test/couchdb/couchdb_vhosts_tests.erl -new file mode 100644 -index 0000000..94b1957 ---- /dev/null -+++ b/test/couchdb/couchdb_vhosts_tests.erl -@@ -0,0 +1,441 @@ -+% 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(couchdb_vhosts_tests). -+ -+-include("couch_eunit.hrl"). -+-include_lib("couchdb/couch_db.hrl"). -+ -+-define(ADMIN_USER, {user_ctx, #user_ctx{roles=[<<"_admin">>]}}). -+-define(TIMEOUT, 1000). -+-define(iofmt(S, A), lists:flatten(io_lib:format(S, A))). -+ -+ -+start() -> -+ {ok, Pid} = couch_server_sup:start_link(?CONFIG_CHAIN), -+ Pid. -+ -+stop(Pid) -> -+ erlang:monitor(process, Pid), -+ couch_server_sup:stop(), -+ receive -+ {'DOWN', _, _, Pid, _} -> -+ ok -+ after ?TIMEOUT -> -+ throw({timeout, server_stop}) -+ end. -+ -+setup() -> -+ DbName = ?tempdb(), -+ {ok, Db} = couch_db:create(DbName, [?ADMIN_USER]), -+ 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), -+ couch_db:close(Db), -+ -+ Addr = couch_config:get("httpd", "bind_address", "127.0.0.1"), -+ Port = integer_to_list(mochiweb_socket_server:get(couch_httpd, port)), -+ Url = "http://" ++ Addr ++ ":" ++ Port, -+ {Url, ?b2l(DbName)}. -+ -+setup_oauth() -> -+ DbName = ?tempdb(), -+ {ok, Db} = couch_db:create(DbName, [?ADMIN_USER]), -+ -+ couch_config:set("couch_httpd_auth", "authentication_db", -+ ?b2l(?tempdb()), false), -+ couch_config:set("oauth_token_users", "otoksec1", "joe", false), -+ couch_config:set("oauth_consumer_secrets", "consec1", "foo", false), -+ couch_config:set("oauth_token_secrets", "otoksec1", "foobar", false), -+ couch_config:set("couch_httpd_auth", "require_valid_user", "true", false), -+ -+ ok = couch_config:set( -+ "vhosts", "oauth-example.com", -+ "/" ++ ?b2l(DbName) ++ "/_design/test/_rewrite/foobar", false), -+ -+ DDoc = couch_doc:from_json_obj({[ -+ {<<"_id">>, <<"_design/test">>}, -+ {<<"language">>, <<"javascript">>}, -+ {<<"rewrites">>, [ -+ {[ -+ {<<"from">>, <<"foobar">>}, -+ {<<"to">>, <<"_info">>} -+ ]} -+ ]} -+ ]}), -+ {ok, _} = couch_db:update_doc(Db, DDoc, []), -+ -+ couch_db:ensure_full_commit(Db), -+ couch_db:close(Db), -+ -+ Addr = couch_config:get("httpd", "bind_address", "127.0.0.1"), -+ Port = integer_to_list(mochiweb_socket_server:get(couch_httpd, port)), -+ Url = "http://" ++ Addr ++ ":" ++ Port, -+ {Url, ?b2l(DbName)}. -+ -+teardown({_, DbName}) -> -+ ok = couch_server:delete(?l2b(DbName), []), -+ ok. -+ -+ -+vhosts_test_() -> -+ { -+ "Virtual Hosts rewrite tests", -+ { -+ setup, -+ fun start/0, fun stop/1, -+ { -+ foreach, -+ fun setup/0, fun teardown/1, -+ [ -+ fun should_return_database_info/1, -+ fun should_return_revs_info/1, -+ fun should_serve_utils_for_vhost/1, -+ fun should_return_virtual_request_path_field_in_request/1, -+ fun should_return_real_request_path_field_in_request/1, -+ fun should_match_wildcard_vhost/1, -+ fun should_return_db_info_for_wildcard_vhost_for_custom_db/1, -+ fun should_replace_rewrite_variables_for_db_and_doc/1, -+ fun should_return_db_info_for_vhost_with_resource/1, -+ fun should_return_revs_info_for_vhost_with_resource/1, -+ fun should_return_db_info_for_vhost_with_wildcard_resource/1, -+ fun should_return_path_for_vhost_with_wildcard_host/1 -+ ] -+ } -+ } -+ }. -+ -+oauth_test_() -> -+ { -+ "Virtual Hosts OAuth tests", -+ { -+ setup, -+ fun start/0, fun stop/1, -+ { -+ foreach, -+ fun setup_oauth/0, fun teardown/1, -+ [ -+ fun should_require_auth/1, -+ fun should_succeed_oauth/1, -+ fun should_fail_oauth_with_wrong_credentials/1 -+ ] -+ } -+ } -+ }. -+ -+ -+should_return_database_info({Url, DbName}) -> -+ ?_test(begin -+ ok = couch_config:set("vhosts", "example.com", "/" ++ DbName, false), -+ case test_request:get(Url, [], [{host_header, "example.com"}]) of -+ {ok, _, _, Body} -> -+ {JsonBody} = ejson:decode(Body), -+ ?assert(proplists:is_defined(<<"db_name">>, JsonBody)); -+ Else -> -+ erlang:error({assertion_failed, -+ [{module, ?MODULE}, -+ {line, ?LINE}, -+ {reason, ?iofmt("Request failed: ~p", [Else])}]}) -+ end -+ end). -+ -+should_return_revs_info({Url, DbName}) -> -+ ?_test(begin -+ ok = couch_config:set("vhosts", "example.com", "/" ++ DbName, false), -+ case test_request:get(Url ++ "/doc1?revs_info=true", [], -+ [{host_header, "example.com"}]) of -+ {ok, _, _, Body} -> -+ {JsonBody} = ejson:decode(Body), -+ ?assert(proplists:is_defined(<<"_revs_info">>, JsonBody)); -+ Else -> -+ erlang:error({assertion_failed, -+ [{module, ?MODULE}, -+ {line, ?LINE}, -+ {reason, ?iofmt("Request failed: ~p", [Else])}]}) -+ end -+ end). -+ -+should_serve_utils_for_vhost({Url, DbName}) -> -+ ?_test(begin -+ ok = couch_config:set("vhosts", "example.com", "/" ++ DbName, false), -+ case test_request:get(Url ++ "/_utils/index.html", [], -+ [{host_header, "example.com"}]) of -+ {ok, _, _, Body} -> -+ ?assertMatch(<<"", _/binary>>, Body); -+ Else -> -+ erlang:error({assertion_failed, -+ [{module, ?MODULE}, -+ {line, ?LINE}, -+ {reason, ?iofmt("Request failed: ~p", [Else])}]}) -+ end -+ end). -+ -+should_return_virtual_request_path_field_in_request({Url, DbName}) -> -+ ?_test(begin -+ ok = couch_config:set("vhosts", "example1.com", -+ "/" ++ DbName ++ "/_design/doc1/_rewrite/", -+ false), -+ case test_request:get(Url, [], [{host_header, "example1.com"}]) of -+ {ok, _, _, Body} -> -+ {Json} = ejson:decode(Body), -+ ?assertEqual(<<"/">>, -+ proplists:get_value(<<"requested_path">>, Json)); -+ Else -> -+ erlang:error({assertion_failed, -+ [{module, ?MODULE}, -+ {line, ?LINE}, -+ {reason, ?iofmt("Request failed: ~p", [Else])}]}) -+ end -+ end). -+ -+should_return_real_request_path_field_in_request({Url, DbName}) -> -+ ?_test(begin -+ ok = couch_config:set("vhosts", "example1.com", -+ "/" ++ DbName ++ "/_design/doc1/_rewrite/", -+ false), -+ case test_request:get(Url, [], [{host_header, "example1.com"}]) of -+ {ok, _, _, Body} -> -+ {Json} = ejson:decode(Body), -+ Path = ?l2b("/" ++ DbName ++ "/_design/doc1/_show/test"), -+ ?assertEqual(Path, proplists:get_value(<<"path">>, Json)); -+ Else -> -+ erlang:error({assertion_failed, -+ [{module, ?MODULE}, -+ {line, ?LINE}, -+ {reason, ?iofmt("Request failed: ~p", [Else])}]}) -+ end -+ end). -+ -+should_match_wildcard_vhost({Url, DbName}) -> -+ ?_test(begin -+ ok = couch_config:set("vhosts", "*.example.com", -+ "/" ++ DbName ++ "/_design/doc1/_rewrite", false), -+ case test_request:get(Url, [], [{host_header, "test.example.com"}]) of -+ {ok, _, _, Body} -> -+ {Json} = ejson:decode(Body), -+ Path = ?l2b("/" ++ DbName ++ "/_design/doc1/_show/test"), -+ ?assertEqual(Path, proplists:get_value(<<"path">>, Json)); -+ Else -> -+ erlang:error({assertion_failed, -+ [{module, ?MODULE}, -+ {line, ?LINE}, -+ {reason, ?iofmt("Request failed: ~p", [Else])}]}) -+ end -+ end). -+ -+should_return_db_info_for_wildcard_vhost_for_custom_db({Url, DbName}) -> -+ ?_test(begin -+ ok = couch_config:set("vhosts", ":dbname.example1.com", -+ "/:dbname", false), -+ Host = DbName ++ ".example1.com", -+ case test_request:get(Url, [], [{host_header, Host}]) of -+ {ok, _, _, Body} -> -+ {JsonBody} = ejson:decode(Body), -+ ?assert(proplists:is_defined(<<"db_name">>, JsonBody)); -+ Else -> -+ erlang:error({assertion_failed, -+ [{module, ?MODULE}, -+ {line, ?LINE}, -+ {reason, ?iofmt("Request failed: ~p", [Else])}]}) -+ end -+ end). -+ -+should_replace_rewrite_variables_for_db_and_doc({Url, DbName}) -> -+ ?_test(begin -+ ok = couch_config:set("vhosts",":appname.:dbname.example1.com", -+ "/:dbname/_design/:appname/_rewrite/", false), -+ Host = "doc1." ++ DbName ++ ".example1.com", -+ case test_request:get(Url, [], [{host_header, Host}]) of -+ {ok, _, _, Body} -> -+ {Json} = ejson:decode(Body), -+ Path = ?l2b("/" ++ DbName ++ "/_design/doc1/_show/test"), -+ ?assertEqual(Path, proplists:get_value(<<"path">>, Json)); -+ Else -> -+ erlang:error({assertion_failed, -+ [{module, ?MODULE}, -+ {line, ?LINE}, -+ {reason, ?iofmt("Request failed: ~p", [Else])}]}) -+ end -+ end). -+ -+should_return_db_info_for_vhost_with_resource({Url, DbName}) -> -+ ?_test(begin -+ ok = couch_config:set("vhosts", -+ "example.com/test", "/" ++ DbName, false), -+ ReqUrl = Url ++ "/test", -+ case test_request:get(ReqUrl, [], [{host_header, "example.com"}]) of -+ {ok, _, _, Body} -> -+ {JsonBody} = ejson:decode(Body), -+ ?assert(proplists:is_defined(<<"db_name">>, JsonBody)); -+ Else -> -+ erlang:error({assertion_failed, -+ [{module, ?MODULE}, -+ {line, ?LINE}, -+ {reason, ?iofmt("Request failed: ~p", [Else])}]}) -+ end -+ end). -+ -+ -+should_return_revs_info_for_vhost_with_resource({Url, DbName}) -> -+ ?_test(begin -+ ok = couch_config:set("vhosts", -+ "example.com/test", "/" ++ DbName, false), -+ ReqUrl = Url ++ "/test/doc1?revs_info=true", -+ case test_request:get(ReqUrl, [], [{host_header, "example.com"}]) of -+ {ok, _, _, Body} -> -+ {JsonBody} = ejson:decode(Body), -+ ?assert(proplists:is_defined(<<"_revs_info">>, JsonBody)); -+ Else -> -+ erlang:error({assertion_failed, -+ [{module, ?MODULE}, -+ {line, ?LINE}, -+ {reason, ?iofmt("Request failed: ~p", [Else])}]}) -+ end -+ end). -+ -+should_return_db_info_for_vhost_with_wildcard_resource({Url, DbName}) -> -+ ?_test(begin -+ ok = couch_config:set("vhosts", "*.example2.com/test", "/*", false), -+ ReqUrl = Url ++ "/test", -+ Host = DbName ++ ".example2.com", -+ case test_request:get(ReqUrl, [], [{host_header, Host}]) of -+ {ok, _, _, Body} -> -+ {JsonBody} = ejson:decode(Body), -+ ?assert(proplists:is_defined(<<"db_name">>, JsonBody)); -+ Else -> -+ erlang:error({assertion_failed, -+ [{module, ?MODULE}, -+ {line, ?LINE}, -+ {reason, ?iofmt("Request failed: ~p", [Else])}]}) -+ end -+ end). -+ -+should_return_path_for_vhost_with_wildcard_host({Url, DbName}) -> -+ ?_test(begin -+ ok = couch_config:set("vhosts", "*/test1", -+ "/" ++ DbName ++ "/_design/doc1/_show/test", -+ false), -+ case test_request:get(Url ++ "/test1") of -+ {ok, _, _, Body} -> -+ {Json} = ejson:decode(Body), -+ Path = ?l2b("/" ++ DbName ++ "/_design/doc1/_show/test"), -+ ?assertEqual(Path, proplists:get_value(<<"path">>, Json)); -+ Else -> -+ erlang:error({assertion_failed, -+ [{module, ?MODULE}, -+ {line, ?LINE}, -+ {reason, ?iofmt("Request failed: ~p", [Else])}]}) -+ end -+ end). -+ -+should_require_auth({Url, _}) -> -+ ?_test(begin -+ case test_request:get(Url, [], [{host_header, "oauth-example.com"}]) of -+ {ok, Code, _, Body} -> -+ ?assertEqual(401, Code), -+ {JsonBody} = ejson:decode(Body), -+ ?assertEqual(<<"unauthorized">>, -+ couch_util:get_value(<<"error">>, JsonBody)); -+ Else -> -+ erlang:error({assertion_failed, -+ [{module, ?MODULE}, -+ {line, ?LINE}, -+ {reason, ?iofmt("Request failed: ~p", [Else])}]}) -+ end -+ end). -+ -+should_succeed_oauth({Url, _}) -> -+ ?_test(begin -+ AuthDbName = couch_config:get("couch_httpd_auth", "authentication_db"), -+ JoeDoc = couch_doc:from_json_obj({[ -+ {<<"_id">>, <<"org.couchdb.user:joe">>}, -+ {<<"type">>, <<"user">>}, -+ {<<"name">>, <<"joe">>}, -+ {<<"roles">>, []}, -+ {<<"password_sha">>, <<"fe95df1ca59a9b567bdca5cbaf8412abd6e06121">>}, -+ {<<"salt">>, <<"4e170ffeb6f34daecfd814dfb4001a73">>} -+ ]}), -+ {ok, AuthDb} = couch_db:open_int(?l2b(AuthDbName), [?ADMIN_USER]), -+ {ok, _} = couch_db:update_doc(AuthDb, JoeDoc, [?ADMIN_USER]), -+ -+ Host = "oauth-example.com", -+ Consumer = {"consec1", "foo", hmac_sha1}, -+ SignedParams = oauth:sign( -+ "GET", "http://" ++ Host ++ "/", [], Consumer, "otoksec1", "foobar"), -+ OAuthUrl = oauth:uri(Url, SignedParams), -+ -+ case test_request:get(OAuthUrl, [], [{host_header, Host}]) of -+ {ok, Code, _, Body} -> -+ ?assertEqual(200, Code), -+ {JsonBody} = ejson:decode(Body), -+ ?assertEqual(<<"test">>, -+ couch_util:get_value(<<"name">>, JsonBody)); -+ Else -> -+ erlang:error({assertion_failed, -+ [{module, ?MODULE}, -+ {line, ?LINE}, -+ {reason, ?iofmt("Request failed: ~p", [Else])}]}) -+ end -+ end). -+ -+should_fail_oauth_with_wrong_credentials({Url, _}) -> -+ ?_test(begin -+ AuthDbName = couch_config:get("couch_httpd_auth", "authentication_db"), -+ JoeDoc = couch_doc:from_json_obj({[ -+ {<<"_id">>, <<"org.couchdb.user:joe">>}, -+ {<<"type">>, <<"user">>}, -+ {<<"name">>, <<"joe">>}, -+ {<<"roles">>, []}, -+ {<<"password_sha">>, <<"fe95df1ca59a9b567bdca5cbaf8412abd6e06121">>}, -+ {<<"salt">>, <<"4e170ffeb6f34daecfd814dfb4001a73">>} -+ ]}), -+ {ok, AuthDb} = couch_db:open_int(?l2b(AuthDbName), [?ADMIN_USER]), -+ {ok, _} = couch_db:update_doc(AuthDb, JoeDoc, [?ADMIN_USER]), -+ -+ Host = "oauth-example.com", -+ Consumer = {"consec1", "bad_secret", hmac_sha1}, -+ SignedParams = oauth:sign( -+ "GET", "http://" ++ Host ++ "/", [], Consumer, "otoksec1", "foobar"), -+ OAuthUrl = oauth:uri(Url, SignedParams), -+ -+ case test_request:get(OAuthUrl, [], [{host_header, Host}]) of -+ {ok, Code, _, Body} -> -+ ?assertEqual(401, Code), -+ {JsonBody} = ejson:decode(Body), -+ ?assertEqual(<<"unauthorized">>, -+ couch_util:get_value(<<"error">>, JsonBody)); -+ Else -> -+ erlang:error({assertion_failed, -+ [{module, ?MODULE}, -+ {line, ?LINE}, -+ {reason, ?iofmt("Request failed: ~p", [Else])}]}) -+ end -+ end). -diff --git a/test/couchdb/couchdb_views_tests.erl b/test/couchdb/couchdb_views_tests.erl -new file mode 100644 -index 0000000..c61f783 ---- /dev/null -+++ b/test/couchdb/couchdb_views_tests.erl -@@ -0,0 +1,669 @@ -+% 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(couchdb_views_tests). -+ -+-include("couch_eunit.hrl"). -+-include_lib("couchdb/couch_db.hrl"). -+-include_lib("couch_mrview/include/couch_mrview.hrl"). -+ -+-define(ADMIN_USER, {user_ctx, #user_ctx{roles=[<<"_admin">>]}}). -+-define(DELAY, 100). -+-define(TIMEOUT, 1000). -+ -+ -+start() -> -+ {ok, Pid} = couch_server_sup:start_link(?CONFIG_CHAIN), -+ Pid. -+ -+stop(Pid) -> -+ erlang:monitor(process, Pid), -+ couch_server_sup:stop(), -+ receive -+ {'DOWN', _, _, Pid, _} -> -+ ok -+ after ?TIMEOUT -> -+ throw({timeout, server_stop}) -+ end. -+ -+setup() -> -+ DbName = ?tempdb(), -+ {ok, Db} = couch_db:create(DbName, [?ADMIN_USER]), -+ ok = couch_db:close(Db), -+ FooRev = create_design_doc(DbName, <<"_design/foo">>, <<"bar">>), -+ query_view(DbName, "foo", "bar"), -+ BooRev = create_design_doc(DbName, <<"_design/boo">>, <<"baz">>), -+ query_view(DbName, "boo", "baz"), -+ {DbName, {FooRev, BooRev}}. -+ -+setup_with_docs() -> -+ DbName = ?tempdb(), -+ {ok, Db} = couch_db:create(DbName, [?ADMIN_USER]), -+ ok = couch_db:close(Db), -+ create_docs(DbName), -+ create_design_doc(DbName, <<"_design/foo">>, <<"bar">>), -+ DbName. -+ -+teardown({DbName, _}) -> -+ teardown(DbName); -+teardown(DbName) when is_binary(DbName) -> -+ couch_server:delete(DbName, [?ADMIN_USER]), -+ ok. -+ -+ -+view_indexes_cleanup_test_() -> -+ { -+ "View indexes cleanup", -+ { -+ setup, -+ fun start/0, fun stop/1, -+ { -+ foreach, -+ fun setup/0, fun teardown/1, -+ [ -+ fun should_have_two_indexes_alive_before_deletion/1, -+ fun should_cleanup_index_file_after_ddoc_deletion/1, -+ fun should_cleanup_all_index_files/1 -+ ] -+ } -+ } -+ }. -+ -+view_group_db_leaks_test_() -> -+ { -+ "View group db leaks", -+ { -+ setup, -+ fun start/0, fun stop/1, -+ { -+ foreach, -+ fun setup_with_docs/0, fun teardown/1, -+ [ -+ fun couchdb_1138/1, -+ fun couchdb_1309/1 -+ ] -+ } -+ } -+ }. -+ -+view_group_shutdown_test_() -> -+ { -+ "View group shutdown", -+ { -+ setup, -+ fun start/0, fun stop/1, -+ [couchdb_1283()] -+ } -+ }. -+ -+ -+should_not_remember_docs_in_index_after_backup_restore_test() -> -+ %% COUCHDB-640 -+ start(), -+ DbName = setup_with_docs(), -+ -+ ok = backup_db_file(DbName), -+ create_doc(DbName, "doc666"), -+ -+ Rows0 = query_view(DbName, "foo", "bar"), -+ ?assert(has_doc("doc1", Rows0)), -+ ?assert(has_doc("doc2", Rows0)), -+ ?assert(has_doc("doc3", Rows0)), -+ ?assert(has_doc("doc666", Rows0)), -+ -+ restore_backup_db_file(DbName), -+ -+ Rows1 = query_view(DbName, "foo", "bar"), -+ ?assert(has_doc("doc1", Rows1)), -+ ?assert(has_doc("doc2", Rows1)), -+ ?assert(has_doc("doc3", Rows1)), -+ ?assertNot(has_doc("doc666", Rows1)), -+ -+ teardown(DbName), -+ stop(whereis(couch_server_sup)). -+ -+ -+should_upgrade_legacy_view_files_test() -> -+ start(), -+ -+ ok = couch_config:set("query_server_config", "commit_freq", "0", false), -+ -+ DbName = <<"test">>, -+ DbFileName = "test.couch", -+ DbFilePath = filename:join([?FIXTURESDIR, DbFileName]), -+ OldViewName = "3b835456c235b1827e012e25666152f3.view", -+ FixtureViewFilePath = filename:join([?FIXTURESDIR, OldViewName]), -+ NewViewName = "a1c5929f912aca32f13446122cc6ce50.view", -+ -+ DbDir = couch_config:get("couchdb", "database_dir"), -+ ViewDir = couch_config:get("couchdb", "view_index_dir"), -+ OldViewFilePath = filename:join([ViewDir, ".test_design", OldViewName]), -+ NewViewFilePath = filename:join([ViewDir, ".test_design", "mrview", -+ NewViewName]), -+ -+ % cleanup -+ Files = [ -+ filename:join([DbDir, DbFileName]), -+ OldViewFilePath, -+ NewViewFilePath -+ ], -+ lists:foreach(fun(File) -> file:delete(File) end, Files), -+ -+ % copy old db file into db dir -+ {ok, _} = file:copy(DbFilePath, filename:join([DbDir, DbFileName])), -+ -+ % copy old view file into view dir -+ ok = filelib:ensure_dir(filename:join([ViewDir, ".test_design"]) ++ "/"), -+ {ok, _} = file:copy(FixtureViewFilePath, OldViewFilePath), -+ -+ % ensure old header -+ OldHeader = read_header(OldViewFilePath), -+ ?assertMatch(#index_header{}, OldHeader), -+ -+ % query view for expected results -+ Rows0 = query_view(DbName, "test", "test"), -+ ?assertEqual(2, length(Rows0)), -+ -+ % ensure old file gone -+ ?assertNot(filelib:is_regular(OldViewFilePath)), -+ -+ % add doc to trigger update -+ DocUrl = db_url(DbName) ++ "/boo", -+ {ok, _, _, _} = test_request:put( -+ DocUrl, [{"Content-Type", "application/json"}], <<"{\"a\":3}">>), -+ -+ % query view for expected results -+ Rows1 = query_view(DbName, "test", "test"), -+ ?assertEqual(3, length(Rows1)), -+ -+ % ensure new header -+ timer:sleep(2000), % have to wait for awhile to upgrade the index -+ NewHeader = read_header(NewViewFilePath), -+ ?assertMatch(#mrheader{}, NewHeader), -+ -+ teardown(DbName), -+ stop(whereis(couch_server_sup)). -+ -+ -+should_have_two_indexes_alive_before_deletion({DbName, _}) -> -+ view_cleanup(DbName), -+ ?_assertEqual(2, count_index_files(DbName)). -+ -+should_cleanup_index_file_after_ddoc_deletion({DbName, {FooRev, _}}) -> -+ delete_design_doc(DbName, <<"_design/foo">>, FooRev), -+ view_cleanup(DbName), -+ ?_assertEqual(1, count_index_files(DbName)). -+ -+should_cleanup_all_index_files({DbName, {FooRev, BooRev}})-> -+ delete_design_doc(DbName, <<"_design/foo">>, FooRev), -+ delete_design_doc(DbName, <<"_design/boo">>, BooRev), -+ view_cleanup(DbName), -+ ?_assertEqual(0, count_index_files(DbName)). -+ -+couchdb_1138(DbName) -> -+ ?_test(begin -+ {ok, IndexerPid} = couch_index_server:get_index( -+ couch_mrview_index, DbName, <<"_design/foo">>), -+ ?assert(is_pid(IndexerPid)), -+ ?assert(is_process_alive(IndexerPid)), -+ ?assertEqual(2, count_db_refs(DbName)), -+ -+ Rows0 = query_view(DbName, "foo", "bar"), -+ ?assertEqual(3, length(Rows0)), -+ ?assertEqual(2, count_db_refs(DbName)), -+ ?assert(is_process_alive(IndexerPid)), -+ -+ create_doc(DbName, "doc1000"), -+ Rows1 = query_view(DbName, "foo", "bar"), -+ ?assertEqual(4, length(Rows1)), -+ ?assertEqual(2, count_db_refs(DbName)), -+ ?assert(is_process_alive(IndexerPid)), -+ -+ Ref1 = get_db_ref_counter(DbName), -+ compact_db(DbName), -+ Ref2 = get_db_ref_counter(DbName), -+ ?assertEqual(2, couch_ref_counter:count(Ref2)), -+ ?assertNotEqual(Ref2, Ref1), -+ ?assertNot(is_process_alive(Ref1)), -+ ?assert(is_process_alive(IndexerPid)), -+ -+ compact_view_group(DbName, "foo"), -+ ?assertEqual(2, count_db_refs(DbName)), -+ Ref3 = get_db_ref_counter(DbName), -+ ?assertEqual(Ref3, Ref2), -+ ?assert(is_process_alive(IndexerPid)), -+ -+ create_doc(DbName, "doc1001"), -+ Rows2 = query_view(DbName, "foo", "bar"), -+ ?assertEqual(5, length(Rows2)), -+ ?assertEqual(2, count_db_refs(DbName)), -+ ?assert(is_process_alive(IndexerPid)) -+ end). -+ -+couchdb_1309(DbName) -> -+ ?_test(begin -+ {ok, IndexerPid} = couch_index_server:get_index( -+ couch_mrview_index, DbName, <<"_design/foo">>), -+ ?assert(is_pid(IndexerPid)), -+ ?assert(is_process_alive(IndexerPid)), -+ ?assertEqual(2, count_db_refs(DbName)), -+ -+ create_doc(DbName, "doc1001"), -+ Rows0 = query_view(DbName, "foo", "bar"), -+ check_rows_value(Rows0, null), -+ ?assertEqual(4, length(Rows0)), -+ ?assertEqual(2, count_db_refs(DbName)), -+ ?assert(is_process_alive(IndexerPid)), -+ -+ update_design_doc(DbName, <<"_design/foo">>, <<"bar">>), -+ {ok, NewIndexerPid} = couch_index_server:get_index( -+ couch_mrview_index, DbName, <<"_design/foo">>), -+ ?assert(is_pid(NewIndexerPid)), -+ ?assert(is_process_alive(NewIndexerPid)), -+ ?assertNotEqual(IndexerPid, NewIndexerPid), -+ ?assertEqual(2, count_db_refs(DbName)), -+ -+ Rows1 = query_view(DbName, "foo", "bar", ok), -+ ?assertEqual(0, length(Rows1)), -+ Rows2 = query_view(DbName, "foo", "bar"), -+ check_rows_value(Rows2, 1), -+ ?assertEqual(4, length(Rows2)), -+ -+ MonRef0 = erlang:monitor(process, IndexerPid), -+ receive -+ {'DOWN', MonRef0, _, _, _} -> -+ ok -+ after ?TIMEOUT -> -+ erlang:error( -+ {assertion_failed, -+ [{module, ?MODULE}, {line, ?LINE}, -+ {reason, "old view group is not dead after ddoc update"}]}) -+ end, -+ -+ MonRef1 = erlang:monitor(process, NewIndexerPid), -+ ok = couch_server:delete(DbName, [?ADMIN_USER]), -+ receive -+ {'DOWN', MonRef1, _, _, _} -> -+ ok -+ after ?TIMEOUT -> -+ erlang:error( -+ {assertion_failed, -+ [{module, ?MODULE}, {line, ?LINE}, -+ {reason, "new view group did not die after DB deletion"}]}) -+ end -+ end). -+ -+couchdb_1283() -> -+ ?_test(begin -+ ok = couch_config:set("couchdb", "max_dbs_open", "3", false), -+ ok = couch_config:set("couchdb", "delayed_commits", "false", false), -+ -+ {ok, MDb1} = couch_db:create(?tempdb(), [?ADMIN_USER]), -+ DDoc = couch_doc:from_json_obj({[ -+ {<<"_id">>, <<"_design/foo">>}, -+ {<<"language">>, <<"javascript">>}, -+ {<<"views">>, {[ -+ {<<"foo">>, {[ -+ {<<"map">>, <<"function(doc) { emit(doc._id, null); }">>} -+ ]}}, -+ {<<"foo2">>, {[ -+ {<<"map">>, <<"function(doc) { emit(doc._id, null); }">>} -+ ]}}, -+ {<<"foo3">>, {[ -+ {<<"map">>, <<"function(doc) { emit(doc._id, null); }">>} -+ ]}}, -+ {<<"foo4">>, {[ -+ {<<"map">>, <<"function(doc) { emit(doc._id, null); }">>} -+ ]}}, -+ {<<"foo5">>, {[ -+ {<<"map">>, <<"function(doc) { emit(doc._id, null); }">>} -+ ]}} -+ ]}} -+ ]}), -+ {ok, _} = couch_db:update_doc(MDb1, DDoc, []), -+ ok = populate_db(MDb1, 100, 100), -+ query_view(MDb1#db.name, "foo", "foo"), -+ ok = couch_db:close(MDb1), -+ -+ {ok, Db1} = couch_db:create(?tempdb(), [?ADMIN_USER]), -+ ok = couch_db:close(Db1), -+ {ok, Db2} = couch_db:create(?tempdb(), [?ADMIN_USER]), -+ ok = couch_db:close(Db2), -+ {ok, Db3} = couch_db:create(?tempdb(), [?ADMIN_USER]), -+ ok = couch_db:close(Db3), -+ -+ Writer1 = spawn_writer(Db1#db.name), -+ Writer2 = spawn_writer(Db2#db.name), -+ -+ ?assert(is_process_alive(Writer1)), -+ ?assert(is_process_alive(Writer2)), -+ -+ ?assertEqual(ok, get_writer_status(Writer1)), -+ ?assertEqual(ok, get_writer_status(Writer2)), -+ -+ {ok, MonRef} = couch_mrview:compact(MDb1#db.name, <<"_design/foo">>, -+ [monitor]), -+ -+ Writer3 = spawn_writer(Db3#db.name), -+ ?assert(is_process_alive(Writer3)), -+ ?assertEqual({error, all_dbs_active}, get_writer_status(Writer3)), -+ -+ ?assert(is_process_alive(Writer1)), -+ ?assert(is_process_alive(Writer2)), -+ ?assert(is_process_alive(Writer3)), -+ -+ receive -+ {'DOWN', MonRef, process, _, Reason} -> -+ ?assertEqual(normal, Reason) -+ after ?TIMEOUT -> -+ erlang:error( -+ {assertion_failed, -+ [{module, ?MODULE}, {line, ?LINE}, -+ {reason, "Failure compacting view group"}]}) -+ end, -+ -+ ?assertEqual(ok, writer_try_again(Writer3)), -+ ?assertEqual(ok, get_writer_status(Writer3)), -+ -+ ?assert(is_process_alive(Writer1)), -+ ?assert(is_process_alive(Writer2)), -+ ?assert(is_process_alive(Writer3)), -+ -+ ?assertEqual(ok, stop_writer(Writer1)), -+ ?assertEqual(ok, stop_writer(Writer2)), -+ ?assertEqual(ok, stop_writer(Writer3)) -+ end). -+ -+create_doc(DbName, DocId) when is_list(DocId) -> -+ create_doc(DbName, ?l2b(DocId)); -+create_doc(DbName, DocId) when is_binary(DocId) -> -+ {ok, Db} = couch_db:open(DbName, [?ADMIN_USER]), -+ Doc666 = couch_doc:from_json_obj({[ -+ {<<"_id">>, DocId}, -+ {<<"value">>, 999} -+ ]}), -+ {ok, _} = couch_db:update_docs(Db, [Doc666]), -+ couch_db:ensure_full_commit(Db), -+ couch_db:close(Db). -+ -+create_docs(DbName) -> -+ {ok, Db} = couch_db:open(DbName, [?ADMIN_USER]), -+ 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). -+ -+populate_db(Db, BatchSize, N) when N > 0 -> -+ Docs = lists:map( -+ fun(_) -> -+ couch_doc:from_json_obj({[ -+ {<<"_id">>, couch_uuids:new()}, -+ {<<"value">>, base64:encode(crypto:rand_bytes(1000))} -+ ]}) -+ end, -+ lists:seq(1, BatchSize)), -+ {ok, _} = couch_db:update_docs(Db, Docs, []), -+ populate_db(Db, BatchSize, N - length(Docs)); -+populate_db(_Db, _, _) -> -+ ok. -+ -+create_design_doc(DbName, DDName, ViewName) -> -+ {ok, Db} = couch_db:open(DbName, [?ADMIN_USER]), -+ DDoc = couch_doc:from_json_obj({[ -+ {<<"_id">>, DDName}, -+ {<<"language">>, <<"javascript">>}, -+ {<<"views">>, {[ -+ {ViewName, {[ -+ {<<"map">>, <<"function(doc) { emit(doc.value, null); }">>} -+ ]}} -+ ]}} -+ ]}), -+ {ok, Rev} = couch_db:update_doc(Db, DDoc, []), -+ couch_db:ensure_full_commit(Db), -+ couch_db:close(Db), -+ Rev. -+ -+update_design_doc(DbName, DDName, ViewName) -> -+ {ok, Db} = couch_db:open(DbName, [?ADMIN_USER]), -+ {ok, Doc} = couch_db:open_doc(Db, DDName, [?ADMIN_USER]), -+ {Props} = couch_doc:to_json_obj(Doc, []), -+ Rev = couch_util:get_value(<<"_rev">>, Props), -+ DDoc = couch_doc:from_json_obj({[ -+ {<<"_id">>, DDName}, -+ {<<"_rev">>, Rev}, -+ {<<"language">>, <<"javascript">>}, -+ {<<"views">>, {[ -+ {ViewName, {[ -+ {<<"map">>, <<"function(doc) { emit(doc.value, 1); }">>} -+ ]}} -+ ]}} -+ ]}), -+ {ok, NewRev} = couch_db:update_doc(Db, DDoc, [?ADMIN_USER]), -+ couch_db:ensure_full_commit(Db), -+ couch_db:close(Db), -+ NewRev. -+ -+delete_design_doc(DbName, DDName, Rev) -> -+ {ok, Db} = couch_db:open(DbName, [?ADMIN_USER]), -+ DDoc = couch_doc:from_json_obj({[ -+ {<<"_id">>, DDName}, -+ {<<"_rev">>, couch_doc:rev_to_str(Rev)}, -+ {<<"_deleted">>, true} -+ ]}), -+ {ok, _} = couch_db:update_doc(Db, DDoc, [Rev]), -+ couch_db:close(Db). -+ -+db_url(DbName) -> -+ Addr = couch_config:get("httpd", "bind_address", "127.0.0.1"), -+ Port = integer_to_list(mochiweb_socket_server:get(couch_httpd, port)), -+ "http://" ++ Addr ++ ":" ++ Port ++ "/" ++ ?b2l(DbName). -+ -+query_view(DbName, DDoc, View) -> -+ query_view(DbName, DDoc, View, false). -+ -+query_view(DbName, DDoc, View, Stale) -> -+ {ok, Code, _Headers, Body} = test_request:get( -+ db_url(DbName) ++ "/_design/" ++ DDoc ++ "/_view/" ++ View -+ ++ case Stale of -+ false -> []; -+ _ -> "?stale=" ++ atom_to_list(Stale) -+ end), -+ ?assertEqual(200, Code), -+ {Props} = ejson:decode(Body), -+ couch_util:get_value(<<"rows">>, Props, []). -+ -+check_rows_value(Rows, Value) -> -+ lists:foreach( -+ fun({Row}) -> -+ ?assertEqual(Value, couch_util:get_value(<<"value">>, Row)) -+ end, Rows). -+ -+view_cleanup(DbName) -> -+ {ok, Db} = couch_db:open(DbName, [?ADMIN_USER]), -+ couch_mrview:cleanup(Db), -+ couch_db:close(Db). -+ -+get_db_ref_counter(DbName) -> -+ {ok, #db{fd_ref_counter = Ref} = Db} = couch_db:open_int(DbName, []), -+ ok = couch_db:close(Db), -+ Ref. -+ -+count_db_refs(DbName) -> -+ Ref = get_db_ref_counter(DbName), -+ % have to sleep a bit to let couchdb cleanup all refs and leave only -+ % active ones. otherwise the related tests will randomly fail due to -+ % count number mismatch -+ timer:sleep(200), -+ couch_ref_counter:count(Ref). -+ -+count_index_files(DbName) -> -+ % call server to fetch the index files -+ RootDir = couch_config:get("couchdb", "view_index_dir"), -+ length(filelib:wildcard(RootDir ++ "/." ++ -+ binary_to_list(DbName) ++ "_design"++"/mrview/*")). -+ -+has_doc(DocId1, Rows) -> -+ DocId = iolist_to_binary(DocId1), -+ lists:any(fun({R}) -> lists:member({<<"id">>, DocId}, R) end, Rows). -+ -+backup_db_file(DbName) -> -+ DbDir = couch_config:get("couchdb", "database_dir"), -+ DbFile = filename:join([DbDir, ?b2l(DbName) ++ ".couch"]), -+ {ok, _} = file:copy(DbFile, DbFile ++ ".backup"), -+ ok. -+ -+restore_backup_db_file(DbName) -> -+ DbDir = couch_config:get("couchdb", "database_dir"), -+ stop(whereis(couch_server_sup)), -+ DbFile = filename:join([DbDir, ?b2l(DbName) ++ ".couch"]), -+ ok = file:delete(DbFile), -+ ok = file:rename(DbFile ++ ".backup", DbFile), -+ start(), -+ ok. -+ -+compact_db(DbName) -> -+ {ok, Db} = couch_db:open_int(DbName, []), -+ {ok, _} = couch_db:start_compact(Db), -+ ok = couch_db:close(Db), -+ wait_db_compact_done(DbName, 10). -+ -+wait_db_compact_done(_DbName, 0) -> -+ erlang:error({assertion_failed, -+ [{module, ?MODULE}, -+ {line, ?LINE}, -+ {reason, "DB compaction failed to finish"}]}); -+wait_db_compact_done(DbName, N) -> -+ {ok, Db} = couch_db:open_int(DbName, []), -+ ok = couch_db:close(Db), -+ case is_pid(Db#db.compactor_pid) of -+ false -> -+ ok; -+ true -> -+ ok = timer:sleep(?DELAY), -+ wait_db_compact_done(DbName, N - 1) -+ end. -+ -+compact_view_group(DbName, DDocId) when is_list(DDocId) -> -+ compact_view_group(DbName, ?l2b("_design/" ++ DDocId)); -+compact_view_group(DbName, DDocId) when is_binary(DDocId) -> -+ ok = couch_mrview:compact(DbName, DDocId), -+ wait_view_compact_done(DbName, DDocId, 10). -+ -+wait_view_compact_done(_DbName, _DDocId, 0) -> -+ erlang:error({assertion_failed, -+ [{module, ?MODULE}, -+ {line, ?LINE}, -+ {reason, "DB compaction failed to finish"}]}); -+wait_view_compact_done(DbName, DDocId, N) -> -+ {ok, Code, _Headers, Body} = test_request:get( -+ db_url(DbName) ++ "/" ++ ?b2l(DDocId) ++ "/_info"), -+ ?assertEqual(200, Code), -+ {Info} = ejson: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(?DELAY), -+ wait_view_compact_done(DbName, DDocId, N - 1) -+ end. -+ -+spawn_writer(DbName) -> -+ Parent = self(), -+ spawn(fun() -> -+ process_flag(priority, high), -+ writer_loop(DbName, Parent) -+ end). -+ -+get_writer_status(Writer) -> -+ Ref = make_ref(), -+ Writer ! {get_status, Ref}, -+ receive -+ {db_open, Ref} -> -+ ok; -+ {db_open_error, Error, Ref} -> -+ Error -+ after ?TIMEOUT -> -+ timeout -+ end. -+ -+writer_try_again(Writer) -> -+ Ref = make_ref(), -+ Writer ! {try_again, Ref}, -+ receive -+ {ok, Ref} -> -+ ok -+ after ?TIMEOUT -> -+ timeout -+ end. -+ -+stop_writer(Writer) -> -+ Ref = make_ref(), -+ Writer ! {stop, Ref}, -+ receive -+ {ok, Ref} -> -+ ok -+ after ?TIMEOUT -> -+ erlang:error({assertion_failed, -+ [{module, ?MODULE}, -+ {line, ?LINE}, -+ {reason, "Timeout on stopping process"}]}) -+ end. -+ -+writer_loop(DbName, Parent) -> -+ case couch_db:open_int(DbName, []) of -+ {ok, Db} -> -+ writer_loop_1(Db, Parent); -+ Error -> -+ writer_loop_2(DbName, Parent, Error) -+ end. -+ -+writer_loop_1(Db, Parent) -> -+ receive -+ {get_status, Ref} -> -+ Parent ! {db_open, Ref}, -+ writer_loop_1(Db, Parent); -+ {stop, Ref} -> -+ ok = couch_db:close(Db), -+ Parent ! {ok, Ref} -+ end. -+ -+writer_loop_2(DbName, Parent, Error) -> -+ receive -+ {get_status, Ref} -> -+ Parent ! {db_open_error, Error, Ref}, -+ writer_loop_2(DbName, Parent, Error); -+ {try_again, Ref} -> -+ Parent ! {ok, Ref}, -+ writer_loop(DbName, Parent) -+ end. -+ -+read_header(File) -> -+ {ok, Fd} = couch_file:open(File), -+ {ok, {_Sig, Header}} = couch_file:read_header(Fd), -+ couch_file:close(Fd), -+ Header. -diff --git a/test/couchdb/eunit.ini b/test/couchdb/eunit.ini -new file mode 100644 -index 0000000..50024a3 ---- /dev/null -+++ b/test/couchdb/eunit.ini -@@ -0,0 +1,28 @@ -+; 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] -+; time to relax! -+uuid = 74696d6520746f2072656c617821 -+ -+[httpd] -+port = 0 -+ -+[log] -+; logging is disabled to remove unwanted noise in stdout from tests processing -+level = none -+include_sasl = false -diff --git a/test/couchdb/fixtures/Makefile.am b/test/couchdb/fixtures/Makefile.am -new file mode 100644 -index 0000000..fa38d3f ---- /dev/null -+++ b/test/couchdb/fixtures/Makefile.am -@@ -0,0 +1,20 @@ -+## 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_PROGRAMS = test_cfg_register -+test_cfg_register_SOURCES = test_cfg_register.c -+test_cfg_register_CFLAGS = -D_BSD_SOURCE -+ -+noinst_SCRIPTS = os_daemon_configer.escript -+ -+all: -+ chmod +x os_daemon_configer.escript -diff --git a/test/couchdb/fixtures/couch_config_tests_1.ini b/test/couchdb/fixtures/couch_config_tests_1.ini -new file mode 100644 -index 0000000..55451da ---- /dev/null -+++ b/test/couchdb/fixtures/couch_config_tests_1.ini -@@ -0,0 +1,22 @@ -+; Licensed to the Apache Software Foundation (ASF) under one -+; or more contributor license agreements. See the NOTICE file -+; distributed with this work for additional information -+; regarding copyright ownership. The ASF licenses this file -+; to you under the Apache License, Version 2.0 (the -+; "License"); you may not use this file except in compliance -+; with the License. You may obtain a copy of the License at -+; -+; http://www.apache.org/licenses/LICENSE-2.0 -+; -+; Unless required by applicable law or agreed to in writing, -+; software distributed under the License is distributed on an -+; "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -+; KIND, either express or implied. See the License for the -+; specific language governing permissions and limitations -+; under the License. -+ -+[couchdb] -+max_dbs_open=10 -+ -+[httpd] -+port=4895 -diff --git a/test/couchdb/fixtures/couch_config_tests_2.ini b/test/couchdb/fixtures/couch_config_tests_2.ini -new file mode 100644 -index 0000000..5f46357 ---- /dev/null -+++ b/test/couchdb/fixtures/couch_config_tests_2.ini -@@ -0,0 +1,22 @@ -+; Licensed to the Apache Software Foundation (ASF) under one -+; or more contributor license agreements. See the NOTICE file -+; distributed with this work for additional information -+; regarding copyright ownership. The ASF licenses this file -+; to you under the Apache License, Version 2.0 (the -+; "License"); you may not use this file except in compliance -+; with the License. You may obtain a copy of the License at -+; -+; http://www.apache.org/licenses/LICENSE-2.0 -+; -+; Unless required by applicable law or agreed to in writing, -+; software distributed under the License is distributed on an -+; "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY -+; KIND, either express or implied. See the License for the -+; specific language governing permissions and limitations -+; under the License. -+ -+[httpd] -+port = 80 -+ -+[fizbang] -+unicode = normalized -diff --git a/test/couchdb/fixtures/couch_stats_aggregates.cfg b/test/couchdb/fixtures/couch_stats_aggregates.cfg -new file mode 100644 -index 0000000..30e475d ---- /dev/null -+++ b/test/couchdb/fixtures/couch_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/test/couchdb/fixtures/couch_stats_aggregates.ini b/test/couchdb/fixtures/couch_stats_aggregates.ini -new file mode 100644 -index 0000000..cc5cd21 ---- /dev/null -+++ b/test/couchdb/fixtures/couch_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/test/couchdb/fixtures/os_daemon_bad_perm.sh b/test/couchdb/fixtures/os_daemon_bad_perm.sh -new file mode 100644 -index 0000000..345c8b4 ---- /dev/null -+++ b/test/couchdb/fixtures/os_daemon_bad_perm.sh -@@ -0,0 +1,17 @@ -+#!/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. -+# -+# Please do not make this file executable as that's the error being tested. -+ -+sleep 5 -diff --git a/test/couchdb/fixtures/os_daemon_can_reboot.sh b/test/couchdb/fixtures/os_daemon_can_reboot.sh -new file mode 100755 -index 0000000..5bc10e8 ---- /dev/null -+++ b/test/couchdb/fixtures/os_daemon_can_reboot.sh -@@ -0,0 +1,15 @@ -+#!/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. -+ -+sleep 2 -diff --git a/test/couchdb/fixtures/os_daemon_configer.escript.in b/test/couchdb/fixtures/os_daemon_configer.escript.in -new file mode 100755 -index 0000000..d2ecfa8 ---- /dev/null -+++ b/test/couchdb/fixtures/os_daemon_configer.escript.in -@@ -0,0 +1,84 @@ -+#! /usr/bin/env escript -+%% -*- erlang -*- -+%%! -DTEST -pa @abs_top_builddir@/src/ejson -+%% -+%% 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. -+ -+read() -> -+ case io:get_line('') of -+ eof -> -+ stop; -+ Data -> -+ ejson:decode(Data) -+ end. -+ -+write(Mesg) -> -+ Data = iolist_to_binary(ejson: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() -> -+ Path = list_to_binary(?FILE), -+ FileName = list_to_binary(filename:basename(?FILE)), -+ {[{FileName, Path}]} = get_cfg(<<"os_daemons">>). -+ -+test_get_cfg2() -> -+ Path = list_to_binary(?FILE), -+ FileName = list_to_binary(filename:basename(?FILE)), -+ Path = get_cfg(<<"os_daemons">>, FileName), -+ <<"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([]) -> -+ do_tests(). -diff --git a/test/couchdb/fixtures/os_daemon_die_on_boot.sh b/test/couchdb/fixtures/os_daemon_die_on_boot.sh -new file mode 100755 -index 0000000..256ee79 ---- /dev/null -+++ b/test/couchdb/fixtures/os_daemon_die_on_boot.sh -@@ -0,0 +1,15 @@ -+#!/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. -+ -+exit 1 -diff --git a/test/couchdb/fixtures/os_daemon_die_quickly.sh b/test/couchdb/fixtures/os_daemon_die_quickly.sh -new file mode 100755 -index 0000000..f5a1368 ---- /dev/null -+++ b/test/couchdb/fixtures/os_daemon_die_quickly.sh -@@ -0,0 +1,15 @@ -+#!/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. -+ -+sleep 1 -diff --git a/test/couchdb/fixtures/os_daemon_looper.escript b/test/couchdb/fixtures/os_daemon_looper.escript -new file mode 100755 -index 0000000..73974e9 ---- /dev/null -+++ b/test/couchdb/fixtures/os_daemon_looper.escript -@@ -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/test/couchdb/fixtures/test_cfg_register.c b/test/couchdb/fixtures/test_cfg_register.c -new file mode 100644 -index 0000000..c910bac ---- /dev/null -+++ b/test/couchdb/fixtures/test_cfg_register.c -@@ -0,0 +1,31 @@ -+// 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 -+#include -+ -+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/test/couchdb/include/couch_eunit.hrl.in b/test/couchdb/include/couch_eunit.hrl.in -new file mode 100644 -index 0000000..ff080e1 ---- /dev/null -+++ b/test/couchdb/include/couch_eunit.hrl.in -@@ -0,0 +1,44 @@ -+% 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_lib("eunit/include/eunit.hrl"). -+ -+-define(BUILDDIR, "@abs_top_builddir@"). -+-define(SOURCEDIR, "@abs_top_srcdir@"). -+-define(CONFIG_CHAIN, [ -+ filename:join([?BUILDDIR, "etc", "couchdb", "default_dev.ini"]), -+ filename:join([?BUILDDIR, "etc", "couchdb", "local_dev.ini"]), -+ filename:join([?SOURCEDIR, "test", "couchdb", "eunit.ini"])]). -+-define(FIXTURESDIR, -+ filename:join([?SOURCEDIR, "test", "couchdb", "fixtures"])). -+-define(TEMPDIR, -+ filename:join([?BUILDDIR, "test", "couchdb", "temp"])). -+ -+-define(tempfile, -+ fun() -> -+ {A, B, C} = erlang:now(), -+ N = node(), -+ FileName = lists:flatten(io_lib:format("~p-~p.~p.~p", [N, A, B, C])), -+ filename:join([?TEMPDIR, FileName]) -+ end). -+-define(tempdb, -+ fun() -> -+ Nums = tuple_to_list(erlang:now()), -+ Prefix = "eunit-test-db", -+ Suffix = lists:concat([integer_to_list(Num) || Num <- Nums]), -+ list_to_binary(Prefix ++ "-" ++ Suffix) -+ end). -+-define(docid, -+ fun() -> -+ {A, B, C} = erlang:now(), -+ lists:flatten(io_lib:format("~p~p~p", [A, B, C])) -+ end). -diff --git a/test/couchdb/json_stream_parse_tests.erl b/test/couchdb/json_stream_parse_tests.erl -new file mode 100644 -index 0000000..92303b6 ---- /dev/null -+++ b/test/couchdb/json_stream_parse_tests.erl -@@ -0,0 +1,151 @@ -+% 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(json_stream_parse_tests). -+ -+-include("couch_eunit.hrl"). -+ -+-define(CASES, -+ [ -+ {1, "1", "integer numeric literial"}, -+ {3.1416, "3.14160", "float numeric literal"}, % text representation may truncate, trail zeroes -+ {-1, "-1", "negative integer numeric literal"}, -+ {-3.1416, "-3.14160", "negative float numeric literal"}, -+ {12.0e10, "1.20000e+11", "float literal in scientific notation"}, -+ {1.234E+10, "1.23400e+10", "another float literal in scientific notation"}, -+ {-1.234E-10, "-1.23400e-10", "negative float literal in scientific notation"}, -+ {10.0, "1.0e+01", "yet another float literal in scientific notation"}, -+ {123.456, "1.23456E+2", "yet another float literal in scientific notation"}, -+ {10.0, "1e1", "yet another float literal in scientific notation"}, -+ {<<"foo">>, "\"foo\"", "string literal"}, -+ {<<"foo", 5, "bar">>, "\"foo\\u0005bar\"", "string literal with \\u0005"}, -+ {<<"">>, "\"\"", "empty string literal"}, -+ {<<"\n\n\n">>, "\"\\n\\n\\n\"", "only new lines literal"}, -+ {<<"\" \b\f\r\n\t\"">>, "\"\\\" \\b\\f\\r\\n\\t\\\"\"", -+ "only white spaces string literal"}, -+ {null, "null", "null literal"}, -+ {true, "true", "true literal"}, -+ {false, "false", "false literal"}, -+ {<<"null">>, "\"null\"", "null string literal"}, -+ {<<"true">>, "\"true\"", "true string literal"}, -+ {<<"false">>, "\"false\"", "false string literal"}, -+ {{[]}, "{}", "empty object literal"}, -+ {{[{<<"foo">>, <<"bar">>}]}, "{\"foo\":\"bar\"}", -+ "simple object literal"}, -+ {{[{<<"foo">>, <<"bar">>}, {<<"baz">>, 123}]}, -+ "{\"foo\":\"bar\",\"baz\":123}", "another simple object literal"}, -+ {[], "[]", "empty array literal"}, -+ {[[]], "[[]]", "empty array literal inside a single element array literal"}, -+ {[1, <<"foo">>], "[1,\"foo\"]", "simple non-empty array literal"}, -+ {[1199344435545.0, 1], "[1199344435545.0,1]", -+ "another simple non-empty array literal"}, -+ {[false, true, 321, null], "[false, true, 321, null]", "array of literals"}, -+ {{[{<<"foo">>, [123]}]}, "{\"foo\":[123]}", -+ "object literal with an array valued property"}, -+ {{[{<<"foo">>, {[{<<"bar">>, true}]}}]}, -+ "{\"foo\":{\"bar\":true}}", "nested object literal"}, -+ {{[{<<"foo">>, []}, {<<"bar">>, {[{<<"baz">>, true}]}}, -+ {<<"alice">>, <<"bob">>}]}, -+ "{\"foo\":[],\"bar\":{\"baz\":true},\"alice\":\"bob\"}", -+ "complex object literal"}, -+ {[-123, <<"foo">>, {[{<<"bar">>, []}]}, null], -+ "[-123,\"foo\",{\"bar\":[]},null]", -+ "complex array literal"} -+ ] -+). -+ -+ -+raw_json_input_test_() -> -+ Tests = lists:map( -+ fun({EJson, JsonString, Desc}) -> -+ {Desc, -+ ?_assert(equiv(EJson, json_stream_parse:to_ejson(JsonString)))} -+ end, ?CASES), -+ {"Tests with raw JSON string as the input", Tests}. -+ -+one_byte_data_fun_test_() -> -+ Tests = lists:map( -+ fun({EJson, JsonString, Desc}) -> -+ DataFun = fun() -> single_byte_data_fun(JsonString) end, -+ {Desc, -+ ?_assert(equiv(EJson, json_stream_parse:to_ejson(DataFun)))} -+ end, ?CASES), -+ {"Tests with a 1 byte output data function as the input", Tests}. -+ -+test_multiple_bytes_data_fun_test_() -> -+ Tests = lists:map( -+ fun({EJson, JsonString, Desc}) -> -+ DataFun = fun() -> multiple_bytes_data_fun(JsonString) end, -+ {Desc, -+ ?_assert(equiv(EJson, json_stream_parse:to_ejson(DataFun)))} -+ end, ?CASES), -+ {"Tests with a multiple bytes output data function as the input", Tests}. -+ -+ -+%% Test for equivalence of Erlang terms. -+%% Due to arbitrary order of construction, equivalent objects might -+%% compare unequal as erlang terms, so we need to carefully recurse -+%% through aggregates (tuples and objects). -+equiv({Props1}, {Props2}) -> -+ equiv_object(Props1, Props2); -+equiv(L1, L2) when is_list(L1), is_list(L2) -> -+ equiv_list(L1, L2); -+equiv(N1, N2) when is_number(N1), is_number(N2) -> -+ N1 == N2; -+equiv(B1, B2) when is_binary(B1), is_binary(B2) -> -+ B1 == B2; -+equiv(true, true) -> -+ true; -+equiv(false, false) -> -+ true; -+equiv(null, null) -> -+ true. -+ -+%% Object representation and traversal order is unknown. -+%% Use the sledgehammer and sort property lists. -+equiv_object(Props1, Props2) -> -+ L1 = lists:keysort(1, Props1), -+ L2 = lists:keysort(1, Props2), -+ Pairs = lists:zip(L1, L2), -+ true = lists:all( -+ fun({{K1, V1}, {K2, V2}}) -> -+ equiv(K1, K2) andalso equiv(V1, V2) -+ end, -+ Pairs). -+ -+%% Recursively compare tuple elements for equivalence. -+equiv_list([], []) -> -+ true; -+equiv_list([V1 | L1], [V2 | L2]) -> -+ equiv(V1, V2) andalso equiv_list(L1, L2). -+ -+single_byte_data_fun([]) -> -+ done; -+single_byte_data_fun([H | T]) -> -+ {<>, fun() -> single_byte_data_fun(T) end}. -+ -+multiple_bytes_data_fun([]) -> -+ done; -+multiple_bytes_data_fun(L) -> -+ N = crypto:rand_uniform(0, 7), -+ {Part, Rest} = split(L, N), -+ {list_to_binary(Part), fun() -> multiple_bytes_data_fun(Rest) end}. -+ -+split(L, N) when length(L) =< N -> -+ {L, []}; -+split(L, N) -> -+ take(N, L, []). -+ -+take(0, L, Acc) -> -+ {lists:reverse(Acc), L}; -+take(N, [H|L], Acc) -> -+ take(N - 1, L, [H | Acc]). -diff --git a/test/couchdb/run.in b/test/couchdb/run.in -new file mode 100644 -index 0000000..2405f63 ---- /dev/null -+++ b/test/couchdb/run.in -@@ -0,0 +1,111 @@ -+#!/usr/bin/env escript -+%% -*- erlang -*- -+%%! -DTEST -env ERL_LIBS @abs_top_builddir@/src:$ERL_LIBS -pa @abs_top_builddir@/test/couchdb/ebin -+%% -+%% 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. -+ -+-define(BUILDDIR, "@abs_top_builddir@"). -+-define(SOURCEDIR, "@abs_top_srcdir@"). -+-define(TESTS_EBIN, filename:join([?BUILDDIR, "test", "couchdb", "ebin", ""])). -+-define(TESTS_TEMP, filename:join([?BUILDDIR, "test", "couchdb", "temp", ""])). -+ -+main([]) -> -+ io:fwrite("Path to test file or directory wasn't specified.~n"), -+ erlang:halt(1); -+main(["-v"]) -> -+ io:fwrite("Path to test file or directory wasn't specified.~n"), -+ erlang:halt(1); -+main(["-v", Path]) -> -+ run(Path, [verbose]); -+main(["-v", _ | _]) -> -+ io:fwrite("Only single tests source path is supported.~n"), -+ erlang:halt(1); -+main([Path]) -> -+ run(Path, []), -+ ok; -+main([_|_]) -> -+ io:fwrite("Only single tests source path is supported.~n"), -+ erlang:halt(1). -+ -+ -+run(Path, Options) -> -+ ensure_dirs(), -+ Mask = "*_tests.erl", -+ Files = list_files(Path, Mask), -+ init_code_path(), -+ Mods = compile(Files), -+ run_tests(Mods, Options). -+ -+ensure_dirs() -> -+ ok = filelib:ensure_dir(?TESTS_EBIN), -+ ok = filelib:ensure_dir(?TESTS_TEMP), -+ ok. -+ -+list_files(Path, Mask)-> -+ AbsPath = filename:absname(Path), -+ case filelib:is_file(AbsPath) of -+ true -> -+ ok; -+ false -> -+ io:fwrite("File or directory not found: ~p~n", [AbsPath]), -+ erlang:halt(1) -+ end, -+ case filelib:is_dir(AbsPath) of -+ true -> -+ case filelib:wildcard(filename:join([AbsPath, Mask])) of -+ [] -> -+ io:fwrite("No test files was found at ~p by mask ~p ~n", -+ [AbsPath, Mask]), -+ erlang:halt(1); -+ Files -> -+ Files -+ end; -+ false -> [AbsPath] -+ end. -+ -+ -+compile(Files) -> -+ lists:map( -+ fun(File)-> -+ io:fwrite("compile ~p~n", [File]), -+ Opts = [report, verbose, {outdir, ?TESTS_EBIN}, -+ {i, filename:join([?BUILDDIR, "test", "couchdb", -+ "include"])}, -+ {i, filename:join([?SOURCEDIR, "src"])}], -+ {ok, Mod} = compile:file(File, Opts), -+ Mod -+ end, -+ Files). -+ -+ -+run_tests(Mods, Options) -> -+ %% disable error_logger to reduce noise in stdout -+ error_logger:tty(false), -+ case eunit:test(Mods, Options) of -+ error -> erlang:halt(1); -+ _ -> ok -+ end. -+ -+ -+init_code_path() -> -+ Paths = [ -+ "couchdb", -+ "ejson", -+ "erlang-oauth", -+ "ibrowse", -+ "mochiweb", -+ "snappy" -+ ], -+ lists:foreach(fun(Name) -> -+ code:add_patha(filename:join([?BUILDDIR, "src", Name])) -+ end, Paths). -diff --git a/test/couchdb/test_request.erl b/test/couchdb/test_request.erl -new file mode 100644 -index 0000000..68e4956 ---- /dev/null -+++ b/test/couchdb/test_request.erl -@@ -0,0 +1,75 @@ -+% Licensed under the Apache License, Version 2.0 (the "License"); you may not -+% use this file except in compliance with the License. You may obtain a copy of -+% the License at -+% -+% http://www.apache.org/licenses/LICENSE-2.0 -+% -+% Unless required by applicable law or agreed to in writing, software -+% distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -+% WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -+% License for the specific language governing permissions and limitations under -+% the License. -+ -+-module(test_request). -+ -+-export([get/1, get/2, get/3]). -+-export([put/2, put/3]). -+-export([options/1, options/2, options/3]). -+-export([request/3, request/4]). -+ -+get(Url) -> -+ request(get, Url, []). -+ -+get(Url, Headers) -> -+ request(get, Url, Headers). -+get(Url, Headers, Opts) -> -+ request(get, Url, Headers, [], Opts). -+ -+ -+put(Url, Body) -> -+ request(put, Url, [], Body). -+ -+put(Url, Headers, Body) -> -+ request(put, Url, Headers, Body). -+ -+ -+options(Url) -> -+ request(options, Url, []). -+ -+options(Url, Headers) -> -+ request(options, Url, Headers). -+ -+options(Url, Headers, Opts) -> -+ request(options, Url, Headers, [], Opts). -+ -+ -+request(Method, Url, Headers) -> -+ request(Method, Url, Headers, []). -+ -+request(Method, Url, Headers, Body) -> -+ request(Method, Url, Headers, Body, [], 3). -+ -+request(Method, Url, Headers, Body, Opts) -> -+ request(Method, Url, Headers, Body, Opts, 3). -+ -+request(_Method, _Url, _Headers, _Body, _Opts, 0) -> -+ {error, request_failed}; -+request(Method, Url, Headers, Body, Opts, N) -> -+ case code:is_loaded(ibrowse) of -+ false -> -+ {ok, _} = ibrowse:start(); -+ _ -> -+ ok -+ end, -+ case ibrowse:send_req(Url, Headers, Method, Body, Opts) of -+ {ok, Code0, RespHeaders, RespBody0} -> -+ Code = list_to_integer(Code0), -+ RespBody = iolist_to_binary(RespBody0), -+ {ok, Code, RespHeaders, RespBody}; -+ {error, {'EXIT', {normal, _}}} -> -+ % Connection closed right after a successful request that -+ % used the same connection. -+ request(Method, Url, Headers, Body, N - 1); -+ Error -> -+ Error -+ end. -diff --git a/test/couchdb/test_web.erl b/test/couchdb/test_web.erl -new file mode 100644 -index 0000000..1de2cd1 ---- /dev/null -+++ b/test/couchdb/test_web.erl -@@ -0,0 +1,112 @@ -+% 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). -+ -+-include("couch_eunit.hrl"). -+ -+-export([start_link/0, stop/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). -+-define(DELAY, 500). -+ -+start_link() -> -+ gen_server:start({local, ?HANDLER}, ?MODULE, [], []), -+ mochiweb_http:start([ -+ {name, ?SERVER}, -+ {loop, {?MODULE, loop}}, -+ {port, 0} -+ ]). -+ -+loop(Req) -> -+ %?debugFmt("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(?DELAY), -+ lists:foreach(fun(C) -> Resp:write_chunk(C) end, BodyChunks), -+ Resp:write_chunk([]), -+ {ok, Resp}; -+ {error, Reason} -> -+ ?debugFmt("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) -> -+ ?assertEqual(ok, gen_server:call(?HANDLER, {set_assert, Fun})). -+ -+check_last() -> -+ gen_server:call(?HANDLER, last_status). -+ -+init(_) -> -+ {ok, nil}. -+ -+terminate(_Reason, _State) -> -+ ok. -+ -+stop() -> -+ gen_server:cast(?SERVER, stop). -+ -+ -+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(stop, State) -> -+ {stop, normal, State}; -+handle_cast(Msg, State) -> -+ ?debugFmt("Ignoring cast message: ~p", [Msg]), -+ {noreply, State}. -+ -+handle_info(Msg, State) -> -+ ?debugFmt("Ignoring info message: ~p", [Msg]), -+ {noreply, State}. -+ -+code_change(_OldVsn, State, _Extra) -> -+ {ok, State}. -diff --git a/test/etap/run.tpl b/test/etap/run.tpl -deleted file mode 100644 -index d6d6dbe..0000000 ---- a/test/etap/run.tpl -+++ /dev/null -@@ -1,32 +0,0 @@ --#!/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%" --BUILDDIR="%abs_top_builddir%" --export ERL_LIBS="$BUILDDIR/src/:$ERL_LIBS" --export ERL_FLAGS="$ERL_FLAGS -pa $BUILDDIR/test/etap/" -- --if test $# -eq 1; then -- OPTS="" -- TGT=$1 --else -- OPTS=$1 -- TGT=$2 --fi -- --if test -f $TGT; then -- prove $OPTS $TGT --else -- prove $OPTS $TGT/*.t --fi diff --git a/couchdb.init b/couchdb.init deleted file mode 100644 index cc9664c..0000000 --- a/couchdb.init +++ /dev/null @@ -1,115 +0,0 @@ -#!/bin/sh -# -# couchdb This is the init script for starting up the CouchDB server -# -# chkconfig: - 26 74 -# description: Starts and stops the CouchDB daemon that handles \ -# all database requests. - -### BEGIN INIT INFO -# Provides: couchdb -# Required-Start: $local_fs $network -# Required-Stop: $local_fs $network -# Should-Start: $remote_fs -# Should-Stop: $remote_fs -# Default-Start: -# Default-Stop: 0 1 2 3 4 5 6 -# Short-Description: start and stop CouchDB database server -# Description: Apache CouchDB is a distributed, fault-tolerant and -# schema-free document-oriented database accessible -# via a RESTful HTTP/JSON API -### END INIT INFO - -# Source function library. -. /etc/rc.d/init.d/functions - -prog=couchdb -exec=/usr/bin/$prog - -# default values -COUCHDB_USER=couchdb -COUCHDB_STDOUT_FILE=/dev/null -COUCHDB_STDERR_FILE=/dev/null -COUCHDB_RESPAWN_TIMEOUT=0 -COUCHDB_OPTIONS= - -[ -e /etc/sysconfig/$prog ] && . /etc/sysconfig/$prog - -pidfile=/var/run/$prog/$prog.pid -lockfile=/var/lock/subsys/$prog - -start() { - [ -x $exec ] || exit 5 - echo -n $"Starting $prog: " - rh_status_q && echo -n "already running" && warning && echo && exit 0 - daemon --user $COUCHDB_USER "$exec \ - -r $COUCHDB_RESPAWN_TIMEOUT \ - -o $COUCHDB_STDOUT_FILE \ - -e $COUCHDB_STDERR_FILE \ - -p $pidfile \ - $COUCHDB_OPTIONS -b >/dev/null" - retval=$? - echo - [ $retval -eq 0 ] && touch $lockfile - return $retval -} - -stop() { - echo -n $"Stopping $prog: " - retval=0 - if ! rh_status_q ; then - echo -n "already stopped" && warning - else - daemon --user $COUCHDB_USER "$exec -d > /dev/null" - retval=$? - fi - echo - [ $retval -eq 0 ] && rm -f $lockfile - return $retval -} - -restart() { - stop - start -} - -reload() { - restart -} - -rh_status() { - # run checks to determine if the service is running or use generic status - status -p $pidfile $prog -} - -rh_status_q() { - rh_status >/dev/null 2>&1 -} - - -case "$1" in - start) - $1 - ;; - stop) - $1 - ;; - restart|force-reload) - restart - ;; - reload) - rh_status_q || exit 7 - $1 - ;; - status) - rh_status - ;; - condrestart|try-restart) - rh_status_q || exit 0 - restart - ;; - *) - echo $"Usage: $0 {start|stop|status|restart|condrestart|try-restart|reload|force-reload}" - exit 2 -esac -exit $? diff --git a/couchdb.service b/couchdb.service deleted file mode 100644 index 9e37bae..0000000 --- a/couchdb.service +++ /dev/null @@ -1,18 +0,0 @@ -[Unit] -Description=CouchDB Server -After=network.target - -[Service] -User=couchdb -Group=couchdb -Type=notify -StandardOutput=journal -StandardError=journal -Restart=always -StartLimitInterval=10 -StartLimitBurst=5 -PIDFile=/var/run/couchdb/couchdb.pid -ExecStart=/usr/libexec/couchdb +Bd -noinput -sasl errlog_type error +K true +A 4 -couch_ini /etc/couchdb/default.ini /etc/couchdb/default.d/ /etc/couchdb/local.d/ /etc/couchdb/local.ini -s couch -pidfile /var/run/couchdb/couchdb.pid - -[Install] -WantedBy=multi-user.target diff --git a/couchdb.spec b/couchdb.spec deleted file mode 100644 index 6aa6234..0000000 --- a/couchdb.spec +++ /dev/null @@ -1,512 +0,0 @@ -%{?filter_setup: -%filter_provides_in %{_libdir}/erlang/lib/.*\.so$ -%filter_setup -} -%{expand: %(NIF_VER=`rpm -q erlang-erts --provides | grep --color=no erl_nif_version` ; if [ "$NIF_VER" != "" ]; then echo %%global __erlang_nif_version $NIF_VER ; fi)} -%{expand: %(DRV_VER=`rpm -q erlang-erts --provides | grep --color=no erl_drv_version` ; if [ "$DRV_VER" != "" ]; then echo %%global __erlang_drv_version $DRV_VER ; fi)} - -Name: couchdb -Version: 1.6.1 -Release: 1%{?dist} -Summary: A document database server, accessible via a RESTful JSON API - -Group: Applications/Databases -License: ASL 2.0 -URL: http://couchdb.apache.org/ -Source0: http://www.apache.org/dist/%{name}/source/%{version}/apache-%{name}-%{version}.tar.gz -Source1: http://www.apache.org/dist/%{name}/source/%{version}/apache-%{name}-%{version}.tar.gz.asc -Source2: %{name}.init -Source3: %{name}.service -Source4: %{name}.tmpfiles.conf -Source5: %{name}.temporary.sh -# FIXME remove as soon as eunit tests will be merged upstream -Source6: %{name}-tests-blobs.tar -Patch1: couchdb-0001-Do-not-gzip-doc-files-and-do-not-install-installatio.patch -Patch2: couchdb-0002-More-directories-to-search-for-place-for-init-script.patch -Patch3: couchdb-0003-Install-into-erllibdir-by-default.patch -Patch4: couchdb-0004-Don-t-use-bundled-libraries.patch -Patch5: couchdb-0005-Fixes-for-system-wide-ibrowse.patch -Patch6: couchdb-0006-Remove-pid-file-after-stop.patch -Patch7: couchdb-0007-Change-respawn-timeout-to-0.patch -Patch8: couchdb-0008-Fix-for-Erlang-R16B01.patch -Patch9: couchdb-0009-README-was-renamed.patch -Patch10: couchdb-0010-Use-_DEFAULT_SOURCE-instead-of-obsolete-_BSD_SOURCE.patch -Patch11: couchdb-0011-Silence-redundant-logging-to-stdout-stderr.patch -Patch12: couchdb-0012-Expand-.d-directories-in-erlang.patch -Patch13: couchdb-0013-Add-systemd-notification-support.patch -Patch14: couchdb-0014-Add-run-script-to-execute-eunit-tests.patch - -BuildRequires: autoconf -BuildRequires: autoconf-archive -BuildRequires: automake -BuildRequires: libtool -BuildRequires: curl-devel >= 7.18.0 -BuildRequires: erlang-erts >= R13B -BuildRequires: erlang-eunit >= R15B -BuildRequires: erlang-ibrowse >= 4.0.1 -BuildRequires: erlang-mochiweb -BuildRequires: erlang-oauth >= 1.3.0 -BuildRequires: erlang-os_mon -BuildRequires: erlang-snappy -BuildRequires: help2man -BuildRequires: js-devel -BuildRequires: libicu-devel - -Requires: erlang-crypto%{?_isa} -# Error:erlang(erlang:max/2) in R12B and earlier -# Error:erlang(erlang:min/2) in R12B and earlier -Requires: erlang-erts%{?_isa} >= R13B -Requires: erlang-ibrowse%{?_isa} >= 4.0.1 -Requires: erlang-inets%{?_isa} -Requires: erlang-kernel%{?_isa} -Requires: erlang-mochiweb%{?_isa} -Requires: erlang-oauth%{?_isa} -Requires: erlang-os_mon%{?_isa} -Requires: erlang-sd_notify%{?_isa} -Requires: erlang-snappy%{?_isa} -Requires: erlang-ssl%{?_isa} -# Error:erlang(unicode:characters_to_binary/1) in R12B and earlier -Requires: erlang-stdlib%{?_isa} >= R13B -Requires: erlang-tools%{?_isa} -Requires: erlang-xmerl%{?_isa} - -%if 0%{?el5}%{?el6} -#Initscripts -Requires(post): chkconfig -Requires(preun): chkconfig initscripts -%else -Requires(pre): systemd -Requires(post): systemd -Requires(preun): systemd -%endif - -# Users and groups -Requires(pre): shadow-utils - -%{?__erlang_nif_version:Requires: %{__erlang_nif_version}} -%{?__erlang_drv_version:Requires: %{__erlang_drv_version}} - -%description -Apache CouchDB is a distributed, fault-tolerant and schema-free -document-oriented database accessible via a RESTful HTTP/JSON API. -Among other features, it provides robust, incremental replication -with bi-directional conflict detection and resolution, and is -queryable and indexable using a table-oriented view engine with -JavaScript acting as the default view definition language. - - -%prep -%setup -q -n apache-%{name}-%{version} -%patch1 -p1 -b .dont_gzip -%patch2 -p1 -b .more_init_dirs -%patch3 -p1 -b .install_into_erldir -%patch4 -p1 -b .remove_bundled_libs -%patch5 -p1 -b .workaround_for_system_wide_ibrowse -%patch6 -p1 -b .remove_pid_file -%patch7 -p1 -b .fix_respawn -%if 0%{?fedora}%{?el7} -%patch8 -p1 -b .r16b01 -%endif -%patch9 -p1 -b .renamed -%if 0%{?fedora} > 20 -# Workaround hard-coded Makefile.am assumptions -%patch10 -p1 -b .default_instead_of_bsd -%endif -%patch11 -p1 -b .redundant_logging -%patch12 -p1 -b .expands_d -%patch13 -p1 -b .sd_notify -%patch14 -p1 -b .no_etap -tar xvf %{SOURCE6} - -#gzip -d -k ./share/doc/build/latex/CouchDB.pdf.gz - -# Remove bundled libraries -rm -rf src/erlang-oauth -rm -rf src/ibrowse -rm -rf src/mochiweb -rm -rf src/snappy -rm -rf src/etap -rm -rf test/etap - -# FIXME remove as soon as eunit tests will be merged upstream -chmod +x test/couchdb/fixtures/os_daemon_looper.escript -chmod +x test/couchdb/fixtures/*.sh -# This is intentional - this daemon shouldn't start -chmod -x test/couchdb/fixtures/os_daemon_bad_perm.sh - - -%build -autoreconf -ivf -%configure --with-erlang=%{_libdir}/erlang/usr/include -make %{?_smp_mflags} - - -%install -make install DESTDIR=%{buildroot} - -%if 0%{?el5}%{?el6} -# Use /etc/sysconfig instead of /etc/default -mv %{buildroot}%{_sysconfdir}/{default,sysconfig} -# Install our custom couchdb initscript -install -D -m 755 %{SOURCE2} %{buildroot}%{_initrddir}/%{name} -%else -# Install /etc/tmpfiles.d entry -install -D -m 644 %{SOURCE4} %{buildroot}%{_tmpfilesdir}/%{name}.conf -# Install systemd entry -install -D -m 755 %{SOURCE3} %{buildroot}%{_unitdir}/%{name}.service -rm -rf %{buildroot}%{_sysconfdir}/rc.d/ -rm -rf %{buildroot}%{_sysconfdir}/default/ -# Temporary systemd + selinux wrapper -# This makes the service run in couchdb_t -install -D -m 755 %{SOURCE5} %{buildroot}%{_libexecdir}/%{name} -%endif - -# Remove *.la files -find %{buildroot} -type f -name "*.la" -delete - -# Remove installed docs (this will mess with versione/unversioned docdirs) -rm -rf %{buildroot}%{_defaultdocdir} - -# Remove unneeded info-files -rm -rf %{buildroot}%{_datadir}/info/ - - -%check -make check-eunit - - -%pre -getent group %{name} >/dev/null || groupadd -r %{name} -getent passwd %{name} >/dev/null || \ -useradd -r -g %{name} -d %{_localstatedir}/lib/%{name} -s /bin/bash \ --c "Couchdb Database Server" %{name} -exit 0 - - -%post -%if 0%{?el5}%{?el6} -/sbin/chkconfig --add %{name} -%else -%systemd_post %{name}.service -%endif - -%preun -%if 0%{?el5}%{?el6} -if [ $1 = 0 ] ; then - /sbin/service %{name} stop >/dev/null 2>&1 - /sbin/chkconfig --del %{name} -fi -%else -%systemd_preun %{name}.service -%endif - - -%postun -%if 0%{?el7}%{?fedora} -%systemd_postun_with_restart %{name}.service -%endif - - -%files -%doc AUTHORS BUGS LICENSE NOTICE README.rst THANKS -%dir %{_sysconfdir}/%{name} -%dir %{_sysconfdir}/%{name}/local.d -%dir %{_sysconfdir}/%{name}/default.d -%config %attr(0644, %{name}, %{name}) %{_sysconfdir}/%{name}/default.ini -%config(noreplace) %attr(0644, %{name}, %{name}) %{_sysconfdir}/%{name}/local.ini -%config(noreplace) %{_sysconfdir}/logrotate.d/%{name} -%if 0%{?el7}%{?fedora} -%{_tmpfilesdir}/%{name}.conf -%{_unitdir}/%{name}.service -%else -%config(noreplace) %{_sysconfdir}/sysconfig/%{name} -%{_initrddir}/%{name} -%endif -%{_bindir}/%{name} -%{_bindir}/couch-config -%{_bindir}/couchjs -%{_libdir}/erlang/lib/couch-%{version}/ -%{_libdir}/erlang/lib/couch_dbupdates-0.1/ -%{_libdir}/erlang/lib/couch_index-0.1/ -%{_libdir}/erlang/lib/couch_mrview-0.1/ -%{_libdir}/erlang/lib/couch_plugins-0.1/ -%{_libdir}/erlang/lib/couch_replicator-0.1/ -%{_libdir}/erlang/lib/ejson-0.1.0/ -%{_libexecdir}/%{name} -%{_datadir}/%{name} -%{_mandir}/man1/%{name}.1.* -%{_mandir}/man1/couchjs.1.* -%dir %attr(0755, %{name}, %{name}) %{_localstatedir}/log/%{name} -%dir %attr(0755, %{name}, %{name}) %{_localstatedir}/run/%{name} -%dir %attr(0755, %{name}, %{name}) %{_localstatedir}/lib/%{name} - - -%changelog -* Sun Sep 07 2014 Peter Lemenkov - 1.6.1-1 -- Ver. 1.6.1 - -* Fri Aug 29 2014 Peter Lemenkov - 1.6.0-13 -- Kill fragile etap tests in favor of eunit-based test-suite - -* Thu Aug 28 2014 Peter Lemenkov - 1.6.0-12 -- Rebuild with Erlang 17.2.1 - -* Tue Aug 26 2014 David Tardon - 1.6.0-11 -- rebuild for ICU 53.1 - -* Sat Aug 16 2014 Fedora Release Engineering - 1.6.0-10 -- Rebuilt for https://fedoraproject.org/wiki/Fedora_21_22_Mass_Rebuild - -* Wed Jul 09 2014 Warren Togami - 1.6.0-9 -- Add systemd notify support - -* Sun Jul 06 2014 Warren Togami - 1.6.0-8 -- SELinux: Use /usr/libexec/couchdb wrapper for systemd ExecStart, executes as couchdb_t - Additional fixes to selinux-policy are required, - see latest status http://wtogami.fedorapeople.org/a/2014/couchdb.txt -- Remove -heart from ExecStart, systemd handles service runtime -- default.ini contains default configuration from upstream. - It has previously warned users to not modify it as it will be overwritten on package upgrade. - Now package upgrades really will overwrite default.ini. -- Configuration is read during CouchDB startup in this order: - default.ini -> default.d/*.ini -> local.d/*.ini -> local.ini - Other packages are meant to drop configuration into default.d/ - Users can modify local.ini or add new files in local.d/ -- CouchDB runtime config changes are written to local.ini - -* Thu Jul 03 2014 Warren Togami - 1.6.0-6 -- silence stdout/stderr to prevent redundant flooding of /var/log/messages - CouchDB already logs these messages to /var/log/couchdb/couch.log - Instead print the log filename to stdout, in case a user who ran it - from the CLI is confused about where the messages went. -- -couch_ini accepts .ini or a .d/ directory. For directories it reads - any *.ini file. Fixes #1002277. - -* Mon Jun 23 2014 Peter Lemenkov - 1.6.0-2 -- Fix building with sligntly older gcc/glibc - -* Sun Jun 22 2014 Peter Lemenkov - 1.6.0-1 -- Ver. 1.6.0 - -* Sat Jun 07 2014 Fedora Release Engineering - 1.5.0-3 -- Rebuilt for https://fedoraproject.org/wiki/Fedora_21_Mass_Rebuild - -* Fri Feb 14 2014 David Tardon - 1.5.0-2 -- rebuild for new ICU - -* Fri Jan 10 2014 Peter Lemenkov - 1.5.0-1 -- Ver. 1.5.0 - -* Fri Oct 25 2013 Peter Lemenkov - 1.3.1-3 -- Rebuild with new requires - __erlang_nif_version, __erlang_drv_version - -* Fri Sep 06 2013 Peter Lemenkov - 1.3.1-2 -- Moved tmpfiles entry to /usr - -* Sun Aug 25 2013 Peter Lemenkov - 1.3.1-1 -- Ver. 1.3.1 - -* Sat Aug 03 2013 Fedora Release Engineering - 1.2.2-4 -- Rebuilt for https://fedoraproject.org/wiki/Fedora_20_Mass_Rebuild - -* Fri Jun 21 2013 Peter Lemenkov - 1.2.2-3 -- Fix for R16B01 ( https://issues.apache.org/jira/browse/COUCHDB-1833 ) - -* Fri May 31 2013 Peter Lemenkov - 1.2.2-2 -- Fix for R16B and latest mochiweb - -* Mon Apr 15 2013 Peter Lemenkov - 1.2.2-1 -- Ver. 1.2.2 (bugfix release) - -* Fri Mar 15 2013 Peter Lemenkov - 1.2.1-4 -- Fix FTBFS in Rawhide (F-19) - -* Fri Feb 08 2013 Jon Ciesla - 1.2.1-3 -- libicu rebuild. - -* Tue Jan 22 2013 Peter Lemenkov - 1.2.1-2 -- Revert systemd-macros - -* Mon Jan 21 2013 Peter Lemenkov - 1.2.1-1 -- Ver. 1.2.1 (security bugfix release) -- Introduce handy systemd-related macros (see rhbz #850069) - -* Tue Oct 30 2012 Peter Lemenkov - 1.2.0-3 -- Unbundle snappy (see rhbz #871149) -- Add _isa to the Requires - -* Mon Sep 24 2012 Peter Lemenkov - 1.2.0-2 -- Build fixes -- Temporarily disable verbosity - -* Mon Sep 24 2012 Peter Lemenkov - 1.2.0-1 -- Ver. 1.2.0 - -* Mon Sep 24 2012 Peter Lemenkov - 1.1.1-4.1 -- Rebuild - -* Wed Jul 18 2012 Fedora Release Engineering - 1.1.1-4 -- Rebuilt for https://fedoraproject.org/wiki/Fedora_18_Mass_Rebuild - -* Wed Jul 04 2012 Peter Lemenkov - 1.1.1-3 -- Improve systemd support - -* Wed May 16 2012 Peter Lemenkov - 1.1.1-2 -- Updated systemd files (added EnvironmentFile option) - -* Sun Mar 11 2012 Peter Lemenkov - 1.1.1-1 -- Ver. 1.1.1 - -* Sun Mar 11 2012 Peter Lemenkov - 1.0.3-6 -- Fix building on f18 - -* Wed Feb 15 2012 Jon Ciesla - 1.0.3-5 -- Migrate to systemd, BZ 771434. - -* Thu Jan 12 2012 Fedora Release Engineering - 1.0.3-4 -- Rebuilt for https://fedoraproject.org/wiki/Fedora_17_Mass_Rebuild - -* Mon Sep 19 2011 Peter Lemenkov - 1.0.3-3 -- Rebuilt with new libicu - -* Mon Aug 15 2011 Kalev Lember - 1.0.3-2 -- Rebuilt for rpm bug #728707 - -* Thu Jul 21 2011 Peter Lemenkov - 1.0.3-1 -- Ver. 1.0.3 - -* Tue Jul 12 2011 Peter Lemenkov - 1.0.2-8 -- Build for EL-5 (see patch99 - quite ugly, I know) - -* Sat Jun 18 2011 Peter Lemenkov - 1.0.2-7 -- Requires ibrowse >= 2.2.0 for building -- Fixes for /var/run mounted as tmpfs (see rhbz #656565, #712681) - -* Mon May 30 2011 Peter Lemenkov - 1.0.2-6 -- Patched patch for new js-1.8.5 - -* Fri May 20 2011 Peter Lemenkov - 1.0.2-5 -- Fixed issue with ibrowse-2.2.0 - -* Thu May 19 2011 Peter Lemenkov - 1.0.2-4 -- Fixed issue with R14B02 - -* Thu May 5 2011 Jan Horak - 1.0.2-3 -- Added Spidermonkey 1.8.5 patch - -* Mon Mar 07 2011 Caolán McNamara 1.0.2-2 -- rebuild for icu 4.6 - -* Thu Nov 25 2010 Peter Lemenkov 1.0.2-1 -- Ver. 1.0.2 -- Patches were rebased - -* Tue Oct 12 2010 Peter Lemenkov 1.0.1-4 -- Added patches for compatibility with R12B5 - -* Mon Oct 11 2010 Peter Lemenkov 1.0.1-3 -- Narrowed list of BuildRequires - -* Thu Aug 26 2010 Peter Lemenkov 1.0.1-2 -- Cleaned up spec-file a bit - -* Fri Aug 6 2010 Peter Lemenkov 1.0.1-1 -- Ver. 1.0.1 - -* Thu Jul 15 2010 Peter Lemenkov 1.0.0-1 -- Ver. 1.0.0 - -* Wed Jul 14 2010 Peter Lemenkov 0.11.1-1 -- Ver. 0.11.1 -- Removed patch for compatibility with Erlang/OTP R14A (merged upstream) - -* Sun Jul 11 2010 Peter Lemenkov 0.11.0-3 -- Compatibility with Erlang R14A (see patch9) - -* Tue Jun 22 2010 Peter Lemenkov 0.11.0-2 -- Massive spec cleanup - -* Tue Jun 22 2010 Peter Lemenkov 0.11.0-1 -- Ver. 0.11.0 (a feature-freeze release candidate) - -* Fri Jun 18 2010 Peter Lemenkov 0.10.2-13 -- Remove ldconfig invocation (no system-wide shared libraries) -- Removed icu-config requires - -* Tue Jun 15 2010 Peter Lemenkov 0.10.2-12 -- Narrow explicit requires - -* Tue Jun 8 2010 Peter Lemenkov 0.10.2-11 -- Remove bundled ibrowse library (see rhbz #581282). - -* Mon Jun 7 2010 Peter Lemenkov 0.10.2-10 -- Use system-wide erlang-mochiweb instead of bundled copy (rhbz #581284) -- Added %%check target and necessary BuildRequires - etap, oauth, mochiweb - -* Wed Jun 2 2010 Peter Lemenkov 0.10.2-9 -- Remove pid-file after stopping CouchDB - -* Tue Jun 1 2010 Peter Lemenkov 0.10.2-8 -- Suppress unneeded message while stopping CouchDB via init-script - -* Mon May 31 2010 Peter Lemenkov 0.10.2-7 -- Do not manually remove pid-file while stopping CouchDB - -* Mon May 31 2010 Peter Lemenkov 0.10.2-6 -- Fix 'stop' and 'status' targets in the init-script (see rhbz #591026) - -* Thu May 27 2010 Peter Lemenkov 0.10.2-5 -- Use system-wide erlang-etap instead of bundled copy (rhbz #581281) - -* Fri May 14 2010 Peter Lemenkov 0.10.2-4 -- Use system-wide erlang-oauth instead of bundled copy (rhbz #581283) - -* Thu May 13 2010 Peter Lemenkov 0.10.2-3 -- Fixed init-script to use /etc/sysconfig/couchdb values (see rhbz #583004) -- Fixed installation location of beam-files (moved to erlang directory) - -* Fri May 7 2010 Peter Lemenkov 0.10.2-2 -- Remove useless BuildRequires - -* Fri May 7 2010 Peter Lemenkov 0.10.2-1 -- Update to 0.10.2 (resolves rhbz #578580 and #572176) -- Fixed chkconfig priority (see rhbz #579568) - -* Fri Apr 02 2010 Caolán McNamara 0.10.0-3 -- rebuild for icu 4.4 - -* Thu Oct 15 2009 Allisson Azevedo 0.10.0-2 -- Added patch to force init_enabled=true in configure.ac. - -* Thu Oct 15 2009 Allisson Azevedo 0.10.0-1 -- Update to 0.10.0. - -* Sun Oct 04 2009 Rahul Sundaram 0.9.1-2 -- Change url. Fixes rhbz#525949 - -* Thu Jul 30 2009 Allisson Azevedo 0.9.1-1 -- Update to 0.9.1. -- Drop couchdb-0.9.0-pid.patch. - -* Fri Jul 24 2009 Fedora Release Engineering - 0.9.0-3 -- Rebuilt for https://fedoraproject.org/wiki/Fedora_12_Mass_Rebuild - -* Tue Apr 21 2009 Allisson Azevedo 0.9.0-2 -- Fix permission for ini files. -- Fix couchdb.init start process. - -* Tue Apr 21 2009 Allisson Azevedo 0.9.0-1 -- Update to 0.9.0. - -* Tue Nov 25 2008 Allisson Azevedo 0.8.1-4 -- Use /etc/sysconfig for settings. - -* Tue Nov 25 2008 Allisson Azevedo 0.8.1-3 -- Fix couchdb_home. -- Added libicu-devel for requires. - -* Tue Nov 25 2008 Allisson Azevedo 0.8.1-2 -- Fix spec issues. - -* Tue Nov 25 2008 Allisson Azevedo 0.8.1-1 -- Initial RPM release diff --git a/couchdb.temporary.sh b/couchdb.temporary.sh deleted file mode 100755 index 2c7ac73..0000000 --- a/couchdb.temporary.sh +++ /dev/null @@ -1,6 +0,0 @@ -#!/bin/sh -# Temporary Launcher for systemd + selinux -# CouchDB upstream at version 2.0 will replace /bin/couchdb with a bare launcher -# At that point we can get rid of this dumb wrapper. - -exec /usr/bin/erl $@ diff --git a/couchdb.tmpfiles.conf b/couchdb.tmpfiles.conf deleted file mode 100644 index b8d7201..0000000 --- a/couchdb.tmpfiles.conf +++ /dev/null @@ -1 +0,0 @@ -d /var/run/couchdb 0755 couchdb couchdb diff --git a/dead.package b/dead.package new file mode 100644 index 0000000..f9a9536 --- /dev/null +++ b/dead.package @@ -0,0 +1,3 @@ +2015-10-28: Retired because it depends on erlang-mochiweb, which was +retired, because it was orphaned for more than six weeks. + diff --git a/sources b/sources deleted file mode 100644 index a61b857..0000000 --- a/sources +++ /dev/null @@ -1,2 +0,0 @@ -01a2c8ab4fcde457529428993901a060 apache-couchdb-1.6.1.tar.gz -b590941d0eb4230317c119fa79fbfa87 couchdb-tests-blobs.tar