Move almost all test code to test/* modules
This commit is contained in:
parent
01e3325b81
commit
e717d797b3
5 changed files with 336 additions and 314 deletions
|
@ -20,9 +20,36 @@
|
||||||
%%
|
%%
|
||||||
%% -------------------------------------------------------------------
|
%% -------------------------------------------------------------------
|
||||||
|
|
||||||
-record(proj, { % Projection
|
-record(proj, { % Projection (OLD!)
|
||||||
epoch :: non_neg_integer(),
|
epoch :: non_neg_integer(),
|
||||||
all :: list(pid()),
|
all :: list(pid()),
|
||||||
active :: list(pid())
|
active :: list(pid())
|
||||||
}).
|
}).
|
||||||
|
|
||||||
|
-type m_csum() :: {none | sha1 | sha1_excl_final_20, binary()}.
|
||||||
|
%% -type m_epoch() :: {m_epoch_n(), m_csum()}.
|
||||||
|
-type m_epoch_n() :: non_neg_integer().
|
||||||
|
-type m_server() :: atom().
|
||||||
|
-type timestamp() :: {non_neg_integer(), non_neg_integer(), non_neg_integer()}.
|
||||||
|
|
||||||
|
-record(projection, {
|
||||||
|
epoch_number :: m_epoch_n(),
|
||||||
|
epoch_csum :: m_csum(),
|
||||||
|
prev_epoch_num :: m_epoch_n(),
|
||||||
|
prev_epoch_csum :: m_csum(),
|
||||||
|
creation_time :: timestamp(),
|
||||||
|
author_server :: m_server(),
|
||||||
|
all_members :: [m_server()],
|
||||||
|
down :: [m_server()],
|
||||||
|
upi :: [m_server()],
|
||||||
|
repairing :: [m_server()],
|
||||||
|
dbg :: list() %proplist()
|
||||||
|
}).
|
||||||
|
|
||||||
|
-record(ch_mgr, {
|
||||||
|
name :: m_server(),
|
||||||
|
proj :: #projection{},
|
||||||
|
%%
|
||||||
|
runenv :: list() %proplist()
|
||||||
|
}).
|
||||||
|
|
||||||
|
|
|
@ -21,16 +21,14 @@
|
||||||
%% -------------------------------------------------------------------
|
%% -------------------------------------------------------------------
|
||||||
-module(machi_chain_manager).
|
-module(machi_chain_manager).
|
||||||
|
|
||||||
-export([]).
|
-include("machi.hrl").
|
||||||
-compile(export_all).
|
|
||||||
|
-export([make_initial_state/3, calc_projection/3, make_projection_summary/1]).
|
||||||
|
|
||||||
-ifdef(TEST).
|
-ifdef(TEST).
|
||||||
-compile(export_all).
|
|
||||||
|
|
||||||
-ifdef(EQC).
|
-ifdef(EQC).
|
||||||
-include_lib("eqc/include/eqc.hrl").
|
-include_lib("eqc/include/eqc.hrl").
|
||||||
-define(QC_OUT(P),
|
|
||||||
eqc:on_output(fun(Str, Args) -> io:format(user, Str, Args) end, P)).
|
|
||||||
-endif.
|
-endif.
|
||||||
-ifdef(PULSE).
|
-ifdef(PULSE).
|
||||||
-compile({parse_transform, pulse_instrument}).
|
-compile({parse_transform, pulse_instrument}).
|
||||||
|
@ -38,37 +36,10 @@
|
||||||
|
|
||||||
-endif. %TEST
|
-endif. %TEST
|
||||||
|
|
||||||
-type m_csum() :: {none | sha1 | sha1_excl_final_20, binary()}.
|
|
||||||
%% -type m_epoch() :: {m_epoch_n(), m_csum()}.
|
|
||||||
-type m_epoch_n() :: non_neg_integer().
|
|
||||||
-type m_server() :: atom().
|
|
||||||
-type timestamp() :: {non_neg_integer(), non_neg_integer(), non_neg_integer()}.
|
|
||||||
|
|
||||||
-record(projection, {
|
|
||||||
epoch_number :: m_epoch_n(),
|
|
||||||
epoch_csum :: m_csum(),
|
|
||||||
prev_epoch_num :: m_epoch_n(),
|
|
||||||
prev_epoch_csum :: m_csum(),
|
|
||||||
creation_time :: timestamp(),
|
|
||||||
author_server :: m_server(),
|
|
||||||
all_members :: [m_server()],
|
|
||||||
down :: [m_server()],
|
|
||||||
upi :: [m_server()],
|
|
||||||
repairing :: [m_server()],
|
|
||||||
dbg :: list() %proplist()
|
|
||||||
}).
|
|
||||||
|
|
||||||
-record(state, {
|
|
||||||
name :: m_server(),
|
|
||||||
proj :: #projection{},
|
|
||||||
%%
|
|
||||||
runenv :: list() %proplist()
|
|
||||||
}).
|
|
||||||
|
|
||||||
make_initial_state(MyName, All_list, Seed) ->
|
make_initial_state(MyName, All_list, Seed) ->
|
||||||
RunEnv = [{seed, Seed},
|
RunEnv = [{seed, Seed},
|
||||||
{network_partitions, []}],
|
{network_partitions, []}],
|
||||||
#state{name=MyName,
|
#ch_mgr{name=MyName,
|
||||||
proj=make_initial_projection(MyName, All_list, All_list, [], []),
|
proj=make_initial_projection(MyName, All_list, All_list, [], []),
|
||||||
runenv=RunEnv}.
|
runenv=RunEnv}.
|
||||||
|
|
||||||
|
@ -97,7 +68,7 @@ make_projection(EpochNum, PrevEpochNum, PrevEpochCSum,
|
||||||
%% that there are no partitions at all?
|
%% that there are no partitions at all?
|
||||||
|
|
||||||
calc_projection(OldThreshold, NoPartitionThreshold,
|
calc_projection(OldThreshold, NoPartitionThreshold,
|
||||||
#state{name=MyName, proj=LastProj, runenv=RunEnv1} = S) ->
|
#ch_mgr{name=MyName, proj=LastProj, runenv=RunEnv1} = S) ->
|
||||||
#projection{epoch_number=OldEpochNum,
|
#projection{epoch_number=OldEpochNum,
|
||||||
epoch_csum=OldEpochCsum,
|
epoch_csum=OldEpochCsum,
|
||||||
all_members=All_list,
|
all_members=All_list,
|
||||||
|
@ -105,7 +76,7 @@ calc_projection(OldThreshold, NoPartitionThreshold,
|
||||||
repairing=OldRepairing_list
|
repairing=OldRepairing_list
|
||||||
} = LastProj,
|
} = LastProj,
|
||||||
LastUp = lists:usort(OldUPI_list ++ OldRepairing_list),
|
LastUp = lists:usort(OldUPI_list ++ OldRepairing_list),
|
||||||
AllMembers = (S#state.proj)#projection.all_members,
|
AllMembers = (S#ch_mgr.proj)#projection.all_members,
|
||||||
{Up, RunEnv2} = calc_up_nodes(MyName, OldThreshold, NoPartitionThreshold,
|
{Up, RunEnv2} = calc_up_nodes(MyName, OldThreshold, NoPartitionThreshold,
|
||||||
AllMembers, RunEnv1),
|
AllMembers, RunEnv1),
|
||||||
NewUp = Up -- LastUp,
|
NewUp = Up -- LastUp,
|
||||||
|
@ -135,7 +106,7 @@ calc_projection(OldThreshold, NoPartitionThreshold,
|
||||||
P = make_projection(OldEpochNum + 1, OldEpochNum, OldEpochCsum,
|
P = make_projection(OldEpochNum + 1, OldEpochNum, OldEpochCsum,
|
||||||
MyName, All_list, Down, NewUPI_list3, Repairing_list5,
|
MyName, All_list, Down, NewUPI_list3, Repairing_list5,
|
||||||
[goo]),
|
[goo]),
|
||||||
{P, S#state{runenv=RunEnv3}}.
|
{P, S#ch_mgr{runenv=RunEnv3}}.
|
||||||
|
|
||||||
calc_up_nodes(MyName, OldThreshold, NoPartitionThreshold,
|
calc_up_nodes(MyName, OldThreshold, NoPartitionThreshold,
|
||||||
AllMembers, RunEnv1) ->
|
AllMembers, RunEnv1) ->
|
||||||
|
@ -208,159 +179,3 @@ make_all_pairs2([_]) ->
|
||||||
[];
|
[];
|
||||||
make_all_pairs2([H1|T]) ->
|
make_all_pairs2([H1|T]) ->
|
||||||
[[{H1, X}, {X, H1}] || X <- T] ++ make_all_pairs(T).
|
[[{H1, X}, {X, H1}] || X <- T] ++ make_all_pairs(T).
|
||||||
|
|
||||||
-ifdef(TEST).
|
|
||||||
|
|
||||||
smoke0_test() ->
|
|
||||||
S0 = ?MODULE:make_initial_state(a, [a,b,c,d], {4,5,6}),
|
|
||||||
lists:foldl(fun(_, S) ->
|
|
||||||
{P1, S1} = calc_projection(20, 1, S),
|
|
||||||
io:format(user, "~p\n", [make_projection_summary(P1)]),
|
|
||||||
S1#state{proj=P1}
|
|
||||||
end, S0, lists:seq(1,10)).
|
|
||||||
|
|
||||||
-ifdef(EQC).
|
|
||||||
gen_rand_seed() ->
|
|
||||||
noshrink({gen_num(), gen_num(), gen_num()}).
|
|
||||||
|
|
||||||
gen_num() ->
|
|
||||||
?LET(I, oneof([int(), largeint()]),
|
|
||||||
erlang:abs(I)).
|
|
||||||
|
|
||||||
calc_projection_prop() ->
|
|
||||||
?FORALL(
|
|
||||||
{Seed, OldThreshold, NoPartitionThreshold},
|
|
||||||
{gen_rand_seed(), choose(0, 101), choose(0,101)},
|
|
||||||
begin
|
|
||||||
Steps = 200,
|
|
||||||
S0 = ?MODULE:make_initial_state(a, [a,b,c,d], Seed),
|
|
||||||
F = fun(_, {S, Acc}) ->
|
|
||||||
{P1, S1} = calc_projection(OldThreshold,
|
|
||||||
NoPartitionThreshold, S),
|
|
||||||
%%io:format(user, "~p\n", [make_projection_summary(P1)]),
|
|
||||||
{S1#state{proj=P1}, [P1|Acc]}
|
|
||||||
end,
|
|
||||||
{_, Projs0} = lists:foldl(F, {S0, []}, lists:seq(1,Steps)),
|
|
||||||
Projs = lists:reverse(Projs0),
|
|
||||||
true = projection_transitions_are_sane(Projs)
|
|
||||||
end).
|
|
||||||
|
|
||||||
calc_projection_test_() ->
|
|
||||||
{timeout, 60,
|
|
||||||
fun() ->
|
|
||||||
true = eqc:quickcheck(eqc:numtests(1000,
|
|
||||||
?QC_OUT(calc_projection_prop())))
|
|
||||||
end}.
|
|
||||||
|
|
||||||
projection_transitions_are_sane([]) ->
|
|
||||||
true;
|
|
||||||
projection_transitions_are_sane([_]) ->
|
|
||||||
true;
|
|
||||||
projection_transitions_are_sane([P1, P2|T]) ->
|
|
||||||
case projection_transition_is_sane(P1, P2) of
|
|
||||||
true ->
|
|
||||||
projection_transitions_are_sane([P2|T]);
|
|
||||||
Else ->
|
|
||||||
Else
|
|
||||||
end.
|
|
||||||
|
|
||||||
projection_transition_is_sane(
|
|
||||||
#projection{epoch_number=Epoch1,
|
|
||||||
epoch_csum=CSum1,
|
|
||||||
prev_epoch_num=PrevEpoch1,
|
|
||||||
prev_epoch_csum=PrevCSum1,
|
|
||||||
creation_time=CreationTime1,
|
|
||||||
author_server=AuthorServer1,
|
|
||||||
all_members=All_list1,
|
|
||||||
down=Down_list1,
|
|
||||||
upi=UPI_list1,
|
|
||||||
repairing=Repairing_list1,
|
|
||||||
dbg=Dbg1} = _P1,
|
|
||||||
#projection{epoch_number=Epoch2,
|
|
||||||
epoch_csum=CSum2,
|
|
||||||
prev_epoch_num=PrevEpoch2,
|
|
||||||
prev_epoch_csum=PrevCSum2,
|
|
||||||
creation_time=CreationTime2,
|
|
||||||
author_server=AuthorServer2,
|
|
||||||
all_members=All_list2,
|
|
||||||
down=Down_list2,
|
|
||||||
upi=UPI_list2,
|
|
||||||
repairing=Repairing_list2,
|
|
||||||
dbg=Dbg2} = _P2) ->
|
|
||||||
true = is_integer(Epoch1) andalso is_integer(Epoch2),
|
|
||||||
true = is_binary(CSum1) andalso is_binary(CSum2),
|
|
||||||
true = is_integer(PrevEpoch1) andalso is_integer(PrevEpoch2),
|
|
||||||
true = is_binary(PrevCSum1) andalso is_binary(PrevCSum2),
|
|
||||||
{_,_,_} = CreationTime1,
|
|
||||||
{_,_,_} = CreationTime2,
|
|
||||||
true = is_atom(AuthorServer1) andalso is_atom(AuthorServer2), % todo will probably change
|
|
||||||
true = is_list(All_list1) andalso is_list(All_list2),
|
|
||||||
true = is_list(Down_list1) andalso is_list(Down_list2),
|
|
||||||
true = is_list(UPI_list1) andalso is_list(UPI_list2),
|
|
||||||
true = is_list(Repairing_list1) andalso is_list(Repairing_list2),
|
|
||||||
true = is_list(Dbg1) andalso is_list(Dbg2),
|
|
||||||
|
|
||||||
true = Epoch2 > Epoch1,
|
|
||||||
true = PrevEpoch2 > PrevEpoch1,
|
|
||||||
All_list1 = All_list2, % todo will probably change
|
|
||||||
|
|
||||||
%% No duplicates
|
|
||||||
true = lists:sort(Down_list2) == lists:usort(Down_list2),
|
|
||||||
true = lists:sort(UPI_list2) == lists:usort(UPI_list2),
|
|
||||||
true = lists:sort(Repairing_list2) == lists:usort(Repairing_list2),
|
|
||||||
|
|
||||||
%% Disjoint-ness
|
|
||||||
true = lists:sort(All_list2) == lists:sort(Down_list2 ++ UPI_list2 ++
|
|
||||||
Repairing_list2),
|
|
||||||
[] = [X || X <- Down_list2, not lists:member(X, All_list2)],
|
|
||||||
[] = [X || X <- UPI_list2, not lists:member(X, All_list2)],
|
|
||||||
[] = [X || X <- Repairing_list2, not lists:member(X, All_list2)],
|
|
||||||
DownS2 = sets:from_list(Down_list2),
|
|
||||||
UPIS2 = sets:from_list(UPI_list2),
|
|
||||||
RepairingS2 = sets:from_list(Repairing_list2),
|
|
||||||
true = sets:is_disjoint(DownS2, UPIS2),
|
|
||||||
true = sets:is_disjoint(DownS2, RepairingS2),
|
|
||||||
true = sets:is_disjoint(UPIS2, RepairingS2),
|
|
||||||
|
|
||||||
%% Additions to the UPI chain may only be at the tail
|
|
||||||
UPI_common_prefix =
|
|
||||||
lists:takewhile(fun(X) -> sets:is_element(X, UPIS2) end, UPI_list1),
|
|
||||||
%% If the common prefix is empty, then one of the inputs must be empty.
|
|
||||||
if UPI_common_prefix == [] ->
|
|
||||||
true = UPI_list1 == [] orelse UPI_list2 == [];
|
|
||||||
true ->
|
|
||||||
true
|
|
||||||
end,
|
|
||||||
true = lists:prefix(UPI_common_prefix, UPI_list1), % sanity
|
|
||||||
true = lists:prefix(UPI_common_prefix, UPI_list2), % sanity
|
|
||||||
UPI_2_suffix = UPI_list2 -- UPI_common_prefix,
|
|
||||||
|
|
||||||
%% Where did elements in UPI_2_suffix come from?
|
|
||||||
%% Only two sources are permitted.
|
|
||||||
[true = lists:member(X, Repairing_list1) % X added after repair done
|
|
||||||
orelse
|
|
||||||
lists:member(X, UPI_list1) % X in UPI_list1 after common pref
|
|
||||||
|| X <- UPI_2_suffix],
|
|
||||||
|
|
||||||
%% The UPI_2_suffix must exactly be equal to: ordered items from
|
|
||||||
%% UPI_list1 concat'ed with ordered items from Repairing_list1.
|
|
||||||
%% Both temp vars below preserve relative order!
|
|
||||||
UPI_2_suffix_from_UPI1 = [X || X <- UPI_2_suffix,
|
|
||||||
lists:member(X, UPI_list1)],
|
|
||||||
UPI_2_suffix_from_Repairing1 = [X || X <- UPI_2_suffix,
|
|
||||||
lists:member(X, Repairing_list1)],
|
|
||||||
%% true?
|
|
||||||
UPI_2_suffix = UPI_2_suffix_from_UPI1 ++ UPI_2_suffix_from_Repairing1,
|
|
||||||
|
|
||||||
true.
|
|
||||||
|
|
||||||
-endif. % EQC
|
|
||||||
|
|
||||||
%% [begin
|
|
||||||
%% %%io:format(user, "S0 ~p\n", [S0]),
|
|
||||||
%% {P1, _S1} = calc_projection(555, 0, S0),
|
|
||||||
%% io:format(user, "P1 ~p\n", [P1]),
|
|
||||||
%% ok % io:format(user, "S1 ~p\n", [S1]).
|
|
||||||
%% end || X <- lists:seq(1, 10)].
|
|
||||||
|
|
||||||
-endif. %TEST
|
|
||||||
|
|
|
@ -27,6 +27,8 @@
|
||||||
|
|
||||||
-ifdef(EQC).
|
-ifdef(EQC).
|
||||||
-include_lib("eqc/include/eqc.hrl").
|
-include_lib("eqc/include/eqc.hrl").
|
||||||
|
-define(QC_OUT(P),
|
||||||
|
eqc:on_output(fun(Str, Args) -> io:format(user, Str, Args) end, P)).
|
||||||
-endif.
|
-endif.
|
||||||
|
|
||||||
-include_lib("eunit/include/eunit.hrl").
|
-include_lib("eunit/include/eunit.hrl").
|
||||||
|
@ -104,116 +106,3 @@ split_overlapping(H1, H2) ->
|
||||||
|
|
||||||
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
|
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
|
||||||
|
|
||||||
-ifdef(TEST).
|
|
||||||
|
|
||||||
-ifdef(EQC).
|
|
||||||
|
|
||||||
repair_merge_test() ->
|
|
||||||
true = eqc:quickcheck(prop_repair_merge()).
|
|
||||||
|
|
||||||
prop_repair_merge() ->
|
|
||||||
?FORALL(S, gen_written_sequences(),
|
|
||||||
begin
|
|
||||||
Merged = repair_merge(S),
|
|
||||||
check_repair_merge(S, Merged)
|
|
||||||
end).
|
|
||||||
|
|
||||||
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
|
|
||||||
|
|
||||||
gen_written_sequences() ->
|
|
||||||
?LET(FLUs, non_empty(list(gen_flu())),
|
|
||||||
[gen_sequence_list(FLU) || FLU <- lists:usort(FLUs)]).
|
|
||||||
|
|
||||||
gen_sequence_list(FLU) ->
|
|
||||||
?LET(Files, non_empty(list(gen_file())),
|
|
||||||
?LET(Items, [gen_sequence_items(FLU, File) || File <- lists:usort(Files)],
|
|
||||||
lists:append(Items))).
|
|
||||||
|
|
||||||
gen_flu() ->
|
|
||||||
elements([a, b, c]).
|
|
||||||
|
|
||||||
gen_file() ->
|
|
||||||
elements(["f1", "f2", "f3"]).
|
|
||||||
|
|
||||||
gen_sequence_items(FLU, File) ->
|
|
||||||
%% Pairs = [{StartingOffset, # of bytes/pages/whatever} ...]
|
|
||||||
?LET(Pairs, non_empty(list({nat(), nat()})),
|
|
||||||
begin
|
|
||||||
%% Pairs2 = [{StartingOffset, EndingOffset} ...]
|
|
||||||
Pairs2 = [{Start, Start + (Num div 2)} || {Start, Num} <- Pairs],
|
|
||||||
%% Pairs3 = *sorted*, Pairs2 all overlapping offsets removed
|
|
||||||
{_, Pairs3} =
|
|
||||||
lists:foldl(
|
|
||||||
fun({NewStart, NewEnd}, {OldEnd, Acc}) ->
|
|
||||||
if NewStart =< OldEnd ->
|
|
||||||
{OldEnd, Acc};
|
|
||||||
true ->
|
|
||||||
{NewEnd, [{File, NewStart, NewEnd, [FLU]}|Acc]}
|
|
||||||
end
|
|
||||||
end, {-1, []}, lists:sort(Pairs2)),
|
|
||||||
%% Now combine any adjacent
|
|
||||||
combine_adjacent(lists:reverse(Pairs3))
|
|
||||||
end).
|
|
||||||
|
|
||||||
combine_adjacent([]=L) ->
|
|
||||||
L;
|
|
||||||
combine_adjacent([_]=L) ->
|
|
||||||
L;
|
|
||||||
combine_adjacent([{F1, P1a, P1z, M1s}, {F2, P2a, P2z, M2s}|T])
|
|
||||||
when F1 == F2, P1z == P2a - 1 ->
|
|
||||||
combine_adjacent([{F1, P1a, P2z, lists:usort(M1s ++ M2s)}|T]);
|
|
||||||
combine_adjacent([H|T]) ->
|
|
||||||
[H|combine_adjacent(T)].
|
|
||||||
|
|
||||||
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
|
|
||||||
|
|
||||||
check_repair_merge(S, Merged) ->
|
|
||||||
?WHENFAIL(begin
|
|
||||||
io:format(user, "Input = ~p\n", [S]),
|
|
||||||
io:format(user, "Merged = ~p\n", [Merged])
|
|
||||||
end,
|
|
||||||
conjunction([{piece_wise, compare_piece_wise_ok(S, Merged)},
|
|
||||||
{non_overlapping, check_strictly_non_overlapping(Merged)}])).
|
|
||||||
|
|
||||||
compare_piece_wise_ok(S, Merged) ->
|
|
||||||
PieceWise1 = lists:sort(make_canonical_form(S)),
|
|
||||||
|
|
||||||
PieceWise2 = make_canonical_form(Merged),
|
|
||||||
if PieceWise1 == PieceWise2 ->
|
|
||||||
true;
|
|
||||||
true ->
|
|
||||||
{correct, PieceWise1, wrong, PieceWise2}
|
|
||||||
end.
|
|
||||||
|
|
||||||
check_strictly_non_overlapping(S) ->
|
|
||||||
try
|
|
||||||
[First|Rest] = S,
|
|
||||||
lists:foldl(fun({F2, _F2a, _F2z, _}=New, {F1, _F1a, _F1z, _})
|
|
||||||
when F2 > F1 ->
|
|
||||||
New;
|
|
||||||
({_F2, F2a, F2z, _}=New, {_F1, F1a, F1z, _})
|
|
||||||
when F2a > F1a, F2a > F1z,
|
|
||||||
F2z > F1a, F2z > F1z ->
|
|
||||||
New
|
|
||||||
end, First, Rest),
|
|
||||||
true
|
|
||||||
catch _:_ ->
|
|
||||||
false
|
|
||||||
end.
|
|
||||||
|
|
||||||
%% Given a list of XXX, we create a list of 1-byte/page/thing
|
|
||||||
%% graunularity items which is equivalent but in a canonical form to
|
|
||||||
%% make correctness testing easier.
|
|
||||||
|
|
||||||
make_canonical_form(ListOfLists) ->
|
|
||||||
lists:sort(make_canonical_form2(lists:flatten(ListOfLists))).
|
|
||||||
|
|
||||||
make_canonical_form2([]) ->
|
|
||||||
[];
|
|
||||||
make_canonical_form2([{File, Start, End, Members}|T]) ->
|
|
||||||
[{File, Pos, Pos, Member} || Pos <- lists:seq(Start, End),
|
|
||||||
Member <- Members] ++
|
|
||||||
make_canonical_form2(T).
|
|
||||||
|
|
||||||
-endif. % EQC
|
|
||||||
-endif. % TEST
|
|
||||||
|
|
|
@ -21,6 +21,8 @@
|
||||||
%% -------------------------------------------------------------------
|
%% -------------------------------------------------------------------
|
||||||
-module(machi_chain_manager_test).
|
-module(machi_chain_manager_test).
|
||||||
|
|
||||||
|
-include("machi.hrl").
|
||||||
|
|
||||||
-export([]).
|
-export([]).
|
||||||
|
|
||||||
-ifdef(TEST).
|
-ifdef(TEST).
|
||||||
|
@ -28,27 +30,30 @@
|
||||||
-ifdef(EQC).
|
-ifdef(EQC).
|
||||||
-include_lib("eqc/include/eqc.hrl").
|
-include_lib("eqc/include/eqc.hrl").
|
||||||
-include_lib("eqc/include/eqc_statem.hrl").
|
-include_lib("eqc/include/eqc_statem.hrl").
|
||||||
|
-define(QC_OUT(P),
|
||||||
|
eqc:on_output(fun(Str, Args) -> io:format(user, Str, Args) end, P)).
|
||||||
-endif.
|
-endif.
|
||||||
|
|
||||||
-include_lib("eunit/include/eunit.hrl").
|
-include_lib("eunit/include/eunit.hrl").
|
||||||
-compile(export_all).
|
-compile(export_all).
|
||||||
-endif.
|
|
||||||
|
|
||||||
-record(s, {
|
-record(s, {
|
||||||
step = 0 :: non_neg_integer(),
|
step = 0 :: non_neg_integer(),
|
||||||
seed :: {integer(), integer(), integer()}
|
seed :: {integer(), integer(), integer()}
|
||||||
}).
|
}).
|
||||||
|
|
||||||
|
smoke0_test() ->
|
||||||
|
MGR = machi_chain_manager,
|
||||||
|
S0 = MGR:make_initial_state(a, [a,b,c,d], {4,5,6}),
|
||||||
|
lists:foldl(fun(_, S) ->
|
||||||
|
{P1, S1} = MGR:calc_projection(20, 1, S),
|
||||||
|
io:format(user, "~p\n", [MGR:make_projection_summary(P1)]),
|
||||||
|
S1#ch_mgr{proj=P1}
|
||||||
|
end, S0, lists:seq(1,10)).
|
||||||
|
|
||||||
gen_all_nodes() ->
|
gen_all_nodes() ->
|
||||||
[a, b, c].
|
[a, b, c].
|
||||||
|
|
||||||
gen_rand_seed() ->
|
|
||||||
noshrink({gen_num(), gen_num(), gen_num()}).
|
|
||||||
|
|
||||||
gen_num() ->
|
|
||||||
?LET(I, oneof([int(), largeint()]),
|
|
||||||
erlang:abs(I)).
|
|
||||||
|
|
||||||
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
|
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
|
||||||
|
|
||||||
initial_state() ->
|
initial_state() ->
|
||||||
|
@ -70,3 +75,139 @@ prop_m() ->
|
||||||
|
|
||||||
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
|
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
|
||||||
|
|
||||||
|
gen_rand_seed() ->
|
||||||
|
noshrink({gen_num(), gen_num(), gen_num()}).
|
||||||
|
|
||||||
|
gen_num() ->
|
||||||
|
?LET(I, oneof([int(), largeint()]),
|
||||||
|
erlang:abs(I)).
|
||||||
|
|
||||||
|
calc_projection_prop() ->
|
||||||
|
MGR = machi_chain_manager,
|
||||||
|
?FORALL(
|
||||||
|
{Seed, OldThreshold, NoPartitionThreshold},
|
||||||
|
{gen_rand_seed(), choose(0, 101), choose(0,101)},
|
||||||
|
begin
|
||||||
|
Steps = 200,
|
||||||
|
S0 = MGR:make_initial_state(a, [a,b,c,d], Seed),
|
||||||
|
F = fun(_, {S, Acc}) ->
|
||||||
|
{P1, S1} = MGR:calc_projection(
|
||||||
|
OldThreshold, NoPartitionThreshold, S),
|
||||||
|
%%io:format(user, "~p\n", [make_projection_summary(P1)]),
|
||||||
|
{S1#ch_mgr{proj=P1}, [P1|Acc]}
|
||||||
|
end,
|
||||||
|
{_, Projs0} = lists:foldl(F, {S0, []}, lists:seq(1,Steps)),
|
||||||
|
Projs = lists:reverse(Projs0),
|
||||||
|
true = projection_transitions_are_sane(Projs)
|
||||||
|
end).
|
||||||
|
|
||||||
|
calc_projection_test_() ->
|
||||||
|
{timeout, 60,
|
||||||
|
fun() ->
|
||||||
|
true = eqc:quickcheck(eqc:numtests(500,
|
||||||
|
?QC_OUT(calc_projection_prop())))
|
||||||
|
end}.
|
||||||
|
|
||||||
|
projection_transitions_are_sane([]) ->
|
||||||
|
true;
|
||||||
|
projection_transitions_are_sane([_]) ->
|
||||||
|
true;
|
||||||
|
projection_transitions_are_sane([P1, P2|T]) ->
|
||||||
|
case projection_transition_is_sane(P1, P2) of
|
||||||
|
true ->
|
||||||
|
projection_transitions_are_sane([P2|T]);
|
||||||
|
Else ->
|
||||||
|
Else
|
||||||
|
end.
|
||||||
|
|
||||||
|
projection_transition_is_sane(
|
||||||
|
#projection{epoch_number=Epoch1,
|
||||||
|
epoch_csum=CSum1,
|
||||||
|
prev_epoch_num=PrevEpoch1,
|
||||||
|
prev_epoch_csum=PrevCSum1,
|
||||||
|
creation_time=CreationTime1,
|
||||||
|
author_server=AuthorServer1,
|
||||||
|
all_members=All_list1,
|
||||||
|
down=Down_list1,
|
||||||
|
upi=UPI_list1,
|
||||||
|
repairing=Repairing_list1,
|
||||||
|
dbg=Dbg1} = _P1,
|
||||||
|
#projection{epoch_number=Epoch2,
|
||||||
|
epoch_csum=CSum2,
|
||||||
|
prev_epoch_num=PrevEpoch2,
|
||||||
|
prev_epoch_csum=PrevCSum2,
|
||||||
|
creation_time=CreationTime2,
|
||||||
|
author_server=AuthorServer2,
|
||||||
|
all_members=All_list2,
|
||||||
|
down=Down_list2,
|
||||||
|
upi=UPI_list2,
|
||||||
|
repairing=Repairing_list2,
|
||||||
|
dbg=Dbg2} = _P2) ->
|
||||||
|
true = is_integer(Epoch1) andalso is_integer(Epoch2),
|
||||||
|
true = is_binary(CSum1) andalso is_binary(CSum2),
|
||||||
|
true = is_integer(PrevEpoch1) andalso is_integer(PrevEpoch2),
|
||||||
|
true = is_binary(PrevCSum1) andalso is_binary(PrevCSum2),
|
||||||
|
{_,_,_} = CreationTime1,
|
||||||
|
{_,_,_} = CreationTime2,
|
||||||
|
true = is_atom(AuthorServer1) andalso is_atom(AuthorServer2), % todo will probably change
|
||||||
|
true = is_list(All_list1) andalso is_list(All_list2),
|
||||||
|
true = is_list(Down_list1) andalso is_list(Down_list2),
|
||||||
|
true = is_list(UPI_list1) andalso is_list(UPI_list2),
|
||||||
|
true = is_list(Repairing_list1) andalso is_list(Repairing_list2),
|
||||||
|
true = is_list(Dbg1) andalso is_list(Dbg2),
|
||||||
|
|
||||||
|
true = Epoch2 > Epoch1,
|
||||||
|
true = PrevEpoch2 > PrevEpoch1,
|
||||||
|
All_list1 = All_list2, % todo will probably change
|
||||||
|
|
||||||
|
%% No duplicates
|
||||||
|
true = lists:sort(Down_list2) == lists:usort(Down_list2),
|
||||||
|
true = lists:sort(UPI_list2) == lists:usort(UPI_list2),
|
||||||
|
true = lists:sort(Repairing_list2) == lists:usort(Repairing_list2),
|
||||||
|
|
||||||
|
%% Disjoint-ness
|
||||||
|
true = lists:sort(All_list2) == lists:sort(Down_list2 ++ UPI_list2 ++
|
||||||
|
Repairing_list2),
|
||||||
|
[] = [X || X <- Down_list2, not lists:member(X, All_list2)],
|
||||||
|
[] = [X || X <- UPI_list2, not lists:member(X, All_list2)],
|
||||||
|
[] = [X || X <- Repairing_list2, not lists:member(X, All_list2)],
|
||||||
|
DownS2 = sets:from_list(Down_list2),
|
||||||
|
UPIS2 = sets:from_list(UPI_list2),
|
||||||
|
RepairingS2 = sets:from_list(Repairing_list2),
|
||||||
|
true = sets:is_disjoint(DownS2, UPIS2),
|
||||||
|
true = sets:is_disjoint(DownS2, RepairingS2),
|
||||||
|
true = sets:is_disjoint(UPIS2, RepairingS2),
|
||||||
|
|
||||||
|
%% Additions to the UPI chain may only be at the tail
|
||||||
|
UPI_common_prefix =
|
||||||
|
lists:takewhile(fun(X) -> sets:is_element(X, UPIS2) end, UPI_list1),
|
||||||
|
%% If the common prefix is empty, then one of the inputs must be empty.
|
||||||
|
if UPI_common_prefix == [] ->
|
||||||
|
true = UPI_list1 == [] orelse UPI_list2 == [];
|
||||||
|
true ->
|
||||||
|
true
|
||||||
|
end,
|
||||||
|
true = lists:prefix(UPI_common_prefix, UPI_list1), % sanity
|
||||||
|
true = lists:prefix(UPI_common_prefix, UPI_list2), % sanity
|
||||||
|
UPI_2_suffix = UPI_list2 -- UPI_common_prefix,
|
||||||
|
|
||||||
|
%% Where did elements in UPI_2_suffix come from?
|
||||||
|
%% Only two sources are permitted.
|
||||||
|
[true = lists:member(X, Repairing_list1) % X added after repair done
|
||||||
|
orelse
|
||||||
|
lists:member(X, UPI_list1) % X in UPI_list1 after common pref
|
||||||
|
|| X <- UPI_2_suffix],
|
||||||
|
|
||||||
|
%% The UPI_2_suffix must exactly be equal to: ordered items from
|
||||||
|
%% UPI_list1 concat'ed with ordered items from Repairing_list1.
|
||||||
|
%% Both temp vars below preserve relative order!
|
||||||
|
UPI_2_suffix_from_UPI1 = [X || X <- UPI_2_suffix,
|
||||||
|
lists:member(X, UPI_list1)],
|
||||||
|
UPI_2_suffix_from_Repairing1 = [X || X <- UPI_2_suffix,
|
||||||
|
lists:member(X, Repairing_list1)],
|
||||||
|
%% true?
|
||||||
|
UPI_2_suffix = UPI_2_suffix_from_UPI1 ++ UPI_2_suffix_from_Repairing1,
|
||||||
|
|
||||||
|
true.
|
||||||
|
|
||||||
|
-endif.
|
||||||
|
|
150
prototype/poc-machi/test/machi_util_test.erl
Normal file
150
prototype/poc-machi/test/machi_util_test.erl
Normal file
|
@ -0,0 +1,150 @@
|
||||||
|
%% -------------------------------------------------------------------
|
||||||
|
%%
|
||||||
|
%% Machi: a small village of replicated files
|
||||||
|
%%
|
||||||
|
%% Copyright (c) 2014 Basho Technologies, Inc. All Rights Reserved.
|
||||||
|
%%
|
||||||
|
%% This file is provided to you under the Apache License,
|
||||||
|
%% Version 2.0 (the "License"); you may not use this file
|
||||||
|
%% except in compliance with the License. You may obtain
|
||||||
|
%% a copy of the License at
|
||||||
|
%%
|
||||||
|
%% http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
%%
|
||||||
|
%% Unless required by applicable law or agreed to in writing,
|
||||||
|
%% software distributed under the License is distributed on an
|
||||||
|
%% "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
|
||||||
|
%% KIND, either express or implied. See the License for the
|
||||||
|
%% specific language governing permissions and limitations
|
||||||
|
%% under the License.
|
||||||
|
%%
|
||||||
|
%% -------------------------------------------------------------------
|
||||||
|
-module(machi_util_test).
|
||||||
|
|
||||||
|
-include("machi.hrl").
|
||||||
|
|
||||||
|
-export([]).
|
||||||
|
|
||||||
|
-ifdef(TEST).
|
||||||
|
|
||||||
|
-ifdef(EQC).
|
||||||
|
-include_lib("eqc/include/eqc.hrl").
|
||||||
|
-define(QC_OUT(P),
|
||||||
|
eqc:on_output(fun(Str, Args) -> io:format(user, Str, Args) end, P)).
|
||||||
|
-endif.
|
||||||
|
|
||||||
|
-include_lib("eunit/include/eunit.hrl").
|
||||||
|
-compile(export_all).
|
||||||
|
|
||||||
|
repair_merge_test_() ->
|
||||||
|
{timeout, 60,
|
||||||
|
fun() ->
|
||||||
|
true = eqc:quickcheck(eqc:numtests(300, ?QC_OUT(prop_repair_merge())))
|
||||||
|
end}.
|
||||||
|
|
||||||
|
prop_repair_merge() ->
|
||||||
|
?FORALL(S, gen_written_sequences(),
|
||||||
|
begin
|
||||||
|
Merged = machi_util:repair_merge(S),
|
||||||
|
check_repair_merge(S, Merged)
|
||||||
|
end).
|
||||||
|
|
||||||
|
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
|
||||||
|
|
||||||
|
gen_written_sequences() ->
|
||||||
|
?LET(FLUs, non_empty(list(gen_flu())),
|
||||||
|
[gen_sequence_list(FLU) || FLU <- lists:usort(FLUs)]).
|
||||||
|
|
||||||
|
gen_sequence_list(FLU) ->
|
||||||
|
?LET(Files, non_empty(list(gen_file())),
|
||||||
|
?LET(Items, [gen_sequence_items(FLU, File) || File <- lists:usort(Files)],
|
||||||
|
lists:append(Items))).
|
||||||
|
|
||||||
|
gen_flu() ->
|
||||||
|
elements([a, b, c]).
|
||||||
|
|
||||||
|
gen_file() ->
|
||||||
|
elements(["f1", "f2", "f3"]).
|
||||||
|
|
||||||
|
gen_sequence_items(FLU, File) ->
|
||||||
|
%% Pairs = [{StartingOffset, # of bytes/pages/whatever} ...]
|
||||||
|
?LET(Pairs, non_empty(list({nat(), nat()})),
|
||||||
|
begin
|
||||||
|
%% Pairs2 = [{StartingOffset, EndingOffset} ...]
|
||||||
|
Pairs2 = [{Start, Start + (Num div 2)} || {Start, Num} <- Pairs],
|
||||||
|
%% Pairs3 = *sorted*, Pairs2 all overlapping offsets removed
|
||||||
|
{_, Pairs3} =
|
||||||
|
lists:foldl(
|
||||||
|
fun({NewStart, NewEnd}, {OldEnd, Acc}) ->
|
||||||
|
if NewStart =< OldEnd ->
|
||||||
|
{OldEnd, Acc};
|
||||||
|
true ->
|
||||||
|
{NewEnd, [{File, NewStart, NewEnd, [FLU]}|Acc]}
|
||||||
|
end
|
||||||
|
end, {-1, []}, lists:sort(Pairs2)),
|
||||||
|
%% Now combine any adjacent
|
||||||
|
combine_adjacent(lists:reverse(Pairs3))
|
||||||
|
end).
|
||||||
|
|
||||||
|
combine_adjacent([]=L) ->
|
||||||
|
L;
|
||||||
|
combine_adjacent([_]=L) ->
|
||||||
|
L;
|
||||||
|
combine_adjacent([{F1, P1a, P1z, M1s}, {F2, P2a, P2z, M2s}|T])
|
||||||
|
when F1 == F2, P1z == P2a - 1 ->
|
||||||
|
combine_adjacent([{F1, P1a, P2z, lists:usort(M1s ++ M2s)}|T]);
|
||||||
|
combine_adjacent([H|T]) ->
|
||||||
|
[H|combine_adjacent(T)].
|
||||||
|
|
||||||
|
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
|
||||||
|
|
||||||
|
check_repair_merge(S, Merged) ->
|
||||||
|
?WHENFAIL(begin
|
||||||
|
io:format(user, "Input = ~p\n", [S]),
|
||||||
|
io:format(user, "Merged = ~p\n", [Merged])
|
||||||
|
end,
|
||||||
|
conjunction([{piece_wise, compare_piece_wise_ok(S, Merged)},
|
||||||
|
{non_overlapping, check_strictly_non_overlapping(Merged)}])).
|
||||||
|
|
||||||
|
compare_piece_wise_ok(S, Merged) ->
|
||||||
|
PieceWise1 = lists:sort(make_canonical_form(S)),
|
||||||
|
|
||||||
|
PieceWise2 = make_canonical_form(Merged),
|
||||||
|
if PieceWise1 == PieceWise2 ->
|
||||||
|
true;
|
||||||
|
true ->
|
||||||
|
{correct, PieceWise1, wrong, PieceWise2}
|
||||||
|
end.
|
||||||
|
|
||||||
|
check_strictly_non_overlapping(S) ->
|
||||||
|
try
|
||||||
|
[First|Rest] = S,
|
||||||
|
lists:foldl(fun({F2, _F2a, _F2z, _}=New, {F1, _F1a, _F1z, _})
|
||||||
|
when F2 > F1 ->
|
||||||
|
New;
|
||||||
|
({_F2, F2a, F2z, _}=New, {_F1, F1a, F1z, _})
|
||||||
|
when F2a > F1a, F2a > F1z,
|
||||||
|
F2z > F1a, F2z > F1z ->
|
||||||
|
New
|
||||||
|
end, First, Rest),
|
||||||
|
true
|
||||||
|
catch _:_ ->
|
||||||
|
false
|
||||||
|
end.
|
||||||
|
|
||||||
|
%% Given a list of XXX, we create a list of 1-byte/page/thing
|
||||||
|
%% graunularity items which is equivalent but in a canonical form to
|
||||||
|
%% make correctness testing easier.
|
||||||
|
|
||||||
|
make_canonical_form(ListOfLists) ->
|
||||||
|
lists:sort(make_canonical_form2(lists:flatten(ListOfLists))).
|
||||||
|
|
||||||
|
make_canonical_form2([]) ->
|
||||||
|
[];
|
||||||
|
make_canonical_form2([{File, Start, End, Members}|T]) ->
|
||||||
|
[{File, Pos, Pos, Member} || Pos <- lists:seq(Start, End),
|
||||||
|
Member <- Members] ++
|
||||||
|
make_canonical_form2(T).
|
||||||
|
|
||||||
|
-endif. % TEST
|
||||||
|
|
Loading…
Reference in a new issue