From cce6d049aa8e438d9bb1615ce69f795ac998ac22 Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Mon, 7 Nov 2016 02:34:55 -0600 Subject: [PATCH 01/20] Try to configure Coveralls --- Makefile | 2 +- rebar.config | 9 ++++++++- rebar.config.script | 10 ++++++++-- 3 files changed, 17 insertions(+), 4 deletions(-) diff --git a/Makefile b/Makefile index b471713..4db2e67 100644 --- a/Makefile +++ b/Makefile @@ -2,4 +2,4 @@ compile: ; rebar3 do compile, xref eunit: ; rebar3 eunit init_dialyzer: ; rebar3 dialyzer -s false dialyzer: ; rebar3 dialyzer -u false -travis: ; rebar3 do xref, dialyzer, eunit +travis: ; rebar3 do xref, dialyzer, eunit && rebar3 coveralls send diff --git a/rebar.config b/rebar.config index 88e6072..e5096bc 100644 --- a/rebar.config +++ b/rebar.config @@ -1,6 +1,5 @@ {erl_first_files, ["src/elli_handler.erl"]}. {erl_opts, [debug_info, {i, "include"}]}. -{cover_enabled, true}. {deps, []}. {xref_checks, [undefined_function_calls,locals_not_used]}. {profiles, [ @@ -16,3 +15,11 @@ ]}. {shell, [{script_file, "bin/shell.escript"}]}. + +{plugins, [ + {coveralls, "1.3.0"} +]}. +{cover_enabled, true}. +{cover_export_enabled, true}. +{coveralls_coverdata, "_build/test/cover/eunit.coverdata"}. +{coveralls_service_name, "travis-ci"}. diff --git a/rebar.config.script b/rebar.config.script index 03e3581..288cbf5 100644 --- a/rebar.config.script +++ b/rebar.config.script @@ -7,6 +7,12 @@ case erl_internal:bif(is_map, 1) of Config1 = lists:keystore(project_plugins, 1, CONFIG, {project_plugins, [Lint]}), Config2 = lists:keystore(provider_hooks, 1, Config1, - {provider_hooks, [{pre, [{eunit, lint}]}]}) - + {provider_hooks, [{pre, [{eunit, lint}]}]}), + case os:getenv("TRAVIS") of + "true" -> + JobId = os:getenv("TRAVIS_JOB_ID"), + Config3 = lists:keystore(coveralls_service_job_id, 1, Config2, + {coveralls_service_job_id, JobId}); + _ -> Config2 + end end. From ec47047358a0ac3cb34cedd8eb3a20fc2c697040 Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Mon, 7 Nov 2016 02:44:23 -0600 Subject: [PATCH 02/20] Update badges Add Coveralls and rearrange. [ci skip] --- README.md | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 2a8fec5..baab683 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,10 @@ # elli - Erlang web server for HTTP APIs -[![Travis CI][travis badge]][travis builds] [![Hex.pm][hex badge]][hex package] -[![Erlang][erlang badge]][erlang downloads] [![Documentation][doc badge]][docs] +[![Erlang][erlang badge]][erlang downloads] +[![Travis CI][travis badge]][travis builds] +[![Coverage Status][coveralls badge]][coveralls link] [![MIT License][license badge]](LICENSE) [travis builds]: https://travis-ci.org/elli-lib/elli @@ -15,6 +16,8 @@ [erlang downloads]: http://www.erlang.org/downloads [doc badge]: https://img.shields.io/badge/docs-edown-green.svg [docs]: doc/README.md +[coveralls badge]: https://coveralls.io/repos/github/elli-lib/elli/badge.svg?branch=develop +[coveralls link]: https://coveralls.io/github/elli-lib/elli?branch=develop [license badge]: https://img.shields.io/badge/license-MIT-blue.svg Elli is a webserver you can run inside your Erlang application to From e339df8863c7f081a4fdff2710bc6b48019d7a73 Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Mon, 7 Nov 2016 17:23:47 -0600 Subject: [PATCH 03/20] Prefer elli_test.hrl over ad hoc redundancy --- test/elli_tests.erl | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/test/elli_tests.erl b/test/elli_tests.erl index b2c61af..9e98c4c 100644 --- a/test/elli_tests.erl +++ b/test/elli_tests.erl @@ -1,6 +1,7 @@ -module(elli_tests). -include_lib("eunit/include/eunit.hrl"). -include("elli.hrl"). +-include("elli_test.hrl"). -define(I2B(I), list_to_binary(integer_to_list(I))). -define(I2L(I), integer_to_list(I)). @@ -598,14 +599,3 @@ invalid_callback_test() -> E -> ?assertMatch(invalid_callback, E) end. - - -%%% Helpers - -status({{_, Status, _}, _, _}) -> - Status. -body({_, _, Body}) -> - Body. - -headers({_, Headers, _}) -> - lists:sort(Headers). From 660d7c8e07871acc9468c18269f58a030105a1d8 Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Mon, 7 Nov 2016 17:48:09 -0600 Subject: [PATCH 04/20] Fix/augment compression tests - Add elli_middleware_compress to mods - Simplify request in no_compress/0 - Add compress/2 helper and gzip/0 and deflate/0 tests --- test/elli_tests.erl | 25 ++++++++++++++++++++++--- 1 file changed, 22 insertions(+), 3 deletions(-) diff --git a/test/elli_tests.erl b/test/elli_tests.erl index 9e98c4c..36d8491 100644 --- a/test/elli_tests.erl +++ b/test/elli_tests.erl @@ -27,6 +27,8 @@ elli_test_() -> ?_test(crash()), ?_test(invalid_return()), ?_test(no_compress()), + ?_test(gzip()), + ?_test(deflate()), ?_test(exception_flow()), ?_test(accept_content_type()), ?_test(user_connection()), @@ -71,6 +73,7 @@ setup() -> Config = [ {mods, [ {elli_metrics_middleware, []}, + {elli_middleware_compress, []}, {elli_example_callback, []} ]} ], @@ -146,15 +149,31 @@ invalid_return() -> ?assertMatch("Internal server error", body(Response)). no_compress() -> - {ok, Response} = httpc:request(get, {"http://localhost:3001/compressed", - [{"Accept-Encoding", "gzip"}]}, - [], []), + {ok, Response} = httpc:request("http://localhost:3001/compressed"), ?assertMatch(200, status(Response)), ?assertMatch([{"connection", "Keep-Alive"}, {"content-length", "1032"}], headers(Response)), ?assertEqual(binary:copy(<<"Hello World!">>, 86), list_to_binary(body(Response))). +compress(Encoding, Length) -> + {ok, Response} = httpc:request(get, {"http://localhost:3001/compressed", + [{"Accept-Encoding", Encoding}]}, + [], []), + ?assertMatch(200, status(Response)), + ?assertMatch([{"connection", "Keep-Alive"}, + {"content-encoding", Encoding}, + {"content-length", Length}], headers(Response)), + ?assertEqual(binary:copy(<<"Hello World!">>, 86), + uncompress(Encoding, body(Response))). + +uncompress("gzip", Data) -> zlib:gunzip(Data); +uncompress("deflate", Data) -> zlib:uncompress(Data). + +gzip() -> compress("gzip", "41"). + +deflate() -> compress("deflate", "29"). + exception_flow() -> {ok, Response} = httpc:request("http://localhost:3001/403"), ?assertMatch(403, status(Response)), From c0eded61a8d42866d21be8362fb116df2d49b19a Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Mon, 7 Nov 2016 17:50:52 -0600 Subject: [PATCH 05/20] Add hello_iolist/0 and headers/0 tests --- test/elli_tests.erl | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/test/elli_tests.erl b/test/elli_tests.erl index 36d8491..d540921 100644 --- a/test/elli_tests.erl +++ b/test/elli_tests.erl @@ -30,6 +30,7 @@ elli_test_() -> ?_test(gzip()), ?_test(deflate()), ?_test(exception_flow()), + ?_test(hello_iolist()), ?_test(accept_content_type()), ?_test(user_connection()), ?_test(get_args()), @@ -43,6 +44,7 @@ elli_test_() -> ?_test(bad_request_line()), ?_test(content_length()), ?_test(user_content_length()), + ?_test(headers()), ?_test(chunked()), ?_test(sendfile()), ?_test(send_no_file()), @@ -181,6 +183,11 @@ exception_flow() -> {"content-length", "9"}], headers(Response)), ?assertMatch("Forbidden", body(Response)). +hello_iolist() -> + Url = "http://localhost:3001/hello/iolist?name=knut", + {ok, Response} = httpc:request(Url), + ?assertMatch("Hello knut", body(Response)). + accept_content_type() -> {ok, Json} = httpc:request(get, {"http://localhost:3001/type?name=knut", [{"Accept", "application/json"}]}, [], []), @@ -282,6 +289,12 @@ user_content_length() -> "foobar">>}, gen_tcp:recv(Client, 0)). +headers() -> + {ok, Response} = httpc:request("http://localhost:3001/headers.html"), + Headers = headers(Response), + + ?assert(proplists:is_defined("x-custom", Headers)), + ?assertMatch("foobar", proplists:get_value("x-custom", Headers)). chunked() -> Expected = "chunk10chunk9chunk8chunk7chunk6chunk5chunk4chunk3chunk2chunk1", From 1dd7d0d7389da18e8a20128763943b7bc8a94b3a Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Mon, 7 Nov 2016 17:51:17 -0600 Subject: [PATCH 06/20] Exclude elli_handler from cover data --- rebar.config | 3 +++ 1 file changed, 3 insertions(+) diff --git a/rebar.config b/rebar.config index e5096bc..3f754f7 100644 --- a/rebar.config +++ b/rebar.config @@ -21,5 +21,8 @@ ]}. {cover_enabled, true}. {cover_export_enabled, true}. +{cover_excl_mods, [ + elli_handler +]}. {coveralls_coverdata, "_build/test/cover/eunit.coverdata"}. {coveralls_service_name, "travis-ci"}. From 216e053af00b39076cc49b735cf1b46b014d3b84 Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Mon, 7 Nov 2016 17:55:39 -0600 Subject: [PATCH 07/20] Add (req) accessors, found and encode_range tests --- test/elli_tests.erl | 51 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) diff --git a/test/elli_tests.erl b/test/elli_tests.erl index d540921..3a039dd 100644 --- a/test/elli_tests.erl +++ b/test/elli_tests.erl @@ -38,6 +38,7 @@ elli_test_() -> ?_test(decoded_get_args_list()), ?_test(post_args()), ?_test(shorthand()), + ?_test(found()), ?_test(too_many_headers()), ?_test(too_big_body()), ?_test(way_too_big_body()), @@ -93,6 +94,43 @@ init_stats() -> clear_stats(_) -> ets:delete(elli_stat_table). + + +accessors_test_() -> + RawPath = <<"/foo/bar">>, + Headers = [{<<"Content-Type">>, <<"application/x-www-form-urlencoded">>}], + Method = 'POST', + Body = <<"name=knut%3D">>, + Name = <<"knut=">>, + Req1 = #req{raw_path = RawPath, + headers = Headers, + method = Method, + body = Body}, + Args = [{<<"name">>, Name}], + Req2 = #req{headers = Headers, args = Args, body = <<>>}, + + [ + %% POST /foo/bar + ?_assertMatch(RawPath, elli_request:raw_path(Req1)), + ?_assertMatch(Headers, elli_request:headers(Req1)), + ?_assertMatch(Method, elli_request:method(Req1)), + ?_assertMatch(Body, elli_request:body(Req1)), + ?_assertMatch(Args, elli_request:post_args_decoded(Req1)), + ?_assertMatch(undefined, elli_request:post_arg(<<"foo">>, Req1)), + ?_assertMatch(undefined, elli_request:post_arg_decoded(<<"foo">>, Req1)), + ?_assertMatch(Name, elli_request:post_arg_decoded(<<"name">>, Req1)), + %% GET /foo/bar + ?_assertMatch(Headers, elli_request:headers(Req2)), + + ?_assertMatch(Args, elli_request:get_args(Req2)), + ?_assertMatch(undefined, elli_request:get_arg_decoded(<<"foo">>, Req2)), + ?_assertMatch(Name, elli_request:get_arg_decoded(<<"name">>, Req2)), + ?_assertMatch([], elli_request:post_args(Req2)), + + ?_assertMatch({error, not_supported}, elli_request:chunk_ref(#req{})) + ]. + + %%% Integration tests %%% Use inets httpc to actually call Elli over the network. @@ -237,6 +275,15 @@ shorthand() -> {"content-length", "5"}], headers(Response)), ?assertMatch("hello", body(Response)). +found() -> + {ok, Response} = httpc:request(get, {"http://localhost:3001/302", []}, + [{autoredirect, false}], []), + ?assertMatch(302, status(Response)), + ?assertMatch([{"connection","Keep-Alive"}, + {"content-length","0"}, + {"location", "/hello/world"}], headers(Response)), + ?assertMatch("", body(Response)). + too_many_headers() -> Headers = lists:duplicate(100, {"X-Foo", "Bar"}), {ok, Response} = httpc:request(get, {"http://localhost:3001/foo", Headers}, @@ -612,6 +659,10 @@ normalize_range_test_() -> ?_assertMatch(invalid_range, elli_util:normalize_range(Invalid6, Size))]. +encode_range_test() -> + Expected = [<<"bytes ">>,<<"*">>,<<"/">>,"42"], + ?assertMatch(Expected, elli_util:encode_range(invalid_range, 42)). + register_test() -> ?assertMatch(undefined, whereis(elli)), Config = [ From a598c8b531151f4d5c1c9ab7baa2d2f51f1241a6 Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Mon, 7 Nov 2016 17:56:39 -0600 Subject: [PATCH 08/20] Update elli_metrics_middleware Add dummy init/2 to increase the coverage percentage... --- test/elli_metrics_middleware.erl | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/test/elli_metrics_middleware.erl b/test/elli_metrics_middleware.erl index dd124d9..a2a2446 100644 --- a/test/elli_metrics_middleware.erl +++ b/test/elli_metrics_middleware.erl @@ -1,5 +1,5 @@ -module(elli_metrics_middleware). --export([handle/2, handle_event/3]). +-export([init/2, handle/2, handle_event/3]). -behaviour(elli_handler). @@ -7,6 +7,9 @@ %% ELLI %% +init(_Req, _Args) -> + ignore. + handle(_Req, _Args) -> ignore. From ab661e86ffb9d1e8a9484886f66faaf816d3febb Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Mon, 7 Nov 2016 17:58:16 -0600 Subject: [PATCH 09/20] Update elli_{example_callback,tests} Add GET /ip route that returns the request's peer, using a binary status and calling elli_request:peer/1 for even more coverage. --- src/elli_example_callback.erl | 3 +++ test/elli_tests.erl | 6 ++++++ 2 files changed, 9 insertions(+) diff --git a/src/elli_example_callback.erl b/src/elli_example_callback.erl index f0333e6..6677064 100644 --- a/src/elli_example_callback.erl +++ b/src/elli_example_callback.erl @@ -181,6 +181,9 @@ handle('GET', [<<"chunked">>], Req) -> handle('GET', [<<"shorthand">>], _Req) -> {200, <<"hello">>}; +handle('GET', [<<"ip">>], Req) -> + {<<"200 OK">>, elli_request:peer(Req)}; + handle('GET', [<<"304">>], _Req) -> %% A "Not Modified" response is exactly like a normal response (so %% Content-Length is included), but the body will not be sent. diff --git a/test/elli_tests.erl b/test/elli_tests.erl index 3a039dd..fb0e47d 100644 --- a/test/elli_tests.erl +++ b/test/elli_tests.erl @@ -38,6 +38,7 @@ elli_test_() -> ?_test(decoded_get_args_list()), ?_test(post_args()), ?_test(shorthand()), + ?_test(ip()), ?_test(found()), ?_test(too_many_headers()), ?_test(too_big_body()), @@ -275,6 +276,11 @@ shorthand() -> {"content-length", "5"}], headers(Response)), ?assertMatch("hello", body(Response)). +ip() -> + {ok, Response} = httpc:request("http://localhost:3001/ip"), + ?assertMatch(200, status(Response)), + ?assertMatch("127.0.0.1", body(Response)). + found() -> {ok, Response} = httpc:request(get, {"http://localhost:3001/302", []}, [{autoredirect, false}], []), From d87974b1add6ba8f28c0fed7c46b7cb1eaefc86f Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Tue, 8 Nov 2016 20:05:07 -0600 Subject: [PATCH 10/20] Add elli_ssl_tests:chunked/0 --- test/elli_ssl_tests.erl | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/test/elli_ssl_tests.erl b/test/elli_ssl_tests.erl index b6fde74..bd002c3 100644 --- a/test/elli_ssl_tests.erl +++ b/test/elli_ssl_tests.erl @@ -6,7 +6,8 @@ elli_ssl_test_() -> {setup, fun setup/0, fun teardown/1, [ - ?_test(hello_world()) + ?_test(hello_world()), + ?_test(chunked()) ]}. %%% Tests @@ -15,6 +16,19 @@ hello_world() -> {ok, Response} = httpc:request("https://localhost:3443/hello/world"), ?assertMatch(200, status(Response)). +chunked() -> + Expected = "chunk10chunk9chunk8chunk7chunk6chunk5chunk4chunk3chunk2chunk1", + + {ok, Response} = httpc:request("https://localhost:3443/chunked"), + + ?assertMatch(200, status(Response)), + ?assertEqual([{"connection", "Keep-Alive"}, + %% httpc adds a content-length, even though elli + %% does not send any for chunked transfers + {"content-length", integer_to_list(length(Expected))}, + {"content-type", "text/event-stream"}], headers(Response)), + ?assertMatch(Expected, body(Response)). + %%% Internal helpers setup() -> From 9304cf20d3c2bab611857ac425916f86728afad6 Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Tue, 8 Nov 2016 20:06:09 -0600 Subject: [PATCH 11/20] Update test/elli_metrics_middleware.erl Add no-op {pre,post}process callbacks for more coverage. --- test/elli_metrics_middleware.erl | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/test/elli_metrics_middleware.erl b/test/elli_metrics_middleware.erl index a2a2446..3861c68 100644 --- a/test/elli_metrics_middleware.erl +++ b/test/elli_metrics_middleware.erl @@ -1,5 +1,5 @@ -module(elli_metrics_middleware). --export([init/2, handle/2, handle_event/3]). +-export([init/2, preprocess/2, handle/2, postprocess/3, handle_event/3]). -behaviour(elli_handler). @@ -10,9 +10,15 @@ init(_Req, _Args) -> ignore. +preprocess(Req, _Args) -> + Req. + handle(_Req, _Args) -> ignore. +postprocess(_Req, Res, _Args) -> + Res. + %% %% ELLI EVENT CALLBACKS From 8c679383edec838c94fad47448d8ade1a6fbb18f Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Wed, 9 Nov 2016 16:35:15 -0600 Subject: [PATCH 12/20] Normalize elli.app.src --- src/elli.app.src | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/elli.app.src b/src/elli.app.src index 7b54789..17d3c45 100644 --- a/src/elli.app.src +++ b/src/elli.app.src @@ -24,7 +24,7 @@ ]}, {env, []}, - {maintainers,["Eric Bailey", "Knut Nesheim", "Tristan Sloughter"]}, - {licenses,["MIT"]}, - {links,[{"Github","https://github.com/elli-lib/elli"}]} + {maintainers, ["Eric Bailey", "Knut Nesheim", "Tristan Sloughter"]}, + {licenses, ["MIT"]}, + {links, [{"GitHub", "https://github.com/elli-lib/elli"}]} ]}. From 944e8a3b713dd7c60f9685392e5cd758e918e0d2 Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Wed, 9 Nov 2016 16:47:32 -0600 Subject: [PATCH 13/20] Update elli_util:file_size/1 Tighten the type spec and ensure the file can actually be read. Changes inspired by elli_fileserve:file_size/1. --- src/elli_util.erl | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/src/elli_util.erl b/src/elli_util.erl index e5391c1..60ff3e2 100644 --- a/src/elli_util.erl +++ b/src/elli_util.erl @@ -54,12 +54,16 @@ encode_range_bytes({Offset, Length}) -> encode_range_bytes(invalid_range) -> <<"*">>. --spec file_size(Filename::file:name()) -> - non_neg_integer() | {error, Reason} - when Reason :: badarg | file:posix(). +-spec file_size(Filename) -> Size | {error, Reason} when + Filename :: file:name_all(), + Size :: non_neg_integer(), + Reason :: file:posix() | badarg | invalid_file. %% @doc: Get the size in bytes of the file. file_size(Filename) -> case file:read_file_info(Filename) of - {ok, #file_info{size = Size}} -> Size; - {error, Reason} -> {error, Reason} + {ok, #file_info{type = regular, access = Perm, size = Size}} + when Perm =:= read orelse Perm =:= read_write -> + Size; + {error, Reason} -> {error, Reason}; + _ -> {error, invalid_file} end. From e5d012a0fa8e837d5b379584e49b97de0b4b9d69 Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Wed, 9 Nov 2016 16:48:55 -0600 Subject: [PATCH 14/20] Refine elli_handler:result() and prefer ?assertMatch --- src/elli_handler.erl | 8 +++++++- src/elli_http.erl | 2 +- src/elli_test.erl | 8 ++++---- 3 files changed, 12 insertions(+), 6 deletions(-) diff --git a/src/elli_handler.erl b/src/elli_handler.erl index 6d2de3c..9342459 100644 --- a/src/elli_handler.erl +++ b/src/elli_handler.erl @@ -22,8 +22,14 @@ | client_closed | client_timeout | invalid_return. --type result() :: {elli:response_code() | ok, elli:body()} +-type result() :: {elli:response_code() | ok, + elli:headers(), + {file, file:name_all()} + | {file, file:name_all(), elli_util:range()}} | {elli:response_code() | ok, elli:headers(), elli:body()} + | {elli:response_code() | ok, elli:body()} + | {chunk, elli:headers()} + | {chunk, elli:headers(), elli:body()} | ignore. -callback handle(Req :: elli:req(), callback_args()) -> result(). diff --git a/src/elli_http.erl b/src/elli_http.erl index 8b5caca..20b12a6 100644 --- a/src/elli_http.erl +++ b/src/elli_http.erl @@ -849,6 +849,6 @@ get_body_test() -> Buffer = binary:copy(<<".">>, 42), Opts = [], Callback = {no, op}, - ?assertEqual({Buffer, <<>>}, + ?assertMatch({Buffer, <<>>}, get_body(Socket, Headers, Buffer, Opts, Callback)). -endif. diff --git a/src/elli_test.erl b/src/elli_test.erl index 7b7298a..8ef375a 100644 --- a/src/elli_test.erl +++ b/src/elli_test.erl @@ -12,7 +12,7 @@ -export([call/5]). --spec call(Method, Path, Headers, Body, Opts) -> elli:req() when +-spec call(Method, Path, Headers, Body, Opts) -> elli_handler:result() when Method :: elli:http_method(), Path :: binary(), Headers :: elli:headers(), @@ -30,13 +30,13 @@ call(Method, Path, Headers, Body, Opts) -> -include_lib("eunit/include/eunit.hrl"). hello_world_test() -> - ?assertEqual({ok, [], <<"Hello World!">>}, + ?assertMatch({ok, [], <<"Hello World!">>}, elli_test:call('GET', <<"/hello/world/">>, [], <<>>, ?EXAMPLE_CONF)), - ?assertEqual({ok, [], <<"Hello Test1">>}, + ?assertMatch({ok, [], <<"Hello Test1">>}, elli_test:call('GET', <<"/hello/?name=Test1">>, [], <<>>, ?EXAMPLE_CONF)), - ?assertEqual({ok, + ?assertMatch({ok, [{<<"Content-type">>, <<"application/json; charset=ISO-8859-1">>}], <<"{\"name\" : \"Test2\"}">>}, From 2d470a213ad7101b559e24c357974fe8e08195cc Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Wed, 9 Nov 2016 18:54:06 -0600 Subject: [PATCH 15/20] Simplify rebar3 config wrt rebar3_lint Since we don't support OTP <18 at the moment, there's no need to overcomplicate this. Also upgrade rebar3_lint from git v0.1.6 to Hex v0.1.7. --- rebar.config | 8 ++++++-- rebar.config.script | 23 ++++++----------------- 2 files changed, 12 insertions(+), 19 deletions(-) diff --git a/rebar.config b/rebar.config index 3f754f7..571f742 100644 --- a/rebar.config +++ b/rebar.config @@ -16,9 +16,13 @@ {shell, [{script_file, "bin/shell.escript"}]}. -{plugins, [ - {coveralls, "1.3.0"} +{project_plugins, [ + {coveralls, "1.3.0"}, + {rebar3_lint, "0.1.7"} ]}. + +{provider_hooks, [{pre, [{eunit, lint}]}]}. + {cover_enabled, true}. {cover_export_enabled, true}. {cover_excl_mods, [ diff --git a/rebar.config.script b/rebar.config.script index 288cbf5..1411e37 100644 --- a/rebar.config.script +++ b/rebar.config.script @@ -1,18 +1,7 @@ -case erl_internal:bif(is_map, 1) of - false -> CONFIG; - true -> - Lint = {rebar3_lint, - {git, "git://github.com/project-fifo/rebar3_lint.git", - {tag, "0.1.6"}}}, - Config1 = lists:keystore(project_plugins, 1, CONFIG, - {project_plugins, [Lint]}), - Config2 = lists:keystore(provider_hooks, 1, Config1, - {provider_hooks, [{pre, [{eunit, lint}]}]}), - case os:getenv("TRAVIS") of - "true" -> - JobId = os:getenv("TRAVIS_JOB_ID"), - Config3 = lists:keystore(coveralls_service_job_id, 1, Config2, - {coveralls_service_job_id, JobId}); - _ -> Config2 - end +case os:getenv("TRAVIS") of + "true" -> + JobId = os:getenv("TRAVIS_JOB_ID"), + lists:keystore(coveralls_service_job_id, 1, CONFIG, + {coveralls_service_job_id, JobId}); + _ -> CONFIG end. From be7edc0c1750d7811f5b7b3095f08b028b9304d5 Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Thu, 10 Nov 2016 13:29:28 -0600 Subject: [PATCH 16/20] Rename do_check_max_size_2x to do_check_max_size_x2 Appease elvis. --- src/elli_http.erl | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/elli_http.erl b/src/elli_http.erl index 20b12a6..0b15a85 100644 --- a/src/elli_http.erl +++ b/src/elli_http.erl @@ -561,19 +561,19 @@ check_max_size(Socket, ContentLength, Buffer, Opts, {Mod, Args}) -> do_check_max_size(Socket, ContentLength, Buffer, MaxSize, {Mod, Args}) when ContentLength > MaxSize -> handle_event(Mod, bad_request, [{body_size, ContentLength}], Args), - do_check_max_size_2x(Socket, ContentLength, Buffer, MaxSize), + do_check_max_size_x2(Socket, ContentLength, Buffer, MaxSize), elli_tcp:close(Socket), exit(normal); do_check_max_size(_, _, _, _, _) -> ok. -do_check_max_size_2x(Socket, ContentLength, Buffer, MaxSize) +do_check_max_size_x2(Socket, ContentLength, Buffer, MaxSize) when ContentLength < MaxSize * 2 -> OnSocket = ContentLength - size(Buffer), elli_tcp:recv(Socket, OnSocket, 60000), Response = [<<"HTTP/1.1 ">>, status(413), <<"\r\n">>, <<"Content-Length: 0">>, <<"\r\n\r\n">>], elli_tcp:send(Socket, Response); -do_check_max_size_2x(_, _, _, _) -> ok. +do_check_max_size_x2(_, _, _, _) -> ok. -spec mk_req(Method, PathTuple, Headers, Body, V, Socket, Callback) -> Req when Method :: elli:http_method(), From 7b1336e6259227af259c245ae1675d602ca530bf Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Thu, 10 Nov 2016 13:39:29 -0600 Subject: [PATCH 17/20] DRY up HTTP response generation This is mostly to appease elvis. --- src/elli_http.erl | 26 ++++++++++++++++---------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/src/elli_http.erl b/src/elli_http.erl index 0b15a85..570730a 100644 --- a/src/elli_http.erl +++ b/src/elli_http.erl @@ -280,10 +280,7 @@ send_server_error(Socket) -> send_rescue_response(Socket, 500, <<"Server Error">>). send_rescue_response(Socket, Code, Body) -> - Response = [<<"HTTP/1.1 ">>, status(Code), <<"\r\n">>, - <<"Content-Length: ">>, integer_to_list(size(Body)), <<"\r\n">>, - <<"\r\n">>, - Body], + Response = http_response(Code, Body), elli_tcp:send(Socket, Response). %% @doc Execute the user callback, translating failure into a proper response. @@ -544,8 +541,7 @@ maybe_send_continue(Socket, Headers) -> % headers contains "Expect:100-continue" case proplists:get_value(<<"Expect">>, Headers, undefined) of <<"100-continue">> -> - Response = [<<"HTTP/1.1 ">>, status(100), <<"\r\n">>, - <<"Content-Length: 0">>, <<"\r\n\r\n">>], + Response = http_response(100), elli_tcp:send(Socket, Response); _Other -> ok @@ -570,8 +566,7 @@ do_check_max_size_x2(Socket, ContentLength, Buffer, MaxSize) when ContentLength < MaxSize * 2 -> OnSocket = ContentLength - size(Buffer), elli_tcp:recv(Socket, OnSocket, 60000), - Response = [<<"HTTP/1.1 ">>, status(413), <<"\r\n">>, - <<"Content-Length: 0">>, <<"\r\n\r\n">>], + Response = http_response(413), elli_tcp:send(Socket, Response); do_check_max_size_x2(_, _, _, _) -> ok. @@ -603,9 +598,20 @@ mk_req(Method, RawPath, Headers, Body, V, Socket, {Mod, Args} = Callback) -> %% HEADERS %% +http_response(Code) -> + http_response(Code, <<>>). + +http_response(Code, Body) -> + http_response(Code, [{<<"Content-Length">>, size(Body)}], Body). + +http_response(Code, Headers, <<>>) -> + [<<"HTTP/1.1 ">>, status(Code), <<"\r\n">>, + encode_headers(Headers), <<"\r\n">>]; +http_response(Code, Headers, Body) -> + [http_response(Code, Headers, <<>>), Body]. + assemble_response_headers(Code, Headers) -> - ResponseHeaders = [<<"HTTP/1.1 ">>, status(Code), <<"\r\n">>, - encode_headers(Headers), <<"\r\n">>], + ResponseHeaders = http_response(Code, Headers, <<>>), s(resp_headers, iolist_size(ResponseHeaders)), ResponseHeaders. From b116cefc9bcb1051826ec8e6d77e056e2217d244 Mon Sep 17 00:00:00 2001 From: Ilya Khaprov Date: Sun, 27 Nov 2016 11:38:11 +0300 Subject: [PATCH 18/20] Fix keep-alive counter & request_start metric. see #19 --- src/elli_http.erl | 42 +++++++++++++++++++++++++----------------- 1 file changed, 25 insertions(+), 17 deletions(-) diff --git a/src/elli_http.erl b/src/elli_http.erl index 570730a..a58c1f1 100644 --- a/src/elli_http.erl +++ b/src/elli_http.erl @@ -70,7 +70,7 @@ keepalive_loop(Socket, Options, Callback) -> keepalive_loop(Socket, NumRequests, Buffer, Options, Callback) -> case ?MODULE:handle_request(Socket, Buffer, Options, Callback) of {keep_alive, NewBuffer} -> - ?MODULE:keepalive_loop(Socket, NumRequests, + ?MODULE:keepalive_loop(Socket, NumRequests + 1, NewBuffer, Options, Callback); {close, _} -> elli_tcp:close(Socket), @@ -87,7 +87,6 @@ keepalive_loop(Socket, NumRequests, Buffer, Options, Callback) -> Callback :: elli_handler:callback(), ConnToken :: {'keep_alive' | 'close', binary()}. handle_request(S, PrevB, Opts, {Mod, Args} = Callback) -> - t(request_start), {Method, RawPath, V, B0} = get_request(S, PrevB, Opts, Callback), t(headers_start), {RequestHeaders, B1} = get_headers(S, V, B0, Opts, Callback), @@ -404,23 +403,17 @@ send_chunk(Socket, Data) -> %% %% @doc Retrieve the request line. -get_request(Socket, Buffer, Options, {Mod, Args} = Callback) -> +get_request(Socket, <<>>, Options, Callback) -> + NewBuffer = recv_request(Socket, <<>>, Options, Callback), + get_request(Socket, NewBuffer, Options, Callback); +get_request(Socket, Buffer, Options, Callback) -> + t(request_start), + get_request_(Socket, Buffer, Options, Callback). + +get_request_(Socket, Buffer, Options, {Mod, Args} = Callback) -> case erlang:decode_packet(http_bin, Buffer, []) of {more, _} -> - case elli_tcp:recv(Socket, 0, request_timeout(Options)) of - {ok, Data} -> - NewBuffer = <>, - get_request(Socket, NewBuffer, Options, Callback); - {error, timeout} -> - handle_event(Mod, request_timeout, [], Args), - elli_tcp:close(Socket), - exit(normal); - {error, Closed} when Closed =:= closed orelse - Closed =:= enotconn -> - handle_event(Mod, request_closed, [], Args), - elli_tcp:close(Socket), - exit(normal) - end; + recv_request(Socket, Buffer, Options, Callback); {ok, {http_request, Method, RawPath, Version}, Rest} -> {Method, RawPath, Version, Rest}; {ok, {http_error, _}, _} -> @@ -433,6 +426,21 @@ get_request(Socket, Buffer, Options, {Mod, Args} = Callback) -> exit(normal) end. +recv_request(Socket, Buffer, Options, {Mod, Args} = _Callback) -> + case elli_tcp:recv(Socket, 0, request_timeout(Options)) of + {ok, Data} -> + <>; + {error, timeout} -> + handle_event(Mod, request_timeout, [], Args), + elli_tcp:close(Socket), + exit(normal); + {error, Closed} when Closed =:= closed orelse + Closed =:= enotconn -> + handle_event(Mod, request_closed, [], Args), + elli_tcp:close(Socket), + exit(normal) + end. + -spec get_headers(Socket, V, Buffer, Opts, Callback) -> Headers when Socket :: elli_tcp:socket(), V :: version(), From 02aed60d6f549baa2e5f9c8a7bfbb39292d43021 Mon Sep 17 00:00:00 2001 From: Ilya Khaprov Date: Sun, 27 Nov 2016 13:59:28 +0300 Subject: [PATCH 19/20] request_start metric fix test. see #19 --- rebar.config | 3 +++ test/elli_tests.erl | 65 +++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 66 insertions(+), 2 deletions(-) diff --git a/rebar.config b/rebar.config index 571f742..949de8a 100644 --- a/rebar.config +++ b/rebar.config @@ -11,6 +11,9 @@ ]}, {doclet, edown_doclet} ]} + ]}, + {test, [ + {deps, [{hackney, "1.6.3"}]} ]} ]}. diff --git a/test/elli_tests.erl b/test/elli_tests.erl index fb0e47d..7259ae0 100644 --- a/test/elli_tests.erl +++ b/test/elli_tests.erl @@ -7,8 +7,8 @@ -define(I2L(I), integer_to_list(I)). -define(README, "README.md"). -define(VTB(T1, T2, LB, UB), - time_diff_to_micro_seconds(T1, T2) > LB andalso - time_diff_to_micro_seconds(T1, T2) < UB). + time_diff_to_micro_seconds(T1, T2) >= LB andalso + time_diff_to_micro_seconds(T1, T2) =< UB). time_diff_to_micro_seconds(T1, T2) -> erlang:convert_time_unit( @@ -23,6 +23,7 @@ elli_test_() -> [{foreach, fun init_stats/0, fun clear_stats/1, [?_test(hello_world()), + ?_test(keep_alive_timings()), ?_test(not_found()), ?_test(crash()), ?_test(invalid_return()), @@ -72,6 +73,7 @@ setup() -> application:start(crypto), application:start(public_key), application:start(ssl), + hackney:start(), inets:start(), Config = [ @@ -167,6 +169,65 @@ hello_world() -> ?assertMatch(true, ?VTB(send_start, send_end, 1, 200)). + +keep_alive_timings() -> + + Transport = hackney_tcp, + Host = <<"localhost">>, + Port = 3001, + Options = [], + {ok, ConnRef} = hackney:connect(Transport, Host, Port, Options), + + ReqBody = <<>>, + ReqHeaders = [], + ReqPath = <<"/hello/world">>, + ReqMethod = get, + Req = {ReqMethod, ReqPath, ReqHeaders, ReqBody}, + + {ok, Status, Headers, HCRef} = hackney:send_request(ConnRef, Req), + keep_alive_timings(Status, Headers, HCRef), + + %% pause between keep-alive requests, + %% request_start is a timestamp of + %% the first bytes of the second request + timer:sleep(1000), + + {ok, Status, Headers, HCRef} = hackney:send_request(ConnRef, Req), + keep_alive_timings(Status, Headers, HCRef), + + hackney:close(ConnRef). + +keep_alive_timings(Status, Headers, HCRef) -> + ?assertMatch(200, Status), + ?assertMatch([{<<"Connection">>,<<"Keep-Alive">>}, + {<<"Content-Length">>,<<"12">>}], Headers), + ?assertMatch({ok, <<"Hello World!">>}, hackney:body(HCRef)), + %% sizes + ?assertMatch(63, get_size_value(resp_headers)), + ?assertMatch(12, get_size_value(resp_body)), + %% timings + ?assertNotMatch(undefined, get_timing_value(request_start)), + ?assertNotMatch(undefined, get_timing_value(headers_start)), + ?assertNotMatch(undefined, get_timing_value(headers_end)), + ?assertNotMatch(undefined, get_timing_value(body_start)), + ?assertNotMatch(undefined, get_timing_value(body_end)), + ?assertNotMatch(undefined, get_timing_value(user_start)), + ?assertNotMatch(undefined, get_timing_value(user_end)), + ?assertNotMatch(undefined, get_timing_value(send_start)), + ?assertNotMatch(undefined, get_timing_value(send_end)), + ?assertNotMatch(undefined, get_timing_value(request_end)), + %% check timings + ?assertMatch(true, + ?VTB(request_start, request_end, 1000000, 1200000)), + ?assertMatch(true, + ?VTB(headers_start, headers_end, 1, 100)), + ?assertMatch(true, + ?VTB(body_start, body_end, 1, 100)), + ?assertMatch(true, + ?VTB(user_start, user_end, 1000000, 1200000)), + ?assertMatch(true, + ?VTB(send_start, send_end, 1, 200)). + not_found() -> {ok, Response} = httpc:request("http://localhost:3001/foobarbaz"), ?assertMatch(404, status(Response)), From f2d490cc391b237712115bf64a75a3faf945db40 Mon Sep 17 00:00:00 2001 From: Ilya Khaprov Date: Tue, 27 Dec 2016 21:48:49 +0300 Subject: [PATCH 20/20] bump to 2.0.1 and regenerate docs --- doc/README.md | 1 + doc/edoc-info | 2 +- doc/elli_handler.md | 2 +- doc/elli_test.md | 2 +- doc/elli_util.md | 4 ++-- src/elli.app.src | 2 +- 6 files changed, 7 insertions(+), 6 deletions(-) diff --git a/doc/README.md b/doc/README.md index 1a00896..05af46e 100644 --- a/doc/README.md +++ b/doc/README.md @@ -159,6 +159,7 @@ about benchmarking HTTP servers. + diff --git a/doc/edoc-info b/doc/edoc-info index 7dfc7ca..98f05d9 100644 --- a/doc/edoc-info +++ b/doc/edoc-info @@ -1,5 +1,5 @@ %% encoding: UTF-8 {application,elli}. -{modules,[elli,elli_example_callback,elli_example_callback_handover, +{modules,[adder,elli,elli_example_callback,elli_example_callback_handover, elli_handler,elli_http,elli_middleware,elli_middleware_compress, elli_request,elli_tcp,elli_test,elli_util]}. diff --git a/doc/elli_handler.md b/doc/elli_handler.md index 2662af9..5ba6bc5 100644 --- a/doc/elli_handler.md +++ b/doc/elli_handler.md @@ -53,6 +53,6 @@ See [`elli_example_callback:handle_event/3`](elli_example_callback.md#handle_eve

