mirror of
https://github.com/Cacti/spine.git
synced 2026-10-24 12:06:59 +09:00
Classify numeric, undefined, multipart and Cacti octet results through one parser, normalize undefined agent values before persistence and flush every SNMP batch consistently. Validate whole non-negative uptimes, retain unknown reindex baselines and free script results through their owning pointer. The mocked poll_host suite checks session-profile boundaries, spike handling, output_regex input and completion writes. Originally step 4/14 of the C17/native-correctness series; its preceding steps have merged. The current base is develop. Earlier validation, before this review follow-up, in isolated copied snapshots on Linux arm64: - Clean GCC build and make check: 15 passed, 1 existing prerequisite skips, 0 failures. - ASan/UBSan: 14 passed, 2 existing privilege/allocator-interposition prerequisite skips, 0 failures; no sanitizer findings. - Production source/profile integrity verification against disposable MariaDB and loopback SNMP fixtures passed. - Release archive extracted into a separate source tree and built/tested out of tree; fuzz checks passed. Earlier local profiles retain source hashes for the copied validation tree; GitHub checks validate the published commit. No production database/device, host installation or live privilege change is part of validation. Existing prerequisite skips were retained; no test or finding was suppressed to obtain a pass. Review follow-up (2026-10-09): Restacked on the corrected preceding branches. The SNMP status fix is introduced in #638, the reindex/completion fixes in #640, and focused regression coverage in #650. Restacked tip validation: isolated Linux arm64 build and make check passed (18 passed, 1 setuid prerequisite skip, no failures). GitHub checks validate the updated published commits; earlier profile/archive/sanitizer evidence above belongs to the previous revisions. ## Current merge dependencies Current base: `develop`. Immediate dependency: `develop` (earlier steps already merged). Next open PR: [#644](https://github.com/Cacti/spine/pull/644). Merge order: #641 → #644 → #654 → #646 → #648 → #649 → #650 → #651 → #655 → #658 → #659. This repository uses squash-only merges and deletes merged branches. Record the old parent SHA, squash one PR at a time, then restack and retarget dependents before merging the next PR. Re-run merge validation and checks on the resulting heads; an ordinary clean merge does not replace squash/restack validation. --------- Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
1132 lines
33 KiB
C
1132 lines
33 KiB
C
/* Unit tests that exercise the shipped objects.
|
|
*
|
|
* The other test binaries in this directory inline the function under test, or
|
|
* replicate its predicate, so they compile standalone. That keeps them cheap
|
|
* but means a fix can land in util.c while the test still passes against the
|
|
* old copy. This binary links the real translation units instead, with
|
|
* tests/fuzz/stubs.c supplying the globals that spine.c would otherwise define,
|
|
* so what runs here is what ships.
|
|
*/
|
|
#include <stdarg.h>
|
|
#include <stddef.h>
|
|
#include <setjmp.h>
|
|
#include <cmocka.h>
|
|
|
|
#include <string.h>
|
|
#include <stdlib.h>
|
|
#include <limits.h>
|
|
#include <errno.h>
|
|
#include <pthread.h>
|
|
#include <signal.h>
|
|
#include <sys/wait.h>
|
|
#include <unistd.h>
|
|
|
|
#include "common.h"
|
|
#include "spine.h"
|
|
#include "util.h"
|
|
#include "ping.h"
|
|
#include "poller.h"
|
|
|
|
#include "nft_popen.h"
|
|
|
|
#include <fcntl.h>
|
|
|
|
#if !defined(ICMP_DEST_UNREACH) && defined(ICMP_UNREACH)
|
|
#define ICMP_DEST_UNREACH ICMP_UNREACH
|
|
#endif
|
|
|
|
/* provided by tests/fuzz/stubs.c, as spine.c would */
|
|
extern int *debug_devices;
|
|
|
|
#ifdef SPINE_TEST_WRAP_WAITPID
|
|
/* Arms __wrap_waitpid() to fail every call for one pid; -1/unset disables it
|
|
* so every other test in this binary keeps hitting the real waitpid(). */
|
|
static pid_t forced_waitpid_error_pid = -1;
|
|
static int forced_waitpid_error_errno = 0;
|
|
|
|
extern pid_t __real_waitpid(pid_t pid, int *status, int options);
|
|
|
|
pid_t __wrap_waitpid(pid_t pid, int *status, int options) {
|
|
if (pid == forced_waitpid_error_pid) {
|
|
errno = forced_waitpid_error_errno;
|
|
return -1;
|
|
}
|
|
return __real_waitpid(pid, status, options);
|
|
}
|
|
#endif
|
|
|
|
/* --- strncopy(): issue#447, the off-by-one when src fills the destination -- */
|
|
|
|
static void test_strncopy_truncates_within_the_buffer(void **state) {
|
|
struct { char dst[8]; unsigned char canary; } b;
|
|
(void) state;
|
|
|
|
memset(&b, 0xAA, sizeof b);
|
|
strncopy(b.dst, "ABCDEFGHIJK", sizeof b.dst);
|
|
|
|
assert_string_equal(b.dst, "ABCDEFG");
|
|
assert_int_equal(strlen(b.dst), sizeof b.dst - 1);
|
|
assert_int_equal(b.canary, 0xAA);
|
|
}
|
|
|
|
static void test_strncopy_copies_a_short_source_whole(void **state) {
|
|
char dst[16];
|
|
(void) state;
|
|
|
|
memset(dst, 0xAA, sizeof dst);
|
|
strncopy(dst, "abc", sizeof dst);
|
|
assert_string_equal(dst, "abc");
|
|
}
|
|
|
|
static void test_strncopy_handles_a_zero_size(void **state) {
|
|
char dst[4] = {'z', 'z', 'z', '\0'};
|
|
(void) state;
|
|
|
|
strncopy(dst, "abc", 0);
|
|
assert_string_equal(dst, "zzz");
|
|
}
|
|
|
|
static void test_strncopy_terminates_an_exact_fit(void **state) {
|
|
struct { char dst[4]; unsigned char canary; } b;
|
|
(void) state;
|
|
|
|
memset(&b, 0xAA, sizeof b);
|
|
strncopy(b.dst, "abc", sizeof b.dst);
|
|
|
|
assert_string_equal(b.dst, "abc");
|
|
assert_int_equal(b.canary, 0xAA);
|
|
}
|
|
|
|
/* --- regex_replace(): returns the match, or the input when it cannot ------- */
|
|
|
|
static void test_regex_replace_returns_the_match(void **state) {
|
|
(void) state;
|
|
assert_string_equal(regex_replace("[0-9][0-9]*", "load 42 avg"), "42");
|
|
assert_string_equal(regex_replace("\\([0-9][0-9]*\\)", "value 42"), "42");
|
|
}
|
|
|
|
static void test_regex_replace_passes_through_on_no_match(void **state) {
|
|
(void) state;
|
|
assert_string_equal(regex_replace("[0-9][0-9]*", "no digits"), "no digits");
|
|
}
|
|
|
|
static void test_regex_replace_passes_through_on_bad_pattern(void **state) {
|
|
(void) state;
|
|
assert_string_equal(regex_replace("[unclosed", "value"), "value");
|
|
assert_string_equal(regex_replace("*", "value"), "value");
|
|
}
|
|
|
|
static void test_spine_appendf_reports_truncation_and_guards(void **state) {
|
|
char buffer[8] = "";
|
|
char success[8] = "";
|
|
char *cursor = buffer;
|
|
char *success_cursor = success;
|
|
char *null_cursor = NULL;
|
|
size_t remaining = sizeof(buffer);
|
|
size_t success_remaining = sizeof(success);
|
|
size_t zero = 0;
|
|
(void) state;
|
|
|
|
assert_true(spine_appendf(&success_cursor, &success_remaining, "%s", "abc"));
|
|
assert_int_equal(success_cursor - success, 3);
|
|
assert_int_equal(success_remaining, sizeof(success) - 3);
|
|
assert_string_equal(success, "abc");
|
|
assert_false(spine_appendf(&cursor, &remaining, "%s", "0123456789"));
|
|
assert_int_equal(cursor - buffer, 7);
|
|
assert_int_equal(remaining, 1);
|
|
assert_int_equal(buffer[7], '\0');
|
|
assert_false(spine_appendf(&cursor, &remaining, "%s", "x"));
|
|
assert_string_equal(buffer, "0123456");
|
|
assert_false(spine_appendf(NULL, &remaining, "%s", "x"));
|
|
assert_false(spine_appendf(&null_cursor, &remaining, "%s", "x"));
|
|
assert_false(spine_appendf(&cursor, NULL, "%s", "x"));
|
|
assert_false(spine_appendf(&cursor, &zero, "%s", "x"));
|
|
}
|
|
|
|
static void test_bounded_formatters(void **state) {
|
|
char capabilities[BUFSIZE];
|
|
char row[DBL_BUFSIZE + SMALL_BUFSIZE];
|
|
char small[32];
|
|
char long_value[DBL_BUFSIZE];
|
|
const char *auth_start;
|
|
const char *auth_end;
|
|
const char *priv_start;
|
|
const char *priv_end;
|
|
(void) state;
|
|
|
|
memset(long_value, '7', sizeof(long_value) - 1);
|
|
long_value[sizeof(long_value) - 1] = '\0';
|
|
|
|
assert_true(format_spine_capabilities(capabilities,
|
|
sizeof(capabilities), long_value, long_value));
|
|
assert_true(strlen(capabilities) < sizeof(capabilities));
|
|
auth_start = strstr(capabilities, "authProtocols: \"");
|
|
assert_non_null(auth_start);
|
|
auth_start += strlen("authProtocols: \"");
|
|
auth_end = strchr(auth_start, '"');
|
|
assert_non_null(auth_end);
|
|
priv_start = strstr(capabilities, "privProtocols: \"");
|
|
assert_non_null(priv_start);
|
|
priv_start += strlen("privProtocols: \"");
|
|
priv_end = strchr(priv_start, '"');
|
|
assert_non_null(priv_end);
|
|
assert_int_equal(auth_end - auth_start, CAPABILITY_PROTOCOL_LIST_MAX);
|
|
assert_int_equal(priv_end - priv_start, CAPABILITY_PROTOCOL_LIST_MAX);
|
|
assert_string_equal(priv_end, "\" }");
|
|
assert_false(format_spine_capabilities(small,
|
|
sizeof(small), long_value, long_value));
|
|
assert_false(format_spine_capabilities(NULL, 0, long_value, long_value));
|
|
|
|
assert_true(format_poller_output_row(row, sizeof(row), 17,
|
|
"traffic_in", "1700000000.25", long_value));
|
|
assert_non_null(strstr(row,
|
|
"(17, 'traffic_in', FROM_UNIXTIME(1700000000.25)"));
|
|
assert_false(format_poller_output_row(small, sizeof(small), 17,
|
|
"traffic_in", "1700000000.25", long_value));
|
|
assert_false(format_poller_output_row(row, sizeof(row), 17,
|
|
"traffic_in", "1700000000); DROP TABLE host", long_value));
|
|
assert_false(format_poller_output_row(NULL, 0, 17,
|
|
"traffic_in", "1700000000.25", long_value));
|
|
}
|
|
|
|
|
|
/* --- predicates ----------------------------------------------------------- */
|
|
|
|
static void test_all_digits(void **state) {
|
|
(void) state;
|
|
assert_int_equal(all_digits("12345"), TRUE);
|
|
assert_int_equal(all_digits("0"), TRUE);
|
|
assert_int_equal(all_digits(""), FALSE); /* empty is not all digits */
|
|
assert_int_equal(all_digits("12a45"), FALSE);
|
|
assert_int_equal(all_digits("-12"), FALSE); /* sign is not a digit */
|
|
assert_int_equal(all_digits(" 12"), FALSE);
|
|
}
|
|
|
|
static void test_is_ipaddress(void **state) {
|
|
(void) state;
|
|
assert_int_equal(is_ipaddress("192.168.0.1"), TRUE);
|
|
assert_int_equal(is_ipaddress("::1"), TRUE);
|
|
assert_int_equal(is_ipaddress("2001:db8::1"), FALSE); /* letters rejected */
|
|
assert_int_equal(is_ipaddress("host.example"), FALSE);
|
|
assert_int_equal(is_ipaddress(""), TRUE); /* vacuously true */
|
|
}
|
|
|
|
/* --- string surgery ------------------------------------------------------- */
|
|
|
|
static void test_trim_family(void **state) {
|
|
char a[32], b[32], c[32];
|
|
(void) state;
|
|
|
|
strcpy(a, " padded "); assert_string_equal(trim(a), "padded");
|
|
strcpy(b, " left"); assert_string_equal(ltrim(b), "left");
|
|
strcpy(c, "right "); assert_string_equal(rtrim(c), "right");
|
|
}
|
|
|
|
static void test_reverse(void **state) {
|
|
char s[16], one[2], empty[1];
|
|
(void) state;
|
|
|
|
strcpy(s, "abcdef"); assert_string_equal(reverse(s), "fedcba");
|
|
strcpy(one, "x"); assert_string_equal(reverse(one), "x");
|
|
empty[0] = '\0'; assert_string_equal(reverse(empty), "");
|
|
}
|
|
|
|
static void test_strpos(void **state) {
|
|
(void) state;
|
|
assert_int_equal(strpos("hello world", "world"), 6);
|
|
assert_int_equal(strpos("hello", "hello"), 0);
|
|
assert_int_equal(strpos("hello", "zzz"), -1);
|
|
assert_int_equal(strpos("hello", ""), 0);
|
|
}
|
|
|
|
static void test_char_count(void **state) {
|
|
(void) state;
|
|
assert_int_equal(char_count("a,b,c", ','), 2);
|
|
assert_int_equal(char_count("none", ','), 0);
|
|
assert_int_equal(char_count("", 'x'), 0);
|
|
assert_int_equal(char_count("anything", '\0'), 1); /* documented shortcut */
|
|
}
|
|
|
|
static void test_strip_alpha(void **state) {
|
|
char a[32], b[32];
|
|
(void) state;
|
|
|
|
strcpy(a, "load42"); assert_string_equal(strip_alpha(a), "42");
|
|
strcpy(b, "abc123def"); assert_string_equal(strip_alpha(b), "123");
|
|
}
|
|
|
|
static void test_add_slashes_doubles_a_backslash(void **state) {
|
|
char in[32];
|
|
char *out;
|
|
(void) state;
|
|
|
|
strcpy(in, "a\\b");
|
|
out = add_slashes(in);
|
|
|
|
assert_non_null(out);
|
|
assert_string_equal(out, "a\\\\b");
|
|
free(out); /* add_slashes() returns owned memory */
|
|
}
|
|
|
|
static void test_add_slashes_passes_plain_text_through(void **state) {
|
|
char in[32];
|
|
char *out;
|
|
(void) state;
|
|
|
|
strcpy(in, "plain");
|
|
out = add_slashes(in);
|
|
|
|
assert_non_null(out);
|
|
assert_string_equal(out, "plain");
|
|
free(out);
|
|
}
|
|
|
|
static void test_row_alias_upsert_version_gate(void **state) {
|
|
(void) state;
|
|
assert_false(db_row_alias_upsert_supported(NULL, 80020));
|
|
assert_false(db_row_alias_upsert_supported("8.0.19", 80019));
|
|
assert_true(db_row_alias_upsert_supported("8.0.20", 80020));
|
|
assert_true(db_row_alias_upsert_supported("8.4.0", 80400));
|
|
assert_true(db_row_alias_upsert_supported("9.1.0", 90100));
|
|
assert_false(db_row_alias_upsert_supported("10.11.6-MariaDB", 101106));
|
|
assert_false(db_row_alias_upsert_supported("5.5.5-10.11.6-MariaDB-log", 50505));
|
|
}
|
|
|
|
/* --- misc ----------------------------------------------------------------- */
|
|
|
|
static void test_file_exists(void **state) {
|
|
(void) state;
|
|
assert_int_equal(file_exists("/etc/hostname") || file_exists("/etc/passwd"), TRUE);
|
|
assert_int_equal(file_exists("/no/such/path/at/all"), FALSE);
|
|
}
|
|
|
|
static void test_get_time_as_double_advances(void **state) {
|
|
double t1, t2;
|
|
(void) state;
|
|
|
|
t1 = get_time_as_double();
|
|
assert_true(t1 > 0.0);
|
|
t2 = get_time_as_double();
|
|
assert_true(t2 >= t1);
|
|
}
|
|
|
|
static void test_get_checksum_is_stable(void **state) {
|
|
unsigned char buf[16];
|
|
unsigned short a, b;
|
|
(void) state;
|
|
|
|
memset(buf, 0x5A, sizeof buf);
|
|
a = get_checksum(buf, sizeof buf);
|
|
b = get_checksum(buf, sizeof buf);
|
|
assert_int_equal(a, b);
|
|
|
|
buf[0] = 0x00;
|
|
assert_int_not_equal(get_checksum(buf, sizeof buf), a);
|
|
}
|
|
|
|
/* --- spine_icmp_classify_reply(): the real classifier --------------------- */
|
|
|
|
#define ICMP_TEST_BUFSIZE 64
|
|
|
|
static void build_ip_icmp(unsigned char *buf, size_t len, uint16_t id, uint16_t seq, int type) {
|
|
struct ip *iph;
|
|
struct icmp *pkt;
|
|
|
|
memset(buf, 0, len);
|
|
|
|
iph = (struct ip *) buf;
|
|
iph->ip_hl = sizeof(struct ip) / 4;
|
|
iph->ip_v = 4;
|
|
/* a raw ICMP socket only delivers ICMP datagrams; the classifier checks it */
|
|
iph->ip_p = IPPROTO_ICMP;
|
|
|
|
pkt = (struct icmp *) (buf + sizeof(struct ip));
|
|
pkt->icmp_type = type;
|
|
pkt->icmp_id = id;
|
|
pkt->icmp_seq = seq;
|
|
}
|
|
|
|
static void test_icmp_classify_accepts_our_reply(void **state) {
|
|
unsigned char buf[ICMP_TEST_BUFSIZE];
|
|
const struct icmp *out = NULL;
|
|
(void) state;
|
|
|
|
build_ip_icmp(buf, sizeof buf, 0x1234, 7, ICMP_ECHOREPLY);
|
|
assert_int_equal(spine_icmp_classify_reply(buf, sizeof buf, 0x1234, 7, &out), SPINE_ICMP_REPLY_OK);
|
|
assert_non_null(out);
|
|
}
|
|
|
|
static void test_icmp_classify_rejects_a_runt(void **state) {
|
|
unsigned char buf[ICMP_TEST_BUFSIZE];
|
|
const struct icmp *out = (const struct icmp *) 1;
|
|
(void) state;
|
|
|
|
build_ip_icmp(buf, sizeof buf, 1, 1, ICMP_ECHOREPLY);
|
|
assert_int_equal(spine_icmp_classify_reply(buf, 4, 1, 1, &out), SPINE_ICMP_REPLY_TOO_SHORT);
|
|
assert_null(out);
|
|
}
|
|
|
|
static void test_icmp_classify_rejects_a_bad_ihl(void **state) {
|
|
unsigned char buf[ICMP_TEST_BUFSIZE];
|
|
const struct icmp *out = NULL;
|
|
(void) state;
|
|
|
|
build_ip_icmp(buf, sizeof buf, 1, 1, ICMP_ECHOREPLY);
|
|
((struct ip *) buf)->ip_hl = 2; /* below sizeof(struct ip) */
|
|
assert_int_equal(spine_icmp_classify_reply(buf, sizeof buf, 1, 1, &out), SPINE_ICMP_REPLY_BAD_HEADER);
|
|
}
|
|
|
|
static void test_icmp_classify_rejects_a_non_echo(void **state) {
|
|
unsigned char buf[ICMP_TEST_BUFSIZE];
|
|
const struct icmp *out = NULL;
|
|
(void) state;
|
|
|
|
/* Type 3 is Destination Unreachable in RFC 792. BSD names the symbol
|
|
* ICMP_UNREACH while Linux names it ICMP_DEST_UNREACH, so keep the wire
|
|
* value explicit in this platform-independent parser test. */
|
|
build_ip_icmp(buf, sizeof buf, 1, 1, 3);
|
|
assert_int_equal(spine_icmp_classify_reply(buf, sizeof buf, 1, 1, &out), SPINE_ICMP_REPLY_NOT_ECHO);
|
|
assert_null(out);
|
|
}
|
|
|
|
static void test_icmp_classify_rejects_another_hosts_reply(void **state) {
|
|
unsigned char buf[ICMP_TEST_BUFSIZE];
|
|
const struct icmp *out = NULL;
|
|
(void) state;
|
|
|
|
build_ip_icmp(buf, sizeof buf, 0xBEEF, 9, ICMP_ECHOREPLY);
|
|
assert_int_equal(spine_icmp_classify_reply(buf, sizeof buf, 0x1234, 9, &out), SPINE_ICMP_REPLY_NOT_OURS);
|
|
assert_null(out);
|
|
}
|
|
|
|
static void test_icmp_classify_rejects_a_null_buffer(void **state) {
|
|
const struct icmp *out = NULL;
|
|
(void) state;
|
|
assert_int_equal(spine_icmp_classify_reply(NULL, ICMP_TEST_BUFSIZE, 1, 1, &out), SPINE_ICMP_REPLY_TOO_SHORT);
|
|
}
|
|
|
|
/* --- get_namebyhost(): the real parser ------------------------------------ */
|
|
|
|
static void test_namebyhost_plain_hostname(void **state) {
|
|
char host[64];
|
|
name_t *n;
|
|
(void) state;
|
|
|
|
strcpy(host, "device.example.net");
|
|
n = get_namebyhost(host, NULL);
|
|
assert_non_null(n);
|
|
assert_string_equal(n->hostname, "device.example.net");
|
|
free(n);
|
|
}
|
|
|
|
static void test_namebyhost_is_reentrant_across_calls(void **state) {
|
|
char a[64], b[64];
|
|
name_t *na, *nb;
|
|
(void) state;
|
|
|
|
strcpy(a, "first.example.net");
|
|
strcpy(b, "second.example.net");
|
|
|
|
na = get_namebyhost(a, NULL);
|
|
nb = get_namebyhost(b, NULL);
|
|
|
|
assert_string_equal(na->hostname, "first.example.net");
|
|
assert_string_equal(nb->hostname, "second.example.net");
|
|
free(na);
|
|
free(nb);
|
|
}
|
|
|
|
static void test_namebyhost_tcpv6_method_keeps_the_full_hostname(void **state) {
|
|
char host[64];
|
|
name_t *n;
|
|
(void) state;
|
|
|
|
strcpy(host, "[fe80::1]:161");
|
|
n = get_namebyhost(host, NULL);
|
|
assert_non_null(n);
|
|
assert_string_equal(n->hostname, "[fe80::1]:161");
|
|
free(n);
|
|
}
|
|
|
|
|
|
/* --- configuration: defaults, the file parser, and set_option() ----------- */
|
|
|
|
static void test_config_defaults_populates_the_set(void **state) {
|
|
(void) state;
|
|
|
|
memset(&set, 0, sizeof set);
|
|
config_defaults();
|
|
|
|
assert_int_equal(set.poller.threads, DEFAULT_THREADS);
|
|
assert_int_equal(set.database.port, DEFAULT_DB_PORT);
|
|
assert_string_equal(set.database.host, DEFAULT_DB_HOST);
|
|
assert_string_equal(set.database.database, DEFAULT_DB_DB);
|
|
}
|
|
|
|
static void test_read_spine_config_rejects_a_missing_file(void **state) {
|
|
(void) state;
|
|
assert_int_equal(read_spine_config("/no/such/spine.conf"), -1);
|
|
}
|
|
|
|
static void test_read_spine_config_reads_settings(void **state) {
|
|
const char *path = "/tmp/spine_test.conf";
|
|
FILE *fp;
|
|
(void) state;
|
|
|
|
fp = fopen(path, "wb");
|
|
assert_non_null(fp);
|
|
fputs("DB_Host testhost\n", fp);
|
|
fputs("DB_Database testdb\n", fp);
|
|
fputs("DB_User testuser\n", fp);
|
|
fputs("DB_Port 3399\n", fp);
|
|
fputs("Poller_Threads 7\n", fp);
|
|
fputs("# a comment line\n", fp);
|
|
fputs("\n", fp);
|
|
fclose(fp);
|
|
|
|
config_defaults();
|
|
assert_int_equal(read_spine_config(path), 0);
|
|
|
|
assert_string_equal(set.database.host, "testhost");
|
|
assert_string_equal(set.database.database, "testdb");
|
|
assert_string_equal(set.database.user, "testuser");
|
|
assert_int_equal(set.database.port, 3399);
|
|
|
|
remove(path);
|
|
}
|
|
|
|
/* --- get_date_format(): cached storage, rebuilt by set_date_format() ------ */
|
|
|
|
static void test_get_date_format_returns_cached_storage(void **state) {
|
|
char *fmt;
|
|
(void) state;
|
|
|
|
config_defaults();
|
|
set_date_format();
|
|
fmt = get_date_format();
|
|
|
|
assert_non_null(fmt);
|
|
assert_true(strlen(fmt) > 0);
|
|
|
|
/* the buffer belongs to util.c and is handed out, not owned by us */
|
|
assert_ptr_equal(fmt, get_date_format());
|
|
}
|
|
|
|
static void test_set_date_format_clamps_an_out_of_range_format(void **state) {
|
|
char *fmt;
|
|
(void) state;
|
|
|
|
config_defaults();
|
|
set.logging.log_datetime_format = GD_MAX + 10;
|
|
set.logging.log_datetime_separator = GDC_MAX + 10;
|
|
|
|
set_date_format();
|
|
fmt = get_date_format();
|
|
|
|
assert_non_null(fmt);
|
|
assert_int_equal(set.logging.log_datetime_format, GD_DEFAULT);
|
|
assert_int_equal(set.logging.log_datetime_separator, GDC_DEFAULT);
|
|
}
|
|
|
|
static void test_get_date_format_covers_each_supported_format(void **state) {
|
|
int fmt_value;
|
|
int sep_value;
|
|
char *fmt;
|
|
(void) state;
|
|
|
|
config_defaults();
|
|
|
|
for (fmt_value = GD_MIN; fmt_value <= GD_MAX; fmt_value++) {
|
|
for (sep_value = GDC_MIN; sep_value <= GDC_MAX; sep_value++) {
|
|
set.logging.log_datetime_format = fmt_value;
|
|
set.logging.log_datetime_separator = sep_value;
|
|
|
|
set_date_format();
|
|
fmt = get_date_format();
|
|
assert_non_null(fmt);
|
|
assert_true(strlen(fmt) > 0);
|
|
}
|
|
}
|
|
}
|
|
|
|
/* --- is_debug_device(): reads the global table stubs.c provides ----------- */
|
|
|
|
static void test_is_debug_device_matches_only_listed_ids(void **state) {
|
|
int table[100];
|
|
int *saved = debug_devices;
|
|
(void) state;
|
|
|
|
memset(table, 0, sizeof table);
|
|
table[0] = 42;
|
|
table[1] = 77;
|
|
debug_devices = table;
|
|
|
|
assert_int_equal(is_debug_device(42), TRUE);
|
|
assert_int_equal(is_debug_device(77), TRUE);
|
|
assert_int_equal(is_debug_device(1), FALSE);
|
|
|
|
debug_devices = saved;
|
|
}
|
|
|
|
/* --- nft_popen(): registry entries have exactly one closing owner -------- */
|
|
|
|
struct close_result {
|
|
int fd;
|
|
int result;
|
|
int error;
|
|
};
|
|
|
|
static void *close_from_thread(void *arg) {
|
|
struct close_result *result = arg;
|
|
|
|
result->result = nft_pclose(result->fd);
|
|
result->error = errno;
|
|
|
|
return NULL;
|
|
}
|
|
|
|
static void test_nft_pclose_has_one_owner_per_registry_entry(void **state) {
|
|
static struct close_result results[2];
|
|
pthread_t threads[2];
|
|
int create_results[2] = {-1, -1};
|
|
int fd;
|
|
int successes = 0;
|
|
int bad_fds = 0;
|
|
int owner_result = -1;
|
|
int join_results[2] = {-1, -1};
|
|
int reap_error;
|
|
int reap_result;
|
|
pid_t child;
|
|
int i;
|
|
(void) state;
|
|
|
|
fd = nft_popen("exit 7", "r");
|
|
assert_true(fd >= 0);
|
|
child = nft_pchild(fd);
|
|
assert_true(child > 0);
|
|
|
|
for (i = 0; i < 2; i++) {
|
|
results[i].fd = fd;
|
|
results[i].result = -1;
|
|
results[i].error = 0;
|
|
create_results[i] = pthread_create(&threads[i], NULL, close_from_thread, &results[i]);
|
|
}
|
|
|
|
for (i = 0; i < 2; i++) {
|
|
if (create_results[i] == 0) {
|
|
join_results[i] = pthread_join(threads[i], NULL);
|
|
}
|
|
|
|
if (results[i].result >= 0) {
|
|
successes++;
|
|
owner_result = results[i].result;
|
|
} else if (results[i].error == EBADF) {
|
|
bad_fds++;
|
|
}
|
|
}
|
|
|
|
/* Avoid leaking the child if thread creation failed before either closer ran. */
|
|
if (successes == 0) {
|
|
(void)nft_pclose(fd);
|
|
}
|
|
|
|
assert_int_equal(create_results[0], 0);
|
|
assert_int_equal(create_results[1], 0);
|
|
assert_int_equal(join_results[0], 0);
|
|
assert_int_equal(join_results[1], 0);
|
|
assert_int_equal(successes, 1);
|
|
assert_int_equal(bad_fds, 1);
|
|
assert_true(WIFEXITED(owner_result));
|
|
assert_int_equal(WEXITSTATUS(owner_result), 7);
|
|
|
|
errno = 0;
|
|
reap_result = waitpid(child, NULL, WNOHANG);
|
|
reap_error = errno;
|
|
assert_int_equal(fcntl(fd, F_GETFD), -1);
|
|
assert_int_equal(errno, EBADF);
|
|
assert_int_equal(reap_result, -1);
|
|
assert_int_equal(reap_error, ECHILD);
|
|
}
|
|
|
|
static void test_nft_pclose_cancellation_releases_registry_entry(void **state) {
|
|
struct close_result result;
|
|
pthread_t thread;
|
|
void *thread_result = NULL;
|
|
pid_t child;
|
|
int cancel_result = -1;
|
|
int create_result;
|
|
int detached = 0;
|
|
int fd;
|
|
int i;
|
|
int join_result = -1;
|
|
int lookup_error;
|
|
int lookup_result;
|
|
char ready;
|
|
int status;
|
|
(void) state;
|
|
|
|
fd = nft_popen("printf x; kill -STOP $$", "r");
|
|
assert_true(fd >= 0);
|
|
child = nft_pchild(fd);
|
|
assert_true(child > 0);
|
|
assert_int_equal(read(fd, &ready, 1), 1);
|
|
assert_int_equal(ready, 'x');
|
|
|
|
result.fd = fd;
|
|
result.result = -1;
|
|
result.error = 0;
|
|
create_result = pthread_create(&thread, NULL, close_from_thread, &result);
|
|
if (create_result == 0) {
|
|
/* The registry transition, rather than elapsed time, proves the closer has
|
|
* taken exclusive ownership and reached waitpid().
|
|
*/
|
|
for (i = 0; i < 5000; i++) {
|
|
errno = 0;
|
|
if (nft_pchild(fd) == -1 && errno == EBADF) {
|
|
detached = 1;
|
|
break;
|
|
}
|
|
usleep(1000);
|
|
}
|
|
|
|
if (detached) {
|
|
cancel_result = pthread_cancel(thread);
|
|
} else {
|
|
kill(child, SIGKILL);
|
|
}
|
|
join_result = pthread_join(thread, &thread_result);
|
|
}
|
|
|
|
errno = 0;
|
|
lookup_result = nft_pchild(fd);
|
|
lookup_error = errno;
|
|
|
|
/* Cancellation stops nft_pclose() before it can reap. Clean up the child
|
|
* before asserting because cmocka assertions longjmp.
|
|
*/
|
|
if (lookup_result > 0) {
|
|
kill(lookup_result, SIGKILL);
|
|
(void)nft_pclose(fd);
|
|
} else if (detached) {
|
|
kill(child, SIGKILL);
|
|
do {
|
|
status = waitpid(child, NULL, 0);
|
|
} while (status < 0 && errno == EINTR);
|
|
}
|
|
|
|
assert_int_equal(create_result, 0);
|
|
assert_true(detached);
|
|
assert_int_equal(cancel_result, 0);
|
|
assert_int_equal(join_result, 0);
|
|
assert_ptr_equal(thread_result, PTHREAD_CANCELED);
|
|
assert_int_equal(lookup_result, -1);
|
|
assert_int_equal(lookup_error, EBADF);
|
|
}
|
|
|
|
static void test_nft_pclose_early_error_preserves_cancellation_mode(void **state) {
|
|
int cancel_state_after;
|
|
int cancel_state_before;
|
|
(void) state;
|
|
|
|
pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, &cancel_state_before);
|
|
pthread_setcancelstate(cancel_state_before, NULL);
|
|
|
|
errno = 0;
|
|
assert_int_equal(nft_pclose(-1), -1);
|
|
assert_int_equal(errno, EBADF);
|
|
errno = 0;
|
|
assert_int_equal(nft_pchild(-1), -1);
|
|
assert_int_equal(errno, EBADF);
|
|
|
|
pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, &cancel_state_after);
|
|
pthread_setcancelstate(cancel_state_after, NULL);
|
|
|
|
assert_int_equal(cancel_state_after, cancel_state_before);
|
|
}
|
|
|
|
/* ---------------------------------------------------------------------------
|
|
* Child process hardening (nft_popen.c)
|
|
*
|
|
* PR #542 removed the close-on-exec and bounded-reap code PR #557 had just
|
|
* added, and nothing failed, because the only guard was a shell script that
|
|
* grepped the source and was deleted in the same commit. These exercise the
|
|
* behaviour against the shipped object instead.
|
|
* ------------------------------------------------------------------------- */
|
|
|
|
static void test_cloexec_is_set_on_both_pipe_ends(void **state) {
|
|
int pdes[2];
|
|
int i;
|
|
|
|
(void) state;
|
|
|
|
assert_true(spine_open_pipe_cloexec(pdes));
|
|
|
|
for (i = 0; i < 2; i++) {
|
|
int flags = fcntl(pdes[i], F_GETFD);
|
|
|
|
assert_true(flags >= 0);
|
|
assert_true((flags & FD_CLOEXEC) != 0);
|
|
}
|
|
|
|
close(pdes[0]);
|
|
close(pdes[1]);
|
|
}
|
|
|
|
static void test_cloexec_pipe_is_a_working_pipe(void **state) {
|
|
int pdes[2];
|
|
char buf[8];
|
|
|
|
(void) state;
|
|
|
|
assert_true(spine_open_pipe_cloexec(pdes));
|
|
assert_int_equal(write(pdes[1], "ok", 2), 2);
|
|
assert_int_equal(read(pdes[0], buf, sizeof(buf)), 2);
|
|
assert_memory_equal(buf, "ok", 2);
|
|
|
|
close(pdes[0]);
|
|
close(pdes[1]);
|
|
}
|
|
|
|
static void test_duplicated_descriptor_is_close_on_exec(void **state) {
|
|
int original;
|
|
int duplicate;
|
|
int flags;
|
|
|
|
(void) state;
|
|
original = open("/dev/null", O_RDONLY);
|
|
assert_true(original >= 0);
|
|
duplicate = spine_dup_cloexec(original);
|
|
assert_true(duplicate >= 0);
|
|
flags = fcntl(duplicate, F_GETFD);
|
|
assert_true(flags >= 0);
|
|
assert_true((flags & FD_CLOEXEC) != 0);
|
|
close(duplicate);
|
|
close(original);
|
|
}
|
|
|
|
static void test_existing_pipe_on_stdout_does_not_close_a_new_child_redirect(void **state) {
|
|
int saved_stdin;
|
|
int saved_stdout;
|
|
int writer;
|
|
int reader;
|
|
int writer_status;
|
|
int wait_status = -1;
|
|
pid_t writer_pid;
|
|
siginfo_t writer_info = {0};
|
|
int reader_status;
|
|
ssize_t bytes;
|
|
char output[32] = {0};
|
|
|
|
(void) state;
|
|
saved_stdin = dup(STDIN_FILENO);
|
|
saved_stdout = dup(STDOUT_FILENO);
|
|
assert_true(saved_stdin >= 0);
|
|
assert_true(saved_stdout >= 0);
|
|
close(STDIN_FILENO);
|
|
close(STDOUT_FILENO);
|
|
|
|
/* The write-mode parent retains fd 1 in PidList. The following read-mode
|
|
* child also redirects its new pipe onto fd 1. Its later PidList close walk
|
|
* must not close that newly installed stdout. */
|
|
writer = nft_popen("exit 0", "w");
|
|
writer_pid = writer >= 0 ? nft_pchild(writer) : -1;
|
|
reader = nft_popen("printf second-child-visible", "r");
|
|
bytes = reader >= 0 ? read(reader, output, sizeof(output) - 1) : -1;
|
|
reader_status = reader >= 0 ? nft_pclose(reader) : -1;
|
|
/* Keep the writer entry and fd 1 registered until after the reader has
|
|
* spawned. Observe its exit without reaping, so close tests ownership
|
|
* rather than the runner scheduling the child within the reap budget. */
|
|
if (writer_pid > 0) {
|
|
do {
|
|
wait_status = waitid(P_PID, (id_t)writer_pid, &writer_info, WEXITED | WNOWAIT);
|
|
} while (wait_status < 0 && errno == EINTR);
|
|
}
|
|
writer_status = writer >= 0 ? nft_pclose(writer) : -1;
|
|
|
|
dup2(saved_stdin, STDIN_FILENO);
|
|
dup2(saved_stdout, STDOUT_FILENO);
|
|
close(saved_stdin);
|
|
close(saved_stdout);
|
|
|
|
assert_true(writer >= 0);
|
|
assert_true(reader >= 0);
|
|
assert_true(bytes > 0);
|
|
assert_string_equal(output, "second-child-visible");
|
|
assert_true(WIFEXITED(reader_status));
|
|
assert_int_equal(WEXITSTATUS(reader_status), 0);
|
|
assert_int_equal(wait_status, 0);
|
|
assert_int_equal(writer_info.si_pid, writer_pid);
|
|
assert_int_equal(writer_info.si_code, CLD_EXITED);
|
|
assert_int_equal(writer_info.si_status, 0);
|
|
assert_true(WIFEXITED(writer_status));
|
|
assert_int_equal(WEXITSTATUS(writer_status), 0);
|
|
}
|
|
|
|
static void test_abandoned_children_are_swept_and_capacity_is_bounded(void **state) {
|
|
pid_t pids[NFT_ABANDONED_MAX + 1];
|
|
int i;
|
|
int created = 0;
|
|
int status;
|
|
|
|
(void) state;
|
|
for (i = 0; i < NFT_ABANDONED_MAX + 1; i++) {
|
|
pids[i] = fork();
|
|
if (pids[i] < 0)
|
|
break;
|
|
if (pids[i] == 0) {
|
|
pause();
|
|
_exit(0);
|
|
}
|
|
created++;
|
|
nft_abandon_child(pids[i], "unit test");
|
|
}
|
|
|
|
if (created != NFT_ABANDONED_MAX + 1) {
|
|
for (i = 0; i < created; i++) {
|
|
(void)kill(pids[i], SIGKILL);
|
|
(void)waitpid(pids[i], &status, 0);
|
|
}
|
|
(void)nft_abandoned_pending();
|
|
skip();
|
|
}
|
|
|
|
assert_int_equal(nft_abandoned_pending(), NFT_ABANDONED_MAX);
|
|
for (i = 0; i < created; i++) {
|
|
assert_int_equal(kill(pids[i], SIGKILL), 0);
|
|
assert_int_equal(waitpid(pids[i], &status, 0), pids[i]);
|
|
}
|
|
assert_int_equal(nft_abandoned_pending(), 0);
|
|
}
|
|
|
|
#ifdef SPINE_TEST_WRAP_WAITPID
|
|
/* waitpid() failing with anything other than ECHILD/EINTR (never true for a
|
|
* real call with WNOHANG) must still kill the child before parking it,
|
|
* rather than leaving a still-running process outside the sweep's reach. */
|
|
static void test_nft_pclose_kills_child_when_waitpid_errors(void **state) {
|
|
int fd;
|
|
int status;
|
|
pid_t child;
|
|
pid_t reaped;
|
|
int real_status;
|
|
int attempts;
|
|
|
|
(void) state;
|
|
fd = nft_popen("printf x; while :; do sleep 1; done", "r");
|
|
assert_true(fd >= 0);
|
|
child = nft_pchild(fd);
|
|
assert_true(child > 0);
|
|
|
|
forced_waitpid_error_pid = child;
|
|
forced_waitpid_error_errno = EINVAL;
|
|
|
|
errno = 0;
|
|
status = nft_pclose(fd);
|
|
|
|
forced_waitpid_error_pid = -1;
|
|
|
|
assert_int_equal(status, -1);
|
|
assert_int_equal(errno, EINVAL);
|
|
|
|
/* Reap for real (bypassing the wrap) to prove the child was killed
|
|
* rather than merely parked. */
|
|
reaped = -1;
|
|
for (attempts = 0; attempts < 100 && reaped != child; attempts++) {
|
|
reaped = __real_waitpid(child, &real_status, WNOHANG);
|
|
if (reaped == 0) {
|
|
usleep(10000);
|
|
reaped = -1;
|
|
}
|
|
}
|
|
assert_int_equal(reaped, child);
|
|
assert_true(WIFSIGNALED(real_status));
|
|
assert_int_equal(WTERMSIG(real_status), SIGKILL);
|
|
|
|
/* nft_abandon_child() still parked it; sweep it back out so it does not
|
|
* leak into a later test's nft_abandoned_pending() count. */
|
|
assert_int_equal(nft_abandoned_pending(), 0);
|
|
}
|
|
#endif
|
|
|
|
/* The descriptor must not survive an exec. A child that inherits the write end
|
|
keeps the pipe open, so the polling thread never sees EOF and blocks to
|
|
script_timeout for a data source that already answered. */
|
|
static void test_pipe_is_not_inherited_across_exec(void **state) {
|
|
int pdes[2];
|
|
int status;
|
|
pid_t pid;
|
|
char fdarg[32];
|
|
|
|
(void) state;
|
|
|
|
assert_true(spine_open_pipe_cloexec(pdes));
|
|
snprintf(fdarg, sizeof(fdarg), "/proc/self/fd/%d", pdes[1]);
|
|
|
|
pid = fork();
|
|
assert_true(pid >= 0);
|
|
|
|
if (pid == 0) {
|
|
/* exits 0 when the descriptor survived exec, 1 when it did not */
|
|
execl("/bin/sh", "sh", "-c", "test -e \"$0\"", fdarg, (char *) NULL);
|
|
_exit(127);
|
|
}
|
|
|
|
assert_int_equal(waitpid(pid, &status, 0), pid);
|
|
assert_true(WIFEXITED(status));
|
|
assert_int_equal(WEXITSTATUS(status), 1);
|
|
|
|
close(pdes[0]);
|
|
close(pdes[1]);
|
|
}
|
|
|
|
static void test_reap_returns_still_running_rather_than_blocking(void **state) {
|
|
int pstat = 0;
|
|
int status;
|
|
pid_t pid;
|
|
|
|
(void) state;
|
|
|
|
pid = fork();
|
|
assert_true(pid >= 0);
|
|
|
|
if (pid == 0) {
|
|
pause();
|
|
_exit(0);
|
|
}
|
|
|
|
/* the shipped code blocked here forever; two attempts must come back */
|
|
assert_int_equal(spine_reap_child_bounded(pid, &pstat, 2), 1);
|
|
|
|
assert_int_equal(kill(pid, SIGKILL), 0);
|
|
assert_int_equal(waitpid(pid, &status, 0), pid);
|
|
}
|
|
|
|
static void test_reap_collects_an_exited_child(void **state) {
|
|
int pstat = 0;
|
|
pid_t pid;
|
|
|
|
(void) state;
|
|
|
|
pid = fork();
|
|
assert_true(pid >= 0);
|
|
|
|
if (pid == 0) {
|
|
_exit(3);
|
|
}
|
|
|
|
assert_int_equal(spine_reap_child_bounded(pid, &pstat, 20), 0);
|
|
assert_true(WIFEXITED(pstat));
|
|
assert_int_equal(WEXITSTATUS(pstat), 3);
|
|
}
|
|
|
|
static void test_reap_reports_an_already_reaped_child(void **state) {
|
|
int pstat = 99;
|
|
int status;
|
|
pid_t pid;
|
|
|
|
(void) state;
|
|
|
|
pid = fork();
|
|
assert_true(pid >= 0);
|
|
|
|
if (pid == 0) {
|
|
_exit(0);
|
|
}
|
|
|
|
assert_int_equal(waitpid(pid, &status, 0), pid);
|
|
|
|
/* ECHILD: someone else took the status, which is success with none */
|
|
assert_int_equal(spine_reap_child_bounded(pid, &pstat, 2), 0);
|
|
assert_int_equal(pstat, 0);
|
|
}
|
|
|
|
static void test_nft_pclose_does_not_block_on_a_lingering_child(void **state) {
|
|
char ready[6] = {0};
|
|
int fd;
|
|
double start;
|
|
int attempts;
|
|
|
|
(void) state;
|
|
fd = nft_popen("printf ready; while :; do sleep 1; done", "r");
|
|
assert_true(fd >= 0);
|
|
assert_int_equal(read(fd, ready, 5), 5);
|
|
assert_string_equal(ready, "ready");
|
|
|
|
start = get_time_as_double();
|
|
assert_int_equal(nft_pclose(fd), -1);
|
|
|
|
/* The removed escalating spin/sleep/SIGTERM/SIGKILL sequence took up to
|
|
* ~5 seconds here. nft_pclose() must kill the child and hand it to the
|
|
* abandoned-pid sweep instead of blocking waiting for it to exit. */
|
|
assert_true(get_time_as_double() - start < 0.5);
|
|
|
|
for (attempts = 0; attempts < 50; attempts++) {
|
|
if (nft_abandoned_pending() == 0) {
|
|
break;
|
|
}
|
|
usleep(20000);
|
|
}
|
|
assert_int_equal(nft_abandoned_pending(), 0);
|
|
}
|
|
|
|
int main(void) {
|
|
|
|
const struct CMUnitTest tests[] = {
|
|
cmocka_unit_test(test_strncopy_truncates_within_the_buffer),
|
|
cmocka_unit_test(test_strncopy_copies_a_short_source_whole),
|
|
cmocka_unit_test(test_strncopy_handles_a_zero_size),
|
|
cmocka_unit_test(test_strncopy_terminates_an_exact_fit),
|
|
cmocka_unit_test(test_regex_replace_returns_the_match),
|
|
cmocka_unit_test(test_regex_replace_passes_through_on_no_match),
|
|
cmocka_unit_test(test_regex_replace_passes_through_on_bad_pattern),
|
|
cmocka_unit_test(test_bounded_formatters),
|
|
cmocka_unit_test(test_spine_appendf_reports_truncation_and_guards),
|
|
cmocka_unit_test(test_all_digits),
|
|
cmocka_unit_test(test_is_ipaddress),
|
|
cmocka_unit_test(test_trim_family),
|
|
cmocka_unit_test(test_reverse),
|
|
cmocka_unit_test(test_strpos),
|
|
cmocka_unit_test(test_char_count),
|
|
cmocka_unit_test(test_strip_alpha),
|
|
cmocka_unit_test(test_add_slashes_doubles_a_backslash),
|
|
cmocka_unit_test(test_add_slashes_passes_plain_text_through),
|
|
cmocka_unit_test(test_row_alias_upsert_version_gate),
|
|
cmocka_unit_test(test_file_exists),
|
|
cmocka_unit_test(test_get_time_as_double_advances),
|
|
cmocka_unit_test(test_get_checksum_is_stable),
|
|
cmocka_unit_test(test_icmp_classify_accepts_our_reply),
|
|
cmocka_unit_test(test_icmp_classify_rejects_a_runt),
|
|
cmocka_unit_test(test_icmp_classify_rejects_a_bad_ihl),
|
|
cmocka_unit_test(test_icmp_classify_rejects_a_non_echo),
|
|
cmocka_unit_test(test_icmp_classify_rejects_another_hosts_reply),
|
|
cmocka_unit_test(test_icmp_classify_rejects_a_null_buffer),
|
|
cmocka_unit_test(test_namebyhost_plain_hostname),
|
|
cmocka_unit_test(test_namebyhost_is_reentrant_across_calls),
|
|
cmocka_unit_test(test_namebyhost_tcpv6_method_keeps_the_full_hostname),
|
|
cmocka_unit_test(test_config_defaults_populates_the_set),
|
|
cmocka_unit_test(test_read_spine_config_rejects_a_missing_file),
|
|
cmocka_unit_test(test_read_spine_config_reads_settings),
|
|
cmocka_unit_test(test_get_date_format_returns_cached_storage),
|
|
cmocka_unit_test(test_set_date_format_clamps_an_out_of_range_format),
|
|
cmocka_unit_test(test_get_date_format_covers_each_supported_format),
|
|
cmocka_unit_test(test_is_debug_device_matches_only_listed_ids),
|
|
cmocka_unit_test(test_nft_pclose_has_one_owner_per_registry_entry),
|
|
cmocka_unit_test(test_nft_pclose_cancellation_releases_registry_entry),
|
|
cmocka_unit_test(test_nft_pclose_early_error_preserves_cancellation_mode),
|
|
cmocka_unit_test(test_cloexec_is_set_on_both_pipe_ends),
|
|
cmocka_unit_test(test_cloexec_pipe_is_a_working_pipe),
|
|
cmocka_unit_test(test_duplicated_descriptor_is_close_on_exec),
|
|
cmocka_unit_test(test_existing_pipe_on_stdout_does_not_close_a_new_child_redirect),
|
|
cmocka_unit_test(test_pipe_is_not_inherited_across_exec),
|
|
cmocka_unit_test(test_reap_returns_still_running_rather_than_blocking),
|
|
cmocka_unit_test(test_reap_collects_an_exited_child),
|
|
cmocka_unit_test(test_reap_reports_an_already_reaped_child),
|
|
cmocka_unit_test(test_nft_pclose_does_not_block_on_a_lingering_child),
|
|
cmocka_unit_test(test_abandoned_children_are_swept_and_capacity_is_bounded),
|
|
#ifdef SPINE_TEST_WRAP_WAITPID
|
|
cmocka_unit_test(test_nft_pclose_kills_child_when_waitpid_errors),
|
|
#endif
|
|
};
|
|
|
|
return cmocka_run_group_tests(tests, NULL, NULL);
|
|
}
|