This is an automated email from the ASF dual-hosted git repository.
nickva pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/couchdb.git
The following commit(s) were added to refs/heads/main by this push:
new b6dab673f Strengthen purge client existence tests
b6dab673f is described below
commit b6dab673f060d47bbfbb89af6ee280a796ddaf6d
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 response 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..a484403f4 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:~p, 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..e78e746cd 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:~p,
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..567b77f49 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:~p,
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.