diff options
Diffstat (limited to 'src/tests/test-port-scheduler.c')
-rw-r--r-- | src/tests/test-port-scheduler.c | 640 |
1 files changed, 640 insertions, 0 deletions
diff --git a/src/tests/test-port-scheduler.c b/src/tests/test-port-scheduler.c new file mode 100644 index 00000000..8f8fbeaa --- /dev/null +++ b/src/tests/test-port-scheduler.c @@ -0,0 +1,640 @@ +/* -*- Mode: C; tab-width: 4; indent-tabs-mode: nil; c-basic-offset: 4 -*- */ +/* + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 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 General Public License for more details: + * + * Copyright (C) 2025 Dan Williams <dan@ioncontrol.co> + */ + +#include <glib.h> +#include <string.h> +#include <locale.h> +#include <stdio.h> + +#include "mm-port-scheduler-rr.h" +#include "mm-log-test.h" + +static GMainLoop *loop; + +/*****************************************************************************/ + +typedef struct { + MMPortScheduler *sched; + guint sig_id; + gpointer source_id; + gint num_pending; + gboolean immediate; + guint idle_id; + + gpointer data; + gpointer data2; +} TestSourceCtx; + +static void +test_source_setup (TestSourceCtx *ctx, + MMPortScheduler *sched, + GCallback send_cmd_func) +{ + ctx->sched = g_object_ref (sched); + mm_port_scheduler_register_source (sched, ctx->source_id, "test"); + ctx->sig_id = g_signal_connect (sched, + MM_PORT_SCHEDULER_SIGNAL_SEND_COMMAND, + send_cmd_func, + ctx); + mm_port_scheduler_notify_num_pending (ctx->sched, ctx->source_id, ctx->num_pending); +} + +static void +test_source_cleanup (TestSourceCtx *ctx) +{ + g_assert_cmpint (ctx->num_pending, ==, 0); + g_signal_handler_disconnect (ctx->sched, ctx->sig_id); + mm_port_scheduler_unregister_source (ctx->sched, ctx->source_id); + g_object_unref (ctx->sched); +} + +/*****************************************************************************/ + +static gboolean +test_ss_command_done (TestSourceCtx *ctx) +{ + ctx->idle_id = 0; + mm_port_scheduler_notify_command_done (ctx->sched, ctx->source_id, ctx->num_pending); + if (ctx->num_pending == 0) + g_main_loop_quit (loop); + return G_SOURCE_REMOVE; +} + +static void +test_ss_send_command (MMPortScheduler *scheduler, + gpointer source, + TestSourceCtx *ctx) +{ + g_assert (scheduler == ctx->sched); + g_assert (source == ctx->source_id); + ctx->num_pending--; + g_assert_cmpint (ctx->num_pending, >=, 0); + + if (ctx->immediate) { + mm_port_scheduler_notify_command_done (ctx->sched, ctx->source_id, ctx->num_pending); + if (ctx->num_pending == 0) + g_main_loop_quit (loop); + } else + ctx->idle_id = g_idle_add ((GSourceFunc)test_ss_command_done, ctx); +} + +static void +test_ss_done (gboolean immediate) +{ + MMPortScheduler *sched; + TestSourceCtx ctx = { + .source_id = GUINT_TO_POINTER (0x1), + .num_pending = 10, + .immediate = immediate, + }; + + sched = MM_PORT_SCHEDULER (mm_port_scheduler_rr_new ()); + test_source_setup (&ctx, sched, G_CALLBACK (test_ss_send_command)); + + g_main_loop_run (loop); + + test_source_cleanup (&ctx); + g_object_unref (sched); +} + +/*****************************************************************************/ + +static void +test_ds_send_command (MMPortScheduler *scheduler, + gpointer source, + TestSourceCtx *ctx) +{ + guint *counter = ctx->data; + + g_assert (scheduler == ctx->sched); + if (source != ctx->source_id) + return; + + ctx->num_pending--; + g_assert_cmpuint (ctx->num_pending, >=, 0); + + mm_port_scheduler_notify_command_done (ctx->sched, ctx->source_id, ctx->num_pending); + + /* assert that the scheduler alternates between the two sources */ + g_assert_cmpuint ((*counter) % 2, ==, ctx->idle_id); + + (*counter)--; + if (*counter == 0) + g_main_loop_quit (loop); +} + +static void +test_ds_ordering (void) +{ + MMPortScheduler *sched; + guint counter; + + TestSourceCtx ctx1 = { + .source_id = GUINT_TO_POINTER (0x1), + .num_pending = 10, + .data = &counter, + .idle_id = 0, /* expected value of (counter % 2) */ + }; + + TestSourceCtx ctx2 = { + .source_id = GUINT_TO_POINTER (0x2), + .num_pending = 10, + .data = &counter, + .idle_id = 1, /* expected value of (counter % 2) */ + }; + + counter = ctx1.num_pending + ctx2.num_pending; + + sched = MM_PORT_SCHEDULER (mm_port_scheduler_rr_new ()); + test_source_setup (&ctx1, sched, G_CALLBACK (test_ds_send_command)); + test_source_setup (&ctx2, sched, G_CALLBACK (test_ds_send_command)); + + g_main_loop_run (loop); + + test_source_cleanup (&ctx1); + test_source_cleanup (&ctx2); + g_object_unref (sched); +} + +/*****************************************************************************/ + +static void +test_ds_uneven_send_command (MMPortScheduler *scheduler, + gpointer source, + TestSourceCtx *ctx) +{ + guint *counter = ctx->data; + + g_assert (scheduler == ctx->sched); + if (source != ctx->source_id) + return; + + ctx->num_pending--; + /* Test that the scheduler only calls each source for the number of pending + * commands it has notified the scheduler are in its queue. + */ + g_assert_cmpint (ctx->num_pending, >=, 0); + + mm_port_scheduler_notify_command_done (ctx->sched, ctx->source_id, ctx->num_pending); + + (*counter)--; + if (*counter == 0) + g_main_loop_quit (loop); +} + +static void +test_ds_uneven_num_pending (void) +{ + MMPortScheduler *sched; + guint counter; + + TestSourceCtx ctx1 = { + .source_id = GUINT_TO_POINTER (0x1), + .num_pending = 10, + .data = &counter, + }; + + TestSourceCtx ctx2 = { + .source_id = GUINT_TO_POINTER (0x2), + .num_pending = 5, + .data = &counter, + }; + + counter = ctx1.num_pending + ctx2.num_pending; + + sched = MM_PORT_SCHEDULER (mm_port_scheduler_rr_new ()); + test_source_setup (&ctx1, sched, G_CALLBACK (test_ds_uneven_send_command)); + test_source_setup (&ctx2, sched, G_CALLBACK (test_ds_uneven_send_command)); + + g_main_loop_run (loop); + + test_source_cleanup (&ctx1); + test_source_cleanup (&ctx2); + g_object_unref (sched); +} + +/*****************************************************************************/ + +static gboolean +ds_later_notify_pending (TestSourceCtx *ctx) +{ + mm_port_scheduler_notify_num_pending (ctx->sched, ctx->source_id, ctx->num_pending); + return G_SOURCE_REMOVE; +} + +static void +test_ds_later_send_command (MMPortScheduler *scheduler, + gpointer source, + TestSourceCtx *ctx) +{ + guint *counter = ctx->data; + + g_assert (scheduler == ctx->sched); + if (source != ctx->source_id) + return; + + ctx->num_pending--; + g_assert_cmpint (ctx->num_pending, >=, 0); + + mm_port_scheduler_notify_command_done (ctx->sched, ctx->source_id, ctx->num_pending); + + /* After we've reached zero pending commands wait a short time and then + * add more to make sure the scheduler wakes up and processes the new pending + * requests */ + if (ctx->num_pending == 0 && ctx->idle_id > 0) { + ctx->num_pending = ctx->idle_id; + ctx->idle_id = 0; + g_timeout_add_seconds (GPOINTER_TO_UINT (ctx->source_id), + (GSourceFunc)ds_later_notify_pending, + ctx); + } + + (*counter)--; + if (*counter == 0) + g_main_loop_quit (loop); +} + +static void +test_ds_num_pending_later (void) +{ + MMPortScheduler *sched; + guint counter; + + TestSourceCtx ctx1 = { + .source_id = GUINT_TO_POINTER (0x1), + .num_pending = 5, + .data = &counter, + .idle_id = 2, /* num pending to add after original num_pending reaches 0 */ + }; + + TestSourceCtx ctx2 = { + .source_id = GUINT_TO_POINTER (0x2), + .num_pending = 4, + .data = &counter, + .idle_id = 1, /* num pending to add after original num_pending reaches 0 */ + }; + + counter = ctx1.num_pending + ctx2.num_pending + ctx1.idle_id + ctx2.idle_id; + + sched = MM_PORT_SCHEDULER (mm_port_scheduler_rr_new ()); + test_source_setup (&ctx1, sched, G_CALLBACK (test_ds_later_send_command)); + test_source_setup (&ctx2, sched, G_CALLBACK (test_ds_later_send_command)); + + g_main_loop_run (loop); + + test_source_cleanup (&ctx1); + test_source_cleanup (&ctx2); + g_object_unref (sched); +} + +/*****************************************************************************/ + +static gboolean +quit_loop (void) +{ + g_main_loop_quit (loop); + return G_SOURCE_REMOVE; +} + +static void +test_ds_bad_notify_send_command (MMPortScheduler *scheduler, + gpointer source, + TestSourceCtx *ctx) +{ + g_assert (scheduler == ctx->sched); + + if (source != ctx->source_id) + return; + + if (GPOINTER_TO_UINT (source) == 0x2) { + /* Second source without any pending commands tries to call + * notify_command_done but this should have no effect; the scheduler + * should ignore the num_pending given here. + */ + mm_port_scheduler_notify_command_done (ctx->sched, ctx->source_id, 15); + return; + } + + g_assert_cmpuint (GPOINTER_TO_UINT (source), ==, 0x1); + + ctx->num_pending--; + g_assert_cmpint (ctx->num_pending, >=, 0); + + mm_port_scheduler_notify_command_done (ctx->sched, ctx->source_id, ctx->num_pending); + + if (ctx->num_pending == 0) { + /* Schedule a timeout to quit the mainloop to give enough time for + * the scheduler to mess up (which we don't expect). + */ + g_timeout_add_seconds (1, (GSourceFunc)quit_loop, NULL); + } +} + +static gboolean +assert_not_reached (void) +{ + g_assert_not_reached (); + return G_SOURCE_REMOVE; +} + +static void +test_ds_bad_notify_done (void) +{ + MMPortScheduler *sched; + guint timeout_id; + + TestSourceCtx ctx1 = { + .source_id = GUINT_TO_POINTER (0x1), + .num_pending = 5, + }; + + TestSourceCtx ctx2 = { + .source_id = GUINT_TO_POINTER (0x2), + .num_pending = 0, + /* This source just hammers notify_done even though it never has any + * pending commands. + */ + }; + + timeout_id = g_timeout_add_seconds (3, (GSourceFunc)assert_not_reached, NULL); + + sched = MM_PORT_SCHEDULER (mm_port_scheduler_rr_new ()); + test_source_setup (&ctx1, sched, G_CALLBACK (test_ds_bad_notify_send_command)); + test_source_setup (&ctx2, sched, G_CALLBACK (test_ds_bad_notify_send_command)); + + g_main_loop_run (loop); + + g_source_remove (timeout_id); + test_source_cleanup (&ctx1); + test_source_cleanup (&ctx2); + g_object_unref (sched); +} + +/*****************************************************************************/ + +static void +test_ds_delay_notify_send_command (MMPortScheduler *scheduler, + gpointer source, + TestSourceCtx *ctx) +{ + guint *counter = ctx->data; + gint64 *last_call = ctx->data2; + gint64 now; + + g_assert (scheduler == ctx->sched); + if (source != ctx->source_id) + return; + + /* Ensure there was at least the inter-port delay time since the last call */ + now = g_get_monotonic_time (); + g_assert_cmpint (now - *last_call, >=, 500); + *last_call = now; + + ctx->num_pending--; + g_assert_cmpuint (ctx->num_pending, >=, 0); + + mm_port_scheduler_notify_command_done (ctx->sched, ctx->source_id, ctx->num_pending); + + (*counter)--; + if (*counter == 0) + g_main_loop_quit (loop); +} + +static void +test_ds_inter_port_delay (void) +{ + MMPortScheduler *sched; + guint counter; + gint64 last_call = 0; + + TestSourceCtx ctx1 = { + .source_id = GUINT_TO_POINTER (0x1), + .data = &counter, + .data2 = &last_call, + .num_pending = 5, + }; + + TestSourceCtx ctx2 = { + .source_id = GUINT_TO_POINTER (0x2), + .data = &counter, + .data2 = &last_call, + .num_pending = 5, + }; + + counter = ctx1.num_pending + ctx2.num_pending; + + sched = MM_PORT_SCHEDULER (mm_port_scheduler_rr_new ()); + g_object_set (sched, MM_PORT_SCHEDULER_RR_INTER_PORT_DELAY, 500, NULL); + test_source_setup (&ctx1, sched, G_CALLBACK (test_ds_delay_notify_send_command)); + test_source_setup (&ctx2, sched, G_CALLBACK (test_ds_delay_notify_send_command)); + + g_main_loop_run (loop); + + test_source_cleanup (&ctx1); + test_source_cleanup (&ctx2); + g_object_unref (sched); +} + +/*****************************************************************************/ + +static void +test_ds_no_delay_notify_send_command (MMPortScheduler *scheduler, + gpointer source, + TestSourceCtx *ctx) +{ + guint *counter = ctx->data; + gint64 *last_call = ctx->data2; + gint64 now; + + g_assert (scheduler == ctx->sched); + if (source != ctx->source_id) + return; + + /* Since the second source has no pending commands, there should be + * no delay between calls since only one source is executing. + */ + now = g_get_monotonic_time (); + g_assert_cmpint (now - *last_call, <, 1000); + *last_call = now; + + ctx->num_pending--; + g_assert_cmpuint (ctx->num_pending, >=, 0); + + mm_port_scheduler_notify_command_done (ctx->sched, ctx->source_id, ctx->num_pending); + + (*counter)--; + if (*counter == 0) + g_main_loop_quit (loop); +} + +static void +test_ds_inter_port_no_delay (void) +{ + MMPortScheduler *sched; + guint counter; + gint64 last_call; + + TestSourceCtx ctx1 = { + .source_id = GUINT_TO_POINTER (0x1), + .data = &counter, + .data2 = &last_call, + .num_pending = 5, + }; + + TestSourceCtx ctx2 = { + .source_id = GUINT_TO_POINTER (0x2), + .data = &counter, + .data2 = &last_call, + .num_pending = 0, + }; + + counter = ctx1.num_pending + ctx2.num_pending; + last_call = g_get_monotonic_time (); + + sched = MM_PORT_SCHEDULER (mm_port_scheduler_rr_new ()); + test_source_setup (&ctx1, sched, G_CALLBACK (test_ds_no_delay_notify_send_command)); + test_source_setup (&ctx2, sched, G_CALLBACK (test_ds_no_delay_notify_send_command)); + + g_main_loop_run (loop); + + test_source_cleanup (&ctx1); + test_source_cleanup (&ctx2); + g_object_unref (sched); +} + +/*****************************************************************************/ + +static void +test_ds_pending_during_done_notify_send_command (MMPortScheduler *scheduler, + gpointer source, + TestSourceCtx *ctx) +{ + guint *counter = ctx->data; + + g_assert (scheduler == ctx->sched); + if (source != ctx->source_id) + return; + + ctx->num_pending--; + g_assert_cmpuint (ctx->num_pending, >=, 0); + + /* Simulate command completion adding more pending commands before calling command-done */ + if (ctx->idle_id > 0) { + ctx->idle_id--; + ctx->num_pending++; /* increase length of fake source's command queue */ + mm_port_scheduler_notify_num_pending (ctx->sched, ctx->source_id, ctx->num_pending); + } + + mm_port_scheduler_notify_command_done (ctx->sched, ctx->source_id, ctx->num_pending); + + (*counter)--; + if (*counter == 0) + g_main_loop_quit (loop); +} + +static void +test_ds_pending_during_done (void) +{ + MMPortScheduler *sched; + guint counter; + + TestSourceCtx ctx1 = { + .source_id = GUINT_TO_POINTER (0x1), + .data = &counter, + .idle_id = 5, /* additional to add during notify-command-done */ + .num_pending = 5, + }; + + TestSourceCtx ctx2 = { + .source_id = GUINT_TO_POINTER (0x2), + .data = &counter, + .idle_id = 5, /* additional to add during notify-command-done */ + .num_pending = 5, + }; + + counter = ctx1.num_pending + ctx2.num_pending + ctx1.idle_id + ctx2.idle_id; + + sched = MM_PORT_SCHEDULER (mm_port_scheduler_rr_new ()); + test_source_setup (&ctx1, sched, G_CALLBACK (test_ds_pending_during_done_notify_send_command)); + test_source_setup (&ctx2, sched, G_CALLBACK (test_ds_pending_during_done_notify_send_command)); + + g_main_loop_run (loop); + + test_source_cleanup (&ctx1); + test_source_cleanup (&ctx2); + g_object_unref (sched); +} + +/*****************************************************************************/ + +static void +test_errors_bad_source_done (void) +{ + MMPortScheduler *sched; + + sched = MM_PORT_SCHEDULER (mm_port_scheduler_rr_new ()); + mm_port_scheduler_notify_command_done (sched, GUINT_TO_POINTER (0x1), 5); + g_object_unref (sched); +} + +/*****************************************************************************/ + +static void +test_errors_source_done_before_loop (void) +{ + MMPortScheduler *sched; + + TestSourceCtx ctx = { + .source_id = GUINT_TO_POINTER (0x1), + }; + + sched = MM_PORT_SCHEDULER (mm_port_scheduler_rr_new ()); + test_source_setup (&ctx, sched, G_CALLBACK (assert_not_reached)); + mm_port_scheduler_notify_command_done (sched, ctx.source_id, 5); + + test_source_cleanup (&ctx); + g_object_unref (sched); +} + +/*****************************************************************************/ + +int main (int argc, char **argv) +{ + int ret; + + setlocale (LC_ALL, ""); + + g_test_init (&argc, &argv, NULL); + + loop = g_main_loop_new (NULL, FALSE); + + g_test_add_data_func ("/MM/port-scheduler/single-source/done-immediate", GUINT_TO_POINTER (TRUE), (GTestDataFunc)test_ss_done); + g_test_add_data_func ("/MM/port-scheduler/single-source/done-idle", GUINT_TO_POINTER (FALSE), (GTestDataFunc)test_ss_done); + g_test_add_data_func ("/MM/port-scheduler/dual-source/ordering", NULL, (GTestDataFunc)test_ds_ordering); + g_test_add_data_func ("/MM/port-scheduler/dual-source/uneven-num-pending", NULL, (GTestDataFunc)test_ds_uneven_num_pending); + g_test_add_data_func ("/MM/port-scheduler/dual-source/num-pending-later", NULL, (GTestDataFunc)test_ds_num_pending_later); + g_test_add_data_func ("/MM/port-scheduler/dual-source/bad-notify-done", NULL, (GTestDataFunc)test_ds_bad_notify_done); + g_test_add_data_func ("/MM/port-scheduler/dual-source/inter-port-delay", NULL, (GTestDataFunc)test_ds_inter_port_delay); + g_test_add_data_func ("/MM/port-scheduler/dual-source/inter-port-no-delay", NULL, (GTestDataFunc)test_ds_inter_port_no_delay); + g_test_add_data_func ("/MM/port-scheduler/dual-source/pending-during-done", NULL, (GTestDataFunc)test_ds_pending_during_done); + g_test_add_data_func ("/MM/port-scheduler/errors/bad-source-done", NULL, (GTestDataFunc)test_errors_bad_source_done); + g_test_add_data_func ("/MM/port-scheduler/errors/source-done-before-loop", NULL, (GTestDataFunc)test_errors_source_done_before_loop); + + ret = g_test_run(); + + g_main_loop_unref (loop); + + return ret; +} |