This module is a PropEr generator for abstract code. It
%%% generates guards, expressions, programs (modules), and terms. It
%%% does not generate macros or other attributes than `function',
%%% `record', `spec', and `type'. The generated programs (guards,
%%% expressions) can be used for testing the Compiler or other modules
%%% traversing programs as abstract forms. Typical examples of the
%%% latter are erl_eval
, erl_pp
,
%%% erl_prettypr
(Syntax Tools), and parse transforms.
%%% Created modules should compile without errors, but will most likely
%%% crash immediately when invoked.
This is an example how to test the Compiler:
%%% %%% ``` %%% test() -> %%% ?FORALL(Abstr, proper_erlang_abstract_code:module(), %%% ?WHENFAIL( %%% begin %%% io:format("~ts\n", [[erl_pp:form(F) || F <- Abstr]]), %%% compile(Abstr, [report_errors]) %%% end, %%% case compile(Abstr, []) of %%% {error, _Es, _Ws} -> false; %%% _ -> true %%% end)). %%% %%% compile(Abstr, Opts) -> %%% compile:noenv_forms(Abstr, Opts). %%% ''' -module(proper_erlang_abstract_code). -export([module/0, module/1, guard/0, guard/1, expr/0, expr/1]). -export([term/0, term/1]). %-compile(export_all). -compile(nowarn_export_all). %-define(debug, true). -ifdef(debug). -define(DEBUG(F, As), io:format(F, As)). -else. -define(DEBUG(F, AS), ok). -endif. -include("proper_internal.hrl"). -type char_fun() :: fun(() -> proper_types:type()). %%% A function that generates characters. The default function %%% chooses from$a..$z | $A..$Z
.
-type atom_fun() :: fun(() -> proper_types:type()).
%%% A function that generates atoms. The default function chooses
%%% from 100 common English words.
-type weight() :: non_neg_integer().
-type limit() :: non_neg_integer().
-type option() ::
{'variables', [atom()]} |
{'weight', {Key :: atom(), Weight :: weight()}} |
{'function', [{FunctionName :: atom(), Arity :: arity()}]} |
{'types', [{TypeName :: atom(), NumOfParams :: arity()}]} |
{'records', [{RecordName:: atom(), [FieldName :: atom()]}]} |
{'limit', [{Name :: atom(), Limit :: limit()}]} |
{'char', char_fun()} |
{'atom', atom_fun()} |
{'set_all_weights', weight()}.
%%% See description below.
-type fa() :: {atom(), arity()}. % function+arity
-type ta() :: {atom(), arity()}. % type+arity
-type rec() :: {RecordName :: atom(), [FieldName :: atom()]}.
-record(gen_state,
{
size = 0 :: proper_gen:size(),
result_type = 'program' :: 'program' | 'guard' | 'expr' | 'term',
functions = [] :: [fa()],
functions_and_auto_imported = [] :: [{weight(), fa()}],
expr_bifs = [] :: [fa()],
guard_bifs = [] :: [fa()],
named_funs = [] :: [fa()],
records = [] :: [rec()],
guard_records = [] :: [rec()],
types = [] :: [ta()],
predef_types = [] :: [ta()],
module :: module(),
options = [] :: [option()],
weights = #{} :: #{Key :: atom() => Weight :: weight()},
limits = #{} :: #{Key :: atom() => Limit :: limit()},
variables = ordsets:new() :: ordsets:ordset(atom()),
simple_char = fun default_simple_char/0 :: char_fun(),
atom = fun default_atom/0 :: atom_fun(),
resize = 'false' :: boolean()
}).
-record(post_state,
{
context = 'expr' :: 'expr' | 'type' | 'record' | 'pattern',
vars = ordsets:new() :: ordsets:ordset(atom()),
vindex = 0 :: non_neg_integer(),
forbidden = ordsets:new() :: ordsets:ordset(atom()),
known_functions = [] :: [fa()],
atom = fun default_atom/0 :: atom_fun()
}).
-define(DEFAULT_SMALL_WEIGHT_PROGRAM, 50). % Needs to be quite high.
-define(DEFAULT_SMALL_WEIGHT_TERM, 50).
-define(MAX_CALL_ARGS, 2).
-define(MAX_FUNCTION_CLAUSES, 2).
-define(MAX_QUALIFIERS, 2).
-define(MAX_IF_CLAUSES, 2).
-define(MAX_CATCH_CLAUSES, 2).
-define(MAX_CLAUSES, 2).
-define(MAX_BODY, 2).
-define(MAX_GUARD, 2).
-define(MAX_GUARD_TESTS, 2).
-define(MAX_MAP, 2).
-define(MAX_TYPE_SPECIFIER, 2).
-define(MAX_RECORD_FIELDS, 3).
-define(MAX_TUPLE, 2).
-define(MAX_BIN_ELEMENTS, 2).
-define(MAX_FUNCTION_TYPES, 2).
-define(MAX_FUNCTION_CONSTRAINTS, 2).
-define(MAX_UNION_TYPES, 4).
-define(MAX_TUPLE_TYPES, 2).
-define(MAX_LIST, 4).
-define(MAX_STRING, 4).
%%% "div 2" is just a suggestion.
-define(RESIZE(S), S#gen_state{size = S#gen_state.size div 2}).
%%% @doc Returns abstract code of a term that can be handled by
%%% erl_parse:normalise/0
.
%%% No pid() or port().
-spec term() -> proper_types:type().
term() ->
term([]).
%%% @doc Same as {@link term/0}, but accepts a list of options.
%%% === Options ===
%%%
%%% Many options are the same as the ones for {@link module/1}.
%%% {atom, {@link atom_fun()}}
- A atom generating
%%% function to replace the default.{char, {@link char_fun()}}
- A character generating
%%% function to replace the default. The function is used when
%%% generating strings and characters.{limit, [{Name, Limit}]}
- Set the limit of
%%% Name
to Limit
. The limit names are:
%%% bin_elements
- Number of segments of a bitstring.list
- Number of elements of a plain list.map
- Number of associations of a map.string
- Number of characters of a string.tuple
- Number of elements of a tuple.{resize, boolean()}
- Use ?SIZED
%%% to limit the size of the generated abstract code. With this
%%% option set to false
(the default) big code
%%% may be generated among the first instances.{set_all_weights, Weight}
- Set the weight of
%%% all keys to Weight
.{weight, {Key, Weight}}
- Set the weight of
%%% Key
to weight Weight
. A weight of zero
%%% means that a construct is not generated. Higher weights means that
%%% a construct i generated relatively often. Groups of weight keys
%%% follow. Notice that the weight of a key is relative to other
%%% keys of the same group. The weight of small
needs
%%% to quite high to avoid generating too deeply nested abstract
%%% code.small
): atom, boolean,
%%% integer, string, char, float, nil
small, bitstring, list, tuple,
%%% map, 'fun'
map
): build_map
list
): plain_list,
%%% cons
bitstring
): bits, bytes
'fun'
):
%%% ext_mfa
{atom, {@link atom_fun()}}
- A atom generating
%%% function to replace the default.{char, {@link char_fun()}}
- A character generating
%%% function to replace the default. The function is used when
%%% generating strings and characters.{functions, [{Name, Arity}]}
- A list of FAs to
%%% be used as names of generated functions. The default is a small
%%% number of functions with a small number of arguments.{limit, [{Name, Limit}]}
- Set the limit of
%%% Name
to Limit
. The limit names are:
%%% bin_elements
- Number of segments of a bitstring.list
- Number of elements of a plain list.map
- Number of associations of a map.string
- Number of characters of a string.tuple
- Number of elements of a tuple.body
- Number of clauses of a body.call_args
- Number of arguments of function call.catch_clauses
- Number of clauses of the
%%% catch
part of a try/catch
.clauses
- Number of clauses of case
,
%%% the of
part of try/catch
, and
%%% receive
.function_clauses
- Number of clauses of
%%% a function.function_constraints
- Number of constraints of
%%% a function specification.function_constraints
- Number of constraints of
%%% a function specification.function_types
- Number of types of
%%% an overloaded function specification.guard
- Number of guards of a clause.guard_tests
- Number of guard tests of a guard.if_clauses
- Number of clauses of
%%% if
.tuple_types
- Number of types (elements)
%%% of tuple types.qualifiers
- Number of qualifiers
%%% of comprehensions.record_fields
- Number of fields of record
%%% declarations.tsl
- Number of elements of
%%% type specifier lists (of segments of bit syntax expressions).union_types
- Number of types of type
%%% union.s{records, [{Name, [Field]}]}
- A list
%%% of record names with field names to be used as names of
%%% generated records. The default is a small number of records
%%% with a small number of fields.{types, [{Name, NumOfParameters}]}
- A list
%%% of TAs to be used as names of generated types. The default
%%% is a small number of types.{resize, boolean()}
- Use ?SIZED
%%% to limit the size of the generated abstract code. With this
%%% option set to false
(the default) big code
%%% may be generated among the first instances.{set_all_weights, Weight}
- Set the weight of
%%% all keys to Weight
.{weight, {Key, Weight}}
- Set the weight of
%%% Key
to weight Weight
. A weight of zero
%%% means that a construct is not generated. Higher weights means that
%%% a construct i generated relatively often. Groups of weight keys
%%% follow. Notice that the weight of a key is relative to other
%%% keys of the same group. Also notice that some keys occur in
%%% more than one group, which makes it all more complicated. The
%%% weight of small
needs to be quite high to avoid
%%% generating too deeply nested abstract code.record_decl, type_decl, function_decl,
%%% function_spec
(type_decl
and
%%% function_spec
are off by default)small
): atom, boolean,
%%% integer, string, char, float, nil, pat_var, var
small, bitstring, list, tuple,
%%% map, match, binop, unop, record, 'case', block, 'if', 'fun',
%%% 'receive', 'try', 'catch', try_of, termcall, varcall, localcall,
%%% extcall
(termcall
is off by default)map
): build_map,
%%% update_map
list
): plain_list, cons,
%%% lc
lc
): lc_gen, blc_gen,
%%% lc_any_filter, lc_guard_filter
bitstring
): bits, blc,
%%% literal_bits
'try', try_of
): no_try_after,
%%% try_after
'catch'
):
%%% no_eclass, any_eclass, lit_eclass, var_eclass,
%%% bad_eclass
'receive'
):
%%% lit_timeout, inf_timeout, var_timeout
'fun'
):
%%% lambda, rec_lambda, local_mfa, ext_mfa, any_mfa
no_guard, yes_guard
small, tuple, map, cons, plain_list, bits,
%%% binop, unop, record, guard_call, remote_guard_call
small, match, tuple, cons, plain_list, bits,
%%% unop, binop, record, map_pattern, string_prefix
pat_var
):
%%% fresh_var, bound_var
_ = V
syntax):
%%% yes_multi_field_init, no_multi_field_init
string_prefix
):
%%% nil, string, string_prefix_list
annotated_type, atom, bitstring, 'fun',
%%% integer_range_type, nil, map, predefined_type, record,
%%% remote_type, singleton_integer_type, tuple, type_union,
%%% type_variable, user_defined_type
yes_constrained_function_type,
%%% no_constrained_function_type
%%% no_overloaded, yes_overloaded
singleton_integer_type
):
%%% integer, char, unop, binop
Specifies the initial state of the finite state machine. As with %%% `proper_statem:initial_state/0', its result should be deterministic. %%%
Specifies what the state data should initially contain. Its result %%% should be deterministic
There should be one instance of this function for each reachable
%%% state `StateName' of the finite state machine. In case `StateName' is a
%%% tuple the function takes a different form, described just below. The
%%% function returns a list of possible transitions ({@type transition()})
%%% from the current state.
%%% At command generation time, the instance of this function with the same
%%% name as the current state's name is called to return the list of possible
%%% transitions. Then, PropEr will randomly choose a transition and,
%%% according to that, generate the next symbolic call to be included in the
%%% command sequence. However, before the call is actually included, a
%%% precondition that might impose constraints on `StateData' is checked.
%%% Note also that PropEr detects transitions that would raise an exception
%%% of class `
There should be one instance of this function for each reachable state %%% `{StateName,Attr1,...,AttrN}' of the finite state machine. The function %%% has similar behaviour to `StateName/1', described above.
This is an optional callback. When it is not defined (or not exported), %%% transitions are chosen with equal probability. When it is defined, it %%% assigns a non-negative integer weight to transitions from `From' to `Target' %%% triggered by symbolic call `Call'. In this case, each transition is chosen %%% with probability proportional to the weight assigned.
Similar to `proper_statem:precondition/2'. Specifies the %%% precondition that should hold about `StateData' so that `Call' can be %%% included in the command sequence. In case precondition doesn't hold, a %%% new transition is chosen using the appropriate `StateName/1' generator. %%% It is possible for more than one transitions to be triggered by the same %%% symbolic call and lead to different target states. In this case, at most %%% one of the target states may have a true precondition. Otherwise, PropEr %%% will not be able to detect which transition was chosen and an exception %%% will be raised.
Similar to `proper_statem:postcondition/3'. Specifies the %%% postcondition that should hold about the result `Res' of the evaluation %%% of `Call'.
Similar to `proper_statem:next_state/3'. Specifies how the %%% transition from `FromState' to `Target' triggered by `Call' affects the %%% `StateData'. `Res' refers to the result of `Call' and can be either %%% symbolic or dynamic.
"proper/include/proper.hrl"
header file,
%%% all API functions of {@module} are automatically
%%% imported, unless `PROPER_NO_IMPORTS' is defined.
%%%
%%% === The role of commands ===
%%% Testcases generated for testing a stateful system are lists of symbolic API
%%% calls to that system. Symbolic representation has several benefits, which
%%% are listed here in increasing order of importance:
%%% Specifies the symbolic initial state of the state machine. This state %%% will be evaluated at command execution time to produce the actual initial %%% state. The function is not only called at command generation time, but %%% also in order to initialize the state every time the command sequence is %%% run (i.e. during normal execution, while shrinking and when checking a %%% counterexample). For this reason, it should be deterministic and %%% self-contained.
Generates a symbolic call to be included in the command sequence, %%% given the current state `S' of the abstract state machine. However, %%% before the call is actually included, a precondition is checked. This %%% function will be repeatedly called to produce the next call to be %%% included in the test case.
Specifies the precondition that should hold so that `Call' can be %%% included in the command sequence, given the current state `S' of the %%% abstract state machine. In case precondition doesn't hold, a new call is %%% chosen using the `command/1' generator. If preconditions are very strict, %%% it will take a lot of tries for PropEr to randomly choose a valid command. %%% Testing will be stopped if the `constraint_tries' limit is reached %%% (see the 'Options' section in the {@link proper} module documentation) and %%% a `{cant_generate,[{proper_statem,commands,4}]}' error will be produced in %%% that case. %%% Preconditions are also important for correct shrinking of failing %%% testcases. When shrinking command sequences, we try to eliminate commands %%% that do not contribute to failure, ensuring that all preconditions still %%% hold. Validating preconditions is necessary because during shrinking we %%% usually attempt to perform a call with the system being in a state %%% different from the state it was when initially running the test.
Specifies the postcondition that should hold about the result `Res' of %%% performing `Call', given the dynamic state `S' of the abstract state %%% machine prior to command execution. This function is called during %%% runtime, this is why the state is dynamic.
Specifies the next state of the abstract state machine, given the %%% current state `S', the symbolic `Call' chosen and its result `Res'. This %%% function is called both at command generation and command execution time %%% in order to update the model state, therefore the state `S' and the %%% result `Res' can be either symbolic or dynamic.
All commands were successfully run and all postconditions were true. %%
There was an error while evaluating the initial state.
A postcondition was false or raised an exception.
A precondition was false or raised an exception.
An exception was raised while running a command.