add script invocation helpers

This commit is contained in:
Conor Hughes 2021-12-04 23:10:36 -08:00
parent 69dc2817a4
commit bb07e35925
10 changed files with 304 additions and 4 deletions

View File

@ -33,7 +33,7 @@ clean: clean_libyachtrock
LIBYACHTROCK_GENERATED_SRC :=
LIBYACHTROCK_GENERATED_SRC := $(patsubst %,$(LIBYACHTROCK_DIR)src/%,$(LIBYACHTROCK_GENERATED_SRC))
LIBYACHTROCK_STATIC_SRC := yachtrock.c runtime.c testcase.c results.c yrutil.c selector.c version.c
LIBYACHTROCK_STATIC_SRC := yachtrock.c runtime.c testcase.c results.c yrutil.c selector.c script_helpers.c version.c
ifeq ($(YACHTROCK_MULTIPROCESS),1)
LIBYACHTROCK_STATIC_SRC += multiprocess.c multiprocess_inferior.c multiprocess_superior.c
endif

View File

@ -2,7 +2,7 @@
test: test_libyachtrock
LIBYACHTROCK_TESTSRC := selftests_collection.c basic_tests.c result_store_tests.c assertion_tests.c testcase_tests.c run_under_store_tests.c selector_tests.c
LIBYACHTROCK_TESTSRC := selftests_collection.c basic_tests.c result_store_tests.c assertion_tests.c testcase_tests.c run_under_store_tests.c selector_tests.c script_helpers_tests.c
ifeq ($(YACHTROCK_MULTIPROCESS),1)
LIBYACHTROCK_TESTSRC += multiprocess_basic_tests.c
endif

View File

@ -6,10 +6,12 @@
#ifdef __cplusplus
#define YACHTROCK_EXTERN extern "C"
#define YACHTROCK_NORETURN
#define YACHTROCK_RESTRICT
#else
#define YACHTROCK_EXTERN extern
#include <stdnoreturn.h>
#define YACHTROCK_NORETURN noreturn
#define YACHTROCK_RESTRICT restrict
#endif
#define YR_STR(X) #X

View File

@ -0,0 +1,43 @@
#ifndef YACHTROCK_SCRIPT_HELPERS_H
#define YACHTROCK_SCRIPT_HELPERS_H
#include <yachtrock/base.h>
#include <stdbool.h>
#define YACHTROCK_HAS_SCRIPT_HELPERS (YACHTROCK_UNIXY && YACHTROCK_POSIXY)
#if YACHTROCK_HAS_SCRIPT_HELPERS
/**
* Invoke a subprocess and wait for it to complete.
*
* The subprocess is invoked with the given arguments, environment, and file descriptors.
*
* The program to be executed is taken from the first element in the argument array, which is
* NULL-terminated and may be relative. The argument array must be provided and it's length must be
* at least 1. The environment array is optional; if not provided, the environment of the caller is
* copied. The given file descriptors are optional; if they are -1, the corresponding stream is
* closed in the subprocess.
*
* If the subprocess was invoked successfully its exit status is written to the int pointed to by
* stat_loc, if it is not a NULL pointer.
*
* If the subprocess was created and waited for successfully, the function returns true. Otherwise,
* it returns false and sets errno.
*/
YACHTROCK_EXTERN bool yr_invoke_subprocess(const char * const * YACHTROCK_RESTRICT argv,
const char * const * YACHTROCK_RESTRICT envp,
int stdin_fd, int stdout_fd, int stderr_fd,
int *stat_loc);
/**
* yr_invoke_subprocess, but it automatically asserts that the invoked process exits with a code of
* 0.
*/
YACHTROCK_EXTERN bool yr_invoke_subprocess_with_assert(const char * const * YACHTROCK_RESTRICT argv,
const char * const * YACHTROCK_RESTRICT envp,
int stdin_fd, int stdout_fd, int stderr_fd,
int *stat_loc);
#endif // YACHTROCK_HAS_SCRIPT_HELPERS
#endif // ndef YACHTROCK_SCRIPT_HELPERS_H

