Before this fix, evaluating `b = a` with assignments of: `a: 1, a: 2, c: 3` would eval AST equivalent to `^c = a`. This happened due to binding normalization generating version numbers larger than the total number of bindings. Later in the pipeline, the number of bindings is used to compute the next version number, which would then conflict with an existing binding. This commit fixes that by not increasing the version number when a repeated binding is normalized.
302 lines
9.4 KiB
Elixir
302 lines
9.4 KiB
Elixir
Code.require_file("test_helper.exs", __DIR__)
|
|
|
|
defmodule CodeTest do
|
|
use ExUnit.Case, async: true
|
|
|
|
doctest Code
|
|
import PathHelpers
|
|
|
|
def genmodule(name) do
|
|
defmodule name do
|
|
Kernel.LexicalTracker.references(__ENV__.lexical_tracker)
|
|
end
|
|
end
|
|
|
|
contents =
|
|
quote do
|
|
defmodule CodeTest.Sample do
|
|
def eval_quoted_info, do: {__MODULE__, __ENV__.file, __ENV__.line}
|
|
end
|
|
end
|
|
|
|
Code.eval_quoted(contents, [], file: "sample.ex", line: 13)
|
|
|
|
describe "eval_string/1,2,3" do
|
|
test "correctly evaluates a string of code" do
|
|
assert Code.eval_string("1 + 2") == {3, []}
|
|
assert Code.eval_string("two = 1 + 1") == {2, [two: 2]}
|
|
end
|
|
|
|
test "keeps bindings on optimized evals" do
|
|
assert Code.eval_string("import Enum", x: 1) == {Enum, [x: 1]}
|
|
end
|
|
|
|
test "supports a %Macro.Env{} struct as the third argument" do
|
|
assert {3, _} = Code.eval_string("a + b", [a: 1, b: 2], __ENV__)
|
|
end
|
|
|
|
test "supports unnamed scopes" do
|
|
assert {%RuntimeError{}, [a: %RuntimeError{}]} =
|
|
Code.eval_string("a = (try do (raise \"hello\") rescue e -> e end)")
|
|
end
|
|
|
|
test "supports the :requires option" do
|
|
assert Code.eval_string("Kernel.if true, do: :ok", [], requires: [Z, Kernel]) == {:ok, []}
|
|
end
|
|
|
|
test "returns bindings from a different context" do
|
|
assert Code.eval_string("var!(a, Sample) = 1") == {1, [{{:a, Sample}, 1}]}
|
|
end
|
|
|
|
test "does not raise on duplicate bindings" do
|
|
# The order of which values win is not guaranteed, but it should evaluate successfully.
|
|
assert Code.eval_string("b = String.Chars.to_string(a)", a: 0, a: 1) ==
|
|
{"1", [{:b, "1"}, {:a, 1}]}
|
|
|
|
assert Code.eval_string("b = String.Chars.to_string(a)", a: 0, a: 1, c: 2) ==
|
|
{"1", [{:c, 2}, {:b, "1"}, {:a, 1}]}
|
|
end
|
|
|
|
test "with many options" do
|
|
options = [
|
|
functions: [{Kernel, [is_atom: 1]}],
|
|
macros: [{Kernel, [and: 2]}],
|
|
aliases: [{K, Kernel}],
|
|
requires: [Kernel]
|
|
]
|
|
|
|
code = "is_atom(:foo) and K.is_list([])"
|
|
assert Code.eval_string(code, [], options) == {true, []}
|
|
end
|
|
|
|
test "yields the correct stacktrace" do
|
|
try do
|
|
Code.eval_string("<<a::size(b)>>", a: :a, b: :b)
|
|
rescue
|
|
_ ->
|
|
assert Enum.any?(__STACKTRACE__, &(elem(&1, 0) == __MODULE__))
|
|
end
|
|
end
|
|
|
|
test "raises streamlined argument errors" do
|
|
assert_raise ArgumentError,
|
|
~r"argument error while evaluating at line 1",
|
|
fn -> Code.eval_string("a <> b", a: :a, b: :b) end
|
|
|
|
assert_raise ArgumentError,
|
|
~r"argument error while evaluating example.ex at line 1",
|
|
fn -> Code.eval_string("a <> b", [a: :a, b: :b], file: "example.ex") end
|
|
|
|
assert_raise ArgumentError,
|
|
~r"argument error while evaluating example.ex between lines 1 and 2",
|
|
fn -> Code.eval_string("a <>\nb", [a: :a, b: :b], file: "example.ex") end
|
|
end
|
|
end
|
|
|
|
test "eval_quoted/1" do
|
|
assert Code.eval_quoted(quote(do: 1 + 2)) == {3, []}
|
|
assert CodeTest.Sample.eval_quoted_info() == {CodeTest.Sample, "sample.ex", 13}
|
|
end
|
|
|
|
test "eval_quoted/2 with %Macro.Env{} at runtime" do
|
|
alias :lists, as: MyList
|
|
quoted = quote(do: MyList.flatten([[1, 2, 3]]))
|
|
|
|
assert Code.eval_quoted(quoted, [], __ENV__) == {[1, 2, 3], []}
|
|
|
|
# Let's check it discards tracers since the lexical tracker is explicitly nil
|
|
assert Code.eval_quoted(quoted, [], %{__ENV__ | tracers: [:bad]}) == {[1, 2, 3], []}
|
|
end
|
|
|
|
test "eval_quoted/2 with %Macro.Env{} at compile time" do
|
|
defmodule CompileTimeEnv do
|
|
alias String.Chars
|
|
{"foo", []} = Code.eval_string("Chars.to_string(:foo)", [], __ENV__)
|
|
end
|
|
end
|
|
|
|
test "eval_file/1" do
|
|
assert Code.eval_file(fixture_path("code_sample.exs")) == {3, [var: 3]}
|
|
|
|
assert_raise Code.LoadError, fn ->
|
|
Code.eval_file("non_existent.exs")
|
|
end
|
|
end
|
|
|
|
test "compile_file/1" do
|
|
assert Code.compile_file(fixture_path("code_sample.exs")) == []
|
|
refute fixture_path("code_sample.exs") in Code.required_files()
|
|
end
|
|
|
|
test "compile_file/1 also emits checker warnings" do
|
|
output =
|
|
ExUnit.CaptureIO.capture_io(:stderr, fn ->
|
|
Code.compile_file(fixture_path("checker_warning.exs"))
|
|
end)
|
|
|
|
assert output =~ "incompatible types"
|
|
end
|
|
|
|
test "require_file/1" do
|
|
assert Code.require_file(fixture_path("code_sample.exs")) == []
|
|
assert fixture_path("code_sample.exs") in Code.required_files()
|
|
assert Code.require_file(fixture_path("code_sample.exs")) == nil
|
|
|
|
Code.unrequire_files([fixture_path("code_sample.exs")])
|
|
refute fixture_path("code_sample.exs") in Code.required_files()
|
|
assert Code.require_file(fixture_path("code_sample.exs")) != nil
|
|
after
|
|
Code.unrequire_files([fixture_path("code_sample.exs")])
|
|
end
|
|
|
|
test "string_to_quoted!/2 errors take lines and columns into account" do
|
|
message = "nofile:1:5: syntax error before: '*'\n |\n 1 | 1 + * 3\n | ^"
|
|
|
|
assert_raise SyntaxError, message, fn ->
|
|
Code.string_to_quoted!("1 + * 3")
|
|
end
|
|
|
|
message = "nofile:10:5: syntax error before: '*'\n |\n 10 | 1 + * 3\n | ^"
|
|
|
|
assert_raise SyntaxError, message, fn ->
|
|
Code.string_to_quoted!("1 + * 3", line: 10)
|
|
end
|
|
|
|
message = "nofile:10:7: syntax error before: '*'\n |\n 10 | 1 + * 3\n | ^"
|
|
|
|
assert_raise SyntaxError, message, fn ->
|
|
Code.string_to_quoted!("1 + * 3", line: 10, column: 3)
|
|
end
|
|
|
|
message = "nofile:11:5: syntax error before: '*'\n |\n 11 | 1 + * 3\n | ^"
|
|
|
|
assert_raise SyntaxError, message, fn ->
|
|
Code.string_to_quoted!(":ok\n1 + * 3", line: 10, column: 3)
|
|
end
|
|
end
|
|
|
|
test "string_to_quoted only requires the List.Chars protocol implementation to work" do
|
|
assert {:ok, 1.23} = Code.string_to_quoted(1.23)
|
|
assert 1.23 = Code.string_to_quoted!(1.23)
|
|
assert {:ok, 1.23, []} = Code.string_to_quoted_with_comments(1.23)
|
|
assert {1.23, []} = Code.string_to_quoted_with_comments!(1.23)
|
|
end
|
|
|
|
test "compile source" do
|
|
assert __MODULE__.__info__(:compile)[:source] == String.to_charlist(__ENV__.file)
|
|
end
|
|
|
|
test "compile info returned with source accessible through keyword module" do
|
|
compile = __MODULE__.__info__(:compile)
|
|
assert Keyword.get(compile, :source) != nil
|
|
end
|
|
|
|
describe "compile_string/1" do
|
|
test "compiles the given string" do
|
|
assert [{CompileStringSample, _}] =
|
|
Code.compile_string("defmodule CompileStringSample, do: :ok")
|
|
after
|
|
:code.purge(CompileSimpleSample)
|
|
:code.delete(CompileSimpleSample)
|
|
end
|
|
|
|
test "works across lexical scopes" do
|
|
assert [{CompileCrossSample, _}] =
|
|
Code.compile_string("CodeTest.genmodule CompileCrossSample")
|
|
after
|
|
:code.purge(CompileCrossSample)
|
|
:code.delete(CompileCrossSample)
|
|
end
|
|
end
|
|
|
|
test "ensure_loaded?/1" do
|
|
assert Code.ensure_loaded?(__MODULE__)
|
|
refute Code.ensure_loaded?(Code.NoFile)
|
|
end
|
|
|
|
test "ensure_loaded!/1" do
|
|
assert Code.ensure_loaded!(__MODULE__) == __MODULE__
|
|
|
|
assert_raise ArgumentError, "could not load module Code.NoFile due to reason :nofile", fn ->
|
|
Code.ensure_loaded!(Code.NoFile)
|
|
end
|
|
end
|
|
|
|
test "ensure_compiled/1" do
|
|
assert Code.ensure_compiled(__MODULE__) == {:module, __MODULE__}
|
|
assert Code.ensure_compiled(Code.NoFile) == {:error, :nofile}
|
|
end
|
|
|
|
test "ensure_compiled!/1" do
|
|
assert Code.ensure_compiled!(__MODULE__) == __MODULE__
|
|
|
|
assert_raise ArgumentError, "could not load module Code.NoFile due to reason :nofile", fn ->
|
|
Code.ensure_compiled!(Code.NoFile)
|
|
end
|
|
end
|
|
|
|
test "put_compiler_option/2 validates options" do
|
|
message = "unknown compiler option: :not_a_valid_option"
|
|
|
|
assert_raise RuntimeError, message, fn ->
|
|
Code.put_compiler_option(:not_a_valid_option, :foo)
|
|
end
|
|
|
|
message = "compiler option :debug_info should be a boolean, got: :not_a_boolean"
|
|
|
|
assert_raise RuntimeError, message, fn ->
|
|
Code.put_compiler_option(:debug_info, :not_a_boolean)
|
|
end
|
|
end
|
|
end
|
|
|
|
defmodule Code.SyncTest do
|
|
use ExUnit.Case
|
|
|
|
import PathHelpers
|
|
|
|
test "path manipulation" do
|
|
path = Path.join(__DIR__, "fixtures")
|
|
Code.prepend_path(path)
|
|
assert to_charlist(path) in :code.get_path()
|
|
|
|
Code.delete_path(path)
|
|
refute to_charlist(path) in :code.get_path()
|
|
end
|
|
|
|
test "purges compiler modules" do
|
|
quoted = quote(do: Agent.start_link(fn -> :ok end))
|
|
Code.compile_quoted(quoted)
|
|
|
|
{:ok, claimed} = Code.purge_compiler_modules()
|
|
assert claimed > 0
|
|
|
|
{:ok, claimed} = Code.purge_compiler_modules()
|
|
assert claimed == 0
|
|
end
|
|
|
|
test "returns previous options when setting compiler options" do
|
|
Code.compiler_options(debug_info: false)
|
|
assert Code.compiler_options(debug_info: true) == %{debug_info: false}
|
|
after
|
|
Code.compiler_options(debug_info: true)
|
|
end
|
|
|
|
test "compile_file/1 return value" do
|
|
assert [{CompileSample, binary}] = Code.compile_file(fixture_path("compile_sample.ex"))
|
|
assert is_binary(binary)
|
|
after
|
|
:code.purge(CompileSample)
|
|
:code.delete(CompileSample)
|
|
end
|
|
|
|
test "require_file/1 return value" do
|
|
assert [{CompileSample, binary}] = Code.require_file(fixture_path("compile_sample.ex"))
|
|
assert is_binary(binary)
|
|
after
|
|
Code.unrequire_files([fixture_path("compile_sample.ex")])
|
|
:code.purge(CompileSample)
|
|
:code.delete(CompileSample)
|
|
end
|
|
end
|