From 9b1c706bc9133312cf59e4da7f77e2171bf645d1 Mon Sep 17 00:00:00 2001 From: sanmiguel Date: Sat, 25 Apr 2020 15:41:25 +0200 Subject: [PATCH 01/11] gen_statem replaces gen_fsm Upgrade from gen_fsm to gen_statem, this should be a like-for-like replacement as far as I can tell. --- src/websocket_client.erl | 244 +++++++++++++++++++++------------------ 1 file changed, 131 insertions(+), 113 deletions(-) diff --git a/src/websocket_client.erl b/src/websocket_client.erl index acfa35a..9f8eb76 100644 --- a/src/websocket_client.erl +++ b/src/websocket_client.erl @@ -3,7 +3,7 @@ %% @doc Erlang websocket client (FSM implementation) -module(websocket_client). --behaviour(gen_fsm). +-behaviour(gen_statem). %-compile([export_all]). -include("websocket_req.hrl"). @@ -14,19 +14,15 @@ -export([cast/2]). -export([send/2]). +-export([callback_mode/0]). -export([init/1]). -export([terminate/3]). --export([handle_event/3]). --export([handle_sync_event/4]). --export([handle_info/3]). +-export([handle_event/4]). -export([code_change/4]). % States --export([disconnected/2]). -export([disconnected/3]). --export([connected/2]). -export([connected/3]). --export([handshaking/2]). -export([handshaking/3]). -type state_name() :: atom(). @@ -102,7 +98,6 @@ handler :: {module(), HState :: term()}, buffer = <<>> :: binary(), reconnect :: boolean(), - reconnect_tref = undefined :: undefined | reference(), ka_attempts = 0 :: non_neg_integer() }). @@ -139,7 +134,7 @@ start_link(FsmName, URL, Handler, HandlerArgs, Opts) when is_list(Opts) -> case http_uri:parse(URL, [{scheme_defaults, [{ws,80},{wss,443}]}]) of {ok, {Protocol, _, Host, Port, Path, Query}} -> InitArgs = [Protocol, Host, Port, Path ++ Query, Handler, HandlerArgs, Opts], - %FsmOpts = [{dbg, [trace]}], + % FsmOpts = [{debug, [log, trace]}], FsmOpts = [], fsm_start_link(FsmName, InitArgs, FsmOpts); {error, _} = Error -> @@ -147,20 +142,25 @@ start_link(FsmName, URL, Handler, HandlerArgs, Opts) when is_list(Opts) -> end. fsm_start_link(undefined, Args, Options) -> - gen_fsm:start_link(?MODULE, Args, Options); + gen_statem:start_link(?MODULE, Args, Options); fsm_start_link(FsmName, Args, Options) -> - gen_fsm:start_link(FsmName, ?MODULE, Args, Options). + gen_statem:start_link(FsmName, ?MODULE, Args, Options). send(Client, Frame) -> - gen_fsm:sync_send_event(Client, {send, Frame}). + gen_statem:call(Client, {send, Frame}). %% Send a frame asynchronously -spec cast(Client :: pid(), websocket_req:frame()) -> ok. cast(Client, Frame) -> - gen_fsm:send_event(Client, {cast, Frame}). + gen_statem:cast(Client, {cast_frame, Frame}). + +-spec callback_mode() -> state_functions. +callback_mode() -> + state_functions. -spec init(list(any())) -> - {ok, state_name(), #context{}}. + {ok, state_name(), #context{}} + | {ok, state_name(), #context{}, gen_statem:action()}. %% NB DO NOT try to use Timeout to do keepalive. init([Protocol, Host, Port, Path, Handler, HandlerArgs, Opts]) -> {Connect, Reconnect, HState} = @@ -191,8 +191,8 @@ init([Protocol, Host, Port, Path, Handler, HandlerArgs, Opts]) -> handler = {Handler, HState}, reconnect = Reconnect }, - Connect andalso gen_fsm:send_event(self(), connect), - {ok, disconnected, Context0}. + {ok, disconnected, Context0, + [ {next_event, internal, connect} || Connect ]}. -spec transport(ws | wss, {verify | verify_fun, term()}, list(inet:option())) -> #transport{}. @@ -250,7 +250,6 @@ connect(#context{ target={_Protocol, Host, Port, _Path}, ka_attempts=KAs }=Context) -> - Context2 = maybe_cancel_reconnect(Context), case (T#transport.mod):connect(Host, Port, T#transport.opts, 6000) of {ok, Socket} -> WSReq1 = websocket_req:socket(Socket, WSReq0), @@ -258,17 +257,17 @@ connect(#context{ ok -> case websocket_req:keepalive(WSReq1) of infinity -> - {next_state, handshaking, Context2#context{ wsreq=WSReq1}}; + {next_state, handshaking, Context#context{ wsreq=WSReq1}}; KeepAlive -> NewTimer = erlang:send_after(KeepAlive, self(), keepalive), WSReq2 = websocket_req:set([{keepalive_timer, NewTimer}], WSReq1), - {next_state, handshaking, Context2#context{ wsreq=WSReq2, ka_attempts=(KAs+1)}} + {next_state, handshaking, Context#context{ wsreq=WSReq2, ka_attempts=(KAs+1)}} end; Error -> - disconnect(Error, Context2) + disconnect(Error, Context) end; {error,_}=Error -> - disconnect(Error, Context2) + disconnect(Error, Context) end. disconnect(Reason, #context{ @@ -279,98 +278,84 @@ disconnect(Reason, #context{ {ok, HState1} -> {next_state, disconnected, Context#context{buffer = <<>>, handler={Handler, HState1}}}; {reconnect, HState1} -> - ok = gen_fsm:send_event(self(), connect), - {next_state, disconnected, Context#context{handler={Handler, HState1}}}; + {next_state, disconnected, Context#context{handler={Handler, HState1}}, + {next_event, cast, connect}}; {reconnect, Interval, HState1} -> - Tref = gen_fsm:send_event_after(Interval, connect), - {next_state, disconnected, Context#context{handler={Handler, HState1}, reconnect_tref=Tref}}; + {next_state, disconnected, Context#context{handler={Handler, HState1}}, + {{timeout, connect}, Interval, connect}}; {close, Reason1, HState1} -> ok = websocket_close(WSReq0, Handler, HState1, Reason1), {stop, Reason1, Context#context{handler={Handler, HState1}}} end. -disconnected(connect, Context0) -> +disconnected(info, Msg, Context) -> + handle_info(Msg, Context); +disconnected(internal, connect, Context0) -> connect(Context0); -disconnected(_Event, Context) -> - % ignore - {next_state, disconnected, Context}. - -disconnected(connect, _From, Context0) -> - %% TODO FIXME This really seems wrong and too easy +disconnected({timeout, connect}, connect, Context0) -> + connect(Context0); +disconnected(internal, _, _) -> + keep_state_and_data; +disconnected({call, From}, connect, Context0) -> case connect(Context0) of {next_state, State, Context1} -> - {reply, ok, State, Context1}; + {next_state, State, Context1, [{reply, From, ok}]} ; Other -> Other end; -disconnected(_Event, _From, Context) -> - {reply, {error, unhandled_sync_event}, disconnected, Context}. - -connected(connect, Context) -> - %% We didn't cancel the reconnect_tref timer before the event was - %% sent +disconnected({call, From}, _, _) -> + {keep_state_and_data, {reply, From, {error, unhandled_sync_event}}}. + +connected({timeout, connect}, connect, _Context) -> + keep_state_and_data; +connected({timeout, keepalive}, keepalive, Context) -> + handle_keepalive(connected, Context); +connected(info, keepalive, Context) -> + handle_keepalive(connected, Context); +connected(info, {TransClosed, _Sock}, + #context{ + transport=#transport{ closed=TransClosed } %% NB: matched + }=Context) -> + disconnect({remote, closed}, Context); +connected(info, {TransError, _Sock, Reason}, + #context{ + transport=#transport{ error=TransError}, + handler={Handler, HState0}, + wsreq=WSReq + }=Context) -> + ok = websocket_close(WSReq, Handler, HState0, {TransError, Reason}), + {stop, {socket_error, Reason}, Context}; +connected(info, {Trans, _Socket, Data}, + #context{ + transport=#transport{ name=Trans } + }=Context) -> + handle_websocket_frame(Data, Context); +connected(info, Msg, Context) -> + handle_info(Msg, Context); +connected(internal, connect, Context) -> {next_state, connected, Context}; -connected({cast, Frame}, #context{wsreq=WSReq}=Context) -> +connected(cast, {cast_frame, Frame}, #context{wsreq=WSReq}=Context) -> case encode_and_send(Frame, WSReq) of ok -> {next_state, connected, Context}; {error, closed} -> {next_state, disconnected, Context} - end. - -connected({send, Frame}, _From, #context{wsreq=WSReq}=Context) -> - {reply, encode_and_send(Frame, WSReq), connected, Context}; -connected(_Event, _From, Context) -> - {reply, {error, unhandled_sync_event}, connected, Context}. - -handshaking(_Event, Context) -> - {next_state, handshaking, Context}. -handshaking(_Event, _From, Context) -> - {reply, {error, unhandled_sync_event}, handshaking, Context}. - --spec handle_event(Event :: term(), state_name(), #context{}) -> - {next_state, state_name(), #context{}} - | {stop, Reason :: term(), #context{}}. -handle_event(_Event, State, Context) -> - {next_state, State, Context}. %% i.e. ignore, do nothing - --spec handle_sync_event(Event :: term(), {From :: pid(), any()}, state_name(), #context{}) -> - {next_state, state_name(), #context{}} - | {reply, Reply :: term(), state_name(), #context{}} - | {stop, Reason :: term(), #context{}} - | {stop, Reason :: term(), Reply :: term(), #context{}}. -handle_sync_event(Event, {_From, Tag}, State, Context) -> - {reply, {noop, Event, Tag}, State, Context}. - --spec handle_info(Info :: term(), state_name(), #context{}) -> - {next_state, state_name(), #context{}} - | {stop, Reason :: term(), #context{}}. -handle_info(keepalive, KAState, #context{ wsreq=WSReq, ka_attempts=KAAttempts }=Context) - when KAState =:= handshaking; KAState =:= connected -> - [KeepAlive, KATimer, KAMax] = - websocket_req:get([keepalive, keepalive_timer, keepalive_max_attempts], WSReq), - case KATimer of - undefined -> ok; - _ -> erlang:cancel_timer(KATimer) - end, - case KAAttempts of - KAMax-> - disconnect({error, keepalive_timeout}, Context); - _ -> - ok = encode_and_send({ping, <<"foo">>}, WSReq), - NewTimer = erlang:send_after(KeepAlive, self(), keepalive), - WSReq1 = websocket_req:set([{keepalive_timer, NewTimer}], WSReq), - {next_state, KAState, Context#context{wsreq=WSReq1, ka_attempts=(KAAttempts+1)}} end; -%% TODO Move Socket into #transport{} from #websocket_req{} so that we can -%% match on it here -handle_info({TransClosed, _Socket}, _CurrState, - #context{ - transport=#transport{ closed=TransClosed } %% NB: matched - }=Context) -> +connected({call, From}, {send, Frame}, #context{wsreq=WSReq}) -> + {keep_state_and_data, {reply, From, encode_and_send(Frame, WSReq)}}; +connected({call, From}, _Event, _Context) -> + {keep_state_and_data, {reply, From, {error, unhandled_sync_event}}}. + +handshaking({timeout, keepalive}, keepalive, Context) -> + handle_keepalive(handshaking, Context); +handshaking(info, keepalive, Context) -> + handle_keepalive(handshaking, Context); +handshaking(info, {TransClosed, _Sock}, + #context{ + transport=#transport{ closed=TransClosed } %% NB: matched + }=Context) -> disconnect({remote, closed}, Context); -handle_info({TransError, _Socket, Reason}, - _AnyState, +handshaking(info, {TransError, _Sock, Reason}, #context{ transport=#transport{ error=TransError}, handler={Handler, HState0}, @@ -378,8 +363,7 @@ handle_info({TransError, _Socket, Reason}, }=Context) -> ok = websocket_close(WSReq, Handler, HState0, {TransError, Reason}), {stop, {socket_error, Reason}, Context}; -handle_info({Trans, _Socket, Data}, - handshaking, +handshaking(info, {Trans, _Socket, Data}, #context{ transport=#transport{ name=Trans }, wsreq=WSReq1, @@ -408,13 +392,54 @@ handle_info({Trans, _Socket, Data}, handler={Handler, HState2}, buffer= <<>>}) end; -handle_info({Trans, _Socket, Data}, - connected, - #context{ - transport=#transport{ name=Trans } - }=Context) -> - handle_websocket_frame(Data, Context); -handle_info(Msg, State, +handshaking(info, Msg, Context) -> + handle_info(Msg, Context); +handshaking({call, From}, _Event, _Context) -> + {keep_state_and_data, {reply, From, {error, unhandled_sync_event}}}; +handshaking(_EType, _Event, _Context) -> + keep_state_and_data. + +-spec handle_event(EventType::gen_statem:event_type(), + Event::term(), + State::atom(), + #context{}) -> + keep_state_and_data. +handle_event(_, _, _, _) -> + keep_state_and_data. %% i.e. ignore, do nothing + +-spec handle_keepalive(state_name(), #context{}) -> + {next_state, state_name(), #context{}} + | {stop, Reason :: term(), #context{}}. +handle_keepalive(KAState, #context{ wsreq=WSReq, ka_attempts=KAAttempts }=Context) + when KAState =:= handshaking; KAState =:= connected -> + [KeepAlive, KATimer, KAMax] = + websocket_req:get([keepalive, keepalive_timer, keepalive_max_attempts], WSReq), + case KATimer of + undefined -> ok; + _ -> erlang:cancel_timer(KATimer) + end, + case KAAttempts of + KAMax-> + disconnect({error, keepalive_timeout}, Context); + _ -> + % case encode_and_send({ping, <<"foo">>}, WSReq) of + % ok -> + ok = encode_and_send({ping, <<"foo">>}, WSReq), + NewTimer = erlang:send_after(KeepAlive, self(), keepalive), + WSReq1 = websocket_req:set([{keepalive_timer, NewTimer}], WSReq), + {next_state, KAState, Context#context{wsreq=WSReq1, ka_attempts=(KAAttempts+1)}}%%; + % {error, _} = Reason -> + % disconnect(Reason, Context) + % end + + end. + +-spec handle_info(Info :: term(), #context{}) -> + {keep_state, #context{}} + | {stop, Reason::term(), #context{}}. +%% TODO Move Socket into #transport{} from #websocket_req{} so that we can +%% match on it here +handle_info(Msg, #context{ wsreq=WSReq, handler={Handler, HState0}, @@ -424,10 +449,10 @@ handle_info(Msg, State, HandlerResponse -> case handle_response(HandlerResponse, Handler, WSReq) of {ok, WSReqN, HStateN} -> - {next_state, State, Context#context{ - handler={Handler, HStateN}, - wsreq=WSReqN, - buffer=Buffer}}; + {keep_state, Context#context{ + handler={Handler, HStateN}, + wsreq=WSReqN, + buffer=Buffer}}; {close, Reason, WSReqN, Handler, HStateN} -> {stop, Reason, Context#context{ wsreq=WSReqN, @@ -563,10 +588,3 @@ websocket_close(WSReq, Handler, HandlerState, Reason) -> erlang:get_stacktrace()]) end. %% TODO {stop, Reason, Context} - -maybe_cancel_reconnect(Context=#context{reconnect_tref=undefined}) -> - Context; -maybe_cancel_reconnect(Context=#context{reconnect_tref=Tref}) when is_reference(Tref) -> - gen_fsm:cancel_timer(Tref), - Context#context{reconnect_tref=undefined}. - From 0aeb91fa647d2ff594157d4b1391dc0a7796e8c2 Mon Sep 17 00:00:00 2001 From: sanmiguel Date: Sat, 25 Apr 2020 15:47:50 +0200 Subject: [PATCH 02/11] Upgrade stacktrace fetching --- src/websocket_client.erl | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/websocket_client.erl b/src/websocket_client.erl index 9f8eb76..0d15a98 100644 --- a/src/websocket_client.erl +++ b/src/websocket_client.erl @@ -458,7 +458,7 @@ handle_info(Msg, wsreq=WSReqN, handler={Handler, HStateN}}} end - catch Class:Reason -> + catch Class:Reason:StackTrace -> %% TODO Maybe a function_clause catch here to allow %% not having to have a catch-all clause in websocket_info CB? error_logger:error_msg( @@ -468,7 +468,7 @@ handle_info(Msg, "** Handler state was ~p~n" "** Stacktrace: ~p~n~n", [Handler, websocket_info, 3, Class, Reason, Msg, HState0, - erlang:get_stacktrace()]), + StackTrace]), websocket_close(WSReq, Handler, HState0, Reason), {stop, Reason, Context} end. @@ -514,7 +514,7 @@ handle_websocket_frame(Data, #context{}=Context0) -> wsreq=WSReqN2, handler={Handler, HStateN2}}} end - catch Class:Reason -> + catch Class:Reason:StackTrace -> error_logger:error_msg( "** Websocket client ~p terminating in ~p/~p~n" " for the reason ~p:~p~n" @@ -522,7 +522,7 @@ handle_websocket_frame(Data, #context{}=Context0) -> "** Handler state was ~p~n" "** Stacktrace: ~p~n~n", [Handler, websocket_handle, 3, Class, Reason, Message, HState0, - erlang:get_stacktrace()]), + StackTrace]), {stop, Reason, Context#context{ wsreq=WSReqN }} end; {recv, WSReqN, BufferN} -> @@ -578,13 +578,13 @@ encode_and_send(Frame, WSReq) -> websocket_close(WSReq, Handler, HandlerState, Reason) -> try Handler:websocket_terminate(Reason, WSReq, HandlerState) - catch Class:Reason2 -> + catch Class:Reason2:StackTrace -> error_logger:error_msg( "** Websocket handler ~p terminating in ~p/~p~n" " for the reason ~p:~p~n" "** Handler state was ~p~n" "** Stacktrace: ~p~n~n", [Handler, websocket_terminate, 3, Class, Reason2, HandlerState, - erlang:get_stacktrace()]) + StackTrace]) end. %% TODO {stop, Reason, Context} From f0fd7de4b89d9ebbaf8b7fd1c3815883aec02c50 Mon Sep 17 00:00:00 2001 From: sanmiguel Date: Sat, 25 Apr 2020 15:50:47 +0200 Subject: [PATCH 03/11] DEPRECATION: Retire old erlang versions Follow official support model: most recent 3 major versions --- .travis.yml | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/.travis.yml b/.travis.yml index 6629fb0..bc39d87 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,9 +1,8 @@ language: erlang otp_release: - - R16B03 - - 17.5 - - 18.3 - - 19.1 + - 20.3.8.9 + - 21.3.8.9 + - 22.3.2 install: wget https://github.com/erlang/rebar3/releases/download/3.4.2/rebar3 && chmod 755 rebar3 From afc528602a2aa28fad0a07595a1c732d295cb634 Mon Sep 17 00:00:00 2001 From: sanmiguel Date: Sat, 25 Apr 2020 15:58:01 +0200 Subject: [PATCH 04/11] Actually check which erlangs travis supports... --- .travis.yml | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index bc39d87..8369150 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,9 +1,12 @@ language: erlang otp_release: - - 20.3.8.9 - - 21.3.8.9 + - 20.3.8 + - 21.3.8 - 22.3.2 +before_script: + kerl list installations + install: wget https://github.com/erlang/rebar3/releases/download/3.4.2/rebar3 && chmod 755 rebar3 script: ./rebar3 update && ./rebar3 ct && ./rebar3 dialyzer && ./rebar3 coveralls send From 155ddcbe4a18481989dc9fd5c7c1ec078c375ee9 Mon Sep 17 00:00:00 2001 From: sanmiguel Date: Sat, 25 Apr 2020 16:04:22 +0200 Subject: [PATCH 05/11] Lock cowboy-1.1.2 for tests --- rebar.config | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rebar.config b/rebar.config index 984bb82..914a768 100644 --- a/rebar.config +++ b/rebar.config @@ -1,7 +1,7 @@ {profiles, [ {test, [ {deps, [ - cowboy, recon, + {cowboy, "1.1.2"}, recon, { proper, {git, "https://github.com/manopapad/proper.git", {tag, "v1.2"}} } ] } ]} ]}. From 087c0257bf6c1891c0850a00d382d07b3bc4da54 Mon Sep 17 00:00:00 2001 From: sanmiguel Date: Sat, 25 Apr 2020 16:06:05 +0200 Subject: [PATCH 06/11] Remove Erlang R20 --- .travis.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 8369150..a564564 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,6 +1,5 @@ language: erlang otp_release: - - 20.3.8 - 21.3.8 - 22.3.2 From d373b1f1e2cebaa5f16a459902dea73f5ca94c98 Mon Sep 17 00:00:00 2001 From: sanmiguel Date: Sat, 25 Apr 2020 16:12:02 +0200 Subject: [PATCH 07/11] Upgrade rebar3 --- .travis.yml | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/.travis.yml b/.travis.yml index a564564..153efd8 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,9 +3,6 @@ otp_release: - 21.3.8 - 22.3.2 -before_script: - kerl list installations - -install: wget https://github.com/erlang/rebar3/releases/download/3.4.2/rebar3 && chmod 755 rebar3 +install: wget https://github.com/erlang/rebar3/releases/download/3.13.1/rebar3 && chmod 755 rebar3 script: ./rebar3 update && ./rebar3 ct && ./rebar3 dialyzer && ./rebar3 coveralls send From 783bb16001f103b1e8a7fafbc7144354489dc1b3 Mon Sep 17 00:00:00 2001 From: sanmiguel Date: Sat, 25 Apr 2020 17:32:04 +0200 Subject: [PATCH 08/11] dialyzer doesn't believe in gen_statem:action() --- src/websocket_client.erl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/websocket_client.erl b/src/websocket_client.erl index 0d15a98..79d2b6b 100644 --- a/src/websocket_client.erl +++ b/src/websocket_client.erl @@ -160,7 +160,7 @@ callback_mode() -> -spec init(list(any())) -> {ok, state_name(), #context{}} - | {ok, state_name(), #context{}, gen_statem:action()}. + | {ok, state_name(), #context{}, [{next_event, internal, connect}]}. %% NB DO NOT try to use Timeout to do keepalive. init([Protocol, Host, Port, Path, Handler, HandlerArgs, Opts]) -> {Connect, Reconnect, HState} = From 5b75fac7e1dfe6ecc64cbd6ddde267cf461167c0 Mon Sep 17 00:00:00 2001 From: sanmiguel Date: Sun, 26 Apr 2020 15:16:24 +0200 Subject: [PATCH 09/11] Add test for reply from websocket_handle --- test/increment_client.erl | 57 +++++++++++++++++++++++++++++++++++++++ test/wc_SUITE.erl | 13 +++++++-- 2 files changed, 68 insertions(+), 2 deletions(-) create mode 100644 test/increment_client.erl diff --git a/test/increment_client.erl b/test/increment_client.erl new file mode 100644 index 0000000..320f824 --- /dev/null +++ b/test/increment_client.erl @@ -0,0 +1,57 @@ +-module(increment_client). + +-behaviour(websocket_client). + +-export([ + start_link/1, + stop/1 + ]). +-export([ + init/1, + onconnect/2, + ondisconnect/2, + websocket_handle/3, + websocket_info/3, + websocket_terminate/3 + ]). + +-record(state, { + waiting = undefined :: undefined | pid() + }). + +start_link(Url) -> + {ok, _} = websocket_client:start_link(Url, ?MODULE, [self()]). + +stop(Pid) -> + Pid ! stop. + +init([Waiting]) -> + {once, #state{waiting=Waiting}}. + +onconnect(_WSReq, State) -> + State#state.waiting ! {ok, self()}, + self() ! {send, {binary, <<"1">>}}, + {ok, State}. + +ondisconnect(Reason, State) -> + {close, Reason, State}. + +websocket_handle({binary, <<"10">>}, _, State) -> + State#state.waiting ! {done, self()}, + % TODO Send something via message to State#state.waiting + {ok, State}; +websocket_handle({binary, NBin}, _, State) -> + %% TODO Add when clause ensuring only integers < 10 are used? + N = binary_to_integer(NBin), + Reply = {binary, integer_to_binary(N + 1)}, + {reply, Reply, State}; +websocket_handle(_, _, State) -> + {ok, State}. + +websocket_info({send, Payload}, _, State) -> + {reply, Payload, State}; +websocket_info(stop, _, State) -> + {close, <<>>, State}. + +websocket_terminate(_, _, _) -> + ok. diff --git a/test/wc_SUITE.erl b/test/wc_SUITE.erl index 415158a..f538692 100644 --- a/test/wc_SUITE.erl +++ b/test/wc_SUITE.erl @@ -16,7 +16,8 @@ test_bad_request/1, test_keepalive_opt/1, test_keepalive_timeout/1, - test_reconnect_after/1 + test_reconnect_after/1, + test_reply_from_handle/1 ]). all() -> @@ -28,7 +29,8 @@ all() -> test_bad_request, test_keepalive_opt, test_keepalive_timeout, - test_reconnect_after + test_reconnect_after, + test_reply_from_handle ]. init_per_suite(Config) -> @@ -157,6 +159,13 @@ test_reconnect_after(_) -> reconnect_interval_client:stop(Pid), ok. +test_reply_from_handle(_) -> + {ok, Pid} = increment_client:start_link("ws://localhost:8080"), + receive {ok, Pid} -> ok after 500 -> ct:fail(timeout) end, + receive {done, Pid} -> ok after 1500 -> ct:fail(timeout) end, + increment_client:stop(Pid), + ok. + short_msg() -> <<"hello">>. medium_msg() -> From 150778cd2b3ae507b871b8b1100463d838446b8b Mon Sep 17 00:00:00 2001 From: sanmiguel Date: Sun, 26 Apr 2020 15:33:55 +0200 Subject: [PATCH 10/11] Add tiny test to ensure all our websocket_req getters work --- test/wsreq_SUITE.erl | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 test/wsreq_SUITE.erl diff --git a/test/wsreq_SUITE.erl b/test/wsreq_SUITE.erl new file mode 100644 index 0000000..6216a2e --- /dev/null +++ b/test/wsreq_SUITE.erl @@ -0,0 +1,33 @@ +-module(wsreq_SUITE). + +-compile([export_all]). + +-include_lib("common_test/include/ct.hrl"). +-include("../src/websocket_req.hrl"). + +all() -> + [ + t_websocket_req_set + ]. + +suite() -> + [{ct_hooks,[cth_surefire]}, {timetrap, {seconds, 30}}]. + + +init_per_suite(Config) -> + Config. + +end_per_suite(_Config) -> + ok. + +t_websocket_req_set(_) -> + CleanReq = #websocket_req{}, + Fields = record_info(fields, websocket_req), + %% Call websocket_req:set([{Field, Value}], #websocket_req{}) + [ begin + Req1 = websocket_req:set([{Field, new_value}], CleanReq), + Req1 /= CleanReq orelse ct:fail({unchanged, Field}) + end + || Field <- Fields ], + ok. + From 828cec61dcac357b4b23437a9bdf2cab9d77faf1 Mon Sep 17 00:00:00 2001 From: sanmiguel Date: Sun, 26 Apr 2020 15:39:42 +0200 Subject: [PATCH 11/11] Correct note on implementation --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index a2e5f23..dba0565 100644 --- a/README.md +++ b/README.md @@ -86,7 +86,7 @@ contiguous text or binary websocket frames. ## TODO -The client has been significantly reworked, now backed by `gen_fsm`. There may still be bugs. +The client has been significantly reworked, now backed by `gen_statem`. There may still be bugs. Please report them. 1. Stop using `verify_none` by default