View File

@ -18,11 +18,11 @@ typedef void (*yr_test_case_teardown_function)(yr_test_case_t testcase);
typedef void (*yr_test_suite_setup_function)(yr_test_suite_t suite);
typedef void (*yr_test_suite_teardown_function)(yr_test_suite_t suite);
#define __YR_DEVARIADICIFY_2(dummy, A, ...) A
#define YR_DEVARIADICIFY_2(dummy, A, ...) A
/**
* Helper macro to define a testcase function.
*/
#define YR_TESTCASE(name, ...) void name(yr_test_case_t __YR_DEVARIADICIFY_2(dummy, ##__VA_ARGS__ , testcase) )
#define YR_TESTCASE(name, ...) void name(yr_test_case_t YR_DEVARIADICIFY_2(dummy, ##__VA_ARGS__ , testcase) )
/**
* A single test case.

View File

@ -6,8 +6,10 @@
#include <yachtrock/base.h>
#include <yachtrock/assert.h>
#include <yachtrock/testcase.h>
#include <yachtrock/script_helpers.h>
#include <yachtrock/results.h>
#include <yachtrock/selector.h>
#include <yachtrock/version.h>
/**
* Runtime callbacks and result hooks that print status to stderr.

111
src/script_helpers.c Normal file
View File

@ -0,0 +1,111 @@
#include <yachtrock/script_helpers.h>
#include <yachtrock/assert.h>
#if YACHTROCK_HAS_SCRIPT_HELPERS
#include <spawn.h>
#include "yrutil.h"
extern char **environ;
static int add_stream_actions(posix_spawn_file_actions_t *actions, int fd, int nominal_fd)
{
int result = 0;
if ( fd == -1 ) {
result = posix_spawn_file_actions_addclose(actions, nominal_fd);
if ( result != 0 ) {
yr_warnc(result, "posix_spawn_file_actions_addclose failed");
}
} else {
result = posix_spawn_file_actions_adddup2(actions, fd, nominal_fd);
if ( result != 0 ) {
yr_warnc(result, "posix_spawn_file_actions_adddup2 failed");
}
}
return result;
}
bool yr_invoke_subprocess(const char * const * YACHTROCK_RESTRICT argv,
const char * const * YACHTROCK_RESTRICT envp,
int stdin_fd, int stdout_fd, int stderr_fd,
int *stat_loc)
{
bool ok = false;
int scratch_err = 0;
YR_RUNTIME_ASSERT(argv && argv[0], "bad argv in %s!", __FUNCTION__);
const char *prog = argv[0];
if ( envp == NULL ) {
envp = (const char **)environ; // we solemnly swear not to perform Shenanigans
}
posix_spawn_file_actions_t file_actions;
if ( (scratch_err = posix_spawn_file_actions_init(&file_actions)) != 0 ) {
errno = scratch_err;
goto out_noactions;
}
if ( (scratch_err = add_stream_actions(&file_actions, stdin_fd, 0)) != 0 ) {
goto out;
}
if ( (scratch_err = add_stream_actions(&file_actions, stdout_fd, 1)) != 0 ) {
goto out;
}
if ( (scratch_err = add_stream_actions(&file_actions, stderr_fd, 2)) != 0 ) {
goto out;
}
pid_t pid = 0;
if ( (scratch_err =
posix_spawn(&pid, prog, &file_actions, NULL, (char **)argv, (char **)envp)) != 0 ) {
yr_warnc(scratch_err, "posix_spawn failed");
goto out;
}
pid_t awaited;
int wait_stat_loc;
do {
awaited = waitpid(pid, &wait_stat_loc, 0);
} while ( awaited >= 0 && !(WIFEXITED(wait_stat_loc) || WIFSIGNALED(wait_stat_loc)) );
if ( awaited < 0 ) {
yr_warn("waitpid failed");
goto out;
}
YR_RUNTIME_ASSERT(awaited == pid, "waitpid() on %d returned bogus pid %d?",
(int)pid, (int)awaited);
if ( stat_loc ) {
*stat_loc = wait_stat_loc;
}
/* All done and it feels so good! */
ok = true;
out:
posix_spawn_file_actions_destroy(&file_actions);
out_noactions:
return ok;
}
YACHTROCK_EXTERN bool yr_invoke_subprocess_with_assert(const char * const * YACHTROCK_RESTRICT argv,
const char * const * YACHTROCK_RESTRICT envp,
int stdin_fd, int stdout_fd, int stderr_fd,
int *stat_loc)
{
int my_stat_loc;
bool invoke_ok = yr_invoke_subprocess(argv, envp, stdin_fd, stdout_fd, stderr_fd, &my_stat_loc);
YR_ASSERT(invoke_ok, "failed to start subprocess!");
if ( invoke_ok ) {
YR_ASSERT(WIFEXITED(my_stat_loc) && WEXITSTATUS(my_stat_loc) == 0,
"subprocess exited uncleanly!");
}
if ( invoke_ok && stat_loc ) {
*stat_loc = my_stat_loc;
}
return invoke_ok;
}
#endif // YACHTROCK_HAS_SCRIPT_HELPERS

