diff --git a/src/erlang_v8_vm.erl b/src/erlang_v8_vm.erl index 6d0f8c4..b819902 100644 --- a/src/erlang_v8_vm.erl +++ b/src/erlang_v8_vm.erl @@ -261,10 +261,12 @@ send_to_port(Port, Op, Ref, Data, _MaxSourceSize) -> receive_port_data(Port). receive_port_data(Port) -> + %% Use of `Ref' below is same as `Context' elsewhere. receive {Port, {data, <<_:8, _Ref:32, "">>}} -> {ok, undefined}; {Port, {data, <>}} -> + %% Defining a named function returns `undefined' from V8, thus expected {ok, undefined}; {Port, {data, <>}} -> case catch jsx:decode(Response, [return_maps]) of @@ -281,6 +283,9 @@ receive_port_data(Port) -> {Port, {data, <>}} -> {call_error, invalid_context}; {Port, {exit_status, _Status}} -> + %% Eval of JS code failed. Probably had syntax error or malformed. + %% No further data available here. Must restart V8. + %% TODO: expand erlang_v8's C++ interface code for fine-grain details {error, crashed}; {Port, Error} -> %% TODO: we should probably special case here. diff --git a/test/erlang_v8_SUITE.erl b/test/erlang_v8_SUITE.erl new file mode 100644 index 0000000..91debff --- /dev/null +++ b/test/erlang_v8_SUITE.erl @@ -0,0 +1,147 @@ +-module(erlang_v8_SUITE). + +-export([ + all/0, + init_per_testcase/2, + end_per_testcase/2 +]). + +-export([ + recover_after_error/1, + invalid_context_test/1 +]). + +-include_lib("common_test/include/ct.hrl"). +-include_lib("eunit/include/eunit.hrl"). + +%%-------------------------------------------------------------------- +%% @public +%% @doc +%% Running tests for this suite +%% @end +%%-------------------------------------------------------------------- +all() -> + [invalid_context_test, + recover_after_error]. + +%%-------------------------------------------------------------------- +%% TEST CASE SETUP +%%-------------------------------------------------------------------- +init_per_testcase(_TestCase, Config) -> + VM = + case erlang_v8:start_vm() of + {ok, Pid} -> Pid; + {error, {already_started, Pid}} -> Pid + end, + [{vm, VM} | Config]. + +%%-------------------------------------------------------------------- +%% TEST CASE TEARDOWN +%%-------------------------------------------------------------------- +end_per_testcase(_TestCase, Config) -> + {vm, VM} = lists:keyfind(vm, 1, Config), + _ = erlang_v8:stop_vm(VM), + lists:delete({vm, VM}, Config). + +%%-------------------------------------------------------------------- +%% TEST CASES +%%-------------------------------------------------------------------- + +%% This demonstrates the breadth and scope of erlang_v8 use cases and +%% limitations. Bottom-line: when in doubt, stop/start V8 vm. +recover_after_error(Config) -> + Payload = erlang:binary_to_list(base64:decode(<<"H4Av/xACRU4=">>)), + Port = 6, + {vm, VM} = lists:keyfind(vm, 1, Config), + {ok, Context1} = erlang_v8:create_context(VM), + + EmptyEval = <<"">>, + ?assertMatch({ok, undefined}, erlang_v8:eval(VM, Context1, EmptyEval)), + + %% Reuse same context: + GoodFunction = <<"function Decoder() { return 42; }">>, + ?assertMatch({ok, undefined}, erlang_v8:eval(VM, Context1, GoodFunction)), + ?assertMatch({ok, 42}, erlang_v8:call(VM, Context1, <<"Decoder">>, [])), + + %% Recover from various errors: + + {ok, Context2} = erlang_v8:create_context(VM), + + BadSymbolInFn = <<"function Decoder() { returrrrrrrn 0; }">>, + ?assertMatch({error, crashed}, erlang_v8:eval(VM, Context2, BadSymbolInFn)), + ?assertMatch( + %% {error, <<"ReferenceError: Decoder is not defined", _/binary>>}, + {error, invalid_context}, + erlang_v8:call(VM, Context2, <<"Decoder">>, [Payload, Port]) + ), + + %% Anything after that will fail until V8 restart + ok = erlang_v8:restart_vm(VM), + + {ok, Context3} = erlang_v8:create_context(VM), + + BadKeyword = <<"defun Decoder() { return 0; }">>, + ?assertMatch({error, crashed}, erlang_v8:eval(VM, Context3, BadKeyword)), + ?assertMatch( + {error, invalid_context}, + erlang_v8:call(VM, Context3, <<"Decoder">>, [Payload, Port]) + ), + + %% Anything after that will fail until V8 restart + ok = erlang_v8:restart_vm(VM), + + {ok, Context4} = erlang_v8:create_context(VM), + + WrongKeyword = <<"for Decoder() { return 0; }">>, + ?assertMatch({error, crashed}, erlang_v8:eval(VM, Context4, WrongKeyword)), + ?assertMatch( + {error, invalid_context}, + erlang_v8:call(VM, Context4, <<"Decoder">>, [Payload, Port]) + ), + + %% Anything after that will fail until V8 restart + ok. + +invalid_context_test(Config) -> + GoodFunction = + <<"function Decoder(bytes, port) {\n" + " var payload = {\"Testing\": \"42\"};\n" + " return payload;\n" + "}">>, + BadFunction = <<"function Decoder() { returrrrrrrn 0; }">>, + + {vm, VM} = lists:keyfind(vm, 1, Config), + {ok, Context1} = erlang_v8:create_context(VM), + {ok, Context2} = erlang_v8:create_context(VM), + ?assertNotMatch(Context1, Context2), + + Payload = erlang:binary_to_list(base64:decode(<<"H4Av/xACRU4=">>)), + Port = 6, + Result = #{<<"Testing">> => <<"42">>}, + + %% Eval good function and ensure function works more than once + ?assertMatch({ok, undefined}, erlang_v8:eval(VM, Context1, GoodFunction)), + ?assertMatch({ok, Result}, erlang_v8:call(VM, Context1, <<"Decoder">>, [Payload, Port])), + ?assertMatch({ok, Result}, erlang_v8:call(VM, Context1, <<"Decoder">>, [Payload, Port])), + + %% Call undefined function + ?assertMatch( + {error, <<"ReferenceError: Decoder is not defined", _/binary>>}, + erlang_v8:call(VM, Context2, <<"Decoder">>, [Payload, Port]) + ), + + %% First Context still works + ?assertMatch({ok, Result}, erlang_v8:call(VM, Context1, <<"Decoder">>, [Payload, Port])), + + %% Eval bad function + ?assertMatch({error, crashed}, erlang_v8:eval(VM, Context2, BadFunction)), + + %% Upon most errors, v8 Port gets killed and restarted, but Erlang PID survives + ?assertMatch(true, erlang:is_process_alive(VM)), + + %% We get invalid context when attempting to reuse first Context: + ?assertMatch( + {error, invalid_context}, + erlang_v8:call(VM, Context1, <<"Decoder">>, [Payload, Port]) + ), + ok.