1#!/usr/bin/env escript
2%% -*- Mode: Erlang; tab-width: 4; c-basic-offset: 4; indent-tabs-mode: nil -*- */
3%%! -smp enable
4
5% Licensed under the Apache License, Version 2.0 (the "License"); you may not
6% use this file except in compliance with the License. You may obtain a copy of
7% the License at
8%
9%   http://www.apache.org/licenses/LICENSE-2.0
10%
11% Unless required by applicable law or agreed to in writing, software
12% distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
13% WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
14% License for the specific language governing permissions and limitations under
15% the License.
16
17-include_lib("couch_set_view/include/couch_set_view.hrl").
18
19% from couch_db.hrl
20-define(MIN_STR, <<>>).
21-define(MAX_STR, <<255>>).
22
23-record(view_query_args, {
24    start_key,
25    end_key,
26    start_docid = ?MIN_STR,
27    end_docid = ?MAX_STR,
28    direction = fwd,
29    inclusive_end = true,
30    limit = 10000000000,
31    skip = 0,
32    group_level = 0,
33    view_type = nil,
34    include_docs = false,
35    conflicts = false,
36    stale = false,
37    multi_get = false,
38    callback = nil,
39    list = nil,
40    run_reduce = true,
41    keys = nil,
42    view_name = nil,
43    debug = false,
44    filter = true,
45    type = main
46}).
47
48test_set_name() -> <<"couch_test_set_index_dev_view">>.
49num_set_partitions() -> 4.
50ddoc_id() -> <<"_design/dev_test">>.
51num_docs() -> 1024.  % keep it a multiple of num_set_partitions()
52docs_per_partition() -> num_docs() div num_set_partitions().
53
54
55main(_) ->
56    test_util:init_code_path(),
57
58    etap:plan(20),
59    case (catch test()) of
60        ok ->
61            etap:end_tests();
62        Other ->
63            etap:diag(io_lib:format("Test died abnormally: ~p", [Other])),
64            etap:bail(Other)
65    end,
66    ok.
67
68
69test() ->
70    couch_set_view_test_util:start_server(test_set_name()),
71
72    etap:diag("Testing development views"),
73
74    test_map_query(0),
75    test_map_query(1),
76    test_map_query(2),
77    test_map_query(3),
78
79    test_reduce_query(0),
80    test_reduce_query(1),
81    test_reduce_query(2),
82    test_reduce_query(3),
83
84    test_set_active_partition(),
85    test_map_query_updated(0),
86    test_cleanup(0),
87
88    couch_set_view_test_util:delete_set_dbs(test_set_name(), num_set_partitions()),
89    couch_set_view_test_util:stop_server(),
90    ok.
91
92
93test_map_query(PartitionId) ->
94    setup_test(),
95    ok = configure_view_group(ddoc_id(), PartitionId),
96
97    {ok, Rows} = (catch query_map_view(<<"test">>)),
98    etap:is(length(Rows), docs_per_partition(),
99        "Got " ++ integer_to_list(docs_per_partition()) ++ " view rows"),
100    verify_rows(Rows, PartitionId),
101
102    shutdown_group().
103
104
105test_reduce_query(PartitionId) ->
106    setup_test(),
107    ok = configure_view_group(ddoc_id(), PartitionId),
108
109    {ok, Reduce} = (catch query_reduce_view(<<"testred">>)),
110    Offset = (PartitionId * docs_per_partition()),
111    ExpectedReduce = lists:sum(lists:seq(
112        1 + Offset, Offset + docs_per_partition())),
113    etap:is(Reduce, ExpectedReduce, "Reduce value is correct"),
114
115    shutdown_group().
116
117
118test_set_active_partition() ->
119    setup_test(),
120    ok = configure_view_group(ddoc_id(), 1),
121
122    {ok, Rows} = (catch query_map_view(<<"test">>)),
123    etap:is(length(Rows), docs_per_partition(),
124        "Got " ++ integer_to_list(docs_per_partition()) ++ " view rows"),
125    verify_rows(Rows, 1),
126
127    % Set a different partition as active
128    couch_set_view_dev:set_active_partition(
129        mapreduce_view, test_set_name(), ddoc_id(), 2),
130    {ok, Rows2} = (catch query_map_view(<<"test">>)),
131    etap:is(length(Rows2), docs_per_partition(),
132        "Got " ++ integer_to_list(docs_per_partition()) ++ " view rows"),
133    verify_rows(Rows2, 2),
134
135    shutdown_group().
136
137
138test_map_query_updated(PartitionId) ->
139    setup_test(),
140    update_design_doc(),
141    ok = configure_view_group(ddoc_id(), PartitionId),
142
143    {ok, Rows} = (catch query_map_view(<<"test">>)),
144    etap:is(length(Rows), docs_per_partition(),
145        "Got " ++ integer_to_list(docs_per_partition()) ++ " view rows"),
146    verify_rows_updated(Rows, PartitionId),
147
148    shutdown_group().
149
150
151test_cleanup(PartitionId) ->
152    setup_test(),
153    ok = configure_view_group(ddoc_id(), PartitionId),
154    {ok, _Rows} = (catch query_map_view(<<"test">>)),
155
156    GroupSig = get_group_sig(),
157    IndexFile = "main_" ++ binary_to_list(GroupSig) ++ ".view.1",
158    etap:is(all_index_files(), [IndexFile], "Index file found"),
159
160    GroupPid = couch_set_view:get_group_pid(
161        mapreduce_view, test_set_name(), ddoc_id(), dev),
162    MonRef = erlang:monitor(process, GroupPid),
163
164    couch_set_view_test_util:delete_set_dbs(test_set_name(), num_set_partitions()),
165
166    receive
167    {'DOWN', MonRef, _, _, shutdown} ->
168        ok
169    after 10000 ->
170        etap:bail("Timeout waiting for group to shutdown after dbs deleted")
171    end,
172
173    etap:is(all_index_files(), [], "Index file got correctly deleted").
174
175
176% As the partitions are populated sequentially we can easily very them
177verify_rows(Rows, PartitionId) ->
178    Offset = (PartitionId * docs_per_partition()),
179    DocList = lists:map(fun(Doc) ->
180        {[{<<"meta">>, {[{<<"id">>, DocId}]}},
181          {<<"json">>, {[{<<"value">>, Value}]}}]} = Doc,
182        {<<"\"", DocId/binary, "\"">>, DocId,
183            list_to_binary(integer_to_list(Value))}
184    end, create_docs(1 + Offset, Offset + docs_per_partition())),
185    etap:is(Rows, lists:sort(DocList), "Returned correct rows").
186
187
188verify_rows_updated(Rows, PartitionId) ->
189    Offset = (PartitionId * docs_per_partition()),
190    DocList = lists:map(fun(Doc) ->
191        {[{<<"meta">>, {[{<<"id">>, DocId}]}},
192          {<<"json">>, {[{<<"value">>, Value}]}}]} = Doc,
193        % the `*3` is the difference to `verify_rows/2`
194        {<<"\"", DocId/binary, "\"">>, DocId,
195            list_to_binary(integer_to_list(Value*3))}
196    end, create_docs(1 + Offset, Offset + docs_per_partition())),
197    etap:is(Rows, lists:sort(DocList), "Returned correct rows").
198
199
200query_map_view(ViewName) ->
201    etap:diag("Querying map view " ++ binary_to_list(ddoc_id()) ++ "/" ++
202        binary_to_list(ViewName)),
203    Req = #set_view_group_req{
204        stale = false,
205        category = dev
206    },
207    {ok, View, Group, _} = couch_set_view:get_map_view(
208        test_set_name(), ddoc_id(), ViewName, Req),
209
210    FoldFun = fun({{{json, Key}, DocId}, {_PartId, {json, Value}}}, _, Acc) ->
211        {ok, [{Key, DocId, Value} | Acc]}
212    end,
213    ViewArgs = #view_query_args{
214        run_reduce = false,
215        view_name = ViewName
216    },
217    {ok, _, Rows} = couch_set_view:fold(Group, View, FoldFun, [], ViewArgs),
218    couch_set_view:release_group(Group),
219    {ok, lists:reverse(Rows)}.
220
221
222query_reduce_view(ViewName) ->
223    etap:diag("Querying reduce view " ++ binary_to_list(ddoc_id()) ++ "/" ++
224        binary_to_list(ViewName)),
225    Req = #set_view_group_req{
226        stale = false,
227        category = dev
228    },
229    {ok, View, Group, _} = couch_set_view:get_reduce_view(
230        test_set_name(), ddoc_id(), ViewName, Req),
231
232    FoldFun = fun(Key, {json, Red}, Acc) ->
233        {ok, [{Key, ejson:decode(Red)} | Acc]}
234    end,
235    ViewArgs = #view_query_args{
236        run_reduce = true,
237        view_name = ViewName
238    },
239    {ok, Rows} = couch_set_view:fold_reduce(Group, View, FoldFun, [], ViewArgs),
240    couch_set_view:release_group(Group),
241    case Rows of
242    [{_Key, RedValue}] ->
243        {ok, RedValue};
244    [] ->
245        empty
246    end.
247
248
249setup_test() ->
250    couch_set_view_test_util:delete_set_dbs(test_set_name(), num_set_partitions()),
251    couch_set_view_test_util:create_set_dbs(test_set_name(), num_set_partitions()),
252
253    DDoc = {[
254        {<<"meta">>, {[{<<"id">>, ddoc_id()}]}},
255        {<<"json">>, {[
256            {<<"views">>, {[
257                {<<"test">>, {[
258                    {<<"map">>, <<"function(doc, meta) { emit(meta.id, doc.value); }">>}
259                ]}},
260                {<<"testred">>, {[
261                    {<<"map">>, <<"function(doc, meta) { emit(meta.id, doc.value); }">>},
262                    {<<"reduce">>, <<"_sum">>}
263                ]}}
264            ]}}
265        ]}}
266    ]},
267    populate_set(DDoc).
268
269
270update_design_doc() ->
271    etap:diag("Updating design document in master database"),
272    DDoc = {[
273        {<<"meta">>, {[{<<"id">>, ddoc_id()}]}},
274        {<<"json">>, {[
275            {<<"views">>, {[
276                {<<"test">>, {[
277                    {<<"map">>, <<"function(doc, meta) { emit(meta.id, doc.value*3); }">>}
278                ]}}
279            ]}}
280        ]}}
281    ]},
282    ok = couch_set_view_test_util:update_ddoc(test_set_name(), DDoc).
283
284
285create_docs(From, To) ->
286    lists:map(
287        fun(I) ->
288            {[
289                {<<"meta">>, {[{<<"id">>, iolist_to_binary(["doc", integer_to_list(I)])}]}},
290                {<<"json">>, {[{<<"value">>, I}]}}
291            ]}
292        end,
293        lists:seq(From, To)).
294
295
296populate_set(DDoc) ->
297    etap:diag("Populating the " ++ integer_to_list(num_set_partitions()) ++
298        " databases with " ++ integer_to_list(num_docs()) ++ " documents"),
299    ok = couch_set_view_test_util:update_ddoc(test_set_name(), DDoc),
300    DocList = create_docs(1, num_docs()),
301    ok = couch_set_view_test_util:populate_set_sequentially(
302        test_set_name(),
303        lists:seq(0, num_set_partitions() - 1),
304        DocList).
305
306
307configure_view_group(DDocId, PartitionId) ->
308    etap:diag("Configuring view group"),
309    try
310        ok = couch_set_view_dev:define_group(
311            mapreduce_view, test_set_name(), DDocId, PartitionId)
312    catch _:Error ->
313        Error
314    end.
315
316
317shutdown_group() ->
318    GroupPid = couch_set_view:get_group_pid(
319        mapreduce_view, test_set_name(), ddoc_id(), dev),
320    couch_set_view_test_util:delete_set_dbs(test_set_name(), num_set_partitions()),
321    MonRef = erlang:monitor(process, GroupPid),
322    receive
323    {'DOWN', MonRef, _, _, _} ->
324        ok
325    after 10000 ->
326        etap:bail("Timeout waiting for group shutdown")
327    end.
328
329
330get_group_sig() ->
331    {ok, Info} = couch_set_view:get_group_info(
332       mapreduce_view, test_set_name(), ddoc_id(), dev),
333    couch_util:get_value(signature, Info).
334
335
336all_index_files() ->
337    IndexDir = couch_set_view:set_index_dir(
338        couch_config:get("couchdb", "view_index_dir"), test_set_name(), dev),
339    filelib:fold_files(
340        IndexDir, ".*\\.view\\.[0-9]+$", false,
341        fun(N, A) -> [filename:basename(N) | A] end, []).
342