diff --git a/Makefile.am b/Makefile.am index 9dfa24b..2f130a8 100644 --- a/Makefile.am +++ b/Makefile.am @@ -18,3 +18,25 @@ libpinelog_la_SOURCES = pinelog.c libpinelog_la_CFLAGS = @PINELOG_CFLAGS@ $(WARN_CFLAGS) libpinelog_la_LDFLAGS = $(WARN_LDFLAGS) + +test_SRCFILES = test_pinelog.c $(libpinelog_la_SOURCES) +test_CFLAGS = \ + -DPINELOG_FATAL_STR='"F"' \ + -DPINELOG_ERROR_STR='"E"' \ + -DPINELOG_WARNING_STR='"W"' \ + -DPINELOG_INFO_STR='"I"' \ + -DPINELOG_DEBUG_STR='"D"' \ + -DPINELOG_TRACE_STR='"T"' \ + -DPINELOG_DEFAULT_LEVEL=PINELOG_LVL_TRACE \ + -DPINELOG_DEFAULT_STREAM=stderr \ + -DPINELOG_TEST + +LOG_DRIVER = env AM_TAP_AWK='$(AWK)' $(SHELL) $(top_srcdir)/tap-driver.sh +TESTS = \ + test_ts_lvl_tr + +check_PROGRAMS = $(TESTS) +test_ts_lvl_tr_SOURCES = $(test_SRCFILES) +test_ts_lvl_tr_CFLAGS = $(WARN_CFLAGS) $(test_CFLAGS) \ + -DPINELOG_SHOW_DATE=1 -DPINELOG_SHOW_LEVEL=1 -DPINELOG_SHOW_BACKTRACE=1 +test_ts_lvl_tr_LDFLAGS = $(WARN_LDFLAGS) diff --git a/configure.ac b/configure.ac index 9c3ee02..133e102 100644 --- a/configure.ac +++ b/configure.ac @@ -10,6 +10,7 @@ AM_INIT_AUTOMAKE([-Wall foreign subdir-objects]) AC_REQUIRE_AUX_FILE([tap-driver.sh]) AC_PROG_CC AC_PROG_CC_STDC +AC_PROG_AWK AM_PROG_AR LT_INIT PKG_PROG_PKG_CONFIG diff --git a/pinelog.c b/pinelog.c index ded5abc..1c793c5 100644 --- a/pinelog.c +++ b/pinelog.c @@ -43,28 +43,28 @@ /********************************************************************** * Configure level strings *********************************************************************/ -#ifndef PINELOG_FATAL -#define PINELOG_FATAL "FATAL" +#ifndef PINELOG_FATAL_STR +#define PINELOG_FATAL_STR "FATAL" #endif -#ifndef PINELOG_ERROR -#define PINELOG_ERROR "ERROR" +#ifndef PINELOG_ERROR_STR +#define PINELOG_ERROR_STR "ERROR" #endif -#ifndef PINELOG_WARNING -#define PINELOG_WARNING "WARNING" +#ifndef PINELOG_WARNING_STR +#define PINELOG_WARNING_STR "WARNING" #endif -#ifndef PINELOG_INFO -#define PINELOG_INFO "INFO" +#ifndef PINELOG_INFO_STR +#define PINELOG_INFO_STR "INFO" #endif -#ifndef PINELOG_DEBUG -#define PINELOG_DEBUG "DEBUG" +#ifndef PINELOG_DEBUG_STR +#define PINELOG_DEBUG_STR "DEBUG" #endif -#ifndef PINELOG_TRACE -#define PINELOG_TRACE "TRACE" +#ifndef PINELOG_TRACE_STR +#define PINELOG_TRACE_STR "TRACE" #endif /********************************************************************** @@ -103,6 +103,13 @@ int pinelog_set_output_stream(FILE *stream) return 0; } +#ifdef PINELOG_TEST +FILE * pinelog_get_output_stream(void) +{ + return output_stream; +} +#endif + int pinelog_set_output_file(const char *file) { FILE *stream; @@ -151,41 +158,46 @@ void pinelog_log_message(int level, const char *file, int line, const char *fmt, level = PINELOG_LVL_TRACE; } - /* Validate and set output stream */ + #if !HAVE_FUNC_ATTRIBUTE_CONSTRUCTOR + /* + * Validate and set output stream. Only necessary if the compiler doesn't + * support the constructor attribute + */ if (output_stream == NULL) { - output_stream = stdout; + output_stream = PINELOG_DEFAULT_STREAM; } + #endif #if PINELOG_SHOW_DATE do { time_t t; struct tm *tmp; - char date_string[20]; + char date_string[30]; t = time(NULL); tmp = localtime(&t); strftime(date_string, sizeof(date_string), "%F %T ", tmp); - fputs(date_string, out_stream); + fputs(date_string, output_stream); } while (0); #endif #if PINELOG_SHOW_LEVEL do { static const char *level_strings[] = { - PINELOG_FATAL, - PINELOG_ERROR, - PINELOG_WARNING, - PINELOG_INFO, - PINELOG_DEBUG, - PINELOG_TRACE, + PINELOG_FATAL_STR, + PINELOG_ERROR_STR, + PINELOG_WARNING_STR, + PINELOG_INFO_STR, + PINELOG_DEBUG_STR, + PINELOG_TRACE_STR, }; - fputs(level_strings[level], out_stream); - fputs(": ", out_stream); - } while (0) + fputs(level_strings[level], output_stream); + fputs(": ", output_stream); + } while (0); #endif #if PINELOG_SHOW_BACKTRACE - fprintf(out_stream, "%s:%d ", file, line); + fprintf(output_stream, "%s:%d ", file, line); #endif va_start(ap, fmt); diff --git a/pinelog.h b/pinelog.h index b7bc0ff..0020ad9 100644 --- a/pinelog.h +++ b/pinelog.h @@ -20,7 +20,9 @@ #include "config.h" #include +#ifndef PINELOG_TEST #include +#endif #ifdef __cplusplus extern "C" { @@ -60,6 +62,15 @@ enum { */ void pinelog_set_defaults(void); +#ifdef PINELOG_TEST +/** + * @brief Get the pointer to the output stream. Only used in test harness. + * + * @returns FILE pointer to output stream + */ +FILE * pinelog_get_output_stream(void); +#endif + /** * @brief Set the output stream. Must be a FILE pointer. * @@ -113,11 +124,16 @@ __attribute__((format(printf, 4, 5))) void pinelog_log_message(int level, const char *file, int line, const char *fmt, ...); +// Test harness will redefine pinelog_exit +#ifndef PINELOG_TEST +#define pinelog_exit exit +#endif + #define PINELOG_FATAL(fmt, ...) do { \ if (PINELOG_LVL_FATAL <= pinelog_get_level()) { \ pinelog_log_message(PINELOG_LVL_FATAL, __FILE__, __LINE__, fmt, ##__VA_ARGS__); \ } \ - exit(1); \ + pinelog_exit(1); \ } while (0) #define PINELOG_ERROR(fmt, ...) do { \ diff --git a/test_pinelog.c b/test_pinelog.c new file mode 100644 index 0000000..dd568a8 --- /dev/null +++ b/test_pinelog.c @@ -0,0 +1,194 @@ +/* + * Pinelog lightweight logging library - test harness + * + * Copyright (C) 2021 Nirenjan Krishnan (nirenjan@nirenjan.org) + * + * SPDX-License-Identifier: MIT + */ + +#include "pinelog.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +/********************************************************************** + * Global variables + *********************************************************************/ +// Test ID (of current test case) +static unsigned int test_id; + +// Observed output stream +static FILE *observed_stream_w; +static FILE *observed_stream_r; + +// Temporary pipe for observed data +static char observed_fifo[NAME_MAX]; + +// Buffer for expected output +static char expected_output[1024]; +static size_t expected_len; + +static void test_case(const char *desc, bool test) +{ + test_id++; + if (test) { + printf("ok %u %s\n", test_id, desc); + } else { + printf("not ok %u %s\n", test_id, desc); + } +} + +static void pinelog_exit(int status) +{ + fprintf(observed_stream_w, "EXIT(%d)\n", status); + expected_len += snprintf(&expected_output[expected_len], + sizeof(expected_output) - expected_len, + "EXIT(%d)\n", status); +} + +static void dump_data(const char *type, size_t len, char *data) +{ + char *line; + printf("# %s (%lu bytes):\n", type, len); + line = strtok(data, "\n"); + while (line != NULL) { + printf("#\t%s\n", line); + line = strtok(NULL, "\n"); + } + printf("\n"); +} + +static void test_setup(int level, int filter, const char *file, int line, const char *fmt, ...) +{ + va_list ap; + + expected_len = 0; + memset(expected_output, 0, sizeof(expected_output)); + + if (level <= filter) { + if (PINELOG_SHOW_DATE) { + time_t t; + struct tm *tmp; + + t = time(NULL); + tmp = localtime(&t); + expected_len += strftime(&expected_output[expected_len], + sizeof(expected_output) - expected_len, + "%F %T ", tmp); + } + + if (PINELOG_SHOW_LEVEL) { + const char * level_string[] = { + PINELOG_FATAL_STR, + PINELOG_ERROR_STR, + PINELOG_WARNING_STR, + PINELOG_INFO_STR, + PINELOG_DEBUG_STR, + PINELOG_TRACE_STR, + }; + expected_len += snprintf(&expected_output[expected_len], + sizeof(expected_output) - expected_len, + "%s: ", level_string[level]); + } + + if (PINELOG_SHOW_BACKTRACE) { + expected_len += snprintf(&expected_output[expected_len], + sizeof(expected_output) - expected_len, + "%s:%d ", file, line); + } + + va_start(ap, fmt); + expected_len += vsnprintf(&expected_output[expected_len], + sizeof(expected_output) - expected_len, + fmt, ap); + va_end(ap); + expected_output[expected_len] = '\n'; + expected_len++; + } +} + +static void test_teardown(const char *desc) +{ + // Compare the output + static char observed[1024]; + size_t observed_len; + int result; + + observed_len = fread(observed, 1, sizeof(observed), observed_stream_r); + + result = ((expected_len == observed_len) && + (memcmp(expected_output, observed, expected_len) == 0)); + test_case(desc, result); + if (!result) { + dump_data("expected", expected_len, expected_output); + dump_data("observed", observed_len, observed); + } +} + +static void verify_defaults(void) +{ + test_case("Get default output stream", + pinelog_get_output_stream() == PINELOG_DEFAULT_STREAM); + test_case("Get default logging level", + pinelog_get_level() == PINELOG_DEFAULT_LEVEL); +} + +#define PINELOG_WARNING PINELOG_WARN + +#define TEST_LOG(lvl, filter, fmt, ...) do { \ + test_setup(PINELOG_LVL_ ## lvl, PINELOG_LVL_ ## filter, \ + __FILE__, __LINE__, fmt, ##__VA_ARGS__); \ + PINELOG_ ## lvl (fmt, ##__VA_ARGS__); \ + test_teardown("Log " #lvl " filter " #filter); \ +} while(0) + +#define TEST(filter, fmt, ...) do { \ + pinelog_set_level(PINELOG_LVL_ ## filter); \ + TEST_LOG(TRACE, filter, fmt, ##__VA_ARGS__); \ + TEST_LOG(DEBUG, filter, fmt, ##__VA_ARGS__); \ + TEST_LOG(INFO, filter, fmt, ##__VA_ARGS__); \ + TEST_LOG(WARNING, filter, fmt, ##__VA_ARGS__); \ + TEST_LOG(ERROR, filter, fmt, ##__VA_ARGS__); \ + TEST_LOG(FATAL, filter, fmt, ##__VA_ARGS__); \ +} while (0) + +int main(int argc, char **argv) +{ + int fifo_fd_r, fifo_fd_w; + snprintf(observed_fifo, sizeof(observed_fifo), "%s.fifo", argv[0]); + mkfifo(observed_fifo, 0777); + + fifo_fd_r = open(observed_fifo, O_RDONLY | O_NONBLOCK); + fifo_fd_w = open(observed_fifo, O_WRONLY | O_NONBLOCK); + observed_stream_r = fdopen(fifo_fd_r, "r"); + observed_stream_w = fdopen(fifo_fd_w, "w"); + + verify_defaults(); + + pinelog_set_output_stream(observed_stream_w); + TEST(TRACE, "testing %s... %d, %f, %u", "testing", -1, 0.0, 1); + TEST(DEBUG, "testing %s... %d, %f, %u", "testing", -1, 0.0, 1); + TEST(INFO, "testing %s... %d, %f, %u", "testing", -1, 0.0, 1); + TEST(WARNING, "testing %s... %d, %f, %u", "testing", -1, 0.0, 1); + TEST(ERROR, "testing %s... %d, %f, %u", "testing", -1, 0.0, 1); + TEST(FATAL, "testing %s... %d, %f, %u", "testing", -1, 0.0, 1); + TEST(NONE, "testing %s... %d, %f, %u", "testing", -1, 0.0, 1); + + printf("1..%u\n", test_id); + + fclose(observed_stream_w); + fclose(observed_stream_r); + close(fifo_fd_w); + close(fifo_fd_r); + unlink(observed_fifo); + + return 0; +}