Вы не можете выбрать более 25 тем Темы должны начинаться с буквы или цифры, могут содержать дефисы(-) и должны содержать не более 35 символов.

612 строки
19 KiB

13 лет назад
13 лет назад
13 лет назад
13 лет назад
13 лет назад
13 лет назад
13 лет назад
13 лет назад
13 лет назад
13 лет назад
  1. %% Copyright (c) 2008-2009 Nick Gerakines <nick@gerakines.net>
  2. %%
  3. %% Permission is hereby granted, free of charge, to any person
  4. %% obtaining a copy of this software and associated documentation
  5. %% files (the "Software"), to deal in the Software without
  6. %% restriction, including without limitation the rights to use,
  7. %% copy, modify, merge, publish, distribute, sublicense, and/or sell
  8. %% copies of the Software, and to permit persons to whom the
  9. %% Software is furnished to do so, subject to the following
  10. %% conditions:
  11. %%
  12. %% The above copyright notice and this permission notice shall be
  13. %% included in all copies or substantial portions of the Software.
  14. %%
  15. %% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
  16. %% EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
  17. %% OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
  18. %% NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
  19. %% HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
  20. %% WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
  21. %% FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
  22. %% OTHER DEALINGS IN THE SOFTWARE.
  23. %%
  24. %% @author Nick Gerakines <nick@gerakines.net> [http://socklabs.com/]
  25. %% @author Jeremy Wall <jeremy@marzhillstudios.com>
  26. %% @version 0.3.4
  27. %% @copyright 2007-2008 Jeremy Wall, 2008-2009 Nick Gerakines
  28. %% @reference http://testanything.org/wiki/index.php/Main_Page
  29. %% @reference http://en.wikipedia.org/wiki/Test_Anything_Protocol
  30. %% @todo Finish implementing the skip directive.
  31. %% @todo Document the messages handled by this receive loop.
  32. %% @todo Explain in documentation why we use a process to handle test input.
  33. %% @doc etap is a TAP testing module for Erlang components and applications.
  34. %% This module allows developers to test their software using the TAP method.
  35. %%
  36. %% <blockquote cite="http://en.wikipedia.org/wiki/Test_Anything_Protocol"><p>
  37. %% TAP, the Test Anything Protocol, is a simple text-based interface between
  38. %% testing modules in a test harness. TAP started life as part of the test
  39. %% harness for Perl but now has implementations in C/C++, Python, PHP, Perl
  40. %% and probably others by the time you read this.
  41. %% </p></blockquote>
  42. %%
  43. %% The testing process begins by defining a plan using etap:plan/1, running
  44. %% a number of etap tests and then calling eta:end_tests/0. Please refer to
  45. %% the Erlang modules in the t directory of this project for example tests.
  46. -module(etap).
  47. -vsn("0.3.4").
  48. -export([
  49. ensure_test_server/0,
  50. start_etap_server/0,
  51. test_server/1,
  52. msg/1, msg/2,
  53. diag/1, diag/2,
  54. expectation_mismatch_message/3,
  55. plan/1,
  56. end_tests/0,
  57. not_ok/2, ok/2, is_ok/2, is/3, isnt/3, any/3, none/3,
  58. fun_is/3, expect_fun/3, expect_fun/4,
  59. is_greater/3,
  60. skip/1, skip/2,
  61. datetime/1,
  62. skip/3,
  63. bail/0, bail/1,
  64. test_state/0, failure_count/0
  65. ]).
  66. -export([
  67. contains_ok/3,
  68. is_before/4
  69. ]).
  70. -export([
  71. is_pid/2,
  72. is_alive/2,
  73. is_mfa/3
  74. ]).
  75. -export([
  76. loaded_ok/2,
  77. can_ok/2, can_ok/3,
  78. has_attrib/2, is_attrib/3,
  79. is_behaviour/2
  80. ]).
  81. -export([
  82. dies_ok/2,
  83. lives_ok/2,
  84. throws_ok/3
  85. ]).
  86. -record(test_state, {
  87. planned = 0,
  88. count = 0,
  89. pass = 0,
  90. fail = 0,
  91. skip = 0,
  92. skip_reason = ""
  93. }).
  94. %% @spec plan(N) -> Result
  95. %% N = unknown | skip | {skip, string()} | integer()
  96. %% Result = ok
  97. %% @doc Create a test plan and boot strap the test server.
  98. plan(unknown) ->
  99. ensure_test_server(),
  100. etap_server ! {self(), plan, unknown},
  101. ok;
  102. plan(skip) ->
  103. io:format("1..0 # skip~n");
  104. plan({skip, Reason}) ->
  105. io:format("1..0 # skip ~s~n", [Reason]);
  106. plan(N) when is_integer(N), N > 0 ->
  107. ensure_test_server(),
  108. etap_server ! {self(), plan, N},
  109. ok.
  110. %% @spec end_tests() -> ok
  111. %% @doc End the current test plan and output test results.
  112. %% @todo This should probably be done in the test_server process.
  113. end_tests() ->
  114. case whereis(etap_server) of
  115. undefined -> self() ! true;
  116. _ -> etap_server ! {self(), state}
  117. end,
  118. State = receive X -> X end,
  119. if
  120. State#test_state.planned == -1 ->
  121. io:format("1..~p~n", [State#test_state.count]);
  122. true ->
  123. ok
  124. end,
  125. case whereis(etap_server) of
  126. undefined -> ok;
  127. _ -> etap_server ! done, ok
  128. end.
  129. bail() ->
  130. bail("").
  131. bail(Reason) ->
  132. etap_server ! {self(), diag, "Bail out! " ++ Reason},
  133. etap_server ! done, ok,
  134. ok.
  135. %% @spec test_state() -> Return
  136. %% Return = test_state_record() | {error, string()}
  137. %% @doc Return the current test state
  138. test_state() ->
  139. etap_server ! {self(), state},
  140. receive
  141. X when is_record(X, test_state) -> X
  142. after
  143. 1000 -> {error, "Timed out waiting for etap server reply.~n"}
  144. end.
  145. %% @spec failure_count() -> Return
  146. %% Return = integer() | {error, string()}
  147. %% @doc Return the current failure count
  148. failure_count() ->
  149. case test_state() of
  150. #test_state{fail=FailureCount} -> FailureCount;
  151. X -> X
  152. end.
  153. %% @spec msg(S) -> ok
  154. %% S = string()
  155. %% @doc Print a message in the test output.
  156. msg(S) -> etap_server ! {self(), diag, S}, ok.
  157. %% @spec msg(Format, Data) -> ok
  158. %% Format = atom() | string() | binary()
  159. %% Data = [term()]
  160. %% UnicodeList = [Unicode]
  161. %% Unicode = int()
  162. %% @doc Print a message in the test output.
  163. %% Function arguments are passed through io_lib:format/2.
  164. msg(Format, Data) -> msg(io_lib:format(Format, Data)).
  165. %% @spec diag(S) -> ok
  166. %% S = string()
  167. %% @doc Print a debug/status message related to the test suite.
  168. diag(S) -> msg("# " ++ S).
  169. %% @spec diag(Format, Data) -> ok
  170. %% Format = atom() | string() | binary()
  171. %% Data = [term()]
  172. %% UnicodeList = [Unicode]
  173. %% Unicode = int()
  174. %% @doc Print a debug/status message related to the test suite.
  175. %% Function arguments are passed through io_lib:format/2.
  176. diag(Format, Data) -> diag(io_lib:format(Format, Data)).
  177. %% @spec expectation_mismatch_message(Got, Expected, Desc) -> ok
  178. %% Got = any()
  179. %% Expected = any()
  180. %% Desc = string()
  181. %% @doc Print an expectation mismatch message in the test output.
  182. expectation_mismatch_message(Got, Expected, Desc) ->
  183. msg(" ---"),
  184. msg(" description: ~p", [Desc]),
  185. msg(" found: ~p", [Got]),
  186. msg(" wanted: ~p", [Expected]),
  187. msg(" ..."),
  188. ok.
  189. % @spec evaluate(Pass, Got, Expected, Desc) -> Result
  190. %% Pass = true | false
  191. %% Got = any()
  192. %% Expected = any()
  193. %% Desc = string()
  194. %% Result = true | false
  195. %% @doc Evaluate a test statement, printing an expectation mismatch message
  196. %% if the test failed.
  197. evaluate(Pass, Got, Expected, Desc) ->
  198. case mk_tap(Pass, Desc) of
  199. false ->
  200. expectation_mismatch_message(Got, Expected, Desc),
  201. false;
  202. true ->
  203. true
  204. end.
  205. %% @spec ok(Expr, Desc) -> Result
  206. %% Expr = true | false
  207. %% Desc = string()
  208. %% Result = true | false
  209. %% @doc Assert that a statement is true.
  210. ok(Expr, Desc) -> evaluate(Expr == true, Expr, true, Desc).
  211. %% @spec not_ok(Expr, Desc) -> Result
  212. %% Expr = true | false
  213. %% Desc = string()
  214. %% Result = true | false
  215. %% @doc Assert that a statement is false.
  216. not_ok(Expr, Desc) -> evaluate(Expr == false, Expr, false, Desc).
  217. %% @spec is_ok(Expr, Desc) -> Result
  218. %% Expr = any()
  219. %% Desc = string()
  220. %% Result = true | false
  221. %% @doc Assert that two values are the same.
  222. is_ok(Expr, Desc) -> evaluate(Expr == ok, Expr, ok, Desc).
  223. %% @spec is(Got, Expected, Desc) -> Result
  224. %% Got = any()
  225. %% Expected = any()
  226. %% Desc = string()
  227. %% Result = true | false
  228. %% @doc Assert that two values are the same.
  229. is(Got, Expected, Desc) -> evaluate(Got == Expected, Got, Expected, Desc).
  230. %% @spec isnt(Got, Expected, Desc) -> Result
  231. %% Got = any()
  232. %% Expected = any()
  233. %% Desc = string()
  234. %% Result = true | false
  235. %% @doc Assert that two values are not the same.
  236. isnt(Got, Expected, Desc) -> evaluate(Got /= Expected, Got, Expected, Desc).
  237. %% @spec is_greater(ValueA, ValueB, Desc) -> Result
  238. %% ValueA = number()
  239. %% ValueB = number()
  240. %% Desc = string()
  241. %% Result = true | false
  242. %% @doc Assert that an integer is greater than another.
  243. is_greater(ValueA, ValueB, Desc) when is_integer(ValueA), is_integer(ValueB) ->
  244. mk_tap(ValueA > ValueB, Desc).
  245. %% @spec any(Got, Items, Desc) -> Result
  246. %% Got = any()
  247. %% Items = [any()]
  248. %% Desc = string()
  249. %% Result = true | false
  250. %% @doc Assert that an item is in a list.
  251. any(Got, Items, Desc) when is_function(Got) ->
  252. is(lists:any(Got, Items), true, Desc);
  253. any(Got, Items, Desc) ->
  254. is(lists:member(Got, Items), true, Desc).
  255. %% @spec none(Got, Items, Desc) -> Result
  256. %% Got = any()
  257. %% Items = [any()]
  258. %% Desc = string()
  259. %% Result = true | false
  260. %% @doc Assert that an item is not in a list.
  261. none(Got, Items, Desc) when is_function(Got) ->
  262. is(lists:any(Got, Items), false, Desc);
  263. none(Got, Items, Desc) ->
  264. is(lists:member(Got, Items), false, Desc).
  265. %% @spec fun_is(Fun, Expected, Desc) -> Result
  266. %% Fun = function()
  267. %% Expected = any()
  268. %% Desc = string()
  269. %% Result = true | false
  270. %% @doc Use an anonymous function to assert a pattern match.
  271. fun_is(Fun, Expected, Desc) when is_function(Fun) ->
  272. is(Fun(Expected), true, Desc).
  273. %% @spec expect_fun(ExpectFun, Got, Desc) -> Result
  274. %% ExpectFun = function()
  275. %% Got = any()
  276. %% Desc = string()
  277. %% Result = true | false
  278. %% @doc Use an anonymous function to assert a pattern match, using actual
  279. %% value as the argument to the function.
  280. expect_fun(ExpectFun, Got, Desc) ->
  281. evaluate(ExpectFun(Got), Got, ExpectFun, Desc).
  282. %% @spec expect_fun(ExpectFun, Got, Desc, ExpectStr) -> Result
  283. %% ExpectFun = function()
  284. %% Got = any()
  285. %% Desc = string()
  286. %% ExpectStr = string()
  287. %% Result = true | false
  288. %% @doc Use an anonymous function to assert a pattern match, using actual
  289. %% value as the argument to the function.
  290. expect_fun(ExpectFun, Got, Desc, ExpectStr) ->
  291. evaluate(ExpectFun(Got), Got, ExpectStr, Desc).
  292. %% @equiv skip(TestFun, "")
  293. skip(TestFun) when is_function(TestFun) ->
  294. skip(TestFun, "").
  295. %% @spec skip(TestFun, Reason) -> ok
  296. %% TestFun = function()
  297. %% Reason = string()
  298. %% @doc Skip a test.
  299. skip(TestFun, Reason) when is_function(TestFun), is_list(Reason) ->
  300. begin_skip(Reason),
  301. catch TestFun(),
  302. end_skip(),
  303. ok.
  304. %% @spec skip(Q, TestFun, Reason) -> ok
  305. %% Q = true | false | function()
  306. %% TestFun = function()
  307. %% Reason = string()
  308. %% @doc Skips a test conditionally. The first argument to this function can
  309. %% either be the 'true' or 'false' atoms or a function that returns 'true' or
  310. %% 'false'.
  311. skip(QFun, TestFun, Reason) when is_function(QFun), is_function(TestFun), is_list(Reason) ->
  312. case QFun() of
  313. true -> begin_skip(Reason), TestFun(), end_skip();
  314. _ -> TestFun()
  315. end,
  316. ok;
  317. skip(Q, TestFun, Reason) when is_function(TestFun), is_list(Reason), Q == true ->
  318. begin_skip(Reason),
  319. TestFun(),
  320. end_skip(),
  321. ok;
  322. skip(_, TestFun, Reason) when is_function(TestFun), is_list(Reason) ->
  323. TestFun(),
  324. ok.
  325. %% @private
  326. begin_skip(Reason) ->
  327. etap_server ! {self(), begin_skip, Reason}.
  328. %% @private
  329. end_skip() ->
  330. etap_server ! {self(), end_skip}.
  331. %% @spec contains_ok(string(), string(), string()) -> true | false
  332. %% @doc Assert that a string is contained in another string.
  333. contains_ok(Source, String, Desc) ->
  334. etap:isnt(
  335. string:str(Source, String),
  336. 0,
  337. Desc
  338. ).
  339. %% @spec is_before(string(), string(), string(), string()) -> true | false
  340. %% @doc Assert that a string comes before another string within a larger body.
  341. is_before(Source, StringA, StringB, Desc) ->
  342. etap:is_greater(
  343. string:str(Source, StringB),
  344. string:str(Source, StringA),
  345. Desc
  346. ).
  347. %% @doc Assert that a given variable is a pid.
  348. is_pid(Pid, Desc) when is_pid(Pid) -> etap:ok(true, Desc);
  349. is_pid(_, Desc) -> etap:ok(false, Desc).
  350. %% @doc Assert that a given process/pid is alive.
  351. is_alive(Pid, Desc) ->
  352. etap:ok(erlang:is_process_alive(Pid), Desc).
  353. %% @doc Assert that the current function of a pid is a given {M, F, A} tuple.
  354. is_mfa(Pid, MFA, Desc) ->
  355. etap:is({current_function, MFA}, erlang:process_info(Pid, current_function), Desc).
  356. %% @spec loaded_ok(atom(), string()) -> true | false
  357. %% @doc Assert that a module has been loaded successfully.
  358. loaded_ok(M, Desc) when is_atom(M) ->
  359. etap:fun_is(fun({module, _}) -> true; (_) -> false end, code:load_file(M), Desc).
  360. %% @spec can_ok(atom(), atom()) -> true | false
  361. %% @doc Assert that a module exports a given function.
  362. can_ok(M, F) when is_atom(M), is_atom(F) ->
  363. Matches = [X || {X, _} <- M:module_info(exports), X == F],
  364. etap:ok(Matches > 0, lists:concat([M, " can ", F])).
  365. %% @spec can_ok(atom(), atom(), integer()) -> true | false
  366. %% @doc Assert that a module exports a given function with a given arity.
  367. can_ok(M, F, A) when is_atom(M); is_atom(F), is_number(A) ->
  368. Matches = [X || X <- M:module_info(exports), X == {F, A}],
  369. etap:ok(Matches > 0, lists:concat([M, " can ", F, "/", A])).
  370. %% @spec has_attrib(M, A) -> true | false
  371. %% M = atom()
  372. %% A = atom()
  373. %% @doc Asserts that a module has a given attribute.
  374. has_attrib(M, A) when is_atom(M), is_atom(A) ->
  375. etap:isnt(
  376. proplists:get_value(A, M:module_info(attributes), 'asdlkjasdlkads'),
  377. 'asdlkjasdlkads',
  378. lists:concat([M, " has attribute ", A])
  379. ).
  380. %% @spec has_attrib(M, A. V) -> true | false
  381. %% M = atom()
  382. %% A = atom()
  383. %% V = any()
  384. %% @doc Asserts that a module has a given attribute with a given value.
  385. is_attrib(M, A, V) when is_atom(M) andalso is_atom(A) ->
  386. etap:is(
  387. proplists:get_value(A, M:module_info(attributes)),
  388. [V],
  389. lists:concat([M, "'s ", A, " is ", V])
  390. ).
  391. %% @spec is_behavior(M, B) -> true | false
  392. %% M = atom()
  393. %% B = atom()
  394. %% @doc Asserts that a given module has a specific behavior.
  395. is_behaviour(M, B) when is_atom(M) andalso is_atom(B) ->
  396. is_attrib(M, behaviour, B).
  397. %% @doc Assert that an exception is raised when running a given function.
  398. dies_ok(F, Desc) ->
  399. case (catch F()) of
  400. {'EXIT', _} -> etap:ok(true, Desc);
  401. _ -> etap:ok(false, Desc)
  402. end.
  403. %% @doc Assert that an exception is not raised when running a given function.
  404. lives_ok(F, Desc) ->
  405. etap:is(try_this(F), success, Desc).
  406. %% @doc Assert that the exception thrown by a function matches the given exception.
  407. throws_ok(F, Exception, Desc) ->
  408. try F() of
  409. _ -> etap:ok(nok, Desc)
  410. catch
  411. _:E ->
  412. etap:is(E, Exception, Desc)
  413. end.
  414. %% @private
  415. %% @doc Run a function and catch any exceptions.
  416. try_this(F) when is_function(F, 0) ->
  417. try F() of
  418. _ -> success
  419. catch
  420. throw:E -> {throw, E};
  421. error:E -> {error, E};
  422. exit:E -> {exit, E}
  423. end.
  424. %% @private
  425. %% @doc Start the etap_server process if it is not running already.
  426. ensure_test_server() ->
  427. case whereis(etap_server) of
  428. undefined ->
  429. proc_lib:start(?MODULE, start_etap_server,[]);
  430. _ ->
  431. diag("The test server is already running.")
  432. end.
  433. %% @private
  434. %% @doc Start the etap_server loop and register itself as the etap_server
  435. %% process.
  436. start_etap_server() ->
  437. catch register(etap_server, self()),
  438. proc_lib:init_ack(ok),
  439. etap:test_server(#test_state{
  440. planned = 0,
  441. count = 0,
  442. pass = 0,
  443. fail = 0,
  444. skip = 0,
  445. skip_reason = ""
  446. }).
  447. %% @private
  448. %% @doc The main etap_server receive/run loop. The etap_server receive loop
  449. %% responds to seven messages apperatining to failure or passing of tests.
  450. %% It is also used to initiate the testing process with the {_, plan, _}
  451. %% message that clears the current test state.
  452. test_server(State) ->
  453. NewState = receive
  454. {_From, plan, unknown} ->
  455. io:format("# Current time local ~s~n", [datetime(erlang:localtime())]),
  456. io:format("# Using etap version ~p~n", [ proplists:get_value(vsn, proplists:get_value(attributes, etap:module_info())) ]),
  457. State#test_state{
  458. planned = -1,
  459. count = 0,
  460. pass = 0,
  461. fail = 0,
  462. skip = 0,
  463. skip_reason = ""
  464. };
  465. {_From, plan, N} ->
  466. io:format("# Current time local ~s~n", [datetime(erlang:localtime())]),
  467. io:format("# Using etap version ~p~n", [ proplists:get_value(vsn, proplists:get_value(attributes, etap:module_info())) ]),
  468. io:format("1..~p~n", [N]),
  469. State#test_state{
  470. planned = N,
  471. count = 0,
  472. pass = 0,
  473. fail = 0,
  474. skip = 0,
  475. skip_reason = ""
  476. };
  477. {_From, begin_skip, Reason} ->
  478. State#test_state{
  479. skip = 1,
  480. skip_reason = Reason
  481. };
  482. {_From, end_skip} ->
  483. State#test_state{
  484. skip = 0,
  485. skip_reason = ""
  486. };
  487. {_From, pass, Desc} ->
  488. FullMessage = skip_diag(
  489. " - " ++ Desc,
  490. State#test_state.skip,
  491. State#test_state.skip_reason
  492. ),
  493. io:format("ok ~p ~s~n", [State#test_state.count + 1, FullMessage]),
  494. State#test_state{
  495. count = State#test_state.count + 1,
  496. pass = State#test_state.pass + 1
  497. };
  498. {_From, fail, Desc} ->
  499. FullMessage = skip_diag(
  500. " - " ++ Desc,
  501. State#test_state.skip,
  502. State#test_state.skip_reason
  503. ),
  504. io:format("not ok ~p ~s~n", [State#test_state.count + 1, FullMessage]),
  505. State#test_state{
  506. count = State#test_state.count + 1,
  507. fail = State#test_state.fail + 1
  508. };
  509. {From, state} ->
  510. From ! State,
  511. State;
  512. {_From, diag, Message} ->
  513. io:format("~s~n", [Message]),
  514. State;
  515. {From, count} ->
  516. From ! State#test_state.count,
  517. State;
  518. {From, is_skip} ->
  519. From ! State#test_state.skip,
  520. State;
  521. done ->
  522. exit(normal)
  523. end,
  524. test_server(NewState).
  525. %% @private
  526. %% @doc Process the result of a test and send it to the etap_server process.
  527. mk_tap(Result, Desc) ->
  528. IsSkip = lib:sendw(etap_server, is_skip),
  529. case [IsSkip, Result] of
  530. [_, true] ->
  531. etap_server ! {self(), pass, Desc},
  532. true;
  533. [1, _] ->
  534. etap_server ! {self(), pass, Desc},
  535. true;
  536. _ ->
  537. etap_server ! {self(), fail, Desc},
  538. false
  539. end.
  540. %% @private
  541. %% @doc Format a date/time string.
  542. datetime(DateTime) ->
  543. {{Year, Month, Day}, {Hour, Min, Sec}} = DateTime,
  544. 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]).
  545. %% @private
  546. %% @doc Craft an output message taking skip/todo into consideration.
  547. skip_diag(Message, 0, _) ->
  548. Message;
  549. skip_diag(_Message, 1, "") ->
  550. " # SKIP";
  551. skip_diag(_Message, 1, Reason) ->
  552. " # SKIP : " ++ Reason.