[PATCH BlueZ 5/5] unit/test-media-player: Add media player tests

George Kiagiadakis <[email protected]>
Newsgroups org.kernel.vger.linux-bluetooth
Message-ID <[email protected]>
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 <config.h>
+#endif
+
+#include <stdbool.h>
+#include <stdint.h>
+#include <errno.h>
+#include <inttypes.h>
+#include <string.h>
+
+#include <glib.h>
+#include <dbus/dbus.h>
+
+#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)
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.