You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@couchdb.apache.org by kx...@apache.org on 2015/12/03 00:02:17 UTC

[17/50] couchdb commit: updated refs/heads/1.x.x to 921006f

Port 130-attachments-md5.t etap test suite to eunit

Add random document id generator macros.
Have to use handmade http client instead of ibrowse since it makes too
complicated sending chunked requests.


Project: http://git-wip-us.apache.org/repos/asf/couchdb/repo
Commit: http://git-wip-us.apache.org/repos/asf/couchdb/commit/dd01551b
Tree: http://git-wip-us.apache.org/repos/asf/couchdb/tree/dd01551b
Diff: http://git-wip-us.apache.org/repos/asf/couchdb/diff/dd01551b

Branch: refs/heads/1.x.x
Commit: dd01551b9d2b042f7beac525fc32a499e4194bcf
Parents: 3a96ebc
Author: Alexander Shorin <kx...@apache.org>
Authored: Wed May 28 05:32:11 2014 +0400
Committer: Alexander Shorin <kx...@apache.org>
Committed: Wed Dec 2 03:49:05 2015 +0300

----------------------------------------------------------------------
 test/couchdb/Makefile.am                   |   1 +
 test/couchdb/couchdb_attachments_tests.erl | 264 ++++++++++++++++++++++++
 test/couchdb/include/couch_eunit.hrl.in    |   5 +
 test/etap/130-attachments-md5.t            | 248 ----------------------
 test/etap/Makefile.am                      |   1 -
 5 files changed, 270 insertions(+), 249 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/couchdb/blob/dd01551b/test/couchdb/Makefile.am
----------------------------------------------------------------------
diff --git a/test/couchdb/Makefile.am b/test/couchdb/Makefile.am
index 1fe4da3..540583b 100644
--- a/test/couchdb/Makefile.am
+++ b/test/couchdb/Makefile.am
@@ -36,6 +36,7 @@ eunit_files = \
     couch_util_tests.erl \
     couch_uuids_tests.erl \
     couch_work_queue_tests.erl \
+    couchdb_attachments_tests.erl \
     couchdb_file_compression_tests.erl \
     couchdb_modules_load_tests.erl \
     couchdb_update_conflicts_tests.erl \

