234 lines
7.9 KiB
Erlang
234 lines
7.9 KiB
Erlang
%%--------------------------------------------------------------------
|
|
%% Copyright (c) 2020-2024 EMQ Technologies Co., Ltd. All Rights Reserved.
|
|
%%
|
|
%% Licensed 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(prop_emqx_ft_assembly).
|
|
|
|
-include_lib("proper/include/proper.hrl").
|
|
|
|
-import(emqx_proper_types, [scaled/2, fixedmap/1, typegen/0, generate/2]).
|
|
|
|
-define(COVERAGE_TIMEOUT, 10000).
|
|
|
|
prop_coverage() ->
|
|
?FORALL(
|
|
#{filesize := Filesize, segsizes := Segsizes, typegen := TypeGen},
|
|
noshrink(
|
|
fixedmap(#{
|
|
filesize => filesize_t(),
|
|
segsizes => segsizes_t(),
|
|
typegen => typegen()
|
|
})
|
|
),
|
|
?TIMEOUT(
|
|
?COVERAGE_TIMEOUT,
|
|
begin
|
|
Segments = generate(segments_t(Filesize, Segsizes), TypeGen),
|
|
ASM1 = append_segments(mk_assembly(Filesize), Segments),
|
|
{Time, ASM2} = timer:tc(emqx_ft_assembly, update, [ASM1]),
|
|
measure(
|
|
#{"Segments" => length(Segments), "Time" => Time},
|
|
case emqx_ft_assembly:status(ASM2) of
|
|
complete ->
|
|
Coverage = emqx_ft_assembly:coverage(ASM2),
|
|
measure(
|
|
#{"CoverageLength" => length(Coverage)},
|
|
is_coverage_complete(Coverage)
|
|
);
|
|
{incomplete, {missing, {segment, _, _}}} ->
|
|
measure("CoverageLength", 0, true)
|
|
end
|
|
)
|
|
end
|
|
)
|
|
).
|
|
|
|
prop_coverage_likely_incomplete() ->
|
|
?FORALL(
|
|
#{filesize := Filesize, segsizes := Segsizes, hole := HoleIn, typegen := TypeGen},
|
|
noshrink(
|
|
fixedmap(#{
|
|
filesize => filesize_t(),
|
|
segsizes => segsizes_t(),
|
|
hole => filesize_t(),
|
|
typegen => typegen()
|
|
})
|
|
),
|
|
?TIMEOUT(?COVERAGE_TIMEOUT, begin
|
|
Hole = HoleIn rem max(Filesize, 1),
|
|
Segments = generate(segments_t(Filesize, Segsizes, Hole), TypeGen),
|
|
ASM1 = append_segments(mk_assembly(Filesize), Segments),
|
|
{Time, ASM2} = timer:tc(emqx_ft_assembly, update, [ASM1]),
|
|
measure(
|
|
#{"Segments" => length(Segments), "Time" => Time},
|
|
case emqx_ft_assembly:status(ASM2) of
|
|
complete ->
|
|
% NOTE: this is still possible due to the nature of `SUCHTHATMAYBE`
|
|
IsComplete = emqx_ft_assembly:coverage(ASM2),
|
|
collect(complete, is_coverage_complete(IsComplete));
|
|
{incomplete, {missing, {segment, _, _}}} ->
|
|
collect(incomplete, true)
|
|
end
|
|
)
|
|
end)
|
|
).
|
|
|
|
prop_coverage_complete() ->
|
|
?FORALL(
|
|
#{filesize := Filesize, segsizes := Segsizes, node := RemoteNode, typegen := TypeGen},
|
|
noshrink(
|
|
fixedmap(#{
|
|
filesize => filesize_t(),
|
|
segsizes => ?SUCHTHAT([BaseSegsize | _], segsizes_t(), BaseSegsize > 0),
|
|
node => remote_node_t(),
|
|
typegen => typegen()
|
|
})
|
|
),
|
|
?TIMEOUT(
|
|
?COVERAGE_TIMEOUT,
|
|
begin
|
|
% Ensure that we have complete coverage
|
|
Segments = generate(segments_t(Filesize, Segsizes), TypeGen),
|
|
ASM1 = mk_assembly(Filesize),
|
|
ASM2 = append_coverage(ASM1, RemoteNode, Filesize, Segsizes),
|
|
ASM3 = append_segments(ASM2, Segments),
|
|
{Time, ASM4} = timer:tc(emqx_ft_assembly, update, [ASM3]),
|
|
measure(
|
|
#{"CoverageMax" => nsegs(Filesize, Segsizes), "Time" => Time},
|
|
case emqx_ft_assembly:status(ASM4) of
|
|
complete ->
|
|
Coverage = emqx_ft_assembly:coverage(ASM4),
|
|
measure(
|
|
#{"Coverage" => length(Coverage)},
|
|
is_coverage_complete(Coverage)
|
|
);
|
|
{incomplete, _} ->
|
|
false
|
|
end
|
|
)
|
|
end
|
|
)
|
|
).
|
|
|
|
measure(NamedSamples, Test) ->
|
|
maps:fold(fun(Name, Sample, Acc) -> measure(Name, Sample, Acc) end, Test, NamedSamples).
|
|
|
|
is_coverage_complete([]) ->
|
|
true;
|
|
is_coverage_complete(Coverage = [_ | Tail]) ->
|
|
is_coverage_complete(Coverage, Tail).
|
|
|
|
is_coverage_complete([_], []) ->
|
|
true;
|
|
is_coverage_complete(
|
|
[{_Node1, #{fragment := {segment, #{offset := O1, size := S1}}}} | Rest],
|
|
[{_Node2, #{fragment := {segment, #{offset := O2}}}} | Tail]
|
|
) ->
|
|
(O1 + S1 == O2) andalso is_coverage_complete(Rest, Tail).
|
|
|
|
mk_assembly(Filesize) ->
|
|
emqx_ft_assembly:append(emqx_ft_assembly:new(Filesize), node(), mk_filemeta(Filesize)).
|
|
|
|
append_segments(ASMIn, Fragments) ->
|
|
lists:foldl(
|
|
fun({Node, {Offset, Size}}, ASM) ->
|
|
emqx_ft_assembly:append(ASM, Node, mk_segment(Offset, Size))
|
|
end,
|
|
ASMIn,
|
|
Fragments
|
|
).
|
|
|
|
append_coverage(ASM, Node, Filesize, Segsizes = [BaseSegsize | _]) ->
|
|
append_coverage(ASM, Node, Filesize, BaseSegsize, 0, nsegs(Filesize, Segsizes)).
|
|
|
|
append_coverage(ASM, Node, Filesize, Segsize, I, NSegs) when I < NSegs ->
|
|
Offset = I * Segsize,
|
|
Size = min(Segsize, Filesize - Offset),
|
|
ASMNext = emqx_ft_assembly:append(ASM, Node, mk_segment(Offset, Size)),
|
|
append_coverage(ASMNext, Node, Filesize, Segsize, I + 1, NSegs);
|
|
append_coverage(ASM, _Node, _Filesize, _Segsize, _, _NSegs) ->
|
|
ASM.
|
|
|
|
mk_filemeta(Filesize) ->
|
|
#{
|
|
path => "MANIFEST.json",
|
|
fragment => {filemeta, #{name => ?MODULE_STRING, size => Filesize}}
|
|
}.
|
|
|
|
mk_segment(Offset, Size) ->
|
|
#{
|
|
path => "SEG" ++ integer_to_list(Offset) ++ integer_to_list(Size),
|
|
fragment => {segment, #{offset => Offset, size => Size}}
|
|
}.
|
|
|
|
nsegs(Filesize, [BaseSegsize | _]) ->
|
|
Filesize div max(1, BaseSegsize) + 1.
|
|
|
|
segments_t(Filesize, Segsizes) ->
|
|
scaled(nsegs(Filesize, Segsizes), list({node_t(), segment_t(Filesize, Segsizes)})).
|
|
|
|
segments_t(Filesize, Segsizes, Hole) ->
|
|
scaled(nsegs(Filesize, Segsizes), list({node_t(), segment_t(Filesize, Segsizes, Hole)})).
|
|
|
|
segment_t(Filesize, Segsizes, Hole) ->
|
|
?SUCHTHATMAYBE(
|
|
{Offset, Size},
|
|
segment_t(Filesize, Segsizes),
|
|
Hole =< Offset orelse Hole > (Offset + Size)
|
|
).
|
|
|
|
segment_t(Filesize, Segsizes) ->
|
|
?LET(
|
|
Segsize,
|
|
oneof(Segsizes),
|
|
?LET(
|
|
Index,
|
|
range(0, Filesize div max(1, Segsize)),
|
|
{Index * Segsize, min(Segsize, Filesize - (Index * Segsize))}
|
|
)
|
|
).
|
|
|
|
filesize_t() ->
|
|
scaled(2000, non_neg_integer()).
|
|
|
|
segsizes_t() ->
|
|
?LET(
|
|
BaseSize,
|
|
segsize_t(),
|
|
oneof([
|
|
[BaseSize, BaseSize * 2],
|
|
[BaseSize, BaseSize * 2, BaseSize * 3],
|
|
[BaseSize, BaseSize * 2, BaseSize * 5]
|
|
])
|
|
).
|
|
|
|
segsize_t() ->
|
|
scaled(50, non_neg_integer()).
|
|
|
|
remote_node_t() ->
|
|
oneof([
|
|
'emqx42@emqx.local',
|
|
'emqx43@emqx.local',
|
|
'emqx44@emqx.local'
|
|
]).
|
|
|
|
node_t() ->
|
|
oneof([
|
|
node(),
|
|
'emqx42@emqx.local',
|
|
'emqx43@emqx.local',
|
|
'emqx44@emqx.local'
|
|
]).
|