Files

345 lines
11 KiB
C

/* Host tests for libonion_platform logging: sinks, levels, rotation. */
#include "test_harness.h"
#include <onion/log.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <unistd.h>
/* --- helpers -------------------------------------------------------------- */
static char g_path[64];
static char g_crash_path[72];
static void cleanup_log_files(void) {
char rotated[80];
unlink(g_path);
for (unsigned i = 1; i <= ONION_LOG_DEFAULT_ROTATE_COUNT + 1; ++i) {
snprintf(rotated, sizeof(rotated), "%s.%u", g_path, i);
unlink(rotated);
}
/* Clean up artifacts produced by the previous one-generation policy. */
snprintf(rotated, sizeof(rotated), "%s.old", g_path);
unlink(rotated);
unlink(g_crash_path);
}
static void begin(const char *tag) {
snprintf(g_path, sizeof(g_path), "/tmp/onion-log-test-%d", (int)getpid());
snprintf(g_crash_path, sizeof(g_crash_path), "%s-crash", g_path);
cleanup_log_files();
onion_log_set_max_bytes(0); /* restore default cap */
onion_log_set_level(ONION_LOG_TRACE);
onion_log_configure(tag, g_path);
}
static void end(void) {
onion_log_configure_crash(NULL);
onion_log_configure("OnionHEN", NULL);
cleanup_log_files();
onion_log_set_level(ONION_LOG_INFO);
onion_log_set_max_bytes(0);
}
static size_t read_file(const char *path, char *out, size_t out_size) {
FILE *f = fopen(path, "r");
if (!f) {
out[0] = '\0';
return 0;
}
size_t n = fread(out, 1, out_size - 1, f);
fclose(f);
out[n] = '\0';
return n;
}
static long file_size(const char *path) {
struct stat st;
return (stat(path, &st) == 0) ? (long)st.st_size : -1;
}
/* --- tests ---------------------------------------------------------------- */
static int test_log_file_sink(void) {
char buf[512];
begin("HostTest");
LOG_INFO("hello %d", 42);
read_file(g_path, buf, sizeof(buf));
TEST_ASSERT_TRUE(strstr(buf, "hello 42") != NULL);
TEST_ASSERT_TRUE(strstr(buf, "[HostTest]") != NULL);
TEST_ASSERT_TRUE(strstr(buf, "info:") != NULL);
TEST_ASSERT_TRUE(strchr(buf, '\n') != NULL);
end();
return 0;
}
/* A daemon restart must append to the previous run instead of truncating it. */
static int test_configure_appends_existing_log(void) {
char buf[512];
snprintf(g_path, sizeof(g_path), "/tmp/onion-log-test-%d", (int)getpid());
cleanup_log_files();
FILE *stale = fopen(g_path, "w");
TEST_ASSERT_TRUE(stale != NULL);
fputs("previous-run\n", stale);
fclose(stale);
onion_log_set_level(ONION_LOG_INFO);
onion_log_configure("Append", g_path);
LOG_INFO("current-run");
TEST_ASSERT_TRUE(file_size(g_path) >= 0);
read_file(g_path, buf, sizeof(buf));
TEST_ASSERT_TRUE(strstr(buf, "previous-run") != NULL);
TEST_ASSERT_TRUE(strstr(buf, "current-run") != NULL);
end();
return 0;
}
static int test_log_configure_tag_only(void) {
onion_log_configure("TagOnly", NULL);
LOG_INFO("no file sink configured"); /* must not crash */
onion_log_configure("OnionHEN", NULL);
return 0;
}
/* The runtime gate must drop records below the threshold. */
static int test_runtime_level_filters(void) {
char buf[1024];
begin("Lvl");
onion_log_set_level(ONION_LOG_WARN);
onion_log_write(ONION_LOG_ERROR, "keep-error");
onion_log_write(ONION_LOG_WARN, "keep-warn");
onion_log_write(ONION_LOG_INFO, "drop-info");
onion_log_write(ONION_LOG_DEBUG, "drop-debug");
LOG_INFO("drop-legacy-info");
read_file(g_path, buf, sizeof(buf));
TEST_ASSERT_TRUE(strstr(buf, "keep-error") != NULL);
TEST_ASSERT_TRUE(strstr(buf, "keep-warn") != NULL);
TEST_ASSERT_TRUE(strstr(buf, "drop-info") == NULL);
TEST_ASSERT_TRUE(strstr(buf, "drop-debug") == NULL);
TEST_ASSERT_TRUE(strstr(buf, "drop-legacy-info") == NULL);
end();
return 0;
}
static int test_level_off_silences_everything(void) {
char buf[512];
begin("Off");
onion_log_set_level(ONION_LOG_OFF);
onion_log_write(ONION_LOG_ERROR, "must-not-appear");
LOG_INFO("must-not-appear-either");
read_file(g_path, buf, sizeof(buf));
TEST_ASSERT_TRUE(strstr(buf, "must-not-appear") == NULL);
end();
return 0;
}
/* An unbounded log on a console the user cannot clean out is a real failure
* mode, so the cap must actually rotate and keep a numbered backup. */
static int test_rotation_caps_size_and_keeps_backup(void) {
char rotated_path[80];
char buf[2048];
begin("Rot");
onion_log_set_max_bytes(512);
for (int i = 0; i < 60; i++) {
onion_log_write(ONION_LOG_ERROR, "record-%02d-padding-padding-padding", i);
}
const long live = file_size(g_path);
TEST_ASSERT_TRUE(live >= 0);
TEST_ASSERT_TRUE(live <= 512);
snprintf(rotated_path, sizeof(rotated_path), "%s.1", g_path);
TEST_ASSERT_TRUE(file_size(rotated_path) > 0);
/* Newest records live in the current file. */
read_file(g_path, buf, sizeof(buf));
TEST_ASSERT_TRUE(strstr(buf, "record-59") != NULL);
end();
return 0;
}
/* Rotation keeps .1 as newest through .3 as the oldest retained backup. */
static int test_rotation_keeps_three_backups(void) {
char rotated_path[80];
char buf[2048];
begin("Rot2");
onion_log_set_max_bytes(256);
for (int i = 0; i < 200; i++) {
onion_log_write(ONION_LOG_ERROR, "spam-%03d-aaaaaaaaaaaaaaaaaaaa", i);
}
for (unsigned i = 1; i <= ONION_LOG_DEFAULT_ROTATE_COUNT; ++i) {
snprintf(rotated_path, sizeof(rotated_path), "%s.%u", g_path, i);
TEST_ASSERT_TRUE(file_size(rotated_path) > 0);
}
snprintf(rotated_path, sizeof(rotated_path), "%s.%u", g_path,
ONION_LOG_DEFAULT_ROTATE_COUNT + 1);
TEST_ASSERT_TRUE(file_size(rotated_path) < 0);
snprintf(rotated_path, sizeof(rotated_path), "%s.1", g_path);
read_file(rotated_path, buf, sizeof(buf));
TEST_ASSERT_TRUE(strstr(buf, "spam-000") == NULL); /* oldest rotated away */
end();
return 0;
}
/* Rotation must use the file's real size, including bytes appended outside
* this logger instance after its descriptor was opened. */
static int test_rotation_observes_external_append(void) {
char rotated_path[80];
char buf[1024];
char padding[480];
begin("External");
onion_log_set_max_bytes(512);
memset(padding, 'p', sizeof(padding));
FILE *external = fopen(g_path, "ab");
TEST_ASSERT_TRUE(external != NULL);
TEST_ASSERT_EQ_INT(sizeof(padding), fwrite(padding, 1, sizeof(padding), external));
fclose(external);
onion_log_write(ONION_LOG_ERROR, "after-external-append");
snprintf(rotated_path, sizeof(rotated_path), "%s.1", g_path);
TEST_ASSERT_TRUE(file_size(rotated_path) >= (long)sizeof(padding));
read_file(g_path, buf, sizeof(buf));
TEST_ASSERT_TRUE(strstr(buf, "after-external-append") != NULL);
end();
return 0;
}
/* Exactly one trailing newline regardless of what the caller passed. */
static int test_single_trailing_newline(void) {
char buf[512];
begin("NL");
onion_log_write(ONION_LOG_ERROR, "trailing-newline-supplied\n");
const size_t n = read_file(g_path, buf, sizeof(buf));
TEST_ASSERT_TRUE(n >= 2);
TEST_ASSERT_EQ_INT('\n', buf[n - 1]);
TEST_ASSERT_TRUE(buf[n - 2] != '\n');
end();
return 0;
}
/* Over-long records must truncate, stay terminated, and still end in \n. */
static int test_oversized_record_truncates(void) {
static char big[0x2000];
char buf[0x2000];
memset(big, 'x', sizeof(big) - 1);
big[sizeof(big) - 1] = '\0';
begin("Big");
onion_log_set_max_bytes(1024u * 1024u);
onion_log_write(ONION_LOG_ERROR, "%s", big);
const size_t n = read_file(g_path, buf, sizeof(buf));
TEST_ASSERT_TRUE(n > 0);
TEST_ASSERT_EQ_INT('\n', buf[n - 1]);
TEST_ASSERT_TRUE(n < sizeof(big)); /* truncated, not overflowed */
end();
return 0;
}
static int test_level_name_roundtrip(void) {
onion_log_level lvl;
TEST_ASSERT_TRUE(onion_log_level_from_name("error", &lvl));
TEST_ASSERT_EQ_INT(ONION_LOG_ERROR, lvl);
TEST_ASSERT_TRUE(onion_log_level_from_name("TRACE", &lvl)); /* case-insensitive */
TEST_ASSERT_EQ_INT(ONION_LOG_TRACE, lvl);
TEST_ASSERT_TRUE(onion_log_level_from_name("Off", &lvl));
TEST_ASSERT_EQ_INT(ONION_LOG_OFF, lvl);
TEST_ASSERT_TRUE(!onion_log_level_from_name("verbose", &lvl));
TEST_ASSERT_TRUE(!onion_log_level_from_name("inf", &lvl)); /* prefix != match */
TEST_ASSERT_TRUE(!onion_log_level_from_name("infoo", &lvl));
TEST_ASSERT_TRUE(!onion_log_level_from_name(NULL, &lvl));
TEST_ASSERT_TRUE(strcmp("warn", onion_log_level_name(ONION_LOG_WARN)) == 0);
TEST_ASSERT_TRUE(strcmp("?", onion_log_level_name((onion_log_level)99)) == 0);
return 0;
}
static int test_set_level_clamps(void) {
onion_log_set_level((onion_log_level)99);
TEST_ASSERT_EQ_INT(ONION_LOG_TRACE, onion_log_get_level());
onion_log_set_level((onion_log_level)-5);
TEST_ASSERT_EQ_INT(ONION_LOG_OFF, onion_log_get_level());
onion_log_set_level(ONION_LOG_INFO);
return 0;
}
/* The crash path bypasses both gates — a fault is always worth recording. */
static int test_emergency_bypasses_level(void) {
char buf[512];
begin("Emerg");
onion_log_set_level(ONION_LOG_OFF);
onion_log_emergency("crash-record %d", 7);
read_file(g_path, buf, sizeof(buf));
TEST_ASSERT_TRUE(strstr(buf, "crash-record 7") != NULL);
end();
return 0;
}
/* Crash records use a dedicated append-only sink; ordinary records stay out. */
static int test_emergency_preserves_crash_sink(void) {
char main_buf[512];
char crash_buf[512];
begin("CrashSink");
FILE *previous = fopen(g_crash_path, "w");
TEST_ASSERT_TRUE(previous != NULL);
fputs("previous-crash\n", previous);
fclose(previous);
onion_log_configure_crash(g_crash_path);
LOG_ERROR("ordinary-record");
onion_log_set_level(ONION_LOG_OFF);
onion_log_emergency("fatal-signal %d", 11);
read_file(g_path, main_buf, sizeof(main_buf));
read_file(g_crash_path, crash_buf, sizeof(crash_buf));
TEST_ASSERT_TRUE(strstr(main_buf, "ordinary-record") != NULL);
TEST_ASSERT_TRUE(strstr(main_buf, "fatal-signal 11") != NULL);
TEST_ASSERT_TRUE(strstr(crash_buf, "previous-crash") != NULL);
TEST_ASSERT_TRUE(strstr(crash_buf, "fatal-signal 11") != NULL);
TEST_ASSERT_TRUE(strstr(crash_buf, "ordinary-record") == NULL);
end();
return 0;
}
int test_platform_log_suite(void) {
int failures = 0;
failures += onion_test_run("log.file_sink", test_log_file_sink);
failures += onion_test_run("log.configure_appends",
test_configure_appends_existing_log);
failures += onion_test_run("log.configure_tag_only", test_log_configure_tag_only);
failures += onion_test_run("log.runtime_level_filters", test_runtime_level_filters);
failures += onion_test_run("log.level_off", test_level_off_silences_everything);
failures += onion_test_run("log.rotation_caps_size", test_rotation_caps_size_and_keeps_backup);
failures += onion_test_run("log.rotation_three_backups", test_rotation_keeps_three_backups);
failures += onion_test_run("log.rotation_external_append",
test_rotation_observes_external_append);
failures += onion_test_run("log.single_newline", test_single_trailing_newline);
failures += onion_test_run("log.oversized_truncates", test_oversized_record_truncates);
failures += onion_test_run("log.level_name_roundtrip", test_level_name_roundtrip);
failures += onion_test_run("log.set_level_clamps", test_set_level_clamps);
failures += onion_test_run("log.emergency_bypasses_level", test_emergency_bypasses_level);
failures += onion_test_run("log.emergency_preserves_crash_sink",
test_emergency_preserves_crash_sink);
return failures;
}