135
test/script_helpers_tests.c Normal file
View File

@ -0,0 +1,135 @@
#include <yachtrock/script_helpers.h>
#if YACHTROCK_HAS_SCRIPT_HELPERS
#include <stdio.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <limits.h>
#include <unistd.h>
#include "yrtests.h"
static const char basic_invoke_script[] =
"#!/bin/sh\n"
"read line\n"
"if [ \"$line\" = \"cool script\" ]; then\n"
" echo invoke success woo\n"
" exit 0\n"
"else\n"
" echo invoke failure boo >&2\n"
" exit 12\n"
"fi\n";
static bool write_basic_invoke_script(char *out_path_buf)
{
char temp_file_name[] = "/tmp/tempscript.XXXX";
int tmpfd = mkstemp(temp_file_name);
YR_ASSERT(tmpfd >= 0);
if ( tmpfd < 0 ) { return false; /* bail */ }
YR_ASSERT_EQUAL(fchmod(tmpfd, 0700), 0);
FILE *f = fdopen(tmpfd, "w");
size_t nwritten = 0;
while ( nwritten < sizeof(basic_invoke_script) && !ferror(f) ) {
size_t this_write = sizeof(basic_invoke_script) - nwritten;
nwritten += fwrite(basic_invoke_script + nwritten, 1, this_write, f);
}
YR_ASSERT_EQUAL(nwritten, sizeof(basic_invoke_script));
fclose(f);
strcpy(out_path_buf, temp_file_name);
return nwritten == sizeof(basic_invoke_script);
}
static YR_TESTCASE(test_script_basic_invoke)
{
char execpath[PATH_MAX];
if ( !write_basic_invoke_script(execpath) ) {
return;
}
fprintf(stderr, "gonna exec %s\n", execpath);
int p[2];
YR_ASSERT_EQUAL(pipe(p), 0);
const char *data = "cool script\n";
YR_ASSERT_EQUAL(write(p[1], data, strlen(data)), strlen(data));
const char *script_argv[] = { execpath, NULL };
YR_ASSERT(yr_invoke_subprocess_with_assert(script_argv, NULL, p[0], 1, 2, NULL));
close(p[0]);
close(p[1]);
}
static YR_TESTCASE(test_script_test_pipe)
{
char execpath[PATH_MAX];
if ( !write_basic_invoke_script(execpath) ) {
return;
}
fprintf(stderr, "gonna exec %s\n", execpath);
int sub_stdin_pipe[2];
int sub_stderr_pipe[2];
YR_ASSERT_EQUAL(pipe(sub_stdin_pipe), 0);
YR_ASSERT_EQUAL(pipe(sub_stderr_pipe), 0);
const char *script_argv[] = { execpath, NULL };
int stat_loc;
close(sub_stdin_pipe[1]);
YR_ASSERT(yr_invoke_subprocess(script_argv, NULL, sub_stdin_pipe[0], 1, sub_stderr_pipe[1], &stat_loc));
char read_buf[128];
memset(read_buf, 0, sizeof(read_buf));
char *expected_read = "invoke failure boo\n";
size_t nread = read(sub_stderr_pipe[0], read_buf, sizeof(read_buf) - 1);
YR_ASSERT_EQUAL(nread, strlen(expected_read), "read %zu bytes instead of %zu",
nread, strlen(expected_read));
fwrite(read_buf, 1, nread, stderr);
close(sub_stderr_pipe[0]);
close(sub_stderr_pipe[1]);
YR_ASSERT(WIFEXITED(stat_loc));
YR_ASSERT_EQUAL(WEXITSTATUS(stat_loc), 12);
}
static YR_TESTCASE(test_test_fails_if_script_fails_invoke_expected_fail)
{
char execpath[PATH_MAX];
if ( !write_basic_invoke_script(execpath) ) {
return;
}
fprintf(stderr, "gonna exec %s\n", execpath);
const char *script_argv[] = { execpath, NULL };
yr_invoke_subprocess_with_assert(script_argv, NULL, -1, 1, 2, NULL);
}
static YR_TESTCASE(test_test_fails_if_script_cant_spawn_invoke_expected_fail)
{
char execpath[PATH_MAX];
if ( !write_basic_invoke_script(execpath) ) {
return;
}
const char *script_argv[] = { "/tmp/nonexistent", NULL };
yr_invoke_subprocess_with_assert(script_argv, NULL, -1, 1, 2, NULL);
}
static YR_TESTCASE(test_test_fails_if_script_fails)
{
// Script is going to die a flaming death without any stdin at all, but it should still fail.
yr_test_suite_t suite =
yr_create_suite_from_functions("testing test fails if script fails",
NULL, YR_NO_CALLBACKS,
test_test_fails_if_script_fails_invoke_expected_fail);
bool ok = yr_basic_run_suite(suite);
free(suite);
YR_ASSERT_FALSE(ok);
suite =
yr_create_suite_from_functions("testing test fails if script can't spawn",
NULL, YR_NO_CALLBACKS,
test_test_fails_if_script_cant_spawn_invoke_expected_fail);
ok = yr_basic_run_suite(suite);
free(suite);
YR_ASSERT_FALSE(ok);
}
yr_test_suite_t yr_create_script_helpers_suite(void)
{
return yr_create_suite_from_functions("script helpers tests", NULL, YR_NO_CALLBACKS,
test_script_basic_invoke, test_script_test_pipe,
test_test_fails_if_script_fails);
}
#endif // YACHTROCK_HAS_SCRIPT_HELPERS

View File

@ -10,6 +10,9 @@ yr_test_suite_collection_t yachtrock_selftests_collection(void)
yr_create_run_under_store_suite(),
#if YACHTROCK_MULTIPROCESS
yr_create_multiprocess_suite(),
#endif
#if YACHTROCK_HAS_SCRIPT_HELPERS
yr_create_script_helpers_suite(),
#endif
yr_create_selector_suite(),
};

View File

@ -11,4 +11,8 @@ extern yr_test_suite_t yr_create_selector_suite(void);
extern yr_test_suite_t yr_create_multiprocess_suite(void);
#endif
#if YACHTROCK_HAS_SCRIPT_HELPERS
extern yr_test_suite_t yr_create_script_helpers_suite(void);
#endif
extern yr_test_suite_collection_t yachtrock_selftests_collection(void);