summaryrefslogtreecommitdiff
path: root/corebinutils
diff options
context:
space:
mode:
authorMehmet Samet Duman <yongdohyun@projecttick.org>2026-04-02 18:29:27 +0300
committerMehmet Samet Duman <yongdohyun@projecttick.org>2026-04-02 18:29:27 +0300
commit3c4ab8392fcf79e40b8bc02a39b9c6d03492fcb7 (patch)
treeea0b462d625dc4c5d104449d721ce29879dbedfc /corebinutils
parent85f60af1bb558bc7248fb64528c5bba92e504adf (diff)
parent2283ae2924693a1f4370a7dc423ace87b52600c2 (diff)
downloadProject-Tick-3c4ab8392fcf79e40b8bc02a39b9c6d03492fcb7.tar.gz
Project-Tick-3c4ab8392fcf79e40b8bc02a39b9c6d03492fcb7.zip
Add 'corebinutils/sleep/' from commit '2283ae2924693a1f4370a7dc423ace87b52600c2'
git-subtree-dir: corebinutils/sleep git-subtree-mainline: 85f60af1bb558bc7248fb64528c5bba92e504adf git-subtree-split: 2283ae2924693a1f4370a7dc423ace87b52600c2
Diffstat (limited to 'corebinutils')
-rw-r--r--corebinutils/sleep/.gitignore25
-rw-r--r--corebinutils/sleep/GNUmakefile37
-rw-r--r--corebinutils/sleep/LICENSE32
-rw-r--r--corebinutils/sleep/README.md43
-rw-r--r--corebinutils/sleep/sleep.1133
-rw-r--r--corebinutils/sleep/sleep.c269
-rw-r--r--corebinutils/sleep/tests/test.sh378
7 files changed, 917 insertions, 0 deletions
diff --git a/corebinutils/sleep/.gitignore b/corebinutils/sleep/.gitignore
new file mode 100644
index 0000000000..a74d30b48c
--- /dev/null
+++ b/corebinutils/sleep/.gitignore
@@ -0,0 +1,25 @@
+*.a
+*.core
+*.lo
+*.nossppico
+*.o
+*.orig
+*.pico
+*.pieo
+*.po
+*.rej
+*.so
+*.so.[0-9]*
+*.sw[nop]
+*~
+.*DS_Store
+.cache
+.clangd
+.ccls-cache
+.depend*
+compile_commands.json
+compile_commands.events.json
+tags
+build/
+out/
+.linux-obj/
diff --git a/corebinutils/sleep/GNUmakefile b/corebinutils/sleep/GNUmakefile
new file mode 100644
index 0000000000..bb7338f0e2
--- /dev/null
+++ b/corebinutils/sleep/GNUmakefile
@@ -0,0 +1,37 @@
+.DEFAULT_GOAL := all
+
+CC ?= cc
+CPPFLAGS ?=
+CPPFLAGS += -D_POSIX_C_SOURCE=200809L -D_XOPEN_SOURCE=700
+CFLAGS ?= -O2
+CFLAGS += -std=c17 -g -Wall -Wextra -Werror
+LDFLAGS ?=
+LDLIBS ?=
+LDLIBS += -lm
+
+OBJDIR := $(CURDIR)/build
+OUTDIR := $(CURDIR)/out
+TARGET := $(OUTDIR)/sleep
+OBJS := $(OBJDIR)/sleep.o
+
+.PHONY: all clean dirs status test
+
+all: $(TARGET)
+
+dirs:
+ @mkdir -p "$(OBJDIR)" "$(OUTDIR)"
+
+$(TARGET): $(OBJS) | dirs
+ $(CC) $(LDFLAGS) -o "$@" $(OBJS) $(LDLIBS)
+
+$(OBJDIR)/sleep.o: $(CURDIR)/sleep.c | dirs
+ $(CC) $(CPPFLAGS) $(CFLAGS) -c "$(CURDIR)/sleep.c" -o "$@"
+
+test: $(TARGET)
+ CC="$(CC)" SLEEP_BIN="$(TARGET)" sh "$(CURDIR)/tests/test.sh"
+
+status:
+ @printf '%s\n' "$(TARGET)"
+
+clean:
+ @rm -rf "$(OBJDIR)" "$(OUTDIR)"
diff --git a/corebinutils/sleep/LICENSE b/corebinutils/sleep/LICENSE
new file mode 100644
index 0000000000..8d4e27aede
--- /dev/null
+++ b/corebinutils/sleep/LICENSE
@@ -0,0 +1,32 @@
+Copyright (c) 1988, 1993, 1994
+ The Regents of the University of California. All rights reserved.
+
+Copyright (c) 2026
+ Project Tick. All rights reserved.
+
+This code is derived from software contributed to Berkeley by
+the Institute of Electrical and Electronics Engineers, Inc.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions
+are met:
+1. Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+2. Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+3. Neither the name of the University nor the names of its contributors
+ may be used to endorse or promote products derived from this software
+ without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND
+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE
+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+SUCH DAMAGE.
diff --git a/corebinutils/sleep/README.md b/corebinutils/sleep/README.md
new file mode 100644
index 0000000000..f3ba39beb5
--- /dev/null
+++ b/corebinutils/sleep/README.md
@@ -0,0 +1,43 @@
+# sleep
+
+Standalone musl-libc-based Linux port of FreeBSD `sleep` for Project Tick BSD/Linux Distribution.
+
+## Build
+
+```sh
+gmake -f GNUmakefile
+gmake -f GNUmakefile CC=musl-gcc
+```
+
+## Test
+
+```sh
+gmake -f GNUmakefile test
+gmake -f GNUmakefile test CC=musl-gcc
+```
+
+## Port Strategy
+
+- Port structure follows the standalone sibling ports such as `bin/hostname` and `bin/date`: local `GNUmakefile`, short technical `README.md`, and shell tests under `tests/`.
+- The FreeBSD source was converted directly to a Linux-native implementation instead of preserving BSD build glue, Capsicum entry, or BSD libc diagnostics.
+- Interval parsing is strict and manpage-driven: operands are parsed with `strtold(3)`, unit handling is explicit, invalid trailing data is rejected, and the final summed interval is rounded up to the next nanosecond so Linux does not undersleep the requested minimum duration.
+
+## Linux API Mapping
+
+- FreeBSD `nanosleep(2)` usage remains `nanosleep(2)` on Linux; interrupted sleeps are resumed with the kernel-provided remaining interval.
+- FreeBSD `SIGINFO` progress reporting maps to Linux `SIGUSR1`, because Linux does not provide `SIGINFO`. The report still prints the estimated remaining time for the current sleep request.
+- FreeBSD `<err.h>` diagnostics are replaced with local `fprintf(3)`-based error handling so the port builds cleanly on musl without BSD libc helpers.
+- FreeBSD Capsicum setup (`caph_limit_stdio()` / `caph_enter()`) is removed. Linux has no equivalent process-sandbox semantic that belongs in `sleep(1)`, and adding a fake compatibility path would be incorrect.
+
+## Supported Semantics On Linux
+
+- `sleep number[unit] [...]` with `s`, `m`, `h`, or `d` suffixes, fractional operands, and multiple operands added together
+- Negative operands when the final sum remains positive, matching the manpage statement that zero or negative final sums exit immediately
+- `--` as an operand separator for callers that want explicit disambiguation before negative numeric operands
+- Progress reporting on Linux via `SIGUSR1`, written to stdout as the manpage requires for the `SIGINFO` path
+
+## Intentionally Unsupported Semantics
+
+- Non-finite `strtold(3)` forms such as `inf`, `infinity`, and `nan` are rejected with an explicit error. Linux has no finite `nanosleep(2)` representation for those values, and the port does not guess at an emulation.
+- Requests that exceed Linux `time_t` / `struct timespec` range fail with an explicit error instead of truncating or wrapping.
+- No GNU `sleep` extensions such as `--help` or `--version` are implemented; `sleep.1` defines no such interface.
diff --git a/corebinutils/sleep/sleep.1 b/corebinutils/sleep/sleep.1
new file mode 100644
index 0000000000..c9069a0157
--- /dev/null
+++ b/corebinutils/sleep/sleep.1
@@ -0,0 +1,133 @@
+.\"-
+.\" Copyright (c) 1990, 1993, 1994
+.\" The Regents of the University of California. All rights reserved.
+.\"
+.\" This code is derived from software contributed to Berkeley by
+.\" the Institute of Electrical and Electronics Engineers, Inc.
+.\"
+.\" Redistribution and use in source and binary forms, with or without
+.\" modification, are permitted provided that the following conditions
+.\" are met:
+.\" 1. Redistributions of source code must retain the above copyright
+.\" notice, this list of conditions and the following disclaimer.
+.\" 2. Redistributions in binary form must reproduce the above copyright
+.\" notice, this list of conditions and the following disclaimer in the
+.\" documentation and/or other materials provided with the distribution.
+.\" 3. Neither the name of the University nor the names of its contributors
+.\" may be used to endorse or promote products derived from this software
+.\" without specific prior written permission.
+.\"
+.\" THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND
+.\" ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+.\" IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+.\" ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE
+.\" FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+.\" DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+.\" OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+.\" HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+.\" LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+.\" OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+.\" SUCH DAMAGE.
+.\"
+.Dd March 22, 2024
+.Dt SLEEP 1
+.Os
+.Sh NAME
+.Nm sleep
+.Nd suspend execution for an interval of time
+.Sh SYNOPSIS
+.Nm
+.Ar number Ns Op Ar unit
+.Op ...
+.Sh DESCRIPTION
+The
+.Nm
+command suspends execution for a minimum of
+.Ar number
+seconds (the default, or unit
+.Li s ) ,
+minutes (unit
+.Li m ) ,
+hours (unit
+.Li h ) ,
+or days (unit
+.Li d ) .
+Intervals can be written in any form allowed by
+.Xr strtod 3 .
+If multiple intervals are given, they are added together.
+If the final sum is zero or negative,
+.Nm
+exits immediately.
+.Pp
+If the
+.Nm
+command receives a signal, it takes the standard action.
+When the
+.Dv SIGINFO
+signal is received, the estimate of the amount of seconds left to
+sleep is printed on the standard output.
+.Sh IMPLEMENTATION NOTES
+The
+.Dv SIGALRM
+signal is not handled specially by this implementation.
+.Sh EXIT STATUS
+.Ex -std
+.Sh EXAMPLES
+To run a command after half an hour:
+.Pp
+.Dl (sleep 0.5h; sh command_file >out 2>err)&
+.Pp
+This incantation would wait half an hour before
+running the script
+.Pa command_file .
+See the
+.Xr at 1
+utility for another way to do this.
+.Pp
+To reiteratively run a command:
+.Pp
+.Bd -literal -offset indent -compact
+while :; do
+ if ! [ -r zzz.rawdata ] ; then
+ sleep 5m
+ else
+ for i in *.rawdata ; do
+ sleep 70
+ awk -f collapse_data "$i"
+ done >results
+ break
+ fi
+done
+.Ed
+.Pp
+The scenario for a script such as this might be: a program currently
+running is taking longer than expected to process a series of
+files, and it would be nice to have
+another program start processing the files created by the first
+program as soon as it is finished (when
+.Pa zzz.rawdata
+is created).
+The script checks every five minutes for the file
+.Pa zzz.rawdata ,
+when the file is found, then another portion processing
+is done courteously by sleeping for 70 seconds in between each
+.Xr awk 1
+job.
+.Sh SEE ALSO
+.Xr nanosleep 2 ,
+.Xr sleep 3
+.Sh STANDARDS
+The
+.Nm
+command is expected to be
+.St -p1003.2
+compatible.
+.Pp
+Support for non-integer intervals, units other than seconds, and
+multiple intervals which are added together are non-portable
+extensions first introduced in GNU sh-utils 2.0a (released in 2002).
+.Sh HISTORY
+A
+.Nm
+command appeared in
+.At v4 .
diff --git a/corebinutils/sleep/sleep.c b/corebinutils/sleep/sleep.c
new file mode 100644
index 0000000000..d56845b4fe
--- /dev/null
+++ b/corebinutils/sleep/sleep.c
@@ -0,0 +1,269 @@
+/*-
+ * SPDX-License-Identifier: BSD-3-Clause
+ *
+ * Copyright (c) 1988, 1993, 1994
+ * The Regents of the University of California. All rights reserved.
+ *
+ * Copyright (c) 2026
+ * Project Tick. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ * notice, this list of conditions and the following disclaimer in the
+ * documentation and/or other materials provided with the distribution.
+ * 3. Neither the name of the University nor the names of its contributors
+ * may be used to endorse or promote products derived from this software
+ * without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+ * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+ * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+ * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+ * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+ * SUCH DAMAGE.
+ */
+
+#include <stdarg.h>
+#include <stdbool.h>
+#include <errno.h>
+#include <limits.h>
+#include <math.h>
+#include <signal.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <time.h>
+#include <unistd.h>
+
+#ifndef SIGINFO
+#define SLEEP_INFO_SIGNAL SIGUSR1
+#else
+#define SLEEP_INFO_SIGNAL SIGINFO
+#endif
+
+#define NSECS_PER_SEC 1000000000L
+
+static volatile sig_atomic_t report_requested;
+
+static void usage(void) __attribute__((noreturn));
+static void die(const char *fmt, ...) __attribute__((noreturn, format(printf, 1, 2)));
+static void die_errno(const char *context) __attribute__((noreturn));
+static void install_info_handler(void);
+static long double seconds_from_timespec(const struct timespec *ts);
+static long double maximum_time_t_seconds(void);
+static void report_remaining(const struct timespec *remaining, long double original);
+static long double scale_interval(long double value, int multiplier, const char *arg);
+static long double parse_interval(const char *arg);
+static struct timespec seconds_to_timespec(long double seconds);
+
+static void
+report_request(int signo)
+{
+ (void)signo;
+ report_requested = 1;
+}
+
+static void
+usage(void)
+{
+ fprintf(stderr, "usage: sleep number[unit] [...]\n"
+ "Unit can be 's' (seconds, the default), "
+ "m (minutes), h (hours), or d (days).\n");
+ exit(1);
+}
+
+static void
+die(const char *fmt, ...)
+{
+ va_list ap;
+
+ fprintf(stderr, "sleep: ");
+ va_start(ap, fmt);
+ vfprintf(stderr, fmt, ap);
+ va_end(ap);
+ fputc('\n', stderr);
+ exit(1);
+}
+
+static void
+die_errno(const char *context)
+{
+ die("%s: %s", context, strerror(errno));
+}
+
+static void
+install_info_handler(void)
+{
+ struct sigaction sa;
+
+ memset(&sa, 0, sizeof(sa));
+ sa.sa_handler = report_request;
+ sigemptyset(&sa.sa_mask);
+ if (sigaction(SLEEP_INFO_SIGNAL, &sa, NULL) != 0)
+ die_errno("sigaction");
+}
+
+static long double
+seconds_from_timespec(const struct timespec *ts)
+{
+ return ((long double)ts->tv_sec +
+ (long double)ts->tv_nsec / (long double)NSECS_PER_SEC);
+}
+
+static long double
+maximum_time_t_seconds(void)
+{
+ int bits;
+
+ bits = (int)(sizeof(time_t) * CHAR_BIT);
+ if ((time_t)-1 < (time_t)0)
+ return (ldexpl(1.0L, bits - 1) - 1.0L);
+ return (ldexpl(1.0L, bits) - 1.0L);
+}
+
+static void
+report_remaining(const struct timespec *remaining, long double original)
+{
+ if (printf("about %.9Lf second(s) left out of the original %.9Lf\n",
+ seconds_from_timespec(remaining), original) < 0)
+ die_errno("stdout");
+ if (fflush(stdout) != 0)
+ die_errno("stdout");
+}
+
+static long double
+scale_interval(long double value, int multiplier, const char *arg)
+{
+ long double result;
+
+ result = value * (long double)multiplier;
+ if (!isfinite(result))
+ die("time interval out of range: %s", arg);
+ return (result);
+}
+
+static long double
+parse_interval(const char *arg)
+{
+ long double value;
+ char *end;
+
+ errno = 0;
+ value = strtold(arg, &end);
+ if (end == arg)
+ die("invalid time interval: %s", arg);
+ if (errno == ERANGE)
+ die("time interval out of range: %s", arg);
+ if (!isfinite(value))
+ die("non-finite time interval is not supported on Linux: %s", arg);
+ if (*end == '\0')
+ return (value);
+ if (end[1] != '\0')
+ die("invalid time interval: %s", arg);
+
+ switch (*end) {
+ case 's':
+ return (value);
+ case 'm':
+ return (scale_interval(value, 60, arg));
+ case 'h':
+ return (scale_interval(value, 60 * 60, arg));
+ case 'd':
+ return (scale_interval(value, 24 * 60 * 60, arg));
+ default:
+ die("unsupported time unit in interval '%s': '%c' (supported: s, m, h, d)",
+ arg, *end);
+ }
+}
+
+static struct timespec
+seconds_to_timespec(long double seconds)
+{
+ struct timespec ts;
+ long double whole;
+ long double fractional;
+ long double nsec;
+ long double max_seconds;
+
+ if (seconds <= 0.0L) {
+ ts.tv_sec = 0;
+ ts.tv_nsec = 0;
+ return (ts);
+ }
+
+ max_seconds = maximum_time_t_seconds();
+ if (seconds > max_seconds + 1.0L)
+ die("requested interval is too large for Linux sleep APIs");
+
+ fractional = modfl(seconds, &whole);
+ if (whole > max_seconds)
+ die("requested interval is too large for Linux sleep APIs");
+
+ ts.tv_sec = (time_t)whole;
+ nsec = ceill(fractional * (long double)NSECS_PER_SEC);
+ if (nsec >= (long double)NSECS_PER_SEC) {
+ if ((long double)ts.tv_sec >= max_seconds)
+ die("requested interval is too large for Linux sleep APIs");
+ ts.tv_sec += 1;
+ ts.tv_nsec = 0;
+ return (ts);
+ }
+ if (nsec <= 0.0L)
+ nsec = 1.0L;
+ ts.tv_nsec = (long)nsec;
+ return (ts);
+}
+
+int
+main(int argc, char *argv[])
+{
+ struct timespec time_to_sleep;
+ long double original;
+ long double seconds;
+ int i;
+
+ if (argc > 1 && strcmp(argv[1], "--") == 0) {
+ argv++;
+ argc--;
+ }
+ if (argc < 2)
+ usage();
+
+ seconds = 0.0L;
+ for (i = 1; i < argc; i++) {
+ long double interval;
+ long double total;
+
+ interval = parse_interval(argv[i]);
+ total = seconds + interval;
+ if (!isfinite(total))
+ die("time interval out of range after adding: %s", argv[i]);
+ seconds = total;
+ }
+ if (seconds <= 0.0L)
+ exit(0);
+
+ time_to_sleep = seconds_to_timespec(seconds);
+ original = seconds_from_timespec(&time_to_sleep);
+ install_info_handler();
+
+ while (nanosleep(&time_to_sleep, &time_to_sleep) != 0) {
+ if (errno != EINTR)
+ die_errno("nanosleep");
+ if (report_requested) {
+ report_remaining(&time_to_sleep, original);
+ report_requested = 0;
+ }
+ }
+
+ exit(0);
+}
diff --git a/corebinutils/sleep/tests/test.sh b/corebinutils/sleep/tests/test.sh
new file mode 100644
index 0000000000..5aab4b9499
--- /dev/null
+++ b/corebinutils/sleep/tests/test.sh
@@ -0,0 +1,378 @@
+#!/bin/sh
+set -eu
+
+ROOT=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)
+SLEEP_BIN=${SLEEP_BIN:-"$ROOT/out/sleep"}
+CC=${CC:-cc}
+TMPDIR=${TMPDIR:-/tmp}
+WORKDIR=$(mktemp -d "$TMPDIR/sleep-test.XXXXXX")
+HELPER_C="$WORKDIR/run_sleep_case.c"
+HELPER_BIN="$WORKDIR/run_sleep_case"
+STDOUT_FILE="$WORKDIR/stdout"
+STDERR_FILE="$WORKDIR/stderr"
+RESULT_FILE="$WORKDIR/result"
+trap 'rm -rf "$WORKDIR"' EXIT INT TERM
+
+export LC_ALL=C
+
+fail() {
+ printf '%s\n' "FAIL: $1" >&2
+ exit 1
+}
+
+assert_eq() {
+ name=$1
+ expected=$2
+ actual=$3
+ if [ "$expected" != "$actual" ]; then
+ printf '%s\n' "FAIL: $name" >&2
+ printf '%s\n' "--- expected ---" >&2
+ printf '%s' "$expected" >&2
+ printf '\n%s\n' "--- actual ---" >&2
+ printf '%s' "$actual" >&2
+ printf '\n' >&2
+ exit 1
+ fi
+}
+
+assert_empty() {
+ name=$1
+ text=$2
+ if [ -n "$text" ]; then
+ printf '%s\n' "FAIL: $name" >&2
+ printf '%s\n' "--- expected empty ---" >&2
+ printf '%s\n' "--- actual ---" >&2
+ printf '%s' "$text" >&2
+ printf '\n' >&2
+ exit 1
+ fi
+}
+
+assert_status() {
+ name=$1
+ expected=$2
+ actual=$3
+ if [ "$expected" -ne "$actual" ]; then
+ printf '%s\n' "FAIL: $name" >&2
+ printf '%s\n' "expected status: $expected" >&2
+ printf '%s\n' "actual status: $actual" >&2
+ exit 1
+ fi
+}
+
+assert_ge() {
+ name=$1
+ actual=$2
+ minimum=$3
+ if [ "$actual" -lt "$minimum" ]; then
+ printf '%s\n' "FAIL: $name" >&2
+ printf '%s\n' "expected >= $minimum" >&2
+ printf '%s\n' "actual: $actual" >&2
+ exit 1
+ fi
+}
+
+assert_match() {
+ name=$1
+ pattern=$2
+ text=$3
+ printf '%s\n' "$text" | grep -Eq "$pattern" || fail "$name"
+}
+
+build_helper() {
+ cat >"$HELPER_C" <<'EOF'
+#define _POSIX_C_SOURCE 200809L
+#define _XOPEN_SOURCE 700
+
+#include <errno.h>
+#include <fcntl.h>
+#include <signal.h>
+#include <stdbool.h>
+#include <stdint.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/types.h>
+#include <sys/wait.h>
+#include <time.h>
+#include <unistd.h>
+
+#define NSECS_PER_SEC 1000000000LL
+
+static int64_t
+ns_since_epoch(const struct timespec *ts)
+{
+ return ((int64_t)ts->tv_sec * NSECS_PER_SEC + (int64_t)ts->tv_nsec);
+}
+
+static void
+sleep_ms(int milliseconds)
+{
+ struct timespec request;
+
+ request.tv_sec = milliseconds / 1000;
+ request.tv_nsec = (long)(milliseconds % 1000) * 1000000L;
+ while (nanosleep(&request, &request) != 0) {
+ if (errno != EINTR) {
+ perror("nanosleep");
+ exit(2);
+ }
+ }
+}
+
+int
+main(int argc, char *argv[])
+{
+ struct timespec started;
+ struct timespec now;
+ pid_t child;
+ int stdout_fd;
+ int stderr_fd;
+ int timeout_ms;
+ int signal_delay_ms;
+ int status;
+ int exit_status;
+ int64_t elapsed_ns;
+ bool signal_sent;
+ char *end;
+
+ if (argc < 6) {
+ fprintf(stderr, "usage: run_sleep_case timeout_ms signal_delay_ms stdout stderr bin [args ...]\n");
+ return (2);
+ }
+
+ errno = 0;
+ timeout_ms = (int)strtol(argv[1], &end, 10);
+ if (errno != 0 || *end != '\0' || timeout_ms <= 0) {
+ fprintf(stderr, "invalid timeout: %s\n", argv[1]);
+ return (2);
+ }
+
+ errno = 0;
+ signal_delay_ms = (int)strtol(argv[2], &end, 10);
+ if (errno != 0 || *end != '\0' || signal_delay_ms < -1) {
+ fprintf(stderr, "invalid signal delay: %s\n", argv[2]);
+ return (2);
+ }
+
+ stdout_fd = open(argv[3], O_WRONLY | O_CREAT | O_TRUNC, 0600);
+ if (stdout_fd < 0) {
+ perror("open stdout");
+ return (2);
+ }
+
+ stderr_fd = open(argv[4], O_WRONLY | O_CREAT | O_TRUNC, 0600);
+ if (stderr_fd < 0) {
+ perror("open stderr");
+ return (2);
+ }
+
+ if (clock_gettime(CLOCK_MONOTONIC, &started) != 0) {
+ perror("clock_gettime");
+ return (2);
+ }
+
+ child = fork();
+ if (child < 0) {
+ perror("fork");
+ return (2);
+ }
+ if (child == 0) {
+ if (dup2(stdout_fd, STDOUT_FILENO) < 0 || dup2(stderr_fd, STDERR_FILENO) < 0) {
+ perror("dup2");
+ _exit(127);
+ }
+ close(stdout_fd);
+ close(stderr_fd);
+ execv(argv[5], &argv[5]);
+ perror("execv");
+ _exit(127);
+ }
+
+ close(stdout_fd);
+ close(stderr_fd);
+
+ signal_sent = false;
+ for (;;) {
+ pid_t waited;
+ int64_t elapsed_ms;
+
+ waited = waitpid(child, &status, WNOHANG);
+ if (waited < 0) {
+ perror("waitpid");
+ return (2);
+ }
+ if (waited == child)
+ break;
+
+ if (clock_gettime(CLOCK_MONOTONIC, &now) != 0) {
+ perror("clock_gettime");
+ return (2);
+ }
+
+ elapsed_ms = (ns_since_epoch(&now) - ns_since_epoch(&started)) / 1000000LL;
+ if (!signal_sent && signal_delay_ms >= 0 && elapsed_ms >= signal_delay_ms) {
+ if (kill(child, SIGUSR1) != 0) {
+ perror("kill");
+ return (2);
+ }
+ signal_sent = true;
+ }
+ if (elapsed_ms >= timeout_ms) {
+ if (kill(child, SIGKILL) != 0 && errno != ESRCH) {
+ perror("kill timeout");
+ return (2);
+ }
+ if (waitpid(child, &status, 0) < 0) {
+ perror("waitpid timeout");
+ return (2);
+ }
+ break;
+ }
+
+ sleep_ms(5);
+ }
+
+ if (clock_gettime(CLOCK_MONOTONIC, &now) != 0) {
+ perror("clock_gettime");
+ return (2);
+ }
+ elapsed_ns = ns_since_epoch(&now) - ns_since_epoch(&started);
+
+ if (WIFEXITED(status))
+ exit_status = WEXITSTATUS(status);
+ else if (WIFSIGNALED(status))
+ exit_status = 128 + WTERMSIG(status);
+ else
+ exit_status = 255;
+
+ printf("status=%d\n", exit_status);
+ printf("elapsed_ns=%lld\n", (long long)elapsed_ns);
+ return (0);
+}
+EOF
+
+ "$CC" -O2 -std=c17 -Wall -Wextra -Werror "$HELPER_C" -o "$HELPER_BIN" \
+ || fail "failed to build helper with $CC"
+}
+
+run_case() {
+ timeout_ms=$1
+ signal_delay_ms=$2
+ shift 2
+
+ "$HELPER_BIN" "$timeout_ms" "$signal_delay_ms" "$STDOUT_FILE" "$STDERR_FILE" \
+ "$SLEEP_BIN" "$@" >"$RESULT_FILE" \
+ || fail "helper failed for: $*"
+
+ LAST_STDOUT=$(cat "$STDOUT_FILE")
+ LAST_STDERR=$(cat "$STDERR_FILE")
+ LAST_STATUS=$(sed -n 's/^status=//p' "$RESULT_FILE")
+ LAST_ELAPSED_NS=$(sed -n 's/^elapsed_ns=//p' "$RESULT_FILE")
+}
+
+[ -x "$SLEEP_BIN" ] || fail "missing binary: $SLEEP_BIN"
+build_helper
+
+usage_text=$(printf '%s\n%s' \
+ "usage: sleep number[unit] [...]" \
+ "Unit can be 's' (seconds, the default), m (minutes), h (hours), or d (days).")
+
+run_case 500 -1
+assert_status "no-arg usage status" 1 "$LAST_STATUS"
+assert_empty "no-arg usage stdout" "$LAST_STDOUT"
+assert_eq "no-arg usage stderr" "$usage_text" "$LAST_STDERR"
+
+run_case 500 -1 --
+assert_status "bare -- usage status" 1 "$LAST_STATUS"
+assert_empty "bare -- usage stdout" "$LAST_STDOUT"
+assert_eq "bare -- usage stderr" "$usage_text" "$LAST_STDERR"
+
+run_case 500 -1 0.03
+assert_status "seconds operand status" 0 "$LAST_STATUS"
+assert_empty "seconds operand stdout" "$LAST_STDOUT"
+assert_empty "seconds operand stderr" "$LAST_STDERR"
+assert_ge "seconds operand elapsed" "$LAST_ELAPSED_NS" 20000000
+
+run_case 500 -1 0.0005m
+assert_status "minutes operand status" 0 "$LAST_STATUS"
+assert_empty "minutes operand stdout" "$LAST_STDOUT"
+assert_empty "minutes operand stderr" "$LAST_STDERR"
+assert_ge "minutes operand elapsed" "$LAST_ELAPSED_NS" 20000000
+
+run_case 500 -1 0.00001h
+assert_status "hours operand status" 0 "$LAST_STATUS"
+assert_empty "hours operand stdout" "$LAST_STDOUT"
+assert_empty "hours operand stderr" "$LAST_STDERR"
+assert_ge "hours operand elapsed" "$LAST_ELAPSED_NS" 25000000
+
+run_case 500 -1 0.0000005d
+assert_status "days operand status" 0 "$LAST_STATUS"
+assert_empty "days operand stdout" "$LAST_STDOUT"
+assert_empty "days operand stderr" "$LAST_STDERR"
+assert_ge "days operand elapsed" "$LAST_ELAPSED_NS" 30000000
+
+run_case 500 -1 -0.02 0.07s
+assert_status "negative operand addition status" 0 "$LAST_STATUS"
+assert_empty "negative operand addition stdout" "$LAST_STDOUT"
+assert_empty "negative operand addition stderr" "$LAST_STDERR"
+assert_ge "negative operand addition elapsed" "$LAST_ELAPSED_NS" 30000000
+
+run_case 500 -1 -- -0.01 0.03s
+assert_status "double-dash negative status" 0 "$LAST_STATUS"
+assert_empty "double-dash negative stdout" "$LAST_STDOUT"
+assert_empty "double-dash negative stderr" "$LAST_STDERR"
+assert_ge "double-dash negative elapsed" "$LAST_ELAPSED_NS" 15000000
+
+run_case 200 -1 1 -1
+assert_status "zero-sum immediate status" 0 "$LAST_STATUS"
+assert_empty "zero-sum immediate stdout" "$LAST_STDOUT"
+assert_empty "zero-sum immediate stderr" "$LAST_STDERR"
+
+run_case 200 -1 bogus
+assert_status "bogus operand status" 1 "$LAST_STATUS"
+assert_empty "bogus operand stdout" "$LAST_STDOUT"
+assert_eq "bogus operand stderr" "sleep: invalid time interval: bogus" "$LAST_STDERR"
+
+run_case 200 -1 1ss
+assert_status "trailing garbage status" 1 "$LAST_STATUS"
+assert_empty "trailing garbage stdout" "$LAST_STDOUT"
+assert_eq "trailing garbage stderr" "sleep: invalid time interval: 1ss" "$LAST_STDERR"
+
+run_case 200 -1 1w
+assert_status "unsupported unit status" 1 "$LAST_STATUS"
+assert_empty "unsupported unit stdout" "$LAST_STDOUT"
+assert_eq "unsupported unit stderr" \
+ "sleep: unsupported time unit in interval '1w': 'w' (supported: s, m, h, d)" \
+ "$LAST_STDERR"
+
+run_case 200 -1 inf
+assert_status "inf status" 1 "$LAST_STATUS"
+assert_empty "inf stdout" "$LAST_STDOUT"
+assert_eq "inf stderr" \
+ "sleep: non-finite time interval is not supported on Linux: inf" \
+ "$LAST_STDERR"
+
+run_case 200 -1 nan
+assert_status "nan status" 1 "$LAST_STATUS"
+assert_empty "nan stdout" "$LAST_STDOUT"
+assert_eq "nan stderr" \
+ "sleep: non-finite time interval is not supported on Linux: nan" \
+ "$LAST_STDERR"
+
+run_case 200 -1 1e30d
+assert_status "too-large status" 1 "$LAST_STATUS"
+assert_empty "too-large stdout" "$LAST_STDOUT"
+assert_eq "too-large stderr" \
+ "sleep: requested interval is too large for Linux sleep APIs" \
+ "$LAST_STDERR"
+
+run_case 1500 50 0.4s
+assert_status "signal report status" 0 "$LAST_STATUS"
+assert_empty "signal report stderr" "$LAST_STDERR"
+assert_match "signal report stdout" \
+ '^about [0-9]+\.[0-9]{9} second\(s\) left out of the original 0\.400000000$' \
+ "$LAST_STDOUT"
+assert_ge "signal report elapsed" "$LAST_ELAPSED_NS" 300000000
+
+printf '%s\n' "PASS"