summaryrefslogtreecommitdiff
path: root/test/etap/011-file-headers.t
blob: 4705f629006979f447c09c880703a05ffd687fe2 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
#!/usr/bin/env escript
%% -*- erlang -*-
%%! -pa ./src/couchdb -sasl errlog_type error -boot start_sasl -noshell

% 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.

filename() -> test_util:build_file("test/etap/temp.011").
sizeblock() -> 4096. % Need to keep this in sync with couch_file.erl

main(_) ->
    test_util:init_code_path(),
    {S1, S2, S3} = now(),
    random:seed(S1, S2, S3),

    etap:plan(17),
    case (catch test()) of
        ok ->
            etap:end_tests();
        Other ->
            etap:diag(io_lib:format("Test died abnormally: ~p", [Other])),
            etap:bail()
    end,
    ok.

test() ->
    {ok, Fd} = couch_file:open(filename(), [create,overwrite]),

    etap:is({ok, 0}, couch_file:bytes(Fd),
        "File should be initialized to contain zero bytes."),

    etap:is(ok, couch_file:write_header(Fd, {<<"some_data">>, 32}),
        "Writing a header succeeds."),

    {ok, Size1} = couch_file:bytes(Fd),
    etap:is_greater(Size1, 0,
        "Writing a header allocates space in the file."),

    etap:is({ok, {<<"some_data">>, 32}}, couch_file:read_header(Fd),
        "Reading the header returns what we wrote."),

    etap:is(ok, couch_file:write_header(Fd, [foo, <<"more">>]),
        "Writing a second header succeeds."),

    {ok, Size2} = couch_file:bytes(Fd),
    etap:is_greater(Size2, Size1,
        "Writing a second header allocates more space."),

    etap:is({ok, [foo, <<"more">>]}, couch_file:read_header(Fd),
        "Reading the second header does not return the first header."),

    % Delete the second header.
    ok = couch_file:truncate(Fd, Size1),

    etap:is({ok, {<<"some_data">>, 32}}, couch_file:read_header(Fd),
        "Reading the header after a truncation returns a previous header."),

    couch_file:write_header(Fd, [foo, <<"more">>]),
    etap:is({ok, Size2}, couch_file:bytes(Fd),
        "Rewriting the same second header returns the same second size."),

    ok = couch_file:close(Fd),

    % Now for the fun stuff. Try corrupting the second header and see
    % if we recover properly.

    % Destroy the 0x1 byte that marks a header
    check_header_recovery(fun(CouchFd, RawFd, Expect, HeaderPos) ->
        etap:isnt(Expect, couch_file:read_header(CouchFd),
            "Should return a different header before corruption."),
        file:pwrite(RawFd, HeaderPos, <<0>>),
        etap:is(Expect, couch_file:read_header(CouchFd),
            "Corrupting the byte marker should read the previous header.")
    end),

    % Corrupt the size.
    check_header_recovery(fun(CouchFd, RawFd, Expect, HeaderPos) ->
        etap:isnt(Expect, couch_file:read_header(CouchFd),
            "Should return a different header before corruption."),
        % +1 for 0x1 byte marker
        file:pwrite(RawFd, HeaderPos+1, <<10/integer>>),
        etap:is(Expect, couch_file:read_header(CouchFd),
            "Corrupting the size should read the previous header.")
    end),

    % Corrupt the MD5 signature
    check_header_recovery(fun(CouchFd, RawFd, Expect, HeaderPos) ->
        etap:isnt(Expect, couch_file:read_header(CouchFd),
            "Should return a different header before corruption."),
        % +5 = +1 for 0x1 byte and +4 for term size.
        file:pwrite(RawFd, HeaderPos+5, <<"F01034F88D320B22">>),
        etap:is(Expect, couch_file:read_header(CouchFd),
            "Corrupting the MD5 signature should read the previous header.")
    end),

    % Corrupt the data
    check_header_recovery(fun(CouchFd, RawFd, Expect, HeaderPos) ->
        etap:isnt(Expect, couch_file:read_header(CouchFd),
            "Should return a different header before corruption."),
        % +21 = +1 for 0x1 byte, +4 for term size and +16 for MD5 sig
        file:pwrite(RawFd, HeaderPos+21, <<"some data goes here!">>),
        etap:is(Expect, couch_file:read_header(CouchFd),
            "Corrupting the header data should read the previous header.")
    end),

    ok.

check_header_recovery(CheckFun) ->
    {ok, Fd} = couch_file:open(filename(), [create,overwrite]),
    {ok, RawFd} = file:open(filename(), [read, write, raw, binary]),

    {ok, _} = write_random_data(Fd),
    ExpectHeader = {some_atom, <<"a binary">>, 756},
    ok = couch_file:write_header(Fd, ExpectHeader),

    {ok, HeaderPos} = write_random_data(Fd),
    ok = couch_file:write_header(Fd, {2342, <<"corruption! greed!">>}),

    CheckFun(Fd, RawFd, {ok, ExpectHeader}, HeaderPos),

    ok = file:close(RawFd),
    ok = couch_file:close(Fd),
    ok.

write_random_data(Fd) ->
    write_random_data(Fd, 100 + random:uniform(1000)).

write_random_data(Fd, 0) ->
    {ok, Bytes} = couch_file:bytes(Fd),
    {ok, (1 + Bytes div sizeblock()) * sizeblock()};
write_random_data(Fd, N) ->
    Choices = [foo, bar, <<"bizzingle">>, "bank", ["rough", stuff]],
    Term = lists:nth(random:uniform(4) + 1, Choices),
    {ok, _} = couch_file:append_term(Fd, Term),
    write_random_data(Fd, N-1).