-
Lucas De Marchi authored
Deduplicate cleanup so it's also easy to parse the line in a different way. Reviewed-by:
Gustavo Sousa <gustavo.sousa@intel.com> Tested-by:
Peter Senna Tschudin <peter.senna@linux.intel.com> Reviewed-by:
Peter Senna Tschudin <peter.senna@linux.intel.com> Link: https://lore.kernel.org/r/20250207231039.2883195-5-lucas.demarchi@intel.com Signed-off-by:
Lucas De Marchi <lucas.demarchi@intel.com>
Lucas De Marchi authoredDeduplicate cleanup so it's also easy to parse the line in a different way. Reviewed-by:
Gustavo Sousa <gustavo.sousa@intel.com> Tested-by:
Peter Senna Tschudin <peter.senna@linux.intel.com> Reviewed-by:
Peter Senna Tschudin <peter.senna@linux.intel.com> Link: https://lore.kernel.org/r/20250207231039.2883195-5-lucas.demarchi@intel.com Signed-off-by:
Lucas De Marchi <lucas.demarchi@intel.com>
settings.c 36.23 KiB
#include "igt_hook.h"
#include "igt_vec.h"
#include "settings.h"
#include "version.h"
#include <ctype.h>
#include <errno.h>
#include <fcntl.h>
#include <getopt.h>
#include <libgen.h>
#include <limits.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/utsname.h>
#include <unistd.h>
enum {
OPT_ABORT_ON_ERROR,
OPT_DISK_USAGE_LIMIT,
OPT_TEST_LIST,
OPT_IGNORE_MISSING,
OPT_PIGLIT_DMESG,
OPT_DMESG_WARN_LEVEL,
OPT_OVERALL_TIMEOUT,
OPT_PER_TEST_TIMEOUT,
OPT_ALLOW_NON_ROOT,
OPT_CODE_COV_SCRIPT,
OPT_ENABLE_CODE_COVERAGE,
OPT_COV_RESULTS_PER_TEST,
OPT_HOOK,
OPT_HELP_HOOK,
OPT_VERSION,
OPT_PRUNE_MODE,
OPT_HELP = 'h',
OPT_NAME = 'n',
OPT_DRY_RUN = 'd',
OPT_INCLUDE = 't',
OPT_EXCLUDE = 'x',
OPT_ENVIRONMENT = 'e',
OPT_FACTS = 'f',
OPT_SYNC = 's',
OPT_LOG_LEVEL = 'l',
OPT_OVERWRITE = 'o',
OPT_MULTIPLE = 'm',
OPT_TIMEOUT = 'c',
OPT_WATCHDOG = 'g',
OPT_BLACKLIST = 'b',
OPT_LIST_ALL = 'L',
};
static struct {
int level;
const char *name;
} log_levels[] = {
{ LOG_LEVEL_NORMAL, "normal" },
{ LOG_LEVEL_QUIET, "quiet" },
{ LOG_LEVEL_VERBOSE, "verbose" },
{ 0, 0 },
};
static struct {
int value;
const char *name;
} abort_conditions[] = {
{ ABORT_TAINT, "taint" },
{ ABORT_LOCKDEP, "lockdep" },
{ ABORT_PING, "ping" },
{ ABORT_ALL, "all" },
{ 0, 0 },
};
static struct {
int value;
const char *name;
} prune_modes[] = {
{ PRUNE_KEEP_DYNAMIC, "keep-dynamic-subtests" },
{ PRUNE_KEEP_DYNAMIC, "keep-dynamic" },
{ PRUNE_KEEP_SUBTESTS, "keep-subtests" },
{ PRUNE_KEEP_ALL, "keep-all" },
{ PRUNE_KEEP_REQUESTED, "keep-requested" },
{ 0, 0 },
};
static const char settings_filename[] = "metadata.txt";
static const char env_filename[] = "environment.txt";
static const char hooks_filename[] = "hooks.txt";
static bool set_log_level(struct settings* settings, const char *level)
{
typeof(*log_levels) *it;
for (it = log_levels; it->name; it++) {
if (!strcmp(level, it->name)) {
settings->log_level = it->level;
return true;
}
}
return false;
}
static bool set_abort_condition(struct settings* settings, const char *cond)
{
typeof(*abort_conditions) *it;
if (!cond) {
settings->abort_mask = ABORT_ALL;
return true;
}
if (strlen(cond) == 0) {
settings->abort_mask = 0;
return true;
}
for (it = abort_conditions; it->name; it++) {
if (!strcmp(cond, it->name)) {
settings->abort_mask |= it->value;
return true;
}
}
return false;
}
static bool set_prune_mode(struct settings* settings, const char *mode)
{
typeof(*prune_modes) *it;
for (it = prune_modes; it->name; it++) {
if (!strcmp(mode, it->name)) {
settings->prune_mode = it->value;
return true;
}
}
return false;
}
static bool parse_abort_conditions(struct settings *settings, const char *optarg)
{
char *dup, *origdup, *p;
if (!optarg)
return set_abort_condition(settings, NULL);
origdup = dup = strdup(optarg);
while (dup) {
if ((p = strchr(dup, ',')) != NULL) {
*p = '\0';
p++;
}
if (!set_abort_condition(settings, dup)) {
free(origdup);
return false;
}
dup = p;
}
free(origdup);
return true;
}
static size_t char_to_multiplier(char c)
{
switch (c) {
case 'k':
case 'K':
return 1024UL;
case 'm':
case 'M':
return 1024UL * 1024UL;
case 'g':
case 'G':
return 1024UL * 1024UL * 1024UL;
}
return 0;
}
static bool parse_usage_limit(struct settings *settings, const char *optarg)
{
size_t value;
char *endptr = NULL;
if (!optarg)
return false;
value = strtoul(optarg, &endptr, 10);
if (*endptr) {
size_t multiplier = char_to_multiplier(*endptr);
if (multiplier == 0)
return false;
value *= multiplier;
}
settings->disk_usage_limit = value;
return true;
}
static const char *usage_str =
"usage: runner [options] [test_root] results-path\n"
" or: runner --list-all [options] [test_root]\n\n"
"Options:\n"
" Piglit compatible:\n"
" -h, --help Show this help message and exit\n"
" -n <test name>, --name <test name>\n"
" Name of this test run\n"
" -d, --dry-run Do not execute the tests\n"
" -t <regex>, --include-tests <regex>\n"
" Run only matching tests (can be used more than once)\n"
" -x <regex>, --exclude-tests <regex>\n"
" Exclude matching tests (can be used more than once)\n"
" --abort-on-monitored-error[=list]\n"
" Abort execution when a fatal condition is detected.\n"
" A comma-separated list of conditions to check can be\n"
" given. If not given, all conditions are checked. An\n"
" empty string as a condition disables aborting\n"
" Possible conditions:\n"
" lockdep - abort when kernel lockdep has been angered.\n"
" taint - abort when kernel becomes fatally tainted.\n"
" ping - abort when a host configured in .igtrc or\n"
" environment variable IGT_PING_HOSTNAME does\n"
" not respond to ping.\n"
" all - abort for all of the above.\n"
" -f, --facts Enable facts tracking\n"
" -s, --sync Sync results to disk after every test\n"
" -l {quiet,verbose,dummy}, --log-level {quiet,verbose,dummy}\n"
" Set the logger verbosity level\n"
" --test-list TEST_LIST\n"
" A file containing a list of tests to run\n"
" -o, --overwrite If the results-path already exists, delete it\n"
" --ignore-missing Ignored but accepted, for piglit compatibility\n"
"\n"
" Incompatible options:\n"
" --allow-non-root Allow running tests without being the root user.\n"
" -m, --multiple-mode Run multiple subtests in the same binary execution.\n"
" If a testlist file is given, consecutive subtests are\n"
" run in the same execution if they are from the same\n"
" binary. Note that in that case relative ordering of the\n"
" subtest execution is dictated by the test binary, not\n"
" the testlist\n"
" --inactivity-timeout <seconds>\n"
" Kill the running test after <seconds> of inactivity in\n"
" the test's stdout, stderr, or dmesg\n"
" --per-test-timeout <seconds>\n"
" Kill the running test after <seconds>. This timeout is per\n"
" subtest, or dynamic subtest. In other words, every subtest,\n"
" even when running in multiple-mode, must finish in <seconds>.\n"
" --overall-timeout <seconds>\n"
" Don't execute more tests after <seconds> has elapsed\n"
" --disk-usage-limit <limit>\n"
" Kill the running test if its logging, both itself and the\n"
" kernel logs, exceed the given limit in bytes. The limit\n"
" parameter can use suffixes k, M and G for kilo/mega/gigabytes,\n"
" respectively. Limit of 0 (default) disables the limit.\n"
" --use-watchdog Use hardware watchdog for lethal enforcement of the\n"
" above timeout. Killing the test process is still\n"
" attempted at timeout trigger.\n"
" --dmesg-warn-level <level>\n"
" Messages with log level equal or lower (more serious)\n"
" to the given one will override the test result to\n"
" dmesg-warn/dmesg-fail, assuming they go through filtering.\n"
" Defaults to 4 (KERN_WARNING).\n"
" --piglit-style-dmesg Filter dmesg like piglit does. Piglit considers matches\n"
" against a short filter list to mean the test result\n"
" should be changed to dmesg-warn/dmesg-fail. Without\n"
" this option everything except matches against a\n"
" (longer) filter list means the test result should\n"
" change. KERN_NOTICE dmesg level is treated as warn,\n"
" unless overridden with --dmesg-warn-level.\n"
" --prune-mode <mode> Control reporting of dynamic subtests by selecting test\n"
" results that are removed from the final results set.\n"
" Possible options:\n"
" keep-dynamic-subtests - Remove subtests that have dynamic\n"
" subtests.\n"
" keep-dynamic - Alias for the above\n"
" keep-subtests - Remove dynamic subtests,\n"
" leaving just the parent subtest.\n"
" keep-all - Don't remove anything (default)\n"
" keep-requested - Remove reported results that are\n"
" not in the requested test set.\n"
" Useful when you have a hand-written\n"
" testlist.\n"
" -b, --blacklist FILENAME\n"
" Exclude all test matching to regexes from FILENAME\n"
" (can be used more than once)\n"
" -e, --environment <KEY or KEY=VALUE>\n"
" Set an environment variable for the test process.\n"
" If only the key is provided, the current value is read\n"
" from the runner's environment (and saved for resumes).\n"
" -L, --list-all List all matching subtests instead of running\n"
" --collect-code-cov Enables gcov-based collect of code coverage for tests.\n"
" Requires --collect-script FILENAME\n"
" --coverage-per-test Stores code coverage results per each test.\n"
" Requires --collect-script FILENAME\n"
" --collect-script FILENAME\n"
" Use FILENAME as script to collect code coverage data.\n"
" --hook HOOK_STR\n"
" Forward HOOK_STR to the --hook option of each test.\n"
" --help-hook\n"
" Show detailed usage information for --hook.\n"
"\n"
" [test_root] Directory that contains the IGT tests. The environment\n"
" variable IGT_TEST_ROOT will be used if set, overriding\n"
" this option if given.\n"
;
__attribute__ ((format (printf, 2, 3)))
static void usage(FILE *f, const char *extra_message, ...)
{
if (extra_message) {
va_list args;
va_start(args, extra_message);
vfprintf(f, extra_message, args);
fputs("\n\n", f);
va_end(args);
}
fputs(usage_str, f);
}
static bool add_regex(struct regex_list *list, char *new)
{
GRegex *regex;
GError *error = NULL;
regex = g_regex_new(new, G_REGEX_CASELESS | G_REGEX_OPTIMIZE, 0, &error);
if (error) {
usage(stderr, "Invalid regex '%s': %s", new, error->message);
g_error_free(error);
return false;
}
list->regexes = realloc(list->regexes,
(list->size + 1) * sizeof(*list->regexes));
list->regex_strings = realloc(list->regex_strings,
(list->size + 1) * sizeof(*list->regex_strings));
list->regexes[list->size] = regex;
list->regex_strings[list->size] = new;
list->size++;
return true;
}
static bool parse_blacklist(struct regex_list *exclude_regexes,
char *blacklist_filename)
{
FILE *f;
char *line = NULL;
size_t line_len = 0;
bool status = false;
if ((f = fopen(blacklist_filename, "r")) == NULL) {
fprintf(stderr, "Cannot open blacklist file %s\n", blacklist_filename);
return false;
}
while (1) {
size_t str_size = 0, idx = 0;
if (getline(&line, &line_len, f) == -1) {
if (errno == EINTR)
continue;
else
break;
}
while (true) {
if (line[idx] == '\n' ||
line[idx] == '\0' ||
line[idx] == '#') /* # starts a comment */
break;
if (!isspace(line[idx]))
str_size = idx + 1;
idx++;
}
if (str_size > 0) {
char *test_regex = strndup(line, str_size);
status = add_regex(exclude_regexes, test_regex);
if (!status)
break;
}
}
free(line);
fclose(f);
return status;
}
static void free_regexes(struct regex_list *regexes)
{
size_t i;
for (i = 0; i < regexes->size; i++) {
free(regexes->regex_strings[i]);
g_regex_unref(regexes->regexes[i]);
}
free(regexes->regex_strings);
free(regexes->regexes);
}
/**
* string_trim_and_duplicate() - returns a duplicated, trimmed string
* @string: string to trim and duplicate
*
* If the provided string is NULL, a NULL is returned. In any other case, a
* newly-allocated string of length up to strlen(string) is returned. The
* returned string has its whitespace removed (as detected by isspace), while
* the original string is left unmodified.
*/
static char *string_trim_and_duplicate(const char *string) {
size_t length;
if (string == NULL)
return NULL;
length = strlen(string);
while (length > 0 && isspace(string[0])) {
string++;
length--;
}
while (length > 0 && isspace(string[length - 1])) {
length--;
}
return strndup(string, length);
}
/**
* add_env_var() - Adds a new environment variable to the runner settings.
* @env_vars: Pointer to the env var list head from the settings.
* @key_value: Environment variable key-value pair string to add.
*
* key_value must be a string like "KEY=VALUE" or just "KEY" if the value is to
* be loaded from the runner's environment variables. In the latter case, if
* the requested variable is not set, the operation will fail.
*
* An empty variable may be set by providing an key_value of "KEY=" or setting
* an empty variable for the runner process, then providing just the "KEY".
*/
static bool add_env_var(struct igt_list_head *env_vars, const char *key_value) {
char *env_kv, *value_str;
struct environment_variable *var = NULL;
if (env_vars == NULL || key_value == NULL || strlen(key_value) == 0)
goto error;
env_kv = strdup(key_value);
value_str = strpbrk(env_kv, "\n=");
if (value_str == env_kv) {
fprintf(stderr, "Missing key for --environment \"%s\"\n", key_value);
goto error;
}
if (value_str != NULL && value_str[0] != '=') {
fprintf(stderr, "Invalid characters in key for --environment \"%s\"\n", key_value);
goto error;
}
if (value_str != NULL) {
/* value provided - copy string contents after '=' */
value_str[0] = '\0';
value_str++;
} else {
/* env_kv is the key - use the runner's environment, if set */
value_str = getenv(env_kv);
if (value_str == NULL) {
fprintf(stderr, "No value provided for --environment \"%s\" and "
"variable is not set for igt_runner\n", env_kv);
goto error;
}
}
var = malloc(sizeof(struct environment_variable));
var->key = string_trim_and_duplicate(env_kv);
if (strlen(var->key) == 0) {
fprintf(stderr, "Environment variable key is empty for \"%s\"\n", key_value);
goto error;
}
var->value = strdup(value_str); /* Can be empty, that's okay */
igt_list_add_tail(&var->link, env_vars);
free(env_kv);
return true;
error:
if (var != NULL)
free(var);
if (env_kv != NULL)
free(env_kv);
return false;
}
static void free_env_vars(struct igt_list_head *env_vars) {
struct environment_variable *iter;
while (!igt_list_empty(env_vars)) {
iter = igt_list_first_entry(env_vars, iter, link);
free(iter->key);
free(iter->value);
igt_list_del(&iter->link);
free(iter);
}
}
static void free_hook_strs(struct igt_vec *hook_strs)
{
for (size_t i = 0; i < igt_vec_length(hook_strs); i++)
free(*((char **)igt_vec_elem(hook_strs, i)));
igt_vec_fini(hook_strs);
}
static bool file_exists_at(int dirfd, const char *filename)
{
return faccessat(dirfd, filename, F_OK, 0) == 0;
}
static bool readable_file(const char *filename)
{
return !access(filename, R_OK);
}
static bool writeable_file(const char *filename)
{
return !access(filename, W_OK);
}
static bool executable_file(const char *filename)
{
return !access(filename, X_OK);
}
static char *_dirname(const char *path)
{
char *tmppath = strdup(path);
char *tmpname = dirname(tmppath);
tmpname = strdup(tmpname);
free(tmppath);
return tmpname;
}
static char *_basename(const char *path)
{
char *tmppath = strdup(path);
char *tmpname = basename(tmppath);
tmpname = strdup(tmpname);
free(tmppath);
return tmpname;
}
char *absolute_path(char *path)
{
char *result = NULL;
char *base, *dir;
char *ret;
result = realpath(path, NULL);
if (result != NULL)
return result;
dir = _dirname(path);
ret = absolute_path(dir);
free(dir);
base = _basename(path);
asprintf(&result, "%s/%s", ret, base);
free(base);
free(ret);
return result;
}
static char *bin_path(char *fname)
{
char *path, *p;
char file[PATH_MAX];
if (strchr(fname, '/'))
return absolute_path(fname);
path = strdup(getenv("PATH"));
p = strtok(path, ":");
do {
if (*p) {
strcpy(file, p);
strcat(file, "/");
strcat(file, fname);
if (executable_file(file)) {
free(path);
return strdup(file);
}
}
p = strtok(NULL, ":");
} while (p);
free(path);
return strdup(fname);
}
static void print_version(void)
{
struct utsname uts;
uname(&uts);
printf("IGT-Version: %s-%s (%s) (%s: %s %s)\n", PACKAGE_VERSION,
IGT_GIT_SHA1, TARGET_CPU_PLATFORM,
uts.sysname, uts.release, uts.machine);
}
void init_settings(struct settings *settings)
{
memset(settings, 0, sizeof(*settings));
IGT_INIT_LIST_HEAD(&settings->env_vars);
igt_vec_init(&settings->hook_strs, sizeof(char *));
}
void clear_settings(struct settings *settings)
{
free(settings->test_list);
free(settings->name);
free(settings->test_root);
free(settings->results_path);
free(settings->code_coverage_script);
free_regexes(&settings->include_regexes);
free_regexes(&settings->exclude_regexes);
free_env_vars(&settings->env_vars);
free_hook_strs(&settings->hook_strs);
init_settings(settings);
}
bool parse_options(int argc, char **argv,
struct settings *settings)
{
int c;
char *env_test_root;
char *hook_str;
static struct option long_options[] = {
{"version", no_argument, NULL, OPT_VERSION},
{"help", no_argument, NULL, OPT_HELP},
{"name", required_argument, NULL, OPT_NAME},
{"dry-run", no_argument, NULL, OPT_DRY_RUN},
{"allow-non-root", no_argument, NULL, OPT_ALLOW_NON_ROOT},
{"include-tests", required_argument, NULL, OPT_INCLUDE},
{"exclude-tests", required_argument, NULL, OPT_EXCLUDE},
{"environment", required_argument, NULL, OPT_ENVIRONMENT},
{"abort-on-monitored-error", optional_argument, NULL, OPT_ABORT_ON_ERROR},
{"disk-usage-limit", required_argument, NULL, OPT_DISK_USAGE_LIMIT},
{"facts", no_argument, NULL, OPT_FACTS},
{"sync", no_argument, NULL, OPT_SYNC},
{"log-level", required_argument, NULL, OPT_LOG_LEVEL},
{"test-list", required_argument, NULL, OPT_TEST_LIST},
{"overwrite", no_argument, NULL, OPT_OVERWRITE},
{"ignore-missing", no_argument, NULL, OPT_IGNORE_MISSING},
{"collect-code-cov", no_argument, NULL, OPT_ENABLE_CODE_COVERAGE},
{"coverage-per-test", no_argument, NULL, OPT_COV_RESULTS_PER_TEST},
{"collect-script", required_argument, NULL, OPT_CODE_COV_SCRIPT},
{"hook", required_argument, NULL, OPT_HOOK},
{"help-hook", no_argument, NULL, OPT_HELP_HOOK},
{"multiple-mode", no_argument, NULL, OPT_MULTIPLE},
{"inactivity-timeout", required_argument, NULL, OPT_TIMEOUT},
{"per-test-timeout", required_argument, NULL, OPT_PER_TEST_TIMEOUT},
{"overall-timeout", required_argument, NULL, OPT_OVERALL_TIMEOUT},
{"use-watchdog", no_argument, NULL, OPT_WATCHDOG},
{"piglit-style-dmesg", no_argument, NULL, OPT_PIGLIT_DMESG},
{"dmesg-warn-level", required_argument, NULL, OPT_DMESG_WARN_LEVEL},
{"prune-mode", required_argument, NULL, OPT_PRUNE_MODE},
{"blacklist", required_argument, NULL, OPT_BLACKLIST},
{"list-all", no_argument, NULL, OPT_LIST_ALL},
{ 0, 0, 0, 0},
};
clear_settings(settings);
optind = 1;
settings->dmesg_warn_level = -1;
settings->prune_mode = -1;
while ((c = getopt_long(argc, argv, "hn:dt:x:e:fsl:omb:L",
long_options, NULL)) != -1) {
switch (c) {
case OPT_VERSION:
print_version();
goto error;
case OPT_HELP:
usage(stdout, NULL);
goto error;
case OPT_NAME:
settings->name = strdup(optarg);
break;
case OPT_DRY_RUN:
settings->dry_run = true;
settings->allow_non_root = true;
break;
case OPT_ALLOW_NON_ROOT:
settings->allow_non_root = true;
break;
case OPT_INCLUDE:
if (!add_regex(&settings->include_regexes, strdup(optarg)))
goto error;
break;
case OPT_EXCLUDE:
if (!add_regex(&settings->exclude_regexes, strdup(optarg)))
goto error;
break;
case OPT_ENVIRONMENT:
if (!add_env_var(&settings->env_vars, optarg))
goto error;
break;
case OPT_ABORT_ON_ERROR:
if (!parse_abort_conditions(settings, optarg))
goto error;
break;
case OPT_DISK_USAGE_LIMIT:
if (!parse_usage_limit(settings, optarg)) {
usage(stderr, "Cannot parse disk usage limit");
goto error;
}
break;
case OPT_FACTS:
settings->facts = true;
break;
case OPT_SYNC:
settings->sync = true;
break;
case OPT_LOG_LEVEL:
if (!set_log_level(settings, optarg)) {
usage(stderr, "Cannot parse log level");
goto error;
}
break;
case OPT_TEST_LIST:
settings->test_list = absolute_path(optarg);
break;
case OPT_OVERWRITE:
settings->overwrite = true;
break;
case OPT_IGNORE_MISSING:
/* Ignored, piglit compatibility */
break;
case OPT_ENABLE_CODE_COVERAGE:
settings->enable_code_coverage = true;
break;
case OPT_COV_RESULTS_PER_TEST:
settings->cov_results_per_test = true;
break;
case OPT_CODE_COV_SCRIPT:
settings->code_coverage_script = bin_path(optarg);
break;
case OPT_HOOK:
hook_str = strdup(optarg);
igt_vec_push(&settings->hook_strs, &hook_str);
break;
case OPT_HELP_HOOK:
igt_hook_print_help(stdout, "--hook");
goto error;
case OPT_MULTIPLE:
settings->multiple_mode = true;
break;
case OPT_TIMEOUT:
settings->inactivity_timeout = atoi(optarg);
break;
case OPT_PER_TEST_TIMEOUT:
settings->per_test_timeout = atoi(optarg);
break;
case OPT_OVERALL_TIMEOUT:
settings->overall_timeout = atoi(optarg);
break;
case OPT_WATCHDOG:
settings->use_watchdog = true;
break;
case OPT_PIGLIT_DMESG:
settings->piglit_style_dmesg = true;
if (settings->dmesg_warn_level < 0)
settings->dmesg_warn_level = 5; /* KERN_NOTICE */
break;
case OPT_DMESG_WARN_LEVEL:
settings->dmesg_warn_level = atoi(optarg);
break;
case OPT_PRUNE_MODE:
if (!set_prune_mode(settings, optarg)) {
usage(stderr, "Cannot parse prune mode");
goto error;
}
break;
case OPT_BLACKLIST:
if (!parse_blacklist(&settings->exclude_regexes,
absolute_path(optarg)))
goto error;
break;
case OPT_LIST_ALL:
settings->list_all = true;
break;
case '?':
usage(stderr, NULL);
goto error;
default:
usage(stderr, "Cannot parse options");
goto error;
}
}
if (settings->dmesg_warn_level < 0)
settings->dmesg_warn_level = 4; /* KERN_WARN */
if (settings->prune_mode < 0)
settings->prune_mode = PRUNE_KEEP_ALL;
if (settings->list_all) { /* --list-all doesn't require results path */
switch (argc - optind) {
case 1:
settings->test_root = absolute_path(argv[optind]);
++optind;
/* fallthrough */
case 0:
break;
default:
usage(stderr, "Too many arguments for --list-all");
goto error;
}
} else {
switch (argc - optind) {
case 2:
settings->test_root = absolute_path(argv[optind]);
++optind;
/* fallthrough */
case 1:
settings->results_path = absolute_path(argv[optind]);
break;
case 0:
usage(stderr, "Results-path missing");
goto error;
default:
usage(stderr, "Extra arguments after results-path");
goto error;
}
if (!settings->name) {
char *name = strdup(settings->results_path);
settings->name = strdup(basename(name));
free(name);
}
}
if ((env_test_root = getenv("IGT_TEST_ROOT")) != NULL) {
free(settings->test_root);
settings->test_root = absolute_path(env_test_root);
}
if (!settings->test_root) {
usage(stderr, "Test root not set");
goto error;
}
return true;
error:
clear_settings(settings);
return false;
}
bool validate_settings(struct settings *settings)
{
int dirfd, fd;
if (settings->test_list && !readable_file(settings->test_list)) {
usage(stderr, "Cannot open test-list file");
return false;
}
if (!settings->results_path) {
usage(stderr, "No results-path set; this shouldn't happen");
return false;
}
if (!settings->test_root) {
usage(stderr, "No test root set; this shouldn't happen");
return false;
}
dirfd = open(settings->test_root, O_DIRECTORY | O_RDONLY);
if (dirfd < 0) {
fprintf(stderr, "Test directory %s cannot be opened\n", settings->test_root);
return false;
}
fd = openat(dirfd, "test-list.txt", O_RDONLY);
if (fd < 0) {
fprintf(stderr, "Cannot open %s/test-list.txt\n", settings->test_root);
close(dirfd);
return false;
}
close(fd);
close(dirfd);
/* enables code coverage when --coverage-per-test is used */
if (settings->cov_results_per_test)
settings->enable_code_coverage = true;
if (!settings->allow_non_root && (getuid() != 0)) {
fprintf(stderr, "Runner needs to run with UID 0 (root).\n");
return false;
}
if (settings->enable_code_coverage) {
if (!executable_file(settings->code_coverage_script)) {
fprintf(stderr, "%s doesn't exist or is not executable\n", settings->code_coverage_script);
return false;
}
if (!writeable_file(GCOV_RESET)) {
if (getuid() != 0)
fprintf(stderr, "Code coverage requires root.\n");
else
fprintf(stderr, "Is GCOV enabled? Can't access %s stat.\n", GCOV_RESET);
return false;
}
}
return true;
}
/**
* fopenat() - wrapper for fdopen(openat(dirfd, filename))
* @dirfd: directory fd to pass to openat
* @filename: file name to open with openat
* @flags: flags to pass to openat
* @openat_mode: (octal) mode to pass to openat
* @fdopen_mode: (text) mode to pass to fdopen
*/
static FILE *fopenat(int dirfd, const char *filename, int flags, mode_t openat_mode, const char *fdopen_mode)
{
int fd;
FILE *f;
if ((fd = openat(dirfd, filename, flags, openat_mode)) < 0) {
usage(stderr, "fopenat(%s, %d) failed: %s",
filename, flags, strerror(errno));
return NULL;
}
f = fdopen(fd, fdopen_mode);
if (!f) {
close(fd);
return NULL;
}
return f;
}
static FILE *fopenat_read(int dirfd, const char *filename)
{
return fopenat(dirfd, filename, O_RDONLY, 0000, "r");
}
static FILE *fopenat_create(int dirfd, const char *filename, bool overwrite)
{
mode_t mode_if_exists = overwrite ? O_TRUNC : O_EXCL;
return fopenat(dirfd, filename, O_CREAT | O_RDWR | mode_if_exists, 0666, "w");
}
static bool serialize_environment(struct settings *settings, int dirfd)
{
struct environment_variable *iter;
FILE *f;
if (file_exists_at(dirfd, env_filename) && !settings->overwrite) {
usage(stderr, "%s already exists, not overwriting", env_filename);
return false;
}
if ((f = fopenat_create(dirfd, env_filename, settings->overwrite)) == NULL)
return false;
igt_list_for_each_entry(iter, &settings->env_vars, link) {
fprintf(f, "%s=%s\n", iter->key, iter->value);
}
if (settings->sync) {
fflush(f);
fsync(fileno(f));
}
fclose(f);
return true;
}
static bool serialize_hook_strs(struct settings *settings, int dirfd)
{
FILE *f;
if (file_exists_at(dirfd, hooks_filename) && !settings->overwrite) {
usage(stderr, "%s already exists, not overwriting", hooks_filename);
return false;
}
if ((f = fopenat_create(dirfd, hooks_filename, settings->overwrite)) == NULL)
return false;
for (size_t i = 0; i < igt_vec_length(&settings->hook_strs); i++) {
const char *s = *((char **)igt_vec_elem(&settings->hook_strs, i));
size_t len;
while (*s) {
len = strcspn(s, "\\\n");
if (len > 0)
fwrite(s, len, 1, f);
s += len;
if (!*s)
break;
fputc('\\', f);
fputc(*s, f);
s++;
}
fputc('\n', f);
fputc('\n', f);
}
if (settings->sync) {
fflush(f);
fsync(fileno(f));
}
fclose(f);
return true;
}
bool serialize_settings(struct settings *settings)
{
#define SERIALIZE_LINE(f, s, name, format) fprintf(f, "%s : " format "\n", #name, s->name)
FILE *f;
int dirfd, covfd;
char path[PATH_MAX];
if (!settings->results_path) {
usage(stderr, "No results-path set; this shouldn't happen");
return false;
}
if ((dirfd = open(settings->results_path, O_DIRECTORY | O_RDONLY)) < 0) {
mkdir(settings->results_path, 0755);
if ((dirfd = open(settings->results_path, O_DIRECTORY | O_RDONLY)) < 0) {
usage(stderr, "Creating results-path failed");
return false;
}
}
if (settings->enable_code_coverage) {
strcpy(path, settings->results_path);
strcat(path, CODE_COV_RESULTS_PATH);
if ((covfd = open(path, O_DIRECTORY | O_RDONLY)) < 0) {
if (mkdir(path, 0755)) {
usage(stderr, "Creating code coverage path failed");
return false;
}
} else {
close(covfd);
}
}
if (file_exists_at(dirfd, settings_filename) && !settings->overwrite) {
usage(stderr, "%s already exists, not overwriting", settings_filename);
close(dirfd);
return false;
}
if ((f = fopenat_create(dirfd, settings_filename, settings->overwrite)) == NULL) {
close(dirfd);
return false;
}
SERIALIZE_LINE(f, settings, abort_mask, "%d");
SERIALIZE_LINE(f, settings, disk_usage_limit, "%zd");
if (settings->test_list)
SERIALIZE_LINE(f, settings, test_list, "%s");
if (settings->name)
SERIALIZE_LINE(f, settings, name, "%s");
SERIALIZE_LINE(f, settings, dry_run, "%d");
SERIALIZE_LINE(f, settings, allow_non_root, "%d");
SERIALIZE_LINE(f, settings, facts, "%d");
SERIALIZE_LINE(f, settings, sync, "%d");
SERIALIZE_LINE(f, settings, log_level, "%d");
SERIALIZE_LINE(f, settings, overwrite, "%d");
SERIALIZE_LINE(f, settings, multiple_mode, "%d");
SERIALIZE_LINE(f, settings, inactivity_timeout, "%d");
SERIALIZE_LINE(f, settings, per_test_timeout, "%d");
SERIALIZE_LINE(f, settings, overall_timeout, "%d");
SERIALIZE_LINE(f, settings, use_watchdog, "%d");
SERIALIZE_LINE(f, settings, piglit_style_dmesg, "%d");
SERIALIZE_LINE(f, settings, dmesg_warn_level, "%d");
SERIALIZE_LINE(f, settings, prune_mode, "%d");
SERIALIZE_LINE(f, settings, test_root, "%s");
SERIALIZE_LINE(f, settings, results_path, "%s");
SERIALIZE_LINE(f, settings, enable_code_coverage, "%d");
SERIALIZE_LINE(f, settings, cov_results_per_test, "%d");
SERIALIZE_LINE(f, settings, code_coverage_script, "%s");
if (settings->sync) {
fflush(f);
fsync(fileno(f));
}
fclose(f);
if (!igt_list_empty(&settings->env_vars)) {
if (!serialize_environment(settings, dirfd)) {
close(dirfd);
return false;
}
}
if (igt_vec_length(&settings->hook_strs)) {
if (!serialize_hook_strs(settings, dirfd)) {
close(dirfd);
return false;
}
}
if (settings->sync)
fsync(dirfd);
close(dirfd);
return true;
#undef SERIALIZE_LINE
}
bool read_settings_from_file(struct settings *settings, FILE *f)
{
#define PARSE_LINE(s, name, val, field, write) \
if (!strcmp(name, #field)) { \
s->field = write; \
goto cleanup; \
}
char *name = NULL, *val = NULL;
settings->dmesg_warn_level = -1;
while (fscanf(f, "%ms : %m[^\n]", &name, &val) == 2) {
int numval = atoi(val);
PARSE_LINE(settings, name, val, abort_mask, numval);
PARSE_LINE(settings, name, val, disk_usage_limit, strtoul(val, NULL, 10));
PARSE_LINE(settings, name, val, test_list, val ? strdup(val) : NULL);
PARSE_LINE(settings, name, val, name, val ? strdup(val) : NULL);
PARSE_LINE(settings, name, val, dry_run, numval);
PARSE_LINE(settings, name, val, allow_non_root, numval);
PARSE_LINE(settings, name, val, facts, numval);
PARSE_LINE(settings, name, val, sync, numval);
PARSE_LINE(settings, name, val, log_level, numval);
PARSE_LINE(settings, name, val, overwrite, numval);
PARSE_LINE(settings, name, val, multiple_mode, numval);
PARSE_LINE(settings, name, val, inactivity_timeout, numval);
PARSE_LINE(settings, name, val, per_test_timeout, numval);
PARSE_LINE(settings, name, val, overall_timeout, numval);
PARSE_LINE(settings, name, val, use_watchdog, numval);
PARSE_LINE(settings, name, val, piglit_style_dmesg, numval);
PARSE_LINE(settings, name, val, dmesg_warn_level, numval);
PARSE_LINE(settings, name, val, prune_mode, numval);
PARSE_LINE(settings, name, val, test_root, val ? strdup(val) : NULL);
PARSE_LINE(settings, name, val, results_path, val ? strdup(val) : NULL);
PARSE_LINE(settings, name, val, enable_code_coverage, numval);
PARSE_LINE(settings, name, val, cov_results_per_test, numval);
PARSE_LINE(settings, name, val, code_coverage_script, val ? strdup(val) : NULL);
printf("Warning: Unknown field in settings file: %s = %s\n",
name, val);
cleanup:
free(name);
free(val);
name = val = NULL;
}
if (settings->dmesg_warn_level < 0) {
if (settings->piglit_style_dmesg)
settings->dmesg_warn_level = 5;
else
settings->dmesg_warn_level = 4;
}
free(name);
free(val);
return true;
#undef PARSE_LINE
}
/**
* read_env_vars_from_file() - load env vars from a file
* @env_vars: list head to add env var entries to
* @f: opened file stream to read
*
* Loads the environment.txt file and adds each line-separated KEY=VALUE pair
* into the provided env_vars list. Lines not containing the '=' K-V separator,
* starting with a '#' (comments) or '=' (missing keys) are ignored.
*
* The function returns true if the env vars were read successfully, or there
* was nothing to be read.
*/
static bool read_env_vars_from_file(struct igt_list_head *env_vars, FILE *f)
{
char *line = NULL;
ssize_t line_length;
size_t line_buffer_length = 0;
while ((line_length = getline(&line, &line_buffer_length, f)) != -1) {
char *line_ptr = line;
while (isspace(*line_ptr)) {
line_length--;
line_ptr++;
}
if (*line_ptr == '\0' || *line_ptr == '#' || *line_ptr == '=')
continue; /* Empty, whitespace, comment or missing key */
if (strchr(line_ptr, '=') == NULL)
continue; /* Missing value */
/* Strip the line feed, but keep trailing whitespace */
if (line_length > 0 && line[line_length - 1] == '\n')
line[line_length - 1] = '\0';
add_env_var(env_vars, line);
}
/* input file can be empty */
if (line != NULL)
free(line);
return true;
}
static bool read_hook_strs_from_file(struct igt_vec *hook_strs, FILE *f)
{
char *line = NULL;
ssize_t line_length;
size_t line_size = 0;
char *buf;
size_t buf_len = 0;
size_t buf_capacity = 128;
buf = malloc(buf_capacity);
while ((line_length = getline(&line, &line_size, f) != -1)) {
char *s = line;
if (buf_len == 0) {
while (isspace(*s)) {
line_length--;
s++;
}
if (*s == '\0' || *s == '#')
continue;
}
if (line_length + 1 > buf_capacity - buf_len) {
while (line_length + 1 > buf_capacity - buf_len)
buf_capacity *= 2;
buf = realloc(buf, buf_capacity);
}
while (true) {
if (*s == '\0' || *s == '\n') {
char *buf_copy;
if (!buf_len)
break;
/* Reached the end of a hook string. */
buf[buf_len] = '\0';
buf_copy = strdup(buf);
igt_vec_push(hook_strs, &buf_copy);
buf_len = 0;
break;
}
if (*s == '\\') {
s++;
if (*s == '\0')
/* Weird case of backslash being the
* last character of the file. */
s--;
}
buf[buf_len++] = *s++;
if (*s == '\0' || *s == '\n')
break;
}
}
if (buf_len) {
char *buf_copy;
buf[buf_len] = '\0';
buf_copy = strdup(buf);
igt_vec_push(hook_strs, &buf_copy);
}
free(buf);
free(line);
return true;
}
bool read_settings_from_dir(struct settings *settings, int dirfd)
{
FILE *f;
clear_settings(settings);
/* settings are always there */
if ((f = fopenat_read(dirfd, settings_filename)) == NULL)
return false;
if (!read_settings_from_file(settings, f)) {
fclose(f);
return false;
}
fclose(f);
/* env file may not exist if no --environment was set */
if (file_exists_at(dirfd, env_filename)) {
if ((f = fopenat_read(dirfd, env_filename)) == NULL)
return false;
if (!read_env_vars_from_file(&settings->env_vars, f)) {
fclose(f);
return false;
}
fclose(f);
}
/* hooks file may not exist if no --hook was passed */
if (file_exists_at(dirfd, hooks_filename)) {
if ((f = fopenat_read(dirfd, hooks_filename)) == NULL)
return false;
if (!read_hook_strs_from_file(&settings->hook_strs, f)) {
fclose(f);
return false;
}
fclose(f);
}
return true;
}