diff options
author | Tony Sun <tony.sun427@gmail.com> | 2017-10-25 11:08:45 -0700 |
---|---|---|
committer | GitHub <noreply@github.com> | 2017-10-25 11:08:45 -0700 |
commit | 53d33e593d6b64e3ddb9251df5e01efa36f7c8da (patch) | |
tree | b3630d7a57ceddc29156ef055ecef26c9a0926bf | |
parent | 54ac3adaabbac689cf344dfc9c53abd137c8ecbb (diff) | |
parent | 3ae2e5c94d8c275c50274809cb52eeffadb2b6b6 (diff) | |
download | couchdb-flexible-js-tests.tar.gz |
Merge branch 'master' into flexible-js-testsflexible-js-tests
27 files changed, 581 insertions, 230 deletions
diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index f364446c3..a64a17e7b 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -22,17 +22,10 @@ Does it provides any behaviour that the end users could notice? --> -## GitHub issue number - -<!-- If this is a significant change, please file a separate issue at: - https://github.com/apache/couchdb/issues - and include the number here and in commit message(s) using - syntax like "Fixes #472" or "Fixes apache/couchdb#472". --> - -## Related Pull Requests +## Related Issues or Pull Requests <!-- If your changes affects multiple components in different - repositories please put links to those pull requests here. --> + repositories please put links to those issues or pull requests here. --> ## Checklist diff --git a/INSTALL.Unix.md b/INSTALL.Unix.md index 3ed2091f0..eb7e72c76 100644 --- a/INSTALL.Unix.md +++ b/INSTALL.Unix.md @@ -135,6 +135,11 @@ You can install this by running: pkg install gmake +You can install the remaining dependencies by running: + + pkg install npm4 help2man openssl icu curl git \ + autoconf automake libtool node spidermonkey185 + ## Installing Once you have satisfied the dependencies you should run: diff --git a/Jenkinsfile b/Jenkinsfile index d5212279e..fed976afc 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -182,11 +182,11 @@ pipeline { deleteDir() } // node }, - ubuntu1204erlang183: { + ubuntu1404erlangdefault: { node(label: 'couchdbtest') { timeout(time: 45, unit: "MINUTES") { - sh 'docker pull couchdbdev/ubuntu-12.04-erlang-18.3' - withDockerContainer(image: 'couchdbdev/ubuntu-12.04-erlang-18.3') { + sh 'docker pull couchdbdev/ubuntu-14.04-erlang-default' + withDockerContainer(image: 'couchdbdev/ubuntu-14.04-erlang-default') { sh 'rm -f apache-couchdb-*.tar.gz' unstash 'tarball' sh ''' @@ -205,11 +205,51 @@ pipeline { deleteDir() } // node }, - ubuntu1404erlangdefault: { + ubuntu1404erlang183: { + node(label: 'couchdbtest') { + timeout(time: 60, unit: "MINUTES") { + sh 'docker pull couchdbdev/ubuntu-14.04-erlang-18.3' + withDockerContainer(image: 'couchdbdev/ubuntu-14.04-erlang-18.3') { + sh 'rm -f apache-couchdb-*.tar.gz' + unstash 'tarball' + sh ''' + cwd=$(pwd) + mkdir -p $COUCHDB_IO_LOG_DIR + + # Build CouchDB from tarball + builddir=$(mktemp -d) + cd $builddir + tar -xf $cwd/apache-couchdb-*.tar.gz + cd apache-couchdb-* + ./configure --with-curl + make all + make check || (build-aux/logfile-uploader.py && false) + + # Build CouchDB packages + cd $builddir + git clone https://github.com/apache/couchdb-pkg + mkdir couchdb + cp $cwd/apache-couchdb-*.tar.gz couchdb + tar -xf $cwd/apache-couchdb-*.tar.gz -C couchdb + cd couchdb-pkg + platform=$(lsb_release -cs) + make $platform PLATFORM=$platform + + # Cleanup & save for posterity + rm -rf $cwd/pkgs/$platform && mkdir -p $cwd/pkgs/$platform + mv ../couchdb/*deb $cwd/pkgs/$platform || true + ''' + } // withDocker + } // timeout + archiveArtifacts artifacts: 'pkgs/**', fingerprint: true + deleteDir() + } // node + }, + ubuntu1604erlangdefault: { node(label: 'couchdbtest') { timeout(time: 45, unit: "MINUTES") { - sh 'docker pull couchdbdev/ubuntu-14.04-erlang-default' - withDockerContainer(image: 'couchdbdev/ubuntu-14.04-erlang-default') { + sh 'docker pull couchdbdev/ubuntu-16.04-erlang-default' + withDockerContainer(image: 'couchdbdev/ubuntu-16.04-erlang-default') { sh 'rm -f apache-couchdb-*.tar.gz' unstash 'tarball' sh ''' @@ -228,11 +268,11 @@ pipeline { deleteDir() } // node }, - ubuntu1404erlang183: { + ubuntu1604erlang183: { node(label: 'couchdbtest') { timeout(time: 60, unit: "MINUTES") { - sh 'docker pull couchdbdev/ubuntu-14.04-erlang-18.3' - withDockerContainer(image: 'couchdbdev/ubuntu-14.04-erlang-18.3') { + sh 'docker pull couchdbdev/ubuntu-16.04-erlang-18.3' + withDockerContainer(image: 'couchdbdev/ubuntu-16.04-erlang-18.3') { sh 'rm -f apache-couchdb-*.tar.gz' unstash 'tarball' sh ''' @@ -268,11 +308,11 @@ pipeline { deleteDir() } // node }, - ubuntu1604erlangdefault: { + debian8erlangdefault: { node(label: 'couchdbtest') { timeout(time: 45, unit: "MINUTES") { - sh 'docker pull couchdbdev/ubuntu-16.04-erlang-default' - withDockerContainer(image: 'couchdbdev/ubuntu-16.04-erlang-default') { + sh 'docker pull couchdbdev/debian-8-erlang-default' + withDockerContainer(image: 'couchdbdev/debian-8-erlang-default') { sh 'rm -f apache-couchdb-*.tar.gz' unstash 'tarball' sh ''' @@ -291,11 +331,11 @@ pipeline { deleteDir() } // node }, - ubuntu1604erlang183: { + debian8erlang183: { node(label: 'couchdbtest') { timeout(time: 60, unit: "MINUTES") { - sh 'docker pull couchdbdev/ubuntu-16.04-erlang-18.3' - withDockerContainer(image: 'couchdbdev/ubuntu-16.04-erlang-18.3') { + sh 'docker pull couchdbdev/debian-8-erlang-18.3' + withDockerContainer(image: 'couchdbdev/debian-8-erlang-18.3') { sh 'rm -f apache-couchdb-*.tar.gz' unstash 'tarball' sh ''' @@ -331,11 +371,11 @@ pipeline { deleteDir() } // node }, - debian8erlangdefault: { + debian9erlangdefault: { node(label: 'couchdbtest') { timeout(time: 45, unit: "MINUTES") { - sh 'docker pull couchdbdev/debian-8-erlang-default' - withDockerContainer(image: 'couchdbdev/debian-8-erlang-default') { + sh 'docker pull couchdbdev/debian-9-erlang-default' + withDockerContainer(image: 'couchdbdev/debian-9-erlang-default') { sh 'rm -f apache-couchdb-*.tar.gz' unstash 'tarball' sh ''' @@ -354,11 +394,11 @@ pipeline { deleteDir() } // node }, - debian8erlang183: { + debian9erlang183: { node(label: 'couchdbtest') { timeout(time: 60, unit: "MINUTES") { - sh 'docker pull couchdbdev/debian-8-erlang-18.3' - withDockerContainer(image: 'couchdbdev/debian-8-erlang-18.3') { + sh 'docker pull couchdbdev/debian-9-erlang-18.3' + withDockerContainer(image: 'couchdbdev/debian-9-erlang-18.3') { sh 'rm -f apache-couchdb-*.tar.gz' unstash 'tarball' sh ''' @@ -429,6 +469,7 @@ pipeline { reprepro -b couchdb-pkg/repo includedeb jessie pkgs/jessie/*deb reprepro -b couchdb-pkg/repo includedeb trusty pkgs/trusty/*deb reprepro -b couchdb-pkg/repo includedeb xenial pkgs/xenial/*deb + reprepro -b couchdb-pkg/repo includedeb stretch pkgs/stretch/*deb ''' echo 'Building CentOS repos...' sh ''' diff --git a/src/couch/src/couch_httpd.erl b/src/couch/src/couch_httpd.erl index b3bbd5baa..1694ac87f 100644 --- a/src/couch/src/couch_httpd.erl +++ b/src/couch/src/couch_httpd.erl @@ -536,7 +536,7 @@ host_for_request(#httpd{mochi_req=MochiReq}) -> Value -> Value end. -absolute_uri(#httpd{mochi_req=MochiReq}=Req, Path) -> +absolute_uri(#httpd{mochi_req=MochiReq}=Req, [$/ | _] = Path) -> Host = host_for_request(Req), XSsl = config:get("httpd", "x_forwarded_ssl", "X-Forwarded-Ssl"), Scheme = case MochiReq:get_header_value(XSsl) of @@ -552,7 +552,9 @@ absolute_uri(#httpd{mochi_req=MochiReq}=Req, Path) -> end end end, - Scheme ++ "://" ++ Host ++ Path. + Scheme ++ "://" ++ Host ++ Path; +absolute_uri(_Req, _Path) -> + throw({bad_request, "path must begin with a /."}). unquote(UrlEncodedString) -> mochiweb_util:unquote(UrlEncodedString). diff --git a/src/ddoc_cache/src/ddoc_cache_lru.erl b/src/ddoc_cache/src/ddoc_cache_lru.erl index 810cbef63..e94934d04 100644 --- a/src/ddoc_cache/src/ddoc_cache_lru.erl +++ b/src/ddoc_cache/src/ddoc_cache_lru.erl @@ -39,6 +39,9 @@ -include("ddoc_cache.hrl"). +-define(OPENER, ddoc_cache_opener). + + -record(st, { pids, % pid -> key dbs, % dbname -> docid -> key -> pid @@ -143,11 +146,11 @@ handle_call(Msg, _From, St) -> handle_cast({evict, DbName}, St) -> - gen_server:abcast(mem3:nodes(), ?MODULE, {do_evict, DbName}), + gen_server:abcast(mem3:nodes(), ?OPENER, {do_evict, DbName}), {noreply, St}; handle_cast({refresh, DbName, DDocIds}, St) -> - gen_server:abcast(mem3:nodes(), ?MODULE, {do_refresh, DbName, DDocIds}), + gen_server:abcast(mem3:nodes(), ?OPENER, {do_evict, DbName, DDocIds}), {noreply, St}; handle_cast({do_evict, DbName}, St) -> diff --git a/src/mango/src/mango_cursor.erl b/src/mango/src/mango_cursor.erl index e0792b737..98b2d52bd 100644 --- a/src/mango/src/mango_cursor.erl +++ b/src/mango/src/mango_cursor.erl @@ -46,7 +46,7 @@ create(Db, Selector0, Opts) -> Selector = mango_selector:normalize(Selector0), - UsableIndexes = mango_idx:get_usable_indexes(Db, Selector0, Opts), + UsableIndexes = mango_idx:get_usable_indexes(Db, Selector, Opts), {use_index, IndexSpecified} = proplists:lookup(use_index, Opts), case {length(UsableIndexes), length(IndexSpecified)} of diff --git a/src/mango/src/mango_cursor_view.erl b/src/mango/src/mango_cursor_view.erl index 59dd52226..3fcec07be 100644 --- a/src/mango/src/mango_cursor_view.erl +++ b/src/mango/src/mango_cursor_view.erl @@ -29,7 +29,7 @@ -include_lib("couch/include/couch_db.hrl"). -include_lib("couch_mrview/include/couch_mrview.hrl"). -include("mango_cursor.hrl"). - +-include("mango_idx_view.hrl"). create(Db, Indexes, Selector, Opts) -> FieldRanges = mango_idx_view:field_ranges(Selector), @@ -61,18 +61,37 @@ explain(Cursor) -> BaseArgs = base_args(Cursor), Args = apply_opts(Opts, BaseArgs), + [{mrargs, {[ {include_docs, Args#mrargs.include_docs}, {view_type, Args#mrargs.view_type}, {reduce, Args#mrargs.reduce}, {start_key, Args#mrargs.start_key}, - {end_key, Args#mrargs.end_key}, + {end_key, maybe_replace_max_json(Args#mrargs.end_key)}, {direction, Args#mrargs.direction}, {stable, Args#mrargs.stable}, {update, Args#mrargs.update} ]}}]. +% replace internal values that cannot +% be represented as a valid UTF-8 string +% with a token for JSON serialization +maybe_replace_max_json([]) -> + []; + +maybe_replace_max_json(?MAX_STR) -> + <<"<MAX>">>; + +maybe_replace_max_json([H | T] = EndKey) when is_list(EndKey) -> + H1 = if H == ?MAX_JSON_OBJ -> <<"<MAX>">>; + true -> H + end, + [H1 | maybe_replace_max_json(T)]; + +maybe_replace_max_json(EndKey) -> + EndKey. + base_args(#cursor{index = Idx} = Cursor) -> #mrargs{ view_type = map, diff --git a/src/mango/src/mango_error.erl b/src/mango/src/mango_error.erl index 4f8ae204d..4c55ef3f6 100644 --- a/src/mango/src/mango_error.erl +++ b/src/mango/src/mango_error.erl @@ -43,7 +43,7 @@ info(mango_cursor, {no_usable_index, selector_unsupported}) -> { 400, <<"no_usable_index">>, - <<"There is no index available for this selector.">> + <<"The index specified with \"use_index\" is not usable for the query.">> }; info(mango_json_bookmark, {invalid_bookmark, BadBookmark}) -> diff --git a/src/mango/src/mango_idx.erl b/src/mango/src/mango_idx.erl index c5f870d5b..4dd2e180d 100644 --- a/src/mango/src/mango_idx.erl +++ b/src/mango/src/mango_idx.erl @@ -20,7 +20,6 @@ -export([ list/1, recover/1, - for_sort/2, new/2, validate_new/2, @@ -36,7 +35,7 @@ def/1, opts/1, columns/1, - is_usable/2, + is_usable/3, start_key/2, end_key/2, cursor_mod/1, @@ -57,9 +56,8 @@ list(Db) -> {ok, Indexes} = ddoc_cache:open(db_to_name(Db), ?MODULE), Indexes. -get_usable_indexes(Db, Selector0, Opts) -> - Selector = mango_selector:normalize(Selector0), +get_usable_indexes(Db, Selector, Opts) -> ExistingIndexes = mango_idx:list(Db), if ExistingIndexes /= [] -> ok; true -> ?MANGO_ERROR({no_usable_index, no_indexes_defined}) @@ -70,13 +68,17 @@ get_usable_indexes(Db, Selector0, Opts) -> ?MANGO_ERROR({no_usable_index, no_index_matching_name}) end, - SortIndexes = mango_idx:for_sort(FilteredIndexes, Opts), - if SortIndexes /= [] -> ok; true -> - ?MANGO_ERROR({no_usable_index, missing_sort_index}) - end, + SortFields = get_sort_fields(Opts), + UsableFilter = fun(I) -> is_usable(I, Selector, SortFields) end, + UsableIndexes0 = lists:filter(UsableFilter, FilteredIndexes), + + case maybe_filter_by_sort_fields(UsableIndexes0, SortFields) of + {ok, SortIndexes} -> + SortIndexes; + {error, no_usable_index} -> + ?MANGO_ERROR({no_usable_index, missing_sort_index}) + end. - UsableFilter = fun(I) -> mango_idx:is_usable(I, Selector) end, - lists:filter(UsableFilter, SortIndexes). recover(Db) -> {ok, DDocs0} = mango_util:open_ddocs(Db), @@ -93,33 +95,38 @@ recover(Db) -> end, DDocs)}. -for_sort(Indexes, Opts) -> - % If a sort was specified we have to find an index that - % can satisfy the request. +get_sort_fields(Opts) -> case lists:keyfind(sort, 1, Opts) of - {sort, {SProps}} when is_list(SProps) -> - for_sort_int(Indexes, {SProps}); + {sort, Sort} -> + mango_sort:fields(Sort); _ -> - Indexes + [] end. -for_sort_int(Indexes, Sort) -> - Fields = mango_sort:fields(Sort), +maybe_filter_by_sort_fields(Indexes, []) -> + {ok, Indexes}; + +maybe_filter_by_sort_fields(Indexes, SortFields) -> FilterFun = fun(Idx) -> Cols = mango_idx:columns(Idx), case {mango_idx:type(Idx), Cols} of {_, all_fields} -> true; {<<"text">>, _} -> - sets:is_subset(sets:from_list(Fields), sets:from_list(Cols)); + sets:is_subset(sets:from_list(SortFields), sets:from_list(Cols)); {<<"json">>, _} -> - lists:prefix(Fields, Cols); + lists:prefix(SortFields, Cols); {<<"special">>, _} -> - lists:prefix(Fields, Cols) + lists:prefix(SortFields, Cols) end end, - lists:filter(FilterFun, Indexes). + case lists:filter(FilterFun, Indexes) of + [] -> + {error, no_usable_index}; + FilteredIndexes -> + {ok, FilteredIndexes} + end. new(Db, Opts) -> @@ -250,9 +257,9 @@ columns(#idx{}=Idx) -> Mod:columns(Idx). -is_usable(#idx{}=Idx, Selector) -> +is_usable(#idx{}=Idx, Selector, SortFields) -> Mod = idx_mod(Idx), - Mod:is_usable(Idx, Selector). + Mod:is_usable(Idx, Selector, SortFields). start_key(#idx{}=Idx, Ranges) -> diff --git a/src/mango/src/mango_idx_special.erl b/src/mango/src/mango_idx_special.erl index a8f94002b..12da1cbe5 100644 --- a/src/mango/src/mango_idx_special.erl +++ b/src/mango/src/mango_idx_special.erl @@ -20,7 +20,7 @@ from_ddoc/1, to_json/1, columns/1, - is_usable/2, + is_usable/3, start_key/1, end_key/1 ]). @@ -63,7 +63,7 @@ columns(#idx{def=all_docs}) -> [<<"_id">>]. -is_usable(#idx{def=all_docs}, Selector) -> +is_usable(#idx{def=all_docs}, Selector, _) -> Fields = mango_idx_view:indexable_fields(Selector), lists:member(<<"_id">>, Fields). diff --git a/src/mango/src/mango_idx_text.erl b/src/mango/src/mango_idx_text.erl index e00c241d2..e4ffc91db 100644 --- a/src/mango/src/mango_idx_text.erl +++ b/src/mango/src/mango_idx_text.erl @@ -22,7 +22,7 @@ from_ddoc/1, to_json/1, columns/1, - is_usable/2, + is_usable/3, get_default_field_options/1 ]). @@ -125,7 +125,7 @@ columns(Idx) -> end. -is_usable(Idx, Selector) -> +is_usable(Idx, Selector, _) -> case columns(Idx) of all_fields -> true; diff --git a/src/mango/src/mango_idx_view.erl b/src/mango/src/mango_idx_view.erl index 4cb039c4a..f1041bbaf 100644 --- a/src/mango/src/mango_idx_view.erl +++ b/src/mango/src/mango_idx_view.erl @@ -20,7 +20,7 @@ remove/2, from_ddoc/1, to_json/1, - is_usable/2, + is_usable/3, columns/1, start_key/1, end_key/1, @@ -34,6 +34,7 @@ -include_lib("couch/include/couch_db.hrl"). -include("mango.hrl"). -include("mango_idx.hrl"). +-include("mango_idx_view.hrl"). validate_new(#idx{}=Idx, _Db) -> @@ -113,12 +114,17 @@ columns(Idx) -> [Key || {Key, _} <- Fields]. -is_usable(Idx, Selector) -> - % This index is usable if all of the columns are +is_usable(Idx, Selector, SortFields) -> + % This index is usable if all of the columns are % restricted by the selector such that they are required to exist % and the selector is not a text search (so requires a text index) RequiredFields = columns(Idx), - mango_selector:has_required_fields(Selector, RequiredFields) + + % sort fields are required to exist in the results so + % we don't need to check the selector for these + RequiredFields1 = ordsets:subtract(lists:usort(RequiredFields), lists:usort(SortFields)), + + mango_selector:has_required_fields(Selector, RequiredFields1) andalso not is_text_search(Selector). @@ -158,11 +164,11 @@ start_key([{'$eq', Key, '$eq', Key} | Rest]) -> end_key([]) -> - [{[]}]; + [?MAX_JSON_OBJ]; end_key([{_, _, '$lt', Key} | Rest]) -> case mango_json:special(Key) of true -> - [{[]}]; + [?MAX_JSON_OBJ]; false -> [Key | end_key(Rest)] end; diff --git a/src/mango/src/mango_idx_view.hrl b/src/mango/src/mango_idx_view.hrl new file mode 100644 index 000000000..0d213e56e --- /dev/null +++ b/src/mango/src/mango_idx_view.hrl @@ -0,0 +1,13 @@ +% 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(MAX_JSON_OBJ, {<<255, 255, 255, 255>>}).
\ No newline at end of file diff --git a/src/mango/src/mango_selector.erl b/src/mango/src/mango_selector.erl index fe3998683..4ff36945a 100644 --- a/src/mango/src/mango_selector.erl +++ b/src/mango/src/mango_selector.erl @@ -609,7 +609,7 @@ has_required_fields([{[{Field, Cond}]} | Rest], RequiredFields) -> _ -> has_required_fields(Rest, lists:delete(Field, RequiredFields)) end. - + %%%%%%%% module tests below %%%%%%%% diff --git a/src/mango/test/01-index-crud-test.py b/src/mango/test/01-index-crud-test.py index 617bfd523..cf5b91865 100644 --- a/src/mango/test/01-index-crud-test.py +++ b/src/mango/test/01-index-crud-test.py @@ -36,7 +36,7 @@ class IndexCrudTests(mango.DbPerClass): try: self.db.create_index(fields) except Exception as e: - assert e.response.status_code == 400 + self.assertEqual(e.response.status_code, 400) else: raise AssertionError("bad create index") @@ -55,7 +55,7 @@ class IndexCrudTests(mango.DbPerClass): try: self.db.create_index(["foo"], idx_type=bt) except Exception as e: - assert e.response.status_code == 400, (bt, e.response.status_code) + self.assertEqual(e.response.status_code, 400, (bt, e.response.status_code)) else: raise AssertionError("bad create index") @@ -71,13 +71,13 @@ class IndexCrudTests(mango.DbPerClass): try: self.db.create_index(["foo"], name=bn) except Exception as e: - assert e.response.status_code == 400 + self.assertEqual(e.response.status_code, 400) else: raise AssertionError("bad create index") try: self.db.create_index(["foo"], ddoc=bn) except Exception as e: - assert e.response.status_code == 400 + self.assertEqual(e.response.status_code, 400) else: raise AssertionError("bad create index") @@ -88,7 +88,7 @@ class IndexCrudTests(mango.DbPerClass): for idx in self.db.list_indexes(): if idx["name"] != "idx_01": continue - assert idx["def"]["fields"] == [{"foo": "asc"}, {"bar": "asc"}] + self.assertEqual(idx["def"]["fields"], [{"foo": "asc"}, {"bar": "asc"}]) return raise AssertionError("index not created") @@ -106,7 +106,7 @@ class IndexCrudTests(mango.DbPerClass): for idx in self.db.list_indexes(): if idx["name"] != "idx_02": continue - assert idx["def"]["fields"] == [{"baz": "asc"}, {"foo": "asc"}] + self.assertEqual(idx["def"]["fields"], [{"baz": "asc"}, {"foo": "asc"}]) return raise AssertionError("index not created") @@ -118,9 +118,9 @@ class IndexCrudTests(mango.DbPerClass): continue ddocid = idx["ddoc"] doc = self.db.open_doc(ddocid) - assert doc["_id"] == ddocid + self.assertEqual(doc["_id"], ddocid) info = self.db.ddoc_info(ddocid) - assert info["name"] == ddocid.split('_design/')[-1] + self.assertEqual(info["name"], ddocid.split('_design/')[-1]) def test_delete_idx_escaped(self): self.db.create_index(["foo", "bar"], name="idx_01") @@ -130,10 +130,10 @@ class IndexCrudTests(mango.DbPerClass): for idx in self.db.list_indexes(): if idx["name"] != "idx_del_1": continue - assert idx["def"]["fields"] == [{"bing": "asc"}] + self.assertEqual(idx["def"]["fields"], [{"bing": "asc"}]) self.db.delete_index(idx["ddoc"].replace("/", "%2F"), idx["name"]) post_indexes = self.db.list_indexes() - assert pre_indexes == post_indexes + self.assertEqual(pre_indexes, post_indexes) def test_delete_idx_unescaped(self): pre_indexes = self.db.list_indexes() @@ -142,10 +142,10 @@ class IndexCrudTests(mango.DbPerClass): for idx in self.db.list_indexes(): if idx["name"] != "idx_del_2": continue - assert idx["def"]["fields"] == [{"bing": "asc"}] + self.assertEqual(idx["def"]["fields"], [{"bing": "asc"}]) self.db.delete_index(idx["ddoc"], idx["name"]) post_indexes = self.db.list_indexes() - assert pre_indexes == post_indexes + self.assertEqual(pre_indexes, post_indexes) def test_delete_idx_no_design(self): pre_indexes = self.db.list_indexes() @@ -154,10 +154,10 @@ class IndexCrudTests(mango.DbPerClass): for idx in self.db.list_indexes(): if idx["name"] != "idx_del_3": continue - assert idx["def"]["fields"] == [{"bing": "asc"}] + self.assertEqual(idx["def"]["fields"], [{"bing": "asc"}]) self.db.delete_index(idx["ddoc"].split("/")[-1], idx["name"]) post_indexes = self.db.list_indexes() - assert pre_indexes == post_indexes + self.assertEqual(pre_indexes, post_indexes) def test_bulk_delete(self): fields = ["field1"] @@ -182,8 +182,8 @@ class IndexCrudTests(mango.DbPerClass): ret = self.db.bulk_delete(docids) - assert ret["fail"][0]["id"] == "_design/this_is_not_an_index_name" - assert len(ret["success"]) == 3 + self.assertEqual(ret["fail"][0]["id"], "_design/this_is_not_an_index_name") + self.assertEqual(len(ret["success"]), 3) for idx in self.db.list_indexes(): assert idx["type"] != "json" @@ -197,18 +197,18 @@ class IndexCrudTests(mango.DbPerClass): for idx in self.db.list_indexes(): if idx["name"] != "idx_recreate": continue - assert idx["def"]["fields"] == [{"bing": "asc"}] + self.assertEqual(idx["def"]["fields"], [{"bing": "asc"}]) self.db.delete_index(idx["ddoc"], idx["name"]) break post_indexes = self.db.list_indexes() - assert pre_indexes == post_indexes + self.assertEqual(pre_indexes, post_indexes) - def test_delete_misisng(self): + def test_delete_missing(self): # Missing design doc try: self.db.delete_index("this_is_not_a_design_doc_id", "foo") except Exception as e: - assert e.response.status_code == 404 + self.assertEqual(e.response.status_code, 404) else: raise AssertionError("bad index delete") @@ -221,7 +221,7 @@ class IndexCrudTests(mango.DbPerClass): try: self.db.delete_index(ddocid, "this_is_not_an_index_name") except Exception as e: - assert e.response.status_code == 404 + self.assertEqual(e.response.status_code, 404) else: raise AssertionError("bad index delete") @@ -229,11 +229,54 @@ class IndexCrudTests(mango.DbPerClass): try: self.db.delete_index(ddocid, idx["name"], idx_type="not_a_real_type") except Exception as e: - assert e.response.status_code == 404 + self.assertEqual(e.response.status_code, 404) else: raise AssertionError("bad index delete") - @unittest.skipUnless(mango.has_text_service(), "requires text service") + def test_limit_skip_index(self): + fields = ["field1"] + ret = self.db.create_index(fields, name="idx_01") + assert ret is True + + fields = ["field2"] + ret = self.db.create_index(fields, name="idx_02") + assert ret is True + + fields = ["field3"] + ret = self.db.create_index(fields, name="idx_03") + assert ret is True + + fields = ["field4"] + ret = self.db.create_index(fields, name="idx_04") + assert ret is True + + fields = ["field5"] + ret = self.db.create_index(fields, name="idx_05") + assert ret is True + + self.assertEqual(len(self.db.list_indexes(limit=2)), 2) + self.assertEqual(len(self.db.list_indexes(limit=5,skip=4)), 2) + self.assertEqual(len(self.db.list_indexes(skip=5)), 1) + self.assertEqual(len(self.db.list_indexes(skip=6)), 0) + self.assertEqual(len(self.db.list_indexes(skip=100)), 0) + self.assertEqual(len(self.db.list_indexes(limit=10000000)), 6) + + try: + self.db.list_indexes(skip=-1) + except Exception as e: + self.assertEqual(e.response.status_code, 500) + + try: + self.db.list_indexes(limit=0) + except Exception as e: + self.assertEqual(e.response.status_code, 500) + + +@unittest.skipUnless(mango.has_text_service(), "requires text service") +class IndexCrudTextTests(mango.DbPerClass): + def setUp(self): + self.db.recreate() + def test_create_text_idx(self): fields = [ {"name":"stringidx", "type" : "string"}, @@ -244,14 +287,13 @@ class IndexCrudTests(mango.DbPerClass): for idx in self.db.list_indexes(): if idx["name"] != "text_idx_01": continue - assert idx["def"]["fields"] == [ + self.assertEqual(idx["def"]["fields"], [ {"stringidx": "string"}, {"booleanidx": "boolean"} - ] + ]) return raise AssertionError("index not created") - @unittest.skipUnless(mango.has_text_service(), "requires text service") def test_create_bad_text_idx(self): bad_fields = [ True, @@ -270,10 +312,10 @@ class IndexCrudTests(mango.DbPerClass): try: self.db.create_text_index(fields=fields) except Exception as e: - assert e.response.status_code == 400 + self.assertEqual(e.response.status_code, 400) else: raise AssertionError("bad create text index") - + def test_limit_skip_index(self): fields = ["field1"] ret = self.db.create_index(fields, name="idx_01") @@ -291,28 +333,26 @@ class IndexCrudTests(mango.DbPerClass): ret = self.db.create_index(fields, name="idx_04") assert ret is True - fields = ["field5"] - ret = self.db.create_index(fields, name="idx_05") + fields = [ + {"name":"stringidx", "type" : "string"}, + {"name":"booleanidx", "type": "boolean"} + ] + ret = self.db.create_text_index(fields=fields, name="idx_05") assert ret is True - skip_add = 0 - - if mango.has_text_service(): - skip_add = 1 - - assert len(self.db.list_indexes(limit=2)) == 2 - assert len(self.db.list_indexes(limit=5,skip=4)) == 2 + skip_add - assert len(self.db.list_indexes(skip=5)) == 1 + skip_add - assert len(self.db.list_indexes(skip=6)) == 0 + skip_add - assert len(self.db.list_indexes(skip=100)) == 0 - assert len(self.db.list_indexes(limit=10000000)) == 6 + skip_add + self.assertEqual(len(self.db.list_indexes(limit=2)), 2) + self.assertEqual(len(self.db.list_indexes(limit=5,skip=4)), 2) + self.assertEqual(len(self.db.list_indexes(skip=5)), 1) + self.assertEqual(len(self.db.list_indexes(skip=6)), 0) + self.assertEqual(len(self.db.list_indexes(skip=100)), 0) + self.assertEqual(len(self.db.list_indexes(limit=10000000)), 6) try: self.db.list_indexes(skip=-1) except Exception as e: - assert e.response.status_code == 500 + self.assertEqual(e.response.status_code, 500) try: self.db.list_indexes(limit=0) except Exception as e: - assert e.response.status_code == 500 + self.assertEqual(e.response.status_code, 500) diff --git a/src/mango/test/02-basic-find-test.py b/src/mango/test/02-basic-find-test.py index a8725ffa8..82554a112 100644 --- a/src/mango/test/02-basic-find-test.py +++ b/src/mango/test/02-basic-find-test.py @@ -222,6 +222,52 @@ class BasicFindTests(mango.UserDocsTests): docs2 = list(reversed(sorted(docs1, key=lambda d: d["age"]))) assert docs1 is not docs2 and docs1 == docs2 + def test_sort_desc_complex(self): + docs = self.db.find({ + "company": {"$lt": "M"}, + "$or": [ + {"company": "Dreamia"}, + {"manager": True} + ] + }, sort=[{"company":"desc"}, {"manager":"desc"}]) + + companies_returned = list(d["company"] for d in docs) + desc_companies = sorted(companies_returned, reverse=True) + self.assertEqual(desc_companies, companies_returned) + + def test_sort_with_primary_sort_not_in_selector(self): + try: + docs = self.db.find({ + "name.last": {"$lt": "M"} + }, sort=[{"name.first":"desc"}]) + except Exception as e: + self.assertEqual(e.response.status_code, 400) + resp = e.response.json() + self.assertEqual(resp["error"], "no_usable_index") + else: + raise AssertionError("expected find error") + + def test_sort_exists_true(self): + docs1 = self.db.find({"age": {"$gt": 0, "$exists": True}}, sort=[{"age":"asc"}]) + docs2 = list(sorted(docs1, key=lambda d: d["age"])) + assert docs1 is not docs2 and docs1 == docs2 + + def test_sort_desc_complex_error(self): + try: + self.db.find({ + "company": {"$lt": "M"}, + "$or": [ + {"company": "Dreamia"}, + {"manager": True} + ] + }, sort=[{"company":"desc"}]) + except Exception as e: + self.assertEqual(e.response.status_code, 400) + resp = e.response.json() + self.assertEqual(resp["error"], "no_usable_index") + else: + raise AssertionError("expected find error") + def test_fields(self): selector = {"age": {"$gt": 0}} docs = self.db.find(selector, fields=["user_id", "location.address"]) @@ -273,5 +319,5 @@ class BasicFindTests(mango.UserDocsTests): assert explain["mrargs"]["update"] == True assert explain["mrargs"]["reduce"] == False assert explain["mrargs"]["start_key"] == [0] - assert explain["mrargs"]["end_key"] == [{}] + assert explain["mrargs"]["end_key"] == ["<MAX>"] assert explain["mrargs"]["include_docs"] == True diff --git a/src/mango/test/03-operator-test.py b/src/mango/test/03-operator-test.py index 1af39f205..239cc7d41 100644 --- a/src/mango/test/03-operator-test.py +++ b/src/mango/test/03-operator-test.py @@ -174,6 +174,9 @@ class OperatorTests: for d in docs: self.assertIn("twitter", d) + # ideally this work be consistent across index types but, alas, it is not + @unittest.skipUnless(not mango.has_text_service(), + "text indexes do not support range queries across type boundaries") def test_lt_includes_null_but_not_missing(self): docs = self.db.find({ "twitter": {"$lt": 1} @@ -183,6 +186,8 @@ class OperatorTests: for d in docs: self.assertEqual(d["twitter"], None) + @unittest.skipUnless(not mango.has_text_service(), + "text indexes do not support range queries across type boundaries") def test_lte_includes_null_but_not_missing(self): docs = self.db.find({ "twitter": {"$lt": 1} diff --git a/src/mango/test/05-index-selection-test.py b/src/mango/test/05-index-selection-test.py index 1cc210382..05571a7e8 100644 --- a/src/mango/test/05-index-selection-test.py +++ b/src/mango/test/05-index-selection-test.py @@ -15,12 +15,7 @@ import user_docs import unittest -class IndexSelectionTests(mango.UserDocsTests): - @classmethod - def setUpClass(klass): - super(IndexSelectionTests, klass).setUpClass() - if mango.has_text_service(): - user_docs.add_text_indexes(klass.db, {}) +class IndexSelectionTests: def test_basic(self): resp = self.db.find({"age": 123}, explain=True) @@ -33,30 +28,6 @@ class IndexSelectionTests(mango.UserDocsTests): }, explain=True) self.assertEqual(resp["index"]["type"], "json") - @unittest.skipUnless(mango.has_text_service(), "requires text service") - def test_with_text(self): - resp = self.db.find({ - "$text" : "Stephanie", - "name.first": "Stephanie", - "name.last": "This doesn't have to match anything." - }, explain=True) - self.assertEqual(resp["index"]["type"], "text") - - @unittest.skipUnless(mango.has_text_service(), "requires text service") - def test_no_view_index(self): - resp = self.db.find({"name.first": "Ohai!"}, explain=True) - self.assertEqual(resp["index"]["type"], "text") - - @unittest.skipUnless(mango.has_text_service(), "requires text service") - def test_with_or(self): - resp = self.db.find({ - "$or": [ - {"name.first": "Stephanie"}, - {"name.last": "This doesn't have to match anything."} - ] - }, explain=True) - self.assertEqual(resp["index"]["type"], "text") - def test_use_most_columns(self): # ddoc id for the age index ddocid = "_design/ad3d537c03cd7c6a43cf8dff66ef70ea54c2b40f" @@ -92,36 +63,6 @@ class IndexSelectionTests(mango.UserDocsTests): else: raise AssertionError("bad find") - def test_uses_all_docs_when_fields_do_not_match_selector(self): - # index exists on ["company", "manager"] but not ["company"] - # so we should fall back to all docs (so we include docs - # with no "manager" field) - selector = { - "company": "Pharmex" - } - docs = self.db.find(selector) - self.assertEqual(len(docs), 1) - self.assertEqual(docs[0]["company"], "Pharmex") - self.assertNotIn("manager", docs[0]) - - resp_explain = self.db.find(selector, explain=True) - self.assertEqual(resp_explain["index"]["type"], "special") - - def test_uses_all_docs_when_selector_doesnt_require_fields_to_exist(self): - # as in test above, use a selector that doesn't overlap with the index - # due to an explicit exists clause - selector = { - "company": "Pharmex", - "manager": {"$exists": False} - } - docs = self.db.find(selector) - self.assertEqual(len(docs), 1) - self.assertEqual(docs[0]["company"], "Pharmex") - self.assertNotIn("manager", docs[0]) - - resp_explain = self.db.find(selector, explain=True) - self.assertEqual(resp_explain["index"]["type"], "special") - def test_uses_index_when_no_range_or_equals(self): # index on ["manager"] should be valid because # selector requires "manager" to exist. The @@ -200,7 +141,77 @@ class IndexSelectionTests(mango.UserDocsTests): with self.assertRaises(KeyError): self.db.save_doc(design_doc) - @unittest.skipUnless(mango.has_text_service(), "requires text service") + +class JSONIndexSelectionTests(mango.UserDocsTests, IndexSelectionTests): + + @classmethod + def setUpClass(klass): + super(JSONIndexSelectionTests, klass).setUpClass() + + def test_uses_all_docs_when_fields_do_not_match_selector(self): + # index exists on ["company", "manager"] but not ["company"] + # so we should fall back to all docs (so we include docs + # with no "manager" field) + selector = { + "company": "Pharmex" + } + docs = self.db.find(selector) + self.assertEqual(len(docs), 1) + self.assertEqual(docs[0]["company"], "Pharmex") + self.assertNotIn("manager", docs[0]) + + resp_explain = self.db.find(selector, explain=True) + + self.assertEqual(resp_explain["index"]["type"], "special") + + def test_uses_all_docs_when_selector_doesnt_require_fields_to_exist(self): + # as in test above, use a selector that doesn't overlap with the index + # due to an explicit exists clause + selector = { + "company": "Pharmex", + "manager": {"$exists": False} + } + docs = self.db.find(selector) + self.assertEqual(len(docs), 1) + self.assertEqual(docs[0]["company"], "Pharmex") + self.assertNotIn("manager", docs[0]) + + resp_explain = self.db.find(selector, explain=True) + self.assertEqual(resp_explain["index"]["type"], "special") + + +@unittest.skipUnless(mango.has_text_service(), "requires text service") +class TextIndexSelectionTests(mango.UserDocsTests, IndexSelectionTests): + + @classmethod + def setUpClass(klass): + super(TextIndexSelectionTests, klass).setUpClass() + + def setUp(self): + self.db.recreate() + user_docs.add_text_indexes(self.db, {}) + + def test_with_text(self): + resp = self.db.find({ + "$text" : "Stephanie", + "name.first": "Stephanie", + "name.last": "This doesn't have to match anything." + }, explain=True) + self.assertEqual(resp["index"]["type"], "text") + + def test_no_view_index(self): + resp = self.db.find({"name.first": "Ohai!"}, explain=True) + self.assertEqual(resp["index"]["type"], "text") + + def test_with_or(self): + resp = self.db.find({ + "$or": [ + {"name.first": "Stephanie"}, + {"name.last": "This doesn't have to match anything."} + ] + }, explain=True) + self.assertEqual(resp["index"]["type"], "text") + def test_manual_bad_text_idx(self): design_doc = { "_id": "_design/bad_text_index", @@ -243,8 +254,8 @@ class MultiTextIndexSelectionTests(mango.UserDocsTests): klass.db.create_text_index(ddoc="foo", analyzer="keyword") klass.db.create_text_index(ddoc="bar", analyzer="email") - def test_view_ok_with_multi_text(self): - resp = self.db.find({"name.last": "A last name"}, explain=True) + def test_fallback_to_json_with_multi_text(self): + resp = self.db.find({"name.first": "A first name", "name.last": "A last name"}, explain=True) self.assertEqual(resp["index"]["type"], "json") def test_multi_text_index_is_error(self): diff --git a/src/mango/test/09-text-sort-test.py b/src/mango/test/09-text-sort-test.py index 1c5557227..a1a644c79 100644 --- a/src/mango/test/09-text-sort-test.py +++ b/src/mango/test/09-text-sort-test.py @@ -19,60 +19,60 @@ class SortTests(mango.UserDocsTextTests): def test_number_sort(self): q = {"age": {"$gt": 0}} docs = self.db.find(q, sort=["age:number"]) - assert len(docs) == 15 - assert docs[0]["age"] == 22 + self.assertEqual(len(docs), 15) + self.assertEqual(docs[0]["age"], 22) def test_number_sort_desc(self): q = {"age": {"$gt": 0}} docs = self.db.find(q, sort=[{"age": "desc"}]) - assert len(docs) == 15 - assert docs[0]["age"] == 79 + self.assertEqual(len(docs), 15) + self.assertEqual(docs[0]["age"], 79) q = {"manager": True} docs = self.db.find(q, sort=[{"age:number": "desc"}]) - assert len(docs) == 11 - assert docs[0]["age"] == 79 + self.assertEqual(len(docs), 10) + self.assertEqual(docs[0]["age"], 79) def test_string_sort(self): q = {"email": {"$gt": None}} docs = self.db.find(q, sort=["email:string"]) - assert len(docs) == 15 - assert docs[0]["email"] == "abbottwatson@talkola.com" + self.assertEqual(len(docs), 15) + self.assertEqual(docs[0]["email"], "abbottwatson@talkola.com") def test_notype_sort(self): q = {"email": {"$gt": None}} try: self.db.find(q, sort=["email"]) except Exception as e: - assert e.response.status_code == 400 + self.assertEqual(e.response.status_code, 400) else: raise AssertionError("Should have thrown error for sort") def test_array_sort(self): q = {"favorites": {"$exists": True}} docs = self.db.find(q, sort=["favorites.[]:string"]) - assert len(docs) == 15 - assert docs[0]["user_id"] == 8 + self.assertEqual(len(docs), 15) + self.assertEqual(docs[0]["user_id"], 8) def test_multi_sort(self): q = {"name": {"$exists": True}} docs = self.db.find(q, sort=["name.last:string", "age:number"]) - assert len(docs) == 15 - assert docs[0]["name"] == {"last":"Ewing","first":"Shelly"} - assert docs[1]["age"] == 22 + self.assertEqual(len(docs), 15) + self.assertEqual(docs[0]["name"], {"last":"Ewing","first":"Shelly"}) + self.assertEqual(docs[1]["age"], 22) def test_guess_type_sort(self): q = {"$or": [{"age":{"$gt": 0}}, {"email": {"$gt": None}}]} docs = self.db.find(q, sort=["age"]) - assert len(docs) == 15 - assert docs[0]["age"] == 22 + self.assertEqual(len(docs), 15) + self.assertEqual(docs[0]["age"], 22) def test_guess_dup_type_sort(self): q = {"$and": [{"age":{"$gt": 0}}, {"email": {"$gt": None}}, {"age":{"$lte": 100}}]} docs = self.db.find(q, sort=["age"]) - assert len(docs) == 15 - assert docs[0]["age"] == 22 + self.assertEqual(len(docs), 15) + self.assertEqual(docs[0]["age"], 22) def test_ambiguous_type_sort(self): q = {"$or": [{"age":{"$gt": 0}}, {"email": {"$gt": None}}, @@ -80,7 +80,7 @@ class SortTests(mango.UserDocsTextTests): try: self.db.find(q, sort=["age"]) except Exception as e: - assert e.response.status_code == 400 + self.assertEqual(e.response.status_code, 400) else: raise AssertionError("Should have thrown error for sort") @@ -88,14 +88,14 @@ class SortTests(mango.UserDocsTextTests): q = {"$or": [{"age":{"$gt": 0}}, {"email": {"$gt": None}}, {"name.last": "Harvey"}]} docs = self.db.find(q, sort=["name.last", "age"]) - assert len(docs) == 15 - assert docs[0]["name"] == {"last":"Ewing","first":"Shelly"} - assert docs[1]["age"] == 22 + self.assertEqual(len(docs), 15) + self.assertEqual(docs[0]["name"], {"last":"Ewing","first":"Shelly"}) + self.assertEqual(docs[1]["age"], 22) def test_guess_mix_sort(self): q = {"$or": [{"age":{"$gt": 0}}, {"email": {"$gt": None}}, {"name.last": "Harvey"}]} docs = self.db.find(q, sort=["name.last:string", "age"]) - assert len(docs) == 15 - assert docs[0]["name"] == {"last":"Ewing","first":"Shelly"} - assert docs[1]["age"] == 22 + self.assertEqual(len(docs), 15) + self.assertEqual(docs[0]["name"], {"last":"Ewing","first":"Shelly"}) + self.assertEqual(docs[1]["age"], 22) diff --git a/src/mango/test/10-disable-array-length-field-test.py b/src/mango/test/10-disable-array-length-field-test.py index 0715f1db9..ce7713b63 100644 --- a/src/mango/test/10-disable-array-length-field-test.py +++ b/src/mango/test/10-disable-array-length-field-test.py @@ -13,28 +13,24 @@ import mango import unittest - +@unittest.skipUnless(mango.has_text_service(), "requires text service") class DisableIndexArrayLengthsTest(mango.UserDocsTextTests): - @classmethod - def setUpClass(klass): - super(DisableIndexArrayLengthsTest, klass).setUpClass() - if mango.has_text_service(): - klass.db.create_text_index(ddoc="disable_index_array_lengths", + def setUp(klass): + self.db.recreate() + self.db.create_text_index(ddoc="disable_index_array_lengths", analyzer="keyword", index_array_lengths=False) - klass.db.create_text_index(ddoc="explicit_enable_index_array_lengths", + self.db.create_text_index(ddoc="explicit_enable_index_array_lengths", analyzer="keyword", index_array_lengths=True) - @unittest.skipUnless(mango.has_text_service(), "requires text service") def test_disable_index_array_length(self): docs = self.db.find({"favorites": {"$size": 4}}, use_index="disable_index_array_lengths") for d in docs: assert len(d["favorites"]) == 0 - @unittest.skipUnless(mango.has_text_service(), "requires text service") def test_enable_index_array_length(self): docs = self.db.find({"favorites": {"$size": 4}}, use_index="explicit_enable_index_array_lengths") diff --git a/src/mango/test/11-ignore-design-docs.py b/src/mango/test/11-ignore-design-docs-test.py index ea7165e3f..ea7165e3f 100644 --- a/src/mango/test/11-ignore-design-docs.py +++ b/src/mango/test/11-ignore-design-docs-test.py diff --git a/src/mango/test/12-use-correct-index.py b/src/mango/test/12-use-correct-index-test.py index 84b425343..5a2b24d3f 100644 --- a/src/mango/test/12-use-correct-index.py +++ b/src/mango/test/12-use-correct-index-test.py @@ -68,15 +68,15 @@ class ChooseCorrectIndexForDocs(mango.DbPerClass): self.assertEqual(explain["index"]["ddoc"], '_design/bbb') def test_choose_index_alphabetically(self): - self.db.create_index(["name", "age", "user_id"], ddoc="aaa") - self.db.create_index(["name", "age", "location"], ddoc="bbb") + self.db.create_index(["name"], ddoc="aaa") + self.db.create_index(["name"], ddoc="bbb") self.db.create_index(["name"], ddoc="zzz") explain = self.db.find({"name": "Eddie", "age": {"$gte": 12}}, explain=True) self.assertEqual(explain["index"]["ddoc"], '_design/aaa') def test_choose_index_most_accurate(self): - self.db.create_index(["name", "location", "user_id"], ddoc="aaa") - self.db.create_index(["name", "age", "user_id"], ddoc="bbb") + self.db.create_index(["name", "age", "user_id"], ddoc="aaa") + self.db.create_index(["name", "age"], ddoc="bbb") self.db.create_index(["name"], ddoc="zzz") explain = self.db.find({"name": "Eddie", "age": {"$gte": 12}}, explain=True) self.assertEqual(explain["index"]["ddoc"], '_design/bbb') @@ -105,3 +105,12 @@ class ChooseCorrectIndexForDocs(mango.DbPerClass): self.db.create_index(["a", "d", "e"]) explain = self.db.find({"a": {"$gt": 0}, "b": {"$gt": 0}, "c": {"$gt": 0}}, explain=True) self.assertEqual(explain["index"]["def"]["fields"], [{'a': 'asc'}, {'b': 'asc'}, {'c': 'asc'}]) + + def test_can_query_with_range_on_secondary_column(self): + self.db.create_index(["age", "name"], ddoc="bbb") + selector = {"age": 10, "name": {"$gte": 0}} + docs = self.db.find(selector) + self.assertEqual(len(docs), 1) + explain = self.db.find(selector, explain=True) + self.assertEqual(explain["index"]["ddoc"], "_design/bbb") + self.assertEqual(explain["mrargs"]["end_key"], [10, '<MAX>']) diff --git a/src/mango/test/14-json-pagination.py b/src/mango/test/14-json-pagination-test.py index ea06e0a2a..ea06e0a2a 100644 --- a/src/mango/test/14-json-pagination.py +++ b/src/mango/test/14-json-pagination-test.py diff --git a/src/mango/test/16-index-selectors.py b/src/mango/test/16-index-selectors-test.py index 3ce659ecf..456b396c7 100644 --- a/src/mango/test/16-index-selectors.py +++ b/src/mango/test/16-index-selectors-test.py @@ -73,6 +73,28 @@ DOCS = [ }, ] +oldschoolnoselectorddoc = { + "_id": "_design/oldschoolnoselector", + "language": "query", + "views": { + "oldschoolnoselector": { + "map": { + "fields": { + "location": "asc" + } + }, + "reduce": "_count", + "options": { + "def": { + "fields": [ + "location" + ] + } + } + } + } +} + oldschoolddoc = { "_id": "_design/oldschool", "language": "query", @@ -178,6 +200,14 @@ class IndexSelectorJson(mango.DbPerClass): resp = self.db.find(selector, explain=True) self.assertEqual(resp["index"]["name"], "NotSelected") + def test_old_selector_with_no_selector_still_supported(self): + selector = {"location": {"$gte": "FRA"}} + self.db.save_doc(oldschoolnoselectorddoc) + resp = self.db.find(selector, explain=True, use_index='oldschoolnoselector') + self.assertEqual(resp["index"]["name"], "oldschoolnoselector") + docs = self.db.find(selector, use_index='oldschoolnoselector') + self.assertEqual(len(docs), 3) + def test_old_selector_still_supported(self): selector = {"location": {"$gte": "FRA"}} self.db.save_doc(oldschoolddoc) diff --git a/src/mango/test/17-multi-type-value-test.py b/src/mango/test/17-multi-type-value-test.py new file mode 100644 index 000000000..d838447d5 --- /dev/null +++ b/src/mango/test/17-multi-type-value-test.py @@ -0,0 +1,90 @@ +# 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. + +import copy +import mango +import unittest + +DOCS = [ + { + "_id": "1", + "name": "Jimi", + "age": 10 + }, + { + "_id": "2", + "name": {"forename":"Eddie"}, + "age": 20 + }, + { + "_id": "3", + "name": None, + "age": 30 + }, + { + "_id": "4", + "name": 1, + "age": 40 + }, + { + "_id": "5", + "forename": "Sam", + "age": 50 + } +] + + +class MultiValueFieldTests: + + def test_can_query_with_name(self): + docs = self.db.find({"name": {"$exists": True}}) + self.assertEqual(len(docs), 4) + for d in docs: + self.assertIn("name", d) + + def test_can_query_with_name_subfield(self): + docs = self.db.find({"name.forename": {"$exists": True}}) + self.assertEqual(len(docs), 1) + self.assertEqual(docs[0]["_id"], "2") + + def test_can_query_with_name_range(self): + docs = self.db.find({"name": {"$gte": 0}}) + # expect to include "Jimi", 1 and {"forename":"Eddie"} + self.assertEqual(len(docs), 3) + for d in docs: + self.assertIn("name", d) + + def test_can_query_with_age_and_name_range(self): + docs = self.db.find({"age": {"$gte": 0, "$lt": 40}, "name": {"$gte": 0}}) + # expect to include "Jimi", 1 and {"forename":"Eddie"} + self.assertEqual(len(docs), 2) + for d in docs: + self.assertIn("name", d) + + + +class MultiValueFieldJSONTests(mango.DbPerClass, MultiValueFieldTests): + def setUp(self): + self.db.recreate() + self.db.save_docs(copy.deepcopy(DOCS)) + self.db.create_index(["name"]) + self.db.create_index(["age", "name"]) + +# @unittest.skipUnless(mango.has_text_service(), "requires text service") +# class MultiValueFieldTextTests(MultiValueFieldDocsNoIndexes, OperatorTests): +# pass + + +class MultiValueFieldAllDocsTests(mango.DbPerClass, MultiValueFieldTests): + def setUp(self): + self.db.recreate() + self.db.save_docs(copy.deepcopy(DOCS)) diff --git a/src/mango/test/README.md b/src/mango/test/README.md index fc2cd62e5..3c99cab9d 100644 --- a/src/mango/test/README.md +++ b/src/mango/test/README.md @@ -7,6 +7,23 @@ To run these, do this in the Mango top level directory: $ virtualenv venv $ source venv/bin/activate - $ pip install nose requests - $ pip install hypothesis - $ nosetests + $ make pip-install + $ make test + +To run an individual test suite: + nosetests --nocapture test/12-use-correct-index.py + +To run the tests with text index support: + MANGO_TEXT_INDEXES=1 nosetests --nocapture test + + +Test configuration +================== + +The following environment variables can be used to configure the test fixtures: + + * `COUCH_HOST` - root url (including port) of the CouchDB instance to run the tests against. Default is `"http://127.0.0.1:15984"`. + * `COUCH_USER` - CouchDB username (with admin premissions). Default is `"testuser"`. + * `COUCH_PASSWORD` - CouchDB password. Default is `"testpass"`. + * `COUCH_AUTH_HEADER` - Optional Authorization header value. If specified, this is used instead of basic authentication with the username/password variables above. + * `MANGO_TEXT_INDEXES` - Set to `"1"` to run the tests only applicable to text indexes. diff --git a/src/mango/test/mango.py b/src/mango/test/mango.py index a275a23d0..ed4cafbe5 100644 --- a/src/mango/test/mango.py +++ b/src/mango/test/mango.py @@ -27,28 +27,46 @@ def random_db_name(): return "mango_test_" + uuid.uuid4().hex def has_text_service(): - return os.path.isfile(os.getcwd() + "/../src/mango_cursor_text.erl") + return os.environ.get('MANGO_TEXT_INDEXES') == '1' + +def get_from_environment(key, default): + value = os.environ.get(key) + return value if value is not None else default class Database(object): - def __init__(self, host, port, dbname, auth=None): - self.host = host - self.port = port + def __init__(self, dbname, + host="127.0.0.1", port="15984", + user='testuser', password='testpass'): + root_url = get_from_environment('COUCH_HOST', "http://{}:{}".format(host, port)) + auth_header = get_from_environment('COUCH_AUTH_HEADER', None) + user = get_from_environment('COUCH_USER', user) + password = get_from_environment('COUCH_PASSWORD', password) + + self.root_url = root_url self.dbname = dbname self.sess = requests.session() - self.sess.auth = ('testuser', 'testpass') + + # allow explicit auth header to be set to enable testing + # against deployments where basic auth isn't available + if auth_header is not None: + self.sess.headers["Authorization"] = auth_header + else: + self.sess.auth = (user, password) + self.sess.headers["Content-Type"] = "application/json" + @property def url(self): - return "http://{}:{}/{}".format(self.host, self.port, self.dbname) + return "{}/{}".format(self.root_url, self.dbname) def path(self, parts): if isinstance(parts, ("".__class__, u"".__class__)): parts = [parts] return "/".join([self.url] + parts) - def create(self, q=1, n=3): + def create(self, q=1, n=1): r = self.sess.get(self.url) if r.status_code == 404: r = self.sess.put(self.url, params={"q":q, "n": n}) @@ -206,7 +224,7 @@ class UsersDbTests(unittest.TestCase): @classmethod def setUpClass(klass): - klass.db = Database("127.0.0.1", "15984", "_users") + klass.db = Database("_users") user_docs.setup_users(klass.db) def setUp(self): @@ -217,7 +235,7 @@ class DbPerClass(unittest.TestCase): @classmethod def setUpClass(klass): - klass.db = Database("127.0.0.1", "15984", random_db_name()) + klass.db = Database(random_db_name()) klass.db.create(q=1, n=3) def setUp(self): |