aboutsummaryrefslogtreecommitdiff
path: root/tools/testing
diff options
context:
space:
mode:
authorTakashi Iwai <tiwai@suse.de>2026-10-19 10:39:44 +0200
committerTakashi Iwai <tiwai@suse.de>2026-10-19 10:40:00 +0200
commit7e246ac8bed799402b743550d283bdd202048fa7 (patch)
treed6dc95e8d6f96a581ec2c37fcaea09911a4099a5 /tools/testing
parent8e8cd9a4c6dc006e9bd51b8ad0079fb4576b48d3 (diff)
parent40409fa23d32c8ed51e832031daa6decd9d6fd11 (diff)
Merge branch 'for-next' into for-linus
Pull 7.4 merge-window stuff. Signed-off-by: Takashi Iwai <tiwai@suse.de>
Diffstat (limited to 'tools/testing')
-rw-r--r--tools/testing/selftests/alsa/.gitignore1
-rw-r--r--tools/testing/selftests/alsa/Makefile2
-rw-r--r--tools/testing/selftests/alsa/aloop-test.c345
-rw-r--r--tools/testing/selftests/alsa/mixer-test.c490
-rw-r--r--tools/testing/selftests/alsa/utimer-test.c6
5 files changed, 842 insertions, 2 deletions
diff --git a/tools/testing/selftests/alsa/.gitignore b/tools/testing/selftests/alsa/.gitignore
index 3dd8e1176b89..7b0e1e9ebf1b 100644
--- a/tools/testing/selftests/alsa/.gitignore
+++ b/tools/testing/selftests/alsa/.gitignore
@@ -1,3 +1,4 @@
+aloop-test
global-timer
mixer-test
pcm-test
diff --git a/tools/testing/selftests/alsa/Makefile b/tools/testing/selftests/alsa/Makefile
index 8dab90ad22bb..afd64a679dc1 100644
--- a/tools/testing/selftests/alsa/Makefile
+++ b/tools/testing/selftests/alsa/Makefile
@@ -16,7 +16,7 @@ LDLIBS+=-lpthread
OVERRIDE_TARGETS = 1
-TEST_GEN_PROGS := mixer-test pcm-test test-pcmtest-driver utimer-test
+TEST_GEN_PROGS := aloop-test mixer-test pcm-test test-pcmtest-driver utimer-test
TEST_GEN_PROGS_EXTENDED := libatest.so global-timer
diff --git a/tools/testing/selftests/alsa/aloop-test.c b/tools/testing/selftests/alsa/aloop-test.c
new file mode 100644
index 000000000000..a58b9fdbdc17
--- /dev/null
+++ b/tools/testing/selftests/alsa/aloop-test.c
@@ -0,0 +1,345 @@
+// SPDX-License-Identifier: GPL-2.0
+/*
+ * Tests for the hw constraints between the two ends of an snd-aloop cable,
+ * with and without the "PCM Notify" control, and for the "PCM Slave"
+ * controls that report the playback side's parameters.
+ *
+ * Needs snd-aloop loaded with the default card id "Loopback". The tests use
+ * the cable between hw:Loopback,0,0 (playback) and hw:Loopback,1,0 (capture).
+ */
+#include <errno.h>
+#include <stdbool.h>
+#include <stdio.h>
+#include <string.h>
+#include <alsa/asoundlib.h>
+#include "kselftest_harness.h"
+
+#define FRAMES 1024
+#define MAX_CHANNELS 4
+
+struct stream_params {
+ snd_pcm_access_t access;
+ snd_pcm_format_t format;
+ unsigned int channels;
+ unsigned int rate;
+};
+
+/* The playback params differ from the capture params in every field. */
+static const struct stream_params capture_params = {
+ SND_PCM_ACCESS_RW_INTERLEAVED, SND_PCM_FORMAT_S16_LE, 2, 44100
+};
+
+static const struct stream_params playback_params = {
+ SND_PCM_ACCESS_RW_NONINTERLEAVED, SND_PCM_FORMAT_S32_LE, 4, 96000
+};
+
+struct probe_result {
+ unsigned int rate_min, rate_max;
+ unsigned int channels_min, channels_max;
+ bool format_ok;
+};
+
+/* Value events seen on the cable's controls */
+enum {
+ EV_ACTIVE = 1 << 0,
+ EV_FORMAT = 1 << 1,
+ EV_RATE = 1 << 2,
+ EV_CHANNELS = 1 << 3,
+ EV_ACCESS = 1 << 4,
+};
+
+FIXTURE(aloop) {
+ char play_name[32];
+ char capt_name[32];
+ snd_ctl_t *ctl;
+ bool saved_notify;
+ bool notify_saved;
+};
+
+/* The cable's controls are on the capture side device. */
+static void cable_ctl_id(snd_ctl_elem_value_t *value, const char *name)
+{
+ snd_ctl_elem_value_set_interface(value, SND_CTL_ELEM_IFACE_PCM);
+ snd_ctl_elem_value_set_name(value, name);
+ snd_ctl_elem_value_set_device(value, 1);
+ snd_ctl_elem_value_set_subdevice(value, 0);
+}
+
+static long cable_ctl_get(snd_ctl_t *ctl, const char *name)
+{
+ snd_ctl_elem_value_t *value;
+
+ snd_ctl_elem_value_alloca(&value);
+ cable_ctl_id(value, name);
+ if (snd_ctl_elem_read(ctl, value) < 0)
+ return -1;
+ if (!strcmp(name, "PCM Slave Access Mode"))
+ return snd_ctl_elem_value_get_enumerated(value, 0);
+ return snd_ctl_elem_value_get_integer(value, 0);
+}
+
+static int set_notify(snd_ctl_t *ctl, bool on)
+{
+ snd_ctl_elem_value_t *value;
+
+ snd_ctl_elem_value_alloca(&value);
+ cable_ctl_id(value, "PCM Notify");
+ snd_ctl_elem_value_set_boolean(value, 0, on);
+ return snd_ctl_elem_write(ctl, value);
+}
+
+/* Read all pending control events and return the EV_* bits for the cable's controls. */
+static unsigned int read_events(snd_ctl_t *ctl)
+{
+ static const struct {
+ const char *name;
+ unsigned int bit;
+ } names[] = {
+ { "PCM Slave Active", EV_ACTIVE },
+ { "PCM Slave Format", EV_FORMAT },
+ { "PCM Slave Rate", EV_RATE },
+ { "PCM Slave Channels", EV_CHANNELS },
+ { "PCM Slave Access Mode", EV_ACCESS },
+ };
+ snd_ctl_event_t *event;
+ unsigned int seen = 0;
+ int i;
+
+ snd_ctl_event_alloca(&event);
+ while (snd_ctl_read(ctl, event) > 0) {
+ if (snd_ctl_event_get_type(event) != SND_CTL_EVENT_ELEM ||
+ !(snd_ctl_event_elem_get_mask(event) & SND_CTL_EVENT_MASK_VALUE) ||
+ snd_ctl_event_elem_get_device(event) != 1 ||
+ snd_ctl_event_elem_get_subdevice(event) != 0)
+ continue;
+ for (i = 0; i < ARRAY_SIZE(names); i++)
+ if (!strcmp(snd_ctl_event_elem_get_name(event), names[i].name))
+ seen |= names[i].bit;
+ }
+ return seen;
+}
+
+/* Open and configure a stream. snd_pcm_hw_params() also prepares it. */
+static int open_pcm(snd_pcm_t **pcm, const char *name, snd_pcm_stream_t stream,
+ const struct stream_params *p)
+{
+ unsigned int buffer_time = 100000;
+ snd_pcm_hw_params_t *hw;
+ int err;
+
+ snd_pcm_hw_params_alloca(&hw);
+ err = snd_pcm_open(pcm, name, stream, 0);
+ if (err < 0)
+ return err;
+ err = snd_pcm_hw_params_any(*pcm, hw);
+ if (err >= 0)
+ err = snd_pcm_hw_params_set_access(*pcm, hw, p->access);
+ if (err >= 0)
+ err = snd_pcm_hw_params_set_format(*pcm, hw, p->format);
+ if (err >= 0)
+ err = snd_pcm_hw_params_set_channels(*pcm, hw, p->channels);
+ if (err >= 0)
+ err = snd_pcm_hw_params_set_rate(*pcm, hw, p->rate, 0);
+ if (err >= 0)
+ err = snd_pcm_hw_params_set_buffer_time_near(*pcm, hw, &buffer_time, NULL);
+ if (err >= 0)
+ err = snd_pcm_hw_params(*pcm, hw);
+ if (err < 0) {
+ snd_pcm_close(*pcm);
+ *pcm = NULL;
+ }
+ return err;
+}
+
+/* What a client probing the device sees, and whether it may use the given format */
+static int probe_pcm(const char *name, snd_pcm_stream_t stream, snd_pcm_format_t format,
+ struct probe_result *res)
+{
+ snd_pcm_hw_params_t *hw;
+ snd_pcm_t *pcm;
+ int err;
+
+ snd_pcm_hw_params_alloca(&hw);
+ err = snd_pcm_open(&pcm, name, stream, 0);
+ if (err < 0)
+ return err;
+ err = snd_pcm_hw_params_any(pcm, hw);
+ if (err >= 0)
+ err = snd_pcm_hw_params_get_rate_min(hw, &res->rate_min, NULL);
+ if (err >= 0)
+ err = snd_pcm_hw_params_get_rate_max(hw, &res->rate_max, NULL);
+ if (err >= 0)
+ err = snd_pcm_hw_params_get_channels_min(hw, &res->channels_min);
+ if (err >= 0)
+ err = snd_pcm_hw_params_get_channels_max(hw, &res->channels_max);
+ if (err >= 0)
+ res->format_ok = !snd_pcm_hw_params_test_format(pcm, hw, format);
+ snd_pcm_close(pcm);
+ return err;
+}
+
+static int start_playback(snd_pcm_t *pcm, const struct stream_params *p)
+{
+ static char silence[FRAMES * MAX_CHANNELS * 4];
+ void *bufs[MAX_CHANNELS];
+ snd_pcm_sframes_t written;
+ unsigned int i;
+
+ if (p->access == SND_PCM_ACCESS_RW_NONINTERLEAVED) {
+ for (i = 0; i < p->channels; i++)
+ bufs[i] = silence + i * FRAMES * 4;
+ written = snd_pcm_writen(pcm, bufs, FRAMES);
+ } else {
+ written = snd_pcm_writei(pcm, silence, FRAMES);
+ }
+ if (written < 0)
+ return written;
+ if (snd_pcm_state(pcm) == SND_PCM_STATE_PREPARED)
+ return snd_pcm_start(pcm);
+ return 0;
+}
+
+FIXTURE_SETUP(aloop) {
+ char ctl_name[32];
+ snd_pcm_t *pcm;
+ int card, err;
+
+ card = snd_card_get_index("Loopback");
+ if (card < 0)
+ SKIP(return, "No Loopback card, snd-aloop is probably not loaded");
+
+ sprintf(ctl_name, "hw:%d", card);
+ sprintf(self->play_name, "hw:%d,0,0", card);
+ sprintf(self->capt_name, "hw:%d,1,0", card);
+
+ err = snd_pcm_open(&pcm, self->capt_name, SND_PCM_STREAM_CAPTURE, SND_PCM_NONBLOCK);
+ if (err == -EBUSY)
+ SKIP(return, "%s is in use", self->capt_name);
+ ASSERT_EQ(err, 0);
+ snd_pcm_close(pcm);
+ err = snd_pcm_open(&pcm, self->play_name, SND_PCM_STREAM_PLAYBACK, SND_PCM_NONBLOCK);
+ if (err == -EBUSY)
+ SKIP(return, "%s is in use", self->play_name);
+ ASSERT_EQ(err, 0);
+ snd_pcm_close(pcm);
+
+ ASSERT_EQ(snd_ctl_open(&self->ctl, ctl_name, SND_CTL_NONBLOCK), 0);
+ ASSERT_EQ(snd_ctl_subscribe_events(self->ctl, 1), 0);
+ self->saved_notify = cable_ctl_get(self->ctl, "PCM Notify");
+ self->notify_saved = true;
+}
+
+FIXTURE_TEARDOWN(aloop) {
+ if (self->notify_saved)
+ set_notify(self->ctl, self->saved_notify);
+ if (self->ctl)
+ snd_ctl_close(self->ctl);
+}
+
+/* Without notify, a playback opened while a capture is set up is pinned to its parameters. */
+TEST_F(aloop, playback_constrained_without_notify) {
+ struct probe_result res;
+ snd_pcm_t *capt;
+
+ ASSERT_EQ(set_notify(self->ctl, false), 0);
+ ASSERT_EQ(open_pcm(&capt, self->capt_name, SND_PCM_STREAM_CAPTURE, &capture_params), 0);
+
+ ASSERT_EQ(probe_pcm(self->play_name, SND_PCM_STREAM_PLAYBACK, playback_params.format,
+ &res), 0);
+ EXPECT_EQ(res.rate_min, capture_params.rate);
+ EXPECT_EQ(res.rate_max, capture_params.rate);
+ EXPECT_EQ(res.channels_min, capture_params.channels);
+ EXPECT_EQ(res.channels_max, capture_params.channels);
+ EXPECT_FALSE(res.format_ok);
+
+ snd_pcm_close(capt);
+}
+
+/* With notify, the playback side is free to pick other parameters. */
+TEST_F(aloop, playback_unconstrained_with_notify) {
+ struct probe_result res;
+ snd_pcm_t *capt;
+
+ ASSERT_EQ(set_notify(self->ctl, true), 0);
+ ASSERT_EQ(open_pcm(&capt, self->capt_name, SND_PCM_STREAM_CAPTURE, &capture_params), 0);
+
+ ASSERT_EQ(probe_pcm(self->play_name, SND_PCM_STREAM_PLAYBACK, playback_params.format,
+ &res), 0);
+ EXPECT_LE(res.rate_min, capture_params.rate);
+ EXPECT_GE(res.rate_max, playback_params.rate);
+ EXPECT_LE(res.channels_min, capture_params.channels);
+ EXPECT_GE(res.channels_max, playback_params.channels);
+ EXPECT_TRUE(res.format_ok);
+
+ snd_pcm_close(capt);
+}
+
+/*
+ * With notify, starting a playback with different parameters stops the
+ * running capture, and the "PCM Slave" controls report the new parameters
+ * with a value event for each one that changed.
+ */
+TEST_F(aloop, params_change_stops_capture_with_notify) {
+ snd_pcm_t *capt, *play;
+ unsigned int events;
+
+ /*
+ * The controls keep the last playback's parameters, and only notify on
+ * a change. Start a playback with the capture's parameters while no
+ * capture is open, so that every control changes below.
+ */
+ ASSERT_EQ(open_pcm(&play, self->play_name, SND_PCM_STREAM_PLAYBACK, &capture_params), 0);
+ ASSERT_EQ(start_playback(play, &capture_params), 0);
+ snd_pcm_close(play);
+
+ ASSERT_EQ(set_notify(self->ctl, true), 0);
+ ASSERT_EQ(open_pcm(&capt, self->capt_name, SND_PCM_STREAM_CAPTURE, &capture_params), 0);
+ ASSERT_EQ(snd_pcm_start(capt), 0);
+ ASSERT_EQ(snd_pcm_state(capt), SND_PCM_STATE_RUNNING);
+ EXPECT_EQ(cable_ctl_get(self->ctl, "PCM Slave Active"), 0);
+ read_events(self->ctl);
+
+ ASSERT_EQ(open_pcm(&play, self->play_name, SND_PCM_STREAM_PLAYBACK, &playback_params), 0)
+ TH_LOG("Playback refused other parameters while the capture is running");
+ ASSERT_EQ(start_playback(play, &playback_params), 0);
+
+ /* loopback_check_format() stops the capture from the playback's start trigger. */
+ EXPECT_NE(snd_pcm_state(capt), SND_PCM_STATE_RUNNING);
+
+ EXPECT_EQ(cable_ctl_get(self->ctl, "PCM Slave Active"), 1);
+ EXPECT_EQ(cable_ctl_get(self->ctl, "PCM Slave Format"), playback_params.format);
+ EXPECT_EQ(cable_ctl_get(self->ctl, "PCM Slave Rate"), playback_params.rate);
+ EXPECT_EQ(cable_ctl_get(self->ctl, "PCM Slave Channels"), playback_params.channels);
+ EXPECT_EQ(cable_ctl_get(self->ctl, "PCM Slave Access Mode"), 1);
+
+ events = read_events(self->ctl);
+ EXPECT_TRUE(events & EV_ACTIVE);
+ EXPECT_TRUE(events & EV_FORMAT);
+ EXPECT_TRUE(events & EV_RATE);
+ EXPECT_TRUE(events & EV_CHANNELS);
+ EXPECT_TRUE(events & EV_ACCESS);
+
+ snd_pcm_close(play);
+ snd_pcm_close(capt);
+}
+
+/* With notify, a capture opened second is still pinned to the playback's parameters. */
+TEST_F(aloop, capture_constrained_with_notify) {
+ struct probe_result res;
+ snd_pcm_t *play;
+
+ ASSERT_EQ(set_notify(self->ctl, true), 0);
+ ASSERT_EQ(open_pcm(&play, self->play_name, SND_PCM_STREAM_PLAYBACK, &playback_params), 0);
+
+ ASSERT_EQ(probe_pcm(self->capt_name, SND_PCM_STREAM_CAPTURE, capture_params.format,
+ &res), 0);
+ EXPECT_EQ(res.rate_min, playback_params.rate);
+ EXPECT_EQ(res.rate_max, playback_params.rate);
+ EXPECT_EQ(res.channels_min, playback_params.channels);
+ EXPECT_EQ(res.channels_max, playback_params.channels);
+ EXPECT_FALSE(res.format_ok);
+
+ snd_pcm_close(play);
+}
+
+TEST_HARNESS_MAIN
diff --git a/tools/testing/selftests/alsa/mixer-test.c b/tools/testing/selftests/alsa/mixer-test.c
index 0857d64c322a..966ef83354b9 100644
--- a/tools/testing/selftests/alsa/mixer-test.c
+++ b/tools/testing/selftests/alsa/mixer-test.c
@@ -28,7 +28,13 @@
#include "kselftest.h"
#include "alsa-local.h"
-#define TESTS_PER_CONTROL 7
+#define TESTS_PER_CONTROL 8
+
+/* Suffixes of the SNDRV_CTL_NAME_IEC958() names, not exported to userspace */
+#define IEC958_DEFAULT "Default"
+#define IEC958_CON_MASK "Con Mask"
+#define IEC958_PRO_MASK "Pro Mask"
+#define IEC958_MASK "Mask"
struct card_data {
snd_ctl_t *handle;
@@ -46,9 +52,14 @@ struct ctl_data {
snd_ctl_elem_id_t *id;
snd_ctl_elem_info_t *info;
snd_ctl_elem_value_t *def_val;
+ snd_ctl_elem_value_t *snapshot;
+ bool snapshot_valid;
+ bool moved;
int elem;
int event_missing;
int event_spurious;
+ int side_effects;
+ unsigned int ev_mask;
struct card_data *card;
struct ctl_data *next;
};
@@ -153,6 +164,10 @@ static void find_controls(void)
if (err < 0)
ksft_exit_fail_msg("Out of memory\n");
+ err = snd_ctl_elem_value_malloc(&ctl_data->snapshot);
+ if (err < 0)
+ ksft_exit_fail_msg("Out of memory\n");
+
snd_ctl_elem_list_get_id(card_data->ctls, ctl,
ctl_data->id);
snd_ctl_elem_info_set_id(ctl_data->info, ctl_data->id);
@@ -201,6 +216,20 @@ static void find_controls(void)
snd_config_delete(config);
}
+/* The control on the same card that an event's numid refers to */
+static struct ctl_data *find_ctl_by_numid(struct card_data *card,
+ unsigned int numid)
+{
+ struct ctl_data *ctl;
+
+ for (ctl = ctl_list; ctl != NULL; ctl = ctl->next)
+ if (ctl->card == card &&
+ snd_ctl_elem_info_get_numid(ctl->info) == numid)
+ return ctl;
+
+ return NULL;
+}
+
/*
* Block for up to timeout ms for an event, returns a negative value
* on error, 0 for no event and 1 for an event.
@@ -259,8 +288,21 @@ static int wait_for_event(struct ctl_data *ctl, int timeout)
mask = snd_ctl_event_elem_get_mask(event);
ev_id = snd_ctl_event_elem_get_numid(event);
if (ev_id != snd_ctl_elem_info_get_numid(ctl->info)) {
+ struct ctl_data *other = find_ctl_by_numid(ctl->card,
+ ev_id);
+
+ /*
+ * Remember that the driver announced this one.
+ * test_ctl_write_side_effects() uses that to tell a
+ * deliberate link from a silent register collision.
+ */
+ if (other)
+ other->ev_mask |= mask;
+
ksft_print_msg("Event for unexpected ctl %s\n",
snd_ctl_event_elem_get_name(event));
+ /* The loop condition must not see the other control's mask */
+ mask = 0;
continue;
}
@@ -842,6 +884,251 @@ static bool test_ctl_write_valid_enumerated(struct ctl_data *ctl)
return !fail;
}
+/*
+ * Find the read only mask control for an IEC958 value control. The two
+ * share device, subdevice and index but not always the interface.
+ */
+static struct ctl_data *find_iec958_mask_ctl(struct ctl_data *ctl,
+ const char *suffix)
+{
+ char name[64];
+ int stem;
+ struct ctl_data *mask;
+
+ stem = strlen(ctl->name) - strlen(IEC958_DEFAULT);
+ if (snprintf(name, sizeof(name), "%.*s%s", stem, ctl->name, suffix) >=
+ (int)sizeof(name))
+ return NULL;
+
+ for (mask = ctl_list; mask != NULL; mask = mask->next) {
+ if (mask->card != ctl->card)
+ continue;
+ if (snd_ctl_elem_info_get_type(mask->info) !=
+ SND_CTL_ELEM_TYPE_IEC958)
+ continue;
+ if (snd_ctl_elem_info_is_inactive(mask->info))
+ continue;
+ if (!snd_ctl_elem_info_is_readable(mask->info))
+ continue;
+ if (snd_ctl_elem_id_get_device(mask->id) !=
+ snd_ctl_elem_id_get_device(ctl->id))
+ continue;
+ if (snd_ctl_elem_id_get_subdevice(mask->id) !=
+ snd_ctl_elem_id_get_subdevice(ctl->id))
+ continue;
+ if (snd_ctl_elem_id_get_index(mask->id) !=
+ snd_ctl_elem_id_get_index(ctl->id))
+ continue;
+ if (strcmp(mask->name, name) == 0)
+ return mask;
+ }
+
+ return NULL;
+}
+
+/*
+ * Read the bits the device says it implements. Bit 0 of the first status
+ * byte picks which mask applies, some devices publish only a plain Mask.
+ */
+static bool read_iec958_mask(struct ctl_data *ctl,
+ const snd_aes_iec958_t *cur,
+ snd_aes_iec958_t *mask)
+{
+ int err;
+ struct ctl_data *mask_ctl;
+ snd_ctl_elem_value_t *val;
+
+ if (!strend(ctl->name, IEC958_DEFAULT))
+ return false;
+
+ if (cur->status[0] & IEC958_AES0_PROFESSIONAL)
+ mask_ctl = find_iec958_mask_ctl(ctl, IEC958_PRO_MASK);
+ else
+ mask_ctl = find_iec958_mask_ctl(ctl, IEC958_CON_MASK);
+ if (!mask_ctl)
+ mask_ctl = find_iec958_mask_ctl(ctl, IEC958_MASK);
+ if (!mask_ctl)
+ return false;
+
+ snd_ctl_elem_value_alloca(&val);
+ snd_ctl_elem_value_set_id(val, mask_ctl->id);
+
+ err = snd_ctl_elem_read(mask_ctl->card->handle, val);
+ if (err < 0) {
+ ksft_print_msg("snd_ctl_elem_read() failed for %s: %s\n",
+ mask_ctl->name, snd_strerror(err));
+ return false;
+ }
+
+ snd_ctl_elem_value_get_iec958(val, mask);
+
+ return true;
+}
+
+/*
+ * Throw away the events from a write we are not checking, one left behind
+ * would make a missing notification look like a notification we got.
+ */
+static int drop_events(struct ctl_data *ctl)
+{
+ int err;
+
+ do {
+ err = wait_for_event(ctl, 0);
+ } while (err > 0);
+
+ return err;
+}
+
+/*
+ * Toggle every bit the device advertises, one at a time. Each one starts
+ * from the value we read since a driver can look at the rest of the block
+ * when it stores a bit, and bit 0 of the first status byte selects the
+ * layout the others are read in so leave that one alone.
+ */
+static bool test_ctl_write_valid_iec958_mask(struct ctl_data *ctl,
+ snd_ctl_elem_value_t *orig_val,
+ const snd_aes_iec958_t *mask)
+{
+ int err, j;
+ unsigned int byte;
+ unsigned char bit;
+ bool fail = false, found = false;
+ snd_aes_iec958_t iec958;
+ snd_ctl_elem_value_t *val;
+ snd_ctl_elem_value_alloca(&val);
+
+ snd_ctl_elem_value_copy(val, orig_val);
+
+ for (byte = 0; byte < sizeof(mask->status); byte++) {
+ for (j = 0; j < 8; j++) {
+ bit = 1 << j;
+
+ if (byte == 0 && bit == IEC958_AES0_PROFESSIONAL)
+ continue;
+ if (!(mask->status[byte] & bit))
+ continue;
+ found = true;
+
+ snd_ctl_elem_value_get_iec958(orig_val, &iec958);
+ iec958.status[byte] ^= bit;
+ snd_ctl_elem_value_set_iec958(val, &iec958);
+
+ err = write_and_verify(ctl, val, NULL);
+ if (err != 0) {
+ ksft_print_msg("%s failed to set advertised status[%u] 0x%02x\n",
+ ctl->name, byte, bit);
+ fail = true;
+ }
+ }
+ }
+
+ if (!found)
+ ksft_print_msg("%s implements no settable status bits\n",
+ ctl->name);
+
+ return !fail;
+}
+
+/*
+ * With nothing advertised all we can do is try non audio, and the device
+ * need not implement even that. Write it once and ignore the result, then
+ * compare against what came back so the value cannot fail, which leaves the
+ * notification counted by event_missing.
+ */
+static bool test_ctl_write_valid_iec958_guess(struct ctl_data *ctl,
+ snd_ctl_elem_value_t *orig_val)
+{
+ int err;
+ snd_aes_iec958_t iec958;
+ snd_ctl_elem_value_t *val, *read_val, *w_val;
+ snd_ctl_elem_value_alloca(&val);
+ snd_ctl_elem_value_alloca(&read_val);
+ snd_ctl_elem_value_alloca(&w_val);
+
+ snd_ctl_elem_value_get_iec958(orig_val, &iec958);
+ iec958.status[0] ^= IEC958_AES0_NONAUDIO;
+ snd_ctl_elem_value_copy(val, orig_val);
+ snd_ctl_elem_value_set_iec958(val, &iec958);
+
+ /* Writing can modify the value so keep a copy to write from */
+ snd_ctl_elem_value_copy(w_val, val);
+ err = snd_ctl_elem_write(ctl->card->handle, w_val);
+ if (err < 0) {
+ ksft_print_msg("snd_ctl_elem_write() failed: %s\n",
+ snd_strerror(err));
+ return false;
+ }
+
+ snd_ctl_elem_value_set_id(read_val, ctl->id);
+ err = snd_ctl_elem_read(ctl->card->handle, read_val);
+ if (err < 0) {
+ ksft_print_msg("snd_ctl_elem_read() failed: %s\n",
+ snd_strerror(err));
+ return false;
+ }
+
+ /* Put it back where we found it, then drop the events from both */
+ snd_ctl_elem_value_copy(w_val, orig_val);
+ err = snd_ctl_elem_write(ctl->card->handle, w_val);
+ if (err < 0) {
+ ksft_print_msg("snd_ctl_elem_write() failed: %s\n",
+ snd_strerror(err));
+ return false;
+ }
+
+ err = drop_events(ctl);
+ if (err < 0) {
+ ksft_print_msg("drop_events() failed for %s: %d\n",
+ ctl->name, err);
+ return false;
+ }
+
+ if (snd_ctl_elem_value_compare(val, read_val)) {
+ ksft_print_msg("%s does not implement status[0] 0x%02x\n",
+ ctl->name, IEC958_AES0_NONAUDIO);
+
+ /* Expect what came back, the notification is still checked */
+ return write_and_verify(ctl, val, read_val) == 0;
+ }
+
+ return write_and_verify(ctl, val, NULL) == 0;
+}
+
+/*
+ * Write the channel status bits and check that userspace is told about it.
+ * Where a mask was found every bit in it has to stick, otherwise we fall
+ * back to one guessed bit the device is free to ignore.
+ */
+static bool test_ctl_write_valid_iec958(struct ctl_data *ctl)
+{
+ int err;
+ snd_aes_iec958_t iec958, mask;
+ snd_ctl_elem_value_t *orig_val;
+ snd_ctl_elem_value_alloca(&orig_val);
+
+ /*
+ * The bytes past the ones a driver implements are compared too,
+ * so start from a read rather than building a value here.
+ */
+ snd_ctl_elem_value_set_id(orig_val, ctl->id);
+ err = snd_ctl_elem_read(ctl->card->handle, orig_val);
+ if (err < 0) {
+ ksft_print_msg("snd_ctl_elem_read() failed: %s\n",
+ snd_strerror(err));
+ return false;
+ }
+
+ snd_ctl_elem_value_get_iec958(orig_val, &iec958);
+
+ if (!read_iec958_mask(ctl, &iec958, &mask)) {
+ ksft_print_msg("%s falling back to non audio\n", ctl->name);
+ return test_ctl_write_valid_iec958_guess(ctl, orig_val);
+ }
+
+ return test_ctl_write_valid_iec958_mask(ctl, orig_val, &mask);
+}
+
static void test_ctl_write_valid(struct ctl_data *ctl)
{
bool pass;
@@ -878,6 +1165,10 @@ static void test_ctl_write_valid(struct ctl_data *ctl)
pass = test_ctl_write_valid_enumerated(ctl);
break;
+ case SND_CTL_ELEM_TYPE_IEC958:
+ pass = test_ctl_write_valid_iec958(ctl);
+ break;
+
default:
/* No tests for this yet */
ksft_test_result_skip("write_valid.%s.%d\n",
@@ -892,6 +1183,202 @@ static void test_ctl_write_valid(struct ctl_data *ctl)
ctl->card->card_name, ctl->elem);
}
+/*
+ * Build the smallest or the largest value the control offers. The smallest
+ * clears the control's register field and the largest sets its top bit, which
+ * is the bit a mask one bit too wide puts in its neighbour.
+ */
+static bool set_limit_value(struct ctl_data *ctl, snd_ctl_elem_value_t *val,
+ bool max)
+{
+ int i, count = snd_ctl_elem_info_get_count(ctl->info);
+
+ snd_ctl_elem_value_set_id(val, ctl->id);
+
+ switch (snd_ctl_elem_info_get_type(ctl->info)) {
+ case SND_CTL_ELEM_TYPE_BOOLEAN:
+ for (i = 0; i < count; i++)
+ snd_ctl_elem_value_set_boolean(val, i, max);
+ return true;
+
+ case SND_CTL_ELEM_TYPE_INTEGER:
+ for (i = 0; i < count; i++)
+ snd_ctl_elem_value_set_integer(val, i, max ?
+ snd_ctl_elem_info_get_max(ctl->info) :
+ snd_ctl_elem_info_get_min(ctl->info));
+ return true;
+
+ case SND_CTL_ELEM_TYPE_INTEGER64:
+ for (i = 0; i < count; i++)
+ snd_ctl_elem_value_set_integer64(val, i, max ?
+ snd_ctl_elem_info_get_max64(ctl->info) :
+ snd_ctl_elem_info_get_min64(ctl->info));
+ return true;
+
+ case SND_CTL_ELEM_TYPE_ENUMERATED:
+ for (i = 0; i < count; i++)
+ snd_ctl_elem_value_set_enumerated(val, i, max ?
+ snd_ctl_elem_info_get_items(ctl->info) - 1 : 0);
+ return true;
+
+ default:
+ /* Nothing sensible to write for the rest */
+ return false;
+ }
+}
+
+/* Note every control on the card that no longer reads as it did */
+static void find_moved_ctls(struct ctl_data *ctl, snd_ctl_elem_value_t *val)
+{
+ struct ctl_data *other;
+ int err;
+
+ for (other = ctl_list; other != NULL; other = other->next) {
+ if (!other->snapshot_valid)
+ continue;
+
+ /*
+ * The buffer is shared and compare() looks at all of it, so
+ * clear what the last control left in the slots this one
+ * does not use.
+ */
+ snd_ctl_elem_value_clear(val);
+ snd_ctl_elem_value_set_id(val, other->id);
+ err = snd_ctl_elem_read(ctl->card->handle, val);
+ if (err < 0) {
+ ksft_print_msg("snd_ctl_elem_read() failed for %s: %s\n",
+ other->name, snd_strerror(err));
+ continue;
+ }
+
+ if (snd_ctl_elem_value_compare(other->snapshot, val))
+ other->moved = true;
+ }
+}
+
+/*
+ * Write one control and look for others on the same card that moved with it.
+ * A driver that links two controls on purpose tells userspace about both, so
+ * only an unannounced change is counted. That is what a control whose
+ * register mask covers bits belonging to its neighbour looks like from here.
+ */
+static void test_ctl_write_side_effects(struct ctl_data *ctl)
+{
+ struct ctl_data *other;
+ snd_ctl_elem_value_t *min_val, *max_val, *read_val;
+ int err;
+
+ snd_ctl_elem_value_alloca(&min_val);
+ snd_ctl_elem_value_alloca(&max_val);
+ snd_ctl_elem_value_alloca(&read_val);
+
+ /* Without a readable default there is nothing to put back */
+ if (snd_ctl_elem_info_is_inactive(ctl->info) ||
+ !snd_ctl_elem_info_is_writable(ctl->info) ||
+ !snd_ctl_elem_info_is_readable(ctl->info) ||
+ !set_limit_value(ctl, min_val, false) ||
+ !set_limit_value(ctl, max_val, true)) {
+ ksft_test_result_skip("write_side_effects.%s.%d\n",
+ ctl->card->card_name, ctl->elem);
+ return;
+ }
+
+ /* Drain first, a stale event would look like the driver announced it */
+ drop_events(ctl);
+
+ /*
+ * Record what the rest of the card reads as. A volatile control can
+ * move on its own so there is nothing to compare it against.
+ */
+ for (other = ctl_list; other != NULL; other = other->next) {
+ other->snapshot_valid = false;
+ other->moved = false;
+ other->ev_mask = 0;
+
+ if (other == ctl || other->card != ctl->card)
+ continue;
+ if (!snd_ctl_elem_info_is_readable(other->info) ||
+ snd_ctl_elem_info_is_volatile(other->info))
+ continue;
+
+ snd_ctl_elem_value_clear(other->snapshot);
+ snd_ctl_elem_value_set_id(other->snapshot, other->id);
+ err = snd_ctl_elem_read(ctl->card->handle, other->snapshot);
+ if (err < 0) {
+ ksft_print_msg("snd_ctl_elem_read() failed for %s: %s\n",
+ other->name, snd_strerror(err));
+ continue;
+ }
+
+ other->snapshot_valid = true;
+ }
+
+ /*
+ * Compare against the snapshot after each write, before anything is
+ * put back. Restoring the control we wrote goes through the same
+ * mask, so doing it first would hide the change we are looking for.
+ */
+ err = snd_ctl_elem_write(ctl->card->handle, min_val);
+ if (err >= 0) {
+ drop_events(ctl);
+ find_moved_ctls(ctl, read_val);
+ err = snd_ctl_elem_write(ctl->card->handle, max_val);
+ }
+ if (err < 0) {
+ ksft_print_msg("snd_ctl_elem_write() failed for %s: %s\n",
+ ctl->name, snd_strerror(err));
+ } else {
+ drop_events(ctl);
+ find_moved_ctls(ctl, read_val);
+ }
+
+ for (other = ctl_list; other != NULL; other = other->next) {
+ if (!other->moved)
+ continue;
+
+ if (other->ev_mask & SND_CTL_EVENT_MASK_VALUE) {
+ ksft_print_msg("Writing %s changed %s, the driver said so\n",
+ ctl->name, other->name);
+ } else {
+ ksft_print_msg("Writing %s silently changed %s\n",
+ ctl->name, other->name);
+ ctl->side_effects++;
+ }
+ }
+
+ /*
+ * The control we wrote goes back first so its mask stops moving the
+ * rest. A plain write keeps this out of the event counters, they
+ * belong to the tests that check them.
+ */
+ snd_ctl_elem_write(ctl->card->handle, ctl->def_val);
+
+ for (other = ctl_list; other != NULL; other = other->next) {
+ if (!other->snapshot_valid ||
+ !snd_ctl_elem_info_is_writable(other->info))
+ continue;
+
+ snd_ctl_elem_value_clear(read_val);
+ snd_ctl_elem_value_set_id(read_val, other->id);
+ if (snd_ctl_elem_read(ctl->card->handle, read_val) < 0)
+ continue;
+
+ if (snd_ctl_elem_value_compare(other->snapshot, read_val))
+ snd_ctl_elem_write(ctl->card->handle, other->snapshot);
+ }
+
+ /* Our own restores queue events, the next test must not see them */
+ drop_events(ctl);
+
+ if (err < 0)
+ ksft_test_result_skip("write_side_effects.%s.%d\n",
+ ctl->card->card_name, ctl->elem);
+ else
+ ksft_test_result(!ctl->side_effects,
+ "write_side_effects.%s.%d\n",
+ ctl->card->card_name, ctl->elem);
+}
+
static bool test_ctl_write_invalid_value(struct ctl_data *ctl,
snd_ctl_elem_value_t *val)
{
@@ -1135,6 +1622,7 @@ int main(void)
test_ctl_name(ctl);
test_ctl_write_default(ctl);
test_ctl_write_valid(ctl);
+ test_ctl_write_side_effects(ctl);
test_ctl_write_invalid(ctl);
test_ctl_event_missing(ctl);
test_ctl_event_spurious(ctl);
diff --git a/tools/testing/selftests/alsa/utimer-test.c b/tools/testing/selftests/alsa/utimer-test.c
index 1a9ff010cb11..07aeaa777649 100644
--- a/tools/testing/selftests/alsa/utimer-test.c
+++ b/tools/testing/selftests/alsa/utimer-test.c
@@ -51,6 +51,9 @@ FIXTURE_SETUP(timer_f) {
self->utimer_info->resolution = (NANO / FRAME_RATE * PERIOD_SIZE);
timer_dev_fd = open("/dev/snd/timer", O_RDONLY);
+ if (timer_dev_fd < 0 &&
+ (errno == ENOENT || errno == ENODEV || errno == ENXIO))
+ SKIP(return, "ALSA timer device unavailable");
ASSERT_GE(timer_dev_fd, 0);
if (ioctl(timer_dev_fd, SNDRV_TIMER_IOCTL_CREATE, self->utimer_info) < 0) {
@@ -156,6 +159,9 @@ TEST(wrong_timers_test) {
};
timer_dev_fd = open("/dev/snd/timer", O_RDONLY);
+ if (timer_dev_fd < 0 &&
+ (errno == ENOENT || errno == ENODEV || errno == ENXIO))
+ SKIP(return, "ALSA timer device unavailable");
ASSERT_GE(timer_dev_fd, 0);
utimer_fd = ioctl(timer_dev_fd, SNDRV_TIMER_IOCTL_CREATE, &wrong_timer);