Skip to content

Commit a76d552

Browse files
Add generator for existing atoms
Co-authored-by: Jan Uhlig <juhlig@hnc-agency.org>
1 parent d079c2f commit a76d552

File tree

4 files changed

+113
-29
lines changed

4 files changed

+113
-29
lines changed

src/proper.erl

Lines changed: 36 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -297,6 +297,10 @@
297297
%%% <dt>`{stop_nodes, boolean()}'</dt>
298298
%%% <dd> Specifies whether parallel PropEr should stop the nodes after running a property
299299
%%% or not. Defaults to true.</dd>
300+
%%% <dt>`{default_atom_generator, atom | existing_atom}'</dt>
301+
%%% <dd> Declares the type of atom generator to use in the {@link proper_types:any/0. `any()'}
302+
%%% to be either {@link proper_types:atom/0. `atom()'} or
303+
%%% {@link proper_types:existing_atom/0. `existing_atom()'}.</dd>
300304
%%% </dl>
301305
%%%
302306
%%% == Spec testing ==
@@ -539,6 +543,7 @@
539543
| 'quiet'
540544
| 'verbose'
541545
| pos_integer()
546+
| {'default_atom_generator', 'atom' | 'existing_atom'}
542547
| {'constraint_tries',pos_integer()}
543548
| {'false_positive_mfas',false_positive_mfas()}
544549
| {'max_shrinks',non_neg_integer()}
@@ -556,29 +561,30 @@
556561
| {'to_file',io:device()}.
557562

558563
-type user_opts() :: [user_opt()] | user_opt().
559-
-record(opts, {output_fun = fun io:format/2 :: output_fun(),
560-
long_result = false :: boolean(),
561-
numtests = 100 :: pos_integer(),
562-
search_steps = 1000 :: pos_integer(),
563-
search_strategy = proper_sa :: proper_target:strategy(),
564-
start_size = 1 :: proper_gen:size(),
565-
seed = os:timestamp() :: proper_gen:seed(),
566-
max_size = 42 :: proper_gen:size(),
567-
max_shrinks = 500 :: non_neg_integer(),
568-
noshrink = false :: boolean(),
569-
constraint_tries = 50 :: pos_integer(),
570-
expect_fail = false :: boolean(),
571-
any_type :: {'type', proper_types:type()} | 'undefined',
572-
spec_timeout = infinity :: timeout(),
573-
skip_mfas = [] :: [mfa()],
574-
false_positive_mfas :: false_positive_mfas(),
575-
setup_funs = [] :: [setup_fun()],
576-
numworkers = 0 :: non_neg_integer(),
577-
property_type = pure :: purity(),
578-
strategy_fun = default_strategy_fun() :: strategy_fun(),
579-
stop_nodes = true :: boolean(),
580-
parent = self() :: pid(),
581-
nocolors = false :: boolean()}).
564+
-record(opts, {output_fun = fun io:format/2 :: output_fun(),
565+
long_result = false :: boolean(),
566+
numtests = 100 :: pos_integer(),
567+
search_steps = 1000 :: pos_integer(),
568+
search_strategy = proper_sa :: proper_target:strategy(),
569+
start_size = 1 :: proper_gen:size(),
570+
seed = os:timestamp() :: proper_gen:seed(),
571+
max_size = 42 :: proper_gen:size(),
572+
max_shrinks = 500 :: non_neg_integer(),
573+
noshrink = false :: boolean(),
574+
constraint_tries = 50 :: pos_integer(),
575+
expect_fail = false :: boolean(),
576+
any_type :: {'type', proper_types:type()} | 'undefined',
577+
default_atom_generator = atom :: 'atom' | 'existing_atom',
578+
spec_timeout = infinity :: timeout(),
579+
skip_mfas = [] :: [mfa()],
580+
false_positive_mfas :: false_positive_mfas(),
581+
setup_funs = [] :: [setup_fun()],
582+
numworkers = 0 :: non_neg_integer(),
583+
property_type = pure :: purity(),
584+
strategy_fun = default_strategy_fun() :: strategy_fun(),
585+
stop_nodes = true :: boolean(),
586+
parent = self() :: pid(),
587+
nocolors = false :: boolean()}).
582588
-type opts() :: #opts{}.
583589
-record(ctx, {mode = new :: 'new' | 'try_shrunk' | 'try_cexm',
584590
bound = [] :: imm_testcase() | counterexample(),
@@ -744,8 +750,10 @@ global_state_init_size_seed(Size, Seed) ->
744750
-spec global_state_init(opts()) -> 'ok'.
745751
global_state_init(#opts{start_size = StartSize, constraint_tries = CTries,
746752
search_strategy = Strategy, search_steps = SearchSteps,
747-
any_type = AnyType, seed = Seed, numworkers = NumWorkers} = Opts) ->
753+
any_type = AnyType, seed = Seed, numworkers = NumWorkers,
754+
default_atom_generator = DefaultAtomGen} = Opts) ->
748755
clean_garbage(),
756+
put('$default_atom_generator', DefaultAtomGen),
749757
put('$size', StartSize - 1),
750758
put('$left', 0),
751759
put('$search_strategy', Strategy),
@@ -772,6 +780,8 @@ global_state_reset(#opts{start_size = StartSize} = Opts) ->
772780
global_state_erase() ->
773781
proper_typeserver:stop(),
774782
proper_arith:rand_stop(),
783+
erase('$default_atom_generator'),
784+
erase('$existing_atoms'),
775785
erase('$any_type'),
776786
erase('$constraint_tries'),
777787
erase('$left'),
@@ -1039,6 +1049,8 @@ parse_opt(UserOpt, Opts) ->
10391049
N when is_integer(N) ->
10401050
?VALIDATE_OPT(?POS_INTEGER(N), Opts#opts{numtests = N});
10411051
%% tuple options, sorted on tag
1052+
{default_atom_generator,G} ->
1053+
?VALIDATE_OPT(G =:= atom orelse G =:= existing_atom, Opts#opts{default_atom_generator = G});
10421054
{constraint_tries,N} ->
10431055
?VALIDATE_OPT(?POS_INTEGER(N), Opts#opts{constraint_tries = N});
10441056
{false_positive_mfas,F} ->

src/proper_gen.erl

Lines changed: 50 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,8 @@
4646
any_gen/1, native_type_gen/2, safe_weighted_union_gen/1,
4747
safe_union_gen/1]).
4848

49+
-export([existing_atom_gen/1]).
50+
4951
%% Public API types
5052
-export_type([instance/0, seed/0, size/0]).
5153
%% Internal types
@@ -418,6 +420,53 @@ atom_gen(Size) ->
418420
atom_rev(Atom) ->
419421
{'$used', atom_to_list(Atom), Atom}.
420422

423+
%% @private
424+
-spec existing_atom_gen(size()) -> atom().
425+
%% We make sure we never clash with internal atoms by ignoring atoms starting with
426+
%% the character '$'.
427+
existing_atom_gen(_Size) ->
428+
case get('$existing_atoms') of
429+
undefined ->
430+
{NextIndex, Atoms} = get_existing_atoms(0, #{}),
431+
Range = maps:size(Atoms),
432+
put('$existing_atoms', {NextIndex, Range, Atoms}),
433+
X = proper_arith:rand_int(1, Range),
434+
maps:get(X, Atoms);
435+
{NextIndex, Range, Atoms} ->
436+
case get_existing_atoms(NextIndex, Atoms) of
437+
{NextIndex, _} ->
438+
X = proper_arith:rand_int(1, Range),
439+
maps:get(X, Atoms);
440+
{NextIndex1, Atoms1} ->
441+
Range1 = maps:size(Atoms1),
442+
put('$existing_atoms', {NextIndex1, Range1, Atoms1}),
443+
X = proper_arith:rand_int(1, Range1),
444+
maps:get(X, Atoms1)
445+
end
446+
end.
447+
448+
-spec get_existing_atoms(non_neg_integer(), #{pos_integer() => atom()}) -> {non_neg_integer(), #{pos_integer() => atom()}}.
449+
get_existing_atoms(StartIndex, AtomMap) ->
450+
get_existing_atoms(StartIndex, maps:size(AtomMap) + 1, AtomMap).
451+
452+
get_existing_atoms(Index, Key, AtomMap) ->
453+
try
454+
binary_to_term(<<131, 75, Index:24>>)
455+
of
456+
'' ->
457+
get_existing_atoms(Index + 1, Key + 1, AtomMap#{Key => ''});
458+
Atom ->
459+
case hd(atom_to_list(Atom)) of
460+
$$ ->
461+
get_existing_atoms(Index + 1, Key, AtomMap);
462+
_ ->
463+
get_existing_atoms(Index + 1, Key + 1, AtomMap#{Key => Atom})
464+
end
465+
catch
466+
error:badarg ->
467+
{Index, AtomMap}
468+
end.
469+
421470
%% @private
422471
-spec binary_gen(size()) -> proper_types:type().
423472
binary_gen(Size) ->
@@ -594,7 +643,7 @@ any_gen(Size) ->
594643
-spec real_any_gen(size()) -> imm_instance().
595644
real_any_gen(0) ->
596645
SimpleTypes = [proper_types:integer(), proper_types:float(),
597-
proper_types:atom()],
646+
proper_types:default_atom()],
598647
union_gen(SimpleTypes);
599648
real_any_gen(Size) ->
600649
FreqChoices = [{?ANY_SIMPLE_PROB,simple}, {?ANY_BINARY_PROB,binary},

src/proper_gen_next.erl

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -315,7 +315,7 @@ float_gen_sa({'$type', TypeProps}) ->
315315

316316
%% List
317317
is_list_type(Type) ->
318-
has_same_generator(Type, proper_types:list(proper_types:atom())).
318+
has_same_generator(Type, proper_types:list(proper_types:default_atom())).
319319

320320
list_choice(empty, Temp) ->
321321
C = ?RANDOM_MOD:uniform(),
@@ -426,7 +426,7 @@ vector_gen_sa(Type) ->
426426

427427
%% atom
428428
is_atom_type(Type) ->
429-
has_same_generator(Type, proper_types:atom()).
429+
has_same_generator(Type, proper_types:default_atom()).
430430

431431
atom_gen_sa(_AtomType) ->
432432
StringType = proper_types:list(proper_types:integer(0, 255)),

src/proper_types.erl

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -139,7 +139,7 @@
139139
-module(proper_types).
140140
-export([is_inst/2, is_inst/3]).
141141

142-
-export([integer/2, float/2, atom/0, binary/0, binary/1, bitstring/0,
142+
-export([integer/2, float/2, atom/0, existing_atom/0, binary/0, binary/1, bitstring/0,
143143
bitstring/1, list/1, vector/2, union/1, weighted_union/1, tuple/1,
144144
loose_tuple/1, exactly/1, fixed_list/1, function/2, map/0, map/2,
145145
any/0, shrink_list/1, safe_union/1, safe_weighted_union/1]).
@@ -160,6 +160,7 @@
160160
native_type/2, distlist/3, with_parameter/3, with_parameters/2,
161161
parameter/1, parameter/2]).
162162
-export([le/2]).
163+
-export([default_atom/0]).
163164

164165
%% Public API types
165166
-export_type([type/0, raw_type/0]).
@@ -674,6 +675,28 @@ atom() ->
674675
{is_instance, fun atom_is_instance/1}
675676
]).
676677

678+
%% @doc All existing atoms. All atoms used internally by PropEr start with a
679+
%% '`$'', so such atoms will never be produced as instances of this type. You
680+
%% should also refrain from using such atoms in your code, to avoid a potential
681+
%% clash.
682+
%% Instances do not shrink.
683+
-spec existing_atom() -> proper_types:type().
684+
existing_atom() ->
685+
?BASIC([
686+
{generator, fun proper_gen:existing_atom_gen/1},
687+
{reverse_gen, fun proper_gen:atom_rev/1},
688+
{is_instance, fun atom_is_instance/1},
689+
{noshrink, true}
690+
]).
691+
692+
%% @private
693+
-spec default_atom() -> proper_types:type().
694+
default_atom() ->
695+
case get('$default_atom_generator') of
696+
existing_atom -> existing_atom();
697+
_ -> atom()
698+
end.
699+
677700
atom_is_instance(X) ->
678701
is_atom(X)
679702
%% We return false for atoms starting with '$', since these are
@@ -1127,7 +1150,7 @@ map(K, V) ->
11271150
%% type if you are certain that you need it.
11281151
-spec any() -> proper_types:type().
11291152
any() ->
1130-
AllTypes = [integer(),float(),atom(),bitstring(),?LAZY(loose_tuple(any())),
1153+
AllTypes = [integer(),float(),default_atom(),bitstring(),?LAZY(loose_tuple(any())),
11311154
?LAZY(list(any())), ?LAZY(map(any(), any()))],
11321155
subtype([{generator, fun proper_gen:any_gen/1}], union(AllTypes)).
11331156

0 commit comments

Comments
 (0)