Skip to content

Commit 12221a0

Browse files
authored
Merge pull request #432 from elixirscript/testing
Test Framework
2 parents 5c39e2c + 1467026 commit 12221a0

File tree

33 files changed

+881
-200
lines changed

33 files changed

+881
-200
lines changed

.ebert.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,3 +11,5 @@ engines:
1111
exclude_paths:
1212
- config
1313
- test
14+
- priv/testrunner/vendor.build.js
15+
- priv/testrunner/esm

.gitignore

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ deploy.sh
1010
.DS_Store
1111
sample/dest
1212
fprof.trace
13-
index.js
1413
/doc
1514
/bench/snapshots
1615
.tern-port
@@ -22,6 +21,6 @@ stdlib_state.bin
2221
test/app/build
2322
.vscode
2423
cover
25-
/priv
24+
/priv/build
2625
/tmp
2726
.esm-cache

Makefile

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ js_test:
1717

1818
elixir_test:
1919
mix test --cover
20+
mix elixirscript.test
2021

2122
clean:
2223
rm -rf priv/build

lib/elixir_script/passes/find_used_modules.ex

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -225,6 +225,8 @@ defmodule ElixirScript.FindUsedModules do
225225
end
226226

227227
defp walk({:try, _, [blocks]}, state) do
228+
walk(Enum, state)
229+
228230
try_block = Keyword.get(blocks, :do)
229231
rescue_block = Keyword.get(blocks, :rescue, nil)
230232
catch_block = Keyword.get(blocks, :catch, nil)
@@ -236,7 +238,8 @@ defmodule ElixirScript.FindUsedModules do
236238
if rescue_block do
237239
Enum.each(rescue_block, fn
238240
{:->, _, [ [{:in, _, [param, names]}], body]} ->
239-
walk({[], [param], [{{:., [], [Enum, :member?]}, [], [param, names]}], body}, state)
241+
Enum.each(names, &walk(&1, state))
242+
walk({[], [param], [{{:., [], [Enum, :member?]}, [], [names, param]}], body}, state)
240243
{:->, _, [ [param], body]} ->
241244
walk({[], [param], [], body}, state)
242245
end)

lib/elixir_script/passes/output.ex

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ defmodule ElixirScript.Output do
6666
|> Generator.generate
6767
|> concat
6868
|> output(module, Map.get(opts, :output), js_modules)
69-
end)
69+
end, timeout: 10_000)
7070
|> Stream.map(fn {:ok, code} -> code end)
7171
|> Enum.to_list()
7272
end

lib/elixir_script/passes/translate.ex

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ defmodule ElixirScript.Translate do
1313
|> Task.async_stream(fn
1414
{module, info} ->
1515
ElixirScript.Translate.Module.compile(module, info, pid)
16-
end)
16+
end, timeout: 10_000)
1717
|> Stream.run()
1818
end
1919
end

lib/elixir_script/passes/translate/forms/try.ex

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,8 +58,34 @@ defmodule ElixirScript.Translate.Forms.Try do
5858

