652 lines
15 KiB
Elixir
652 lines
15 KiB
Elixir
Code.require_file("test_helper.exs", __DIR__)
|
|
|
|
require EEx
|
|
|
|
defmodule EExTest.Compiled do
|
|
def before_compile do
|
|
{__ENV__.line, hd(tl(get_stacktrace()))}
|
|
end
|
|
|
|
EEx.function_from_string(:def, :string_sample, "<%= a + b %>", [:a, :b])
|
|
|
|
filename = Path.join(__DIR__, "fixtures/eex_template_with_bindings.eex")
|
|
EEx.function_from_file(:defp, :private_file_sample, filename, [:bar])
|
|
|
|
filename = Path.join(__DIR__, "fixtures/eex_template_with_bindings.eex")
|
|
EEx.function_from_file(:def, :public_file_sample, filename, [:bar])
|
|
|
|
def file_sample(arg), do: private_file_sample(arg)
|
|
|
|
def after_compile do
|
|
{__ENV__.line, hd(tl(get_stacktrace()))}
|
|
end
|
|
|
|
@file "unknown"
|
|
def unknown do
|
|
{__ENV__.line, hd(tl(get_stacktrace()))}
|
|
end
|
|
|
|
defp get_stacktrace do
|
|
try do
|
|
:erlang.error("failed")
|
|
rescue
|
|
_ -> __STACKTRACE__
|
|
end
|
|
end
|
|
end
|
|
|
|
defmodule Clause do
|
|
defmacro defclause(expr, block) do
|
|
quote do
|
|
def unquote(expr), unquote(block)
|
|
end
|
|
end
|
|
end
|
|
|
|
defmodule EExTest do
|
|
use ExUnit.Case, async: true
|
|
|
|
doctest EEx
|
|
doctest EEx.Engine
|
|
doctest EEx.SmartEngine
|
|
|
|
describe "evaluates" do
|
|
test "simple string" do
|
|
assert_eval("foo bar", "foo bar")
|
|
end
|
|
|
|
test "Unicode" do
|
|
template = """
|
|
• <%= "•" %> •
|
|
<%= "Jößé Vâlìm" %> Jößé Vâlìm
|
|
"""
|
|
|
|
assert_eval(" • • •\n Jößé Vâlìm Jößé Vâlìm\n", template)
|
|
end
|
|
|
|
test "trim mode" do
|
|
string = "<%= 123 %> \n456\n <%= 789 %>"
|
|
expected = "123456\n789"
|
|
assert_eval(expected, string, [], trim: true)
|
|
end
|
|
|
|
test "trim mode with middle expression" do
|
|
string = """
|
|
<%= cond do %>
|
|
<% false -> %>
|
|
this
|
|
<% true -> %>
|
|
that
|
|
<% end %>
|
|
"""
|
|
|
|
expected = " that\n"
|
|
assert_eval(expected, string, [], trim: true)
|
|
end
|
|
|
|
test "trim mode with multiple lines" do
|
|
string = """
|
|
<%= "First line" %>
|
|
<%= "Second line" %>
|
|
<%= "Third line" %>
|
|
<%= "Fourth line" %>
|
|
"""
|
|
|
|
expected = "First lineSecond lineThird lineFourth line"
|
|
assert_eval(expected, string, [], trim: true)
|
|
end
|
|
|
|
test "embedded code" do
|
|
assert_eval("foo bar", "foo <%= :bar %>")
|
|
end
|
|
|
|
test "embedded code with binding" do
|
|
assert EEx.eval_string("foo <%= bar %>", bar: 1) == "foo 1"
|
|
end
|
|
|
|
test "embedded code with do end when true" do
|
|
assert_eval("foo bar", "foo <%= if true do %>bar<% end %>")
|
|
end
|
|
|
|
test "embedded code with do end when false" do
|
|
assert_eval("foo ", "foo <%= if false do %>bar<% end %>")
|
|
end
|
|
|
|
test "embedded code with do preceeded by bracket" do
|
|
assert_eval("foo bar", "foo <%= if {true}do %>bar<% end %>")
|
|
assert_eval("foo bar", "foo <%= if (true)do %>bar<% end %>")
|
|
assert_eval("foo bar", "foo <%= if [true]do %>bar<% end %>")
|
|
end
|
|
|
|
test "embedded code with do end and expression" do
|
|
assert_eval("foo bar", "foo <%= if true do %><%= :bar %><% end %>")
|
|
end
|
|
|
|
test "embedded code with do end and multiple expressions" do
|
|
assert_eval(
|
|
"foo bar baz",
|
|
"foo <%= if true do %>bar <% Process.put(:eex_text, 1) %><%= :baz %><% end %>"
|
|
)
|
|
|
|
assert Process.get(:eex_text) == 1
|
|
end
|
|
|
|
test "embedded code with middle expression" do
|
|
assert_eval("foo bar", "foo <%= if true do %>bar<% else %>baz<% end %>")
|
|
end
|
|
|
|
test "embedded code with evaluated middle expression" do
|
|
assert_eval("foo baz", "foo <%= if false do %>bar<% else %>baz<% end %>")
|
|
end
|
|
|
|
test "embedded code with nested do end" do
|
|
assert_eval("foo bar", "foo <%= if true do %><%= if true do %>bar<% end %><% end %>")
|
|
end
|
|
|
|
test "embedded code with nested do end with middle expression" do
|
|
assert_eval(
|
|
"foo baz",
|
|
"foo <%= if true do %><%= if false do %>bar<% else %>baz<% end %><% end %>"
|
|
)
|
|
end
|
|
|
|
test "embedded code with end followed by bracket" do
|
|
assert_eval(
|
|
" 101 102 103 ",
|
|
"<%= Enum.map([1, 2, 3], fn x -> %> <%= 100 + x %> <% end) %>"
|
|
)
|
|
|
|
assert_eval(
|
|
" 101 102 103 ",
|
|
"<%= apply Enum, :map, [[1, 2, 3], fn x -> %> <%= 100 + x %> <% end] %>"
|
|
)
|
|
|
|
assert_eval(
|
|
" 101 102 103 ",
|
|
"<%= #{__MODULE__}.tuple_map {[1, 2, 3], fn x -> %> <%= 100 + x %> <% end} %>"
|
|
)
|
|
|
|
assert_eval(
|
|
" 101 102 103 ",
|
|
"<%= apply(Enum, :map, [[1, 2, 3], fn x -> %> <%= 100 + x %> <% end]) %>"
|
|
)
|
|
|
|
assert_eval(
|
|
" 101 102 103 ",
|
|
"<%= Enum.map([1, 2, 3], (fn x -> %> <%= 100 + x %> <% end) ) %>"
|
|
)
|
|
end
|
|
|
|
test "embedded code with variable definition" do
|
|
assert_eval("foo 1", "foo <% bar = 1 %><%= bar %>")
|
|
end
|
|
|
|
test "embedded code with require" do
|
|
assert_eval("foo 1,2,3", "foo <% require Enum, as: E %><%= E.join [1, 2, 3], \",\" %>")
|
|
end
|
|
|
|
test "with end of token" do
|
|
assert_eval("foo bar %>", "foo bar %>")
|
|
end
|
|
end
|
|
|
|
describe "raises syntax errors" do
|
|
test "when the token is invalid" do
|
|
assert_raise EEx.SyntaxError, "nofile:1: missing token '%>'", fn ->
|
|
EEx.compile_string("foo <%= bar")
|
|
end
|
|
end
|
|
|
|
test "when middle expression is found without a start expression" do
|
|
assert_raise EEx.SyntaxError, "nofile:1: unexpected middle of expression <% else %>", fn ->
|
|
EEx.compile_string("<% if true %> foo<% else %>bar<% end %>")
|
|
end
|
|
end
|
|
|
|
test "when end expression is found without a start expression" do
|
|
assert_raise EEx.SyntaxError, "nofile:1: unexpected end of expression <% end %>", fn ->
|
|
EEx.compile_string("foo <% end %>")
|
|
end
|
|
end
|
|
|
|
test "when start expression is found without an end expression" do
|
|
msg = "nofile:2: unexpected end of string, expected a closing '<% end %>'"
|
|
|
|
assert_raise EEx.SyntaxError, msg, fn ->
|
|
EEx.compile_string("foo\n<% if true do %>")
|
|
end
|
|
end
|
|
|
|
test "when nested end expression is found without a start expression" do
|
|
assert_raise EEx.SyntaxError, "nofile:1: unexpected end of expression <% end %>", fn ->
|
|
EEx.compile_string("foo <% if true do %><% end %><% end %>")
|
|
end
|
|
end
|
|
|
|
test "when middle expression has a modifier" do
|
|
assert ExUnit.CaptureIO.capture_io(:stderr, fn ->
|
|
EEx.compile_string("foo <%= if true do %>true<%= else %>false<% end %>")
|
|
end) =~ ~s[unexpected beginning of EEx tag \"<%=\" on \"<%= else %>\"]
|
|
end
|
|
|
|
test "when end expression has a modifier" do
|
|
assert ExUnit.CaptureIO.capture_io(:stderr, fn ->
|
|
EEx.compile_string("foo <%= if true do %>true<% else %>false<%= end %>")
|
|
end) =~
|
|
~s[unexpected beginning of EEx tag \"<%=\" on end of expression \"<%= end %>\"]
|
|
end
|
|
|
|
test "when trying to use marker '/' without implementation" do
|
|
msg =
|
|
~r/unsupported EEx syntax <%\/ %> \(the syntax is valid but not supported by the current EEx engine\)/
|
|
|
|
assert_raise EEx.SyntaxError, msg, fn ->
|
|
EEx.compile_string("<%/ true %>")
|
|
end
|
|
end
|
|
|
|
test "when trying to use marker '|' without implementation" do
|
|
msg =
|
|
~r/unsupported EEx syntax <%| %> \(the syntax is valid but not supported by the current EEx engine\)/
|
|
|
|
assert_raise EEx.SyntaxError, msg, fn ->
|
|
EEx.compile_string("<%| true %>")
|
|
end
|
|
end
|
|
end
|
|
|
|
describe "error messages" do
|
|
test "honor line numbers" do
|
|
assert_raise EEx.SyntaxError, "nofile:99: missing token '%>'", fn ->
|
|
EEx.compile_string("foo <%= bar", line: 99)
|
|
end
|
|
end
|
|
|
|
test "honor file names" do
|
|
assert_raise EEx.SyntaxError, "my_file.eex:1: missing token '%>'", fn ->
|
|
EEx.compile_string("foo <%= bar", file: "my_file.eex")
|
|
end
|
|
end
|
|
end
|
|
|
|
describe "environment" do
|
|
test "respects line numbers" do
|
|
expected = """
|
|
foo
|
|
2
|
|
"""
|
|
|
|
string = """
|
|
foo
|
|
<%= __ENV__.line %>
|
|
"""
|
|
|
|
assert_eval(expected, string)
|
|
end
|
|
|
|
test "respects line numbers inside nested expressions" do
|
|
expected = """
|
|
foo
|
|
|
|
3
|
|
|
|
5
|
|
"""
|
|
|
|
string = """
|
|
foo
|
|
<%= if true do %>
|
|
<%= __ENV__.line %>
|
|
<% end %>
|
|
<%= __ENV__.line %>
|
|
"""
|
|
|
|
assert_eval(expected, string)
|
|
end
|
|
|
|
test "respects line numbers inside start expression" do
|
|
expected = """
|
|
foo
|
|
|
|
true
|
|
|
|
5
|
|
"""
|
|
|
|
string = """
|
|
foo
|
|
<%= if __ENV__.line == 2 do %>
|
|
<%= true %>
|
|
<% end %>
|
|
<%= __ENV__.line %>
|
|
"""
|
|
|
|
assert_eval(expected, string)
|
|
end
|
|
|
|
test "respects line numbers inside middle expression with ->" do
|
|
expected = """
|
|
foo
|
|
|
|
true
|
|
|
|
7
|
|
"""
|
|
|
|
string = """
|
|
foo
|
|
<%= cond do %>
|
|
<% false -> %> false
|
|
<% __ENV__.line == 4 -> %>
|
|
<%= true %>
|
|
<% end %>
|
|
<%= __ENV__.line %>
|
|
"""
|
|
|
|
assert_eval(expected, string)
|
|
end
|
|
|
|
test "respects line number inside middle expressions with keywords" do
|
|
expected = """
|
|
foo
|
|
|
|
5
|
|
|
|
7
|
|
"""
|
|
|
|
string = """
|
|
foo
|
|
<%= if false do %>
|
|
<%= __ENV__.line %>
|
|
<% else %>
|
|
<%= __ENV__.line %>
|
|
<% end %>
|
|
<%= __ENV__.line %>
|
|
"""
|
|
|
|
assert_eval(expected, string)
|
|
end
|
|
|
|
test "respects files" do
|
|
assert_eval("sample.ex", "<%= __ENV__.file %>", [], file: "sample.ex")
|
|
end
|
|
end
|
|
|
|
describe "clauses" do
|
|
test "inside functions" do
|
|
expected = """
|
|
|
|
Number 1
|
|
|
|
Number 2
|
|
|
|
Number 3
|
|
|
|
"""
|
|
|
|
string = """
|
|
<%= Enum.map [1, 2, 3], fn x -> %>
|
|
Number <%= x %>
|
|
<% end %>
|
|
"""
|
|
|
|
assert_eval(expected, string)
|
|
end
|
|
|
|
test "inside multiple functions" do
|
|
expected = """
|
|
|
|
A 1
|
|
|
|
B 2
|
|
|
|
A 3
|
|
|
|
"""
|
|
|
|
string = """
|
|
<%= #{__MODULE__}.switching_map [1, 2, 3], fn x -> %>
|
|
A <%= x %>
|
|
<% end, fn x -> %>
|
|
B <%= x %>
|
|
<% end %>
|
|
"""
|
|
|
|
assert_eval(expected, string)
|
|
end
|
|
|
|
test "inside callback and do block" do
|
|
expected = """
|
|
|
|
|
|
A 1
|
|
|
|
B 2
|
|
|
|
A 3
|
|
|
|
"""
|
|
|
|
string = """
|
|
<% require #{__MODULE__} %>
|
|
<%= #{__MODULE__}.switching_macro [1, 2, 3], fn x -> %>
|
|
A <%= x %>
|
|
<% end do %>
|
|
B <%= x %>
|
|
<% end %>
|
|
"""
|
|
|
|
assert_eval(expected, string)
|
|
end
|
|
|
|
test "inside cond" do
|
|
expected = """
|
|
foo
|
|
|
|
true
|
|
|
|
"""
|
|
|
|
string = """
|
|
foo
|
|
<%= cond do %>
|
|
<% false -> %> false
|
|
<% fn -> 1 end -> %>
|
|
<%= true %>
|
|
<% end %>
|
|
"""
|
|
|
|
assert_eval(expected, string)
|
|
end
|
|
|
|
test "inside cond with do end" do
|
|
string = """
|
|
<% y = ["a", "b", "c"] %>
|
|
<%= cond do %>
|
|
<% "a" in y -> %>
|
|
Good
|
|
<% true -> %>
|
|
<% if true do %>true<% else %>false<% end %>
|
|
Bad
|
|
<% end %>
|
|
"""
|
|
|
|
assert_eval("\n\n Good\n \n", string)
|
|
end
|
|
end
|
|
|
|
describe "buffers" do
|
|
test "unused buffers are kept out" do
|
|
string = """
|
|
<%= 123 %>
|
|
<% if true do %>
|
|
<%= 456 %>
|
|
<% end %>
|
|
<%= 789 %>
|
|
"""
|
|
|
|
assert_eval("123\n\n789\n", string)
|
|
end
|
|
|
|
test "inside comprehensions" do
|
|
string = """
|
|
<%= for _name <- packages || [] do %>
|
|
<% end %>
|
|
<%= all || :done %>
|
|
"""
|
|
|
|
assert_eval("\ndone\n", string, packages: nil, all: nil)
|
|
end
|
|
end
|
|
|
|
describe "from file" do
|
|
test "evaluates the source" do
|
|
filename = Path.join(__DIR__, "fixtures/eex_template.eex")
|
|
result = EEx.eval_file(filename)
|
|
assert_normalized_newline_equal("foo bar.\n", result)
|
|
end
|
|
|
|
test "evaluates the source with bindings" do
|
|
filename = Path.join(__DIR__, "fixtures/eex_template_with_bindings.eex")
|
|
result = EEx.eval_file(filename, bar: 1)
|
|
assert_normalized_newline_equal("foo 1\n", result)
|
|
end
|
|
|
|
test "raises an Exception when file is missing" do
|
|
msg = "could not read file \"non-existent.eex\": no such file or directory"
|
|
|
|
assert_raise File.Error, msg, fn ->
|
|
filename = "non-existent.eex"
|
|
EEx.compile_file(filename)
|
|
end
|
|
end
|
|
|
|
test "sets external resource attribute" do
|
|
assert EExTest.Compiled.__info__(:attributes)[:external_resource] ==
|
|
[Path.join(__DIR__, "fixtures/eex_template_with_bindings.eex")]
|
|
end
|
|
end
|
|
|
|
describe "precompiled" do
|
|
test "from string" do
|
|
assert EExTest.Compiled.string_sample(1, 2) == "3"
|
|
end
|
|
|
|
test "from file" do
|
|
assert_normalized_newline_equal("foo 1\n", EExTest.Compiled.file_sample(1))
|
|
assert_normalized_newline_equal("foo 1\n", EExTest.Compiled.public_file_sample(1))
|
|
end
|
|
|
|
test "from file does not affect backtrace" do
|
|
file = to_charlist(Path.relative_to_cwd(__ENV__.file))
|
|
|
|
assert EExTest.Compiled.before_compile() ==
|
|
{7, {EExTest.Compiled, :before_compile, 0, [file: file, line: 7]}}
|
|
|
|
assert EExTest.Compiled.after_compile() ==
|
|
{21, {EExTest.Compiled, :after_compile, 0, [file: file, line: 21]}}
|
|
|
|
assert EExTest.Compiled.unknown() ==
|
|
{26, {EExTest.Compiled, :unknown, 0, [file: 'unknown', line: 26]}}
|
|
end
|
|
end
|
|
|
|
defmodule TestEngine do
|
|
@behaviour EEx.Engine
|
|
|
|
def init(_opts) do
|
|
"INIT"
|
|
end
|
|
|
|
def handle_body(body) do
|
|
"BODY(#{body})"
|
|
end
|
|
|
|
def handle_begin(_) do
|
|
"BEGIN"
|
|
end
|
|
|
|
def handle_end(buffer) do
|
|
buffer <> ":END"
|
|
end
|
|
|
|
def handle_text(buffer, text) do
|
|
buffer <> ":TEXT(#{String.trim(text)})"
|
|
end
|
|
|
|
def handle_expr(buffer, "/", expr) do
|
|
buffer <> ":DIV(#{Macro.to_string(expr)})"
|
|
end
|
|
|
|
def handle_expr(buffer, "=", expr) do
|
|
buffer <> ":EQUAL(#{Macro.to_string(expr)})"
|
|
end
|
|
|
|
def handle_expr(buffer, mark, expr) do
|
|
EEx.Engine.handle_expr(buffer, mark, expr)
|
|
end
|
|
end
|
|
|
|
describe "custom engines" do
|
|
test "text" do
|
|
assert_eval("BODY(INIT:TEXT(foo))", "foo", [], engine: TestEngine)
|
|
end
|
|
|
|
test "custom marker" do
|
|
assert_eval("BODY(INIT:TEXT(foo):DIV(:bar))", "foo <%/ :bar %>", [], engine: TestEngine)
|
|
end
|
|
|
|
test "begin/end" do
|
|
assert_eval(
|
|
~s[BODY(INIT:TEXT(foo):EQUAL(if do\n "BEGIN:TEXT(this):END"\nelse\n "BEGIN:TEXT(that):END"\nend))],
|
|
"foo <%= if do %>this<% else %>that<% end %>",
|
|
[],
|
|
engine: TestEngine
|
|
)
|
|
end
|
|
|
|
test "not implemented custom marker" do
|
|
msg =
|
|
~r/unsupported EEx syntax <%| %> \(the syntax is valid but not supported by the current EEx engine\)/
|
|
|
|
assert_raise EEx.SyntaxError, msg, fn ->
|
|
assert_eval({:wrapped, "foo baz"}, "foo <%| :bar %>", [], engine: TestEngine)
|
|
end
|
|
end
|
|
end
|
|
|
|
defp assert_eval(expected, actual, binding \\ [], opts \\ []) do
|
|
opts = Keyword.merge([file: __ENV__.file, engine: opts[:engine] || EEx.Engine], opts)
|
|
result = EEx.eval_string(actual, binding, opts)
|
|
assert result == expected
|
|
end
|
|
|
|
defp assert_normalized_newline_equal(expected, actual) do
|
|
assert String.replace(expected, "\r\n", "\n") == String.replace(actual, "\r\n", "\n")
|
|
end
|
|
|
|
def tuple_map({list, callback}) do
|
|
Enum.map(list, callback)
|
|
end
|
|
|
|
def switching_map(list, a, b) do
|
|
list
|
|
|> Enum.with_index()
|
|
|> Enum.map(fn
|
|
{element, index} when rem(index, 2) == 0 -> a.(element)
|
|
{element, index} when rem(index, 2) == 1 -> b.(element)
|
|
end)
|
|
end
|
|
|
|
defmacro switching_macro(list, a, do: block) do
|
|
quote do
|
|
b = fn var!(x) ->
|
|
unquote(block)
|
|
end
|
|
|
|
unquote(__MODULE__).switching_map(unquote(list), unquote(a), b)
|
|
end
|
|
end
|
|
end
|