Files
2026-08-24 10:03:24 +08:00

505 lines
15 KiB
C

/*
* test-ba.c
* SPDX-FileCopyrightText: 2016-2025 BlueALSA developers
* SPDX-License-Identifier: MIT
*/
#if HAVE_CONFIG_H
# include <config.h>
#endif
#include <assert.h>
#include <errno.h>
#include <pthread.h>
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <bluetooth/bluetooth.h>
#include <bluetooth/hci.h>
#include <check.h>
#include <glib.h>
#include "a2dp.h"
#include "ba-adapter.h"
#include "ba-device.h"
#include "ba-rfcomm.h"
#include "ba-transport.h"
#include "ba-transport-pcm.h"
#include "ba-config.h"
#include "bluealsa-dbus.h"
#include "bluez.h"
#include "hfp.h"
#include "midi.h"
#include "storage.h"
#include "shared/a2dp-codecs.h"
#include "shared/log.h"
#include "inc/check.inc"
/* Keep persistent storage in the current directory. */
#define TEST_BLUEALSA_STORAGE_DIR "storage-test-ba"
int midi_transport_alsa_seq_create(struct ba_transport *t) { (void)t; return 0; }
int midi_transport_alsa_seq_delete(struct ba_transport *t) { (void)t; return 0; }
int midi_transport_start(struct ba_transport *t) { (void)t; return 0; }
int midi_transport_stop(struct ba_transport *t) { (void)t; return 0; }
void *ba_rfcomm_thread(struct ba_transport *t) { (void)t; return 0; }
int bluealsa_dbus_pcm_register(struct ba_transport_pcm *pcm) {
debug("%s: %p", __func__, (void *)pcm); (void)pcm; return 0; }
void bluealsa_dbus_pcm_update(struct ba_transport_pcm *pcm, unsigned int mask) {
debug("%s: %p %#x", __func__, (void *)pcm, mask); (void)pcm; (void)mask; }
void bluealsa_dbus_pcm_unregister(struct ba_transport_pcm *pcm) {
debug("%s: %p", __func__, (void *)pcm); (void)pcm; }
struct ba_rfcomm *ba_rfcomm_new(struct ba_transport *sco, int fd) {
debug("%s: %p", __func__, (void *)sco); (void)sco; (void)fd; return NULL; }
void ba_rfcomm_destroy(struct ba_rfcomm *r) {
debug("%s: %p", __func__, (void *)r); (void)r; }
int ba_rfcomm_send_signal(struct ba_rfcomm *r, enum ba_rfcomm_signal sig) {
debug("%s: %p: %#x", __func__, (void *)r, sig); (void)r; (void)sig; return 0; }
bool bluez_a2dp_set_configuration(const char *current_dbus_sep_path,
const struct a2dp_sep_config *sep, const void *configuration, GError **error) {
debug("%s: %s: %p", __func__, current_dbus_sep_path, sep);
(void)current_dbus_sep_path; (void)sep; (void)configuration; (void)error;
return false; }
CK_START_TEST(test_ba_adapter) {
struct ba_adapter *a;
ck_assert_ptr_ne(a = ba_adapter_new(0), NULL);
ck_assert_str_eq(a->hci.name, "hci0");
ba_adapter_unref(a);
ck_assert_ptr_eq(ba_adapter_lookup(0), NULL);
ck_assert_ptr_ne(a = ba_adapter_new(5), NULL);
ck_assert_int_eq(a->hci.dev_id, 5);
ck_assert_str_eq(a->hci.name, "hci5");
ck_assert_ptr_eq(ba_adapter_lookup(5), a);
ba_adapter_unref(a);
ba_adapter_unref(a);
ck_assert_ptr_eq(ba_adapter_lookup(5), NULL);
} CK_END_TEST
CK_START_TEST(test_ba_device) {
struct ba_adapter *a;
struct ba_device *d;
ck_assert_ptr_ne(a = ba_adapter_new(0), NULL);
bdaddr_t addr = {{ 0x12, 0x34, 0x56, 0x78, 0x90, 0xAB }};
ck_assert_ptr_ne(d = ba_device_new(a, &addr), NULL);
ba_adapter_unref(a);
ck_assert_ptr_eq(d->a, a);
ck_assert_int_eq(bacmp(&d->addr, &addr), 0);
ck_assert_str_eq(d->ba_dbus_path, "/org/bluealsa/hci0/dev_AB_90_78_56_34_12");
ck_assert_str_eq(d->bluez_dbus_path, "/org/bluez/hci0/dev_AB_90_78_56_34_12");
ck_assert_ptr_eq(ba_device_lookup(a, &addr), d);
ba_device_unref(d);
ba_device_unref(d);
ck_assert_ptr_eq(ba_adapter_lookup(0), NULL);
} CK_END_TEST
CK_START_TEST(test_ba_transport) {
struct ba_adapter *a;
struct ba_device *d;
struct ba_transport *t;
bdaddr_t addr = { 0 };
ck_assert_ptr_ne(a = ba_adapter_new(0), NULL);
ck_assert_ptr_ne(d = ba_device_new(a, &addr), NULL);
ck_assert_int_eq(storage_device_clear(d), 0);
ck_assert_ptr_ne(t = ba_transport_new_sco(d,
BA_TRANSPORT_PROFILE_HFP_AG, "/owner", "/path", -1), NULL);
ba_adapter_unref(a);
ba_device_unref(d);
ck_assert_ptr_eq(t->d, d);
ck_assert_int_eq(t->profile, BA_TRANSPORT_PROFILE_HFP_AG);
ck_assert_str_eq(t->bluez_dbus_owner, "/owner");
ck_assert_str_eq(t->bluez_dbus_path, "/path");
ck_assert_ptr_eq(ba_transport_lookup(d, "/path"), t);
ba_transport_unref(t);
ba_transport_unref(t);
ck_assert_ptr_eq(ba_adapter_lookup(0), NULL);
} CK_END_TEST
#if ENABLE_MIDI
CK_START_TEST(test_ba_transport_midi) {
struct ba_adapter *a;
struct ba_device *d;
struct ba_transport *t;
bdaddr_t addr = { 0 };
ck_assert_ptr_ne(a = ba_adapter_new(0), NULL);
ck_assert_ptr_ne(d = ba_device_new(a, &addr), NULL);
ck_assert_int_eq(storage_device_clear(d), 0);
ck_assert_ptr_ne(t = ba_transport_new_midi(d,
BA_TRANSPORT_PROFILE_MIDI, "/owner", "/path"), NULL);
ba_adapter_unref(a);
ba_device_unref(d);
ck_assert_int_eq(ba_transport_acquire(t), 0);
ck_assert_int_eq(ba_transport_release(t), 0);
ba_transport_destroy(t);
ck_assert_ptr_eq(ba_adapter_lookup(0), NULL);
} CK_END_TEST
#endif
CK_START_TEST(test_ba_transport_sco_one_only) {
struct ba_adapter *a;
struct ba_device *d;
struct ba_transport *t_sco_hsp;
struct ba_transport *t_sco_hfp;
bdaddr_t addr = { 0 };
ck_assert_ptr_ne(a = ba_adapter_new(0), NULL);
ck_assert_ptr_ne(d = ba_device_new(a, &addr), NULL);
ck_assert_int_eq(storage_device_clear(d), 0);
t_sco_hsp = ba_transport_new_sco(d, BA_TRANSPORT_PROFILE_HSP_AG, "/owner", "/path/sco", -1);
ck_assert_ptr_ne(t_sco_hsp, NULL);
t_sco_hfp = ba_transport_new_sco(d, BA_TRANSPORT_PROFILE_HFP_AG, "/owner", "/path/sco", -1);
ck_assert_ptr_eq(t_sco_hfp, NULL);
ck_assert_int_eq(errno, EBUSY);
ba_transport_unref(t_sco_hsp);
ba_adapter_unref(a);
ba_device_unref(d);
ck_assert_ptr_eq(ba_adapter_lookup(0), NULL);
} CK_END_TEST
CK_START_TEST(test_ba_transport_sco_default_codec) {
struct ba_adapter *a;
struct ba_device *d;
struct ba_transport *t_sco;
bdaddr_t addr = { 0 };
ck_assert_ptr_ne(a = ba_adapter_new(0), NULL);
ck_assert_ptr_ne(d = ba_device_new(a, &addr), NULL);
ck_assert_int_eq(storage_device_clear(d), 0);
t_sco = ba_transport_new_sco(d, BA_TRANSPORT_PROFILE_HSP_AG, "/owner", "/path/sco", -1);
ck_assert_int_eq(ba_transport_get_codec(t_sco), HFP_CODEC_CVSD);
ba_transport_unref(t_sco);
#if ENABLE_MSBC
a->hci.features[2] = LMP_TRSP_SCO;
a->hci.features[3] = LMP_ESCO;
config.hfp.codecs.msbc = true;
ck_assert_int_eq(storage_device_clear(d), 0);
t_sco = ba_transport_new_sco(d, BA_TRANSPORT_PROFILE_HFP_AG, "/owner", "/path/sco", -1);
ck_assert_int_eq(ba_transport_get_codec(t_sco), HFP_CODEC_UNDEFINED);
ba_transport_unref(t_sco);
config.hfp.codecs.msbc = false;
ck_assert_int_eq(storage_device_clear(d), 0);
t_sco = ba_transport_new_sco(d, BA_TRANSPORT_PROFILE_HFP_AG, "/owner", "/path/sco", -1);
ck_assert_int_eq(ba_transport_get_codec(t_sco), HFP_CODEC_CVSD);
ba_transport_unref(t_sco);
#else
ck_assert_int_eq(storage_device_clear(d), 0);
t_sco = ba_transport_new_sco(d, BA_TRANSPORT_PROFILE_HFP_AG, "/owner", "/path/sco", -1);
ck_assert_int_eq(ba_transport_get_codec(t_sco), HFP_CODEC_CVSD);
ba_transport_unref(t_sco);
#endif
ba_adapter_unref(a);
ba_device_unref(d);
ck_assert_ptr_eq(ba_adapter_lookup(0), NULL);
} CK_END_TEST
void * sco_enc_thread(struct ba_transport_pcm * t_pcm);
static void * cleanup_thread(struct ba_transport_pcm * t_pcm) {
pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, NULL);
ba_transport_pcm_thread_cleanup(t_pcm);
return NULL;
}
CK_START_TEST(test_ba_transport_threads_sync_termination) {
struct ba_adapter *a;
struct ba_device *d;
struct ba_transport *t_sco;
bdaddr_t addr = { 0 };
ck_assert_ptr_ne(a = ba_adapter_new(0), NULL);
ck_assert_ptr_ne(d = ba_device_new(a, &addr), NULL);
ck_assert_int_eq(storage_device_clear(d), 0);
t_sco = ba_transport_new_sco(d, BA_TRANSPORT_PROFILE_HSP_AG, "/owner", "/path/sco", -1);
ck_assert_ptr_ne(t_sco, NULL);
t_sco->bt_fd = 0;
t_sco->mtu_read = 48;
t_sco->mtu_write = 48;
ck_assert_int_eq(ba_transport_pcm_start(&t_sco->sco.pcm_spk, sco_enc_thread, "enc"), 0);
ck_assert_int_eq(ba_transport_pcm_state_wait_running(&t_sco->sco.pcm_spk), 0);
ck_assert_int_eq(ba_transport_pcm_start(&t_sco->sco.pcm_mic, cleanup_thread, "dec"), 0);
ck_assert_int_eq(ba_transport_pcm_state_wait_running(&t_sco->sco.pcm_mic), -1);
ck_assert_int_eq(ba_transport_pcm_state_wait_terminated(&t_sco->sco.pcm_spk), 0);
ck_assert_int_eq(ba_transport_pcm_state_wait_terminated(&t_sco->sco.pcm_mic), 0);
ba_adapter_unref(a);
ba_device_unref(d);
ba_transport_unref(t_sco);
ck_assert_ptr_eq(ba_adapter_lookup(0), NULL);
} CK_END_TEST
CK_START_TEST(test_ba_transport_pcm_format) {
uint16_t format_u8 = BA_TRANSPORT_PCM_FORMAT_U8;
uint16_t format_s32_4le = BA_TRANSPORT_PCM_FORMAT_S32_4LE;
ck_assert_int_eq(format_u8, 0x0108);
ck_assert_int_eq(BA_TRANSPORT_PCM_FORMAT_SIGN(format_u8), 0);
ck_assert_int_eq(BA_TRANSPORT_PCM_FORMAT_WIDTH(format_u8), 8);
ck_assert_int_eq(BA_TRANSPORT_PCM_FORMAT_BYTES(format_u8), 1);
ck_assert_int_eq(BA_TRANSPORT_PCM_FORMAT_ENDIAN(format_u8), 0);
ck_assert_int_eq(format_s32_4le, 0x8420);
ck_assert_int_eq(BA_TRANSPORT_PCM_FORMAT_SIGN(format_s32_4le), 1);
ck_assert_int_eq(BA_TRANSPORT_PCM_FORMAT_WIDTH(format_s32_4le), 32);
ck_assert_int_eq(BA_TRANSPORT_PCM_FORMAT_BYTES(format_s32_4le), 4);
ck_assert_int_eq(BA_TRANSPORT_PCM_FORMAT_ENDIAN(format_s32_4le), 0);
} CK_END_TEST
static int sep_transport_init(struct ba_transport *t) {
(void)t;
return 0;
}
CK_START_TEST(test_ba_transport_pcm_volume) {
struct ba_adapter *a;
struct ba_device *d;
struct ba_transport *t_a2dp;
struct ba_transport *t_sco;
bdaddr_t addr = { 0 };
ck_assert_ptr_ne(a = ba_adapter_new(0), NULL);
ck_assert_ptr_ne(d = ba_device_new(a, &addr), NULL);
ck_assert_int_eq(storage_device_clear(d), 0);
struct a2dp_sep sep = {
.config = { .type = A2DP_SINK, .codec_id = A2DP_CODEC_SBC },
.transport_init = sep_transport_init };
a2dp_sbc_t configuration = { .channel_mode = SBC_CHANNEL_MODE_STEREO };
ck_assert_ptr_ne(t_a2dp = ba_transport_new_a2dp(d,
BA_TRANSPORT_PROFILE_A2DP_SINK, "/owner", "/path/a2dp", &sep,
&configuration), NULL);
ck_assert_ptr_ne(t_sco = ba_transport_new_sco(d,
BA_TRANSPORT_PROFILE_HFP_AG, "/owner", "/path/sco", -1), NULL);
ba_adapter_unref(a);
ba_device_unref(d);
ck_assert_int_eq(ba_transport_pcm_volume_range_to_level(0, BLUEZ_MEDIA_TRANSPORT_A2DP_VOLUME_MAX), -9600);
ck_assert_int_eq(ba_transport_pcm_volume_level_to_range(-9600, BLUEZ_MEDIA_TRANSPORT_A2DP_VOLUME_MAX), 0);
ck_assert_int_eq(ba_transport_pcm_volume_range_to_level(127, BLUEZ_MEDIA_TRANSPORT_A2DP_VOLUME_MAX), 0);
ck_assert_int_eq(ba_transport_pcm_volume_level_to_range(0, BLUEZ_MEDIA_TRANSPORT_A2DP_VOLUME_MAX), 127);
ck_assert_int_eq(ba_transport_pcm_volume_range_to_level(0, HFP_VOLUME_GAIN_MAX), -9600);
ck_assert_int_eq(ba_transport_pcm_volume_level_to_range(-9600, HFP_VOLUME_GAIN_MAX), 0);
ck_assert_int_eq(ba_transport_pcm_volume_range_to_level(15, HFP_VOLUME_GAIN_MAX), 0);
ck_assert_int_eq(ba_transport_pcm_volume_level_to_range(0, HFP_VOLUME_GAIN_MAX), 15);
ba_transport_unref(t_a2dp);
ba_transport_unref(t_sco);
ck_assert_ptr_eq(ba_adapter_lookup(0), NULL);
} CK_END_TEST
static int test_cascade_free_transport_unref(struct ba_transport *t) {
return ba_transport_unref(t), 0;
}
CK_START_TEST(test_cascade_free) {
struct ba_adapter *a;
struct ba_device *d;
struct ba_transport *t;
bdaddr_t addr = { 0 };
ck_assert_ptr_ne(a = ba_adapter_new(0), NULL);
ck_assert_ptr_ne(d = ba_device_new(a, &addr), NULL);
ck_assert_int_eq(storage_device_clear(d), 0);
ck_assert_ptr_ne(t = ba_transport_new_sco(d,
BA_TRANSPORT_PROFILE_HFP_AG, "/owner", "/path", -1), NULL);
t->bt_fd = 0; /* release() is called for acquired transport only */
t->release = test_cascade_free_transport_unref;
ba_device_unref(d);
ba_adapter_destroy(a);
/* verify that cascade free was performed */
ck_assert_ptr_eq(ba_adapter_lookup(0), NULL);
} CK_END_TEST
CK_START_TEST(test_storage) {
const char *storage_path = TEST_BLUEALSA_STORAGE_DIR "/00:11:22:33:44:55";
const char *storage_data =
"[/org/bluealsa/hci0/dev_00_11_22_33_44_55/a2dpsnk/source]\n"
"ClientDelays=SBC:-200\n"
"SoftVolume=false\n"
"Volume=-5600;-4800;\n"
"Mute=false;true;\n";
FILE *f;
ck_assert_ptr_ne(f = fopen(storage_path, "w"), NULL);
ck_assert_int_eq(fwrite(storage_data, strlen(storage_data), 1, f), 1);
ck_assert_int_eq(fclose(f), 0);
struct ba_adapter *a;
struct ba_device *d;
struct ba_transport *t;
bdaddr_t addr;
str2ba(&storage_path[sizeof(TEST_BLUEALSA_STORAGE_DIR)], &addr);
ck_assert_ptr_ne(a = ba_adapter_new(0), NULL);
ck_assert_ptr_ne(d = ba_device_new(a, &addr), NULL);
struct a2dp_sep sep = {
.config = { .type = A2DP_SINK, .codec_id = A2DP_CODEC_SBC },
.transport_init = sep_transport_init };
a2dp_sbc_t configuration = { .channel_mode = SBC_CHANNEL_MODE_STEREO };
ck_assert_ptr_ne(t = ba_transport_new_a2dp(d,
BA_TRANSPORT_PROFILE_A2DP_SINK, "/owner", "/path", &sep,
&configuration), NULL);
/* This test does not link with A2DP functionality,
* so the PCM has to be initialized manually. */
t->media.pcm.channels = 2;
/* check if persistent storage was loaded */
ck_assert_int_eq(t->media.pcm.soft_volume, false);
ck_assert_int_eq(t->media.pcm.volume[0].level, -5600);
ck_assert_int_eq(t->media.pcm.volume[0].soft_mute, false);
ck_assert_int_eq(t->media.pcm.volume[1].level, -4800);
ck_assert_int_eq(t->media.pcm.volume[1].soft_mute, true);
ck_assert_int_eq(t->media.pcm.client_delay_dms, -200);
bool muted = true;
int level = ba_transport_pcm_volume_range_to_level(100, BLUEZ_MEDIA_TRANSPORT_A2DP_VOLUME_MAX);
ba_transport_pcm_volume_set(&t->media.pcm.volume[0], &level, &muted, NULL);
ba_transport_pcm_volume_set(&t->media.pcm.volume[1], &level, &muted, NULL);
t->media.pcm.client_delay_dms = 140;
ba_adapter_unref(a);
ba_device_unref(d);
ba_transport_unref(t);
ck_assert_ptr_eq(ba_adapter_lookup(0), NULL);
char buffer[1024] = { 0 };
ck_assert_ptr_ne(f = fopen(storage_path, "r"), NULL);
ck_assert_int_gt(fread(buffer, 1, sizeof(buffer), f), 0);
ck_assert_int_eq(fclose(f), 0);
const char *storage_data_new =
"[/org/bluealsa/hci0/dev_00_11_22_33_44_55/a2dpsnk/source]\n"
"ClientDelays=SBC:140;\n"
"SoftVolume=false\n"
"Volume=-344;-344;\n"
"Mute=true;true;\n"
"\n"
"[/org/bluealsa/hci0/dev_00_11_22_33_44_55/a2dpsnk/sink]\n"
"ClientDelays=\n"
"SoftVolume=false\n"
"Volume=\n"
"Mute=\n";
/* check if persistent storage was updated */
ck_assert_str_eq(buffer, storage_data_new);
} CK_END_TEST
int main(void) {
assert(mkdir(TEST_BLUEALSA_STORAGE_DIR, 0755) == 0 || errno == EEXIST);
assert(storage_init(TEST_BLUEALSA_STORAGE_DIR) == 0);
atexit(storage_destroy);
#if ENABLE_MSBC
config.hfp.codecs.msbc = false;
#endif
#if ENABLE_LC3_SWB
config.hfp.codecs.lc3_swb = false;
#endif
Suite *s = suite_create(__FILE__);
TCase *tc = tcase_create(__FILE__);
SRunner *sr = srunner_create(s);
suite_add_tcase(s, tc);
tcase_add_test(tc, test_ba_adapter);
tcase_add_test(tc, test_ba_device);
tcase_add_test(tc, test_ba_transport);
#if ENABLE_MIDI
tcase_add_test(tc, test_ba_transport_midi);
#endif
tcase_add_test(tc, test_ba_transport_sco_one_only);
tcase_add_test(tc, test_ba_transport_sco_default_codec);
tcase_add_test(tc, test_ba_transport_threads_sync_termination);
tcase_add_test(tc, test_ba_transport_pcm_format);
tcase_add_test(tc, test_ba_transport_pcm_volume);
tcase_add_test(tc, test_cascade_free);
tcase_add_test(tc, test_storage);
srunner_run_all(sr, CK_ENV);
int nf = srunner_ntests_failed(sr);
srunner_free(sr);
return nf == 0 ? 0 : 1;
}