http://git-wip-us.apache.org/repos/asf/couchdb/blob/dd01551b/test/couchdb/couchdb_attachments_tests.erl
----------------------------------------------------------------------
diff --git a/test/couchdb/couchdb_attachments_tests.erl b/test/couchdb/couchdb_attachments_tests.erl
new file mode 100644
index 0000000..aef1873
--- /dev/null
+++ b/test/couchdb/couchdb_attachments_tests.erl
@@ -0,0 +1,264 @@
+% 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.
+
+-module(couchdb_attachments_tests).
+
+-include("couch_eunit.hrl").
+-include_lib("couchdb/couch_db.hrl").
+
+-define(TIMEOUT, 1000).
+-define(TIMEWAIT, 100).
+-define(i2l(I), integer_to_list(I)).
+
+
+start() ->
+    {ok, Pid} = couch_server_sup:start_link(?CONFIG_CHAIN),
+    Pid.
+
+stop(Pid) ->
+    erlang:monitor(process, Pid),
+    couch_server_sup:stop(),
+    receive
+        {'DOWN', _, _, Pid, _} ->
+            ok
+    after ?TIMEOUT ->
+        throw({timeout, server_stop})
+    end.
+
+setup() ->
+    DbName = ?tempdb(),
+    {ok, Db} = couch_db:create(DbName, []),
+    ok = couch_db:close(Db),
+    Addr = couch_config:get("httpd", "bind_address", any),
+    Port = mochiweb_socket_server:get(couch_httpd, port),
+    Host = Addr ++ ":" ++ ?i2l(Port),
+    {Host, ?b2l(DbName)}.
+
+teardown({_, DbName}) ->
+    ok = couch_server:delete(?l2b(DbName), []),
+    ok.
+
+
+attachments_test_() ->
+    {
+        "Attachments tests",
+        {
+            setup,
+            fun start/0, fun stop/1,
+            [
+                attachments_md5_tests()
+            ]
+        }
+    }.
+
+attachments_md5_tests() ->
+    {
+        "Attachments MD5 tests",
+        {
+            foreach,
+            fun setup/0, fun teardown/1,
+            [
+                fun should_upload_attachment_without_md5/1,
+                fun should_upload_attachment_by_chunks_without_md5/1,
+                fun should_upload_attachment_with_valid_md5_header/1,
+                fun should_upload_attachment_by_chunks_with_valid_md5_header/1,
+                fun should_upload_attachment_by_chunks_with_valid_md5_trailer/1,
+                fun should_reject_attachment_with_invalid_md5/1,
+                fun should_reject_chunked_attachment_with_invalid_md5/1,
+                fun should_reject_chunked_attachment_with_invalid_md5_trailer/1
+            ]
+        }
+    }.
+
+
+should_upload_attachment_without_md5({Host, DbName}) ->
+    ?_test(begin
+        AttUrl = string:join(["", DbName, ?docid(), "readme.txt"], "/"),
+        Body = "We all live in a yellow submarine!",
+        Headers = [
+            {"Content-Length", "34"},
+            {"Content-Type", "text/plain"},
+            {"Host", Host}
+        ],
+        {ok, Code, Json} = request("PUT", AttUrl, Headers, Body),
+        ?assertEqual(201, Code),
+        ?assertEqual(true, get_json(Json, [<<"ok">>]))
+    end).
+
+should_upload_attachment_by_chunks_without_md5({Host, DbName}) ->
+    ?_test(begin
+        AttUrl = string:join(["", DbName, ?docid(), "readme.txt"], "/"),
+        AttData = <<"We all live in a yellow submarine!">>,
+        <<Part1:21/binary, Part2:13/binary>> = AttData,
+        Body = chunked_body([Part1, Part2]),
+        Headers = [
+            {"Content-Type", "text/plain"},
+            {"Transfer-Encoding", "chunked"},
+            {"Host", Host}
+        ],
+        {ok, Code, Json} = request("PUT", AttUrl, Headers, Body),
+        ?assertEqual(201, Code),
+        ?assertEqual(true, get_json(Json, [<<"ok">>]))
+    end).
+
+should_upload_attachment_with_valid_md5_header({Host, DbName}) ->
+    ?_test(begin
+        AttUrl = string:join(["", DbName, ?docid(), "readme.txt"], "/"),
+        Body = "We all live in a yellow submarine!",
+        Headers = [
+            {"Content-Length", "34"},
+            {"Content-Type", "text/plain"},
+            {"Content-MD5", ?b2l(base64:encode(couch_util:md5(Body)))},
+            {"Host", Host}
+        ],
+        {ok, Code, Json} = request("PUT", AttUrl, Headers, Body),
+        ?assertEqual(201, Code),
+        ?assertEqual(true, get_json(Json, [<<"ok">>]))
+    end).
+
+should_upload_attachment_by_chunks_with_valid_md5_header({Host, DbName}) ->
+    ?_test(begin
+        AttUrl = string:join(["", DbName, ?docid(), "readme.txt"], "/"),
+        AttData = <<"We all live in a yellow submarine!">>,
+        <<Part1:21/binary, Part2:13/binary>> = AttData,
+        Body = chunked_body([Part1, Part2]),
+        Headers = [
+            {"Content-Type", "text/plain"},
+            {"Content-MD5", ?b2l(base64:encode(couch_util:md5(AttData)))},
+            {"Host", Host},
+            {"Transfer-Encoding", "chunked"}
+        ],
+        {ok, Code, Json} = request("PUT", AttUrl, Headers, Body),
+        ?assertEqual(201, Code),
+        ?assertEqual(true, get_json(Json, [<<"ok">>]))
+    end).
+
+should_upload_attachment_by_chunks_with_valid_md5_trailer({Host, DbName}) ->
+    ?_test(begin
+        AttUrl = string:join(["", DbName, ?docid(), "readme.txt"], "/"),
+        AttData = <<"We all live in a yellow submarine!">>,
+        <<Part1:21/binary, Part2:13/binary>> = AttData,
+        Body = [chunked_body([Part1, Part2]),
+                "Content-MD5: ", base64:encode(couch_util:md5(AttData)),
+                "\r\n"],
+        Headers = [
+            {"Content-Type", "text/plain"},
+            {"Host", Host},
+            {"Trailer", "Content-MD5"},
+            {"Transfer-Encoding", "chunked"}
+        ],
+        {ok, Code, Json} = request("PUT", AttUrl, Headers, Body),
+        ?assertEqual(201, Code),
+        ?assertEqual(true, get_json(Json, [<<"ok">>]))
+    end).
+
+should_reject_attachment_with_invalid_md5({Host, DbName}) ->
+    ?_test(begin
+        AttUrl = string:join(["", DbName, ?docid(), "readme.txt"], "/"),
+        Body = "We all live in a yellow submarine!",
+        Headers = [
+            {"Content-Length", "34"},
+            {"Content-Type", "text/plain"},
+            {"Content-MD5", ?b2l(base64:encode(<<"foobar!">>))},
+            {"Host", Host}
+        ],
+        {ok, Code, Json} = request("PUT", AttUrl, Headers, Body),
+        ?assertEqual(400, Code),
+        ?assertEqual(<<"content_md5_mismatch">>,
+                     get_json(Json, [<<"error">>]))
+    end).
+
+
+should_reject_chunked_attachment_with_invalid_md5({Host, DbName}) ->
+    ?_test(begin
+        AttUrl = string:join(["", DbName, ?docid(), "readme.txt"], "/"),
+        AttData = <<"We all live in a yellow submarine!">>,
+        <<Part1:21/binary, Part2:13/binary>> = AttData,
+        Body = chunked_body([Part1, Part2]),
+        Headers = [
+            {"Content-Type", "text/plain"},
+            {"Content-MD5", ?b2l(base64:encode(<<"foobar!">>))},
+            {"Host", Host},
+            {"Transfer-Encoding", "chunked"}
+        ],
+        {ok, Code, Json} = request("PUT", AttUrl, Headers, Body),
+        ?assertEqual(400, Code),
+        ?assertEqual(<<"content_md5_mismatch">>,
+                     get_json(Json, [<<"error">>]))
+    end).
+
+should_reject_chunked_attachment_with_invalid_md5_trailer({Host, DbName}) ->
+    ?_test(begin
+        AttUrl = string:join(["", DbName, ?docid(), "readme.txt"], "/"),
+        AttData = <<"We all live in a yellow submarine!">>,
+        <<Part1:21/binary, Part2:13/binary>> = AttData,
+        Body = [chunked_body([Part1, Part2]),
+                "Content-MD5: ", base64:encode(<<"foobar!">>),
+                "\r\n"],
+        Headers = [
+            {"Content-Type", "text/plain"},
+            {"Host", Host},
+            {"Trailer", "Content-MD5"},
+            {"Transfer-Encoding", "chunked"}
+        ],
+        {ok, Code, Json} = request("PUT", AttUrl, Headers, Body),
+        ?assertEqual(400, Code),
+        ?assertEqual(<<"content_md5_mismatch">>,
+                     get_json(Json, [<<"error">>]))
+    end).
+
+
+get_json(Json, Path) ->
+    couch_util:get_nested_json_value(Json, Path).
+
+to_hex(Val) ->
+    to_hex(Val, []).
+
+to_hex(0, Acc) ->
+    Acc;
+to_hex(Val, Acc) ->
+    to_hex(Val div 16, [hex_char(Val rem 16) | Acc]).
+
+hex_char(V) when V < 10 -> $0 + V;
+hex_char(V) -> $A + V - 10.
+
+chunked_body(Chunks) ->
+    chunked_body(Chunks, []).
+
+chunked_body([], Acc) ->
+    iolist_to_binary(lists:reverse(Acc, "0\r\n"));
+chunked_body([Chunk | Rest], Acc) ->
+    Size = to_hex(size(Chunk)),
+    chunked_body(Rest, ["\r\n", Chunk, "\r\n", Size | Acc]).
+
+get_socket() ->
+    Options = [binary, {packet, 0}, {active, false}],
+    Addr = couch_config:get("httpd", "bind_address", any),
+    Port = mochiweb_socket_server:get(couch_httpd, port),
+    {ok, Sock} = gen_tcp:connect(Addr, Port, Options),
+    Sock.
+
+request(Method, Url, Headers, Body) ->
+    RequestHead = [Method, " ", Url, " HTTP/1.1"],
+    RequestHeaders = [[string:join([Key, Value], ": "), "\r\n"]
+                      || {Key, Value} <- Headers],
+    Request = [RequestHead, "\r\n", RequestHeaders, "\r\n", Body, "\r\n"],
+    Sock = get_socket(),
+    gen_tcp:send(Sock, list_to_binary(lists:flatten(Request))),
+    timer:sleep(?TIMEWAIT),  % must wait to receive complete response
+    {ok, R} = gen_tcp:recv(Sock, 0),
+    gen_tcp:close(Sock),
+    [Header, Body1] = re:split(R, "\r\n\r\n", [{return, binary}]),
+    {ok, {http_response, _, Code, _}, _} =
+        erlang:decode_packet(http, Header, []),
+    Json = ejson:decode(Body1),
+    {ok, Code, Json}.

