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.

Reply via email to