test: Add ability to capture child process output

ASTERISK-30037

Change-Id: Icbf84ce05addb197a458361c35d784e460d8d6c2
This commit is contained in:
Philip Prindeville 2022-05-02 22:49:54 -06:00 committed by George Joseph
parent d13afaf302
commit b9df2c481b
3 changed files with 307 additions and 0 deletions

View File

@ -208,6 +208,27 @@ enum ast_test_command {
*/
struct ast_test;
/*!
* \brief A capture of running an external process.
*
* This contains a buffer holding stdout, another containing stderr,
* the process id of the child, and its exit code.
*/
struct ast_test_capture {
/*! \brief buffer holding stdout */
char *outbuf;
/*! \brief length of buffer holding stdout */
size_t outlen;
/*! \brief buffer holding stderr */
char *errbuf;
/*! \brief length of buffer holding stderr */
size_t errlen;
/*! \brief process id of child */
pid_t pid;
/*! \brief exit code of child */
int exitcode;
};
/*!
* \brief Contains all the initialization information required to store a new test definition
*/
@ -417,5 +438,40 @@ int __ast_test_status_update(const char *file, const char *func, int line, struc
} \
})
/*!
* \brief Release the storage (buffers) associated with capturing
* the output of an external child process.
*
* \since 19.4.0
*
* \param capture The structure describing the child process and its
* associated output.
*/
void ast_test_capture_free(struct ast_test_capture *capture);
/*!
* \brief Run a child process and capture its output and exit code.
*
* \!since 19.4.0
*
* \param capture The structure describing the child process and its
* associated output.
*
* \param file The name of the file to execute (uses $PATH to locate).
*
* \param argv The NULL-terminated array of arguments to pass to the
* child process, starting with the command name itself.
*
* \param data The buffer of input to be sent to child process's stdin;
* optional and may be NULL.
*
* \param datalen The length of the buffer, if not NULL, otherwise zero.
*
* \retval 1 for success
* \retval other failure
*/
int ast_test_capture_command(struct ast_test_capture *capture, const char *file, char *const argv[], const char *data, unsigned datalen);
#endif /* TEST_FRAMEWORK */
#endif /* _AST_TEST_H */

View File

@ -167,6 +167,9 @@ lock.o: _ASTCFLAGS+=$(call get_menuselect_cflags,DETECT_DEADLOCKS)
options.o: _ASTCFLAGS+=$(call get_menuselect_cflags,REF_DEBUG)
sched.o: _ASTCFLAGS+=$(call get_menuselect_cflags,DEBUG_SCHEDULER DUMP_SCHEDULER)
tcptls.o: _ASTCFLAGS+=$(OPENSSL_INCLUDE) -Wno-deprecated-declarations
# since we're using open_memstream(), we need to release the buffer with
# the native free() function or we might get unexpected behavior.
test.o: _ASTCFLAGS+=-DASTMM_LIBC=ASTMM_IGNORE
uuid.o: _ASTCFLAGS+=$(UUID_INCLUDE)
stasis.o: _ASTCFLAGS+=$(call get_menuselect_cflags,AO2_DEBUG)
time.o: _ASTCFLAGS+=-D_XOPEN_SOURCE=700

View File

