aboutsummaryrefslogtreecommitdiff
path: root/usr.bin/mail/tests/mailx_signal_test.c
diff options
context:
space:
mode:
Diffstat (limited to 'usr.bin/mail/tests/mailx_signal_test.c')
-rw-r--r--usr.bin/mail/tests/mailx_signal_test.c196
1 files changed, 196 insertions, 0 deletions
diff --git a/usr.bin/mail/tests/mailx_signal_test.c b/usr.bin/mail/tests/mailx_signal_test.c
new file mode 100644
index 000000000000..be03b54fd51e
--- /dev/null
+++ b/usr.bin/mail/tests/mailx_signal_test.c
@@ -0,0 +1,196 @@
+/*-
+ * Copyright (c) 2025 Klara, Inc.
+ *
+ * SPDX-License-Identifier: BSD-2-Clause
+ */
+
+#include <sys/poll.h>
+#include <sys/wait.h>
+
+#include <fcntl.h>
+#include <signal.h>
+#include <stdbool.h>
+#include <stdlib.h>
+#include <time.h>
+#include <unistd.h>
+
+#include <atf-c.h>
+
+#define MAILX "mailx"
+#define BODY "hello\n"
+#define BODYLEN (sizeof(BODY) - 1)
+
+/*
+ * When interactive, mailx(1) should print a message on receipt of SIGINT,
+ * then exit cleanly on receipt of a second.
+ *
+ * When not interactive, mailx(1) should terminate on receipt of SIGINT.
+ *
+ * In either case, mailx(1) should terminate on receipt of SIGHUP.
+ */
+static void
+mailx_signal_test(int signo, bool interactive)
+{
+ char obuf[1024] = "";
+ char ebuf[1024] = "";
+ struct pollfd fds[2];
+ int ipd[2], opd[2], epd[2], spd[2];
+ time_t start, now;
+ size_t olen = 0, elen = 0;
+ ssize_t rlen;
+ pid_t pid;
+ int kc, status;
+
+ /* input, output, error, sync pipes */
+ if (pipe(ipd) != 0 || pipe(opd) != 0 || pipe(epd) != 0 ||
+ pipe(spd) != 0 || fcntl(spd[1], F_SETFD, FD_CLOEXEC) != 0)
+ atf_tc_fail("failed to pipe");
+ /* fork child */
+ if ((pid = fork()) < 0)
+ atf_tc_fail("failed to fork");
+ if (pid == 0) {
+ /* child */
+ sigset_t set;
+
+ /*
+ * Ensure mailx(1) will handle SIGINT; i.e., that it's not
+ * ignored or blocked.
+ */
+ (void)signal(signo, SIG_DFL);
+ sigemptyset(&set);
+ sigaddset(&set, signo);
+ ATF_REQUIRE_INTEQ(0, sigprocmask(SIG_UNBLOCK, &set, NULL));
+
+ dup2(ipd[0], STDIN_FILENO);
+ close(ipd[0]);
+ close(ipd[1]);
+ dup2(opd[1], STDOUT_FILENO);
+ close(opd[0]);
+ close(opd[1]);
+ dup2(epd[1], STDERR_FILENO);
+ close(epd[0]);
+ close(epd[1]);
+ close(spd[0]);
+ /* force dead.letter to go to cwd */
+ setenv("HOME", ".", 1);
+ /* exec mailx */
+ execlp(MAILX,
+ MAILX,
+ interactive ? "-Is" : "-s",
+ "test",
+ "test@example.com",
+ NULL);
+ _exit(2);
+ }
+ /* parent */
+ close(ipd[0]);
+ close(opd[1]);
+ close(epd[1]);
+ close(spd[1]);
+ /* block until child execs or exits */
+ (void)read(spd[0], &spd[1], sizeof(spd[1]));
+ /* send one line of input */
+ ATF_REQUIRE_INTEQ(BODYLEN, write(ipd[1], BODY, BODYLEN));
+ /* give it a chance to process */
+ poll(NULL, 0, 2000);
+ /* send first signal */
+ ATF_CHECK_INTEQ(0, kill(pid, signo));
+ kc = 1;
+ /* receive output until child terminates */
+ fds[0].fd = opd[0];
+ fds[0].events = POLLIN;
+ fds[1].fd = epd[0];
+ fds[1].events = POLLIN;
+ time(&start);
+ for (;;) {
+ ATF_REQUIRE(poll(fds, 2, 1000) >= 0);
+ if (fds[0].revents == POLLIN && olen < sizeof(obuf)) {
+ rlen = read(opd[0], obuf + olen, sizeof(obuf) - olen - 1);
+ ATF_REQUIRE(rlen >= 0);
+ olen += rlen;
+ }
+ if (fds[1].revents == POLLIN && elen < sizeof(ebuf)) {
+ rlen = read(epd[0], ebuf + elen, sizeof(ebuf) - elen - 1);
+ ATF_REQUIRE(rlen >= 0);
+ elen += rlen;
+ }
+ time(&now);
+ if (now - start > 1 && elen > 0 && kc == 1) {
+ ATF_CHECK_INTEQ(0, kill(pid, signo));
+ kc++;
+ }
+ if (now - start > 15 && kc > 0) {
+ (void)kill(pid, SIGKILL);
+ kc = -1;
+ }
+ if (waitpid(pid, &status, WNOHANG) == pid)
+ break;
+ }
+ close(ipd[1]);
+ close(opd[0]);
+ close(epd[0]);
+ close(spd[0]);
+ /*
+ * In interactive mode, SIGINT results in a prompt, and a second
+ * SIGINT results in exit(1). In all other cases, we should see
+ * the signal terminate the process.
+ */
+ if (interactive && signo == SIGINT) {
+ ATF_CHECK(WIFEXITED(status));
+ if (WIFEXITED(status))
+ ATF_CHECK_INTEQ(1, WEXITSTATUS(status));
+ ATF_CHECK_INTEQ(2, kc);
+ ATF_CHECK_STREQ("", obuf);
+ ATF_CHECK_MATCH("Interrupt -- one more to kill letter", ebuf);
+ } else {
+ ATF_CHECK(WIFSIGNALED(status));
+ if (WIFSIGNALED(status))
+ ATF_CHECK_INTEQ(signo, WTERMSIG(status));
+ ATF_CHECK_INTEQ(1, kc);
+ ATF_CHECK_STREQ("", obuf);
+ ATF_CHECK_STREQ("", ebuf);
+ }
+ /*
+ * In interactive mode, and only in interactive mode, mailx should
+ * save whatever was typed before termination in ~/dead.letter.
+ * This is why we set HOME to "." in the child.
+ */
+ if (interactive) {
+ atf_utils_compare_file("dead.letter", BODY);
+ } else {
+ ATF_CHECK_INTEQ(-1, access("dead.letter", F_OK));
+ }
+}
+
+ATF_TC_WITHOUT_HEAD(mailx_sighup_interactive);
+ATF_TC_BODY(mailx_sighup_interactive, tc)
+{
+ mailx_signal_test(SIGHUP, true);
+}
+
+ATF_TC_WITHOUT_HEAD(mailx_sighup_noninteractive);
+ATF_TC_BODY(mailx_sighup_noninteractive, tc)
+{
+ mailx_signal_test(SIGHUP, false);
+}
+
+ATF_TC_WITHOUT_HEAD(mailx_sigint_interactive);
+ATF_TC_BODY(mailx_sigint_interactive, tc)
+{
+ mailx_signal_test(SIGINT, true);
+}
+
+ATF_TC_WITHOUT_HEAD(mailx_sigint_noninteractive);
+ATF_TC_BODY(mailx_sigint_noninteractive, tc)
+{
+ mailx_signal_test(SIGINT, false);
+}
+
+ATF_TP_ADD_TCS(tp)
+{
+ ATF_TP_ADD_TC(tp, mailx_sighup_interactive);
+ ATF_TP_ADD_TC(tp, mailx_sighup_noninteractive);
+ ATF_TP_ADD_TC(tp, mailx_sigint_interactive);
+ ATF_TP_ADD_TC(tp, mailx_sigint_noninteractive);
+ return (atf_no_error());
+}