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:
Thomas Vincent authored and GitHub committed 2026-03-25 20:32:40 -04:00
1 parent 0dd91c4602
commit 70459ac82c
15 files changed
+2471 -93

No files matched your search

+1 -1
View File
@@ -31,7 +31,7 @@ bin_PROGRAMS = spine
man_MANS = spine.1 man_MANS = spine.1
EXTRA_DIST = spine.1 EXTRA_DIST = spine.1 uthash.h
# Docker targets — require Dockerfile and Dockerfile.dev (from PR #401) # Docker targets — require Dockerfile and Dockerfile.dev (from PR #401)
.PHONY: docker docker-dev verify cppcheck .PHONY: docker docker-dev verify cppcheck
+2
View File
@@ -159,4 +159,6 @@
# include <grp.h> # include <grp.h>
#endif #endif
#include "uthash.h"
#endif /* SPINE_COMMON_H */ #endif /* SPINE_COMMON_H */
+47 -65
View File
@@ -86,6 +86,7 @@
#include "common.h" #include "common.h"
#include "spine.h" #include "spine.h"
#include <spawn.h>
/* An instance of this struct is created for each popen() fd. */ /* An instance of this struct is created for each popen() fd. */
static struct pid static struct pid
@@ -126,7 +127,8 @@ int nft_popen(const char * command, const char * type) {
struct pid *cur; struct pid *cur;
struct pid *p; struct pid *p;
int pdes[2]; int pdes[2];
int fd, pid, twoway; int fd, twoway;
pid_t pid;
char *argv[4]; char *argv[4];
char *command_copy; char *command_copy;
char shell_cmd[] = "sh"; char shell_cmd[] = "sh";
@@ -180,90 +182,70 @@ int nft_popen(const char * command, const char * type) {
*/ */
pthread_mutex_lock(&ListMutex); pthread_mutex_lock(&ListMutex);
/* Fork. */ /* Build file actions for posix_spawn to replace vfork+execve. */
retry: posix_spawn_file_actions_t fa;
switch (pid = vfork()) { if (posix_spawn_file_actions_init(&fa) != 0) {
case -1: /* Error. */ SPINE_LOG(("ERROR: SCRIPT: posix_spawn_file_actions_init failed"));
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"));
}
(void)close(pdes[0]); (void)close(pdes[0]);
(void)close(pdes[1]); (void)close(pdes[1]);
pthread_mutex_unlock(&ListMutex); pthread_mutex_unlock(&ListMutex);
free(command_copy); free(command_copy);
pthread_setcancelstate(cancel_state, NULL); pthread_setcancelstate(cancel_state, NULL);
return -1; return -1;
/* NOTREACHED */ }
case 0: /* Child. */
if (*type == 'r') { if (*type == 'r') {
/* The dup2() to STDIN_FILENO is repeated to avoid posix_spawn_file_actions_addclose(&fa, pdes[0]);
* 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) { if (pdes[1] != STDOUT_FILENO) {
(void)dup2(pdes[1], STDOUT_FILENO); posix_spawn_file_actions_adddup2(&fa, pdes[1], STDOUT_FILENO);
(void)close(pdes[1]); posix_spawn_file_actions_addclose(&fa, pdes[1]);
if (twoway) if (twoway)
(void)dup2(STDOUT_FILENO, STDIN_FILENO); posix_spawn_file_actions_adddup2(&fa, STDOUT_FILENO, STDIN_FILENO);
}else if (twoway && (pdes[1] != STDIN_FILENO)) } else if (twoway && (pdes[1] != STDIN_FILENO)) {
(void)dup2(pdes[1], STDIN_FILENO); posix_spawn_file_actions_adddup2(&fa, pdes[1], STDIN_FILENO);
}else { }
} else {
if (pdes[0] != STDIN_FILENO) { if (pdes[0] != STDIN_FILENO) {
(void)dup2(pdes[0], STDIN_FILENO); posix_spawn_file_actions_adddup2(&fa, pdes[0], STDIN_FILENO);
(void)close(pdes[0]); posix_spawn_file_actions_addclose(&fa, pdes[0]);
} }
(void)close(pdes[1]); posix_spawn_file_actions_addclose(&fa, pdes[1]);
} }
/* Close all the other pipes in the child process. /* Close all other pipes in the child (Posix.2 requirement). */
* Posix.2 requires this, tho I don't know why.
*/
for (p = PidList; p; p = p->next) for (p = PidList; p; p = p->next)
(void)close(p->fd); posix_spawn_file_actions_addclose(&fa, p->fd);
/* Execute the command. */ /* Spawn the child process with retry on EAGAIN/ENOMEM. */
#if defined(__CYGWIN__) #if defined(__CYGWIN__)
if (set.cygwinshloc == 0) { const char *spawn_shell = (set.cygwinshloc == 0) ? "sh.exe" : "/bin/sh";
execve("sh.exe", argv, environ);
}else{
execve("/bin/sh", argv, environ);
}
#else #else
execve("/bin/sh", argv, environ); const char *spawn_shell = "/bin/sh";
#endif #endif
_exit(127);
/* NOTREACHED */ 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. */ /* Parent. */
if (*type == 'r') { if (*type == 'r') {
fd = pdes[0]; fd = pdes[0];
+49 -24
View File
@@ -267,6 +267,9 @@ void poll_host(int device_counter, int host_id, int host_thread, int host_thread
limits[0] = '\0'; 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 */ /* single polling interval query for items */
if (set.poller_id == 0) { if (set.poller_id == 0) {
if (set.total_snmp_ports == 1) { 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, " "snmp_version, snmp_username, snmp_password, "
"rrd_name, rrd_path, arg1, arg2, arg3, local_data_id, " "rrd_name, rrd_path, arg1, arg2, arg3, local_data_id, "
"rrd_num, snmp_port, snmp_timeout, " "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" " FROM poller_item"
" WHERE host_id = %i" " WHERE host_id = %i"
" AND deleted = '' %s", host_id, limits); " AND deleted = '' %s", regex_col, host_id, limits);
} else { } else {
snprintf(query1, BUFSIZE, snprintf(query1, BUFSIZE,
"SELECT SQL_NO_CACHE action, hostname, snmp_community, " "SELECT SQL_NO_CACHE action, hostname, snmp_community, "
"snmp_version, snmp_username, snmp_password, " "snmp_version, snmp_username, snmp_password, "
"rrd_name, rrd_path, arg1, arg2, arg3, local_data_id, " "rrd_name, rrd_path, arg1, arg2, arg3, local_data_id, "
"rrd_num, snmp_port, snmp_timeout, " "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" " FROM poller_item"
" WHERE host_id = %i" " WHERE host_id = %i"
" AND deleted = ''" " 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 */ /* 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, " "snmp_version, snmp_username, snmp_password, "
"rrd_name, rrd_path, arg1, arg2, arg3, local_data_id, " "rrd_name, rrd_path, arg1, arg2, arg3, local_data_id, "
"rrd_num, snmp_port, snmp_timeout, " "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" " FROM poller_item"
" WHERE host_id = %i" " WHERE host_id = %i"
" AND rrd_next_step <= 0" " AND rrd_next_step <= 0"
" %s", host_id, limits); " %s", regex_col, host_id, limits);
} else { } else {
snprintf(query5, BUFSIZE, snprintf(query5, BUFSIZE,
"SELECT SQL_NO_CACHE action, hostname, snmp_community, " "SELECT SQL_NO_CACHE action, hostname, snmp_community, "
"snmp_version, snmp_username, snmp_password, " "snmp_version, snmp_username, snmp_password, "
"rrd_name, rrd_path, arg1, arg2, arg3, local_data_id, " "rrd_name, rrd_path, arg1, arg2, arg3, local_data_id, "
"rrd_num, snmp_port, snmp_timeout, " "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" " FROM poller_item"
" WHERE host_id = %i" " WHERE host_id = %i"
" AND rrd_next_step <= 0" " AND rrd_next_step <= 0"
" ORDER BY snmp_port %s", host_id, limits); " ORDER BY snmp_port %s", regex_col, host_id, limits);
} }
} else { } else {
if (set.total_snmp_ports == 1) { 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, " "snmp_version, snmp_username, snmp_password, "
"rrd_name, rrd_path, arg1, arg2, arg3, local_data_id, " "rrd_name, rrd_path, arg1, arg2, arg3, local_data_id, "
"rrd_num, snmp_port, snmp_timeout, " "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" " FROM poller_item"
" WHERE host_id = %i" " WHERE host_id = %i"
" %s", host_id, limits); " %s", regex_col, host_id, limits);
} else { } else {
snprintf(query5, BUFSIZE, snprintf(query5, BUFSIZE,
"SELECT SQL_NO_CACHE action, hostname, snmp_community, " "SELECT SQL_NO_CACHE action, hostname, snmp_community, "
"snmp_version, snmp_username, snmp_password, " "snmp_version, snmp_username, snmp_password, "
"rrd_name, rrd_path, arg1, arg2, arg3, local_data_id, " "rrd_name, rrd_path, arg1, arg2, arg3, local_data_id, "
"rrd_num, snmp_port, snmp_timeout, " "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" " FROM poller_item"
" WHERE host_id = %i" " 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, " "snmp_version, snmp_username, snmp_password, "
"rrd_name, rrd_path, arg1, arg2, arg3, local_data_id, " "rrd_name, rrd_path, arg1, arg2, arg3, local_data_id, "
"rrd_num, snmp_port, snmp_timeout, " "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" " FROM poller_item"
" WHERE host_id = %i" " 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 { } else {
snprintf(query1, BUFSIZE, snprintf(query1, BUFSIZE,
"SELECT SQL_NO_CACHE action, hostname, snmp_community, " "SELECT SQL_NO_CACHE action, hostname, snmp_community, "
"snmp_version, snmp_username, snmp_password, " "snmp_version, snmp_username, snmp_password, "
"rrd_name, rrd_path, arg1, arg2, arg3, local_data_id, " "rrd_name, rrd_path, arg1, arg2, arg3, local_data_id, "
"rrd_num, snmp_port, snmp_timeout, " "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" " FROM poller_item"
" WHERE host_id = %i" " WHERE host_id = %i"
" AND poller_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 */ /* 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, " "snmp_version, snmp_username, snmp_password, "
"rrd_name, rrd_path, arg1, arg2, arg3, local_data_id, " "rrd_name, rrd_path, arg1, arg2, arg3, local_data_id, "
"rrd_num, snmp_port, snmp_timeout, " "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" " FROM poller_item"
" WHERE host_id = %i" " WHERE host_id = %i"
" AND rrd_next_step <= 0" " 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 { } else {
snprintf(query5, BUFSIZE, snprintf(query5, BUFSIZE,
"SELECT SQL_NO_CACHE action, hostname, snmp_community, " "SELECT SQL_NO_CACHE action, hostname, snmp_community, "
"snmp_version, snmp_username, snmp_password, " "snmp_version, snmp_username, snmp_password, "
"rrd_name, rrd_path, arg1, arg2, arg3, local_data_id, " "rrd_name, rrd_path, arg1, arg2, arg3, local_data_id, "
"rrd_num, snmp_port, snmp_timeout, " "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" " FROM poller_item"
" WHERE host_id = %i" " WHERE host_id = %i"
" AND rrd_next_step <= 0" " AND rrd_next_step <= 0"
" AND poller_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);
} }
} else { } else {
if (set.total_snmp_ports == 1) { 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, " "snmp_version, snmp_username, snmp_password, "
"rrd_name, rrd_path, arg1, arg2, arg3, local_data_id, " "rrd_name, rrd_path, arg1, arg2, arg3, local_data_id, "
"rrd_num, snmp_port, snmp_timeout, " "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" " FROM poller_item"
" WHERE host_id = %i" " 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 { } else {
snprintf(query5, BUFSIZE, snprintf(query5, BUFSIZE,
"SELECT SQL_NO_CACHE action, hostname, snmp_community, " "SELECT SQL_NO_CACHE action, hostname, snmp_community, "
"snmp_version, snmp_username, snmp_password, " "snmp_version, snmp_username, snmp_password, "
"rrd_name, rrd_path, arg1, arg2, arg3, local_data_id, " "rrd_name, rrd_path, arg1, arg2, arg3, local_data_id, "
"rrd_num, snmp_port, snmp_timeout, " "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" " FROM poller_item"
" WHERE host_id = %i" " WHERE host_id = %i"
" AND poller_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].arg3[0] = '\0';
poller_items[i].local_data_id = 0; poller_items[i].local_data_id = 0;
poller_items[i].rrd_num = 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]); 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, if (row[19] != NULL) snprintf(poller_items[i].snmp_engine_id,
sizeof(poller_items[i].snmp_engine_id), "%s", row[19]); 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); SET_UNDEFINED(poller_items[i].result);
i++; 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); snprintf(poller_items[snmp_oids[j].array_position].result, RESULTS_BUFFER, "%s", snmp_oids[j].result);
thread_end = get_time_as_double(); thread_end = get_time_as_double();
+2
View File
@@ -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_init(void);
extern void snmp_spine_close(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); 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);
+7 -1
View File
@@ -327,6 +327,7 @@ int main(int argc, char *argv[]) {
set.parent_fork = SPINE_PARENT; set.parent_fork = SPINE_PARENT;
set.mode = REMOTE_ONLINE; set.mode = REMOTE_ONLINE;
set.has_device_0 = FALSE; set.has_device_0 = FALSE;
set.has_output_regex = FALSE;
for (argv++; *argv; argv++) { for (argv++; *argv; argv++) {
char *arg = *argv; char *arg = *argv;
@@ -562,6 +563,12 @@ int main(int argc, char *argv[]) {
} }
db_free_result(result); 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 */ /* 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,'NO_ZERO_DATE', ''))");
db_insert(&mysql, LOCAL, "SET SESSION sql_mode = (SELECT REPLACE(@@sql_mode,'ONLY_FULL_GROUP_BY', ''))"); 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(threads);
SPINE_FREE(ids); SPINE_FREE(ids);
SPINE_FREE(conf_file); SPINE_FREE(conf_file);
SPINE_FREE(debug_devices);
SPINE_FREE(host_time); SPINE_FREE(host_time);
SPINE_FREE(php_processes); SPINE_FREE(php_processes);
+2
View File
@@ -366,6 +366,7 @@ typedef struct config_struct {
int end_host_id; int end_host_id;
char host_id_list[BIG_BUFSIZE]; char host_id_list[BIG_BUFSIZE];
int has_device_0; int has_device_0;
int has_output_regex;
/* database connection information */ /* database connection information */
char db_host[BUFSIZE]; char db_host[BUFSIZE];
char db_db[BUFSIZE]; char db_db[BUFSIZE];
@@ -469,6 +470,7 @@ typedef struct target_struct {
char arg1[1024]; char arg1[1024];
char arg2[255]; char arg2[255];
char arg3[255]; char arg3[255];
char output_regex[255];
} target_t; } target_t;
/*! SNMP OID's Structure /*! SNMP OID's Structure
+310
View File
@@ -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 ]]
+250
View File
@@ -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 ]]
+213
View File
@@ -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 ]]
+2 -2
View File
@@ -21,7 +21,7 @@ CREATE TABLE IF NOT EXISTS `host` (
`snmp_version` tinyint(1) unsigned NOT NULL DEFAULT 1, `snmp_version` tinyint(1) unsigned NOT NULL DEFAULT 1,
`snmp_username` varchar(50) NOT NULL DEFAULT '', `snmp_username` varchar(50) NOT NULL DEFAULT '',
`snmp_password` 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_passphrase` varchar(200) NOT NULL DEFAULT '',
`snmp_priv_protocol` char(6) NOT NULL DEFAULT '', `snmp_priv_protocol` char(6) NOT NULL DEFAULT '',
`snmp_context` varchar(64) 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_version` tinyint(1) unsigned NOT NULL DEFAULT 0,
`snmp_username` varchar(50) NOT NULL DEFAULT '', `snmp_username` varchar(50) NOT NULL DEFAULT '',
`snmp_password` 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_passphrase` varchar(200) NOT NULL DEFAULT '',
`snmp_priv_protocol` char(6) NOT NULL DEFAULT '', `snmp_priv_protocol` char(6) NOT NULL DEFAULT '',
`snmp_context` varchar(64) NOT NULL DEFAULT '', `snmp_context` varchar(64) NOT NULL DEFAULT '',
+2
View File
@@ -4,6 +4,8 @@
# Allow the SNMPv3 test user read access with auth+priv # Allow the SNMPv3 test user read access with auth+priv
rouser testuser priv rouser testuser priv
# Allow SNMPv2c read access for integration tests
rocommunity public
# Suppress TCP wrappers log noise # Suppress TCP wrappers log noise
dontLogTCPWrappersConnects yes dontLogTCPWrappersConnects yes
+62
View File
@@ -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)
+386
View File
@@ -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(&regex, exp, 0);
if (reti) {
return value;
}
reti = regexec(&regex, 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(&regex);
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);
}
+1136
View File
File diff suppressed because it is too large. Load diff