Igt-dev Archive on lore.kernel.org
 help / color / mirror / Atom feed
From: Daniel Vetter <daniel@ffwll.ch>
To: Petri Latvala <petri.latvala@intel.com>
Cc: igt-dev@lists.freedesktop.org,
	Tomi Sarvela <tomi.p.sarvela@intel.com>,
	Martin Peres <martin.peres@linux.intel.com>
Subject: Re: [igt-dev] [PATCH i-g-t 3/4] runner: New test runner
Date: Mon, 7 May 2018 20:34:16 +0200	[thread overview]
Message-ID: <20180507183416.GH28661@phenom.ffwll.local> (raw)
In-Reply-To: <20180430092848.10830-4-petri.latvala@intel.com>

On Mon, Apr 30, 2018 at 12:28:47PM +0300, Petri Latvala wrote:
> This is a new test runner to replace piglit. Piglit has been very
> useful as a test runner, but certain improvements have been very
> difficult if possible at all in a generic test running framework.
> 
> Important improvements over piglit:
> 
> - Faster to launch. Being able to make assumptions about what we're
>   executing makes it possible to save significant amounts of time. For
>   example, a testlist file's line "igt@somebinary@somesubtest" already
>   has all the information we need to construct the correct command
>   line to execute that particular subtest, instead of listing all
>   subtests of all test binaries and mapping them to command
>   lines. Same goes for the regexp filters command line flags -t and
>   -x; If we use -x somebinaryname, we don't need to list subtests from
>   somebinaryname, we already know none of them will get executed.
> 
> - Logs of incomplete tests. Piglit collects test output to memory and
>   dumps them to a file when the test is complete. The new runner
>   writes all output to disk immediately.
> 
> - Ability to execute multiple subtests in one binary execution. This
>   was possible with piglit, but its semantics made it very hard to
>   implement in practice. For example, having a testlist file not only
>   selected a subset of tests to run, but also mandated that they be
>   executed in the same order.
> 
> - Flexible timeout support. Instead of mandating a time tests cannot
>   exceed, the new runner has a timeout on inactivity. Activity is
>   any output on the test's stdout or stderr, or kernel activity via
>   /dev/kmsg.
> 
> The runner is fairly piglit compatible. The command line is very
> similar, with a few additions. IGT_TEST_ROOT environment flag is still
> supported, but can also be set via command line (in place of igt.py in
> piglit command line).
> 
> The results are a set of log files, processed into a piglit-compatible
> results.json file (BZ2 compression TODO). There are some new fields in
> the json for extra information:
> 
> - "igt-version" contains the IGT version line. In
>   multiple-subtests-mode the version information is only printed once,
>   so it needs to be duplicated to all subtest results this way.
> - "dmesg-warnings" contains the dmesg lines that triggered a
>   dmesg-warn/dmesg-fail state.

Hm, I thought agreement was that if we do a new test runner then we
definitely don't want it to capture&analyze dmesg? That's why I've
originally done

https://patchwork.freedesktop.org/series/39263/

which you then took over. Is that still part of the overall plan, just not
yet there?
-Daniel

> - Runtime information will be different. Piglit takes a timestamp at
>   the beginning and at the end of execution for runtime. The new
>   runner uses the subtest output text. The binary execution time will
>   also be included; The key "igt@somebinary" will have the runtime of
>   the binary "somebinary", whereas "igt@somebinary@a" etc will have
>   the runtime of the subtests. Substracting the subtest runtimes from
>   the binary runtime yields the total time spent doing setup in
>   igt_fixture blocks.
> 
> Signed-off-by: Petri Latvala <petri.latvala@intel.com>
> Cc: Maarten Lankhorst <maarten.lankhorst@linux.intel.com>
> Cc: Arkadiusz Hiler <arkadiusz.hiler@intel.com>
> Cc: Tomi Sarvela <tomi.p.sarvela@intel.com>
> Cc: Martin Peres <martin.peres@linux.intel.com>
> ---

[ snip]

> +/*
> + * This regexp controls the kmsg handling. All kernel log records that
> + * have log level of warning or higher convert the result to
> + * dmesg-warn/dmesg-fail unless they match this regexp.
> + */
> +
> +#define _ "|"
> +static const char igt_dmesg_whitelist[] =
> +	"ACPI: button: The lid device is not compliant to SW_LID" _
> +	"ACPI: .*: Unable to dock!" _
> +	"IRQ [0-9]+: no longer affine to CPU[0-9]+" _
> +	"IRQ fixup: irq [0-9]+ move in progress, old vector [0-9]+" _
> +	/* i915 tests set module options, expected message */
> +	"Setting dangerous option [a-z_]+ - tainting kernel" _
> +	/* Raw printk() call, uses default log level (warn) */
> +	"Suspending console\\(s\\) \\(use no_console_suspend to debug\\)" _
> +	"atkbd serio[0-9]+: Failed to (deactivate|enable) keyboard on isa[0-9]+/serio[0-9]+" _
> +	"cache: parent cpu[0-9]+ should not be sleeping" _
> +	"hpet[0-9]+: lost [0-9]+ rtc interrupts" _
> +	/* i915 selftests terminate normally with ENODEV from the
> +	 * module load after the testing finishes, which produces this
> +	 * message.
> +	 */
> +	"i915: probe of [0-9:.]+ failed with error -25" _
> +	/* swiotbl warns even when asked not to */
> +	"mock: DMA: Out of SW-IOMMU space for [0-9]+ bytes" _
> +	"usb usb[0-9]+: root hub lost power or was reset"

