Continous integration / Tests (push) Canceled after 0s
Testing clients / Build beanstalkd (push) Canceled after 0s
Continous integration / Docker build (push) Canceled after 0s
Testing clients / Test Pheanstalk PHP (push) Canceled after 0s
Testing clients / Test Greenstalk Python (3.9) (push) Canceled after 0s
Testing clients / Test Pystalk Python (3.10) (push) Canceled after 0s
575 lines
11 KiB
C
575 lines
11 KiB
C
/* CT - simple-minded unit testing for C */
|
|
|
|
#include <signal.h>
|
|
#include <string.h>
|
|
#include <stdlib.h>
|
|
#include <stdio.h>
|
|
#include <stdarg.h>
|
|
#include <sys/types.h>
|
|
#include <sys/stat.h>
|
|
#include <unistd.h>
|
|
#include <sys/wait.h>
|
|
#include <fcntl.h>
|
|
#include <dirent.h>
|
|
#include <errno.h>
|
|
#include <sys/time.h>
|
|
#include <stdint.h>
|
|
#include <inttypes.h>
|
|
#include "internal.h"
|
|
#include "ct.h"
|
|
|
|
|
|
static char *curdir;
|
|
static int rjobfd = -1, wjobfd = -1;
|
|
int fail = 0; /* bool */
|
|
static int64 bstart, bdur;
|
|
static int btiming; /* bool */
|
|
static int64 bbytes;
|
|
enum { Second = 1000 * 1000 * 1000 };
|
|
enum { BenchTime = Second };
|
|
enum { MaxN = 1000 * 1000 * 1000 };
|
|
|
|
|
|
|
|
#ifdef __MACH__
|
|
# include <mach/mach_time.h>
|
|
|
|
static int64
|
|
nstime()
|
|
{
|
|
return (int64)mach_absolute_time();
|
|
}
|
|
|
|
#else
|
|
# include <time.h>
|
|
|
|
static int64
|
|
nstime()
|
|
{
|
|
struct timespec t;
|
|
clock_gettime(CLOCK_MONOTONIC, &t);
|
|
return (int64)(t.tv_sec)*Second + t.tv_nsec;
|
|
}
|
|
|
|
#endif
|
|
|
|
void
|
|
ctlogpn(const char *p, int n, const char *fmt, ...)
|
|
{
|
|
va_list arg;
|
|
|
|
printf("%s:%d: ", p, n);
|
|
va_start(arg, fmt);
|
|
vprintf(fmt, arg);
|
|
va_end(arg);
|
|
putchar('\n');
|
|
}
|
|
|
|
|
|
void
|
|
ctfail(void)
|
|
{
|
|
fail = 1;
|
|
}
|
|
|
|
|
|
void
|
|
ctfailnow(void)
|
|
{
|
|
fflush(NULL);
|
|
abort();
|
|
}
|
|
|
|
|
|
char *
|
|
ctdir(void)
|
|
{
|
|
return curdir;
|
|
}
|
|
|
|
|
|
void
|
|
ctresettimer(void)
|
|
{
|
|
bdur = 0;
|
|
bstart = nstime();
|
|
}
|
|
|
|
|
|
void
|
|
ctstarttimer(void)
|
|
{
|
|
if (!btiming) {
|
|
bstart = nstime();
|
|
btiming = 1;
|
|
}
|
|
}
|
|
|
|
|
|
void
|
|
ctstoptimer(void)
|
|
{
|
|
if (btiming) {
|
|
bdur += nstime() - bstart;
|
|
btiming = 0;
|
|
}
|
|
}
|
|
|
|
|
|
void
|
|
ctsetbytes(int n)
|
|
{
|
|
bbytes = (int64)n;
|
|
}
|
|
|
|
|
|
static void
|
|
die(int code, int err, const char *msg)
|
|
{
|
|
putc('\n', stderr);
|
|
|
|
if (msg && *msg) {
|
|
fputs(msg, stderr);
|
|
fputs(": ", stderr);
|
|
}
|
|
|
|
fputs(strerror(err), stderr);
|
|
putc('\n', stderr);
|
|
exit(code);
|
|
}
|
|
|
|
|
|
static int
|
|
tmpfd(void)
|
|
{
|
|
FILE *f = tmpfile();
|
|
if (!f) {
|
|
die(1, errno, "tmpfile");
|
|
}
|
|
return fileno(f);
|
|
}
|
|
|
|
|
|
static int
|
|
failed(int s)
|
|
{
|
|
return WIFSIGNALED(s) && (WTERMSIG(s) == SIGABRT);
|
|
}
|
|
|
|
|
|
static void
|
|
waittest(Test *ts)
|
|
{
|
|
Test *t;
|
|
int pid, stat;
|
|
|
|
pid = wait3(&stat, 0, 0);
|
|
if (pid == -1) {
|
|
die(3, errno, "wait");
|
|
}
|
|
killpg(pid, SIGKILL);
|
|
|
|
for (t=ts; t->f; t++) {
|
|
if (t->pid == pid) {
|
|
t->status = stat;
|
|
if (!t->status) {
|
|
putchar('.');
|
|
} else if (failed(t->status)) {
|
|
putchar('F');
|
|
} else {
|
|
putchar('E');
|
|
}
|
|
fflush(stdout);
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
static void
|
|
start(Test *t)
|
|
{
|
|
t->fd = tmpfd();
|
|
strcpy(t->dir, TmpDirPat);
|
|
if (mkdtemp(t->dir) == NULL) {
|
|
die(1, errno, "mkdtemp");
|
|
}
|
|
fflush(NULL);
|
|
t->pid = fork();
|
|
if (t->pid < 0) {
|
|
die(1, errno, "fork");
|
|
} else if (!t->pid) {
|
|
setpgid(0, 0);
|
|
if (dup2(t->fd, 1) == -1) {
|
|
die(3, errno, "dup2");
|
|
}
|
|
if (close(t->fd) == -1) {
|
|
die(3, errno, "fclose");
|
|
}
|
|
if (dup2(1, 2) == -1) {
|
|
die(3, errno, "dup2");
|
|
}
|
|
curdir = t->dir;
|
|
t->f();
|
|
if (fail) {
|
|
ctfailnow();
|
|
}
|
|
exit(0);
|
|
}
|
|
setpgid(t->pid, t->pid);
|
|
}
|
|
|
|
|
|
static void
|
|
runalltest(Test *ts, int limit)
|
|
{
|
|
int nrun = 0;
|
|
Test *t;
|
|
for (t=ts; t->f; t++) {
|
|
if (nrun >= limit) {
|
|
waittest(ts);
|
|
nrun--;
|
|
}
|
|
start(t);
|
|
nrun++;
|
|
}
|
|
for (; nrun; nrun--) {
|
|
waittest(ts);
|
|
}
|
|
}
|
|
|
|
|
|
static void
|
|
copyfd(FILE *out, int in)
|
|
{
|
|
ssize_t n;
|
|
char buf[1024]; /* arbitrary size */
|
|
|
|
while ((n = read(in, buf, sizeof(buf))) != 0) {
|
|
if (fwrite(buf, 1, n, out) != (size_t)n) {
|
|
die(3, errno, "fwrite");
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
/*
|
|
Removes path and all of its children.
|
|
Writes errors to stderr and keeps going.
|
|
If path doesn't exist, rmtree returns silently.
|
|
*/
|
|
static void
|
|
rmtree(char *path)
|
|
{
|
|
int r = unlink(path);
|
|
if (r == 0 || errno == ENOENT) {
|
|
return; /* success */
|
|
}
|
|
int unlinkerr = errno;
|
|
|
|
DIR *d = opendir(path);
|
|
if (!d) {
|
|
if (errno == ENOTDIR) {
|
|
fprintf(stderr, "ct: unlink: %s\n", strerror(unlinkerr));
|
|
} else {
|
|
perror("ct: opendir");
|
|
}
|
|
fprintf(stderr, "ct: path %s\n", path);
|
|
return;
|
|
}
|
|
struct dirent *ent;
|
|
while ((ent = readdir(d))) {
|
|
if (strcmp(ent->d_name, ".") == 0 || strcmp(ent->d_name, "..") == 0) {
|
|
continue;
|
|
}
|
|
int n = strlen(path) + 1 + strlen(ent->d_name);
|
|
char s[n+1];
|
|
sprintf(s, "%s/%s", path, ent->d_name);
|
|
rmtree(s);
|
|
}
|
|
closedir(d);
|
|
r = rmdir(path);
|
|
if (r == -1) {
|
|
perror("ct: rmdir");
|
|
fprintf(stderr, "ct: path %s\n", path);
|
|
}
|
|
}
|
|
|
|
|
|
static void
|
|
runbenchn(Benchmark *b, int n)
|
|
{
|
|
int outfd = tmpfd();
|
|
int durfd = tmpfd();
|
|
strcpy(b->dir, TmpDirPat);
|
|
if (mkdtemp(b->dir) == NULL) {
|
|
die(1, errno, "mkdtemp");
|
|
}
|
|
fflush(NULL);
|
|
int pid = fork();
|
|
if (pid < 0) {
|
|
die(1, errno, "fork");
|
|
} else if (!pid) {
|
|
setpgid(0, 0);
|
|
if (dup2(outfd, 1) == -1) {
|
|
die(3, errno, "dup2");
|
|
}
|
|
if (close(outfd) == -1) {
|
|
die(3, errno, "fclose");
|
|
}
|
|
if (dup2(1, 2) == -1) {
|
|
die(3, errno, "dup2");
|
|
}
|
|
curdir = b->dir;
|
|
ctstarttimer();
|
|
b->f(n);
|
|
ctstoptimer();
|
|
if (write(durfd, &bdur, sizeof bdur) != sizeof bdur) {
|
|
die(3, errno, "write");
|
|
}
|
|
if (write(durfd, &bbytes, sizeof bbytes) != sizeof bbytes) {
|
|
die(3, errno, "write");
|
|
}
|
|
exit(0);
|
|
}
|
|
setpgid(pid, pid);
|
|
|
|
pid = waitpid(pid, &b->status, 0);
|
|
if (pid == -1) {
|
|
die(3, errno, "wait");
|
|
}
|
|
killpg(pid, SIGKILL);
|
|
rmtree(b->dir);
|
|
if (b->status != 0) {
|
|
putchar('\n');
|
|
lseek(outfd, 0, SEEK_SET);
|
|
copyfd(stdout, outfd);
|
|
return;
|
|
}
|
|
|
|
lseek(durfd, 0, SEEK_SET);
|
|
int r = read(durfd, &b->dur, sizeof b->dur);
|
|
if (r != sizeof b->dur) {
|
|
perror("read");
|
|
b->status = 1;
|
|
}
|
|
r = read(durfd, &b->bytes, sizeof b->bytes);
|
|
if (r != sizeof b->bytes) {
|
|
perror("read");
|
|
b->status = 1;
|
|
}
|
|
}
|
|
|
|
|
|
/* rounddown10 rounds a number down to the nearest power of 10. */
|
|
static int
|
|
rounddown10(int n)
|
|
{
|
|
int tens = 0;
|
|
/* tens = floor(log_10(n)) */
|
|
while (n >= 10) {
|
|
n = n / 10;
|
|
tens++;
|
|
}
|
|
/* result = 10**tens */
|
|
int i, result = 1;
|
|
for (i = 0; i < tens; i++) {
|
|
result *= 10;
|
|
}
|
|
return result;
|
|
}
|
|
|
|
|
|
/* roundup rounds n up to a number of the form [1eX, 2eX, 5eX]. */
|
|
static int
|
|
roundup(int n)
|
|
{
|
|
int base = rounddown10(n);
|
|
if (n <= base)
|
|
return base;
|
|
if (n <= 2*base)
|
|
return 2*base;
|
|
if (n <= 3*base)
|
|
return 3*base;
|
|
if (n <= 5*base)
|
|
return 5*base;
|
|
return 10*base;
|
|
}
|
|
|
|
|
|
static int
|
|
min(int a, int b)
|
|
{
|
|
if (a < b) {
|
|
return a;
|
|
}
|
|
return b;
|
|
}
|
|
|
|
|
|
static int
|
|
max(int a, int b)
|
|
{
|
|
if (a > b) {
|
|
return a;
|
|
}
|
|
return b;
|
|
}
|
|
|
|
|
|
static void
|
|
runbench(Benchmark *b)
|
|
{
|
|
printf("%s\t", b->name);
|
|
fflush(stdout);
|
|
int n = 1;
|
|
runbenchn(b, n);
|
|
while (b->status == 0 && b->dur < BenchTime && n < MaxN) {
|
|
int last = n;
|
|
/* Predict iterations/sec. */
|
|
int nsop = b->dur / n;
|
|
if (nsop == 0) {
|
|
n = MaxN;
|
|
} else {
|
|
n = BenchTime / nsop;
|
|
}
|
|
/* Run more iterations than we think we'll need for a second (1.2x).
|
|
Don't grow too fast in case we had timing errors previously.
|
|
Be sure to run at least one more than last time. */
|
|
n = max(min(n+n/5, 100*last), last+1);
|
|
/* Round up to something easy to read. */
|
|
n = roundup(n);
|
|
runbenchn(b, n);
|
|
}
|
|
if (b->status == 0) {
|
|
printf("%8d\t%10" PRId64 " ns/op", n, b->dur/n);
|
|
if (b->bytes > 0) {
|
|
double mbs = 0;
|
|
if (b->dur > 0) {
|
|
int64 sec = b->dur / 1000L / 1000L / 1000L;
|
|
int64 nsec = b->dur % 1000000000L;
|
|
double dur = (double)sec + (double)nsec*.0000000001;
|
|
mbs = ((double)b->bytes * (double)n / 1000000) / dur;
|
|
}
|
|
printf("\t%7.2f MB/s", mbs);
|
|
}
|
|
putchar('\n');
|
|
} else {
|
|
if (failed(b->status)) {
|
|
printf("failure");
|
|
} else {
|
|
printf("error");
|
|
if (WIFEXITED(b->status)) {
|
|
printf(" (exit status %d)", WEXITSTATUS(b->status));
|
|
}
|
|
if (WIFSIGNALED(b->status)) {
|
|
printf(" (signal %d)", WTERMSIG(b->status));
|
|
}
|
|
}
|
|
putchar('\n');
|
|
}
|
|
}
|
|
|
|
|
|
static void
|
|
runallbench(Benchmark *b)
|
|
{
|
|
for (; b->f; b++) {
|
|
runbench(b);
|
|
}
|
|
}
|
|
|
|
|
|
static int
|
|
report(Test *t)
|
|
{
|
|
int nfail = 0, nerr = 0;
|
|
|
|
putchar('\n');
|
|
for (; t->f; t++) {
|
|
rmtree(t->dir);
|
|
if (!t->status) {
|
|
continue;
|
|
}
|
|
|
|
printf("\n%s: ", t->name);
|
|
if (failed(t->status)) {
|
|
nfail++;
|
|
printf("failure");
|
|
} else {
|
|
nerr++;
|
|
printf("error");
|
|
if (WIFEXITED(t->status)) {
|
|
printf(" (exit status %d)", WEXITSTATUS(t->status));
|
|
}
|
|
if (WIFSIGNALED(t->status)) {
|
|
printf(" (signal %d)", WTERMSIG(t->status));
|
|
}
|
|
}
|
|
|
|
putchar('\n');
|
|
lseek(t->fd, 0, SEEK_SET);
|
|
copyfd(stdout, t->fd);
|
|
}
|
|
|
|
if (nfail || nerr) {
|
|
printf("\n%d failures; %d errors.\n", nfail, nerr);
|
|
} else {
|
|
printf("\nPASS\n");
|
|
}
|
|
return nfail || nerr;
|
|
}
|
|
|
|
|
|
static int
|
|
readtokens()
|
|
{
|
|
int n = 1;
|
|
char c, *s;
|
|
char *v = getenv("MAKEFLAGS");
|
|
if (v == NULL)
|
|
return n;
|
|
if ((s = strstr(v, " --jobserver-fds="))) {
|
|
rjobfd = (int)strtol(s+17, &s, 10); /* skip " --jobserver-fds=" */
|
|
wjobfd = (int)strtol(s+1, NULL, 10); /* skip comma */
|
|
}
|
|
if (rjobfd >= 0) {
|
|
fcntl(rjobfd, F_SETFL, fcntl(rjobfd, F_GETFL)|O_NONBLOCK);
|
|
while (read(rjobfd, &c, 1) > 0) {
|
|
n++;
|
|
}
|
|
}
|
|
return n;
|
|
}
|
|
|
|
|
|
static void
|
|
writetokens(int n)
|
|
{
|
|
char c = '+';
|
|
if (wjobfd >= 0) {
|
|
fcntl(wjobfd, F_SETFL, fcntl(wjobfd, F_GETFL)|O_NONBLOCK);
|
|
for (; n>1; n--) {
|
|
if (write(wjobfd, &c, 1) != 1) {
|
|
/* ignore error; nothing we can do anyway */
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
int
|
|
main(int argc, char **argv)
|
|
{
|
|
int n = readtokens();
|
|
runalltest(ctmaintest, n);
|
|
writetokens(n);
|
|
int code = report(ctmaintest);
|
|
if (code != 0) {
|
|
return code;
|
|
}
|
|
if (argc == 2 && strcmp(argv[1], "-b") == 0) {
|
|
runallbench(ctmainbench);
|
|
}
|
|
return 0;
|
|
}
|