From dad2d49b696c66ccf868bc89b35a6529f9e15777 Mon Sep 17 00:00:00 2001 From: Dan Williams Date: Tue, 29 Apr 2025 18:52:13 -0500 Subject: tests: test call DTMF functionality Signed-off-by: Dan Williams --- src/meson.build | 68 ++++-- src/mm-test-utils.h | 52 +++++ src/tests/fake-call.c | 409 +++++++++++++++++++++++++++++++++++ src/tests/fake-call.h | 77 +++++++ src/tests/fake-modem.c | 463 ++++++++++++++++++++++++++++++++++++++++ src/tests/fake-modem.h | 57 +++++ src/tests/meson.build | 19 +- src/tests/test-base-call.c | 519 +++++++++++++++++++++++++++++++++++++++++++++ 8 files changed, 1651 insertions(+), 13 deletions(-) create mode 100644 src/mm-test-utils.h create mode 100644 src/tests/fake-call.c create mode 100644 src/tests/fake-call.h create mode 100644 src/tests/fake-modem.c create mode 100644 src/tests/fake-modem.h create mode 100644 src/tests/test-base-call.c diff --git a/src/meson.build b/src/meson.build index 6b727a1d..04d199d1 100644 --- a/src/meson.build +++ b/src/meson.build @@ -315,18 +315,11 @@ daemon_enums_types_dep = declare_dependency( include_directories: '.', ) -# Additional vendor plugins -subdir('plugins') - -# ModemManager daemon -sources = files( - 'main.c', - 'mm-auth-provider.c', +base_sources = files( 'mm-base-bearer.c', 'mm-base-call.c', 'mm-call-at.c', 'mm-base-cbm.c', - 'mm-base-manager.c', 'mm-base-modem-at.c', 'mm-base-modem.c', 'mm-base-sim.c', @@ -341,7 +334,6 @@ sources = files( 'mm-dispatcher-connection.c', 'mm-dispatcher-fcc-unlock.c', 'mm-dispatcher-modem-setup.c', - 'mm-filter.c', 'mm-iface-modem-3gpp.c', 'mm-iface-modem-3gpp-profile-manager.c', 'mm-iface-modem-3gpp-ussd.c', @@ -359,15 +351,68 @@ sources = files( 'mm-iface-modem-voice.c', 'mm-iface-op-lock.c', 'mm-log-helpers.c', + 'mm-private-boxed-types.c', + 'mm-sleep-context.c', +) + +# MM base library (used by MM and tests) +incs = [ + top_inc, + kerneldevice_inc, +] + +deps = [ + libmm_glib_dep, + libhelpers_dep, + libauth_dep, + libport_dep, + libqcdm_dep, + daemon_enums_types_dep, +] + +private_deps = [] + +c_args = [ + '-DMM_COMPILATION', + '-DPLUGINDIR="@0@"'.format(mm_prefix / mm_pkglibdir), + '-DMODEMSETUPDIRPACKAGE="@0@"'.format(mm_prefix / mm_pkglibdir / 'modem-setup.d'), + '-DMODEMSETUPDIRUSER="@0@"'.format(mm_prefix / mm_pkgsysconfdir / 'modem-setup.d'), + '-DFCCUNLOCKDIRPACKAGE="@0@"'.format(mm_prefix / mm_pkglibdir / 'fcc-unlock.d'), + '-DFCCUNLOCKDIRUSER="@0@"'.format(mm_prefix / mm_pkgsysconfdir / 'fcc-unlock.d'), + '-DCONNECTIONDIRPACKAGE="@0@"'.format(mm_prefix / mm_pkglibdir / 'connection.d'), + '-DCONNECTIONDIRUSER="@0@"'.format(mm_prefix / mm_pkgsysconfdir / 'connection.d'), +] + +libmmbase = static_library( + 'mmbase', + sources: base_sources + daemon_enums_sources, + include_directories: incs, + dependencies: deps, + c_args: c_args, +) + +libmmbase_dep = declare_dependency( + include_directories: ['.', kerneldevice_inc], + dependencies: deps, + link_with: libmmbase, +) + +# Additional vendor plugins +subdir('plugins') + +# ModemManager daemon +sources = files( + 'main.c', + 'mm-base-manager.c', + 'mm-filter.c', 'mm-plugin.c', 'mm-plugin-manager.c', 'mm-port-probe.c', 'mm-port-probe-at.c', - 'mm-private-boxed-types.c', - 'mm-sleep-context.c', ) sources += daemon_enums_sources +sources += base_sources deps = [ gmodule_dep, @@ -383,7 +428,6 @@ endif c_args = [ '-DMM_COMPILATION', - '-DPLUGINDIR="@0@"'.format(mm_prefix / mm_pkglibdir), '-DMODEMSETUPDIRPACKAGE="@0@"'.format(mm_prefix / mm_pkglibdir / 'modem-setup.d'), '-DMODEMSETUPDIRUSER="@0@"'.format(mm_prefix / mm_pkgsysconfdir / 'modem-setup.d'), '-DFCCUNLOCKDIRPACKAGE="@0@"'.format(mm_prefix / mm_pkglibdir / 'fcc-unlock.d'), diff --git a/src/mm-test-utils.h b/src/mm-test-utils.h new file mode 100644 index 00000000..f484ff1e --- /dev/null +++ b/src/mm-test-utils.h @@ -0,0 +1,52 @@ +/* -*- 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 + */ + +#ifndef MM_TEST_UTILS_H +#define MM_TEST_UTILS_H + +#include +#include + +/*****************************************************************************/ + +/* cmp = TRUE (s1 contains s2) or FALSE (s1 does not contain s2) */ +#define mm_assert_strstr(s1, cmp, s2) \ + G_STMT_START { \ + const char *__s1 = (s1), *__s2 = (s2); \ + if (strstr (__s1, __s2) == cmp) ; else \ + g_assertion_message_cmpstr (G_LOG_DOMAIN, __FILE__, __LINE__, G_STRFUNC, \ + #s1 " %s " #s2, \ + __s1, \ + cmp ? "contains" : "does not contain", \ + __s2); \ + } G_STMT_END + +/* Asserts that err's message contains s1 */ +#define mm_assert_error_str(err, s1) \ + G_STMT_START { \ + if (!err || !err->message || !strstr (err->message, s1)) { \ + GString *gstring; \ + gstring = g_string_new ("assertion failed "); \ + if (err) \ + g_string_append_printf (gstring, "%s (%s, %d) did not contain '%s'", \ + err->message, g_quark_to_string (err->domain), err->code, s1); \ + else \ + g_string_append_printf (gstring, "%s is NULL", #err); \ + g_assertion_message (G_LOG_DOMAIN, __FILE__, __LINE__, G_STRFUNC, gstring->str); \ + g_string_free (gstring, TRUE); \ + } \ + } G_STMT_END + +#endif /* MM_TEST_UTILS_H */ diff --git a/src/tests/fake-call.c b/src/tests/fake-call.c new file mode 100644 index 00000000..dd77e094 --- /dev/null +++ b/src/tests/fake-call.c @@ -0,0 +1,409 @@ +/* -*- 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 + */ + +#include +#include +#include +#include +#include + +#define _LIBMM_INSIDE_MM +#include + +#include "fake-call.h" + +G_DEFINE_TYPE (MMFakeCall, mm_fake_call, MM_TYPE_BASE_CALL) + +/*****************************************************************************/ +/* Start the CALL */ + +static gboolean +call_start_finish (MMBaseCall *self, + GAsyncResult *res, + GError **error) +{ + return g_task_propagate_boolean (G_TASK (res), error); +} + +static gboolean +call_start_ready (GTask *task) +{ + MMFakeCall *self; + + self = g_task_get_source_object (task); + + self->priv->idle_id = 0; + + if (self->priv->start_error_msg) { + g_task_return_new_error (task, + MM_CORE_ERROR, + MM_CORE_ERROR_FAILED, + "%s", + self->priv->start_error_msg); + } else + g_task_return_boolean (task, TRUE); + + g_object_unref (task); + return G_SOURCE_REMOVE; +} + +static void +call_start (MMBaseCall *_self, + GCancellable *cancellable, + GAsyncReadyCallback callback, + gpointer user_data) +{ + MMFakeCall *self = MM_FAKE_CALL (_self); + GTask *task; + + g_assert_cmpint (self->priv->idle_id, ==, 0); + + task = g_task_new (self, NULL, callback, user_data); + self->priv->idle_id = g_idle_add ((GSourceFunc) call_start_ready, task); +} + +/*****************************************************************************/ +/* Accept the call */ + +static gboolean +call_accept_finish (MMBaseCall *self, + GAsyncResult *res, + GError **error) +{ + return g_task_propagate_boolean (G_TASK (res), error); +} + +static gboolean +call_accept_ready (GTask *task) +{ + MMFakeCall *self; + + self = g_task_get_source_object (task); + + self->priv->idle_id = 0; + + if (self->priv->accept_error_msg) { + g_task_return_new_error (task, + MM_CORE_ERROR, + MM_CORE_ERROR_FAILED, + "%s", + self->priv->accept_error_msg); + } else + g_task_return_boolean (task, TRUE); + + g_object_unref (task); + return G_SOURCE_REMOVE; +} + +static void +call_accept (MMBaseCall *_self, + GAsyncReadyCallback callback, + gpointer user_data) +{ + MMFakeCall *self = MM_FAKE_CALL (_self); + GTask *task; + + g_assert_cmpint (self->priv->idle_id, ==, 0); + + task = g_task_new (self, NULL, callback, user_data); + self->priv->idle_id = g_idle_add ((GSourceFunc) call_accept_ready, task); +} + +/*****************************************************************************/ +/* Deflect the call */ + +static gboolean +call_deflect_finish (MMBaseCall *self, + GAsyncResult *res, + GError **error) +{ + return g_task_propagate_boolean (G_TASK (res), error); +} + +static gboolean +call_deflect_ready (GTask *task) +{ + MMFakeCall *self; + + self = g_task_get_source_object (task); + + self->priv->idle_id = 0; + + if (self->priv->deflect_error_msg) { + g_task_return_new_error (task, + MM_CORE_ERROR, + MM_CORE_ERROR_FAILED, + "%s", + self->priv->deflect_error_msg); + } else + g_task_return_boolean (task, TRUE); + + g_object_unref (task); + return G_SOURCE_REMOVE; +} + +static void +call_deflect (MMBaseCall *_self, + const gchar *number, + GAsyncReadyCallback callback, + gpointer user_data) +{ + MMFakeCall *self = MM_FAKE_CALL (_self); + GTask *task; + + g_assert_cmpint (self->priv->idle_id, ==, 0); + + task = g_task_new (self, NULL, callback, user_data); + self->priv->idle_id = g_idle_add ((GSourceFunc) call_deflect_ready, task); +} + +/*****************************************************************************/ +/* Hangup the call */ + +static gboolean +call_hangup_finish (MMBaseCall *self, + GAsyncResult *res, + GError **error) +{ + return g_task_propagate_boolean (G_TASK (res), error); +} + +static gboolean +call_hangup_ready (GTask *task) +{ + MMFakeCall *self; + + self = g_task_get_source_object (task); + + self->priv->idle_id = 0; + + if (self->priv->hangup_error_msg) { + g_task_return_new_error (task, + MM_CORE_ERROR, + MM_CORE_ERROR_FAILED, + "%s", + self->priv->hangup_error_msg); + } else + g_task_return_boolean (task, TRUE); + + g_object_unref (task); + return G_SOURCE_REMOVE; +} + +static void +call_hangup (MMBaseCall *_self, + GAsyncReadyCallback callback, + gpointer user_data) +{ + MMFakeCall *self = MM_FAKE_CALL (_self); + GTask *task; + + g_assert_cmpint (self->priv->idle_id, ==, 0); + + task = g_task_new (self, NULL, callback, user_data); + self->priv->idle_id = g_idle_add ((GSourceFunc) call_hangup_ready, task); +} + +/*****************************************************************************/ +/* Send DTMF tone to call */ + +static gssize +call_send_dtmf_finish (MMBaseCall *self, + GAsyncResult *res, + GError **error) +{ + return g_task_propagate_int (G_TASK (res), error); +} + +static gboolean +call_send_dtmf_ready (GTask *task) +{ + MMFakeCall *self; + + self = g_task_get_source_object (task); + + self->priv->idle_id = 0; + + if (self->priv->dtmf_error_msg) { + g_task_return_new_error (task, + MM_CORE_ERROR, + MM_CORE_ERROR_FAILED, + "%s", + self->priv->dtmf_error_msg); + } else + g_task_return_int (task, self->priv->dtmf_num_accepted); + + self->priv->dtmf_in_send = FALSE; + g_object_unref (task); + return G_SOURCE_REMOVE; +} + +static void +call_send_dtmf (MMBaseCall *_self, + const gchar *dtmf, + GAsyncReadyCallback callback, + gpointer user_data) +{ + MMFakeCall *self = MM_FAKE_CALL (_self); + GTask *task; + + g_assert_cmpint (self->priv->idle_id, ==, 0); + + task = g_task_new (self, NULL, callback, user_data); + + if (!self->priv->dtmf_sent) + self->priv->dtmf_sent = g_string_new (""); + self->priv->dtmf_num_accepted = MIN (self->priv->dtmf_accept_len, strlen (dtmf)); + g_string_append_len (self->priv->dtmf_sent, dtmf, self->priv->dtmf_num_accepted); + + self->priv->idle_id = g_idle_add ((GSourceFunc) call_send_dtmf_ready, task); + self->priv->dtmf_in_send = TRUE; +} + +/*****************************************************************************/ +/* Stop DTMF tone */ + +static gboolean +call_stop_dtmf_finish (MMBaseCall *self, + GAsyncResult *res, + GError **error) +{ + return g_task_propagate_boolean (G_TASK (res), error); +} + +static gboolean +call_stop_dtmf_ready (GTask *task) +{ + MMFakeCall *self; + + self = g_task_get_source_object (task); + + self->priv->idle_id = 0; + self->priv->dtmf_stop_called = TRUE; + + if (self->priv->dtmf_stop_error_msg) { + g_task_return_new_error (task, + MM_CORE_ERROR, + MM_CORE_ERROR_FAILED, + "%s", + self->priv->dtmf_stop_error_msg); + } else + g_task_return_boolean (task, TRUE); + + g_object_unref (task); + return G_SOURCE_REMOVE; +} + +static void +call_stop_dtmf (MMBaseCall *_self, + GAsyncReadyCallback callback, + gpointer user_data) +{ + MMFakeCall *self = MM_FAKE_CALL (_self); + GTask *task; + + g_assert_cmpint (self->priv->idle_id, ==, 0); + g_assert_false (self->priv->dtmf_in_send); + + task = g_task_new (self, NULL, callback, user_data); + + self->priv->idle_id = g_idle_add ((GSourceFunc) call_stop_dtmf_ready, task); +} + +void +mm_fake_call_enable_dtmf_stop (MMFakeCall *self, + gboolean enable) +{ + MMBaseCallClass *base_call_class = MM_BASE_CALL_GET_CLASS (self); + + base_call_class->stop_dtmf = enable ? call_stop_dtmf : NULL; + base_call_class->stop_dtmf_finish = enable ? call_stop_dtmf_finish : NULL; +} + +/*****************************************************************************/ + +MMFakeCall * +mm_fake_call_new (GDBusConnection *connection, + MMIfaceModemVoice *voice, + MMCallDirection direction, + const gchar *number, + const guint dtmf_tone_duration) +{ + MMFakeCall *call; + + call = MM_FAKE_CALL (g_object_new (MM_TYPE_FAKE_CALL, + MM_BASE_CALL_CONNECTION, connection, + MM_BASE_CALL_IFACE_MODEM_VOICE, voice, + MM_CALL_DIRECTION, direction, + MM_CALL_NUMBER, number, + MM_CALL_DTMF_TONE_DURATION, dtmf_tone_duration, + MM_BASE_CALL_SKIP_INCOMING_TIMEOUT, TRUE, + MM_BASE_CALL_SUPPORTS_DIALING_TO_RINGING, TRUE, + MM_BASE_CALL_SUPPORTS_RINGING_TO_ACTIVE, TRUE, + NULL)); + return call; +} + +static void +mm_fake_call_init (MMFakeCall *self) +{ + self->priv = G_TYPE_INSTANCE_GET_PRIVATE (self, MM_TYPE_FAKE_CALL, MMFakeCallPrivate); +} + +static void +dispose (GObject *object) +{ + MMFakeCall *self = MM_FAKE_CALL (object); + + if (self->priv->idle_id) + g_source_remove (self->priv->idle_id); + self->priv->idle_id = 0; + + if (self->priv->dtmf_sent) { + g_string_free (self->priv->dtmf_sent, TRUE); + self->priv->dtmf_sent = NULL; + } + + G_OBJECT_CLASS (mm_fake_call_parent_class)->dispose (object); +} + +static void +finalize (GObject *object) +{ + G_OBJECT_CLASS (mm_fake_call_parent_class)->finalize (object); +} + +static void +mm_fake_call_class_init (MMFakeCallClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + MMBaseCallClass *base_call_class = MM_BASE_CALL_CLASS (klass); + + g_type_class_add_private (object_class, sizeof (MMFakeCallPrivate)); + + object_class->dispose = dispose; + object_class->finalize = finalize; + + base_call_class->start = call_start; + base_call_class->start_finish = call_start_finish; + base_call_class->accept = call_accept; + base_call_class->accept_finish = call_accept_finish; + base_call_class->deflect = call_deflect; + base_call_class->deflect_finish = call_deflect_finish; + base_call_class->hangup = call_hangup; + base_call_class->hangup_finish = call_hangup_finish; + base_call_class->send_dtmf = call_send_dtmf; + base_call_class->send_dtmf_finish = call_send_dtmf_finish; +} diff --git a/src/tests/fake-call.h b/src/tests/fake-call.h new file mode 100644 index 00000000..1b035333 --- /dev/null +++ b/src/tests/fake-call.h @@ -0,0 +1,77 @@ +/* -*- 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 + */ + +#ifndef MM_FAKE_CALL_H +#define MM_FAKE_CALL_H + +#include +#include + +#include "mm-base-call.h" +#include "mm-iface-modem-voice.h" + +#define MM_TYPE_FAKE_CALL (mm_fake_call_get_type ()) +#define MM_FAKE_CALL(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), MM_TYPE_FAKE_CALL, MMFakeCall)) +#define MM_FAKE_CALL_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST ((klass), MM_TYPE_FAKE_CALL, MMFakeCallClass)) +#define MM_IS_FAKE_CALL(obj) (G_TYPE_CHECK_INSTANCE_TYPE ((obj), MM_TYPE_FAKE_CALL)) +#define MM_IS_FAKE_CALL_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass), MM_TYPE_FAKE_CALL)) +#define MM_FAKE_CALL_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS ((obj), MM_TYPE_FAKE_CALL, MMFakeCallClass)) + +typedef struct _MMFakeCall MMFakeCall; +typedef struct _MMFakeCallClass MMFakeCallClass; +typedef struct _MMFakeCallPrivate MMFakeCallPrivate; + +struct _MMFakeCallPrivate { + const gchar *start_error_msg; + const gchar *accept_error_msg; + const gchar *deflect_error_msg; + const gchar *hangup_error_msg; + + /* DTMF */ + const gchar *dtmf_error_msg; + const gchar *dtmf_stop_error_msg; + /* How many DTMF characters we can accept at a time */ + guint dtmf_accept_len; + /* How many DTMF characters were actually accepted */ + guint dtmf_num_accepted; + GString *dtmf_sent; + gboolean dtmf_in_send; + gboolean dtmf_stop_called; + + guint idle_id; +}; + +struct _MMFakeCall { + MMBaseCall parent; + MMFakeCallPrivate *priv; +}; + +struct _MMFakeCallClass { + MMBaseCallClass parent; +}; + +GType mm_fake_call_get_type (void); +G_DEFINE_AUTOPTR_CLEANUP_FUNC (MMFakeCall, g_object_unref) + +MMFakeCall *mm_fake_call_new (GDBusConnection *connection, + MMIfaceModemVoice *voice, + MMCallDirection direction, + const gchar *number, + const guint dtmf_tone_duration); + +void mm_fake_call_enable_dtmf_stop (MMFakeCall *self, + gboolean enable); + +#endif /* MM_FAKE_CALL_H */ diff --git a/src/tests/fake-modem.c b/src/tests/fake-modem.c new file mode 100644 index 00000000..0d1100bc --- /dev/null +++ b/src/tests/fake-modem.c @@ -0,0 +1,463 @@ +/* -*- 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 + */ + +#include +#include +#include +#include +#include + +#define _LIBMM_INSIDE_MM +#include + +#include "fake-modem.h" +#include "mm-iface-modem.h" +#include "mm-iface-modem-voice.h" +#include "mm-call-list.h" +#include "fake-call.h" +#include "mm-bind.h" + +#define MM_FAKE_MODEM_PATH "fake-modem-path" + +static void iface_modem_init (MMIfaceModemInterface *iface); +static void iface_modem_voice_init (MMIfaceModemVoiceInterface *iface); + +G_DEFINE_TYPE_EXTENDED (MMFakeModem, mm_fake_modem, MM_TYPE_BASE_MODEM, 0, + G_IMPLEMENT_INTERFACE (MM_TYPE_IFACE_MODEM, iface_modem_init) + G_IMPLEMENT_INTERFACE (MM_TYPE_IFACE_MODEM_VOICE, iface_modem_voice_init)) + +enum { + PROP_0, + PROP_CONNECTION, + PROP_PATH, + PROP_MODEM_DBUS_SKELETON, + PROP_MODEM_STATE, + PROP_MODEM_SIM, + PROP_MODEM_SIM_SLOTS, + PROP_MODEM_SIM_HOT_SWAP_SUPPORTED, + PROP_MODEM_PERIODIC_SIGNAL_CHECK_DISABLED, + PROP_MODEM_PERIODIC_ACCESS_TECH_CHECK_DISABLED, + PROP_MODEM_PERIODIC_CALL_LIST_CHECK_DISABLED, + PROP_MODEM_CARRIER_CONFIG_MAPPING, + PROP_MODEM_BEARER_LIST, + PROP_MODEM_INDICATION_CALL_LIST_RELOAD_ENABLED, + PROP_MODEM_VOICE_DBUS_SKELETON, + PROP_MODEM_VOICE_CALL_LIST, + PROP_LAST +}; + +static GParamSpec *properties[PROP_LAST]; + +struct _MMFakeModemPrivate { + GDBusConnection *connection; + guint dbus_id; + gchar *path; + + GObject *modem_dbus_skeleton; + MMModemState modem_state; + GObject *modem_sim; + GPtrArray *modem_sim_slots; + gboolean sim_hot_swap_supported; + gboolean periodic_signal_check_disabled; + gboolean periodic_access_tech_check_disabled; + gboolean periodic_call_list_check_disabled; + gchar *carrier_config_mapping; + MMBearerList *modem_bearer_list; + gboolean indication_call_list_reload_enabled; + GObject *modem_voice_dbus_skeleton; + MMCallList *modem_voice_call_list; +}; + +/*****************************************************************************/ + +const gchar * +mm_fake_modem_get_path (MMFakeModem *self) +{ + return self->priv->path; +} + +MMCallList * +mm_fake_modem_get_call_list (MMFakeModem *self) +{ + return self->priv->modem_voice_call_list; +} + +gboolean +mm_fake_modem_export_interfaces (MMFakeModem *self, GError **error) +{ + g_assert (self->priv->path); + g_assert (self->priv->connection); + + if (self->priv->modem_dbus_skeleton) { + if (!g_dbus_interface_skeleton_export (G_DBUS_INTERFACE_SKELETON (self->priv->modem_dbus_skeleton), + self->priv->connection, + self->priv->path, + error)) + return FALSE; + } + + if (self->priv->modem_voice_dbus_skeleton) { + if (!g_dbus_interface_skeleton_export (G_DBUS_INTERFACE_SKELETON (self->priv->modem_voice_dbus_skeleton), + self->priv->connection, + self->priv->path, + error)) + return FALSE; + } + + return TRUE; +} + +/*****************************************************************************/ + +static gboolean +modem_voice_check_support_finish (MMIfaceModemVoice *self, + GAsyncResult *res, + GError **error) +{ + gboolean foobar; + + foobar = g_task_propagate_boolean (G_TASK (res), error); + return foobar; +} + +static void +modem_voice_check_support (MMIfaceModemVoice *self, + GAsyncReadyCallback callback, + gpointer user_data) +{ + GTask *task; + + task = g_task_new (self, NULL, callback, user_data); + g_task_return_boolean (task, TRUE); + g_object_unref (task); +} + +/*****************************************************************************/ + +static MMBaseCall * +modem_voice_create_call (MMIfaceModemVoice *_self, + MMCallDirection direction, + const gchar *number, + const guint dtmf_tone_duration) +{ + MMFakeModem *self = MM_FAKE_MODEM (_self); + + return MM_BASE_CALL (mm_fake_call_new (self->priv->connection, + _self, + direction, + number, + dtmf_tone_duration)); +} + +/*****************************************************************************/ + +MMFakeModem * +mm_fake_modem_new (GDBusConnection *connection) +{ + return MM_FAKE_MODEM (g_object_new (MM_TYPE_FAKE_MODEM, + MM_BINDABLE_CONNECTION, connection, + NULL)); +} + +static void +set_property (GObject *object, + guint prop_id, + const GValue *value, + GParamSpec *pspec) +{ + MMFakeModem *self = MM_FAKE_MODEM (object); + + switch (prop_id) { + case PROP_PATH: + g_free (self->priv->path); + self->priv->path = g_value_dup_string (value); + break; + case PROP_CONNECTION: + g_clear_object (&self->priv->connection); + self->priv->connection = g_value_dup_object (value); + break; + case PROP_MODEM_DBUS_SKELETON: + g_clear_object (&self->priv->modem_dbus_skeleton); + self->priv->modem_dbus_skeleton = g_value_dup_object (value); + break; + case PROP_MODEM_STATE: + self->priv->modem_state = g_value_get_enum (value); + break; + case PROP_MODEM_SIM: + g_clear_object (&self->priv->modem_sim); + self->priv->modem_sim = g_value_dup_object (value); + break; + case PROP_MODEM_SIM_SLOTS: + g_clear_pointer (&self->priv->modem_sim_slots, g_ptr_array_unref); + self->priv->modem_sim_slots = g_value_dup_boxed (value); + break; + case PROP_MODEM_SIM_HOT_SWAP_SUPPORTED: + self->priv->sim_hot_swap_supported = g_value_get_boolean (value); + break; + case PROP_MODEM_PERIODIC_SIGNAL_CHECK_DISABLED: + self->priv->periodic_signal_check_disabled = g_value_get_boolean (value); + break; + case PROP_MODEM_PERIODIC_ACCESS_TECH_CHECK_DISABLED: + self->priv->periodic_access_tech_check_disabled = g_value_get_boolean (value); + break; + case PROP_MODEM_PERIODIC_CALL_LIST_CHECK_DISABLED: + self->priv->periodic_call_list_check_disabled = g_value_get_boolean (value); + break; + case PROP_MODEM_CARRIER_CONFIG_MAPPING: + self->priv->carrier_config_mapping = g_value_dup_string (value); + break; + case PROP_MODEM_BEARER_LIST: + g_clear_object (&self->priv->modem_bearer_list); + self->priv->modem_bearer_list = g_value_dup_object (value); + break; + case PROP_MODEM_INDICATION_CALL_LIST_RELOAD_ENABLED: + self->priv->indication_call_list_reload_enabled = g_value_get_boolean (value); + break; + case PROP_MODEM_VOICE_DBUS_SKELETON: + g_clear_object (&self->priv->modem_voice_dbus_skeleton); + self->priv->modem_voice_dbus_skeleton = g_value_dup_object (value); + break; + case PROP_MODEM_VOICE_CALL_LIST: + g_clear_object (&self->priv->modem_voice_call_list); + self->priv->modem_voice_call_list = g_value_dup_object (value); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } +} + +static void +get_property (GObject *object, + guint prop_id, + GValue *value, + GParamSpec *pspec) +{ + MMFakeModem *self = MM_FAKE_MODEM (object); + + switch (prop_id) { + case PROP_PATH: + g_value_set_string (value, self->priv->path); + break; + case PROP_CONNECTION: + g_value_set_object (value, self->priv->connection); + break; + case PROP_MODEM_DBUS_SKELETON: + g_value_set_object (value, self->priv->modem_dbus_skeleton); + break; + case PROP_MODEM_STATE: + g_value_set_enum (value, self->priv->modem_state); + break; + case PROP_MODEM_SIM: + g_value_set_object (value, self->priv->modem_sim); + break; + case PROP_MODEM_SIM_SLOTS: + g_value_set_boxed (value, self->priv->modem_sim_slots); + break; + case PROP_MODEM_SIM_HOT_SWAP_SUPPORTED: + g_value_set_boolean (value, self->priv->sim_hot_swap_supported); + break; + case PROP_MODEM_PERIODIC_SIGNAL_CHECK_DISABLED: + g_value_set_boolean (value, self->priv->periodic_signal_check_disabled); + break; + case PROP_MODEM_PERIODIC_ACCESS_TECH_CHECK_DISABLED: + g_value_set_boolean (value, self->priv->periodic_access_tech_check_disabled); + break; + case PROP_MODEM_PERIODIC_CALL_LIST_CHECK_DISABLED: + g_value_set_boolean (value, self->priv->periodic_call_list_check_disabled); + break; + case PROP_MODEM_CARRIER_CONFIG_MAPPING: + g_value_set_string (value, self->priv->carrier_config_mapping); + break; + case PROP_MODEM_BEARER_LIST: + g_value_set_object (value, self->priv->modem_bearer_list); + break; + case PROP_MODEM_INDICATION_CALL_LIST_RELOAD_ENABLED: + g_value_set_boolean (value, self->priv->indication_call_list_reload_enabled); + break; + case PROP_MODEM_VOICE_DBUS_SKELETON: + g_value_set_object (value, self->priv->modem_voice_dbus_skeleton); + break; + case PROP_MODEM_VOICE_CALL_LIST: + g_value_set_object (value, self->priv->modem_voice_call_list); + break; + default: + G_OBJECT_WARN_INVALID_PROPERTY_ID (object, prop_id, pspec); + break; + } +} + +static void +mm_fake_modem_init (MMFakeModem *self) +{ + static guint id = 0; + + /* Initialize private data */ + self->priv = G_TYPE_INSTANCE_GET_PRIVATE (self, + MM_TYPE_FAKE_MODEM, + MMFakeModemPrivate); + + /* Each modem is given a unique id to build its own DBus path */ + self->priv->dbus_id = id++; + self->priv->path = g_strdup_printf (MM_DBUS_MODEM_PREFIX "/%d", self->priv->dbus_id); +} + +static void +finalize (GObject *object) +{ + MMFakeModem *self = MM_FAKE_MODEM (object); + + g_free (self->priv->path); + g_free (self->priv->carrier_config_mapping); + + G_OBJECT_CLASS (mm_fake_modem_parent_class)->finalize (object); +} + +static void +dispose (GObject *object) +{ + MMFakeModem *self = MM_FAKE_MODEM (object); + + if (self->priv->modem_dbus_skeleton) { + mm_iface_modem_shutdown (MM_IFACE_MODEM (object)); + g_clear_object (&self->priv->modem_dbus_skeleton); + } + if (self->priv->modem_voice_dbus_skeleton) { + mm_iface_modem_voice_shutdown (MM_IFACE_MODEM_VOICE (object)); + g_clear_object (&self->priv->modem_voice_dbus_skeleton); + } + g_clear_object (&self->priv->modem_sim); + g_clear_pointer (&self->priv->modem_sim_slots, g_ptr_array_unref); + g_clear_object (&self->priv->modem_bearer_list); + g_clear_object (&self->priv->modem_voice_call_list); + g_clear_object (&self->priv->connection); + + G_OBJECT_CLASS (mm_fake_modem_parent_class)->dispose (object); +} + +static void +iface_modem_init (MMIfaceModemInterface *iface) +{ +} + +static void +iface_modem_voice_init (MMIfaceModemVoiceInterface *iface) +{ + iface->check_support = modem_voice_check_support; + iface->check_support_finish = modem_voice_check_support_finish; + iface->create_call = modem_voice_create_call; + +#if 0 + iface->load_call_list = modem_voice_load_call_list; + iface->load_call_list_finish = modem_voice_load_call_list_finish; + iface->hold_and_accept = modem_voice_hold_and_accept; + iface->hold_and_accept_finish = modem_voice_hold_and_accept_finish; + iface->hangup_and_accept = modem_voice_hangup_and_accept; + iface->hangup_and_accept_finish = modem_voice_hangup_and_accept_finish; + iface->hangup_all = modem_voice_hangup_all; + iface->hangup_all_finish = modem_voice_hangup_all_finish; + iface->join_multiparty = modem_voice_join_multiparty; + iface->join_multiparty_finish = modem_voice_join_multiparty_finish; + iface->leave_multiparty = modem_voice_leave_multiparty; + iface->leave_multiparty_finish = modem_voice_leave_multiparty_finish; + iface->transfer = modem_voice_transfer; + iface->transfer_finish = modem_voice_transfer_finish; + iface->call_waiting_setup = modem_voice_call_waiting_setup; + iface->call_waiting_setup_finish = modem_voice_call_waiting_setup_finish; + iface->call_waiting_query = modem_voice_call_waiting_query; + iface->call_waiting_query_finish = modem_voice_call_waiting_query_finish; +#endif +} + +static void +mm_fake_modem_class_init (MMFakeModemClass *klass) +{ + GObjectClass *object_class = G_OBJECT_CLASS (klass); + + g_type_class_add_private (object_class, sizeof (MMFakeModemPrivate)); + + object_class->set_property = set_property; + object_class->get_property = get_property; + object_class->finalize = finalize; + object_class->dispose = dispose; + + properties[PROP_CONNECTION] = + g_param_spec_object (MM_BINDABLE_CONNECTION, + "Connection", + "GDBus connection to the system bus.", + G_TYPE_DBUS_CONNECTION, + G_PARAM_READWRITE); + g_object_class_install_property (object_class, PROP_CONNECTION, properties[PROP_CONNECTION]); + + properties[PROP_PATH] = + g_param_spec_string (MM_FAKE_MODEM_PATH, + "Path", + "DBus path of the call", + NULL, + G_PARAM_READWRITE); + g_object_class_install_property (object_class, PROP_PATH, properties[PROP_PATH]); + + g_object_class_override_property (object_class, + PROP_MODEM_DBUS_SKELETON, + MM_IFACE_MODEM_DBUS_SKELETON); + + g_object_class_override_property (object_class, + PROP_MODEM_STATE, + MM_IFACE_MODEM_STATE); + + g_object_class_override_property (object_class, + PROP_MODEM_SIM, + MM_IFACE_MODEM_SIM); + + g_object_class_override_property (object_class, + PROP_MODEM_SIM_SLOTS, + MM_IFACE_MODEM_SIM_SLOTS); + + g_object_class_override_property (object_class, + PROP_MODEM_SIM_HOT_SWAP_SUPPORTED, + MM_IFACE_MODEM_SIM_HOT_SWAP_SUPPORTED); + + g_object_class_override_property (object_class, + PROP_MODEM_PERIODIC_SIGNAL_CHECK_DISABLED, + MM_IFACE_MODEM_PERIODIC_SIGNAL_CHECK_DISABLED); + + g_object_class_override_property (object_class, + PROP_MODEM_PERIODIC_ACCESS_TECH_CHECK_DISABLED, + MM_IFACE_MODEM_PERIODIC_ACCESS_TECH_CHECK_DISABLED); + + g_object_class_override_property (object_class, + PROP_MODEM_PERIODIC_CALL_LIST_CHECK_DISABLED, + MM_IFACE_MODEM_VOICE_PERIODIC_CALL_LIST_CHECK_DISABLED); + + g_object_class_override_property (object_class, + PROP_MODEM_CARRIER_CONFIG_MAPPING, + MM_IFACE_MODEM_CARRIER_CONFIG_MAPPING); + + g_object_class_override_property (object_class, + PROP_MODEM_BEARER_LIST, + MM_IFACE_MODEM_BEARER_LIST); + + g_object_class_override_property (object_class, + PROP_MODEM_INDICATION_CALL_LIST_RELOAD_ENABLED, + MM_IFACE_MODEM_VOICE_INDICATION_CALL_LIST_RELOAD_ENABLED); + + g_object_class_override_property (object_class, + PROP_MODEM_VOICE_DBUS_SKELETON, + MM_IFACE_MODEM_VOICE_DBUS_SKELETON); + + g_object_class_override_property (object_class, + PROP_MODEM_VOICE_CALL_LIST, + MM_IFACE_MODEM_VOICE_CALL_LIST); +} diff --git a/src/tests/fake-modem.h b/src/tests/fake-modem.h new file mode 100644 index 00000000..df1378c3 --- /dev/null +++ b/src/tests/fake-modem.h @@ -0,0 +1,57 @@ +/* -*- 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 + */ + +#ifndef MM_FAKE_MODEM_H +#define MM_FAKE_MODEM_H + +#include +#include + +#include "mm-base-modem.h" +#include "mm-call-list.h" + +#define MM_TYPE_FAKE_MODEM (mm_fake_modem_get_type ()) +#define MM_FAKE_MODEM(obj) (G_TYPE_CHECK_INSTANCE_CAST ((obj), MM_TYPE_FAKE_MODEM, MMFakeModem)) +#define MM_FAKE_MODEM_CLASS(klass) (G_TYPE_CHECK_CLASS_CAST ((klass), MM_TYPE_FAKE_MODEM, MMFakeModemClass)) +#define MM_IS_FAKE_MODEM(obj) (G_TYPE_CHECK_INSTANCE_TYPE ((obj), MM_TYPE_FAKE_MODEM)) +#define MM_IS_FAKE_MODEM_CLASS(klass) (G_TYPE_CHECK_CLASS_TYPE ((klass), MM_TYPE_FAKE_MODEM)) +#define MM_FAKE_MODEM_GET_CLASS(obj) (G_TYPE_INSTANCE_GET_CLASS ((obj), MM_TYPE_FAKE_MODEM, MMFakeModemClass)) + +typedef struct _MMFakeModem MMFakeModem; +typedef struct _MMFakeModemClass MMFakeModemClass; +typedef struct _MMFakeModemPrivate MMFakeModemPrivate; + +struct _MMFakeModem { + MMBaseModem parent; + MMFakeModemPrivate *priv; +}; + +struct _MMFakeModemClass { + MMBaseModemClass parent; +}; + +GType mm_fake_modem_get_type (void); +G_DEFINE_AUTOPTR_CLEANUP_FUNC (MMFakeModem, g_object_unref) + +MMFakeModem *mm_fake_modem_new (GDBusConnection *connection); + +const gchar *mm_fake_modem_get_path (MMFakeModem *self); + +gboolean mm_fake_modem_export_interfaces (MMFakeModem *self, + GError **error); + +MMCallList *mm_fake_modem_get_call_list (MMFakeModem *self); + +#endif /* MM_FAKE_MODEM_H */ diff --git a/src/tests/meson.build b/src/tests/meson.build index 5c6764b9..a9216c24 100644 --- a/src/tests/meson.build +++ b/src/tests/meson.build @@ -31,6 +31,11 @@ if enable_mbim test_units += {'modem-helpers-mbim': libkerneldevice_dep} endif +c_args = [ + '-DTEST_SERVICES="@0@"'.format(build_root / 'data/tests'), + '-DTESTUDEVRULESDIR="@0@"'.format(src_dir), +] + foreach test_unit, test_deps: test_units test_name = 'test-' + test_unit @@ -39,12 +44,24 @@ foreach test_unit, test_deps: test_units sources: test_name + '.c', include_directories: top_inc, dependencies: test_deps, - c_args: '-DTESTUDEVRULESDIR="@0@"'.format(src_dir) + c_args: c_args, ) test(test_name, exe) endforeach +# base call test +exe = executable( + 'test-base-call', + sources: [ 'test-base-call.c', 'fake-modem.c', 'fake-call.c' ], + include_directories: top_inc, + dependencies: libmmbase_dep, + c_args: c_args, +) + +test('test-base-call', exe) + + if get_option('fuzzer') fuzzer_tests = ['test-sms-part-3gpp-fuzzer', 'test-sms-part-3gpp-tr-fuzzer', diff --git a/src/tests/test-base-call.c b/src/tests/test-base-call.c new file mode 100644 index 00000000..c7987d61 --- /dev/null +++ b/src/tests/test-base-call.c @@ -0,0 +1,519 @@ +/* -*- 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 + */ + +#include +#include +#include +#include +#include + +#define _LIBMM_INSIDE_MM +#include + +#include "mm-base-call.h" +#include "mm-context.h" +#include "mm-call-list.h" +#include "mm-iface-modem-voice.h" +#include "fake-modem.h" +#include "fake-call.h" +#include "mm-log.h" +#include "mm-test-utils.h" + +/****************************************************************/ +/* Make the linker happy */ + +#if defined WITH_QMI + +typedef struct MMBroadbandModemQmi MMBroadbandModemQmi; +GType mm_broadband_modem_qmi_get_type (void); +MMPortQmi *mm_broadband_modem_qmi_peek_port_qmi (MMBroadbandModemQmi *self); + +GType +mm_broadband_modem_qmi_get_type (void) +{ + return G_TYPE_INVALID; +} + +MMPortQmi * +mm_broadband_modem_qmi_peek_port_qmi (MMBroadbandModemQmi *self) +{ + return NULL; +} + +#endif /* WITH_QMI */ + +#if defined WITH_MBIM + +typedef struct MMBroadbandModemMbim MMBroadbandModemMbim; +GType mm_broadband_modem_mbim_get_type (void); +MMPortMbim *mm_broadband_modem_mbim_peek_port_mbim (MMBroadbandModemMbim *self); + +GType +mm_broadband_modem_mbim_get_type (void) +{ + return G_TYPE_INVALID; +} + +MMPortMbim * +mm_broadband_modem_mbim_peek_port_mbim (MMBroadbandModemMbim *self) +{ + return NULL; +} + +#endif /* WITH_MBIM */ + +/****************************************************************/ + +typedef struct { + const gchar *desc; + + const gchar *start_error_msg; + const gchar *accept_error_msg; + const gchar *deflect_error_msg; + const gchar *hangup_error_msg; + + const gchar *number; + + /* DTMF */ + const gchar *dtmf_error_msg; + const gchar *dtmf_stop_error_msg; + const guint dtmf_accept_len; /* how many chars modem can accept at a time */ + const guint dtmf_tone_duration; + const gchar *dtmf; + const guint dtmf_min_duration; +} Testcase; + +typedef struct { + GTestDBus *dbus; + GDBusConnection *connection; + GMainLoop *loop; + guint name_id; + + MmGdbusModemVoice *voice_proxy; + MMFakeModem *modem; + + MmGdbusCall *call_proxy; + MMFakeCall *call; + + GError *error; + + const Testcase *tc; +} TestFixture; + +/****************************************************************/ + +static MMFakeCall * +get_call (TestFixture *tf) +{ + MMCallList *list; + MMFakeCall *call; + const gchar *call_path; + + list = mm_fake_modem_get_call_list (tf->modem); + g_assert (list); + + g_assert (tf->call_proxy); + call_path = g_dbus_proxy_get_object_path (G_DBUS_PROXY (tf->call_proxy)); + call = (MMFakeCall *) mm_call_list_get_call (list, call_path); + g_assert (call); + return call; +} + +/****************************************************************/ + +static void +dtmf_send_ready (MmGdbusCall *call, + GAsyncResult *res, + TestFixture *tf) +{ + if (!mm_gdbus_call_call_send_dtmf_finish (call, res, &tf->error)) + g_assert_true (tf->error); + g_main_loop_quit (tf->loop); +} + +static void +dtmf_call_start_ready (MmGdbusCall *call, + GAsyncResult *res, + TestFixture *tf) +{ + gboolean success; + g_autoptr(GError) error = NULL; + const MMCallInfo cinfo = { + .index = 1, + .number = (gchar *) tf->tc->number, + .direction = MM_CALL_DIRECTION_OUTGOING, + .state = MM_CALL_STATE_ACTIVE, + }; + + success = mm_gdbus_call_call_start_finish (call, res, &error); + g_assert_no_error (error); + g_assert_true (success); + + /* Set the call active */ + mm_iface_modem_voice_report_call (MM_IFACE_MODEM_VOICE (tf->modem), &cinfo); + g_main_loop_quit (tf->loop); +} + +static void +dtmf_proxy_ready (gpointer unused, + GAsyncResult *res, + TestFixture *tf) +{ + g_autoptr(GError) error = NULL; + g_autoptr(MMCallList) list = NULL; + + tf->call_proxy = mm_gdbus_call_proxy_new_for_bus_finish (res, &error); + g_assert_no_error (error); + g_assert (tf->call_proxy); + g_main_loop_quit (tf->loop); +} + +static void +dtmf_create_call_ready (MMIfaceModemVoice *self, + GAsyncResult *res, + TestFixture *tf) +{ + g_autoptr(GError) error = NULL; + g_autofree gchar *call_path = NULL; + gboolean success; + + success = mm_gdbus_modem_voice_call_create_call_finish (MM_GDBUS_MODEM_VOICE (self), + &call_path, + res, + &error); + g_assert_true (success); + g_assert_no_error (error); + + /* Create our call proxy */ + mm_gdbus_call_proxy_new_for_bus (G_BUS_TYPE_SESSION, + G_DBUS_PROXY_FLAGS_NONE, + "org.freedesktop.ModemManager1", + call_path, + NULL, + (GAsyncReadyCallback) dtmf_proxy_ready, + tf); +} + +static void +test_dtmf (TestFixture *tf, const Testcase *tc, gboolean test_stop) +{ + g_autoptr(GError) error = NULL; + g_autoptr(GVariant) dictionary = NULL; + g_autoptr(MMCallProperties) call_props = NULL; + MMFakeCall *call; + guint dtmf_len_no_pause = 0; + const gchar *p; + gint64 call_start_time; + + tf->tc = tc; + + call_props = mm_call_properties_new (); + mm_call_properties_set_number (call_props, tf->tc->number); + mm_call_properties_set_dtmf_tone_duration (call_props, tf->tc->dtmf_tone_duration); + dictionary = mm_call_properties_get_dictionary (call_props); + + /* Create the voice call we'll use for DTMF */ + mm_gdbus_modem_voice_call_create_call (tf->voice_proxy, + dictionary, + NULL, + (GAsyncReadyCallback)dtmf_create_call_ready, + tf); + g_main_loop_run (tf->loop); + + /* start the voice call */ + mm_gdbus_call_call_start (tf->call_proxy, + NULL, + (GAsyncReadyCallback) dtmf_call_start_ready, + tf); + g_main_loop_run (tf->loop); + + /* Get the call and copy expectations into it */ + call = get_call (tf); + call->priv->dtmf_accept_len = tf->tc->dtmf_accept_len; + call->priv->start_error_msg = tf->tc->start_error_msg; + call->priv->accept_error_msg = tf->tc->accept_error_msg; + call->priv->deflect_error_msg = tf->tc->deflect_error_msg; + call->priv->hangup_error_msg = tf->tc->hangup_error_msg; + call->priv->dtmf_error_msg = tf->tc->dtmf_error_msg; + call->priv->dtmf_stop_error_msg = tf->tc->dtmf_stop_error_msg; + + mm_fake_call_enable_dtmf_stop (call, test_stop); + +g_message ("####### about to run %s", tf->tc->desc); + /* Run the test */ + call_start_time = g_get_real_time (); + mm_gdbus_call_call_send_dtmf (tf->call_proxy, + tf->tc->dtmf, + NULL, + (GAsyncReadyCallback) dtmf_send_ready, + tf); + g_main_loop_run (tf->loop); + + /* Validate results */ + if (tf->tc->start_error_msg) + mm_assert_error_str (tf->error, tf->tc->start_error_msg); + else if (tf->tc->accept_error_msg) + mm_assert_error_str (tf->error, tf->tc->accept_error_msg); + else if (tf->tc->deflect_error_msg) + mm_assert_error_str (tf->error, tf->tc->deflect_error_msg); + else if (tf->tc->hangup_error_msg) + mm_assert_error_str (tf->error, tf->tc->hangup_error_msg); + else if (tf->tc->dtmf_error_msg) + mm_assert_error_str (tf->error, tf->tc->dtmf_error_msg); + else if (test_stop && tf->tc->dtmf_stop_error_msg) + mm_assert_error_str (tf->error, tf->tc->dtmf_stop_error_msg); + else { + p = tf->tc->dtmf; + while (*p++) { + if (*p != MM_CALL_DTMF_PAUSE_CHAR) + dtmf_len_no_pause++; + } + g_assert_cmpint (strlen (call->priv->dtmf_sent->str), ==, dtmf_len_no_pause); + + if (tf->tc->dtmf_min_duration) { + g_assert_cmpint (call_start_time + (tf->tc->dtmf_min_duration * G_USEC_PER_SEC), <=, g_get_real_time ()); + } + } + if (test_stop && !tf->tc->dtmf_error_msg) + g_assert_true (call->priv->dtmf_stop_called); +} + +static void +test_dtmf_nostop (TestFixture *tf, gconstpointer user_data) +{ + test_dtmf (tf, (const Testcase *) user_data, FALSE); +} + +static void +test_dtmf_need_stop (TestFixture *tf, gconstpointer user_data) +{ + test_dtmf (tf, (const Testcase *) user_data, TRUE); +} + +/************************************************************/ + +static void +on_bus_acquired (GDBusConnection *connection, + const gchar *name, + TestFixture *tf) +{ + tf->connection = connection; +} + +static void +name_acquired_cb (GDBusConnection *connection, + const gchar *name, + TestFixture *tf) +{ + g_main_loop_quit (tf->loop); +} + +static void +voice_init_ready (MMIfaceModemVoice *_self, + GAsyncResult *result, + TestFixture *tf) +{ + g_autoptr(GError) error = NULL; + gboolean success; + + success = mm_iface_modem_voice_initialize_finish (_self, result, &error); + g_assert_no_error (error); + g_assert (success); + g_main_loop_quit (tf->loop); +} + +static void +voice_proxy_ready (gpointer unused, + GAsyncResult *res, + TestFixture *tf) +{ + g_autoptr(GError) error = NULL; + + tf->voice_proxy = mm_gdbus_modem_voice_proxy_new_for_bus_finish (res, &error); + g_assert_no_error (error); + g_assert (tf->voice_proxy); + g_main_loop_quit (tf->loop); +} + +static void +test_fixture_setup (TestFixture *tf, gconstpointer unused) +{ + g_autoptr(GError) error = NULL; + gboolean success; + + success = mm_log_setup (mm_context_get_log_level (), + mm_context_get_log_file (), + mm_context_get_log_journal (), + mm_context_get_log_timestamps (), + mm_context_get_log_relative_timestamps (), + mm_context_get_log_personal_info (), + &error); + g_assert_no_error (error); + g_assert (success); + + tf->loop = g_main_loop_new (NULL, FALSE); + + /* Create the global dbus-daemon for this test suite */ + tf->dbus = g_test_dbus_new (G_TEST_DBUS_NONE); + g_assert (tf->dbus); + + /* Add the private directory with our in-tree service files, + * TEST_SERVICES is defined by the build system to point + * to the right directory. */ + g_test_dbus_add_service_dir (tf->dbus, TEST_SERVICES); + + /* Start the private DBus daemon */ + g_test_dbus_up (tf->dbus); + + /* Acquire name, don't allow replacement */ + tf->name_id = g_bus_own_name (G_BUS_TYPE_SESSION, + MM_DBUS_SERVICE, + G_BUS_NAME_OWNER_FLAGS_NONE, + (GBusAcquiredCallback) on_bus_acquired, + (GBusNameAcquiredCallback) name_acquired_cb, + NULL, + tf, + NULL); + /* Wait for name acquired */ + g_main_loop_run (tf->loop); + + /* Create and export the server-side modem voice interface */ + g_assert (tf->connection); + tf->modem = mm_fake_modem_new (tf->connection); + mm_iface_modem_voice_initialize (MM_IFACE_MODEM_VOICE (tf->modem), + NULL, + (GAsyncReadyCallback) voice_init_ready, + tf); + g_main_loop_run (tf->loop); + + if (!mm_fake_modem_export_interfaces (tf->modem, &error)) + g_assert_no_error (error); + + /* Create client-side modem proxy */ + mm_gdbus_modem_voice_proxy_new_for_bus (G_BUS_TYPE_SESSION, + G_DBUS_PROXY_FLAGS_NONE, + "org.freedesktop.ModemManager1", + mm_fake_modem_get_path (tf->modem), + NULL, + (GAsyncReadyCallback) voice_proxy_ready, + tf); + g_main_loop_run (tf->loop); +} + +static void +test_fixture_cleanup (TestFixture *tf, gconstpointer unused) +{ + g_bus_unown_name (tf->name_id); + mm_iface_modem_voice_shutdown (MM_IFACE_MODEM_VOICE (tf->modem)); + + g_clear_error (&tf->error); + + g_clear_object (&tf->call_proxy); + g_clear_object (&tf->call); + g_clear_object (&tf->voice_proxy); + /* Run dispose to break a ref cycle in case there's still a FakeCall hanging around */ + g_object_run_dispose (G_OBJECT (tf->modem)); + g_clear_object (&tf->modem); + g_dbus_connection_close_sync (tf->connection, NULL, NULL); + g_test_dbus_stop (tf->dbus); + g_test_dbus_down (tf->dbus); + g_clear_object (&tf->dbus); + g_main_loop_unref (tf->loop); + mm_log_shutdown (); +} + +/****************************************************************/ + +static const Testcase tests[] = { + { + .desc = "/MM/Call/DTMF/send-one-accept-len", + .number = "911", + .dtmf_accept_len = 1, + .dtmf_tone_duration = 300, + .dtmf = "987654321", + }, + { + .desc = "/MM/Call/DTMF/send-single-tone", + .number = "911", + .dtmf_accept_len = 3, + .dtmf_tone_duration = 300, + .dtmf = "9", + }, + { + .desc = "/MM/Call/DTMF/send-multi-tone", + .number = "911", + .dtmf_accept_len = 3, + .dtmf_tone_duration = 300, + .dtmf = "123", + }, + { + .desc = "/MM/Call/DTMF/send-pause", + .number = "911", + .dtmf_accept_len = 10, + .dtmf_tone_duration = 300, + .dtmf = "123,,4", + .dtmf_min_duration = 4, + }, + /* Error testing */ + { + .desc = "/MM/Call/DTMF/send-error", + .number = "911", + .dtmf_accept_len = 1, + .dtmf = "123", + .dtmf_error_msg = "send failure", + }, + { + .desc = "/MM/Call/DTMF/stop-error", + .number = "911", + .dtmf_accept_len = 1, + .dtmf = "123", + .dtmf_stop_error_msg = "stop failure", + }, +}; + + + +#define TCASE(n, d, f) \ + g_test_add (n, \ + TestFixture, \ + d, \ + test_fixture_setup, \ + f, \ + test_fixture_cleanup); \ + +#define TCASE_DTMF_STOP(n, d, f) \ + { \ + g_autofree gchar *desc = g_strdup_printf ("%s-stop", n); \ + TCASE(desc, d, f); \ + } + +int main (int argc, char **argv) +{ + const gchar *test_args[] = { argv[0], "--test-session" }; + guint i; + + setlocale (LC_ALL, ""); + + g_test_init (&argc, &argv, NULL); + mm_context_init (G_N_ELEMENTS (test_args), (gchar **) test_args); + + for (i = 0; i < G_N_ELEMENTS (tests); i++) { + TCASE(tests[i].desc, &tests[i], test_dtmf_nostop); + /* Test everything again for paired start/stop (eg QMI) */ + TCASE_DTMF_STOP(tests[i].desc, &tests[i], test_dtmf_need_stop); + } + + return g_test_run (); +} -- cgit v1.2.3-70-g09d2