Return-Path: Delivered-To: apmail-couchdb-commits-archive@www.apache.org Received: (qmail 30285 invoked from network); 26 Nov 2010 16:30:10 -0000 Received: from unknown (HELO mail.apache.org) (140.211.11.3) by 140.211.11.9 with SMTP; 26 Nov 2010 16:30:10 -0000 Received: (qmail 80756 invoked by uid 500); 26 Nov 2010 16:30:10 -0000 Delivered-To: apmail-couchdb-commits-archive@couchdb.apache.org Received: (qmail 80638 invoked by uid 500); 26 Nov 2010 16:30:09 -0000 Mailing-List: contact commits-help@couchdb.apache.org; run by ezmlm Precedence: bulk List-Help: List-Unsubscribe: List-Post: List-Id: Reply-To: dev@couchdb.apache.org Delivered-To: mailing list commits@couchdb.apache.org Received: (qmail 80631 invoked by uid 99); 26 Nov 2010 16:30:09 -0000 Received: from athena.apache.org (HELO athena.apache.org) (140.211.11.136) by apache.org (qpsmtpd/0.29) with ESMTP; Fri, 26 Nov 2010 16:30:09 +0000 X-ASF-Spam-Status: No, hits=-2000.0 required=10.0 tests=ALL_TRUSTED X-Spam-Check-By: apache.org Received: from [140.211.11.4] (HELO eris.apache.org) (140.211.11.4) by apache.org (qpsmtpd/0.29) with ESMTP; Fri, 26 Nov 2010 16:30:06 +0000 Received: by eris.apache.org (Postfix, from userid 65534) id D5D1D23889ED; Fri, 26 Nov 2010 16:28:34 +0000 (UTC) Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit Subject: svn commit: r1039446 - /couchdb/branches/mochiweb-1.4.1/src/mochiweb/ Date: Fri, 26 Nov 2010 16:28:34 -0000 To: commits@couchdb.apache.org From: rnewson@apache.org X-Mailer: svnmailer-1.0.8 Message-Id: <20101126162834.D5D1D23889ED@eris.apache.org> Author: rnewson Date: Fri Nov 26 16:28:33 2010 New Revision: 1039446 URL: http://svn.apache.org/viewvc?rev=1039446&view=rev Log: Upgrade to mochiweb 1.4.1. Added: couchdb/branches/mochiweb-1.4.1/src/mochiweb/mochiweb_request_tests.erl Modified: couchdb/branches/mochiweb-1.4.1/src/mochiweb/Makefile.am couchdb/branches/mochiweb-1.4.1/src/mochiweb/mochijson2.erl couchdb/branches/mochiweb-1.4.1/src/mochiweb/mochinum.erl couchdb/branches/mochiweb-1.4.1/src/mochiweb/mochiweb.app.in couchdb/branches/mochiweb-1.4.1/src/mochiweb/mochiweb.app.src couchdb/branches/mochiweb-1.4.1/src/mochiweb/mochiweb_acceptor.erl couchdb/branches/mochiweb-1.4.1/src/mochiweb/mochiweb_html.erl couchdb/branches/mochiweb-1.4.1/src/mochiweb/mochiweb_http.erl couchdb/branches/mochiweb-1.4.1/src/mochiweb/mochiweb_request.erl couchdb/branches/mochiweb-1.4.1/src/mochiweb/mochiweb_socket_server.erl couchdb/branches/mochiweb-1.4.1/src/mochiweb/mochiweb_util.erl Modified: couchdb/branches/mochiweb-1.4.1/src/mochiweb/Makefile.am URL: http://svn.apache.org/viewvc/couchdb/branches/mochiweb-1.4.1/src/mochiweb/Makefile.am?rev=1039446&r1=1039445&r2=1039446&view=diff ============================================================================== --- couchdb/branches/mochiweb-1.4.1/src/mochiweb/Makefile.am (original) +++ couchdb/branches/mochiweb-1.4.1/src/mochiweb/Makefile.am Fri Nov 26 16:28:33 2010 @@ -10,7 +10,7 @@ ## License for the specific language governing permissions and limitations under ## the License. -mochiwebebindir = $(localerlanglibdir)/mochiweb-7c2bc2/ebin +mochiwebebindir = $(localerlanglibdir)/mochiweb-1.4.1/ebin mochiweb_file_collection = \ mochifmt.erl \ Modified: couchdb/branches/mochiweb-1.4.1/src/mochiweb/mochijson2.erl URL: http://svn.apache.org/viewvc/couchdb/branches/mochiweb-1.4.1/src/mochiweb/mochijson2.erl?rev=1039446&r1=1039445&r2=1039446&view=diff ============================================================================== --- couchdb/branches/mochiweb-1.4.1/src/mochiweb/mochijson2.erl (original) +++ couchdb/branches/mochiweb-1.4.1/src/mochiweb/mochijson2.erl Fri Nov 26 16:28:33 2010 @@ -4,6 +4,38 @@ %% @doc Yet another JSON (RFC 4627) library for Erlang. mochijson2 works %% with binaries as strings, arrays as lists (without an {array, _}) %% wrapper and it only knows how to decode UTF-8 (and ASCII). +%% +%% JSON terms are decoded as follows (javascript -> erlang): +%%
    +%%
  • {"key": "value"} -> +%% {struct, [{<<"key">>, <<"value">>}]}
  • +%%
  • ["array", 123, 12.34, true, false, null] -> +%% [<<"array">>, 123, 12.34, true, false, null] +%%
  • +%%
+%%
    +%%
  • Strings in JSON decode to UTF-8 binaries in Erlang
  • +%%
  • Objects decode to {struct, PropList}
  • +%%
  • Numbers decode to integer or float
  • +%%
  • true, false, null decode to their respective terms.
  • +%%
+%% The encoder will accept the same format that the decoder will produce, +%% but will also allow additional cases for leniency: +%%
    +%%
  • atoms other than true, false, null will be considered UTF-8 +%% strings (even as a proplist key) +%%
  • +%%
  • {json, IoList} will insert IoList directly into the output +%% with no validation +%%
  • +%%
  • {array, Array} will be encoded as Array +%% (legacy mochijson style) +%%
  • +%%
  • A non-empty raw proplist will be encoded as an object as long +%% as the first pair does not have an atom key of json, struct, +%% or array +%%
  • +%%
