summaryrefslogtreecommitdiff
path: root/corebinutils/pkill/tests
diff options
context:
space:
mode:
authorMehmet Samet Duman <yongdohyun@projecttick.org>2026-04-02 18:27:44 +0300
committerMehmet Samet Duman <yongdohyun@projecttick.org>2026-04-02 18:27:44 +0300
commite99e87d16cdba1de0cfd84ce1a231ad3371e43db (patch)
treeb0fb3898bdcd7ece9ebd4769b6e3695f1fd022f4 /corebinutils/pkill/tests
parent4f84a681e5fc545999076703b5920d4c4cf29a78 (diff)
parentc07449c1ae05a076b9e554267513c794c86e3ba5 (diff)
downloadProject-Tick-e99e87d16cdba1de0cfd84ce1a231ad3371e43db.tar.gz
Project-Tick-e99e87d16cdba1de0cfd84ce1a231ad3371e43db.zip
Add 'corebinutils/pkill/' from commit 'c07449c1ae05a076b9e554267513c794c86e3ba5'
git-subtree-dir: corebinutils/pkill git-subtree-mainline: 4f84a681e5fc545999076703b5920d4c4cf29a78 git-subtree-split: c07449c1ae05a076b9e554267513c794c86e3ba5
Diffstat (limited to 'corebinutils/pkill/tests')
-rw-r--r--corebinutils/pkill/tests/spin_helper.c190
-rw-r--r--corebinutils/pkill/tests/test.sh730
2 files changed, 920 insertions, 0 deletions
diff --git a/corebinutils/pkill/tests/spin_helper.c b/corebinutils/pkill/tests/spin_helper.c
new file mode 100644
index 0000000000..bf3ec550ba
--- /dev/null
+++ b/corebinutils/pkill/tests/spin_helper.c
@@ -0,0 +1,190 @@
+/*-
+ * SPDX-License-Identifier: BSD-2-Clause
+ *
+ * Copyright (c) 2023 Klara, Inc.
+ * 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.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE AUTHOR 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 AUTHOR 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.
+ *
+ * Linux-native test helper for pkill/pgrep tests.
+ * Replaces BSD __DECONST and <sys/cdefs.h>.
+ *
+ * Usage:
+ * spin_helper --spin flagfile sentinel
+ * Creates flagfile, then spins until killed.
+ *
+ * spin_helper --short flagfile sentinel
+ * Re-execs with short arguments, then spins.
+ *
+ * spin_helper --long flagfile sentinel
+ * Re-execs with maximally long arguments, then spins.
+ */
+
+#ifndef _GNU_SOURCE
+#define _GNU_SOURCE
+#endif
+
+#include <errno.h>
+#include <fcntl.h>
+#include <limits.h>
+#include <signal.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <unistd.h>
+
+static volatile sig_atomic_t got_signal;
+
+static void
+sighandler(int signo)
+{
+ got_signal = signo;
+}
+
+static void
+exec_shortargs(char *argv[])
+{
+ char *nargv[] = { argv[0], (char *)"--spin", argv[2], argv[3], NULL };
+ char *nenvp[] = { NULL };
+
+ execve(argv[0], nargv, nenvp);
+ perror("execve");
+ _exit(1);
+}
+
+static void
+exec_largeargs(char *argv[])
+{
+ size_t bufsz;
+ char *s = NULL;
+ char *nargv[] = {
+ argv[0], (char *)"--spin", argv[2], NULL, argv[3], NULL
+ };
+ char *nenvp[] = { NULL };
+
+ /*
+ * Compute maximum argument size. Account for each argument + NUL
+ * terminator, plus an extra NUL.
+ */
+ long arg_max = sysconf(_SC_ARG_MAX);
+
+ if (arg_max <= 0)
+ arg_max = 131072;
+
+ bufsz = (size_t)arg_max -
+ ((strlen(argv[0]) + 1) + sizeof("--spin") +
+ (strlen(argv[2]) + 1) + (strlen(argv[3]) + 1) + 1);
+
+ /*
+ * Keep trying with smaller sizes until execve stops returning
+ * E2BIG.
+ */
+ do {
+ char *ns = realloc(s, bufsz + 1);
+
+ if (ns == NULL)
+ abort();
+ s = ns;
+ memset(s, 'x', bufsz);
+ s[bufsz] = '\0';
+ nargv[3] = s;
+
+ execve(argv[0], nargv, nenvp);
+ bufsz--;
+ } while (errno == E2BIG && bufsz > 0);
+
+ perror("execve");
+ _exit(1);
+}
+
+int
+main(int argc, char *argv[])
+{
+ if (argc > 1 && strcmp(argv[1], "--spin") == 0) {
+ int fd;
+ struct sigaction sa;
+
+ if (argc < 4) {
+ fprintf(stderr,
+ "usage: %s --spin flagfile sentinel\n",
+ argv[0]);
+ return 1;
+ }
+
+ /* Install signal handler for SIGUSR1 and SIGTERM. */
+ memset(&sa, 0, sizeof(sa));
+ sa.sa_handler = sighandler;
+ sigemptyset(&sa.sa_mask);
+ sigaction(SIGUSR1, &sa, NULL);
+ sigaction(SIGTERM, &sa, NULL);
+
+ /* Create flag file to indicate readiness. */
+ fd = open(argv[2], O_RDWR | O_CREAT | O_TRUNC, 0644);
+ if (fd < 0) {
+ perror(argv[2]);
+ return 1;
+ }
+ /* Write our PID to the flag file. */
+ dprintf(fd, "%d\n", (int)getpid());
+ close(fd);
+
+ /* Spin until signal received. */
+ while (got_signal == 0)
+ pause();
+
+ /*
+ * Write received signal name to the sentinel file
+ * so the test can verify which signal was delivered.
+ */
+ {
+ const char *sname = "UNKNOWN";
+
+ if (got_signal == SIGUSR1)
+ sname = "USR1";
+ else if (got_signal == SIGTERM)
+ sname = "TERM";
+
+ FILE *fp = fopen(argv[argc - 1], "w");
+
+ if (fp != NULL) {
+ fprintf(fp, "%s\n", sname);
+ fclose(fp);
+ }
+ }
+
+ return 0;
+ }
+
+ if (argc != 4) {
+ fprintf(stderr,
+ "usage: %s [--short | --long] flagfile sentinel\n",
+ argv[0]);
+ return 1;
+ }
+
+ if (strcmp(argv[1], "--short") == 0)
+ exec_shortargs(argv);
+ else
+ exec_largeargs(argv);
+
+ return 1;
+}
diff --git a/corebinutils/pkill/tests/test.sh b/corebinutils/pkill/tests/test.sh
new file mode 100644
index 0000000000..94a271a45d
--- /dev/null
+++ b/corebinutils/pkill/tests/test.sh
@@ -0,0 +1,730 @@
+#!/bin/sh
+# Comprehensive test suite for pkill/pgrep Linux port.
+# Follows the same pattern as bin/kill/tests/test.sh.
+set -eu
+
+ROOT=$(CDPATH='' cd -- "$(dirname -- "$0")/.." && pwd)
+PKILL_BIN=${PKILL_BIN:-"$ROOT/out/pkill"}
+PGREP_BIN=${PGREP_BIN:-"$ROOT/out/pgrep"}
+HELPER_BIN=${HELPER_BIN:-"$ROOT/out/spin_helper"}
+CC=${CC:-cc}
+TMPDIR=${TMPDIR:-/tmp}
+WORKDIR=$(mktemp -d "$TMPDIR/pkill-test.XXXXXX")
+STDOUT_FILE="$WORKDIR/stdout"
+STDERR_FILE="$WORKDIR/stderr"
+
+# Unique name: must be <= 15 chars to stay within Linux /proc/PID/stat comm limit.
+# Format: pkt + truncated_PID. "pkt" (3) + "_" (1) + up to 11 PID digits.
+# In practice PID is at most 7 digits on Linux, so max = 11 chars.
+UNIQUE="pkt_$$"
+# Guard against pathological shells where $$ is very long.
+if [ ${#UNIQUE} -gt 15 ]; then
+ _suffix=$(echo "$$" | tail -c8)
+ UNIQUE="pkt_$_suffix"
+fi
+HELPER_COPY="$WORKDIR/$UNIQUE"
+
+PIDS_TO_KILL=""
+
+export LC_ALL=C
+
+cleanup() {
+ for p in $PIDS_TO_KILL; do
+ kill -KILL "$p" 2>/dev/null || true
+ wait "$p" 2>/dev/null || true
+ done
+ rm -rf "$WORKDIR"
+}
+trap cleanup EXIT INT TERM
+
+PASS_COUNT=0
+FAIL_COUNT=0
+
+fail() {
+ printf 'FAIL: %s\n' "$1" >&2
+ FAIL_COUNT=$((FAIL_COUNT + 1))
+ exit 1
+}
+
+pass() {
+ PASS_COUNT=$((PASS_COUNT + 1))
+ printf ' ok: %s\n' "$1"
+}
+
+assert_eq() {
+ name=$1; expected=$2; actual=$3
+ if [ "$expected" != "$actual" ]; then
+ printf 'FAIL: %s\n' "$name" >&2
+ printf '--- expected ---\n%s\n--- actual ---\n%s\n' \
+ "$expected" "$actual" >&2
+ exit 1
+ fi
+}
+
+assert_contains() {
+ name=$1; text=$2; pattern=$3
+ case $text in
+ *"$pattern"*) ;;
+ *) fail "$name: expected to contain '$pattern', got: $text" ;;
+ esac
+}
+
+assert_not_contains() {
+ name=$1; text=$2; pattern=$3
+ case $text in
+ *"$pattern"*) fail "$name: should not contain '$pattern'" ;;
+ *) ;;
+ esac
+}
+
+assert_empty() {
+ name=$1; text=$2
+ if [ -n "$text" ]; then
+ printf 'FAIL: %s\n--- expected empty ---\n--- actual ---\n%s\n' \
+ "$name" "$text" >&2
+ exit 1
+ fi
+}
+
+assert_status() {
+ name=$1; expected=$2; actual=$3
+ if [ "$expected" -ne "$actual" ]; then
+ printf 'FAIL: %s\nexpected status: %s\nactual status: %s\n' \
+ "$name" "$expected" "$actual" >&2
+ exit 1
+ fi
+}
+
+run_capture() {
+ if "$@" >"$STDOUT_FILE" 2>"$STDERR_FILE"; then
+ LAST_STATUS=0
+ else
+ LAST_STATUS=$?
+ fi
+ LAST_STDOUT=$(cat "$STDOUT_FILE")
+ LAST_STDERR=$(cat "$STDERR_FILE")
+}
+
+# Start a helper process with a unique command name.
+# Uses --spin directly so that comm matches the helper copy's basename
+# immediately without any execve race.
+# Sets HELPER_PID, HELPER_SENTINEL.
+start_helper() {
+ tag=$1
+ flag="$WORKDIR/${tag}.flag"
+ sentinel="$WORKDIR/${tag}.sentinel"
+ rm -f "$flag" "$sentinel"
+
+ # --spin: creates flag file then waits for signal.
+ "$HELPER_COPY" --spin "$flag" "$sentinel" &
+ HELPER_PID=$!
+ PIDS_TO_KILL="$PIDS_TO_KILL $HELPER_PID"
+
+ # Wait for flag file to appear (readiness signal).
+ tries=0
+ while [ ! -f "$flag" ]; do
+ tries=$((tries + 1))
+ if [ $tries -gt 100 ]; then
+ fail "helper '$tag' (pid=$HELPER_PID) did not become ready"
+ fi
+ sleep 0.05
+ done
+ HELPER_SENTINEL="$sentinel"
+}
+
+# Start two helpers for oldest/newest tests.
+start_two_helpers() {
+ start_helper "older"
+ OLDER_PID=$HELPER_PID
+ OLDER_SENTINEL=$HELPER_SENTINEL
+ sleep 0.2 # ensure different starttime
+ start_helper "newer"
+ NEWER_PID=$HELPER_PID
+ NEWER_SENTINEL=$HELPER_SENTINEL
+}
+
+stop_helper() {
+ pid=$1
+ kill -KILL "$pid" 2>/dev/null || true
+ wait "$pid" 2>/dev/null || true
+ PIDS_TO_KILL=$(echo "$PIDS_TO_KILL" | sed "s/ *$pid//g")
+}
+
+wait_helper_exit() {
+ pid=$1
+ tries=0
+ while kill -0 "$pid" 2>/dev/null; do
+ tries=$((tries + 1))
+ if [ $tries -gt 50 ]; then
+ fail "helper pid=$pid did not exit"
+ fi
+ sleep 0.1
+ done
+ wait "$pid" 2>/dev/null || true
+ PIDS_TO_KILL=$(echo "$PIDS_TO_KILL" | sed "s/ *$pid//g")
+}
+
+# ============================================================
+# Prerequisite checks
+# ============================================================
+[ -x "$PKILL_BIN" ] || fail "missing pkill binary: $PKILL_BIN"
+[ -x "$PGREP_BIN" ] || fail "missing pgrep binary: $PGREP_BIN"
+[ -x "$HELPER_BIN" ] || fail "missing helper binary: $HELPER_BIN"
+[ -d /proc ] || fail "/proc not available"
+
+# Create a helper copy with unique name.
+cp "$HELPER_BIN" "$HELPER_COPY"
+chmod +x "$HELPER_COPY"
+
+printf '=== pkill/pgrep test suite ===\n'
+printf 'helper name: %s\n' "$UNIQUE"
+
+# ============================================================
+# 1. Usage / error tests
+# ============================================================
+
+# pgrep with no arguments → status 2
+run_capture "$PGREP_BIN"
+assert_status "pgrep no args: status" 2 "$LAST_STATUS"
+assert_empty "pgrep no args: stdout" "$LAST_STDOUT"
+assert_contains "pgrep no args: stderr" "$LAST_STDERR" "usage:"
+pass "pgrep no args → usage error"
+
+# pkill with no arguments → status 2
+run_capture "$PKILL_BIN"
+assert_status "pkill no args: status" 2 "$LAST_STATUS"
+assert_empty "pkill no args: stdout" "$LAST_STDOUT"
+assert_contains "pkill no args: stderr" "$LAST_STDERR" "usage:"
+pass "pkill no args → usage error"
+
+# Bad regex → status 2
+run_capture "$PGREP_BIN" '[invalid'
+assert_status "bad regex: status" 2 "$LAST_STATUS"
+assert_empty "bad regex: stdout" "$LAST_STDOUT"
+assert_contains "bad regex: stderr" "$LAST_STDERR" "Cannot compile regular expression"
+pass "bad regex → error"
+
+# -n and -o together → status 3
+run_capture "$PGREP_BIN" -n -o 'anything'
+assert_status "-n -o together: status" 3 "$LAST_STATUS"
+assert_contains "-n -o together: stderr" "$LAST_STDERR" "mutually exclusive"
+pass "-n -o together → error"
+
+# BSD-only options produce explicit errors on Linux
+run_capture "$PGREP_BIN" -j any 'test'
+assert_status "-j: status" 2 "$LAST_STATUS"
+assert_contains "-j: stderr" "$LAST_STDERR" "not supported on Linux"
+pass "-j → explicit unsupported error"
+
+run_capture "$PGREP_BIN" -c user 'test'
+assert_status "-c: status" 2 "$LAST_STATUS"
+assert_contains "-c: stderr" "$LAST_STDERR" "not supported on Linux"
+pass "-c → explicit unsupported error"
+
+run_capture "$PGREP_BIN" -M /dev/null 'test'
+assert_status "-M: status" 2 "$LAST_STATUS"
+assert_contains "-M: stderr" "$LAST_STDERR" "not supported on Linux"
+pass "-M → explicit unsupported error"
+
+run_capture "$PGREP_BIN" -N /dev/null 'test'
+assert_status "-N: status" 2 "$LAST_STATUS"
+assert_contains "-N: stderr" "$LAST_STDERR" "not supported on Linux"
+pass "-N → explicit unsupported error"
+
+# -L without -F → error
+run_capture "$PGREP_BIN" -L 'test'
+assert_status "-L without -F: status" 3 "$LAST_STATUS"
+assert_contains "-L without -F: stderr" "$LAST_STDERR" "doesn't make sense"
+pass "-L without -F → error"
+
+# pgrep-only options in pkill mode → usage
+run_capture "$PKILL_BIN" -d, 'test'
+assert_status "pkill -d: status" 2 "$LAST_STATUS"
+pass "pkill -d → usage error"
+
+run_capture "$PKILL_BIN" -q 'test'
+assert_status "pkill -q: status" 2 "$LAST_STATUS"
+pass "pkill -q → usage error"
+
+# pkill-only option in pgrep mode → usage
+run_capture "$PGREP_BIN" -I 'test'
+assert_status "pgrep -I: status" 2 "$LAST_STATUS"
+pass "pgrep -I → usage error"
+
+# ============================================================
+# 2. pgrep basic matching
+# ============================================================
+start_helper basic
+
+run_capture "$PGREP_BIN" "$UNIQUE"
+assert_status "pgrep basic: status" 0 "$LAST_STATUS"
+assert_contains "pgrep basic: stdout" "$LAST_STDOUT" "$HELPER_PID"
+pass "pgrep basic match"
+
+# Ensure pgrep does not match itself.
+assert_not_contains "pgrep self-exclusion" "$LAST_STDOUT" "$$"
+pass "pgrep excludes self"
+
+stop_helper "$HELPER_PID"
+
+# No match → status 1
+run_capture "$PGREP_BIN" "nonexistent_process_name_$$$RANDOM"
+assert_status "pgrep no match: status" 1 "$LAST_STATUS"
+assert_empty "pgrep no match: stdout" "$LAST_STDOUT"
+pass "pgrep no match → status 1"
+
+# ============================================================
+# 3. pgrep -x (exact match)
+# ============================================================
+start_helper exact
+
+run_capture "$PGREP_BIN" -x "$UNIQUE"
+assert_status "pgrep -x exact: status" 0 "$LAST_STATUS"
+assert_contains "pgrep -x exact: stdout" "$LAST_STDOUT" "$HELPER_PID"
+pass "pgrep -x exact match"
+
+# Partial name should NOT match with -x.
+partial=$(echo "$UNIQUE" | cut -c1-6)
+run_capture "$PGREP_BIN" -x "$partial"
+# It should not match the helper (could match other things though)
+assert_not_contains "pgrep -x partial" "$LAST_STDOUT" "$HELPER_PID"
+pass "pgrep -x rejects partial match"
+
+stop_helper "$HELPER_PID"
+
+# ============================================================
+# 4. pgrep -f (match full args)
+# ============================================================
+start_helper fullarg
+
+run_capture "$PGREP_BIN" -f -- "--spin.*$WORKDIR"
+assert_status "pgrep -f: status" 0 "$LAST_STATUS"
+assert_contains "pgrep -f: stdout" "$LAST_STDOUT" "$HELPER_PID"
+pass "pgrep -f matches full cmdline"
+
+stop_helper "$HELPER_PID"
+
+# ============================================================
+# 5. pgrep -i (case insensitive)
+# ============================================================
+start_helper casematch
+
+UPPER=$(echo "$UNIQUE" | tr '[:lower:]' '[:upper:]')
+run_capture "$PGREP_BIN" -i "$UPPER"
+assert_status "pgrep -i: status" 0 "$LAST_STATUS"
+assert_contains "pgrep -i: stdout" "$LAST_STDOUT" "$HELPER_PID"
+pass "pgrep -i case insensitive"
+
+stop_helper "$HELPER_PID"
+
+# ============================================================
+# 6. pgrep -l (long output)
+# ============================================================
+start_helper longout
+
+run_capture "$PGREP_BIN" -l "$UNIQUE"
+assert_status "pgrep -l: status" 0 "$LAST_STATUS"
+assert_contains "pgrep -l: stdout" "$LAST_STDOUT" "$HELPER_PID"
+# Long format should include process name.
+assert_contains "pgrep -l shows name" "$LAST_STDOUT" "$UNIQUE"
+pass "pgrep -l long output"
+
+stop_helper "$HELPER_PID"
+
+# ============================================================
+# 7. pgrep -l -f (long + full args)
+# ============================================================
+start_helper longfull
+
+run_capture "$PGREP_BIN" -l -f "$UNIQUE"
+assert_status "pgrep -lf: status" 0 "$LAST_STATUS"
+# Should show PID and full cmdline
+assert_contains "pgrep -lf: shows PID" "$LAST_STDOUT" "$HELPER_PID"
+assert_contains "pgrep -lf: shows --spin" "$LAST_STDOUT" "--spin"
+pass "pgrep -l -f long + full args"
+
+stop_helper "$HELPER_PID"
+
+# ============================================================
+# 8. pgrep -v (inverse)
+# ============================================================
+start_helper inverse
+
+# -v should NOT include the helper in results when it matches.
+# We search for patterns that DO match, and check helper PID is absent.
+run_capture "$PGREP_BIN" -v "$UNIQUE"
+assert_status "pgrep -v: status" 0 "$LAST_STATUS"
+assert_not_contains "pgrep -v no helper" "$LAST_STDOUT" "$HELPER_PID"
+pass "pgrep -v inverse match"
+
+stop_helper "$HELPER_PID"
+
+# ============================================================
+# 9. pgrep -q (quiet)
+# ============================================================
+start_helper quiettest
+
+run_capture "$PGREP_BIN" -q "$UNIQUE"
+assert_status "pgrep -q: status" 0 "$LAST_STATUS"
+assert_empty "pgrep -q: stdout" "$LAST_STDOUT"
+pass "pgrep -q quiet mode"
+
+stop_helper "$HELPER_PID"
+
+# ============================================================
+# 10. pgrep -d (delimiter)
+# ============================================================
+start_helper delim1
+PID1=$HELPER_PID
+start_helper delim2
+PID2=$HELPER_PID
+
+run_capture "$PGREP_BIN" -d, "$UNIQUE"
+assert_status "pgrep -d: status" 0 "$LAST_STATUS"
+# Output should contain comma-separated PIDs
+assert_contains "pgrep -d: comma" "$LAST_STDOUT" ","
+pass "pgrep -d custom delimiter"
+
+stop_helper "$PID1"
+stop_helper "$PID2"
+
+# ============================================================
+# 11. pgrep -n (newest) / -o (oldest)
+# ============================================================
+start_two_helpers
+
+run_capture "$PGREP_BIN" -n "$UNIQUE"
+assert_status "pgrep -n: status" 0 "$LAST_STATUS"
+assert_contains "pgrep -n: newest" "$LAST_STDOUT" "$NEWER_PID"
+assert_not_contains "pgrep -n: not older" "$LAST_STDOUT" "$OLDER_PID"
+pass "pgrep -n selects newest"
+
+run_capture "$PGREP_BIN" -o "$UNIQUE"
+assert_status "pgrep -o: status" 0 "$LAST_STATUS"
+assert_contains "pgrep -o: oldest" "$LAST_STDOUT" "$OLDER_PID"
+assert_not_contains "pgrep -o: not newer" "$LAST_STDOUT" "$NEWER_PID"
+pass "pgrep -o selects oldest"
+
+stop_helper "$OLDER_PID"
+stop_helper "$NEWER_PID"
+
+# ============================================================
+# 12. pgrep -P ppid (parent PID filter)
+# ============================================================
+start_helper ppidtest
+
+PPID_OF_HELPER=$(awk '{print $4}' "/proc/$HELPER_PID/stat" 2>/dev/null || echo "")
+if [ -n "$PPID_OF_HELPER" ]; then
+ run_capture "$PGREP_BIN" -P "$PPID_OF_HELPER" "$UNIQUE"
+ assert_status "pgrep -P: status" 0 "$LAST_STATUS"
+ assert_contains "pgrep -P: match" "$LAST_STDOUT" "$HELPER_PID"
+ pass "pgrep -P parent PID filter"
+
+ # Wrong parent → no match
+ run_capture "$PGREP_BIN" -P 1 "$UNIQUE"
+ if [ "$PPID_OF_HELPER" != "1" ]; then
+ assert_status "pgrep -P wrong parent: status" 1 "$LAST_STATUS"
+ pass "pgrep -P wrong parent → no match"
+ fi
+else
+ printf ' SKIP: pgrep -P (cannot read ppid)\n'
+fi
+
+stop_helper "$HELPER_PID"
+
+# ============================================================
+# 13. pgrep -u euid (effective user ID filter)
+# ============================================================
+start_helper euidtest
+
+MY_EUID=$(id -u)
+run_capture "$PGREP_BIN" -u "$MY_EUID" "$UNIQUE"
+assert_status "pgrep -u euid: status" 0 "$LAST_STATUS"
+assert_contains "pgrep -u euid: match" "$LAST_STDOUT" "$HELPER_PID"
+pass "pgrep -u effective UID filter"
+
+# Wrong euid → no match
+run_capture "$PGREP_BIN" -u 99999 "$UNIQUE"
+assert_status "pgrep -u wrong euid: status" 1 "$LAST_STATUS"
+pass "pgrep -u wrong UID → no match"
+
+stop_helper "$HELPER_PID"
+
+# ============================================================
+# 14. pgrep -U ruid (real user ID filter)
+# ============================================================
+start_helper ruidtest
+
+MY_RUID=$(id -ru)
+run_capture "$PGREP_BIN" -U "$MY_RUID" "$UNIQUE"
+assert_status "pgrep -U ruid: status" 0 "$LAST_STATUS"
+assert_contains "pgrep -U ruid: match" "$LAST_STDOUT" "$HELPER_PID"
+pass "pgrep -U real UID filter"
+
+stop_helper "$HELPER_PID"
+
+# ============================================================
+# 15. pgrep -G rgid (real group ID filter)
+# ============================================================
+start_helper rgidtest
+
+MY_RGID=$(id -rg)
+run_capture "$PGREP_BIN" -G "$MY_RGID" "$UNIQUE"
+assert_status "pgrep -G rgid: status" 0 "$LAST_STATUS"
+assert_contains "pgrep -G rgid: match" "$LAST_STDOUT" "$HELPER_PID"
+pass "pgrep -G real GID filter"
+
+# Wrong GID
+run_capture "$PGREP_BIN" -G 99999 "$UNIQUE"
+assert_status "pgrep -G wrong gid: status" 1 "$LAST_STATUS"
+pass "pgrep -G wrong GID → no match"
+
+stop_helper "$HELPER_PID"
+
+# ============================================================
+# 16. pgrep -g pgrp (process group filter)
+# ============================================================
+start_helper pgrptest
+
+# The helper's pgrp is its own PID (if it calls setpgid) or inherited.
+# Read from proc.
+HELPER_PGRP=$(awk '{print $5}' "/proc/$HELPER_PID/stat" 2>/dev/null || echo "")
+if [ -n "$HELPER_PGRP" ] && [ "$HELPER_PGRP" != "0" ]; then
+ run_capture "$PGREP_BIN" -g "$HELPER_PGRP" "$UNIQUE"
+ assert_status "pgrep -g: status" 0 "$LAST_STATUS"
+ assert_contains "pgrep -g: match" "$LAST_STDOUT" "$HELPER_PID"
+ pass "pgrep -g process group filter"
+else
+ printf ' SKIP: pgrep -g (cannot read pgrp)\n'
+fi
+
+stop_helper "$HELPER_PID"
+
+# ============================================================
+# 17. pgrep -s sid (session ID filter)
+# ============================================================
+start_helper sidtest
+
+HELPER_SID=$(awk '{print $6}' "/proc/$HELPER_PID/stat" 2>/dev/null || echo "")
+if [ -n "$HELPER_SID" ] && [ "$HELPER_SID" != "0" ]; then
+ run_capture "$PGREP_BIN" -s "$HELPER_SID" "$UNIQUE"
+ assert_status "pgrep -s: status" 0 "$LAST_STATUS"
+ assert_contains "pgrep -s: match" "$LAST_STDOUT" "$HELPER_PID"
+ pass "pgrep -s session ID filter"
+else
+ printf ' SKIP: pgrep -s (cannot read sid)\n'
+fi
+
+stop_helper "$HELPER_PID"
+
+# ============================================================
+# 18. pgrep -F pidfile
+# ============================================================
+start_helper pidfile
+
+PIDFILE="$WORKDIR/test.pid"
+echo "$HELPER_PID" > "$PIDFILE"
+
+run_capture "$PGREP_BIN" -F "$PIDFILE"
+assert_status "pgrep -F: status" 0 "$LAST_STATUS"
+assert_contains "pgrep -F: match" "$LAST_STDOUT" "$HELPER_PID"
+pass "pgrep -F pidfile"
+
+# Non-existent pidfile → error
+run_capture "$PGREP_BIN" -F "$WORKDIR/nonexistent.pid"
+assert_status "pgrep -F nonexist: status" 3 "$LAST_STATUS"
+assert_contains "pgrep -F nonexist: stderr" "$LAST_STDERR" "Cannot open"
+pass "pgrep -F nonexistent → error"
+
+# Empty pidfile → error
+> "$WORKDIR/empty.pid"
+run_capture "$PGREP_BIN" -F "$WORKDIR/empty.pid"
+assert_status "pgrep -F empty: status" 3 "$LAST_STATUS"
+assert_contains "pgrep -F empty: stderr" "$LAST_STDERR" "empty"
+pass "pgrep -F empty pidfile → error"
+
+stop_helper "$HELPER_PID"
+
+# ============================================================
+# 19. pkill basic (send SIGTERM)
+# ============================================================
+start_helper pkill_basic
+
+run_capture "$PKILL_BIN" "$UNIQUE"
+assert_status "pkill basic: status" 0 "$LAST_STATUS"
+
+# Wait for helper to receive signal and exit.
+wait_helper_exit "$HELPER_PID"
+
+# Check sentinel file for signal name.
+if [ -f "$HELPER_SENTINEL" ]; then
+ signame=$(cat "$HELPER_SENTINEL")
+ assert_eq "pkill basic: signal" "TERM" "$signame"
+fi
+pass "pkill basic SIGTERM"
+
+# ============================================================
+# 20. pkill -SIGUSR1 (custom signal)
+# ============================================================
+start_helper pkill_sig
+
+run_capture "$PKILL_BIN" -USR1 "$UNIQUE"
+assert_status "pkill -USR1: status" 0 "$LAST_STATUS"
+
+wait_helper_exit "$HELPER_PID"
+
+if [ -f "$HELPER_SENTINEL" ]; then
+ signame=$(cat "$HELPER_SENTINEL")
+ assert_eq "pkill -USR1: signal" "USR1" "$signame"
+fi
+pass "pkill -USR1 custom signal"
+
+# ============================================================
+# 21. pkill -<number> (numeric signal)
+# ============================================================
+start_helper pkill_num
+
+# SIGUSR1 is typically 10
+run_capture "$PKILL_BIN" -10 "$UNIQUE"
+assert_status "pkill -10: status" 0 "$LAST_STATUS"
+
+wait_helper_exit "$HELPER_PID"
+
+if [ -f "$HELPER_SENTINEL" ]; then
+ signame=$(cat "$HELPER_SENTINEL")
+ assert_eq "pkill -10: signal" "USR1" "$signame"
+fi
+pass "pkill -<number> numeric signal"
+
+# ============================================================
+# 22. pkill -f (match full args)
+# ============================================================
+start_helper pkill_full
+
+run_capture "$PKILL_BIN" -f -- "--spin.*$WORKDIR"
+assert_status "pkill -f: status" 0 "$LAST_STATUS"
+
+wait_helper_exit "$HELPER_PID"
+pass "pkill -f matches full cmdline"
+
+# ============================================================
+# 23. pkill with -l (list mode)
+# ============================================================
+start_helper pkill_list
+
+run_capture "$PKILL_BIN" -l "$UNIQUE"
+assert_status "pkill -l: status" 0 "$LAST_STATUS"
+assert_contains "pkill -l: stdout" "$LAST_STDOUT" "kill"
+assert_contains "pkill -l: stdout" "$LAST_STDOUT" "$HELPER_PID"
+pass "pkill -l shows kill command"
+
+wait_helper_exit "$HELPER_PID"
+
+# ============================================================
+# 24. pkill no match → status 1
+# ============================================================
+run_capture "$PKILL_BIN" "nonexistent_process_$$$RANDOM"
+assert_status "pkill no match: status" 1 "$LAST_STATUS"
+pass "pkill no match → status 1"
+
+# ============================================================
+# 25. pgrep -S (include kernel threads)
+# ============================================================
+# This should not error out. We just check it runs.
+run_capture "$PGREP_BIN" -S 'kthreadd'
+# kthreadd might or might not be visible depending on environment.
+# Just verify it doesn't crash.
+if [ "$LAST_STATUS" -ne 0 ] && [ "$LAST_STATUS" -ne 1 ]; then
+ fail "pgrep -S unexpected status: $LAST_STATUS"
+fi
+pass "pgrep -S kernel threads (no crash)"
+
+# ============================================================
+# 26. pgrep -a (include ancestors)
+# ============================================================
+start_helper ancestors
+
+# Without -a, our shell (ancestor) might be excluded from results.
+# With -a, ancestors are included. Hard to test precisely, but
+# verify -a doesn't cause errors.
+run_capture "$PGREP_BIN" -a "$UNIQUE"
+assert_status "pgrep -a: status" 0 "$LAST_STATUS"
+pass "pgrep -a include ancestors (no crash)"
+
+stop_helper "$HELPER_PID"
+
+# ============================================================
+# 27. Multiple patterns
+# ============================================================
+start_helper multi1
+PID_M1=$HELPER_PID
+
+run_capture "$PGREP_BIN" "$UNIQUE"
+assert_status "multi pattern: status" 0 "$LAST_STATUS"
+assert_contains "multi pattern: match" "$LAST_STDOUT" "$PID_M1"
+pass "multiple pattern matching"
+
+stop_helper "$PID_M1"
+
+# ============================================================
+# 28. User name filter (if available)
+# ============================================================
+start_helper usertest
+
+MY_USER=$(id -un)
+run_capture "$PGREP_BIN" -u "$MY_USER" "$UNIQUE"
+assert_status "pgrep -u username: status" 0 "$LAST_STATUS"
+assert_contains "pgrep -u username: match" "$LAST_STDOUT" "$HELPER_PID"
+pass "pgrep -u username filter"
+
+# Unknown user → error
+run_capture "$PGREP_BIN" -u "nonexistent_user_xyz_$$" "$UNIQUE"
+assert_status "pgrep -u unknown user: status" 2 "$LAST_STATUS"
+assert_contains "pgrep -u unknown: stderr" "$LAST_STDERR" "Unknown user"
+pass "pgrep -u unknown user → error"
+
+stop_helper "$HELPER_PID"
+
+# ============================================================
+# 29. Group name filter
+# ============================================================
+start_helper grouptest
+
+MY_GROUP=$(id -gn)
+run_capture "$PGREP_BIN" -G "$MY_GROUP" "$UNIQUE"
+assert_status "pgrep -G groupname: status" 0 "$LAST_STATUS"
+assert_contains "pgrep -G groupname: match" "$LAST_STDOUT" "$HELPER_PID"
+pass "pgrep -G groupname filter"
+
+# Unknown group → error
+run_capture "$PGREP_BIN" -G "nonexistent_group_xyz_$$" "$UNIQUE"
+assert_status "pgrep -G unknown group: status" 2 "$LAST_STATUS"
+assert_contains "pgrep -G unknown: stderr" "$LAST_STDERR" "Unknown group"
+pass "pgrep -G unknown group → error"
+
+stop_helper "$HELPER_PID"
+
+# ============================================================
+# 30. pgrep -F -L (pidfile with lock check)
+# ============================================================
+start_helper locktest
+
+LOCKPID_FILE="$WORKDIR/locked.pid"
+echo "$HELPER_PID" > "$LOCKPID_FILE"
+
+# File is not locked, so -L should report error.
+run_capture "$PGREP_BIN" -F "$LOCKPID_FILE" -L
+assert_status "pgrep -F -L unlocked: status" 3 "$LAST_STATUS"
+assert_contains "pgrep -F -L: stderr" "$LAST_STDERR" "can be locked"
+pass "pgrep -F -L detects unlocked pidfile"
+
+stop_helper "$HELPER_PID"
+
+# ============================================================
+# Summary
+# ============================================================
+printf '\n=== %d tests passed ===\n' "$PASS_COUNT"
+if [ "$FAIL_COUNT" -ne 0 ]; then
+ printf '=== %d tests FAILED ===\n' "$FAIL_COUNT"
+ exit 1
+fi