From mboxrd@z Thu Jan 1 00:00:00 1970 Received: from bali.collaboradmins.com (bali.collaboradmins.com [148.251.105.195]) (using TLSv1.2 with cipher ECDHE-RSA-AES256-GCM-SHA384 (256/256 bits)) (No client certificate requested) by smtp.subspace.kernel.org (Postfix) with ESMTPS id DAF4B40BCA0 for ; Fri, 21 Aug 2026 19:35:26 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=148.251.105.195 ARC-Seal:i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1787340929; cv=none; b=CIeIXYNw3yWHQ5DImus2gOLzvnDKALHoW1Vlwy80TWw8w5wBkJtWJn1kKO1TmOB706P8vOs5KGylLVIaplcA3L9n+COe4Q8DZK0vGWwgD/qFzkbI+uHud3/YgGjKlFNkI4IvYqLL6aXmdkVoE7PRr/Y3EIA4hCK7S+G9meQxs5U= ARC-Message-Signature:i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1787340929; c=relaxed/simple; bh=TcKcokyrWxXOS94HWwXn7jjxx0kjjR47OqYNflg824Q=; h=From:To:Cc:Subject:Date:Message-ID:In-Reply-To:References: MIME-Version; b=EY9v5FMaMmUFEt1zTZBYqfIz+F+g8y5OfPD96wueAJfPBjIp71ddvJDZjzCO7PiHyEgplnbHYVivOZ6h4yX4462RTeqIgeBVDhy1cQP7v2Ox2oUJzvHDoIbG9cgI64rpGHzYF3fnxxEUDj/pZcNXKKm/u+EJphWsbXO/LDWY56A= ARC-Authentication-Results:i=1; smtp.subspace.kernel.org; dmarc=pass (p=none dis=none) header.from=collabora.com; spf=pass smtp.mailfrom=collabora.com; dkim=pass (2048-bit key) header.d=collabora.com header.i=@collabora.com header.b=eNd5aXAW; arc=none smtp.client-ip=148.251.105.195 Authentication-Results: smtp.subspace.kernel.org; dmarc=pass (p=none dis=none) header.from=collabora.com Authentication-Results: smtp.subspace.kernel.org; spf=pass smtp.mailfrom=collabora.com Authentication-Results: smtp.subspace.kernel.org; dkim=pass (2048-bit key) header.d=collabora.com header.i=@collabora.com header.b="eNd5aXAW" DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/simple; d=collabora.com; s=mail; t=1787340923; bh=TcKcokyrWxXOS94HWwXn7jjxx0kjjR47OqYNflg824Q=; h=From:To:Cc:Subject:Date:In-Reply-To:References:From; b=eNd5aXAWPDkTDhIfzy7GuDLIQerbJ/t25IuUOn5M7gfhnbknHQfVPh3DLK69Vg6wR kOvw77Q1rdJwu87JsOMqirm7XU/D3VkvPt6aTeKGJuF1FqTa12yR5hoKitsR5MXN+r NsFRI7rkMdvgHpcVFSGvlz4rcgYtJYeaWYzVFCzxgXFBgcx+aa1CnN5k7CV9xKI0Jo m2IHLpt7z1tsWjO82R4VpnjCa5IaBzDECfF2sPhptg6USDWghJVbFH1wY1VF7UBBeE 4D9pxW7DZW4GC3f8nCqRrMP7Cg0+m4vxs2ELFU4B7LX5PPJxaHryQDcPt99owIqglW 5e7InpCn0uybA== Received: from vninja (unknown [100.64.1.54]) (using TLSv1.3 with cipher TLS_AES_256_GCM_SHA384 (256/256 bits) key-exchange x25519 server-signature RSA-PSS (4096 bits) server-digest SHA256) (No client certificate requested) (Authenticated sender: gkiagia) by bali.collaboradmins.com (Postfix) with ESMTPSA id 7F78717E0D69; Fri, 21 Aug 2026 21:35:23 +0200 (CEST) From: George Kiagiadakis To: linux-bluetooth@vger.kernel.org Cc: George Kiagiadakis Subject: [PATCH BlueZ 5/5] unit/test-media-player: Add media player tests Date: Fri, 21 Aug 2026 22:34:49 +0300 Message-ID: <20260821193449.1336263-6-george.kiagiadakis@collabora.com> X-Mailer: git-send-email 2.53.0 In-Reply-To: <20260821193449.1336263-1-george.kiagiadakis@collabora.com> References: <20260821193449.1336263-1-george.kiagiadakis@collabora.com> Precedence: bulk X-Mailing-List: linux-bluetooth@vger.kernel.org List-Id: List-Subscribe: List-Unsubscribe: MIME-Version: 1.0 Content-Transfer-Encoding: 8bit Cover the D-Bus surface of profiles/audio/player.c that the AVRCP controller drives, using a private session bus. Nine tests: play_item_without_scope Play() on a /NowPlaying item of a player that never set a scope play_item_with_scope the browsable case still works play_item_busy a second overlapping Play() is refused play_item_busy_with_list_items Play() during a pending ListItems() is refused, pinning the mutual exclusion between the two list_items_scope_change a pending ListItems() is still answered when the scope moves meanwhile no_folder_without_scope MediaFolder1 is not registered without a scope, which is why MediaItem1 was the only entry point able to observe an unset one play_item_destroy_pending destroying a player answers whatever request is still in flight total_items_scope_change the count reported by the player is applied when the scope moves search_busy a busy Search() reports EBUSY Against the tree before this series play_item_without_scope, play_item_busy and play_item_destroy_pending crash, all three because they play a /NowPlaying item on a player with no scope, list_items_scope_change times out with NoReply, total_items_scope_change reads a stale count and search_busy reports the wrong error. The remaining three pass there as well and guard against regressions. Assisted-by: Claude:claude-opus-5 valgrind --- .gitignore | 1 + Makefile.am | 13 + unit/test-media-player.c | 838 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 852 insertions(+) create mode 100644 unit/test-media-player.c diff --git a/.gitignore b/.gitignore index c5efe8536..8485f3f46 100644 --- a/.gitignore +++ b/.gitignore @@ -105,6 +105,7 @@ unit/test-uuid unit/test-crc unit/test-textfile unit/test-gdbus-client +unit/test-media-player unit/test-sdp unit/test-lib unit/test-mgmt diff --git a/Makefile.am b/Makefile.am index 2754e1b7f..ab7457252 100644 --- a/Makefile.am +++ b/Makefile.am @@ -687,6 +687,19 @@ unit_test_gdbus_client_SOURCES = unit/test-gdbus-client.c unit_test_gdbus_client_LDADD = gdbus/libgdbus-internal.la \ src/libshared-glib.la $(GLIB_LIBS) $(DBUS_LIBS) +unit_tests += unit/test-media-player + +unit_test_media_player_SOURCES = unit/test-media-player.c \ + profiles/audio/player.h \ + profiles/audio/player.c \ + src/log.h src/log.c \ + src/error.h src/error.c \ + src/dbus-common.h src/dbus-common.c +unit_test_media_player_LDADD = gdbus/libgdbus-internal.la \ + src/libshared-glib.la \ + lib/libbluetooth-internal.la \ + $(GLIB_LIBS) $(DBUS_LIBS) + if OBEX unit_tests += unit/test-gobex-header unit/test-gobex-packet unit/test-gobex \ unit/test-gobex-transfer unit/test-gobex-apparam diff --git a/unit/test-media-player.c b/unit/test-media-player.c new file mode 100644 index 000000000..ef2d5dff3 --- /dev/null +++ b/unit/test-media-player.c @@ -0,0 +1,838 @@ +// SPDX-License-Identifier: GPL-2.0-or-later +/* + * + * BlueZ - Bluetooth protocol stack for Linux + * + * Copyright (C) 2026 Collabora Ltd. + * + */ + +#ifdef HAVE_CONFIG_H +#include +#endif + +#include +#include +#include +#include +#include + +#include +#include + +#include "gdbus/gdbus.h" + +#include "src/shared/tester.h" +#include "src/dbus-common.h" +#include "profiles/audio/player.h" + +#define ERROR_INTERFACE "org.bluez.Error" + +#define SERVICE_NAME "org.bluez.unit.test-media-player" +#define DEVICE_PATH "/org/bluez/unit/dev_00_00_00_00_00_00" +#define PLAYER_PATH DEVICE_PATH "/avrcp/player0" +#define NOWPLAYING_PATH PLAYER_PATH "/NowPlaying/item2" +#define FILESYSTEM_PATH PLAYER_PATH "/Filesystem/item2" + +#define ITEM_UID 2 +#define TOTAL_ITEMS 42 + +struct context { + DBusConnection *dbus_conn; + struct media_player *mp; + DBusPendingCall *pending; + DBusPendingCall *pending2; + bool defer_completion; + unsigned int play_item_calls; + unsigned int list_items_calls; + unsigned int expected_play_calls; + char *play_item_name; + uint64_t play_item_uid; + const char *expected_name; + bool change_scope_on_list; + bool destroy_on_play; + bool check_total_items; + unsigned int total_items_calls; +}; + +static struct context *context; + +static gboolean complete_play_item(gpointer user_data) +{ + if (context == NULL) + return FALSE; + + /* + * Completion has to be deferred until media_item_play() has + * returned, since the pending message is only stored once the + * play_item callback has succeeded. + */ + media_player_play_item_complete(context->mp, 0); + + return FALSE; +} + +static gboolean destroy_player(gpointer user_data) +{ + if (context == NULL || context->mp == NULL) + return FALSE; + + /* + * Destroyed from the main loop rather than from the callback, so + * that the pending message has already been stored. + */ + media_player_destroy(context->mp); + context->mp = NULL; + + return FALSE; +} + +static int test_play_item(struct media_player *mp, const char *name, + uint64_t uid, void *user_data) +{ + struct context *ctx = user_data; + + ctx->play_item_calls++; + g_free(ctx->play_item_name); + ctx->play_item_name = g_strdup(name); + ctx->play_item_uid = uid; + + if (ctx->destroy_on_play) + g_idle_add(destroy_player, NULL); + else if (!ctx->defer_completion) + g_idle_add(complete_play_item, NULL); + + return 0; +} + +static gboolean complete_list_items(gpointer user_data) +{ + if (context == NULL) + return FALSE; + + /* + * A browsed player can be re-addressed at any time, which makes + * avrcp call media_player_set_folder() and move the scope while + * the ListItems request is still pending. + */ + if (context->change_scope_on_list) + media_player_set_folder(context->mp, "/Other", 1); + + media_player_list_complete(context->mp, NULL, 0); + + return FALSE; +} + +static int test_list_items(struct media_player *mp, const char *name, + uint32_t start, uint32_t end, void *user_data) +{ + struct context *ctx = user_data; + + ctx->list_items_calls++; + + if (!ctx->defer_completion) + g_idle_add(complete_list_items, NULL); + + return 0; +} + +static void call_get_number_of_items(DBusPendingCall **pending, + DBusPendingCallNotifyFunction notify); +static void number_of_items_reply(DBusPendingCall *call, void *user_data); + +static gboolean complete_total_items(gpointer user_data) +{ + if (context == NULL) + return FALSE; + + media_player_total_items_complete(context->mp, TOTAL_ITEMS); + + call_get_number_of_items(&context->pending2, number_of_items_reply); + + return FALSE; +} + +static int test_search(struct media_player *mp, const char *string, + void *user_data) +{ + return 0; +} + +static int test_total_items(struct media_player *mp, const char *name, + void *user_data) +{ + struct context *ctx = user_data; + + ctx->total_items_calls++; + + if (ctx->check_total_items) + g_idle_add(complete_total_items, NULL); + + return 0; +} + +static const struct media_player_callback test_callbacks = { + .play_item = test_play_item, + .list_items = test_list_items, + .total_items = test_total_items, + .search = test_search, +}; + +static struct context *create_context(void) +{ + struct context *ctx = g_new0(struct context, 1); + DBusError err; + + dbus_error_init(&err); + + ctx->dbus_conn = g_dbus_setup_private(DBUS_BUS_SESSION, SERVICE_NAME, + &err); + if (ctx->dbus_conn == NULL) { + if (dbus_error_is_set(&err)) { + tester_debug("D-Bus setup failed: %s", err.message); + dbus_error_free(&err); + } + + g_free(ctx); + tester_test_abort(); + return NULL; + } + + /* Avoid D-Bus library calling _exit() before next test finishes. */ + dbus_connection_set_exit_on_disconnect(ctx->dbus_conn, FALSE); + + g_dbus_attach_object_manager(ctx->dbus_conn); + + set_dbus_connection(ctx->dbus_conn); + + ctx->mp = media_player_controller_create(DEVICE_PATH, "avrcp", 0); + g_assert(ctx->mp != NULL); + + media_player_set_callbacks(ctx->mp, &test_callbacks, ctx); + + return ctx; +} + +static void destroy_context(void) +{ + if (context == NULL) + return; + + if (context->pending) { + dbus_pending_call_cancel(context->pending); + dbus_pending_call_unref(context->pending); + } + + if (context->pending2) { + dbus_pending_call_cancel(context->pending2); + dbus_pending_call_unref(context->pending2); + } + + if (context->mp) + media_player_destroy(context->mp); + + set_dbus_connection(NULL); + + g_dbus_detach_object_manager(context->dbus_conn); + + dbus_connection_flush(context->dbus_conn); + dbus_connection_close(context->dbus_conn); + dbus_connection_unref(context->dbus_conn); + + g_free(context->play_item_name); + g_free(context); + context = NULL; +} + +static void play_reply(DBusPendingCall *call, void *user_data) +{ + struct context *ctx = user_data; + DBusMessage *reply = dbus_pending_call_steal_reply(call); + + if (reply == NULL) { + tester_warn("Play() got no reply"); + tester_test_failed(); + return; + } + + if (dbus_message_get_type(reply) == DBUS_MESSAGE_TYPE_ERROR) { + tester_warn("Play() returned error: %s", + dbus_message_get_error_name(reply)); + goto failed; + } + + if (ctx->play_item_calls != 1) { + tester_warn("play_item called %u times, expected 1", + ctx->play_item_calls); + goto failed; + } + + if (g_strcmp0(ctx->play_item_name, ctx->expected_name)) { + tester_warn("play_item name is '%s', expected '%s'", + ctx->play_item_name, ctx->expected_name); + goto failed; + } + + if (ctx->play_item_uid != ITEM_UID) { + tester_warn("play_item uid is %" PRIu64 ", expected %u", + ctx->play_item_uid, ITEM_UID); + goto failed; + } + + dbus_message_unref(reply); + tester_test_passed(); + return; + +failed: + dbus_message_unref(reply); + tester_test_failed(); +} + +/* The first Play() of the busy test is never completed on purpose. */ +static void ignore_reply(DBusPendingCall *call, void *user_data) +{ + DBusMessage *reply = dbus_pending_call_steal_reply(call); + + if (reply) + dbus_message_unref(reply); +} + +/* The second, overlapping Play() is the one under test here. */ +static void busy_reply(DBusPendingCall *call, void *user_data) +{ + struct context *ctx = user_data; + DBusMessage *reply = dbus_pending_call_steal_reply(call); + + if (reply == NULL) { + tester_warn("Play() got no reply"); + tester_test_failed(); + return; + } + + if (dbus_message_get_type(reply) != DBUS_MESSAGE_TYPE_ERROR) { + tester_warn("overlapping Play() succeeded, expected an error"); + goto failed; + } + + if (ctx->play_item_calls != ctx->expected_play_calls) { + tester_warn("play_item called %u times, expected %u", + ctx->play_item_calls, ctx->expected_play_calls); + goto failed; + } + + dbus_message_unref(reply); + tester_test_passed(); + return; + +failed: + dbus_message_unref(reply); + tester_test_failed(); +} + +/* ListItems must always be answered, even if the scope moved meanwhile. */ +static void list_reply(DBusPendingCall *call, void *user_data) +{ + DBusMessage *reply = dbus_pending_call_steal_reply(call); + + if (reply == NULL) { + tester_warn("ListItems() got no reply"); + tester_test_failed(); + return; + } + + if (dbus_message_get_type(reply) == DBUS_MESSAGE_TYPE_ERROR) { + tester_warn("ListItems() returned error: %s", + dbus_message_get_error_name(reply)); + dbus_message_unref(reply); + tester_test_failed(); + return; + } + + dbus_message_unref(reply); + tester_test_passed(); +} + +/* Without a scope org.bluez.MediaFolder1 must not be registered at all. */ +static void no_folder_reply(DBusPendingCall *call, void *user_data) +{ + struct context *ctx = user_data; + DBusMessage *reply = dbus_pending_call_steal_reply(call); + + if (reply == NULL) { + tester_warn("ListItems() got no reply"); + tester_test_failed(); + return; + } + + if (dbus_message_get_type(reply) != DBUS_MESSAGE_TYPE_ERROR) { + tester_warn("ListItems() succeeded without a scope"); + goto failed; + } + + if (ctx->list_items_calls != 0) { + tester_warn("list_items called %u times, expected 0", + ctx->list_items_calls); + goto failed; + } + + dbus_message_unref(reply); + tester_test_passed(); + return; + +failed: + dbus_message_unref(reply); + tester_test_failed(); +} + +/* Destroying the player must answer whatever request is in flight. */ +static void destroyed_reply(DBusPendingCall *call, void *user_data) +{ + DBusMessage *reply = dbus_pending_call_steal_reply(call); + const char *name; + + if (reply == NULL) { + tester_warn("Play() got no reply"); + tester_test_failed(); + return; + } + + if (dbus_message_get_type(reply) != DBUS_MESSAGE_TYPE_ERROR) { + tester_warn("Play() succeeded, expected an error"); + goto failed; + } + + name = dbus_message_get_error_name(reply); + if (g_strcmp0(name, ERROR_INTERFACE ".Failed")) { + tester_warn("Play() failed with '%s', expected '%s'", name, + ERROR_INTERFACE ".Failed"); + goto failed; + } + + dbus_message_unref(reply); + tester_test_passed(); + return; + +failed: + dbus_message_unref(reply); + tester_test_failed(); +} + +/* NumberOfItems has to pick up the count reported by the player. */ +static void number_of_items_reply(DBusPendingCall *call, void *user_data) +{ + DBusMessage *reply = dbus_pending_call_steal_reply(call); + DBusMessageIter iter, var; + dbus_uint32_t items; + + if (reply == NULL) { + tester_warn("Get(NumberOfItems) got no reply"); + tester_test_failed(); + return; + } + + if (dbus_message_get_type(reply) == DBUS_MESSAGE_TYPE_ERROR) { + tester_warn("Get(NumberOfItems) returned error: %s", + dbus_message_get_error_name(reply)); + goto failed; + } + + if (!dbus_message_iter_init(reply, &iter) || + dbus_message_iter_get_arg_type(&iter) != + DBUS_TYPE_VARIANT) { + tester_warn("Get(NumberOfItems) reply is malformed"); + goto failed; + } + + dbus_message_iter_recurse(&iter, &var); + if (dbus_message_iter_get_arg_type(&var) != DBUS_TYPE_UINT32) { + tester_warn("NumberOfItems is not a uint32"); + goto failed; + } + + dbus_message_iter_get_basic(&var, &items); + + if (items != TOTAL_ITEMS) { + tester_warn("NumberOfItems is %u, expected %u", items, + TOTAL_ITEMS); + goto failed; + } + + dbus_message_unref(reply); + tester_test_passed(); + return; + +failed: + dbus_message_unref(reply); + tester_test_failed(); +} + +static void call_get_number_of_items(DBusPendingCall **pending, + DBusPendingCallNotifyFunction notify) +{ + DBusMessage *msg; + const char *iface = "org.bluez.MediaFolder1"; + const char *prop = "NumberOfItems"; + + msg = dbus_message_new_method_call(SERVICE_NAME, PLAYER_PATH, + "org.freedesktop.DBus.Properties", "Get"); + g_assert(msg != NULL); + + g_assert(dbus_message_append_args(msg, DBUS_TYPE_STRING, &iface, + DBUS_TYPE_STRING, &prop, + DBUS_TYPE_INVALID)); + + g_assert(dbus_connection_send_with_reply(context->dbus_conn, msg, + pending, 2000)); + g_assert(*pending != NULL); + + g_assert(dbus_pending_call_set_notify(*pending, notify, context, NULL)); + + dbus_message_unref(msg); +} + +/* A busy Search() has to report the same error as its siblings. */ +static void search_busy_reply(DBusPendingCall *call, void *user_data) +{ + DBusMessage *reply = dbus_pending_call_steal_reply(call); + const char *desc; + + if (reply == NULL) { + tester_warn("Search() got no reply"); + tester_test_failed(); + return; + } + + if (dbus_message_get_type(reply) != DBUS_MESSAGE_TYPE_ERROR) { + tester_warn("Search() succeeded, expected an error"); + goto failed; + } + + if (!dbus_message_get_args(reply, NULL, DBUS_TYPE_STRING, &desc, + DBUS_TYPE_INVALID)) { + tester_warn("Search() error carries no description"); + goto failed; + } + + if (g_strcmp0(desc, strerror(EBUSY))) { + tester_warn("Search() failed with '%s', expected '%s'", desc, + strerror(EBUSY)); + goto failed; + } + + dbus_message_unref(reply); + tester_test_passed(); + return; + +failed: + dbus_message_unref(reply); + tester_test_failed(); +} + +static void call_search(DBusPendingCall **pending, + DBusPendingCallNotifyFunction notify) +{ + DBusMessage *msg; + DBusMessageIter iter, dict; + const char *string = "needle"; + + msg = dbus_message_new_method_call(SERVICE_NAME, PLAYER_PATH, + "org.bluez.MediaFolder1", "Search"); + g_assert(msg != NULL); + + dbus_message_iter_init_append(msg, &iter); + dbus_message_iter_append_basic(&iter, DBUS_TYPE_STRING, &string); + dbus_message_iter_open_container(&iter, DBUS_TYPE_ARRAY, "{sv}", &dict); + dbus_message_iter_close_container(&iter, &dict); + + g_assert(dbus_connection_send_with_reply(context->dbus_conn, msg, + pending, 2000)); + g_assert(*pending != NULL); + + g_assert(dbus_pending_call_set_notify(*pending, notify, context, NULL)); + + dbus_message_unref(msg); +} + +static void call_list_items(DBusPendingCall **pending, + DBusPendingCallNotifyFunction notify) +{ + DBusMessage *msg; + DBusMessageIter iter, dict; + + msg = dbus_message_new_method_call(SERVICE_NAME, PLAYER_PATH, + "org.bluez.MediaFolder1", "ListItems"); + g_assert(msg != NULL); + + dbus_message_iter_init_append(msg, &iter); + dbus_message_iter_open_container(&iter, DBUS_TYPE_ARRAY, "{sv}", &dict); + dbus_message_iter_close_container(&iter, &dict); + + g_assert(dbus_connection_send_with_reply(context->dbus_conn, msg, + pending, 2000)); + g_assert(*pending != NULL); + + g_assert(dbus_pending_call_set_notify(*pending, notify, context, NULL)); + + dbus_message_unref(msg); +} + +static void call_play(const char *path, DBusPendingCall **pending, + DBusPendingCallNotifyFunction notify) +{ + DBusMessage *msg; + + msg = dbus_message_new_method_call(SERVICE_NAME, path, + "org.bluez.MediaItem1", "Play"); + g_assert(msg != NULL); + + g_assert(dbus_connection_send_with_reply(context->dbus_conn, msg, + pending, 2000)); + g_assert(*pending != NULL); + + g_assert(dbus_pending_call_set_notify(*pending, notify, context, NULL)); + + dbus_message_unref(msg); +} + +static struct media_item *create_nowplaying_item(void) +{ + g_assert(media_player_create_folder(context->mp, "/NowPlaying", + PLAYER_FOLDER_TYPE_MIXED, 0) != NULL); + media_player_set_playlist(context->mp, "/NowPlaying"); + + return media_player_set_playlist_item(context->mp, ITEM_UID); +} + +/* + * A player advertising the NowPlaying feature bit but not the Browsing + * feature bit gets a /NowPlaying folder holding playable items, while the + * player scope stays unset because SetBrowsedPlayer is never issued. + * + * Playing such an item must not dereference the unset scope. + */ +static void test_play_item_without_scope(const void *data) +{ + context = create_context(); + if (context == NULL) + return; + + context->expected_name = NOWPLAYING_PATH; + + g_assert(create_nowplaying_item() != NULL); + + call_play(NOWPLAYING_PATH, &context->pending, play_reply); +} + +static struct media_item *create_filesystem_item(void) +{ + struct media_item *item; + + g_assert(media_player_create_folder(context->mp, "/Filesystem", + PLAYER_FOLDER_TYPE_MIXED, 0) != NULL); + media_player_set_folder(context->mp, "/Filesystem", 1); + + item = media_player_create_item(context->mp, "track", + PLAYER_ITEM_TYPE_AUDIO, ITEM_UID); + g_assert(item != NULL); + media_item_set_playable(item, true); + + return item; +} + +/* A browsable player sets a scope, which must keep working. */ +static void test_play_item_with_scope(const void *data) +{ + context = create_context(); + if (context == NULL) + return; + + context->expected_name = FILESYSTEM_PATH; + + g_assert(create_filesystem_item() != NULL); + + call_play(FILESYSTEM_PATH, &context->pending, play_reply); +} + +/* A Play() issued while another one is still pending must be rejected. */ +static void test_play_item_busy(const void *data) +{ + context = create_context(); + if (context == NULL) + return; + + context->expected_name = NOWPLAYING_PATH; + context->defer_completion = true; + context->expected_play_calls = 1; + + g_assert(create_nowplaying_item() != NULL); + + call_play(NOWPLAYING_PATH, &context->pending, ignore_reply); + call_play(NOWPLAYING_PATH, &context->pending2, busy_reply); +} + +/* + * ListItems() and Play() share one pending-request slot per player, so a + * Play() issued while a ListItems() is still outstanding must be rejected. + */ +static void test_play_item_busy_with_list_items(const void *data) +{ + context = create_context(); + if (context == NULL) + return; + + context->defer_completion = true; + context->expected_play_calls = 0; + + g_assert(create_filesystem_item() != NULL); + + call_list_items(&context->pending, ignore_reply); + call_play(FILESYSTEM_PATH, &context->pending2, busy_reply); +} + +/* + * The scope can move while a request is pending, which is what avrcp does + * on SetBrowsedPlayer. The outstanding ListItems() must still be answered. + */ +static void test_list_items_scope_change(const void *data) +{ + context = create_context(); + if (context == NULL) + return; + + context->change_scope_on_list = true; + + g_assert(media_player_create_folder(context->mp, "/Filesystem", + PLAYER_FOLDER_TYPE_MIXED, 0) != NULL); + g_assert(media_player_create_folder(context->mp, "/Other", + PLAYER_FOLDER_TYPE_MIXED, 0) != NULL); + media_player_set_folder(context->mp, "/Filesystem", 1); + + call_list_items(&context->pending, list_reply); +} + +/* + * Without a scope MediaFolder1 is not registered at all, which is why + * MediaItem1.Play() was the only entry point reachable with an unset scope. + */ +static void test_no_folder_without_scope(const void *data) +{ + context = create_context(); + if (context == NULL) + return; + + g_assert(create_nowplaying_item() != NULL); + + call_list_items(&context->pending, no_folder_reply); +} + +/* + * Destroying a player while a request is still in flight must answer it, + * rather than leave the caller waiting for the D-Bus timeout. + */ +static void test_play_item_destroy_pending(const void *data) +{ + context = create_context(); + if (context == NULL) + return; + + context->defer_completion = true; + context->destroy_on_play = true; + + g_assert(create_nowplaying_item() != NULL); + + call_play(NOWPLAYING_PATH, &context->pending, destroyed_reply); +} + +/* + * A player reporting the total number of items has to see it applied, + * including when the scope moves without any D-Bus request in flight, + * which is what avrcp does on SetBrowsedPlayer. + */ +static void test_total_items_scope_change(const void *data) +{ + context = create_context(); + if (context == NULL) + return; + + context->check_total_items = true; + + g_assert(media_player_create_folder(context->mp, "/Filesystem", + PLAYER_FOLDER_TYPE_MIXED, 0) != NULL); + g_assert(media_player_create_folder(context->mp, "/Other", + PLAYER_FOLDER_TYPE_MIXED, 0) != NULL); + + media_player_set_folder(context->mp, "/Filesystem", 0); + media_player_set_folder(context->mp, "/Other", 0); + + g_assert(context->total_items_calls == 1); +} + +/* + * Search() shares the pending request slot with the other browsing + * requests, so a busy Search() must report EBUSY like they do. + */ +static void test_search_busy(const void *data) +{ + context = create_context(); + if (context == NULL) + return; + + context->defer_completion = true; + + g_assert(media_player_create_folder(context->mp, "/Filesystem", + PLAYER_FOLDER_TYPE_MIXED, 0) != NULL); + media_player_set_folder(context->mp, "/Filesystem", 1); + media_player_set_searchable(context->mp, true); + + call_list_items(&context->pending, ignore_reply); + call_search(&context->pending2, search_busy_reply); +} + +static void test_teardown(const void *data) +{ + destroy_context(); + + tester_teardown_complete(); +} + +int main(int argc, char *argv[]) +{ + tester_init(&argc, &argv); + + tester_add("/media_player/play_item_without_scope", NULL, NULL, + test_play_item_without_scope, + test_teardown); + + tester_add("/media_player/play_item_with_scope", NULL, NULL, + test_play_item_with_scope, + test_teardown); + + tester_add("/media_player/play_item_busy", NULL, NULL, + test_play_item_busy, + test_teardown); + + tester_add("/media_player/play_item_busy_with_list_items", NULL, NULL, + test_play_item_busy_with_list_items, + test_teardown); + + tester_add("/media_player/list_items_scope_change", NULL, NULL, + test_list_items_scope_change, + test_teardown); + + tester_add("/media_player/no_folder_without_scope", NULL, NULL, + test_no_folder_without_scope, + test_teardown); + + tester_add("/media_player/play_item_destroy_pending", NULL, NULL, + test_play_item_destroy_pending, + test_teardown); + + tester_add("/media_player/total_items_scope_change", NULL, NULL, + test_total_items_scope_change, + test_teardown); + + tester_add("/media_player/search_busy", NULL, NULL, + test_search_busy, test_teardown); + + return tester_run(); +} -- 2.54.0 (Apple Git-157)