@ -48,6 +48,16 @@
#include "asterisk/astobj2.h"
#include "asterisk/stasis.h"
#include "asterisk/json.h"
#include "asterisk/app.h" /* for ast_replace_sigchld(), etc. */
#include <stdio.h>
#include <errno.h>
#include <fcntl.h>
#include <string.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <sys/wait.h>
#include <signal.h>
/*! \since 12
* \brief The topic for test suite messages
@ -100,6 +110,42 @@ enum test_mode {
TEST_NAME_CATEGORY = 2,
};
#define zfclose(fp) \
({ if (fp != NULL) { \
fclose(fp); \
fp = NULL; \
} \
(void)0; \
})
#define zclose(fd) \
({ if (fd != -1) { \
close(fd); \
fd = -1; \
} \
(void)0; \
})
#define movefd(oldfd, newfd) \
({ if (oldfd != newfd) { \
dup2(oldfd, newfd); \
close(oldfd); \
oldfd = -1; \
} \
(void)0; \
})
#define lowerfd(oldfd) \
({ int newfd = dup(oldfd); \
if (newfd > oldfd) \
close(newfd); \
else { \
close(oldfd); \
oldfd = newfd; \
} \
(void)0; \
})
/*! List of registered test definitions */
static AST_LIST_HEAD_STATIC(tests, ast_test);
@ -267,6 +313,207 @@ void ast_test_set_result(struct ast_test *test, enum ast_test_result_state state
test->state = state;
}
void ast_test_capture_free(struct ast_test_capture *capture)
{
if (capture) {
free(capture->outbuf);
capture->outbuf = NULL;
free(capture->errbuf);
capture->errbuf = NULL;
}
capture->pid = -1;
capture->exitcode = -1;
}
int ast_test_capture_command(struct ast_test_capture *capture, const char *file, char *const argv[], const char *data, unsigned datalen)
{
int fd0[2] = { -1, -1 }, fd1[2] = { -1, -1 }, fd2[2] = { -1, -1 };
pid_t pid = -1;
int status = 0;
memset(capture, 0, sizeof(*capture));
capture->pid = capture->exitcode = -1;
if (data != NULL && datalen > 0) {
if (pipe(fd0) == -1) {
ast_log(LOG_ERROR, "Couldn't open stdin pipe: %s\n", strerror(errno));
goto cleanup;
}
fcntl(fd0[1], F_SETFL, fcntl(fd0[1], F_GETFL, 0) | O_NONBLOCK);
} else {
if ((fd0[0] = open("/dev/null", O_RDONLY)) == -1) {
ast_log(LOG_ERROR, "Couldn't open /dev/null: %s\n", strerror(errno));
goto cleanup;
}
}
if (pipe(fd1) == -1) {
ast_log(LOG_ERROR, "Couldn't open stdout pipe: %s\n", strerror(errno));
goto cleanup;
}
if (pipe(fd2) == -1) {
ast_log(LOG_ERROR, "Couldn't open stdout pipe: %s\n", strerror(errno));
goto cleanup;
}
/* we don't want anyone else reaping our children */
ast_replace_sigchld();
if ((pid = fork()) == -1) {
ast_log(LOG_ERROR, "Failed to fork(): %s\n", strerror(errno));
goto cleanup;
} else if (pid == 0) {
fclose(stdin);
zclose(fd0[1]);
zclose(fd1[0]);
zclose(fd2[0]);
movefd(fd0[0], 0);
movefd(fd1[1], 1);
movefd(fd2[1], 2);
execvp(file, argv);
ast_log(LOG_ERROR, "Failed to execv(): %s\n", strerror(errno));
exit(1);
} else {
FILE *cmd = NULL, *out = NULL, *err = NULL;
char buf[BUFSIZ];
int wstatus, n, nfds;
fd_set readfds, writefds;
unsigned i;
zclose(fd0[0]);
zclose(fd1[1]);
zclose(fd2[1]);
lowerfd(fd0[1]);
lowerfd(fd1[0]);
lowerfd(fd2[0]);
if ((cmd = fmemopen(buf, sizeof(buf), "w")) == NULL) {
ast_log(LOG_ERROR, "Failed to open memory buffer: %s\n", strerror(errno));
kill(pid, SIGKILL);
goto cleanup;
}
for (i = 0; argv[i] != NULL; ++i) {
if (i > 0) {
fputc(' ', cmd);
}
fputs(argv[i], cmd);
}
zfclose(cmd);
ast_log(LOG_TRACE, "run: %.*s\n", (int)sizeof(buf), buf);
if ((out = open_memstream(&capture->outbuf, &capture->outlen)) == NULL) {
ast_log(LOG_ERROR, "Failed to open output buffer: %s\n", strerror(errno));
kill(pid, SIGKILL);
goto cleanup;
}
if ((err = open_memstream(&capture->errbuf, &capture->errlen)) == NULL) {
ast_log(LOG_ERROR, "Failed to open error buffer: %s\n", strerror(errno));
kill(pid, SIGKILL);
goto cleanup;
}
while (1) {
n = waitpid(pid, &wstatus, WNOHANG);
if (n == pid && WIFEXITED(wstatus)) {
zclose(fd0[1]);
zclose(fd1[0]);
zclose(fd2[0]);
zfclose(out);
zfclose(err);
capture->pid = pid;
capture->exitcode = WEXITSTATUS(wstatus);
ast_log(LOG_TRACE, "run: pid %d exits %d\n", capture->pid, capture->exitcode);
break;
}
/* a function that does the opposite of ffs()
* would be handy here for finding the highest
* descriptor number.
*/
nfds = MAX(fd0[1], MAX(fd1[0], fd2[0])) + 1;
FD_ZERO(&readfds);
FD_ZERO(&writefds);
if (fd0[1] != -1) {
if (data != NULL && datalen > 0)
FD_SET(fd0[1], &writefds);
}
if (fd1[0] != -1) {
FD_SET(fd1[0], &readfds);
}
if (fd2[0] != -1) {
FD_SET(fd2[0], &readfds);
}
/* not clear that exception fds are meaningful
* with non-network descriptors.
*/
n = select(nfds, &readfds, &writefds, NULL, NULL);
if (FD_ISSET(fd0[1], &writefds)) {
n = write(fd0[1], data, datalen);
if (n > 0) {
data += n;
datalen -= MIN(datalen, n);
/* out of data, so close stdin */
if (datalen == 0)
zclose(fd0[1]);
} else {
zclose(fd0[1]);
}
}
if (FD_ISSET(fd1[0], &readfds)) {
n = read(fd1[0], buf, sizeof(buf));
if (n > 0) {
fwrite(buf, sizeof(char), n, out);
} else {
zclose(fd1[0]);
}
}
if (FD_ISSET(fd2[0], &readfds)) {
n = read(fd2[0], buf, sizeof(buf));
if (n > 0) {
fwrite(buf, sizeof(char), n, err);
} else {
zclose(fd2[0]);
}
}
}
status = 1;
cleanup:
ast_unreplace_sigchld();
zfclose(cmd);
zfclose(out);
zfclose(err);
zclose(fd0[1]);
zclose(fd1[0]);
zclose(fd1[1]);
zclose(fd2[0]);
zclose(fd2[1]);
return status;
}
}
/*
* These are the Java reserved words we need to munge so Jenkins
* doesn't barf on them.
@ -1242,3 +1489,4 @@ int ast_test_init(void)
return 0;
}