5959
defp process_rescue_block(rescue_block, state) do
6060
processed_clauses = Enum.map(rescue_block, fn
61+
{:->, _, [ [{:in, _, [{:_, context, atom}, names]}], body]} ->
62+
names = Enum.map(names, &make_exception_ast(&1))
63+
64+
param = {:_e, context, atom}
65+
reason_call = {{:., [], [{:_e0, context, atom}, :__reason]}, [], []}
66+
reason_call = {{:., [], [reason_call, :__struct__]}, [], []}
67+
reason_call = {{:., [], [reason_call, :__MODULE__]}, [], []}
68+
69+
{ast, _} = Clause.compile({
70+
[],
71+
[param],
72+
[{{:., [], [Enum, :member?]}, [], [names, reason_call]}],
73+
body},
74+
state)
75+
ast
6176
{:->, _, [ [{:in, _, [param, names]}], body]} ->
62-
{ast, _} = Clause.compile({[], [param], [{{:., [], [Enum, :member?]}, [], [param, names]}], body}, state)
77+
names = Enum.map(names, &make_exception_ast(&1))
78+
79+
reason_call = {{:., [], [param, :__reason]}, [], []}
80+
reason_call = {{:., [], [reason_call, :__struct__]}, [], []}
81+
reason_call = {{:., [], [reason_call, :__MODULE__]}, [], []}
82+
83+
{ast, _} = Clause.compile({
84+
[],
85+
[param],
86+
[{{:., [], [Enum, :member?]}, [], [names, reason_call]}],
87+
body},
88+
state)
6389
ast
6490
{:->, _, [ [param], body]} ->
6591
{ast, _} = Clause.compile({[], [param], [], body}, state)
@@ -77,6 +103,10 @@ defmodule ElixirScript.Translate.Forms.Try do
77103

78104
end
79105

106+
defp make_exception_ast(name) do
107+
{{:., [], [name, :__MODULE__]}, [], []}
108+
end
109+
80110
defp process_after_block(after_block, state) do
81111
translated_body = prepare_function_body(after_block, state)
82112
translated_body = JS.block_statement(translated_body)

lib/elixir_script_test/test.ex

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
defmodule ElixirScript.Test do
2+
@moduledoc """
3+
Unit Testing Framework for ElixirScript.
4+
5+
Requires node.js 8.3.0 and above
6+
7+
Uses assertions from ExUnit as well as has a similar api to ExUnit with a few differences
8+
9+
## Example
10+
11+
An basic setup of a test. Modified from ExUnit's example
12+
13+
```elixir
14+
# File: assertion_test.exs
15+
# 1) Create a new test module (test case) and use "ElixirScript.Test".
16+
defmodule AssertionTest do
17+
use ElixirScript.Test
18+
19+
# 2) Use the "test" macro.
20+
test "the truth" do
21+
assert true
22+
end
23+
end
24+
```
25+
26+
To run the test above, use the `ElixirScript.Test.start/1` function, giving it the path to the test
27+
```
28+
ElixirScript.Test.start("assertion_test.exs")
29+
```
30+
31+
## Integration with Mix
32+
33+
To run tests using mix, run `mix elixirscript.test`. This will run all tests in the test_elixir_script directory.
34+
35+
36+
## Callbacks
37+
38+
ElixirScript defines the following callbacks
39+
40+
* setup/1
41+
* setup/2
42+
* setup_all/1
43+
* setup_all/2
44+
* teardown/1
45+
* teardown/2
46+
* teardown_all/1
47+
* teardown_all/2
48+
49+
The `setup` and `setup_all` callbacks work exactly as they would in ExUnit. Instead of having an `on_exit` callback,
50+
ElixirScript.Test has `teardown` callbacks. `teardown` is called after each test and `teardown_all` after all tests
51+
in the file have run.
52+
53+
```elixir
54+
defmodule AssertionTest do
55+
use ElixirScript.Test
56+
57+
# run before test
58+
setup do
59+
admin = create_admin_function()
60+
[admin: admin]
61+
end
62+
63+
test "the truth", %{admin: admin} do
64+
assert admin.is_authenticated
65+
end
66+
67+
# run after test
68+
teardown, %{admin: admin} do
69+
destroy_admin_function(admin)
70+
end
71+
end
72+
```
73+
"""
74+
75+
defmacro __using__(_opts) do
76+
quote do
77+
import ElixirScript.Test.Callbacks, only: [
78+
test: 2, test: 3,
79+
setup: 1, setup: 2,
80+
setup_all: 1, setup_all: 2,
81+
teardown: 1, teardown: 2,
82+
teardown_all: 1, teardown_all: 2
83+
]
84+
import ExUnit.Assertions
85+
86+
def __elixirscript_test_module__, do: true
87+
end
88+
end
89+
90+
@doc """
91+
Runs tests found in the given path. Accepts wildcards
92+
"""
93+
@spec start(binary(), map()) :: :ok | :error
94+
def start(path, _opts \\ %{}) do
95+
output = Path.join([System.tmp_dir!(), "elixirscript_tests"])
96+
File.mkdir_p!(output)
97+
98+
ElixirScript.Compiler.compile(path, [output: output])
99+
100+
js_files = output
101+
|> Path.expand
102+
|> Path.join("Elixir.*.js")
103+
|> Path.wildcard()
104+
105+
exit_status = node_test_runner(js_files)
106+
107+
# Delete directory at the end
108+
File.rm_rf!(output)
109+
110+
case exit_status do
111+
0 ->
112+
:ok
113+
_ ->
114+
:error
115+
end
116+
end
117+
118+
defp node_test_runner(js_files) do
119+
test_script_path = Path.join([:code.priv_dir(:elixir_script), "testrunner", "index.js"])
120+
test_script_path = [test_script_path] ++ js_files
121+
{_, exit_status} = System.cmd(
122+
"node",
123+
test_script_path,
124+
into: IO.stream(:stdio, :line)
125+
)
126+
127+
exit_status
128+
end
129+
end
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
defmodule ElixirScript.Test.Callbacks do
2+
@moduledoc """
3+
Defines ElixirScript.Test callbacks
4+
"""
5+
6+
@doc """
7+
Called before all tests are run in a test file
8+
"""
9+
defmacro setup_all(context \\ quote(do: _), contents) do
10+
do_setup(context, contents, :__elixirscript_test_setup_all)
11+
end
12+
13+
@doc """
14+
Called before each test is run in a test file
15+
"""
16+
defmacro setup(context \\ quote(do: _), contents) do
17+
do_setup(context, contents, :__elixirscript_test_setup)
18+
end
19+
20+
defp do_setup(context, contents, name) do
21+
contents =
22+
case contents do
23+
[do: block] ->
24+
quote do
25+
unquote(block)
26+
end
27+
_ ->
28+
quote do
29+
try(unquote(contents))
30+
end
31+
end
32+
33+
context = Macro.escape(context)
34+
contents = Macro.escape(contents, unquote: true)
35+
36+
quote bind_quoted: [context: context, contents: contents, name: name] do
37+
def unquote(name)(unquote(context)) do
38+
unquote(contents)
39+
end
40+
end
41+
end
42+
43+
@doc """
44+
Called after all tests are run in a test file
45+
"""
46+
defmacro teardown_all(context \\ quote(do: _), contents) do
47+
do_teardown(context, contents, :__elixirscript_test_teardown_all)
48+
end
49+
50+
@doc """
51+
Called after each test is run in a test file
52+
"""
53+
defmacro teardown(context \\ quote(do: _), contents) do
54+
do_teardown(context, contents, :__elixirscript_test_teardown)
55+
end
56+
57+
defp do_teardown(context, contents, name) do
58+
contents =
59+
case contents do
60+
[do: block] ->
61+
quote do
62+
unquote(block)
63+
:ok
64+
end
65+
_ ->
66+
quote do
67+
try(unquote(contents))
68+
:ok
69+
end
70+
end
71+
72+
context = Macro.escape(context)
73+
contents = Macro.escape(contents, unquote: true)
74+
75+
quote bind_quoted: [context: context, contents: contents, name: name] do
76+
def unquote(name)(unquote(context)) do
77+
unquote(contents)
78+
end
79+
end
80+
end
81+
82+
@doc """
83+
Defines a test
84+
"""
85+
defmacro test(message, context \\ quote(do: _), contents) do
86+
contents =
87+
case contents do
88+
[do: block] ->
89+
quote do
90+
unquote(block)
91+
:ok
92+
end
93+
_ ->
94+
quote do
95+
try(unquote(contents))
96+
:ok
97+
end
98+
end
99+
100+
context = Macro.escape(context)
101+
contents = Macro.escape(contents, unquote: true)
102+
name = message
103+
|> String.replace(" ", "_")
104+
|> String.replace(~r/[^A-Za-z0-9]/, "")
105+
106+
name = String.to_atom("__elixirscript_test_case_#{name}")
107+
108+
quote bind_quoted: [context: context, contents: contents, message: message, name: name] do
109+
def unquote(name)() do
110+
%{
111+
message: unquote(message),
112+
test: fn(context) -> unquote(contents) end
113+
}
114+
end
115+
end
116+
end
117+
end

lib/mix/tasks/elixirscript.test.ex

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
defmodule Mix.Tasks.Elixirscript.Test do
2+
@moduledoc """
3+
Runs ElixirScript Tests
4+
"""
5+
use Mix.Task
6+
7+
@shortdoc "Runs ElixirScript Tests"
8+
@preferred_cli_env :test
9+
10+
def run(_args) do
11+
Mix.Task.run "app.start"
12+
13+
path = Path.join([default_test_path(), "**", "*_test.exs"])
14+
case ElixirScript.Test.start(path) do
15+
:error ->
16+
System.at_exit(fn _ -> exit({:shutdown, 1}) end)
17+
:ok ->
18+
:ok
19+
end
20+
end
21+
22+
defp default_test_path do
23+
if File.dir?("test_elixir_script") do
24+
"test_elixir_script"
25+
else
26+
""
27+
end
28+
end
29+
end

0 commit comments

Comments
 (0)