http://git-wip-us.apache.org/repos/asf/couchdb/blob/dd01551b/test/couchdb/include/couch_eunit.hrl.in
----------------------------------------------------------------------
diff --git a/test/couchdb/include/couch_eunit.hrl.in b/test/couchdb/include/couch_eunit.hrl.in
index 6a46d2f..ff080e1 100644
--- a/test/couchdb/include/couch_eunit.hrl.in
+++ b/test/couchdb/include/couch_eunit.hrl.in
@@ -37,3 +37,8 @@
             Suffix = lists:concat([integer_to_list(Num) || Num <- Nums]),
             list_to_binary(Prefix ++ "-" ++ Suffix)
     end).
+-define(docid,
+    fun() ->
+        {A, B, C} = erlang:now(),
+        lists:flatten(io_lib:format("~p~p~p", [A, B, C]))
+    end).

http://git-wip-us.apache.org/repos/asf/couchdb/blob/dd01551b/test/etap/130-attachments-md5.t
----------------------------------------------------------------------
diff --git a/test/etap/130-attachments-md5.t b/test/etap/130-attachments-md5.t
deleted file mode 100755
index a91c9bf..0000000
--- a/test/etap/130-attachments-md5.t
+++ /dev/null
@@ -1,248 +0,0 @@
-#!/usr/bin/env escript
-% 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.
-
-test_db_name() ->
-    <<"etap-test-db">>.
-
-docid() ->
-    case get(docid) of
-        undefined ->
-            put(docid, 1),
-            "1";
-        Count ->
-            put(docid, Count+1),
-            integer_to_list(Count+1)
-    end.
-
-main(_) ->
-    test_util:init_code_path(),
-    
-    etap:plan(16),
-    case (catch test()) of
-        ok ->
-            etap:end_tests();
-        Other ->
-            etap:diag(io_lib:format("Test died abnormally: ~p", [Other])),
-            etap:bail(Other)
-    end,
-    ok.
-
-test() ->
-    couch_server_sup:start_link(test_util:config_files()),
-    Addr = couch_config:get("httpd", "bind_address", any),
-    put(addr, Addr),
-    put(port, mochiweb_socket_server:get(couch_httpd, port)),
-    timer:sleep(1000),
-
-    couch_server:delete(test_db_name(), []),
-    couch_db:create(test_db_name(), []),
-
-    test_identity_without_md5(),
-    test_chunked_without_md5(),
-
-    test_identity_with_valid_md5(),
-    test_chunked_with_valid_md5_header(),
-    test_chunked_with_valid_md5_trailer(),
-
-    test_identity_with_invalid_md5(),
-    test_chunked_with_invalid_md5_header(),
-    test_chunked_with_invalid_md5_trailer(),
-
-    couch_server:delete(test_db_name(), []),
-    couch_server_sup:stop(),
-    ok.
-
-test_identity_without_md5() ->
-    Data = [
-        "PUT /", test_db_name(), "/", docid(), "/readme.txt HTTP/1.1\r\n",
-        "Content-Type: text/plain\r\n",
-        "Content-Length: 34\r\n",
-        "\r\n",
-        "We all live in a yellow submarine!"],
-
-    {Code, Json} = do_request(Data),
-    etap:is(Code, 201, "Stored with identity encoding and no MD5"),
-    etap:is(get_json(Json, [<<"ok">>]), true, "Body indicates success.").
-
-test_chunked_without_md5() ->
-    AttData = <<"We all live in a yellow submarine!">>,
-    <<Part1:21/binary, Part2:13/binary>> = AttData,
-    Data = [
-        "PUT /", test_db_name(), "/", docid(), "/readme.txt HTTP/1.1\r\n",
-        "Content-Type: text/plain\r\n",
-        "Transfer-Encoding: chunked\r\n",
-        "\r\n",
-        to_hex(size(Part1)), "\r\n",
-        Part1, "\r\n",
-        to_hex(size(Part2)), "\r\n",
-        Part2, "\r\n"
-        "0\r\n"
-        "\r\n"],
-
-    {Code, Json} = do_request(Data),
-    etap:is(Code, 201, "Stored with chunked encoding and no MD5"),
-    etap:is(get_json(Json, [<<"ok">>]), true, "Body indicates success.").
-
-test_identity_with_valid_md5() ->
-    AttData = "We all live in a yellow submarine!",
-    Data = [
-        "PUT /", test_db_name(), "/", docid(), "/readme.txt HTTP/1.1\r\n",
-        "Content-Type: text/plain\r\n",
-        "Content-Length: 34\r\n",
-        "Content-MD5: ", base64:encode(couch_util:md5(AttData)), "\r\n",
-        "\r\n",
-        AttData],
-
-    {Code, Json} = do_request(Data),
-    etap:is(Code, 201, "Stored with identity encoding and valid MD5"),
-    etap:is(get_json(Json, [<<"ok">>]), true, "Body indicates success.").
-
-test_chunked_with_valid_md5_header() ->
-    AttData = <<"We all live in a yellow submarine!">>,
-    <<Part1:21/binary, Part2:13/binary>> = AttData,
-    Data = [
-        "PUT /", test_db_name(), "/", docid(), "/readme.txt HTTP/1.1\r\n",
-        "Content-Type: text/plain\r\n",
-        "Transfer-Encoding: chunked\r\n",
-        "Content-MD5: ", base64:encode(couch_util:md5(AttData)), "\r\n",
-        "\r\n",
-        to_hex(size(Part1)), "\r\n",
-        Part1, "\r\n",
-        to_hex(size(Part2)), "\r\n",
-        Part2, "\r\n",
-        "0\r\n",
-        "\r\n"],
-
-    {Code, Json} = do_request(Data),
-    etap:is(Code, 201, "Stored with chunked encoding and valid MD5 header."),
-    etap:is(get_json(Json, [<<"ok">>]), true, "Body indicates success.").
-
-test_chunked_with_valid_md5_trailer() ->
-    AttData = <<"We all live in a yellow submarine!">>,
-    <<Part1:21/binary, Part2:13/binary>> = AttData,
-    Data = [
-        "PUT /", test_db_name(), "/", docid(), "/readme.txt HTTP/1.1\r\n",
-        "Content-Type: text/plain\r\n",
-        "Transfer-Encoding: chunked\r\n",
-        "Trailer: Content-MD5\r\n",
-        "\r\n",
-        to_hex(size(Part1)), "\r\n",
-        Part1, "\r\n",
-        to_hex(size(Part2)), "\r\n",
-        Part2, "\r\n",
-        "0\r\n",
-        "Content-MD5: ", base64:encode(couch_util:md5(AttData)), "\r\n",
-        "\r\n"],
-
-    {Code, Json} = do_request(Data),
-    etap:is(Code, 201, "Stored with chunked encoding and valid MD5 trailer."),
-    etap:is(get_json(Json, [<<"ok">>]), true, "Body indicates success.").
-
-test_identity_with_invalid_md5() ->
-    Data = [
-        "PUT /", test_db_name(), "/", docid(), "/readme.txt HTTP/1.1\r\n",
-        "Content-Type: text/plain\r\n",
-        "Content-Length: 34\r\n",
-        "Content-MD5: ", base64:encode(<<"foobar!">>), "\r\n",
-        "\r\n",
-        "We all live in a yellow submarine!"],
-
-    {Code, Json} = do_request(Data),
-    etap:is(Code, 400, "Invalid MD5 header causes an error: identity"),
-    etap:is(
-        get_json(Json, [<<"error">>]),
-        <<"content_md5_mismatch">>,
-        "Body indicates reason for failure."
-    ).
-
-test_chunked_with_invalid_md5_header() ->
-    AttData = <<"We all live in a yellow submarine!">>,
-    <<Part1:21/binary, Part2:13/binary>> = AttData,
-    Data = [
-        "PUT /", test_db_name(), "/", docid(), "/readme.txt HTTP/1.1\r\n",
-        "Content-Type: text/plain\r\n",
-        "Transfer-Encoding: chunked\r\n",
-        "Content-MD5: ", base64:encode(<<"so sneaky...">>), "\r\n",
-        "\r\n",
-        to_hex(size(Part1)), "\r\n",
-        Part1, "\r\n",
-        to_hex(size(Part2)), "\r\n",
-        Part2, "\r\n",
-        "0\r\n",
-        "\r\n"],
-
-    {Code, Json} = do_request(Data),
-    etap:is(Code, 400, "Invalid MD5 header causes an error: chunked"),
-    etap:is(
-        get_json(Json, [<<"error">>]),
-        <<"content_md5_mismatch">>,
-        "Body indicates reason for failure."
-    ).
-
-test_chunked_with_invalid_md5_trailer() ->
-    AttData = <<"We all live in a yellow submarine!">>,
-    <<Part1:21/binary, Part2:13/binary>> = AttData,
-    Data = [
-        "PUT /", test_db_name(), "/", docid(), "/readme.txt HTTP/1.1\r\n",
-        "Content-Type: text/plain\r\n",
-        "Transfer-Encoding: chunked\r\n",
-        "Trailer: Content-MD5\r\n",
-        "\r\n",
-        to_hex(size(Part1)), "\r\n",
-        Part1, "\r\n",
-        to_hex(size(Part2)), "\r\n",
-        Part2, "\r\n",
-        "0\r\n",
-        "Content-MD5: ", base64:encode(<<"Kool-Aid Fountain!">>), "\r\n",
-        "\r\n"],
-
-    {Code, Json} = do_request(Data),
-    etap:is(Code, 400, "Invalid MD5 Trailer causes an error"),
-    etap:is(
-        get_json(Json, [<<"error">>]),
-        <<"content_md5_mismatch">>,
-        "Body indicates reason for failure."
-    ).
-
-
-get_socket() ->
-    Options = [binary, {packet, 0}, {active, false}],
-    {ok, Sock} = gen_tcp:connect(get(addr), get(port), Options),
-    Sock.
-
-do_request(Request) ->
-    Sock = get_socket(),
-    gen_tcp:send(Sock, list_to_binary(lists:flatten(Request))),
-    timer:sleep(1000),
-    {ok, R} = gen_tcp:recv(Sock, 0),
-    gen_tcp:close(Sock),
-    [Header, Body] = re:split(R, "\r\n\r\n", [{return, binary}]),
-    {ok, {http_response, _, Code, _}, _} =
-        erlang:decode_packet(http, Header, []),
-    Json = ejson:decode(Body),
-    {Code, Json}.
-
-get_json(Json, Path) ->
-    couch_util:get_nested_json_value(Json, Path).
-
-to_hex(Val) ->
-    to_hex(Val, []).
-
-to_hex(0, Acc) ->
-    Acc;
-to_hex(Val, Acc) ->
-    to_hex(Val div 16, [hex_char(Val rem 16) | Acc]).
-
-hex_char(V) when V < 10 -> $0 + V;
-hex_char(V) -> $A + V - 10.
-

http://git-wip-us.apache.org/repos/asf/couchdb/blob/dd01551b/test/etap/Makefile.am
----------------------------------------------------------------------
diff --git a/test/etap/Makefile.am b/test/etap/Makefile.am
index abe252d..f54e927 100644
--- a/test/etap/Makefile.am
+++ b/test/etap/Makefile.am
@@ -36,7 +36,6 @@ fixture_files = \
     fixtures/test.couch
 
 tap_files = \
-    130-attachments-md5.t \
     140-attachment-comp.t \
     150-invalid-view-seq.t \
     160-vhosts.t \