Commit aba51cd0 authored by Jaroslav Kysela's avatar Jaroslav Kysela Committed by Takashi Iwai

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: default avatarJaroslav Kysela <perex@perex.cz>
Reviewed-by: default avatarMark Brown <broonie@kernel.org>
Link: https://lore.kernel.org/r/20221108115914.3751090-1-perex@perex.czSigned-off-by: default avatarTakashi Iwai <tiwai@suse.de>
parent 3827597a
...@@ -7,6 +7,8 @@ ifeq ($(LDLIBS),) ...@@ -7,6 +7,8 @@ ifeq ($(LDLIBS),)
LDLIBS += -lasound LDLIBS += -lasound
endif endif
TEST_GEN_PROGS := mixer-test TEST_GEN_PROGS := mixer-test pcm-test
pcm-test: pcm-test.c conf.c
include ../lib.mk include ../lib.mk
// 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 */
// 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;
}
#
# 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
# }
#}
}
This diff is collapsed.
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment