diff options
| author | Takashi Iwai <tiwai@suse.de> | 2026-10-19 10:39:44 +0200 |
|---|---|---|
| committer | Takashi Iwai <tiwai@suse.de> | 2026-10-19 10:40:00 +0200 |
| commit | 7e246ac8bed799402b743550d283bdd202048fa7 (patch) | |
| tree | d6dc95e8d6f96a581ec2c37fcaea09911a4099a5 /tools/testing | |
| parent | 8e8cd9a4c6dc006e9bd51b8ad0079fb4576b48d3 (diff) | |
| parent | 40409fa23d32c8ed51e832031daa6decd9d6fd11 (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/.gitignore | 1 | ||||
| -rw-r--r-- | tools/testing/selftests/alsa/Makefile | 2 | ||||
| -rw-r--r-- | tools/testing/selftests/alsa/aloop-test.c | 345 | ||||
| -rw-r--r-- | tools/testing/selftests/alsa/mixer-test.c | 490 | ||||
| -rw-r--r-- | tools/testing/selftests/alsa/utimer-test.c | 6 |
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); |
