diff options
author | David Robillard <d@drobilla.net> | 2021-01-13 20:00:25 +0100 |
---|---|---|
committer | David Robillard <d@drobilla.net> | 2022-01-14 01:13:45 -0500 |
commit | 0825ceb561b2f52cfa253cb8bb0613896f903363 (patch) | |
tree | 461d338415debf2fa9b489b53db738fbf7f8ddd8 | |
parent | af81ace5f5a8f4bb0df93dd937395c65e92a5b6a (diff) | |
download | serd-0825ceb561b2f52cfa253cb8bb0613896f903363.tar.gz serd-0825ceb561b2f52cfa253cb8bb0613896f903363.tar.bz2 serd-0825ceb561b2f52cfa253cb8bb0613896f903363.zip |
Add extensible logging API
-rw-r--r-- | NEWS | 1 | ||||
-rw-r--r-- | include/serd/serd.h | 216 | ||||
-rw-r--r-- | meson.build | 2 | ||||
-rw-r--r-- | src/log.c | 240 | ||||
-rw-r--r-- | src/n3.c | 113 | ||||
-rw-r--r-- | src/reader.c | 9 | ||||
-rw-r--r-- | src/reader.h | 10 | ||||
-rw-r--r-- | src/serd_config.h | 13 | ||||
-rw-r--r-- | src/serdi.c | 10 | ||||
-rw-r--r-- | src/world.c | 76 | ||||
-rw-r--r-- | src/world.h | 13 | ||||
-rw-r--r-- | src/writer.c | 52 | ||||
-rw-r--r-- | test/meson.build | 19 | ||||
-rw-r--r-- | test/test_log.c | 128 |
14 files changed, 734 insertions, 168 deletions
@@ -2,6 +2,7 @@ serd (1.0.1) unstable; * Add SerdBuffer for mutable buffers to keep SerdChunk const-correct * Add SerdWorld for shared library state + * Add extensible logging API * Add option for writing terse output without newlines * Add support for parsing variables * Add support for writing terse collections diff --git a/include/serd/serd.h b/include/serd/serd.h index b0892573..403e637c 100644 --- a/include/serd/serd.h +++ b/include/serd/serd.h @@ -66,6 +66,14 @@ SERD_API \ SERD_MALLOC_FUNC +#if defined(__MINGW32__) +# define SERD_LOG_FUNC(fmt, a) __attribute__((format(gnu_printf, fmt, a))) +#elif defined(__GNUC__) +# define SERD_LOG_FUNC(fmt, a) __attribute__((format(printf, fmt, a))) +#else +# define SERD_LOG_FUNC(fmt, a) +#endif + #ifdef __cplusplus extern "C" { # if defined(__GNUC__) @@ -1183,23 +1191,6 @@ serd_statement_equals(const SerdStatement* SERD_NULLABLE a, /// Global library state typedef struct SerdWorldImpl SerdWorld; -/// An error description -typedef struct { - SerdStatus status; ///< Error code - const SerdCaret* SERD_NULLABLE caret; ///< File origin of error - const char* SERD_NONNULL fmt; ///< Printf-style format string - va_list* SERD_NONNULL args; ///< Arguments for fmt -} SerdError; - -/** - Callback function for errors. - - @param handle Handle for user data. - @param error Error description. -*/ -typedef SerdStatus (*SerdErrorFunc)(void* SERD_NULLABLE handle, - const SerdError* SERD_NONNULL error); - /** Create a new Serd World. @@ -1237,16 +1228,195 @@ const SerdNode* SERD_NONNULL serd_world_get_blank(SerdWorld* SERD_NONNULL world); /** - Set a function to be called when errors occur. + @} + @defgroup serd_logging Logging + @{ +*/ + +/// Log entry level, compatible with syslog +typedef enum { + SERD_LOG_LEVEL_EMERGENCY, ///< Emergency, system is unusable + SERD_LOG_LEVEL_ALERT, ///< Action must be taken immediately + SERD_LOG_LEVEL_CRITICAL, ///< Critical condition + SERD_LOG_LEVEL_ERROR, ///< Error + SERD_LOG_LEVEL_WARNING, ///< Warning + SERD_LOG_LEVEL_NOTICE, ///< Normal but significant condition + SERD_LOG_LEVEL_INFO, ///< Informational message + SERD_LOG_LEVEL_DEBUG ///< Debug message +} SerdLogLevel; + +/** + A structured log field. + + Fields are used to add metadata to log messages. Syslog-compatible keys + should be used where possible, otherwise, keys should be namespaced to + prevent clashes. + + Serd itself uses the following keys: - The `error_func` will be called with `handle` as its first argument. If - no error function is set, errors are printed to stderr. + - ERRNO - The `errno` of the original system error if any (decimal string) + - SERD_COL - The 1-based column number in the file (decimal string) + - SERD_FILE - The file which caused this message (string) + - SERD_LINE - The 1-based line number in the file (decimal string) +*/ +typedef struct { + const char* SERD_NONNULL key; ///< Field name + const char* SERD_NONNULL value; ///< Field value +} SerdLogField; + +/** + Function for handling log messages. + + By default, the log is printed to `stderr`. This can be overridden by + passing a function of this type to serd_set_log_func(). + + @param handle Pointer to opaque user data. + @param level Log level. + @param n_fields Number of entries in `fields`. + @param fields An array of `n_fields` extra log fields. + @param message Log message. +*/ +typedef SerdStatus (*SerdLogFunc)(void* SERD_NULLABLE handle, + SerdLogLevel level, + size_t n_fields, + const SerdLogField* SERD_NULLABLE fields, + SerdStringView message); + +/// A #SerdLogFunc that does nothing (for suppressing log output) +SERD_CONST_API +SerdStatus +serd_quiet_log_func(void* SERD_NULLABLE handle, + SerdLogLevel level, + size_t n_fields, + const SerdLogField* SERD_NULLABLE fields, + SerdStringView message); + +/** + Set a function to be called with log messages (typically errors). + + If no custom logging function is set, then messages are printed to stderr. + + @param world World that will send log entries to the given function. + + @param log_func Log function to call for every log message. Each call to + this function represents a complete log message with an implicit trailing + newline. + + @param handle Opaque handle that will be passed to every invocation of + `log_func`. */ SERD_API void -serd_world_set_error_func(SerdWorld* SERD_NONNULL world, - SerdErrorFunc SERD_NULLABLE error_func, - void* SERD_NULLABLE handle); +serd_set_log_func(SerdWorld* SERD_NONNULL world, + SerdLogFunc SERD_NULLABLE log_func, + void* SERD_NULLABLE handle); + +/** + Write a message to the log with a `va_list`. + + This is the fundamental and most powerful function for writing entries to + the log, the others are convenience wrappers that ultimately call this. + + This writes a single complete entry to the log, and so may not be used to + print parts of a line like a more general printf-like function. There + should be no trailing newline in `fmt`. Arguments following `fmt` should + correspond to conversion specifiers in the format string as in printf from + the standard C library. + + @param world World to log to. + @param level Log level. + @param n_fields Number of entries in `fields`. + @param fields An array of `n_fields` extra log fields. + @param fmt Format string. + @param args Arguments for `fmt`. + + @return A status code, which is always #SERD_SUCCESS with the default log + function. If a custom log function is set with serd_set_log_func() and it + returns an error, then that error is returned here. +*/ +SERD_API +SERD_LOG_FUNC(5, 0) +SerdStatus +serd_vxlogf(const SerdWorld* SERD_NONNULL world, + SerdLogLevel level, + size_t n_fields, + const SerdLogField* SERD_NULLABLE fields, + const char* SERD_NONNULL fmt, + va_list args); + +/** + Write a message to the log with extra fields. + + This is a convenience wrapper for serd_vxlogf() that takes the format + arguments directly. +*/ +SERD_API +SERD_LOG_FUNC(5, 6) +SerdStatus +serd_xlogf(const SerdWorld* SERD_NONNULL world, + SerdLogLevel level, + size_t n_fields, + const SerdLogField* SERD_NULLABLE fields, + const char* SERD_NONNULL fmt, + ...); + +/** + Write a simple message to the log. + + This is a convenience wrapper for serd_vxlogf() which sets no extra fields. +*/ +SERD_API +SERD_LOG_FUNC(3, 0) +SerdStatus +serd_vlogf(const SerdWorld* SERD_NONNULL world, + SerdLogLevel level, + const char* SERD_NONNULL fmt, + va_list args); + +/** + Write a simple message to the log. + + This is a convenience wrapper for serd_vlogf() that takes the format + arguments directly. +*/ +SERD_API +SERD_LOG_FUNC(3, 4) +SerdStatus +serd_logf(const SerdWorld* SERD_NONNULL world, + SerdLogLevel level, + const char* SERD_NONNULL fmt, + ...); + +/** + Write a message to the log with a caret position. + + This is a convenience wrapper for serd_vxlogf() which sets `SERD_FILE`, + `SERD_LINE`, and `SERD_COL` to the position of the given caret. Entries are + typically printed with a GCC-style prefix like "file.ttl:16:4". +*/ +SERD_API +SERD_LOG_FUNC(4, 0) +SerdStatus +serd_vlogf_at(const SerdWorld* SERD_NONNULL world, + SerdLogLevel level, + const SerdCaret* SERD_NULLABLE caret, + const char* SERD_NONNULL fmt, + va_list args); + +/** + Write a message to the log with a caret position. + + This is a convenience wrapper for serd_vlogf_at() that takes the format + arguments directly. +*/ +SERD_API +SERD_LOG_FUNC(4, 5) +SerdStatus +serd_logf_at(const SerdWorld* SERD_NONNULL world, + SerdLogLevel level, + const SerdCaret* SERD_NULLABLE caret, + const char* SERD_NONNULL fmt, + ...); /** @} diff --git a/meson.build b/meson.build index cdc35ffc..3b6dc21e 100644 --- a/meson.build +++ b/meson.build @@ -53,6 +53,7 @@ if get_option('strict') '/wd4706', # assignment within conditional expression '/wd4710', # function not inlined '/wd4711', # function selected for automatic inline expansion + '/wd4774', # format string is not a string literal '/wd4800', # implicit conversion from int to bool '/wd4820', # padding added after construct '/wd4996', # POSIX name for this item is deprecated @@ -86,6 +87,7 @@ sources = [ 'src/byte_source.c', 'src/caret.c', 'src/env.c', + 'src/log.c', 'src/n3.c', 'src/node.c', 'src/nodes.c', diff --git a/src/log.c b/src/log.c new file mode 100644 index 00000000..23db44f9 --- /dev/null +++ b/src/log.c @@ -0,0 +1,240 @@ +/* + Copyright 2011-2022 David Robillard <d@drobilla.net> + + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THIS SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ + +#include "world.h" + +#include "serd/serd.h" + +#include <stdarg.h> +#include <stdbool.h> +#include <stdio.h> +#include <string.h> + +static int +level_color(const SerdLogLevel level) +{ + switch (level) { + case SERD_LOG_LEVEL_EMERGENCY: + case SERD_LOG_LEVEL_ALERT: + case SERD_LOG_LEVEL_CRITICAL: + case SERD_LOG_LEVEL_ERROR: + return 31; // Red + case SERD_LOG_LEVEL_WARNING: + return 33; // Yellow + case SERD_LOG_LEVEL_NOTICE: + case SERD_LOG_LEVEL_INFO: + case SERD_LOG_LEVEL_DEBUG: + break; + } + + return 1; // White +} + +static void +serd_ansi_start(const bool enabled, + FILE* const stream, + const int color, + const bool bold) +{ + if (enabled) { + fprintf(stream, bold ? "\033[0;%d;1m" : "\033[0;%dm", color); + } +} + +static void +serd_ansi_reset(const bool enabled, FILE* const stream) +{ + if (enabled) { + fprintf(stream, "\033[0m"); + fflush(stream); + } +} + +static const char* const log_level_strings[] = {"emergency", + "alert", + "critical", + "error", + "warning", + "note", + "info", + "debug"}; + +SerdStatus +serd_quiet_log_func(void* const handle, + const SerdLogLevel level, + const size_t n_fields, + const SerdLogField* const fields, + const SerdStringView message) +{ + (void)handle; + (void)level; + (void)n_fields; + (void)fields; + (void)message; + return SERD_SUCCESS; +} + +static const char* +get_log_field(const size_t n_fields, + const SerdLogField* const fields, + const char* const key) +{ + for (size_t i = 0; i < n_fields; ++i) { + if (!strcmp(fields[i].key, key)) { + return fields[i].value; + } + } + + return NULL; +} + +void +serd_set_log_func(SerdWorld* const world, + const SerdLogFunc log_func, + void* const handle) +{ + world->log_func = log_func; + world->log_handle = handle; +} + +SerdStatus +serd_vxlogf(const SerdWorld* const world, + const SerdLogLevel level, + const size_t n_fields, + const SerdLogField* const fields, + const char* const fmt, + va_list args) +{ + if (world->log_func) { + char message[512] = {0}; + const int r = vsnprintf(message, sizeof(message), fmt, args); + + return (r <= 0 || (size_t)r >= sizeof(message)) + ? SERD_ERR_BAD_ARG + : world->log_func(world->log_handle, + level, + n_fields, + fields, + SERD_SUBSTRING(message, (size_t)r)); + } + + // Print input file and position prefix if available + const char* const file = get_log_field(n_fields, fields, "SERD_FILE"); + const char* const line = get_log_field(n_fields, fields, "SERD_LINE"); + const char* const col = get_log_field(n_fields, fields, "SERD_COL"); + if (file) { + serd_ansi_start(world->stderr_color, stderr, 1, true); + if (line && col) { + fprintf(stderr, "%s:%s:%s: ", file, line, col); + } else { + fprintf(stderr, "%s: ", file); + } + serd_ansi_reset(world->stderr_color, stderr); + } + + // Print GCC-style level prefix (error, warning, etc) + serd_ansi_start(world->stderr_color, stderr, level_color(level), true); + fprintf(stderr, "%s: ", log_level_strings[level]); + serd_ansi_reset(world->stderr_color, stderr); + + // Format and print the message itself + vfprintf(stderr, fmt, args); + fprintf(stderr, "\n"); + + return SERD_SUCCESS; +} + +SerdStatus +serd_xlogf(const SerdWorld* const world, + const SerdLogLevel level, + const size_t n_fields, + const SerdLogField* const fields, + const char* const fmt, + ...) +{ + va_list args; + va_start(args, fmt); + + const SerdStatus st = serd_vxlogf(world, level, n_fields, fields, fmt, args); + + va_end(args); + return st; +} + +SerdStatus +serd_vlogf(const SerdWorld* const world, + const SerdLogLevel level, + const char* const fmt, + va_list args) +{ + return serd_vxlogf(world, level, 0u, NULL, fmt, args); +} + +SerdStatus +serd_logf(const SerdWorld* const world, + const SerdLogLevel level, + const char* const fmt, + ...) +{ + va_list args; + va_start(args, fmt); + + const SerdStatus st = serd_vxlogf(world, level, 0u, NULL, fmt, args); + + va_end(args); + return st; +} + +SerdStatus +serd_vlogf_at(const SerdWorld* SERD_NONNULL world, + SerdLogLevel level, + const SerdCaret* caret, + const char* SERD_NONNULL fmt, + va_list args) +{ + if (!caret) { + return serd_vxlogf(world, level, 0u, NULL, fmt, args); + } + + char line[24]; + char col[24]; + snprintf(line, sizeof(line), "%u", serd_caret_line(caret)); + snprintf(col, sizeof(col), "%u", serd_caret_column(caret)); + + const SerdLogField fields[] = { + {"SERD_FILE", serd_node_string(serd_caret_name(caret))}, + {"SERD_LINE", line}, + {"SERD_COL", col}, + }; + + return serd_vxlogf(world, level, 3, fields, fmt, args); +} + +SerdStatus +serd_logf_at(const SerdWorld* SERD_NONNULL world, + SerdLogLevel level, + const SerdCaret* caret, + const char* SERD_NONNULL fmt, + ...) +{ + va_list args; + va_start(args, fmt); + + const SerdStatus st = serd_vlogf_at(world, level, caret, fmt, args); + + va_end(args); + return st; +} @@ -62,7 +62,7 @@ read_HEX(SerdReader* const reader) return (uint8_t)eat_byte_safe(reader, c); } - r_err(reader, SERD_ERR_BAD_SYNTAX, "invalid hexadecimal digit `%c'\n", c); + r_err(reader, SERD_ERR_BAD_SYNTAX, "invalid hexadecimal digit `%c'", c); return 0; } @@ -107,10 +107,9 @@ read_UCHAR(SerdReader* const reader, } else if (code < 0x00110000) { size = 4; } else { - r_err(reader, - SERD_ERR_BAD_SYNTAX, - "unicode character 0x%X out of range\n", - code); + r_err( + reader, SERD_ERR_BAD_SYNTAX, "unicode character 0x%X out of range", code); + *char_code = 0xFFFD; const SerdStatus st = push_bytes(reader, dest, replacement_char, 3); return st ? st : SERD_SUCCESS; @@ -199,14 +198,14 @@ read_utf8_bytes(SerdReader* const reader, { *size = utf8_num_bytes(c); if (*size <= 1 || *size > 4) { - return bad_char(reader, "invalid UTF-8 start 0x%X\n", c); + return bad_char(reader, "invalid UTF-8 start 0x%X", c); } bytes[0] = c; for (unsigned i = 1; i < *size; ++i) { const int b = peek_byte(reader); if (b == EOF || ((uint8_t)b & 0x80) == 0) { - return bad_char(reader, "invalid UTF-8 continuation 0x%X\n", (uint8_t)b); + return bad_char(reader, "invalid UTF-8 continuation 0x%X", (uint8_t)b); } eat_byte_safe(reader, b); @@ -355,10 +354,10 @@ read_STRING_LITERAL_LONG(SerdReader* const reader, uint32_t code = 0; if ((st = read_ECHAR(reader, ref)) && (st = read_UCHAR(reader, ref, &code))) { - return r_err(reader, st, "invalid escape `\\%c'\n", peek_byte(reader)); + return r_err(reader, st, "invalid escape `\\%c'", peek_byte(reader)); } } else if (c == EOF) { - st = r_err(reader, SERD_ERR_NO_DATA, "unexpected end of file\n"); + st = r_err(reader, SERD_ERR_NO_DATA, "unexpected end of file"); } else if (c == q) { eat_byte_safe(reader, q); const int q2 = eat_byte_safe(reader, peek_byte(reader)); @@ -393,16 +392,15 @@ read_STRING_LITERAL(SerdReader* const reader, uint32_t code = 0; switch (c) { case EOF: - return r_err( - reader, SERD_ERR_BAD_SYNTAX, "end of file in short string\n"); + return r_err(reader, SERD_ERR_BAD_SYNTAX, "end of file in short string"); case '\n': case '\r': - return r_err(reader, SERD_ERR_BAD_SYNTAX, "line end in short string\n"); + return r_err(reader, SERD_ERR_BAD_SYNTAX, "line end in short string"); case '\\': eat_byte_safe(reader, c); if ((st = read_ECHAR(reader, ref)) && (st = read_UCHAR(reader, ref, &code))) { - return r_err(reader, st, "invalid escape `\\%c'\n", peek_byte(reader)); + return r_err(reader, st, "invalid escape `\\%c'", peek_byte(reader)); } break; default: @@ -426,7 +424,7 @@ read_String(SerdReader* const reader, SerdNode* const node) const int q2 = peek_byte(reader); if (q2 == EOF) { - return r_err(reader, SERD_ERR_BAD_SYNTAX, "unexpected end of file\n"); + return r_err(reader, SERD_ERR_BAD_SYNTAX, "unexpected end of file"); } if (q2 != q1) { // Short string (not triple quoted) @@ -436,7 +434,7 @@ read_String(SerdReader* const reader, SerdNode* const node) eat_byte_safe(reader, q2); const int q3 = peek_byte(reader); if (q3 == EOF) { - return r_err(reader, SERD_ERR_BAD_SYNTAX, "unexpected end of file\n"); + return r_err(reader, SERD_ERR_BAD_SYNTAX, "unexpected end of file"); } if (q3 != q1) { // Empty short string ("" or '') @@ -445,7 +443,7 @@ read_String(SerdReader* const reader, SerdNode* const node) if (!fancy_syntax(reader)) { return r_err( - reader, SERD_ERR_BAD_SYNTAX, "syntax does not support long literals\n"); + reader, SERD_ERR_BAD_SYNTAX, "syntax does not support long literals"); } eat_byte_safe(reader, q3); @@ -478,7 +476,7 @@ read_PN_CHARS_BASE(SerdReader* const reader, SerdNode* const dest) return st; } else if (!is_PN_CHARS_BASE(code)) { r_err( - reader, SERD_ERR_BAD_SYNTAX, "invalid character U+%04X in name\n", code); + reader, SERD_ERR_BAD_SYNTAX, "invalid character U+%04X in name", code); if (reader->strict) { return SERD_ERR_BAD_SYNTAX; } @@ -508,7 +506,7 @@ read_PN_CHARS(SerdReader* const reader, SerdNode* const dest) return st; } else if (!is_PN_CHARS(code)) { return r_err( - reader, SERD_ERR_BAD_SYNTAX, "invalid character U+%04X in name\n", code); + reader, SERD_ERR_BAD_SYNTAX, "invalid character U+%04X in name", code); } return st; } @@ -566,7 +564,7 @@ read_PN_LOCAL_ESC(SerdReader* const reader, SerdNode* const dest) break; } - return r_err(reader, SERD_ERR_BAD_SYNTAX, "invalid escape\n"); + return r_err(reader, SERD_ERR_BAD_SYNTAX, "invalid escape"); } static SerdStatus @@ -608,7 +606,7 @@ read_PN_LOCAL(SerdReader* const reader, break; default: if ((st = read_PLX(reader, dest)) > SERD_FAILURE) { - return r_err(reader, st, "bad escape\n"); + return r_err(reader, st, "bad escape"); } else if (st != SERD_SUCCESS && read_PN_CHARS_BASE(reader, dest)) { return SERD_FAILURE; } @@ -618,7 +616,7 @@ read_PN_LOCAL(SerdReader* const reader, if (c == '.' || c == ':') { st = push_byte(reader, dest, eat_byte_safe(reader, c)); } else if ((st = read_PLX(reader, dest)) > SERD_FAILURE) { - return r_err(reader, st, "bad escape\n"); + return r_err(reader, st, "bad escape"); } else if (st != SERD_SUCCESS && (st = read_PN_CHARS(reader, dest))) { break; } @@ -654,7 +652,7 @@ read_PN_PREFIX_tail(SerdReader* const reader, SerdNode* const dest) if ((st = read_PN_CHARS(reader, dest))) { return r_err(reader, st > SERD_FAILURE ? st : SERD_ERR_BAD_SYNTAX, - "prefix ends with `.'\n"); + "prefix ends with `.'"); } } @@ -678,7 +676,7 @@ read_LANGTAG(SerdReader* const reader) { int c = peek_byte(reader); if (!is_alpha(c)) { - return r_err(reader, SERD_ERR_BAD_SYNTAX, "unexpected `%c'\n", c); + return r_err(reader, SERD_ERR_BAD_SYNTAX, "unexpected `%c'", c); } SerdNode* node = push_node(reader, SERD_LITERAL, "", 0); @@ -705,19 +703,19 @@ read_IRIREF_scheme(SerdReader* const reader, SerdNode* const dest) { int c = peek_byte(reader); if (!is_alpha(c)) { - return r_err(reader, SERD_ERR_BAD_SYNTAX, "bad IRI scheme start `%c'\n", c); + return r_err(reader, SERD_ERR_BAD_SYNTAX, "bad IRI scheme start `%c'", c); } SerdStatus st = SERD_SUCCESS; while ((c = peek_byte(reader)) != EOF) { if (c == '>') { - return r_err(reader, SERD_ERR_BAD_SYNTAX, "missing IRI scheme\n"); + return r_err(reader, SERD_ERR_BAD_SYNTAX, "missing IRI scheme"); } if (!is_uri_scheme_char(c)) { return r_err(reader, SERD_ERR_BAD_SYNTAX, - "bad IRI scheme char U+%04X (%c)\n", + "bad IRI scheme char U+%04X (%c)", (unsigned)c, (char)c); } @@ -747,7 +745,7 @@ read_IRIREF(SerdReader* const reader, SerdNode** const dest) } if (!fancy_syntax(reader) && (st = read_IRIREF_scheme(reader, *dest))) { - return r_err(reader, st, "expected IRI scheme\n"); + return r_err(reader, st, "expected IRI scheme"); } uint32_t code = 0; @@ -757,12 +755,12 @@ read_IRIREF(SerdReader* const reader, SerdNode** const dest) case '"': case '<': return r_err( - reader, SERD_ERR_BAD_SYNTAX, "invalid IRI character `%c'\n", c); + reader, SERD_ERR_BAD_SYNTAX, "invalid IRI character `%c'", c); case '>': return SERD_SUCCESS; case '\\': if (read_UCHAR(reader, *dest, &code)) { - return r_err(reader, SERD_ERR_BAD_SYNTAX, "invalid IRI escape\n"); + return r_err(reader, SERD_ERR_BAD_SYNTAX, "invalid IRI escape"); } switch (code) { case 0: @@ -771,7 +769,7 @@ read_IRIREF(SerdReader* const reader, SerdNode** const dest) case '>': return r_err(reader, SERD_ERR_BAD_SYNTAX, - "invalid escaped IRI character U+%04X\n", + "invalid escaped IRI character U+%04X", code); default: break; @@ -783,12 +781,12 @@ read_IRIREF(SerdReader* const reader, SerdNode** const dest) case '|': case '}': return r_err( - reader, SERD_ERR_BAD_SYNTAX, "invalid IRI character `%c'\n", c); + reader, SERD_ERR_BAD_SYNTAX, "invalid IRI character `%c'", c); default: if (c <= 0x20) { st = r_err(reader, SERD_ERR_BAD_SYNTAX, - "invalid IRI character (escape %%%02X)\n", + "invalid IRI character (escape %%%02X)", (unsigned)c); if (reader->strict) { break; @@ -841,7 +839,7 @@ read_0_9(SerdReader* const reader, SerdNode* const str, const bool at_least_one) } if (at_least_one && count == 0) { - return r_err(reader, SERD_ERR_BAD_SYNTAX, "expected digit\n"); + return r_err(reader, SERD_ERR_BAD_SYNTAX, "expected digit"); } return st; @@ -987,7 +985,7 @@ read_Var(SerdReader* const reader, SerdNode** const dest) { if (!(reader->flags & SERD_READ_VARIABLES)) { return r_err( - reader, SERD_ERR_BAD_SYNTAX, "syntax does not support variables\n"); + reader, SERD_ERR_BAD_SYNTAX, "syntax does not support variables"); } if (!(*dest = push_node(reader, SERD_VARIABLE, "", 0))) { @@ -1039,7 +1037,7 @@ read_verb(SerdReader* reader, SerdNode** dest) if ((st = read_PrefixedName(reader, *dest, false, &ate_dot)) || ate_dot) { *dest = NULL; return r_err( - reader, st > SERD_FAILURE ? st : SERD_ERR_BAD_SYNTAX, "expected verb\n"); + reader, st > SERD_FAILURE ? st : SERD_ERR_BAD_SYNTAX, "expected verb"); } return SERD_SUCCESS; @@ -1067,7 +1065,7 @@ read_BLANK_NODE_LABEL(SerdReader* const reader, if (is_digit(c) || c == '_') { TRY(st, push_byte(reader, n, eat_byte_safe(reader, c))); } else if ((st = read_PN_CHARS(reader, n))) { - return r_err(reader, st, "invalid name start\n"); + return r_err(reader, st, "invalid name start"); } while ((c = peek_byte(reader))) { // Middle: (PN_CHARS | '.')* @@ -1098,7 +1096,7 @@ read_BLANK_NODE_LABEL(SerdReader* const reader, } else if (reader->seen_genid && buf[reader->bprefix_len] == 'B') { return r_err(reader, SERD_ERR_ID_CLASH, - "found both `b' and `B' blank IDs, prefix required\n"); + "found both `b' and `B' blank IDs, prefix required"); } } } @@ -1142,7 +1140,7 @@ read_anon(SerdReader* const reader, TRY(st, read_predicateObjectList(reader, ctx, &ate_dot_in_list)); if (ate_dot_in_list) { - return r_err(reader, SERD_ERR_BAD_SYNTAX, "`.' inside blank\n"); + return r_err(reader, SERD_ERR_BAD_SYNTAX, "`.' inside blank"); } read_ws_star(reader); @@ -1185,14 +1183,14 @@ read_object(SerdReader* const reader, case '?': break; default: - return r_err(reader, SERD_ERR_BAD_SYNTAX, "expected: ':', '<', or '_'\n"); + return r_err(reader, SERD_ERR_BAD_SYNTAX, "expected: ':', '<', or '_'"); } } switch (c) { case EOF: case ')': - return r_err(reader, SERD_ERR_BAD_SYNTAX, "expected object\n"); + return r_err(reader, SERD_ERR_BAD_SYNTAX, "expected object"); case '$': case '?': ret = read_Var(reader, &o); @@ -1260,7 +1258,7 @@ read_object(SerdReader* const reader, } else if ((ret = read_PN_PREFIX_tail(reader, o)) > SERD_FAILURE || (ret = read_PrefixedName(reader, o, false, ate_dot))) { ret = (ret > SERD_FAILURE) ? ret : SERD_ERR_BAD_SYNTAX; - return r_err(reader, ret, "expected prefixed name\n"); + return r_err(reader, ret, "expected prefixed name"); } } @@ -1285,7 +1283,7 @@ read_objectList(SerdReader* const reader, ReadContext ctx, bool* const ate_dot) TRY(st, read_object(reader, &ctx, true, ate_dot)); if (!fancy_syntax(reader) && peek_delim(reader, ',')) { return r_err( - reader, SERD_ERR_BAD_SYNTAX, "syntax does not support abbreviation\n"); + reader, SERD_ERR_BAD_SYNTAX, "syntax does not support abbreviation"); } while (st <= SERD_FAILURE && !*ate_dot && eat_delim(reader, ',')) { @@ -1317,7 +1315,7 @@ read_predicateObjectList(SerdReader* const reader, switch (c = peek_byte(reader)) { case EOF: serd_stack_pop_to(&reader->stack, orig_stack_size); - return r_err(reader, SERD_ERR_BAD_SYNTAX, "unexpected end of file\n"); + return r_err(reader, SERD_ERR_BAD_SYNTAX, "unexpected end of file"); case '.': case ']': case '}': @@ -1331,7 +1329,7 @@ read_predicateObjectList(SerdReader* const reader, if (!ate_semi) { serd_stack_pop_to(&reader->stack, orig_stack_size); - return r_err(reader, SERD_ERR_BAD_SYNTAX, "missing ';' or '.'\n"); + return r_err(reader, SERD_ERR_BAD_SYNTAX, "missing ';' or '.'"); } } @@ -1443,7 +1441,7 @@ read_subject(SerdReader* const reader, } if (ate_dot) { - return r_err(reader, SERD_ERR_BAD_SYNTAX, "subject ends with `.'\n"); + return r_err(reader, SERD_ERR_BAD_SYNTAX, "subject ends with `.'"); } return st; @@ -1470,7 +1468,7 @@ read_labelOrSubject(SerdReader* const reader, SerdNode** const dest) if (!read_iri(reader, dest, &ate_dot)) { return SERD_SUCCESS; } else { - return r_err(reader, SERD_ERR_BAD_SYNTAX, "expected label or subject\n"); + return r_err(reader, SERD_ERR_BAD_SYNTAX, "expected label or subject"); } } } @@ -1521,7 +1519,7 @@ read_base(SerdReader* const reader, const bool sparql, const bool token) } if (peek_byte(reader) == '.') { - return r_err(reader, SERD_ERR_BAD_SYNTAX, "full stop after SPARQL BASE\n"); + return r_err(reader, SERD_ERR_BAD_SYNTAX, "full stop after SPARQL BASE"); } return SERD_SUCCESS; @@ -1577,7 +1575,7 @@ read_directive(SerdReader* const reader) switch (peek_byte(reader)) { case 'B': case 'P': - return r_err(reader, SERD_ERR_BAD_SYNTAX, "uppercase directive\n"); + return r_err(reader, SERD_ERR_BAD_SYNTAX, "uppercase directive"); } } @@ -1592,7 +1590,7 @@ read_directive(SerdReader* const reader) break; } - return r_err(reader, SERD_ERR_BAD_SYNTAX, "invalid directive\n"); + return r_err(reader, SERD_ERR_BAD_SYNTAX, "invalid directive"); } static SerdStatus @@ -1611,12 +1609,12 @@ read_wrappedGraph(SerdReader* const reader, ReadContext* const ctx) ctx->subject = 0; if ((st = read_subject(reader, *ctx, &ctx->subject, &s_type))) { - return r_err(reader, st, "expected subject\n"); + return r_err(reader, st, "expected subject"); } if (read_triples(reader, *ctx, &ate_dot) && s_type != '[') { return r_err( - reader, SERD_ERR_BAD_SYNTAX, "missing predicate object list\n"); + reader, SERD_ERR_BAD_SYNTAX, "missing predicate object list"); } serd_stack_pop_to(&reader->stack, orig_stack_size); @@ -1630,7 +1628,7 @@ read_wrappedGraph(SerdReader* const reader, ReadContext* const ctx) eat_byte_safe(reader, '}'); read_ws_star(reader); if (peek_byte(reader) == '.') { - return r_err(reader, SERD_ERR_BAD_SYNTAX, "graph followed by `.'\n"); + return r_err(reader, SERD_ERR_BAD_SYNTAX, "graph followed by `.'"); } return SERD_SUCCESS; @@ -1662,7 +1660,7 @@ read_n3_statement(SerdReader* const reader) case '@': if (!fancy_syntax(reader)) { return r_err( - reader, SERD_ERR_BAD_SYNTAX, "syntax does not support directives\n"); + reader, SERD_ERR_BAD_SYNTAX, "syntax does not support directives"); } TRY(st, read_directive(reader)); read_ws_star(reader); @@ -1673,7 +1671,7 @@ read_n3_statement(SerdReader* const reader) read_ws_star(reader); } else { return r_err( - reader, SERD_ERR_BAD_SYNTAX, "syntax does not support graphs\n"); + reader, SERD_ERR_BAD_SYNTAX, "syntax does not support graphs"); } break; default: @@ -1695,7 +1693,7 @@ read_n3_statement(SerdReader* const reader) read_ws_star(reader); } else if (read_ws_star(reader) && peek_byte(reader) == '{') { if (s_type == '(' || (s_type == '[' && !*ctx.flags)) { - return r_err(reader, SERD_ERR_BAD_SYNTAX, "invalid graph name\n"); + return r_err(reader, SERD_ERR_BAD_SYNTAX, "invalid graph name"); } ctx.graph = ctx.subject; @@ -1709,7 +1707,7 @@ read_n3_statement(SerdReader* const reader) if (ate_dot && (reader->strict || (s_type != '('))) { return r_err( - reader, SERD_ERR_BAD_SYNTAX, "unexpected end of statement\n"); + reader, SERD_ERR_BAD_SYNTAX, "unexpected end of statement"); } return st > SERD_FAILURE ? st : SERD_ERR_BAD_SYNTAX; @@ -1768,8 +1766,7 @@ read_nquadsDoc(SerdReader* const reader) } if (peek_byte(reader) == '@') { - r_err( - reader, SERD_ERR_BAD_SYNTAX, "syntax does not support directives\n"); + r_err(reader, SERD_ERR_BAD_SYNTAX, "syntax does not support directives"); return SERD_ERR_BAD_SYNTAX; } diff --git a/src/reader.c b/src/reader.c index d70cbb53..c2962bdb 100644 --- a/src/reader.c +++ b/src/reader.c @@ -22,7 +22,6 @@ #include "stack.h" #include "statement.h" #include "system.h" -#include "world.h" #include <stdarg.h> #include <stdio.h> @@ -37,8 +36,10 @@ r_err(SerdReader* const reader, const SerdStatus st, const char* const fmt, ...) { va_list args; va_start(args, fmt); - const SerdError e = {st, &reader->source->caret, fmt, &args}; - serd_world_error(reader->world, &e); + + serd_vlogf_at( + reader->world, SERD_LOG_LEVEL_ERROR, &reader->source->caret, fmt, args); + va_end(args); return st; } @@ -253,7 +254,7 @@ skip_bom(SerdReader* const me) serd_byte_source_advance(me->source) || serd_byte_source_peek(me->source) != 0xBF || serd_byte_source_advance(me->source)) { - r_err(me, SERD_ERR_BAD_SYNTAX, "corrupt byte order mark\n"); + r_err(me, SERD_ERR_BAD_SYNTAX, "corrupt byte order mark"); return SERD_ERR_BAD_SYNTAX; } } diff --git a/src/reader.h b/src/reader.h index b08c3fd0..cc8656d3 100644 --- a/src/reader.h +++ b/src/reader.h @@ -28,12 +28,6 @@ #include <stdint.h> #include <stdio.h> -#if defined(__GNUC__) -# define SERD_LOG_FUNC(fmt, arg1) __attribute__((format(printf, fmt, arg1))) -#else -# define SERD_LOG_FUNC(fmt, arg1) -#endif - typedef struct { SerdNode* graph; SerdNode* subject; @@ -45,7 +39,7 @@ typedef struct { struct SerdReaderImpl { SerdWorld* world; const SerdSink* sink; - SerdErrorFunc error_func; + SerdLogFunc log_func; void* error_handle; SerdNode* rdf_first; SerdNode* rdf_rest; @@ -131,7 +125,7 @@ eat_byte_check(SerdReader* reader, const int byte) const int c = peek_byte(reader); if (c != byte) { return r_err( - reader, SERD_ERR_BAD_SYNTAX, "expected `%c', not `%c'\n", byte, c); + reader, SERD_ERR_BAD_SYNTAX, "expected `%c', not `%c'", byte, c); } eat_byte_safe(reader, byte); diff --git a/src/serd_config.h b/src/serd_config.h index d5a6db33..53378a48 100644 --- a/src/serd_config.h +++ b/src/serd_config.h @@ -51,6 +51,13 @@ # endif # endif +// POSIX.1-2001: isatty() +# ifndef HAVE_ISATTY +# if defined(_POSIX_VERSION) && _POSIX_VERSION >= 200112L +# define HAVE_ISATTY +# endif +# endif + // POSIX.1-2001: posix_fadvise() # ifndef HAVE_POSIX_FADVISE # ifndef __APPLE__ @@ -90,6 +97,12 @@ # define USE_FILENO 0 #endif +#ifdef HAVE_ISATTY +# define USE_ISATTY 1 +#else +# define USE_ISATTY 0 +#endif + #ifdef HAVE_POSIX_FADVISE # define USE_POSIX_FADVISE 1 #else diff --git a/src/serdi.c b/src/serdi.c index 9a5495af..28cf870d 100644 --- a/src/serdi.c +++ b/src/serdi.c @@ -86,14 +86,6 @@ missing_arg(const char* const name, const char opt) } static SerdStatus -quiet_error_func(void* const handle, const SerdError* const e) -{ - (void)handle; - (void)e; - return SERD_SUCCESS; -} - -static SerdStatus read_file(SerdWorld* const world, SerdSyntax syntax, const SerdReaderFlags flags, @@ -333,7 +325,7 @@ main(int argc, char** argv) serd_writer_new(world, output_syntax, writer_flags, env, byte_sink); if (quiet) { - serd_world_set_error_func(world, quiet_error_func, NULL); + serd_set_log_func(world, serd_quiet_log_func, NULL); } if (root_uri) { diff --git a/src/world.c b/src/world.c index bd70d615..d0ad60da 100644 --- a/src/world.c +++ b/src/world.c @@ -16,48 +16,53 @@ #include "world.h" -#include "caret.h" #include "namespaces.h" #include "node.h" +#include "serd_config.h" -#include <stdarg.h> +#if USE_FILENO && USE_ISATTY +# include <unistd.h> +#endif + +#include <stdbool.h> #include <stdio.h> #include <stdlib.h> #include <string.h> #define BLANK_CHARS 12 -SerdStatus -serd_world_error(const SerdWorld* const world, const SerdError* const e) +static bool +terminal_supports_color(FILE* const stream) { - if (world->error_func) { - world->error_func(world->error_handle, e); - } else { - fprintf(stderr, "error: "); - if (e->caret) { - fprintf(stderr, - "%s:%u:%u: ", - serd_node_string(e->caret->file), - e->caret->line, - e->caret->col); - } - vfprintf(stderr, e->fmt, *e->args); + // https://no-color.org/ + // NOLINTNEXTLINE(concurrency-mt-unsafe) + if (getenv("NO_COLOR")) { + return false; } - return e->status; -} -SerdStatus -serd_world_errorf(const SerdWorld* const world, - const SerdStatus st, - const char* const fmt, - ...) -{ - va_list args; - va_start(args, fmt); - const SerdError e = {st, NULL, fmt, &args}; - serd_world_error(world, &e); - va_end(args); - return st; + // https://bixense.com/clicolors/ + // NOLINTNEXTLINE(concurrency-mt-unsafe) + const char* const clicolor_force = getenv("CLICOLOR_FORCE"); + if (clicolor_force && !!strcmp(clicolor_force, "0")) { + return true; + } + + // https://bixense.com/clicolors/ + // NOLINTNEXTLINE(concurrency-mt-unsafe) + const char* const clicolor = getenv("CLICOLOR"); + if (clicolor && !strcmp(clicolor, "0")) { + return false; + } + +#if USE_FILENO && USE_ISATTY + + // Assume support if stream is a TTY (blissfully ignoring termcap nightmares) + return isatty(fileno(stream)); + +#else + (void)stream; + return false; +#endif } SerdWorld* @@ -85,6 +90,8 @@ serd_world_new(void) world->blank_node = serd_new_blank(SERD_STRING("b00000000000")); world->nodes = nodes; + world->stderr_color = terminal_supports_color(stderr); + return world; } @@ -115,12 +122,3 @@ serd_world_get_blank(SerdWorld* const world) return world->blank_node; } - -void -serd_world_set_error_func(SerdWorld* world, - SerdErrorFunc error_func, - void* handle) -{ - world->error_func = error_func; - world->error_handle = handle; -} diff --git a/src/world.h b/src/world.h index a70a6e28..8043663e 100644 --- a/src/world.h +++ b/src/world.h @@ -19,12 +19,13 @@ #include "serd/serd.h" +#include <stdbool.h> #include <stdint.h> struct SerdWorldImpl { SerdNodes* nodes; - SerdErrorFunc error_func; - void* error_handle; + SerdLogFunc log_func; + void* log_handle; SerdNode* blank_node; const SerdNode* rdf_first; const SerdNode* rdf_nil; @@ -34,12 +35,8 @@ struct SerdWorldImpl { const SerdNode* xsd_decimal; const SerdNode* xsd_integer; uint32_t next_blank_id; -}; - -SerdStatus -serd_world_error(const SerdWorld* world, const SerdError* e); -SerdStatus -serd_world_errorf(const SerdWorld* world, SerdStatus st, const char* fmt, ...); + bool stderr_color; +}; #endif // SERD_WORLD_H diff --git a/src/writer.c b/src/writer.c index 1ad62a18..3d4cad19 100644 --- a/src/writer.c +++ b/src/writer.c @@ -28,6 +28,7 @@ #include <assert.h> #include <errno.h> +#include <stdarg.h> #include <stdbool.h> #include <stdint.h> #include <stdio.h> @@ -146,6 +147,19 @@ write_node(SerdWriter* writer, SerdField field, SerdStatementFlags flags); +SERD_LOG_FUNC(3, 4) +static SerdStatus +w_err(SerdWriter* const writer, const SerdStatus st, const char* const fmt, ...) +{ + va_list args; + va_start(args, fmt); + + serd_vlogf(writer->world, SERD_LOG_LEVEL_ERROR, fmt, args); + + va_end(args); + return st; +} + static bool supports_abbrev(const SerdWriter* writer) { @@ -242,10 +256,13 @@ sink(const void* buf, size_t len, SerdWriter* writer) char message[1024] = {0}; serd_system_strerror(errno, message, sizeof(message)); - serd_world_errorf( - writer->world, SERD_ERR_BAD_WRITE, "write error (%s)\n", message); + w_err(writer, SERD_ERR_BAD_WRITE, "write error (%s)", message); } else { - serd_world_errorf(writer->world, SERD_ERR_BAD_WRITE, "write error\n"); + w_err(writer, + SERD_ERR_BAD_WRITE, + "unknown write error, %zu / %zu bytes written", + written, + len); } } @@ -270,8 +287,7 @@ write_character(SerdWriter* writer, const uint32_t c = parse_utf8_char(utf8, size); switch (*size) { case 0: - serd_world_errorf( - writer->world, SERD_ERR_BAD_ARG, "invalid UTF-8 start: %X\n", utf8[0]); + w_err(writer, SERD_ERR_BAD_TEXT, "invalid UTF-8 start: %X", utf8[0]); *st = SERD_ERR_BAD_TEXT; return 0; case 1: @@ -822,11 +838,10 @@ write_uri_node(SerdWriter* const writer, if (!has_scheme && !supports_uriref(writer) && !serd_env_base_uri(writer->env)) { - serd_world_errorf(writer->world, - SERD_ERR_BAD_ARG, - "syntax does not support URI reference <%s>\n", - node_str); - return SERD_ERR_BAD_ARG; + return w_err(writer, + SERD_ERR_BAD_ARG, + "syntax does not support URI reference <%s>", + node_str); } return write_full_uri_node(writer, node); @@ -843,11 +858,10 @@ write_curie(SerdWriter* const writer, const SerdNode* const node) if (writer->syntax == SERD_NTRIPLES || writer->syntax == SERD_NQUADS) { if ((st = serd_env_expand_in_place(writer->env, node, &prefix, &suffix))) { - serd_world_errorf(writer->world, - st, - "undefined namespace prefix `%s'\n", - serd_node_string(node)); - return st; + return w_err(writer, + st, + "undefined namespace prefix in `%s'", + serd_node_string(node)); } TRY(st, esink("<", 1, writer)); @@ -1236,10 +1250,10 @@ serd_writer_end_anon(SerdWriter* writer, const SerdNode* node) } if (writer->anon_stack_size == 0) { - return serd_world_errorf(writer->world, - SERD_ERR_UNKNOWN, - "unexpected end of anonymous node `%s'\n", - serd_node_string(node)); + return w_err(writer, + SERD_ERR_BAD_CALL, + "unexpected end of anonymous node `%s'", + serd_node_string(node)); } SerdStatus st = write_sep(writer, writer->context.flags, SEP_ANON_END); diff --git a/test/meson.build b/test/meson.build index 222ec1bc..4d9d2b43 100644 --- a/test/meson.build +++ b/test/meson.build @@ -10,6 +10,7 @@ unit_tests = [ 'caret', 'env', 'free_null', + 'log', 'node', 'nodes', 'overflow', @@ -102,6 +103,24 @@ if get_option('utils') suite: ['serdi', 'options']) endforeach + test('ansi_clicolor_force', + serdi, + args: files('bad/bad-lang.ttl'), + env: test_env + ['CLICOLOR_FORCE=1'], + should_fail: true) + + test('ansi_clicolor_off', + serdi, + args: files('bad/bad-lang.ttl'), + env: test_env + ['CLICOLOR=0'], + should_fail: true) + + test('ansi_no_color', + serdi, + args: files('bad/bad-lang.ttl'), + env: test_env + ['NO_COLOR=1'], + should_fail: true) + test('none', serdi, env: test_env, diff --git a/test/test_log.c b/test/test_log.c new file mode 100644 index 00000000..88d14dc6 --- /dev/null +++ b/test/test_log.c @@ -0,0 +1,128 @@ +/* + Copyright 2021 David Robillard <d@drobilla.net> + + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted, provided that the above + copyright notice and this permission notice appear in all copies. + + THIS SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +*/ + +#undef NDEBUG + +#include "serd/serd.h" + +#include <assert.h> +#include <stdbool.h> +#include <string.h> + +static SerdStatus +custom_log_func(void* const handle, + const SerdLogLevel level, + const size_t n_fields, + const SerdLogField* const fields, + const SerdStringView message) +{ + (void)message; + + bool* const called = (bool*)handle; + + assert(level == SERD_LOG_LEVEL_NOTICE); + assert(n_fields == 1); + assert(!strcmp(fields[0].key, "TEST_KEY")); + assert(!strcmp(fields[0].value, "TEST VALUE")); + assert(!strcmp(message.buf, "test message 42")); + assert(message.len == strlen("test message 42")); + + *called = true; + return SERD_SUCCESS; +} + +static void +test_bad_arg(void) +{ + SerdWorld* const world = serd_world_new(); + bool called = false; + + serd_set_log_func(world, custom_log_func, &called); + + assert(serd_logf(world, SERD_LOG_LEVEL_ERROR, "%s", "") == SERD_ERR_BAD_ARG); + + serd_world_free(world); +} + +static void +test_default_log(void) +{ + SerdWorld* const world = serd_world_new(); + + for (unsigned i = 0; i <= SERD_LOG_LEVEL_DEBUG; ++i) { + const SerdLogLevel level = (SerdLogLevel)i; + + assert(!serd_logf(world, level, "test")); + } + + serd_world_free(world); +} + +static void +test_custom_log(void) +{ + SerdWorld* const world = serd_world_new(); + bool called = false; + + serd_set_log_func(world, custom_log_func, &called); + + const SerdLogField fields[1] = {{"TEST_KEY", "TEST VALUE"}}; + assert(!serd_xlogf( + world, SERD_LOG_LEVEL_NOTICE, 1, fields, "test message %d", 42)); + + assert(called); + serd_world_free(world); +} + +static void +test_caret(void) +{ + SerdWorld* const world = serd_world_new(); + SerdNode* const name = serd_new_string(SERD_STRING("filename")); + SerdCaret* const caret = serd_caret_new(name, 46, 2); + + serd_logf_at(world, SERD_LOG_LEVEL_NOTICE, caret, "is just ahead of me"); + serd_logf_at(world, SERD_LOG_LEVEL_NOTICE, NULL, "isn't"); + + serd_caret_free(caret); + serd_node_free(name); + serd_world_free(world); +} + +static void +test_filename_only(void) +{ + SerdWorld* const world = serd_world_new(); + + const SerdLogField fields[2] = {{"TEST_KEY", "TEST VALUE"}, + {"SERD_FILE", "somename"}}; + + assert(!serd_xlogf(world, SERD_LOG_LEVEL_INFO, 2, fields, "no numbers here")); + + serd_world_free(world); +} + +int +main(void) +{ + test_bad_arg(); + test_default_log(); + test_custom_log(); + test_caret(); + test_filename_only(); + + return 0; +} |