selftests: alsa - add PCM test

This initial code does a simple sample transfer tests. By default,
all PCM devices are detected and tested with short and long
buffering parameters for 4 seconds. If the sample transfer timing
is not in a +-100ms boundary, the test fails. Only the interleaved
buffering scheme is supported in this version.

The configuration may be modified with the configuration files.
A specific hardware configuration is detected and activated
using the sysfs regex matching. This allows to use the DMI string
(/sys/class/dmi/id/* tree) or any other system parameters
exposed in sysfs for the matching for the CI automation.

The configuration file may also specify the PCM device list to detect
the missing PCM devices.

v1..v2:
  - added missing alsa-local.h header file

Cc: Mark Brown <broonie@kernel.org>
Cc: Pierre-Louis Bossart <pierre-louis.bossart@intel.com>
Cc: Liam Girdwood <liam.r.girdwood@intel.com>
Cc: Jesse Barnes <jsbarnes@google.com>
Cc: Jimmy Cheng-Yi Chiang <cychiang@google.com>
Cc: Curtis Malainey <cujomalainey@google.com>
Cc: Brian Norris <briannorris@chromium.org>
Signed-off-by: Jaroslav Kysela <perex@perex.cz>
Reviewed-by: Mark Brown <broonie@kernel.org>
Link: https://lore.kernel.org/r/20221108115914.3751090-1-perex@perex.cz
Signed-off-by: Takashi Iwai <tiwai@suse.de>
This commit is contained in:
Jaroslav Kysela
2022-11-16 15:02:17 +01:00
committed by Takashi Iwai
parent 3827597a89
commit aba51cd094
5 changed files with 1003 additions and 1 deletions
+3 -1
View File
@@ -7,6 +7,8 @@ ifeq ($(LDLIBS),)
LDLIBS += -lasound
endif
TEST_GEN_PROGS := mixer-test
TEST_GEN_PROGS := mixer-test pcm-test
pcm-test: pcm-test.c conf.c
include ../lib.mk
+22
View File
@@ -0,0 +1,22 @@
// SPDX-License-Identifier: GPL-2.0
//
// kselftest configuration helpers for the hw specific configuration
//
// Original author: Jaroslav Kysela <perex@perex.cz>
// Copyright (c) 2022 Red Hat Inc.
#ifndef __ALSA_LOCAL_H
#define __ALSA_LOCAL_H
#include <alsa/asoundlib.h>
void conf_load(void);
void conf_free(void);
snd_config_t *conf_by_card(int card);
snd_config_t *conf_get_subtree(snd_config_t *root, const char *key1, const char *key2);
int conf_get_count(snd_config_t *root, const char *key1, const char *key2);
const char *conf_get_string(snd_config_t *root, const char *key1, const char *key2, const char *def);
long conf_get_long(snd_config_t *root, const char *key1, const char *key2, long def);
int conf_get_bool(snd_config_t *root, const char *key1, const char *key2, int def);
#endif /* __ALSA_LOCAL_H */
+379
View File
@@ -0,0 +1,379 @@
// SPDX-License-Identifier: GPL-2.0
//
// kselftest configuration helpers for the hw specific configuration
//
// Original author: Jaroslav Kysela <perex@perex.cz>
// Copyright (c) 2022 Red Hat Inc.
#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
#include <errno.h>
#include <assert.h>
#include <dirent.h>
#include <regex.h>
#include <sys/stat.h>
#include "../kselftest.h"
#include "alsa-local.h"
#define SYSFS_ROOT "/sys"
struct card_data {
int card;
snd_config_t *config;
const char *filename;
struct card_data *next;
};
static struct card_data *conf_cards;
static struct card_data *conf_data_by_card(int card, bool msg)
{
struct card_data *conf;
for (conf = conf_cards; conf; conf = conf->next) {
if (conf->card == card) {
if (msg)
ksft_print_msg("using hw card config %s for card %d\n",
conf->filename, card);
return conf;
}
}
return NULL;
}
static int dump_config_tree(snd_config_t *top)
{
snd_output_t *out;
int err;
err = snd_output_stdio_attach(&out, stdout, 0);
if (err < 0)
ksft_exit_fail_msg("stdout attach\n");
if (snd_config_save(top, out))
ksft_exit_fail_msg("config save\n");
snd_output_close(out);
}
static snd_config_t *load(const char *filename)
{
snd_config_t *dst;
snd_input_t *input;
int err;
err = snd_input_stdio_open(&input, filename, "r");
if (err < 0)
ksft_exit_fail_msg("Unable to parse filename %s\n", filename);
err = snd_config_top(&dst);
if (err < 0)
ksft_exit_fail_msg("Out of memory\n");
err = snd_config_load(dst, input);
snd_input_close(input);
if (err < 0)
ksft_exit_fail_msg("Unable to parse filename %s\n", filename);
return dst;
}
static char *sysfs_get(const char *sysfs_root, const char *id)
{
char path[PATH_MAX], link[PATH_MAX + 1];
struct stat sb;
ssize_t len;
char *e;
int fd;
if (id[0] == '/')
id++;
snprintf(path, sizeof(path), "%s/%s", sysfs_root, id);
if (lstat(path, &sb) != 0)
return NULL;
if (S_ISLNK(sb.st_mode)) {
len = readlink(path, link, sizeof(link) - 1);
if (len <= 0) {
ksft_exit_fail_msg("sysfs: cannot read link '%s': %s\n",
path, strerror(errno));
return NULL;
}
link[len] = '\0';
e = strrchr(link, '/');
if (e)
return strdup(e + 1);
return NULL;
}
if (S_ISDIR(sb.st_mode))
return NULL;
if ((sb.st_mode & S_IRUSR) == 0)
return NULL;
fd = open(path, O_RDONLY);
if (fd < 0) {
if (errno == ENOENT)
return NULL;
ksft_exit_fail_msg("sysfs: open failed for '%s': %s\n",
path, strerror(errno));
}
len = read(fd, path, sizeof(path)-1);
close(fd);
if (len < 0)
ksft_exit_fail_msg("sysfs: unable to read value '%s': %s\n",
path, errno);
while (len > 0 && path[len-1] == '\n')
len--;
path[len] = '\0';
e = strdup(path);
if (e == NULL)
ksft_exit_fail_msg("Out of memory\n");
return e;
}
static bool sysfs_match(const char *sysfs_root, snd_config_t *config)
{
snd_config_t *node, *path_config, *regex_config;
snd_config_iterator_t i, next;
const char *path_string, *regex_string, *v;
regex_t re;
regmatch_t match[1];
int iter = 0, ret;
snd_config_for_each(i, next, config) {
node = snd_config_iterator_entry(i);
if (snd_config_search(node, "path", &path_config))
ksft_exit_fail_msg("Missing path field in the sysfs block\n");
if (snd_config_search(node, "regex", &regex_config))
ksft_exit_fail_msg("Missing regex field in the sysfs block\n");
if (snd_config_get_string(path_config, &path_string))
ksft_exit_fail_msg("Path field in the sysfs block is not a string\n");
if (snd_config_get_string(regex_config, &regex_string))
ksft_exit_fail_msg("Regex field in the sysfs block is not a string\n");
iter++;
v = sysfs_get(sysfs_root, path_string);
if (!v)
return false;
if (regcomp(&re, regex_string, REG_EXTENDED))
ksft_exit_fail_msg("Wrong regex '%s'\n", regex_string);
ret = regexec(&re, v, 1, match, 0);
regfree(&re);
if (ret)
return false;
}
return iter > 0;
}
static bool test_filename1(int card, const char *filename, const char *sysfs_card_root)
{
struct card_data *data, *data2;
snd_config_t *config, *sysfs_config, *card_config, *sysfs_card_config, *node;
snd_config_iterator_t i, next;
config = load(filename);
if (snd_config_search(config, "sysfs", &sysfs_config) ||
snd_config_get_type(sysfs_config) != SND_CONFIG_TYPE_COMPOUND)
ksft_exit_fail_msg("Missing global sysfs block in filename %s\n", filename);
if (snd_config_search(config, "card", &card_config) ||
snd_config_get_type(card_config) != SND_CONFIG_TYPE_COMPOUND)
ksft_exit_fail_msg("Missing global card block in filename %s\n", filename);
if (!sysfs_match(SYSFS_ROOT, sysfs_config))
return false;
snd_config_for_each(i, next, card_config) {
node = snd_config_iterator_entry(i);
if (snd_config_search(node, "sysfs", &sysfs_card_config) ||
snd_config_get_type(sysfs_card_config) != SND_CONFIG_TYPE_COMPOUND)
ksft_exit_fail_msg("Missing card sysfs block in filename %s\n", filename);
if (!sysfs_match(sysfs_card_root, sysfs_card_config))
continue;
data = malloc(sizeof(*data));
if (!data)
ksft_exit_fail_msg("Out of memory\n");
data2 = conf_data_by_card(card, false);
if (data2)
ksft_exit_fail_msg("Duplicate card '%s' <-> '%s'\n", filename, data2->filename);
data->card = card;
data->filename = filename;
data->config = node;
data->next = conf_cards;
conf_cards = data;
return true;
}
return false;
}
static bool test_filename(const char *filename)
{
char fn[128];
int card;
for (card = 0; card < 32; card++) {
snprintf(fn, sizeof(fn), "%s/class/sound/card%d", SYSFS_ROOT, card);
if (access(fn, R_OK) == 0 && test_filename1(card, filename, fn))
return true;
}
return false;
}
static int filename_filter(const struct dirent *dirent)
{
size_t flen;
if (dirent == NULL)
return 0;
if (dirent->d_type == DT_DIR)
return 0;
flen = strlen(dirent->d_name);
if (flen <= 5)
return 0;
if (strncmp(&dirent->d_name[flen-5], ".conf", 5) == 0)
return 1;
return 0;
}
void conf_load(void)
{
const char *fn = "conf.d";
struct dirent **namelist;
int n, j;
n = scandir(fn, &namelist, filename_filter, alphasort);
if (n < 0)
ksft_exit_fail_msg("scandir: %s\n", strerror(errno));
for (j = 0; j < n; j++) {
size_t sl = strlen(fn) + strlen(namelist[j]->d_name) + 2;
char *filename = malloc(sl);
if (filename == NULL)
ksft_exit_fail_msg("Out of memory\n");
sprintf(filename, "%s/%s", fn, namelist[j]->d_name);
if (test_filename(filename))
filename = NULL;
free(filename);
free(namelist[j]);
}
free(namelist);
}
void conf_free(void)
{
struct card_data *conf;
while (conf_cards) {
conf = conf_cards;
conf_cards = conf->next;
snd_config_delete(conf->config);
}
}
snd_config_t *conf_by_card(int card)
{
struct card_data *conf;
conf = conf_data_by_card(card, true);
if (conf)
return conf->config;
return NULL;
}
static int conf_get_by_keys(snd_config_t *root, const char *key1,
const char *key2, snd_config_t **result)
{
int ret;
if (key1) {
ret = snd_config_search(root, key1, &root);
if (ret != -ENOENT && ret < 0)
return ret;
}
if (key2)
ret = snd_config_search(root, key2, &root);
if (ret >= 0)
*result = root;
return ret;
}
snd_config_t *conf_get_subtree(snd_config_t *root, const char *key1, const char *key2)
{
int ret;
if (!root)
return NULL;
ret = conf_get_by_keys(root, key1, key2, &root);
if (ret == -ENOENT)
return NULL;
if (ret < 0)
ksft_exit_fail_msg("key '%s'.'%s' search error: %s\n", key1, key2, snd_strerror(ret));
return root;
}
int conf_get_count(snd_config_t *root, const char *key1, const char *key2)
{
snd_config_t *cfg;
snd_config_iterator_t i, next;
int count, ret;
if (!root)
return -1;
ret = conf_get_by_keys(root, key1, key2, &cfg);
if (ret == -ENOENT)
return -1;
if (ret < 0)
ksft_exit_fail_msg("key '%s'.'%s' search error: %s\n", key1, key2, snd_strerror(ret));
if (snd_config_get_type(cfg) != SND_CONFIG_TYPE_COMPOUND)
ksft_exit_fail_msg("key '%s'.'%s' is not a compound\n", key1, key2);
count = 0;
snd_config_for_each(i, next, cfg)
count++;
return count;
}
const char *conf_get_string(snd_config_t *root, const char *key1, const char *key2, const char *def)
{
snd_config_t *cfg;
const char *s;
int ret;
if (!root)
return def;
ret = conf_get_by_keys(root, key1, key2, &cfg);
if (ret == -ENOENT)
return def;
if (ret < 0)
ksft_exit_fail_msg("key '%s'.'%s' search error: %s\n", key1, key2, snd_strerror(ret));
if (snd_config_get_string(cfg, &s))
ksft_exit_fail_msg("key '%s'.'%s' is not a string\n", key1, key2);
return s;
}
long conf_get_long(snd_config_t *root, const char *key1, const char *key2, long def)
{
snd_config_t *cfg;
long l;
int ret;
if (!root)
return def;
ret = conf_get_by_keys(root, key1, key2, &cfg);
if (ret == -ENOENT)
return def;
if (ret < 0)
ksft_exit_fail_msg("key '%s'.'%s' search error: %s\n", key1, key2, snd_strerror(ret));
if (snd_config_get_integer(cfg, &l))
ksft_exit_fail_msg("key '%s'.'%s' is not an integer\n", key1, key2);
return l;
}
int conf_get_bool(snd_config_t *root, const char *key1, const char *key2, int def)
{
snd_config_t *cfg;
long l;
int ret;
if (!root)
return def;
ret = conf_get_by_keys(root, key1, key2, &cfg);
if (ret == -ENOENT)
return def;
if (ret < 0)
ksft_exit_fail_msg("key '%s'.'%s' search error: %s\n", key1, key2, snd_strerror(ret));
ret = snd_config_get_bool(cfg);
if (ret < 0)
ksft_exit_fail_msg("key '%s'.'%s' is not an bool\n", key1, key2);
return !!ret;
}
@@ -0,0 +1,76 @@
#
# Example configuration for Lenovo ThinkPad P1 Gen2
#
#
# Use regex match for the string read from the given sysfs path
#
# The sysfs root directory (/sys) is hardwired in the test code
# (may be changed on demand).
#
# All strings must match.
#
sysfs [
{
path "class/dmi/id/product_sku"
regex "LENOVO_MT_20QU_BU_Think_FM_ThinkPad P1 Gen 2"
}
]
card.hda {
#
# Use regex match for the /sys/class/sound/card*/ tree (relative)
#
sysfs [
{
path "device/subsystem_device"
regex "0x229e"
}
{
path "device/subsystem_vendor"
regex "0x17aa"
}
]
#
# PCM configuration
#
# pcm.0.0 - device 0 subdevice 0
#
pcm.0.0 {
PLAYBACK {
test.time1 {
access RW_INTERLEAVED # can be omitted - default
format S16_LE # can be omitted - default
rate 48000 # can be omitted - default
channels 2 # can be omitted - default
period_size 512
buffer_size 4096
}
test.time2 {
access RW_INTERLEAVED
format S16_LE
rate 48000
channels 2
period_size 24000
buffer_size 192000
}
}
CAPTURE {
# use default tests, check for the presence
}
}
#
# uncomment to force the missing device checks
#
#pcm.0.2 {
# PLAYBACK {
# # check for the presence
# }
#}
#pcm.0.3 {
# CAPTURE {
# # check for the presence
# }
#}
}
File diff suppressed because it is too large Load Diff