The above really isn't stuff we should have in the runner :-)
-Daniel

> +	;
> +#undef _
> +
> +static regex_t re;
> +
> +static bool init_regex_whitelist()
> +{
> +	int status = -1;
> +
> +	if (status == -1) {
> +		if (regcomp(&re, igt_dmesg_whitelist, REG_EXTENDED | REG_NOSUB) != 0) {
> +			fprintf(stderr, "Cannot compile dmesg whitelist regexp\n");
> +			status = 0;
> +			return false;
> +		}
> +
> +		status = 1;
> +	}
> +
> +	return status;
> +}
> +
> +static bool fill_from_dmesg(int fd, char *binary,
> +			    struct subtests *subtests,
> +			    struct json_object *tests)
> +{
> +	char *line = NULL, *warnings = NULL, *dmesg = NULL;
> +	size_t linelen = 0, warningslen = 0, dmesglen = 0;
> +	struct json_object *current_test = NULL;
> +	FILE *f = fdopen(fd, "r");
> +	char *igt_name = NULL;
> +	ssize_t read;
> +	size_t i;
> +
> +	if (!f) {
> +		return false;
> +	}
> +
> +	if (init_regex_whitelist() != 1) {
> +		fclose(f);
> +		return false;
> +	}
> +
> +	while ((read = getline(&line, &linelen, f)) > 0) {
> +		char formatted[256];
> +		unsigned flags;
> +		unsigned long long seq, ts_usec;
> +		char continuation;
> +		char *message, *subtest;
> +		int s;
> +
> +		s = sscanf(line, "%u,%llu,%llu,%c;", &flags, &seq, &ts_usec, &continuation);
> +
> +		if (s != 4) {
> +			/*
> +			 * Machine readable key/value pairs begin with
> +			 * a space. We ignore them.
> +			 */
> +			if (line[0] != ' ') {
> +				fprintf(stderr, "Cannot parse kmsg record: %s\n", line);
> +			}
> +
> +			continue;
> +		}
> +
> +		message = strchr(line, ';');
> +		if (!message) {
> +			fprintf(stderr, "No ; found, this shouldn't happen\n");
> +			return false;
> +		}
> +		message++;
> +		snprintf(formatted, sizeof(formatted), "<%u> [%llu.%06llu] %s",
> +			 flags & 0x07, ts_usec / 1000000, ts_usec % 1000000, message);
> +
> +		if ((subtest = strstr(message, starting_subtest_dmesg)) != NULL) {
> +			if (current_test != NULL) {
> +				/* Done with the previous subtest, file up */
> +				json_object_object_add(current_test, "dmesg",
> +						       json_object_new_string_len(dmesg, dmesglen));
> +				if (warnings) {
> +					json_object_object_add(current_test, "dmesg-warnings",
> +							       json_object_new_string_len(warnings, warningslen));
> +					add_or_override_result(current_test, "pass", "dmesg-warn");
> +					add_or_override_result(current_test, "fail", "dmesg-fail");
> +					add_or_override_result(current_test, "warn", "dmesg-warn");
> +				}
> +				free(dmesg);
> +				free(warnings);
> +				dmesg = warnings = NULL;
> +				dmesglen = warningslen = 0;
> +			}
> +
> +			subtest += starting_subtest_dmesg_len;
> +			igt_name = gen_igt_name(binary, subtest);
> +			current_test = get_or_create_json_object(tests, igt_name);
> +		}
> +
> +		if ((flags & 0x07) <= 4 && continuation != 'c' &&
> +		    regexec(&re, message, (size_t)0, NULL, 0) == REG_NOMATCH) {
> +			append_line(&warnings, &warningslen, formatted);
> +		}
> +		append_line(&dmesg, &dmesglen, formatted);
> +	}
> +
> +	if (current_test != NULL) {
> +		json_object_object_add(current_test, "dmesg",
> +				       json_object_new_string_len(dmesg, dmesglen));
> +		if (warnings) {
> +			json_object_object_add(current_test, "dmesg-warnings",
> +					       json_object_new_string_len(warnings, warningslen));
> +			add_or_override_result(current_test, "pass", "dmesg-warn");
> +			add_or_override_result(current_test, "fail", "dmesg-fail");
> +			add_or_override_result(current_test, "warn", "dmesg-warn");
> +		}
> +	} else {
> +		/*
> +		 * Didn't get any subtest messages at all. If there
> +		 * are subtests, add all of the dmesg gotten to all of
> +		 * them.
> +		 */
> +		for (i = 0; i < subtests->size; i++) {
> +			igt_name = gen_igt_name(binary, subtests->names[i]);
> +			current_test = get_or_create_json_object(tests, igt_name);
> +			json_object_object_add(current_test, "dmesg",
> +					       json_object_new_string_len(dmesg, dmesglen));
> +			/*
> +			 * Don't bother with warnings, any subtests
> +			 * there are would have skip as their result
> +			 * anyway.
> +			 */
> +		}
> +
> +		if (i == 0) {
> +			/* There were no subtests */
> +			igt_name = gen_igt_name(binary, NULL);
> +			current_test = get_or_create_json_object(tests, igt_name);
> +			json_object_object_add(current_test, "dmesg",
> +					       json_object_new_string_len(dmesg, dmesglen));
> +			if (warnings) {
> +				json_object_object_add(current_test, "dmesg-warnings",
> +						       json_object_new_string_len(warnings, warningslen));
> +				add_or_override_result(current_test, "pass", "dmesg-warn");
> +				add_or_override_result(current_test, "fail", "dmesg-fail");
> +				add_or_override_result(current_test, "warn", "dmesg-warn");
> +			}
> +		}
> +	}
> +
> +	/*
> +	 * Add an empty string as the dmesg of all subtests that
> +	 * didn't get any dmesg yet.
> +	 */
> +	for (i = 0; i < subtests->size; i++) {
> +		igt_name = gen_igt_name(binary, subtests->names[i]);
> +		current_test = get_or_create_json_object(tests, igt_name);
> +		if (!json_object_object_get_ex(current_test, "dmesg", NULL)) {
> +			json_object_object_add(current_test, "dmesg",
> +					       json_object_new_string(""));
> +		}
> +	}
> +
> +	free(dmesg);
> +	free(warnings);
> +	fclose(f);
> +	return true;
> +}
> +
> +static char *result_from_exitcode(int exitcode)
> +{
> +	switch (exitcode) {
> +	case IGT_EXIT_TIMEOUT:
> +		return "timeout";
> +	case IGT_EXIT_SKIP:
> +		return "skip";
> +	case IGT_EXIT_SUCCESS:
> +		return "pass";
> +	case IGT_EXIT_INVALID:
> +		return "notrun";
> +	default:
> +		return "fail";
> +	}
> +}
> +
> +static void add_subtest(struct subtests *subtests, char *subtest)
> +{
> +	size_t len = strlen(subtest);
> +
> +	if (len == 0)
> +		return;
> +
> +	if (subtest[len - 1] == '\n')
> +		subtest[len - 1] = '\0';
> +
> +	subtests->size++;
> +	subtests->names = realloc(subtests->names, sizeof(*subtests->names) * subtests->size);
> +	subtests->names[subtests->size - 1] = subtest;
> +}
> +
> +static bool fill_from_journal(int fd, char *binary,
> +				  struct subtests *subtests,
> +				  struct json_object *tests)
> +{
> +	FILE *f = fdopen(fd, "r");
> +	char *line = NULL;
> +	size_t linelen = 0;
> +	ssize_t read;
> +	char exitline[] = "exit:";
> +	size_t exitlen = sizeof(exitline) - 1;
> +	char timeoutline[] = "timeout:";
> +	size_t timeoutlen = sizeof(timeoutline) - 1;
> +	int exitcode = 10000;
> +
> +	while ((read = getline(&line, &linelen, f)) > 0) {
> +		if (read >= exitlen && !memcmp(line, exitline, exitlen)) {
> +			char *p = strchr(line, '(');
> +			char *igt_name = gen_igt_name(binary, NULL);
> +			double time = 0.0;
> +			struct json_object *obj = get_or_create_json_object(tests, igt_name);
> +
> +			exitcode = atoi(line + exitlen);
> +
> +			if (p) {
> +				time = strtod(p + 1, NULL);
> +			}
> +
> +			add_runtime(obj, time);
> +		} else if (read >= timeoutlen && !memcmp(line, timeoutline, timeoutlen)) {
> +			if (subtests->size) {
> +				char *last_subtest = subtests->names[subtests->size - 1];
> +				char *igt_name = gen_igt_name(binary, last_subtest);
> +				char *p = strchr(line, '(');
> +				double time = 0.0;
> +				struct json_object *obj = get_or_create_json_object(tests, igt_name);
> +
> +				json_object_object_add(obj, "result",
> +						       json_object_new_string("timeout"));
> +
> +				if (p) {
> +					time = strtod(p + 1, NULL);
> +				}
> +
> +				add_runtime(obj, time);
> +
> +				igt_name = gen_igt_name(binary, NULL);
> +				obj = get_or_create_json_object(tests, igt_name);
> +				add_runtime(obj, time);
> +			}
> +		} else {
> +			add_subtest(subtests, strdup(line));
> +		}
> +	}
> +
> +	if (subtests->size == 0 && exitcode != 10000) {
> +		char *igt_name = gen_igt_name(binary, NULL);
> +		struct json_object *obj = get_or_create_json_object(tests, igt_name);
> +		char *result = result_from_exitcode(exitcode);
> +		json_object_object_add(obj, "result",
> +				       json_object_new_string(result));
> +	}
> +
> +	free(line);
> +	return true;
> +}
> +
> +static bool parse_test_directory(int dirfd, char *binary, struct json_object *tests)
> +{
> +	int fds[_F_LAST];
> +	struct subtests subtests = {};
> +
> +	if (!open_output_files(dirfd, fds, false)) {
> +		fprintf(stderr, "Error opening output files\n");
> +		return false;
> +	}
> +
> +	/* fill_from_journal also fills the subtests struct */
> +	if (!fill_from_journal(fds[_F_JOURNAL], binary, &subtests, tests)) {
> +		fprintf(stderr, "Error reading from journal\n");
> +		return false;
> +	}
> +
> +	/* Order of these is important */
> +	if (!fill_from_output(fds[_F_OUT], binary, true, &subtests, tests) ||
> +	    !fill_from_output(fds[_F_ERR], binary, false, &subtests, tests) ||
> +	    !fill_from_dmesg(fds[_F_DMESG], binary, &subtests, tests)) {
> +		fprintf(stderr, "Error parsing output files\n");
> +		return false;
> +	}
> +
> +	close_outputs(fds);
> +
> +	return true;
> +}
> +
> +bool generate_results(int dirfd)
> +{
> +	struct settings settings;
> +	struct job_list job_list;
> +	struct json_object *obj, *tests;
> +	int resultsfd, testdirfd, unamefd;
> +	const char *json_string;
> +	size_t i;
> +
> +	init_settings(&settings);
> +	init_job_list(&job_list);
> +
> +	if (!read_settings(&settings, dirfd)) {
> +		fprintf(stderr, "resultgen: Cannot parse settings\n");
> +		return false;
> +	}
> +
> +	if (!read_job_list(&job_list, dirfd)) {
> +		fprintf(stderr, "resultgen: Cannot parse job list\n");
> +		return false;
> +	}
> +
> +	/* TODO: settings.overwrite */
> +	if ((resultsfd = openat(dirfd, "results.json", O_WRONLY | O_CREAT | O_TRUNC, 0666)) < 0) {
> +		fprintf(stderr, "resultgen: Cannot create results file\n");
> +		return false;
> +	}
> +
> +	obj = json_object_new_object();
> +	json_object_object_add(obj, "__type__", json_object_new_string("TestrunResult"));
> +	json_object_object_add(obj, "results_version", json_object_new_int(9));
> +	json_object_object_add(obj, "name",
> +			       settings.name ?
> +			       json_object_new_string(settings.name) :
> +			       json_object_new_string(""));
> +
> +	if ((unamefd = openat(dirfd, "uname.txt", O_RDONLY)) >= 0) {
> +		char buf[128];
> +		ssize_t r = read(unamefd, buf, 128);
> +
> +		if (r > 0 && buf[r - 1] == '\n')
> +			r--;
> +
> +		json_object_object_add(obj, "uname",
> +				       json_object_new_string_len(buf, r));
> +		close(unamefd);
> +	}
> +
> +	/* lspci */
> +	/* results_version */
> +	/* glxinfo */
> +	/* wglinfo */
> +	/* clinfo */
> +	/* options */
> +	/* time_elapsed */
> +	/* totals */
> +
> +	tests = json_object_new_object();
> +	json_object_object_add(obj, "tests", tests);
> +
> +	for (i = 0; i < job_list.size; i++) {
> +		char name[16];
> +
> +		snprintf(name, 16, "%zd", i);
> +		if ((testdirfd = openat(dirfd, name, O_DIRECTORY | O_RDONLY)) < 0) {
> +			fprintf(stderr, "Warning: Cannot open result directory %s\n", name);
> +			break;
> +		}
> +
> +		if (!parse_test_directory(testdirfd, job_list.entries[i].binary, tests)) {
> +			close(resultsfd);
> +			return false;
> +		}
> +	}
> +
> +	json_string = json_object_to_json_string_ext(obj, JSON_C_TO_STRING_PRETTY);
> +	write(resultsfd, json_string, strlen(json_string));
> +	return true;
> +}
> +
> +bool generate_results_path(char *resultspath)
> +{
> +	int dirfd = open(resultspath, O_DIRECTORY | O_RDONLY);
> +
> +	if (dirfd < 0)
> +		return false;
> +
> +	return generate_results(dirfd);
> +}
> diff --git a/runner/resultgen.h b/runner/resultgen.h
> new file mode 100644
> index 00000000..83a0876b
> --- /dev/null
> +++ b/runner/resultgen.h
> @@ -0,0 +1,9 @@
> +#ifndef RUNNER_RESULTGEN_H
> +#define RUNNER_RESULTGEN_H
> +
> +#include <stdbool.h>
> +
> +bool generate_results(int dirfd);
> +bool generate_results_path(char *resultspath);
> +
> +#endif
> diff --git a/runner/results.c b/runner/results.c
> new file mode 100644
> index 00000000..3eb7cb15
> --- /dev/null
> +++ b/runner/results.c
> @@ -0,0 +1,26 @@
> +#include <fcntl.h>
> +#include <stdio.h>
> +#include <stdlib.h>
> +#include <sys/stat.h>
> +#include <sys/types.h>
> +
> +#include "resultgen.h"
> +
> +int main(int argc, char **argv)
> +{
> +	int dirfd;
> +
> +	if (argc < 2)
> +		exit(1);
> +
> +	dirfd = open(argv[1], O_DIRECTORY | O_RDONLY);
> +	if (dirfd < 0)
> +		exit(1);
> +
> +	if (generate_results(dirfd)) {
> +		printf("Results generated\n");
> +		exit(0);
> +	}
> +
> +	exit(1);
> +}
> diff --git a/runner/runner.c b/runner/runner.c
> new file mode 100644
> index 00000000..b685786a
> --- /dev/null
> +++ b/runner/runner.c
> @@ -0,0 +1,40 @@
> +#include <stdio.h>
> +#include <string.h>
> +
> +#include "settings.h"
> +#include "job_list.h"
> +#include "executor.h"
> +#include "resultgen.h"
> +
> +int main(int argc, char **argv)
> +{
> +	struct settings settings;
> +	struct job_list job_list;
> +	struct execute_state state;
> +
> +	init_settings(&settings);
> +	init_job_list(&job_list);
> +
> +	if (!parse_options(argc, argv, &settings)) {
> +		return 1;
> +	}
> +
> +	if (!create_job_list(&job_list, &settings)) {
> +		return 1;
> +	}
> +
> +	if (!initialize_execute_state(&state, &settings, &job_list)) {
> +		return 1;
> +	}
> +
> +	if (!execute(&state, &settings, &job_list)) {
> +		return 1;
> +	}
> +
> +	if (!generate_results_path(settings.results_path)) {
> +		return 1;
> +	}
> +
> +	printf("Done.\n");
> +	return 0;
> +}
> diff --git a/runner/settings.c b/runner/settings.c
> new file mode 100644
> index 00000000..50e4ea10
> --- /dev/null
> +++ b/runner/settings.c
> @@ -0,0 +1,506 @@
> +#include "settings.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 <unistd.h>
> +
> +enum {
> +	OPT_ABORT_ON_ERROR,
> +	OPT_TEST_LIST,
> +	OPT_IGNORE_MISSING,
> +	OPT_HELP = 'h',
> +	OPT_NAME = 'n',
> +	OPT_DRY_RUN = 'd',
> +	OPT_INCLUDE = 't',
> +	OPT_EXCLUDE = 'x',
> +	OPT_SYNC = 's',
> +	OPT_LOG_LEVEL = 'l',
> +	OPT_OVERWRITE = 'o',
> +	OPT_MULTIPLE = 'm',
> +	OPT_TIMEOUT = 'c',
> +	OPT_WATCHDOG = 'g',
> +};
> +
> +static struct {
> +	int level;
> +	const char *name;
> +} log_levels[] = {
> +	{ LOG_LEVEL_NORMAL, "normal" },
> +	{ LOG_LEVEL_QUIET, "quiet" },
> +	{ LOG_LEVEL_VERBOSE, "verbose" },
> +	{ 0, 0 },
> +};
> +
> +static bool set_log_level(struct settings* settings, const char *level)
> +{
> +	int c;
> +
> +	for (c = 0; log_levels[c].name != NULL; c++) {
> +		if (!strcmp(level, log_levels[c].name)) {
> +			settings->log_level = log_levels[c].level;
> +			return true;
> +		}
> +	}
> +
> +	return false;
> +}
> +
> +static const char *usage_str =
> +	"usage: runner [options] [test_root] results-path\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\n"
> +	"                        Abort execution when a fatal condition is detected.\n"
> +	"                        <TODO>\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"
> +	"  -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"
> +	"  --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"
> +	"  [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"
> +	;
> +
> +static void usage(const char *extra_message, bool use_stderr)
> +{
> +	FILE *f = use_stderr ? stderr : stdout;
> +
> +	if (extra_message)
> +		fprintf(f, "%s\n\n", extra_message);
> +
> +	fprintf(f, "%s", usage_str);
> +}
> +
> +static bool add_regex(struct regex_list *list, char *new)
> +{
> +	regex_t *regex;
> +	size_t buflen;
> +	char *buf;
> +	int s;
> +
> +	regex = malloc(sizeof(*regex));
> +
> +	if ((s = regcomp(regex, new,
> +			 REG_EXTENDED | REG_NOSUB)) != 0) {
> +		buflen = regerror(s, regex, NULL, 0);
> +		buf = malloc(buflen);
> +		regerror(s, regex, buf, buflen);
> +		usage(buf, true);
> +
> +		free(buf);
> +		regfree(regex);
> +		free(regex);
> +		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 void free_regexes(struct regex_list *regexes)
> +{
> +	size_t i;
> +
> +	for (i = 0; i < regexes->size; i++) {
> +		free(regexes->regex_strings[i]);
> +		regfree(regexes->regexes[i]);
> +		free(regexes->regexes[i]);
> +	}
> +	free(regexes->regex_strings);
> +	free(regexes->regexes);
> +}
> +
> +static bool readable_file(char *filename)
> +{
> +	return !access(filename, R_OK);
> +}
> +
> +void init_settings(struct settings *settings)
> +{
> +	memset(settings, 0, sizeof(*settings));
> +}
> +
> +void free_settings(struct settings *settings)
> +{
> +	free(settings->test_list);
> +	free(settings->name);
> +	free(settings->test_root);
> +	free(settings->results_path);
> +
> +	free_regexes(&settings->include_regexes);
> +	free_regexes(&settings->exclude_regexes);
> +
> +	init_settings(settings);
> +}
> +
> +bool parse_options(int argc, char **argv,
> +		   struct settings *settings)
> +{
> +	int c;
> +	char *env_test_root;
> +
> +	static struct option long_options[] = {
> +		{"help", no_argument, NULL, OPT_HELP},
> +		{"name", required_argument, NULL, OPT_NAME},
> +		{"dry-run", no_argument, NULL, OPT_DRY_RUN},
> +		{"include-tests", required_argument, NULL, OPT_INCLUDE},
> +		{"exclude-tests", required_argument, NULL, OPT_EXCLUDE},
> +		{"abort-on-monitored-error", no_argument, NULL, OPT_ABORT_ON_ERROR},
> +		{"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},
> +		{"multiple-mode", no_argument, NULL, OPT_MULTIPLE},
> +		{"inactivity-timeout", required_argument, NULL, OPT_TIMEOUT},
> +		{"use-watchdog", no_argument, NULL, OPT_WATCHDOG},
> +		{ 0, 0, 0, 0},
> +	};
> +
> +	free_settings(settings);
> +
> +	optind = 1;
> +
> +	while ((c = getopt_long(argc, argv, "hn:dt:x:sl:om", long_options, NULL)) != -1) {
> +		switch (c) {
> +		case OPT_HELP:
> +			usage(NULL, false);
> +			goto error;
> +		case OPT_NAME:
> +			settings->name = strdup(optarg);
> +			break;
> +		case OPT_DRY_RUN:
> +			settings->dry_run = 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_ABORT_ON_ERROR:
> +			settings->abort_on_error = true;
> +			break;
> +		case OPT_SYNC:
> +			settings->sync = true;
> +			break;
> +		case OPT_LOG_LEVEL:
> +			if (!set_log_level(settings, optarg)) {
> +				usage("Cannot parse log level", true);
> +				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_MULTIPLE:
> +			settings->multiple_mode = true;
> +			break;
> +		case OPT_TIMEOUT:
> +			settings->inactivity_timeout = atoi(optarg);
> +			break;
> +		case OPT_WATCHDOG:
> +			settings->use_watchdog = true;
> +			break;
> +		case '?':
> +			usage(NULL, true);
> +			goto error;
> +		default:
> +			usage("Cannot parse options", true);
> +			goto error;
> +		}
> +	}
> +
> +	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("Results-path missing", true);
> +		goto error;
> +	default:
> +		usage("Extra arguments after results-path", true);
> +		goto error;
> +	}
> +
> +	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("Test root not set", true);
> +		goto error;
> +	}
> +
> +	if (!settings->name) {
> +		char *name = strdup(settings->results_path);
> +		settings->name = strdup(basename(name));
> +		free(name);
> +	}
> +
> +	return true;
> +
> + error:
> +	free_settings(settings);
> +	return false;
> +}
> +
> +bool validate_settings(struct settings *settings)
> +{
> +	int dirfd, fd;
> +
> +	if (settings->test_list && !readable_file(settings->test_list)) {
> +		usage("Cannot open test-list file", true);
> +		return false;
> +	}
> +
> +	if (!settings->results_path) {
> +		usage("No results-path set; this shouldn't happen", true);
> +		return false;
> +	}
> +
> +	if (!settings->test_root) {
> +		usage("No test root set; this shouldn't happen", true);
> +		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);
> +
> +	return true;
> +}
> +
> +char *absolute_path(char *path)
> +{
> +	char *result = NULL;
> +	char *tmppath, *tmpname;
> +
> +	result = realpath(path, NULL);
> +	if (result != NULL)
> +		return result;
> +
> +	tmppath = strdup(path);
> +	tmpname = dirname(tmppath);
> +	free(result);
> +	result = realpath(tmpname, NULL);
> +	free(tmppath);
> +
> +	if (result != NULL) {
> +		tmppath = strdup(path);
> +		tmpname = basename(tmppath);
> +		strcat(result, "/");
> +		strcat(result, tmpname);
> +		free(tmppath);
> +		return result;
> +	}
> +
> +	free(result);
> +	return NULL;
> +}
> +
> +static char settings_filename[] = "metadata.txt";
> +bool serialize_settings(struct settings *settings)
> +{
> +#define SERIALIZE_LINE(f, s, name, format) fprintf(f, "%s : " format "\n", #name, s->name)
> +
> +	int dirfd, fd;
> +	FILE *f;
> +
> +	if (!settings->results_path) {
> +		usage("No results-path set; this shouldn't happen", true);
> +		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("Creating results-path failed", true);
> +			return false;
> +		}
> +	}
> +
> +	if ((fd = openat(dirfd, settings_filename, O_RDONLY)) >= 0) {
> +		close(fd);
> +
> +		if (!settings->overwrite) {
> +			/* Serialization data already exists, not overwriting */
> +			close(dirfd);
> +			return false;
> +		}
> +
> +		if (unlinkat(dirfd, settings_filename, 0) != 0) {
> +			usage("Error overwriting old settings metadata", true);
> +			close(dirfd);
> +			return false;
> +		}
> +	}
> +
> +	if ((fd = openat(dirfd, settings_filename, O_CREAT | O_EXCL | O_WRONLY, 0666)) < 0) {
> +		usage("Creating settings serialization file failed", true);
> +		close(dirfd);
> +		return false;
> +	}
> +
> +	f = fdopen(fd, "w");
> +	if (!f) {
> +		close(fd);
> +		close(dirfd);
> +		return false;
> +	}
> +
> +	SERIALIZE_LINE(f, settings, abort_on_error, "%d");
> +	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, 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, use_watchdog, "%d");
> +	SERIALIZE_LINE(f, settings, test_root, "%s");
> +	SERIALIZE_LINE(f, settings, results_path, "%s");
> +
> +	if (settings->sync) {
> +		fsync(fd);
> +		fsync(dirfd);
> +	}
> +
> +	fclose(f);
> +	close(dirfd);
> +	return true;
> +
> +#undef SERIALIZE_LINE
> +}
> +
> +static char *maybe_strdup(char *str)
> +{
> +	if (!str)
> +		return NULL;
> +
> +	return strdup(str);
> +}
> +
> +bool read_settings(struct settings *settings, int dirfd)
> +{
> +#define PARSE_LINE(s, name, val, field, write) \
> +	if (!strcmp(name, #field)) {	       \
> +		s->field = write;	       \
> +		free(name);		       \
> +		free(val);		       \
> +		name = val = NULL;	       \
> +		continue;		       \
> +	}
> +
> +	int fd;
> +	FILE *f;
> +	char *name = NULL, *val = NULL;
> +
> +	free_settings(settings);
> +
> +	if ((fd = openat(dirfd, settings_filename, O_RDONLY)) < 0)
> +		return false;
> +
> +	f = fdopen(fd, "r");
> +	if (!f) {
> +		close(fd);
> +		return false;
> +	}
> +
> +	while (fscanf(f, "%ms : %ms", &name, &val) == 2) {
> +		int numval = atoi(val);
> +		PARSE_LINE(settings, name, val, abort_on_error, numval);
> +		PARSE_LINE(settings, name, val, test_list, maybe_strdup(val));
> +		PARSE_LINE(settings, name, val, name, maybe_strdup(val));
> +		PARSE_LINE(settings, name, val, dry_run, 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, use_watchdog, numval);
> +		PARSE_LINE(settings, name, val, test_root, maybe_strdup(val));
> +		PARSE_LINE(settings, name, val, results_path, maybe_strdup(val));
> +
> +		printf("Warning: Unknown field in settings file: %s = %s\n",
> +		       name, val);
> +		free(name);
> +		free(val);
> +		name = val = NULL;
> +	}
> +
> +	free(name);
> +	free(val);
> +	fclose(f);
> +
> +	return true;
> +
> +#undef PARSE_LINE
> +}
> diff --git a/runner/settings.h b/runner/settings.h
> new file mode 100644
> index 00000000..9d1f03fb
> --- /dev/null
> +++ b/runner/settings.h
> @@ -0,0 +1,111 @@
> +#ifndef RUNNER_SETTINGS_H
> +#define RUNNER_SETTINGS_H
> +
> +#include <stdbool.h>
> +#include <stddef.h>
> +#include <sys/types.h>
> +#include <regex.h>
> +
> +enum {
> +	LOG_LEVEL_NORMAL = 0,
> +	LOG_LEVEL_QUIET = -1,
> +	LOG_LEVEL_VERBOSE = 1,
> +};
> +
> +struct regex_list {
> +	char **regex_strings;
> +	regex_t** regexes;
> +	size_t size;
> +};
> +
> +struct settings {
> +	bool abort_on_error;
> +	char *test_list;
> +	char *name;
> +	bool dry_run;
> +	struct regex_list include_regexes;
> +	struct regex_list exclude_regexes;
> +	bool sync;
> +	int log_level;
> +	bool overwrite;
> +	bool multiple_mode;
> +	int inactivity_timeout;
> +	bool use_watchdog;
> +	char *test_root;
> +	char *results_path;
> +};
> +
> +/**
> + * init_settings:
> + *
> + * Initializes a settings object to an empty state (all values NULL, 0
> + * or false).
> + *
> + * @settings: Object to initialize. Storage for it must exist.
> + */
> +void init_settings(struct settings *settings);
> +
> +/**
> + * free_settings:
> + *
> + * Releases all allocated resources for a settings object and
> + * initializes it to an empty state (see #init_settings).
> + *
> + * @settings: Object to release and initialize.
> + */
> +void free_settings(struct settings *settings);
> +
> +/**
> + * parse_options:
> + *
> + * Parses command line options and sets the settings object to
> + * designated values.
> + *
> + * The function can be called again on the same settings object. The
> + * old values will be properly released and cleared. On a parse
> + * failure, the settings object will be in an empty state (see
> + * #init_settings) and usage instructions will be printed with an
> + * error message.
> + *
> + * @argc: Argument count
> + * @argv: Argument array. First element is the program name.
> + * @settings: Settings object to fill with values. Must have proper
> + * storage.
> + *
> + * Returns: True on successful parse, false on error.
> + */
> +bool parse_options(int argc, char **argv,
> +		   struct settings *settings);
> +
> +/**
> + * validate_settings:
> + *
> + * Checks the settings object against the system to see if executing
> + * on it can be done. Checks pathnames for existence and access
> + * rights. Note that this function will not check that the designated
> + * job listing (through a test-list file or the -t/-x flags) yields a
> + * non-zero amount of testing to be done. On errors, usage
> + * instructions will be printed with an error message.
> + *
> + * @settings: Settings object to check.
> + *
> + * Returns: True on valid settings, false on any error.
> + */
> +bool validate_settings(struct settings *settings);
> +
> +/* TODO: Better place for this */
> +char *absolute_path(char *path);
> +
> +/**
> + * serialize_settings:
> + *
> + * Serializes the settings object to a file in the results_path
> + * directory.
> + *
> + * @settings: Settings object to serialize.
> + */
> +bool serialize_settings(struct settings *settings);
> +
> +bool read_settings(struct settings *settings, int dirfd);
> +
> +#endif
> -- 
> 2.14.1
> 
> _______________________________________________
> igt-dev mailing list
> igt-dev@lists.freedesktop.org
> https://lists.freedesktop.org/mailman/listinfo/igt-dev

-- 
Daniel Vetter
Software Engineer, Intel Corporation
http://blog.ffwll.ch
_______________________________________________
igt-dev mailing list
igt-dev@lists.freedesktop.org
https://lists.freedesktop.org/mailman/listinfo/igt-dev

  parent reply	other threads:[~2018-05-07 18:34 UTC|newest]

Thread overview: 18+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2018-04-30  9:28 [igt-dev] [PATCH i-g-t 0/4] New runner to rule them all Petri Latvala
2018-04-30  9:28 ` [igt-dev] [PATCH i-g-t 1/4] lib: Print subtest starting/ending line to stderr too Petri Latvala
2018-04-30  9:28 ` [igt-dev] [PATCH i-g-t 2/4] uwildmat: Case-insensitive test selection Petri Latvala
2018-04-30  9:28 ` [igt-dev] [PATCH i-g-t 3/4] runner: New test runner Petri Latvala
2018-05-03 12:04   ` Arkadiusz Hiler
2018-05-04 13:45   ` Arkadiusz Hiler
2018-05-07 18:34   ` Daniel Vetter [this message]
2018-05-24  8:40     ` Petri Latvala
2018-05-08  8:47   ` Arkadiusz Hiler
2018-05-08  8:50     ` Chris Wilson
2018-05-11 10:45   ` Arkadiusz Hiler
2018-05-11 10:50     ` Chris Wilson
2018-05-11 10:51       ` Chris Wilson
2018-04-30  9:28 ` [igt-dev] [PATCH i-g-t 4/4] runner: Unit tests for the runner Petri Latvala
2018-04-30 14:54 ` [igt-dev] ✓ Fi.CI.BAT: success for New runner to rule them all Patchwork
2018-04-30 20:13 ` [igt-dev] ✓ Fi.CI.IGT: " Patchwork
2018-05-11 11:24 ` [igt-dev] [PATCH i-g-t 0/4] " David Weinehall
2018-06-04 22:17 ` Eric Anholt

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=20180507183416.GH28661@phenom.ffwll.local \
    --to=daniel@ffwll.ch \
    --cc=igt-dev@lists.freedesktop.org \
    --cc=martin.peres@linux.intel.com \
    --cc=petri.latvala@intel.com \
    --cc=tomi.p.sarvela@intel.com \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox