labfy-investigation/tests/test_background_task.c
2026-07-17 10:55:40 +02:00

1469 lines
27 KiB
C

/******************************************************************************
* @file test_background_task.c
* @brief Tests du module BackgroundTask.
******************************************************************************/
#include "core/background_task.h"
#include <assert.h>
#include <stdio.h>
#include <string.h>
#include <glib.h>
/**
* @brief Délai maximal d'attente d'un test asynchrone.
*/
#define TEST_BACKGROUND_TASK_TIMEOUT_SECONDS 5
/**
* @struct TestBackgroundTaskResult
* @brief Résultat produit par le worker de test.
*/
typedef struct
{
char *text;
gboolean *destroyed;
} TestBackgroundTaskResult;
/**
* @struct TestBackgroundTaskWorkerData
* @brief Données transmises au worker de succès.
*/
typedef struct
{
gboolean *result_destroyed;
} TestBackgroundTaskWorkerData;
/**
* @struct TestBackgroundTaskCompletionData
* @brief Contexte utilisé par le callback final.
*/
typedef struct
{
GMainLoop *main_loop;
guint completion_count;
} TestBackgroundTaskCompletionData;
typedef struct
{
guint *worker_data_destroy_count;
guint *result_destroy_count;
} TestBackgroundTaskLifetimeWorkerData;
typedef struct
{
guint *destroy_count;
} TestBackgroundTaskLifetimeResult;
typedef struct
{
GMainLoop *main_loop;
guint completion_count;
guint completion_data_destroy_count;
} TestBackgroundTaskLifetimeCompletionData;
/**
* @brief Détruit un résultat produit par le worker.
*
* @param user_data Pointeur vers TestBackgroundTaskResult.
*/
static void test_background_task_result_free(
gpointer user_data
)
{
TestBackgroundTaskResult *result =
user_data;
if (result == NULL)
{
return;
}
if (result->destroyed != NULL)
{
*result->destroyed = TRUE;
}
g_free(
result->text
);
g_free(
result
);
}
/**
* @brief Termine un test qui a dépassé son délai maximal.
*
* @param user_data Boucle principale du test.
*
* @return G_SOURCE_REMOVE.
*/
static gboolean test_background_task_timeout(
gpointer user_data
)
{
GMainLoop *main_loop = user_data;
if (main_loop != NULL)
{
g_main_loop_quit(
main_loop
);
}
return G_SOURCE_REMOVE;
}
/**
* @brief Worker réalisant une tâche avec succès.
*
* @param task Tâche en cours.
* @param cancellable Objet d'annulation.
* @param worker_data Données TestBackgroundTaskWorkerData.
* @param result Emplacement recevant le résultat.
* @param error Emplacement recevant une erreur.
*
* @return TRUE.
*/
static gboolean test_background_task_success_worker(
BackgroundTask *task,
GCancellable *cancellable,
gpointer worker_data,
gpointer *result,
GError **error
)
{
TestBackgroundTaskWorkerData *data =
worker_data;
TestBackgroundTaskResult *worker_result =
NULL;
assert(task != NULL);
assert(cancellable != NULL);
assert(data != NULL);
assert(result != NULL);
assert(error != NULL);
assert(*error == NULL);
background_task_report_progress(
task,
0.25,
"Préparation"
);
background_task_report_progress(
task,
0.75,
"Finalisation"
);
worker_result = g_new0(
TestBackgroundTaskResult,
1
);
worker_result->text = g_strdup(
"Résultat valide"
);
worker_result->destroyed =
data->result_destroyed;
*result = worker_result;
return TRUE;
}
/**
* @brief Vérifie le résultat d'une tâche réussie.
*
* @param task Tâche terminée.
* @param user_data Données TestBackgroundTaskCompletionData.
*/
static void test_background_task_success_completed(
BackgroundTask *task,
gpointer user_data
)
{
TestBackgroundTaskCompletionData *completion_data =
user_data;
const TestBackgroundTaskResult *result =
NULL;
char *status_message = NULL;
GError *error = NULL;
assert(task != NULL);
assert(completion_data != NULL);
assert(completion_data->main_loop != NULL);
completion_data->completion_count++;
assert(
completion_data->completion_count == 1
);
assert(
background_task_get_state(task) ==
BACKGROUND_TASK_STATE_COMPLETED
);
assert(
background_task_get_progress(task) == 1.0
);
assert(
background_task_get_started_at_us(task) > 0
);
assert(
background_task_get_finished_at_us(task) >=
background_task_get_started_at_us(task)
);
error = background_task_dup_error(
task
);
assert(error == NULL);
result = background_task_get_result(
task
);
assert(result != NULL);
assert(result->text != NULL);
assert(
strcmp(
result->text,
"Résultat valide"
) == 0
);
status_message =
background_task_dup_status_message(
task
);
assert(status_message != NULL);
assert(
strcmp(
status_message,
"Finalisation"
) == 0
);
g_free(
status_message
);
g_main_loop_quit(
completion_data->main_loop
);
}
/**
* @brief Vérifie la construction et l'état initial d'une tâche.
*/
static void test_background_task_creation(void)
{
BackgroundTask *task = NULL;
char *status_message = NULL;
GError *error = NULL;
assert(
background_task_new(NULL) == NULL
);
assert(
background_task_new("") == NULL
);
task = background_task_new(
"Test de construction"
);
assert(task != NULL);
assert(
strcmp(
background_task_get_title(task),
"Test de construction"
) == 0
);
assert(
background_task_get_state(task) ==
BACKGROUND_TASK_STATE_PENDING
);
assert(
background_task_get_progress(task) == 0.0
);
assert(
background_task_get_started_at_us(task) == 0
);
assert(
background_task_get_finished_at_us(task) == 0
);
assert(
background_task_get_result(task) == NULL
);
status_message =
background_task_dup_status_message(
task
);
assert(status_message == NULL);
error = background_task_dup_error(
task
);
assert(error == NULL);
/*
* Une demande d'annulation avant le démarrage est ignorée.
*/
background_task_cancel(
task
);
assert(
!background_task_is_cancelled(task)
);
/*
* Une progression signalée avant le démarrage est ignorée.
*/
background_task_report_progress(
task,
0.50,
"Message ignoré"
);
assert(
background_task_get_progress(task) == 0.0
);
status_message =
background_task_dup_status_message(
task
);
assert(status_message == NULL);
background_task_unref(
task
);
background_task_unref(
NULL
);
}
/**
* @brief Vérifie une exécution asynchrone réussie.
*/
static void test_background_task_success(void)
{
BackgroundTask *task = NULL;
TestBackgroundTaskWorkerData *worker_data =
NULL;
TestBackgroundTaskCompletionData
completion_data = {0};
GMainLoop *main_loop = NULL;
GError *error = NULL;
guint timeout_source_id = 0;
gboolean result_destroyed = FALSE;
main_loop = g_main_loop_new(
NULL,
FALSE
);
assert(main_loop != NULL);
completion_data.main_loop =
main_loop;
completion_data.completion_count = 0;
worker_data = g_new0(
TestBackgroundTaskWorkerData,
1
);
worker_data->result_destroyed =
&result_destroyed;
task = background_task_new(
"Test de succès"
);
assert(task != NULL);
assert(
background_task_start(
task,
test_background_task_success_worker,
worker_data,
g_free,
test_background_task_result_free,
test_background_task_success_completed,
&completion_data,
NULL,
&error
)
);
assert(error == NULL);
assert(
background_task_get_state(task) ==
BACKGROUND_TASK_STATE_RUNNING
);
assert(
background_task_get_started_at_us(task) > 0
);
timeout_source_id = g_timeout_add_seconds(
TEST_BACKGROUND_TASK_TIMEOUT_SECONDS,
test_background_task_timeout,
main_loop
);
assert(timeout_source_id != 0);
g_main_loop_run(
main_loop
);
assert(
completion_data.completion_count == 1
);
/*
* Le délai n'a pas été déclenché puisque la tâche s'est terminée.
*/
assert(
g_source_remove(
timeout_source_id
)
);
assert(!result_destroyed);
/*
* Le résultat reste la propriété de BackgroundTask jusqu'au dernier
* unref.
*/
background_task_unref(
task
);
assert(result_destroyed);
g_main_loop_unref(
main_loop
);
}
/**
* @brief Worker retournant volontairement une erreur normale.
*/
static gboolean test_background_task_failure_worker(
BackgroundTask *task,
GCancellable *cancellable,
gpointer worker_data,
gpointer *result,
GError **error
)
{
(void) task;
(void) cancellable;
(void) worker_data;
(void) result;
assert(error != NULL);
assert(*error == NULL);
g_set_error_literal(
error,
G_IO_ERROR,
G_IO_ERROR_FAILED,
"Échec volontaire du worker."
);
return FALSE;
}
/**
* @brief Vérifie la finalisation d'une tâche en échec.
*/
static void test_background_task_failure_completed(
BackgroundTask *task,
gpointer user_data
)
{
TestBackgroundTaskCompletionData *completion_data =
user_data;
GError *error = NULL;
assert(task != NULL);
assert(completion_data != NULL);
completion_data->completion_count++;
assert(
completion_data->completion_count == 1
);
assert(
background_task_get_state(task) ==
BACKGROUND_TASK_STATE_FAILED
);
assert(
background_task_get_result(task) == NULL
);
assert(
background_task_get_started_at_us(task) > 0
);
assert(
background_task_get_finished_at_us(task) >=
background_task_get_started_at_us(task)
);
error = background_task_dup_error(
task
);
assert(error != NULL);
assert(
g_error_matches(
error,
G_IO_ERROR,
G_IO_ERROR_FAILED
)
);
assert(
strcmp(
error->message,
"Échec volontaire du worker."
) == 0
);
g_error_free(
error
);
g_main_loop_quit(
completion_data->main_loop
);
}
/**
* @brief Vérifie la conservation d'une erreur normale de worker.
*/
static void test_background_task_failure(void)
{
BackgroundTask *task = NULL;
TestBackgroundTaskCompletionData
completion_data = {0};
GMainLoop *main_loop = NULL;
GError *error = NULL;
guint timeout_source_id = 0;
main_loop = g_main_loop_new(
NULL,
FALSE
);
assert(main_loop != NULL);
completion_data.main_loop =
main_loop;
task = background_task_new(
"Test d'échec"
);
assert(task != NULL);
assert(
background_task_start(
task,
test_background_task_failure_worker,
NULL,
NULL,
NULL,
test_background_task_failure_completed,
&completion_data,
NULL,
&error
)
);
assert(error == NULL);
timeout_source_id = g_timeout_add_seconds(
TEST_BACKGROUND_TASK_TIMEOUT_SECONDS,
test_background_task_timeout,
main_loop
);
assert(timeout_source_id != 0);
g_main_loop_run(
main_loop
);
assert(
completion_data.completion_count == 1
);
assert(
g_source_remove(
timeout_source_id
)
);
background_task_unref(
task
);
g_main_loop_unref(
main_loop
);
}
/**
* @brief Worker violant le contrat en échouant sans GError.
*/
static gboolean test_background_task_invalid_protocol_worker(
BackgroundTask *task,
GCancellable *cancellable,
gpointer worker_data,
gpointer *result,
GError **error
)
{
(void) task;
(void) cancellable;
(void) worker_data;
(void) result;
assert(error != NULL);
assert(*error == NULL);
return FALSE;
}
/**
* @brief Vérifie la détection d'une violation du contrat du worker.
*/
static void test_background_task_invalid_protocol_completed(
BackgroundTask *task,
gpointer user_data
)
{
TestBackgroundTaskCompletionData *completion_data =
user_data;
GError *error = NULL;
assert(task != NULL);
assert(completion_data != NULL);
completion_data->completion_count++;
assert(
completion_data->completion_count == 1
);
assert(
background_task_get_state(task) ==
BACKGROUND_TASK_STATE_FAILED
);
assert(
background_task_get_result(task) == NULL
);
error = background_task_dup_error(
task
);
assert(error != NULL);
assert(
error->domain ==
BACKGROUND_TASK_ERROR
);
assert(
error->code ==
BACKGROUND_TASK_ERROR_WORKER_PROTOCOL
);
g_error_free(
error
);
g_main_loop_quit(
completion_data->main_loop
);
}
/**
* @brief Vérifie qu'un worker incorrect est transformé en échec contrôlé.
*/
static void test_background_task_invalid_protocol(void)
{
BackgroundTask *task = NULL;
TestBackgroundTaskCompletionData
completion_data = {0};
GMainLoop *main_loop = NULL;
GError *error = NULL;
guint timeout_source_id = 0;
main_loop = g_main_loop_new(
NULL,
FALSE
);
assert(main_loop != NULL);
completion_data.main_loop =
main_loop;
task = background_task_new(
"Test de protocole invalide"
);
assert(task != NULL);
assert(
background_task_start(
task,
test_background_task_invalid_protocol_worker,
NULL,
NULL,
NULL,
test_background_task_invalid_protocol_completed,
&completion_data,
NULL,
&error
)
);
assert(error == NULL);
timeout_source_id = g_timeout_add_seconds(
TEST_BACKGROUND_TASK_TIMEOUT_SECONDS,
test_background_task_timeout,
main_loop
);
assert(timeout_source_id != 0);
g_main_loop_run(
main_loop
);
assert(
completion_data.completion_count == 1
);
assert(
g_source_remove(
timeout_source_id
)
);
background_task_unref(
task
);
g_main_loop_unref(
main_loop
);
}
/**
* @brief Worker assez long pour tester l'annulation et le double démarrage.
*/
static gboolean test_background_task_wait_worker(
BackgroundTask *task,
GCancellable *cancellable,
gpointer worker_data,
gpointer *result,
GError **error
)
{
guint step = 0;
(void) worker_data;
assert(task != NULL);
assert(cancellable != NULL);
assert(result != NULL);
assert(error != NULL);
assert(*error == NULL);
for (step = 0; step < 100; step++)
{
if (g_cancellable_set_error_if_cancelled(
cancellable,
error
))
{
return FALSE;
}
background_task_report_progress(
task,
(double) step / 100.0,
"Traitement en cours"
);
g_usleep(
10000
);
}
*result = NULL;
return TRUE;
}
/**
* @brief Termine une tâche simple réussie.
*/
static void test_background_task_simple_completed(
BackgroundTask *task,
gpointer user_data
)
{
TestBackgroundTaskCompletionData *completion_data =
user_data;
assert(task != NULL);
assert(completion_data != NULL);
completion_data->completion_count++;
assert(
completion_data->completion_count == 1
);
assert(
background_task_get_state(task) ==
BACKGROUND_TASK_STATE_COMPLETED
);
g_main_loop_quit(
completion_data->main_loop
);
}
/**
* @brief Demande l'annulation depuis le contexte principal.
*
* @return G_SOURCE_REMOVE.
*/
static gboolean test_background_task_cancel_timeout(
gpointer user_data
)
{
BackgroundTask *task = user_data;
assert(task != NULL);
background_task_cancel(
task
);
return G_SOURCE_REMOVE;
}
/**
* @brief Libère une référence utilisée comme donnée GLib.
*/
static void test_background_task_unref_notify(
gpointer user_data
)
{
background_task_unref(
user_data
);
}
/**
* @brief Vérifie la finalisation d'une tâche annulée.
*/
static void test_background_task_cancelled_completed(
BackgroundTask *task,
gpointer user_data
)
{
TestBackgroundTaskCompletionData *completion_data =
user_data;
GError *error = NULL;
assert(task != NULL);
assert(completion_data != NULL);
completion_data->completion_count++;
assert(
completion_data->completion_count == 1
);
assert(
background_task_get_state(task) ==
BACKGROUND_TASK_STATE_CANCELLED
);
assert(
background_task_is_cancelled(task)
);
assert(
background_task_get_result(task) == NULL
);
error = background_task_dup_error(
task
);
assert(error != NULL);
assert(
g_error_matches(
error,
G_IO_ERROR,
G_IO_ERROR_CANCELLED
)
);
g_error_free(
error
);
g_main_loop_quit(
completion_data->main_loop
);
}
/**
* @brief Vérifie l'annulation coopérative d'une tâche.
*/
static void test_background_task_cancellation(void)
{
BackgroundTask *task = NULL;
TestBackgroundTaskCompletionData
completion_data = {0};
GMainLoop *main_loop = NULL;
GError *error = NULL;
guint cancel_source_id = 0;
guint timeout_source_id = 0;
main_loop = g_main_loop_new(
NULL,
FALSE
);
assert(main_loop != NULL);
completion_data.main_loop =
main_loop;
task = background_task_new(
"Test d'annulation"
);
assert(task != NULL);
assert(
background_task_start(
task,
test_background_task_wait_worker,
NULL,
NULL,
NULL,
test_background_task_cancelled_completed,
&completion_data,
NULL,
&error
)
);
assert(error == NULL);
/*
* La source conserve sa propre référence à la tâche jusqu'à son
* déclenchement ou sa destruction.
*/
cancel_source_id = g_timeout_add_full(
G_PRIORITY_DEFAULT,
50,
test_background_task_cancel_timeout,
background_task_ref(task),
test_background_task_unref_notify
);
assert(cancel_source_id != 0);
timeout_source_id = g_timeout_add_seconds(
TEST_BACKGROUND_TASK_TIMEOUT_SECONDS,
test_background_task_timeout,
main_loop
);
assert(timeout_source_id != 0);
g_main_loop_run(
main_loop
);
assert(
completion_data.completion_count == 1
);
assert(
g_source_remove(
timeout_source_id
)
);
background_task_unref(
task
);
g_main_loop_unref(
main_loop
);
}
/**
* @brief Marque une donnée comme détruite.
*/
static void test_background_task_mark_destroyed(
gpointer user_data
)
{
gboolean *destroyed = user_data;
assert(destroyed != NULL);
*destroyed = TRUE;
}
/**
* @brief Vérifie qu'une tâche ne peut être démarrée qu'une fois.
*/
static void test_background_task_double_start(void)
{
BackgroundTask *task = NULL;
TestBackgroundTaskCompletionData
completion_data = {0};
GMainLoop *main_loop = NULL;
GError *first_error = NULL;
GError *second_error = NULL;
guint timeout_source_id = 0;
gboolean second_worker_data_destroyed = FALSE;
gboolean second_completion_data_destroyed = FALSE;
main_loop = g_main_loop_new(
NULL,
FALSE
);
assert(main_loop != NULL);
completion_data.main_loop =
main_loop;
task = background_task_new(
"Test de double démarrage"
);
assert(task != NULL);
assert(
background_task_start(
task,
test_background_task_wait_worker,
NULL,
NULL,
NULL,
test_background_task_simple_completed,
&completion_data,
NULL,
&first_error
)
);
assert(first_error == NULL);
assert(
!background_task_start(
task,
test_background_task_wait_worker,
&second_worker_data_destroyed,
test_background_task_mark_destroyed,
NULL,
NULL,
&second_completion_data_destroyed,
test_background_task_mark_destroyed,
&second_error
)
);
assert(second_error != NULL);
assert(
second_error->domain ==
BACKGROUND_TASK_ERROR
);
assert(
second_error->code ==
BACKGROUND_TASK_ERROR_ALREADY_STARTED
);
/*
* Le démarrage ayant échoué, la propriété n'a pas été transférée.
*/
assert(!second_worker_data_destroyed);
assert(!second_completion_data_destroyed);
g_error_free(
second_error
);
timeout_source_id = g_timeout_add_seconds(
TEST_BACKGROUND_TASK_TIMEOUT_SECONDS,
test_background_task_timeout,
main_loop
);
assert(timeout_source_id != 0);
g_main_loop_run(
main_loop
);
assert(
g_source_remove(
timeout_source_id
)
);
background_task_unref(
task
);
/*
* La première tâche ne doit jamais s'approprier les données fournies
* lors de la seconde tentative refusée.
*/
assert(!second_worker_data_destroyed);
assert(!second_completion_data_destroyed);
g_main_loop_unref(
main_loop
);
}
static void test_background_task_lifetime_worker_data_free(
gpointer user_data
)
{
TestBackgroundTaskLifetimeWorkerData *worker_data =
user_data;
assert(worker_data != NULL);
assert(worker_data->worker_data_destroy_count != NULL);
(*worker_data->worker_data_destroy_count)++;
g_free(
worker_data
);
}
static void test_background_task_lifetime_result_free(
gpointer user_data
)
{
TestBackgroundTaskLifetimeResult *result =
user_data;
assert(result != NULL);
assert(result->destroy_count != NULL);
(*result->destroy_count)++;
g_free(
result
);
}
static void test_background_task_lifetime_completion_data_free(
gpointer user_data
)
{
TestBackgroundTaskLifetimeCompletionData *completion_data =
user_data;
assert(completion_data != NULL);
completion_data->completion_data_destroy_count++;
}
static gboolean test_background_task_lifetime_worker(
BackgroundTask *task,
GCancellable *cancellable,
gpointer worker_data,
gpointer *result,
GError **error
)
{
TestBackgroundTaskLifetimeWorkerData *data =
worker_data;
TestBackgroundTaskLifetimeResult *worker_result =
NULL;
assert(task != NULL);
assert(cancellable != NULL);
assert(data != NULL);
assert(result != NULL);
assert(error != NULL);
assert(*error == NULL);
worker_result = g_new0(
TestBackgroundTaskLifetimeResult,
1
);
worker_result->destroy_count =
data->result_destroy_count;
*result = worker_result;
return TRUE;
}
static void test_background_task_lifetime_completed(
BackgroundTask *task,
gpointer user_data
)
{
TestBackgroundTaskLifetimeCompletionData *completion_data =
user_data;
assert(task != NULL);
assert(completion_data != NULL);
completion_data->completion_count++;
assert(
completion_data->completion_count == 1
);
assert(
background_task_get_state(task) ==
BACKGROUND_TASK_STATE_COMPLETED
);
assert(
background_task_get_result(task) != NULL
);
g_main_loop_quit(
completion_data->main_loop
);
}
/**
* @brief Vérifie que la tâche survit à la libération immédiate
* de la référence de l'appelant.
*/
static void test_background_task_internal_reference(void)
{
BackgroundTask *task = NULL;
TestBackgroundTaskLifetimeWorkerData *worker_data =
NULL;
TestBackgroundTaskLifetimeCompletionData
completion_data = {0};
GMainLoop *main_loop = NULL;
GError *error = NULL;
guint timeout_source_id = 0;
guint worker_data_destroy_count = 0;
guint result_destroy_count = 0;
main_loop = g_main_loop_new(
NULL,
FALSE
);
assert(main_loop != NULL);
completion_data.main_loop =
main_loop;
worker_data = g_new0(
TestBackgroundTaskLifetimeWorkerData,
1
);
worker_data->worker_data_destroy_count =
&worker_data_destroy_count;
worker_data->result_destroy_count =
&result_destroy_count;
task = background_task_new(
"Test de référence interne"
);
assert(task != NULL);
assert(
background_task_start(
task,
test_background_task_lifetime_worker,
worker_data,
test_background_task_lifetime_worker_data_free,
test_background_task_lifetime_result_free,
test_background_task_lifetime_completed,
&completion_data,
test_background_task_lifetime_completion_data_free,
&error
)
);
assert(error == NULL);
/*
* La référence de l'appelant disparaît immédiatement.
* La référence interne de l'exécution doit maintenir task en vie.
*/
background_task_unref(
task
);
task = NULL;
timeout_source_id = g_timeout_add_seconds(
TEST_BACKGROUND_TASK_TIMEOUT_SECONDS,
test_background_task_timeout,
main_loop
);
assert(timeout_source_id != 0);
g_main_loop_run(
main_loop
);
assert(
g_source_remove(
timeout_source_id
)
);
/*
* Termine les éventuelles destructions attachées à la source GTask.
*/
while (g_main_context_iteration(
NULL,
FALSE
))
{
}
assert(
completion_data.completion_count == 1
);
assert(
completion_data.completion_data_destroy_count == 1
);
assert(
worker_data_destroy_count == 1
);
assert(
result_destroy_count == 1
);
g_main_loop_unref(
main_loop
);
}
int main(void)
{
test_background_task_creation();
test_background_task_success();
test_background_task_failure();
test_background_task_invalid_protocol();
test_background_task_cancellation();
test_background_task_double_start();
test_background_task_internal_reference();
printf(
"BackgroundTask : tests de construction et de succès valides.\n"
);
return 0;
}