mirror of
https://github.com/Cacti/spine.git
synced 2026-10-23 03:27:10 +09:00
fix(build): resolve compilation failures on develop (#514)
## Summary Fixes #515. Spine on `develop` fails to compile after recent feature branch merges (everything after `4837ddd`). This PR fixes 6 build/runtime bugs, replaces deprecated `vfork`, and adds 68 automated tests. ## Fixes ### Build-breaking (prevented compilation) 1. **Missing uthash.h** - vendored v2.3.0 (header-only, BSD license). The O(1) hash table feature added `#include "uthash.h"` but never committed the file. 2. **Extra closing braces in poller.c** - duplicate `}` in POLLER_ACTION_SCRIPT and POLLER_ACTION_PHP_SCRIPT_SERVER cases broke the switch statement. ### Runtime bugs 3. **SNMP_FREE macro collision** - spine's `SNMP_FREE` (calls `snmp_host_cleanup`) collided with net-snmp's `SNMP_FREE` (calls `free`). Renamed to `SNMP_SESSION_FREE`. The `errstr` from `snmp_sess_error()` now correctly uses net-snmp's `free()` instead of `snmp_host_cleanup()`. 4. **output_regex column detection** - the output_regex feature (#210) assumed the column exists, but baseline Cacti does not have it. Added `db_column_exists()` check at startup to conditionally include it in queries. ### Deprecation warnings 5. **vfork in nft_popen.c** - replaced with `posix_spawn` using file actions, matching the approach already used in `php.c`. Eliminates macOS deprecation warning. ### Test fixture 6. **snmp_auth_protocol column width** - `char(6)` too narrow for `SHA-256` (7 chars). Fixed to `char(7)` in both host and poller_item tables. Added `rocommunity public` to snmpd.conf for SNMPv2c test coverage. ## Test Results (68/68 pass) | Suite | Count | Coverage | |-------|-------|----------| | Unit (cmocka) | 16 | uthash macros, config init, regex_replace, strncopy | | Smoke | 21 | SNMPv3 + SNMPv2c polling, DB writes, multi-device, runtime validation | | Output regex | 15 | Column absent, present, regex filtering, removal fallback | | DB column detect | 16 | 3 scenarios: absent, added via ALTER TABLE, dropped | ## Files changed (15) | File | Change | |------|--------| | `uthash.h` | New: vendored header | | `Makefile.am` | Add uthash.h to EXTRA_DIST | | `common.h` | Add uthash.h include | | `poller.c` | Fix braces, conditional output_regex in queries | | `spine.c` | Add has_output_regex detection at startup | | `spine.h` | Add has_output_regex to config struct | | `snmp.h` | Add SNMP_SESSION_FREE macro | | `sql.c` | Fix indentation in db_query(), restore missing continue | | `nft_popen.c` | Replace vfork+execve with posix_spawn | | `tests/snmpv3/db/init.sql` | Fix snmp_auth_protocol char(7) | | `tests/snmpv3/snmpd/snmpd.conf` | Add rocommunity public | | `tests/integration/smoke_test.sh` | New: e2e smoke test | | `tests/integration/test_output_regex.sh` | New: output_regex scenarios | | `tests/integration/test_db_column_detect.sh` | New: db_column_exists scenarios | | `tests/unit/test_build_fixes.c` + `Makefile` | New: cmocka unit tests | ## Follow-up PRs - #517: Replace deprecated POSIX semaphores with pthread wrapper (9 remaining warnings) - SNMP error handling, spike kill, script server path testing Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
This commit is contained in:
1 parent
0dd91c4602
commit
70459ac82c
15 files changed
+2483
-105
No files matched your search
+1
-1
@@ -31,7 +31,7 @@ bin_PROGRAMS = spine
|
||||
|
||||
man_MANS = spine.1
|
||||
|
||||
EXTRA_DIST = spine.1
|
||||
EXTRA_DIST = spine.1 uthash.h
|
||||
|
||||
# Docker targets — require Dockerfile and Dockerfile.dev (from PR #401)
|
||||
.PHONY: docker docker-dev verify cppcheck
|
||||
|
||||
@@ -159,4 +159,6 @@
|
||||
# include <grp.h>
|
||||
#endif
|
||||
|
||||
#include "uthash.h"
|
||||
|
||||
#endif /* SPINE_COMMON_H */
|
||||
+59
-77
@@ -86,6 +86,7 @@
|
||||
|
||||
#include "common.h"
|
||||
#include "spine.h"
|
||||
#include <spawn.h>
|
||||
|
||||
/* An instance of this struct is created for each popen() fd. */
|
||||
static struct pid
|
||||
@@ -126,7 +127,8 @@ int nft_popen(const char * command, const char * type) {
|
||||
struct pid *cur;
|
||||
struct pid *p;
|
||||
int pdes[2];
|
||||
int fd, pid, twoway;
|
||||
int fd, twoway;
|
||||
pid_t pid;
|
||||
char *argv[4];
|
||||
char *command_copy;
|
||||
char shell_cmd[] = "sh";
|
||||
@@ -180,90 +182,70 @@ int nft_popen(const char * command, const char * type) {
|
||||
*/
|
||||
pthread_mutex_lock(&ListMutex);
|
||||
|
||||
/* Fork. */
|
||||
retry:
|
||||
switch (pid = vfork()) {
|
||||
case -1: /* Error. */
|
||||
switch (errno) {
|
||||
case EAGAIN:
|
||||
if (retry_count < 3) {
|
||||
retry_count++;
|
||||
#ifndef SOLAR_THREAD
|
||||
/* take a moment */
|
||||
usleep(50000);
|
||||
#endif
|
||||
goto retry;
|
||||
}else{
|
||||
SPINE_LOG(("ERROR: SCRIPT: Could not fork. Out of Resources nft_popen.c"));
|
||||
}
|
||||
case ENOMEM:
|
||||
if (retry_count < 3) {
|
||||
retry_count++;
|
||||
#ifndef SOLAR_THREAD
|
||||
/* take a moment */
|
||||
usleep(50000);
|
||||
#endif
|
||||
goto retry;
|
||||
}else{
|
||||
SPINE_LOG(("ERROR: SCRIPT Could not fork. Out of Memory nft_popen.c"));
|
||||
}
|
||||
default:
|
||||
SPINE_LOG(("ERROR: SCRIPT Could not fork. Unknown Reason nft_popen.c"));
|
||||
}
|
||||
|
||||
/* Build file actions for posix_spawn to replace vfork+execve. */
|
||||
posix_spawn_file_actions_t fa;
|
||||
if (posix_spawn_file_actions_init(&fa) != 0) {
|
||||
SPINE_LOG(("ERROR: SCRIPT: posix_spawn_file_actions_init failed"));
|
||||
(void)close(pdes[0]);
|
||||
(void)close(pdes[1]);
|
||||
pthread_mutex_unlock(&ListMutex);
|
||||
free(command_copy);
|
||||
pthread_setcancelstate(cancel_state, NULL);
|
||||
|
||||
return -1;
|
||||
/* NOTREACHED */
|
||||
case 0: /* Child. */
|
||||
if (*type == 'r') {
|
||||
/* The dup2() to STDIN_FILENO is repeated to avoid
|
||||
* writing to pdes[1], which might corrupt the
|
||||
* parent's copy. This isn't good enough in
|
||||
* general, since the _exit() is no return, so
|
||||
* the compiler is free to corrupt all the local
|
||||
* variables.
|
||||
*/
|
||||
(void)close(pdes[0]);
|
||||
if (pdes[1] != STDOUT_FILENO) {
|
||||
(void)dup2(pdes[1], STDOUT_FILENO);
|
||||
(void)close(pdes[1]);
|
||||
if (twoway)
|
||||
(void)dup2(STDOUT_FILENO, STDIN_FILENO);
|
||||
}else if (twoway && (pdes[1] != STDIN_FILENO))
|
||||
(void)dup2(pdes[1], STDIN_FILENO);
|
||||
}else {
|
||||
if (pdes[0] != STDIN_FILENO) {
|
||||
(void)dup2(pdes[0], STDIN_FILENO);
|
||||
(void)close(pdes[0]);
|
||||
}
|
||||
(void)close(pdes[1]);
|
||||
}
|
||||
|
||||
/* Close all the other pipes in the child process.
|
||||
* Posix.2 requires this, tho I don't know why.
|
||||
*/
|
||||
for (p = PidList; p; p = p->next)
|
||||
(void)close(p->fd);
|
||||
|
||||
/* Execute the command. */
|
||||
#if defined(__CYGWIN__)
|
||||
if (set.cygwinshloc == 0) {
|
||||
execve("sh.exe", argv, environ);
|
||||
}else{
|
||||
execve("/bin/sh", argv, environ);
|
||||
}
|
||||
#else
|
||||
execve("/bin/sh", argv, environ);
|
||||
#endif
|
||||
_exit(127);
|
||||
/* NOTREACHED */
|
||||
}
|
||||
|
||||
if (*type == 'r') {
|
||||
posix_spawn_file_actions_addclose(&fa, pdes[0]);
|
||||
if (pdes[1] != STDOUT_FILENO) {
|
||||
posix_spawn_file_actions_adddup2(&fa, pdes[1], STDOUT_FILENO);
|
||||
posix_spawn_file_actions_addclose(&fa, pdes[1]);
|
||||
if (twoway)
|
||||
posix_spawn_file_actions_adddup2(&fa, STDOUT_FILENO, STDIN_FILENO);
|
||||
} else if (twoway && (pdes[1] != STDIN_FILENO)) {
|
||||
posix_spawn_file_actions_adddup2(&fa, pdes[1], STDIN_FILENO);
|
||||
}
|
||||
} else {
|
||||
if (pdes[0] != STDIN_FILENO) {
|
||||
posix_spawn_file_actions_adddup2(&fa, pdes[0], STDIN_FILENO);
|
||||
posix_spawn_file_actions_addclose(&fa, pdes[0]);
|
||||
}
|
||||
posix_spawn_file_actions_addclose(&fa, pdes[1]);
|
||||
}
|
||||
|
||||
/* Close all other pipes in the child (Posix.2 requirement). */
|
||||
for (p = PidList; p; p = p->next)
|
||||
posix_spawn_file_actions_addclose(&fa, p->fd);
|
||||
|
||||
/* Spawn the child process with retry on EAGAIN/ENOMEM. */
|
||||
#if defined(__CYGWIN__)
|
||||
const char *spawn_shell = (set.cygwinshloc == 0) ? "sh.exe" : "/bin/sh";
|
||||
#else
|
||||
const char *spawn_shell = "/bin/sh";
|
||||
#endif
|
||||
|
||||
int spawn_err;
|
||||
retry:
|
||||
spawn_err = posix_spawn(&pid, spawn_shell, &fa, NULL, argv, environ);
|
||||
|
||||
if (spawn_err != 0) {
|
||||
if ((spawn_err == EAGAIN || spawn_err == ENOMEM) && retry_count < 3) {
|
||||
retry_count++;
|
||||
usleep(50000);
|
||||
goto retry;
|
||||
}
|
||||
|
||||
SPINE_LOG(("ERROR: SCRIPT: posix_spawn failed: %s", strerror(spawn_err)));
|
||||
posix_spawn_file_actions_destroy(&fa);
|
||||
(void)close(pdes[0]);
|
||||
(void)close(pdes[1]);
|
||||
pthread_mutex_unlock(&ListMutex);
|
||||
free(command_copy);
|
||||
pthread_setcancelstate(cancel_state, NULL);
|
||||
return -1;
|
||||
}
|
||||
|
||||
posix_spawn_file_actions_destroy(&fa);
|
||||
|
||||
/* Parent. */
|
||||
if (*type == 'r') {
|
||||
fd = pdes[0];
|
||||
|
||||
@@ -267,6 +267,9 @@ void poll_host(int device_counter, int host_id, int host_thread, int host_thread
|
||||
limits[0] = '\0';
|
||||
}
|
||||
|
||||
/* optional output_regex column (added in Cacti 1.3.1) */
|
||||
const char *regex_col = set.has_output_regex ? ", output_regex" : "";
|
||||
|
||||
/* single polling interval query for items */
|
||||
if (set.poller_id == 0) {
|
||||
if (set.total_snmp_ports == 1) {
|
||||
@@ -275,21 +278,23 @@ void poll_host(int device_counter, int host_id, int host_thread, int host_thread
|
||||
"snmp_version, snmp_username, snmp_password, "
|
||||
"rrd_name, rrd_path, arg1, arg2, arg3, local_data_id, "
|
||||
"rrd_num, snmp_port, snmp_timeout, "
|
||||
"snmp_auth_protocol, snmp_priv_passphrase, snmp_priv_protocol, snmp_context, snmp_engine_id "
|
||||
"snmp_auth_protocol, snmp_priv_passphrase, snmp_priv_protocol, snmp_context, snmp_engine_id"
|
||||
"%s"
|
||||
" FROM poller_item"
|
||||
" WHERE host_id = %i"
|
||||
" AND deleted = '' %s", host_id, limits);
|
||||
" AND deleted = '' %s", regex_col, host_id, limits);
|
||||
} else {
|
||||
snprintf(query1, BUFSIZE,
|
||||
"SELECT SQL_NO_CACHE action, hostname, snmp_community, "
|
||||
"snmp_version, snmp_username, snmp_password, "
|
||||
"rrd_name, rrd_path, arg1, arg2, arg3, local_data_id, "
|
||||
"rrd_num, snmp_port, snmp_timeout, "
|
||||
"snmp_auth_protocol, snmp_priv_passphrase, snmp_priv_protocol, snmp_context, snmp_engine_id "
|
||||
"snmp_auth_protocol, snmp_priv_passphrase, snmp_priv_protocol, snmp_context, snmp_engine_id"
|
||||
"%s"
|
||||
" FROM poller_item"
|
||||
" WHERE host_id = %i"
|
||||
" AND deleted = ''"
|
||||
" ORDER BY snmp_port %s", host_id, limits);
|
||||
" ORDER BY snmp_port %s", regex_col, host_id, limits);
|
||||
}
|
||||
|
||||
/* host structure for uptime checks */
|
||||
@@ -321,22 +326,24 @@ void poll_host(int device_counter, int host_id, int host_thread, int host_thread
|
||||
"snmp_version, snmp_username, snmp_password, "
|
||||
"rrd_name, rrd_path, arg1, arg2, arg3, local_data_id, "
|
||||
"rrd_num, snmp_port, snmp_timeout, "
|
||||
"snmp_auth_protocol, snmp_priv_passphrase, snmp_priv_protocol, snmp_context, snmp_engine_id "
|
||||
"snmp_auth_protocol, snmp_priv_passphrase, snmp_priv_protocol, snmp_context, snmp_engine_id"
|
||||
"%s"
|
||||
" FROM poller_item"
|
||||
" WHERE host_id = %i"
|
||||
" AND rrd_next_step <= 0"
|
||||
" %s", host_id, limits);
|
||||
" %s", regex_col, host_id, limits);
|
||||
} else {
|
||||
snprintf(query5, BUFSIZE,
|
||||
"SELECT SQL_NO_CACHE action, hostname, snmp_community, "
|
||||
"snmp_version, snmp_username, snmp_password, "
|
||||
"rrd_name, rrd_path, arg1, arg2, arg3, local_data_id, "
|
||||
"rrd_num, snmp_port, snmp_timeout, "
|
||||
"snmp_auth_protocol, snmp_priv_passphrase, snmp_priv_protocol, snmp_context, snmp_engine_id "
|
||||
"snmp_auth_protocol, snmp_priv_passphrase, snmp_priv_protocol, snmp_context, snmp_engine_id"
|
||||
"%s"
|
||||
" FROM poller_item"
|
||||
" WHERE host_id = %i"
|
||||
" AND rrd_next_step <= 0"
|
||||
" ORDER BY snmp_port %s", host_id, limits);
|
||||
" ORDER BY snmp_port %s", regex_col, host_id, limits);
|
||||
}
|
||||
} else {
|
||||
if (set.total_snmp_ports == 1) {
|
||||
@@ -345,20 +352,22 @@ void poll_host(int device_counter, int host_id, int host_thread, int host_thread
|
||||
"snmp_version, snmp_username, snmp_password, "
|
||||
"rrd_name, rrd_path, arg1, arg2, arg3, local_data_id, "
|
||||
"rrd_num, snmp_port, snmp_timeout, "
|
||||
"snmp_auth_protocol, snmp_priv_passphrase, snmp_priv_protocol, snmp_context, snmp_engine_id "
|
||||
"snmp_auth_protocol, snmp_priv_passphrase, snmp_priv_protocol, snmp_context, snmp_engine_id"
|
||||
"%s"
|
||||
" FROM poller_item"
|
||||
" WHERE host_id = %i"
|
||||
" %s", host_id, limits);
|
||||
" %s", regex_col, host_id, limits);
|
||||
} else {
|
||||
snprintf(query5, BUFSIZE,
|
||||
"SELECT SQL_NO_CACHE action, hostname, snmp_community, "
|
||||
"snmp_version, snmp_username, snmp_password, "
|
||||
"rrd_name, rrd_path, arg1, arg2, arg3, local_data_id, "
|
||||
"rrd_num, snmp_port, snmp_timeout, "
|
||||
"snmp_auth_protocol, snmp_priv_passphrase, snmp_priv_protocol, snmp_context, snmp_engine_id "
|
||||
"snmp_auth_protocol, snmp_priv_passphrase, snmp_priv_protocol, snmp_context, snmp_engine_id"
|
||||
"%s"
|
||||
" FROM poller_item"
|
||||
" WHERE host_id = %i"
|
||||
" ORDER BY snmp_port %s", host_id, limits);
|
||||
" ORDER BY snmp_port %s", regex_col, host_id, limits);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -411,21 +420,23 @@ void poll_host(int device_counter, int host_id, int host_thread, int host_thread
|
||||
"snmp_version, snmp_username, snmp_password, "
|
||||
"rrd_name, rrd_path, arg1, arg2, arg3, local_data_id, "
|
||||
"rrd_num, snmp_port, snmp_timeout, "
|
||||
"snmp_auth_protocol, snmp_priv_passphrase, snmp_priv_protocol, snmp_context, snmp_engine_id "
|
||||
"snmp_auth_protocol, snmp_priv_passphrase, snmp_priv_protocol, snmp_context, snmp_engine_id"
|
||||
"%s"
|
||||
" FROM poller_item"
|
||||
" WHERE host_id = %i"
|
||||
" AND poller_id=%i %s", host_id, set.poller_id, limits);
|
||||
" AND poller_id=%i %s", regex_col, host_id, set.poller_id, limits);
|
||||
} else {
|
||||
snprintf(query1, BUFSIZE,
|
||||
"SELECT SQL_NO_CACHE action, hostname, snmp_community, "
|
||||
"snmp_version, snmp_username, snmp_password, "
|
||||
"rrd_name, rrd_path, arg1, arg2, arg3, local_data_id, "
|
||||
"rrd_num, snmp_port, snmp_timeout, "
|
||||
"snmp_auth_protocol, snmp_priv_passphrase, snmp_priv_protocol, snmp_context, snmp_engine_id "
|
||||
"snmp_auth_protocol, snmp_priv_passphrase, snmp_priv_protocol, snmp_context, snmp_engine_id"
|
||||
"%s"
|
||||
" FROM poller_item"
|
||||
" WHERE host_id = %i"
|
||||
" AND poller_id=%i"
|
||||
" ORDER BY snmp_port %s", host_id, set.poller_id, limits);
|
||||
" ORDER BY snmp_port %s", regex_col, host_id, set.poller_id, limits);
|
||||
}
|
||||
|
||||
/* host structure for uptime checks */
|
||||
@@ -457,23 +468,25 @@ void poll_host(int device_counter, int host_id, int host_thread, int host_thread
|
||||
"snmp_version, snmp_username, snmp_password, "
|
||||
"rrd_name, rrd_path, arg1, arg2, arg3, local_data_id, "
|
||||
"rrd_num, snmp_port, snmp_timeout, "
|
||||
"snmp_auth_protocol, snmp_priv_passphrase, snmp_priv_protocol, snmp_context, snmp_engine_id "
|
||||
"snmp_auth_protocol, snmp_priv_passphrase, snmp_priv_protocol, snmp_context, snmp_engine_id"
|
||||
"%s"
|
||||
" FROM poller_item"
|
||||
" WHERE host_id = %i"
|
||||
" AND rrd_next_step <= 0"
|
||||
" AND poller_id = %i %s", host_id, set.poller_id, limits);
|
||||
" AND poller_id = %i %s", regex_col, host_id, set.poller_id, limits);
|
||||
} else {
|
||||
snprintf(query5, BUFSIZE,
|
||||
"SELECT SQL_NO_CACHE action, hostname, snmp_community, "
|
||||
"snmp_version, snmp_username, snmp_password, "
|
||||
"rrd_name, rrd_path, arg1, arg2, arg3, local_data_id, "
|
||||
"rrd_num, snmp_port, snmp_timeout, "
|
||||
"snmp_auth_protocol, snmp_priv_passphrase, snmp_priv_protocol, snmp_context, snmp_engine_id "
|
||||
"snmp_auth_protocol, snmp_priv_passphrase, snmp_priv_protocol, snmp_context, snmp_engine_id"
|
||||
"%s"
|
||||
" FROM poller_item"
|
||||
" WHERE host_id = %i"
|
||||
" AND rrd_next_step <= 0"
|
||||
" AND poller_id = %i"
|
||||
" ORDER BY snmp_port %s", host_id, set.poller_id, limits);
|
||||
" ORDER BY snmp_port %s", regex_col, host_id, set.poller_id, limits);
|
||||
}
|
||||
} else {
|
||||
if (set.total_snmp_ports == 1) {
|
||||
@@ -482,21 +495,23 @@ void poll_host(int device_counter, int host_id, int host_thread, int host_thread
|
||||
"snmp_version, snmp_username, snmp_password, "
|
||||
"rrd_name, rrd_path, arg1, arg2, arg3, local_data_id, "
|
||||
"rrd_num, snmp_port, snmp_timeout, "
|
||||
"snmp_auth_protocol, snmp_priv_passphrase, snmp_priv_protocol, snmp_context, snmp_engine_id "
|
||||
"snmp_auth_protocol, snmp_priv_passphrase, snmp_priv_protocol, snmp_context, snmp_engine_id"
|
||||
"%s"
|
||||
" FROM poller_item"
|
||||
" WHERE host_id = %i"
|
||||
" AND poller_id = %i %s", host_id, set.poller_id, limits);
|
||||
" AND poller_id = %i %s", regex_col, host_id, set.poller_id, limits);
|
||||
} else {
|
||||
snprintf(query5, BUFSIZE,
|
||||
"SELECT SQL_NO_CACHE action, hostname, snmp_community, "
|
||||
"snmp_version, snmp_username, snmp_password, "
|
||||
"rrd_name, rrd_path, arg1, arg2, arg3, local_data_id, "
|
||||
"rrd_num, snmp_port, snmp_timeout, "
|
||||
"snmp_auth_protocol, snmp_priv_passphrase, snmp_priv_protocol, snmp_context, snmp_engine_id "
|
||||
"snmp_auth_protocol, snmp_priv_passphrase, snmp_priv_protocol, snmp_context, snmp_engine_id"
|
||||
"%s"
|
||||
" FROM poller_item"
|
||||
" WHERE host_id = %i"
|
||||
" AND poller_id = %i"
|
||||
" ORDER BY snmp_port %s", host_id, set.poller_id, limits);
|
||||
" ORDER BY snmp_port %s", regex_col, host_id, set.poller_id, limits);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1280,6 +1295,7 @@ void poll_host(int device_counter, int host_id, int host_thread, int host_thread
|
||||
poller_items[i].arg3[0] = '\0';
|
||||
poller_items[i].local_data_id = 0;
|
||||
poller_items[i].rrd_num = 0;
|
||||
poller_items[i].output_regex[0] = '\0';
|
||||
|
||||
if (row[0] != NULL) poller_items[i].action = atoi(row[0]);
|
||||
|
||||
@@ -1314,6 +1330,10 @@ void poll_host(int device_counter, int host_id, int host_thread, int host_thread
|
||||
if (row[19] != NULL) snprintf(poller_items[i].snmp_engine_id,
|
||||
sizeof(poller_items[i].snmp_engine_id), "%s", row[19]);
|
||||
|
||||
if (set.has_output_regex && row[20] != NULL)
|
||||
snprintf(poller_items[i].output_regex,
|
||||
sizeof(poller_items[i].output_regex), "%s", row[20]);
|
||||
|
||||
SET_UNDEFINED(poller_items[i].result);
|
||||
|
||||
i++;
|
||||
@@ -1544,6 +1564,11 @@ void poll_host(int device_counter, int host_id, int host_thread, int host_thread
|
||||
}
|
||||
}
|
||||
|
||||
if (strlen(poller_items[snmp_oids[j].array_position].output_regex)) {
|
||||
snprintf(temp_result, RESULTS_BUFFER, "%s", regex_replace(poller_items[snmp_oids[j].array_position].output_regex, snmp_oids[j].result));
|
||||
snprintf(snmp_oids[j].result, RESULTS_BUFFER, "%s", temp_result);
|
||||
}
|
||||
|
||||
snprintf(poller_items[snmp_oids[j].array_position].result, RESULTS_BUFFER, "%s", snmp_oids[j].result);
|
||||
|
||||
thread_end = get_time_as_double();
|
||||
|
||||
@@ -31,6 +31,8 @@
|
||||
+-------------------------------------------------------------------------+
|
||||
*/
|
||||
|
||||
#define SNMP_SESSION_FREE(s) { if (s != NULL) { snmp_host_cleanup(s); s = NULL; } }
|
||||
|
||||
extern void snmp_spine_init(void);
|
||||
extern void snmp_spine_close(void);
|
||||
extern void *snmp_host_init(int host_id, char *hostname, int snmp_version, char *snmp_community, char *snmp_username, char *snmp_password, char *snmp_auth_protocol, char *snmp_priv_passphrase, char *snmp_priv_protocol, char *snmp_context, char *snmp_engine_id, int snmp_port, int snmp_timeout);
|
||||
|
||||
@@ -327,6 +327,7 @@ int main(int argc, char *argv[]) {
|
||||
set.parent_fork = SPINE_PARENT;
|
||||
set.mode = REMOTE_ONLINE;
|
||||
set.has_device_0 = FALSE;
|
||||
set.has_output_regex = FALSE;
|
||||
|
||||
for (argv++; *argv; argv++) {
|
||||
char *arg = *argv;
|
||||
@@ -562,6 +563,12 @@ int main(int argc, char *argv[]) {
|
||||
}
|
||||
db_free_result(result);
|
||||
|
||||
/* check if poller_item has the output_regex column (added in Cacti 1.3.1) */
|
||||
if (db_column_exists(&mysql, LOCAL, "poller_item", "output_regex")) {
|
||||
set.has_output_regex = TRUE;
|
||||
SPINE_LOG_DEBUG(("DEBUG: poller_item.output_regex column detected"));
|
||||
}
|
||||
|
||||
/* Since MySQL 5.7 the sql_mode defaults are too strict for cacti */
|
||||
db_insert(&mysql, LOCAL, "SET SESSION sql_mode = (SELECT REPLACE(@@sql_mode,'NO_ZERO_DATE', ''))");
|
||||
db_insert(&mysql, LOCAL, "SET SESSION sql_mode = (SELECT REPLACE(@@sql_mode,'ONLY_FULL_GROUP_BY', ''))");
|
||||
@@ -1074,7 +1081,6 @@ int main(int argc, char *argv[]) {
|
||||
SPINE_FREE(threads);
|
||||
SPINE_FREE(ids);
|
||||
SPINE_FREE(conf_file);
|
||||
SPINE_FREE(debug_devices);
|
||||
SPINE_FREE(host_time);
|
||||
SPINE_FREE(php_processes);
|
||||
|
||||
|
||||
@@ -366,6 +366,7 @@ typedef struct config_struct {
|
||||
int end_host_id;
|
||||
char host_id_list[BIG_BUFSIZE];
|
||||
int has_device_0;
|
||||
int has_output_regex;
|
||||
/* database connection information */
|
||||
char db_host[BUFSIZE];
|
||||
char db_db[BUFSIZE];
|
||||
@@ -469,6 +470,7 @@ typedef struct target_struct {
|
||||
char arg1[1024];
|
||||
char arg2[255];
|
||||
char arg3[255];
|
||||
char output_regex[255];
|
||||
} target_t;
|
||||
|
||||
/*! SNMP OID's Structure
|
||||
|
||||
Executable
+310
@@ -0,0 +1,310 @@
|
||||
#!/usr/bin/env bash
|
||||
# Spine build smoke test — validates that the binary compiles, links,
|
||||
# and exercises the SNMP/SQL code paths that were broken on develop.
|
||||
#
|
||||
# Requires: docker compose (uses the snmpv3 test fixture).
|
||||
# Usage: ./tests/integration/smoke_test.sh
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
|
||||
COMPOSE=(docker compose -f "$REPO_ROOT/tests/snmpv3/docker-compose.yml")
|
||||
PASS=0
|
||||
FAIL=0
|
||||
CLEANUP_NEEDED=0
|
||||
|
||||
pass() { echo " PASS: $*"; PASS=$((PASS+1)); }
|
||||
fail() { echo " FAIL: $*"; FAIL=$((FAIL+1)); }
|
||||
|
||||
cleanup() {
|
||||
if [[ $CLEANUP_NEEDED -eq 1 ]]; then
|
||||
echo ""
|
||||
echo "=== Cleanup: tearing down containers ==="
|
||||
"${COMPOSE[@]}" down -v --remove-orphans 2>/dev/null || true
|
||||
fi
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
wait_for_db() {
|
||||
local max_wait=120
|
||||
local elapsed=0
|
||||
echo " Waiting for database with seed data (up to ${max_wait}s)..."
|
||||
while [[ $elapsed -lt $max_wait ]]; do
|
||||
local count
|
||||
count=$("${COMPOSE[@]}" exec -T db mariadb -uspine -pspine cacti \
|
||||
-N -e "SELECT COUNT(*) FROM host;" 2>/dev/null || echo "0")
|
||||
if [[ "$count" -gt 0 ]]; then
|
||||
echo " Database ready with seed data after ${elapsed}s"
|
||||
return 0
|
||||
fi
|
||||
sleep 3
|
||||
elapsed=$((elapsed + 3))
|
||||
done
|
||||
echo " Database not ready after ${max_wait}s"
|
||||
return 1
|
||||
}
|
||||
|
||||
wait_for_snmpd() {
|
||||
local max_wait=30
|
||||
local elapsed=0
|
||||
echo " Waiting for snmpd to become healthy (up to ${max_wait}s)..."
|
||||
while [[ $elapsed -lt $max_wait ]]; do
|
||||
if "${COMPOSE[@]}" exec -T snmpd snmpget -v3 -u testuser -l authPriv \
|
||||
-a SHA-256 -A authpass1234 -x AES -X privpass1234 \
|
||||
localhost:1161 .1.3.6.1.2.1.1.3.0 >/dev/null 2>&1; then
|
||||
echo " snmpd ready after ${elapsed}s"
|
||||
return 0
|
||||
fi
|
||||
sleep 2
|
||||
elapsed=$((elapsed + 2))
|
||||
done
|
||||
echo " snmpd not ready after ${max_wait}s"
|
||||
return 1
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Phase 1: Build — confirms the fix compiles inside a clean container
|
||||
# ---------------------------------------------------------------------------
|
||||
echo ""
|
||||
echo "=== Phase 1: Docker build (compile from source) ==="
|
||||
|
||||
if "${COMPOSE[@]}" build spine 2>&1; then
|
||||
pass "spine Docker image built successfully"
|
||||
else
|
||||
fail "spine Docker image build failed"
|
||||
echo ""
|
||||
echo "=== Results: ${PASS} passed, ${FAIL} failed ==="
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Phase 2: Binary sanity — version, help
|
||||
# ---------------------------------------------------------------------------
|
||||
echo ""
|
||||
echo "=== Phase 2: binary sanity checks ==="
|
||||
|
||||
version_output=$("${COMPOSE[@]}" run --rm --no-deps --entrypoint spine spine --version 2>&1 || true)
|
||||
|
||||
if echo "$version_output" | grep -q "SPINE.*Copyright"; then
|
||||
pass "spine --version outputs banner"
|
||||
else
|
||||
fail "spine --version did not produce expected banner: $version_output"
|
||||
fi
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Phase 3: Start infrastructure and poll — exercises SNMP + SQL paths
|
||||
# ---------------------------------------------------------------------------
|
||||
echo ""
|
||||
echo "=== Phase 3: start db + snmpd, run SNMPv3 poll ==="
|
||||
CLEANUP_NEEDED=1
|
||||
|
||||
"${COMPOSE[@]}" up -d db snmpd 2>&1
|
||||
|
||||
if wait_for_db; then
|
||||
pass "database ready with seed data"
|
||||
else
|
||||
fail "database did not start or seed data missing"
|
||||
"${COMPOSE[@]}" logs db 2>&1 | grep -i error | tail -5
|
||||
echo ""
|
||||
echo "=== Results: ${PASS} passed, ${FAIL} failed ==="
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if wait_for_snmpd; then
|
||||
pass "snmpd accepting SNMP queries"
|
||||
else
|
||||
fail "snmpd did not start"
|
||||
fi
|
||||
|
||||
# Run spine against the test fixture
|
||||
poll_output=$("${COMPOSE[@]}" run --rm --entrypoint spine spine \
|
||||
--conf=/etc/spine/spine.conf -f 1 -l 1 -S -M 2>&1 || true)
|
||||
echo "$poll_output"
|
||||
|
||||
# Check that spine connected to the database (exercises get_cacti_version, db_query)
|
||||
if echo "$poll_output" | grep -qi "ERROR.*MySQL\|Cannot connect"; then
|
||||
fail "spine could not connect to database"
|
||||
else
|
||||
pass "spine connected to database"
|
||||
fi
|
||||
|
||||
# Check that SNMP polling ran (exercises the switch cases in poller.c)
|
||||
if echo "$poll_output" | grep -q "SNMP: v3:.*value:"; then
|
||||
pass "SNMPv3 poll returned data"
|
||||
elif echo "$poll_output" | grep -q "Device\[1\].*SNMP"; then
|
||||
pass "SNMP poll executed for device 1"
|
||||
elif echo "$poll_output" | grep -q "WARNING.*SNMP timeout"; then
|
||||
pass "SNMP poll attempted (timeout is acceptable in test environment)"
|
||||
else
|
||||
fail "no evidence of SNMP polling activity"
|
||||
fi
|
||||
|
||||
# Check device was polled
|
||||
if echo "$poll_output" | grep -q "Devices: 1"; then
|
||||
pass "device 1 was polled"
|
||||
else
|
||||
fail "device was not polled (Devices: 0)"
|
||||
fi
|
||||
|
||||
# Check spine did not crash or segfault
|
||||
if echo "$poll_output" | grep -qi "segfault\|SIGSEGV\|Aborted\|core dump"; then
|
||||
fail "spine crashed during polling"
|
||||
else
|
||||
pass "spine completed without crash"
|
||||
fi
|
||||
|
||||
# Check memory cleanup ran (validates SPINE_FREE fix in spine.c)
|
||||
if echo "$poll_output" | grep -q "Allocated Variable Memory Freed"; then
|
||||
pass "memory cleanup completed (SPINE_FREE fix validated)"
|
||||
else
|
||||
fail "memory cleanup did not complete"
|
||||
fi
|
||||
|
||||
# Check DB close ran (validates get_cacti_version MYSQL_RES fix in util.c)
|
||||
if echo "$poll_output" | grep -q "MYSQL Free & Close Completed"; then
|
||||
pass "database close completed (MYSQL_RES fix validated)"
|
||||
else
|
||||
fail "database close did not complete"
|
||||
fi
|
||||
|
||||
# Verify SNMPv3 data was written to MySQL
|
||||
v3_polls=$("${COMPOSE[@]}" exec -T db mariadb -uspine -pspine cacti \
|
||||
-N -e "SELECT total_polls FROM host WHERE id=1;" 2>/dev/null || echo "0")
|
||||
if [[ "$v3_polls" -gt 0 ]]; then
|
||||
pass "host table updated (total_polls=$v3_polls)"
|
||||
else
|
||||
fail "host table not updated after SNMPv3 poll"
|
||||
fi
|
||||
|
||||
v3_output=$("${COMPOSE[@]}" exec -T db mariadb -uspine -pspine cacti \
|
||||
-N -e "SELECT output FROM poller_output WHERE local_data_id=1;" 2>/dev/null || echo "")
|
||||
if [[ -n "$v3_output" && "$v3_output" != "NULL" ]]; then
|
||||
pass "poller_output written for SNMPv3 (value=$v3_output)"
|
||||
else
|
||||
fail "poller_output empty after SNMPv3 poll"
|
||||
fi
|
||||
|
||||
v3_sysdescr=$("${COMPOSE[@]}" exec -T db mariadb -uspine -pspine cacti \
|
||||
-N -e "SELECT snmp_sysDescr FROM host WHERE id=1;" 2>/dev/null || echo "")
|
||||
if [[ -n "$v3_sysdescr" ]]; then
|
||||
pass "snmp_sysDescr populated ($v3_sysdescr)"
|
||||
else
|
||||
echo " INFO: snmp_sysDescr empty (run with -M to populate)"
|
||||
fi
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Phase 4: Add SNMPv2c host and re-poll (exercises poller.c switch cases)
|
||||
# ---------------------------------------------------------------------------
|
||||
echo ""
|
||||
echo "=== Phase 4: SNMPv2c poll test ==="
|
||||
|
||||
"${COMPOSE[@]}" exec -T db mariadb -uspine -pspine cacti -e "
|
||||
INSERT IGNORE INTO host (
|
||||
id, hostname, snmp_community, snmp_version, snmp_port, snmp_timeout,
|
||||
max_oids, availability_method, ping_method, status, poller_id,
|
||||
device_threads, deleted
|
||||
) VALUES (
|
||||
2, 'snmpd', 'public', 2, 1161, 1000,
|
||||
10, 2, 0, 3, 1, 1, ''
|
||||
);
|
||||
INSERT IGNORE INTO poller_item (
|
||||
local_data_id, host_id, action, hostname, snmp_community,
|
||||
snmp_version, snmp_port, snmp_timeout,
|
||||
rrd_name, rrd_path, rrd_num, rrd_step,
|
||||
arg1, deleted, poller_id
|
||||
) VALUES (
|
||||
2, 2, 0, 'snmpd', 'public',
|
||||
2, 1161, 1000,
|
||||
'uptime', '/dev/null', 1, 300,
|
||||
'.1.3.6.1.2.1.1.3.0', '', 1
|
||||
);
|
||||
" 2>/dev/null
|
||||
|
||||
v2c_output=$("${COMPOSE[@]}" run --rm --entrypoint spine spine \
|
||||
--conf=/etc/spine/spine.conf -f 2 -l 2 -S 2>&1 || true)
|
||||
echo "$v2c_output"
|
||||
|
||||
if echo "$v2c_output" | grep -q "Device\[2\]"; then
|
||||
pass "SNMPv2c poll executed for device 2"
|
||||
else
|
||||
fail "no evidence of SNMPv2c polling for device 2"
|
||||
fi
|
||||
|
||||
if echo "$v2c_output" | grep -qi "segfault\|SIGSEGV\|Aborted"; then
|
||||
fail "spine crashed during SNMPv2c poll"
|
||||
else
|
||||
pass "SNMPv2c poll completed without crash"
|
||||
fi
|
||||
|
||||
# Verify SNMPv2c data was written to MySQL
|
||||
v2c_polls=$("${COMPOSE[@]}" exec -T db mariadb -uspine -pspine cacti \
|
||||
-N -e "SELECT total_polls FROM host WHERE id=2;" 2>/dev/null || echo "0")
|
||||
if [[ "$v2c_polls" -gt 0 ]]; then
|
||||
pass "host table updated for SNMPv2c (total_polls=$v2c_polls)"
|
||||
else
|
||||
fail "host table not updated after SNMPv2c poll"
|
||||
fi
|
||||
|
||||
v2c_db_output=$("${COMPOSE[@]}" exec -T db mariadb -uspine -pspine cacti \
|
||||
-N -e "SELECT output FROM poller_output WHERE local_data_id=2;" 2>/dev/null || echo "")
|
||||
if [[ -n "$v2c_db_output" && "$v2c_db_output" != "NULL" ]]; then
|
||||
pass "poller_output written for SNMPv2c (value=$v2c_db_output)"
|
||||
else
|
||||
fail "poller_output empty after SNMPv2c poll"
|
||||
fi
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Phase 5: Runtime fix validation — multi-device poll + DB integrity checks
|
||||
# ---------------------------------------------------------------------------
|
||||
echo ""
|
||||
echo "=== Phase 5: runtime fix validation ==="
|
||||
|
||||
# Poll both devices simultaneously; exercises the poller.c switch statement
|
||||
# across two concurrent threads to confirm multi-device dispatch is intact.
|
||||
multi_output=$("${COMPOSE[@]}" run --rm --entrypoint spine spine \
|
||||
--conf=/etc/spine/spine.conf -f 1 -l 2 -S 2>&1 || true)
|
||||
echo "$multi_output"
|
||||
|
||||
if echo "$multi_output" | grep -q "Devices: 2"; then
|
||||
pass "multi-device poll processed both devices (switch statement intact)"
|
||||
else
|
||||
fail "multi-device poll did not process 2 devices"
|
||||
fi
|
||||
|
||||
# "Unknown column" errors indicate a schema mismatch that the output_regex
|
||||
# detection fix was meant to guard against.
|
||||
if echo "$multi_output" | grep -qi "Unknown column"; then
|
||||
fail "SQL 'Unknown column' error detected — output_regex schema mismatch"
|
||||
else
|
||||
pass "no SQL 'Unknown column' errors (output_regex detection valid)"
|
||||
fi
|
||||
|
||||
# poller_time rows are written at the end of each spine run; their presence
|
||||
# confirms the DB write path ran to completion for both polls.
|
||||
pt_count=$("${COMPOSE[@]}" exec -T db mariadb -uspine -pspine cacti \
|
||||
-N -e "SELECT COUNT(*) FROM poller_time;" 2>/dev/null || echo "0")
|
||||
if [[ "$pt_count" -ge 2 ]]; then
|
||||
pass "poller_time has entries for both poll runs (pt_count=$pt_count)"
|
||||
else
|
||||
fail "poller_time missing entries — DB write path incomplete (pt_count=$pt_count)"
|
||||
fi
|
||||
|
||||
# host_errors rows are inserted when spine records a device-level failure.
|
||||
# An empty table means neither poll encountered an error condition.
|
||||
host_err_count=$("${COMPOSE[@]}" exec -T db mariadb -uspine -pspine cacti \
|
||||
-N -e "SELECT COUNT(*) FROM host_errors;" 2>/dev/null || echo "-1")
|
||||
if [[ "$host_err_count" -eq 0 ]]; then
|
||||
pass "host_errors table is empty (no polling errors)"
|
||||
elif [[ "$host_err_count" -eq -1 ]]; then
|
||||
# Table may not exist in all schema versions; treat as non-fatal.
|
||||
echo " INFO: host_errors table not found — skipping check"
|
||||
else
|
||||
fail "host_errors has $host_err_count row(s) — polling errors recorded"
|
||||
fi
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Summary
|
||||
# ---------------------------------------------------------------------------
|
||||
echo ""
|
||||
echo "=== Results: ${PASS} passed, ${FAIL} failed ==="
|
||||
[[ $FAIL -eq 0 ]]
|
||||
Executable
+250
@@ -0,0 +1,250 @@
|
||||
#!/usr/bin/env bash
|
||||
# Integration tests for db_column_exists() runtime detection in spine.
|
||||
#
|
||||
# Exercises three schema lifecycle states for poller_item.output_regex:
|
||||
# 1. Absent (baseline Cacti 1.3.0 schema) -- no detection log, no SQL errors
|
||||
# 2. Present (ALTER TABLE ADD COLUMN) -- detection log fires, correct output
|
||||
# 3. Removed (ALTER TABLE DROP COLUMN) -- graceful fallback, correct output
|
||||
#
|
||||
# For each state the test also verifies that poller_output is written and
|
||||
# host.total_polls is incremented, confirming end-to-end poll correctness.
|
||||
#
|
||||
# Requires: docker compose (uses the snmpv3 test fixture).
|
||||
# Usage: ./tests/integration/test_db_column_detect.sh
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
|
||||
COMPOSE=(docker compose -f "$REPO_ROOT/tests/snmpv3/docker-compose.yml")
|
||||
PASS=0
|
||||
FAIL=0
|
||||
|
||||
pass() { echo " PASS: $*"; PASS=$((PASS+1)); }
|
||||
fail() { echo " FAIL: $*"; FAIL=$((FAIL+1)); }
|
||||
|
||||
cleanup() {
|
||||
echo ""
|
||||
echo "=== Cleanup ==="
|
||||
"${COMPOSE[@]}" down -v --remove-orphans 2>/dev/null || true
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
wait_for_db() {
|
||||
local max_wait=120
|
||||
local elapsed=0
|
||||
echo " Waiting for database with seed data (up to ${max_wait}s)..."
|
||||
while [[ $elapsed -lt $max_wait ]]; do
|
||||
local count
|
||||
count=$("${COMPOSE[@]}" exec -T db mariadb -uspine -pspine cacti \
|
||||
-N -e "SELECT COUNT(*) FROM host;" 2>/dev/null || echo "0")
|
||||
if [[ "$count" -gt 0 ]]; then
|
||||
echo " Database ready after ${elapsed}s"
|
||||
return 0
|
||||
fi
|
||||
sleep 3
|
||||
elapsed=$((elapsed + 3))
|
||||
done
|
||||
echo " Database not ready after ${max_wait}s"
|
||||
return 1
|
||||
}
|
||||
|
||||
# Reset between runs: clear poller_output and zero total_polls so each
|
||||
# scenario starts from a known baseline and the assertions are unambiguous.
|
||||
reset_between_runs() {
|
||||
"${COMPOSE[@]}" exec -T db mariadb -uspine -pspine cacti -e "
|
||||
TRUNCATE poller_output;
|
||||
UPDATE host SET total_polls = 0 WHERE id = 1;
|
||||
" 2>/dev/null
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Setup
|
||||
# ---------------------------------------------------------------------------
|
||||
echo ""
|
||||
echo "=== Setup: build and start infrastructure ==="
|
||||
"${COMPOSE[@]}" build spine 2>&1 | tail -1
|
||||
"${COMPOSE[@]}" up -d db snmpd 2>&1
|
||||
wait_for_db || { fail "database did not start"; exit 1; }
|
||||
pass "infrastructure ready"
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Scenario 1: Baseline schema -- output_regex column absent
|
||||
#
|
||||
# Spine must complete without SQL errors. The detection log must NOT appear
|
||||
# (the column is absent so db_column_exists() returns FALSE and the branch
|
||||
# is skipped entirely). poller_output and total_polls must be updated.
|
||||
# ---------------------------------------------------------------------------
|
||||
echo ""
|
||||
echo "=== Scenario 1: column absent (baseline schema) ==="
|
||||
|
||||
col_check=$("${COMPOSE[@]}" exec -T db mariadb -uspine -pspine cacti \
|
||||
-N -e "SHOW COLUMNS FROM poller_item LIKE 'output_regex';" 2>/dev/null || echo "")
|
||||
|
||||
if [[ -z "$col_check" ]]; then
|
||||
pass "output_regex column absent before run"
|
||||
else
|
||||
fail "output_regex column unexpectedly present at baseline"
|
||||
fi
|
||||
|
||||
reset_between_runs
|
||||
|
||||
output1=$("${COMPOSE[@]}" run --rm --entrypoint spine spine \
|
||||
--conf=/etc/spine/spine.conf -f 1 -l 1 -S 2>&1 || true)
|
||||
|
||||
# No SQL errors or crashes
|
||||
if echo "$output1" | grep -qi "segfault\|SIGSEGV\|Aborted\|Unknown column\|FATAL.*Database"; then
|
||||
fail "scenario 1: spine crashed or SQL error (column absent)"
|
||||
else
|
||||
pass "scenario 1: spine ran without error (column absent)"
|
||||
fi
|
||||
|
||||
# Detection log must NOT appear -- column is absent so the branch is not taken
|
||||
if echo "$output1" | grep -q "output_regex column detected"; then
|
||||
fail "scenario 1: detection log fired but column is absent"
|
||||
else
|
||||
pass "scenario 1: detection log correctly absent"
|
||||
fi
|
||||
|
||||
# poller_output written
|
||||
s1_output=$("${COMPOSE[@]}" exec -T db mariadb -uspine -pspine cacti \
|
||||
-N -e "SELECT output FROM poller_output WHERE local_data_id=1;" 2>/dev/null || echo "")
|
||||
if [[ -n "$s1_output" ]]; then
|
||||
pass "scenario 1: poller_output written (value=$s1_output)"
|
||||
else
|
||||
fail "scenario 1: poller_output empty"
|
||||
fi
|
||||
|
||||
# total_polls incremented
|
||||
s1_polls=$("${COMPOSE[@]}" exec -T db mariadb -uspine -pspine cacti \
|
||||
-N -e "SELECT total_polls FROM host WHERE id=1;" 2>/dev/null || echo "0")
|
||||
if [[ "$s1_polls" -gt 0 ]]; then
|
||||
pass "scenario 1: host.total_polls incremented (total_polls=$s1_polls)"
|
||||
else
|
||||
fail "scenario 1: host.total_polls not incremented"
|
||||
fi
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Scenario 2: Column added -- db_column_exists() must detect it
|
||||
#
|
||||
# After ALTER TABLE the detection log must appear in spine's output and
|
||||
# polling must remain fully functional.
|
||||
# ---------------------------------------------------------------------------
|
||||
echo ""
|
||||
echo "=== Scenario 2: column added (ALTER TABLE ADD COLUMN) ==="
|
||||
|
||||
"${COMPOSE[@]}" exec -T db mariadb -uspine -pspine cacti -e "
|
||||
ALTER TABLE poller_item
|
||||
ADD COLUMN output_regex varchar(255) NOT NULL DEFAULT '' AFTER arg3;
|
||||
" 2>/dev/null
|
||||
|
||||
col_check=$("${COMPOSE[@]}" exec -T db mariadb -uspine -pspine cacti \
|
||||
-N -e "SHOW COLUMNS FROM poller_item LIKE 'output_regex';" 2>/dev/null || echo "")
|
||||
|
||||
if [[ -n "$col_check" ]]; then
|
||||
pass "output_regex column present after ALTER TABLE ADD"
|
||||
else
|
||||
fail "output_regex column missing after ALTER TABLE ADD"
|
||||
fi
|
||||
|
||||
reset_between_runs
|
||||
|
||||
output2=$("${COMPOSE[@]}" run --rm --entrypoint spine spine \
|
||||
--conf=/etc/spine/spine.conf -f 1 -l 1 -S 2>&1 || true)
|
||||
|
||||
# Detection log must appear (log_verbosity=5 / POLLER_VERBOSITY_DEBUG in seed)
|
||||
if echo "$output2" | grep -q "output_regex column detected"; then
|
||||
pass "scenario 2: db_column_exists() detection logged"
|
||||
else
|
||||
fail "scenario 2: detection log absent -- db_column_exists() may not be firing"
|
||||
fi
|
||||
|
||||
# No crashes or SQL errors
|
||||
if echo "$output2" | grep -qi "segfault\|SIGSEGV\|Aborted\|Unknown column\|FATAL.*Database"; then
|
||||
fail "scenario 2: spine crashed or SQL error (column present)"
|
||||
else
|
||||
pass "scenario 2: spine ran without error (column present)"
|
||||
fi
|
||||
|
||||
# poller_output written
|
||||
s2_output=$("${COMPOSE[@]}" exec -T db mariadb -uspine -pspine cacti \
|
||||
-N -e "SELECT output FROM poller_output WHERE local_data_id=1;" 2>/dev/null || echo "")
|
||||
if [[ -n "$s2_output" ]]; then
|
||||
pass "scenario 2: poller_output written (value=$s2_output)"
|
||||
else
|
||||
fail "scenario 2: poller_output empty"
|
||||
fi
|
||||
|
||||
# total_polls incremented
|
||||
s2_polls=$("${COMPOSE[@]}" exec -T db mariadb -uspine -pspine cacti \
|
||||
-N -e "SELECT total_polls FROM host WHERE id=1;" 2>/dev/null || echo "0")
|
||||
if [[ "$s2_polls" -gt 0 ]]; then
|
||||
pass "scenario 2: host.total_polls incremented (total_polls=$s2_polls)"
|
||||
else
|
||||
fail "scenario 2: host.total_polls not incremented"
|
||||
fi
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Scenario 3: Column removed -- spine must fall back gracefully
|
||||
#
|
||||
# After ALTER TABLE DROP COLUMN spine must not crash, must not emit an
|
||||
# "Unknown column" error, and must continue writing output and updating polls.
|
||||
# ---------------------------------------------------------------------------
|
||||
echo ""
|
||||
echo "=== Scenario 3: column removed (ALTER TABLE DROP COLUMN) ==="
|
||||
|
||||
"${COMPOSE[@]}" exec -T db mariadb -uspine -pspine cacti -e "
|
||||
ALTER TABLE poller_item DROP COLUMN output_regex;
|
||||
" 2>/dev/null
|
||||
|
||||
col_check=$("${COMPOSE[@]}" exec -T db mariadb -uspine -pspine cacti \
|
||||
-N -e "SHOW COLUMNS FROM poller_item LIKE 'output_regex';" 2>/dev/null || echo "")
|
||||
|
||||
if [[ -z "$col_check" ]]; then
|
||||
pass "output_regex column absent after ALTER TABLE DROP"
|
||||
else
|
||||
fail "output_regex column still present after ALTER TABLE DROP"
|
||||
fi
|
||||
|
||||
reset_between_runs
|
||||
|
||||
output3=$("${COMPOSE[@]}" run --rm --entrypoint spine spine \
|
||||
--conf=/etc/spine/spine.conf -f 1 -l 1 -S 2>&1 || true)
|
||||
|
||||
# Must not crash or produce a SQL error referencing the dropped column
|
||||
if echo "$output3" | grep -qi "segfault\|SIGSEGV\|Aborted\|Unknown column\|FATAL.*Database"; then
|
||||
fail "scenario 3: spine crashed or SQL error after column removal"
|
||||
else
|
||||
pass "scenario 3: spine handled column removal gracefully"
|
||||
fi
|
||||
|
||||
# Detection log must NOT appear -- column is absent again
|
||||
if echo "$output3" | grep -q "output_regex column detected"; then
|
||||
fail "scenario 3: detection log fired but column was dropped"
|
||||
else
|
||||
pass "scenario 3: detection log correctly absent after drop"
|
||||
fi
|
||||
|
||||
# poller_output written
|
||||
s3_output=$("${COMPOSE[@]}" exec -T db mariadb -uspine -pspine cacti \
|
||||
-N -e "SELECT output FROM poller_output WHERE local_data_id=1;" 2>/dev/null || echo "")
|
||||
if [[ -n "$s3_output" ]]; then
|
||||
pass "scenario 3: poller_output written (value=$s3_output)"
|
||||
else
|
||||
fail "scenario 3: poller_output empty after column removal"
|
||||
fi
|
||||
|
||||
# total_polls incremented
|
||||
s3_polls=$("${COMPOSE[@]}" exec -T db mariadb -uspine -pspine cacti \
|
||||
-N -e "SELECT total_polls FROM host WHERE id=1;" 2>/dev/null || echo "0")
|
||||
if [[ "$s3_polls" -gt 0 ]]; then
|
||||
pass "scenario 3: host.total_polls incremented (total_polls=$s3_polls)"
|
||||
else
|
||||
fail "scenario 3: host.total_polls not incremented after column removal"
|
||||
fi
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Summary
|
||||
# ---------------------------------------------------------------------------
|
||||
echo ""
|
||||
echo "=== Results: ${PASS} passed, ${FAIL} failed ==="
|
||||
[[ $FAIL -eq 0 ]]
|
||||
Executable
+213
@@ -0,0 +1,213 @@
|
||||
#!/usr/bin/env bash
|
||||
# Integration tests for output_regex column detection.
|
||||
#
|
||||
# Validates that spine correctly detects the presence or absence of the
|
||||
# output_regex column in poller_item and adjusts its queries accordingly.
|
||||
#
|
||||
# Requires: docker compose (uses the snmpv3 test fixture).
|
||||
# Usage: ./tests/integration/test_output_regex.sh
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
|
||||
COMPOSE=(docker compose -f "$REPO_ROOT/tests/snmpv3/docker-compose.yml")
|
||||
PASS=0
|
||||
FAIL=0
|
||||
|
||||
pass() { echo " PASS: $*"; PASS=$((PASS+1)); }
|
||||
fail() { echo " FAIL: $*"; FAIL=$((FAIL+1)); }
|
||||
|
||||
cleanup() {
|
||||
echo ""
|
||||
echo "=== Cleanup ==="
|
||||
"${COMPOSE[@]}" down -v --remove-orphans 2>/dev/null || true
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
wait_for_db() {
|
||||
local max_wait=120
|
||||
local elapsed=0
|
||||
while [[ $elapsed -lt $max_wait ]]; do
|
||||
local count
|
||||
count=$("${COMPOSE[@]}" exec -T db mariadb -uspine -pspine cacti \
|
||||
-N -e "SELECT COUNT(*) FROM host;" 2>/dev/null || echo "0")
|
||||
if [[ "$count" -gt 0 ]]; then
|
||||
return 0
|
||||
fi
|
||||
sleep 3
|
||||
elapsed=$((elapsed + 3))
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Setup
|
||||
# ---------------------------------------------------------------------------
|
||||
echo ""
|
||||
echo "=== Setup: build and start infrastructure ==="
|
||||
"${COMPOSE[@]}" build spine 2>&1 | tail -1
|
||||
"${COMPOSE[@]}" up -d db snmpd 2>&1
|
||||
echo " Waiting for database..."
|
||||
wait_for_db || { fail "database did not start"; exit 1; }
|
||||
pass "infrastructure ready"
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Test 1: Spine works WITHOUT output_regex column (baseline Cacti schema)
|
||||
# ---------------------------------------------------------------------------
|
||||
echo ""
|
||||
echo "=== Test 1: poll WITHOUT output_regex column ==="
|
||||
|
||||
has_col=$("${COMPOSE[@]}" exec -T db mariadb -uspine -pspine cacti \
|
||||
-N -e "SHOW COLUMNS FROM poller_item LIKE 'output_regex';" 2>/dev/null || echo "")
|
||||
|
||||
if [[ -z "$has_col" ]]; then
|
||||
pass "output_regex column absent (baseline schema)"
|
||||
else
|
||||
fail "output_regex column unexpectedly present"
|
||||
fi
|
||||
|
||||
output1=$("${COMPOSE[@]}" run --rm --entrypoint spine spine \
|
||||
--conf=/etc/spine/spine.conf -f 1 -l 1 -S 2>&1 || true)
|
||||
|
||||
if echo "$output1" | grep -qi "segfault\|SIGSEGV\|Aborted\|Unknown column"; then
|
||||
fail "spine crashed or SQL error without output_regex column"
|
||||
else
|
||||
pass "spine ran without output_regex column"
|
||||
fi
|
||||
|
||||
if echo "$output1" | grep -q "Devices: 1"; then
|
||||
pass "device polled without output_regex"
|
||||
else
|
||||
fail "device not polled without output_regex"
|
||||
fi
|
||||
|
||||
v1_output=$("${COMPOSE[@]}" exec -T db mariadb -uspine -pspine cacti \
|
||||
-N -e "SELECT output FROM poller_output WHERE local_data_id=1;" 2>/dev/null || echo "")
|
||||
if [[ -n "$v1_output" ]]; then
|
||||
pass "poller_output written without output_regex (value=$v1_output)"
|
||||
else
|
||||
fail "poller_output empty without output_regex"
|
||||
fi
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Test 2: Add output_regex column, verify spine detects and uses it
|
||||
# ---------------------------------------------------------------------------
|
||||
echo ""
|
||||
echo "=== Test 2: poll WITH output_regex column ==="
|
||||
|
||||
"${COMPOSE[@]}" exec -T db mariadb -uspine -pspine cacti -e "
|
||||
ALTER TABLE poller_item ADD COLUMN output_regex varchar(255) NOT NULL DEFAULT '' AFTER arg3;
|
||||
" 2>/dev/null
|
||||
|
||||
has_col=$("${COMPOSE[@]}" exec -T db mariadb -uspine -pspine cacti \
|
||||
-N -e "SHOW COLUMNS FROM poller_item LIKE 'output_regex';" 2>/dev/null || echo "")
|
||||
|
||||
if [[ -n "$has_col" ]]; then
|
||||
pass "output_regex column added"
|
||||
else
|
||||
fail "output_regex column not found after ALTER TABLE"
|
||||
fi
|
||||
|
||||
# Clear previous output
|
||||
"${COMPOSE[@]}" exec -T db mariadb -uspine -pspine cacti -e "
|
||||
TRUNCATE poller_output;
|
||||
" 2>/dev/null
|
||||
|
||||
output2=$("${COMPOSE[@]}" run --rm --entrypoint spine spine \
|
||||
--conf=/etc/spine/spine.conf -f 1 -l 1 -S 2>&1 || true)
|
||||
|
||||
if echo "$output2" | grep -q "poller_item.output_regex column detected"; then
|
||||
pass "spine detected output_regex column"
|
||||
else
|
||||
fail "spine did not log output_regex detection"
|
||||
fi
|
||||
|
||||
if echo "$output2" | grep -qi "segfault\|SIGSEGV\|Aborted\|Unknown column"; then
|
||||
fail "spine crashed or SQL error with output_regex column"
|
||||
else
|
||||
pass "spine ran with output_regex column"
|
||||
fi
|
||||
|
||||
if echo "$output2" | grep -q "Devices: 1"; then
|
||||
pass "device polled with output_regex"
|
||||
else
|
||||
fail "device not polled with output_regex"
|
||||
fi
|
||||
|
||||
v2_output=$("${COMPOSE[@]}" exec -T db mariadb -uspine -pspine cacti \
|
||||
-N -e "SELECT output FROM poller_output WHERE local_data_id=1;" 2>/dev/null || echo "")
|
||||
if [[ -n "$v2_output" ]]; then
|
||||
pass "poller_output written with output_regex (value=$v2_output)"
|
||||
else
|
||||
fail "poller_output empty with output_regex"
|
||||
fi
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Test 3: Set a regex pattern and verify it filters the SNMP value
|
||||
# ---------------------------------------------------------------------------
|
||||
echo ""
|
||||
echo "=== Test 3: output_regex filtering ==="
|
||||
|
||||
# Set a regex that extracts just the integer part of sysUpTime
|
||||
"${COMPOSE[@]}" exec -T db mariadb -uspine -pspine cacti -e "
|
||||
UPDATE poller_item SET output_regex = '([0-9]+)' WHERE local_data_id = 1;
|
||||
TRUNCATE poller_output;
|
||||
" 2>/dev/null
|
||||
|
||||
output3=$("${COMPOSE[@]}" run --rm --entrypoint spine spine \
|
||||
--conf=/etc/spine/spine.conf -f 1 -l 1 -S 2>&1 || true)
|
||||
|
||||
if echo "$output3" | grep -qi "segfault\|SIGSEGV\|Aborted"; then
|
||||
fail "spine crashed with output_regex pattern set"
|
||||
else
|
||||
pass "spine ran with output_regex pattern"
|
||||
fi
|
||||
|
||||
v3_output=$("${COMPOSE[@]}" exec -T db mariadb -uspine -pspine cacti \
|
||||
-N -e "SELECT output FROM poller_output WHERE local_data_id=1;" 2>/dev/null || echo "")
|
||||
if [[ -n "$v3_output" ]]; then
|
||||
pass "poller_output written with regex filtering (value=$v3_output)"
|
||||
else
|
||||
fail "poller_output empty with regex filtering"
|
||||
fi
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Test 4: Drop column again, verify spine falls back gracefully
|
||||
# ---------------------------------------------------------------------------
|
||||
echo ""
|
||||
echo "=== Test 4: column removal fallback ==="
|
||||
|
||||
"${COMPOSE[@]}" exec -T db mariadb -uspine -pspine cacti -e "
|
||||
ALTER TABLE poller_item DROP COLUMN output_regex;
|
||||
TRUNCATE poller_output;
|
||||
" 2>/dev/null
|
||||
|
||||
output4=$("${COMPOSE[@]}" run --rm --entrypoint spine spine \
|
||||
--conf=/etc/spine/spine.conf -f 1 -l 1 -S 2>&1 || true)
|
||||
|
||||
if echo "$output4" | grep -qi "segfault\|SIGSEGV\|Aborted\|Unknown column"; then
|
||||
fail "spine crashed after output_regex column removed"
|
||||
else
|
||||
pass "spine gracefully handled column removal"
|
||||
fi
|
||||
|
||||
if echo "$output4" | grep -q "Devices: 1"; then
|
||||
pass "device polled after column removal"
|
||||
else
|
||||
fail "device not polled after column removal"
|
||||
fi
|
||||
|
||||
v4_output=$("${COMPOSE[@]}" exec -T db mariadb -uspine -pspine cacti \
|
||||
-N -e "SELECT output FROM poller_output WHERE local_data_id=1;" 2>/dev/null || echo "")
|
||||
if [[ -n "$v4_output" ]]; then
|
||||
pass "poller_output written after column removal (value=$v4_output)"
|
||||
else
|
||||
fail "poller_output empty after column removal"
|
||||
fi
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Summary
|
||||
# ---------------------------------------------------------------------------
|
||||
echo ""
|
||||
echo "=== Results: ${PASS} passed, ${FAIL} failed ==="
|
||||
[[ $FAIL -eq 0 ]]
|
||||
@@ -21,7 +21,7 @@ CREATE TABLE IF NOT EXISTS `host` (
|
||||
`snmp_version` tinyint(1) unsigned NOT NULL DEFAULT 1,
|
||||
`snmp_username` varchar(50) NOT NULL DEFAULT '',
|
||||
`snmp_password` varchar(50) NOT NULL DEFAULT '',
|
||||
`snmp_auth_protocol` char(6) NOT NULL DEFAULT '',
|
||||
`snmp_auth_protocol` char(7) NOT NULL DEFAULT '',
|
||||
`snmp_priv_passphrase` varchar(200) NOT NULL DEFAULT '',
|
||||
`snmp_priv_protocol` char(6) NOT NULL DEFAULT '',
|
||||
`snmp_context` varchar(64) NOT NULL DEFAULT '',
|
||||
@@ -69,7 +69,7 @@ CREATE TABLE IF NOT EXISTS `poller_item` (
|
||||
`snmp_version` tinyint(1) unsigned NOT NULL DEFAULT 0,
|
||||
`snmp_username` varchar(50) NOT NULL DEFAULT '',
|
||||
`snmp_password` varchar(50) NOT NULL DEFAULT '',
|
||||
`snmp_auth_protocol` char(6) NOT NULL DEFAULT '',
|
||||
`snmp_auth_protocol` char(7) NOT NULL DEFAULT '',
|
||||
`snmp_priv_passphrase` varchar(200) NOT NULL DEFAULT '',
|
||||
`snmp_priv_protocol` char(6) NOT NULL DEFAULT '',
|
||||
`snmp_context` varchar(64) NOT NULL DEFAULT '',
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
# Allow the SNMPv3 test user read access with auth+priv
|
||||
rouser testuser priv
|
||||
|
||||
# Allow SNMPv2c read access for integration tests
|
||||
rocommunity public
|
||||
|
||||
# Suppress TCP wrappers log noise
|
||||
dontLogTCPWrappersConnects yes
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
# Unit test Makefile for spine/tests/unit
|
||||
#
|
||||
# Compiles test_build_fixes.c as a fully self-contained binary. No MySQL or
|
||||
# SNMP daemon required; the test file inlines the two functions under test to
|
||||
# avoid link dependencies on util.o.
|
||||
#
|
||||
# Usage:
|
||||
# make -- build and run
|
||||
# make build -- build only
|
||||
# make clean -- remove build artefacts
|
||||
|
||||
CC := cc
|
||||
BINDIR := build
|
||||
|
||||
CMOCKA_INC := /opt/homebrew/Cellar/cmocka/2.0.2/include
|
||||
CMOCKA_LIB := /opt/homebrew/Cellar/cmocka/2.0.2/lib
|
||||
|
||||
SPINE_ROOT := ../..
|
||||
|
||||
OPENSSL_INC := /opt/homebrew/opt/openssl@3/include
|
||||
MYSQL_INC := /opt/homebrew/opt/mysql-client/include/mysql
|
||||
NETSNMP_INC := /opt/homebrew/opt/net-snmp/include/net-snmp
|
||||
NETSNMP_INC2 := /opt/homebrew/opt/net-snmp/include/net-snmp/..
|
||||
|
||||
CFLAGS := \
|
||||
-std=gnu23 \
|
||||
-DHAVE_CONFIG_H \
|
||||
-I$(CMOCKA_INC) \
|
||||
-I$(SPINE_ROOT) \
|
||||
-I$(SPINE_ROOT)/config \
|
||||
-I$(OPENSSL_INC) \
|
||||
-I$(MYSQL_INC) \
|
||||
-I$(NETSNMP_INC) \
|
||||
-I$(NETSNMP_INC2) \
|
||||
-Wall \
|
||||
-Wextra \
|
||||
-Wno-unused-parameter
|
||||
|
||||
LDFLAGS := \
|
||||
-L$(CMOCKA_LIB) \
|
||||
-lcmocka \
|
||||
-Wl,-rpath,$(CMOCKA_LIB)
|
||||
|
||||
TARGET := $(BINDIR)/test_build_fixes
|
||||
|
||||
.PHONY: all build run clean
|
||||
|
||||
all: run
|
||||
|
||||
build: $(TARGET)
|
||||
|
||||
$(BINDIR):
|
||||
mkdir -p $(BINDIR)
|
||||
|
||||
$(TARGET): test_build_fixes.c | $(BINDIR)
|
||||
$(CC) $(CFLAGS) $< -o $@ $(LDFLAGS)
|
||||
|
||||
run: $(TARGET)
|
||||
$(TARGET)
|
||||
|
||||
clean:
|
||||
rm -rf $(BINDIR)
|
||||
@@ -0,0 +1,386 @@
|
||||
/*
|
||||
* Unit tests for spine build fixes.
|
||||
*
|
||||
* Covers:
|
||||
* - uthash macro availability (HASH_ADD_INT, HASH_FIND_INT, HASH_DEL, HASH_COUNT)
|
||||
* - config_struct default field values (has_output_regex, has_device_0)
|
||||
* - regex_replace() correctness
|
||||
* - strncopy() buffer-overflow safety
|
||||
*
|
||||
* These tests exercise only pure functions and data structures; no MySQL or
|
||||
* SNMP daemon is required.
|
||||
*/
|
||||
|
||||
#include <stdarg.h>
|
||||
#include <stddef.h>
|
||||
#include <setjmp.h>
|
||||
#include <cmocka.h>
|
||||
|
||||
#include <string.h>
|
||||
#include <stdlib.h>
|
||||
|
||||
/*
|
||||
* Pull in spine types without dragging along MySQL/SNMP link dependencies.
|
||||
* We define stubs for the few symbols util.c references at file scope so the
|
||||
* translation unit compiles standalone.
|
||||
*/
|
||||
#define HAVE_CONFIG_H 1
|
||||
#include "config/config.h"
|
||||
|
||||
/* Minimal spine.h constants needed before the full header. */
|
||||
#ifndef FALSE
|
||||
#define FALSE 0
|
||||
#endif
|
||||
#ifndef TRUE
|
||||
#define TRUE 1
|
||||
#endif
|
||||
|
||||
/* Buffer sizes referenced by spine.h and util.c. */
|
||||
#define BUFSIZE 256
|
||||
#define SMALL_BUFSIZE 256
|
||||
#define DBL_BUFSIZE 512
|
||||
#define BIG_BUFSIZE 1024
|
||||
#define MEGA_BUFSIZE (1024 * 1024)
|
||||
#define MAX_MATCHES 5
|
||||
|
||||
#include "uthash.h"
|
||||
|
||||
/* -------------------------------------------------------------------------
|
||||
* uthash tests
|
||||
* ------------------------------------------------------------------------- */
|
||||
|
||||
typedef struct {
|
||||
int id;
|
||||
UT_hash_handle hh;
|
||||
} item_t;
|
||||
|
||||
static void test_uthash_add_find(void **state) {
|
||||
(void)state;
|
||||
|
||||
item_t *table = NULL;
|
||||
item_t *a = calloc(1, sizeof(item_t));
|
||||
item_t *b = calloc(1, sizeof(item_t));
|
||||
assert_non_null(a);
|
||||
assert_non_null(b);
|
||||
|
||||
a->id = 1;
|
||||
b->id = 2;
|
||||
|
||||
HASH_ADD_INT(table, id, a);
|
||||
HASH_ADD_INT(table, id, b);
|
||||
|
||||
assert_int_equal(HASH_COUNT(table), 2);
|
||||
|
||||
item_t *found = NULL;
|
||||
int key = 1;
|
||||
HASH_FIND_INT(table, &key, found);
|
||||
assert_non_null(found);
|
||||
assert_int_equal(found->id, 1);
|
||||
|
||||
key = 2;
|
||||
HASH_FIND_INT(table, &key, found);
|
||||
assert_non_null(found);
|
||||
assert_int_equal(found->id, 2);
|
||||
|
||||
/* Clean up. */
|
||||
HASH_DEL(table, a); free(a);
|
||||
HASH_DEL(table, b); free(b);
|
||||
assert_int_equal(HASH_COUNT(table), 0);
|
||||
}
|
||||
|
||||
static void test_uthash_find_missing(void **state) {
|
||||
(void)state;
|
||||
|
||||
item_t *table = NULL;
|
||||
item_t *e = calloc(1, sizeof(item_t));
|
||||
assert_non_null(e);
|
||||
e->id = 42;
|
||||
HASH_ADD_INT(table, id, e);
|
||||
|
||||
item_t *found = NULL;
|
||||
int key = 99;
|
||||
HASH_FIND_INT(table, &key, found);
|
||||
assert_null(found);
|
||||
|
||||
HASH_DEL(table, e); free(e);
|
||||
}
|
||||
|
||||
static void test_uthash_del_reduces_count(void **state) {
|
||||
(void)state;
|
||||
|
||||
item_t *table = NULL;
|
||||
item_t *items[3];
|
||||
for (int i = 0; i < 3; i++) {
|
||||
items[i] = calloc(1, sizeof(item_t));
|
||||
assert_non_null(items[i]);
|
||||
items[i]->id = i + 10;
|
||||
HASH_ADD_INT(table, id, items[i]);
|
||||
}
|
||||
assert_int_equal(HASH_COUNT(table), 3);
|
||||
|
||||
HASH_DEL(table, items[1]); free(items[1]);
|
||||
assert_int_equal(HASH_COUNT(table), 2);
|
||||
|
||||
int key = 11;
|
||||
item_t *found = NULL;
|
||||
HASH_FIND_INT(table, &key, found);
|
||||
assert_null(found);
|
||||
|
||||
HASH_DEL(table, items[0]); free(items[0]);
|
||||
HASH_DEL(table, items[2]); free(items[2]);
|
||||
}
|
||||
|
||||
/* -------------------------------------------------------------------------
|
||||
* config_struct default field tests
|
||||
*
|
||||
* We test the field layout and zero-initialisation semantics directly without
|
||||
* calling config_defaults() (which would pull in MySQL). Zero-initialising a
|
||||
* config_struct and checking has_device_0 / has_output_regex == FALSE verifies
|
||||
* both that the fields exist at the expected offsets and that their "off"
|
||||
* sentinel is FALSE (0) -- the same invariant config_defaults() relies on.
|
||||
* ------------------------------------------------------------------------- */
|
||||
|
||||
/*
|
||||
* Minimal forward declaration of just the fields we need. We reproduce the
|
||||
* struct layout verbatim from spine.h lines 351-377 so we can reference field
|
||||
* names without including the full header (which would require MySQL headers).
|
||||
*/
|
||||
typedef struct {
|
||||
int stdout_notty;
|
||||
int stderr_notty;
|
||||
int poller_id;
|
||||
int poller_interval;
|
||||
int parent_fork;
|
||||
int num_parent_processes;
|
||||
int script_timeout;
|
||||
int active_profiles;
|
||||
int total_snmp_ports;
|
||||
int threads;
|
||||
int threads_set;
|
||||
int logfile_processed;
|
||||
int boost_enabled;
|
||||
int boost_redirect;
|
||||
int cygwinshloc;
|
||||
int snmponly;
|
||||
int SQL_readonly;
|
||||
int start_host_id;
|
||||
int end_host_id;
|
||||
char host_id_list[BIG_BUFSIZE];
|
||||
int has_device_0; /* line 376 */
|
||||
int has_output_regex; /* line 377 */
|
||||
} config_subset_t;
|
||||
|
||||
static void test_config_has_device_0_default(void **state) {
|
||||
(void)state;
|
||||
|
||||
config_subset_t s;
|
||||
memset(&s, 0, sizeof(s));
|
||||
|
||||
assert_int_equal(s.has_device_0, FALSE);
|
||||
}
|
||||
|
||||
static void test_config_has_output_regex_default(void **state) {
|
||||
(void)state;
|
||||
|
||||
config_subset_t s;
|
||||
memset(&s, 0, sizeof(s));
|
||||
|
||||
assert_int_equal(s.has_output_regex, FALSE);
|
||||
}
|
||||
|
||||
/* -------------------------------------------------------------------------
|
||||
* regex_replace tests
|
||||
*
|
||||
* We inline a self-contained copy of regex_replace so this file compiles
|
||||
* without linking against util.o (which drags in spine_log, MySQL, etc.).
|
||||
* The logic is a verbatim copy of util.c:2147-2180 with only the static
|
||||
* thread-local buffer renamed to avoid ODR conflicts if the linker ever
|
||||
* sees both.
|
||||
* ------------------------------------------------------------------------- */
|
||||
|
||||
#include <regex.h>
|
||||
|
||||
static char *regex_replace_impl(char *exp, char *value) {
|
||||
regex_t regex;
|
||||
int reti;
|
||||
static __thread char test_msgbuf[SMALL_BUFSIZE];
|
||||
regmatch_t matches[MAX_MATCHES];
|
||||
size_t match_len;
|
||||
|
||||
reti = regcomp(®ex, exp, 0);
|
||||
if (reti) {
|
||||
return value;
|
||||
}
|
||||
|
||||
reti = regexec(®ex, value, MAX_MATCHES, matches, 0);
|
||||
if (!reti) {
|
||||
match_len = (size_t)(matches[0].rm_eo - matches[0].rm_so);
|
||||
if (match_len >= SMALL_BUFSIZE) {
|
||||
match_len = SMALL_BUFSIZE - 1;
|
||||
}
|
||||
memcpy(test_msgbuf, value + matches[0].rm_so, match_len);
|
||||
test_msgbuf[match_len] = '\0';
|
||||
}
|
||||
|
||||
regfree(®ex);
|
||||
return (reti) ? value : test_msgbuf;
|
||||
}
|
||||
|
||||
static void test_regex_replace_numeric_extraction(void **state) {
|
||||
(void)state;
|
||||
|
||||
/* BRE: [0-9][0-9]* is "one or more digits" (+ is literal in BRE).
|
||||
* Matches the first run of digits in the input. */
|
||||
char *result = regex_replace_impl("[0-9][0-9]*", "load: 42.5 util");
|
||||
assert_string_equal(result, "42");
|
||||
}
|
||||
|
||||
static void test_regex_replace_no_match_returns_input(void **state) {
|
||||
(void)state;
|
||||
|
||||
/* BRE pattern that cannot match the input; original value must be returned. */
|
||||
char *result = regex_replace_impl("[0-9][0-9]*", "no digits here");
|
||||
assert_string_equal(result, "no digits here");
|
||||
}
|
||||
|
||||
static void test_regex_replace_invalid_regex_returns_input(void **state) {
|
||||
(void)state;
|
||||
|
||||
/* An invalid regex pattern (unbalanced bracket) makes regcomp() fail;
|
||||
* the function must return the original value, not crash. */
|
||||
char *result = regex_replace_impl("[invalid", "some value");
|
||||
assert_string_equal(result, "some value");
|
||||
}
|
||||
|
||||
static void test_regex_replace_full_match(void **state) {
|
||||
(void)state;
|
||||
|
||||
/* Pattern matches the entire string. */
|
||||
char *result = regex_replace_impl("^hello$", "hello");
|
||||
assert_string_equal(result, "hello");
|
||||
}
|
||||
|
||||
static void test_regex_replace_decimal_number(void **state) {
|
||||
(void)state;
|
||||
|
||||
/* BRE decimal extraction: [.][0-9][0-9]* matches the fractional part.
|
||||
* Input "temp=23.7C" has no leading sign, so the match starts at '.'. */
|
||||
char *result = regex_replace_impl("[.][0-9][0-9]*", "temp=23.7C");
|
||||
assert_string_equal(result, ".7");
|
||||
}
|
||||
|
||||
/* -------------------------------------------------------------------------
|
||||
* strncopy tests
|
||||
*
|
||||
* Same approach: inline the implementation to avoid util.o link dependency.
|
||||
* ------------------------------------------------------------------------- */
|
||||
|
||||
#include <string.h>
|
||||
#include <assert.h>
|
||||
|
||||
static char *strncopy_impl(char *dst, const char *src, size_t obuf) {
|
||||
size_t copy_len;
|
||||
|
||||
assert(dst != NULL);
|
||||
assert(src != NULL);
|
||||
|
||||
if (obuf == 0) return dst;
|
||||
|
||||
copy_len = strnlen(src, obuf - 1);
|
||||
|
||||
if (copy_len) {
|
||||
strncpy(dst, src, copy_len);
|
||||
}
|
||||
|
||||
dst[copy_len] = '\0';
|
||||
return dst;
|
||||
}
|
||||
|
||||
static void test_strncopy_normal(void **state) {
|
||||
(void)state;
|
||||
|
||||
char buf[16];
|
||||
char *ret = strncopy_impl(buf, "hello", sizeof(buf));
|
||||
assert_ptr_equal(ret, buf);
|
||||
assert_string_equal(buf, "hello");
|
||||
}
|
||||
|
||||
static void test_strncopy_truncates_at_n_minus_1(void **state) {
|
||||
(void)state;
|
||||
|
||||
char buf[5];
|
||||
strncopy_impl(buf, "toolong", sizeof(buf));
|
||||
/* Must be NUL-terminated and at most 4 content bytes. */
|
||||
assert_int_equal(buf[4], '\0');
|
||||
assert_memory_equal(buf, "tool", 4);
|
||||
}
|
||||
|
||||
static void test_strncopy_empty_src(void **state) {
|
||||
(void)state;
|
||||
|
||||
char buf[8] = "garbage";
|
||||
strncopy_impl(buf, "", sizeof(buf));
|
||||
assert_string_equal(buf, "");
|
||||
}
|
||||
|
||||
static void test_strncopy_exact_fit(void **state) {
|
||||
(void)state;
|
||||
|
||||
/* src length == buf size - 1: should fit exactly with NUL. */
|
||||
char buf[6];
|
||||
strncopy_impl(buf, "hello", sizeof(buf));
|
||||
assert_string_equal(buf, "hello");
|
||||
assert_int_equal(buf[5], '\0');
|
||||
}
|
||||
|
||||
static void test_strncopy_returns_dst(void **state) {
|
||||
(void)state;
|
||||
|
||||
char buf[32];
|
||||
char *ret = strncopy_impl(buf, "check", sizeof(buf));
|
||||
assert_ptr_equal(ret, buf);
|
||||
}
|
||||
|
||||
static void test_strncopy_zero_size(void **state) {
|
||||
(void)state;
|
||||
|
||||
/* obuf == 0 must return without touching dst. */
|
||||
char buf[4] = "abc";
|
||||
strncopy_impl(buf, "xyz", 0);
|
||||
/* dst is unchanged */
|
||||
assert_string_equal(buf, "abc");
|
||||
}
|
||||
|
||||
/* -------------------------------------------------------------------------
|
||||
* main
|
||||
* ------------------------------------------------------------------------- */
|
||||
|
||||
int main(void) {
|
||||
const struct CMUnitTest tests[] = {
|
||||
/* uthash */
|
||||
cmocka_unit_test(test_uthash_add_find),
|
||||
cmocka_unit_test(test_uthash_find_missing),
|
||||
cmocka_unit_test(test_uthash_del_reduces_count),
|
||||
|
||||
/* config_struct defaults */
|
||||
cmocka_unit_test(test_config_has_device_0_default),
|
||||
cmocka_unit_test(test_config_has_output_regex_default),
|
||||
|
||||
/* regex_replace */
|
||||
cmocka_unit_test(test_regex_replace_numeric_extraction),
|
||||
cmocka_unit_test(test_regex_replace_no_match_returns_input),
|
||||
cmocka_unit_test(test_regex_replace_invalid_regex_returns_input),
|
||||
cmocka_unit_test(test_regex_replace_full_match),
|
||||
cmocka_unit_test(test_regex_replace_decimal_number),
|
||||
|
||||
/* strncopy */
|
||||
cmocka_unit_test(test_strncopy_normal),
|
||||
cmocka_unit_test(test_strncopy_truncates_at_n_minus_1),
|
||||
cmocka_unit_test(test_strncopy_empty_src),
|
||||
cmocka_unit_test(test_strncopy_exact_fit),
|
||||
cmocka_unit_test(test_strncopy_returns_dst),
|
||||
cmocka_unit_test(test_strncopy_zero_size),
|
||||
};
|
||||
|
||||
return cmocka_run_group_tests(tests, NULL, NULL);
|
||||
}
|
||||
Reference in new issue
Block a user