| % 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(chttpd_changes_test). |
| |
| -include_lib("couch/include/couch_db.hrl"). |
| -include_lib("couch/include/couch_eunit.hrl"). |
| |
| -define(USER, "chttpd_changes_test_admin"). |
| -define(PASS, "pass"). |
| -define(AUTH, {basic_auth, {?USER, ?PASS}}). |
| -define(JSON, {"Content-Type", "application/json"}). |
| |
| -define(DOC1, <<"doc1">>). |
| -define(DDOC2, <<"_design/doc2">>). |
| -define(DOC3, <<"doc3">>). |
| -define(REVA, <<"a">>). |
| -define(REVB, <<"b">>). |
| -define(REVC, <<"c">>). |
| -define(REVD, <<"d">>). |
| -define(DELETED, true). |
| -define(LEAFREV, false). |
| |
| % doc1 starts as rev-a, then gets 2 conflicting revisions b and c |
| % ddoc2 starts as deleted at rev-a, then gets re-created as rev-c |
| % doc3 starts as rev-a, then gets deleted as rev-c |
| % |
| test_docs() -> |
| [ |
| {?DOC1, [?REVA], ?LEAFREV}, |
| {?DDOC2, [?REVA], ?DELETED}, |
| {?DOC3, [?REVA], ?LEAFREV}, |
| {?DOC1, [?REVB, ?REVA], ?LEAFREV}, |
| {?DOC1, [?REVC, ?REVA], ?LEAFREV}, |
| {?DOC1, [?REVD, ?REVA], ?DELETED}, |
| {?DOC3, [?REVB, ?REVA], ?DELETED}, |
| {?DDOC2, [?REVC, ?REVA], ?LEAFREV} |
| ]. |
| |
| % These are run against a Q=1, N=1 db, so we can make |
| % some stronger assumptions about the exact Seq prefixes |
| % returned sequences will have |
| % |
| changes_test_() -> |
| { |
| setup, |
| fun setup_basic/0, |
| fun teardown_basic/1, |
| with([ |
| ?TDEF(t_basic), |
| ?TDEF(t_basic_post), |
| ?TDEF(t_continuous), |
| ?TDEF(t_continuous_zero_timeout), |
| ?TDEF(t_longpoll), |
| ?TDEF(t_limit_zero), |
| ?TDEF(t_continuous_limit_zero), |
| ?TDEF(t_continuous_limit_under_total), |
| ?TDEF(t_continuous_limit_exact_total), |
| ?TDEF(t_continuous_limit_over_total), |
| ?TDEF(t_limit_one), |
| ?TDEF(t_since_now), |
| ?TDEF(t_continuous_since_now), |
| ?TDEF(t_longpoll_since_now), |
| ?TDEF(t_style_all_docs), |
| ?TDEF(t_reverse), |
| ?TDEF(t_continuous_reverse), |
| ?TDEF(t_continuous_reverse_limit_0), |
| ?TDEF(t_continuous_reverse_limit_under_total), |
| ?TDEF(t_continuous_reverse_limit_exact_total), |
| ?TDEF(t_continuous_reverse_limit_over_total), |
| ?TDEF(t_reverse_limit_zero), |
| ?TDEF(t_reverse_limit_one), |
| ?TDEF(t_seq_interval), |
| ?TDEF(t_selector_filter), |
| ?TDEF(t_design_filter), |
| ?TDEF(t_docs_id_filter), |
| ?TDEF(t_docs_id_filter_over_limit) |
| ]) |
| }. |
| |
| % For Q=8 sharded dbs, unlike Q=1, we cannot make strong |
| % assumptions about the exact sequence IDs for each row |
| % so we'll test all the changes return and that the sequences |
| % are increasing. |
| % |
| changes_q8_test_() -> |
| { |
| setup, |
| fun setup_q8/0, |
| fun teardown_basic/1, |
| with([ |
| ?TDEF(t_basic_q8), |
| ?TDEF(t_continuous_q8), |
| ?TDEF(t_limit_zero), |
| ?TDEF(t_limit_one_q8), |
| ?TDEF(t_since_now), |
| ?TDEF(t_longpoll_since_now), |
| ?TDEF(t_reverse_q8), |
| ?TDEF(t_reverse_limit_zero), |
| ?TDEF(t_reverse_limit_one_q8), |
| ?TDEF(t_selector_filter), |
| ?TDEF(t_design_filter), |
| ?TDEF(t_docs_id_filter_q8) |
| ]) |
| }. |
| |
| % These tests are separate as they create additional design docs during |
| % the test. That ends up bumping the update sequence in the db, so |
| % last_seq and other sequences returned become dependent on the test |
| % order. To avoid that dependence, run them in a separate suite with |
| % a foreach construct instead of a setup one. This way setup/teardown |
| % happens for each individual test case. |
| % |
| changes_js_filters_test_() -> |
| { |
| foreach, |
| fun setup_basic/0, |
| fun teardown_basic/1, |
| [ |
| ?TDEF_FE(t_js_filter), |
| ?TDEF_FE(t_js_filter_no_match), |
| ?TDEF_FE(t_js_filter_with_query_param), |
| ?TDEF_FE(t_view_filter), |
| ?TDEF_FE(t_view_filter_no_match) |
| ] |
| }. |
| |
| changes_include_docs_test_() -> |
| { |
| foreach, |
| fun setup_basic/0, |
| fun teardown_basic/1, |
| [ |
| ?TDEF_FE(t_include_docs), |
| ?TDEF_FE(t_all_docs_include_docs), |
| ?TDEF_FE(t_selector_include_docs), |
| ?TDEF_FE(t_selector_all_docs_include_docs), |
| ?TDEF_FE(t_conflicts), |
| ?TDEF_FE(t_conflicts_all_docs), |
| ?TDEF_FE(t_conflicts_include_docs), |
| ?TDEF_FE(t_conflicts_all_docs_include_docs), |
| ?TDEF_FE(t_conflicts_selector_include_docs), |
| ?TDEF_FE(t_conflicts_selector_all_docs_include_docs), |
| ?TDEF_FE(t_js_filter_include_docs), |
| ?TDEF_FE(t_js_filter_conflicts_include_docs), |
| ?TDEF_FE(t_js_filter_all_docs_conflicts_include_docs1), |
| ?TDEF_FE(t_js_filter_all_docs_conflicts_include_docs2), |
| ?TDEF_FE(t_view_include_docs), |
| ?TDEF_FE(t_view_conflicts_include_docs), |
| ?TDEF_FE(t_view_all_docs_conflicts_include_docs) |
| ] |
| }. |
| |
| changes_open_doc_times_test_() -> |
| { |
| foreach, |
| fun setup_basic/0, |
| fun teardown_basic/1, |
| [ |
| ?TDEF_FE(t_selector_open_doc_times), |
| ?TDEF_FE(t_js_filter_open_doc_times), |
| ?TDEF_FE(t_view_open_doc_times) |
| ] |
| }. |
| |
| t_basic({_, DbUrl}) -> |
| Res = {Seq, Pending, Rows} = changes(DbUrl), |
| ?assertEqual(8, Seq), |
| ?assertEqual(0, Pending), |
| ?assertEqual( |
| [ |
| {6, {?DOC1, <<"2-c">>}, ?LEAFREV}, |
| {7, {?DOC3, <<"2-b">>}, ?DELETED}, |
| {8, {?DDOC2, <<"2-c">>}, ?LEAFREV} |
| ], |
| Rows |
| ), |
| % since=0 is the default, so it should look exactly the same |
| ?assertEqual(Res, changes(DbUrl, "?since=0")). |
| |
| t_basic_q8({_, DbUrl}) -> |
| {Seq, Pending, Rows} = changes(DbUrl), |
| ?assertEqual(8, Seq), |
| ?assertEqual(0, Pending), |
| {Seqs, Revs, _Deleted} = lists:unzip3(Rows), |
| ?assertEqual( |
| [ |
| {?DDOC2, <<"2-c">>}, |
| {?DOC1, <<"2-c">>}, |
| {?DOC3, <<"2-b">>} |
| ], |
| lists:sort(Revs) |
| ), |
| ?assertEqual(Seqs, lists:sort(Seqs)). |
| |
| t_basic_post({_, DbUrl}) -> |
| {Seq, Pending, Rows} = changes_post(DbUrl, #{}), |
| ?assertEqual(8, Seq), |
| ?assertEqual(0, Pending), |
| ?assertEqual( |
| [ |
| {6, {?DOC1, <<"2-c">>}, ?LEAFREV}, |
| {7, {?DOC3, <<"2-b">>}, ?DELETED}, |
| {8, {?DDOC2, <<"2-c">>}, ?LEAFREV} |
| ], |
| Rows |
| ). |
| |
| t_continuous({_, DbUrl}) -> |
| Params = "?feed=continuous&timeout=10", |
| {Seq, Pending, Rows} = changes(DbUrl, Params), |
| ?assertEqual(8, Seq), |
| ?assertEqual(0, Pending), |
| ?assertEqual( |
| [ |
| {6, {?DOC1, <<"2-c">>}, ?LEAFREV}, |
| {7, {?DOC3, <<"2-b">>}, ?DELETED}, |
| {8, {?DDOC2, <<"2-c">>}, ?LEAFREV} |
| ], |
| Rows |
| ). |
| |
| t_continuous_q8({_, DbUrl}) -> |
| Params = "?feed=continuous&timeout=10", |
| {Seq, Pending, Rows} = changes(DbUrl, Params), |
| ?assertEqual(8, Seq), |
| ?assertEqual(0, Pending), |
| {Seqs, Revs, _Deleted} = lists:unzip3(Rows), |
| ?assertEqual( |
| [ |
| {?DDOC2, <<"2-c">>}, |
| {?DOC1, <<"2-c">>}, |
| {?DOC3, <<"2-b">>} |
| ], |
| lists:sort(Revs) |
| ), |
| ?assertEqual(Seqs, lists:sort(Seqs)). |
| |
| t_continuous_zero_timeout({_, DbUrl}) -> |
| Params = "?feed=continuous&timeout=0", |
| {Seq, Pending, Rows} = changes(DbUrl, Params), |
| ?assertEqual(8, Seq), |
| ?assertEqual(0, Pending), |
| ?assertEqual( |
| [ |
| {6, {?DOC1, <<"2-c">>}, ?LEAFREV}, |
| {7, {?DOC3, <<"2-b">>}, ?DELETED}, |
| {8, {?DDOC2, <<"2-c">>}, ?LEAFREV} |
| ], |
| Rows |
| ). |
| |
| t_longpoll({_, DbUrl}) -> |
| Params = "?feed=longpoll", |
| {Seq, Pending, Rows} = changes(DbUrl, Params), |
| ?assertEqual(8, Seq), |
| ?assertEqual(0, Pending), |
| ?assertEqual( |
| [ |
| {6, {?DOC1, <<"2-c">>}, ?LEAFREV}, |
| {7, {?DOC3, <<"2-b">>}, ?DELETED}, |
| {8, {?DDOC2, <<"2-c">>}, ?LEAFREV} |
| ], |
| Rows |
| ). |
| |
| t_limit_zero({_, DbUrl}) -> |
| Params = "?limit=0", |
| ?assertEqual({0, 3, []}, changes(DbUrl, Params)). |
| |
| t_continuous_limit_zero({_, DbUrl}) -> |
| Params = "?feed=continuous&limit=0", |
| ?assertEqual({0, 3, []}, changes(DbUrl, Params)). |
| |
| t_continuous_limit_under_total({_, DbUrl}) -> |
| Params = "?feed=continuous&&limit=1", |
| ?assertEqual({6, 2, [{6, {?DOC1, <<"2-c">>}, ?LEAFREV}]}, changes(DbUrl, Params)). |
| |
| t_continuous_limit_exact_total({_, DbUrl}) -> |
| Params = "?feed=continuous&limit=3", |
| ?assertMatch({8, 0, [{6, _, _}, {7, _, _}, {8, _, _}]}, changes(DbUrl, Params)). |
| |
| t_continuous_limit_over_total({_, DbUrl}) -> |
| Params = "?feed=continuous&timeout=600&limit=4", |
| T0 = erlang:monotonic_time(millisecond), |
| ?assertMatch({8, 0, [_, _, _]}, changes(DbUrl, Params)), |
| DtMSec = erlang:monotonic_time(millisecond) - T0, |
| ?assert(DtMSec > 500). |
| |
| t_limit_one({_, DbUrl}) -> |
| Params = "?limit=1", |
| ?assertEqual( |
| {6, 2, [ |
| {6, {?DOC1, <<"2-c">>}, ?LEAFREV} |
| ]}, |
| changes(DbUrl, Params) |
| ). |
| |
| t_limit_one_q8({_, DbUrl}) -> |
| Params = "?limit=1", |
| ?assertMatch( |
| {_, _, [ |
| {_, {<<_/binary>>, <<_/binary>>}, _} |
| ]}, |
| changes(DbUrl, Params) |
| ). |
| |
| t_style_all_docs({_, DbUrl}) -> |
| Params = "?style=all_docs", |
| {Seq, Pending, Rows} = changes(DbUrl, Params), |
| ?assertEqual(8, Seq), |
| ?assertEqual(0, Pending), |
| ?assertEqual( |
| [ |
| {6, {?DOC1, [<<"2-c">>, <<"2-b">>, <<"2-d">>]}, ?LEAFREV}, |
| {7, {?DOC3, <<"2-b">>}, ?DELETED}, |
| {8, {?DDOC2, <<"2-c">>}, ?LEAFREV} |
| ], |
| Rows |
| ). |
| |
| t_since_now({_, DbUrl}) -> |
| Params = "?since=now", |
| ?assertEqual({8, 0, []}, changes(DbUrl, Params)). |
| |
| t_continuous_since_now({_, DbUrl}) -> |
| Params = "?feed=continuous&timeout=10&since=now", |
| ?assertEqual({8, 0, []}, changes(DbUrl, Params)). |
| |
| t_longpoll_since_now({_, DbUrl}) -> |
| Params = "?feed=longpoll&timeout=10&since=now", |
| ?assertEqual({8, 0, []}, changes(DbUrl, Params)). |
| |
| t_reverse({_, DbUrl}) -> |
| Params = "?descending=true", |
| {Seq, Pending, Rows} = changes(DbUrl, Params), |
| ?assertEqual(6, Seq), |
| ?assertEqual(0, Pending), |
| ?assertEqual( |
| [ |
| {8, {?DDOC2, <<"2-c">>}, ?LEAFREV}, |
| {7, {?DOC3, <<"2-b">>}, ?DELETED}, |
| {6, {?DOC1, <<"2-c">>}, ?LEAFREV} |
| ], |
| Rows |
| ). |
| |
| t_continuous_reverse({_, DbUrl}) -> |
| Params = "?feed=continuous&timeout=10&descending=true", |
| {Seq, Pending, Rows} = changes(DbUrl, Params), |
| ?assertEqual(6, Seq), |
| ?assertEqual(0, Pending), |
| ?assertEqual( |
| [ |
| {8, {?DDOC2, <<"2-c">>}, ?LEAFREV}, |
| {7, {?DOC3, <<"2-b">>}, ?DELETED}, |
| {6, {?DOC1, <<"2-c">>}, ?LEAFREV} |
| ], |
| Rows |
| ). |
| |
| t_continuous_reverse_limit_0({_, DbUrl}) -> |
| Params = "?feed=continuous&descending=true&limit=0", |
| ?assertEqual({8, 3, []}, changes(DbUrl, Params)). |
| |
| t_continuous_reverse_limit_under_total({_, DbUrl}) -> |
| Params = "?feed=continuous&descending=true&limit=1", |
| ?assertMatch({8, 2, [{8, _, _}]}, changes(DbUrl, Params)). |
| |
| t_continuous_reverse_limit_exact_total({_, DbUrl}) -> |
| Params = "?feed=continuous&descending=true&limit=3", |
| ?assertMatch({6, 0, [{8, _, _}, {7, _, _}, {6, _, _}]}, changes(DbUrl, Params)). |
| |
| t_continuous_reverse_limit_over_total({_, DbUrl}) -> |
| Params = "?feed=continuous&descending=true&limit=999", |
| ?assertMatch({6, 0, [{8, _, _}, {7, _, _}, {6, _, _}]}, changes(DbUrl, Params)). |
| |
| t_reverse_q8({_, DbUrl}) -> |
| Params = "?descending=true", |
| {Seq, Pending, Rows} = changes(DbUrl, Params), |
| ?assertEqual(8, Seq), |
| ?assertEqual(0, Pending), |
| {Seqs, Revs, _Deleted} = lists:unzip3(Rows), |
| ?assertEqual( |
| [ |
| {?DDOC2, <<"2-c">>}, |
| {?DOC1, <<"2-c">>}, |
| {?DOC3, <<"2-b">>} |
| ], |
| lists:sort(Revs) |
| ), |
| ?assertEqual(Seqs, lists:sort(Seqs)). |
| |
| t_reverse_limit_zero({_, DbUrl}) -> |
| Params = "?descending=true&limit=0", |
| ?assertEqual({8, 3, []}, changes(DbUrl, Params)). |
| |
| t_reverse_limit_one({_, DbUrl}) -> |
| Params = "?descending=true&limit=1", |
| ?assertEqual( |
| {8, 2, [ |
| {8, {?DDOC2, <<"2-c">>}, ?LEAFREV} |
| ]}, |
| changes(DbUrl, Params) |
| ). |
| |
| t_reverse_limit_one_q8({_, DbUrl}) -> |
| Params = "?descending=true&limit=1", |
| ?assertMatch( |
| {8, 2, [ |
| {_, {<<_/binary>>, <<_/binary>>}, _} |
| ]}, |
| changes(DbUrl, Params) |
| ). |
| |
| t_seq_interval({_, DbUrl}) -> |
| Params = "?seq_interval=3", |
| {Seq, Pending, Rows} = changes(DbUrl, Params), |
| ?assertEqual(8, Seq), |
| ?assertEqual(0, Pending), |
| ?assertEqual( |
| [ |
| {null, {?DOC1, <<"2-c">>}, ?LEAFREV}, |
| {7, {?DOC3, <<"2-b">>}, ?DELETED}, |
| {null, {?DDOC2, <<"2-c">>}, ?LEAFREV} |
| ], |
| Rows |
| ). |
| |
| t_selector_filter({_, DbUrl}) -> |
| Params = "?filter=_selector", |
| Body = #{<<"selector">> => #{<<"_id">> => ?DOC1}}, |
| {Seq, Pending, Rows} = changes_post(DbUrl, Body, Params), |
| ?assertEqual(8, Seq), |
| ?assertEqual(0, Pending), |
| ?assertMatch([{_, {?DOC1, <<"2-c">>}, ?LEAFREV}], Rows). |
| |
| t_design_filter({_, DbUrl}) -> |
| Params = "?filter=_design", |
| {Seq, Pending, Rows} = changes(DbUrl, Params), |
| ?assertEqual(8, Seq), |
| ?assertEqual(2, Pending), |
| ?assertMatch([{_, {?DDOC2, <<"2-c">>}, ?LEAFREV}], Rows). |
| |
| t_docs_id_filter({_, DbUrl}) -> |
| Params = "?filter=_doc_ids", |
| Body = #{<<"doc_ids">> => [?DOC3, ?DOC1]}, |
| meck:reset(couch_changes), |
| {_, _, Rows} = changes_post(DbUrl, Body, Params), |
| ?assertEqual(1, meck:num_calls(couch_changes, send_changes_doc_ids, 6)), |
| ?assertEqual( |
| [ |
| {6, {?DOC1, <<"2-c">>}, ?LEAFREV}, |
| {7, {?DOC3, <<"2-b">>}, ?DELETED} |
| ], |
| Rows |
| ). |
| |
| t_docs_id_filter_q8({_, DbUrl}) -> |
| Params = "?filter=_doc_ids", |
| Body = #{<<"doc_ids">> => [?DOC3, ?DOC1]}, |
| {_, _, Rows} = changes_post(DbUrl, Body, Params), |
| {Seqs, Revs, _Deleted} = lists:unzip3(Rows), |
| ?assertEqual( |
| [ |
| {?DOC1, <<"2-c">>}, |
| {?DOC3, <<"2-b">>} |
| ], |
| lists:sort(Revs) |
| ), |
| ?assertEqual(Seqs, lists:sort(Seqs)). |
| |
| t_docs_id_filter_over_limit({_, DbUrl}) -> |
| Params = "?filter=_doc_ids", |
| Body = #{<<"doc_ids">> => [<<"missingdoc">>, ?DOC3, <<"notthere">>, ?DOC1]}, |
| meck:reset(couch_changes), |
| {_, _, Rows} = changes_post(DbUrl, Body, Params), |
| ?assertEqual(0, meck:num_calls(couch_changes, send_changes_doc_ids, 6)), |
| ?assertEqual( |
| [ |
| {6, {?DOC1, <<"2-c">>}, ?LEAFREV}, |
| {7, {?DOC3, <<"2-b">>}, ?DELETED} |
| ], |
| Rows |
| ). |
| |
| t_js_filter({_, DbUrl}) -> |
| DDocId = "_design/filters", |
| FilterFun = <<"function(doc, req) {return (doc._id == 'doc3')}">>, |
| DDoc = #{<<"filters">> => #{<<"f">> => FilterFun}}, |
| DDocUrl = DbUrl ++ "/" ++ DDocId, |
| {_, #{<<"rev">> := Rev, <<"ok">> := true}} = req(put, DDocUrl, DDoc), |
| Params = "?filter=filters/f", |
| {Seq, Pending, Rows} = changes(DbUrl, Params), |
| ?assertEqual(9, Seq), |
| ?assertEqual(0, Pending), |
| ?assertEqual( |
| [ |
| {7, {?DOC3, <<"2-b">>}, ?DELETED} |
| ], |
| Rows |
| ), |
| delete_ddocs(DDocUrl, Rev). |
| |
| t_js_filter_no_match({_, DbUrl}) -> |
| DDocId = "_design/filters", |
| FilterFun = <<"function(doc, req) {return false}">>, |
| DDoc = #{<<"filters">> => #{<<"f">> => FilterFun}}, |
| DDocUrl = DbUrl ++ "/" ++ DDocId, |
| {_, #{<<"rev">> := Rev, <<"ok">> := true}} = req(put, DDocUrl, DDoc), |
| Params = "?filter=filters/f", |
| ?assertEqual({9, 0, []}, changes(DbUrl, Params)), |
| delete_ddocs(DDocUrl, Rev). |
| |
| t_js_filter_with_query_param({_, DbUrl}) -> |
| DDocId = "_design/filters", |
| FilterFun = <<"function(doc, req) {return (req.query.yup == 1)}">>, |
| DDoc = #{<<"filters">> => #{<<"f">> => FilterFun}}, |
| DDocUrl = DbUrl ++ "/" ++ DDocId, |
| {_, #{<<"rev">> := Rev, <<"ok">> := true}} = req(put, DDocUrl, DDoc), |
| Params = "?filter=filters/f&yup=1", |
| {Seq, Pending, Rows} = changes(DbUrl, Params), |
| ?assertEqual(9, Seq), |
| ?assertEqual(0, Pending), |
| ?assertMatch( |
| [ |
| {6, {?DOC1, <<"2-c">>}, ?LEAFREV}, |
| {7, {?DOC3, <<"2-b">>}, ?DELETED}, |
| {8, {?DDOC2, <<"2-c">>}, ?LEAFREV}, |
| {9, {<<"_design/filters">>, <<"1-", _/binary>>}, ?LEAFREV} |
| ], |
| Rows |
| ), |
| delete_ddocs(DDocUrl, Rev). |
| |
| t_view_filter({_, DbUrl}) -> |
| {DDocUrl, Rev} = create_ddocs(DbUrl, ?DOC1, view), |
| Params = "?filter=_view&view=views/v", |
| {Seq, Pending, Rows} = changes(DbUrl, Params), |
| ?assertEqual(9, Seq), |
| ?assertEqual(0, Pending), |
| ?assertEqual([{6, {?DOC1, <<"2-c">>}, ?LEAFREV}], Rows), |
| delete_ddocs(DDocUrl, Rev). |
| |
| t_view_filter_no_match({_, DbUrl}) -> |
| {DDocUrl, Rev} = create_ddocs(DbUrl, <<"docX">>, view), |
| Params = "?filter=_view&view=views/v", |
| ?assertEqual({9, 0, []}, changes(DbUrl, Params)), |
| delete_ddocs(DDocUrl, Rev). |
| |
| t_include_docs({_, DbUrl}) -> |
| Params = "?include_docs=true", |
| {200, #{<<"results">> := Res1}} = req(get, DbUrl ++ "/_changes" ++ Params), |
| ?assertMatch( |
| [ |
| #{ |
| <<"changes">> := [#{<<"rev">> := <<"2-c">>}], |
| <<"doc">> := #{<<"_id">> := ?DOC1, <<"_rev">> := <<"2-c">>} |
| }, |
| #{ |
| <<"changes">> := [#{<<"rev">> := <<"2-b">>}], |
| <<"deleted">> := ?DELETED, |
| <<"doc">> := #{ |
| <<"_id">> := ?DOC3, <<"_rev">> := <<"2-b">>, <<"_deleted">> := ?DELETED |
| } |
| }, |
| #{ |
| <<"changes">> := [#{<<"rev">> := <<"2-c">>}], |
| <<"doc">> := #{<<"_id">> := ?DDOC2, <<"_rev">> := <<"2-c">>} |
| } |
| ], |
| Res1 |
| ), |
| {200, #{<<"results">> := Res2}} = req(post, DbUrl ++ "/_changes" ++ Params), |
| ?assertEqual(Res1, Res2). |
| |
| t_all_docs_include_docs({_, DbUrl}) -> |
| Params = "?style=all_docs&include_docs=true", |
| {200, #{<<"results">> := Res1}} = req(get, DbUrl ++ "/_changes" ++ Params), |
| ?assertMatch( |
| [ |
| #{ |
| <<"changes">> := [ |
| #{<<"rev">> := <<"2-c">>}, #{<<"rev">> := <<"2-b">>}, #{<<"rev">> := <<"2-d">>} |
| ], |
| <<"doc">> := #{<<"_id">> := ?DOC1, <<"_rev">> := <<"2-c">>} |
| }, |
| #{ |
| <<"changes">> := [#{<<"rev">> := <<"2-b">>}], |
| <<"deleted">> := ?DELETED, |
| <<"doc">> := #{ |
| <<"_id">> := ?DOC3, <<"_rev">> := <<"2-b">>, <<"_deleted">> := ?DELETED |
| } |
| }, |
| #{ |
| <<"changes">> := [#{<<"rev">> := <<"2-c">>}], |
| <<"doc">> := #{<<"_id">> := ?DDOC2, <<"_rev">> := <<"2-c">>} |
| } |
| ], |
| Res1 |
| ), |
| {200, #{<<"results">> := Res2}} = req(post, DbUrl ++ "/_changes" ++ Params), |
| ?assertEqual(Res1, Res2). |
| |
| t_selector_include_docs({_, DbUrl}) -> |
| Body = #{<<"selector">> => #{<<"_id">> => ?DOC1}}, |
| Params = "?filter=_selector&include_docs=true", |
| {200, #{<<"results">> := Res}} = req(post, DbUrl ++ "/_changes" ++ Params, Body), |
| ?assertMatch( |
| [ |
| #{ |
| <<"changes">> := [#{<<"rev">> := <<"2-c">>}], |
| <<"doc">> := #{<<"_id">> := ?DOC1, <<"_rev">> := <<"2-c">>} |
| } |
| ], |
| Res |
| ). |
| |
| t_selector_all_docs_include_docs({_, DbUrl}) -> |
| Body = #{<<"selector">> => #{<<"_id">> => ?DOC1}}, |
| Params = "?filter=_selector&style=all_docs&include_docs=true", |
| {200, #{<<"results">> := Res}} = req(post, DbUrl ++ "/_changes" ++ Params, Body), |
| ?assertMatch( |
| [ |
| #{ |
| <<"changes">> := [ |
| #{<<"rev">> := <<"2-c">>}, #{<<"rev">> := <<"2-b">>}, #{<<"rev">> := <<"2-d">>} |
| ], |
| <<"doc">> := #{<<"_id">> := ?DOC1, <<"_rev">> := <<"2-c">>} |
| } |
| ], |
| Res |
| ). |
| |
| t_conflicts({_, DbUrl}) -> |
| Params = "?conflicts=true", |
| {200, #{<<"results">> := Res1}} = req(get, DbUrl ++ "/_changes" ++ Params), |
| ?assertMatch( |
| [ |
| #{<<"changes">> := [#{<<"rev">> := <<"2-c">>}]}, |
| #{<<"changes">> := [#{<<"rev">> := <<"2-b">>}]}, |
| #{<<"changes">> := [#{<<"rev">> := <<"2-c">>}]} |
| ], |
| Res1 |
| ), |
| {200, #{<<"results">> := Res2}} = req(post, DbUrl ++ "/_changes" ++ Params), |
| ?assertEqual(Res1, Res2). |
| |
| t_conflicts_all_docs({_, DbUrl}) -> |
| Params = "?conflicts=true&style=all_docs", |
| {200, #{<<"results">> := Res1}} = req(get, DbUrl ++ "/_changes" ++ Params), |
| ?assertMatch( |
| [ |
| #{ |
| <<"changes">> := [ |
| #{<<"rev">> := <<"2-c">>}, #{<<"rev">> := <<"2-b">>}, #{<<"rev">> := <<"2-d">>} |
| ] |
| }, |
| #{<<"changes">> := [#{<<"rev">> := <<"2-b">>}]}, |
| #{<<"changes">> := [#{<<"rev">> := <<"2-c">>}]} |
| ], |
| Res1 |
| ), |
| {200, #{<<"results">> := Res2}} = req(post, DbUrl ++ "/_changes" ++ Params), |
| ?assertEqual(Res1, Res2). |
| |
| t_conflicts_include_docs({_, DbUrl}) -> |
| Params = "?conflicts=true&include_docs=true", |
| {200, #{<<"results">> := Res1}} = req(get, DbUrl ++ "/_changes" ++ Params), |
| ?assertMatch( |
| [ |
| #{ |
| <<"changes">> := [#{<<"rev">> := <<"2-c">>}], |
| <<"doc">> := #{ |
| <<"_id">> := ?DOC1, <<"_rev">> := <<"2-c">>, <<"_conflicts">> := [<<"2-b">>] |
| } |
| }, |
| #{ |
| <<"changes">> := [#{<<"rev">> := <<"2-b">>}], |
| <<"deleted">> := ?DELETED, |
| <<"doc">> := #{ |
| <<"_id">> := ?DOC3, <<"_rev">> := <<"2-b">>, <<"_deleted">> := ?DELETED |
| } |
| }, |
| #{ |
| <<"changes">> := [#{<<"rev">> := <<"2-c">>}], |
| <<"doc">> := #{<<"_id">> := ?DDOC2, <<"_rev">> := <<"2-c">>} |
| } |
| ], |
| Res1 |
| ), |
| {200, #{<<"results">> := Res2}} = req(post, DbUrl ++ "/_changes" ++ Params), |
| ?assertEqual(Res1, Res2). |
| |
| t_conflicts_all_docs_include_docs({_, DbUrl}) -> |
| Params = "?conflicts=true&style=all_docs&include_docs=true", |
| {200, #{<<"results">> := Res1}} = req(get, DbUrl ++ "/_changes" ++ Params), |
| ?assertMatch( |
| [ |
| #{ |
| <<"changes">> := [ |
| #{<<"rev">> := <<"2-c">>}, #{<<"rev">> := <<"2-b">>}, #{<<"rev">> := <<"2-d">>} |
| ], |
| <<"doc">> := #{ |
| <<"_id">> := ?DOC1, <<"_rev">> := <<"2-c">>, <<"_conflicts">> := [<<"2-b">>] |
| } |
| }, |
| #{ |
| <<"changes">> := [#{<<"rev">> := <<"2-b">>}], |
| <<"deleted">> := ?DELETED, |
| <<"doc">> := #{ |
| <<"_id">> := ?DOC3, <<"_rev">> := <<"2-b">>, <<"_deleted">> := ?DELETED |
| } |
| }, |
| #{ |
| <<"changes">> := [#{<<"rev">> := <<"2-c">>}], |
| <<"doc">> := #{<<"_id">> := ?DDOC2, <<"_rev">> := <<"2-c">>} |
| } |
| ], |
| Res1 |
| ), |
| {200, #{<<"results">> := Res2}} = req(post, DbUrl ++ "/_changes" ++ Params), |
| ?assertEqual(Res1, Res2). |
| |
| t_conflicts_selector_include_docs({_, DbUrl}) -> |
| Body = #{<<"selector">> => #{<<"_id">> => ?DOC1}}, |
| Params = "?conflicts=true&filter=_selector&include_docs=true", |
| {200, #{<<"results">> := Res}} = req(post, DbUrl ++ "/_changes" ++ Params, Body), |
| ?assertMatch( |
| [ |
| #{ |
| <<"changes">> := [#{<<"rev">> := <<"2-c">>}], |
| <<"doc">> := #{ |
| <<"_id">> := ?DOC1, <<"_rev">> := <<"2-c">>, <<"_conflicts">> := [<<"2-b">>] |
| } |
| } |
| ], |
| Res |
| ). |
| |
| t_conflicts_selector_all_docs_include_docs({_, DbUrl}) -> |
| Body = #{<<"selector">> => #{<<"_id">> => ?DOC1}}, |
| Params = "?conflicts=true&filter=_selector&style=all_docs&include_docs=true", |
| {200, #{<<"results">> := Res}} = req(post, DbUrl ++ "/_changes" ++ Params, Body), |
| ?assertMatch( |
| [ |
| #{ |
| <<"changes">> := [ |
| #{<<"rev">> := <<"2-c">>}, #{<<"rev">> := <<"2-b">>}, #{<<"rev">> := <<"2-d">>} |
| ], |
| <<"doc">> := #{ |
| <<"_id">> := ?DOC1, <<"_rev">> := <<"2-c">>, <<"_conflicts">> := [<<"2-b">>] |
| } |
| } |
| ], |
| Res |
| ). |
| |
| t_js_filter_include_docs({_, DbUrl}) -> |
| {DDocUrl, Rev} = create_ddocs(DbUrl, ?DOC3, custom), |
| Params = "?filter=filters/f&include_docs=true", |
| {200, #{<<"results">> := Res1}} = req(get, DbUrl ++ "/_changes" ++ Params), |
| ?assertMatch( |
| [ |
| #{ |
| <<"changes">> := [#{<<"rev">> := <<"2-b">>}], |
| <<"deleted">> := ?DELETED, |
| <<"doc">> := #{ |
| <<"_id">> := ?DOC3, <<"_rev">> := <<"2-b">>, <<"_deleted">> := ?DELETED |
| } |
| } |
| ], |
| Res1 |
| ), |
| {200, #{<<"results">> := Res2}} = req(post, DbUrl ++ "/_changes" ++ Params), |
| ?assertEqual(Res1, Res2), |
| delete_ddocs(DDocUrl, Rev). |
| |
| t_js_filter_conflicts_include_docs({_, DbUrl}) -> |
| {DDocUrl, Rev} = create_ddocs(DbUrl, ?DOC3, custom), |
| Params = "?filter=filters/f&conflicts=true&include_docs=true", |
| {200, #{<<"results">> := Res1}} = req(get, DbUrl ++ "/_changes" ++ Params), |
| ?assertMatch( |
| [ |
| #{ |
| <<"changes">> := [#{<<"rev">> := <<"2-b">>}], |
| <<"deleted">> := ?DELETED, |
| <<"doc">> := #{ |
| <<"_id">> := ?DOC3, <<"_rev">> := <<"2-b">>, <<"_deleted">> := ?DELETED |
| } |
| } |
| ], |
| Res1 |
| ), |
| {200, #{<<"results">> := Res2}} = req(post, DbUrl ++ "/_changes" ++ Params), |
| ?assertEqual(Res1, Res2), |
| delete_ddocs(DDocUrl, Rev). |
| |
| t_js_filter_all_docs_conflicts_include_docs1({_, DbUrl}) -> |
| {DDocUrl, Rev} = create_ddocs(DbUrl, ?DOC1, custom), |
| Params = "?filter=filters/f&style=all_docs&conflicts=true&include_docs=true", |
| {200, #{<<"results">> := Res1}} = req(get, DbUrl ++ "/_changes" ++ Params), |
| ?assertMatch( |
| [ |
| #{ |
| <<"changes">> := [ |
| #{<<"rev">> := <<"2-c">>}, #{<<"rev">> := <<"2-b">>}, #{<<"rev">> := <<"2-d">>} |
| ], |
| <<"doc">> := #{ |
| <<"_id">> := ?DOC1, <<"_rev">> := <<"2-c">>, <<"_conflicts">> := [<<"2-b">>] |
| } |
| } |
| ], |
| Res1 |
| ), |
| {200, #{<<"results">> := Res2}} = req(post, DbUrl ++ "/_changes" ++ Params), |
| ?assertEqual(Res1, Res2), |
| delete_ddocs(DDocUrl, Rev). |
| |
| t_js_filter_all_docs_conflicts_include_docs2({_, DbUrl}) -> |
| {DDocUrl, Rev} = create_ddocs(DbUrl, ?DOC3, custom), |
| Params = "?filter=filters/f&style=all_docs&conflicts=true&include_docs=true", |
| {200, #{<<"results">> := Res1}} = req(get, DbUrl ++ "/_changes" ++ Params), |
| ?assertMatch( |
| [ |
| #{ |
| <<"changes">> := [#{<<"rev">> := <<"2-b">>}], |
| <<"deleted">> := ?DELETED, |
| <<"doc">> := #{ |
| <<"_id">> := ?DOC3, <<"_rev">> := <<"2-b">>, <<"_deleted">> := ?DELETED |
| } |
| } |
| ], |
| Res1 |
| ), |
| {200, #{<<"results">> := Res2}} = req(post, DbUrl ++ "/_changes" ++ Params), |
| ?assertEqual(Res1, Res2), |
| delete_ddocs(DDocUrl, Rev). |
| |
| t_view_include_docs({_, DbUrl}) -> |
| {DDocUrl, Rev} = create_ddocs(DbUrl, ?DOC1, view), |
| Params = "?filter=_view&view=views/v&include_docs=true", |
| {200, #{<<"results">> := Res1}} = req(get, DbUrl ++ "/_changes" ++ Params), |
| ?assertMatch( |
| [ |
| #{ |
| <<"changes">> := [#{<<"rev">> := <<"2-c">>}], |
| <<"doc">> := #{<<"_id">> := ?DOC1, <<"_rev">> := <<"2-c">>} |
| } |
| ], |
| Res1 |
| ), |
| {200, #{<<"results">> := Res2}} = req(post, DbUrl ++ "/_changes" ++ Params), |
| ?assertEqual(Res1, Res2), |
| delete_ddocs(DDocUrl, Rev). |
| |
| t_view_conflicts_include_docs({_, DbUrl}) -> |
| {DDocUrl, Rev} = create_ddocs(DbUrl, ?DOC1, view), |
| Params = "?filter=_view&view=views/v&conflicts=true&include_docs=true", |
| {200, #{<<"results">> := Res1}} = req(get, DbUrl ++ "/_changes" ++ Params), |
| ?assertMatch( |
| [ |
| #{ |
| <<"changes">> := [#{<<"rev">> := <<"2-c">>}], |
| <<"doc">> := #{ |
| <<"_id">> := ?DOC1, <<"_rev">> := <<"2-c">>, <<"_conflicts">> := [<<"2-b">>] |
| } |
| } |
| ], |
| Res1 |
| ), |
| {200, #{<<"results">> := Res2}} = req(post, DbUrl ++ "/_changes" ++ Params), |
| ?assertEqual(Res1, Res2), |
| delete_ddocs(DDocUrl, Rev). |
| |
| t_view_all_docs_conflicts_include_docs({_, DbUrl}) -> |
| {DDocUrl, Rev} = create_ddocs(DbUrl, ?DOC1, view), |
| Params = "?filter=_view&view=views/v&style=all_docs&conflicts=true&include_docs=true", |
| {200, #{<<"results">> := Res1}} = req(get, DbUrl ++ "/_changes" ++ Params), |
| ?assertMatch( |
| [ |
| #{ |
| <<"changes">> := [ |
| #{<<"rev">> := <<"2-c">>}, #{<<"rev">> := <<"2-b">>}, #{<<"rev">> := <<"2-d">>} |
| ], |
| <<"doc">> := #{ |
| <<"_id">> := ?DOC1, <<"_rev">> := <<"2-c">>, <<"_conflicts">> := [<<"2-b">>] |
| } |
| } |
| ], |
| Res1 |
| ), |
| {200, #{<<"results">> := Res2}} = req(post, DbUrl ++ "/_changes" ++ Params), |
| ?assertEqual(Res1, Res2), |
| delete_ddocs(DDocUrl, Rev). |
| |
| t_selector_open_doc_times({_, DbUrl}) -> |
| Body = #{<<"selector">> => #{<<"_id">> => ?DOC1}}, |
| F = "?filter=_selector", |
| I = "&include_docs=true", |
| S = "&style=all_docs", |
| Filter = called_times(fun() -> changes_post(DbUrl, Body, F) end, DbUrl), |
| FilterDocs = called_times(fun() -> changes_post(DbUrl, Body, F ++ I) end, DbUrl), |
| FilterAllDocs = called_times(fun() -> changes_post(DbUrl, Body, F ++ I ++ S) end, DbUrl), |
| ?assertEqual({3, 3, 5}, {Filter, FilterDocs, FilterAllDocs}). |
| |
| t_js_filter_open_doc_times({_, DbUrl}) -> |
| {DDocUrl, Rev} = create_ddocs(DbUrl, ?DOC1, custom), |
| F = "?filter=filters/f", |
| I = "&include_docs=true", |
| S = "&style=all_docs", |
| Filter = called_times(fun() -> changes(DbUrl, F) end, DbUrl), |
| FilterDocs = called_times(fun() -> changes(DbUrl, F ++ I) end, DbUrl), |
| FilterAllDocs = called_times(fun() -> changes(DbUrl, F ++ I ++ S) end, DbUrl), |
| ?assertEqual({4, 4, 6}, {Filter, FilterDocs, FilterAllDocs}), |
| delete_ddocs(DDocUrl, Rev). |
| |
| t_view_open_doc_times({_, DbUrl}) -> |
| {DDocUrl, Rev} = create_ddocs(DbUrl, ?DOC1, view), |
| F = "?filter=_view&view=views/v", |
| I = "&include_docs=true", |
| S = "&style=all_docs", |
| Filter = called_times(fun() -> changes(DbUrl, F) end, DbUrl), |
| FilterDocs = called_times(fun() -> changes(DbUrl, F ++ I) end, DbUrl), |
| FilterAllDocs = called_times(fun() -> changes(DbUrl, F ++ S ++ I) end, DbUrl), |
| ?assertEqual({4, 4, 6}, {Filter, FilterDocs, FilterAllDocs}), |
| delete_ddocs(DDocUrl, Rev). |
| |
| % Utility functions |
| setup_ctx(DbCreateParams) -> |
| Ctx = test_util:start_couch([chttpd]), |
| Hashed = couch_passwords:hash_admin_password(?PASS), |
| ok = config:set("admins", ?USER, ?b2l(Hashed), _Persist = false), |
| Addr = config:get("chttpd", "bind_address", "127.0.0.1"), |
| Db = ?b2l(?tempdb()), |
| Port = mochiweb_socket_server:get(chttpd, port), |
| Url = lists:concat(["http://", Addr, ":", Port, "/"]), |
| ok = create_db(Url, Db, DbCreateParams), |
| {Ctx, Url ++ Db}. |
| |
| teardown_ctx({Ctx, DbUrl}) -> |
| meck:unload(), |
| delete_db(DbUrl), |
| ok = config:delete("admins", ?USER, _Persist = false), |
| test_util:stop_couch(Ctx). |
| |
| setup_q8() -> |
| {Ctx, DbUrl} = setup_ctx("?q=8"), |
| ok = create_docs(DbUrl, test_docs()), |
| {Ctx, DbUrl}. |
| |
| setup_basic() -> |
| {Ctx, DbUrl} = setup_ctx("?q=1"), |
| ok = create_docs(DbUrl, test_docs()), |
| CfgKey = "changes_doc_ids_optimization_threshold", |
| ok = config:set("couchdb", CfgKey, "2", _Persist = false), |
| meck:new(couch_changes, [passthrough]), |
| meck:new(couch_db, [passthrough]), |
| {Ctx, DbUrl}. |
| |
| teardown_basic({Ctx, DbUrl}) -> |
| CfgKey = "changes_doc_ids_optimization_threshold", |
| ok = config:delete("couchdb", CfgKey, _Persist = false), |
| teardown_ctx({Ctx, DbUrl}). |
| |
| create_db(Top, Db, Params) -> |
| {201, #{}} = req(put, Top ++ Db ++ Params), |
| ok. |
| |
| delete_db(DbUrl) -> |
| {200, #{}} = req(delete, DbUrl). |
| |
| doc_fun({Id, Revs, Deleted}) -> |
| Doc = #{ |
| <<"_id">> => Id, |
| <<"_revisions">> => #{ |
| <<"ids">> => Revs, |
| <<"start">> => length(Revs) |
| } |
| }, |
| case Deleted of |
| true -> Doc#{<<"_deleted">> => true}; |
| false -> Doc |
| end. |
| |
| create_docs(DbUrl, DocRevs) -> |
| lists:foreach( |
| fun(#{<<"_id">> := Id} = Doc) -> |
| Url = DbUrl ++ "/" ++ binary_to_list(Id), |
| {_, #{<<"ok">> := true}} = req(put, Url ++ "?new_edits=false", Doc) |
| end, |
| lists:map(fun doc_fun/1, DocRevs) |
| ). |
| |
| create_ddocs(DbUrl, DocId, Type) -> |
| case Type of |
| custom -> |
| DDocId = "_design/filters", |
| FilterFun = <<"function(doc, req) {return (doc._id == '", DocId/binary, "')}">>, |
| DDoc = #{<<"filters">> => #{<<"f">> => FilterFun}}; |
| view -> |
| DDocId = "_design/views", |
| ViewFun = <<"function(doc) {if (doc._id == '", DocId/binary, "') {emit(1, 1);}}">>, |
| DDoc = #{<<"views">> => #{<<"v">> => #{<<"map">> => ViewFun}}} |
| end, |
| DDocUrl = DbUrl ++ "/" ++ DDocId, |
| {_, #{<<"rev">> := Rev, <<"ok">> := true}} = req(put, DDocUrl, DDoc), |
| {DDocUrl, Rev}. |
| |
| delete_ddocs(DDocUrl, Rev) -> |
| {200, #{}} = req(delete, DDocUrl ++ "?rev=" ++ binary_to_list(Rev)). |
| |
| changes(DbUrl) -> |
| changes(DbUrl, ""). |
| |
| changes_post(DbUrl, #{} = Body) -> |
| changes_post(DbUrl, Body, ""). |
| |
| changes(DbUrl, Params) when is_list(Params) -> |
| {Code, Res} = reqraw(get, DbUrl ++ "/_changes" ++ Params), |
| ?assertEqual(200, Code), |
| res(Res, Params). |
| |
| changes_post(DbUrl, #{} = Body, Params) -> |
| {Code, Res} = reqraw(post, DbUrl ++ "/_changes" ++ Params, Body), |
| ?assertEqual(200, Code), |
| res(Res, Params). |
| |
| req(Method, Url) -> |
| {Code, Res} = reqraw(Method, Url), |
| {Code, json(Res)}. |
| |
| req(Method, Url, #{} = Body) -> |
| {Code, Res} = reqraw(Method, Url, Body), |
| {Code, json(Res)}. |
| |
| reqraw(Method, Url) -> |
| Headers = [?JSON, ?AUTH], |
| {ok, Code, _, Res} = test_request:request(Method, Url, Headers), |
| {Code, Res}. |
| |
| reqraw(Method, Url, #{} = Body) -> |
| reqraw(Method, Url, jiffy:encode(Body)); |
| reqraw(Method, Url, Body) -> |
| Headers = [?JSON, ?AUTH], |
| {ok, Code, _, Res} = test_request:request(Method, Url, Headers, Body), |
| {Code, Res}. |
| |
| json(Bin) when is_binary(Bin) -> |
| jiffy:decode(Bin, [return_maps]). |
| |
| jsonl(Bin) when is_binary(Bin) -> |
| Lines = [string:trim(L) || L <- binary:split(Bin, <<"\n">>, [global])], |
| [json(L) || L <- Lines, L =/= <<>>]. |
| |
| res(<<_/binary>> = Bin, Params) -> |
| Continuous = string:find(Params, "feed=continuous") =/= nomatch, |
| case Continuous of |
| true -> parse_response(jsonl(Bin)); |
| false -> parse_response(json(Bin)) |
| end. |
| |
| parse_response(#{} = Resp) -> |
| #{ |
| <<"last_seq">> := LastSeq, |
| <<"pending">> := Pending, |
| <<"results">> := Results |
| } = Resp, |
| Results1 = lists:map(fun parse_row/1, Results), |
| {seq(LastSeq), Pending, Results1}; |
| parse_response([#{} | _] = Lines) -> |
| #{<<"pending">> := Pending, <<"last_seq">> := LastSeq} = lists:last(Lines), |
| Results1 = lists:map(fun parse_row/1, lists:droplast(Lines)), |
| {seq(LastSeq), Pending, Results1}. |
| |
| parse_row(#{} = Row) -> |
| #{ |
| <<"changes">> := Revs, |
| <<"id">> := Id, |
| <<"seq">> := Seq |
| } = Row, |
| Revs1 = lists:map(fun(#{<<"rev">> := Rev}) -> Rev end, Revs), |
| Revs2 = |
| case Revs1 of |
| [Rev] -> Rev; |
| [_ | _] -> Revs1 |
| end, |
| case maps:get(<<"deleted">>, Row, false) of |
| true -> {seq(Seq), {Id, Revs2}, ?DELETED}; |
| false -> {seq(Seq), {Id, Revs2}, ?LEAFREV} |
| end. |
| |
| % This will be reliable for q=1 dbs only. |
| % |
| seq(<<_/binary>> = Seq) -> |
| [NumStr, _] = binary:split(Seq, <<"-">>), |
| binary_to_integer(NumStr); |
| seq(null) -> |
| null. |
| |
| called_times(ReqFun, DbUrl) -> |
| meck:reset(couch_db), |
| ReqFun(), |
| open_doc_calls(DbUrl). |
| |
| open_doc_calls(DbUrl) -> |
| #{path := "/" ++ DbName0} = uri_string:parse(DbUrl), |
| DbName = ?l2b(DbName0), |
| FoldFun = |
| fun([Db, IdOrDocInfo, _Opts], Acc) -> |
| case {mem3:dbname(couch_db:name(Db)), IdOrDocInfo} of |
| {DbName, #doc_info{}} -> Acc + 1; |
| _ -> Acc |
| end |
| end, |
| lists:foldl(FoldFun, 0, meck_history(couch_db, open_doc, 3)). |
| |
| meck_history(Mod, Fun, Arity) -> |
| [A || {_Pid, {_M, F, A}, _R} <- meck:history(Mod), F =:= Fun, length(A) =:= Arity]. |