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    ets:new(ipv6, [set, protected, named_table]),
91    ets:insert(ipv6, {is_ipv6, false}),
92    couch_server_sup:start_link(test_util:config_files()),
93    couch_file_write_guard:disable_for_testing(),
94    couch_server:delete(<<"etap-test-db">>, []),
95    {ok, Db} = couch_db:create(<<"etap-test-db">>, []),
96    ok = couch_db:update_doc(Db, #doc{id = <<"1">>,body = <<"{\"foo\":1}">>},
97            [full_commit]),
98    ok = couch_db:update_doc(Db, #doc{id = <<"2">>,body = <<"{\"foo\":1}">>},
99            [full_commit]),
100    couch_db:close(Db),
101    {ok, Db2} = couch_db:open(<<"etap-test-db">>, []),
102
103    {ok, Doc1} = couch_db:open_doc(Db2, <<"1">>),
104    etap:is(Doc1#doc.id, <<"1">>, "Doc 1 was created."),
105
106    {ok, Doc2} = couch_db:open_doc(Db2, <<"2">>),
107    etap:is(Doc2#doc.id, <<"2">>, "Doc 2 was created."),
108
109    etap:is(couch_db:update_header_pos(Db2, 0, 0), retry_new_file_version,
110            "Should retry, earlier file version"),
111    etap:is(couch_db:update_header_pos(Db2, 1, 0), update_behind_couchdb,
112            "Should be behind couchdb"),
113    etap:is(couch_db:update_header_pos(Db2, 2, 0), update_file_ahead_of_couchdb,
114            "Should be ahead couchdb"),
115
116
117    DbRootDir = couch_config:get("couchdb", "database_dir", "."),
118    Filename = filename:join(DbRootDir, "etap-test-db.couch.1"),
119    {ok, Fd} = couch_file:open(Filename),
120    {ok, FileLen} = couch_file:bytes(Fd),
121
122    Header = Db2#db.header,
123    Header2 = Header#db_header{update_seq = 0},
124
125    % calculate where new header goes
126    case FileLen rem ?SIZE_BLOCK of
127    0 ->
128        NewHeaderPos = FileLen + ?SIZE_BLOCK;
129    BlockOffset ->
130        NewHeaderPos = FileLen + (?SIZE_BLOCK - BlockOffset)
131    end,
132
133    HeaderBin = couch_db_updater:db_header_to_header_bin(Header2),
134    etap:is(couch_file:write_header_bin(Fd, HeaderBin), {ok, NewHeaderPos},
135            "Should write new header outside of couchdb"),
136    couch_file:flush(Fd),
137    {ok, FileLen2} = couch_file:bytes(Fd),
138    etap:is(couch_db:update_header_pos(Db2, 1, NewHeaderPos), update_behind_couchdb,
139            "Should be ahead couchdb"),
140
141    Header3 = Header#db_header{update_seq = Header#db_header.update_seq + 1},
142    HeaderBin3 = couch_db_updater:db_header_to_header_bin(Header3),
143    NewHeaderPos2 = NewHeaderPos + ?SIZE_BLOCK,
144    etap:is(couch_file:write_header_bin(Fd, HeaderBin3), {ok, NewHeaderPos2},
145            "Should write new header outside of couchdb"),
146    couch_file:flush(Fd),
147    etap:is(couch_db:update_header_pos(Db2, 1, NewHeaderPos2), ok,
148            "Should accept new header"),
149    couch_db:close(Db2),
150
151    {ok, Db3} = couch_db:open(<<"etap-test-db">>, []),
152
153    etap:is(Db3#db.header, Header3,
154            "Db header should be what we just wrote"),
155
156    {ok, CompactPid} = couch_db:start_compact(Db3),
157    monitor(process, CompactPid),
158    receive
159    {'DOWN', _MonitorRef, _Type, CompactPid, normal} ->
160        Finished = true
161    after 3000 ->
162        Finished = false
163    end,
164    etap:is(Finished, true,
165              "Db should still be compactable."),
166    couch_db:close(Db3),
167    couch_server:delete(<<"etap-test-db">>, []),
168    ets:delete(ipv6),
169    ok.
170