erlydtl_functional_tests.erl 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341
  1. %%%-------------------------------------------------------------------
  2. %%% File: erlydtl_tests.erl
  3. %%% @author Roberto Saccon <rsaccon@gmail.com> [http://rsaccon.com]
  4. %%% @author Evan Miller <emmiller@gmail.com>
  5. %%% @copyright 2008 Roberto Saccon, Evan Miller
  6. %%% @doc ErlyDTL test suite
  7. %%% @end
  8. %%%
  9. %%% The MIT License
  10. %%%
  11. %%% Copyright (c) 2007 Roberto Saccon
  12. %%%
  13. %%% Permission is hereby granted, free of charge, to any person obtaining a copy
  14. %%% of this software and associated documentation files (the "Software"), to deal
  15. %%% in the Software without restriction, including without limitation the rights
  16. %%% to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
  17. %%% copies of the Software, and to permit persons to whom the Software is
  18. %%% furnished to do so, subject to the following conditions:
  19. %%%
  20. %%% The above copyright notice and this permission notice shall be included in
  21. %%% all copies or substantial portions of the Software.
  22. %%%
  23. %%% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
  24. %%% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
  25. %%% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
  26. %%% AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
  27. %%% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
  28. %%% OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
  29. %%% THE SOFTWARE.
  30. %%%
  31. %%% @since 2008-02-11 by Roberto Saccon
  32. %%%-------------------------------------------------------------------
  33. -module(erlydtl_functional_tests).
  34. -author('rsaccon@gmail.com').
  35. -author('emmiller@gmail.com').
  36. %% API
  37. -export([run_tests/0, run_test/1]).
  38. test_list() ->
  39. %% order is important.
  40. ["autoescape", "comment", "extends", "filters", "for", "for_list",
  41. "for_tuple", "for_list_preset", "for_preset", "for_records",
  42. "for_records_preset", "include", "if", "if_preset", "ifequal",
  43. "ifequal_preset", "ifnotequal", "ifnotequal_preset", "now",
  44. "var", "var_preset", "cycle", "custom_tag", "custom_tag1",
  45. "custom_tag2", "custom_tag3", "custom_tag4", "custom_call",
  46. "include_template", "include_path", "ssi", "extends_path",
  47. "extends_path2", "trans", "extends2", "extends3",
  48. "recursive_block", "extend_recursive_block"
  49. ].
  50. setup_compile("for_list_preset") ->
  51. CompileVars = [{fruit_list, [["apple", "apples"], ["banana", "bananas"], ["coconut", "coconuts"]]}],
  52. {ok, CompileVars};
  53. setup_compile("for_preset") ->
  54. CompileVars = [{fruit_list, ["preset-apple", "preset-banana", "preset-coconut"]}],
  55. {ok, CompileVars};
  56. setup_compile("for_records_preset") ->
  57. Link1a = [{name, "Amazon (preset)"}, {url, "http://amazon.com"}],
  58. Link2a = [{name, "Google (preset)"}, {url, "http://google.com"}],
  59. Link3a = [{name, "Microsoft (preset)"}, {url, "http://microsoft.com"}],
  60. CompileVars = [{software_links, [Link1a, Link2a, Link3a]}],
  61. {ok, CompileVars};
  62. setup_compile("if_preset") ->
  63. CompileVars = [{var1, "something"}],
  64. {ok, CompileVars};
  65. setup_compile("ifequal_preset") ->
  66. CompileVars = [{var1, "foo"}, {var2, "foo"}],
  67. {ok, CompileVars};
  68. setup_compile("ifnotequal_preset") ->
  69. CompileVars = [{var1, "foo"}, {var2, "foo"}],
  70. {ok, CompileVars};
  71. setup_compile("var_preset") ->
  72. CompileVars = [{preset_var1, "preset-var1"}, {preset_var2, "preset-var2"}],
  73. {ok, CompileVars};
  74. setup_compile("extends2") ->
  75. File = templates_dir("input/extends2"),
  76. Error = {none, erlydtl_compiler, unexpected_extends_tag},
  77. {{error, [{File, [Error]}], []}, []};
  78. setup_compile("extends3") ->
  79. File = templates_dir("input/extends3"),
  80. Include = templates_dir("input/imaginary"),
  81. Error = {none, erlydtl_compiler, {read_file, Include, enoent}},
  82. {{error, [{File, [Error]}], []}, []};
  83. setup_compile(_) ->
  84. {ok, []}.
  85. %% @spec (Name::string()) -> {CompileStatus::atom(), PresetVars::list(),
  86. %% RenderStatus::atom(), RenderVars::list()} | skip
  87. %% @doc
  88. %% @end
  89. %%--------------------------------------------------------------------
  90. setup("autoescape") ->
  91. RenderVars = [{var1, "<b>bold</b>"}],
  92. {ok, RenderVars};
  93. setup("extends") ->
  94. RenderVars = [{base_var, "base-barstring"}, {test_var, "test-barstring"}],
  95. {ok, RenderVars};
  96. setup("filters") ->
  97. RenderVars = [
  98. {date_var1, {1975,7,24}},
  99. {datetime_var1, {{1975,7,24}, {7,13,1}}},
  100. {'list', ["eins", "zwei", "drei"]}
  101. ],
  102. {ok, RenderVars};
  103. setup("for") ->
  104. RenderVars = [{fruit_list, ["apple", "banana", "coconut"]}],
  105. {ok, RenderVars};
  106. setup("for_list") ->
  107. RenderVars = [{fruit_list, [["apple", "apples", "$1"], ["banana", "bananas", "$2"], ["coconut", "coconuts", "$500"]]}],
  108. {ok, RenderVars};
  109. setup("for_tuple") ->
  110. RenderVars = [{fruit_list, [{"apple", "apples"}, {"banana", "bananas"}, {"coconut", "coconuts"}]}],
  111. {ok, RenderVars};
  112. setup("for_records") ->
  113. Link1 = [{name, "Amazon"}, {url, "http://amazon.com"}],
  114. Link2 = [{name, "Google"}, {url, "http://google.com"}],
  115. Link3 = [{name, "Microsoft"}, {url, "http://microsoft.com"}],
  116. RenderVars = [{link_list, [Link1, Link2, Link3]}],
  117. {ok, RenderVars};
  118. setup("for_records_preset") ->
  119. Link1b = [{name, "Canon"}, {url, "http://canon.com"}],
  120. Link2b = [{name, "Leica"}, {url, "http://leica.com"}],
  121. Link3b = [{name, "Nikon"}, {url, "http://nikon.com"}],
  122. RenderVars = [{photo_links, [Link1b, Link2b, Link3b]}],
  123. {ok, RenderVars};
  124. setup("include") ->
  125. RenderVars = [{var1, "foostring1"}, {var2, "foostring2"}],
  126. {ok, RenderVars};
  127. setup("if") ->
  128. RenderVars = [{var1, "something"}],
  129. {ok, RenderVars};
  130. setup("ifequal") ->
  131. RenderVars = [{var1, "foo"}, {var2, "foo"}, {var3, "bar"}],
  132. {ok, RenderVars};
  133. setup("ifequal_preset") ->
  134. RenderVars = [{var3, "bar"}],
  135. {ok, RenderVars};
  136. setup("ifnotequal") ->
  137. RenderVars = [{var1, "foo"}, {var2, "foo"}, {var3, "bar"}],
  138. {ok, RenderVars};
  139. setup("now") ->
  140. {ok, [], [], skip_check};
  141. setup("var") ->
  142. RenderVars = [{var1, "foostring1"}, {var2, "foostring2"}, {var_not_used, "foostring3"}],
  143. {ok, RenderVars};
  144. setup("var_preset") ->
  145. RenderVars = [{var1, "foostring1"}, {var2, "foostring2"}],
  146. {ok, RenderVars};
  147. setup("cycle") ->
  148. RenderVars = [{test, [integer_to_list(X) || X <- lists:seq(1, 20)]},
  149. {a, "Apple"}, {b, "Banana"}, {c, "Cherry"}],
  150. {ok, RenderVars};
  151. setup("include_template") ->
  152. RenderVars = [{base_var, "base-barstring"}, {test_var, "test-barstring"}],
  153. {ok, RenderVars};
  154. setup("include_path") ->
  155. RenderVars = [{base_var, "base-barstring"}, {test_var, "test-barstring"}],
  156. {ok, RenderVars};
  157. setup("extends_path") ->
  158. RenderVars = [{base_var, "base-barstring"}, {test_var, "test-barstring"}],
  159. {ok, RenderVars};
  160. setup("extends_path2") ->
  161. RenderVars = [{base_var, "base-barstring"}, {test_var, "test-barstring"}],
  162. {ok, RenderVars};
  163. setup("trans") ->
  164. RenderVars = [{locale, "reverse"}],
  165. {ok, RenderVars};
  166. setup("locale") ->
  167. {ok, _RenderVars = [{locale, "ru"}]};
  168. setup("custom_tag1") ->
  169. {ok, [{a, <<"a1">>}], [{locale, ru}], <<"b1\n">>};
  170. setup("custom_tag2") ->
  171. {ok, [{a, <<"a1">>}], [{locale, ru}, {foo, bar}], <<"b2\n">>};
  172. setup("custom_tag3") ->
  173. {ok, [{a, <<"a1">>}], [{locale, ru}], <<"b3\n">>};
  174. setup("custom_tag4") ->
  175. {ok, [], [], <<"a\n">>};
  176. setup("ssi") ->
  177. RenderVars = [{path, filename:absname(filename:join(["tests", "input", "ssi_include.html"]))}],
  178. {ok, RenderVars};
  179. %%--------------------------------------------------------------------
  180. %% Custom tags
  181. %%--------------------------------------------------------------------
  182. setup("custom_call") ->
  183. RenderVars = [{var1, "something"}],
  184. {ok, RenderVars};
  185. setup(_) ->
  186. {ok, []}.
  187. run_tests() ->
  188. io:format("Running functional tests...~n"),
  189. file:set_cwd(erlydtl_deps:get_base_dir()),
  190. case [filelib:ensure_dir(
  191. filename:join([templates_dir(Dir), "foo"]))
  192. || Dir <- ["output", "expect"]] -- [ok,ok]
  193. of
  194. [] ->
  195. case fold_tests() of
  196. {N, []}->
  197. Msg = lists:concat(["All ", N, " functional tests passed~n~n"]),
  198. io:format(Msg),
  199. {ok, Msg};
  200. {N, Errs} ->
  201. io:format(
  202. "~b / ~b functional tests failed.~nErrors: ~n",
  203. [length(Errs), N]),
  204. [io:format(" ~s [~s] ~s~n", [Name, Error, Reason])
  205. || {Name, Error, Reason} <- Errs],
  206. throw(failed)
  207. end;
  208. Err ->
  209. [io:format("Ensure dir failed: ~p~n~n", [Reason]) || {error, Reason} <- Err],
  210. throw(failed)
  211. end.
  212. run_test(Name) ->
  213. test_compile_render(Name).
  214. %%====================================================================
  215. %% Internal functions
  216. %%====================================================================
  217. fold_tests() ->
  218. lists:foldl(fun(Name, {AccCount, AccErrs}) ->
  219. Res = case catch test_compile_render(Name) of
  220. ok -> {AccCount + 1, AccErrs};
  221. {'EXIT', Reason} ->
  222. {AccCount + 1, [{Name, crash,
  223. io_lib:format("~p", [Reason])}
  224. | AccErrs]};
  225. {Error, Reason} ->
  226. {AccCount + 1, [{Name, Error, Reason}
  227. | AccErrs]}
  228. end,
  229. io:format("~n"), Res
  230. end, {0, []}, test_list()).
  231. test_compile_render(Name) ->
  232. File = filename:join([templates_docroot(), Name]),
  233. Module = "functional_test_" ++ Name,
  234. io:format(" Template: ~p, ... ", [Name]),
  235. case setup_compile(Name) of
  236. {CompileStatus, CompileVars} ->
  237. Options = [
  238. {vars, CompileVars},
  239. force_recompile,
  240. return_errors,
  241. return_warnings,
  242. %% debug_compiler,
  243. {custom_tags_modules, [erlydtl_custom_tags]}],
  244. io:format("compiling ... "),
  245. case erlydtl:compile(File, Module, Options) of
  246. {ok, Mod, [{File, [{none,erlydtl_compiler,no_out_dir}]}]} ->
  247. if CompileStatus =:= ok -> test_render(Name, Mod);
  248. true ->
  249. io:format("missing error"),
  250. {error, "compiling should have failed :" ++ File}
  251. end;
  252. {error, _, _}=CompileStatus ->
  253. io:format("ok");
  254. Err ->
  255. io:format("failed"),
  256. {compile_error, io_lib:format("Actual: ~p, Expected: ~p", [Err, CompileStatus])}
  257. end;
  258. skip -> io:format("skipped")
  259. end.
  260. test_render(Name, Module) ->
  261. File = filename:join([templates_docroot(), Name]),
  262. {RenderStatus, Vars, Opts, RenderResult} =
  263. case setup(Name) of
  264. {RS, V} -> {RS, V, [], get_expected_result(Name)};
  265. {RS, V, O} -> {RS, V, O, get_expected_result(Name)};
  266. {RS, V, O, R} -> {RS, V, O, R}
  267. end,
  268. io:format("rendering ... "),
  269. case catch Module:render(Vars, Opts) of
  270. {ok, Output} ->
  271. Data = iolist_to_binary(Output),
  272. if RenderStatus =:= ok ->
  273. if RenderResult =:= undefined ->
  274. [with_template_filename(
  275. Dir, Name,
  276. fun(F) -> file:write_file(F, Data) end)
  277. || Dir <- ["output", "expect"]],
  278. io:format("~n #### NOTE: created new expected output file: \"tests/expect/~s\"."
  279. "~n Please verify contents.", [Name]);
  280. RenderResult =:= Data ->
  281. io:format("ok");
  282. RenderResult =:= skip_check ->
  283. io:format("ok (not checked for regression)");
  284. true ->
  285. io:format("failed"),
  286. with_template_filename(
  287. "output", Name,
  288. fun(F) -> file:write_file(F, Data) end),
  289. {error, io_lib:format(
  290. "Expected output does not match rendered output~n"
  291. "==Expected==~n~p~n--Actual--~n~p~n==End==~n",
  292. [RenderResult, Data])}
  293. end;
  294. true ->
  295. io:format("missing error"),
  296. {missing_error, "rendering should have failed :" ++ File}
  297. end;
  298. {'EXIT', Reason} ->
  299. io:format("failed"),
  300. {render_error, io_lib:format("failed invoking render method of ~p ~p", [Module, Reason])};
  301. Err ->
  302. if RenderStatus =:= error -> io:format("ok");
  303. true -> io:format("failed"),
  304. {render_error, io_lib:format("~p", [Err])}
  305. end
  306. end.
  307. get_expected_result(Name) ->
  308. with_template_filename(
  309. "expect", Name,
  310. fun(F) ->
  311. case filelib:is_regular(F) of
  312. true -> {ok, Data} = file:read_file(F), Data;
  313. false -> undefined
  314. end
  315. end).
  316. with_template_filename(Dir, Name, Fun) ->
  317. FileName = filename:join([templates_dir(Dir), Name]),
  318. Fun(FileName).
  319. templates_docroot() -> templates_dir("input").
  320. templates_dir(Name) -> filename:join(["tests", Name]).