From f3d13cdfeeaf1f4c0dd938bdee455b0812678eb0 Mon Sep 17 00:00:00 2001 From: Adam Kocoloski Date: Wed, 11 Aug 2010 15:41:54 -0400 Subject: move etap to rebar layout and add simple .app template --- apps/etap/src/etap.app.src | 6 + apps/etap/src/etap.erl | 416 +++++++++++++++++++++++++++++++++++++ apps/etap/src/etap_application.erl | 72 +++++++ apps/etap/src/etap_can.erl | 79 +++++++ apps/etap/src/etap_exception.erl | 66 ++++++ apps/etap/src/etap_process.erl | 42 ++++ apps/etap/src/etap_report.erl | 343 ++++++++++++++++++++++++++++++ apps/etap/src/etap_request.erl | 89 ++++++++ apps/etap/src/etap_string.erl | 47 +++++ apps/etap/src/etap_web.erl | 65 ++++++ src/etap/etap.erl | 416 ------------------------------------- src/etap/etap_application.erl | 72 ------- src/etap/etap_can.erl | 79 ------- src/etap/etap_exception.erl | 66 ------ src/etap/etap_process.erl | 42 ---- src/etap/etap_report.erl | 343 ------------------------------ src/etap/etap_request.erl | 89 -------- src/etap/etap_string.erl | 47 ----- src/etap/etap_web.erl | 65 ------ 19 files changed, 1225 insertions(+), 1219 deletions(-) create mode 100644 apps/etap/src/etap.app.src create mode 100644 apps/etap/src/etap.erl create mode 100644 apps/etap/src/etap_application.erl create mode 100644 apps/etap/src/etap_can.erl create mode 100644 apps/etap/src/etap_exception.erl create mode 100644 apps/etap/src/etap_process.erl create mode 100644 apps/etap/src/etap_report.erl create mode 100644 apps/etap/src/etap_request.erl create mode 100644 apps/etap/src/etap_string.erl create mode 100644 apps/etap/src/etap_web.erl delete mode 100644 src/etap/etap.erl delete mode 100644 src/etap/etap_application.erl delete mode 100644 src/etap/etap_can.erl delete mode 100644 src/etap/etap_exception.erl delete mode 100644 src/etap/etap_process.erl delete mode 100644 src/etap/etap_report.erl delete mode 100644 src/etap/etap_request.erl delete mode 100644 src/etap/etap_string.erl delete mode 100644 src/etap/etap_web.erl 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 +%% +%% 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 [http://socklabs.com/] +%% @author Jeremy Wall +%% @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. +%% +%%

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

+%% +%% 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 +%% +%% 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 [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 +%% +%% 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 +%% +%% 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 +%% +%% 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 +%% +%% 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, "", []), + io:format(IndexFD, "", []), + 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, "
", [RowClass]), + io:format(IndexFD, "~s", [atom_to_list(Module) ++ "_report.html", atom_to_list(Module)]), + io:format(IndexFD, " + + + + +
~p%  + + +
+
+ ", [CovPer, CovPer, UnCovPer]), + io:format(IndexFD, "
", []), + 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, "

Generated on ~s.

~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, "
", []), + io:format(IndexFD, "Total + + + + +
~p%  + + +
+
+ ", [TotalCovPer, TotalCovPer, TotalUnCovPer]), + io:format(IndexFD, "
", []) + end, + io:format(IndexFD, "", []), + 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("~s ~s", [Number, PadNu, Line]); +out_line(Number, highlight, Line) -> + PadNu = string:right(integer_to_list(Number), 5, $.), + io_lib:format("~s ~s", [Number, PadNu, Line]); +out_line(Number, 0, Line) -> + PadNu = string:right(integer_to_list(Number), 5, $.), + io_lib:format("~s ~s", [Number, PadNu, Line]); +out_line(Number, _, Line) -> + PadNu = string:right(integer_to_list(Number), 5, $.), + io_lib:format("~s ~s", [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(" + + + ~s - C0 code coverage information + + + + +

C0 code coverage information

+

Generated on ~s with etap 0.3.4. +

+ + + + + + + + + + + + + + + + + + + + +
NameTotal linesLines of codeTotal coverageCode coverage
+ ~s + + ?? + + ?? + + ?? + + + + + +
~p%  + + +
+
+
", [Module, etap:datetime({date(), time()}), atom_to_list(Module) ++ "_report.html", Module, CovPer, CovPer, UnCovPer]).
+
+%% @private
+footer() ->
+    "

Generated using etap 0.3.4.

+ + + ". 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 +%% +%% 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 +%% +%% 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 [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 +%% +%% 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 [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. diff --git a/src/etap/etap.erl b/src/etap/etap.erl deleted file mode 100644 index 5ad5dba3..00000000 --- a/src/etap/etap.erl +++ /dev/null @@ -1,416 +0,0 @@ -%% Copyright (c) 2008-2009 Nick Gerakines -%% -%% 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 [http://socklabs.com/] -%% @author Jeremy Wall -%% @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. -%% -%%

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

-%% -%% 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/src/etap/etap_application.erl b/src/etap/etap_application.erl deleted file mode 100644 index 98b52751..00000000 --- a/src/etap/etap_application.erl +++ /dev/null @@ -1,72 +0,0 @@ -%% Copyright (c) 2008-2009 Nick Gerakines -%% -%% 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 [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/src/etap/etap_can.erl b/src/etap/etap_can.erl deleted file mode 100644 index 552b7174..00000000 --- a/src/etap/etap_can.erl +++ /dev/null @@ -1,79 +0,0 @@ -%% Copyright (c) 2008-2009 Nick Gerakines -%% -%% 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/src/etap/etap_exception.erl b/src/etap/etap_exception.erl deleted file mode 100644 index ba660727..00000000 --- a/src/etap/etap_exception.erl +++ /dev/null @@ -1,66 +0,0 @@ -%% Copyright (c) 2008-2009 Nick Gerakines -%% -%% 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/src/etap/etap_process.erl b/src/etap/etap_process.erl deleted file mode 100644 index 69f5ba00..00000000 --- a/src/etap/etap_process.erl +++ /dev/null @@ -1,42 +0,0 @@ -%% Copyright (c) 2008-2009 Nick Gerakines -%% -%% 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/src/etap/etap_report.erl b/src/etap/etap_report.erl deleted file mode 100644 index 6d692fb6..00000000 --- a/src/etap/etap_report.erl +++ /dev/null @@ -1,343 +0,0 @@ -%% Copyright (c) 2008-2009 Nick Gerakines -%% -%% 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, "", []), - io:format(IndexFD, "", []), - 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, "
", [RowClass]), - io:format(IndexFD, "~s", [atom_to_list(Module) ++ "_report.html", atom_to_list(Module)]), - io:format(IndexFD, " - - - - -
~p%  - - -
-
- ", [CovPer, CovPer, UnCovPer]), - io:format(IndexFD, "
", []), - 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, "

Generated on ~s.

~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, "
", []), - io:format(IndexFD, "Total - - - - -
~p%  - - -
-
- ", [TotalCovPer, TotalCovPer, TotalUnCovPer]), - io:format(IndexFD, "
", []) - end, - io:format(IndexFD, "", []), - 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("~s ~s", [Number, PadNu, Line]); -out_line(Number, highlight, Line) -> - PadNu = string:right(integer_to_list(Number), 5, $.), - io_lib:format("~s ~s", [Number, PadNu, Line]); -out_line(Number, 0, Line) -> - PadNu = string:right(integer_to_list(Number), 5, $.), - io_lib:format("~s ~s", [Number, PadNu, Line]); -out_line(Number, _, Line) -> - PadNu = string:right(integer_to_list(Number), 5, $.), - io_lib:format("~s ~s", [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(" - - - ~s - C0 code coverage information - - - - -

C0 code coverage information

-

Generated on ~s with etap 0.3.4. -

- - - - - - - - - - - - - - - - - - - - -
NameTotal linesLines of codeTotal coverageCode coverage
- ~s - - ?? - - ?? - - ?? - - - - - -
~p%  - - -
-
-
", [Module, etap:datetime({date(), time()}), atom_to_list(Module) ++ "_report.html", Module, CovPer, CovPer, UnCovPer]).
-
-%% @private
-footer() ->
-    "

Generated using etap 0.3.4.

- - - ". diff --git a/src/etap/etap_request.erl b/src/etap/etap_request.erl deleted file mode 100644 index 9fd23aca..00000000 --- a/src/etap/etap_request.erl +++ /dev/null @@ -1,89 +0,0 @@ -%% Copyright (c) 2008-2009 Nick Gerakines -%% -%% 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/src/etap/etap_string.erl b/src/etap/etap_string.erl deleted file mode 100644 index 67aa3d54..00000000 --- a/src/etap/etap_string.erl +++ /dev/null @@ -1,47 +0,0 @@ -%% Copyright (c) 2008-2009 Nick Gerakines -%% -%% 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 [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/src/etap/etap_web.erl b/src/etap/etap_web.erl deleted file mode 100644 index fb7aee16..00000000 --- a/src/etap/etap_web.erl +++ /dev/null @@ -1,65 +0,0 @@ -%% Copyright (c) 2008-2009 Nick Gerakines -%% -%% 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 [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. -- cgit v1.2.3