Add test case for thread exiting with pending IO

A thread exiting should not have its IO canceled, that should only happen
when the parent exits. Test that we are able to issue IO from a thread,
with the parent reaping the completion when the thread exits.

Signed-off-by: Jens Axboe <axboe@kernel.dk>
diff --git a/test/Makefile b/test/Makefile
index 6aa1788..ce5d9e3 100644
--- a/test/Makefile
+++ b/test/Makefile
@@ -102,6 +102,7 @@
 	stdout \
 	submit-reuse \
 	teardowns \
+	thread-exit \
 	timeout \
 	timeout-new \
 	timeout-overflow \
@@ -226,6 +227,7 @@
 	stdout.c \
 	submit-reuse.c \
 	teardowns.c \
+	thread-exit.c \
 	timeout-new.c \
 	timeout-overflow.c \
 	timeout.c \
@@ -248,6 +250,7 @@
 wakeup-hang: XCFLAGS = -lpthread
 pipe-eof: XCFLAGS = -lpthread
 timeout-new: XCFLAGS = -lpthread
+thread-exit: XCFLAGS = -lpthread
 
 install: $(test_targets) runtests.sh runtests-loop.sh
 	$(INSTALL) -D -d -m 755 $(datadir)/liburing-test/
diff --git a/test/thread-exit.c b/test/thread-exit.c
new file mode 100644
index 0000000..722edbc
--- /dev/null
+++ b/test/thread-exit.c
@@ -0,0 +1,156 @@
+/* SPDX-License-Identifier: MIT */
+/*
+ * Description: test that thread pool issued requests don't cancel on thread
+ *		exit, but do get canceled once the parent exits. Do both
+ *		writes that finish and a poll request that sticks around.
+ *
+ */
+#include <errno.h>
+#include <stdio.h>
+#include <unistd.h>
+#include <stdlib.h>
+#include <string.h>
+#include <fcntl.h>
+#include <sys/poll.h>
+#include <pthread.h>
+
+#include "liburing.h"
+
+#define NR_IOS	8
+#define WSIZE	512
+
+static int create_file(const char *file, size_t size)
+{
+	ssize_t ret;
+	char *buf;
+	int fd;
+
+	buf = malloc(size);
+	memset(buf, 0xaa, size);
+
+	fd = open(file, O_WRONLY | O_CREAT, 0644);
+	if (fd < 0) {
+		perror("open file");
+		return 1;
+	}
+	ret = write(fd, buf, size);
+	close(fd);
+	free(buf);
+	return ret != size;
+}
+
+struct d {
+	int fd;
+	struct io_uring *ring;
+	unsigned long off;
+	int pipe_fd;
+	int err;
+};
+
+static void *do_io(void *data)
+{
+	struct d *d = data;
+	struct io_uring_sqe *sqe;
+	char *buffer;
+	int ret;
+
+	buffer = malloc(WSIZE);
+	memset(buffer, 0x5a, WSIZE);
+	sqe = io_uring_get_sqe(d->ring);
+	if (!sqe) {
+		d->err++;
+		return NULL;
+	}
+	io_uring_prep_write(sqe, d->fd, buffer, WSIZE, d->off);
+	sqe->user_data = d->off;
+
+	sqe = io_uring_get_sqe(d->ring);
+	if (!sqe) {
+		d->err++;
+		return NULL;
+	}
+	io_uring_prep_poll_add(sqe, d->pipe_fd, POLLIN);
+
+	ret = io_uring_submit(d->ring);
+	if (ret != 2)
+		d->err++;
+
+	free(buffer);
+	return NULL;
+}
+
+int main(int argc, char *argv[])
+{
+	struct io_uring ring;
+	const char *fname;
+	pthread_t thread;
+	int ret, do_unlink, i, fd;
+	struct d d;
+	int fds[2];
+
+	if (pipe(fds) < 0) {
+		perror("pipe");
+		return 1;
+	}
+
+	ret = io_uring_queue_init(32, &ring, 0);
+	if (ret) {
+		fprintf(stderr, "ring setup failed\n");
+		return 1;
+	}
+
+	if (argc > 1) {
+		fname = argv[1];
+		do_unlink = 0;
+	} else {
+		fname = ".thread.exit";
+		do_unlink = 1;
+	}
+
+	if (do_unlink && create_file(fname, 4096)) {
+		fprintf(stderr, "file create failed\n");
+		return 1;
+	}
+
+	fd = open(fname, O_WRONLY);
+	if (fd < 0) {
+		perror("open");
+		return 1;
+	}
+
+	d.fd = fd;
+	d.ring = &ring;
+	d.off = 0;
+	d.pipe_fd = fds[0];
+	d.err = 0;
+	for (i = 0; i < NR_IOS; i++) {
+		memset(&thread, 0, sizeof(thread));
+		pthread_create(&thread, NULL, do_io, &d);
+		pthread_join(thread, NULL);
+		d.off += WSIZE;
+	}
+
+	for (i = 0; i < NR_IOS; i++) {
+		struct io_uring_cqe *cqe;
+
+		ret = io_uring_wait_cqe(&ring, &cqe);
+		if (ret) {
+			fprintf(stderr, "io_uring_wait_cqe=%d\n", ret);
+			goto err;
+		}
+		if (cqe->res != WSIZE) {
+			fprintf(stderr, "cqe->res=%d, Expected %d\n", cqe->res,
+								WSIZE);
+			goto err;
+		}
+		io_uring_cqe_seen(&ring, cqe);
+	}
+
+	if (do_unlink)
+		unlink(fname);
+	return d.err;
+err:
+	if (do_unlink)
+		unlink(fname);
+	return 1;
+}