Skip to content

Commit 6f0363e

Browse files
committed
Add property-based tests for find_index_position
Tests two properties covering offset and timestamp specs across randomly generated index data with strictly increasing offsets and timestamps. Query values cover before the first entry, within range, and beyond the last entry. - Offset spec: returns the last chunk whose offset is at or before the query, or the first chunk if the query is before all chunks - Timestamp spec: returns the first chunk whose timestamp is at or after the query, or the last chunk if the query is after all chunks Also exports `find_index_position/2` under `-ifdef(TEST)`.
1 parent 1670b09 commit 6f0363e

2 files changed

Lines changed: 91 additions & 4 deletions

File tree

src/rabbitmq_stream_s3_log_reader.erl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ tier.
8383
]).
8484

8585
-ifdef(TEST).
86-
-export([find_fragment/3]).
86+
-export([find_fragment/3, find_index_position/2]).
8787
-endif.
8888

8989
%%%===================================================================

test/unit_SUITE.erl

Lines changed: 90 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,9 @@ all() ->
1818
array_partition_point,
1919
array_binary_search_by,
2020
array_rfind,
21-
array_fold
21+
array_fold,
22+
find_index_position_offset,
23+
find_index_position_timestamp
2224
].
2325

2426
init_per_suite(Config) ->
@@ -265,8 +267,8 @@ array_rfind(_Config) ->
265267
).
266268

267269
%% rfind returns the rightmost index satisfying the predicate, or undefined.
268-
%% - undefined => no entry satisfies the predicate
269-
%% - {ok, Idx} => entry at Idx satisfies predicate, no entry at Idx+1..N-1 does
270+
%% - undefined => no entry satisfies the predicate
271+
%% - Idx => entry at Idx satisfies predicate, no entry at Idx+1..N-1 does
270272
prop_array_rfind() ->
271273
?FORALL(
272274
{Values, Threshold},
@@ -330,3 +332,88 @@ prop_array_fold() ->
330332

331333
ints_to_array(Ints) ->
332334
iolist_to_binary([<<V:64/unsigned>> || V <- Ints]).
335+
336+
%% -------------------------------------------------------------------------
337+
%% find_index_position properties
338+
%% -------------------------------------------------------------------------
339+
340+
find_index_position_offset(_Config) ->
341+
rabbit_ct_proper_helpers:run_proper(
342+
fun prop_find_index_position_offset/0, [], 500
343+
).
344+
345+
%% Offset spec: returns the last chunk whose offset =< O.
346+
%% If O is before all chunks, returns the first chunk.
347+
prop_find_index_position_offset() ->
348+
?FORALL(
349+
{Chunks, QueryOffset},
350+
gen_index_data(),
351+
begin
352+
IndexData = chunks_to_index(Chunks),
353+
{ChunkId, _Ts, _Pos} = rabbitmq_stream_s3_log_reader:find_index_position(
354+
IndexData, {offset, QueryOffset}
355+
),
356+
Offsets = [O || {O, _Ts} <- Chunks],
357+
%% Expected: last offset =< QueryOffset, or first offset if all are greater.
358+
Expected =
359+
case lists:takewhile(fun(O) -> O =< QueryOffset end, Offsets) of
360+
[] -> hd(Offsets);
361+
Matching -> lists:last(Matching)
362+
end,
363+
ChunkId =:= Expected
364+
end
365+
).
366+
367+
find_index_position_timestamp(_Config) ->
368+
rabbit_ct_proper_helpers:run_proper(
369+
fun prop_find_index_position_timestamp/0, [], 500
370+
).
371+
372+
%% Timestamp spec: returns the first chunk whose timestamp >= Ts.
373+
%% If Ts is after all chunks, returns the last chunk.
374+
prop_find_index_position_timestamp() ->
375+
?FORALL(
376+
{Chunks, QueryTs},
377+
gen_index_data(),
378+
begin
379+
IndexData = chunks_to_index(Chunks),
380+
{ChunkId, _Ts, _Pos} = rabbitmq_stream_s3_log_reader:find_index_position(
381+
IndexData, {timestamp, QueryTs}
382+
),
383+
%% Expected: first chunk whose timestamp >= QueryTs, or last chunk.
384+
Expected =
385+
case lists:dropwhile(fun({_O, Ts}) -> Ts < QueryTs end, Chunks) of
386+
[] -> element(1, lists:last(Chunks));
387+
[{O, _Ts} | _] -> O
388+
end,
389+
ChunkId =:= Expected
390+
end
391+
).
392+
393+
%% Generates a non-empty list of {Offset, Timestamp} pairs with strictly
394+
%% increasing offsets and timestamps, plus a random query value in a range
395+
%% that covers before the first entry, within the range, and beyond the last.
396+
gen_index_data() ->
397+
?LET(
398+
{N, Steps},
399+
{integer(1, 20), non_empty(list(integer(1, 100)))},
400+
begin
401+
StepsN = lists:sublist(Steps ++ lists:duplicate(N, 1), N),
402+
{Chunks, LastOffset, LastTs} = lists:foldl(
403+
fun(Step, {Acc, NextOffset, NextTs}) ->
404+
{[{NextOffset, NextTs} | Acc], NextOffset + Step, NextTs + Step}
405+
end,
406+
{[], 1, 1_000_000},
407+
StepsN
408+
),
409+
SortedChunks = lists:reverse(Chunks),
410+
%% Query range covers before first entry (0), within range, and beyond last.
411+
?LET(Query, integer(0, max(LastOffset, LastTs) + 200), {SortedChunks, Query})
412+
end
413+
).
414+
415+
chunks_to_index(Chunks) ->
416+
iolist_to_binary([
417+
?INDEX_RECORD(O, Ts, Pos)
418+
|| {Pos, {O, Ts}} <- lists:zip(lists:seq(0, length(Chunks) - 1), Chunks)
419+
]).

0 commit comments

Comments
 (0)