@@ -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
2426init_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
270272prop_array_rfind () ->
271273 ? FORALL (
272274 {Values , Threshold },
@@ -330,3 +332,88 @@ prop_array_fold() ->
330332
331333ints_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