diff options
Diffstat (limited to 'apps/etap')
-rw-r--r-- | apps/etap/src/etap.app.src | 6 | ||||
-rw-r--r-- | apps/etap/src/etap.erl | 416 | ||||
-rw-r--r-- | apps/etap/src/etap_application.erl | 72 | ||||
-rw-r--r-- | apps/etap/src/etap_can.erl | 79 | ||||
-rw-r--r-- | apps/etap/src/etap_exception.erl | 66 | ||||
-rw-r--r-- | apps/etap/src/etap_process.erl | 42 | ||||
-rw-r--r-- | apps/etap/src/etap_report.erl | 343 | ||||
-rw-r--r-- | apps/etap/src/etap_request.erl | 89 | ||||
-rw-r--r-- | apps/etap/src/etap_string.erl | 47 | ||||
-rw-r--r-- | apps/etap/src/etap_web.erl | 65 |
10 files changed, 1225 insertions, 0 deletions
diff --git a/apps/etap/src/etap.app.src b/apps/etap/src/etap.app.src new file mode 100644 index 00000000..fe6af267 --- /dev/null +++ b/apps/etap/src/etap.app.src @@ -0,0 +1,6 @@ +{application, etap, [ + {description, "TAP compliant testing library"}, + {vsn, "unknown"}, + {registered, []}, + {applications, [kernel, stdlib]} +]}. diff --git a/apps/etap/src/etap.erl b/apps/etap/src/etap.erl new file mode 100644 index 00000000..5ad5dba3 --- /dev/null +++ b/apps/etap/src/etap.erl @@ -0,0 +1,416 @@ +%% Copyright (c) 2008-2009 Nick Gerakines <nick@gerakines.net> +%% +%% Permission is hereby granted, free of charge, to any person +%% obtaining a copy of this software and associated documentation +%% files (the "Software"), to deal in the Software without +%% restriction, including without limitation the rights to use, +%% copy, modify, merge, publish, distribute, sublicense, and/or sell +%% copies of the Software, and to permit persons to whom the +%% Software is furnished to do so, subject to the following +%% conditions: +%% +%% The above copyright notice and this permission notice shall be +%% included in all copies or substantial portions of the Software. +%% +%% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +%% EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +%% OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +%% NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +%% HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +%% WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +%% FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +%% OTHER DEALINGS IN THE SOFTWARE. +%% +%% @author Nick Gerakines <nick@gerakines.net> [http://socklabs.com/] +%% @author Jeremy Wall <jeremy@marzhillstudios.com> +%% @version 0.3.4 +%% @copyright 2007-2008 Jeremy Wall, 2008-2009 Nick Gerakines +%% @reference http://testanything.org/wiki/index.php/Main_Page +%% @reference http://en.wikipedia.org/wiki/Test_Anything_Protocol +%% @todo Finish implementing the skip directive. +%% @todo Document the messages handled by this receive loop. +%% @todo Explain in documentation why we use a process to handle test input. +%% @doc etap is a TAP testing module for Erlang components and applications. +%% This module allows developers to test their software using the TAP method. +%% +%% <blockquote cite="http://en.wikipedia.org/wiki/Test_Anything_Protocol"><p> +%% TAP, the Test Anything Protocol, is a simple text-based interface between +%% testing modules in a test harness. TAP started life as part of the test +%% harness for Perl but now has implementations in C/C++, Python, PHP, Perl +%% and probably others by the time you read this. +%% </p></blockquote> +%% +%% The testing process begins by defining a plan using etap:plan/1, running +%% a number of etap tests and then calling eta:end_tests/0. Please refer to +%% the Erlang modules in the t directory of this project for example tests. +-module(etap). +-export([ + ensure_test_server/0, start_etap_server/0, test_server/1, + diag/1, diag/2, plan/1, end_tests/0, not_ok/2, ok/2, is/3, isnt/3, + any/3, none/3, fun_is/3, is_greater/3, skip/1, skip/2, + ensure_coverage_starts/0, ensure_coverage_ends/0, coverage_report/0, + datetime/1, skip/3, bail/0, bail/1 +]). +-record(test_state, {planned = 0, count = 0, pass = 0, fail = 0, skip = 0, skip_reason = ""}). +-vsn("0.3.4"). + +%% @spec plan(N) -> Result +%% N = unknown | skip | {skip, string()} | integer() +%% Result = ok +%% @doc Create a test plan and boot strap the test server. +plan(unknown) -> + ensure_coverage_starts(), + ensure_test_server(), + etap_server ! {self(), plan, unknown}, + ok; +plan(skip) -> + io:format("1..0 # skip~n"); +plan({skip, Reason}) -> + io:format("1..0 # skip ~s~n", [Reason]); +plan(N) when is_integer(N), N > 0 -> + ensure_coverage_starts(), + ensure_test_server(), + etap_server ! {self(), plan, N}, + ok. + +%% @spec end_tests() -> ok +%% @doc End the current test plan and output test results. +%% @todo This should probably be done in the test_server process. +end_tests() -> + ensure_coverage_ends(), + etap_server ! {self(), state}, + State = receive X -> X end, + if + State#test_state.planned == -1 -> + io:format("1..~p~n", [State#test_state.count]); + true -> + ok + end, + case whereis(etap_server) of + undefined -> ok; + _ -> etap_server ! done, ok + end. + +%% @private +ensure_coverage_starts() -> + case os:getenv("COVER") of + false -> ok; + _ -> + BeamDir = case os:getenv("COVER_BIN") of false -> "ebin"; X -> X end, + cover:compile_beam_directory(BeamDir) + end. + +%% @private +%% @doc Attempts to write out any collected coverage data to the cover/ +%% directory. This function should not be called externally, but it could be. +ensure_coverage_ends() -> + case os:getenv("COVER") of + false -> ok; + _ -> + filelib:ensure_dir("cover/"), + Name = lists:flatten([ + io_lib:format("~.16b", [X]) || X <- binary_to_list(erlang:md5( + term_to_binary({make_ref(), now()}) + )) + ]), + cover:export("cover/" ++ Name ++ ".coverdata") + end. + +%% @spec coverage_report() -> ok +%% @doc Use the cover module's covreage report builder to create code coverage +%% reports from recently created coverdata files. +coverage_report() -> + [cover:import(File) || File <- filelib:wildcard("cover/*.coverdata")], + lists:foreach( + fun(Mod) -> + cover:analyse_to_file(Mod, atom_to_list(Mod) ++ "_coverage.txt", []) + end, + cover:imported_modules() + ), + ok. + +bail() -> + bail(""). + +bail(Reason) -> + etap_server ! {self(), diag, "Bail out! " ++ Reason}, + ensure_coverage_ends(), + etap_server ! done, ok, + ok. + + +%% @spec diag(S) -> ok +%% S = string() +%% @doc Print a debug/status message related to the test suite. +diag(S) -> etap_server ! {self(), diag, "# " ++ S}, ok. + +%% @spec diag(Format, Data) -> ok +%% Format = atom() | string() | binary() +%% Data = [term()] +%% UnicodeList = [Unicode] +%% Unicode = int() +%% @doc Print a debug/status message related to the test suite. +%% Function arguments are passed through io_lib:format/2. +diag(Format, Data) -> diag(io_lib:format(Format, Data)). + +%% @spec ok(Expr, Desc) -> Result +%% Expr = true | false +%% Desc = string() +%% Result = true | false +%% @doc Assert that a statement is true. +ok(Expr, Desc) -> mk_tap(Expr == true, Desc). + +%% @spec not_ok(Expr, Desc) -> Result +%% Expr = true | false +%% Desc = string() +%% Result = true | false +%% @doc Assert that a statement is false. +not_ok(Expr, Desc) -> mk_tap(Expr == false, Desc). + +%% @spec is(Got, Expected, Desc) -> Result +%% Got = any() +%% Expected = any() +%% Desc = string() +%% Result = true | false +%% @doc Assert that two values are the same. +is(Got, Expected, Desc) -> + case mk_tap(Got == Expected, Desc) of + false -> + etap_server ! {self(), diag, " ---"}, + etap_server ! {self(), diag, io_lib:format(" description: ~p", [Desc])}, + etap_server ! {self(), diag, io_lib:format(" found: ~p", [Got])}, + etap_server ! {self(), diag, io_lib:format(" wanted: ~p", [Expected])}, + etap_server ! {self(), diag, " ..."}, + false; + true -> true + end. + +%% @spec isnt(Got, Expected, Desc) -> Result +%% Got = any() +%% Expected = any() +%% Desc = string() +%% Result = true | false +%% @doc Assert that two values are not the same. +isnt(Got, Expected, Desc) -> mk_tap(Got /= Expected, Desc). + +%% @spec is_greater(ValueA, ValueB, Desc) -> Result +%% ValueA = number() +%% ValueB = number() +%% Desc = string() +%% Result = true | false +%% @doc Assert that an integer is greater than another. +is_greater(ValueA, ValueB, Desc) when is_integer(ValueA), is_integer(ValueB) -> + mk_tap(ValueA > ValueB, Desc). + +%% @spec any(Got, Items, Desc) -> Result +%% Got = any() +%% Items = [any()] +%% Desc = string() +%% Result = true | false +%% @doc Assert that an item is in a list. +any(Got, Items, Desc) -> + is(lists:member(Got, Items), true, Desc). + +%% @spec none(Got, Items, Desc) -> Result +%% Got = any() +%% Items = [any()] +%% Desc = string() +%% Result = true | false +%% @doc Assert that an item is not in a list. +none(Got, Items, Desc) -> + is(lists:member(Got, Items), false, Desc). + +%% @spec fun_is(Fun, Expected, Desc) -> Result +%% Fun = function() +%% Expected = any() +%% Desc = string() +%% Result = true | false +%% @doc Use an anonymous function to assert a pattern match. +fun_is(Fun, Expected, Desc) when is_function(Fun) -> + is(Fun(Expected), true, Desc). + +%% @equiv skip(TestFun, "") +skip(TestFun) when is_function(TestFun) -> + skip(TestFun, ""). + +%% @spec skip(TestFun, Reason) -> ok +%% TestFun = function() +%% Reason = string() +%% @doc Skip a test. +skip(TestFun, Reason) when is_function(TestFun), is_list(Reason) -> + begin_skip(Reason), + catch TestFun(), + end_skip(), + ok. + +%% @spec skip(Q, TestFun, Reason) -> ok +%% Q = true | false | function() +%% TestFun = function() +%% Reason = string() +%% @doc Skips a test conditionally. The first argument to this function can +%% either be the 'true' or 'false' atoms or a function that returns 'true' or +%% 'false'. +skip(QFun, TestFun, Reason) when is_function(QFun), is_function(TestFun), is_list(Reason) -> + case QFun() of + true -> begin_skip(Reason), TestFun(), end_skip(); + _ -> TestFun() + end, + ok; + +skip(Q, TestFun, Reason) when is_function(TestFun), is_list(Reason), Q == true -> + begin_skip(Reason), + TestFun(), + end_skip(), + ok; + +skip(_, TestFun, Reason) when is_function(TestFun), is_list(Reason) -> + TestFun(), + ok. + +%% @private +begin_skip(Reason) -> + etap_server ! {self(), begin_skip, Reason}. + +%% @private +end_skip() -> + etap_server ! {self(), end_skip}. + +% --- +% Internal / Private functions + +%% @private +%% @doc Start the etap_server process if it is not running already. +ensure_test_server() -> + case whereis(etap_server) of + undefined -> + proc_lib:start(?MODULE, start_etap_server,[]); + _ -> + diag("The test server is already running.") + end. + +%% @private +%% @doc Start the etap_server loop and register itself as the etap_server +%% process. +start_etap_server() -> + catch register(etap_server, self()), + proc_lib:init_ack(ok), + etap:test_server(#test_state{ + planned = 0, + count = 0, + pass = 0, + fail = 0, + skip = 0, + skip_reason = "" + }). + + +%% @private +%% @doc The main etap_server receive/run loop. The etap_server receive loop +%% responds to seven messages apperatining to failure or passing of tests. +%% It is also used to initiate the testing process with the {_, plan, _} +%% message that clears the current test state. +test_server(State) -> + NewState = receive + {_From, plan, unknown} -> + io:format("# Current time local ~s~n", [datetime(erlang:localtime())]), + io:format("# Using etap version ~p~n", [ proplists:get_value(vsn, proplists:get_value(attributes, etap:module_info())) ]), + State#test_state{ + planned = -1, + count = 0, + pass = 0, + fail = 0, + skip = 0, + skip_reason = "" + }; + {_From, plan, N} -> + io:format("# Current time local ~s~n", [datetime(erlang:localtime())]), + io:format("# Using etap version ~p~n", [ proplists:get_value(vsn, proplists:get_value(attributes, etap:module_info())) ]), + io:format("1..~p~n", [N]), + State#test_state{ + planned = N, + count = 0, + pass = 0, + fail = 0, + skip = 0, + skip_reason = "" + }; + {_From, begin_skip, Reason} -> + State#test_state{ + skip = 1, + skip_reason = Reason + }; + {_From, end_skip} -> + State#test_state{ + skip = 0, + skip_reason = "" + }; + {_From, pass, Desc} -> + FullMessage = skip_diag( + " - " ++ Desc, + State#test_state.skip, + State#test_state.skip_reason + ), + io:format("ok ~p ~s~n", [State#test_state.count + 1, FullMessage]), + State#test_state{ + count = State#test_state.count + 1, + pass = State#test_state.pass + 1 + }; + + {_From, fail, Desc} -> + FullMessage = skip_diag( + " - " ++ Desc, + State#test_state.skip, + State#test_state.skip_reason + ), + io:format("not ok ~p ~s~n", [State#test_state.count + 1, FullMessage]), + State#test_state{ + count = State#test_state.count + 1, + fail = State#test_state.fail + 1 + }; + {From, state} -> + From ! State, + State; + {_From, diag, Message} -> + io:format("~s~n", [Message]), + State; + {From, count} -> + From ! State#test_state.count, + State; + {From, is_skip} -> + From ! State#test_state.skip, + State; + done -> + exit(normal) + end, + test_server(NewState). + +%% @private +%% @doc Process the result of a test and send it to the etap_server process. +mk_tap(Result, Desc) -> + IsSkip = lib:sendw(etap_server, is_skip), + case [IsSkip, Result] of + [_, true] -> + etap_server ! {self(), pass, Desc}, + true; + [1, _] -> + etap_server ! {self(), pass, Desc}, + true; + _ -> + etap_server ! {self(), fail, Desc}, + false + end. + +%% @private +%% @doc Format a date/time string. +datetime(DateTime) -> + {{Year, Month, Day}, {Hour, Min, Sec}} = DateTime, + io_lib:format("~4.10.0B-~2.10.0B-~2.10.0B ~2.10.0B:~2.10.0B:~2.10.0B", [Year, Month, Day, Hour, Min, Sec]). + +%% @private +%% @doc Craft an output message taking skip/todo into consideration. +skip_diag(Message, 0, _) -> + Message; +skip_diag(_Message, 1, "") -> + " # SKIP"; +skip_diag(_Message, 1, Reason) -> + " # SKIP : " ++ Reason. diff --git a/apps/etap/src/etap_application.erl b/apps/etap/src/etap_application.erl new file mode 100644 index 00000000..98b52751 --- /dev/null +++ b/apps/etap/src/etap_application.erl @@ -0,0 +1,72 @@ +%% Copyright (c) 2008-2009 Nick Gerakines <nick@gerakines.net> +%% +%% Permission is hereby granted, free of charge, to any person +%% obtaining a copy of this software and associated documentation +%% files (the "Software"), to deal in the Software without +%% restriction, including without limitation the rights to use, +%% copy, modify, merge, publish, distribute, sublicense, and/or sell +%% copies of the Software, and to permit persons to whom the +%% Software is furnished to do so, subject to the following +%% conditions: +%% +%% The above copyright notice and this permission notice shall be +%% included in all copies or substantial portions of the Software. +%% +%% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +%% EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +%% OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +%% NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +%% HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +%% WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +%% FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +%% OTHER DEALINGS IN THE SOFTWARE. +%% +%% @author Nick Gerakines <nick@gerakines.net> [http://socklabs.com/] +%% @copyright 2008 Nick Gerakines +%% @reference http://testanything.org/wiki/index.php/Main_Page +%% @reference http://en.wikipedia.org/wiki/Test_Anything_Protocol +%% @todo Explain in documentation why we use a process to handle test input. +%% @todo Add test to verify the number of members in a pg2 group. +%% @doc Provide test functionality to the application and related behaviors. +-module(etap_application). +-export([ + start_ok/2, ensure_loaded/3, load_ok/2, + pg2_group_exists/2, pg2_group_doesntexist/2 +]). + +%% @spec load_ok(string(), string()) -> true | false +%% @doc Assert that an application can be loaded successfully. +load_ok(AppName, Desc) -> + etap:ok(application:load(AppName) == ok, Desc). + +%% @spec start_ok(string(), string()) -> true | false +%% @doc Assert that an application can be started successfully. +start_ok(AppName, Desc) -> + etap:ok(application:start(AppName) == ok, Desc). + +%% @spec ensure_loaded(string(), string(), string()) -> true | false +%% @doc Assert that an application has been loaded successfully. +ensure_loaded(AppName, AppVsn, Desc) -> + etap:any( + fun(Match) -> case Match of {AppName, _, AppVsn} -> true; _ -> false end end, + application:loaded_applications(), + Desc + ). + +%% @spec pg2_group_exists(string(), string()) -> true | false +%% @doc Assert that a pg2 group exists. +pg2_group_exists(GroupName, Desc) -> + etap:any( + fun(Match) -> Match == GroupName end, + pg2:which_groups(), + Desc + ). + +%% @spec pg2_group_doesntexist(string(), string()) -> true | false +%% @doc Assert that a pg2 group does not exists. +pg2_group_doesntexist(GroupName, Desc) -> + etap:none( + fun(Match) -> Match == GroupName end, + pg2:which_groups(), + Desc + ). diff --git a/apps/etap/src/etap_can.erl b/apps/etap/src/etap_can.erl new file mode 100644 index 00000000..552b7174 --- /dev/null +++ b/apps/etap/src/etap_can.erl @@ -0,0 +1,79 @@ +%% Copyright (c) 2008-2009 Nick Gerakines <nick@gerakines.net> +%% +%% Permission is hereby granted, free of charge, to any person +%% obtaining a copy of this software and associated documentation +%% files (the "Software"), to deal in the Software without +%% restriction, including without limitation the rights to use, +%% copy, modify, merge, publish, distribute, sublicense, and/or sell +%% copies of the Software, and to permit persons to whom the +%% Software is furnished to do so, subject to the following +%% conditions: +%% +%% The above copyright notice and this permission notice shall be +%% included in all copies or substantial portions of the Software. +%% +%% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +%% EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +%% OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +%% NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +%% HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +%% WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +%% FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +%% OTHER DEALINGS IN THE SOFTWARE. +%% +%% @reference http://testanything.org/wiki/index.php/Main_Page +%% @reference http://en.wikipedia.org/wiki/Test_Anything_Protocol +%% @doc Provide test functionality modules +-module(etap_can). + +-export([ + loaded_ok/2, can_ok/2, can_ok/3, + has_attrib/2, is_attrib/3, is_behaviour/2 +]). + +%% @spec loaded_ok(atom(), string()) -> true | false +%% @doc Assert that a module has been loaded successfully. +loaded_ok(M, Desc) when is_atom(M) -> + etap:fun_is(fun({module, _}) -> true; (_) -> false end, code:load_file(M), Desc). + +%% @spec can_ok(atom(), atom()) -> true | false +%% @doc Assert that a module exports a given function. +can_ok(M, F) when is_atom(M), is_atom(F) -> + Matches = [X || {X, _} <- M:module_info(exports), X == F], + etap:ok(Matches > 0, lists:concat([M, " can ", F])). + +%% @spec can_ok(atom(), atom(), integer()) -> true | false +%% @doc Assert that a module exports a given function with a given arity. +can_ok(M, F, A) when is_atom(M); is_atom(F), is_number(A) -> + Matches = [X || X <- M:module_info(exports), X == {F, A}], + etap:ok(Matches > 0, lists:concat([M, " can ", F, "/", A])). + +%% @spec has_attrib(M, A) -> true | false +%% M = atom() +%% A = atom() +%% @doc Asserts that a module has a given attribute. +has_attrib(M, A) when is_atom(M), is_atom(A) -> + etap:isnt( + proplists:get_value(A, M:module_info(attributes), 'asdlkjasdlkads'), + 'asdlkjasdlkads', + lists:concat([M, " has attribute ", A]) + ). + +%% @spec has_attrib(M, A. V) -> true | false +%% M = atom() +%% A = atom() +%% V = any() +%% @doc Asserts that a module has a given attribute with a given value. +is_attrib(M, A, V) when is_atom(M) andalso is_atom(A) -> + etap:is( + proplists:get_value(A, M:module_info(attributes)), + [V], + lists:concat([M, "'s ", A, " is ", V]) + ). + +%% @spec is_behavior(M, B) -> true | false +%% M = atom() +%% B = atom() +%% @doc Asserts that a given module has a specific behavior. +is_behaviour(M, B) when is_atom(M) andalso is_atom(B) -> + is_attrib(M, behaviour, B). diff --git a/apps/etap/src/etap_exception.erl b/apps/etap/src/etap_exception.erl new file mode 100644 index 00000000..ba660727 --- /dev/null +++ b/apps/etap/src/etap_exception.erl @@ -0,0 +1,66 @@ +%% Copyright (c) 2008-2009 Nick Gerakines <nick@gerakines.net> +%% +%% Permission is hereby granted, free of charge, to any person +%% obtaining a copy of this software and associated documentation +%% files (the "Software"), to deal in the Software without +%% restriction, including without limitation the rights to use, +%% copy, modify, merge, publish, distribute, sublicense, and/or sell +%% copies of the Software, and to permit persons to whom the +%% Software is furnished to do so, subject to the following +%% conditions: +%% +%% The above copyright notice and this permission notice shall be +%% included in all copies or substantial portions of the Software. +%% +%% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +%% EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +%% OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +%% NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +%% HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +%% WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +%% FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +%% OTHER DEALINGS IN THE SOFTWARE. +%% +%% @reference http://testanything.org/wiki/index.php/Main_Page +%% @reference http://en.wikipedia.org/wiki/Test_Anything_Protocol +%% @doc Adds exception based testing to the etap suite. +-module(etap_exception). + +-export([dies_ok/2, lives_ok/2, throws_ok/3]). + +% --- +% External / Public functions + +%% @doc Assert that an exception is raised when running a given function. +dies_ok(F, Desc) -> + case (catch F()) of + {'EXIT', _} -> etap:ok(true, Desc); + _ -> etap:ok(false, Desc) + end. + +%% @doc Assert that an exception is not raised when running a given function. +lives_ok(F, Desc) -> + etap:is(try_this(F), success, Desc). + +%% @doc Assert that the exception thrown by a function matches the given exception. +throws_ok(F, Exception, Desc) -> + try F() of + _ -> etap:ok(nok, Desc) + catch + _:E -> + etap:is(E, Exception, Desc) + end. + +% --- +% Internal / Private functions + +%% @private +%% @doc Run a function and catch any exceptions. +try_this(F) when is_function(F, 0) -> + try F() of + _ -> success + catch + throw:E -> {throw, E}; + error:E -> {error, E}; + exit:E -> {exit, E} + end. diff --git a/apps/etap/src/etap_process.erl b/apps/etap/src/etap_process.erl new file mode 100644 index 00000000..69f5ba00 --- /dev/null +++ b/apps/etap/src/etap_process.erl @@ -0,0 +1,42 @@ +%% Copyright (c) 2008-2009 Nick Gerakines <nick@gerakines.net> +%% +%% Permission is hereby granted, free of charge, to any person +%% obtaining a copy of this software and associated documentation +%% files (the "Software"), to deal in the Software without +%% restriction, including without limitation the rights to use, +%% copy, modify, merge, publish, distribute, sublicense, and/or sell +%% copies of the Software, and to permit persons to whom the +%% Software is furnished to do so, subject to the following +%% conditions: +%% +%% The above copyright notice and this permission notice shall be +%% included in all copies or substantial portions of the Software. +%% +%% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +%% EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +%% OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +%% NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +%% HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +%% WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +%% FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +%% OTHER DEALINGS IN THE SOFTWARE. +%% +%% @doc Adds process/pid testing to the etap suite. +-module(etap_process). + +-export([is_pid/2, is_alive/2, is_mfa/3]). + +% --- +% External / Public functions + +%% @doc Assert that a given variable is a pid. +is_pid(Pid, Desc) when is_pid(Pid) -> etap:ok(true, Desc); +is_pid(_, Desc) -> etap:ok(false, Desc). + +%% @doc Assert that a given process/pid is alive. +is_alive(Pid, Desc) -> + etap:ok(erlang:is_process_alive(Pid), Desc). + +%% @doc Assert that the current function of a pid is a given {M, F, A} tuple. +is_mfa(Pid, MFA, Desc) -> + etap:is({current_function, MFA}, erlang:process_info(Pid, current_function), Desc). diff --git a/apps/etap/src/etap_report.erl b/apps/etap/src/etap_report.erl new file mode 100644 index 00000000..6d692fb6 --- /dev/null +++ b/apps/etap/src/etap_report.erl @@ -0,0 +1,343 @@ +%% Copyright (c) 2008-2009 Nick Gerakines <nick@gerakines.net> +%% +%% Permission is hereby granted, free of charge, to any person +%% obtaining a copy of this software and associated documentation +%% files (the "Software"), to deal in the Software without +%% restriction, including without limitation the rights to use, +%% copy, modify, merge, publish, distribute, sublicense, and/or sell +%% copies of the Software, and to permit persons to whom the +%% Software is furnished to do so, subject to the following +%% conditions: +%% +%% The above copyright notice and this permission notice shall be +%% included in all copies or substantial portions of the Software. +%% +%% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +%% EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +%% OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +%% NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +%% HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +%% WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +%% FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +%% OTHER DEALINGS IN THE SOFTWARE. +%% +%% @doc A module for creating nice looking code coverage reports. +-module(etap_report). +-export([create/0]). + +%% @spec create() -> ok +%% @doc Create html code coverage reports for each module that code coverage +%% data exists for. +create() -> + [cover:import(File) || File <- filelib:wildcard("cover/*.coverdata")], + Modules = lists:foldl( + fun(Module, Acc) -> + [{Module, file_report(Module)} | Acc] + end, + [], + cover:imported_modules() + ), + index(Modules). + +%% @private +index(Modules) -> + {ok, IndexFD} = file:open("cover/index.html", [write]), + io:format(IndexFD, "<html><head><style> + table.percent_graph { height: 12px; border:1px solid #E2E6EF; empty-cells: show; } + table.percent_graph td.covered { height: 10px; background: #00f000; } + table.percent_graph td.uncovered { height: 10px; background: #e00000; } + .odd { background-color: #ddd; } + .even { background-color: #fff; } + </style></head>", []), + io:format(IndexFD, "<body>", []), + lists:foldl( + fun({Module, {Good, Bad, Source}}, LastRow) -> + case {Good + Bad, Source} of + {0, _} -> LastRow; + {_, none} -> LastRow; + _ -> + CovPer = round((Good / (Good + Bad)) * 100), + UnCovPer = round((Bad / (Good + Bad)) * 100), + RowClass = case LastRow of 1 -> "odd"; _ -> "even" end, + io:format(IndexFD, "<div class=\"~s\">", [RowClass]), + io:format(IndexFD, "<a href=\"~s\">~s</a>", [atom_to_list(Module) ++ "_report.html", atom_to_list(Module)]), + io:format(IndexFD, " + <table cellspacing='0' cellpadding='0' align='right'> + <tr> + <td><tt>~p%</tt> </td><td> + <table cellspacing='0' class='percent_graph' cellpadding='0' width='100'> + <tr><td class='covered' width='~p' /><td class='uncovered' width='~p' /></tr> + </table> + </td> + </tr> + </table> + ", [CovPer, CovPer, UnCovPer]), + io:format(IndexFD, "</div>", []), + case LastRow of + 1 -> 0; + 0 -> 1 + end + end + end, + 0, + lists:sort(Modules) + ), + {TotalGood, TotalBad} = lists:foldl( + fun({_, {Good, Bad, Source}}, {TGood, TBad}) -> + case Source of none -> {TGood, TBad}; _ -> {TGood + Good, TBad + Bad} end + end, + {0, 0}, + Modules + ), + io:format(IndexFD, "<p>Generated on ~s.</p>~n", [etap:datetime({date(), time()})]), + case TotalGood + TotalBad of + 0 -> ok; + _ -> + TotalCovPer = round((TotalGood / (TotalGood + TotalBad)) * 100), + TotalUnCovPer = round((TotalBad / (TotalGood + TotalBad)) * 100), + io:format(IndexFD, "<div>", []), + io:format(IndexFD, "Total + <table cellspacing='0' cellpadding='0' align='right'> + <tr> + <td><tt>~p%</tt> </td><td> + <table cellspacing='0' class='percent_graph' cellpadding='0' width='100'> + <tr><td class='covered' width='~p' /><td class='uncovered' width='~p' /></tr> + </table> + </td> + </tr> + </table> + ", [TotalCovPer, TotalCovPer, TotalUnCovPer]), + io:format(IndexFD, "</div>", []) + end, + io:format(IndexFD, "</body></html>", []), + file:close(IndexFD), + ok. + +%% @private +file_report(Module) -> + {ok, Data} = cover:analyse(Module, calls, line), + Source = find_source(Module), + {Good, Bad} = collect_coverage(Data, {0, 0}), + case {Source, Good + Bad} of + {none, _} -> ok; + {_, 0} -> ok; + _ -> + {ok, SourceFD} = file:open(Source, [read]), + {ok, WriteFD} = file:open("cover/" ++ atom_to_list(Module) ++ "_report.html", [write]), + io:format(WriteFD, "~s", [header(Module, Good, Bad)]), + output_lines(Data, WriteFD, SourceFD, 1), + io:format(WriteFD, "~s", [footer()]), + file:close(WriteFD), + file:close(SourceFD), + ok + end, + {Good, Bad, Source}. + +%% @private +collect_coverage([], Acc) -> Acc; +collect_coverage([{{_, _}, 0} | Data], {Good, Bad}) -> + collect_coverage(Data, {Good, Bad + 1}); +collect_coverage([_ | Data], {Good, Bad}) -> + collect_coverage(Data, {Good + 1, Bad}). + +%% @private +output_lines(Data, WriteFD, SourceFD, LineNumber) -> + {Match, NextData} = datas_match(Data, LineNumber), + case io:get_line(SourceFD, '') of + eof -> ok; + Line = "%% @todo" ++ _ -> + io:format(WriteFD, "~s", [out_line(LineNumber, highlight, Line)]), + output_lines(NextData, WriteFD, SourceFD, LineNumber + 1); + Line = "% " ++ _ -> + io:format(WriteFD, "~s", [out_line(LineNumber, none, Line)]), + output_lines(NextData, WriteFD, SourceFD, LineNumber + 1); + Line -> + case Match of + {true, CC} -> + io:format(WriteFD, "~s", [out_line(LineNumber, CC, Line)]), + output_lines(NextData, WriteFD, SourceFD, LineNumber + 1); + false -> + io:format(WriteFD, "~s", [out_line(LineNumber, none, Line)]), + output_lines(NextData, WriteFD, SourceFD, LineNumber + 1) + end + end. + +%% @private +out_line(Number, none, Line) -> + PadNu = string:right(integer_to_list(Number), 5, $.), + io_lib:format("<span class=\"marked\"><a name=\"line~p\"></a>~s ~s</span>", [Number, PadNu, Line]); +out_line(Number, highlight, Line) -> + PadNu = string:right(integer_to_list(Number), 5, $.), + io_lib:format("<span class=\"highlight\"><a name=\"line~p\"></a>~s ~s</span>", [Number, PadNu, Line]); +out_line(Number, 0, Line) -> + PadNu = string:right(integer_to_list(Number), 5, $.), + io_lib:format("<span class=\"uncovered\"><a name=\"line~p\"></a>~s ~s</span>", [Number, PadNu, Line]); +out_line(Number, _, Line) -> + PadNu = string:right(integer_to_list(Number), 5, $.), + io_lib:format("<span class=\"covered\"><a name=\"line~p\"></a>~s ~s</span>", [Number, PadNu, Line]). + +%% @private +datas_match([], _) -> {false, []}; +datas_match([{{_, Line}, CC} | Datas], LineNumber) when Line == LineNumber -> {{true, CC}, Datas}; +datas_match(Data, _) -> {false, Data}. + +%% @private +find_source(Module) when is_atom(Module) -> + Root = filename:rootname(Module), + Dir = filename:dirname(Root), + XDir = case os:getenv("SRC") of false -> "src"; X -> X end, + find_source([ + filename:join([Dir, Root ++ ".erl"]), + filename:join([Dir, "..", "src", Root ++ ".erl"]), + filename:join([Dir, "src", Root ++ ".erl"]), + filename:join([Dir, "elibs", Root ++ ".erl"]), + filename:join([Dir, "..", "elibs", Root ++ ".erl"]), + filename:join([Dir, XDir, Root ++ ".erl"]) + ]); +find_source([]) -> none; +find_source([Test | Tests]) -> + case filelib:is_file(Test) of + true -> Test; + false -> find_source(Tests) + end. + +%% @private +header(Module, Good, Bad) -> + io:format("Good ~p~n", [Good]), + io:format("Bad ~p~n", [Bad]), + CovPer = round((Good / (Good + Bad)) * 100), + UnCovPer = round((Bad / (Good + Bad)) * 100), + io:format("CovPer ~p~n", [CovPer]), + io_lib:format("<!DOCTYPE html PUBLIC \"-//W3C//DTD XHTML 1.0 Transitional//EN\" \"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd\"> + <html lang='en' xml:lang='en' xmlns='http://www.w3.org/1999/xhtml'> + <head> + <title>~s - C0 code coverage information</title> + <style type='text/css'>body { background-color: rgb(240, 240, 245); }</style> + <style type='text/css'>span.marked0 { + background-color: rgb(185, 210, 200); + display: block; + } + span.marked { display: block; background-color: #ffffff; } + span.highlight { display: block; background-color: #fff9d7; } + span.covered { display: block; background-color: #f7f7f7 ; } + span.uncovered { display: block; background-color: #ffebe8 ; } + span.overview { + border-bottom: 1px solid #E2E6EF; + } + div.overview { + border-bottom: 1px solid #E2E6EF; + } + body { + font-family: verdana, arial, helvetica; + } + div.footer { + font-size: 68%; + margin-top: 1.5em; + } + h1, h2, h3, h4, h5, h6 { + margin-bottom: 0.5em; + } + h5 { + margin-top: 0.5em; + } + .hidden { + display: none; + } + div.separator { + height: 10px; + } + table.percent_graph { + height: 12px; + border: 1px solid #E2E6EF; + empty-cells: show; + } + table.percent_graph td.covered { + height: 10px; + background: #00f000; + } + table.percent_graph td.uncovered { + height: 10px; + background: #e00000; + } + table.percent_graph td.NA { + height: 10px; + background: #eaeaea; + } + table.report { + border-collapse: collapse; + width: 100%; + } + table.report td.heading { + background: #dcecff; + border: 1px solid #E2E6EF; + font-weight: bold; + text-align: center; + } + table.report td.heading:hover { + background: #c0ffc0; + } + table.report td.text { + border: 1px solid #E2E6EF; + } + table.report td.value { + text-align: right; + border: 1px solid #E2E6EF; + } + table.report tr.light { + background-color: rgb(240, 240, 245); + } + table.report tr.dark { + background-color: rgb(230, 230, 235); + } + </style> + </head> + <body> + <h3>C0 code coverage information</h3> + <p>Generated on ~s with <a href='http://github.com/ngerakines/etap'>etap 0.3.4</a>. + </p> + <table class='report'> + <thead> + <tr> + <td class='heading'>Name</td> + <td class='heading'>Total lines</td> + <td class='heading'>Lines of code</td> + <td class='heading'>Total coverage</td> + <td class='heading'>Code coverage</td> + </tr> + </thead> + <tbody> + <tr class='light'> + + <td> + <a href='~s'>~s</a> + </td> + <td class='value'> + <tt>??</tt> + </td> + <td class='value'> + <tt>??</tt> + </td> + <td class='value'> + <tt>??</tt> + </td> + <td> + <table cellspacing='0' cellpadding='0' align='right'> + <tr> + <td><tt>~p%</tt> </td><td> + <table cellspacing='0' class='percent_graph' cellpadding='0' width='100'> + <tr><td class='covered' width='~p' /><td class='uncovered' width='~p' /></tr> + </table> + </td> + </tr> + </table> + </td> + </tr> + </tbody> + </table><pre>", [Module, etap:datetime({date(), time()}), atom_to_list(Module) ++ "_report.html", Module, CovPer, CovPer, UnCovPer]). + +%% @private +footer() -> + "</pre><hr /><p>Generated using <a href='http://github.com/ngerakines/etap'>etap 0.3.4</a>.</p> + </body> + </html> + ". diff --git a/apps/etap/src/etap_request.erl b/apps/etap/src/etap_request.erl new file mode 100644 index 00000000..9fd23aca --- /dev/null +++ b/apps/etap/src/etap_request.erl @@ -0,0 +1,89 @@ +%% Copyright (c) 2008-2009 Nick Gerakines <nick@gerakines.net> +%% +%% Permission is hereby granted, free of charge, to any person +%% obtaining a copy of this software and associated documentation +%% files (the "Software"), to deal in the Software without +%% restriction, including without limitation the rights to use, +%% copy, modify, merge, publish, distribute, sublicense, and/or sell +%% copies of the Software, and to permit persons to whom the +%% Software is furnished to do so, subject to the following +%% conditions: +%% +%% The above copyright notice and this permission notice shall be +%% included in all copies or substantial portions of the Software. +%% +%% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +%% EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +%% OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +%% NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +%% HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +%% WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +%% FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +%% OTHER DEALINGS IN THE SOFTWARE. +%% +%% @doc Provides test functionality against a specific web request. Many of +%% the exported methods can be used to build your own more complex tests. +-module(etap_request, [Method, Url, InHeaders, InBody, Status, OutHeaders, OutBody]). + +-export([status_is/2]). + +-export([ + method/0, url/0, status/0, status_code/0, status_line/0, rheaders/0, + has_rheader/1, rheader/1, rbody/0, header_is/3, body_is/2, + body_has_string/2 +]). + +% --- +% Tests + +%% @doc Assert that response status code is the given status code. +status_is(Code, Desc) -> + etap:is(status_code(), Code, Desc). + +header_is(Name, Value, Desc) -> + etap:is(rheader(Name), Value, Desc). + +body_is(Value, Desc) -> + etap:is(rbody(), Value, Desc). + +body_has_string(String, Desc) when is_list(OutBody), is_list(String) -> + etap_string:contains_ok(OutBody, String, Desc). + +% --- +% Accessor functions + +%% @doc Access a request's method. +method() -> Method. + +%% @doc Access a request's URL. +url() -> Url. + +%% @doc Access a request's status. +status() -> Status. + +%% @doc Access a request's status code. +status_code() -> + {_, Code, _} = Status, + Code. + +%% @doc Access a request's status line. +status_line() -> + {_, _, Line} = Status, + Line. + +%% @doc Access a request's headers. +rheaders() -> OutHeaders. + +%% @doc Dertermine if a specific request header exists. +has_rheader(Key) -> + lists:keymember(Key, 1, OutHeaders). + +%% @doc Return a specific request header. +rheader(Key) -> + case lists:keysearch(Key, 1, OutHeaders) of + false -> undefined; + {value, {Key, Value}} -> Value + end. + +%% @doc Access the request's body. +rbody() -> OutBody. diff --git a/apps/etap/src/etap_string.erl b/apps/etap/src/etap_string.erl new file mode 100644 index 00000000..67aa3d54 --- /dev/null +++ b/apps/etap/src/etap_string.erl @@ -0,0 +1,47 @@ +%% Copyright (c) 2008-2009 Nick Gerakines <nick@gerakines.net> +%% +%% Permission is hereby granted, free of charge, to any person +%% obtaining a copy of this software and associated documentation +%% files (the "Software"), to deal in the Software without +%% restriction, including without limitation the rights to use, +%% copy, modify, merge, publish, distribute, sublicense, and/or sell +%% copies of the Software, and to permit persons to whom the +%% Software is furnished to do so, subject to the following +%% conditions: +%% +%% The above copyright notice and this permission notice shall be +%% included in all copies or substantial portions of the Software. +%% +%% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +%% EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +%% OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +%% NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +%% HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +%% WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +%% FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +%% OTHER DEALINGS IN THE SOFTWARE. +%% +%% @author Nick Gerakines <nick@gerakines.net> [http://socklabs.com/] +%% @copyright 2008 Nick Gerakines +%% @doc Provide testing functionality for strings. +-module(etap_string). + +-export([contains_ok/3, is_before/4]). + +%% @spec contains_ok(string(), string(), string()) -> true | false +%% @doc Assert that a string is contained in another string. +contains_ok(Source, String, Desc) -> + etap:isnt( + string:str(Source, String), + 0, + Desc + ). + +%% @spec is_before(string(), string(), string(), string()) -> true | false +%% @doc Assert that a string comes before another string within a larger body. +is_before(Source, StringA, StringB, Desc) -> + etap:is_greater( + string:str(Source, StringB), + string:str(Source, StringA), + Desc + ). diff --git a/apps/etap/src/etap_web.erl b/apps/etap/src/etap_web.erl new file mode 100644 index 00000000..fb7aee16 --- /dev/null +++ b/apps/etap/src/etap_web.erl @@ -0,0 +1,65 @@ +%% Copyright (c) 2008-2009 Nick Gerakines <nick@gerakines.net> +%% +%% Permission is hereby granted, free of charge, to any person +%% obtaining a copy of this software and associated documentation +%% files (the "Software"), to deal in the Software without +%% restriction, including without limitation the rights to use, +%% copy, modify, merge, publish, distribute, sublicense, and/or sell +%% copies of the Software, and to permit persons to whom the +%% Software is furnished to do so, subject to the following +%% conditions: +%% +%% The above copyright notice and this permission notice shall be +%% included in all copies or substantial portions of the Software. +%% +%% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +%% EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +%% OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +%% NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +%% HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +%% WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +%% FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +%% OTHER DEALINGS IN THE SOFTWARE. +%% +%% @author Nick Gerakines <nick@gerakines.net> [http://socklabs.com/] +%% @copyright 2008 Nick Gerakines +%% @todo Support cookies. +%% @doc Provide testing functionality for web requests. +-module(etap_web). + +-export([simple_200/2, simple_404/2, build_request/4]). + +%% @doc Fetch a url and verify that it returned a 200 status. +simple_200(Url, Desc) -> + Request = build_request(get, Url, [], []), + Request:status_is(200, Desc). + +%% @doc Fetch a url and verify that it returned a 404 status. +simple_404(Url, Desc) -> + Request = build_request(get, Url, [], []), + Request:status_is(404, Desc). + +%% @doc Create and return a request structure. +build_request(Method, Url, Headers, Body) + when Method==options;Method==get;Method==head;Method==delete;Method==trace -> + try http:request(Method, {Url, Headers}, [{autoredirect, false}], []) of + {ok, {OutStatus, OutHeaders, OutBody}} -> + etap_request:new(Method, Url, Headers, Body, OutStatus, OutHeaders, OutBody); + _ -> error + catch + _:_ -> error + end; + +%% @doc Create and return a request structure. +build_request(Method, Url, Headers, Body) when Method == post; Method == put -> + ContentType = case lists:keysearch("Content-Type", 1, Headers) of + {value, {"Content-Type", X}} -> X; + _ -> [] + end, + try http:request(Method, {Url, Headers, ContentType, Body}, [{autoredirect, false}], []) of + {ok, {OutStatus, OutHeaders, OutBody}} -> + etap_request:new(Method, Url, Headers, Body, OutStatus, OutHeaders, OutBody); + _ -> error + catch + _:_ -> error + end. |