xref: /5.5.2/couchdb/test/etap/011-file-headers.t (revision 6b32ccaa)
1#!/usr/bin/env escript
2%% -*- erlang -*-
3%%! -pa ./src/couchdb -sasl errlog_type error -noshell -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
17filename() -> test_util:build_file("test/etap/temp.011").
18sizeblock() -> 4096. % Need to keep this in sync with couch_file.erl
19
20main(_) ->
21    test_util:init_code_path(),
22    {S1, S2, S3} = now(),
23    random:seed(S1, S2, S3),
24
25    etap:plan(34),
26    case (catch test()) of
27        ok ->
28            etap:end_tests();
29        Other ->
30            etap:diag(io_lib:format("Test died abnormally: ~p", [Other])),
31            etap:bail()
32    end,
33    ok.
34
35test() ->
36    couch_file_write_guard:sup_start_link(),
37    test_couchdb(),
38    test_find_header(),
39    ok.
40
41test_couchdb() ->
42    {ok, Fd} = couch_file:open(filename(), [create,overwrite]),
43
44    etap:is({ok, 0}, couch_file:bytes(Fd),
45        "File should be initialized to contain zero bytes."),
46
47    etap:is({ok, 0}, couch_file:write_header(Fd, {<<"some_data">>, 32}),
48        "Writing a header succeeds."),
49    ok = couch_file:flush(Fd),
50    {ok, Size1} = couch_file:bytes(Fd),
51    etap:is_greater(Size1, 0,
52        "Writing a header allocates space in the file."),
53
54    etap:is({ok, {<<"some_data">>, 32}, 0}, couch_file:read_header(Fd),
55        "Reading the header returns what we wrote."),
56
57    etap:is({ok, 4096}, couch_file:write_header(Fd, [foo, <<"more">>]),
58        "Writing a second header succeeds."),
59
60    {ok, Size2} = couch_file:bytes(Fd),
61    etap:is_greater(Size2, Size1,
62        "Writing a second header allocates more space."),
63
64    ok = couch_file:flush(Fd),
65    etap:is({ok, [foo, <<"more">>], 4096}, couch_file:read_header(Fd),
66        "Reading the second header does not return the first header."),
67
68    % Delete the second header.
69    ok = couch_file:truncate(Fd, Size1),
70
71    etap:is({ok, {<<"some_data">>, 32}, 0}, couch_file:read_header(Fd),
72        "Reading the header after a truncation returns a previous header."),
73
74    couch_file:write_header(Fd, [foo, <<"more">>]),
75    etap:is({ok, Size2}, couch_file:bytes(Fd),
76        "Rewriting the same second header returns the same second size."),
77
78    couch_file:write_header(Fd, erlang:make_tuple(5000, <<"CouchDB">>)),
79    ok = couch_file:flush(Fd),
80    etap:is(
81        couch_file:read_header(Fd),
82        {ok, erlang:make_tuple(5000, <<"CouchDB">>), 8192},
83        "Headers larger than the block size can be saved (COUCHDB-1319)"
84    ),
85
86    ok = couch_file:close(Fd),
87
88    % Now for the fun stuff. Try corrupting the second header and see
89    % if we recover properly.
90
91    % Destroy the 0x1 byte that marks a header
92    check_header_recovery(fun(CouchFd, RawFd, Expect, HeaderPos) ->
93        ok = couch_file:flush(CouchFd),
94        etap:isnt(Expect, couch_file:read_header(CouchFd),
95            "Should return a different header before corruption."),
96        file:pwrite(RawFd, HeaderPos, <<0>>),
97        etap:is(Expect, couch_file:read_header(CouchFd),
98            "Corrupting the byte marker should read the previous header.")
99    end),
100
101    % Corrupt the size.
102    check_header_recovery(fun(CouchFd, RawFd, Expect, HeaderPos) ->
103        ok = couch_file:flush(CouchFd),
104        etap:isnt(Expect, couch_file:read_header(CouchFd),
105            "Should return a different header before corruption."),
106        % +1 for 0x1 byte marker
107        file:pwrite(RawFd, HeaderPos+1, <<10/integer>>),
108        etap:is(Expect, couch_file:read_header(CouchFd),
109            "Corrupting the size should read the previous header.")
110    end),
111
112    % Corrupt the MD5 signature
113    check_header_recovery(fun(CouchFd, RawFd, Expect, HeaderPos) ->
114        ok = couch_file:flush(CouchFd),
115        etap:isnt(Expect, couch_file:read_header(CouchFd),
116            "Should return a different header before corruption."),
117        % +5 = +1 for 0x1 byte and +4 for term size.
118        file:pwrite(RawFd, HeaderPos+5, <<"F01034F88D320B22">>),
119        etap:is(Expect, couch_file:read_header(CouchFd),
120            "Corrupting the MD5 signature should read the previous header.")
121    end),
122
123    % Corrupt the data
124    check_header_recovery(fun(CouchFd, RawFd, Expect, HeaderPos) ->
125        ok = couch_file:flush(CouchFd),
126        etap:isnt(Expect, couch_file:read_header(CouchFd),
127            "Should return a different header before corruption."),
128        % +21 = +1 for 0x1 byte, +4 for term size and +16 for MD5 sig
129        file:pwrite(RawFd, HeaderPos+21, <<"some data goes here!">>),
130        etap:is(Expect, couch_file:read_header(CouchFd),
131            "Corrupting the header data should read the previous header.")
132    end).
133
134test_find_header() ->
135    {ok, Fd} = couch_file:open(filename(), [create, overwrite]),
136
137    etap:is({ok, 0}, couch_file:bytes(Fd),
138        "File should be initialized to contain zero bytes."),
139    etap:is({ok, 0}, couch_file:write_header(Fd, {<<"some_data">>, 32}),
140        "Writing a header succeeds."),
141    ok = couch_file:flush(Fd),
142
143    etap:is(couch_file:find_header_bin(Fd, 0),
144        {ok, term_to_binary({<<"some_data">>, 32}), 0},
145        "Found header at the beginning of the file."),
146
147    etap:is(couch_file:find_header_bin(Fd, eof),
148        {ok, term_to_binary({<<"some_data">>, 32}), 0},
149        "Found header at the beginning of the file when searching from "
150        "the end of the file."),
151
152    etap:is({ok, 4096}, couch_file:write_header(Fd, [foo, <<"more">>]),
153        "Writing a second header succeeds."),
154    ok = couch_file:flush(Fd),
155
156    etap:is(couch_file:find_header_bin(Fd, 0),
157        {ok, term_to_binary({<<"some_data">>, 32}), 0},
158        "Finding header at the beginning of the file still works."),
159
160    etap:is(couch_file:find_header_bin(Fd, 4096),
161        {ok, term_to_binary([foo, <<"more">>]), 4096},
162        "Finding second header by supplying its exact position works."),
163
164    etap:is(couch_file:find_header_bin(Fd, eof),
165        {ok, term_to_binary([foo, <<"more">>]), 4096},
166        "Finding second header by searching from the end of the file works."),
167
168    etap:is(couch_file:find_header_bin(Fd, 4095),
169        {ok, term_to_binary({<<"some_data">>, 32}), 0},
170        "Finding first header by supplying a position just one byte before "
171        "the second header."),
172
173    etap:is(couch_file:find_header_bin(Fd, 3000),
174        {ok, term_to_binary({<<"some_data">>, 32}), 0},
175        "Finding first header by supplying a position between the first and "
176        "the first and the second header."),
177
178    etap:is(couch_file:find_header_bin(Fd, 5000),
179        {ok, term_to_binary([foo, <<"more">>]), 4096},
180        "Finding second header by supplying a position that is within the "
181        "second header."),
182
183    {ok, Size1} = couch_file:bytes(Fd),
184    etap:is(couch_file:find_header_bin(Fd, Size1 + 1000),
185        {ok, term_to_binary([foo, <<"more">>]), 4096},
186        "Finding second header by supplying a position that is bigger than "
187        "the file size."),
188
189    etap:is({ok, 8192},
190        couch_file:write_header(Fd, erlang:make_tuple(5000, <<"Data">>)),
191        "Writing a third header that is > 4KB succeeds."),
192    ok = couch_file:flush(Fd),
193    {ok, Header1, 8192} = couch_file:find_header_bin(Fd, 8192),
194    etap:ok(byte_size(Header1) > 4096, "Header is really > 4KB."),
195
196    etap:is(couch_file:find_header_bin(Fd, 8000),
197        {ok, term_to_binary([foo, <<"more">>]), 4096},
198        "Finding second header by supplying a position that is between the "
199        "second and the third header."),
200
201    etap:is(couch_file:find_header_bin(Fd, eof),
202        {ok, term_to_binary(erlang:make_tuple(5000, <<"Data">>)), 8192},
203        "Finding third header by searching from the end of the file works.").
204
205
206check_header_recovery(CheckFun) ->
207    {ok, Fd} = couch_file:open(filename(), [create,overwrite]),
208    {ok, RawFd} = file:open(filename(), [read, write, raw, binary]),
209
210    {ok, _} = write_random_data(Fd),
211    ExpectHeader = {some_atom, <<"a binary">>, 756},
212    {ok, ValidHeaderPos} = couch_file:write_header(Fd, ExpectHeader),
213
214    {ok, HeaderPos} = write_random_data(Fd),
215    {ok, _} = couch_file:write_header(Fd, {2342, <<"corruption! greed!">>}),
216
217    CheckFun(Fd, RawFd, {ok, ExpectHeader, ValidHeaderPos}, HeaderPos),
218
219    ok = file:close(RawFd),
220    ok = couch_file:close(Fd),
221    ok.
222
223write_random_data(Fd) ->
224    write_random_data(Fd, 100 + random:uniform(1000)).
225
226write_random_data(Fd, 0) ->
227    {ok, Bytes} = couch_file:bytes(Fd),
228    {ok, (1 + Bytes div sizeblock()) * sizeblock()};
229write_random_data(Fd, N) ->
230    Choices = [foo, bar, <<"bizzingle">>, "bank", ["rough", stuff]],
231    Term = lists:nth(random:uniform(4) + 1, Choices),
232    {ok, _, _} = couch_file:append_term(Fd, Term),
233    write_random_data(Fd, N-1).
234
235