/*****************************************************************************
* randomizer.c
*****************************************************************************
* Copyright (C) 2018 VLC authors and VideoLAN
*
* This program is free software; you can redistribute it and/or modify it
* under the terms of the GNU Lesser General Public License as published by
* the Free Software Foundation; either version 2.1 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public License
* along with this program; if not, write to the Free Software Foundation,
* Inc., 51 Franklin Street, Fifth Floor, Boston MA 02110-1301, USA.
*****************************************************************************/
#ifdef HAVE_CONFIG_H
# include "config.h"
#endif
#ifdef TEST_RANDOMIZER
# undef NDEBUG
#endif
#include <assert.h>
#include <vlc_common.h>
#include <vlc_rand.h>
#include "randomizer.h"
/**
* \addtogroup playlist_randomizer Playlist randomizer helper
* \ingroup playlist
*
* Playlist helper to manage random playback.
*
* The purpose is to guarantee the following rules:
* - an item must never be selected twice
* - "prev" navigates back in the history of previously selected items
* - insertions and removals can occur at any time; all these rules must still
* apply
* - the user can request to select a specific item (typically by
* double-clicking on an item in the playlist)
*
* If loop (repeat) is enabled:
* - the random order must be "reshuffled" on every cycle
* - an item must never be selected twice in each cycle
* - the same item must never be selected twice in a row; in particular, it
* must not be the first item of a new cycle if it was the last item of the
* previous one (except if the playlist contains only one item)
* - crossing a new "cycle" is invisible to the user (e.g. it is still
* possible to navigate to previous selected items)
*
* To achieve these goals, a "randomizer" stores a single vector containing all
* the items of the playlist, along with 3 indexes.
*
* The whole vector is not shuffled at once: instead, steps of the Fisher-Yates
* algorithm are executed one-by-one on demand. This has several advantages:
* - on insertions and removals, there is no need to reshuffle or shift the
* whole array;
* - if loop is enabled, the history of the last cycle can be kept in place.
*
* 'head' indicates the end of the items already determined for the current
* cycle (if loop is disabled, there is only one cycle).
* (0 <= head <= size)
*
* 'next' points to the item after the current one (we use 'next' instead of
* 'current' so that all indexes are unsigned, while 'current' could be -1).
* The current item is the one returned by the previous call to _Prev() or
* _Next(). Each call to _Next() makes 'next' (and possibly 'head') move
* forward, each call to _Prev() makes it move back (modulo size).
* 'next' is always in the determined range (0 <= next <= head) or in the
* "history" range (history < next < size).
*
* 'history' is only used in loop mode, and references the first item of the
* ordered history from the last cycle.
*
* 0 next head history size
* |---------------|-----|.............|-------------|
* <-------------------> <----------->
* determinated range history range
*
* Here is a sample scenario to understand how it works.
*
* The playlist initially adds 5 items (A, B, C, D and E).
*
* history
* next |
* head |
* | |
* A B C D E
*
* The playlist calls _Next() to retrieve the next random item. The randomizer
* picks one item (say, D), and swaps it with the current head (A). _Next()
* returns D.
*
* history
* next |
* head |
* | |
* D B C A E
* <--->
* determined range
*
* The playlist calls _Next() one more time. The randomizer selects one item
* outside the determined range (say, E). _Next() returns E.
*
* history
* next |
* head |
* | |
* D E C A B
* <-------->
* determined range
*
* The playlist calls _Next() one more time. The randomizer selects C (already
* in place). _Next() returns C.
*
* history
* next |
* head |
* | |
* D E C A B
* <------------->
* determined range
*
* The playlist then calls _Prev(). Since the "current" item is C, the previous
* one is E, so _Prev() returns E, and 'next' moves back.
*
* history
* next |
* | head |
* | | |
* D E C A B
* <------------->
* determined range
*
* The playlist calls _Next(), which returns C, as expected.
*
* history
* next |
* head |
* | |
* D E C A B
* <------------->
* determined range
*
* The playlist calls _Next(), the randomizer selects B, and returns it.
*
* history
* next |
* head |
* | |
* D E C B A
* <------------------>
* determined range
*
* The playlist calls _Next(), the randomizer selects the last item (it has no
* choice). 'next' and 'head' now point one item past the end (their value is
* the vector size).
*
* history
* next
* head
* |
* D E C B A
* <----------------------->
* determined range
*
* At this point, if loop is disabled, it is not possible to call _Next()
* anymore (_HasNext() returns false). So let's enable it by calling
* _SetLoop(), then let's call _Next() again.
*
* This will start a new loop cycle. Firstly, 'next' and 'head' are reset, and
* the whole vector belongs to the last cycle history.
*
* history
* next
* head
* |
* D E C B A
* <------------------------>
* history range
*
* Secondly, to avoid selecting A twice in a row (as the last item of the
* previous cycle and the first item of the new one), the randomizer will
* immediately determine another item in the vector (say C) to be the first of
* the new cycle. The items that belong to the history are kept in order.
* 'head' and 'history' move forward.
*
* history
* next |
* | head
* | |
* C D E B A
* <---><------------------>
* determined history range
* range
*
* Finally, it will actually select and return the first item (C).
*
* history
* next
* head
* |
* C D E B A
* <---><------------------>
* determined history range
* range
*
* Then, the user adds an item to the playlist (F). This item is added in front
* of history.
*
* history
* next |
* head |
* | |
* C F D E B A
* <---> <------------------>
* determined history range
* range
*
* The playlist calls _Next(), the randomizer randomly selects E. E
* "disappears" from the history of the last cycle. This is a general property:
* each item may not appear more than once in the "history" (both from the last
* and the new cycle). The history order is preserved.
*
* history
* next |
* head |
* | |
* C E F D B A
* <--------> <-------------->
* determined history range
* range
*
* The playlist then calls _Prev() 3 times, that yields C, then A, then B.
* 'next' is decremented (modulo size) on each call.
*
* history
* | next
* head | |
* | | |
* C E F D B A
* <--------> <-------------->
* determined history range
* range
*/
/* On auto-reshuffle, avoid selecting the same item before at least
* NOT_SAME_BEFORE other items have been selected (between the end of the
* previous shuffle and the start of the new shuffle). */
#define NOT_SAME_BEFORE 1
void
randomizer_Init(struct randomizer *r)
{
vlc_vector_init(&r->items);
/* initialize separately instead of using vlc_lrand48() to avoid locking
* the mutex for every random number generation */
vlc_rand_bytes(r->xsubi, sizeof(r->xsubi));
r->loop = false;
r->head = 0;
r->next = 0;
r->history = 0;
}
void
randomizer_Destroy(struct randomizer *r)
{
vlc_vector_destroy(&r->items);
}
void
randomizer_SetLoop(struct randomizer *r, bool loop)
{
r->loop = loop;
}
static inline ssize_t
randomizer_IndexOf(struct randomizer *r, const vlc_playlist_item_t *item)
{
ssize_t index;
vlc_vector_index_of(&r->items, item, &index);
return index;
}
bool
randomizer_Count(struct randomizer *r)
{
return r->items.size;
}
void
randomizer_Reshuffle(struct randomizer *r)
{
/* yeah, it's that simple */
r->head = 0;
r->next = 0;
r->history = r->items.size;
}
static inline void
swap_items(struct randomizer *r, int i, int j)
{
vlc_playlist_item_t *item = r->items.data[i];
r->items.data[i] = r->items.data[j];
r->items.data[j] = item;
}
static inline void
randomizer_DetermineOne_(struct randomizer *r, size_t avoid_last_n)
{
assert(r->head < r->items.size);
assert(r->items.size - r->head > avoid_last_n);
size_t range_len = r->items.size - r->head - avoid_last_n;
size_t selected = r->head + (nrand48(r->xsubi) % range_len);
swap_items(r, r->head, selected);
if (r->head == r->history)
r->history++;
r->head++;
}
static inline void
randomizer_DetermineOne(struct randomizer *r)
{
randomizer_DetermineOne_(r, 0);
}
/* An autoreshuffle occurs if loop is enabled, once all item have been played.
* In that case, we reshuffle and determine first items so that the same item
* may not be selected before NOT_SAME_BEFORE selections. */
static void
randomizer_AutoReshuffle(struct randomizer *r)
{
assert(r->items.size > 0);
r->head = 0;
r->next = 0;
r->history = 0; /* the whole content is history */
size_t avoid_last_n = NOT_SAME_BEFORE;
if (avoid_last_n > r->items.size - 1)
/* cannot ignore all */
avoid_last_n = r->items.size - 1;
while (avoid_last_n)
randomizer_DetermineOne_(r, avoid_last_n--);
}
bool
randomizer_HasPrev(struct randomizer *r)
{
if (!r->loop)
/* a previous exists if the current is > 0, i.e. next > 1 */
return r->next > 1;
if (!r->items.size)
/* avoid modulo 0 */
return false;
/* there is no previous only if (current - history) == 0 (modulo size),
* i.e. (next - history) == 1 (modulo size) */
return (r->next + r->items.size - r->history) % r->items.size != 1;
}
bool
randomizer_HasNext(struct randomizer *r)
{
return r->loop || r->next < r->items.size;
}
vlc_playlist_item_t *
randomizer_PeekPrev(struct randomizer *r)
{
assert(randomizer_HasPrev(r));
size_t index = (r->next + r->items.size - 2) % r->items.size;
return r->items.data[index];
}
vlc_playlist_item_t *
randomizer_PeekNext(struct randomizer *r)
{
assert(randomizer_HasNext(r));
if (r->next == r->items.size && r->next == r->history)
{
assert(r->loop);
randomizer_AutoReshuffle(r);
}
if (r->next == r->head)
/* execute 1 step of the Fisher-Yates shuffle */
randomizer_DetermineOne(r);
return r->items.data[r->next];
}
vlc_playlist_item_t *
randomizer_Prev(struct randomizer *r)
{
assert(randomizer_HasPrev(r));
vlc_playlist_item_t *item = randomizer_PeekPrev(r);
r->next = r->next ? r->next - 1 : r->items.size - 1;
return item;
}
vlc_playlist_item_t *
randomizer_Next(struct randomizer *r)
{
assert(randomizer_HasNext(r));
vlc_playlist_item_t *item = randomizer_PeekNext(r);
r->next++;
if (r->next == r->items.size && r->next != r->head)
r->next = 0;
return item;
}
bool
randomizer_Add(struct randomizer *r, vlc_playlist_item_t *items[], size_t count)
{
if (!vlc_vector_insert_all(&r->items, r->history, items, count))
return false;
/* the insertion shifted history (and possibly next) */
if (r->next > r->history)
r->next += count;
r->history += count;
return true;
}
static void
randomizer_SelectIndex(struct randomizer *r, size_t index)
{
vlc_playlist_item_t *selected = r->items.data[index];
if (r->history && index >= r->history)
{
if (index > r->history)
{
memmove(&r->items.data[r->history + 1],
&r->items.data[r->history],
(index - r->history) * sizeof(selected));
index = r->history;
}
r->history = (r->history + 1) % r->items.size;
}
if (index >= r->head)
{
r->items.data[index] = r->items.data[r->head];
r->items.data[r->head] = selected;
r->head++;
}
else if (index < r->items.size - 1)
{
memmove(&r->items.data[index],
&r->items.data[index + 1],
(r->head - index - 1) * sizeof(selected));
r->items.data[r->head - 1] = selected;
}
r->next = r->head;
}
void
randomizer_Select(struct randomizer *r, const vlc_playlist_item_t *item)
{
ssize_t index = randomizer_IndexOf(r, item);
assert(index != -1); /* item must exist */
randomizer_SelectIndex(r, (size_t) index);
}
static void
randomizer_RemoveAt(struct randomizer *r, size_t index)
{
/*
* 0 head history next size
* |-----------|...................................|---------|-----|
* |<--------->| |<------------->|
* ordered order irrelevant ordered
*/
/* update next before index may be updated */
if (index < r->next)
r->next--;
if (index < r->head)
{
/* item was selected, keep the selected part ordered */
memmove(&r->items.data[index],
&r->items.data[index + 1],
(r->head - index - 1) * sizeof(*r->items.data));
r->head--;
index = r->head; /* the new index to remove */
}
if (index < r->history)
{
/* this part is unordered, no need to shift all items */
r->items.data[index] = r->items.data[r->history - 1];
index = r->history - 1;
r->history--;
}
if (index < r->items.size - 1)
{
/* shift the ordered history part by one */
memmove(&r->items.data[index],
&r->items.data[index + 1],
(r->items.size - index - 1) * sizeof(*r->items.data));
}
r->items.size--;
}
static void
randomizer_RemoveOne(struct randomizer *r, const vlc_playlist_item_t *item)
{
ssize_t index = randomizer_IndexOf(r, item);
assert(index >= 0); /* item must exist */
randomizer_RemoveAt(r, index);
}
void
randomizer_Remove(struct randomizer *r, vlc_playlist_item_t *const items[],
size_t count)
{
for (size_t i = 0; i < count; ++i)
randomizer_RemoveOne(r, items[i]);
vlc_vector_autoshrink(&r->items);
}
void
randomizer_Clear(struct randomizer *r)
{
vlc_vector_clear(&r->items);
r->head = 0;
r->next = 0;
r->history = 0;
}
#ifndef DOC
#ifdef TEST_RANDOMIZER
/* fake structure to simplify tests */
struct vlc_playlist_item {
size_t index;
};
static void
ArrayInit(vlc_playlist_item_t *array[], size_t len)
{
for (size_t i = 0; i < len; ++i)
{
array[i] = malloc(sizeof(*array[i]));
assert(array[i]);
array[i]->index = i;
}
}
static void
ArrayDestroy(vlc_playlist_item_t *array[], size_t len)
{
for (size_t i = 0; i < len; ++i)
free(array[i]);
}
static void
test_all_items_selected_exactly_once(void)
{
struct randomizer randomizer;
randomizer_Init(&randomizer);
#define SIZE 100
vlc_playlist_item_t *items[SIZE];
ArrayInit(items, SIZE);
bool ok = randomizer_Add(&randomizer, items, SIZE);
assert(ok);
bool selected[SIZE] = {0};
for (int i = 0; i < SIZE; ++i)
{
assert(randomizer_HasNext(&randomizer));
vlc_playlist_item_t *item = randomizer_Next(&randomizer);
assert(item);
assert(!selected[item->index]); /* never selected twice */
selected[item->index] = true;
}
assert(!randomizer_HasNext(&randomizer)); /* no more items */
for (int i = 0; i < SIZE; ++i)
assert(selected[i]); /* all selected */
ArrayDestroy(items, SIZE);
randomizer_Destroy(&randomizer);
#undef SIZE
}
static void
test_all_items_selected_exactly_once_per_cycle(void)
{
struct randomizer randomizer;
randomizer_Init(&randomizer);
randomizer_SetLoop(&randomizer, true);
#define SIZE 100
vlc_playlist_item_t *items[SIZE];
ArrayInit(items, SIZE);
bool ok = randomizer_Add(&randomizer, items, SIZE);
assert(ok);
for (int cycle = 0; cycle < 4; ++cycle)
{
bool selected[SIZE] = {0};
for (int i = 0; i < SIZE; ++i)
{
assert(randomizer_HasNext(&randomizer));
vlc_playlist_item_t *item = randomizer_Next(&randomizer);
assert(item);
assert(!selected[item->index]); /* never selected twice */
selected[item->index] = true;
}
assert(randomizer_HasNext(&randomizer)); /* still has items in loop */
for (int i = 0; i < SIZE; ++i)
assert(selected[i]); /* all selected */
}
ArrayDestroy(items, SIZE);
randomizer_Destroy(&randomizer);
#undef SIZE
}
static void
test_all_items_selected_exactly_once_with_additions(void)
{
struct randomizer randomizer;
randomizer_Init(&randomizer);
#define SIZE 100
vlc_playlist_item_t *items[SIZE];
ArrayInit(items, SIZE);
bool ok = randomizer_Add(&randomizer, items, 75);
assert(ok);
bool selected[SIZE] = {0};
for (int i = 0; i < 50; ++i)
{
assert(randomizer_HasNext(&randomizer));
vlc_playlist_item_t *item = randomizer_Next(&randomizer);
assert(item);
assert(!selected[item->index]); /* never selected twice */
selected[item->index] = true;
}
ok = randomizer_Add(&randomizer, &items[75], 25);
assert(ok);
for (int i = 50; i < SIZE; ++i)
{
assert(randomizer_HasNext(&randomizer));
vlc_playlist_item_t *item = randomizer_Next(&randomizer);
assert(item);
assert(!selected[item->index]); /* never selected twice */
selected[item->index] = true;
}
assert(!randomizer_HasNext(&randomizer)); /* no more items */
for (int i = 0; i < SIZE; ++i)
assert(selected[i]); /* all selected */
ArrayDestroy(items, SIZE);
randomizer_Destroy(&randomizer);
#undef SIZE
}
static void
test_all_items_selected_exactly_once_with_removals(void)
{
struct randomizer randomizer;
randomizer_Init(&randomizer);
#define SIZE 100
vlc_playlist_item_t *items[SIZE];
ArrayInit(items, SIZE);
bool ok = randomizer_Add(&randomizer, items, SIZE);
assert(ok);
bool selected[SIZE] = {0};
for (int i = 0; i < 50; ++i)
{
assert(randomizer_HasNext(&randomizer));
vlc_playlist_item_t *item = randomizer_Next(&randomizer);
assert(item);
assert(!selected[item->index]); /* never selected twice */
selected[item->index] = true;
}
vlc_playlist_item_t *to_remove[20];
/* copy 10 items already selected */
memcpy(to_remove, &randomizer.items.data[20], 10 * sizeof(*to_remove));
/* copy 10 items not already selected */
memcpy(&to_remove[10], &randomizer.items.data[70], 10 * sizeof(*to_remove));
randomizer_Remove(&randomizer, to_remove, 20);
for (int i = 50; i < SIZE - 10; ++i)
{
assert(randomizer_HasNext(&randomizer));
vlc_playlist_item_t *item = randomizer_Next(&randomizer);
assert(item);
assert(!selected[item->index]); /* never selected twice */
selected[item->index] = true;
}
assert(!randomizer_HasNext(&randomizer)); /* no more items */
int count = 0;
for (int i = 0; i < SIZE; ++i)
if (selected[i])
count++;
assert(count == SIZE - 10);
ArrayDestroy(items, SIZE);
randomizer_Destroy(&randomizer);
#undef SIZE
}
static void
test_cycle_after_manual_selection(void)
{
struct randomizer randomizer;
randomizer_Init(&randomizer);
randomizer_SetLoop(&randomizer, true);
#define SIZE 100
vlc_playlist_item_t *items[SIZE];
ArrayInit(items, SIZE);
bool ok = randomizer_Add(&randomizer, items, 100);
assert(ok);
/* force selection of the first item */
randomizer_Select(&randomizer, randomizer.items.data[0]);
for (int i = 0; i < 2 * SIZE; ++i)
{
assert(randomizer_HasNext(&randomizer));
vlc_playlist_item_t *item = randomizer_Next(&randomizer);
assert(item);
}
assert(randomizer_HasNext(&randomizer)); /* still has items in loop */
ArrayDestroy(items, SIZE);
randomizer_Destroy(&randomizer);
#undef SIZE
}
static void
test_cycle_with_additions_and_removals(void)
{
struct randomizer randomizer;
randomizer_Init(&randomizer);
randomizer_SetLoop(&randomizer, true);
#define SIZE 100
vlc_playlist_item_t *items[SIZE];
ArrayInit(items, SIZE);
bool ok = randomizer_Add(&randomizer, items, 80);
assert(ok);
for (int i = 0; i < 30; ++i)
{
assert(randomizer_HasNext(&randomizer));
vlc_playlist_item_t *item = randomizer_Next(&randomizer);
assert(item);
}
vlc_playlist_item_t *to_remove[20];
/* copy 10 items already selected */
memcpy(to_remove, &randomizer.items.data[15], 10 * sizeof(*to_remove));
/* copy 10 items not already selected */
memcpy(&to_remove[10], &randomizer.items.data[60], 10 * sizeof(*to_remove));
randomizer_Remove(&randomizer, to_remove, 20);
/* it remains 40 items in the first cycle (30 already selected, and 10
* removed from the 50 remaining) */
for (int i = 0; i < 40; ++i)
{
assert(randomizer_HasNext(&randomizer));
vlc_playlist_item_t *item = randomizer_Next(&randomizer);
assert(item);
}
/* the first cycle is complete */
assert(randomizer_HasNext(&randomizer));
/* force the determination of the first item of the next cycle */
vlc_playlist_item_t *item = randomizer_PeekNext(&randomizer);
assert(item);
assert(randomizer.items.size == 60);
assert(randomizer.history == 1);
/* save current history */
vlc_playlist_item_t *history[59];
memcpy(history, &randomizer.items.data[1], 59 * sizeof(*history));
/* insert 20 new items */
ok = randomizer_Add(&randomizer, &items[80], 20);
assert(ok);
assert(randomizer.items.size == 80);
assert(randomizer.history == 21);
for (int i = 0; i < 59; ++i)
assert(history[i] == randomizer.items.data[21 + i]);
/* remove 10 items in the history part */
memcpy(to_remove, &randomizer.items.data[30], 10 * sizeof(*to_remove));
randomizer_Remove(&randomizer, to_remove, 10);
assert(randomizer.items.size == 70);
assert(randomizer.history == 21);
/* the other items in the history must be kept in order */
for (int i = 0; i < 9; ++i)
assert(history[i] == randomizer.items.data[21 + i]);
for (int i = 0; i < 40; ++i)
assert(history[i + 19] == randomizer.items.data[30 + i]);
ArrayDestroy(items, SIZE);
randomizer_Destroy(&randomizer);
#undef SIZE
}
static void
test_force_select_new_item(void)
{
struct randomizer randomizer;
randomizer_Init(&randomizer);
#define SIZE 100
vlc_playlist_item_t *items[SIZE];
ArrayInit(items, SIZE);
bool ok = randomizer_Add(&randomizer, items, SIZE);
assert(ok);
bool selected[SIZE] = {0};
for (int i = 0; i < SIZE; ++i)
{
vlc_playlist_item_t *item;
if (i != 50)
{
assert(randomizer_HasNext(&randomizer));
item = randomizer_Next(&randomizer);
}
else
{
/* force the selection of a new item not already selected */
item = randomizer.items.data[62];
randomizer_Select(&randomizer, item);
/* the item should now be the last selected one */
assert(randomizer.items.data[randomizer.next - 1] == item);
}
assert(item);
assert(!selected[item->index]); /* never selected twice */
selected[item->index] = true;
}
assert(!randomizer_HasNext(&randomizer)); /* no more items */
for (int i = 0; i < SIZE; ++i)
assert(selected[i]); /* all selected */
ArrayDestroy(items, SIZE);
randomizer_Destroy(&randomizer);
}
static void
test_force_select_item_already_selected(void)
{
struct randomizer randomizer;
randomizer_Init(&randomizer);
#define SIZE 100
vlc_playlist_item_t *items[SIZE];
ArrayInit(items, SIZE);
bool ok = randomizer_Add(&randomizer, items, SIZE);
assert(ok);
bool selected[SIZE] = {0};
/* we need an additional loop cycle, since we select the same item twice */
for (int i = 0; i < SIZE + 1; ++i)
{
vlc_playlist_item_t *item;
if (i != 50)
{
assert(randomizer_HasNext(&randomizer));
item = randomizer_Next(&randomizer);
}
else
{
/* force the selection of an item already selected */
item = randomizer.items.data[42];
randomizer_Select(&randomizer, item);
/* the item should now be the last selected one */
assert(randomizer.items.data[randomizer.next - 1] == item);
}
assert(item);
/* never selected twice, except for item 50 */
assert((i != 50) ^ selected[item->index]);
selected[item->index] = true;
}
assert(!randomizer_HasNext(&randomizer)); /* no more items */
for (int i = 0; i < SIZE; ++i)
assert(selected[i]); /* all selected */
ArrayDestroy(items, SIZE);
randomizer_Destroy(&randomizer);
#undef SIZE
}
static void
test_prev(void)
{
struct randomizer randomizer;
randomizer_Init(&randomizer);
#define SIZE 10
vlc_playlist_item_t *items[SIZE];
ArrayInit(items, SIZE);
bool ok = randomizer_Add(&randomizer, items, SIZE);
assert(ok);
assert(!randomizer_HasPrev(&randomizer));
vlc_playlist_item_t *actual[SIZE];
for (int i = 0; i < SIZE; ++i)
{
assert(randomizer_HasNext(&randomizer));
actual[i] = randomizer_Next(&randomizer);
assert(actual[i]);
}
assert(!randomizer_HasNext(&randomizer));
for (int i = SIZE - 2; i >= 0; --i)
{
assert(randomizer_HasPrev(&randomizer));
vlc_playlist_item_t *item = randomizer_Prev(&randomizer);
assert(item == actual[i]);
}
assert(!randomizer_HasPrev(&randomizer));
for (int i = 1; i < SIZE; ++i)
{
assert(randomizer_HasNext(&randomizer));
vlc_playlist_item_t *item = randomizer_Next(&randomizer);
assert(item == actual[i]);
}
ArrayDestroy(items, SIZE);
randomizer_Destroy(&randomizer);
#undef SIZE
}
static void
test_prev_with_select(void)
{
struct randomizer randomizer;
randomizer_Init(&randomizer);
#define SIZE 10
vlc_playlist_item_t *items[SIZE];
ArrayInit(items, SIZE);
bool ok = randomizer_Add(&randomizer, items, SIZE);
assert(ok);
assert(!randomizer_HasPrev(&randomizer));
vlc_playlist_item_t *actual[SIZE];
for (int i = 0; i < 5; ++i)
{
assert(randomizer_HasNext(&randomizer));
actual[i] = randomizer_Next(&randomizer);
assert(actual[i]);
}
randomizer_Select(&randomizer, actual[2]);
vlc_playlist_item_t *item;
assert(randomizer_HasPrev(&randomizer));
item = randomizer_Prev(&randomizer);
assert(item == actual[4]);
assert(randomizer_HasPrev(&randomizer));
item = randomizer_Prev(&randomizer);
assert(item == actual[3]);
assert(randomizer_HasPrev(&randomizer));
item = randomizer_Prev(&randomizer);
assert(item == actual[1]);
assert(randomizer_HasPrev(&randomizer));
item = randomizer_Prev(&randomizer);
assert(item == actual[0]);
assert(!randomizer_HasPrev(&randomizer));
ArrayDestroy(items, SIZE);
randomizer_Destroy(&randomizer);
#undef SIZE
}
static void
test_prev_across_reshuffle_loops(void)
{
struct randomizer randomizer;
randomizer_Init(&randomizer);
#define SIZE 10
vlc_playlist_item_t *items[SIZE];
ArrayInit(items, SIZE);
bool ok = randomizer_Add(&randomizer, items, SIZE);
assert(ok);
assert(!randomizer_HasPrev(&randomizer));
vlc_playlist_item_t *actual[SIZE];
for (int i = 0; i < SIZE; ++i)
{
assert(randomizer_HasNext(&randomizer));
actual[i] = randomizer_Next(&randomizer);
assert(actual[i]);
}
assert(!randomizer_HasNext(&randomizer));
randomizer_SetLoop(&randomizer, true);
assert(randomizer_HasNext(&randomizer));
vlc_playlist_item_t *actualnew[4];
/* determine the 4 first items */
for (int i = 0; i < 4; ++i)
{
assert(randomizer_HasNext(&randomizer));
actualnew[i] = randomizer_Next(&randomizer);
assert(actualnew[i]);
}
/* go back to the first */
for (int i = 2; i >= 0; --i)
{
assert(randomizer_HasPrev(&randomizer));
actualnew[i] = randomizer_Prev(&randomizer);
assert(actualnew[i]);
}
assert(actualnew[0] == randomizer.items.data[0]);
/* from now, any "prev" goes back to the history */
int index_in_actual = 9;
for (int i = 0; i < 6; ++i)
{
assert(randomizer_HasPrev(&randomizer));
vlc_playlist_item_t *item = randomizer_Prev(&randomizer);
int j;
for (j = 3; j >= 0; --j)
if (item == actualnew[j])
break;
bool in_actualnew = j != 0;
if (in_actualnew)
/* the item has been selected for the new order, it is not in the
* history anymore */
index_in_actual--;
else
/* the remaining previous items are retrieved in reverse order in
* the history */
assert(item == actual[index_in_actual]);
}
/* no more history: 4 in the current shuffle, 6 in the history */
assert(!randomizer_HasPrev(&randomizer));
ArrayDestroy(items, SIZE);
randomizer_Destroy(&randomizer);
#undef SIZE
}
/* when loop is enabled, we must take care that the last items of the
* previous order are not the same as the first items of the new order */
static void
test_loop_respect_not_same_before(void)
{
struct randomizer randomizer;
randomizer_Init(&randomizer);
randomizer_SetLoop(&randomizer, true);
#define SIZE (NOT_SAME_BEFORE + 2)
vlc_playlist_item_t *items[SIZE];
ArrayInit(items, SIZE);
bool ok = randomizer_Add(&randomizer, items, SIZE);
assert(ok);
vlc_playlist_item_t *actual[SIZE];
for (int i = 0; i < SIZE; ++i)
{
assert(randomizer_HasNext(&randomizer));
actual[i] = randomizer_Next(&randomizer);
}
for (int cycle = 0; cycle < 20; cycle++)
{
/* check that the first items are not the same as the last ones of the
* previous order */
for (int i = 0; i < NOT_SAME_BEFORE; ++i)
{
assert(randomizer_HasNext(&randomizer));
actual[i] = randomizer_Next(&randomizer);
for (int j = (i + SIZE - NOT_SAME_BEFORE) % SIZE;
j != i;
j = (j + 1) % SIZE)
{
assert(actual[i] != actual[j]);
}
}
for (int i = NOT_SAME_BEFORE; i < SIZE; ++i)
{
assert(randomizer_HasNext(&randomizer));
actual[i] = randomizer_Next(&randomizer);
}
}
ArrayDestroy(items, SIZE);
randomizer_Destroy(&randomizer);
#undef SIZE
}
/* if there are less items than NOT_SAME_BEFORE, obviously we can't avoid
* repeating last items in the new order, but it must still work */
static void
test_loop_respect_not_same_before_impossible(void)
{
struct randomizer randomizer;
randomizer_Init(&randomizer);
randomizer_SetLoop(&randomizer, true);
#define SIZE NOT_SAME_BEFORE
vlc_playlist_item_t *items[SIZE];
ArrayInit(items, SIZE);
bool ok = randomizer_Add(&randomizer, items, SIZE);
assert(ok);
for (int i = 0; i < 10 * SIZE; ++i)
{
assert(randomizer_HasNext(&randomizer));
vlc_playlist_item_t *item = randomizer_Next(&randomizer);
assert(item);
}
ArrayDestroy(items, SIZE);
randomizer_Destroy(&randomizer);
#undef SIZE
}
static void
test_has_prev_next_empty(void)
{
struct randomizer randomizer;
randomizer_Init(&randomizer);
assert(!randomizer_HasPrev(&randomizer));
assert(!randomizer_HasNext(&randomizer));
randomizer_SetLoop(&randomizer, true);
assert(!randomizer_HasPrev(&randomizer));
/* there are always next items in loop mode */
assert(randomizer_HasNext(&randomizer));
randomizer_Destroy(&randomizer);
}
int main(void)
{
test_all_items_selected_exactly_once();
test_all_items_selected_exactly_once_per_cycle();
test_all_items_selected_exactly_once_with_additions();
test_all_items_selected_exactly_once_with_removals();
test_cycle_after_manual_selection();
test_cycle_with_additions_and_removals();
test_force_select_new_item();
test_force_select_item_already_selected();
test_prev();
test_prev_with_select();
test_prev_across_reshuffle_loops();
test_loop_respect_not_same_before();
test_loop_respect_not_same_before_impossible();
test_has_prev_next_empty();
}
#endif
#endif
¤ Dauer der Verarbeitung: 0.15 Sekunden
(vorverarbeitet am 2026-11-04)
¤
*© Formatika GbR, Deutschland
|
|