您最多选择25个主题 主题必须以字母或数字开头,可以包含连字符 (-),并且长度不得超过35个字符

612 行
19 KiB

  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.