diff options
-rw-r--r-- | lib/stdlib/doc/src/gen_statem.xml | 272 | ||||
-rw-r--r-- | lib/stdlib/src/gen_statem.erl | 134 | ||||
-rw-r--r-- | lib/stdlib/test/gen_statem_SUITE.erl | 242 |
3 files changed, 439 insertions, 209 deletions
diff --git a/lib/stdlib/doc/src/gen_statem.xml b/lib/stdlib/doc/src/gen_statem.xml index f7ce0f61ae..f608e10469 100644 --- a/lib/stdlib/doc/src/gen_statem.xml +++ b/lib/stdlib/doc/src/gen_statem.xml @@ -31,8 +31,15 @@ <module>gen_statem</module> <modulesummary>Generic State Machine Behaviour</modulesummary> <description> - <p>A behaviour module for implementing a state machine, primarily - a finite state machine, but an infinite state space is possible. + <p>A behaviour module for implementing a state machine. + Two callback modes are supported. One for a finite state + machine like <seealso marker="gen_fsm">gen_fsm</seealso> + that require the state to be an atom and use that state as + the name of the callback function for a particular state, + and one without restriction on the state that use the same + callback function for all states. + </p> + <p> A generic state machine process (gen_statem) implemented using this module will have a standard set of interface functions and include functionality for tracing and error reporting. @@ -55,7 +62,7 @@ gen_statem:stop -----> Module:terminate/2 gen_statem:call gen_statem:cast erlang:send -erlang:'!' -----> Module:State/5 +erlang:'!' -----> Module:StateName/5 Module:handle_event/5 - -----> Module:terminate/3 @@ -75,47 +82,39 @@ erlang:'!' -----> Module:State/5 <p>The "<em>state function</em>" for a specific <seealso marker="#type-state">state</seealso> in a gen_statem is the callback function that is called - for all events in this state. - An event can can be postponed causing it to be retried - after the state has changed, or more precisely; - after a state change all postponed events are retried - in the new state. + for all events in this state, and is selected depending on + <seealso marker="#type-callback_mode">callback_mode</seealso> + that the implementation selects during gen_statem init. </p> - <p>The state machine - <seealso marker="#type-state"><c>State</c></seealso> - is normally an atom in which case the - <seealso marker="#state_function">state function</seealso> - that will be called is - <seealso marker="#Module:State/5"><c>Module:State/5</c></seealso>. - For a - <seealso marker="#type-state"><c>State</c></seealso> - that is <em>not</em> an atom the - <seealso marker="#state_function">state function</seealso> - <seealso marker="#Module:handle_event/5"> - <c>Module:handle_event/5</c> - </seealso> will be called. - If you use <c>handle_event</c> as a - <seealso marker="#type-state">state</seealso> and later - decide to use non-atom states you will then have to - rewrite your code to stop using that state. + <p>When + <seealso marker="#type-callback_mode">callback_mode</seealso> + is <c>state_functions</c> the state has to be an atom and + is used as the state function name. + See + <seealso marker="#Module:StateName/5"> + <c>Module:StateName/5</c> + </seealso>. + This naturally collects all code for a specific state + in one function and hence dispatches on state first. </p> - <p>When using an atom-only - <seealso marker="#type-state">State</seealso> - it becomes fairly obvious in the implementation code - which events are handled in which state - since there are different callback functions for different states. + <p>When + <seealso marker="#type-callback_mode">callback_mode</seealso> + is <c>handle_event_function</c> the state can be any term + and the state function name is + <seealso marker="#Module:handle_event/5"> + <c>Module:handle_event/5</c> + </seealso>. + This makes it easy to dispatch on state or on event as + you like but you will have to implement it. + Also be careful about which events you handle in which + states so you do not accidentally postpone one event + forever creating an infinite busy loop. </p> <p> - When using a non-atom <seealso marker="#type-state">State</seealso> - all events are handled in the callback function - <seealso marker="#Module:handle_event/5"> - <c>Module:handle_event/5</c> - </seealso> - so it may require more coding discipline to ensure what events - are handled in which state. Therefore it might be a wee bit - easier to accidentally postpone an event in two or more states - and not handling it in any of them causing a tight infinite - loop when the event bounces to be retried between the states. + An event can can be postponed causing it to be retried + after the state has changed, or more precisely; + after a state change all postponed events are retried + in the new state. </p> <p>A gen_statem handles system messages as documented in <seealso marker="sys">sys</seealso>. @@ -239,19 +238,23 @@ erlang:'!' -----> Module:State/5 <datatype> <name name="state" /> <desc> - <p>If the gen_statem <c>State</c> is an <c>atom()</c>, the - <seealso marker="#state_function">state function</seealso> is - <seealso marker="#Module:State/5">Module:State/5</seealso>. - If it is any other <c>term()</c> the - <seealso marker="#state_function">state function</seealso> is - <seealso marker="#Module:handle_event/5"> - Module:handle_event/5 - </seealso>. After a state change (<c>NewState =/= State</c>) + <p>After a state change (<c>NewState =/= State</c>) all postponed events are retried. </p> </desc> </datatype> <datatype> + <name name="state_name" /> + <desc> + <p>If + <seealso marker="#type-callback_mode"> + callback_mode + </seealso> is <c>state_functions</c>, which is the default, + the state has to be of this type i.e an <c>atom()</c>. + </p> + </desc> + </datatype> + <datatype> <name name="state_data" /> <desc> <p>A <c>term()</c> in which the state machine implementation @@ -297,6 +300,33 @@ erlang:'!' -----> Module:State/5 </desc> </datatype> <datatype> + <name name="init_option" /> + <desc> + <p>Option that only is valid when initializing the gen_statem</p> + </desc> + </datatype> + <datatype> + <name name="callback_mode" /> + <desc> + <taglist> + <tag><c>state_functions</c></tag> + <item>The state has to be of type + <seealso marker="#type-state_name"><c>state_name()</c></seealso> + and one callback function per state that is + <seealso marker="#Module:StateName/5"> + <c>Module:StateName/5</c> + </seealso> is used. This is the default. + </item> + <tag><c>handle_event_function</c></tag> + <item>The state can be any term and the callback function + <seealso marker="#Module:handle_event/5"> + <c>Module:handle_event/5</c> + </seealso> is used for all states. + </item> + </taglist> + </desc> + </datatype> + <datatype> <name name="state_op" /> <desc> <p>Either a @@ -472,6 +502,53 @@ erlang:'!' -----> Module:State/5 </taglist> </desc> </datatype> + <datatype> + <name name="state_callback_result" /> + <desc> + <taglist> + <tag> + <c>{stop,<anno>Reason</anno>,<anno>NewStateData</anno>}</c> + </tag> + <item>The same as + <c>{stop,<anno>Reason</anno>,[],<anno>NewStateData</anno>}</c> + </item> + <tag><c>{stop, + <anno>Reason</anno>, + <anno>Replies</anno>, + <anno>NewStateData</anno>}</c> + </tag> + <item>The gen_statem will first send all + <c><anno>Replies</anno></c> and then terminate by calling + <seealso marker="#Module:terminate/3"> + <c>Module:terminate/3</c> + </seealso> with <c>Reason</c>. + </item> + <tag> + <c> + {next_state,<anno>NewState</anno>,<anno>NewStateData</anno>} + </c> + </tag> + <item>The same as + <c> + {next_state,<anno>NewState</anno>,<anno>NewStateData</anno>,[]} + </c> + </item> + <tag> + <c> + {next_state, + <anno>NewState</anno>, + <anno>NewStateData</anno>, + <anno>StateOps</anno>} + </c> + </tag> + <item>The gen_statem will do a state transition to + <c><anno>NewState</anno></c> + (which may be the same as <c>State</c>) + and execute all <c>StateOps</c> + </item> + </taglist> + </desc> + </datatype> </datatypes> <funcs> @@ -777,13 +854,16 @@ erlang:'!' -----> Module:State/5 <p><c><anno>Module</anno></c>, <c><anno>Options</anno></c> and <c><anno>Server</anno></c> have the same meanings as when calling - <seealso marker="#start_link/3">gen_statem:start[_link]/3,4</seealso>. + <seealso marker="#start_link/3"> + gen_statem:start[_link]/3,4 + </seealso>. However, the <seealso marker="#type-server_name"> <c>server_name()</c> </seealso> name must have been registered accordingly <em>before</em> this function is called.</p> - <p><c><anno>State</anno></c> and <c><anno>StateData</anno></c> + <p><c><anno>State</anno></c>, <c><anno>StateData</anno></c> + and <c><anno>StateOps</anno></c> have the same meanings as in the return value of <seealso marker="#Module:init/1">Module:init/1</seealso>. Also, the callback module <c><anno>Module</anno></c> @@ -821,8 +901,13 @@ erlang:'!' -----> Module:State/5 <v> | {ok,State,StateData,StateOps}</v> <v> | {stop,Reason} | ignore</v> <v>State = <seealso marker="#type-state">state()</seealso></v> - <v>StateData = <seealso marker="#type-state_data">state_data()</seealso></v> - <v>StateOps = [<seealso marker="#type-state_op">state_op()</seealso>]</v> + <v>StateData = + <seealso marker="#type-state_data">state_data()</seealso> + </v> + <v>StateOps = + [<seealso marker="#type-state_op">state_op()</seealso> + | <seealso marker="#type-init_option">init_option()</seealso>] + </v> <v>Reason = term()</v> </type> <desc> @@ -843,10 +928,15 @@ erlang:'!' -----> Module:State/5 of the gen_statem. </p> <p>The <seealso marker="#type-state_op"><c>StateOps</c></seealso> - are executed before entering the first + are executed when entering the first <seealso marker="#type-state">state</seealso> just as for a <seealso marker="#state_function">state function</seealso>. </p> + <p>This function allows an option to select the callback mode + of the gen_statem. See + <seealso marker="#type-init_option">init_option</seealso>. + This option is not allowed from the state function(s). + </p> <p>If something goes wrong during the initialization the function should return <c>{stop,Reason}</c> or <c>ignore</c>. See @@ -856,10 +946,10 @@ erlang:'!' -----> Module:State/5 </func> <func> - <name>Module:handle_event(EventType, EventContent, - PrevState, State, StateData) -> Result + <name>Module:StateName(EventType, EventContent, + PrevStateName, StateName, StateData) -> Result </name> - <name>Module:State(EventType, EventContent, + <name>Module:handle_event(EventType, EventContent, PrevState, State, StateData) -> Result </name> <fsummary>Handle an event</fsummary> @@ -868,41 +958,23 @@ erlang:'!' -----> Module:State/5 <seealso marker="#type-event_type">event_type()</seealso> </v> <v>EventContent = term()</v> - <v>Result = {stop,Reason,NewStateData}</v> - <d> The same as <c>{stop,Reason,[],NewStateData}</c></d> - <v> | {stop,Reason,Reply,NewStateData}</v> - <d> The same as - <c>{stop,Reason,[Reply],NewStateData}</c> - </d> - <v> | {stop,Reason,Replies,NewStateData}</v> - <d> The gen_statem will first send all - <c>Replies</c> and then call - <seealso marker="#Module:terminate/3"> - <c>Module:terminate/3</c> - </seealso> with <c>Reason</c> and terminate. - </d> - <v> | {next_state,NewState,NewStateData}</v> - <d> The same as - <c>{next_state,NewState,NewStateData,NewStateData,[]}</c> - </d> - <v> | {next_state,NewState,NewStateData,StateOps}</v> - <d> The gen_statem will do a state transition to - <c>NewState</c> (which may be the same as <c>State</c>) - and execute all <c>StateOps</c> - </d> - <v>Reason = term()</v> - <v>PrevState = State = NewState = + <v>PrevStateName = + <seealso marker="#type-state_name">state_name()</seealso> + | reference() + </v> + <v>StateName = + <seealso marker="#type-state_name">state_name()</seealso> + </v> + <v>PrevState = State = <seealso marker="#type-state">state()</seealso> </v> <v>StateData = NewStateData = <seealso marker="#type-state_data">state_data()</seealso> </v> - <v>Reply = - <seealso marker="#type-reply_operation">reply_operation()</seealso> - </v> - <v>Replies = [Reply]</v> - <v>StateOps = - [<seealso marker="#type-state_op">state_op()</seealso>] + <v>Result = + <seealso marker="#type-state_callback_result"> + state_callback_result() + </seealso> </v> </type> <desc> @@ -923,19 +995,18 @@ erlang:'!' -----> Module:State/5 <c>gen_statem:reply(Client, Reply)</c> </seealso>. </p> - <p><seealso marker="#type-state"><c>State</c></seealso> - is the internal state of the gen_statem which - when <c>State</c> is an <c>atom()</c> - is the same as this function's name, so it is seldom useful, - except for example when comparing with <c>PrevState</c> - that is the gen_statem's previous state, or in - <seealso marker="#Module:handle_event/5"> - Module:handle_event/5 - </seealso> since that function is common for all states - that are not an <c>atom()</c>. - </p> - <p>If this function returns with - <seealso marker="#type-state"><c>NewState =/= State</c></seealso> + <p><c>StateName</c> is rarely useful. One exception is if + you call a common event handling function from your state + function then you might want to pass <c>StateName</c>. + </p> + <p><c>PrevStateName</c> and <c>PrevState</c> are rarely useful. + They can be used when you want to do something only at the + first event in a state. Note that when gen_statem enters + its first state this is set to a <c>reference()</c> since + that can not match the state. + </p> + <p>If this function returns with a new state that + does not match equal (<c>=/=</c>) to the current state all postponed events will be retried in the new state. </p> <p>See <seealso marker="#type-state_op">state_op()</seealso> @@ -946,7 +1017,7 @@ erlang:'!' -----> Module:State/5 </func> <func> - <name>Module:terminate(Reason, State, StateData)</name> + <name>Module:terminate(Reason, State, StateData) -> Ignored</name> <fsummary>Clean up before termination</fsummary> <type> <v>Reason = normal | shutdown | {shutdown,term()} | term()</v> @@ -956,6 +1027,7 @@ erlang:'!' -----> Module:State/5 state_data() </seealso> </v> + <v>Ignored = term()</v> </type> <desc> <p>This function is called by a gen_statem when it is about to diff --git a/lib/stdlib/src/gen_statem.erl b/lib/stdlib/src/gen_statem.erl index 7c3cd8c2f3..e6bb38fc2c 100644 --- a/lib/stdlib/src/gen_statem.erl +++ b/lib/stdlib/src/gen_statem.erl @@ -44,6 +44,9 @@ -export( [wakeup_from_hibernate/3]). +%% Fix problem for doc build +-export_type([state_callback_result/0]). + %%%========================================================================== %%% Interface functions. %%%========================================================================== @@ -51,14 +54,18 @@ -type client() :: {To :: pid(), Tag :: term()}. % Reply-to specifier for call -type state() :: - atom() | % Calls state callback function State/5 - term(). % Calls state callback function handle_event/5 + state_name() | % For state callback function StateName/5 + term(). % For state callback function handle_event/5 +-type state_name() :: atom(). -type state_data() :: term(). -type event_type() :: {'call',Client :: client()} | 'cast' | 'info' | 'timeout' | 'internal'. -type event_predicate() :: % Return true for the event in question fun((event_type(), term()) -> boolean()). +-type init_option() :: + {'callback_mode', callback_mode()}. +-type callback_mode() :: 'state_functions' | 'handle_event_function'. -type state_op() :: %% First NewState and NewStateData are set, %% then all state_operations() are executed in order of @@ -97,6 +104,21 @@ -type reply_operation() :: {'reply', % Reply to a client Client :: client(), Reply :: term()}. +-type state_callback_result() :: + {stop, % Stop the server + Reason :: term(), + NewStateData :: state_data()} | + {stop, % Stop the server + Reason :: term(), + Replies :: [reply_operation()] | reply_operation(), + NewStateData :: state_data()} | + {next_state, % {next_state,NewState,NewStateData,[]} + NewState :: state(), + NewStateData :: state_data()} | + {next_state, % State transition, maybe to the same state + NewState :: state(), + NewStateData :: state_data(), + StateOps :: [state_op()] | state_op()}. %% The state machine init function. It is called only once and %% the server is not running until this function has returned @@ -104,40 +126,36 @@ %% for all events to this server. -callback init(Args :: term()) -> {'ok', state(), state_data()} | - {'ok', state(), state_data(), [state_op()]} | + {'ok', state(), state_data(), [state_op()|init_option()]} | 'ignore' | {'stop', Reason :: term()}. -%% An example callback for a fictive state 'handle_event' -%% that you should avoid having. See below. +%% Example callback for callback_mode =:= state_functions +%% state name 'state_name'. +%% +%% In this mode all states has to be type state_name() i.e atom(). %% %% Note that state callbacks and only state callbacks have arity 5 %% and that is intended. +-callback state_name( + event_type(), + EventContent :: term(), + PrevStateName :: state_name() | reference(), + StateName :: state_name(), % Current state + StateData :: state_data()) -> + state_callback_result(). +%% +%% Callback for callback_mode =:= handle_event_function. %% -%% You should not actually use 'handle_event' as a state name, -%% since it is the callback function that is used if you would use -%% a State that is not an atom(). This is because since there is -%% no obvious way to decide on a state function name from any term(). +%% Note that state callbacks and only state callbacks have arity 5 +%% and that is intended. -callback handle_event( event_type(), EventContent :: term(), PrevState :: state(), State :: state(), % Current state StateData :: state_data()) -> - {stop, % Stop the server - Reason :: term(), - NewStateData :: state_data()} | - {stop, % Stop the server - Reason :: term(), - [reply_operation()] | reply_operation(), - NewStateData :: state_data()} | - {next_state, % {next_state,NewState,NewStateData,[]} - NewState :: state(), - NewStateData :: state_data()} | - {next_state, % State transition, maybe to the same state - NewState :: state(), - NewStateData :: state_data(), - [state_op()] | state_op()}. + state_callback_result(). %% Clean up before the server terminates. -callback terminate( @@ -171,8 +189,11 @@ -optional_callbacks( [format_status/2, % Has got a default implementation - handle_event/5]). % Only needed for State not an atom() -%% For every atom() State there has to be a State/5 callback function + %% + state_name/5, % Example for callback_mode =:= state_functions: + %% there has to be a StateName/5 callback function for every StateName. + %% + handle_event/5]). % For callback_mode =:= handle_event_function %% Type validation functions client({Pid,Tag}) when is_pid(Pid), is_reference(Tag) -> @@ -366,7 +387,8 @@ enter_loop(Module, Options, State, StateData) -> -spec enter_loop( Module :: module(), Options :: [debug_opt()], State :: state(), StateData :: state_data(), - Server_or_StateOps :: server_name() | pid() | [state_op()]) -> + Server_or_StateOps :: + server_name() | pid() | [state_op()|init_option()]) -> no_return(). enter_loop(Module, Options, State, StateData, Server_or_StateOps) -> if @@ -384,7 +406,7 @@ enter_loop(Module, Options, State, StateData, Server_or_StateOps) -> Module :: module(), Options :: [debug_opt()], State :: state(), StateData :: state_data(), Server :: server_name() | pid(), - StateOps :: [state_op()]) -> + StateOps :: [state_op()|init_option()]) -> no_return(). enter_loop(Module, Options, State, StateData, Server, StateOps) -> Parent = gen:get_parent(), @@ -410,11 +432,12 @@ do_send(Proc, Msg) -> end. %% Here init_it and all enter_loop functions converge -enter(Module, Options, State, StateData, Server, StateOps, Parent) -> +enter(Module, Options, State, StateData, Server, InitOps, Parent) -> Name = gen:get_proc_name(Server), Debug = gen:debug_options(Name, Options), PrevState = make_ref(), S = #{ + callback_mode => state_functions, module => Module, name => Name, prev_state => PrevState, @@ -423,9 +446,17 @@ enter(Module, Options, State, StateData, Server, StateOps, Parent) -> timer => undefined, postponed => [], hibernate => false}, - loop_event_state_ops( - Parent, Debug, S, [], {event,undefined}, - State, StateData, [{retry,false}|StateOps]). + case collect_init_options(InitOps) of + {CallbackMode,StateOps} -> + loop_event_state_ops( + Parent, Debug, + S#{callback_mode := CallbackMode}, + [], {event,undefined}, + State, StateData, + [{retry,false}|StateOps]); + [Reason] -> + terminate(Reason, Debug, S, []) + end. %%%========================================================================== %%% gen callbacks @@ -643,13 +674,13 @@ loop_receive(Parent, Debug, S, Event, Timer) -> %% The loop_event* functions optimize S map handling by dismantling it, %% passing the parts in arguments to avoid map lookups and construct the %% new S map in one go on exit. Premature optimization, I know, but -%% the code was way to readable and there were quite some map lookups -%% repeated in different functions. +%% there were quite some map lookups repeated in different functions. loop_events(Parent, Debug, S, [], _Timer) -> loop(Parent, Debug, S); loop_events( Parent, Debug, - #{module := Module, + #{callback_mode := CallbackMode, + module := Module, prev_state := PrevState, state := State, state_data := StateData} = S, @@ -657,11 +688,11 @@ loop_events( _ = (Timer =/= undefined) andalso cancel_timer(Timer), Func = - if - is_atom(State) -> - State; - true -> - handle_event + case CallbackMode of + handle_event_function -> + handle_event; + state_functions -> + State end, try Module:Func(Type, Content, PrevState, State, StateData) of Result -> @@ -675,7 +706,10 @@ loop_events( %% Process an undef to check for the simple mistake %% of calling a nonexistent state function case erlang:get_stacktrace() of - [{Module,State,[Event,StateData]=Args,_}|Stacktrace] -> + [{Module,Func, + [Type,Content,PrevState,State,StateData]=Args, + _} + |Stacktrace] -> terminate( error, {undef_state_function,{Module,State,Args}}, @@ -709,7 +743,7 @@ loop_event_result(Parent, Debug, S, Events, Event, Result) -> end, BadReplies = reply_then_terminate(Reason, Debug, NewS, Q, Replies), - %% Since it returned Replies was bad + %% Since we got back here Replies was bad terminate( {bad_return_value,{stop,Reason,BadReplies,NewStateData}}, Debug, NewS, Q); @@ -794,6 +828,24 @@ loop_event_state_ops( %%--------------------------------------------------------------------------- %% Server helpers +collect_init_options(InitOps) -> + collect_init_options(lists:reverse(InitOps), state_functions, []). +%% Keep the last of each kind +collect_init_options([], CallbackMode, StateOps) -> + {CallbackMode,StateOps}; +collect_init_options([InitOp|InitOps] = IOIOs, CallbackMode, StateOps) -> + case InitOp of + {callback_mode,Mode} + when Mode =:= state_functions; + Mode =:= handle_event_function -> + collect_init_options(InitOps, Mode, StateOps); + {callback_mode,_} -> + [{bad_init_ops,IOIOs}]; + _ -> % Collect others as StateOps + collect_init_options( + InitOps, CallbackMode, [InitOp|StateOps]) + end. + collect_state_options(StateOps) -> collect_state_options( lists:reverse(StateOps), false, false, undefined, []). diff --git a/lib/stdlib/test/gen_statem_SUITE.erl b/lib/stdlib/test/gen_statem_SUITE.erl index 51e08f5ec1..8a96f0e8e2 100644 --- a/lib/stdlib/test/gen_statem_SUITE.erl +++ b/lib/stdlib/test/gen_statem_SUITE.erl @@ -19,7 +19,7 @@ %% -module(gen_statem_SUITE). --include_lib("test_server/include/test_server.hrl"). +-include_lib("common_test/include/ct.hrl"). -compile(export_all). -behaviour(gen_statem). @@ -30,7 +30,11 @@ suite() -> [{ct_hooks,[ts_install_cth]}]. all() -> [{group, start}, + {group, start_handle_event}, + {group, stop}, + {group, stop_handle_event}, {group, abnormal}, + {group, abnormal_handle_event}, shutdown, {group, sys}, hibernate, enter_loop]. @@ -38,13 +42,24 @@ groups() -> [{start, [], [start1, start2, start3, start4, start5, start6, start7, start8, start9, start10, start11, start12]}, + {start_handle_event, [], + [start1, start2, start3, start4, start5, start6, start7, + start8, start9, start10, start11, start12]}, {stop, [], [stop1, stop2, stop3, stop4, stop5, stop6, stop7, stop8, stop9, stop10]}, + {stop_handle_event, [], + [stop1, stop2, stop3, stop4, stop5, stop6, stop7, stop8, stop9, stop10]}, {abnormal, [], [abnormal1, abnormal2]}, + {abnormal_handle_event, [], [abnormal1, abnormal2]}, {sys, [], [sys1, call_format_status, error_format_status, terminate_crash_format, + get_state, replace_state]}, + {sys_handle_event, [], + [sys1, + call_format_status, + error_format_status, terminate_crash_format, get_state, replace_state]}]. init_per_suite(Config) -> @@ -53,6 +68,12 @@ init_per_suite(Config) -> end_per_suite(_Config) -> ok. +init_per_group(GroupName, Config) + when GroupName =:= start_handle_event; + GroupName =:= stop_handle_event; + GroupName =:= abnormal_handle_event; + GroupName =:= sys_handle_event -> + [{init_ops,[{callback_mode,handle_event_function}]}|Config]; init_per_group(_GroupName, Config) -> Config. @@ -65,6 +86,7 @@ init_per_testcase(_CaseName, Config) -> %%% dbg:tracer(), %%% dbg:p(all, c), %%% dbg:tpl(gen_statem, cx), +%%% dbg:tpl(sys, cx), [{watchdog, Dog} | Config]. end_per_testcase(_CaseName, Config) -> @@ -88,7 +110,7 @@ end_per_testcase(_CaseName, Config) -> start1(Config) when is_list(Config) -> %%OldFl = process_flag(trap_exit, true), - {ok,Pid0} = gen_statem:start_link(?MODULE, [], []), + {ok,Pid0} = gen_statem:start_link(?MODULE, start_arg(Config, []), []), ok = do_func_test(Pid0), ok = do_sync_func_test(Pid0), stop_it(Pid0), @@ -102,7 +124,7 @@ start1(Config) when is_list(Config) -> %% anonymous w. shutdown start2(Config) when is_list(Config) -> %% Dont link when shutdown - {ok,Pid0} = gen_statem:start(?MODULE, [], []), + {ok,Pid0} = gen_statem:start(?MODULE, start_arg(Config, []), []), ok = do_func_test(Pid0), ok = do_sync_func_test(Pid0), stopped = gen_statem:call(Pid0, {stop,shutdown}), @@ -113,13 +135,15 @@ start2(Config) when is_list(Config) -> start3(Config) when is_list(Config) -> %%OldFl = process_flag(trap_exit, true), - {ok,Pid0} = gen_statem:start(?MODULE, [], [{timeout,5}]), + {ok,Pid0} = + gen_statem:start(?MODULE, start_arg(Config, []), [{timeout,5}]), ok = do_func_test(Pid0), ok = do_sync_func_test(Pid0), stop_it(Pid0), - {error,timeout} = gen_statem:start(?MODULE, sleep, - [{timeout,5}]), + {error,timeout} = + gen_statem:start( + ?MODULE, start_arg(Config, sleep), [{timeout,5}]), %%process_flag(trap_exit, OldFl), ok = verify_empty_msgq(). @@ -128,7 +152,7 @@ start3(Config) when is_list(Config) -> start4(Config) when is_list(Config) -> OldFl = process_flag(trap_exit, true), - ignore = gen_statem:start(?MODULE, ignore, []), + ignore = gen_statem:start(?MODULE, start_arg(Config, ignore), []), process_flag(trap_exit, OldFl), ok = verify_empty_msgq(). @@ -138,14 +162,14 @@ start5(suite) -> []; start5(Config) when is_list(Config) -> OldFl = process_flag(trap_exit, true), - {error,stopped} = gen_statem:start(?MODULE, stop, []), + {error,stopped} = gen_statem:start(?MODULE, start_arg(Config, stop), []), process_flag(trap_exit, OldFl), ok = verify_empty_msgq(). %% anonymous linked start6(Config) when is_list(Config) -> - {ok,Pid} = gen_statem:start_link(?MODULE, [], []), + {ok,Pid} = gen_statem:start_link(?MODULE, start_arg(Config, []), []), ok = do_func_test(Pid), ok = do_sync_func_test(Pid), stop_it(Pid), @@ -157,11 +181,11 @@ start7(Config) when is_list(Config) -> STM = {global,my_stm}, {ok,Pid} = - gen_statem:start_link(STM, ?MODULE, [], []), + gen_statem:start_link(STM, ?MODULE, start_arg(Config, []), []), {error,{already_started,Pid}} = - gen_statem:start_link(STM, ?MODULE, [], []), + gen_statem:start_link(STM, ?MODULE, start_arg(Config, []), []), {error,{already_started,Pid}} = - gen_statem:start(STM, ?MODULE, [], []), + gen_statem:start(STM, ?MODULE, start_arg(Config, []), []), ok = do_func_test(Pid), ok = do_sync_func_test(Pid), @@ -179,9 +203,9 @@ start8(Config) when is_list(Config) -> STM = {local,Name}, {ok,Pid} = - gen_statem:start(STM, ?MODULE, [], []), + gen_statem:start(STM, ?MODULE, start_arg(Config, []), []), {error,{already_started,Pid}} = - gen_statem:start(STM, ?MODULE, [], []), + gen_statem:start(STM, ?MODULE, start_arg(Config, []), []), ok = do_func_test(Pid), ok = do_sync_func_test(Pid), @@ -199,9 +223,9 @@ start9(Config) when is_list(Config) -> STM = {local,Name}, {ok,Pid} = - gen_statem:start_link(STM, ?MODULE, [], []), + gen_statem:start_link(STM, ?MODULE, start_arg(Config, []), []), {error,{already_started,Pid}} = - gen_statem:start(STM, ?MODULE, [], []), + gen_statem:start(STM, ?MODULE, start_arg(Config, []), []), ok = do_func_test(Pid), ok = do_sync_func_test(Pid), @@ -217,11 +241,11 @@ start10(Config) when is_list(Config) -> STM = {global,my_stm}, {ok,Pid} = - gen_statem:start(STM, ?MODULE, [], []), + gen_statem:start(STM, ?MODULE, start_arg(Config, []), []), {error,{already_started,Pid}} = - gen_statem:start(STM, ?MODULE, [], []), + gen_statem:start(STM, ?MODULE, start_arg(Config, []), []), {error,{already_started,Pid}} = - gen_statem:start_link(STM, ?MODULE, [], []), + gen_statem:start_link(STM, ?MODULE, start_arg(Config, []), []), ok = do_func_test(Pid), ok = do_sync_func_test(Pid), @@ -238,19 +262,19 @@ start11(Config) when is_list(Config) -> GlobalSTM = {global,Name}, {ok,Pid} = - gen_statem:start_link(LocalSTM, ?MODULE, [], []), + gen_statem:start_link(LocalSTM, ?MODULE, start_arg(Config, []), []), stop_it(Pid), {ok,_Pid1} = - gen_statem:start_link(LocalSTM, ?MODULE, [], []), + gen_statem:start_link(LocalSTM, ?MODULE, start_arg(Config, []), []), stop_it(Name), {ok,Pid2} = - gen_statem:start(GlobalSTM, ?MODULE, [], []), + gen_statem:start(GlobalSTM, ?MODULE, start_arg(Config, []), []), stop_it(Pid2), receive after 1 -> true end, Result = - gen_statem:start(GlobalSTM, ?MODULE, [], []), + gen_statem:start(GlobalSTM, ?MODULE, start_arg(Config, []), []), io:format("Result = ~p~n",[Result]), {ok,_Pid3} = Result, stop_it(GlobalSTM), @@ -263,11 +287,11 @@ start12(Config) when is_list(Config) -> VIA = {via,dummy_via,my_stm}, {ok,Pid} = - gen_statem:start_link(VIA, ?MODULE, [], []), + gen_statem:start_link(VIA, ?MODULE, start_arg(Config, []), []), {error,{already_started,Pid}} = - gen_statem:start_link(VIA, ?MODULE, [], []), + gen_statem:start_link(VIA, ?MODULE, start_arg(Config, []), []), {error,{already_started,Pid}} = - gen_statem:start(VIA, ?MODULE, [], []), + gen_statem:start(VIA, ?MODULE, start_arg(Config, []), []), ok = do_func_test(Pid), ok = do_sync_func_test(Pid), @@ -279,23 +303,23 @@ start12(Config) when is_list(Config) -> %% Anonymous, reason 'normal' -stop1(_Config) -> - {ok,Pid} = gen_statem:start(?MODULE, [], []), +stop1(Config) -> + {ok,Pid} = gen_statem:start(?MODULE, start_arg(Config, []), []), ok = gen_statem:stop(Pid), false = erlang:is_process_alive(Pid), noproc = ?EXPECT_FAILURE(gen_statem:stop(Pid), Reason). %% Anonymous, other reason -stop2(_Config) -> - {ok,Pid} = gen_statem:start(?MODULE, [], []), +stop2(Config) -> + {ok,Pid} = gen_statem:start(?MODULE, start_arg(Config, []), []), ok = gen_statem:stop(Pid, other_reason, infinity), false = erlang:is_process_alive(Pid), ok. %% Anonymous, invalid timeout -stop3(_Config) -> - {ok,Pid} = gen_statem:start(?MODULE, [], []), +stop3(Config) -> + {ok,Pid} = gen_statem:start(?MODULE, start_arg(Config, []), []), _ = ?EXPECT_FAILURE( gen_statem:stop(Pid, other_reason, invalid_timeout), @@ -306,8 +330,10 @@ stop3(_Config) -> ok. %% Registered name -stop4(_Config) -> - {ok,Pid} = gen_statem:start({local,to_stop},?MODULE, [], []), +stop4(Config) -> + {ok,Pid} = + gen_statem:start( + {local,to_stop},?MODULE, start_arg(Config, []), []), ok = gen_statem:stop(to_stop), false = erlang:is_process_alive(Pid), noproc = @@ -315,9 +341,11 @@ stop4(_Config) -> ok. %% Registered name and local node -stop5(_Config) -> +stop5(Config) -> Name = to_stop, - {ok,Pid} = gen_statem:start({local,Name},?MODULE, [], []), + {ok,Pid} = + gen_statem:start( + {local,Name},?MODULE, start_arg(Config, []), []), ok = gen_statem:stop({Name,node()}), false = erlang:is_process_alive(Pid), noproc = @@ -325,9 +353,9 @@ stop5(_Config) -> ok. %% Globally registered name -stop6(_Config) -> +stop6(Config) -> STM = {global,to_stop}, - {ok,Pid} = gen_statem:start(STM, ?MODULE, [], []), + {ok,Pid} = gen_statem:start(STM, ?MODULE, start_arg(Config, []), []), ok = gen_statem:stop(STM), false = erlang:is_process_alive(Pid), noproc = @@ -335,11 +363,10 @@ stop6(_Config) -> ok. %% 'via' registered name -stop7(_Config) -> +stop7(Config) -> VIA = {via,dummy_via,to_stop}, dummy_via:reset(), - {ok,Pid} = gen_statem:start(VIA, - ?MODULE, [], []), + {ok,Pid} = gen_statem:start(VIA, ?MODULE, start_arg(Config, []), []), ok = gen_statem:stop(VIA), false = erlang:is_process_alive(Pid), noproc = @@ -347,46 +374,55 @@ stop7(_Config) -> ok. %% Anonymous on remote node -stop8(_Config) -> +stop8(Config) -> {ok,Node} = ?t:start_node(gen_statem_stop8, slave, []), Dir = filename:dirname(code:which(?MODULE)), rpc:call(Node, code, add_path, [Dir]), - {ok,Pid} = rpc:call(Node, gen_statem,start, [?MODULE,[],[]]), + {ok,Pid} = + rpc:call( + Node, gen_statem,start, + [?MODULE,start_arg(Config, []),[]]), ok = gen_statem:stop(Pid), false = rpc:call(Node, erlang, is_process_alive, [Pid]), noproc = ?EXPECT_FAILURE(gen_statem:stop(Pid), Reason1), true = ?t:stop_node(Node), - {nodedown,Node} = + {{nodedown,Node},{sys,terminate,_}} = ?EXPECT_FAILURE(gen_statem:stop(Pid), Reason2), ok. %% Registered name on remote node -stop9(_Config) -> +stop9(Config) -> Name = to_stop, LocalSTM = {local,Name}, {ok,Node} = ?t:start_node(gen_statem__stop9, slave, []), STM = {Name,Node}, Dir = filename:dirname(code:which(?MODULE)), rpc:call(Node, code, add_path, [Dir]), - {ok,Pid} = rpc:call(Node, gen_statem, start, [LocalSTM,?MODULE,[],[]]), + {ok,Pid} = + rpc:call( + Node, gen_statem, start, + [LocalSTM,?MODULE,start_arg(Config, []),[]]), ok = gen_statem:stop(STM), undefined = rpc:call(Node,erlang,whereis,[Name]), false = rpc:call(Node,erlang,is_process_alive,[Pid]), noproc = ?EXPECT_FAILURE(gen_statem:stop(STM), Reason1), true = ?t:stop_node(Node), - {nodedown,Node} = + {{nodedown,Node},{sys,terminate,_}} = ?EXPECT_FAILURE(gen_statem:stop(STM), Reason2), ok. %% Globally registered name on remote node -stop10(_Config) -> +stop10(Config) -> STM = {global,to_stop}, {ok,Node} = ?t:start_node(gen_statem_stop10, slave, []), Dir = filename:dirname(code:which(?MODULE)), rpc:call(Node,code,add_path,[Dir]), - {ok,Pid} = rpc:call(Node, gen_statem, start, [STM,?MODULE,[],[]]), + {ok,Pid} = + rpc:call( + Node, gen_statem, start, + [STM,?MODULE,start_arg(Config, []),[]]), global:sync(), ok = gen_statem:stop(STM), false = rpc:call(Node, erlang, is_process_alive, [Pid]), @@ -402,7 +438,8 @@ abnormal1(Config) when is_list(Config) -> Name = abnormal1, LocalSTM = {local,Name}, - {ok, _Pid} = gen_statem:start(LocalSTM, ?MODULE, [], []), + {ok, _Pid} = + gen_statem:start(LocalSTM, ?MODULE, start_arg(Config, []), []), %% timeout call. delayed = gen_statem:call(Name, {delayed_answer,1}, 100), @@ -410,13 +447,14 @@ abnormal1(Config) when is_list(Config) -> ?EXPECT_FAILURE( gen_statem:call(Name, {delayed_answer,1000}, 10), Reason), + ok = gen_statem:stop(Name), ok = verify_empty_msgq(). %% Check that bad return values makes the stm crash. Note that we must %% trap exit since we must link to get the real bad_return_ error abnormal2(Config) when is_list(Config) -> OldFl = process_flag(trap_exit, true), - {ok,Pid} = gen_statem:start_link(?MODULE, [], []), + {ok,Pid} = gen_statem:start_link(?MODULE, start_arg(Config, []), []), %% bad return value in the gen_statem loop {{bad_return_value,badreturn},_} = @@ -433,7 +471,7 @@ abnormal2(Config) when is_list(Config) -> shutdown(Config) when is_list(Config) -> process_flag(trap_exit, true), - {ok,Pid0} = gen_statem:start_link(?MODULE, [], []), + {ok,Pid0} = gen_statem:start_link(?MODULE, start_arg(Config, []), []), ok = do_func_test(Pid0), ok = do_sync_func_test(Pid0), stopped = gen_statem:call(Pid0, {stop,{shutdown,reason}}), @@ -454,7 +492,7 @@ shutdown(Config) when is_list(Config) -> sys1(Config) when is_list(Config) -> - {ok,Pid} = gen_statem:start(?MODULE, [], []), + {ok,Pid} = gen_statem:start(?MODULE, start_arg(Config, []), []), {status, Pid, {module,gen_statem}, _} = sys:get_status(Pid), sys:suspend(Pid), Parent = self(), @@ -477,7 +515,7 @@ sys1(Config) when is_list(Config) -> stop_it(Pid). call_format_status(Config) when is_list(Config) -> - {ok,Pid} = gen_statem:start(?MODULE, [], []), + {ok,Pid} = gen_statem:start(?MODULE, start_arg(Config, []), []), Status = sys:get_status(Pid), {status,Pid,_Mod,[_PDict,running,_,_, Data]} = Status, [format_status_called|_] = lists:reverse(Data), @@ -485,7 +523,9 @@ call_format_status(Config) when is_list(Config) -> %% check that format_status can handle a name being an atom (pid is %% already checked by the previous test) - {ok, Pid2} = gen_statem:start({local, gstm}, ?MODULE, [], []), + {ok, Pid2} = + gen_statem:start( + {local, gstm}, ?MODULE, start_arg(Config, []), []), Status2 = sys:get_status(gstm), {status,Pid2,_Mod,[_PDict2,running,_,_,Data2]} = Status2, [format_status_called|_] = lists:reverse(Data2), @@ -494,13 +534,17 @@ call_format_status(Config) when is_list(Config) -> %% check that format_status can handle a name being a term other than a %% pid or atom GlobalName1 = {global,"CallFormatStatus"}, - {ok,Pid3} = gen_statem:start(GlobalName1, ?MODULE, [], []), + {ok,Pid3} = + gen_statem:start( + GlobalName1, ?MODULE, start_arg(Config, []), []), Status3 = sys:get_status(GlobalName1), {status,Pid3,_Mod,[_PDict3,running,_,_,Data3]} = Status3, [format_status_called|_] = lists:reverse(Data3), stop_it(Pid3), GlobalName2 = {global,{name, "term"}}, - {ok,Pid4} = gen_statem:start(GlobalName2, ?MODULE, [], []), + {ok,Pid4} = + gen_statem:start( + GlobalName2, ?MODULE, start_arg(Config, []), []), Status4 = sys:get_status(GlobalName2), {status,Pid4,_Mod,[_PDict4,running,_,_, Data4]} = Status4, [format_status_called|_] = lists:reverse(Data4), @@ -510,13 +554,15 @@ call_format_status(Config) when is_list(Config) -> %% pid or atom dummy_via:reset(), ViaName1 = {via,dummy_via,"CallFormatStatus"}, - {ok,Pid5} = gen_statem:start(ViaName1, ?MODULE, [], []), + {ok,Pid5} = gen_statem:start(ViaName1, ?MODULE, start_arg(Config, []), []), Status5 = sys:get_status(ViaName1), {status,Pid5,_Mod, [_PDict5,running,_,_, Data5]} = Status5, [format_status_called|_] = lists:reverse(Data5), stop_it(Pid5), ViaName2 = {via,dummy_via,{name,"term"}}, - {ok, Pid6} = gen_statem:start(ViaName2, ?MODULE, [], []), + {ok, Pid6} = + gen_statem:start( + ViaName2, ?MODULE, start_arg(Config, []), []), Status6 = sys:get_status(ViaName2), {status,Pid6,_Mod,[_PDict6,running,_,_,Data6]} = Status6, [format_status_called|_] = lists:reverse(Data6), @@ -528,7 +574,9 @@ error_format_status(Config) when is_list(Config) -> error_logger_forwarder:register(), OldFl = process_flag(trap_exit, true), StateData = "called format_status", - {ok,Pid} = gen_statem:start(?MODULE, {state_data,StateData}, []), + {ok,Pid} = + gen_statem:start( + ?MODULE, start_arg(Config, {state_data,StateData}), []), %% bad return value in the gen_statem loop {{bad_return_value,badreturn},_} = ?EXPECT_FAILURE(gen_statem:call(Pid, badreturn), Reason), @@ -562,7 +610,9 @@ terminate_crash_format(Config) when is_list(Config) -> error_logger_forwarder:register(), OldFl = process_flag(trap_exit, true), StateData = crash_terminate, - {ok,Pid} = gen_statem:start(?MODULE, {state_data,StateData}, []), + {ok,Pid} = + gen_statem:start( + ?MODULE, start_arg(Config, {state_data,StateData}), []), stop_it(Pid), Self = self(), receive @@ -595,7 +645,9 @@ terminate_crash_format(Config) when is_list(Config) -> get_state(Config) when is_list(Config) -> State = self(), - {ok,Pid} = gen_statem:start(?MODULE, {state_data,State}, []), + {ok,Pid} = + gen_statem:start( + ?MODULE, start_arg(Config, {state_data,State}), []), {idle,State} = sys:get_state(Pid), {idle,State} = sys:get_state(Pid, 5000), stop_it(Pid), @@ -603,13 +655,16 @@ get_state(Config) when is_list(Config) -> %% check that get_state can handle a name being an atom (pid is %% already checked by the previous test) {ok,Pid2} = - gen_statem:start({local,gstm}, ?MODULE, {state_data,State}, []), + gen_statem:start( + {local,gstm}, ?MODULE, start_arg(Config, {state_data,State}), []), {idle,State} = sys:get_state(gstm), {idle,State} = sys:get_state(gstm, 5000), stop_it(Pid2), %% check that get_state works when pid is sys suspended - {ok,Pid3} = gen_statem:start(?MODULE, {state_data,State}, []), + {ok,Pid3} = + gen_statem:start( + ?MODULE, start_arg(Config, {state_data,State}), []), {idle,State} = sys:get_state(Pid3), ok = sys:suspend(Pid3), {idle,State} = sys:get_state(Pid3, 5000), @@ -619,7 +674,9 @@ get_state(Config) when is_list(Config) -> replace_state(Config) when is_list(Config) -> State = self(), - {ok, Pid} = gen_statem:start(?MODULE, {state_data,State}, []), + {ok, Pid} = + gen_statem:start( + ?MODULE, start_arg(Config, {state_data,State}), []), {idle,State} = sys:get_state(Pid), NState1 = "replaced", Replace1 = fun({StateName, _}) -> {StateName,NState1} end, @@ -650,7 +707,9 @@ replace_state(Config) when is_list(Config) -> hibernate(Config) when is_list(Config) -> OldFl = process_flag(trap_exit, true), - {ok,Pid0} = gen_statem:start_link(?MODULE, hiber_now, []), + {ok,Pid0} = + gen_statem:start_link( + ?MODULE, start_arg(Config, hiber_now), []), is_in_erlang_hibernate(Pid0), stop_it(Pid0), receive @@ -659,7 +718,8 @@ hibernate(Config) when is_list(Config) -> ?t:fail(gen_statem_did_not_die) end, - {ok,Pid} = gen_statem:start_link(?MODULE, hiber, []), + {ok,Pid} = + gen_statem:start_link(?MODULE, start_arg(Config, hiber), []), true = ({current_function,{erlang,hibernate,3}} =/= erlang:process_info(Pid,current_function)), hibernating = gen_statem:call(Pid, hibernate_sync), @@ -1031,6 +1091,14 @@ verify_empty_msgq() -> [] = ?t:messages_get(), ok. +start_arg(Config, Arg) -> + case lists:keyfind(init_ops, 1, Config) of + {_,Ops} -> + {init_ops,Arg,Ops}; + false -> + Arg + end. + %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %% %% The State Machine @@ -1052,7 +1120,16 @@ init(hiber_now) -> {ok,hiber_idle,[],[hibernate]}; init({state_data, StateData}) -> {ok,idle,StateData}; -init(_) -> +init({init_ops,Arg,InitOps}) -> + case init(Arg) of + {ok,State,Data,Ops} -> + {ok,State,Data,InitOps++Ops}; + {ok,State,Data} -> + {ok,State,Data,InitOps}; + Other -> + Other + end; +init([]) -> {ok,idle,state_data}. terminate(_, _State, crash_terminate) -> @@ -1247,6 +1324,35 @@ handle_common_events(cast, {'alive?',Pid}, _, State, Data) -> handle_common_events(_, _, _, _, _) -> undefined. +%% Dispatcher to test callback_mode handle_event_function +%% +%% Wrap the state in a 1 element list just to test non-atom +%% states. Note that the state from init/1 is not wrapped +%% so both atom and non-atom states are tested. +handle_event(Type, Event, PrevState, State, Data) -> + PrevStateName = unwrap(PrevState), + StateName = unwrap(State), + case + ?MODULE:StateName( + Type, Event, PrevStateName, StateName, Data) of + {next_state,NewState,NewStateData} -> + {next_state,wrap(NewState),NewStateData}; + {next_state,NewState,NewStateData,StateOps} -> + {next_state,wrap(NewState),NewStateData,StateOps}; + Other -> + Other + end. + +unwrap([State]) -> + State; +unwrap(State) -> + State. + +wrap(State) -> + [State]. + + + code_change(_OldVsn, State, StateData, _Extra) -> {ok,State,StateData}. |