1#!/usr/bin/env escript
2%% -*- erlang -*-
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-define(SIZE_BLOCK, 4096).
18
19-record(user_ctx, {
20    name = null,
21    roles = [],
22    handler
23}).
24
25-record(db, {
26    main_pid = nil,
27    update_pid = nil,
28    compactor_pid = nil,
29    instance_start_time, % number of microsecs since jan 1 1970 as a binary string
30    fd,
31    fd_ref_counter,
32    header = nil,
33    committed_update_seq,
34    fulldocinfo_by_id_btree,
35    docinfo_by_seq_btree,
36    local_docs_btree,
37    update_seq,
38    name,
39    filepath,
40    security = [],
41    security_ptr = nil,
42    user_ctx = #user_ctx{},
43    waiting_delayed_commit = nil,
44    fsync_options = [],
45    options = []
46}).
47
48
49-record(db_header,
50    {disk_version,
51     update_seq = 0,
52     docinfo_by_id_btree_state = nil,
53     docinfo_by_seq_btree_state = nil,
54     local_docs_btree_state = nil,
55     purge_seq = 0,
56     purged_docs = nil,
57     security_ptr = nil
58    }).
59
60-record(doc,
61    {
62    id = <<>>,
63    rev = {0, <<>>},
64
65    % the binary body
66    body = <<"{}">>,
67    content_meta = 0, % should be 0-255 only.
68
69    deleted = false,
70
71    % key/value tuple of meta information, provided when using special options:
72    % couch_db:open_doc(Db, Id, Options).
73    meta = []
74    }).
75
76main(_) ->
77    test_util:init_code_path(),
78
79    etap:plan(11),
80    case (catch test()) of
81        ok ->
82            etap:end_tests();
83        Other ->
84            etap:diag(io_lib:format("Test died abnormally: ~p", [Other])),
85            etap:bail(Other)
86    end,
87    ok.
88
89test() ->
90    couch_server_sup:start_link(test_util:config_files()),
91    couch_file_write_guard:disable_for_testing(),
92    couch_server:delete(<<"etap-test-db">>, []),
93    {ok, Db} = couch_db:create(<<"etap-test-db">>, []),
94    ok = couch_db:update_doc(Db, #doc{id = <<"1">>,body = <<"{\"foo\":1}">>},
95            [full_commit]),
96    ok = couch_db:update_doc(Db, #doc{id = <<"2">>,body = <<"{\"foo\":1}">>},
97            [full_commit]),
98    couch_db:close(Db),
99    {ok, Db2} = couch_db:open(<<"etap-test-db">>, []),
100
101    {ok, Doc1} = couch_db:open_doc(Db2, <<"1">>),
102    etap:is(Doc1#doc.id, <<"1">>, "Doc 1 was created."),
103
104    {ok, Doc2} = couch_db:open_doc(Db2, <<"2">>),
105    etap:is(Doc2#doc.id, <<"2">>, "Doc 2 was created."),
106
107    etap:is(couch_db:update_header_pos(Db2, 0, 0), retry_new_file_version,
108            "Should retry, earlier file version"),
109    etap:is(couch_db:update_header_pos(Db2, 1, 0), update_behind_couchdb,
110            "Should be behind couchdb"),
111    etap:is(couch_db:update_header_pos(Db2, 2, 0), update_file_ahead_of_couchdb,
112            "Should be ahead couchdb"),
113
114
115    DbRootDir = couch_config:get("couchdb", "database_dir", "."),
116    Filename = filename:join(DbRootDir, "etap-test-db.couch.1"),
117    {ok, Fd} = couch_file:open(Filename),
118    {ok, FileLen} = couch_file:bytes(Fd),
119
120    Header = Db2#db.header,
121    Header2 = Header#db_header{update_seq = 0},
122
123    % calculate where new header goes
124    case FileLen rem ?SIZE_BLOCK of
125    0 ->
126        NewHeaderPos = FileLen + ?SIZE_BLOCK;
127    BlockOffset ->
128        NewHeaderPos = FileLen + (?SIZE_BLOCK - BlockOffset)
129    end,
130
131    HeaderBin = couch_db_updater:db_header_to_header_bin(Header2),
132    etap:is(couch_file:write_header_bin(Fd, HeaderBin), {ok, NewHeaderPos},
133            "Should write new header outside of couchdb"),
134    couch_file:flush(Fd),
135    {ok, FileLen2} = couch_file:bytes(Fd),
136    etap:is(couch_db:update_header_pos(Db2, 1, NewHeaderPos), update_behind_couchdb,
137            "Should be ahead couchdb"),
138
139    Header3 = Header#db_header{update_seq = Header#db_header.update_seq + 1},
140    HeaderBin3 = couch_db_updater:db_header_to_header_bin(Header3),
141    NewHeaderPos2 = NewHeaderPos + ?SIZE_BLOCK,
142    etap:is(couch_file:write_header_bin(Fd, HeaderBin3), {ok, NewHeaderPos2},
143            "Should write new header outside of couchdb"),
144    couch_file:flush(Fd),
145    etap:is(couch_db:update_header_pos(Db2, 1, NewHeaderPos2), ok,
146            "Should accept new header"),
147    couch_db:close(Db2),
148
149    {ok, Db3} = couch_db:open(<<"etap-test-db">>, []),
150
151    etap:is(Db3#db.header, Header3,
152            "Db header should be what we just wrote"),
153
154    {ok, CompactPid} = couch_db:start_compact(Db3),
155    monitor(process, CompactPid),
156    receive
157    {'DOWN', _MonitorRef, _Type, CompactPid, normal} ->
158        Finished = true
159    after 3000 ->
160        Finished = false
161    end,
162    etap:is(Finished, true,
163              "Db should still be compactable."),
164    couch_db:close(Db3),
165    couch_server:delete(<<"etap-test-db">>, []),
166    ok.
167