summaryrefslogtreecommitdiffstats
path: root/t/helper/test-run-command.c
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--t/helper/test-run-command.c462
1 files changed, 462 insertions, 0 deletions
diff --git a/t/helper/test-run-command.c b/t/helper/test-run-command.c
new file mode 100644
index 0000000..c0ed872
--- /dev/null
+++ b/t/helper/test-run-command.c
@@ -0,0 +1,462 @@
+/*
+ * test-run-command.c: test run command API.
+ *
+ * (C) 2009 Ilari Liusvaara <ilari.liusvaara@elisanet.fi>
+ *
+ * This code is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 2 as
+ * published by the Free Software Foundation.
+ */
+
+#include "test-tool.h"
+#include "run-command.h"
+#include "strvec.h"
+#include "strbuf.h"
+#include "parse-options.h"
+#include "string-list.h"
+#include "thread-utils.h"
+#include "wildmatch.h"
+
+static int number_callbacks;
+static int parallel_next(struct child_process *cp,
+ struct strbuf *err,
+ void *cb,
+ void **task_cb UNUSED)
+{
+ struct child_process *d = cb;
+ if (number_callbacks >= 4)
+ return 0;
+
+ strvec_pushv(&cp->args, d->args.v);
+ if (err)
+ strbuf_addstr(err, "preloaded output of a child\n");
+ else
+ fprintf(stderr, "preloaded output of a child\n");
+
+ number_callbacks++;
+ return 1;
+}
+
+static int no_job(struct child_process *cp UNUSED,
+ struct strbuf *err,
+ void *cb UNUSED,
+ void **task_cb UNUSED)
+{
+ if (err)
+ strbuf_addstr(err, "no further jobs available\n");
+ else
+ fprintf(stderr, "no further jobs available\n");
+ return 0;
+}
+
+static int task_finished(int result UNUSED,
+ struct strbuf *err,
+ void *pp_cb UNUSED,
+ void *pp_task_cb UNUSED)
+{
+ if (err)
+ strbuf_addstr(err, "asking for a quick stop\n");
+ else
+ fprintf(stderr, "asking for a quick stop\n");
+ return 1;
+}
+
+struct testsuite {
+ struct string_list tests, failed;
+ int next;
+ int quiet, immediate, verbose, verbose_log, trace, write_junit_xml;
+};
+#define TESTSUITE_INIT { \
+ .tests = STRING_LIST_INIT_DUP, \
+ .failed = STRING_LIST_INIT_DUP, \
+}
+
+static int next_test(struct child_process *cp, struct strbuf *err, void *cb,
+ void **task_cb)
+{
+ struct testsuite *suite = cb;
+ const char *test;
+ if (suite->next >= suite->tests.nr)
+ return 0;
+
+ test = suite->tests.items[suite->next++].string;
+ strvec_pushl(&cp->args, "sh", test, NULL);
+ if (suite->quiet)
+ strvec_push(&cp->args, "--quiet");
+ if (suite->immediate)
+ strvec_push(&cp->args, "-i");
+ if (suite->verbose)
+ strvec_push(&cp->args, "-v");
+ if (suite->verbose_log)
+ strvec_push(&cp->args, "-V");
+ if (suite->trace)
+ strvec_push(&cp->args, "-x");
+ if (suite->write_junit_xml)
+ strvec_push(&cp->args, "--write-junit-xml");
+
+ strbuf_addf(err, "Output of '%s':\n", test);
+ *task_cb = (void *)test;
+
+ return 1;
+}
+
+static int test_finished(int result, struct strbuf *err, void *cb,
+ void *task_cb)
+{
+ struct testsuite *suite = cb;
+ const char *name = (const char *)task_cb;
+
+ if (result)
+ string_list_append(&suite->failed, name);
+
+ strbuf_addf(err, "%s: '%s'\n", result ? "FAIL" : "SUCCESS", name);
+
+ return 0;
+}
+
+static int test_failed(struct strbuf *out, void *cb, void *task_cb)
+{
+ struct testsuite *suite = cb;
+ const char *name = (const char *)task_cb;
+
+ string_list_append(&suite->failed, name);
+ strbuf_addf(out, "FAILED TO START: '%s'\n", name);
+
+ return 0;
+}
+
+static const char * const testsuite_usage[] = {
+ "test-run-command testsuite [<options>] [<pattern>...]",
+ NULL
+};
+
+static int testsuite(int argc, const char **argv)
+{
+ struct testsuite suite = TESTSUITE_INIT;
+ int max_jobs = 1, i, ret = 0;
+ DIR *dir;
+ struct dirent *d;
+ struct option options[] = {
+ OPT_BOOL('i', "immediate", &suite.immediate,
+ "stop at first failed test case(s)"),
+ OPT_INTEGER('j', "jobs", &max_jobs, "run <N> jobs in parallel"),
+ OPT_BOOL('q', "quiet", &suite.quiet, "be terse"),
+ OPT_BOOL('v', "verbose", &suite.verbose, "be verbose"),
+ OPT_BOOL('V', "verbose-log", &suite.verbose_log,
+ "be verbose, redirected to a file"),
+ OPT_BOOL('x', "trace", &suite.trace, "trace shell commands"),
+ OPT_BOOL(0, "write-junit-xml", &suite.write_junit_xml,
+ "write JUnit-style XML files"),
+ OPT_END()
+ };
+ struct run_process_parallel_opts opts = {
+ .get_next_task = next_test,
+ .start_failure = test_failed,
+ .task_finished = test_finished,
+ .data = &suite,
+ };
+
+ argc = parse_options(argc, argv, NULL, options,
+ testsuite_usage, PARSE_OPT_STOP_AT_NON_OPTION);
+
+ if (max_jobs <= 0)
+ max_jobs = online_cpus();
+
+ dir = opendir(".");
+ if (!dir)
+ die("Could not open the current directory");
+ while ((d = readdir(dir))) {
+ const char *p = d->d_name;
+
+ if (*p != 't' || !isdigit(p[1]) || !isdigit(p[2]) ||
+ !isdigit(p[3]) || !isdigit(p[4]) || p[5] != '-' ||
+ !ends_with(p, ".sh"))
+ continue;
+
+ /* No pattern: match all */
+ if (!argc) {
+ string_list_append(&suite.tests, p);
+ continue;
+ }
+
+ for (i = 0; i < argc; i++)
+ if (!wildmatch(argv[i], p, 0)) {
+ string_list_append(&suite.tests, p);
+ break;
+ }
+ }
+ closedir(dir);
+
+ if (!suite.tests.nr)
+ die("No tests match!");
+ if (max_jobs > suite.tests.nr)
+ max_jobs = suite.tests.nr;
+
+ fprintf(stderr, "Running %"PRIuMAX" tests (%d at a time)\n",
+ (uintmax_t)suite.tests.nr, max_jobs);
+
+ opts.processes = max_jobs;
+ run_processes_parallel(&opts);
+
+ if (suite.failed.nr > 0) {
+ ret = 1;
+ fprintf(stderr, "%"PRIuMAX" tests failed:\n\n",
+ (uintmax_t)suite.failed.nr);
+ for (i = 0; i < suite.failed.nr; i++)
+ fprintf(stderr, "\t%s\n", suite.failed.items[i].string);
+ }
+
+ string_list_clear(&suite.tests, 0);
+ string_list_clear(&suite.failed, 0);
+
+ return ret;
+}
+
+static uint64_t my_random_next = 1234;
+
+static uint64_t my_random(void)
+{
+ uint64_t res = my_random_next;
+ my_random_next = my_random_next * 1103515245 + 12345;
+ return res;
+}
+
+static int quote_stress_test(int argc, const char **argv)
+{
+ /*
+ * We are running a quote-stress test.
+ * spawn a subprocess that runs quote-stress with a
+ * special option that echoes back the arguments that
+ * were passed in.
+ */
+ char special[] = ".?*\\^_\"'`{}()[]<>@~&+:;$%"; // \t\r\n\a";
+ int i, j, k, trials = 100, skip = 0, msys2 = 0;
+ struct strbuf out = STRBUF_INIT;
+ struct strvec args = STRVEC_INIT;
+ struct option options[] = {
+ OPT_INTEGER('n', "trials", &trials, "number of trials"),
+ OPT_INTEGER('s', "skip", &skip, "skip <n> trials"),
+ OPT_BOOL('m', "msys2", &msys2, "test quoting for MSYS2's sh"),
+ OPT_END()
+ };
+ const char * const usage[] = {
+ "test-tool run-command quote-stress-test <options>",
+ NULL
+ };
+
+ argc = parse_options(argc, argv, NULL, options, usage, 0);
+
+ setenv("MSYS_NO_PATHCONV", "1", 0);
+
+ for (i = 0; i < trials; i++) {
+ struct child_process cp = CHILD_PROCESS_INIT;
+ size_t arg_count, arg_offset;
+ int ret = 0;
+
+ strvec_clear(&args);
+ if (msys2)
+ strvec_pushl(&args, "sh", "-c",
+ "printf %s\\\\0 \"$@\"", "skip", NULL);
+ else
+ strvec_pushl(&args, "test-tool", "run-command",
+ "quote-echo", NULL);
+ arg_offset = args.nr;
+
+ if (argc > 0) {
+ trials = 1;
+ arg_count = argc;
+ for (j = 0; j < arg_count; j++)
+ strvec_push(&args, argv[j]);
+ } else {
+ arg_count = 1 + (my_random() % 5);
+ for (j = 0; j < arg_count; j++) {
+ char buf[20];
+ size_t min_len = 1;
+ size_t arg_len = min_len +
+ (my_random() % (ARRAY_SIZE(buf) - min_len));
+
+ for (k = 0; k < arg_len; k++)
+ buf[k] = special[my_random() %
+ ARRAY_SIZE(special)];
+ buf[arg_len] = '\0';
+
+ strvec_push(&args, buf);
+ }
+ }
+
+ if (i < skip)
+ continue;
+
+ strvec_pushv(&cp.args, args.v);
+ strbuf_reset(&out);
+ if (pipe_command(&cp, NULL, 0, &out, 0, NULL, 0) < 0)
+ return error("Failed to spawn child process");
+
+ for (j = 0, k = 0; j < arg_count; j++) {
+ const char *arg = args.v[j + arg_offset];
+
+ if (strcmp(arg, out.buf + k))
+ ret = error("incorrectly quoted arg: '%s', "
+ "echoed back as '%s'",
+ arg, out.buf + k);
+ k += strlen(out.buf + k) + 1;
+ }
+
+ if (k != out.len)
+ ret = error("got %d bytes, but consumed only %d",
+ (int)out.len, (int)k);
+
+ if (ret) {
+ fprintf(stderr, "Trial #%d failed. Arguments:\n", i);
+ for (j = 0; j < arg_count; j++)
+ fprintf(stderr, "arg #%d: '%s'\n",
+ (int)j, args.v[j + arg_offset]);
+
+ strbuf_release(&out);
+ strvec_clear(&args);
+
+ return ret;
+ }
+
+ if (i && (i % 100) == 0)
+ fprintf(stderr, "Trials completed: %d\n", (int)i);
+ }
+
+ strbuf_release(&out);
+ strvec_clear(&args);
+
+ return 0;
+}
+
+static int quote_echo(int argc, const char **argv)
+{
+ while (argc > 1) {
+ fwrite(argv[1], strlen(argv[1]), 1, stdout);
+ fputc('\0', stdout);
+ argv++;
+ argc--;
+ }
+
+ return 0;
+}
+
+static int inherit_handle(const char *argv0)
+{
+ struct child_process cp = CHILD_PROCESS_INIT;
+ char path[PATH_MAX];
+ int tmp;
+
+ /* First, open an inheritable handle */
+ xsnprintf(path, sizeof(path), "out-XXXXXX");
+ tmp = xmkstemp(path);
+
+ strvec_pushl(&cp.args,
+ "test-tool", argv0, "inherited-handle-child", NULL);
+ cp.in = -1;
+ cp.no_stdout = cp.no_stderr = 1;
+ if (start_command(&cp) < 0)
+ die("Could not start child process");
+
+ /* Then close it, and try to delete it. */
+ close(tmp);
+ if (unlink(path))
+ die("Could not delete '%s'", path);
+
+ if (close(cp.in) < 0 || finish_command(&cp) < 0)
+ die("Child did not finish");
+
+ return 0;
+}
+
+static int inherit_handle_child(void)
+{
+ struct strbuf buf = STRBUF_INIT;
+
+ if (strbuf_read(&buf, 0, 0) < 0)
+ die("Could not read stdin");
+ printf("Received %s\n", buf.buf);
+ strbuf_release(&buf);
+
+ return 0;
+}
+
+int cmd__run_command(int argc, const char **argv)
+{
+ struct child_process proc = CHILD_PROCESS_INIT;
+ int jobs;
+ int ret;
+ struct run_process_parallel_opts opts = {
+ .data = &proc,
+ };
+
+ if (argc > 1 && !strcmp(argv[1], "testsuite"))
+ return testsuite(argc - 1, argv + 1);
+ if (!strcmp(argv[1], "inherited-handle"))
+ return inherit_handle(argv[0]);
+ if (!strcmp(argv[1], "inherited-handle-child"))
+ return inherit_handle_child();
+
+ if (argc >= 2 && !strcmp(argv[1], "quote-stress-test"))
+ return !!quote_stress_test(argc - 1, argv + 1);
+
+ if (argc >= 2 && !strcmp(argv[1], "quote-echo"))
+ return !!quote_echo(argc - 1, argv + 1);
+
+ if (argc < 3)
+ return 1;
+ while (!strcmp(argv[1], "env")) {
+ if (!argv[2])
+ die("env specifier without a value");
+ strvec_push(&proc.env, argv[2]);
+ argv += 2;
+ argc -= 2;
+ }
+ if (argc < 3) {
+ ret = 1;
+ goto cleanup;
+ }
+ strvec_pushv(&proc.args, (const char **)argv + 2);
+
+ if (!strcmp(argv[1], "start-command-ENOENT")) {
+ if (start_command(&proc) < 0 && errno == ENOENT) {
+ ret = 0;
+ goto cleanup;
+ }
+ fprintf(stderr, "FAIL %s\n", argv[1]);
+ return 1;
+ }
+ if (!strcmp(argv[1], "run-command")) {
+ ret = run_command(&proc);
+ goto cleanup;
+ }
+
+ if (!strcmp(argv[1], "--ungroup")) {
+ argv += 1;
+ argc -= 1;
+ opts.ungroup = 1;
+ }
+
+ jobs = atoi(argv[2]);
+ strvec_clear(&proc.args);
+ strvec_pushv(&proc.args, (const char **)argv + 3);
+
+ if (!strcmp(argv[1], "run-command-parallel")) {
+ opts.get_next_task = parallel_next;
+ } else if (!strcmp(argv[1], "run-command-abort")) {
+ opts.get_next_task = parallel_next;
+ opts.task_finished = task_finished;
+ } else if (!strcmp(argv[1], "run-command-no-jobs")) {
+ opts.get_next_task = no_job;
+ opts.task_finished = task_finished;
+ } else {
+ ret = 1;
+ fprintf(stderr, "check usage\n");
+ goto cleanup;
+ }
+ opts.processes = jobs;
+ run_processes_parallel(&opts);
+ ret = 0;
+cleanup:
+ child_process_clear(&proc);
+ return ret;
+}