Add infrastructure for JS testing
This commit is contained in:
parent
dfb24274b0
commit
d3ea64cc15
15 changed files with 368 additions and 131 deletions
3
config/test.exs
Normal file
3
config/test.exs
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
import Config
|
||||
|
||||
config :nodejs, debug_mode: true
|
||||
14
js/index.mjs
Normal file
14
js/index.mjs
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
class Boxed {
|
||||
constructor(type, value) {
|
||||
this.type = type;
|
||||
this.value = value;
|
||||
}
|
||||
}
|
||||
|
||||
export const boxed = (type, value) => {
|
||||
return new Boxed(type, value);
|
||||
};
|
||||
|
||||
export default {
|
||||
boxed,
|
||||
};
|
||||
128
lib/beam_js.ex
128
lib/beam_js.ex
|
|
@ -2,132 +2,10 @@ defmodule BeamJs do
|
|||
@moduledoc """
|
||||
Documentation for `BeamJs`.
|
||||
"""
|
||||
require Logger
|
||||
|
||||
def module_code(module) do
|
||||
module
|
||||
|> BeamFile.byte_code()
|
||||
|> ok(fn {_, _, _, _, _, sections} ->
|
||||
{:ok, Enum.filter(sections, fn
|
||||
{:function, _fun_name, _arity, _, _code} ->
|
||||
true
|
||||
_other ->
|
||||
false
|
||||
end)}
|
||||
end)
|
||||
|> ok(fn functions ->
|
||||
Enum.map(functions, fn {:function, name, arity, _, code} ->
|
||||
{"#{name}/#{arity}", transform_function_code(code)}
|
||||
end)
|
||||
|> Map.new()
|
||||
end)
|
||||
end
|
||||
|
||||
def transform_function_code(ops) do
|
||||
Enum.reduce(ops, [], fn
|
||||
{:label, number}, [{line_no, head_ops} | tail] ->
|
||||
[{number, []} | [{line_no, Enum.reverse(head_ops)} | tail]]
|
||||
|
||||
{:label, number}, acc ->
|
||||
[{number, []} | acc]
|
||||
|
||||
{:line, _line_number}, [] ->
|
||||
[]
|
||||
|
||||
other_op, [{number, head} | tail] ->
|
||||
[{number, [other_op|head]} | tail]
|
||||
end)
|
||||
|> Enum.reverse()
|
||||
end
|
||||
|
||||
def encode_fun(label_sections) do
|
||||
"{\n" <> Enum.map_join(label_sections, ",\n", fn {label, ops} ->
|
||||
body = Enum.map_join(ops, "", &encode_op/1)
|
||||
" #{label}: (p) => {\n#{body}}"
|
||||
end) <> "}"
|
||||
end
|
||||
|
||||
def encode_op(tuple) when is_tuple(tuple) do
|
||||
op_name = elem(tuple, 0)
|
||||
args =
|
||||
try do
|
||||
encode_args(tuple)
|
||||
rescue
|
||||
e ->
|
||||
Logger.error("Error encoding op: #{inspect(tuple)}")
|
||||
reraise e, __STACKTRACE__
|
||||
end
|
||||
|
||||
" BeamJs.Ops.#{op_name}(p, " <> Enum.join(args, ", ") <> ");\n"
|
||||
end
|
||||
|
||||
def encode_op(atom) when is_atom(atom), do: " BeamJs.Ops.#{atom}(p);\n"
|
||||
|
||||
def encode_args({:allocate, frame_size, live}), do: [number(frame_size), number(live)]
|
||||
def encode_args({:line, number}), do: [number(number)]
|
||||
def encode_args({:func_info, {:atom, m}, {:atom, f}, arity}), do: [quoted_string(m), quoted_string(f), number(arity)]
|
||||
def encode_args({:select_val, source, {:f, fail_label}, comparison}), do: [encode_source(source), number(fail_label), encode_value(comparison)]
|
||||
def encode_args({:move, source, {register_bank, number}}) when register_bank in [:x, :y], do: [encode_source(source), quoted_string(register_bank), number(number)]
|
||||
def encode_args({:call, _live, {_m, f, a}}), do: [quoted_string(f), number(a)]
|
||||
def encode_args({:call_ext_only, _live, {:extfunc, m, f, a}}), do: [quoted_string(m), quoted_string(f), number(a)]
|
||||
def encode_args({:call_only, _live, {_m, f, a}}), do: [quoted_string(f), number(a)]
|
||||
def encode_args({:deallocate, live}), do: [number(live)]
|
||||
def encode_args({:gc_bif, bif, {:f, fail_to}, _unknown, args, {register_bank, number}}) when register_bank in [:x, :y] do
|
||||
[
|
||||
quoted_string(bif),
|
||||
number(fail_to),
|
||||
"[" <> (args |> Enum.map(&encode_source/1) |> Enum.join(", ")) <> "]",
|
||||
register_bank,
|
||||
number
|
||||
]
|
||||
end
|
||||
|
||||
def encode_source({:tr, source, _}), do: encode_source(source)
|
||||
def encode_source({:x, number}), do: "p.registers.x[#{number}]"
|
||||
def encode_source({:y, number}), do: "p.registers.y[#{number}]"
|
||||
def encode_source({:integer, number}), do: number(number)
|
||||
def encode_source(value), do: encode_value(value)
|
||||
|
||||
|
||||
def encode_value({:literal, value}), do: encode_value(value)
|
||||
|
||||
def encode_value({type, value}) when type in [:atom, :x, :y, :list] do
|
||||
~s|BeamJs.boxed("#{type}", #{encode_value(value)})|
|
||||
end
|
||||
|
||||
def encode_value(tuple) when is_tuple(tuple) do
|
||||
~s|BeamJs.boxed("tuple", #{encode_value(Tuple.to_list(tuple))})|
|
||||
end
|
||||
|
||||
def encode_value(value) when is_binary(value) do
|
||||
if String.valid?(value) do
|
||||
quoted_string(value)
|
||||
else
|
||||
# Likely a raw bytes array
|
||||
base64 = value |> Base.encode64() |> quoted_string()
|
||||
~s|atob(#{base64})|
|
||||
defimpl JSON.Encoder, for: BeamJs.Boxed do
|
||||
def encode(value, encoder) do
|
||||
["BeamJs.boxed(", encoder.(value.type, encoder), ", ", encoder.(value.value, encoder),")"]
|
||||
end
|
||||
end
|
||||
|
||||
def encode_value(value) when is_list(value) do
|
||||
~s|[#{Enum.map_join(value, ", ", &encode_value/1)}]|
|
||||
end
|
||||
|
||||
def encode_value(value) when is_atom(value) do
|
||||
quoted_string(value)
|
||||
end
|
||||
|
||||
def encode_value(value) when is_number(value) do
|
||||
"#{value}"
|
||||
end
|
||||
|
||||
defp quoted_string(v), do: ~s|"#{v}"|
|
||||
defp number(n), do: "#{n}"
|
||||
|
||||
def fact(0), do: 1
|
||||
def fact(1), do: 1
|
||||
def fact(n), do: fact(n - 1) + fact(n - 2)
|
||||
|
||||
defp ok({:ok, value}, fun), do: fun.(value)
|
||||
defp ok(other, _fun), do: other
|
||||
end
|
||||
|
|
|
|||
3
lib/beam_js/boxed.ex
Normal file
3
lib/beam_js/boxed.ex
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
defmodule BeamJs.Boxed do
|
||||
defstruct [:type, :value]
|
||||
end
|
||||
191
lib/beam_js/encoder.ex
Normal file
191
lib/beam_js/encoder.ex
Normal file
|
|
@ -0,0 +1,191 @@
|
|||
defmodule BeamJs.Encoder do
|
||||
require Logger
|
||||
|
||||
alias BeamJs.Boxed
|
||||
|
||||
def encode_module(mod_ast) do
|
||||
"import BeamJs from '../js/index.mjs';\n" <>
|
||||
(
|
||||
mod_ast
|
||||
|> Enum.map_join("\n", fn {fn_name, ops} ->
|
||||
encode_fun_to_js_export(ops, fn_name)
|
||||
end)
|
||||
)
|
||||
end
|
||||
|
||||
def encode_fun_to_js_value(label_sections) do
|
||||
Enum.map(label_sections, fn {label, ops} ->
|
||||
body = Enum.map(ops, &encode_op/1)
|
||||
[label, body]
|
||||
end)
|
||||
|> JSON.encode!()
|
||||
end
|
||||
|
||||
def encode_fun_to_js_export(label_sections, function_name) do
|
||||
label_sections
|
||||
|> encode_fun_to_js_value()
|
||||
|> then(&"export const #{function_name} = #{&1};")
|
||||
end
|
||||
|
||||
def encode_op(tuple) when is_tuple(tuple) do
|
||||
op_name = elem(tuple, 0)
|
||||
args =
|
||||
try do
|
||||
encode_op_args(tuple)
|
||||
rescue
|
||||
e ->
|
||||
Logger.error("Error encoding op: #{inspect(tuple)}")
|
||||
reraise e, __STACKTRACE__
|
||||
end
|
||||
|
||||
[op_name | args]
|
||||
end
|
||||
|
||||
def encode_op(atom) when is_atom(atom), do: [atom]
|
||||
|
||||
def encode_op_args(op)
|
||||
def encode_op_args({:allocate, frame_size, live}), do: [frame_size, live]
|
||||
def encode_op_args({:allocate_heap, n, {:alloc, list}, n2}) do
|
||||
[
|
||||
n,
|
||||
Enum.map(list, &encode_value/1),
|
||||
n2
|
||||
]
|
||||
end
|
||||
def encode_op_args({:bs_create_bin, {:f, fail_to}, alloc, live, unit, {dest_bank, dest_number}, {:list, segments}}) do
|
||||
[
|
||||
fail_to,
|
||||
alloc,
|
||||
live,
|
||||
unit,
|
||||
dest_bank,
|
||||
dest_number,
|
||||
Enum.map(segments, &encode_value/1)
|
||||
]
|
||||
end
|
||||
def encode_op_args({:line, number}), do: [number]
|
||||
def encode_op_args({:func_info, {:atom, m}, {:atom, f}, arity}), do: [Atom.to_string(m), Atom.to_string(f), arity]
|
||||
def encode_op_args({:make_fun3, {m, f, a}, x, y, {dest_bank, dest_number}, {:list, list}}) do
|
||||
[
|
||||
m,
|
||||
f,
|
||||
a,
|
||||
x,
|
||||
y,
|
||||
dest_bank,
|
||||
dest_number,
|
||||
Enum.map(list, &encode_value/1)
|
||||
]
|
||||
end
|
||||
def encode_op_args({:select_val, source, {:f, fail_label}, {:list, f_list}}), do: [encode_source(source), fail_label, encode_f_list(f_list)]
|
||||
def encode_op_args({:move, source, dest}), do: [encode_source(source), encode_source(dest)]
|
||||
def encode_op_args({:call, _live, {_m, f, a}}), do: [f, a]
|
||||
def encode_op_args({:call_ext, _live, {:extfunc, m, f, a}}), do: [m, f, a]
|
||||
def encode_op_args({:call_ext_only, _live, {:extfunc, m, f, a}}), do: [m, f, a]
|
||||
def encode_op_args({:call_only, _live, {_m, f, a}}), do: [f, a]
|
||||
def encode_op_args({:deallocate, live}), do: [live]
|
||||
def encode_op_args({:gc_bif, bif, {:f, fail_to}, _unknown, args, {register_bank, number}}) when register_bank in [:x, :y] do
|
||||
[
|
||||
bif,
|
||||
fail_to,
|
||||
Enum.map(args, &encode_source/1),
|
||||
register_bank,
|
||||
number
|
||||
]
|
||||
end
|
||||
def encode_op_args({:get_tuple_element, {source_bank, source_number}, index, {dest_bank, dest_number}}) when source_bank in [:x, :y] and dest_bank in [:x, :y] do
|
||||
[
|
||||
source_bank,
|
||||
source_number,
|
||||
index,
|
||||
dest_bank,
|
||||
dest_number
|
||||
]
|
||||
end
|
||||
def encode_op_args({:init_yregs, {:list, sources}}), do: [Enum.map(sources, &encode_source/1)]
|
||||
def encode_op_args({:jump, {:f, to}}), do: [to]
|
||||
def encode_op_args({:put_list, {bank_a, number_a}, {bank_b, number_b}, {bank_c, number_c}}), do: [bank_a, number_a, bank_b, number_b, bank_c, number_c]
|
||||
def encode_op_args({:put_list, {bank_a, number_a}, nil, {bank_c, number_c}}), do: [bank_a, number_a, nil, nil, bank_c, number_c]
|
||||
def encode_op_args({:bif, bif, {:f, fail_to}, args, {register_bank, number}}) when register_bank in [:x, :y] do
|
||||
[
|
||||
bif,
|
||||
fail_to,
|
||||
Enum.map(args, &encode_source/1),
|
||||
register_bank,
|
||||
number
|
||||
]
|
||||
end
|
||||
def encode_op_args({:select_tuple_arity, {:tr, {source_bank, source_number}, {:t_tuple, 0, false, %{}}}, {:f, fail_to},
|
||||
{:list, f_list}}) do
|
||||
[
|
||||
source_bank,
|
||||
source_number,
|
||||
fail_to,
|
||||
encode_f_list(f_list)
|
||||
]
|
||||
end
|
||||
def encode_op_args({:swap, {a_bank, a_n}, {b_bank, b_n}}), do: [a_bank, a_n, b_bank, b_n]
|
||||
def encode_op_args({:test, test_name, {:f, fail_to}, args}) do
|
||||
[
|
||||
test_name,
|
||||
fail_to,
|
||||
Enum.map(args, &encode_source/1)
|
||||
]
|
||||
end
|
||||
def encode_op_args({:test_heap, {:alloc, list}, b}), do: [Enum.map(list, &encode_value/1), b]
|
||||
def encode_op_args({:test_heap, a, b}), do: [a, b]
|
||||
def encode_op_args({:trim, n, remaining}), do: [n, remaining]
|
||||
def encode_op_args({:try, {register_bank, number}, {:f, fail_to}}) when register_bank in [:x, :y], do: [register_bank, number, fail_to]
|
||||
def encode_op_args({:try_case, {register_bank, number}}) when register_bank in [:x, :y], do: [register_bank, number]
|
||||
def encode_op_args({:try_end, {register_bank, number}}) when register_bank in [:x, :y], do: [register_bank, number]
|
||||
|
||||
def encode_f_list(comparison) do
|
||||
comparison
|
||||
|> Enum.chunk_every(2)
|
||||
|> Enum.map(fn [value, {:f, number}] ->
|
||||
[encode_value(value), number]
|
||||
end)
|
||||
end
|
||||
|
||||
def encode_source({:tr, source, _}), do: encode_source(source)
|
||||
def encode_source({:x, number}), do: [:x, number]
|
||||
def encode_source({:y, number}), do: [:y, number]
|
||||
def encode_source(value), do: encode_value(value)
|
||||
|
||||
|
||||
def encode_value({:literal, value}), do: encode_value(value)
|
||||
|
||||
def encode_value({type, value}) when type in [:atom, :list, :integer] do
|
||||
%Boxed{type: type, value: value}
|
||||
end
|
||||
|
||||
def encode_value(tuple) when is_tuple(tuple) do
|
||||
%Boxed{type: "tuple", value: encode_value(Tuple.to_list(tuple))}
|
||||
end
|
||||
|
||||
def encode_value(value) when is_binary(value) do
|
||||
if String.valid?(value) do
|
||||
value
|
||||
else
|
||||
# Likely a raw bytes array
|
||||
base64 = value |> Base.encode64() |> quoted_string()
|
||||
~s|atob(#{base64})|
|
||||
end
|
||||
end
|
||||
|
||||
def encode_value(value) when is_list(value) do
|
||||
Enum.map(value, &encode_value/1)
|
||||
end
|
||||
|
||||
def encode_value(value) when is_atom(value) do
|
||||
%Boxed{type: :atom, value: value}
|
||||
end
|
||||
|
||||
def encode_value(value) when is_integer(value) do
|
||||
%Boxed{type: :integer, value: value}
|
||||
end
|
||||
|
||||
def encode_value(map) when is_map(map), do: %Boxed{type: :map, value: map |> Enum.map(fn {key, value} -> {encode_value(key), encode_value(value)} end) |> Map.new()}
|
||||
|
||||
defp quoted_string(v), do: ~s|"#{v}"|
|
||||
end
|
||||
21
lib/beam_js/function.ex
Normal file
21
lib/beam_js/function.ex
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
defmodule BeamJs.Function do
|
||||
def to_ast(ops) do
|
||||
Enum.reduce(ops, [], fn
|
||||
{:label, number}, [{line_no, head_ops} | tail] ->
|
||||
[{number, []} | [{line_no, Enum.reverse(head_ops)} | tail]]
|
||||
|
||||
{:label, number}, acc ->
|
||||
[{number, []} | acc]
|
||||
|
||||
{:line, _line_number}, [] ->
|
||||
[]
|
||||
|
||||
other_op, [{number, head} | tail] ->
|
||||
[{number, [other_op|head]} | tail]
|
||||
end)
|
||||
|> then(fn [{label, head_ops}|tail] ->
|
||||
[{label, Enum.reverse(head_ops)}|tail]
|
||||
end)
|
||||
|> Enum.reverse()
|
||||
end
|
||||
end
|
||||
53
lib/beam_js/js_runtime.ex
Normal file
53
lib/beam_js/js_runtime.ex
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
defmodule BeamJs.JsRuntime do
|
||||
use GenServer
|
||||
|
||||
def start_link() do
|
||||
GenServer.start_link(__MODULE__, [])
|
||||
end
|
||||
|
||||
def run(pid, code) do
|
||||
GenServer.call(pid, {:run, code})
|
||||
end
|
||||
|
||||
@impl GenServer
|
||||
def init(_opts) do
|
||||
path = System.find_executable("node")
|
||||
port = Port.open({:spawn_executable, path}, [:binary, args: ["-i"]])
|
||||
wait_for_prompt()
|
||||
|
||||
{:ok, %{port: port}}
|
||||
end
|
||||
|
||||
@impl GenServer
|
||||
def handle_call({:run, code}, _from, %{port: port} = state) do
|
||||
send(port, {self(), {:command, code <> "\r\n"}})
|
||||
result =
|
||||
receive do
|
||||
{_port, {:data, msg}} -> msg
|
||||
end
|
||||
|
||||
result =
|
||||
result
|
||||
|> String.trim_trailing("\n> ")
|
||||
|> String.trim("'")
|
||||
case JSON.decode(result) do
|
||||
{:ok, result} ->
|
||||
{:reply, {:ok, result}, state}
|
||||
{:error, _decode_error} ->
|
||||
{:reply, {:error, result}, state}
|
||||
end
|
||||
end
|
||||
|
||||
@impl GenServer
|
||||
def terminate(_reason, %{port: port}) do
|
||||
Port.close(port)
|
||||
end
|
||||
|
||||
defp wait_for_prompt do
|
||||
receive do
|
||||
{_port, {:data, "> "}} -> :ok
|
||||
{_port, {:data, _other}} ->
|
||||
wait_for_prompt()
|
||||
end
|
||||
end
|
||||
end
|
||||
32
lib/beam_js/module.ex
Normal file
32
lib/beam_js/module.ex
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
defmodule BeamJs.Module do
|
||||
import BeamJs.Utils
|
||||
|
||||
alias BeamJs.Function
|
||||
|
||||
def to_ast(module, opts \\ []) do
|
||||
function_map =
|
||||
module
|
||||
|> BeamFile.byte_code()
|
||||
|> ok(fn {_, _, _, _, _, sections} ->
|
||||
{:ok, Enum.filter(sections, fn
|
||||
{:function, _fun_name, _arity, _, _code} ->
|
||||
true
|
||||
_other ->
|
||||
false
|
||||
end)}
|
||||
end)
|
||||
|> ok(fn functions ->
|
||||
Enum.map(functions, fn {:function, name, arity, _, code} ->
|
||||
{"#{name}$#{arity}", Function.to_ast(code)}
|
||||
end)
|
||||
|> Map.new()
|
||||
end)
|
||||
|
||||
case Keyword.get(opts, :only) do
|
||||
nil ->
|
||||
function_map
|
||||
only ->
|
||||
Map.take(function_map, only)
|
||||
end
|
||||
end
|
||||
end
|
||||
4
lib/beam_js/utils.ex
Normal file
4
lib/beam_js/utils.ex
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
defmodule BeamJs.Utils do
|
||||
def ok({:ok, value}, fun), do: fun.(value)
|
||||
def ok(other, _fun), do: other
|
||||
end
|
||||
4
mix.exs
4
mix.exs
|
|
@ -6,11 +6,15 @@ defmodule BeamJs.MixProject do
|
|||
app: :beam_js,
|
||||
version: "0.1.0",
|
||||
elixir: "~> 1.19",
|
||||
elixirc_paths: elixirc_paths(Mix.env()),
|
||||
start_permanent: Mix.env() == :prod,
|
||||
deps: deps()
|
||||
]
|
||||
end
|
||||
|
||||
def elixirc_paths(:test), do: ["lib", "test/support"]
|
||||
def elixirc_paths(_), do: ["lib"]
|
||||
|
||||
# Run "mix help compile.app" to learn about applications.
|
||||
def application do
|
||||
[
|
||||
|
|
|
|||
3
mix.lock
3
mix.lock
|
|
@ -1,3 +1,6 @@
|
|||
%{
|
||||
"beam_file": {:hex, :beam_file, "0.6.4", "73660f76330e6b29be2b8ae3779b30cb67a7803cde88a21e3e74371130eed4d4", [:mix], [], "hexpm", "3f295dba08a68360903e86be4f183d7fb70f762ee37ee176438dde23ea494431"},
|
||||
"jason": {:hex, :jason, "1.4.4", "b9226785a9aa77b6857ca22832cffa5d5011a667207eb2a0ad56adb5db443b8a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "c5eb0cab91f094599f94d55bc63409236a8ec69a21a67814529e8d5f6cc90b3b"},
|
||||
"poolboy": {:hex, :poolboy, "1.5.2", "392b007a1693a64540cead79830443abf5762f5d30cf50bc95cb2c1aaafa006b", [:rebar3], [], "hexpm", "dad79704ce5440f3d5a3681c8590b9dc25d1a561e8f5a9c995281012860901e3"},
|
||||
"ssl_verify_fun": {:hex, :ssl_verify_fun, "1.1.7", "354c321cf377240c7b8716899e182ce4890c5938111a1296add3ec74cf1715df", [:make, :mix, :rebar3], [], "hexpm", "fe4c190e8f37401d30167c8c405eda19469f34577987c76dde613e838bbc67f8"},
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,19 @@
|
|||
defmodule BeamJsTest do
|
||||
use ExUnit.Case
|
||||
use BeamJs.JsCase
|
||||
doctest BeamJs
|
||||
|
||||
test "greets the world" do
|
||||
assert BeamJs.hello() == :world
|
||||
test "can run JS code", %{js: js} do
|
||||
assert run(js, "1") == {:ok, 1}
|
||||
end
|
||||
|
||||
test "can encode and import", %{js: js} do
|
||||
js_src =
|
||||
Example
|
||||
|> BeamJs.Module.to_ast(only: ["fact$1"])
|
||||
|> BeamJs.Encoder.encode_module()
|
||||
|
||||
File.write!("tmp/example.mjs", js_src)
|
||||
|
||||
assert {:ok, [[_label, [["func_info", "Elixir.Example", "fact", 1]]]| _rest]} = run(js, ~s|const Example = await import('./tmp/example.mjs');JSON.stringify(Example.fact$1)|)
|
||||
end
|
||||
end
|
||||
|
|
|
|||
5
test/support/example.ex
Normal file
5
test/support/example.ex
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
defmodule Example do
|
||||
def fact(0), do: 1
|
||||
def fact(1), do: 1
|
||||
def fact(n), do: fact(n - 1) + fact(n - 2)
|
||||
end
|
||||
14
test/support/js_case.ex
Normal file
14
test/support/js_case.ex
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
defmodule BeamJs.JsCase do
|
||||
use ExUnit.CaseTemplate
|
||||
|
||||
using do
|
||||
quote do
|
||||
import BeamJs.JsRuntime, only: [run: 2]
|
||||
end
|
||||
end
|
||||
|
||||
setup _tags do
|
||||
{:ok, pid} = BeamJs.JsRuntime.start_link()
|
||||
%{js: pid}
|
||||
end
|
||||
end
|
||||
|
|
@ -1 +1,2 @@
|
|||
ExUnit.start()
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue