/* SPDX-License-Identifier: MIT */
/*
 * Description: run various min_timeout tests
 *
 */
#include <errno.h>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <sys/time.h>
#include <pthread.h>

#include "liburing.h"
#include "helpers.h"

struct data {
	pthread_barrier_t startup;
	unsigned long usec_sleep;
	int out_fds[8];
	int nr_fds;
};

static int time_pass(struct timeval *start, unsigned long min_t,
		     unsigned long max_t, const char *name)
{
	unsigned long elapsed;

	elapsed = mtime_since_now(start);
	if (elapsed < min_t || elapsed > max_t) {
		fprintf(stderr, "%s fails time check\n", name);
		fprintf(stderr, " elapsed=%lu, min=%lu, max=%lu\n", elapsed,
				min_t, max_t);
		return T_EXIT_FAIL;
	}
	return T_EXIT_PASS;
}

static void *pipe_write(void *data)
{
	struct data *d = data;
	char buf[32];
	int i;

	memset(buf, 0x55, sizeof(buf));

	pthread_barrier_wait(&d->startup);

	if (d->usec_sleep)
		usleep(d->usec_sleep);

	for (i = 0; i < d->nr_fds; i++) {
		int ret;

		ret = write(d->out_fds[i], buf, sizeof(buf));
		if (ret < 0) {
			perror("write");
			return NULL;
		}
	}

	return NULL;
}

static int __test_writes(struct io_uring *ring, int npipes, int usec_sleep,
			 int usec_wait, int min_t, int max_t, const char *name)
{
	struct __kernel_timespec ts;
	struct io_uring_cqe *cqe;
	struct io_uring_sqe *sqe;
	struct timeval tv;
	int ret, i, fds[4][2];
	pthread_t thread;
	struct data d;
	char buf[32];
	void *tret;

	for (i = 0; i < npipes; i++) {
		if (pipe(fds[i]) < 0) {
			perror("pipe");
			return T_EXIT_FAIL;
		}
		d.out_fds[i] = fds[i][1];
	}
	d.nr_fds = npipes;

	pthread_barrier_init(&d.startup, NULL, 2);
	d.usec_sleep = usec_sleep;

	pthread_create(&thread, NULL, pipe_write, &d);
	pthread_barrier_wait(&d.startup);

	for (i = 0; i < npipes; i++) {
		sqe = io_uring_get_sqe(ring);
		io_uring_prep_read(sqe, fds[i][0], buf, sizeof(buf), 0);
	}

	io_uring_submit(ring);

	ts.tv_sec = 1;
	ts.tv_nsec = 0;
	gettimeofday(&tv, NULL);
	ret = io_uring_wait_cqes_min_timeout(ring, &cqe, 4, &ts, usec_wait, NULL);
	if (ret) {
		fprintf(stderr, "wait_cqes: %d\n", ret);
		return T_EXIT_FAIL;
	}

	ret = time_pass(&tv, min_t, max_t, name);

	io_uring_cq_advance(ring, npipes);

	pthread_join(thread, &tret);
	for (i = 0; i < npipes; i++) {
		close(fds[i][0]);
		close(fds[i][1]);
	}
	return ret;
}
/*
 * Test doing min_wait for N events, where 0 events are already available
 * on wait enter but N/2 are posted within the min_wait window. We'll expect to
 * return when the min_wait window expires.
 */
static int test_some_wait(struct io_uring *ring)
{
	return __test_writes(ring, 2, 1000, 100000, 95, 120, __FUNCTION__);
}

/*
 * Test doing min_wait for N events, where 0 events are already available
 * on wait enter but N are posted within the min_wait window. We'll expect to
 * return upon arrival of the N events, not the full min_wait window.
 */
static int test_post_wait(struct io_uring *ring)
{
	return __test_writes(ring, 4, 10000, 200000, 9, 12, __FUNCTION__);
}

/*
 * Test doing min_wait for N events, where 0 events are already available
 * on wait enter and one is posted after the min_wait timeout has expired.
 * That first event should cause wait to abort, even if the task has asked
 * for more to wait on.
 */
static int test_late(struct io_uring *ring)
{
	return __test_writes(ring, 1, 100000, 10000, 95, 120, __FUNCTION__);
}

static int __test_nop(struct io_uring *ring, int nr_nops, int min_t, int max_t,
		      unsigned long long_wait, const char *name)
{
	struct __kernel_timespec ts;
	struct io_uring_cqe *cqe;
	struct timeval tv;
	int i, ret;

	for (i = 0; i < nr_nops; i++) {
		struct io_uring_sqe *sqe;

		sqe = io_uring_get_sqe(ring);
		io_uring_prep_nop(sqe);
	}

	if (nr_nops)
		io_uring_submit(ring);

	ts.tv_sec = 0;
	ts.tv_nsec = long_wait * 1000;
	gettimeofday(&tv, NULL);
	ret = io_uring_wait_cqes_min_timeout(ring, &cqe, 4, &ts, 50000, NULL);
	io_uring_cq_advance(ring, nr_nops);
	if (nr_nops) {
		if (ret) {
			fprintf(stderr, "wait_cqes: %d\n", ret);
			return T_EXIT_FAIL;
		}
	} else {
		if (ret != -ETIME) {
			fprintf(stderr, "wait_cqes: %d\n", ret);
			return T_EXIT_FAIL;
		}
	}

	return time_pass(&tv, min_t, max_t, name);
}

/*
 * Test doing min_wait for N events, where N/2 events are already available
 * on wait enter. This should abort waiting after min_wait, not do the full
 * wait.
 */
static int test_some(struct io_uring *ring)
{
	return __test_nop(ring, 2, 45, 55, 100000, __FUNCTION__);
}

/*
 * Test doing min_wait for N events, where N events are already available
 * on wait enter.
 */
static int test_already(struct io_uring *ring)
{
	return __test_nop(ring, 4, 0, 1, 100000, __FUNCTION__);
}

/*
 * Test doing min_wait for N events, and nothing ever gets posted. We'd
 * expect the time to be the normal wait time, not the min_wait time.
 */
static int test_nothing(struct io_uring *ring)
{
	return __test_nop(ring, 0, 95, 110, 100000, __FUNCTION__);
}

/*
 * Test doing min_wait for N events, and nothing ever gets posted, and use
 * a min_wait time that's bigger than the total wait. We only expect the
 * min_wait to elapse.
 */
static int test_min_wait_biggest(struct io_uring *ring)
{
	return __test_nop(ring, 0, 45, 55, 20000, __FUNCTION__);
}

/*
 * Test doing min_wait for N events, and nothing ever gets posted, and use
 * a min_wait time that's roughly equal to the total wait. We only expect the
 * min_wait to elapse.
 */
static int test_min_wait_equal(struct io_uring *ring)
{
	return __test_nop(ring, 0, 45, 55, 50001, __FUNCTION__);
}

int main(int argc, char *argv[])
{
	struct io_uring ring1, ring2;
	struct io_uring_params p = { };
	int ret;

	if (argc > 1)
		return 0;

	ret = t_create_ring_params(8, &ring1, &p);
	if (ret == T_SETUP_SKIP)
		return T_EXIT_SKIP;
	else if (ret != T_SETUP_OK)
		return ret;
	if (!(p.features & IORING_FEAT_MIN_TIMEOUT))
		return T_EXIT_SKIP;

	p.flags = IORING_SETUP_SINGLE_ISSUER|IORING_SETUP_DEFER_TASKRUN;
	ret = t_create_ring_params(8, &ring2, &p);
	if (ret == T_SETUP_SKIP)
		return T_EXIT_SKIP;
	else if (ret != T_SETUP_OK)
		return ret;

	ret = test_already(&ring1);
	if (ret == T_EXIT_FAIL || ret == T_EXIT_SKIP)
		return ret;

	ret = test_already(&ring2);
	if (ret == T_EXIT_FAIL)
		return T_EXIT_FAIL;

	ret = test_some(&ring1);
	if (ret == T_EXIT_FAIL || ret == T_EXIT_SKIP)
		return ret;

	ret = test_some(&ring2);
	if (ret == T_EXIT_FAIL)
		return T_EXIT_FAIL;

	ret = test_late(&ring1);
	if (ret == T_EXIT_FAIL || ret == T_EXIT_SKIP)
		return ret;

	ret = test_late(&ring2);
	if (ret == T_EXIT_FAIL)
		return T_EXIT_FAIL;

	ret = test_post_wait(&ring1);
	if (ret == T_EXIT_FAIL || ret == T_EXIT_SKIP)
		return ret;

	ret = test_post_wait(&ring2);
	if (ret == T_EXIT_FAIL)
		return T_EXIT_FAIL;

	ret = test_some_wait(&ring1);
	if (ret == T_EXIT_FAIL || ret == T_EXIT_SKIP)
		return ret;

	ret = test_some_wait(&ring2);
	if (ret == T_EXIT_FAIL)
		return T_EXIT_FAIL;

	ret = test_nothing(&ring1);
	if (ret == T_EXIT_FAIL || ret == T_EXIT_SKIP)
		return ret;

	ret = test_nothing(&ring2);
	if (ret == T_EXIT_FAIL)
		return T_EXIT_FAIL;

	ret = test_min_wait_biggest(&ring1);
	if (ret == T_EXIT_FAIL || ret == T_EXIT_SKIP)
		return ret;

	ret = test_min_wait_biggest(&ring2);
	if (ret == T_EXIT_FAIL)
		return T_EXIT_FAIL;

	ret = test_min_wait_equal(&ring1);
	if (ret == T_EXIT_FAIL || ret == T_EXIT_SKIP)
		return ret;

	ret = test_min_wait_equal(&ring2);
	if (ret == T_EXIT_FAIL)
		return T_EXIT_FAIL;

	io_uring_queue_exit(&ring1);
	io_uring_queue_exit(&ring2);
	return T_EXIT_PASS;
}