-module(mochijson2). -author('bob@mochimedia.com'). @@ -101,10 +133,16 @@ json_encode(F, _State) when is_float(F) mochinum:digits(F); json_encode(S, State) when is_binary(S); is_atom(S) -> json_encode_string(S, State); -json_encode(Array, State) when is_list(Array) -> - json_encode_array(Array, State); +json_encode([{K, _}|_] = Props, State) when (K =/= struct andalso + K =/= array andalso + K =/= json) -> + json_encode_proplist(Props, State); json_encode({struct, Props}, State) when is_list(Props) -> json_encode_proplist(Props, State); +json_encode(Array, State) when is_list(Array) -> + json_encode_array(Array, State); +json_encode({array, Array}, State) when is_list(Array) -> + json_encode_array(Array, State); json_encode({json, IoList}, _State) -> IoList; json_encode(Bad, #encoder{handler=null}) -> @@ -732,6 +770,15 @@ key_encode_test() -> ?assertEqual( <<"{\"foo\":1}">>, iolist_to_binary(encode({struct, [{"foo", 1}]}))), + ?assertEqual( + <<"{\"foo\":1}">>, + iolist_to_binary(encode([{foo, 1}]))), + ?assertEqual( + <<"{\"foo\":1}">>, + iolist_to_binary(encode([{<<"foo">>, 1}]))), + ?assertEqual( + <<"{\"foo\":1}">>, + iolist_to_binary(encode([{"foo", 1}]))), ?assertEqual( <<"{\"\\ud834\\udd20\":1}">>, iolist_to_binary( Modified: couchdb/branches/mochiweb-1.4.1/src/mochiweb/mochinum.erl URL: http://svn.apache.org/viewvc/couchdb/branches/mochiweb-1.4.1/src/mochiweb/mochinum.erl?rev=1039446&r1=1039445&r2=1039446&view=diff ============================================================================== --- couchdb/branches/mochiweb-1.4.1/src/mochiweb/mochinum.erl (original) +++ couchdb/branches/mochiweb-1.4.1/src/mochiweb/mochinum.erl Fri Nov 26 16:28:33 2010 @@ -29,11 +29,10 @@ digits(N) when is_integer(N) -> digits(0.0) -> "0.0"; digits(Float) -> - {Frac, Exp} = frexp(Float), - Exp1 = Exp - 53, - Frac1 = trunc(abs(Frac) * (1 bsl 53)), - [Place | Digits] = digits1(Float, Exp1, Frac1), - R = insert_decimal(Place, [$0 + D || D <- Digits]), + {Frac1, Exp1} = frexp_int(Float), + [Place0 | Digits0] = digits1(Float, Exp1, Frac1), + {Place, Digits} = transform_digits(Place0, Digits0), + R = insert_decimal(Place, Digits), case Float < 0 of true -> [$- | R]; @@ -64,7 +63,6 @@ int_pow(X, N) when N > 0 -> int_ceil(X) -> T = trunc(X), case (X - T) of - Neg when Neg < 0 -> T; Pos when Pos > 0 -> T + 1; _ -> T end. @@ -228,6 +226,20 @@ log2floor(Int, N) -> log2floor(Int bsr 1, 1 + N). +transform_digits(Place, [0 | Rest]) -> + transform_digits(Place, Rest); +transform_digits(Place, Digits) -> + {Place, [$0 + D || D <- Digits]}. + + +frexp_int(F) -> + case unpack(F) of + {_Sign, 0, Frac} -> + {Frac, ?MIN_EXP}; + {_Sign, Exp, Frac} -> + {Frac + (1 bsl 52), Exp - 53 - ?FLOAT_BIAS} + end. + %% %% Tests %% @@ -235,21 +247,21 @@ log2floor(Int, N) -> -ifdef(TEST). int_ceil_test() -> - 1 = int_ceil(0.0001), - 0 = int_ceil(0.0), - 1 = int_ceil(0.99), - 1 = int_ceil(1.0), - -1 = int_ceil(-1.5), - -2 = int_ceil(-2.0), + ?assertEqual(1, int_ceil(0.0001)), + ?assertEqual(0, int_ceil(0.0)), + ?assertEqual(1, int_ceil(0.99)), + ?assertEqual(1, int_ceil(1.0)), + ?assertEqual(-1, int_ceil(-1.5)), + ?assertEqual(-2, int_ceil(-2.0)), ok. int_pow_test() -> - 1 = int_pow(1, 1), - 1 = int_pow(1, 0), - 1 = int_pow(10, 0), - 10 = int_pow(10, 1), - 100 = int_pow(10, 2), - 1000 = int_pow(10, 3), + ?assertEqual(1, int_pow(1, 1)), + ?assertEqual(1, int_pow(1, 0)), + ?assertEqual(1, int_pow(10, 0)), + ?assertEqual(10, int_pow(10, 1)), + ?assertEqual(100, int_pow(10, 2)), + ?assertEqual(1000, int_pow(10, 3)), ok. digits_test() -> @@ -274,9 +286,9 @@ digits_test() -> ?assertEqual("4503599627370496.0", digits(4503599627370496.0)), %% small denormalized number - %% 4.94065645841246544177e-324 + %% 4.94065645841246544177e-324 =:= 5.0e-324 <> = <<0,0,0,0,0,0,0,1>>, - ?assertEqual("4.9406564584124654e-324", + ?assertEqual("5.0e-324", digits(SmallDenorm)), ?assertEqual(SmallDenorm, list_to_float(digits(SmallDenorm))), @@ -301,31 +313,42 @@ digits_test() -> digits(LargeNorm)), ?assertEqual(LargeNorm, list_to_float(digits(LargeNorm))), + %% issue #10 - mochinum:frexp(math:pow(2, -1074)). + ?assertEqual("5.0e-324", + digits(math:pow(2, -1074))), ok. frexp_test() -> %% zero - {0.0, 0} = frexp(0.0), + ?assertEqual({0.0, 0}, frexp(0.0)), %% one - {0.5, 1} = frexp(1.0), + ?assertEqual({0.5, 1}, frexp(1.0)), %% negative one - {-0.5, 1} = frexp(-1.0), + ?assertEqual({-0.5, 1}, frexp(-1.0)), %% small denormalized number %% 4.94065645841246544177e-324 <> = <<0,0,0,0,0,0,0,1>>, - {0.5, -1073} = frexp(SmallDenorm), + ?assertEqual({0.5, -1073}, frexp(SmallDenorm)), %% large denormalized number %% 2.22507385850720088902e-308 <> = <<0,15,255,255,255,255,255,255>>, - {0.99999999999999978, -1022} = frexp(BigDenorm), + ?assertEqual( + {0.99999999999999978, -1022}, + frexp(BigDenorm)), %% small normalized number %% 2.22507385850720138309e-308 <> = <<0,16,0,0,0,0,0,0>>, - {0.5, -1021} = frexp(SmallNorm), + ?assertEqual({0.5, -1021}, frexp(SmallNorm)), %% large normalized number %% 1.79769313486231570815e+308 <> = <<127,239,255,255,255,255,255,255>>, - {0.99999999999999989, 1024} = frexp(LargeNorm), + ?assertEqual( + {0.99999999999999989, 1024}, + frexp(LargeNorm)), + %% issue #10 - mochinum:frexp(math:pow(2, -1074)). + ?assertEqual( + {0.5, -1073}, + frexp(math:pow(2, -1074))), ok. -endif. Modified: couchdb/branches/mochiweb-1.4.1/src/mochiweb/mochiweb.app.in URL: http://svn.apache.org/viewvc/couchdb/branches/mochiweb-1.4.1/src/mochiweb/mochiweb.app.in?rev=1039446&r1=1039445&r2=1039446&view=diff ============================================================================== --- couchdb/branches/mochiweb-1.4.1/src/mochiweb/mochiweb.app.in (original) +++ couchdb/branches/mochiweb-1.4.1/src/mochiweb/mochiweb.app.in Fri Nov 26 16:28:33 2010 @@ -1,6 +1,6 @@ {application, mochiweb, [{description, "MochiMedia Web Server"}, - {vsn, "7c2bc2"}, + {vsn, "1.4.1"}, {modules, [ mochihex, mochijson, Modified: couchdb/branches/mochiweb-1.4.1/src/mochiweb/mochiweb.app.src URL: http://svn.apache.org/viewvc/couchdb/branches/mochiweb-1.4.1/src/mochiweb/mochiweb.app.src?rev=1039446&r1=1039445&r2=1039446&view=diff ============================================================================== --- couchdb/branches/mochiweb-1.4.1/src/mochiweb/mochiweb.app.src (original) +++ couchdb/branches/mochiweb-1.4.1/src/mochiweb/mochiweb.app.src Fri Nov 26 16:28:33 2010 @@ -1,7 +1,7 @@ %% This is generated from src/mochiweb.app.src {application, mochiweb, [{description, "MochiMedia Web Server"}, - {vsn, "7c2bc2"}, + {vsn, "1.4.1"}, {modules, []}, {registered, []}, {mod, {mochiweb_app, []}}, Modified: couchdb/branches/mochiweb-1.4.1/src/mochiweb/mochiweb_acceptor.erl URL: http://svn.apache.org/viewvc/couchdb/branches/mochiweb-1.4.1/src/mochiweb/mochiweb_acceptor.erl?rev=1039446&r1=1039445&r2=1039446&view=diff ============================================================================== --- couchdb/branches/mochiweb-1.4.1/src/mochiweb/mochiweb_acceptor.erl (original) +++ couchdb/branches/mochiweb-1.4.1/src/mochiweb/mochiweb_acceptor.erl Fri Nov 26 16:28:33 2010 @@ -22,7 +22,7 @@ init(Server, Listen, Loop) -> {error, closed} -> exit(normal); {error, timeout} -> - exit(normal); + init(Server, Listen, Loop); {error, esslaccept} -> exit(normal); Other -> Modified: couchdb/branches/mochiweb-1.4.1/src/mochiweb/mochiweb_html.erl URL: http://svn.apache.org/viewvc/couchdb/branches/mochiweb-1.4.1/src/mochiweb/mochiweb_html.erl?rev=1039446&r1=1039445&r2=1039446&view=diff ============================================================================== --- couchdb/branches/mochiweb-1.4.1/src/mochiweb/mochiweb_html.erl (original) +++ couchdb/branches/mochiweb-1.4.1/src/mochiweb/mochiweb_html.erl Fri Nov 26 16:28:33 2010 @@ -131,6 +131,11 @@ to_html([], Acc) -> lists:reverse(Acc); to_html([{'=', Content} | Rest], Acc) -> to_html(Rest, [Content | Acc]); +to_html([{pi, Bin} | Rest], Acc) -> + Open = [<<">, + Bin, + <<"?>">>], + to_html(Rest, [Open | Acc]); to_html([{pi, Tag, Attrs} | Rest], Acc) -> Open = [<<">, Tag, @@ -216,6 +221,9 @@ to_tokens([{Tag0, [T0={'=', _C0} | R1]} to_tokens([{Tag0, [T0={comment, _C0} | R1]} | Rest], Acc) -> %% Allow {comment, iolist()} to_tokens([{Tag0, R1} | Rest], [T0 | Acc]); +to_tokens([{Tag0, [T0={pi, _S0} | R1]} | Rest], Acc) -> + %% Allow {pi, binary()} + to_tokens([{Tag0, R1} | Rest], [T0 | Acc]); to_tokens([{Tag0, [T0={pi, _S0, _A0} | R1]} | Rest], Acc) -> %% Allow {pi, binary(), list()} to_tokens([{Tag0, R1} | Rest], [T0 | Acc]); @@ -290,6 +298,9 @@ tokenize(B, S=#decoder{offset=O}) -> tokenize_doctype(B, ?ADV_COL(S, 10)); <<_:O/binary, "> -> tokenize_cdata(B, ?ADV_COL(S, 9)); + <<_:O/binary, "> -> + {Body, S1} = raw_qgt(B, ?ADV_COL(S, 2)), + {{pi, Body}, S1}; <<_:O/binary, "> -> {Tag, S1} = tokenize_literal(B, ?ADV_COL(S, 2)), {Attrs, S2} = tokenize_attributes(B, S1), @@ -309,7 +320,7 @@ tokenize(B, S=#decoder{offset=O}) -> {Tag, S1} = tokenize_literal(B, ?INC_COL(S)), {Attrs, S2} = tokenize_attributes(B, S1), {S3, HasSlash} = find_gt(B, S2), - Singleton = HasSlash orelse is_singleton(norm(binary_to_list(Tag))), + Singleton = HasSlash orelse is_singleton(Tag), {{start_tag, Tag, Attrs, Singleton}, S3}; _ -> tokenize_data(B, S) @@ -333,6 +344,8 @@ tree([{start_tag, Tag, Attrs, true} | Re tree(Rest, append_stack_child(norm({Tag, Attrs}), S)); tree([{start_tag, Tag, Attrs, false} | Rest], S) -> tree(Rest, stack(norm({Tag, Attrs}), S)); +tree([T={pi, _Raw} | Rest], S) -> + tree(Rest, append_stack_child(T, S)); tree([T={pi, _Tag, _Attrs} | Rest], S) -> tree(Rest, append_stack_child(T, S)); tree([T={comment, _Comment} | Rest], S) -> @@ -367,6 +380,10 @@ stack(T1, Stack) -> append_stack_child(StartTag, [{Name, Attrs, Acc} | Stack]) -> [{Name, Attrs, [StartTag | Acc]} | Stack]. +destack(<<"br">>, Stack) -> + %% This is an ugly hack to make dumb_br_test() pass, + %% this makes it such that br can never have children. + Stack; destack(TagName, Stack) when is_list(Stack) -> F = fun (X) -> case X of @@ -387,8 +404,8 @@ destack(TagName, Stack) when is_list(Sta {_, []} -> %% Actually was a singleton Stack; - {Pre, [{T1, A1, []} | Post1]} -> - [{T0, A0, [{T1, A1, lists:reverse(Pre)} | Post1]} + {Pre, [{T1, A1, Acc1} | Post1]} -> + [{T0, A0, [{T1, A1, Acc1 ++ lists:reverse(Pre)} | Post1]} | Post0] end; _ -> @@ -459,10 +476,51 @@ tokenize_attr_value(Attr, B, S) -> case B of <<_:O/binary, "=", _/binary>> -> S2 = skip_whitespace(B, ?INC_COL(S1)), - tokenize_word_or_literal(B, S2); + tokenize_quoted_or_unquoted_attr_value(B, S2); _ -> {Attr, S1} end. + +tokenize_quoted_or_unquoted_attr_value(B, S=#decoder{offset=O}) -> + case B of + <<_:O/binary>> -> + { [], S }; + <<_:O/binary, Q, _/binary>> when Q =:= ?QUOTE orelse + Q =:= ?SQUOTE -> + tokenize_quoted_attr_value(B, ?INC_COL(S), [], Q); + <<_:O/binary, _/binary>> -> + tokenize_unquoted_attr_value(B, S, []) + end. + +tokenize_quoted_attr_value(B, S=#decoder{offset=O}, Acc, Q) -> + case B of + <<_:O/binary>> -> + { iolist_to_binary(lists:reverse(Acc)), S }; + <<_:O/binary, $&, _/binary>> -> + {{data, Data, false}, S1} = tokenize_charref(B, ?INC_COL(S)), + tokenize_quoted_attr_value(B, S1, [Data|Acc], Q); + <<_:O/binary, Q, _/binary>> -> + { iolist_to_binary(lists:reverse(Acc)), ?INC_COL(S) }; + <<_:O/binary, $\n, _/binary>> -> + { iolist_to_binary(lists:reverse(Acc)), ?INC_LINE(S) }; + <<_:O/binary, C, _/binary>> -> + tokenize_quoted_attr_value(B, ?INC_COL(S), [C|Acc], Q) + end. + +tokenize_unquoted_attr_value(B, S=#decoder{offset=O}, Acc) -> + case B of + <<_:O/binary>> -> + { iolist_to_binary(lists:reverse(Acc)), S }; + <<_:O/binary, $&, _/binary>> -> + {{data, Data, false}, S1} = tokenize_charref(B, ?INC_COL(S)), + tokenize_unquoted_attr_value(B, S1, [Data|Acc]); + <<_:O/binary, $/, $>, _/binary>> -> + { iolist_to_binary(lists:reverse(Acc)), S }; + <<_:O/binary, C, _/binary>> when ?PROBABLE_CLOSE(C) -> + { iolist_to_binary(lists:reverse(Acc)), S }; + <<_:O/binary, C, _/binary>> -> + tokenize_unquoted_attr_value(B, ?INC_COL(S), [C|Acc]) + end. skip_whitespace(B, S=#decoder{offset=O}) -> case B of @@ -472,8 +530,17 @@ skip_whitespace(B, S=#decoder{offset=O}) S end. -tokenize_literal(Bin, S) -> - tokenize_literal(Bin, S, []). +tokenize_literal(Bin, S=#decoder{offset=O}) -> + case Bin of + <<_:O/binary, C, _/binary>> when C =:= $> + orelse C =:= $/ + orelse C =:= $= -> + %% Handle case where tokenize_literal would consume + %% 0 chars. http://github.com/mochi/mochiweb/pull/13 + {[C], ?INC_COL(S)}; + _ -> + tokenize_literal(Bin, S, []) + end. tokenize_literal(Bin, S=#decoder{offset=O}, Acc) -> case Bin of @@ -486,13 +553,33 @@ tokenize_literal(Bin, S=#decoder{offset= orelse C =:= $=) -> tokenize_literal(Bin, ?INC_COL(S), [C | Acc]); _ -> - {iolist_to_binary(lists:reverse(Acc)), S} + {iolist_to_binary(string:to_lower(lists:reverse(Acc))), S} + end. + +raw_qgt(Bin, S=#decoder{offset=O}) -> + raw_qgt(Bin, S, O). + +raw_qgt(Bin, S=#decoder{offset=O}, Start) -> + case Bin of + <<_:O/binary, "?>", _/binary>> -> + Len = O - Start, + <<_:Start/binary, Raw:Len/binary, _/binary>> = Bin, + {Raw, ?ADV_COL(S, 2)}; + <<_:O/binary, C, _/binary>> -> + raw_qgt(Bin, ?INC_CHAR(S, C), Start); + <<_:O/binary>> -> + <<_:Start/binary, Raw/binary>> = Bin, + {Raw, S} end. find_qgt(Bin, S=#decoder{offset=O}) -> case Bin of <<_:O/binary, "?>", _/binary>> -> ?ADV_COL(S, 2); + <<_:O/binary, ">", _/binary>> -> + ?ADV_COL(S, 1); + <<_:O/binary, "/>", _/binary>> -> + ?ADV_COL(S, 2); %% tokenize_attributes takes care of this state: %% <<_:O/binary, C, _/binary>> -> %% find_qgt(Bin, ?INC_CHAR(S, C)); @@ -570,7 +657,7 @@ tokenize_word_or_literal(Bin, S=#decoder tokenize_word(Bin, ?INC_COL(S), C); <<_:O/binary, C, _/binary>> when not ?IS_WHITESPACE(C) -> %% Sanity check for whitespace - tokenize_literal(Bin, S, []) + tokenize_literal(Bin, S) end. tokenize_word(Bin, S, Quote) -> @@ -880,6 +967,15 @@ parse_test() -> {<<"br">>, [], []}, <<"bar">>]}]}, parse(<<"foo
bar">>)), + %% Case insensitive tags + ?assertEqual( + {<<"html">>, [], + [{<<"head">>, [], [<<"foo">>, + {<<"br">>, [], []}, + <<"BAR">>]}, + {<<"body">>, [{<<"class">>, <<"">>}, {<<"bgcolor">>, <<"#Aa01fF">>}], []} + ]}, + parse(<<"foo
BAR">>)), ok. exhaustive_is_singleton_test() -> @@ -1056,6 +1152,113 @@ doctype_test() -> mochiweb_html:parse("" "" "")), + %% http://github.com/mochi/mochiweb/pull/13 + ?assertEqual( + {<<"html">>,[],[{<<"head">>,[],[]}]}, + mochiweb_html:parse("" + "" + "")), + ok. + +dumb_br_test() -> + %% http://code.google.com/p/mochiweb/issues/detail?id=71 + ?assertEqual( + {<<"div">>,[],[{<<"br">>, [], []}, {<<"br">>, [], []}, <<"z">>]}, + mochiweb_html:parse("


z

")), + ?assertEqual( + {<<"div">>,[],[{<<"br">>, [], []}, {<<"br">>, [], []}, <<"z">>]}, + mochiweb_html:parse("


z

")), + ?assertEqual( + {<<"div">>,[],[{<<"br">>, [], []}, {<<"br">>, [], []}, <<"z">>, {<<"br">>, [], []}, {<<"br">>, [], []}]}, + mochiweb_html:parse("


z

")), + ?assertEqual( + {<<"div">>,[],[{<<"br">>, [], []}, {<<"br">>, [], []}, <<"z">>]}, + mochiweb_html:parse("


z

")). + + +php_test() -> + %% http://code.google.com/p/mochiweb/issues/detail?id=71 + ?assertEqual( + [{pi, <<"php\n">>}], + mochiweb_html:tokens( + "")), + ?assertEqual( + {<<"div">>, [], [{pi, <<"php\n">>}]}, + mochiweb_html:parse( + "
")), + ok. + +parse_unquoted_attr_test() -> + D0 = <<"">>, + ?assertEqual( + {<<"html">>,[],[ + { <<"img">>, [ { <<"src">>, <<"/images/icon.png">> } ], [] } + ]}, + mochiweb_html:parse(D0)), + + D1 = <<"">>, + ?assertEqual( + {<<"html">>,[],[ + { <<"img">>, [ { <<"src">>, <<"/images/icon.png">> } ], [] } + ]}, + mochiweb_html:parse(D1)), + + D2 = <<"">>, + ?assertEqual( + {<<"html">>,[],[ + { <<"img">>, [ { <<"src">>, <<"/images/icon>.png">> }, { <<"width">>, <<"100">> } ], [] } + ]}, + mochiweb_html:parse(D2)), + ok. + +parse_quoted_attr_test() -> + D0 = <<"">>, + ?assertEqual( + {<<"html">>,[],[ + { <<"img">>, [ { <<"src">>, <<"/images/icon.png">> } ], [] } + ]}, + mochiweb_html:parse(D0)), + + D1 = <<"">>, + ?assertEqual( + {<<"html">>,[],[ + { <<"img">>, [ { <<"src">>, <<"/images/icon.png'>">> } ], [] } + ]}, + mochiweb_html:parse(D1)), + + D2 = <<"">>, + ?assertEqual( + {<<"html">>,[],[ + { <<"img">>, [ { <<"src">>, <<"/images/icon>.png">> } ], [] } + ]}, + mochiweb_html:parse(D2)), + ok. + +parse_missing_attr_name_test() -> + D0 = <<"">>, + ?assertEqual( + {<<"html">>, [ { <<"=">>, <<"=">> }, { <<"black">>, <<"black">> } ], [] }, + mochiweb_html:parse(D0)), ok. +parse_broken_pi_test() -> + D0 = <<"">>, + ?assertEqual( + {<<"html">>, [], [ + { pi, <<"xml:namespace">>, [ { <<"prefix">>, <<"o">> }, + { <<"ns">>, <<"urn:schemas-microsoft-com:office:office">> } ] } + ] }, + mochiweb_html:parse(D0)), + ok. + +parse_funny_singletons_test() -> + D0 = <<"x">>, + ?assertEqual( + {<<"html">>, [], [ + { <<"input">>, [], [] }, + { <<"input">>, [], [ <<"x">> ] } + ] }, + mochiweb_html:parse(D0)), + ok. + -endif. Modified: couchdb/branches/mochiweb-1.4.1/src/mochiweb/mochiweb_http.erl URL: http://svn.apache.org/viewvc/couchdb/branches/mochiweb-1.4.1/src/mochiweb/mochiweb_http.erl?rev=1039446&r1=1039445&r2=1039446&view=diff ============================================================================== --- couchdb/branches/mochiweb-1.4.1/src/mochiweb/mochiweb_http.erl (original) +++ couchdb/branches/mochiweb-1.4.1/src/mochiweb/mochiweb_http.erl Fri Nov 26 16:28:33 2010 @@ -35,6 +35,16 @@ start() -> start([{ip, "127.0.0.1"}, {loop, {?MODULE, default_body}}]). +%% @spec start(Options) -> ServerRet +%% Options = [option()] +%% Option = {name, atom()} | {ip, string() | tuple()} | {backlog, integer()} +%% | {nodelay, boolean()} | {acceptor_pool_size, integer()} +%% | {ssl, boolean()} | {profile_fun, undefined | (Props) -> ok} +%% @doc Start a mochiweb server. +%% profile_fun is used to profile accept timing. +%% After each accept, if defined, profile_fun is called with a proplist of a subset of the mochiweb_socket_server state and timing information. +%% The proplist is as follows: [{name, Name}, {port, Port}, {active_sockets, ActiveSockets}, {timing, Timing}]. +%% @end start(Options) -> mochiweb_socket_server:start(parse_options(Options)). @@ -90,22 +100,23 @@ loop(Socket, Body) -> request(Socket, Body). request(Socket, Body) -> - case mochiweb_socket:recv(Socket, 0, ?REQUEST_RECV_TIMEOUT) of - {ok, {http_request, Method, Path, Version}} -> + mochiweb_socket:setopts(Socket, [{active, once}]), + receive + {Protocol, _, {http_request, Method, Path, Version}} when Protocol == http orelse Protocol == ssl -> mochiweb_socket:setopts(Socket, [{packet, httph}]), headers(Socket, {Method, Path, Version}, [], Body, 0); - {error, {http_error, "\r\n"}} -> + {Protocol, _, {http_error, "\r\n"}} when Protocol == http orelse Protocol == ssl -> request(Socket, Body); - {error, {http_error, "\n"}} -> + {Protocol, _, {http_error, "\n"}} when Protocol == http orelse Protocol == ssl -> request(Socket, Body); - {error, closed} -> - mochiweb_socket:close(Socket), - exit(normal); - {error, timeout} -> + {tcp_closed, _} -> mochiweb_socket:close(Socket), exit(normal); _Other -> handle_invalid_request(Socket) + after ?REQUEST_RECV_TIMEOUT -> + mochiweb_socket:close(Socket), + exit(normal) end. reentry(Body) -> @@ -118,21 +129,23 @@ headers(Socket, Request, Headers, _Body, mochiweb_socket:setopts(Socket, [{packet, raw}]), handle_invalid_request(Socket, Request, Headers); headers(Socket, Request, Headers, Body, HeaderCount) -> - case mochiweb_socket:recv(Socket, 0, ?HEADERS_RECV_TIMEOUT) of - {ok, http_eoh} -> - mochiweb_socket:setopts(Socket, [{packet, raw}]), - Req = mochiweb:new_request({Socket, Request, - lists:reverse(Headers)}), + mochiweb_socket:setopts(Socket, [{active, once}]), + receive + {Protocol, _, http_eoh} when Protocol == http orelse Protocol == ssl -> + Req = new_request(Socket, Request, Headers), call_body(Body, Req), ?MODULE:after_response(Body, Req); - {ok, {http_header, _, Name, _, Value}} -> + {Protocol, _, {http_header, _, Name, _, Value}} when Protocol == http orelse Protocol == ssl -> headers(Socket, Request, [{Name, Value} | Headers], Body, 1 + HeaderCount); - {error, closed} -> + {tcp_closed, _} -> mochiweb_socket:close(Socket), exit(normal); _Other -> handle_invalid_request(Socket, Request, Headers) + after ?HEADERS_RECV_TIMEOUT -> + mochiweb_socket:close(Socket), + exit(normal) end. call_body({M, F}, Req) -> @@ -144,13 +157,15 @@ handle_invalid_request(Socket) -> handle_invalid_request(Socket, {'GET', {abs_path, "/"}, {0,9}}, []). handle_invalid_request(Socket, Request, RevHeaders) -> - mochiweb_socket:setopts(Socket, [{packet, raw}]), - Req = mochiweb:new_request({Socket, Request, - lists:reverse(RevHeaders)}), + Req = new_request(Socket, Request, RevHeaders), Req:respond({400, [], []}), mochiweb_socket:close(Socket), exit(normal). +new_request(Socket, Request, RevHeaders) -> + mochiweb_socket:setopts(Socket, [{packet, raw}]), + mochiweb:new_request({Socket, Request, lists:reverse(RevHeaders)}). + after_response(Body, Req) -> Socket = Req:get(socket), case Req:should_close() of @@ -162,6 +177,8 @@ after_response(Body, Req) -> ?MODULE:loop(Socket, Body) end. +parse_range_request("bytes=0-") -> + undefined; parse_range_request(RawRange) when is_list(RawRange) -> try "bytes=" ++ RangeString = RawRange, Modified: couchdb/branches/mochiweb-1.4.1/src/mochiweb/mochiweb_request.erl URL: http://svn.apache.org/viewvc/couchdb/branches/mochiweb-1.4.1/src/mochiweb/mochiweb_request.erl?rev=1039446&r1=1039445&r2=1039446&view=diff ============================================================================== --- couchdb/branches/mochiweb-1.4.1/src/mochiweb/mochiweb_request.erl (original) +++ couchdb/branches/mochiweb-1.4.1/src/mochiweb/mochiweb_request.erl Fri Nov 26 16:28:33 2010 @@ -40,8 +40,8 @@ %% @type response(). A mochiweb_response parameterized module instance. %% @type ioheaders() = headers() | [{key(), value()}]. -% 10 second default idle timeout --define(IDLE_TIMEOUT, 10000). +% 5 minute default idle timeout +-define(IDLE_TIMEOUT, 300000). % Maximum recv_body() length of 1MB -define(MAX_RECV_BODY, (1024*1024)). @@ -382,8 +382,8 @@ ok({ContentType, ResponseHeaders, Body}) %% @doc Return true if the connection must be closed. If false, using %% Keep-Alive should be safe. should_close() -> - ForceClose = erlang:get(mochiweb_request_force_close) =/= undefined, - DidNotRecv = erlang:get(mochiweb_request_recv) =:= undefined, + ForceClose = erlang:get(?SAVE_FORCE_CLOSE) =/= undefined, + DidNotRecv = erlang:get(?SAVE_RECV) =:= undefined, ForceClose orelse Version < {1, 0} %% Connection: close orelse get_header_value("connection") =:= "close" @@ -405,6 +405,7 @@ cleanup() -> ?SAVE_PATH, ?SAVE_RECV, ?SAVE_BODY, + ?SAVE_BODY_LENGTH, ?SAVE_POST, ?SAVE_COOKIE, ?SAVE_FORCE_CLOSE]], Added: couchdb/branches/mochiweb-1.4.1/src/mochiweb/mochiweb_request_tests.erl URL: http://svn.apache.org/viewvc/couchdb/branches/mochiweb-1.4.1/src/mochiweb/mochiweb_request_tests.erl?rev=1039446&view=auto ============================================================================== --- couchdb/branches/mochiweb-1.4.1/src/mochiweb/mochiweb_request_tests.erl (added) +++ couchdb/branches/mochiweb-1.4.1/src/mochiweb/mochiweb_request_tests.erl Fri Nov 26 16:28:33 2010 @@ -0,0 +1,63 @@ +-module(mochiweb_request_tests). + +-include_lib("eunit/include/eunit.hrl"). +-ifdef(TEST). + +accepts_content_type_test() -> + Req1 = mochiweb_request:new(nil, 'GET', "/foo", {1, 1}, + mochiweb_headers:make([{"Accept", "multipart/related"}])), + ?assertEqual(true, Req1:accepts_content_type("multipart/related")), + + Req2 = mochiweb_request:new(nil, 'GET', "/foo", {1, 1}, + mochiweb_headers:make([{"Accept", "text/html"}])), + ?assertEqual(false, Req2:accepts_content_type("multipart/related")), + + Req3 = mochiweb_request:new(nil, 'GET', "/foo", {1, 1}, + mochiweb_headers:make([{"Accept", "text/html, multipart/*"}])), + ?assertEqual(true, Req3:accepts_content_type("multipart/related")), + + Req4 = mochiweb_request:new(nil, 'GET', "/foo", {1, 1}, + mochiweb_headers:make([{"Accept", "text/html, multipart/*; q=0.0"}])), + ?assertEqual(false, Req4:accepts_content_type("multipart/related")), + + Req5 = mochiweb_request:new(nil, 'GET', "/foo", {1, 1}, + mochiweb_headers:make([{"Accept", "text/html, multipart/*; q=0"}])), + ?assertEqual(false, Req5:accepts_content_type("multipart/related")), + + Req6 = mochiweb_request:new(nil, 'GET', "/foo", {1, 1}, + mochiweb_headers:make([{"Accept", "text/html, */*; q=0.0"}])), + ?assertEqual(false, Req6:accepts_content_type("multipart/related")), + + Req7 = mochiweb_request:new(nil, 'GET', "/foo", {1, 1}, + mochiweb_headers:make([{"Accept", "multipart/*; q=0.0, */*"}])), + ?assertEqual(false, Req7:accepts_content_type("multipart/related")), + + Req8 = mochiweb_request:new(nil, 'GET', "/foo", {1, 1}, + mochiweb_headers:make([{"Accept", "*/*; q=0.0, multipart/*"}])), + ?assertEqual(true, Req8:accepts_content_type("multipart/related")), + + Req9 = mochiweb_request:new(nil, 'GET', "/foo", {1, 1}, + mochiweb_headers:make([{"Accept", "*/*; q=0.0, multipart/related"}])), + ?assertEqual(true, Req9:accepts_content_type("multipart/related")), + + Req10 = mochiweb_request:new(nil, 'GET', "/foo", {1, 1}, + mochiweb_headers:make([{"Accept", "text/html; level=1"}])), + ?assertEqual(true, Req10:accepts_content_type("text/html;level=1")), + + Req11 = mochiweb_request:new(nil, 'GET', "/foo", {1, 1}, + mochiweb_headers:make([{"Accept", "text/html; level=1, text/html"}])), + ?assertEqual(true, Req11:accepts_content_type("text/html")), + + Req12 = mochiweb_request:new(nil, 'GET', "/foo", {1, 1}, + mochiweb_headers:make([{"Accept", "text/html; level=1; q=0.0, text/html"}])), + ?assertEqual(false, Req12:accepts_content_type("text/html;level=1")), + + Req13 = mochiweb_request:new(nil, 'GET', "/foo", {1, 1}, + mochiweb_headers:make([{"Accept", "text/html; level=1; q=0.0, text/html"}])), + ?assertEqual(false, Req13:accepts_content_type("text/html; level=1")), + + Req14 = mochiweb_request:new(nil, 'GET', "/foo", {1, 1}, + mochiweb_headers:make([{"Accept", "text/html;level=1;q=0.1, text/html"}])), + ?assertEqual(true, Req14:accepts_content_type("text/html; level=1")). + +-endif. Modified: couchdb/branches/mochiweb-1.4.1/src/mochiweb/mochiweb_socket_server.erl URL: http://svn.apache.org/viewvc/couchdb/branches/mochiweb-1.4.1/src/mochiweb/mochiweb_socket_server.erl?rev=1039446&r1=1039445&r2=1039446&view=diff ============================================================================== --- couchdb/branches/mochiweb-1.4.1/src/mochiweb/mochiweb_socket_server.erl (original) +++ couchdb/branches/mochiweb-1.4.1/src/mochiweb/mochiweb_socket_server.erl Fri Nov 26 16:28:33 2010 @@ -12,7 +12,7 @@ -export([start/1, stop/1]). -export([init/1, handle_call/3, handle_cast/2, terminate/2, code_change/3, handle_info/2]). --export([get/2]). +-export([get/2, set/3]). -record(mochiweb_socket_server, {port, @@ -28,7 +28,10 @@ acceptor_pool_size=16, ssl=false, ssl_opts=[{ssl_imp, new}], - acceptor_pool=sets:new()}). + acceptor_pool=sets:new(), + profile_fun=undefined}). + +-define(is_old_state(State), not is_record(State, mochiweb_socket_server)). start(State=#mochiweb_socket_server{}) -> start_server(State); @@ -38,6 +41,12 @@ start(Options) -> get(Name, Property) -> gen_server:call(Name, {get, Property}). +set(Name, profile_fun, Fun) -> + gen_server:cast(Name, {set, profile_fun, Fun}); +set(Name, Property, _Value) -> + error_logger:info_msg("?MODULE:set for ~p with ~p not implemented~n", + [Name, Property]). + stop(Name) when is_atom(Name) -> gen_server:cast(Name, stop); stop(Pid) when is_pid(Pid) -> @@ -102,7 +111,10 @@ parse_options([{ssl, Ssl} | Rest], State parse_options(Rest, State#mochiweb_socket_server{ssl=Ssl}); parse_options([{ssl_opts, SslOpts} | Rest], State) when is_list(SslOpts) -> SslOpts1 = [{ssl_imp, new} | proplists:delete(ssl_imp, SslOpts)], - parse_options(Rest, State#mochiweb_socket_server{ssl_opts=SslOpts1}). + parse_options(Rest, State#mochiweb_socket_server{ssl_opts=SslOpts1}); +parse_options([{profile_fun, ProfileFun} | Rest], State) when is_function(ProfileFun) -> + parse_options(Rest, State#mochiweb_socket_server{profile_fun=ProfileFun}). + start_server(State=#mochiweb_socket_server{ssl=Ssl, name=Name}) -> case Ssl of @@ -123,7 +135,7 @@ start_server(State=#mochiweb_socket_serv ensure_int(N) when is_integer(N) -> N; ensure_int(S) when is_list(S) -> - integer_to_list(S). + list_to_integer(S). ipv6_supported() -> case (catch inet:getaddr("localhost", inet6)) of @@ -157,7 +169,7 @@ init(State=#mochiweb_socket_server{ip=Ip {stop, eacces} -> case Port < 1024 of true -> - case fdsrv:start() of + case catch fdsrv:start() of {ok, _} -> case fdsrv:bind_socket(tcp, Port) of {ok, Fd} -> @@ -203,6 +215,28 @@ do_get(port, #mochiweb_socket_server{por do_get(active_sockets, #mochiweb_socket_server{active_sockets=ActiveSockets}) -> ActiveSockets. + +state_to_proplist(#mochiweb_socket_server{name=Name, + port=Port, + active_sockets=ActiveSockets}) -> + [{name, Name}, {port, Port}, {active_sockets, ActiveSockets}]. + +upgrade_state(State = #mochiweb_socket_server{}) -> + State; +upgrade_state({mochiweb_socket_server, Port, Loop, Name, + Max, IP, Listen, NoDelay, Backlog, ActiveSockets, + AcceptorPoolSize, SSL, SSL_opts, + AcceptorPool}) -> + #mochiweb_socket_server{port=Port, loop=Loop, name=Name, max=Max, ip=IP, + listen=Listen, nodelay=NoDelay, backlog=Backlog, + active_sockets=ActiveSockets, + acceptor_pool_size=AcceptorPoolSize, + ssl=SSL, + ssl_opts=SSL_opts, + acceptor_pool=AcceptorPool}. + +handle_call(Req, From, State) when ?is_old_state(State) -> + handle_call(Req, From, upgrade_state(State)); handle_call({get, Property}, _From, State) -> Res = do_get(Property, State), {reply, Res, State}; @@ -210,13 +244,33 @@ handle_call(_Message, _From, State) -> Res = error, {reply, Res, State}. -handle_cast({accepted, Pid, _Timing}, + +handle_cast(Req, State) when ?is_old_state(State) -> + handle_cast(Req, upgrade_state(State)); +handle_cast({accepted, Pid, Timing}, State=#mochiweb_socket_server{active_sockets=ActiveSockets}) -> State1 = State#mochiweb_socket_server{active_sockets=1 + ActiveSockets}, + case State#mochiweb_socket_server.profile_fun of + undefined -> + undefined; + F when is_function(F) -> + catch F([{timing, Timing} | state_to_proplist(State1)]) + end, {noreply, recycle_acceptor(Pid, State1)}; +handle_cast({set, profile_fun, ProfileFun}, State) -> + State1 = case ProfileFun of + ProfileFun when is_function(ProfileFun); ProfileFun =:= undefined -> + State#mochiweb_socket_server{profile_fun=ProfileFun}; + _ -> + State + end, + {noreply, State1}; handle_cast(stop, State) -> {stop, normal, State}. + +terminate(Reason, State) when ?is_old_state(State) -> + terminate(Reason, upgrade_state(State)); terminate(_Reason, #mochiweb_socket_server{listen=Listen, port=Port}) -> mochiweb_socket:close(Listen), case Port < 1024 of @@ -244,6 +298,8 @@ recycle_acceptor(Pid, State=#mochiweb_so State#mochiweb_socket_server{active_sockets=ActiveSockets - 1} end. +handle_info(Msg, State) when ?is_old_state(State) -> + handle_info(Msg, upgrade_state(State)); handle_info({'EXIT', Pid, normal}, State) -> {noreply, recycle_acceptor(Pid, State)}; handle_info({'EXIT', Pid, Reason}, @@ -258,6 +314,20 @@ handle_info({'EXIT', Pid, Reason}, ok end, {noreply, recycle_acceptor(Pid, State)}; + +% this is what release_handler needs to get a list of modules, +% since our supervisor modules list is set to 'dynamic' +% see sasl-2.1.9.2/src/release_handler_1.erl get_dynamic_mods +handle_info({From, Tag, get_modules}, State = #mochiweb_socket_server{name={local,Mod}}) -> + From ! {element(2,Tag), [Mod]}, + {noreply, State}; + +% If for some reason we can't get the module name, send empty list to avoid release_handler timeout: +handle_info({From, Tag, get_modules}, State) -> + error_logger:info_msg("mochiweb_socket_server replying to dynamic modules request as '[]'~n",[]), + From ! {element(2,Tag), []}, + {noreply, State}; + handle_info(Info, State) -> error_logger:info_report([{'INFO', Info}, {'State', State}]), {noreply, State}. @@ -269,4 +339,26 @@ handle_info(Info, State) -> %% -include_lib("eunit/include/eunit.hrl"). -ifdef(TEST). + +upgrade_state_test() -> + OldState = {mochiweb_socket_server, + port, loop, name, + max, ip, listen, + nodelay, backlog, + active_sockets, + acceptor_pool_size, + ssl, ssl_opts, acceptor_pool}, + State = upgrade_state(OldState), + CmpState = #mochiweb_socket_server{port=port, loop=loop, + name=name, max=max, ip=ip, + listen=listen, nodelay=nodelay, + backlog=backlog, + active_sockets=active_sockets, + acceptor_pool_size=acceptor_pool_size, + ssl=ssl, ssl_opts=ssl_opts, + acceptor_pool=acceptor_pool, + profile_fun=undefined}, + ?assertEqual(CmpState, State). + -endif. + Modified: couchdb/branches/mochiweb-1.4.1/src/mochiweb/mochiweb_util.erl URL: http://svn.apache.org/viewvc/couchdb/branches/mochiweb-1.4.1/src/mochiweb/mochiweb_util.erl?rev=1039446&r1=1039445&r2=1039446&view=diff ============================================================================== --- couchdb/branches/mochiweb-1.4.1/src/mochiweb/mochiweb_util.erl (original) +++ couchdb/branches/mochiweb-1.4.1/src/mochiweb/mochiweb_util.erl Fri Nov 26 16:28:33 2010 @@ -620,7 +620,7 @@ cmd_port_test_spool(Port, Acc) -> cmd_port_test_spool(Port, ["\n", Data | Acc]); {Port, Unknown} -> throw({unknown, Unknown}) - after 100 -> + after 1000 -> throw(timeout) end.