diff --git a/tests/meson.build b/tests/meson.build index ecec6b9..67f63d3 100644 --- a/tests/meson.build +++ b/tests/meson.build @@ -115,4 +115,35 @@ t = executable('util', test_sources, ) test('util', t, env: test_env) +mock_link_args = [ test_link_args, + '-Wl,--wrap=lfb_init', + '-Wl,--wrap=lfb_uninit', + '-Wl,--wrap=lfb_event_set_feedback_profile', + '-Wl,--wrap=lfb_event_trigger_feedback_async', + '-Wl,--wrap=lfb_event_trigger_feedback_finish', + '-Wl,--wrap=lfb_event_end_feedback_async', + '-Wl,--wrap=lfb_event_end_feedback_finish', + '-Wl,--wrap=calls_contacts_provider_new', + ] +cmocka = dependency ('cmocka', required: false) + +if cmocka.found () + test_sources = [ + 'mock-call.c', 'mock-call.h', 'mock-libfeedback.h', + 'test-ringer.c' + ] + + t = executable('ringer', test_sources, + c_args : test_cflags, + link_args: mock_link_args, + pie: true, + link_with : [calls_vala, libcalls], + dependencies: [calls_deps, cmocka], + include_directories : [ + calls_includes, + ] + ) + test('ringer', t, env: test_env) +endif + endif diff --git a/tests/mock-call.c b/tests/mock-call.c new file mode 100644 index 0000000..62c9d1e --- /dev/null +++ b/tests/mock-call.c @@ -0,0 +1,192 @@ +/* + * Copyright (C) 2021 Purism SPC + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Author: Evangelos Ribeiro Tzaras + * + */ + +#include "mock-call.h" + +#include + +enum { + PROP_0, + PROP_NAME, + PROP_ID, + PROP_STATE, + PROP_LAST_PROP, +}; +static GParamSpec *props[PROP_LAST_PROP]; + +struct _CallsMockCall +{ + CallsCall parent_instance; + + char *id; + char *display_name; + CallsCallState state; +}; + +G_DEFINE_TYPE (CallsMockCall, calls_mock_call, CALLS_TYPE_CALL) + + +static void +calls_mock_call_answer (CallsCall *call) +{ + g_assert (CALLS_IS_MOCK_CALL (call)); + g_assert_cmpint (calls_call_get_state (call), ==, CALLS_CALL_STATE_INCOMING); + + calls_mock_call_set_state (CALLS_MOCK_CALL (call), CALLS_CALL_STATE_ACTIVE); +} + + +static void +calls_mock_call_hang_up (CallsCall *call) +{ + g_assert (CALLS_IS_MOCK_CALL (call)); + g_assert_cmpint (calls_call_get_state (call), !=, CALLS_CALL_STATE_DISCONNECTED); + + calls_mock_call_set_state (CALLS_MOCK_CALL (call), CALLS_CALL_STATE_DISCONNECTED); +} + + +static CallsCallState +calls_mock_call_get_state (CallsCall *call) +{ + g_assert (CALLS_IS_MOCK_CALL (call)); + + return CALLS_MOCK_CALL (call)->state; +} + +static void +calls_mock_call_get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + CallsMockCall *self = CALLS_MOCK_CALL (object); + + switch (prop_id) { + case PROP_ID: + g_value_set_string (value, self->id); + break; + case PROP_NAME: + g_value_set_string (value, self->display_name); + break; + case PROP_STATE: + g_value_set_enum (value, self->state); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + } +} + + +static void +calls_mock_call_finalize (GObject *object) +{ + CallsMockCall *self = CALLS_MOCK_CALL (object); + + g_free (self->id); + g_free (self->display_name); + + G_OBJECT_CLASS (calls_mock_call_parent_class)->finalize (object); +} + + +static void +calls_mock_call_class_init (CallsMockCallClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + CallsCallClass *call_class = CALLS_CALL_CLASS (klass); + + call_class->answer = calls_mock_call_answer; + call_class->hang_up = calls_mock_call_hang_up; + call_class->get_state = calls_mock_call_get_state; + + object_class->get_property = calls_mock_call_get_property; + object_class->finalize = calls_mock_call_finalize; + + g_object_class_override_property (object_class, + PROP_ID, + "id"); + props[PROP_ID] = g_object_class_find_property (object_class, "id"); + + g_object_class_override_property (object_class, + PROP_NAME, + "name"); + props[PROP_NAME] = g_object_class_find_property (object_class, "name"); + + g_object_class_override_property (object_class, + PROP_STATE, + "state"); + props[PROP_STATE] = g_object_class_find_property (object_class, "state"); + +} + + +static void +calls_mock_call_init (CallsMockCall *self) +{ + self->display_name = g_strdup ("John Doe"); + self->id = g_strdup ("0800 1234"); + self->state = CALLS_CALL_STATE_INCOMING; +} + + +CallsMockCall * +calls_mock_call_new (void) +{ + return g_object_new (CALLS_TYPE_MOCK_CALL, NULL); +} + + +void +calls_mock_call_set_id (CallsMockCall *self, + const char *id) +{ + g_return_if_fail (CALLS_IS_MOCK_CALL (self)); + + g_free (self->id); + self->id = g_strdup (id); + + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_ID]); +} + + +void +calls_mock_call_set_name (CallsMockCall *self, + const char *name) +{ + g_return_if_fail (CALLS_IS_MOCK_CALL (self)); + + g_free (self->display_name); + self->display_name = g_strdup (name); + + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_NAME]); +} + + +void +calls_mock_call_set_state (CallsMockCall *self, + CallsCallState state) +{ + CallsCallState old_state; + + g_return_if_fail (CALLS_IS_MOCK_CALL (self)); + + old_state = self->state; + if (old_state == state) + return; + + self->state = state; + + g_object_notify_by_pspec (G_OBJECT (self), props[PROP_STATE]); + g_signal_emit_by_name (CALLS_CALL (self), + "state-changed", + state, + old_state); +} + diff --git a/tests/mock-call.h b/tests/mock-call.h new file mode 100644 index 0000000..4593568 --- /dev/null +++ b/tests/mock-call.h @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2021 Purism SPC + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Author: Evangelos Ribeiro Tzaras + * + */ + +#pragma once + +#include "calls-call.h" + +G_BEGIN_DECLS + +#define CALLS_TYPE_MOCK_CALL (calls_mock_call_get_type()) + +G_DECLARE_FINAL_TYPE (CallsMockCall, calls_mock_call, CALLS, MOCK_CALL, CallsCall) + +CallsMockCall *calls_mock_call_new (void); +void calls_mock_call_set_id (CallsMockCall *self, + const char *id); +void calls_mock_call_set_name (CallsMockCall *self, + const char *name); +void calls_mock_call_set_state (CallsMockCall *self, + CallsCallState state); +void calls_mock_call_set_inbound (CallsMockCall *self, + gboolean inbound); + +G_END_DECLS diff --git a/tests/mock-contacts-provider.h b/tests/mock-contacts-provider.h new file mode 100644 index 0000000..dc85931 --- /dev/null +++ b/tests/mock-contacts-provider.h @@ -0,0 +1,5 @@ +#pragma once + +#include "calls-contacts-provider.h" + +CallsContactsProvider *__wrap_calls_contacts_provider_new (CallsSettings *seetings); diff --git a/tests/mock-libfeedback.h b/tests/mock-libfeedback.h new file mode 100644 index 0000000..66bdcab --- /dev/null +++ b/tests/mock-libfeedback.h @@ -0,0 +1,28 @@ +#pragma once + +#define LIBFEEDBACK_USE_UNSTABLE_API + +#include +#include + + +gboolean __wrap_lfb_init (const char *app_id, + GError **error); +void __wrap_lfb_uninit (void); +void __wrap_lfb_event_set_feedback_profile (LfbEvent *self, + const char *profile); + +void __wrap_lfb_event_trigger_feedback_async (LfbEvent *self, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data); +gboolean __wrap_lfb_event_trigger_feedback_finish (LfbEvent *self, + GAsyncResult *res, + GError **error); +void __wrap_lfb_event_end_feedback_async (LfbEvent *self, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data); +gboolean __wrap_lfb_event_end_feedback_finish (LfbEvent *self, + GAsyncResult *res, + GError **error); diff --git a/tests/mock/mock-libfeedback.c b/tests/mock/mock-libfeedback.c new file mode 100644 index 0000000..073225b --- /dev/null +++ b/tests/mock/mock-libfeedback.c @@ -0,0 +1,83 @@ +#include "mock-libfeedback.h" + +#include +#include + + +static gboolean +on_mock_timeout (gpointer user_data) +{ + GTask *task = user_data; + + g_task_return_boolean (task, TRUE); + return G_SOURCE_REMOVE; +} + +/* could probably also live without mocking/stubbing lfb_{un,}init() + * and run under GTestDBus since we're actually only interested + * in the feedback trigger and end functions + */ +gboolean +__wrap_lfb_init (const char *app_id, + GError **error) +{ + return TRUE; +} + + +void +__wrap_lfb_uninit (void) +{ + return; +} + + +void +__wrap_lfb_event_set_feedback_profile (LfbEvent *self, + const char *profile) +{ + return; +} + + +void +__wrap_lfb_event_trigger_feedback_async (LfbEvent *self, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data) +{ + GTask *trigger_task = g_task_new (self, cancellable, callback, user_data); + int mock_time_ms = mock_type (int); + gboolean mock_will_be_cancelled = mock_type (gboolean); + + if (!mock_will_be_cancelled) + g_timeout_add (mock_time_ms, on_mock_timeout, trigger_task); +} + + +gboolean +__wrap_lfb_event_trigger_feedback_finish (LfbEvent *self, + GAsyncResult *res, + GError **error) +{ + return TRUE; +} + + +void +__wrap_lfb_event_end_feedback_async (LfbEvent *self, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data) +{ + ; +} + + +gboolean +__wrap_lfb_event_end_feedback_finish (LfbEvent *self, + GAsyncResult *res, + GError **error) +{ + return TRUE; +} diff --git a/tests/test-ringer.c b/tests/test-ringer.c new file mode 100644 index 0000000..893591e --- /dev/null +++ b/tests/test-ringer.c @@ -0,0 +1,509 @@ +/* + * Copyright (C) 2021 Purism SPC + * + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Author: Evangelos Ribeiro Tzaras + * + */ + +#include "calls-manager.h" +#include "calls-ringer.h" +#include "mock-call.h" +#include "mock-libfeedback.h" +#include "mock-contacts-provider.h" + +#include +#include + + +/* mock libfeedback functions */ + +static gboolean +on_mock_timeout (gpointer user_data) +{ + GTask *task; + GCancellable *cancellable; + + if (!G_IS_TASK (user_data)) + return G_SOURCE_REMOVE; + + task = G_TASK (user_data); + cancellable = g_task_get_cancellable (task); + + if (!g_cancellable_is_cancelled (cancellable)) + g_task_return_boolean (task, TRUE); + + return G_SOURCE_REMOVE; +} + +static gboolean +on_check_task_cancelled (gpointer user_data) +{ + GTask *task; + + if (!G_IS_TASK (user_data)) + return G_SOURCE_REMOVE; + + task = G_TASK (user_data); + + if (g_task_get_completed (task) || g_task_return_error_if_cancelled (task)) + return G_SOURCE_REMOVE; + + return G_SOURCE_CONTINUE; +} + + +gboolean +__wrap_lfb_init (const char *app_id, + GError **error) +{ + return TRUE; +} + + +void +__wrap_lfb_uninit (void) +{ + return; +} + + +void +__wrap_lfb_event_set_feedback_profile (LfbEvent *self, + const char *profile) +{ + return; +} + + +void +__wrap_lfb_event_trigger_feedback_async (LfbEvent *self, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data) +{ + GTask *trigger_task = g_task_new (self, cancellable, callback, user_data); + int mock_time_ms = mock_type (int); + + g_timeout_add (mock_time_ms, on_mock_timeout, trigger_task); + g_idle_add (G_SOURCE_FUNC (on_check_task_cancelled), trigger_task); +} + + +gboolean +__wrap_lfb_event_trigger_feedback_finish (LfbEvent *self, + GAsyncResult *res, + GError **error) +{ + GTask *task = G_TASK (res); + gboolean ret; + + ret = g_task_propagate_boolean (task, error); + + g_object_unref (task); + + return ret; +} + + +void +__wrap_lfb_event_end_feedback_async (LfbEvent *self, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data) +{ + GTask *trigger_task = g_task_new (self, cancellable, callback, user_data); + int mock_time_ms = mock_type (int); + + g_timeout_add (mock_time_ms, on_mock_timeout, trigger_task); +} + + +gboolean +__wrap_lfb_event_end_feedback_finish (LfbEvent *self, + GAsyncResult *res, + GError **error) +{ + GTask *task = G_TASK (res); + gboolean ret; + + ret = g_task_propagate_boolean (G_TASK (res), error); + + g_object_unref (task); + + return ret; +} + + +CallsContactsProvider * +__wrap_calls_contacts_provider_new (CallsSettings *settings) +{ + return NULL; +} + + +/* add or remove calls */ + +static void +add_call (CallsManager *manager, + CallsMockCall *call) +{ + g_assert (CALLS_IS_MANAGER (manager)); + g_assert (CALLS_IS_MOCK_CALL (call)); + + g_signal_emit_by_name (manager, "call-add", CALLS_CALL (call), NULL); +} + + +static void +remove_call (CallsManager *manager, + CallsMockCall *call) +{ + g_assert (CALLS_IS_MANAGER (manager)); + g_assert (CALLS_IS_MOCK_CALL (call)); + + g_signal_emit_by_name (manager, "call-remove", CALLS_CALL (call), NULL); +} + +/* TestData setup and tear down */ +typedef struct { + CallsManager *manager; + CallsRinger *ringer; + CallsMockCall *call_one; + CallsMockCall *call_two; + GMainLoop *loop; +} TestData; + + +static int +setup_test_data (void **state) +{ + TestData *data = g_new0 (TestData, 1); + + if (data == NULL) + return -1; + + data->manager = calls_manager_get_default (); + data->ringer = calls_ringer_new (); + data->call_one = calls_mock_call_new (); + data->call_two = calls_mock_call_new (); + data->loop = g_main_loop_new (NULL, FALSE); + + *state = data; + + return 0; +} + + +static int +tear_down_test_data (void **state) +{ + TestData *data = *state; + + g_object_unref (data->call_one); + g_object_unref (data->call_two); + g_object_unref (data->ringer); + g_main_loop_unref (data->loop); + + g_free (data); + return 0; +} + +/* t1: test_ringing_incoming_call */ +static void +t1_on_ringer_call_accepted (CallsRinger *ringer, + GParamSpec *pspec, + gpointer user_data) +{ + static guint test_phase = 0; + TestData *data = user_data; + + switch (test_phase++) { + case 0: /* incoming call */ + assert_true (calls_ringer_get_is_ringing (ringer)); + calls_call_answer (CALLS_CALL (data->call_one)); + break; + case 1: /* incoming call accepted */ + assert_false (calls_ringer_get_is_ringing (ringer)); + g_main_loop_quit ((GMainLoop *) data->loop); + break; + default: + g_assert_not_reached (); /* did not find equivalent cmocka assertion */ + } +} + + +static void +test_ringing_accept_call (void **state) +{ + TestData *data = *state; + + assert_false (calls_ringer_get_is_ringing (data->ringer)); + + g_signal_connect (data->ringer, + "notify::ringing", + G_CALLBACK (t1_on_ringer_call_accepted), + data); + + /* delay before completion of __wrap_lfb_event_trigger_feedback_async() */ + will_return (__wrap_lfb_event_trigger_feedback_async, 10); + /* delay before completion of __wrap_lfb_event_end_feedback_async() */ + will_return (__wrap_lfb_event_end_feedback_async, 10); + + calls_mock_call_set_state (data->call_one, CALLS_CALL_STATE_INCOMING); + add_call (data->manager, data->call_one); + + /* main loop will quit in callback of notify::ring */ + g_main_loop_run (data->loop); + + remove_call (data->manager, data->call_one); + assert_false (calls_ringer_get_is_ringing (data->ringer)); +} + +/* t2: test_ringing_hang_up_call */ +static void +t2_on_ringer_call_hang_up (CallsRinger *ringer, + GParamSpec *pspec, + gpointer user_data) +{ + static guint test_phase = 0; + TestData *data = user_data; + + switch (test_phase++) { + case 0: /* incoming call */ + assert_true (calls_ringer_get_is_ringing (ringer)); + calls_call_hang_up (CALLS_CALL (data->call_one)); + break; + case 1: /* incoming call hung up */ + assert_false (calls_ringer_get_is_ringing (ringer)); + g_main_loop_quit ((GMainLoop *) data->loop); + break; + default: + g_assert_not_reached (); /* did not find equivalent cmocka assertion */ + } +} + + +static void +test_ringing_hang_up_call (void **state) +{ + TestData *data = *state; + + assert_false (calls_ringer_get_is_ringing (data->ringer)); + + g_signal_connect (data->ringer, + "notify::ringing", + G_CALLBACK (t2_on_ringer_call_hang_up), + data); + + /* delay before completion of __wrap_lfb_event_trigger_feedback_async() */ + will_return (__wrap_lfb_event_trigger_feedback_async, 10); + /* delay before completion of __wrap_lfb_event_end_feedback_async() */ + will_return (__wrap_lfb_event_end_feedback_async, 10); + + calls_mock_call_set_state (data->call_one, CALLS_CALL_STATE_INCOMING); + add_call (data->manager, data->call_one); + + /* main loop will quit in callback of notify::ring */ + g_main_loop_run (data->loop); + + remove_call (data->manager, data->call_one); + assert_false (calls_ringer_get_is_ringing (data->ringer)); +} + + +/* t3: test_ringing_hang_up_call_ringer_cancelled */ +static gboolean +t3_on_ringer_timeout (gpointer user_data) +{ + TestData *data = user_data; + static guint test_phase = 0; + + assert_false (calls_ringer_get_is_ringing (data->ringer)); + if (test_phase == 0) { + calls_call_hang_up (CALLS_CALL (data->call_one)); + test_phase++; + return G_SOURCE_CONTINUE; + } + + g_main_loop_quit ((GMainLoop *) data->loop); + + return G_SOURCE_REMOVE; +} + + +/** this test should use cancellable code path by hanging up before + * event triggering completes + */ +static void +test_ringing_hang_up_call_ringer_cancelled (void **state) +{ + TestData *data = *state; + + assert_false (calls_ringer_get_is_ringing (data->ringer)); + + /* delay before completion of __wrap_lfb_event_trigger_feedback_async() */ + will_return (__wrap_lfb_event_trigger_feedback_async, 50); + + calls_mock_call_set_state (data->call_one, CALLS_CALL_STATE_INCOMING); + add_call (data->manager, data->call_one); + + g_timeout_add (10, G_SOURCE_FUNC (t3_on_ringer_timeout), data); + + /* main loop will quit in t3_on_ringer_timeout() */ + g_main_loop_run (data->loop); + + remove_call (data->manager, data->call_one); + assert_false (calls_ringer_get_is_ringing (data->ringer)); +} + +/* t4: test_ringing_silence_call */ +static void +t4_on_ringer_call_silence (CallsRinger *ringer, + GParamSpec *pspec, + gpointer user_data) +{ + static guint test_phase = 0; + TestData *data = user_data; + + switch (test_phase++) { + case 0: /* incoming call */ + assert_true (calls_ringer_get_is_ringing (ringer)); + calls_call_silence_ring (CALLS_CALL (data->call_one)); + assert_true (calls_call_get_silenced (CALLS_CALL (data->call_one))); + break; + case 1: /* incoming call hung up */ + assert_false (calls_ringer_get_is_ringing (ringer)); + g_main_loop_quit ((GMainLoop *) data->loop); + break; + default: + g_assert_not_reached (); /* did not find equivalent cmocka assertion */ + } +} + + +static void +test_ringing_silence_call (void **state) +{ + TestData *data = *state; + + assert_false (calls_ringer_get_is_ringing (data->ringer)); + + g_signal_connect (data->ringer, + "notify::ringing", + G_CALLBACK (t4_on_ringer_call_silence), + data); + + /* delay before completion of __wrap_lfb_event_trigger_feedback_async() */ + will_return (__wrap_lfb_event_trigger_feedback_async, 10); + /* delay before completion of __wrap_lfb_event_end_feedback_async() */ + will_return (__wrap_lfb_event_end_feedback_async, 10); + + calls_mock_call_set_state (data->call_one, CALLS_CALL_STATE_INCOMING); + add_call (data->manager, data->call_one); + + /* main loop will quit in callback of notify::ring */ + g_main_loop_run (data->loop); + + remove_call (data->manager, data->call_one); + assert_false (calls_ringer_get_is_ringing (data->ringer)); +} + + +/* t5: test_ringing_multiple_call */ +static gboolean +t5_remove_calls (gpointer user_data) +{ + static guint test_phase = 0; + TestData *data = user_data; + + if (test_phase == 0) { + remove_call (data->manager, data->call_one); + test_phase++; + return G_SOURCE_CONTINUE; + } + + assert_true (calls_ringer_get_is_ringing (data->ringer)); + remove_call (data->manager, data->call_two); + + return G_SOURCE_REMOVE; +} + + +static void +t5_on_ringer_multiple_calls (CallsRinger *ringer, + GParamSpec *pspec, + gpointer user_data) +{ + static guint test_phase = 0; + TestData *data = user_data; + + switch (test_phase++) { + case 0: /* add second call, and schedule call removal */ + assert_true (calls_ringer_get_is_ringing (ringer)); + add_call (data->manager, data->call_two); + g_timeout_add (25, t5_remove_calls, data); + break; + case 1: /* both calls should be removed now */ + assert_false (calls_ringer_get_is_ringing (ringer)); + g_main_loop_quit ((GMainLoop *) data->loop); + break; + default: + g_assert_not_reached (); /* did not find equivalent cmocka assertion */ + } +} + + +static void +test_ringing_multiple_calls (void **state) +{ + TestData *data = *state; + + assert_false (calls_ringer_get_is_ringing (data->ringer)); + + g_signal_connect (data->ringer, + "notify::ringing", + G_CALLBACK (t5_on_ringer_multiple_calls), + data); + + /* delay before completion of __wrap_lfb_event_trigger_feedback_async() */ + will_return (__wrap_lfb_event_trigger_feedback_async, 10); + /* delay before completion of __wrap_lfb_event_end_feedback_async() */ + will_return (__wrap_lfb_event_end_feedback_async, 10); + + calls_mock_call_set_state (data->call_one, CALLS_CALL_STATE_INCOMING); + add_call (data->manager, data->call_one); + + /* main loop will quit in callback of notify::ring */ + g_main_loop_run (data->loop); + + assert_false (calls_ringer_get_is_ringing (data->ringer)); +} + + +int +main (int argc, + char *argv[]) +{ + const struct CMUnitTest tests[] = { + cmocka_unit_test_setup_teardown (test_ringing_accept_call, + setup_test_data, + tear_down_test_data), + cmocka_unit_test_setup_teardown (test_ringing_hang_up_call, + setup_test_data, + tear_down_test_data), + cmocka_unit_test_setup_teardown (test_ringing_hang_up_call_ringer_cancelled, + setup_test_data, + tear_down_test_data), + cmocka_unit_test_setup_teardown (test_ringing_silence_call, + setup_test_data, + tear_down_test_data), + cmocka_unit_test_setup_teardown (test_ringing_multiple_calls, + setup_test_data, + tear_down_test_data), + }; + + return cmocka_run_group_tests (tests, NULL, NULL); +} +