Files
spine/tests/unit/test_classify_result.c
Thomas Vincent 9be5f8ad2e fix(results): [develop] normalize output and unify SNMP batch persistence (#641)
Classify numeric, undefined, multipart and Cacti octet results through
one parser, normalize undefined agent values before persistence and
flush every SNMP batch consistently. Validate whole non-negative
uptimes, retain unknown reindex baselines and free script results
through their owning pointer. The mocked poll_host suite checks
session-profile boundaries, spike handling, output_regex input and
completion writes.

Originally step 4/14 of the C17/native-correctness series; its preceding
steps have merged. The current base is develop.

Earlier validation, before this review follow-up, in isolated copied
snapshots on Linux arm64:

- Clean GCC build and make check: 15 passed, 1 existing prerequisite
skips, 0 failures.
- ASan/UBSan: 14 passed, 2 existing privilege/allocator-interposition
prerequisite skips, 0 failures; no sanitizer findings.
- Production source/profile integrity verification against disposable
MariaDB and loopback SNMP fixtures passed.
- Release archive extracted into a separate source tree and built/tested
out of tree; fuzz checks passed.

Earlier local profiles retain source hashes for the copied validation
tree; GitHub checks validate the published commit. No production
database/device, host installation or live privilege change is part of
validation. Existing prerequisite skips were retained; no test or
finding was suppressed to obtain a pass.



Review follow-up (2026-10-09): Restacked on the corrected preceding
branches. The SNMP status fix is introduced in #638, the
reindex/completion fixes in #640, and focused regression coverage in
#650.

Restacked tip validation: isolated Linux arm64 build and make check
passed (18 passed, 1 setuid prerequisite skip, no failures).
GitHub checks validate the updated published commits; earlier
profile/archive/sanitizer evidence above belongs to the previous
revisions.

## Current merge dependencies

Current base: `develop`. Immediate dependency: `develop` (earlier steps
already merged). Next open PR:
[#644](https://github.com/Cacti/spine/pull/644).

Merge order: #641 → #644 → #654 → #646 → #648 → #649 → #650 → #651 →
#655 → #658 → #659.

This repository uses squash-only merges and deletes merged branches.
Record the old parent SHA, squash one PR at a time, then restack and
retarget dependents before merging the next PR. Re-run merge validation
and checks on the resulting heads; an ordinary clean merge does not
replace squash/restack validation.

---------

Signed-off-by: Thomas Vincent <thomasvincent@gmail.com>
2026-10-12 18:39:39 -04:00

189 lines
7.6 KiB
C

/* Table tests for classify_result(), linked against the shipped util.c.
*
* Each row is a raw script or SNMP output, what Spine must decide it is, and
* the exact text that reaches poller_output. An output that cannot be read
* as a whole value must come back as U, never as a guess.
*/
#include <stdarg.h>
#include <stddef.h>
#include <setjmp.h>
#include <cmocka.h>
#include <string.h>
#include <stdlib.h>
#include "common.h"
#include "spine.h"
#include "util.h"
typedef struct {
const char *raw;
result_kind_t kind;
const char *text;
} classify_case_t;
static const classify_case_t cases[] = {
/* integers, including the padding scripts and BSD wc -l emit */
{"42", RESULT_COUNTER, "42"},
{" 42", RESULT_COUNTER, "42"},
{"42\n", RESULT_COUNTER, "42"},
{" 10\n", RESULT_COUNTER, "10"},
{"42\r\n", RESULT_COUNTER, "42"},
{"\"42\"", RESULT_COUNTER, "42"},
{"'42'", RESULT_COUNTER, "42"},
{"+7", RESULT_COUNTER, "+7"},
{"-5", RESULT_SIGNED, "-5"},
{"18446744073709551615", RESULT_COUNTER, "18446744073709551615"},
{"20000000000001", RESULT_COUNTER, "20000000000001"},
{"-9223372036854775808", RESULT_SIGNED, "-9223372036854775808"},
/* beyond 64 bits it is still a number, so the text is kept whole */
{"18446744073709551616", RESULT_FLOAT, "18446744073709551616"},
{"-9223372036854775809", RESULT_FLOAT, "-9223372036854775809"},
/* floats */
{"3.14", RESULT_FLOAT, "3.14"},
{"1.5e3", RESULT_FLOAT, "1.5e3"},
{".5", RESULT_FLOAT, ".5"},
{"-12.5", RESULT_FLOAT, "-12.5"},
{"1e999", RESULT_UNKNOWN, "U"},
/* strtod() accepts these; Cacti does not, and neither may we */
{"0x1A", RESULT_UNKNOWN, "U"},
{"nan", RESULT_UNKNOWN, "U"},
{"NaN", RESULT_UNKNOWN, "U"},
{"inf", RESULT_UNKNOWN, "U"},
{"U", RESULT_UNKNOWN, "U"},
/* hex octet strings, converted without a double in between */
{"1F FF FF FF FF FF FF F1", RESULT_HEX_COUNTER, "2305843009213693937"},
{"20 00 00 00 00 00 01", RESULT_HEX_COUNTER, "9007199254740993"},
{"FF FF FF FF FF FF FF FF", RESULT_HEX_COUNTER, "18446744073709551615"},
{"00 00 00 00 00 00 00 00 01", RESULT_HEX_COUNTER, "1"},
{"AA-BB-CC", RESULT_HEX_COUNTER, "11189196"},
{"\"FF ff\t\"", RESULT_HEX_COUNTER, "65535"},
{"01 FF FF FF FF FF FF FF FF", RESULT_UNKNOWN, "U"},
{"FFFFFFFFFFFFFFFF", RESULT_UNKNOWN, "U"},
{"AA BB CC", RESULT_HEX_COUNTER, "11189196"},
/* Cacti splits octets on space, '-' and ':' only */
{"AA\tBB", RESULT_UNKNOWN, "U"},
{"zz", RESULT_UNKNOWN, "U"},
/* a lone octet is hex once it is not decimal, as in Cacti */
{"FF", RESULT_HEX_COUNTER, "255"},
{"ff", RESULT_HEX_COUNTER, "255"},
{" FF", RESULT_HEX_COUNTER, "255"},
{"0A", RESULT_HEX_COUNTER, "10"},
{"1F", RESULT_HEX_COUNTER, "31"},
{"9E", RESULT_HEX_COUNTER, "158"},
{"00", RESULT_COUNTER, "00"},
/* nothing that could be a value */
{"", RESULT_UNKNOWN, "U"},
{" ", RESULT_UNKNOWN, "U"},
{"---", RESULT_UNKNOWN, "U"},
{"no sample", RESULT_UNKNOWN, "U"},
{"1.2.3", RESULT_UNKNOWN, "U"},
/* Cacti multipart output; it wins over hex, as it always has */
{"a:1 b:2", RESULT_MULTIPART, "a:1 b:2"},
{"a:1 b!2", RESULT_MULTIPART, "a:1 b!2"},
{" a:1 b:2\n", RESULT_MULTIPART, "a:1 b:2"},
{"00:1b:44:11:3a:b7", RESULT_MULTIPART, "00:1b:44:11:3a:b7"},
{"ff:ff:ff:ff:ff:ff:ff:ff", RESULT_MULTIPART, "ff:ff:ff:ff:ff:ff:ff:ff"},
{"AA\tBB:CC", RESULT_MULTIPART, "AA\tBB:CC"},
{"a:1 b:2", RESULT_UNKNOWN, "U"},
/* a number wrapped in text, which Cacti's strip_alpha() also accepts */
{"4096 Bytes", RESULT_COUNTER, "4096"},
{"text -12.5 Bytes", RESULT_FLOAT, "-12.5"},
/* once read as the octets 0x042C */
{"-42C", RESULT_SIGNED, "-42"},
};
static void test_classify_table(void **state) {
(void) state;
for (size_t i = 0; i < sizeof(cases) / sizeof(cases[0]); i++) {
classified_result_t out;
char raw[64];
/* classify a copy, then check the copy is untouched */
strncopy(raw, cases[i].raw, sizeof(raw));
result_kind_t kind = classify_result(raw, &out);
print_message("case %zu: '%s'\n", i, cases[i].raw);
assert_int_equal(kind, cases[i].kind);
assert_int_equal(out.kind, cases[i].kind);
assert_string_equal(out.text, cases[i].text);
assert_string_equal(raw, cases[i].raw);
}
}
static void test_classify_values(void **state) {
classified_result_t out;
(void) state;
assert_int_equal(classify_result("18446744073709551615", &out), RESULT_COUNTER);
assert_true(out.value.counter == UINT64_MAX);
assert_int_equal(classify_result("-9223372036854775808", &out), RESULT_SIGNED);
assert_true(out.value.integer == INT64_MIN);
assert_int_equal(classify_result("1.5e3", &out), RESULT_FLOAT);
assert_true(out.value.real == 1500.0);
/* 2^53 + 1 has no exact double; the octet path must not use one */
assert_int_equal(classify_result("20 00 00 00 00 00 01", &out), RESULT_HEX_COUNTER);
assert_true(out.value.counter == 9007199254740993ULL);
assert_int_equal(classify_result(" 10\n", &out), RESULT_COUNTER);
assert_true(out.value.counter == 10);
assert_int_equal(classify_result(NULL, &out), RESULT_UNKNOWN);
assert_string_equal(out.text, "U");
}
static void test_classify_longer_than_results_buffer(void **state) {
char *raw = malloc(RESULTS_BUFFER * 2);
classified_result_t out;
(void) state;
assert_non_null(raw);
/* Truncating a long value would store a different one. */
memset(raw, '7', RESULTS_BUFFER + 10);
raw[RESULTS_BUFFER + 10] = '\0';
assert_int_equal(classify_result(raw, &out), RESULT_UNKNOWN);
assert_string_equal(out.text, "U");
memset(raw, 'a', RESULTS_BUFFER + 10);
raw[1] = ':';
assert_int_equal(classify_result(raw, &out), RESULT_UNKNOWN);
/* Padding does not count against the buffer. */
memset(raw, ' ', RESULTS_BUFFER + 10);
raw[RESULTS_BUFFER] = '4';
raw[RESULTS_BUFFER + 1] = '2';
assert_int_equal(classify_result(raw, &out), RESULT_COUNTER);
assert_string_equal(out.text, "42");
/* A short value behind a long run of padding is still that value. */
memset(raw, ' ', RESULTS_BUFFER + 10);
raw[RESULTS_BUFFER + 8] = 'f';
raw[RESULTS_BUFFER + 9] = 'f';
assert_int_equal(classify_result(raw, &out), RESULT_HEX_COUNTER);
assert_string_equal(out.text, "255");
/* The longest storable value still fits. */
memset(raw, 'a', RESULTS_BUFFER - 1);
raw[1] = ':';
raw[RESULTS_BUFFER - 1] = '\0';
assert_int_equal(classify_result(raw, &out), RESULT_MULTIPART);
assert_int_equal(strlen(out.text), RESULTS_BUFFER - 1);
free(raw);
}
int main(void) {
const struct CMUnitTest tests[] = {
cmocka_unit_test(test_classify_table),
cmocka_unit_test(test_classify_values),
cmocka_unit_test(test_classify_longer_than_results_buffer),
};
return cmocka_run_group_tests(tests, NULL, NULL);
}