%%% Copyright 2010-2013 Manolis Papadakis <[email protected]>,
%%% Eirini Arvaniti <[email protected]>
%%% and Kostis Sagonas <[email protected]>
%%%
%%% This file is part of PropEr.
%%%
%%% PropEr is free software: you can redistribute it and/or modify
%%% it under the terms of the GNU General Public License as published by
%%% the Free Software Foundation, either version 3 of the License, or
%%% (at your option) any later version.
%%%
%%% PropEr is distributed in the hope that it will be useful,
%%% but WITHOUT ANY WARRANTY; without even the implied warranty of
%%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
%%% GNU General Public License for more details.
%%%
%%% You should have received a copy of the GNU General Public License
%%% along with PropEr. If not, see <http://www.gnu.org/licenses/>.
%%% @copyright 2010-2013 Manolis Papadakis, Eirini Arvaniti and Kostis Sagonas
%%% @version {@version}
%%% @author Manolis Papadakis
%%% @doc Type manipulation functions and predefined types.
%%%
%%% == Basic types ==
%%% This module defines all the basic types of the PropEr type system as
%%% functions. See the <a href="#index">function index</a> for an overview.
%%%
%%% Types can be combined in tuples or lists to produce other types. Exact
%%% values (such as exact numbers, atoms, binaries and strings) can be combined
%%% with types inside such structures, like in this example of the type of a
%%% tagged tuple: ``{'result', integer()}''.
%%%
%%% When including the PropEr header file, all
%%% <a href="#index">API functions</a> of this module are automatically
%%% imported, unless `PROPER_NO_IMPORTS' is defined.
%%%
%%% == Customized types ==
%%% The following operators can be applied to basic types in order to produce
%%% new ones:
%%%
%%% <dl>
%%% <dt>`?LET(<Xs>, <Xs_type>, <In>)'</dt>
%%% <dd>To produce an instance of this type, all appearances of the variables
%%% in `<Xs>' are replaced inside `<In>' by their corresponding values in a
%%% randomly generated instance of `<Xs_type>'. It's OK for the `<In>' part to
%%% evaluate to a type - in that case, an instance of the inner type is
%%% generated recursively.</dd>
%%% <dt>`?SUCHTHAT(<X>, <Type>, <Condition>)'</dt>
%%% <dd>This produces a specialization of `<Type>', which only includes those
%%% members of `<Type>' that satisfy the constraint `<Condition>' - that is,
%%% those members for which the function `fun(<X>) -> <Condition> end' returns
%%% `true'. If the constraint is very strict - that is, only a small
%%% percentage of instances of `<Type>' pass the test - it will take a lot of
%%% tries for the instance generation subsystem to randomly produce a valid
%%% instance. This will result in slower testing, and testing may even be
%%% stopped short, in case the `constraint_tries' limit is reached (see the
%%% "Options" section in the documentation of the {@link proper} module). If
%%% this is the case, it would be more appropriate to generate valid instances
%%% of the specialized type using the `?LET' macro. Also make sure that even
%%% small instances can satisfy the constraint, since PropEr will only try
%%% small instances at the start of testing. If this is not possible, you can
%%% instruct PropEr to start at a larger size, by supplying a suitable value
%%% for the `start_size' option (see the "Options" section in the
%%% documentation of the {@link proper} module).</dd>
%%% <dt>`?SUCHTHATMAYBE(<X>, <Type>, <Condition>)'</dt>
%%% <dd>Equivalent to the `?SUCHTHAT' macro, but the constraint `<Condition>'
%%% is considered non-strict: if the `constraint_tries' limit is reached, the
%%% generator will just return an instance of `<Type>' instead of failing,
%%% even if that instance doesn't satisfy the constraint.</dd>
%%% <dt>`?SHRINK(<Generator>, <List_of_alt_gens>)'</dt>
%%% <dd>This creates a type whose instances are generated by evaluating the
%%% statement block `<Generator>' (this may evaluate to a type, which will
%%% then be generated recursively). If an instance of such a type is to be
%%% shrunk, the generators in `<List_of_alt_gens>' are first run to produce
%%% hopefully simpler instances of the type. Thus, the generators in the
%%% second argument should be simpler than the default. The simplest ones
%%% should be at the front of the list, since those are the generators
%%% preferred by the shrinking subsystem. Like the main `<Generator>', the
%%% alternatives may also evaluate to a type, which is generated recursively.
%%% </dd>
%%% <dt>`?LETSHRINK(<List_of_variables>, <List_of_types>, <Generator>)'</dt>
%%% <dd>This is created by combining a `?LET' and a `?SHRINK' macro. Instances
%%% are generated by applying a randomly generated list of values inside
%%% `<Generator>' (just like a `?LET', with the added constraint that the
%%% variables and types must be provided in a list - alternatively,
%%% `<List_of_types>' may be a list or vector type). When shrinking instances
%%% of such a type, the sub-instances that were combined to produce it are
%%% first tried in place of the failing instance.</dd>
%%% <dt>`?LAZY(<Generator>)'</dt>
%%% <dd>This construct returns a type whose only purpose is to delay the
%%% evaluation of `<Generator>' (`<Generator>' can return a type, which will
%%% be generated recursively). Using this, you can simulate the lazy
%%% generation of instances:
%%% ``` stream() -> ?LAZY(frequency([ {1,[]}, {3,[0|stream()]} ])). '''
%%% The above type produces lists of zeroes with an average length of 3. Note
%%% that, had we not enclosed the generator with a `?LAZY' macro, the
%%% evaluation would continue indefinitely, due to the eager evaluation of
%%% the Erlang language.</dd>
%%% <dt>`non_empty(<List_or_binary_type>)'</dt>
%%% <dd>See the documentation for {@link non_empty/1}.</dd>
%%% <dt>`noshrink(<Type>)'</dt>
%%% <dd>See the documentation for {@link noshrink/1}.</dd>
%%% <dt>`default(<Default_value>, <Type>)'</dt>
%%% <dd>See the documentation for {@link default/2}.</dd>
%%% <dt>`with_parameter(<Parameter>, <Value>, <Type>)'</dt>
%%% <dd>See the documentation for {@link with_parameter/3}.</dd>
%%% <dt>`with_parameters(<Param_value_pairs>, <Type>)'</dt>
%%% <dd>See the documentation for {@link with_parameters/2}.</dd>
%%% </dl>
%%%
%%% == Size manipulation ==
%%% The following operators are related to the `size' parameter, which controls
%%% the maximum size of produced instances. The actual size of a produced
%%% instance is chosen randomly, but can never exceed the value of the `size'
%%% parameter at the moment of generation. A more accurate definition is the
%%% following: the maximum instance of `size S' can never be smaller than the
%%% maximum instance of `size S-1'. The actual size of an instance is measured
%%% differently for each type: the actual size of a list is its length, while
%%% the actual size of a tree may be the number of its internal nodes. Some
%%% types, e.g. unions, have no notion of size, thus their generation is not
%%% influenced by the value of `size'. The `size' parameter starts at 1 and
%%% grows automatically during testing.
%%%
%%% <dl>
%%% <dt>`?SIZED(<S>, <Generator>)'</dt>
%%% <dd>Creates a new type, whose instances are produced by replacing all
%%% appearances of the `<S>' parameter inside the statement block
%%% `<Generator>' with the value of the `size' parameter. It's OK for the
%%% `<Generator>' to return a type - in that case, an instance of the inner
%%% type is generated recursively.</dd>
%%% <dt>`resize(<New_size>, <Type>)'</dt>
%%% <dd>See the documentation for {@link resize/2}.</dd>
%%% </dl>
-module(proper_types).
-export([is_inst/2, is_inst/3]).
-export([integer/2, float/2, atom/0, binary/0, binary/1, bitstring/0,
bitstring/1, list/1, vector/2, union/1, weighted_union/1, tuple/1,
loose_tuple/1, exactly/1, fixed_list/1, function/2, any/0,
shrink_list/1, safe_union/1, safe_weighted_union/1]).
-export([integer/0, non_neg_integer/0, pos_integer/0, neg_integer/0, range/2,
float/0, non_neg_float/0, number/0, boolean/0, byte/0, char/0,
list/0, tuple/0, string/0, wunion/1, term/0, timeout/0, arity/0]).
-export([int/0, nat/0, largeint/0, real/0, bool/0, choose/2, elements/1,
oneof/1, frequency/1, return/1, default/2, orderedlist/1, function0/1,
function1/1, function2/1, function3/1, function4/1,
weighted_default/2]).
-export([resize/2, non_empty/1, noshrink/1]).
-export([cook_outer/1, is_type/1, equal_types/2, is_raw_type/1, to_binary/1,
from_binary/1, get_prop/2, find_prop/2, safe_is_instance/2,
is_instance/2, unwrap/1, weakly/1, strongly/1, satisfies_all/2,
new_type/2, subtype/2]).
-export([lazy/1, sized/1, bind/3, shrinkwith/2, add_constraint/3,
native_type/2, distlist/3, with_parameter/3, with_parameters/2,
parameter/1, parameter/2]).
-export([le/2]).
-export_type([type/0, raw_type/0, extint/0, extnum/0]).
-include("proper_internal.hrl").
%%------------------------------------------------------------------------------
%% Comparison with erl_types
%%------------------------------------------------------------------------------
%% Missing types
%% -------------------
%% will do:
%% records, maybe_improper_list(T,S), nonempty_improper_list(T,S)
%% maybe_improper_list(), maybe_improper_list(T), iolist, iodata
%% don't need:
%% nonempty_{list,string,maybe_improper_list}
%% won't do:
%% pid, port, ref, identifier, none, no_return, module, mfa, node
%% array, dict, digraph, set, gb_tree, gb_set, queue, tid
%% Missing type information
%% ------------------------
%% bin types:
%% other unit sizes? what about size info?
%% functions:
%% generally some fun, unspecified number of arguments but specified
%% return type
%% any:
%% doesn't cover functions and improper lists
%%------------------------------------------------------------------------------
%% Type declaration macros
%%------------------------------------------------------------------------------
-define(BASIC(PropList), new_type(PropList,basic)).
-define(WRAPPER(PropList), new_type(PropList,wrapper)).
-define(CONSTRUCTED(PropList), new_type(PropList,constructed)).
-define(CONTAINER(PropList), new_type(PropList,container)).
-define(SUBTYPE(Type,PropList), subtype(PropList,Type)).
%%------------------------------------------------------------------------------
%% Types
%%------------------------------------------------------------------------------
-type type_kind() :: 'basic' | 'wrapper' | 'constructed' | 'container' | atom().
-type instance_test() :: fun((proper_gen:imm_instance()) -> boolean())
| {'typed',
fun((proper_types:type(),
proper_gen:imm_instance()) -> boolean())}.
-type index() :: pos_integer().
%% @alias
-type value() :: term().
%% @private_type
%% @alias
-type extint() :: integer() | 'inf'.
%% @private_type
%% @alias
-type extnum() :: number() | 'inf'.
-type constraint_fun() :: fun((proper_gen:instance()) -> boolean()).
-opaque type() :: {'$type', [type_prop()]}.
%% A type of the PropEr type system
%% @type raw_type(). You can consider this as an equivalent of {@type type()}.
-type raw_type() :: type() | [raw_type()] | loose_tuple(raw_type()) | term().
-type type_prop_name() :: 'kind' | 'generator' | 'reverse_gen' | 'parts_type'
| 'combine' | 'alt_gens' | 'shrink_to_parts'
| 'size_transform' | 'is_instance' | 'shrinkers'
| 'noshrink' | 'internal_type' | 'internal_types'
| 'get_length' | 'split' | 'join' | 'get_indices'
| 'remove' | 'retrieve' | 'update' | 'constraints'
| 'parameters' | 'env' | 'subenv'.
-type type_prop_value() :: term().
-type type_prop() ::
{'kind', type_kind()}
| {'generator', proper_gen:generator()}
| {'reverse_gen', proper_gen:reverse_gen()}
| {'parts_type', type()}
| {'combine', proper_gen:combine_fun()}
| {'alt_gens', proper_gen:alt_gens()}
| {'shrink_to_parts', boolean()}
| {'size_transform', fun((size()) -> size())}
| {'is_instance', instance_test()}
| {'shrinkers', [proper_shrink:shrinker()]}
| {'noshrink', boolean()}
| {'internal_type', raw_type()}
| {'internal_types', tuple() | maybe_improper_list(type(),type() | [])}
%% The items returned by 'remove' must be of this type.
| {'get_length', fun((proper_gen:imm_instance()) -> length())}
%% If this is a container type, this should return the number of elements
%% it contains.
| {'split', fun((proper_gen:imm_instance()) -> [proper_gen:imm_instance()])
| fun((length(),proper_gen:imm_instance()) ->
{proper_gen:imm_instance(),proper_gen:imm_instance()})}
%% If present, the appropriate form depends on whether get_length is
%% defined: if get_length is undefined, this must be in the one-argument
%% form (e.g. a tree should be split into its subtrees), else it must be
%% in the two-argument form (e.g. a list should be split in two at the
%% index provided).
| {'join', fun((proper_gen:imm_instance(),proper_gen:imm_instance()) ->
proper_gen:imm_instance())}
| {'get_indices', fun((proper_types:type(),
proper_gen:imm_instance()) -> [index()])}
%% If this is a container type, this should return a list of indices we
%% can use to remove or insert elements from the given instance.
| {'remove', fun((index(),proper_gen:imm_instance()) ->
proper_gen:imm_instance())}
| {'retrieve', fun((index(), proper_gen:imm_instance() | tuple()
| maybe_improper_list(type(),type() | [])) ->
value() | type())}
| {'update', fun((index(),value(),proper_gen:imm_instance()) ->
proper_gen:imm_instance())}
| {'constraints', [{constraint_fun(), boolean()}]}
%% A list of constraints on instances of this type: each constraint is a
%% tuple of a fun that must return 'true' for each valid instance and a
%% boolean field that specifies whether the condition is strict.
| {'parameters', [{atom(),value()}]}
| {'env', term()}
| {'subenv', term()}.
%%------------------------------------------------------------------------------
%% Type manipulation functions
%%------------------------------------------------------------------------------
%% TODO: We shouldn't need the fully qualified type name in the range of these
%% functions.
%% @private
%% TODO: just cook/1 ?
-spec cook_outer(raw_type()) -> proper_types:type().
cook_outer(Type = {'$type',_Props}) ->
Type;
cook_outer(RawType) ->
if
is_tuple(RawType) -> tuple(tuple_to_list(RawType));
%% CAUTION: this must handle improper lists
is_list(RawType) -> fixed_list(RawType);
%% default case (covers integers, floats, atoms, binaries, ...):
true -> exactly(RawType)
end.
%% @private
-spec is_type(term()) -> boolean().
is_type({'$type',_Props}) ->
true;
is_type(_) ->
false.
%% @private
-spec equal_types(proper_types:type(), proper_types:type()) -> boolean().
equal_types(SameType, SameType) ->
true;
equal_types(_, _) ->
false.
%% @private
-spec is_raw_type(term()) -> boolean().
is_raw_type({'$type',_TypeProps}) ->
true;
is_raw_type(X) ->
if
is_tuple(X) -> is_raw_type_list(tuple_to_list(X));
is_list(X) -> is_raw_type_list(X);
true -> false
end.
-spec is_raw_type_list(maybe_improper_list()) -> boolean().
%% CAUTION: this must handle improper lists
is_raw_type_list(List) ->
proper_arith:safe_any(fun is_raw_type/1, List).
%% @private
-spec to_binary(proper_types:type()) -> binary().
to_binary(Type) ->
term_to_binary(Type).
%% @private
%% TODO: restore: -spec from_binary(binary()) -> proper_types:type().
from_binary(Binary) ->
binary_to_term(Binary).
-spec type_from_list([type_prop()]) -> proper_types:type().
type_from_list(KeyValueList) ->
{'$type',KeyValueList}.
-spec add_prop(type_prop_name(), type_prop_value(), proper_types:type()) ->
proper_types:type().
add_prop(PropName, Value, {'$type',Props}) ->
{'$type',lists:keystore(PropName, 1, Props, {PropName, Value})}.
-spec add_props([type_prop()], proper_types:type()) -> proper_types:type().
add_props(PropList, {'$type',OldProps}) ->
{'$type', lists:foldl(fun({N,_}=NV,Acc) ->
lists:keystore(N, 1, Acc, NV)
end, OldProps, PropList)}.
-spec append_to_prop(type_prop_name(), type_prop_value(),
proper_types:type()) -> proper_types:type().
append_to_prop(PropName, Value, {'$type',Props}) ->
Val = case lists:keyfind(PropName, 1, Props) of
{PropName, V} ->
V;
_ ->
[]
end,
{'$type', lists:keystore(PropName, 1, Props,
{PropName, lists:reverse([Value|Val])})}.
-spec append_list_to_prop(type_prop_name(), [type_prop_value()],
proper_types:type()) -> proper_types:type().
append_list_to_prop(PropName, List, {'$type',Props}) ->
{PropName, Val} = lists:keyfind(PropName, 1, Props),
{'$type', lists:keystore(PropName, 1, Props, {PropName, Val++List})}.
%% @private
-spec get_prop(type_prop_name(), proper_types:type()) -> type_prop_value().
get_prop(PropName, {'$type',Props}) ->
{_PropName, Val} = lists:keyfind(PropName, 1, Props),
Val.
%% @private
-spec find_prop(type_prop_name(), proper_types:type()) ->
{'ok',type_prop_value()} | 'error'.
find_prop(PropName, {'$type',Props}) ->
case lists:keyfind(PropName, 1, Props) of
{PropName, Value} ->
{ok, Value};
_ ->
error
end.
%% @private
-spec new_type([type_prop()], type_kind()) -> proper_types:type().
new_type(PropList, Kind) ->
Type = type_from_list(PropList),
add_prop(kind, Kind, Type).
%% @private
-spec subtype([type_prop()], proper_types:type()) -> proper_types:type().
%% TODO: should the 'is_instance' function etc. be reset for subtypes?
subtype(PropList, Type) ->
add_props(PropList, Type).
%% @private
-spec is_inst(proper_gen:instance(), raw_type()) ->
boolean() | {'error',{'typeserver',term()}}.
is_inst(Instance, RawType) ->
is_inst(Instance, RawType, 10).
%% @private
-spec is_inst(proper_gen:instance(), raw_type(), size()) ->
boolean() | {'error',{'typeserver',term()}}.
is_inst(Instance, RawType, Size) ->
proper:global_state_init_size(Size),
Result = safe_is_instance(Instance, RawType),
proper:global_state_erase(),
Result.
%% @private
-spec safe_is_instance(proper_gen:imm_instance(), raw_type()) ->
boolean() | {'error',{'typeserver',term()}}.
safe_is_instance(ImmInstance, RawType) ->
try is_instance(ImmInstance, RawType) catch
throw:{'$typeserver',SubReason} -> {error, {typeserver,SubReason}}
end.
%% @private
-spec is_instance(proper_gen:imm_instance(), raw_type()) -> boolean().
%% TODO: If the second argument is not a type, let it pass (don't even check for
%% term equality?) - if it's a raw type, don't cook it, instead recurse
%% into it.
is_instance(ImmInstance, RawType) ->
CleanInstance = proper_gen:clean_instance(ImmInstance),
Type = cook_outer(RawType),
(case get_prop(kind, Type) of
wrapper -> wrapper_test(ImmInstance, Type);
constructed -> constructed_test(ImmInstance, Type);
_ -> false
end
orelse
case find_prop(is_instance, Type) of
{ok,{typed, IsInstance}} -> IsInstance(Type, ImmInstance);
{ok,IsInstance} -> IsInstance(ImmInstance);
error -> false
end)
andalso weakly(satisfies_all(CleanInstance, Type)).
-spec wrapper_test(proper_gen:imm_instance(), proper_types:type()) -> boolean().
wrapper_test(ImmInstance, Type) ->
%% TODO: check if it's actually a raw type that's returned?
lists:any(fun(T) -> is_instance(ImmInstance, T) end, unwrap(Type)).
%% @private
%% TODO: restore:-spec unwrap(proper_types:type()) -> [proper_types:type(),...].
%% TODO: check if it's actually a raw type that's returned?
unwrap(Type) ->
RawInnerTypes = proper_gen:alt_gens(Type) ++ [proper_gen:normal_gen(Type)],
[cook_outer(T) || T <- RawInnerTypes].
-spec constructed_test(proper_gen:imm_instance(), proper_types:type()) ->
boolean().
constructed_test({'$used',ImmParts,ImmInstance}, Type) ->
PartsType = get_prop(parts_type, Type),
Combine = get_prop(combine, Type),
is_instance(ImmParts, PartsType) andalso
begin
%% TODO: check if it's actually a raw type that's returned?
%% TODO: move construction code to proper_gen
%% TODO: non-type => should we check for strict term equality?
RawInnerType = Combine(proper_gen:clean_instance(ImmParts)),
is_instance(ImmInstance, RawInnerType)
end;
constructed_test({'$to_part',ImmInstance}, Type) ->
PartsType = get_prop(parts_type, Type),
get_prop(shrink_to_parts, Type) =:= true andalso
%% TODO: we reject non-container types
get_prop(kind, PartsType) =:= container andalso
case {find_prop(internal_type,PartsType),
find_prop(internal_types,PartsType)} of
{{ok,EachPartType},error} ->
%% The parts are in a list or a vector.
is_instance(ImmInstance, EachPartType);
{error,{ok,PartTypesList}} ->
%% The parts are in a fixed list.
%% TODO: It should always be a proper list.
lists:any(fun(T) -> is_instance(ImmInstance,T) end, PartTypesList)
end;
constructed_test(_CleanInstance, _Type) ->
%% TODO: can we do anything better?
false.
%% @private
-spec weakly({boolean(),boolean()}) -> boolean().
weakly({B1,_B2}) -> B1.
%% @private
-spec strongly({boolean(),boolean()}) -> boolean().
strongly({_B1,B2}) -> B2.
-spec satisfies(proper_gen:instance(), {constraint_fun(),boolean()})
-> {boolean(),boolean()}.
satisfies(Instance, {Test,false}) ->
{true,Test(Instance)};
satisfies(Instance, {Test,true}) ->
Result = Test(Instance),
{Result,Result}.
%% @private
-spec satisfies_all(proper_gen:instance(), proper_types:type()) ->
{boolean(),boolean()}.
satisfies_all(Instance, Type) ->
case find_prop(constraints, Type) of
{ok, Constraints} ->
L = [satisfies(Instance, C) || C <- Constraints],
{L1,L2} = lists:unzip(L),
{lists:all(fun(B) -> B end, L1), lists:all(fun(B) -> B end, L2)};
error ->
{true,true}
end.
%%------------------------------------------------------------------------------
%% Type definition functions
%%------------------------------------------------------------------------------
%% @private
-spec lazy(proper_gen:nosize_generator()) -> proper_types:type().
lazy(Gen) ->
?WRAPPER([
{generator, Gen}
]).
%% @private
-spec sized(proper_gen:sized_generator()) -> proper_types:type().
sized(Gen) ->
?WRAPPER([
{generator, Gen}
]).
%% @private
-spec bind(raw_type(), proper_gen:combine_fun(), boolean()) ->
proper_types:type().
bind(RawPartsType, Combine, ShrinkToParts) ->
PartsType = cook_outer(RawPartsType),
?CONSTRUCTED([
{parts_type, PartsType},
{combine, Combine},
{shrink_to_parts, ShrinkToParts}
]).
%% @private
-spec shrinkwith(proper_gen:nosize_generator(), proper_gen:alt_gens()) ->
proper_types:type().
shrinkwith(Gen, DelaydAltGens) ->
?WRAPPER([
{generator, Gen},
{alt_gens, DelaydAltGens}
]).
%% @private
-spec add_constraint(raw_type(), constraint_fun(), boolean()) ->
proper_types:type().
add_constraint(RawType, Condition, IsStrict) ->
Type = cook_outer(RawType),
append_to_prop(constraints, {Condition,IsStrict}, Type).
%% @private
-spec native_type(mod_name(), string()) -> proper_types:type().
native_type(Mod, TypeStr) ->
?WRAPPER([
{generator, fun() -> proper_gen:native_type_gen(Mod,TypeStr) end}
]).
%%------------------------------------------------------------------------------
%% Basic types
%%------------------------------------------------------------------------------
%% @doc All integers between `Low' and `High', bounds included.
%% `Low' and `High' must be Erlang expressions that evaluate to integers, with
%% `Low =< High'. Additionally, `Low' and `High' may have the value `inf', in
%% which case they represent minus infinity and plus infinity respectively.
%% Instances shrink towards 0 if `Low =< 0 =< High', or towards the bound with
%% the smallest absolute value otherwise.
-spec integer(extint(), extint()) -> proper_types:type().
integer(Low, High) ->
?BASIC([
{env, {Low, High}},
{generator, {typed, fun integer_gen/2}},
{is_instance, {typed, fun integer_is_instance/2}},
{shrinkers, [fun number_shrinker/3]}
]).
integer_gen(Type, Size) ->
{Low, High} = get_prop(env, Type),
proper_gen:integer_gen(Size, Low, High).
integer_is_instance(Type, X) ->
{Low, High} = get_prop(env, Type),
is_integer(X) andalso le(Low, X) andalso le(X, High).
number_shrinker(X, Type, S) ->
{Low, High} = get_prop(env, Type),
proper_shrink:number_shrinker(X, Low, High, S).
%% @doc All floats between `Low' and `High', bounds included.
%% `Low' and `High' must be Erlang expressions that evaluate to floats, with
%% `Low =< High'. Additionally, `Low' and `High' may have the value `inf', in
%% which case they represent minus infinity and plus infinity respectively.
%% Instances shrink towards 0.0 if `Low =< 0.0 =< High', or towards the bound
%% with the smallest absolute value otherwise.
-spec float(extnum(), extnum()) -> proper_types:type().
float(Low, High) ->
?BASIC([
{env, {Low, High}},
{generator, {typed, fun float_gen/2}},
{is_instance, {typed, fun float_is_instance/2}},
{shrinkers, [fun number_shrinker/3]}
]).
float_gen(Type, Size) ->
{Low, High} = get_prop(env, Type),
proper_gen:float_gen(Size, Low, High).
float_is_instance(Type, X) ->
{Low, High} = get_prop(env, Type),
is_float(X) andalso le(Low, X) andalso le(X, High).
%% @private
-spec le(extnum(), extnum()) -> boolean().
le(inf, _B) -> true;
le(_A, inf) -> true;
le(A, B) -> A =< B.
%% @doc All atoms. All atoms used internally by PropEr start with a '`$'', so
%% such atoms will never be produced as instances of this type. You should also
%% refrain from using such atoms in your code, to avoid a potential clash.
%% Instances shrink towards the empty atom, ''.
-spec atom() -> proper_types:type().
atom() ->
?WRAPPER([
{generator, fun proper_gen:atom_gen/1},
{reverse_gen, fun proper_gen:atom_rev/1},
{size_transform, fun(Size) -> erlang:min(Size,255) end},
{is_instance, fun atom_is_instance/1}
]).
atom_is_instance(X) ->
is_atom(X)
%% We return false for atoms starting with '$', since these are
%% atoms used internally and never produced by the atom generator.
andalso (X =:= '' orelse hd(atom_to_list(X)) =/= $$).
%% @doc All binaries. Instances shrink towards the empty binary, `<<>>'.
-spec binary() -> proper_types:type().
binary() ->
?WRAPPER([
{generator, fun proper_gen:binary_gen/1},
{reverse_gen, fun proper_gen:binary_rev/1},
{is_instance, fun erlang:is_binary/1}
]).
%% @doc All binaries with a byte size of `Len'.
%% `Len' must be an Erlang expression that evaluates to a non-negative integer.
%% Instances shrink towards binaries of zeroes.
-spec binary(length()) -> proper_types:type().
binary(Len) ->
?WRAPPER([
{env, Len},
{generator, {typed, fun binary_len_gen/1}},
{reverse_gen, fun proper_gen:binary_rev/1},
{is_instance, {typed, fun binary_len_is_instance/2}}
]).
binary_len_gen(Type) ->
Len = get_prop(env, Type),
proper_gen:binary_len_gen(Len).
binary_len_is_instance(Type, X) ->
Len = get_prop(env, Type),
is_binary(X) andalso byte_size(X) =:= Len.
%% @doc All bitstrings. Instances shrink towards the empty bitstring, `<<>>'.
-spec bitstring() -> proper_types:type().
bitstring() ->
?WRAPPER([
{generator, fun proper_gen:bitstring_gen/1},
{reverse_gen, fun proper_gen:bitstring_rev/1},
{is_instance, fun erlang:is_bitstring/1}
]).
%% @doc All bitstrings with a bit size of `Len'.
%% `Len' must be an Erlang expression that evaluates to a non-negative integer.
%% Instances shrink towards bitstrings of zeroes
-spec bitstring(length()) -> proper_types:type().
bitstring(Len) ->
?WRAPPER([
{env, Len},
{generator, {typed, fun bitstring_len_gen/1}},
{reverse_gen, fun proper_gen:bitstring_rev/1},
{is_instance, {typed, fun bitstring_len_is_instance/2}}
]).
bitstring_len_gen(Type) ->
Len = get_prop(env, Type),
proper_gen:bitstring_len_gen(Len).
bitstring_len_is_instance(Type, X) ->
Len = get_prop(env, Type),
is_bitstring(X) andalso bit_size(X) =:= Len.
%% @doc All lists containing elements of type `ElemType'.
%% Instances shrink towards the empty list, `[]'.
-spec list(ElemType::raw_type()) -> proper_types:type().
% TODO: subtyping would be useful here (list, vector, fixed_list)
list(RawElemType) ->
ElemType = cook_outer(RawElemType),
?CONTAINER([
{generator, {typed, fun list_gen/2}},
{is_instance, {typed, fun list_is_instance/2}},
{internal_type, ElemType},
{get_length, fun erlang:length/1},
{split, fun lists:split/2},
{join, fun lists:append/2},
{get_indices, fun list_get_indices/2},
{remove, fun proper_arith:list_remove/2},
{retrieve, fun lists:nth/2},
{update, fun proper_arith:list_update/3}
]).
list_gen(Type, Size) ->
ElemType = get_prop(internal_type, Type),
proper_gen:list_gen(Size, ElemType).
list_is_instance(Type, X) ->
ElemType = get_prop(internal_type, Type),
list_test(X, ElemType).
%% @doc A type that generates exactly the list `List'. Instances shrink towards
%% shorter sublists of the original list.
-spec shrink_list([term()]) -> proper_types:type().
shrink_list(List) ->
?CONTAINER([
{env, List},
{generator, {typed, fun shrink_list_gen/1}},
{is_instance, {typed, fun shrink_list_is_instance/2}},
{get_length, fun erlang:length/1},
{split, fun lists:split/2},
{join, fun lists:append/2},
{get_indices, fun list_get_indices/2},
{remove, fun proper_arith:list_remove/2}
]).
shrink_list_gen(Type) ->
get_prop(env, Type).
shrink_list_is_instance(Type, X) ->
List = get_prop(env, Type),
is_sublist(X, List).
-spec is_sublist([term()], [term()]) -> boolean().
is_sublist([], _) -> true;
is_sublist(_, []) -> false;
is_sublist([H|T1], [H|T2]) -> is_sublist(T1, T2);
is_sublist(Slice, [_|T2]) -> is_sublist(Slice, T2).
-spec list_test(proper_gen:imm_instance(), proper_types:type()) -> boolean().
list_test(X, ElemType) ->
is_list(X) andalso lists:all(fun(E) -> is_instance(E, ElemType) end, X).
%% @private
-spec list_get_indices(proper_gen:generator(), list()) -> [position()].
list_get_indices(_, List) ->
lists:seq(1, length(List)).
%% @private
%% This assumes that:
%% - instances of size S are always valid instances of size >S
%% - any recursive calls inside Gen are lazy
-spec distlist(size(), proper_gen:sized_generator(), boolean()) ->
proper_types:type().
distlist(Size, Gen, NonEmpty) ->
ParentType = case NonEmpty of
true -> non_empty(list(Gen(Size)));
false -> list(Gen(Size))
end,
?SUBTYPE(ParentType, [
{subenv, {Size, Gen, NonEmpty}},
{generator, {typed, fun distlist_gen/1}}
]).
distlist_gen(Type) ->
{Size, Gen, NonEmpty} = get_prop(subenv, Type),
proper_gen:distlist_gen(Size, Gen, NonEmpty).
%% @doc All lists of length `Len' containing elements of type `ElemType'.
%% `Len' must be an Erlang expression that evaluates to a non-negative integer.
-spec vector(length(), ElemType::raw_type()) -> proper_types:type().
vector(Len, RawElemType) ->
ElemType = cook_outer(RawElemType),
?CONTAINER([
{env, Len},
{generator, {typed, fun vector_gen/1}},
{is_instance, {typed, fun vector_is_instance/2}},
{internal_type, ElemType},
{get_indices, fun vector_get_indices/2},
{retrieve, fun lists:nth/2},
{update, fun proper_arith:list_update/3}
]).
vector_gen(Type) ->
Len = get_prop(env, Type),
ElemType = get_prop(internal_type, Type),
proper_gen:vector_gen(Len, ElemType).
vector_is_instance(Type, X) ->
Len = get_prop(env, Type),
ElemType = get_prop(internal_type, Type),
is_list(X)
andalso length(X) =:= Len
andalso lists:all(fun(E) -> is_instance(E, ElemType) end, X).
vector_get_indices(Type, _X) ->
lists:seq(1, get_prop(env, Type)).
%% @doc The union of all types in `ListOfTypes'. `ListOfTypes' can't be empty.
%% The random instance generator is equally likely to choose any one of the
%% types in `ListOfTypes'. The shrinking subsystem will always try to shrink an
%% instance of a type union to an instance of the first type in `ListOfTypes',
%% thus you should write the simplest case first.
-spec union(ListOfTypes::[raw_type(),...]) -> proper_types:type().
union(RawChoices) ->
Choices = [cook_outer(C) || C <- RawChoices],
?BASIC([
{env, Choices},
{generator, {typed, fun union_gen/1}},
{is_instance, {typed, fun union_is_instance/2}},
{shrinkers, [fun union_shrinker_1/3, fun union_shrinker_2/3]}
]).
union_gen(Type) ->
Choices = get_prop(env,Type),
proper_gen:union_gen(Choices).
union_is_instance(Type, X) ->
Choices = get_prop(env, Type),
lists:any(fun(C) -> is_instance(X, C) end, Choices).
union_shrinker_1(X, Type, S) ->
Choices = get_prop(env, Type),
proper_shrink:union_first_choice_shrinker(X, Choices, S).
union_shrinker_2(X, Type, S) ->
Choices = get_prop(env, Type),
proper_shrink:union_recursive_shrinker(X, Choices, S).
%% @doc A specialization of {@link union/1}, where each type in `ListOfTypes' is
%% assigned a frequency. Frequencies must be Erlang expressions that evaluate to
%% positive integers. Types with larger frequencies are more likely to be chosen
%% by the random instance generator. The shrinking subsystem will ignore the
%% frequencies and try to shrink towards the first type in the list.
-spec weighted_union(ListOfTypes::[{frequency(),raw_type()},...]) ->
proper_types:type().
weighted_union(RawFreqChoices) ->
CookFreqType = fun({Freq,RawType}) -> {Freq,cook_outer(RawType)} end,
FreqChoices = lists:map(CookFreqType, RawFreqChoices),
Choices = [T || {_F,T} <- FreqChoices],
?SUBTYPE(union(Choices), [
{subenv, FreqChoices},
{generator, {typed, fun weighted_union_gen/1}}
]).
weighted_union_gen(Gen) ->
FreqChoices = get_prop(subenv, Gen),
proper_gen:weighted_union_gen(FreqChoices).
%% @private
-spec safe_union([raw_type(),...]) -> proper_types:type().
safe_union(RawChoices) ->
Choices = [cook_outer(C) || C <- RawChoices],
subtype(
[{subenv, Choices},
{generator, {typed, fun safe_union_gen/1}}],
union(Choices)).
safe_union_gen(Type) ->
Choices = get_prop(subenv, Type),
proper_gen:safe_union_gen(Choices).
%% @private
-spec safe_weighted_union([{frequency(),raw_type()},...]) ->
proper_types:type().
safe_weighted_union(RawFreqChoices) ->
CookFreqType = fun({Freq,RawType}) ->
{Freq,cook_outer(RawType)} end,
FreqChoices = lists:map(CookFreqType, RawFreqChoices),
Choices = [T || {_F,T} <- FreqChoices],
subtype([{subenv, FreqChoices},
{generator, {typed, fun safe_weighted_union_gen/1}}],
union(Choices)).
safe_weighted_union_gen(Type) ->
FreqChoices = get_prop(subenv, Type),
proper_gen:safe_weighted_union_gen(FreqChoices).
%% @doc All tuples whose i-th element is an instance of the type at index i of
%% `ListOfTypes'. Also written simply as a tuple of types.
-spec tuple(ListOfTypes::[raw_type()]) -> proper_types:type().
tuple(RawFields) ->
Fields = [cook_outer(F) || F <- RawFields],
?CONTAINER([
{env, Fields},
{generator, {typed, fun tuple_gen/1}},
{is_instance, {typed, fun tuple_is_instance/2}},
{internal_types, list_to_tuple(Fields)},
{get_indices, fun tuple_get_indices/2},
{retrieve, fun erlang:element/2},
{update, fun tuple_update/3}
]).
tuple_gen(Type) ->
Fields = get_prop(env, Type),
proper_gen:tuple_gen(Fields).
tuple_is_instance(Type, X) ->
Fields = get_prop(env, Type),
is_tuple(X) andalso fixed_list_test(tuple_to_list(X), Fields).
tuple_get_indices(Type, _X) ->
lists:seq(1, length(get_prop(env, Type))).
-spec tuple_update(index(), value(), tuple()) -> tuple().
tuple_update(Index, NewElem, Tuple) ->
setelement(Index, Tuple, NewElem).
%% @doc Tuples whose elements are all of type `ElemType'.
%% Instances shrink towards the 0-size tuple, `{}'.
-spec loose_tuple(ElemType::raw_type()) -> proper_types:type().
loose_tuple(RawElemType) ->
ElemType = cook_outer(RawElemType),
?WRAPPER([
{env, ElemType},
{generator, {typed, fun loose_tuple_gen/2}},
{reverse_gen, {typed, fun loose_tuple_rev/2}},
{is_instance, {typed, fun loose_tuple_is_instance/2}}
]).
loose_tuple_gen(Type, Size) ->
ElemType = get_prop(env, Type),
proper_gen:loose_tuple_gen(Size, ElemType).
loose_tuple_rev(Type, X) ->
ElemType = get_prop(env, Type),
proper_gen:loose_tuple_rev(X, ElemType).
loose_tuple_is_instance(Type, X) ->
ElemType = get_prop(env, Type),
is_tuple(X) andalso list_test(tuple_to_list(X), ElemType).
%% @doc Singleton type consisting only of `E'. `E' must be an evaluated term.
%% Also written simply as `E'.
-spec exactly(term()) -> proper_types:type().
exactly(E) ->
?BASIC([
{env, E},
{generator, {typed, fun exactly_gen/1}},
{is_instance, {typed, fun exactly_is_instance/2}}
]).
exactly_gen(Type) ->
E = get_prop(env, Type),
proper_gen:exactly_gen(E).
exactly_is_instance(Type, X) ->
E = get_prop(env, Type),
X =:= E.
%% @doc All lists whose i-th element is an instance of the type at index i of
%% `ListOfTypes'. Also written simply as a list of types.
-spec fixed_list(ListOfTypes::maybe_improper_list(raw_type(),raw_type()|[])) ->
proper_types:type().
fixed_list(MaybeImproperRawFields) ->
%% CAUTION: must handle improper lists
{Fields, Internal, Len, Retrieve, Update} =
case proper_arith:cut_improper_tail(MaybeImproperRawFields) of
% TODO: have cut_improper_tail return the length and use it in test?
{ProperRawHead, ImproperRawTail} ->
HeadLen = length(ProperRawHead),
CookedHead = [cook_outer(F) || F <- ProperRawHead],
CookedTail = cook_outer(ImproperRawTail),
{{CookedHead,CookedTail},
CookedHead ++ CookedTail,
HeadLen + 1,
fun(I,L) -> improper_list_retrieve(I, L, HeadLen) end,
fun(I,V,L) -> improper_list_update(I, V, L, HeadLen) end};
ProperRawFields ->
LocalFields = [cook_outer(F) || F <- ProperRawFields],
{LocalFields,
LocalFields,
length(ProperRawFields),
fun lists:nth/2,
fun proper_arith:list_update/3}
end,
?CONTAINER([
{env, {Fields, Len}},
{generator, {typed, fun fixed_list_gen/1}},
{is_instance, {typed, fun fixed_list_is_instance/2}},
{internal_types, Internal},
{get_indices, fun fixed_list_get_indices/2},
{retrieve, Retrieve},
{update, Update}
]).
fixed_list_gen(Type) ->
{Fields, _} = get_prop(env, Type),
proper_gen:fixed_list_gen(Fields).
fixed_list_is_instance(Type, X) ->
{Fields, _} = get_prop(env, Type),
fixed_list_test(X, Fields).
fixed_list_get_indices(Type, _X) ->
{_, Len} = get_prop(env, Type),
lists:seq(1, Len).
-spec fixed_list_test(proper_gen:imm_instance(),
[proper_types:type()] | {[proper_types:type()],
proper_types:type()}) ->
boolean().
fixed_list_test(X, {ProperHead,ImproperTail}) ->
is_list(X) andalso
begin
ProperHeadLen = length(ProperHead),
proper_arith:head_length(X) >= ProperHeadLen andalso
begin
{XHead,XTail} = lists:split(ProperHeadLen, X),
fixed_list_test(XHead, ProperHead)
andalso is_instance(XTail, ImproperTail)
end
end;
fixed_list_test(X, ProperFields) ->
is_list(X)
andalso length(X) =:= length(ProperFields)
andalso lists:all(fun({E,T}) -> is_instance(E, T) end,
lists:zip(X, ProperFields)).
%% TODO: Move these 2 functions to proper_arith?
-spec improper_list_retrieve(index(), nonempty_improper_list(value(),value()),
pos_integer()) -> value().
improper_list_retrieve(Index, List, HeadLen) ->
case Index =< HeadLen of
true -> lists:nth(Index, List);
false -> lists:nthtail(HeadLen, List)
end.
-spec improper_list_update(index(), value(),
nonempty_improper_list(value(),value()),
pos_integer()) ->
nonempty_improper_list(value(),value()).
improper_list_update(Index, Value, List, HeadLen) ->
case Index =< HeadLen of
%% TODO: This happens to work, but is not implied by list_update's spec.
true -> proper_arith:list_update(Index, Value, List);
false -> lists:sublist(List, HeadLen) ++ Value
end.
%% @doc All pure functions that map instances of `ArgTypes' to instances of
%% `RetType'. The syntax `function(Arity, RetType)' is also acceptable.
-spec function(ArgTypes::[raw_type()] | arity(), RetType::raw_type()) ->
proper_types:type().
function(Arity, RawRetType) when is_integer(Arity), Arity >= 0, Arity =< 255 ->
RetType = cook_outer(RawRetType),
?BASIC([
{env, {Arity, RetType}},
{generator, {typed, fun function_gen/1}},
{is_instance, {typed, fun function_is_instance/2}}
]);
function(RawArgTypes, RawRetType) ->
function(length(RawArgTypes), RawRetType).
function_gen(Type) ->
{Arity, RetType} = get_prop(env, Type),
proper_gen:function_gen(Arity, RetType).
function_is_instance(Type, X) ->
{Arity, RetType} = get_prop(env, Type),
is_function(X, Arity)
%% TODO: what if it's not a function we produced?
andalso equal_types(RetType, proper_gen:get_ret_type(X)).
%% @doc All Erlang terms (that PropEr can produce). For reasons of efficiency,
%% functions are never produced as instances of this type.<br />
%% CAUTION: Instances of this type are expensive to produce, shrink and instance-
%% check, both in terms of processing time and consumed memory. Only use this
%% type if you are certain that you need it.
-spec any() -> proper_types:type().
any() ->
AllTypes = [integer(),float(),atom(),bitstring(),?LAZY(loose_tuple(any())),
?LAZY(list(any()))],
?SUBTYPE(union(AllTypes), [
{generator, fun proper_gen:any_gen/1}
]).
%%------------------------------------------------------------------------------
%% Type aliases
%%------------------------------------------------------------------------------
%% @equiv integer(inf, inf)
-spec integer() -> proper_types:type().
integer() -> integer(inf, inf).
%% @equiv integer(0, inf)
-spec non_neg_integer() -> proper_types:type().
non_neg_integer() -> integer(0, inf).
%% @equiv integer(1, inf)
-spec pos_integer() -> proper_types:type().
pos_integer() -> integer(1, inf).
%% @equiv integer(inf, -1)
-spec neg_integer() -> proper_types:type().
neg_integer() -> integer(inf, -1).
%% @equiv integer(Low, High)
-spec range(extint(), extint()) -> proper_types:type().
range(Low, High) -> integer(Low, High).
%% @equiv float(inf, inf)
-spec float() -> proper_types:type().
float() -> float(inf, inf).
%% @equiv float(0.0, inf)
-spec non_neg_float() -> proper_types:type().
non_neg_float() -> float(0.0, inf).
%% @equiv union([integer(), float()])
-spec number() -> proper_types:type().
number() -> union([integer(), float()]).
%% @doc The atoms `true' and `false'. Instances shrink towards `false'.
-spec boolean() -> proper_types:type().
boolean() -> union(['false', 'true']).
%% @equiv integer(0, 255)
-spec byte() -> proper_types:type().
byte() -> integer(0, 255).
%% @equiv integer(0, 16#10ffff)
-spec char() -> proper_types:type().
char() -> integer(0, 16#10ffff).
%% @equiv list(any())
-spec list() -> proper_types:type().
list() -> list(any()).
%% @equiv loose_tuple(any())
-spec tuple() -> proper_types:type().
tuple() -> loose_tuple(any()).
%% @equiv list(char())
-spec string() -> proper_types:type().
string() -> list(char()).
%% @equiv weighted_union(FreqChoices)
-spec wunion([{frequency(),raw_type()},...]) -> proper_types:type().
wunion(FreqChoices) -> weighted_union(FreqChoices).
%% @equiv any()
-spec term() -> proper_types:type().
term() -> any().
%% @equiv union([non_neg_integer() | infinity])
-spec timeout() -> proper_types:type().
timeout() -> union([non_neg_integer(), 'infinity']).
%% @equiv integer(0, 255)
-spec arity() -> proper_types:type().
arity() -> integer(0, 255).
%%------------------------------------------------------------------------------
%% QuickCheck compatibility types
%%------------------------------------------------------------------------------
%% @doc Small integers (bound by the current value of the `size' parameter).
%% Instances shrink towards `0'.
-spec int() -> proper_types:type().
int() -> ?SIZED(Size, integer(-Size,Size)).
%% @doc Small non-negative integers (bound by the current value of the `size'
%% parameter). Instances shrink towards `0'.
-spec nat() -> proper_types:type().
nat() -> ?SIZED(Size, integer(0,Size)).
%% @equiv integer()
-spec largeint() -> proper_types:type().
largeint() -> integer().
%% @equiv float()
-spec real() -> proper_types:type().
real() -> float().
%% @equiv boolean()
-spec bool() -> proper_types:type().
bool() -> boolean().
%% @equiv integer(Low, High)
-spec choose(extint(), extint()) -> proper_types:type().
choose(Low, High) -> integer(Low, High).
%% @equiv union(Choices)
-spec elements([raw_type(),...]) -> proper_types:type().
elements(Choices) -> union(Choices).
%% @equiv union(Choices)
-spec oneof([raw_type(),...]) -> proper_types:type().
oneof(Choices) -> union(Choices).
%% @equiv weighted_union(Choices)
-spec frequency([{frequency(),raw_type()},...]) -> proper_types:type().
frequency(FreqChoices) -> weighted_union(FreqChoices).
%% @equiv exactly(E)
-spec return(term()) -> proper_types:type().
return(E) -> exactly(E).
%% @doc Adds a default value, `Default', to `Type'.
%% The default serves as a primary shrinking target for instances, while it
%% is also chosen by the random instance generation subsystem half the time.
-spec default(raw_type(), raw_type()) -> proper_types:type().
default(Default, Type) ->
union([Default, Type]).
%% @doc All sorted lists containing elements of type `ElemType'.
%% Instances shrink towards the empty list, `[]'.
-spec orderedlist(ElemType::raw_type()) -> proper_types:type().
orderedlist(RawElemType) ->
?LET(L, list(RawElemType), lists:sort(L)).
%% @equiv function(0, RetType)
-spec function0(raw_type()) -> proper_types:type().
function0(RetType) ->
function(0, RetType).
%% @equiv function(1, RetType)
-spec function1(raw_type()) -> proper_types:type().
function1(RetType) ->
function(1, RetType).
%% @equiv function(2, RetType)
-spec function2(raw_type()) -> proper_types:type().
function2(RetType) ->
function(2, RetType).
%% @equiv function(3, RetType)
-spec function3(raw_type()) -> proper_types:type().
function3(RetType) ->
function(3, RetType).
%% @equiv function(4, RetType)
-spec function4(raw_type()) -> proper_types:type().
function4(RetType) ->
function(4, RetType).
%% @doc A specialization of {@link default/2}, where `Default' and `Type' are
%% assigned weights to be considered by the random instance generator. The
%% shrinking subsystem will ignore the weights and try to shrink using the
%% default value.
-spec weighted_default({frequency(),raw_type()}, {frequency(),raw_type()}) ->
proper_types:type().
weighted_default(Default, Type) ->
weighted_union([Default, Type]).
%%------------------------------------------------------------------------------
%% Additional type specification functions
%%------------------------------------------------------------------------------
%% @doc Overrides the `size' parameter used when generating instances of
%% `Type' with `NewSize'. Has no effect on size-less types, such as unions.
%% Also, this will not affect the generation of any internal types contained in
%% `Type', such as the elements of a list - those will still be generated
%% using the test-wide value of `size'. One use of this function is to modify
%% types to produce instances that grow faster or slower, like so:
%% ```?SIZED(Size, resize(Size * 2, list(integer()))'''
%% The above specifies a list type that grows twice as fast as normal lists.
-spec resize(size(), Type::raw_type()) -> proper_types:type().
resize(NewSize, RawType) ->
Type = cook_outer(RawType),
case find_prop(size_transform, Type) of
{ok,Transform} ->
add_prop(size_transform, fun(_S) -> Transform(NewSize) end, Type);
error ->
add_prop(size_transform, fun(_S) -> NewSize end, Type)
end.
%% @doc This is a predefined constraint that can be applied to random-length
%% list and binary types to ensure that the produced values are never empty.
%%
%% e.g. {@link list/0}, {@link string/0}, {@link binary/0})
-spec non_empty(ListType::raw_type()) -> proper_types:type().
non_empty(RawListType) ->
?SUCHTHAT(L, RawListType, L =/= [] andalso L =/= <<>>).
%% @doc Creates a new type which is equivalent to `Type', but whose instances
%% are never shrunk by the shrinking subsystem.
-spec noshrink(Type::raw_type()) -> proper_types:type().
noshrink(RawType) ->
add_prop(noshrink, true, cook_outer(RawType)).
%% @doc Associates the atom key `Parameter' with the value `Value' while
%% generating instances of `Type'.
-spec with_parameter(atom(), value(), Type::raw_type()) -> proper_types:type().
with_parameter(Parameter, Value, RawType) ->
with_parameters([{Parameter,Value}], RawType).
%% @doc Similar to {@link with_parameter/3}, but accepts a list of
%% `{Parameter, Value}' pairs.
-spec with_parameters([{atom(),value()}], Type::raw_type()) ->
proper_types:type().
with_parameters(PVlist, RawType) ->
Type = cook_outer(RawType),
case find_prop(parameters, Type) of
{ok,Params} when is_list(Params) ->
append_list_to_prop(parameters, PVlist, Type);
error ->
add_prop(parameters, PVlist, Type)
end.
%% @doc Returns the value associated with `Parameter', or `Default' in case
%% `Parameter' is not associated with any value.
-spec parameter(atom(), value()) -> value().
parameter(Parameter, Default) ->
Parameters =
case erlang:get('$parameters') of
undefined -> [];
List -> List
end,
proplists:get_value(Parameter, Parameters, Default).
%% @equiv parameter(Parameter, undefined)
-spec parameter(atom()) -> value().
parameter(Parameter) ->
parameter(Parameter, undefined).