This is an automated email from the ASF dual-hosted git repository. nickva pushed a commit to branch fix-validity-check-for-failed-purge-client-check in repository https://gitbox.apache.org/repos/asf/couchdb.git
commit 13fcb6661cdd884924895961264c0927c2cbadeb Author: Nick Vatamaniuc <[email protected]> AuthorDate: Mon Jul 13 16:25:22 2026 -0400 Strengthen purge client existence tests Previously, in some cases if we could not positively verify if a purge client existed (say we got a timeout from a clustered read) we'd assume the client didn't exist, when in fact the safest thing is to assume the clients exists until we get a definite reponse it doesn't To fix the issue we return `true` for client existence checks on unexpected errors when before we defaulted to `false`. --- src/couch_mrview/src/couch_mrview_index.erl | 18 +- .../test/eunit/couch_mrview_purge_client_tests.erl | 213 +++++++++++++++++++++ src/dreyfus/src/dreyfus_util.erl | 29 ++- .../test/eunit/dreyfus_purge_client_tests.erl | 142 ++++++++++++++ src/nouveau/src/nouveau_util.erl | 29 ++- .../test/eunit/nouveau_purge_client_tests.erl | 141 ++++++++++++++ 6 files changed, 555 insertions(+), 17 deletions(-) diff --git a/src/couch_mrview/src/couch_mrview_index.erl b/src/couch_mrview/src/couch_mrview_index.erl index f0a649b77..66600ef03 100644 --- a/src/couch_mrview/src/couch_mrview_index.erl +++ b/src/couch_mrview/src/couch_mrview_index.erl @@ -245,15 +245,27 @@ verify_index_exists(DbName, Props) -> DocSig = couch_util:get_value(<<"signature">>, Props), match_signatures(IdxSig, DocSig); {not_found, _} -> - false + false; + Else -> + cannot_verify(DbName, Props, Else) end end) end catch - _:_ -> - false + Tag:Reason -> + cannot_verify(DbName, Props, {Tag, Reason}) end. +% Couldn't verify index exists when checking purge client validity. There may +% be a timeout or or db is not available. In such cases assume the client is +% valid to avoid the chance of compacting away purge before valid clients saw +% and processed them. +cannot_verify(DbName, Props, Error) -> + DDocId = couch_util:get_value(<<"ddoc_id">>, Props), + Fmt = "~p : cannot verify purge client db:~p ddoc:~p error:~w, assume exists", + couch_log:warning(Fmt, [?MODULE, DbName, DDocId, Error]), + true. + match_signatures(IdxSig, DocSig) when is_binary(IdxSig), is_list(DocSig) -> % Compatibility clause. In versions =< 3.5.1 mvrview signatures in purge % checkpoints where written as lists of integers instead of a binary. After diff --git a/src/couch_mrview/test/eunit/couch_mrview_purge_client_tests.erl b/src/couch_mrview/test/eunit/couch_mrview_purge_client_tests.erl new file mode 100644 index 000000000..eb5a3fb8e --- /dev/null +++ b/src/couch_mrview/test/eunit/couch_mrview_purge_client_tests.erl @@ -0,0 +1,213 @@ +% 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_purge_client_tests). + +-include_lib("couch/include/couch_eunit.hrl"). +-include_lib("couch/include/couch_db.hrl"). +-include_lib("couch_mrview/include/couch_mrview.hrl"). + +-define(DDOC_ID, <<"_design/viewddoc">>). + +purge_client_verification_test_() -> + { + setup, + fun setup_all/0, + fun teardown_all/1, + { + foreach, + fun setup/0, + fun teardown/1, + [ + ?TDEF_FE(t_verify_matching_index), + ?TDEF_FE(t_verify_false_on_wrong_type), + ?TDEF_FE(t_verify_false_on_missing_ddoc), + ?TDEF_FE(t_verify_false_on_signature_mismatch), + ?TDEF_FE(t_verify_true_on_ddoc_read_error), + ?TDEF_FE(t_verify_true_on_exception), + ?TDEF_FE(t_verify_true_on_missing_db), + ?TDEF_FE(t_verify_true_on_failed_clustered_ddoc_read), + ?TDEF_FE(t_compaction_retains_history_on_unverifiable_checkpoint, 15), + ?TDEF_FE(t_compaction_retains_history_for_live_checkpoint, 15), + ?TDEF_FE(t_compaction_prunes_history_of_deleted_index, 15) + ] + } + }. + +setup_all() -> + test_util:start_couch([mem3, fabric]). + +teardown_all(Ctx) -> + test_util:stop_couch(Ctx). + +setup() -> + DbName = ?tempdb(), + {ok, Db} = couch_db:create(DbName, [?ADMIN_CTX]), + {ok, _} = couch_db:update_doc(Db, couch_doc:from_json_obj(ddoc()), []), + ok = couch_db:close(Db), + DbName. + +teardown(DbName) -> + catch meck:unload(), + couch_server:delete(DbName, [?ADMIN_CTX]). + +ddoc() -> + {[ + {<<"_id">>, ?DDOC_ID}, + {<<"views">>, + {[ + {<<"v">>, + {[ + {<<"map">>, <<"function(doc){emit(doc._id, 1);}">>} + ]}} + ]}} + ]}. + +props(DbName) -> + {ok, Db} = couch_db:open_int(DbName, []), + try + {ok, DDoc} = couch_db:get_design_doc(Db, ?DDOC_ID), + props_from(DbName, DDoc) + after + couch_db:close(Db) + end. + +props_from(DbName, DDoc) -> + {ok, #mrst{sig = Sig}} = couch_mrview_util:ddoc_to_mrst(DbName, DDoc), + [ + {<<"type">>, <<"mrview">>}, + {<<"ddoc_id">>, ?DDOC_ID}, + {<<"signature">>, couch_util:to_hex_bin(Sig)} + ]. + +replace(Key, Val, Props) -> + lists:keyreplace(Key, 1, Props, {Key, Val}). + +t_verify_matching_index(DbName) -> + ?assert(couch_mrview_index:verify_index_exists(DbName, props(DbName))). + +t_verify_false_on_wrong_type(DbName) -> + Props = replace(<<"type">>, <<"dreyfus">>, props(DbName)), + ?assertNot(couch_mrview_index:verify_index_exists(DbName, Props)). + +t_verify_false_on_missing_ddoc(DbName) -> + Props = replace(<<"ddoc_id">>, <<"_design/missing">>, props(DbName)), + ?assertNot(couch_mrview_index:verify_index_exists(DbName, Props)). + +t_verify_false_on_signature_mismatch(DbName) -> + Props = replace(<<"signature">>, <<"deadbeef">>, props(DbName)), + ?assertNot(couch_mrview_index:verify_index_exists(DbName, Props)). + +% Even on timeout we'd like to return true. We only want to return false if we've +% gotten a response that it's definitely gone. +t_verify_true_on_ddoc_read_error(DbName) -> + Props = props(DbName), + meck:new(couch_db, [passthrough]), + meck:expect(couch_db, get_design_doc, fun(_, _) -> {error, timeout} end), + ?assert(couch_mrview_index:verify_index_exists(DbName, Props)). + +t_verify_true_on_exception(DbName) -> + Props = props(DbName), + meck:new(couch_db, [passthrough]), + meck:expect(couch_db, get_design_doc, fun(_, _) -> meck:exception(error, boom) end), + ?assert(couch_mrview_index:verify_index_exists(DbName, Props)). + +t_verify_true_on_missing_db(DbName) -> + ?assert(couch_mrview_index:verify_index_exists(?tempdb(), props(DbName))). + +t_verify_true_on_failed_clustered_ddoc_read(_DbName) -> + % Clustered reads will fail but we'll still return true until cleanup runs + ShardDb = <<"shards/00000000-ffffffff/", (?tempdb())/binary, ".1234567890">>, + {ok, Db} = couch_db:create(ShardDb, [?ADMIN_CTX]), + {ok, _} = couch_db:update_doc(Db, couch_doc:from_json_obj(ddoc()), []), + ok = couch_db:close(Db), + Props = props_from(ShardDb, couch_doc:from_json_obj(ddoc())), + try + ?assert(couch_mrview_index:verify_index_exists(ShardDb, Props)) + after + couch_server:delete(ShardDb, [?ADMIN_CTX]) + end. + +t_compaction_retains_history_on_unverifiable_checkpoint(DbName) -> + ok = prepare_purges(DbName), + ok = write_checkpoint(DbName, props(DbName)), + meck:new(couch_db, [passthrough]), + meck:expect(couch_db, get_design_doc, fun(_, _) -> {error, timeout} end), + ok = compact(DbName), + % purge info kept since we couldn't verify the client + ?assertEqual(1, oldest_purge_seq(DbName)). + +t_compaction_retains_history_for_live_checkpoint(DbName) -> + ok = prepare_purges(DbName), + ok = write_checkpoint(DbName, props(DbName)), + ok = compact(DbName), + ?assertEqual(1, oldest_purge_seq(DbName)). + +t_compaction_prunes_history_of_deleted_index(DbName) -> + ok = prepare_purges(DbName), + Props = replace(<<"ddoc_id">>, <<"_design/missing">>, props(DbName)), + ok = write_checkpoint(DbName, Props), + ok = compact(DbName), + % Now we could check index is definitely gone the oldest purge seq moved up + ?assertEqual(4, oldest_purge_seq(DbName)). + +prepare_purges(DbName) -> + {ok, Db} = couch_db:open_int(DbName, [?ADMIN_CTX]), + try + ok = couch_db:set_purge_infos_limit(Db, 2), + lists:foreach( + fun(I) -> + Id = integer_to_binary(I), + Doc = couch_doc:from_json_obj({[{<<"_id">>, Id}]}), + {ok, Rev} = couch_db:update_doc(Db, Doc, []), + {ok, _} = couch_db:purge_docs(Db, [{couch_uuids:random(), Id, [Rev]}]) + end, + lists:seq(1, 5) + ) + after + couch_db:close(Db) + end. + +write_checkpoint(DbName, Props) -> + Sig = couch_util:get_value(<<"signature">>, Props), + Doc = couch_doc:from_json_obj( + {[ + {<<"_id">>, <<"_local/purge-mrview-", Sig/binary>>}, + {<<"purge_seq">>, 0}, + {<<"updated_on">>, erlang:system_time(second)} + | Props + ]} + ), + {ok, Db} = couch_db:open_int(DbName, [?ADMIN_CTX]), + try + {ok, _} = couch_db:update_doc(Db, Doc, []), + ok + after + couch_db:close(Db) + end. + +compact(DbName) -> + {ok, Db} = couch_db:open_int(DbName, []), + try + {ok, _} = couch_db:start_compact(Db), + ok = couch_db:wait_for_compaction(Db) + after + couch_db:close(Db) + end. + +oldest_purge_seq(DbName) -> + {ok, Db} = couch_db:open_int(DbName, []), + try + couch_db:get_oldest_purge_seq(Db) + after + couch_db:close(Db) + end. diff --git a/src/dreyfus/src/dreyfus_util.erl b/src/dreyfus/src/dreyfus_util.erl index d651da9bd..2e4a90f4c 100644 --- a/src/dreyfus/src/dreyfus_util.erl +++ b/src/dreyfus/src/dreyfus_util.erl @@ -442,20 +442,35 @@ verify_index_exists(DbName, Props) -> couch_util:with_db(DbName, fun(Db) -> case couch_db:get_design_doc(Db, DDocId) of {ok, #doc{} = DDoc} -> - {ok, IdxState} = dreyfus_index:design_doc_to_index( - DbName, DDoc, IndexName - ), - IdxState#index.sig == Sig; + case dreyfus_index:design_doc_to_index(DbName, DDoc, IndexName) of + {ok, #index{sig = IdxSig}} -> + IdxSig == Sig; + {error, _} -> + false + end; {not_found, _} -> - false + false; + Else -> + cannot_verify(DbName, Props, Else) end end) end catch - _:_ -> - false + Tag:Reason -> + cannot_verify(DbName, Props, {Tag, Reason}) end. +% Couldn't verify index exists when checking purge client validity. There may +% be a timeout or or db is not available. In such cases assume the client is +% valid to avoid the chance of compacting away purge before valid clients saw +% and processed them. +cannot_verify(DbName, Props, Error) -> + DDocId = couch_util:get_value(<<"ddoc_id">>, Props), + IndexName = couch_util:get_value(<<"indexname">>, Props), + Fmt = "~p : can't verify purge client db:~p ddoc:~p index:~p error:~w, assume exists", + couch_log:warning(Fmt, [?MODULE, DbName, DDocId, IndexName, Error]), + true. + -ifdef(TEST). -include_lib("eunit/include/eunit.hrl"). diff --git a/src/dreyfus/test/eunit/dreyfus_purge_client_tests.erl b/src/dreyfus/test/eunit/dreyfus_purge_client_tests.erl new file mode 100644 index 000000000..e24459e97 --- /dev/null +++ b/src/dreyfus/test/eunit/dreyfus_purge_client_tests.erl @@ -0,0 +1,142 @@ +% 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(dreyfus_purge_client_tests). + +-include_lib("couch/include/couch_eunit.hrl"). +-include_lib("couch/include/couch_db.hrl"). +-include_lib("dreyfus/include/dreyfus.hrl"). + +-define(DDOC_ID, <<"_design/searchddoc">>). +-define(INDEX_NAME, <<"searchidx">>). + +purge_client_verification_test_() -> + { + setup, + fun setup_all/0, + fun teardown_all/1, + { + foreach, + fun setup/0, + fun teardown/1, + [ + ?TDEF_FE(t_verify_matching_index), + ?TDEF_FE(t_verify_false_on_wrong_type), + ?TDEF_FE(t_verify_false_on_missing_ddoc), + ?TDEF_FE(t_verify_false_on_missing_index), + ?TDEF_FE(t_verify_false_on_signature_mismatch), + ?TDEF_FE(t_verify_true_on_ddoc_read_error), + ?TDEF_FE(t_verify_true_on_exception), + ?TDEF_FE(t_verify_true_on_missing_db), + ?TDEF_FE(t_verify_true_on_failed_clustered_ddoc_read) + ] + } + }. + +setup_all() -> + test_util:start_couch([mem3, fabric]). + +teardown_all(Ctx) -> + test_util:stop_couch(Ctx). + +setup() -> + DbName = ?tempdb(), + {ok, Db} = couch_db:create(DbName, [?ADMIN_CTX]), + {ok, _} = couch_db:update_doc(Db, couch_doc:from_json_obj(ddoc()), []), + ok = couch_db:close(Db), + DbName. + +teardown(DbName) -> + catch meck:unload(), + couch_server:delete(DbName, [?ADMIN_CTX]). + +ddoc() -> + {[ + {<<"_id">>, ?DDOC_ID}, + {<<"indexes">>, + {[ + {?INDEX_NAME, + {[ + {<<"index">>, <<"function(doc){index(\"def\", doc.val);}">>} + ]}} + ]}} + ]}. + +props(DbName) -> + {ok, Db} = couch_db:open_int(DbName, []), + try + {ok, DDoc} = couch_db:get_design_doc(Db, ?DDOC_ID), + props_from(DbName, DDoc) + after + couch_db:close(Db) + end. + +props_from(DbName, DDoc) -> + {ok, #index{sig = Sig}} = dreyfus_index:design_doc_to_index(DbName, DDoc, ?INDEX_NAME), + [ + {<<"type">>, <<"dreyfus">>}, + {<<"ddoc_id">>, ?DDOC_ID}, + {<<"indexname">>, ?INDEX_NAME}, + {<<"signature">>, Sig} + ]. + +replace(Key, Val, Props) -> + lists:keyreplace(Key, 1, Props, {Key, Val}). + +t_verify_matching_index(DbName) -> + ?assert(dreyfus_util:verify_index_exists(DbName, props(DbName))). + +t_verify_false_on_wrong_type(DbName) -> + Props = replace(<<"type">>, <<"mrview">>, props(DbName)), + ?assertNot(dreyfus_util:verify_index_exists(DbName, Props)). + +t_verify_false_on_missing_ddoc(DbName) -> + Props = replace(<<"ddoc_id">>, <<"_design/missing">>, props(DbName)), + ?assertNot(dreyfus_util:verify_index_exists(DbName, Props)). + +t_verify_false_on_missing_index(DbName) -> + Props = replace(<<"indexname">>, <<"missingidx">>, props(DbName)), + ?assertNot(dreyfus_util:verify_index_exists(DbName, Props)). + +t_verify_false_on_signature_mismatch(DbName) -> + Props = replace(<<"signature">>, <<"deadbeef">>, props(DbName)), + ?assertNot(dreyfus_util:verify_index_exists(DbName, Props)). + +% On timeout or other random error we want to still return that index exists as +% we don't want purges to be pruned until we're sure each client has seen them +t_verify_true_on_ddoc_read_error(DbName) -> + Props = props(DbName), + meck:new(couch_db, [passthrough]), + meck:expect(couch_db, get_design_doc, fun(_, _) -> {error, timeout} end), + ?assert(dreyfus_util:verify_index_exists(DbName, Props)). + +t_verify_true_on_exception(DbName) -> + Props = props(DbName), + meck:new(couch_db, [passthrough]), + meck:expect(couch_db, get_design_doc, fun(_, _) -> meck:exception(error, boom) end), + ?assert(dreyfus_util:verify_index_exists(DbName, Props)). + +t_verify_true_on_missing_db(DbName) -> + ?assert(dreyfus_util:verify_index_exists(?tempdb(), props(DbName))). + +t_verify_true_on_failed_clustered_ddoc_read(_DbName) -> + % Clustered ddoc read fails here but we'll still expect verify index to return true + ShardDb = <<"shards/00000000-ffffffff/", (?tempdb())/binary, ".1234567890">>, + {ok, Db} = couch_db:create(ShardDb, [?ADMIN_CTX]), + {ok, _} = couch_db:update_doc(Db, couch_doc:from_json_obj(ddoc()), []), + ok = couch_db:close(Db), + Props = props_from(ShardDb, couch_doc:from_json_obj(ddoc())), + try + ?assert(dreyfus_util:verify_index_exists(ShardDb, Props)) + after + couch_server:delete(ShardDb, [?ADMIN_CTX]) + end. diff --git a/src/nouveau/src/nouveau_util.erl b/src/nouveau/src/nouveau_util.erl index f51742d3e..067868dae 100644 --- a/src/nouveau/src/nouveau_util.erl +++ b/src/nouveau/src/nouveau_util.erl @@ -122,20 +122,35 @@ verify_index_exists(DbName, Props) -> couch_util:with_db(DbName, fun(Db) -> case couch_db:get_design_doc(Db, DDocId) of {ok, #doc{} = DDoc} -> - {ok, IdxState} = design_doc_to_index( - DbName, DDoc, IndexName - ), - IdxState#index.sig == Sig; + case design_doc_to_index(DbName, DDoc, IndexName) of + {ok, #index{sig = IdxSig}} -> + IdxSig == Sig; + {error, _} -> + false + end; {not_found, _} -> - false + false; + Else -> + cannot_verify(DbName, Props, Else) end end) end catch - _:_ -> - false + Tag:Reason -> + cannot_verify(DbName, Props, {Tag, Reason}) end. +% Couldn't verify index exists when checking purge client validity. There may +% be a timeout or or db is not available. In such cases assume the client is +% valid to avoid the chance of compacting away purge before valid clients saw +% and processed them. +cannot_verify(DbName, Props, Error) -> + DDocId = couch_util:get_value(<<"ddoc_id">>, Props), + IndexName = couch_util:get_value(<<"indexname">>, Props), + Fmt = "~p : can't verify purge client db:~p ddoc:~p index:~p error:~w, assume exists", + couch_log:warning(Fmt, [?MODULE, DbName, DDocId, IndexName, Error]), + true. + ensure_local_purge_docs(DbName, DDocs) -> couch_util:with_db(DbName, fun(Db) -> lists:foreach( diff --git a/src/nouveau/test/eunit/nouveau_purge_client_tests.erl b/src/nouveau/test/eunit/nouveau_purge_client_tests.erl new file mode 100644 index 000000000..f9a95bef9 --- /dev/null +++ b/src/nouveau/test/eunit/nouveau_purge_client_tests.erl @@ -0,0 +1,141 @@ +% 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(nouveau_purge_client_tests). + +-include_lib("couch/include/couch_eunit.hrl"). +-include_lib("couch/include/couch_db.hrl"). +-include_lib("nouveau/include/nouveau.hrl"). + +-define(DDOC_ID, <<"_design/searchddoc">>). +-define(INDEX_NAME, <<"searchidx">>). + +purge_client_verification_test_() -> + { + setup, + fun setup_all/0, + fun teardown_all/1, + { + foreach, + fun setup/0, + fun teardown/1, + [ + ?TDEF_FE(t_verify_matching_index), + ?TDEF_FE(t_verify_false_on_wrong_type), + ?TDEF_FE(t_verify_false_on_missing_ddoc), + ?TDEF_FE(t_verify_false_on_missing_index), + ?TDEF_FE(t_verify_false_on_signature_mismatch), + ?TDEF_FE(t_verify_true_on_ddoc_read_error), + ?TDEF_FE(t_verify_true_on_exception), + ?TDEF_FE(t_verify_true_on_missing_db), + ?TDEF_FE(t_verify_true_on_failed_clustered_ddoc_read) + ] + } + }. + +setup_all() -> + test_util:start_couch([mem3, fabric]). + +teardown_all(Ctx) -> + test_util:stop_couch(Ctx). + +setup() -> + DbName = ?tempdb(), + {ok, Db} = couch_db:create(DbName, [?ADMIN_CTX]), + {ok, _} = couch_db:update_doc(Db, couch_doc:from_json_obj(ddoc()), []), + ok = couch_db:close(Db), + DbName. + +teardown(DbName) -> + catch meck:unload(), + couch_server:delete(DbName, [?ADMIN_CTX]). + +ddoc() -> + {[ + {<<"_id">>, ?DDOC_ID}, + {<<"nouveau">>, + {[ + {?INDEX_NAME, + {[ + {<<"index">>, <<"function(doc){index(\"string\", \"f\", doc.val);}">>} + ]}} + ]}} + ]}. + +props(DbName) -> + {ok, Db} = couch_db:open_int(DbName, []), + try + {ok, DDoc} = couch_db:get_design_doc(Db, ?DDOC_ID), + props_from(DbName, DDoc) + after + couch_db:close(Db) + end. + +props_from(DbName, DDoc) -> + {ok, #index{sig = Sig}} = nouveau_util:design_doc_to_index(DbName, DDoc, ?INDEX_NAME), + [ + {<<"type">>, <<"nouveau">>}, + {<<"ddoc_id">>, ?DDOC_ID}, + {<<"indexname">>, ?INDEX_NAME}, + {<<"signature">>, Sig} + ]. + +replace(Key, Val, Props) -> + lists:keyreplace(Key, 1, Props, {Key, Val}). + +t_verify_matching_index(DbName) -> + ?assert(nouveau_util:verify_index_exists(DbName, props(DbName))). + +t_verify_false_on_wrong_type(DbName) -> + Props = replace(<<"type">>, <<"dreyfus">>, props(DbName)), + ?assertNot(nouveau_util:verify_index_exists(DbName, Props)). + +t_verify_false_on_missing_ddoc(DbName) -> + Props = replace(<<"ddoc_id">>, <<"_design/missing">>, props(DbName)), + ?assertNot(nouveau_util:verify_index_exists(DbName, Props)). + +t_verify_false_on_missing_index(DbName) -> + Props = replace(<<"indexname">>, <<"missingidx">>, props(DbName)), + ?assertNot(nouveau_util:verify_index_exists(DbName, Props)). + +t_verify_false_on_signature_mismatch(DbName) -> + Props = replace(<<"signature">>, <<"f00123">>, props(DbName)), + ?assertNot(nouveau_util:verify_index_exists(DbName, Props)). + +t_verify_true_on_ddoc_read_error(DbName) -> + Props = props(DbName), + meck:new(couch_db, [passthrough]), + meck:expect(couch_db, get_design_doc, fun(_, _) -> {error, timeout} end), + ?assert(nouveau_util:verify_index_exists(DbName, Props)). + +t_verify_true_on_exception(DbName) -> + Props = props(DbName), + meck:new(couch_db, [passthrough]), + meck:expect(couch_db, get_design_doc, fun(_, _) -> meck:exception(error, boom) end), + ?assert(nouveau_util:verify_index_exists(DbName, Props)). + +t_verify_true_on_missing_db(DbName) -> + ?assert(nouveau_util:verify_index_exists(?tempdb(), props(DbName))). + +t_verify_true_on_failed_clustered_ddoc_read(_DbName) -> + % Clustered reads will fail but we still want to return true until + % cleanup runs + ShardDb = <<"shards/00000000-ffffffff/", (?tempdb())/binary, ".1234567890">>, + {ok, Db} = couch_db:create(ShardDb, [?ADMIN_CTX]), + {ok, _} = couch_db:update_doc(Db, couch_doc:from_json_obj(ddoc()), []), + ok = couch_db:close(Db), + Props = props_from(ShardDb, couch_doc:from_json_obj(ddoc())), + try + ?assert(nouveau_util:verify_index_exists(ShardDb, Props)) + after + couch_server:delete(ShardDb, [?ADMIN_CTX]) + end.