-result() = {elli:response_code() | ok, elli:body()} | {elli:response_code() | ok, elli:headers(), elli:body()} | ignore
+result() = {elli:response_code() | ok, elli:headers(), {file, file:name_all()} | {file, file:name_all(), elli_util:range()}} | {elli:response_code() | ok, elli:headers(), elli:body()} | {elli:response_code() | ok, elli:body()} | {chunk, elli:headers()} | {chunk, elli:headers(), elli:body()} | ignore
 
diff --git a/doc/elli_test.md b/doc/elli_test.md index aa893f7..12f4df9 100644 --- a/doc/elli_test.md +++ b/doc/elli_test.md @@ -32,7 +32,7 @@ The unit tests below test `elli_example_callback`. ### call/5 ###

-call(Method, Path, Headers, Body, Opts) -> elli:req()
+call(Method, Path, Headers, Body, Opts) -> elli_handler:result()
 
diff --git a/doc/elli_util.md b/doc/elli_util.md index e4ce653..de10432 100644 --- a/doc/elli_util.md +++ b/doc/elli_util.md @@ -49,10 +49,10 @@ Encode Range to a Content-Range value. ### file_size/1 ###

-file_size(Filename::file:name()) -> non_neg_integer() | {error, Reason}
+file_size(Filename) -> Size | {error, Reason}
 
- + Get the size in bytes of the file. diff --git a/src/elli.app.src b/src/elli.app.src index 17d3c45..4786b15 100644 --- a/src/elli.app.src +++ b/src/elli.app.src @@ -1,7 +1,7 @@ {application, elli, [ {description, "Erlang web server for HTTP APIs"}, - {vsn, "2.0.0"}, + {vsn, "2.0.1"}, {modules, [ elli, elli_example_callback,
adder
elli
elli_example_callback
elli_example_callback_handover