From 64afdd52658afe1042aa5ec991e5a1794de8f360 Mon Sep 17 00:00:00 2001 From: David Hummel <6109326+hummeltech@users.noreply.github.com> Date: Sun, 18 Oct 2026 21:16:43 -0700 Subject: [PATCH 1/7] FreeBSD test updates --- .github/workflows/build-and-test.yml | 4 ---- CMakeLists.txt | 5 +++++ docs/build/building_on_freebsd.md | 2 -- tests/CMakeLists.txt | 3 +++ tests/unit_test_cache_expire.cpp | 2 ++ 5 files changed, 10 insertions(+), 6 deletions(-) diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml index 5e2949200..b6ba0a3d2 100644 --- a/.github/workflows/build-and-test.yml +++ b/.github/workflows/build-and-test.yml @@ -228,12 +228,8 @@ jobs: FreeBSD: env: CFLAGS: --coverage - CTEST_CLIENT_HOST: ::1 - CTEST_SERVER_HOST: localhost CXXFLAGS: --coverage INSTALL_PREFIX: /usr/local - LIBRARY_PATH: /usr/local/lib - TMPDIR: /tmp name: >- FreeBSD ${{ matrix.freebsd_release }} diff --git a/CMakeLists.txt b/CMakeLists.txt index ffbc85e73..c2686ff08 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -106,6 +106,11 @@ if(NOT MALLOC_LIB STREQUAL "libc") endif() endif() +# Use /usr/local/lib for FreeBSD +if(CMAKE_SYSTEM_NAME MATCHES "FreeBSD") + link_directories(/usr/local/lib) +endif() + #----------------------------------------------------------------------------- # # Set variables diff --git a/docs/build/building_on_freebsd.md b/docs/build/building_on_freebsd.md index 7e414583a..a5e5a05ba 100644 --- a/docs/build/building_on_freebsd.md +++ b/docs/build/building_on_freebsd.md @@ -34,8 +34,6 @@ sudo pkg install --yes \ # Download, Build, Test & Install `mod_tile` export CMAKE_BUILD_PARALLEL_LEVEL=$(sysctl -n hw.ncpu) -export CTEST_CLIENT_HOST="::1" -export CTEST_SERVER_HOST="localhost" export LIBRARY_PATH="/usr/local/lib" rm -rf /tmp/mod_tile_src /tmp/mod_tile_build mkdir /tmp/mod_tile_src /tmp/mod_tile_build diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 9379aa87d..608b0370f 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -39,6 +39,9 @@ find_program(TOUCH_EXECUTABLE NAMES gtouch touch REQUIRED) if(DEFINED ENV{CTEST_SERVER_HOST}) # To the value of environment variable "CTEST_SERVER_HOST" set(CTEST_SERVER_HOST "$ENV{CTEST_SERVER_HOST}") +elseif(CMAKE_SYSTEM_NAME MATCHES "FreeBSD") + # To localhost on FreeBSD + set(CTEST_SERVER_HOST "localhost") else() # Or to 0.0.0.0 by default set(CTEST_SERVER_HOST "0.0.0.0") diff --git a/tests/unit_test_cache_expire.cpp b/tests/unit_test_cache_expire.cpp index 5e67265c1..664e2539e 100644 --- a/tests/unit_test_cache_expire.cpp +++ b/tests/unit_test_cache_expire.cpp @@ -89,6 +89,8 @@ TEST_CASE("cache_expire.c", "[cache_expire]") || Catch::Matchers::Contains("Failed to lookup HTCP cache host: Name could not be resolved at this time") || + Catch::Matchers::Contains("Failed to lookup HTCP cache host: Name does not resolve") + || Catch::Matchers::Contains("Failed to lookup HTCP cache host: Name or service not known") || Catch::Matchers::Contains("Failed to lookup HTCP cache host: Temporary failure in name resolution") From f598560fa3b4cb96a804f5fbeb74080c8eef6283 Mon Sep 17 00:00:00 2001 From: David Hummel <6109326+hummeltech@users.noreply.github.com> Date: Fri, 23 Oct 2026 15:38:10 -0700 Subject: [PATCH 2/7] Improving testing --- Makefile.am | 2 +- includes/render_submit_queue.h | 28 +- src/CMakeLists.txt | 1 + src/parameterize_style.cpp | 3 +- src/protocol_helper.c | 13 + src/render_old.c | 11 - src/render_submit_queue.c | 337 ++++++----- src/request_queue.c | 7 +- src/store_file_utils.c | 3 + src/store_memcached.c | 24 +- src/store_rados.c | 58 +- src/store_ro_composite.c | 33 +- src/store_ro_http_proxy.c | 29 +- tests/CMakeLists.txt | 40 +- tests/catch_main.cpp | 2 +- tests/catch_test_common.cpp | 268 +++++++-- tests/catch_test_common.hpp | 71 ++- tests/gen_tile_test.cpp | 314 +++++----- tests/render_expired_test.cpp | 47 +- tests/render_list_test.cpp | 128 ++-- tests/render_old_test.cpp | 41 +- tests/render_speedtest_test.cpp | 32 +- tests/renderd_test.cpp | 23 +- tests/unit_test_cache_expire.cpp | 93 +-- tests/unit_test_metatile.cpp | 62 +- tests/unit_test_parameterize_style.cpp | 55 +- tests/unit_test_protocol_helper.cpp | 101 +++- tests/unit_test_render_submit_queue.cpp | 770 +++++++++++++++++++++--- tests/unit_test_renderd_config.cpp | 230 ++++--- tests/unit_test_request_queue.cpp | 652 ++++++++++++++++++++ tests/unit_test_store.cpp | 180 ++++++ tests/unit_test_store_file.cpp | 556 +++++++++++++++++ tests/unit_test_store_file_utils.cpp | 213 +++++++ tests/unit_test_store_memcached.cpp | 259 ++++++++ tests/unit_test_store_null.cpp | 121 ++++ tests/unit_test_store_rados.cpp | 422 +++++++++++++ tests/unit_test_store_ro_composite.cpp | 450 ++++++++++++++ tests/unit_test_store_ro_http_proxy.cpp | 478 +++++++++++++++ tests/unit_test_sys_utils.cpp | 29 +- tests/unit_tests.cpp | 66 +- 40 files changed, 5487 insertions(+), 765 deletions(-) create mode 100644 tests/unit_test_request_queue.cpp create mode 100644 tests/unit_test_store.cpp create mode 100644 tests/unit_test_store_file.cpp create mode 100644 tests/unit_test_store_file_utils.cpp create mode 100644 tests/unit_test_store_memcached.cpp create mode 100644 tests/unit_test_store_null.cpp create mode 100644 tests/unit_test_store_rados.cpp create mode 100644 tests/unit_test_store_ro_composite.cpp create mode 100644 tests/unit_test_store_ro_http_proxy.cpp diff --git a/Makefile.am b/Makefile.am index aa31f1c09..5288efddc 100644 --- a/Makefile.am +++ b/Makefile.am @@ -132,7 +132,7 @@ render_speedtest_test_LDADD = $(GLIB_LIBS) catch_main.o catch_test_common.o unit_tests_SOURCES = \ tests/unit_tests.cpp unit_tests_CXXFLAGS = -Isrc -unit_tests_LDADD = $(GLIB_LIBS) $(INIPARSER_LDFLAGS) $(MAPNIK_LDFLAGS) $(PTHREAD_CFLAGS) catch_main.o catch_test_common.o +unit_tests_LDADD = $(STORE_LDFLAGS) $(INIPARSER_LDFLAGS) $(MAPNIK_LDFLAGS) $(PTHREAD_CFLAGS) catch_main.o catch_test_common.o CLEANFILES=*.slo mod_tile.la stderr.out src/*.slo src/*.lo src/.libs/* src/*.la diff --git a/includes/render_submit_queue.h b/includes/render_submit_queue.h index 8c216aa0a..1b27b97b6 100644 --- a/includes/render_submit_queue.h +++ b/includes/render_submit_queue.h @@ -17,16 +17,40 @@ #ifndef RENDERSUBQUEUE_H #define RENDERSUBQUEUE_H + +#include + +#include "render_config.h" + #ifdef __cplusplus extern "C" { #endif +struct qItem { + char *mapname; + int x, y, z; + struct qItem *next; +}; + +struct speed_stat { + time_t time_min; + time_t time_max; + time_t time_total; + int noRendered; +}; + +struct speed_stats { + struct speed_stat stat[MAX_ZOOM + 1]; +}; + int make_connection(const char *spath); +void check_load(void); void enqueue(const char *xmlname, int x, int y, int z); -void spawn_workers(int num, const char *socketpath, int maxLoad); -void wait_for_empty_queue(void); void finish_workers(void); void print_statistics(void); +void spawn_workers(int num, const char *socketpath, int maxLoad); +void *thread_main(void *arg); +void wait_for_empty_queue(void); #ifdef __cplusplus } diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index c182ac8d7..77c1acf8e 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -239,6 +239,7 @@ set(unit_tests_LIBS ${COMMON_LIBRARIES} ${INIPARSER_LIBRARIES} ${LIBMAPNIK_LIBRARIES} + ${STORE_LIBRARIES} ) add_executable(unit_tests ${unit_tests_SRCS}) set_target_properties(unit_tests PROPERTIES RUNTIME_OUTPUT_DIRECTORY ${PROJECT_BINARY_DIR}/tests) diff --git a/src/parameterize_style.cpp b/src/parameterize_style.cpp index 454253e1b..b6d809a1d 100644 --- a/src/parameterize_style.cpp +++ b/src/parameterize_style.cpp @@ -83,7 +83,8 @@ static void parameterize_map_language(mapnik::Map &m, char *parameter) parameterize_function_ptr init_parameterization_function(const char *function_name) { - if (strcmp(function_name, "") == 0) { + + if (!function_name || strcmp(function_name, "") == 0) { g_logger(G_LOG_LEVEL_DEBUG, "Parameterize_style not specified (or empty string specified)"); return NULL; } else if (strcmp(function_name, "language") == 0) { diff --git a/src/protocol_helper.c b/src/protocol_helper.c index 3f496caa8..13dc66050 100644 --- a/src/protocol_helper.c +++ b/src/protocol_helper.c @@ -27,6 +27,14 @@ int send_cmd(struct protocol * cmd, int fd) { int ret; + int socket_type; + socklen_t optlen = sizeof(socket_type); + + if (fd < 0) { + g_logger(G_LOG_LEVEL_WARNING, "Attempted to send cmd on invalid fd %d", fd); + return -1; + } + g_logger(G_LOG_LEVEL_DEBUG, "Sending render cmd(%i %s %i/%i/%i) with protocol version %i to fd %i", cmd->cmd, cmd->xmlname, cmd->z, cmd->x, cmd->y, cmd->ver, fd); if ((cmd->ver > 3) || (cmd->ver < 1)) { @@ -34,6 +42,11 @@ int send_cmd(struct protocol * cmd, int fd) return -1; } + if (getsockopt(fd, SOL_SOCKET, SO_TYPE, &socket_type, &optlen) < 0) { + g_logger(G_LOG_LEVEL_ERROR, "fd %d is not a valid socket: %s", fd, strerror(errno)); + return -1; + } + switch (cmd->ver) { case 1: ret = send(fd, cmd, sizeof(struct protocol_v1), 0); diff --git a/src/render_old.c b/src/render_old.c index 151e065a5..c382c93b6 100644 --- a/src/render_old.c +++ b/src/render_old.c @@ -85,17 +85,6 @@ static time_t get_planet_time(const char *tile_dir) return planet_time; } -static void check_load(void) -{ - double avg = get_load_avg(); - - while (avg >= max_load) { - g_logger(G_LOG_LEVEL_MESSAGE, "Load average %f, sleeping", avg); - sleep(5); - avg = get_load_avg(); - } -} - static void descend(const char *tile_dir, const char *search, int verbose) { DIR *tiles = opendir(search); diff --git a/src/render_submit_queue.c b/src/render_submit_queue.c index abd3333b6..ebff3192b 100644 --- a/src/render_submit_queue.c +++ b/src/render_submit_queue.c @@ -47,22 +47,6 @@ static int maxLoad = 0; static unsigned int qMaxLen; static unsigned int qLen; -struct qItem { - char *mapname; - int x, y, z; - struct qItem *next; -}; - -struct speed_stat { - time_t time_min; - time_t time_max; - time_t time_total; - int noRendered; -}; - -struct speed_stats { - struct speed_stat stat[MAX_ZOOM + 1]; -}; struct speed_stats performance_stats; @@ -72,77 +56,6 @@ static int no_workers; static pthread_t *workers; static int work_complete; -static void check_load(void) -{ - double avg = get_load_avg(); - - while (avg >= maxLoad) { - int seconds = 5; - g_logger(G_LOG_LEVEL_DEBUG, "Load average %.2f, sleeping %is", avg, seconds); - sleep(seconds); - avg = get_load_avg(); - } -} - -static int process(struct protocol *cmd, int fd) -{ - struct timeval tim; - time_t t1; - time_t t2; - int ret = 0; - struct protocol rsp; - - gettimeofday(&tim, NULL); - t1 = tim.tv_sec * 1000 + (tim.tv_usec / 1000); - - g_logger(G_LOG_LEVEL_DEBUG, "Sending request"); - - if (send_cmd(cmd, fd) < 1) { - g_logger(G_LOG_LEVEL_ERROR, "send error: %s", strerror(errno)); - }; - - bzero(&rsp, sizeof(rsp)); - - g_logger(G_LOG_LEVEL_DEBUG, "Waiting for response"); - - ret = recv_cmd(&rsp, fd, 1); - - if (ret < 1) { - return 0; - } - - g_logger(G_LOG_LEVEL_DEBUG, "Got response %i", rsp.cmd); - - if (rsp.cmd != cmdDone) { - int seconds = 1; - g_logger(G_LOG_LEVEL_DEBUG, "Rendering not done with command %i, sleeping %is", rsp.cmd, seconds); - sleep(seconds); - } else { - gettimeofday(&tim, NULL); - t2 = tim.tv_sec * 1000 + (tim.tv_usec / 1000); - pthread_mutex_lock(&qStatsLock); - t1 = t2 - t1; - performance_stats.stat[cmd->z].noRendered++; - performance_stats.stat[cmd->z].time_total += t1; - - if ((performance_stats.stat[cmd->z].time_min > t1) || (performance_stats.stat[cmd->z].time_min == 0)) { - performance_stats.stat[cmd->z].time_min = t1; - } - - if (performance_stats.stat[cmd->z].time_max < t1) { - performance_stats.stat[cmd->z].time_max = t1; - } - - pthread_mutex_unlock(&qStatsLock); - } - - if (!ret) { - g_logger(G_LOG_LEVEL_ERROR, "Socket send error: %s", strerror(errno)); - } - - return ret; -} - static struct protocol *fetch(void) { pthread_mutex_lock(&qLock); @@ -189,46 +102,59 @@ static struct protocol *fetch(void) return cmd; } -void enqueue(const char *xmlname, int x, int y, int z) +static int process(struct protocol *cmd, int fd) { - g_logger(G_LOG_LEVEL_DEBUG, "Enqueueing tile (xmlname=%s x=%i y=%i z=%i)", xmlname, x, y, z); + struct timeval tim; + time_t t1; + time_t t2; + int ret = 0; + struct protocol rsp; - // Add this path in the local render queue - struct qItem *e = (struct qItem *)malloc(sizeof(struct qItem)); + gettimeofday(&tim, NULL); + t1 = tim.tv_sec * 1000 + (tim.tv_usec / 1000); - e->mapname = strdup(xmlname); - e->x = x; - e->y = y; - e->z = z; - e->next = NULL; + g_logger(G_LOG_LEVEL_DEBUG, "Sending request"); - if (!e->mapname) { - g_logger(G_LOG_LEVEL_CRITICAL, "Malloc failure"); - exit(1); - } + if (send_cmd(cmd, fd) < 1) { + g_logger(G_LOG_LEVEL_ERROR, "send error: %s", strerror(errno)); + }; - pthread_mutex_lock(&qLock); + bzero(&rsp, sizeof(rsp)); - while (qLen == qMaxLen) { - int ret = pthread_cond_wait(&qCondNotFull, &qLock); + g_logger(G_LOG_LEVEL_DEBUG, "Waiting for response"); - if (ret != 0) { - g_logger(G_LOG_LEVEL_WARNING, "pthread_cond_wait(qCondNotFull): %s", strerror(ret)); - } + ret = recv_cmd(&rsp, fd, 1); + + if (ret < 1) { + return 0; } - // Append item to end of queue - if (qTail) { - qTail->next = e; + g_logger(G_LOG_LEVEL_DEBUG, "Got response %i", rsp.cmd); + + if (rsp.cmd != cmdDone) { + int seconds = 1; + g_logger(G_LOG_LEVEL_DEBUG, "Rendering not done with command %i, sleeping %is", rsp.cmd, seconds); + sleep(seconds); } else { - qHead = e; - } + gettimeofday(&tim, NULL); + t2 = tim.tv_sec * 1000 + (tim.tv_usec / 1000); + pthread_mutex_lock(&qStatsLock); + t1 = t2 - t1; + performance_stats.stat[cmd->z].noRendered++; + performance_stats.stat[cmd->z].time_total += t1; - qTail = e; - qLen++; - pthread_cond_signal(&qCondNotEmpty); + if ((performance_stats.stat[cmd->z].time_min > t1) || (performance_stats.stat[cmd->z].time_min == 0)) { + performance_stats.stat[cmd->z].time_min = t1; + } - pthread_mutex_unlock(&qLock); + if (performance_stats.stat[cmd->z].time_max < t1) { + performance_stats.stat[cmd->z].time_max = t1; + } + + pthread_mutex_unlock(&qStatsLock); + } + + return ret; } int make_connection(const char *spath) @@ -331,7 +257,7 @@ int make_connection(const char *spath) int name_info = getnameinfo(rp->ai_addr, rp->ai_addrlen, resolved_addr, sizeof(resolved_addr), resolved_port, sizeof(resolved_port), NI_NUMERICHOST | NI_NUMERICSERV); if (name_info != 0) { - g_logger(G_LOG_LEVEL_CRITICAL, "cannot retrieve name info: %d", name_info); + g_logger(G_LOG_LEVEL_CRITICAL, "cannot retrieve name info: %s", gai_strerror(name_info)); exit(2); } @@ -356,44 +282,112 @@ int make_connection(const char *spath) return fd; } -void *thread_main(void *arg) +void check_load(void) { - const char *spath = (const char *)arg; - int fd = make_connection(spath); + double avg = get_load_avg(); - if (fd < 0) { - g_logger(G_LOG_LEVEL_ERROR, "connect failed for: %s", spath); - return NULL; + while (avg >= maxLoad) { + int seconds = 5; + g_logger(G_LOG_LEVEL_DEBUG, "Load average %.2f, sleeping %is", avg, seconds); + sleep(seconds); + avg = get_load_avg(); } +} - while (1) { - struct protocol *cmd; - check_load(); +void display_rate(struct timeval start, struct timeval end, int num) +{ + int d_s, d_us; + float sec; - if (!(cmd = fetch())) { - break; - } + d_s = end.tv_sec - start.tv_sec; + d_us = end.tv_usec - start.tv_usec; - while (process(cmd, fd) < 1) { - g_logger(G_LOG_LEVEL_ERROR, "connection to renderd lost"); - close(fd); - fd = FD_INVALID; + sec = d_s + d_us / 1000000.0; - while (fd < 0) { - int seconds = 30; - g_logger(G_LOG_LEVEL_WARNING, "sleeping for %i seconds", seconds); - sleep(30); - g_logger(G_LOG_LEVEL_WARNING, "attempting to reconnect"); - fd = make_connection(spath); - } + g_logger(G_LOG_LEVEL_MESSAGE, "\t%d in %.2f seconds (%.2f/s)", num, sec, num / sec); +} + +void enqueue(const char *xmlname, int x, int y, int z) +{ + g_logger(G_LOG_LEVEL_DEBUG, "Enqueueing tile (xmlname=%s x=%i y=%i z=%i)", xmlname, x, y, z); + + // Add this path in the local render queue + struct qItem *e = (struct qItem *)malloc(sizeof(struct qItem)); + + if (!e) { + g_logger(G_LOG_LEVEL_CRITICAL, "Error allocating queue item: %s", strerror(errno)); + exit(1); + } + + e->mapname = strdup(xmlname); + e->x = x; + e->y = y; + e->z = z; + e->next = NULL; + + if (!e->mapname) { + free(e); + g_logger(G_LOG_LEVEL_CRITICAL, "Error allocating queue item mapname: %s", strerror(errno)); + exit(1); + } + + pthread_mutex_lock(&qLock); + + while (qLen == qMaxLen) { + int ret = pthread_cond_wait(&qCondNotFull, &qLock); + + if (ret != 0) { + g_logger(G_LOG_LEVEL_WARNING, "pthread_cond_wait(qCondNotFull): %s", strerror(ret)); } + } - free(cmd); + // Append item to end of queue + if (qTail) { + qTail->next = e; + } else { + qHead = e; } - close(fd); + qTail = e; + qLen++; + pthread_cond_signal(&qCondNotEmpty); - return NULL; + pthread_mutex_unlock(&qLock); +} + +void finish_workers(void) +{ + g_logger(G_LOG_LEVEL_MESSAGE, "Waiting for rendering threads to finish"); + pthread_mutex_lock(&qLock); + work_complete = 1; + pthread_mutex_unlock(&qLock); + pthread_cond_broadcast(&qCondNotEmpty); + + for (int i = 0; i < no_workers; i++) { + pthread_join(workers[i], NULL); + } + + free(workers); + no_workers = 0; + workers = NULL; +} + +void print_statistics(void) +{ + int i; + g_logger(G_LOG_LEVEL_MESSAGE, "*****************************************************"); + + for (i = 0; i <= MAX_ZOOM; i++) { + if (performance_stats.stat[i].noRendered == 0) { + continue; + } + + g_logger(G_LOG_LEVEL_MESSAGE, "Zoom %02i: min: %4.1f avg: %4.1f max: %4.1f over a total of %8.1fs in %i requests", + i, performance_stats.stat[i].time_min / 1000.0, (performance_stats.stat[i].time_total / (float)performance_stats.stat[i].noRendered) / 1000.0, + performance_stats.stat[i].time_max / 1000.0, performance_stats.stat[i].time_total / 1000.0, performance_stats.stat[i].noRendered); + } + + g_logger(G_LOG_LEVEL_MESSAGE, "*****************************************************"); } void spawn_workers(int num, const char *spath, int max_load) @@ -428,23 +422,44 @@ void spawn_workers(int num, const char *spath, int max_load) } } -void print_statistics(void) +void *thread_main(void *arg) { - int i; - printf("*****************************************************\n"); + const char *spath = (const char *)arg; + int fd = make_connection(spath); - for (i = 0; i <= MAX_ZOOM; i++) { - if (performance_stats.stat[i].noRendered == 0) { - continue; + if (fd < 0) { + g_logger(G_LOG_LEVEL_ERROR, "connect failed for: %s", spath); + return NULL; + } + + while (1) { + struct protocol *cmd; + check_load(); + + if (!(cmd = fetch())) { + break; + } + + while (process(cmd, fd) < 1) { + g_logger(G_LOG_LEVEL_ERROR, "connection to renderd lost"); + close(fd); + fd = FD_INVALID; + + while (fd < 0) { + int seconds = 30; + g_logger(G_LOG_LEVEL_WARNING, "sleeping for %i seconds", seconds); + sleep(30); + g_logger(G_LOG_LEVEL_WARNING, "attempting to reconnect"); + fd = make_connection(spath); + } } - printf("Zoom %02i: min: %4.1f avg: %4.1f max: %4.1f over a total of %8.1fs in %i requests\n", - i, performance_stats.stat[i].time_min / 1000.0, (performance_stats.stat[i].time_total / (float)performance_stats.stat[i].noRendered) / 1000.0, - performance_stats.stat[i].time_max / 1000.0, performance_stats.stat[i].time_total / 1000.0, performance_stats.stat[i].noRendered); + free(cmd); } - printf("*****************************************************\n"); - printf("*****************************************************\n"); + close(fd); + + return NULL; } void wait_for_empty_queue() @@ -458,19 +473,3 @@ void wait_for_empty_queue() pthread_mutex_unlock(&qLock); } - -void finish_workers(void) -{ - g_logger(G_LOG_LEVEL_MESSAGE, "Waiting for rendering threads to finish"); - pthread_mutex_lock(&qLock); - work_complete = 1; - pthread_mutex_unlock(&qLock); - pthread_cond_broadcast(&qCondNotEmpty); - - for (int i = 0; i < no_workers; i++) { - pthread_join(workers[i], NULL); - } - - free(workers); - workers = NULL; -} diff --git a/src/request_queue.c b/src/request_queue.c index 7f5cbf2bd..4ec360a34 100644 --- a/src/request_queue.c +++ b/src/request_queue.c @@ -283,7 +283,7 @@ enum protoCmd request_queue_add_request(struct request_queue * queue, struct ite req = &(item->req); if (queue == NULL) { - g_logger(G_LOG_LEVEL_CRITICAL, "queue os NULL"); + g_logger(G_LOG_LEVEL_CRITICAL, "queue is NULL"); exit(3); } @@ -352,7 +352,10 @@ enum protoCmd request_queue_add_request(struct request_queue * queue, struct ite pthread_cond_signal(&queue->qCond); } else { + // This seems to be unreachable + /* LCOV_EXCL_START */ free(item); + /* LCOV_EXCL_STOP */ } pthread_mutex_unlock(&queue->qLock); @@ -453,7 +456,7 @@ void request_queue_copy_stats(struct request_queue * queue, stats_struct * stats struct request_queue * request_queue_init() { int res; - struct request_queue * queue = calloc(1, sizeof(struct request_queue)); + struct request_queue * queue = (struct request_queue *)calloc(1, sizeof(struct request_queue)); if (queue == NULL) { return NULL; diff --git a/src/store_file_utils.c b/src/store_file_utils.c index f1e9d1377..84fe8ca28 100644 --- a/src/store_file_utils.c +++ b/src/store_file_utils.c @@ -59,7 +59,10 @@ int mkdirp(const char *path) p = tmp; if (!*p) { + // This seems to be unreachable + /* LCOV_EXCL_START */ return 0; + /* LCOV_EXCL_STOP */ } p++; // Ignore leading / diff --git a/src/store_memcached.c b/src/store_memcached.c index b9ca8f17a..50d20bc3c 100644 --- a/src/store_memcached.c +++ b/src/store_memcached.c @@ -89,7 +89,7 @@ static int memcached_tile_read(struct storage_backend * store, const char *xmlco meta_offset = (x & mask) * METATILE + (y & mask); memcached_xyzo_to_storagekey(xmlconfig, options, x, y, z, meta_path); - buf_raw = memcached_get(store->storage_ctx, meta_path, strlen(meta_path), &len, &flags, &rc); + buf_raw = memcached_get((memcached_st *)store->storage_ctx, meta_path, strlen(meta_path), &len, &flags, &rc); if (rc != MEMCACHED_SUCCESS) { free(m); @@ -149,7 +149,7 @@ static struct stat_info memcached_tile_stat(struct storage_backend * store, cons offset = (x & mask) * METATILE + (y & mask); memcached_xyzo_to_storagekey(xmlconfig, options, x, y, z, meta_path); - buf = memcached_get(store->storage_ctx, meta_path, strlen(meta_path), &len, &flags, &rc); + buf = memcached_get((memcached_st *)store->storage_ctx, meta_path, strlen(meta_path), &len, &flags, &rc); if (rc != MEMCACHED_SUCCESS) { tile_stat.size = -1; @@ -184,7 +184,7 @@ static int memcached_metatile_write(struct storage_backend * store, const char * char tmp[PATH_MAX]; struct stat_info tile_stat; int sz2 = sz + sizeof(struct stat_info); - char * buf2 = malloc(sz2); + char * buf2 = (char *)malloc(sz2); memcached_return_t rc; if (buf2 == NULL) { @@ -204,14 +204,14 @@ static int memcached_metatile_write(struct storage_backend * store, const char * snprintf(meta_path, PATH_MAX - 1, "%s/%d/%d/%d.meta", xmlconfig, x, y, z); - rc = memcached_set(store->storage_ctx, meta_path, strlen(meta_path), buf2, sz2, (time_t)0, (uint32_t)0); + rc = memcached_set((memcached_st *)store->storage_ctx, meta_path, strlen(meta_path), buf2, sz2, (time_t)0, (uint32_t)0); free(buf2); if (rc != MEMCACHED_SUCCESS) { return -1; } - memcached_flush_buffers(store->storage_ctx); + memcached_flush_buffers((memcached_st *)store->storage_ctx); return sz; } @@ -224,7 +224,7 @@ static int memcached_metatile_delete(struct storage_backend * store, const char //TODO: deal with options memcached_xyz_to_storagekey(xmlconfig, x, y, z, meta_path); - rc = memcached_delete(store->storage_ctx, meta_path, strlen(meta_path), 0); + rc = memcached_delete((memcached_st *)store->storage_ctx, meta_path, strlen(meta_path), 0); if (rc != MEMCACHED_SUCCESS) { return -1; @@ -244,7 +244,7 @@ static int memcached_metatile_expire(struct storage_backend * store, const char //TODO: deal with options memcached_xyz_to_storagekey(xmlconfig, x, y, z, meta_path); - buf = memcached_get(store->storage_ctx, meta_path, strlen(meta_path), &len, &flags, &rc); + buf = memcached_get((memcached_st *)store->storage_ctx, meta_path, strlen(meta_path), &len, &flags, &rc); if (rc != MEMCACHED_SUCCESS) { return -1; @@ -254,7 +254,7 @@ static int memcached_metatile_expire(struct storage_backend * store, const char // memcached_get() does not return a CAS value, so a compare-and-swap cannot be used here // (the previous code passed an uninitialised CAS token). Replace the existing item instead. - rc = memcached_replace(store->storage_ctx, meta_path, strlen(meta_path), buf, len, 0, flags); + rc = memcached_replace((memcached_st *)store->storage_ctx, meta_path, strlen(meta_path), buf, len, 0, flags); if (rc != MEMCACHED_SUCCESS) { free(buf); @@ -267,7 +267,7 @@ static int memcached_metatile_expire(struct storage_backend * store, const char static int memcached_close_storage(struct storage_backend * store) { - memcached_free(store->storage_ctx); + memcached_free((memcached_st *)store->storage_ctx); store->storage_ctx = NULL; return 0; } @@ -278,10 +278,10 @@ struct storage_backend * init_storage_memcached(const char * connection_string) { #ifndef HAVE_LIBMEMCACHED - g_logger(G_LOG_LEVEL_ERROR, "init_storage_memcached: Support for memcached has not been compiled into this program"); + g_logger(G_LOG_LEVEL_ERROR, "init_storage_memcached: Support for memcached storage has not been compiled into this program"); return NULL; #else - struct storage_backend * store = malloc(sizeof(struct storage_backend)); + struct storage_backend * store = (struct storage_backend *)malloc(sizeof(struct storage_backend)); memcached_st * ctx; char * connection_str = "--server=localhost"; @@ -292,7 +292,7 @@ struct storage_backend * init_storage_memcached(const char * connection_string) if (strcmp(connection_string, "memcached://")) { int connection_string_len = strnlen(connection_string, PATH_MAX); - connection_str = malloc(sizeof(char) * connection_string_len); + connection_str = (char *)malloc(sizeof(char) * connection_string_len); // The length of the string "memcached://" is 12 snprintf(connection_str, connection_string_len - 2, "--server=%.*s", connection_string_len - 12, connection_string + 12); } diff --git a/src/store_rados.c b/src/store_rados.c index fc246321c..3f0e4aaed 100644 --- a/src/store_rados.c +++ b/src/store_rados.c @@ -48,7 +48,7 @@ #ifdef HAVE_LIBRADOS -static pthread_mutex_t qLock; +static pthread_mutex_t rados_qLock; struct metadata_cache { char * data; @@ -236,7 +236,7 @@ static int rados_metatile_write(struct storage_backend * store, const char *xmlc char tmp[PATH_MAX]; struct stat_info tile_stat; int sz2 = sz + sizeof(struct stat_info); - char * buf2 = malloc(sz2); + char * buf2 = (char *)malloc(sz2); int err; if (buf2 == NULL) { @@ -256,7 +256,15 @@ static int rados_metatile_write(struct storage_backend * store, const char *xmlc rados_xyzo_to_storagekey(xmlconfig, options, x, y, z, meta_path); g_logger(G_LOG_LEVEL_DEBUG, "Trying to create and write a tile to %s", rados_tile_storage_id(store, xmlconfig, options, x, y, z, tmp)); - err = rados_write_full(((struct rados_ctx *)store->storage_ctx)->io, meta_path, buf2, sz2); + struct rados_ctx * ctx = (struct rados_ctx *)store->storage_ctx; + + if (!ctx || !ctx->io) { + g_logger(G_LOG_LEVEL_ERROR, "rados_metatile_write: cannot write %s: invalid rados io context", tmp); + free(buf2); + return -1; + } + + err = rados_write_full(ctx->io, meta_path, buf2, sz2); if (err < 0) { g_logger(G_LOG_LEVEL_ERROR, "cannot write %s: %s", rados_tile_storage_id(store, xmlconfig, options, x, y, z, tmp), strerror(-err)); @@ -280,8 +288,14 @@ static int rados_metatile_delete(struct storage_backend * store, const char *xml //TODO: deal with options const char *options = ""; rados_xyzo_to_storagekey(xmlconfig, options, x, y, z, meta_path); + g_logger(G_LOG_LEVEL_DEBUG, "Trying to delete a tile at %s", rados_tile_storage_id(store, xmlconfig, options, x, y, z, tmp)); - err = rados_remove(ctx->io, meta_path); + if (!ctx || !ctx->io) { + g_logger(G_LOG_LEVEL_ERROR, "rados_metatile_delete: cannot delete %s: invalid rados io context", tmp); + return -1; + } + + err = rados_remove(ctx->io, meta_path); if (err < 0) { g_logger(G_LOG_LEVEL_ERROR, "failed to delete %s: %s", rados_tile_storage_id(store, xmlconfig, options, x, y, z, tmp), strerror(-err)); @@ -303,6 +317,13 @@ static int rados_metatile_expire(struct storage_backend * store, const char *xml //TODO: deal with options const char *options = ""; rados_xyzo_to_storagekey(xmlconfig, options, x, y, z, meta_path); + g_logger(G_LOG_LEVEL_DEBUG, "Trying to expire a tile at %s", rados_tile_storage_id(store, xmlconfig, options, x, y, z, tmp)); + + if (!ctx || !ctx->io) { + g_logger(G_LOG_LEVEL_ERROR, "rados_metatile_expire: cannot expire %s: invalid rados io context", tmp); + return -1; + } + err = rados_read(ctx->io, meta_path, (char *)&tile_stat, sizeof(struct stat_info), 0); if (err < 0) { @@ -331,10 +352,21 @@ static int rados_metatile_expire(struct storage_backend * store, const char *xml static int rados_close_storage(struct storage_backend * store) { + if (!store || !store->storage_ctx) { + g_logger(G_LOG_LEVEL_ERROR, "rados_close_storage: cannot close storage: invalid storage context"); + return -1; + } + struct rados_ctx * ctx = (struct rados_ctx *)store->storage_ctx; - rados_ioctx_destroy(ctx->io); - rados_shutdown(ctx->cluster); + if (ctx->io) { + rados_ioctx_destroy(ctx->io); + } + + if (ctx->cluster) { + rados_shutdown(ctx->cluster); + } + g_logger(G_LOG_LEVEL_DEBUG, "rados_close_storage: Closed rados backend"); free(ctx->metadata_cache.data); free(ctx->pool); @@ -352,11 +384,11 @@ struct storage_backend * init_storage_rados(const char * connection_string) { #ifndef HAVE_LIBRADOS - g_logger(G_LOG_LEVEL_ERROR, "init_storage_rados: Support for rados has not been compiled into this program"); + g_logger(G_LOG_LEVEL_ERROR, "init_storage_rados: Support for rados storage has not been compiled into this program"); return NULL; #else - struct rados_ctx * ctx = malloc(sizeof(struct rados_ctx)); - struct storage_backend * store = malloc(sizeof(struct storage_backend)); + struct rados_ctx * ctx = (struct rados_ctx *)malloc(sizeof(struct rados_ctx)); + struct storage_backend * store = (struct storage_backend *)malloc(sizeof(struct storage_backend)); char * conf = NULL; const char * tmp; int err; @@ -375,7 +407,7 @@ struct storage_backend * init_storage_rados(const char * connection_string) i++; } - ctx->pool = calloc(i + 1, sizeof(char)); + ctx->pool = (char *)calloc(i + 1, sizeof(char)); memcpy(ctx->pool, tmp, i * sizeof(char)); conf = strdup(&(tmp[i])); @@ -397,9 +429,9 @@ struct storage_backend * init_storage_rados(const char * connection_string) return NULL; } - pthread_mutex_lock(&qLock); + pthread_mutex_lock(&rados_qLock); err = rados_connect(ctx->cluster); - pthread_mutex_unlock(&qLock); + pthread_mutex_unlock(&rados_qLock); if (err < 0) { g_logger(G_LOG_LEVEL_ERROR, "init_storage_rados: failed to connect to rados cluster: %s", strerror(-err)); @@ -420,7 +452,7 @@ struct storage_backend * init_storage_rados(const char * connection_string) g_logger(G_LOG_LEVEL_DEBUG, "init_storage_rados: Initialised rados backend for pool %s with config %s", ctx->pool, conf); - ctx->metadata_cache.data = malloc(sizeof(struct stat_info) + sizeof(struct meta_layout) + METATILE * METATILE * sizeof(struct entry)); + ctx->metadata_cache.data = (char *)malloc(sizeof(struct stat_info) + sizeof(struct meta_layout) + METATILE * METATILE * sizeof(struct entry)); if (ctx->metadata_cache.data == NULL) { rados_ioctx_destroy(ctx->io); diff --git a/src/store_ro_composite.c b/src/store_ro_composite.c index a95897368..1b0298fc9 100644 --- a/src/store_ro_composite.c +++ b/src/store_ro_composite.c @@ -39,7 +39,7 @@ #ifdef WANT_STORE_COMPOSITE -struct tile_cache { +struct store_ro_composite_tile_cache { struct stat_info st_stat; char * tile; int x, y, z; @@ -51,7 +51,7 @@ struct ro_composite_ctx { char xmlconfig_primary[XMLCONFIG_MAX]; struct storage_backend * store_secondary; char xmlconfig_secondary[XMLCONFIG_MAX]; - struct tile_cache cache; + struct store_ro_composite_tile_cache cache; int render_size; }; @@ -113,8 +113,13 @@ static int ro_composite_tile_read(struct storage_backend * store, const char *xm closure.max_size = sz; imageA = cairo_image_surface_create_from_png_stream(&read_png_stream_from_byte_array, &closure); - if (!imageA) { + if (!imageA || cairo_surface_status(imageA) != CAIRO_STATUS_SUCCESS) { snprintf(log_msg, 1024, "ro_composite_tile_read: Failed to decode png data from primary backend\n"); + + if (imageA) { + cairo_surface_destroy(imageA); + } + return -1; } @@ -129,18 +134,28 @@ static int ro_composite_tile_read(struct storage_backend * store, const char *xm closure.max_size = sz; imageB = cairo_image_surface_create_from_png_stream(&read_png_stream_from_byte_array, &closure); - if (!imageB) { + if (!imageB || cairo_surface_status(imageB) != CAIRO_STATUS_SUCCESS) { snprintf(log_msg, 1024, "ro_composite_tile_read: Failed to decode png data from secondary backend\n"); cairo_surface_destroy(imageA); + + if (imageB) { + cairo_surface_destroy(imageB); + } + return -1; } imageC = cairo_image_surface_create(CAIRO_FORMAT_ARGB32, ctx->render_size, ctx->render_size); - if (!imageC) { + if (!imageC || cairo_surface_status(imageC) != CAIRO_STATUS_SUCCESS) { snprintf(log_msg, 1024, "ro_composite_tile_read: Failed to create output png\n"); cairo_surface_destroy(imageA); cairo_surface_destroy(imageB); + + if (imageC) { + cairo_surface_destroy(imageC); + } + return -1; } @@ -229,8 +244,8 @@ struct storage_backend * init_storage_ro_composite(const char * connection_strin g_logger(G_LOG_LEVEL_ERROR, "init_storage_ro_coposite: Support for compositing storage has not been compiled into this program"); return NULL; #else - struct storage_backend * store = malloc(sizeof(struct storage_backend)); - struct ro_composite_ctx * ctx = calloc(1, sizeof(struct ro_composite_ctx)); + struct storage_backend * store = (struct storage_backend *)malloc(sizeof(struct storage_backend)); + struct ro_composite_ctx * ctx = (struct ro_composite_ctx *)calloc(1, sizeof(struct ro_composite_ctx)); char * connection_string_primary; char * connection_string_secondary; char * tmp; @@ -251,7 +266,7 @@ struct storage_backend * init_storage_ro_composite(const char * connection_strin return NULL; } - connection_string_secondary = strstr(connection_string, "}{"); + connection_string_secondary = (char *)strstr(connection_string, "}{"); if (connection_string_secondary == NULL) { g_logger(G_LOG_LEVEL_ERROR, "init_storage_ro_composite: malformed connection string, missing '}{' separator: %s", connection_string); @@ -260,7 +275,7 @@ struct storage_backend * init_storage_ro_composite(const char * connection_strin return NULL; } - connection_string_primary = malloc(strlen(connection_string) - strlen("composite:{") - strlen(connection_string_secondary) + 1); + connection_string_primary = (char *)malloc(strlen(connection_string) - strlen("composite:{") - strlen(connection_string_secondary) + 1); if (connection_string_primary == NULL) { g_logger(G_LOG_LEVEL_ERROR, "init_storage_ro_composite: failed to allocate memory for primary connection string"); diff --git a/src/store_ro_http_proxy.c b/src/store_ro_http_proxy.c index f7df56f32..c2428421a 100644 --- a/src/store_ro_http_proxy.c +++ b/src/store_ro_http_proxy.c @@ -38,10 +38,10 @@ #ifdef HAVE_LIBCURL -static pthread_mutex_t qLock; +static pthread_mutex_t ro_http_proxy_mutex; static int done_global_init = 0; -struct tile_cache { +struct ro_http_proxy_tile_cache { struct stat_info st_stat; char * tile; int x, y, z; @@ -51,7 +51,7 @@ struct tile_cache { struct ro_http_proxy_ctx { CURL * ctx; char * baseurl; - struct tile_cache cache; + struct ro_http_proxy_tile_cache cache; }; struct MemoryStruct { @@ -63,12 +63,12 @@ struct MemoryStruct { static size_t write_memory_callback(void *contents, size_t size, size_t nmemb, void *userp) { size_t realsize = size * nmemb; - struct MemoryStruct * chunk = userp; + struct MemoryStruct * chunk = (struct MemoryStruct *)userp; if (chunk->memory) { - chunk->memory = realloc(chunk->memory, chunk->size + realsize); + chunk->memory = (char *)realloc(chunk->memory, chunk->size + realsize); } else { - chunk->memory = malloc(realsize); + chunk->memory = (char *)malloc(realsize); } g_logger(G_LOG_LEVEL_DEBUG, "ro_http_proxy_tile_read: writing a chunk: Position %lu, size %lu", chunk->size, realsize); @@ -102,7 +102,12 @@ static int ro_http_proxy_tile_retrieve(struct storage_backend * store, const cha chunk.memory = NULL; chunk.size = 0; - path = malloc(PATH_MAX); + path = (char *)malloc(PATH_MAX); + + if (!path) { + g_logger(G_LOG_LEVEL_ERROR, "ro_http_proxy_tile_fetch: failed to allocate memory for path"); + return -1; + } ro_http_proxy_xyz_to_storagekey(store, x, y, z, path); g_logger(G_LOG_LEVEL_DEBUG, "ro_http_proxy_tile_fetch: proxing file %s", path); @@ -274,11 +279,11 @@ struct storage_backend * init_storage_ro_http_proxy(const char * connection_stri { #ifndef HAVE_LIBCURL - g_logger(G_LOG_LEVEL_ERROR, "init_storage_ro_http_proxy: Support for curl and therefore the http proxy storage has not been compiled into this program"); + g_logger(G_LOG_LEVEL_ERROR, "init_storage_ro_http_proxy: Support for http proxy storage has not been compiled into this program"); return NULL; #else - struct storage_backend * store = malloc(sizeof(struct storage_backend)); - struct ro_http_proxy_ctx * ctx = malloc(sizeof(struct ro_http_proxy_ctx)); + struct storage_backend * store = (struct storage_backend *)malloc(sizeof(struct storage_backend)); + struct ro_http_proxy_ctx * ctx = (struct ro_http_proxy_ctx *)malloc(sizeof(struct ro_http_proxy_ctx)); CURLcode res; g_logger(G_LOG_LEVEL_DEBUG, "init_storage_ro_http_proxy: initialising proxy storage backend for %s", connection_string); @@ -304,7 +309,7 @@ struct storage_backend * init_storage_ro_http_proxy(const char * connection_stri ctx->cache.xmlname[0] = 0; ctx->baseurl = strdup(&(connection_string[strlen("ro_http_proxy://")])); - pthread_mutex_lock(&qLock); + pthread_mutex_lock(&ro_http_proxy_mutex); if (!done_global_init) { g_logger(G_LOG_LEVEL_DEBUG, "init_storage_ro_http_proxy: Global init of curl"); @@ -314,7 +319,7 @@ struct storage_backend * init_storage_ro_http_proxy(const char * connection_stri res = CURLE_OK; } - pthread_mutex_unlock(&qLock); + pthread_mutex_unlock(&ro_http_proxy_mutex); if (res != CURLE_OK) { g_logger(G_LOG_LEVEL_ERROR, "init_storage_ro_http_proxy: failed to initialise global curl: %s", curl_easy_strerror(res)); diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 608b0370f..5f0ab69e9 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -443,6 +443,7 @@ foreach(STORAGE_BACKEND_INDEX RANGE ${STORAGE_BACKENDS_LENGTH}) set_tests_properties(render_list_${SOCKET_TYPE}_${STORAGE_BACKEND} PROPERTIES DEPENDS render_speedtest_${SOCKET_TYPE}_${STORAGE_BACKEND} FIXTURES_REQUIRED services_started_${STORAGE_BACKEND} + PASS_REGULAR_EXPRESSION "\t24 in [0-9.]+ seconds" TIMEOUT 90 ) add_test(NAME render_list_config_${SOCKET_TYPE}_${STORAGE_BACKEND} @@ -450,19 +451,20 @@ foreach(STORAGE_BACKEND_INDEX RANGE ${STORAGE_BACKENDS_LENGTH}) $ \ --all \ --config ${RENDERD_CONF} \ - --max-lat 85.0511 \ - --max-lon 180 \ - --max-zoom 5 \ - --min-lat -85.0511 \ - --min-lon -180 \ - --min-zoom 5 + --max-lat 49.4 \ + --max-lon -66.9 \ + --max-zoom 8 \ + --min-lat 24.4 \ + --min-lon -125.0 \ + --min-zoom 0 " WORKING_DIRECTORY tests ) set_tests_properties(render_list_config_${SOCKET_TYPE}_${STORAGE_BACKEND} PROPERTIES DEPENDS render_speedtest_${SOCKET_TYPE}_${STORAGE_BACKEND} FIXTURES_REQUIRED services_started_${STORAGE_BACKEND} - TIMEOUT 60 + PASS_REGULAR_EXPRESSION "\t47 in [0-9.]+ seconds" + TIMEOUT 90 ) add_test(NAME render_list_stdin_${SOCKET_TYPE}_${STORAGE_BACKEND} COMMAND ${BASH} -c " @@ -481,11 +483,13 @@ foreach(STORAGE_BACKEND_INDEX RANGE ${STORAGE_BACKENDS_LENGTH}) set_tests_properties(render_list_stdin_${SOCKET_TYPE}_${STORAGE_BACKEND} PROPERTIES DEPENDS render_speedtest_${SOCKET_TYPE}_${STORAGE_BACKEND} FIXTURES_REQUIRED services_started_${STORAGE_BACKEND} + PASS_REGULAR_EXPRESSION "\t10 in [0-9.]+ seconds" TIMEOUT 90 ) add_test(NAME render_list_stdin_config_${SOCKET_TYPE}_${STORAGE_BACKEND} COMMAND ${BASH} -c " echo '0 0 0' | $ \ + --force \ --config ${RENDERD_CONF} \ --verbose " @@ -494,6 +498,23 @@ foreach(STORAGE_BACKEND_INDEX RANGE ${STORAGE_BACKENDS_LENGTH}) set_tests_properties(render_list_stdin_config_${SOCKET_TYPE}_${STORAGE_BACKEND} PROPERTIES DEPENDS render_speedtest_${SOCKET_TYPE}_${STORAGE_BACKEND} FIXTURES_REQUIRED services_started_${STORAGE_BACKEND} + PASS_REGULAR_EXPRESSION "\t1 in [0-9.]+ seconds" + TIMEOUT 60 + ) + add_test(NAME render_list_ignore_clean_${SOCKET_TYPE}_${STORAGE_BACKEND} + COMMAND ${BASH} -c " + ${TOUCH_EXECUTABLE} -d '-1 month' ${TEST_TILES_DIR}/planet-import-complete + echo '0 0 0' | $ \ + --config ${RENDERD_CONF} \ + --verbose + " + WORKING_DIRECTORY tests + ) + set_tests_properties(render_list_ignore_clean_${SOCKET_TYPE}_${STORAGE_BACKEND} PROPERTIES + DEPENDS render_list_stdin_config_${SOCKET_TYPE}_${STORAGE_BACKEND} + FIXTURES_REQUIRED services_started_${STORAGE_BACKEND} + PASS_REGULAR_EXPRESSION "Tile .+/0/0/0.meta is clean, ignoring" + RUN_SERIAL TRUE TIMEOUT 60 ) add_test(NAME render_old_${SOCKET_TYPE}_${STORAGE_BACKEND} @@ -514,6 +535,7 @@ foreach(STORAGE_BACKEND_INDEX RANGE ${STORAGE_BACKENDS_LENGTH}) set_tests_properties(render_old_${SOCKET_TYPE}_${STORAGE_BACKEND} PROPERTIES DEPENDS render_speedtest_${SOCKET_TYPE}_${STORAGE_BACKEND} FIXTURES_REQUIRED services_started_${STORAGE_BACKEND} + RUN_SERIAL TRUE TIMEOUT 90 ) add_test(NAME render_old_config_${SOCKET_TYPE}_${STORAGE_BACKEND} @@ -527,6 +549,7 @@ foreach(STORAGE_BACKEND_INDEX RANGE ${STORAGE_BACKENDS_LENGTH}) set_tests_properties(render_old_config_${SOCKET_TYPE}_${STORAGE_BACKEND} PROPERTIES DEPENDS render_speedtest_${SOCKET_TYPE}_${STORAGE_BACKEND} FIXTURES_REQUIRED services_started_${STORAGE_BACKEND} + RUN_SERIAL TRUE TIMEOUT 60 ) add_test(NAME render_old_config_timestamp_${SOCKET_TYPE}_${STORAGE_BACKEND} @@ -788,6 +811,7 @@ foreach(STORAGE_BACKEND_INDEX RANGE ${STORAGE_BACKENDS_LENGTH}) ) set_tests_properties(tile_expired_${MAP_NAME}_${STORAGE_BACKEND} PROPERTIES FIXTURES_REQUIRED services_started_${STORAGE_BACKEND} + RUN_SERIAL TRUE ) endforeach() @@ -1323,7 +1347,7 @@ add_compile_definitions( RENDERD_CONF="${PROJECT_SOURCE_DIR}/etc/renderd/renderd.conf.examples" ) include_directories(${PROJECT_SOURCE_DIR}/includes) -link_libraries(${GLIB_LIBRARIES}) +link_libraries(${GLIB_LIBRARIES} Threads::Threads) #----------------------------------------------------------------------------- # diff --git a/tests/catch_main.cpp b/tests/catch_main.cpp index 9f23b2ff9..9d8ef3fc6 100644 --- a/tests/catch_main.cpp +++ b/tests/catch_main.cpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 2007 - 2023 by mod_tile contributors (see AUTHORS file) + * Copyright (c) 2007 - 2026 by mod_tile contributors (see AUTHORS file) * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License diff --git a/tests/catch_test_common.cpp b/tests/catch_test_common.cpp index 0ccdfd8be..039e03050 100644 --- a/tests/catch_test_common.cpp +++ b/tests/catch_test_common.cpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 2007 - 2023 by mod_tile contributors (see AUTHORS file) + * Copyright (c) 2007 - 2026 by mod_tile contributors (see AUTHORS file) * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License @@ -15,6 +15,10 @@ * along with this program; If not, see http://www.gnu.org/licenses/. */ +#include +#include +#include +#include #include #include #include @@ -22,21 +26,20 @@ #include #include #include -#include -#include +#include #include -#include -#include #include #include #include +#include #include #include +#include +#include #include "catch_test_common.hpp" -#include "pstreams/pstream.hpp" - #include "protocol.h" +#include "pstreams/pstream.hpp" extern int foreground; @@ -49,7 +52,6 @@ std::string read_stderr(int buffer_size) { std::string buffer(buffer_size, '\0'); ssize_t len = read(captured_stderr.pipes[0], &buffer[0], buffer_size); - // Stop at the terminating zero written by get_captured_stderr() buffer.resize(len > 0 ? strnlen(buffer.c_str(), len) : 0); return buffer; } @@ -58,21 +60,15 @@ std::string read_stdout(int buffer_size) { std::string buffer(buffer_size, '\0'); ssize_t len = read(captured_stdout.pipes[0], &buffer[0], buffer_size); - // Stop at the terminating zero written by get_captured_stdout() buffer.resize(len > 0 ? strnlen(buffer.c_str(), len) : 0); return buffer; } void capture_stderr() { - // Flush stderr first if you've previously printed something fflush(stderr); - // Save stderr so it can be restored later - int temp_stderr; - temp_stderr = dup(fileno(stderr)); - - // Redirect stderr to a new pipe + int temp_stderr = dup(fileno(stderr)); int pipes[2]; if (pipe(pipes) != 0) { @@ -88,14 +84,9 @@ void capture_stderr() void capture_stdout() { - // Flush stdout first if you've previously printed something fflush(stdout); - // Save stdout so it can be restored later - int temp_stdout; - temp_stdout = dup(fileno(stdout)); - - // Redirect stdout to a new pipe + int temp_stdout = dup(fileno(stdout)); int pipes[2]; if (pipe(pipes) != 0) { @@ -111,16 +102,13 @@ void capture_stdout() std::string get_captured_stderr(bool print) { - // Terminate captured output with a zero if (write(captured_stderr.pipes[1], "", 1) != 1) { throw std::runtime_error("capture: write() failed"); } - // Restore stderr fflush(stderr); dup2(captured_stderr.temp_fd, fileno(stderr)); - // Save & return the captured stderr std::string log_lines = read_stderr(); if (print) { @@ -132,16 +120,13 @@ std::string get_captured_stderr(bool print) std::string get_captured_stdout(bool print) { - // Terminate captured output with a zero if (write(captured_stdout.pipes[1], "", 1) != 1) { throw std::runtime_error("capture: write() failed"); } - // Restore stdout fflush(stdout); dup2(captured_stdout.temp_fd, fileno(stdout)); - // Save & return the captured stdout std::string log_lines = read_stdout(); if (print) { @@ -227,48 +212,38 @@ int remove_recursive(const char *path) { struct stat st; - // 1. Get path metadata if (lstat(path, &st) != 0) { return -1; } - // 2. If it's not a directory, just delete it (file, symlink, etc.) if (!S_ISDIR(st.st_mode)) { return unlink(path); } - // 3. It's a directory: open it to iterate over contents DIR *d = opendir(path); - if (!d) { return -1; } struct dirent *entry; - int result = 0; while ((entry = readdir(d)) != NULL) { - // Skip the special "." and ".." entries if (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0) { continue; } - // Build the full sub-path char sub_path[PATH_MAX]; snprintf(sub_path, sizeof(sub_path), "%s/%s", path, entry->d_name); - // Recursive call result = remove_recursive(sub_path); - if (result != 0) { - break; // Stop early on error + break; } } closedir(d); - // 4. Once the directory is empty, remove the directory itself if (result == 0) { result = rmdir(path); } @@ -285,21 +260,38 @@ extern "C" { int exit_status = 0; int mock_errno = 0; + bool exit_pthread = false; + bool fail_next_asprintf = false; + bool fail_next_calloc = false; bool fail_next_connect = false; bool fail_next_getaddrinfo = false; bool fail_next_getaddrinfo_empty_res = false; bool fail_next_getloadavg = false; + bool fail_next_getnameinfo = false; + bool fail_next_getprotobyname = false; + bool fail_next_gmtime_r = false; bool fail_next_malloc = false; bool fail_next_mkdir = false; bool fail_next_open = false; + bool fail_next_pthread_cond_init = false; + bool fail_next_pthread_cond_wait = false; + bool fail_next_pthread_create = false; + bool fail_next_pthread_mutex_init = false; + bool fail_next_read = false; bool fail_next_recv = false; + bool fail_next_rename = false; bool fail_next_socket = false; + bool fail_next_strdup = false; bool fail_next_strndup = false; bool fail_next_strtok = false; + bool fail_next_utime = false; bool fail_next_write = false; - bool fail_next_next_asprintf = false; + bool force_real_sleep = false; + + bool pass_next_connect = false; + bool pass_next_sleep = false; int fail_next_next_recv_reponse_size = -1; int fail_next_next_recv_reponse_version = -1; @@ -307,8 +299,27 @@ extern "C" { int fail_next_recv_reponse_size = -1; int fail_next_recv_reponse_version = -1; + int fail_asprintf_on_call_count = 0; + int fail_malloc_on_call_count = 0; + int fail_read_on_call_count = 0; + jmp_buf exit_jump; + void reset_asprintf_test_count(void) + { + fail_asprintf_on_call_count = 0; + } + + void reset_malloc_test_count(void) + { + fail_malloc_on_call_count = 0; + } + + void reset_read_call_count(void) + { + fail_read_on_call_count = 0; + } + void mocked_exit(int status) { exit_status = status; @@ -317,13 +328,12 @@ extern "C" { void mocked_g_logger(int log_level, const char *format, ...) { + (void)log_level; char *log_message; va_list args; va_start(args, format); - int len = vasprintf(&log_message, format, args); - va_end(args); if (len < 0) { @@ -340,13 +350,17 @@ extern "C" { { if (fail_next_asprintf) { fail_next_asprintf = false; + errno = (mock_errno != 0) ? mock_errno : ENOMEM; *strp = nullptr; return -1; } - if (fail_next_next_asprintf) { - fail_next_asprintf = true; - fail_next_next_asprintf = false; + if (fail_asprintf_on_call_count > 0) { + if (--fail_asprintf_on_call_count == 0) { + errno = (mock_errno != 0) ? mock_errno : ENOMEM; + *strp = nullptr; + return -1; + } } va_list args; @@ -356,13 +370,30 @@ extern "C" { return result; } + void *mocked_calloc(size_t num, size_t size) + { + if (fail_next_calloc) { + fail_next_calloc = false; + errno = (mock_errno != 0) ? mock_errno : ENOMEM; + return nullptr; + } + + return calloc(num, size); + } + int mocked_connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen) { if (fail_next_connect) { fail_next_connect = false; + errno = (mock_errno != 0) ? mock_errno : EACCES; return -1; } + if (pass_next_connect) { + pass_next_connect = false; + return 0; + } + return connect(sockfd, addr, addrlen); } @@ -392,13 +423,51 @@ extern "C" { return getloadavg(loadavg, nelem); } + int mocked_getnameinfo(const struct sockaddr *addr, socklen_t addrlen, char *host, socklen_t hostlen, char *serv, socklen_t servlen, int flags) + { + if (fail_next_getnameinfo) { + fail_next_getnameinfo = false; + return (mock_errno != 0) ? mock_errno : EAI_AGAIN; + } + + return getnameinfo(addr, addrlen, host, hostlen, serv, servlen, flags); + } + + struct protoent *mocked_getprotobyname(const char *name) + { + if (fail_next_getprotobyname) { + fail_next_getprotobyname = false; + return nullptr; + } + + return getprotobyname(name); + } + + struct tm *mocked_gmtime_r(const time_t *timer, struct tm *tp) + { + if (fail_next_gmtime_r) { + fail_next_gmtime_r = false; + return NULL; + } + + return gmtime_r(timer, tp); + } + void *mocked_malloc(size_t size) { if (fail_next_malloc) { fail_next_malloc = false; + errno = (mock_errno != 0) ? mock_errno : ENOMEM; return nullptr; } + if (fail_malloc_on_call_count > 0) { + if (--fail_malloc_on_call_count == 0) { + errno = (mock_errno != 0) ? mock_errno : ENOMEM; + return nullptr; + } + } + return malloc(size); } @@ -427,6 +496,64 @@ extern "C" { return result; } + int mocked_pthread_cond_init(pthread_cond_t *cond, const pthread_condattr_t *cond_attr) + { + if (fail_next_pthread_cond_init) { + fail_next_pthread_cond_init = false; + return (mock_errno != 0) ? mock_errno : ENOMEM; + } + + return pthread_cond_init(cond, cond_attr); + } + + int mocked_pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex) + { + if (fail_next_pthread_cond_wait) { + fail_next_pthread_cond_wait = false; + return (mock_errno != 0) ? mock_errno : EINVAL; + } + + return pthread_cond_wait(cond, mutex); + } + + int mocked_pthread_create(pthread_t *newthread, const pthread_attr_t *attr, void *(*start_routine)(void *), void *arg) + { + if (fail_next_pthread_create) { + fail_next_pthread_create = false; + return (mock_errno != 0) ? mock_errno : EAGAIN; + } + + return pthread_create(newthread, attr, start_routine, arg); + } + + int mocked_pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *mutexattr) + { + if (fail_next_pthread_mutex_init) { + fail_next_pthread_mutex_init = false; + return (mock_errno != 0) ? mock_errno : ENOMEM; + } + + return pthread_mutex_init(mutex, mutexattr); + } + + ssize_t mocked_read(int fd, void *buf, size_t nbytes) + { + if (fail_next_read) { + fail_next_read = false; + errno = (mock_errno != 0) ? mock_errno : EIO; + return -1; + } + + if (fail_read_on_call_count > 0) { + if (--fail_read_on_call_count == 0) { + errno = (mock_errno != 0) ? mock_errno : EIO; + return -1; + } + } + + return read(fd, buf, nbytes); + } + ssize_t mocked_recv(int fd, void *buf, size_t n, int flags) { if (fail_next_recv) { @@ -464,6 +591,36 @@ extern "C" { return recv(fd, buf, n, flags); } + int mocked_rename(const char *__old, const char *__new) + { + if (fail_next_rename) { + fail_next_rename = false; + errno = (mock_errno != 0) ? mock_errno : EACCES; + return -1; + } + + return rename(__old, __new); + } + + unsigned int mocked_sleep(unsigned int seconds) + { + if (force_real_sleep) { + return sleep(seconds); + } + + if (exit_pthread) { + pthread_exit(NULL); + } + + if (pass_next_sleep) { + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + return 0; + } + + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + return 0; + } + int mocked_socket(int domain, int type, int protocol) noexcept { if (fail_next_socket) { @@ -474,10 +631,22 @@ extern "C" { return socket(domain, type, protocol); } + char *mocked_strdup(const char *s) + { + if (fail_next_strdup) { + fail_next_strdup = false; + errno = (mock_errno != 0) ? mock_errno : ENOMEM; + return nullptr; + } + + return strdup(s); + } + char *mocked_strndup(const char *s, size_t n) { if (fail_next_strndup) { fail_next_strndup = false; + errno = (mock_errno != 0) ? mock_errno : ENOMEM; return nullptr; } @@ -494,6 +663,17 @@ extern "C" { return strtok(s, delim); } + int mocked_utime(const char *file, const struct utimbuf *file_times) + { + if (fail_next_utime) { + fail_next_utime = false; + errno = (mock_errno != 0) ? mock_errno : EPERM; + return -1; + } + + return utime(file, file_times); + } + ssize_t mocked_write(int fd, const void *buf, size_t count) { if (fail_next_write) { diff --git a/tests/catch_test_common.hpp b/tests/catch_test_common.hpp index 012a2a09d..809c51fdc 100644 --- a/tests/catch_test_common.hpp +++ b/tests/catch_test_common.hpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 2007 - 2023 by mod_tile contributors (see AUTHORS file) + * Copyright (c) 2007 - 2026 by mod_tile contributors (see AUTHORS file) * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License @@ -16,6 +16,8 @@ */ #include +#include +#include #include #include #include @@ -48,21 +50,88 @@ int run_command(const std::string &file, std::vector argv = {}, con std::string create_tile_dir(const std::string &dir_name = "mod_tile_test", const char *tmp_dir = getenv("TMPDIR")); int delete_tile_dir(const std::string &tile_dir); +extern std::string err_log_lines, out_log_lines; + extern "C" { + extern int exit_status; + extern int mock_errno; + + extern bool exit_pthread; + + extern bool fail_next_asprintf; + extern bool fail_next_calloc; + extern bool fail_next_connect; + extern bool fail_next_getaddrinfo; + extern bool fail_next_getaddrinfo_empty_res; + extern bool fail_next_getloadavg; + extern bool fail_next_getnameinfo; + extern bool fail_next_getprotobyname; + extern bool fail_next_gmtime_r; + extern bool fail_next_malloc; + extern bool fail_next_mkdir; + extern bool fail_next_open; + extern bool fail_next_pthread_cond_init; + extern bool fail_next_pthread_cond_wait; + extern bool fail_next_pthread_create; + extern bool fail_next_pthread_mutex_init; + extern bool fail_next_read; + extern bool fail_next_recv; + extern bool fail_next_rename; + extern bool fail_next_socket; + extern bool fail_next_strdup; + extern bool fail_next_strndup; + extern bool fail_next_strtok; + extern bool fail_next_utime; + extern bool fail_next_write; + + extern bool force_real_sleep; + + extern bool pass_next_connect; + extern bool pass_next_sleep; + + extern int fail_next_next_recv_reponse_size; + extern int fail_next_next_recv_reponse_version; + extern int fail_next_recv_reponse_cmd; + extern int fail_next_recv_reponse_size; + extern int fail_next_recv_reponse_version; + + extern int fail_asprintf_on_call_count; + extern int fail_malloc_on_call_count; + extern int fail_read_on_call_count; + + extern jmp_buf exit_jump; + + void reset_asprintf_test_count(void); + void reset_malloc_test_count(void); + void reset_read_call_count(void); + void mocked_exit(int status); void mocked_g_logger(int log_level, const char *format, ...); int mocked_asprintf(char **strp, const char *fmt, ...); + void *mocked_calloc(size_t num, size_t size); int mocked_connect(int sockfd, const struct sockaddr *addr, socklen_t addrlen); int mocked_getaddrinfo(const char *node, const char *service, const struct addrinfo *hints, struct addrinfo **res); int mocked_getloadavg(double loadavg[], int nelem); + int mocked_getnameinfo(const struct sockaddr *addr, socklen_t addrlen, char *host, socklen_t hostlen, char *serv, socklen_t servlen, int flags); + struct protoent *mocked_getprotobyname(const char *name); + struct tm *mocked_gmtime_r(const time_t *timer, struct tm *tp); void *mocked_malloc(size_t size); int mocked_mkdir(const char *path, mode_t mode); int mocked_open(const char *pathname, int flags, ...); + int mocked_pthread_cond_init(pthread_cond_t *cond, const pthread_condattr_t *cond_attr); + int mocked_pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex); + int mocked_pthread_create(pthread_t *__restrict newthread, const pthread_attr_t *__restrict attr, void *(*start_routine)(void *), void *__restrict arg); + int mocked_pthread_mutex_init(pthread_mutex_t *mutex, const pthread_mutexattr_t *mutexattr); + ssize_t mocked_read(int fd, void *buf, size_t nbytes); ssize_t mocked_recv(int fd, void *buf, size_t n, int flags); + int mocked_rename(const char *__old, const char *__new); + unsigned int mocked_sleep(unsigned int seconds); int mocked_socket(int domain, int type, int protocol) noexcept; + char *mocked_strdup(const char *s); char *mocked_strndup(const char *s, size_t n); char *mocked_strtok(char *s, const char *delim); + int mocked_utime(const char *file, const struct utimbuf *file_times); ssize_t mocked_write(int fd, const void *buf, size_t count); } diff --git a/tests/gen_tile_test.cpp b/tests/gen_tile_test.cpp index d5c612ebb..e38f3be4e 100644 --- a/tests/gen_tile_test.cpp +++ b/tests/gen_tile_test.cpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 2007 - 2023 by mod_tile contributors (see AUTHORS file) + * Copyright (c) 2007 - 2026 by mod_tile contributors (see AUTHORS file) * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License @@ -116,13 +116,15 @@ void *fetch_thread(void *arg) TEST_CASE("renderd/queueing", "request queueing") { - SECTION("renderd/queueing/initialisation", "test the initialisation of the request queue") { + SECTION("renderd/queueing/initialisation", "test the initialisation of the request queue") + { request_queue *queue = request_queue_init(); REQUIRE(queue != NULL); request_queue_close(queue); } - SECTION("renderd/queueing/simple request add", "test the addition of a single request") { + SECTION("renderd/queueing/simple request add", "test the addition of a single request") + { request_queue *queue = request_queue_init(); struct item *item = init_render_request(cmdRender); @@ -132,7 +134,8 @@ TEST_CASE("renderd/queueing", "request queueing") request_queue_close(queue); } - SECTION("renderd/queueing/simple request add priority", "test the addition of requests with different priorities") { + SECTION("renderd/queueing/simple request add priority", "test the addition of requests with different priorities") + { struct item *item; enum protoCmd res; request_queue *queue = request_queue_init(); @@ -188,7 +191,8 @@ TEST_CASE("renderd/queueing", "request queueing") request_queue_close(queue); } - SECTION("renderd/queueing/simple request fetch", "test the fetching of a single request") { + SECTION("renderd/queueing/simple request fetch", "test the fetching of a single request") + { request_queue *queue = request_queue_init(); struct item *item = init_render_request(cmdRender); @@ -229,7 +233,8 @@ TEST_CASE("renderd/queueing", "request queueing") request_queue_close(queue); } - SECTION("renderd/queueing/simple request fetch priority", "test the fetching of requests with different priorities and their ordering") { + SECTION("renderd/queueing/simple request fetch priority", "test the fetching of requests with different priorities and their ordering") + { struct item *item2; request_queue *queue = request_queue_init(); @@ -275,7 +280,8 @@ TEST_CASE("renderd/queueing", "request queueing") request_queue_close(queue); } - SECTION("renderd/queueing/pending requests", "test if de-duplication of requests work") { + SECTION("renderd/queueing/pending requests", "test if de-duplication of requests work") + { enum protoCmd res; struct item *item; request_queue *queue = request_queue_init(); @@ -352,7 +358,8 @@ TEST_CASE("renderd/queueing", "request queueing") request_queue_close(queue); } - SECTION("renderd/queueing/overflow requests", "test if requests correctly overflow from one request priority to the next") { + SECTION("renderd/queueing/overflow requests", "test if requests correctly overflow from one request priority to the next") + { enum protoCmd res; struct item *item; request_queue *queue = request_queue_init(); @@ -385,13 +392,15 @@ TEST_CASE("renderd/queueing", "request queueing") request_queue_close(queue); } - SECTION("renderd/queueing/multithreading request addition", "test if there are any issues with multithreading") { + SECTION("renderd/queueing/multithreading request addition", "test if there are any issues with multithreading") + { pthread_t *addition_threads; request_queue *queue; REQUIRE((NO_THREADS * NO_QUEUE_REQUESTS) < DIRTY_LIMIT); - for (int j = 0; j < NO_TEST_REPEATS; j++) { // As we are looking for race conditions, repeat this test many times + for (int j = 0; j < NO_TEST_REPEATS; j++) { + // As we are looking for race conditions, repeat this test many times addition_threads = (pthread_t *)calloc(NO_THREADS, sizeof(pthread_t)); queue = request_queue_init(); void *status; @@ -416,12 +425,14 @@ TEST_CASE("renderd/queueing", "request queueing") } } - SECTION("renderd/queueing/multithreading request fetch", "test if there are any issues with multithreading") { + SECTION("renderd/queueing/multithreading request fetch", "test if there are any issues with multithreading") + { pthread_t *fetch_threads; struct request_queue *queue; struct item *item; - for (int j = 0; j < NO_TEST_REPEATS; j++) { // As we are looking for race conditions, repeat this test many times + for (int j = 0; j < NO_TEST_REPEATS; j++) { + // As we are looking for race conditions, repeat this test many times fetch_threads = (pthread_t *)calloc(NO_THREADS, sizeof(pthread_t)); queue = request_queue_init(); void *status; @@ -453,12 +464,14 @@ TEST_CASE("renderd/queueing", "request queueing") } } - SECTION("renderd/queueing/multithreading request fetch on empty queue", "test if there are any issues with multithreading") { + SECTION("renderd/queueing/multithreading request fetch on empty queue", "test if there are any issues with multithreading") + { pthread_t *fetch_threads; pthread_t *addition_threads; struct request_queue *queue; - for (int j = 0; j < NO_TEST_REPEATS; j++) { // As we are looking for race conditions, repeat this test many times + for (int j = 0; j < NO_TEST_REPEATS; j++) { + // As we are looking for race conditions, repeat this test many times fetch_threads = (pthread_t *)calloc(NO_THREADS, sizeof(pthread_t)); addition_threads = (pthread_t *)calloc(NO_THREADS, sizeof(pthread_t)); queue = request_queue_init(); @@ -494,7 +507,8 @@ TEST_CASE("renderd/queueing", "request queueing") } } - SECTION("renderd/queueing/clear fd", "Test if the clearing of fd work for queues") { + SECTION("renderd/queueing/clear fd", "Test if the clearing of fd work for queues") + { struct request_queue *queue = request_queue_init(); struct item *item; @@ -552,7 +566,8 @@ TEST_CASE("renderd", "tile generation") int found, ret; std::string err_log_lines, out_log_lines; - SECTION("render_init", "should throw nice error if paths are invalid") { + SECTION("render_init", "should throw nice error if paths are invalid") + { start_capture(); render_init("doesnotexist", "doesnotexist", 1); std::tie(err_log_lines, out_log_lines) = end_capture(); @@ -561,7 +576,8 @@ TEST_CASE("renderd", "tile generation") REQUIRE(found > -1); } - SECTION("rx_request/bad", "should return cmdNotDone") { + SECTION("rx_request/bad", "should return cmdNotDone") + { int pipefd[2]; REQUIRE(pipe(pipefd) == 0); struct protocol *req = (struct protocol *)malloc(sizeof(struct protocol)); @@ -621,7 +637,8 @@ TEST_CASE("renderd", "tile generation") free(req); } - SECTION("send_response", "should complete") { + SECTION("send_response", "should complete") + { auto rsp = GENERATE(cmdRender, cmdRenderPrio, cmdRenderLow, cmdRenderBulk); render_request_queue = request_queue_init(); @@ -645,7 +662,8 @@ TEST_CASE("storage-backend", "Tile storage backend router") int found; std::string err_log_lines, out_log_lines, tile_dir = create_tile_dir(); - SECTION("storage/initialise", "should return NULL") { + SECTION("storage/initialise", "should return NULL") + { start_capture(); // empty string @@ -698,7 +716,8 @@ TEST_CASE("file storage-backend", "File Tile storage backend") std::string tile_dir = create_tile_dir(); std::string xmlconfig(XMLCONFIG_DEFAULT); - SECTION("storage/initialise", "should return tile_dir storage_ctx") { + SECTION("storage/initialise", "should return tile_dir storage_ctx") + { struct storage_backend *store = NULL; store = init_storage_backend(tile_dir.c_str()); @@ -708,7 +727,8 @@ TEST_CASE("file storage-backend", "File Tile storage backend") store->close_storage(store); } - SECTION("storage/stat/non existent", "should return 0 size") { + SECTION("storage/stat/non existent", "should return 0 size") + { struct storage_backend *store = NULL; struct stat_info sinfo; @@ -721,7 +741,8 @@ TEST_CASE("file storage-backend", "File Tile storage backend") store->close_storage(store); } - SECTION("storage/read/non existent", "should return 0 size") { + SECTION("storage/read/non existent", "should return 0 size") + { struct storage_backend *store = NULL; int size; char *buf = (char *)malloc(10000); @@ -739,7 +760,8 @@ TEST_CASE("file storage-backend", "File Tile storage backend") free(err_msg); } - SECTION("storage/write/full metatile", "should complete") { + SECTION("storage/write/full metatile", "should complete") + { struct storage_backend *store = NULL; store = init_storage_backend(tile_dir.c_str()); @@ -763,7 +785,8 @@ TEST_CASE("file storage-backend", "File Tile storage backend") SUCCEED(); } - SECTION("storage/stat/full metatile", "should complete") { + SECTION("storage/stat/full metatile", "should complete") + { struct storage_backend *store = NULL; struct stat_info sinfo; @@ -803,7 +826,8 @@ TEST_CASE("file storage-backend", "File Tile storage backend") SUCCEED(); } - SECTION("storage/read/full metatile", "should complete") { + SECTION("storage/read/full metatile", "should complete") + { struct storage_backend *store = NULL; char *buf; char *buf_tmp; @@ -850,7 +874,8 @@ TEST_CASE("file storage-backend", "File Tile storage backend") SUCCEED(); } - SECTION("storage/read/partial metatile", "should return correct data") { + SECTION("storage/read/partial metatile", "should return correct data") + { struct storage_backend *store = NULL; char *buf; char *buf_tmp; @@ -901,7 +926,8 @@ TEST_CASE("file storage-backend", "File Tile storage backend") free(buf_tmp); } - SECTION("storage/expire metatile", "should expire the tile") { + SECTION("storage/expire metatile", "should expire the tile") + { struct storage_backend *store = NULL; struct stat_info sinfo; @@ -934,7 +960,8 @@ TEST_CASE("file storage-backend", "File Tile storage backend") store->close_storage(store); } - SECTION("storage/delete metatile", "should delete the tile") { + SECTION("storage/delete metatile", "should delete the tile") + { struct storage_backend *store = NULL; struct stat_info sinfo; @@ -963,7 +990,8 @@ TEST_CASE("file storage-backend", "File Tile storage backend") store->close_storage(store); } - SECTION("storage/tile_storage_id", "should return -1") { + SECTION("storage/tile_storage_id", "should return -1") + { struct storage_backend *store = NULL; char *string = (char *)malloc(PATH_MAX - 1); @@ -985,7 +1013,8 @@ TEST_CASE("memcached storage-backend", "MemcacheD Tile storage backend") std::string err_log_lines, out_log_lines; #ifdef HAVE_LIBMEMCACHED - SECTION("memcached storage/initialise", "should not return NULL") { + SECTION("memcached storage/initialise", "should not return NULL") + { start_capture(1); REQUIRE(init_storage_backend("memcached://") != NULL); std::tie(err_log_lines, out_log_lines) = end_capture(); @@ -995,12 +1024,13 @@ TEST_CASE("memcached storage-backend", "MemcacheD Tile storage backend") } #else - SECTION("memcached storage/initialise", "should return NULL") { + SECTION("memcached storage/initialise", "should return NULL") + { start_capture(); REQUIRE(init_storage_backend("memcached://") == NULL); std::tie(err_log_lines, out_log_lines) = end_capture(); - found = err_log_lines.find("init_storage_memcached: Support for memcached has not been compiled into this program"); + found = err_log_lines.find("init_storage_memcached: Support for memcached storage has not been compiled into this program"); REQUIRE(found > -1); } @@ -1011,7 +1041,8 @@ TEST_CASE("null storage-backend", "NULL Tile storage backend") { std::string xmlconfig(XMLCONFIG_DEFAULT); - SECTION("storage/initialise", "should return NULL storage_ctx") { + SECTION("storage/initialise", "should return NULL storage_ctx") + { struct storage_backend *store = NULL; store = init_storage_backend("null://"); @@ -1021,7 +1052,8 @@ TEST_CASE("null storage-backend", "NULL Tile storage backend") store->close_storage(store); } - SECTION("storage/stat", "should return -1") { + SECTION("storage/stat", "should return -1") + { struct storage_backend *store = NULL; struct stat_info sinfo; @@ -1038,7 +1070,8 @@ TEST_CASE("null storage-backend", "NULL Tile storage backend") store->close_storage(store); } - SECTION("storage/read", "should return -1") { + SECTION("storage/read", "should return -1") + { struct storage_backend *store = NULL; int size; char *buf = (char *)malloc(10000); @@ -1056,7 +1089,8 @@ TEST_CASE("null storage-backend", "NULL Tile storage backend") free(err_msg); } - SECTION("storage/write/full metatile", "should complete") { + SECTION("storage/write/full metatile", "should complete") + { struct storage_backend *store = NULL; store = init_storage_backend("null://"); @@ -1077,7 +1111,8 @@ TEST_CASE("null storage-backend", "NULL Tile storage backend") SUCCEED(); } - SECTION("storage/expire metatile", "should return 0") { + SECTION("storage/expire metatile", "should return 0") + { struct storage_backend *store = NULL; struct stat_info sinfo; @@ -1115,7 +1150,8 @@ TEST_CASE("null storage-backend", "NULL Tile storage backend") store->close_storage(store); } - SECTION("storage/delete metatile", "should return 0") { + SECTION("storage/delete metatile", "should return 0") + { struct storage_backend *store = NULL; struct stat_info sinfo; @@ -1153,7 +1189,8 @@ TEST_CASE("null storage-backend", "NULL Tile storage backend") store->close_storage(store); } - SECTION("storage/tile_storage_id", "should return -1") { + SECTION("storage/tile_storage_id", "should return -1") + { struct storage_backend *store = NULL; char *string = (char *)malloc(PATH_MAX - 1); @@ -1169,7 +1206,8 @@ TEST_CASE("null storage-backend", "NULL Tile storage backend") TEST_CASE("rados storage-backend", "RADOS Tile storage backend") { - SECTION("storage/initialise", "should return NULL") { + SECTION("storage/initialise", "should return NULL") + { int found; std::string err_log_lines, out_log_lines; @@ -1180,7 +1218,7 @@ TEST_CASE("rados storage-backend", "RADOS Tile storage backend") #ifdef HAVE_LIBRADOS found = err_log_lines.find("init_storage_rados: failed to read rados config file"); #else - found = err_log_lines.find("init_storage_rados: Support for rados has not been compiled into this program"); + found = err_log_lines.find("init_storage_rados: Support for rados storage has not been compiled into this program"); #endif REQUIRE(found > -1); } @@ -1222,7 +1260,8 @@ TEST_CASE("ro_composite storage-backend", "RO Composite Tile storage backend") TEST_CASE("ro_http_proxy storage-backend", "RO HTTP Proxy Tile storage backend") { #ifdef HAVE_LIBCURL - SECTION("storage/initialise", "should return 1") { + SECTION("storage/initialise", "should return 1") + { struct storage_backend *store = NULL; store = init_storage_backend("ro_http_proxy://"); @@ -1232,7 +1271,8 @@ TEST_CASE("ro_http_proxy storage-backend", "RO HTTP Proxy Tile storage backend") } #else - SECTION("storage/initialise", "should return NULL") { + SECTION("storage/initialise", "should return NULL") + { int found; std::string err_log_lines, out_log_lines; @@ -1240,7 +1280,7 @@ TEST_CASE("ro_http_proxy storage-backend", "RO HTTP Proxy Tile storage backend") REQUIRE(init_storage_backend("ro_http_proxy://") == NULL); std::tie(err_log_lines, out_log_lines) = end_capture(); - found = err_log_lines.find("init_storage_ro_http_proxy: Support for curl and therefore the http proxy storage has not been compiled into this program"); + found = err_log_lines.find("init_storage_ro_http_proxy: Support for http proxy storage has not been compiled into this program"); REQUIRE(found > -1); } @@ -1249,7 +1289,8 @@ TEST_CASE("ro_http_proxy storage-backend", "RO HTTP Proxy Tile storage backend") TEST_CASE("projections", "Test projections") { - SECTION("projections/bounds/spherical", "should return 1") { + SECTION("projections/bounds/spherical", "should return 1") + { const char *projection_srs; int x_multiplier, y_multiplier; double expected_minx, expected_miny, expected_maxx, expected_maxy; @@ -1257,27 +1298,26 @@ TEST_CASE("projections", "Test projections") mapnik::box2d bbox; std::tie(projection_srs, x_multiplier, y_multiplier, expected_minx, expected_miny, expected_maxx, expected_maxy, expected_rounded_minx, expected_rounded_miny, expected_rounded_maxx, expected_rounded_maxy) = - GENERATE(table({ - // 313086.06785625 = 2*20037508.3428 / (2^10 / 8) - std::make_tuple("+proj=merc +a=6378137 +b=6378137", - 1, 1, - -20037508.3428, -20037508.3428, 20037508.3428, 20037508.3428, - 19724422.0, 19724422.0, -19724422.0, -19724422.0), - // 156543.033928125 = 2*20037508.3428 / (2^10 / 8 * 2) - std::make_tuple("+proj=eqc +lat_ts=0 +lat_0=0 +lon_0=0 +x_0=0 +y_0=0 +ellps=WGS84 +datum=WGS84 +units=m +no_defs", - 2, 1, - -20037508.3428, -10018754.1714, 20037508.3428, 10018754.1714, - 19880965.0, 9862211.0, -19880965.0, -9862211.0), - // 5468.75 = 700000 / (2^10 / 8) - std::make_tuple("+proj=tmerc +lat_0=49 +lon_0=-2 +k=0.9996012717 +x_0=400000 +y_0=-100000 +ellps=airy +datum=OSGB36 +units=m +no_defs", - 1, 2, - 0.0, 0.0, 700000.0, 1400000.0, - 694531.0, 1394531.0, 5469.0, 5469.0), - // 313086.06785625 = 2*20037508.3428 / (2^10 / 8) - std::make_tuple("", - 1, 1, - -20037508.3428, -20037508.3428, 20037508.3428, 20037508.3428, - 19724422.0, 19724422.0, -19724422.0, -19724422.0)})); + GENERATE(table({// 313086.06785625 = 2*20037508.3428 / (2^10 / 8) + std::make_tuple("+proj=merc +a=6378137 +b=6378137", + 1, 1, + -20037508.3428, -20037508.3428, 20037508.3428, 20037508.3428, + 19724422.0, 19724422.0, -19724422.0, -19724422.0), + // 156543.033928125 = 2*20037508.3428 / (2^10 / 8 * 2) + std::make_tuple("+proj=eqc +lat_ts=0 +lat_0=0 +lon_0=0 +x_0=0 +y_0=0 +ellps=WGS84 +datum=WGS84 +units=m +no_defs", + 2, 1, + -20037508.3428, -10018754.1714, 20037508.3428, 10018754.1714, + 19880965.0, 9862211.0, -19880965.0, -9862211.0), + // 5468.75 = 700000 / (2^10 / 8) + std::make_tuple("+proj=tmerc +lat_0=49 +lon_0=-2 +k=0.9996012717 +x_0=400000 +y_0=-100000 +ellps=airy +datum=OSGB36 +units=m +no_defs", + 1, 2, + 0.0, 0.0, 700000.0, 1400000.0, + 694531.0, 1394531.0, 5469.0, 5469.0), + // 313086.06785625 = 2*20037508.3428 / (2^10 / 8) + std::make_tuple("", + 1, 1, + -20037508.3428, -20037508.3428, 20037508.3428, 20037508.3428, + 19724422.0, 19724422.0, -19724422.0, -19724422.0)})); struct projectionconfig *prj = get_projection(projection_srs); @@ -1309,22 +1349,23 @@ TEST_CASE("g_logger", "Test g_logger.c") std::string err_log_lines, out_log_lines, expected_output; std::tie(log_level, expected_output) = - GENERATE(table({ - std::make_tuple(G_LOG_LEVEL_ERROR, "ERROR"), - std::make_tuple(G_LOG_LEVEL_CRITICAL, "CRITICAL"), - std::make_tuple(G_LOG_LEVEL_WARNING, "WARNING"), - std::make_tuple(G_LOG_LEVEL_MESSAGE, "MESSAGE"), - std::make_tuple(G_LOG_LEVEL_INFO, "INFO"), - std::make_tuple(G_LOG_LEVEL_DEBUG, "DEBUG"), - std::make_tuple(0, "UNKNOWN")})); - - SECTION("g_logger_level_name: " + expected_output, "should return the expected string") { + GENERATE(table({std::make_tuple(G_LOG_LEVEL_ERROR, "ERROR"), + std::make_tuple(G_LOG_LEVEL_CRITICAL, "CRITICAL"), + std::make_tuple(G_LOG_LEVEL_WARNING, "WARNING"), + std::make_tuple(G_LOG_LEVEL_MESSAGE, "MESSAGE"), + std::make_tuple(G_LOG_LEVEL_INFO, "INFO"), + std::make_tuple(G_LOG_LEVEL_DEBUG, "DEBUG"), + std::make_tuple(0, "UNKNOWN")})); + + SECTION("g_logger_level_name: " + expected_output, "should return the expected string") + { std::string result; result = g_logger_level_name(log_level); REQUIRE(result == expected_output); } - SECTION("g_logger background: " + expected_output, "should log the expected string") { + SECTION("g_logger background: " + expected_output, "should log the expected string") + { start_capture(); g_logger(log_level, "BACKGROUND TEST"); std::tie(err_log_lines, out_log_lines) = end_capture(); @@ -1335,25 +1376,26 @@ TEST_CASE("g_logger", "Test g_logger.c") found_out = out_log_lines.find(expected_output); switch (log_level) { - case 0: - // BACKGROUND UNKNOWN messages do not log - REQUIRE(found_err == -1); - REQUIRE(found_out == -1); - break; - - case G_LOG_LEVEL_DEBUG: - // BACKGROUND DEBUG messages do not log - REQUIRE(found_err == -1); - REQUIRE(found_out == -1); - break; - - default: - REQUIRE(found_err > -1); - REQUIRE(found_out == -1); + case 0: + // BACKGROUND UNKNOWN messages do not log + REQUIRE(found_err == -1); + REQUIRE(found_out == -1); + break; + + case G_LOG_LEVEL_DEBUG: + // BACKGROUND DEBUG messages do not log + REQUIRE(found_err == -1); + REQUIRE(found_out == -1); + break; + + default: + REQUIRE(found_err > -1); + REQUIRE(found_out == -1); } } - SECTION("g_logger foreground: " + expected_output, "should log the expected string") { + SECTION("g_logger foreground: " + expected_output, "should log the expected string") + { std::string message = expected_output + " FOREGROUND TEST"; start_capture(); foreground = 1; @@ -1365,30 +1407,31 @@ TEST_CASE("g_logger", "Test g_logger.c") found_out = out_log_lines.find(message); switch (log_level) { - case 0: - REQUIRE(found_err == -1); - REQUIRE(found_out == -1); - break; - - case G_LOG_LEVEL_INFO: - // FOREGROUND INFO messages log to stdout - REQUIRE(found_err == -1); - REQUIRE(found_out > -1); - break; - - case G_LOG_LEVEL_DEBUG: - // FOREGROUND DEBUG messages log to stdout and only when G_MESSAGES_DEBUG={all,debug} - REQUIRE(found_err == -1); - REQUIRE(found_out == -1); - break; - - default: - REQUIRE(found_err > -1); - REQUIRE(found_out == -1); + case 0: + REQUIRE(found_err == -1); + REQUIRE(found_out == -1); + break; + + case G_LOG_LEVEL_INFO: + // FOREGROUND INFO messages log to stdout + REQUIRE(found_err == -1); + REQUIRE(found_out > -1); + break; + + case G_LOG_LEVEL_DEBUG: + // FOREGROUND DEBUG messages log to stdout and only when G_MESSAGES_DEBUG={all,debug} + REQUIRE(found_err == -1); + REQUIRE(found_out == -1); + break; + + default: + REQUIRE(found_err > -1); + REQUIRE(found_out == -1); } } - SECTION("g_logger foreground debug: " + expected_output, "should log the expected string") { + SECTION("g_logger foreground debug: " + expected_output, "should log the expected string") + { std::string message = expected_output + " FOREGROUND DEBUG TEST"; start_capture(1); g_logger(log_level, "%s", message.c_str()); @@ -1398,33 +1441,34 @@ TEST_CASE("g_logger", "Test g_logger.c") found_out = out_log_lines.find(message); switch (log_level) { - case 0: - REQUIRE(found_err == -1); - REQUIRE(found_out == -1); - break; - - case G_LOG_LEVEL_INFO: - // FOREGROUND INFO messages log to stdout - REQUIRE(found_err == -1); - REQUIRE(found_out > -1); - break; - - case G_LOG_LEVEL_DEBUG: - // FOREGROUND DEBUG messages log to stdout and only when G_MESSAGES_DEBUG={all,debug} - REQUIRE(found_err == -1); - REQUIRE(found_out > -1); - break; - - default: - REQUIRE(found_err > -1); - REQUIRE(found_out == -1); + case 0: + REQUIRE(found_err == -1); + REQUIRE(found_out == -1); + break; + + case G_LOG_LEVEL_INFO: + // FOREGROUND INFO messages log to stdout + REQUIRE(found_err == -1); + REQUIRE(found_out > -1); + break; + + case G_LOG_LEVEL_DEBUG: + // FOREGROUND DEBUG messages log to stdout and only when G_MESSAGES_DEBUG={all,debug} + REQUIRE(found_err == -1); + REQUIRE(found_out > -1); + break; + + default: + REQUIRE(found_err > -1); + REQUIRE(found_out == -1); } } } TEST_CASE("metatile", "Test metatile.cpp") { - SECTION("metatile/get & set", "should return the same string") { + SECTION("metatile/get & set", "should return the same string") + { metaTile tiles("", "", 1024, 1024, 10); for (int yy = 0; yy < METATILE; yy++) { diff --git a/tests/render_expired_test.cpp b/tests/render_expired_test.cpp index 288dd974e..c85892fad 100644 --- a/tests/render_expired_test.cpp +++ b/tests/render_expired_test.cpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 2007 - 2023 by mod_tile contributors (see AUTHORS file) + * Copyright (c) 2007 - 2026 by mod_tile contributors (see AUTHORS file) * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License @@ -40,28 +40,32 @@ extern std::string err_log_lines, out_log_lines; TEST_CASE("render_expired common", "common testing") { - SECTION("invalid long option", "should return 1") { + SECTION("invalid long option", "should return 1") + { std::vector argv = {"--doesnotexist"}; int status = run_command(test_binary, argv); REQUIRE(WEXITSTATUS(status) == 1); } - SECTION("invalid short options", "should return 1") { + SECTION("invalid short options", "should return 1") + { std::vector argv = {"-oesnotexist"}; int status = run_command(test_binary, argv); REQUIRE(WEXITSTATUS(status) == 1); } - SECTION("--help", "should return 0") { + SECTION("--help", "should return 0") + { std::vector argv = {"--help"}; int status = run_command(test_binary, argv); REQUIRE(WEXITSTATUS(status) == 0); } - SECTION("--version", "should show version number") { + SECTION("--version", "should show version number") + { std::vector argv = {"--version"}; int status = run_command(test_binary, argv); @@ -69,7 +73,8 @@ TEST_CASE("render_expired common", "common testing") REQUIRE(out_log_lines == VERSION); } - SECTION("--config invalid file", "should return 1") { + SECTION("--config invalid file", "should return 1") + { std::string renderd_conf = "/path/is/invalid"; std::vector argv = {"--config", renderd_conf}; @@ -83,7 +88,8 @@ TEST_CASE("render_expired specific", "specific testing") { std::string renderd_conf = (std::string)RENDERD_CONF; - SECTION("--config with invalid --map", "should return 1") { + SECTION("--config with invalid --map", "should return 1") + { std::string map = "invalid"; std::vector argv = {"--config", renderd_conf, "--map", map}; @@ -92,7 +98,8 @@ TEST_CASE("render_expired specific", "specific testing") REQUIRE_THAT(err_log_lines, Catch::Matchers::Contains("Map section '" + map + "' does not exist in config file '" + renderd_conf + "'.")); } - SECTION("--config with valid --map and --tile-dir with invalid path", "should return 1") { + SECTION("--config with valid --map and --tile-dir with invalid path", "should return 1") + { std::string map = "example-map"; std::string tile_dir = GENERATE("invalid", "/path/is/invalid"); std::vector argv = {"--config", renderd_conf, "--map", map, "--tile-dir", tile_dir}; @@ -102,7 +109,8 @@ TEST_CASE("render_expired specific", "specific testing") REQUIRE_THAT(err_log_lines, Catch::Matchers::Contains("Failed to initialise storage backend " + tile_dir)); } - SECTION("--config with valid --map, --verbose and bad input lines", "should return 0") { + SECTION("--config with valid --map, --verbose and bad input lines", "should return 0") + { std::string input = "z/x/y\nx y z\n"; std::string map = "example-map"; std::string tile_dir = P_tmpdir; @@ -114,7 +122,8 @@ TEST_CASE("render_expired specific", "specific testing") REQUIRE_THAT(err_log_lines, Catch::Matchers::Contains("Read invalid line: x y z")); } - SECTION("--touch-from 0 with --max-zoom 19, --verbose and overlapping input lines", "should return 0") { + SECTION("--touch-from 0 with --max-zoom 19, --verbose and overlapping input lines", "should return 0") + { std::string input = "16/56715/4908\n17/113420/9816\n18/226860/19632\n19/453726/39265\n"; std::string tile_dir = P_tmpdir; std::vector argv = {"--max-zoom", std::to_string(19), "--tile-dir", tile_dir, "--touch-from", std::to_string(0), "--verbose"}; @@ -131,7 +140,8 @@ TEST_CASE("render_expired specific", "specific testing") REQUIRE_THAT(err_log_lines, Catch::Matchers::Contains("Already requested metatile containing '19/453726/39265'")); } - SECTION("--tile-dir with invalid option", "should return 1") { + SECTION("--tile-dir with invalid option", "should return 1") + { std::string tile_dir = "invalid"; std::vector argv = {"--tile-dir", tile_dir}; @@ -140,7 +150,8 @@ TEST_CASE("render_expired specific", "specific testing") REQUIRE_THAT(err_log_lines, Catch::Matchers::Contains("init_storage_backend: No valid storage backend found for options: " + tile_dir)); } - SECTION("--tile-dir with invalid path", "should return 1") { + SECTION("--tile-dir with invalid path", "should return 1") + { std::string tile_dir = "/path/is/invalid"; std::vector argv = {"--tile-dir", tile_dir}; @@ -149,7 +160,8 @@ TEST_CASE("render_expired specific", "specific testing") REQUIRE_THAT(err_log_lines, Catch::Matchers::Contains("init_storage_backend: Failed to stat " + tile_dir + " with error: No such file or directory")); } - SECTION("--num-threads subceeds minimum of 1", "should return 1") { + SECTION("--num-threads subceeds minimum of 1", "should return 1") + { std::vector argv = {"--num-threads", "0"}; int status = run_command(test_binary, argv); @@ -157,7 +169,8 @@ TEST_CASE("render_expired specific", "specific testing") REQUIRE_THAT(err_log_lines, Catch::Matchers::Contains("Invalid number of threads, must be >= 1 (0 was provided)")); } - SECTION("--min-zoom/--max-zoom exceeds maximum of MAX_ZOOM", "should return 1") { + SECTION("--min-zoom/--max-zoom exceeds maximum of MAX_ZOOM", "should return 1") + { std::vector argv = {GENERATE("--max-zoom", "--min-zoom"), std::to_string(MAX_ZOOM + 1)}; int status = run_command(test_binary, argv); @@ -165,7 +178,8 @@ TEST_CASE("render_expired specific", "specific testing") REQUIRE_THAT(err_log_lines, Catch::Matchers::Contains("zoom, must be <= 20 (21 was provided)")); } - SECTION("--min-zoom exceeds --max-zoom", "should return 1") { + SECTION("--min-zoom exceeds --max-zoom", "should return 1") + { std::vector argv = {"--max-zoom", "1", "--min-zoom", "2"}; int status = run_command(test_binary, argv); @@ -178,7 +192,8 @@ TEST_CASE("render_expired min/max int generator", "min/max int generator testing { std::string option = GENERATE("--delete-from", "--max-load", "--max-zoom", "--min-zoom", "--num-threads", "--touch-from"); - SECTION(option + " option is positive with --help", "should return 0") { + SECTION(option + " option is positive with --help", "should return 0") + { std::vector argv = {option, "1", "--help"}; int status = run_command(test_binary, argv); diff --git a/tests/render_list_test.cpp b/tests/render_list_test.cpp index a5a63ac32..a743bc5a8 100644 --- a/tests/render_list_test.cpp +++ b/tests/render_list_test.cpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 2007 - 2023 by mod_tile contributors (see AUTHORS file) + * Copyright (c) 2007 - 2026 by mod_tile contributors (see AUTHORS file) * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License @@ -40,28 +40,32 @@ extern std::string err_log_lines, out_log_lines; TEST_CASE("render_list common", "common testing") { - SECTION("invalid long option", "should return 1") { + SECTION("invalid long option", "should return 1") + { std::vector argv = {"--doesnotexist"}; int status = run_command(test_binary, argv); REQUIRE(WEXITSTATUS(status) == 1); } - SECTION("invalid short options", "should return 1") { + SECTION("invalid short options", "should return 1") + { std::vector argv = {"-doesnotexist"}; int status = run_command(test_binary, argv); REQUIRE(WEXITSTATUS(status) == 1); } - SECTION("--help", "should return 0") { + SECTION("--help", "should return 0") + { std::vector argv = {"--help"}; int status = run_command(test_binary, argv); REQUIRE(WEXITSTATUS(status) == 0); } - SECTION("--version", "should show version number") { + SECTION("--version", "should show version number") + { std::vector argv = {"--version"}; int status = run_command(test_binary, argv); @@ -69,7 +73,8 @@ TEST_CASE("render_list common", "common testing") REQUIRE(out_log_lines == VERSION); } - SECTION("--config invalid file", "should return 1") { + SECTION("--config invalid file", "should return 1") + { std::string renderd_conf = "/path/is/invalid"; std::vector argv = {"--config", renderd_conf}; @@ -83,7 +88,8 @@ TEST_CASE("render_list specific", "specific testing") { std::string renderd_conf = (std::string)RENDERD_CONF; - SECTION("--config with invalid --map", "should return 1") { + SECTION("--config with invalid --map", "should return 1") + { std::string map = "invalid"; std::vector argv = {"--config", renderd_conf, "--map", map}; @@ -92,7 +98,8 @@ TEST_CASE("render_list specific", "specific testing") REQUIRE_THAT(err_log_lines, Catch::Matchers::Contains("Map section '" + map + "' does not exist in config file '" + renderd_conf + "'.")); } - SECTION("--config with valid --map and --tile-dir with invalid path", "should return 1") { + SECTION("--config with valid --map and --tile-dir with invalid path", "should return 1") + { std::string map = "example-map"; std::string tile_dir = "/path/is/invalid"; std::vector argv = {"--config", renderd_conf, "--map", map, "--tile-dir", tile_dir}; @@ -103,7 +110,8 @@ TEST_CASE("render_list specific", "specific testing") REQUIRE_THAT(err_log_lines, Catch::Matchers::Contains("Failed to initialise storage backend " + tile_dir)); } - SECTION("--config with valid --map, --verbose and bad input lines", "should return 0") { + SECTION("--config with valid --map, --verbose and bad input lines", "should return 0") + { std::string map = "example-map"; std::string tile_dir = P_tmpdir; std::vector argv = {"--config", renderd_conf, "--map", map, "--tile-dir", tile_dir, "--verbose"}; @@ -115,7 +123,8 @@ TEST_CASE("render_list specific", "specific testing") REQUIRE_THAT(err_log_lines, Catch::Matchers::Contains("bad line 0: x y z")); } - SECTION("--config with valid --map, --verbose and invalid zoom input lines", "should return 0") { + SECTION("--config with valid --map, --verbose and invalid zoom input lines", "should return 0") + { std::string map = "example-map"; std::string tile_dir = P_tmpdir; std::vector argv = {"--config", renderd_conf, "--map", map, "--tile-dir", tile_dir, "--verbose"}; @@ -127,7 +136,8 @@ TEST_CASE("render_list specific", "specific testing") REQUIRE_THAT(err_log_lines, Catch::Matchers::Contains("Ignoring tile, zoom 100 outside valid range (0..20)")); } - SECTION("--tile-dir with invalid option", "should return 1") { + SECTION("--tile-dir with invalid option", "should return 1") + { std::string tile_dir = "invalid"; std::vector argv = {"--tile-dir", tile_dir}; @@ -137,7 +147,8 @@ TEST_CASE("render_list specific", "specific testing") REQUIRE_THAT(err_log_lines, Catch::Matchers::Contains("Failed to initialise storage backend " + tile_dir)); } - SECTION("--tile-dir with invalid path", "should return 1") { + SECTION("--tile-dir with invalid path", "should return 1") + { std::string tile_dir = "/path/is/invalid"; std::vector argv = {"--tile-dir", tile_dir}; @@ -147,7 +158,8 @@ TEST_CASE("render_list specific", "specific testing") REQUIRE_THAT(err_log_lines, Catch::Matchers::Contains("Failed to initialise storage backend " + tile_dir)); } - SECTION("--num-threads subceeds minimum of 1", "should return 1") { + SECTION("--num-threads subceeds minimum of 1", "should return 1") + { std::vector argv = {"--num-threads", "0"}; int status = run_command(test_binary, argv); @@ -155,7 +167,8 @@ TEST_CASE("render_list specific", "specific testing") REQUIRE_THAT(err_log_lines, Catch::Matchers::Contains("Invalid number of threads, must be >= 1 (0 was provided)")); } - SECTION("--min-zoom/--max-zoom exceeds maximum of MAX_ZOOM", "should return 1") { + SECTION("--min-zoom/--max-zoom exceeds maximum of MAX_ZOOM", "should return 1") + { std::vector argv = {GENERATE("--max-zoom", "--min-zoom"), std::to_string(MAX_ZOOM + 1)}; int status = run_command(test_binary, argv); @@ -163,7 +176,8 @@ TEST_CASE("render_list specific", "specific testing") REQUIRE_THAT(err_log_lines, Catch::Matchers::Contains("zoom, must be <= 20 (21 was provided)")); } - SECTION("--min-zoom exceeds --max-zoom", "should return 1") { + SECTION("--min-zoom exceeds --max-zoom", "should return 1") + { std::vector argv = {"--max-zoom", "1", "--min-zoom", "2"}; int status = run_command(test_binary, argv); @@ -171,7 +185,8 @@ TEST_CASE("render_list specific", "specific testing") REQUIRE_THAT(err_log_lines, Catch::Matchers::Contains("Specified min zoom (2) is larger than max zoom (1).")); } - SECTION("--all --min-zoom not equal to --max-zoom with X/Y options", "should return 1") { + SECTION("--all --min-zoom not equal to --max-zoom with X/Y options", "should return 1") + { std::vector argv = {"--all", "--max-x", "1", "--max-zoom", "10", "--max-y", "1", "--min-zoom", "9"}; int status = run_command(test_binary, argv); @@ -179,7 +194,8 @@ TEST_CASE("render_list specific", "specific testing") REQUIRE_THAT(err_log_lines, Catch::Matchers::Contains("min-zoom must be equal to max-zoom when using min-x, max-x, min-y, or max-y options")); } - SECTION("--all --max-x/y options exceed maximum (2^zoom-1)", "should return 1") { + SECTION("--all --max-x/y options exceed maximum (2^zoom-1)", "should return 1") + { std::vector argv = {"--all", "--max-x", "2", "--max-y", "2", "--max-zoom", "1", "--min-zoom", "1"}; int status = run_command(test_binary, argv); @@ -187,7 +203,8 @@ TEST_CASE("render_list specific", "specific testing") REQUIRE_THAT(err_log_lines, Catch::Matchers::Contains("Invalid range, x and y values must be <= 1 (2^zoom-1)")); } - SECTION("--all --min-x/y options exceed maximum (2^zoom-1)", "should return 1") { + SECTION("--all --min-x/y options exceed maximum (2^zoom-1)", "should return 1") + { std::vector argv = {"--all", "--max-zoom", "1", "--min-x", "2", "--min-y", "2", "--min-zoom", "1"}; int status = run_command(test_binary, argv); @@ -195,7 +212,8 @@ TEST_CASE("render_list specific", "specific testing") REQUIRE_THAT(err_log_lines, Catch::Matchers::Contains("Invalid range, x and y values must be <= 1 (2^zoom-1)")); } - SECTION("--all --min-x exceeds --max-x", "should return 1") { + SECTION("--all --min-x exceeds --max-x", "should return 1") + { std::vector argv = {"--all", "--max-x", "1", "--max-zoom", "1", "--min-x", "2", "--min-zoom", "1"}; int status = run_command(test_binary, argv); @@ -203,7 +221,8 @@ TEST_CASE("render_list specific", "specific testing") REQUIRE_THAT(err_log_lines, Catch::Matchers::Contains("Specified min-x (2) is larger than max-x (1).")); } - SECTION("--all --min-y exceeds --max-y", "should return 1") { + SECTION("--all --min-y exceeds --max-y", "should return 1") + { std::vector argv = {"--all", "--max-y", "1", "--max-zoom", "1", "--min-y", "2", "--min-zoom", "1"}; int status = run_command(test_binary, argv); @@ -211,7 +230,8 @@ TEST_CASE("render_list specific", "specific testing") REQUIRE_THAT(err_log_lines, Catch::Matchers::Contains("Specified min-y (2) is larger than max-y (1).")); } - SECTION("--all --max-lat --max-lon --min-lat --min-lon with --min-x/y and/or --max-x/y", "should return 1") { + SECTION("--all --max-lat --max-lon --min-lat --min-lon with --min-x/y and/or --max-x/y", "should return 1") + { std::vector suboptions = GENERATE(std::vector({"--min-x", "0"}), std::vector({"--min-y", "0"}), std::vector({"--max-x", "0"}), std::vector({"--max-y", "0"}), std::vector({"--min-x", "0", "--min-y", "0", "--max-x", "0", "--max-y", "0"})); std::vector argv = {"--all", "--max-lat", "0", "--max-lon", "0", "--min-lat", "0", "--min-lon", "0"}; argv.insert(argv.end(), suboptions.begin(), suboptions.end()); @@ -221,7 +241,8 @@ TEST_CASE("render_list specific", "specific testing") REQUIRE_THAT(err_log_lines, Catch::Matchers::Contains("min-lat, min-lon, max-lat & max-lon cannot be used together with min-x, max-x, min-y, or max-y")); } - SECTION("--all --min-lat exceeds --max-lat", "should return 1") { + SECTION("--all --min-lat exceeds --max-lat", "should return 1") + { std::vector argv = {"--all", "--max-lat", "0", "--max-lon", "0", "--min-lat", "1", "--min-lon", "0"}; int status = run_command(test_binary, argv); @@ -229,7 +250,8 @@ TEST_CASE("render_list specific", "specific testing") REQUIRE_THAT(err_log_lines, Catch::Matchers::Contains("Specified min-lat (1.000000) is larger than max-lat (0.000000).")); } - SECTION("--all --min-lon exceeds --max-lon", "should return 1") { + SECTION("--all --min-lon exceeds --max-lon", "should return 1") + { std::vector argv = {"--all", "--max-lat", "0", "--max-lon", "0", "--min-lat", "0", "--min-lon", "1"}; int status = run_command(test_binary, argv); @@ -260,14 +282,16 @@ TEST_CASE("render_list min/max int generator", "min/max int generator testing") { std::string option = GENERATE("--max-load", "--max-x", "--max-y", "--max-zoom", "--min-x", "--min-y", "--min-zoom", "--num-threads"); - SECTION(option + " option is positive with --help", "should return 0") { + SECTION(option + " option is positive with --help", "should return 0") + { std::vector argv = {option, "1", "--help"}; int status = run_command(test_binary, argv); REQUIRE(WEXITSTATUS(status) == 0); } - SECTION(option + " option is negative", "should return 1") { + SECTION(option + " option is negative", "should return 1") + { std::vector argv = {option, "-1"}; int status = run_command(test_binary, argv); @@ -276,7 +300,8 @@ TEST_CASE("render_list min/max int generator", "min/max int generator testing") REQUIRE_THAT(err_log_lines, Catch::Matchers::Contains("(-1 was provided)")); } - SECTION(option + " option is float", "should return 1") { + SECTION(option + " option is float", "should return 1") + { std::vector argv = {option, "1.23456789"}; int status = run_command(test_binary, argv); @@ -284,7 +309,8 @@ TEST_CASE("render_list min/max int generator", "min/max int generator testing") REQUIRE_THAT(err_log_lines, Catch::Matchers::Contains("must be an integer (1.23456789 was provided)")); } - SECTION(option + " option is not an integer", "should return 1") { + SECTION(option + " option is not an integer", "should return 1") + { std::vector argv = {option, "invalid"}; int status = run_command(test_binary, argv); @@ -299,21 +325,24 @@ TEST_CASE("render_list min/max lat generator", "min/max double generator testing double min = -85.051100; double max = 85.051100; - SECTION(option + " option is positive with --help", "should return 0") { + SECTION(option + " option is positive with --help", "should return 0") + { std::vector argv = {option, std::to_string(max), "--help"}; int status = run_command(test_binary, argv); REQUIRE(WEXITSTATUS(status) == 0); } - SECTION(option + " option is negative with --help", "should return 0") { + SECTION(option + " option is negative with --help", "should return 0") + { std::vector argv = {option, std::to_string(min), "--help"}; int status = run_command(test_binary, argv); REQUIRE(WEXITSTATUS(status) == 0); } - SECTION(option + " option is too large", "should return 1") { + SECTION(option + " option is too large", "should return 1") + { std::vector argv = {option, std::to_string(max + .1)}; int status = run_command(test_binary, argv); @@ -321,7 +350,8 @@ TEST_CASE("render_list min/max lat generator", "min/max double generator testing REQUIRE_THAT(err_log_lines, Catch::Matchers::Contains("must be <= 85.051100 (85.151100 was provided)")); } - SECTION(option + " option is too small", "should return 1") { + SECTION(option + " option is too small", "should return 1") + { std::vector argv = {option, std::to_string(min - .1)}; int status = run_command(test_binary, argv); @@ -329,7 +359,8 @@ TEST_CASE("render_list min/max lat generator", "min/max double generator testing REQUIRE_THAT(err_log_lines, Catch::Matchers::Contains("must be >= -85.051100 (-85.151100 was provided)")); } - SECTION(option + " option is not a double", "should return 1") { + SECTION(option + " option is not a double", "should return 1") + { std::vector argv = {option, "invalid"}; int status = run_command(test_binary, argv); @@ -337,21 +368,24 @@ TEST_CASE("render_list min/max lat generator", "min/max double generator testing REQUIRE_THAT(err_log_lines, Catch::Matchers::Contains("must be a double (invalid was provided)")); } - SECTION(option + " option is positive with --help", "should return 0") { + SECTION(option + " option is positive with --help", "should return 0") + { std::vector argv = {option, std::to_string(max), "--help"}; int status = run_command(test_binary, argv); REQUIRE(WEXITSTATUS(status) == 0); } - SECTION(option + " option is negative with --help", "should return 0") { + SECTION(option + " option is negative with --help", "should return 0") + { std::vector argv = {option, std::to_string(min), "--help"}; int status = run_command(test_binary, argv); REQUIRE(WEXITSTATUS(status) == 0); } - SECTION(option + " option is double with --help", "should return 0") { + SECTION(option + " option is double with --help", "should return 0") + { std::vector argv = {option, "1.23456789", "--help"}; int status = run_command(test_binary, argv); @@ -365,21 +399,24 @@ TEST_CASE("render_list min/max lon generator", "min/max double generator testing double min = -180.000000; double max = 180.000000; - SECTION(option + " option is positive with --help", "should return 0") { + SECTION(option + " option is positive with --help", "should return 0") + { std::vector argv = {option, std::to_string(max), "--help"}; int status = run_command(test_binary, argv); REQUIRE(WEXITSTATUS(status) == 0); } - SECTION(option + " option is negative with --help", "should return 0") { + SECTION(option + " option is negative with --help", "should return 0") + { std::vector argv = {option, std::to_string(min), "--help"}; int status = run_command(test_binary, argv); REQUIRE(WEXITSTATUS(status) == 0); } - SECTION(option + " option is too large", "should return 1") { + SECTION(option + " option is too large", "should return 1") + { std::vector argv = {option, std::to_string(max + .1)}; int status = run_command(test_binary, argv); @@ -387,7 +424,8 @@ TEST_CASE("render_list min/max lon generator", "min/max double generator testing REQUIRE_THAT(err_log_lines, Catch::Matchers::Contains("must be <= 180.000000 (180.100000 was provided)")); } - SECTION(option + " option is too small", "should return 1") { + SECTION(option + " option is too small", "should return 1") + { std::vector argv = {option, std::to_string(min - .1)}; int status = run_command(test_binary, argv); @@ -395,7 +433,8 @@ TEST_CASE("render_list min/max lon generator", "min/max double generator testing REQUIRE_THAT(err_log_lines, Catch::Matchers::Contains("must be >= -180.000000 (-180.100000 was provided)")); } - SECTION(option + " option is not a double", "should return 1") { + SECTION(option + " option is not a double", "should return 1") + { std::vector argv = {option, "invalid"}; int status = run_command(test_binary, argv); @@ -403,21 +442,24 @@ TEST_CASE("render_list min/max lon generator", "min/max double generator testing REQUIRE_THAT(err_log_lines, Catch::Matchers::Contains("must be a double (invalid was provided)")); } - SECTION(option + " option is positive with --help", "should return 0") { + SECTION(option + " option is positive with --help", "should return 0") + { std::vector argv = {option, std::to_string(max), "--help"}; int status = run_command(test_binary, argv); REQUIRE(WEXITSTATUS(status) == 0); } - SECTION(option + " option is negative with --help", "should return 0") { + SECTION(option + " option is negative with --help", "should return 0") + { std::vector argv = {option, std::to_string(min), "--help"}; int status = run_command(test_binary, argv); REQUIRE(WEXITSTATUS(status) == 0); } - SECTION(option + " option is double with --help", "should return 0") { + SECTION(option + " option is double with --help", "should return 0") + { std::vector argv = {option, "1.23456789", "--help"}; int status = run_command(test_binary, argv); diff --git a/tests/render_old_test.cpp b/tests/render_old_test.cpp index f9074014e..4d0d0821a 100644 --- a/tests/render_old_test.cpp +++ b/tests/render_old_test.cpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 2007 - 2023 by mod_tile contributors (see AUTHORS file) + * Copyright (c) 2007 - 2026 by mod_tile contributors (see AUTHORS file) * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License @@ -39,28 +39,32 @@ extern std::string err_log_lines, out_log_lines; TEST_CASE("render_old common", "common testing") { - SECTION("invalid long option", "should return 1") { + SECTION("invalid long option", "should return 1") + { std::vector argv = {"--doesnotexist"}; int status = run_command(test_binary, argv); REQUIRE(WEXITSTATUS(status) == 1); } - SECTION("invalid short options", "should return 1") { + SECTION("invalid short options", "should return 1") + { std::vector argv = {"-doesnotexist"}; int status = run_command(test_binary, argv); REQUIRE(WEXITSTATUS(status) == 1); } - SECTION("--help", "should return 0") { + SECTION("--help", "should return 0") + { std::vector argv = {"--help"}; int status = run_command(test_binary, argv); REQUIRE(WEXITSTATUS(status) == 0); } - SECTION("--version", "should show version number") { + SECTION("--version", "should show version number") + { std::vector argv = {"--version"}; int status = run_command(test_binary, argv); @@ -68,7 +72,8 @@ TEST_CASE("render_old common", "common testing") REQUIRE(out_log_lines == VERSION); } - SECTION("--config invalid file", "should return 1") { + SECTION("--config invalid file", "should return 1") + { std::string renderd_conf = "/path/is/invalid"; std::vector argv = {"--config", renderd_conf}; @@ -82,7 +87,8 @@ TEST_CASE("render_old specific", "specific testing") { std::string renderd_conf = (std::string)RENDERD_CONF; - SECTION("--config with invalid --map", "should return 1") { + SECTION("--config with invalid --map", "should return 1") + { std::string map = "invalid"; std::vector argv = {"--config", renderd_conf, "--map", map}; @@ -91,7 +97,8 @@ TEST_CASE("render_old specific", "specific testing") REQUIRE_THAT(err_log_lines, Catch::Matchers::Contains("Map section '" + map + "' does not exist in config file '" + renderd_conf + "'.")); } - SECTION("--num-threads subceeds minimum of 1", "should return 1") { + SECTION("--num-threads subceeds minimum of 1", "should return 1") + { std::vector argv = {"--num-threads", "0"}; int status = run_command(test_binary, argv); @@ -99,7 +106,8 @@ TEST_CASE("render_old specific", "specific testing") REQUIRE_THAT(err_log_lines, Catch::Matchers::Contains("Invalid number of threads, must be >= 1 (0 was provided)")); } - SECTION("--min-zoom/--max-zoom exceeds maximum of MAX_ZOOM", "should return 1") { + SECTION("--min-zoom/--max-zoom exceeds maximum of MAX_ZOOM", "should return 1") + { std::vector argv = {GENERATE("--max-zoom", "--min-zoom"), std::to_string(MAX_ZOOM + 1)}; int status = run_command(test_binary, argv); @@ -107,7 +115,8 @@ TEST_CASE("render_old specific", "specific testing") REQUIRE_THAT(err_log_lines, Catch::Matchers::Contains("zoom, must be <= 20 (21 was provided)")); } - SECTION("--min-zoom exceeds --max-zoom", "should return 1") { + SECTION("--min-zoom exceeds --max-zoom", "should return 1") + { std::vector argv = {"--max-zoom", "1", "--min-zoom", "2"}; int status = run_command(test_binary, argv); @@ -115,7 +124,8 @@ TEST_CASE("render_old specific", "specific testing") REQUIRE_THAT(err_log_lines, Catch::Matchers::Contains("Specified min zoom (2) is larger than max zoom (1).")); } - SECTION("--timestamp is not an integer", "should return 1") { + SECTION("--timestamp is not an integer", "should return 1") + { std::string timestamp = "invalid"; std::vector argv = {"--timestamp", timestamp}; @@ -123,7 +133,8 @@ TEST_CASE("render_old specific", "specific testing") REQUIRE(WEXITSTATUS(status) == 1); } - SECTION("--timestamp is a date with --help", "should return 0") { + SECTION("--timestamp is a date with --help", "should return 0") + { std::string timestamp = GENERATE("01/01/69", "01/01/101"); std::vector argv = {"--timestamp", timestamp, "--help"}; @@ -131,7 +142,8 @@ TEST_CASE("render_old specific", "specific testing") REQUIRE(WEXITSTATUS(status) == 0); } - SECTION("--timestamp is an integer with --help", "should return 0") { + SECTION("--timestamp is an integer with --help", "should return 0") + { std::string timestamp = "111"; std::vector argv = {"--timestamp", timestamp, "--help"}; @@ -144,7 +156,8 @@ TEST_CASE("render_old min/max int generator", "min/max int generator testing") { std::string option = GENERATE("--max-load", "--max-zoom", "--min-zoom", "--num-threads"); - SECTION(option + " option is positive with --help", "should return 0") { + SECTION(option + " option is positive with --help", "should return 0") + { std::vector argv = {option, "1", "--help"}; int status = run_command(test_binary, argv); diff --git a/tests/render_speedtest_test.cpp b/tests/render_speedtest_test.cpp index 2414dbe3c..2eff7dba5 100644 --- a/tests/render_speedtest_test.cpp +++ b/tests/render_speedtest_test.cpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 2007 - 2023 by mod_tile contributors (see AUTHORS file) + * Copyright (c) 2007 - 2026 by mod_tile contributors (see AUTHORS file) * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License @@ -39,28 +39,32 @@ extern std::string err_log_lines, out_log_lines; TEST_CASE("render_speedtest common", "common testing") { - SECTION("invalid long option", "should return 1") { + SECTION("invalid long option", "should return 1") + { std::vector argv = {"--doesnotexist"}; int status = run_command(test_binary, argv); REQUIRE(WEXITSTATUS(status) == 1); } - SECTION("invalid short options", "should return 1") { + SECTION("invalid short options", "should return 1") + { std::vector argv = {"-doesnotexist"}; int status = run_command(test_binary, argv); REQUIRE(WEXITSTATUS(status) == 1); } - SECTION("--help", "should return 0") { + SECTION("--help", "should return 0") + { std::vector argv = {"--help"}; int status = run_command(test_binary, argv); REQUIRE(WEXITSTATUS(status) == 0); } - SECTION("--version", "should show version number") { + SECTION("--version", "should show version number") + { std::vector argv = {"--version"}; int status = run_command(test_binary, argv); @@ -68,7 +72,8 @@ TEST_CASE("render_speedtest common", "common testing") REQUIRE(out_log_lines == VERSION); } - SECTION("--config invalid file", "should return 1") { + SECTION("--config invalid file", "should return 1") + { std::string renderd_conf = "/path/is/invalid"; std::vector argv = {"--config", renderd_conf}; @@ -82,7 +87,8 @@ TEST_CASE("render_speedtest specific", "specific testing") { std::string renderd_conf = (std::string)RENDERD_CONF; - SECTION("--config with invalid --map", "should return 1") { + SECTION("--config with invalid --map", "should return 1") + { std::string map = "invalid"; std::vector argv = {"--config", renderd_conf, "--map", map}; @@ -91,7 +97,8 @@ TEST_CASE("render_speedtest specific", "specific testing") REQUIRE_THAT(err_log_lines, Catch::Matchers::Contains("Map section '" + map + "' does not exist in config file '" + renderd_conf + "'.")); } - SECTION("--num-threads subceeds minimum of 1", "should return 1") { + SECTION("--num-threads subceeds minimum of 1", "should return 1") + { std::vector argv = {"--num-threads", "0"}; int status = run_command(test_binary, argv); @@ -99,7 +106,8 @@ TEST_CASE("render_speedtest specific", "specific testing") REQUIRE_THAT(err_log_lines, Catch::Matchers::Contains("Invalid number of threads, must be >= 1 (0 was provided)")); } - SECTION("--min-zoom/--max-zoom exceeds maximum of MAX_ZOOM", "should return 1") { + SECTION("--min-zoom/--max-zoom exceeds maximum of MAX_ZOOM", "should return 1") + { std::vector argv = {GENERATE("--max-zoom", "--min-zoom"), std::to_string(MAX_ZOOM + 1)}; int status = run_command(test_binary, argv); @@ -107,7 +115,8 @@ TEST_CASE("render_speedtest specific", "specific testing") REQUIRE_THAT(err_log_lines, Catch::Matchers::Contains("zoom, must be <= 20 (21 was provided)")); } - SECTION("--min-zoom exceeds --max-zoom", "should return 1") { + SECTION("--min-zoom exceeds --max-zoom", "should return 1") + { std::vector argv = {"--max-zoom", "1", "--min-zoom", "2"}; int status = run_command(test_binary, argv); @@ -120,7 +129,8 @@ TEST_CASE("render_speedtest min/max int generator", "min/max int generator testi { std::string option = GENERATE("--max-zoom", "--min-zoom", "--num-threads"); - SECTION(option + " option is positive with --help", "should return 0") { + SECTION(option + " option is positive with --help", "should return 0") + { std::vector argv = {option, "1", "--help"}; int status = run_command(test_binary, argv); diff --git a/tests/renderd_test.cpp b/tests/renderd_test.cpp index 54850a40a..ea82330f4 100644 --- a/tests/renderd_test.cpp +++ b/tests/renderd_test.cpp @@ -1,5 +1,5 @@ /* - * Copyright (c) 2007 - 2023 by mod_tile contributors (see AUTHORS file) + * Copyright (c) 2007 - 2026 by mod_tile contributors (see AUTHORS file) * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License @@ -34,28 +34,32 @@ extern std::string err_log_lines, out_log_lines; TEST_CASE("renderd common", "common testing") { - SECTION("invalid long option", "should return 1") { + SECTION("invalid long option", "should return 1") + { std::vector argv = {"--doesnotexist"}; int status = run_command(test_binary, argv); REQUIRE(WEXITSTATUS(status) == 1); } - SECTION("invalid short options", "should return 1") { + SECTION("invalid short options", "should return 1") + { std::vector argv = {"-doesnotexist"}; int status = run_command(test_binary, argv); REQUIRE(WEXITSTATUS(status) == 1); } - SECTION("--help", "should return 0") { + SECTION("--help", "should return 0") + { std::vector argv = {"--help"}; int status = run_command(test_binary, argv); REQUIRE(WEXITSTATUS(status) == 0); } - SECTION("--version", "should show version number") { + SECTION("--version", "should show version number") + { std::vector argv = {"--version"}; int status = run_command(test_binary, argv); @@ -63,7 +67,8 @@ TEST_CASE("renderd common", "common testing") REQUIRE(out_log_lines == VERSION); } - SECTION("--config invalid file", "should return 1") { + SECTION("--config invalid file", "should return 1") + { std::string renderd_conf = "/path/is/invalid"; std::vector argv = {"--foreground", "--config", renderd_conf}; @@ -77,14 +82,16 @@ TEST_CASE("renderd specific", "specific testing") { std::string option = "--slave"; - SECTION(option + " is positive with --help", "should return 0") { + SECTION(option + " is positive with --help", "should return 0") + { std::vector argv = {"--foreground", option, "1", "--help"}; int status = run_command(test_binary, argv); REQUIRE(WEXITSTATUS(status) == 0); } - SECTION(option + " subceeds minimum of 0", "should return 1") { + SECTION(option + " subceeds minimum of 0", "should return 1") + { std::vector argv = {"--foreground", option, "-1"}; int status = run_command(test_binary, argv); diff --git a/tests/unit_test_cache_expire.cpp b/tests/unit_test_cache_expire.cpp index 664e2539e..c67e55647 100644 --- a/tests/unit_test_cache_expire.cpp +++ b/tests/unit_test_cache_expire.cpp @@ -1,14 +1,26 @@ +/* + * Copyright (c) 2007 - 2026 by mod_tile contributors (see AUTHORS file) + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; If not, see http://www.gnu.org/licenses/. + */ + #include "catch/catch.hpp" #include "catch_test_common.hpp" #include "cache_expire.h" -extern bool fail_next_connect; -extern bool fail_next_getaddrinfo; -extern bool fail_next_getaddrinfo_empty_res; -extern bool fail_next_malloc; -extern bool fail_next_socket; -extern std::string err_log_lines; +static void cache_expire_url(int sock, char *url); TEST_CASE("cache_expire.c", "[cache_expire]") { @@ -18,19 +30,22 @@ TEST_CASE("cache_expire.c", "[cache_expire]") std::string host("host"); std::string uri("/uri/"); - SECTION("cache_expire_url function") { + SECTION("cache_expire_url function") + { std::string url("http://" + host + uri + std::to_string(z) + "/" + std::to_string(x) + "/" + std::to_string(y) + ".png"); err_log_lines.clear(); - SECTION("cache_expire_url", "should return") { + SECTION("cache_expire_url", "should return") + { int sock = 0; cache_expire_url(sock, (char *)url.c_str()); REQUIRE_THAT(err_log_lines, Catch::Matchers::Contains("Failed to send HTCP purge for " + url)); } - SECTION("cache_expire_url handles malloc failure", "should return") { + SECTION("cache_expire_url handles malloc failure", "should return") + { int sock = 0; fail_next_malloc = true; @@ -39,7 +54,8 @@ TEST_CASE("cache_expire.c", "[cache_expire]") REQUIRE(err_log_lines == ""); } - SECTION("cache_expire_url handles negative sock", "should return") { + SECTION("cache_expire_url handles negative sock", "should return") + { int sock = -1; cache_expire_url(sock, (char *)url.c_str()); @@ -47,24 +63,28 @@ TEST_CASE("cache_expire.c", "[cache_expire]") } } - SECTION("cache_expire function") { + SECTION("cache_expire function") + { err_log_lines.clear(); - SECTION("cache_expire", "should return") { + SECTION("cache_expire", "should return") + { int sock = 0; cache_expire(sock, (char *)host.c_str(), (char *)uri.c_str(), x, y, z); REQUIRE_THAT(err_log_lines, Catch::Matchers::Contains("Failed to send HTCP purge for http://" + host + uri + std::to_string(z) + "/" + std::to_string(x) + "/" + std::to_string(y) + ".png")); } - SECTION("cache_expire handles malloc failure", "should return") { + SECTION("cache_expire handles malloc failure", "should return") + { int sock = 0; cache_expire(sock, (char *)host.c_str(), (char *)uri.c_str(), x, y, z); REQUIRE_THAT(err_log_lines, Catch::Matchers::Contains("Failed to send HTCP purge for http://" + host + uri + std::to_string(z) + "/" + std::to_string(x) + "/" + std::to_string(y) + ".png")); } - SECTION("cache_expire handles negative sock", "should return") { + SECTION("cache_expire handles negative sock", "should return") + { int sock = -1; cache_expire(sock, (char *)host.c_str(), (char *)uri.c_str(), x, y, z); @@ -72,34 +92,32 @@ TEST_CASE("cache_expire.c", "[cache_expire]") } } - SECTION("init_cache_expire function") { + SECTION("init_cache_expire function") + { err_log_lines.clear(); - SECTION("init_cache_expire", "should return") { + SECTION("init_cache_expire", "should return") + { std::string htcphost("localhost"); init_cache_expire((char *)htcphost.c_str()); REQUIRE(err_log_lines == ""); } - SECTION("init_cache_expire with nonexistent host", "should return") { + SECTION("init_cache_expire with nonexistent host", "should return") + { std::string htcphost("nonexistenthost"); init_cache_expire((char *)htcphost.c_str()); REQUIRE_THAT(err_log_lines, - Catch::Matchers::Contains("Failed to lookup HTCP cache host: Address family for hostname not supported") - || - Catch::Matchers::Contains("Failed to lookup HTCP cache host: Name could not be resolved at this time") - || - Catch::Matchers::Contains("Failed to lookup HTCP cache host: Name does not resolve") - || - Catch::Matchers::Contains("Failed to lookup HTCP cache host: Name or service not known") - || - Catch::Matchers::Contains("Failed to lookup HTCP cache host: Temporary failure in name resolution") - || - Catch::Matchers::Contains("Failed to lookup HTCP cache host: nodename nor servname provided, or not known") - ); + Catch::Matchers::Contains("Failed to lookup HTCP cache host: Address family for hostname not supported") || + Catch::Matchers::Contains("Failed to lookup HTCP cache host: Name could not be resolved at this time") || + Catch::Matchers::Contains("Failed to lookup HTCP cache host: Name does not resolve") || + Catch::Matchers::Contains("Failed to lookup HTCP cache host: Name or service not known") || + Catch::Matchers::Contains("Failed to lookup HTCP cache host: Temporary failure in name resolution") || + Catch::Matchers::Contains("Failed to lookup HTCP cache host: nodename nor servname provided, or not known")); } - SECTION("init_cache_expire with failed socket", "should return") { + SECTION("init_cache_expire with failed socket", "should return") + { std::string htcphost("localhost"); fail_next_socket = true; @@ -108,20 +126,20 @@ TEST_CASE("cache_expire.c", "[cache_expire]") REQUIRE(err_log_lines == ""); } - SECTION("init_cache_expire with failed getaddrinfo", "should return") { + SECTION("init_cache_expire with failed getaddrinfo", "should return") + { std::string htcphost("localhost"); fail_next_getaddrinfo = true; init_cache_expire((char *)htcphost.c_str()); REQUIRE_THAT(err_log_lines, - Catch::Matchers::Contains("Failed to lookup HTCP cache host: Bad value for ai_flags") - || - Catch::Matchers::Contains("Failed to lookup HTCP cache host: Unknown error") - ); + Catch::Matchers::Contains("Failed to lookup HTCP cache host: Bad value for ai_flags") || + Catch::Matchers::Contains("Failed to lookup HTCP cache host: Unknown error")); } - SECTION("init_cache_expire with empty getaddrinfo response", "should return") { + SECTION("init_cache_expire with empty getaddrinfo response", "should return") + { std::string htcphost("localhost"); fail_next_getaddrinfo_empty_res = true; @@ -130,7 +148,8 @@ TEST_CASE("cache_expire.c", "[cache_expire]") REQUIRE_THAT(err_log_lines, Catch::Matchers::Contains("Failed to create HTCP cache socket")); } - SECTION("init_cache_expire with failed connect", "should return") { + SECTION("init_cache_expire with failed connect", "should return") + { std::string htcphost("localhost"); fail_next_connect = true; diff --git a/tests/unit_test_metatile.cpp b/tests/unit_test_metatile.cpp index 817ae7c4d..c82eb21a2 100644 --- a/tests/unit_test_metatile.cpp +++ b/tests/unit_test_metatile.cpp @@ -1,15 +1,26 @@ +/* + * Copyright (c) 2007 - 2026 by mod_tile contributors (see AUTHORS file) + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; If not, see http://www.gnu.org/licenses/. + */ + #include "catch/catch.hpp" #include "catch_test_common.hpp" #include "metatile.h" #include "store.h" -extern bool fail_next_malloc; -extern bool fail_next_mkdir; -extern bool fail_next_open; -extern bool fail_next_write; -extern std::string err_log_lines; - TEST_CASE("metatile.cpp", "[metatile]") { int x = 1024; @@ -25,7 +36,8 @@ TEST_CASE("metatile.cpp", "[metatile]") metaTile tiles(xmlconfig.c_str(), "", x, y, z); - SECTION("metaTile::save function") { + SECTION("metaTile::save function") + { tile_dir = create_tile_dir("mod_tile.unit_test_metatile"); store = init_storage_backend(tile_dir.c_str()); @@ -40,7 +52,8 @@ TEST_CASE("metatile.cpp", "[metatile]") } } - SECTION("metaTile::save handles malloc failure", "should return") { + SECTION("metaTile::save handles malloc failure", "should return") + { fail_next_malloc = true; tiles.save(store); @@ -48,7 +61,8 @@ TEST_CASE("metatile.cpp", "[metatile]") REQUIRE_THAT(err_log_lines, Catch::Matchers::Contains("Failed to write metatile. Out of memory")); } - SECTION("metaTile::save handles mkdir failure", "should return") { + SECTION("metaTile::save handles mkdir failure", "should return") + { fail_next_mkdir = true; tiles.save(store); @@ -56,7 +70,8 @@ TEST_CASE("metatile.cpp", "[metatile]") REQUIRE_THAT(err_log_lines, Catch::Matchers::Contains("Error creating directory " + metatile_path)); } - SECTION("metaTile::save handles open failure", "should return") { + SECTION("metaTile::save handles open failure", "should return") + { fail_next_open = true; tiles.save(store); @@ -64,7 +79,8 @@ TEST_CASE("metatile.cpp", "[metatile]") REQUIRE_THAT(err_log_lines, Catch::Matchers::Contains("Error creating file " + metatile_path)); } - SECTION("metaTile::save handles write failure", "should return") { + SECTION("metaTile::save handles write failure", "should return") + { fail_next_write = true; tiles.save(store); @@ -78,11 +94,12 @@ TEST_CASE("metatile.cpp", "[metatile]") tiles.clear(); } - - SECTION("metaTile::set function") { + SECTION("metaTile::set function") + { err_log_lines.clear(); - SECTION("metaTile::set", "then metaTile::get, then metaTile::clear, then metaTile::get") { + SECTION("metaTile::set", "then metaTile::get, then metaTile::clear, then metaTile::get") + { for (int xx = 0; xx < METATILE; xx++) { for (int yy = 0; yy < METATILE; yy++) { std::string tile_set_data(tile_data_string + " " + std::to_string(xx) + " " + std::to_string(yy)); @@ -104,11 +121,12 @@ TEST_CASE("metatile.cpp", "[metatile]") } } - - SECTION("metaTile::expire_tiles function") { + SECTION("metaTile::expire_tiles function") + { err_log_lines.clear(); - SECTION("metaTile::expire_tile", "should return") { + SECTION("metaTile::expire_tile", "should return") + { int sock = 0; tiles.expire_tiles(sock, "host", "/uri/"); @@ -116,7 +134,8 @@ TEST_CASE("metatile.cpp", "[metatile]") REQUIRE_THAT(err_log_lines, Catch::Matchers::Contains("Failed to send HTCP purge for http://" + host + uri + std::to_string(z) + "/" + std::to_string(x) + "/" + std::to_string(y) + ".png")); } - SECTION("metaTile::expire_tiles handles negative sock", "should return") { + SECTION("metaTile::expire_tiles handles negative sock", "should return") + { int sock = -1; tiles.expire_tiles(sock, "host", "/uri/"); @@ -124,11 +143,12 @@ TEST_CASE("metatile.cpp", "[metatile]") } } - - SECTION("metaTile::xyz_to_meta_offset function") { + SECTION("metaTile::xyz_to_meta_offset function") + { err_log_lines.clear(); - SECTION("metaTile::xyz_to_meta_offset", "should return") { + SECTION("metaTile::xyz_to_meta_offset", "should return") + { int i = 0; for (int xx = 0; xx < METATILE; xx++) { diff --git a/tests/unit_test_parameterize_style.cpp b/tests/unit_test_parameterize_style.cpp index 3cfd17fb5..56436c26b 100644 --- a/tests/unit_test_parameterize_style.cpp +++ b/tests/unit_test_parameterize_style.cpp @@ -1,9 +1,25 @@ +/* + * Copyright (c) 2007 - 2026 by mod_tile contributors (see AUTHORS file) + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; If not, see http://www.gnu.org/licenses/. + */ + #include #include #include #include -#include -#include +#include #if MAPNIK_MAJOR_VERSION < 4 #include @@ -15,11 +31,12 @@ #include "config.h" #include "parameterize_style.hpp" -extern std::string err_log_lines; +static void parameterize_map_language(mapnik::Map &m, char *parameter); TEST_CASE("parameterize_style.cpp", "[parameterize_style]") { - SECTION("parameterize_map_language function") { + SECTION("parameterize_map_language function") + { mapnik::datasource_cache::instance().register_datasources(MAPNIK_PLUGINS_DIR); mapnik::Map map(256, 256); mapnik::load_map(map, MAPNIK_XML); @@ -32,15 +49,17 @@ TEST_CASE("parameterize_style.cpp", "[parameterize_style]") err_log_lines.clear(); - SECTION("parameterize_map_language with empty parameter", "should return") { - const char * parameter = ""; + SECTION("parameterize_map_language with empty parameter", "should return") + { + const char *parameter = ""; parameterize_map_language(map, (char *)parameter); REQUIRE_THAT(err_log_lines, Catch::Matchers::Contains("Internationalizing map to language parameter: ")); } - SECTION("parameterize_map_language modifies 'table' parameter", "should return") { - const char * parameter = "en,de,_"; + SECTION("parameterize_map_language modifies 'table' parameter", "should return") + { + const char *parameter = "en,de,_"; layer = map.get_layer(0); REQUIRE(layer.datasource()->params().get("table") == std::string(",name")); @@ -53,24 +72,36 @@ TEST_CASE("parameterize_style.cpp", "[parameterize_style]") } } - SECTION("init_parameterization_function function") { + SECTION("init_parameterization_function function") + { err_log_lines.clear(); - SECTION("init_parameterization_function with empty function_name", "should return NULL") { + SECTION("init_parameterization_function with NULL function_name", "should return NULL") + { + parameterize_function_ptr response = init_parameterization_function(nullptr); + + REQUIRE(response == NULL); + REQUIRE_THAT(err_log_lines, Catch::Matchers::Contains("Parameterize_style not specified (or empty string specified)")); + } + + SECTION("init_parameterization_function with empty function_name", "should return NULL") + { parameterize_function_ptr response = init_parameterization_function(""); REQUIRE(response == NULL); REQUIRE_THAT(err_log_lines, Catch::Matchers::Contains("Parameterize_style not specified (or empty string specified)")); } - SECTION("init_parameterization_function with non-'language' function_name", "should return NULL") { + SECTION("init_parameterization_function with non-'language' function_name", "should return NULL") + { parameterize_function_ptr response = init_parameterization_function("doesnotexist"); REQUIRE(response == NULL); REQUIRE_THAT(err_log_lines, Catch::Matchers::Contains("unknown parameterization function for 'doesnotexist'")); } - SECTION("init_parameterization_function with 'language' function_name", "should return parameterize_map_language") { + SECTION("init_parameterization_function with 'language' function_name", "should return parameterize_map_language") + { parameterize_function_ptr response = init_parameterization_function("language"); REQUIRE(response == parameterize_map_language); diff --git a/tests/unit_test_protocol_helper.cpp b/tests/unit_test_protocol_helper.cpp index f14d6f77e..493def9ce 100644 --- a/tests/unit_test_protocol_helper.cpp +++ b/tests/unit_test_protocol_helper.cpp @@ -1,17 +1,27 @@ +/* + * Copyright (c) 2007 - 2026 by mod_tile contributors (see AUTHORS file) + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; If not, see http://www.gnu.org/licenses/. + */ + #include #include "catch/catch.hpp" #include "catch_test_common.hpp" -#include "protocol.h" #include "protocol_helper.h" -extern int fail_next_next_recv_reponse_size; -extern int fail_next_next_recv_reponse_version; -extern int fail_next_recv_reponse_size; -extern int fail_next_recv_reponse_version; -extern std::string err_log_lines; - TEST_CASE("protocol_helper.c", "[protocol_helper]") { int x = 1024; @@ -31,17 +41,20 @@ TEST_CASE("protocol_helper.c", "[protocol_helper]") cmd->y = y; cmd->z = z; - SECTION("recv_cmd function") { + SECTION("recv_cmd function") + { fd = pipefd[0]; - SECTION("recv_cmd with invalid fd", "should return -1") { + SECTION("recv_cmd with invalid fd", "should return -1") + { ret = recv_cmd(&rsp, fd, block); REQUIRE(ret == -1); REQUIRE_THAT(err_log_lines, Catch::Matchers::Contains("Failed to read cmd on fd " + std::to_string(fd))); } - SECTION("recv_cmd with invalid version", "should return -1") { + SECTION("recv_cmd with invalid version", "should return -1") + { fail_next_recv_reponse_size = sizeof(struct protocol); fail_next_recv_reponse_version = GENERATE(0, 4); std::string expected_message = "Failed to receive render cmd with unknown protocol version " + std::to_string(fail_next_recv_reponse_version); @@ -52,7 +65,8 @@ TEST_CASE("protocol_helper.c", "[protocol_helper]") REQUIRE_THAT(err_log_lines, Catch::Matchers::Contains(expected_message)); } - SECTION("recv_cmd with incomplete response", "should return 0") { + SECTION("recv_cmd with incomplete response", "should return 0") + { fail_next_recv_reponse_size = 1; ret = recv_cmd(&rsp, fd, block); @@ -61,7 +75,8 @@ TEST_CASE("protocol_helper.c", "[protocol_helper]") REQUIRE_THAT(err_log_lines, Catch::Matchers::Contains("Read incomplete cmd on fd " + std::to_string(fd))); } - SECTION("recv_cmd with invalid version and correct size response", "should return -1") { + SECTION("recv_cmd with invalid version and correct size response", "should return -1") + { fail_next_recv_reponse_size = GENERATE(sizeof(struct protocol_v1), sizeof(struct protocol_v2), sizeof(struct protocol)); fail_next_recv_reponse_version = GENERATE(0, 4); @@ -74,7 +89,8 @@ TEST_CASE("protocol_helper.c", "[protocol_helper]") REQUIRE_THAT(err_log_lines, Catch::Matchers::Contains("Failed to receive render cmd with unknown protocol version " + std::to_string(expected_version))); } - SECTION("recv_cmd with correct size response (v1)", "should return") { + SECTION("recv_cmd with correct size response (v1)", "should return") + { fail_next_recv_reponse_size = sizeof(struct protocol_v1); fail_next_recv_reponse_version = 1; @@ -87,7 +103,8 @@ TEST_CASE("protocol_helper.c", "[protocol_helper]") REQUIRE_THAT(err_log_lines, Catch::Matchers::Contains("Got incoming request with protocol version " + std::to_string(expected_version))); } - SECTION("recv_cmd with correct size response (v2)", "should return") { + SECTION("recv_cmd with correct size response (v2)", "should return") + { fail_next_recv_reponse_size = sizeof(struct protocol_v1); fail_next_recv_reponse_version = 2; fail_next_next_recv_reponse_size = sizeof(struct protocol_v2) - fail_next_recv_reponse_size; @@ -102,7 +119,8 @@ TEST_CASE("protocol_helper.c", "[protocol_helper]") REQUIRE_THAT(err_log_lines, Catch::Matchers::Contains("Got incoming request with protocol version " + std::to_string(expected_version))); } - SECTION("recv_cmd with correct size response (v3)", "should return") { + SECTION("recv_cmd with correct size response (v3)", "should return") + { fail_next_recv_reponse_size = sizeof(struct protocol_v1); fail_next_recv_reponse_version = 3; fail_next_next_recv_reponse_size = sizeof(struct protocol) - fail_next_recv_reponse_size; @@ -117,7 +135,8 @@ TEST_CASE("protocol_helper.c", "[protocol_helper]") REQUIRE_THAT(err_log_lines, Catch::Matchers::Contains("Got incoming request with protocol version " + std::to_string(expected_version))); } - SECTION("recv_cmd with incorrect size response (v2-v3)", "should return") { + SECTION("recv_cmd with incorrect size response (v2-v3)", "should return") + { fail_next_recv_reponse_size = sizeof(struct protocol_v1); fail_next_recv_reponse_version = GENERATE(2, 3); fail_next_next_recv_reponse_size = sizeof(struct protocol_v1); @@ -133,7 +152,8 @@ TEST_CASE("protocol_helper.c", "[protocol_helper]") REQUIRE_THAT(err_log_lines, Catch::Matchers::Contains("Got incoming request with protocol version " + std::to_string(expected_version))); } - SECTION("recv_cmd with incomplete second response (v2-v3)", "should return -1") { + SECTION("recv_cmd with incomplete second response (v2-v3)", "should return -1") + { fail_next_recv_reponse_size = sizeof(struct protocol_v1); fail_next_recv_reponse_version = GENERATE(2, 3); fail_next_next_recv_reponse_size = 0; @@ -150,10 +170,23 @@ TEST_CASE("protocol_helper.c", "[protocol_helper]") } } - SECTION("send_cmd function") { + SECTION("send_cmd function") + { fd = pipefd[1]; - SECTION("send_cmd with invalid version", "should return -1") { + SECTION("send_cmd with negative/invalid file descriptor", "should return -1") + { + cmd->ver = 1; + int invalid_fd = -1; + + ret = send_cmd(cmd, invalid_fd); + + REQUIRE(ret == -1); + REQUIRE_THAT(err_log_lines, Catch::Matchers::Contains("Attempted to send cmd on invalid fd " + std::to_string(invalid_fd))); + } + + SECTION("send_cmd with unknown protocol version", "should return -1") + { cmd->ver = GENERATE(0, 4); ret = send_cmd(cmd, fd); @@ -162,15 +195,41 @@ TEST_CASE("protocol_helper.c", "[protocol_helper]") REQUIRE_THAT(err_log_lines, Catch::Matchers::Contains("Failed to send render cmd with unknown protocol version " + std::to_string(cmd->ver))); } - SECTION("send_cmd with invalid fd", "should return -1") { + SECTION("send_cmd with pipe descriptor (non-socket ENOTTY check)", "should fail socket validation") + { cmd->ver = GENERATE(1, 2, 3); ret = send_cmd(cmd, fd); REQUIRE(ret == -1); - REQUIRE_THAT(err_log_lines, Catch::Matchers::Contains("Failed to send render cmd on fd " + std::to_string(fd))); + REQUIRE_THAT(err_log_lines, Catch::Matchers::Contains("fd " + std::to_string(fd) + " is not a valid socket")); + } + + SECTION("send_cmd successfully sends across protocol versions 1, 2, and 3") + { + int sv[2]; + REQUIRE(socketpair(AF_UNIX, SOCK_STREAM, 0, sv) == 0); + int socket_fd = sv[0]; + + int cmd_ver; + size_t expected_size; + + std::tie(cmd_ver, expected_size) = + GENERATE(table({std::make_tuple(1, sizeof(struct protocol_v1)), + std::make_tuple(2, sizeof(struct protocol_v2)), + std::make_tuple(3, sizeof(struct protocol))})); + + cmd->ver = cmd_ver; + ret = send_cmd(cmd, socket_fd); + + REQUIRE(ret == expected_size); + + close(sv[0]); + close(sv[1]); } } + close(pipefd[0]); + close(pipefd[1]); free(cmd); } diff --git a/tests/unit_test_render_submit_queue.cpp b/tests/unit_test_render_submit_queue.cpp index 262751451..d000bbf8f 100644 --- a/tests/unit_test_render_submit_queue.cpp +++ b/tests/unit_test_render_submit_queue.cpp @@ -1,7 +1,24 @@ +/* + * Copyright (c) 2007 - 2026 by mod_tile contributors (see AUTHORS file) + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; If not, see http://www.gnu.org/licenses/. + */ + +#include #include -#include -#include -#include +#include +#include #include "catch/catch.hpp" #include "catch_test_common.hpp" @@ -9,37 +26,122 @@ #include "protocol.h" #include "render_submit_queue.h" -extern bool fail_next_getloadavg; -extern bool fail_next_socket; -extern int exit_status; -extern int fail_next_next_recv_reponse_size; -extern int fail_next_next_recv_reponse_version; -extern int fail_next_recv_reponse_cmd; -extern int fail_next_recv_reponse_size; -extern int fail_next_recv_reponse_version; -extern jmp_buf exit_jump; -extern std::string err_log_lines; +extern int maxLoad; +extern int no_workers; +extern int work_complete; +extern pthread_cond_t qCondNotEmpty; +extern pthread_cond_t qCondNotFull; +extern pthread_mutex_t qLock; +extern pthread_mutex_t qStatsLock; +extern pthread_t *workers; +extern struct qItem *qHead; +extern struct qItem *qTail; +extern struct speed_stats performance_stats; +extern unsigned int qLen; +extern unsigned int qMaxLen; + +static struct protocol *fetch(void); +static int process(struct protocol *cmd, int fd); TEST_CASE("render_submit_queue.c", "[render_submit_queue]") { - SECTION("check_load function") { + maxLoad = 999; + + SECTION("fetch function") + { + struct protocol *fetch_response = NULL; + err_log_lines.clear(); + exit_status = 0; - maxLoad = 999; - auto start = std::chrono::high_resolution_clock::now(); + pthread_mutex_unlock(&qLock); - SECTION("check_load with max load of 999", "should return") { - check_load(); + SECTION("qLen=0, work_complete=1", "should return NULL") + { + qLen = 0; + work_complete = 1; + + fetch_response = fetch(); + + REQUIRE(fetch_response == NULL); } - SECTION("check_load with max load of 999 and unobtainable load average (which returns 1000)", "should return after sleeping 5 seconds") { - fail_next_getloadavg = true; - check_load(); - REQUIRE(std::chrono::duration_cast(std::chrono::high_resolution_clock::now() - start).count() >= 5); + SECTION("qLen=0, work_complete=0", "should block then return NULL cleanly") + { + qLen = 0; + work_complete = 0; + + pthread_t worker; + pthread_create(&worker, NULL, (void *(*)(void *))fetch, NULL); + std::this_thread::sleep_for(std::chrono::milliseconds(50)); + + pthread_mutex_lock(&qLock); + work_complete = 1; + pthread_cond_broadcast(&qCondNotEmpty); + pthread_mutex_unlock(&qLock); + + pthread_join(worker, (void **)&fetch_response); + REQUIRE(fetch_response == NULL); + } + + SECTION("qHead!=NULL, qLen=2, work_complete=0", "should return popped item") + { + char *map1 = strdup("map1"); + char *map2 = strdup("map2"); + qItem *item2 = new qItem{map2, 1024, 1024, 10, NULL}; + qHead = new qItem{map1, 1024, 1024, 10, item2}; + qTail = item2; + qLen = 2; + work_complete = 0; + + fetch_response = fetch(); + + REQUIRE(fetch_response != NULL); + REQUIRE(qLen == 1); + REQUIRE(qHead == item2); + + free(fetch_response); + free(item2->mapname); + delete item2; + } + + SECTION("qHead!=NULL, qLen=1, work_complete=0", "should return popped item") + { + char *mapname = strdup("mapname"); + qHead = new qItem{mapname, 1024, 1024, 10, NULL}; + qTail = qHead; + qLen = 1; + work_complete = 0; + + fetch_response = fetch(); + + REQUIRE(fetch_response != NULL); + REQUIRE(qLen == 0); + REQUIRE(qHead == NULL); + + free(fetch_response); + } + + SECTION("qHead=NULL, qLen=1, work_complete=0", "should exit 1") + { + qHead = NULL; + qLen = 1; + work_complete = 0; + + if (setjmp(exit_jump) == 0) { + fetch_response = fetch(); + FAIL("fetch should have called exit() but didn't"); + } else { + SUCCEED("Captured expected exit() call due to null qHead failure"); + } + + REQUIRE(exit_status == 1); + REQUIRE_THAT(err_log_lines, Catch::Matchers::Contains("Queue failure, null qHead with " + std::to_string(qLen) + " items in list")); } } - SECTION("process function") { + SECTION("process function") + { int fd, ret; int pipefd[2]; REQUIRE(pipe(pipefd) == 0); @@ -57,16 +159,31 @@ TEST_CASE("render_submit_queue.c", "[render_submit_queue]") err_log_lines.clear(); - SECTION("process with incomplete response", "should return 0") { + SECTION("process with incomplete response", "should return 0") + { ret = process(cmd, fd); REQUIRE(ret == 0); REQUIRE_THAT(err_log_lines, Catch::Matchers::Contains("Sending request")); REQUIRE_THAT(err_log_lines, Catch::Matchers::Contains("Waiting for response")); + REQUIRE_THAT(err_log_lines, Catch::Matchers::Contains("Failed to read cmd on fd " + std::to_string(fd))); REQUIRE_THAT(err_log_lines, !Catch::Matchers::Contains("Got response")); } - SECTION("process with cmdNotDone response", "should return positive after sleeping 1 second") { + SECTION("process with incomplete response", "should return 0") + { + fail_next_recv_reponse_size = 1; + ret = process(cmd, fd); + + REQUIRE(ret == 0); + REQUIRE_THAT(err_log_lines, Catch::Matchers::Contains("Sending request")); + REQUIRE_THAT(err_log_lines, Catch::Matchers::Contains("Waiting for response")); + REQUIRE_THAT(err_log_lines, Catch::Matchers::Contains("Read incomplete cmd on fd " + std::to_string(fd))); + REQUIRE_THAT(err_log_lines, !Catch::Matchers::Contains("Got response")); + } + + SECTION("process with cmdNotDone response", "should return positive after sleeping 1 second") + { fail_next_next_recv_reponse_size = 64; fail_next_next_recv_reponse_version = 3; fail_next_recv_reponse_size = sizeof(struct protocol) - 64; @@ -80,10 +197,17 @@ TEST_CASE("render_submit_queue.c", "[render_submit_queue]") REQUIRE_THAT(err_log_lines, Catch::Matchers::Contains("Waiting for response")); REQUIRE_THAT(err_log_lines, Catch::Matchers::Contains("Got response")); REQUIRE_THAT(err_log_lines, Catch::Matchers::Contains("Rendering not done with command")); - REQUIRE(std::chrono::duration_cast(std::chrono::high_resolution_clock::now() - start).count() >= 1); + REQUIRE_THAT(err_log_lines, Catch::Matchers::Contains("sleeping 1s")); + REQUIRE(std::chrono::duration_cast(std::chrono::high_resolution_clock::now() - start).count() >= 100); } - SECTION("process with cmdDone response", "should return positive") { + SECTION("process with cmdDone response", "should return positive") + { + pthread_mutex_lock(&qStatsLock); + performance_stats.stat[10].time_max = -1; + performance_stats.stat[10].noRendered = 0; + pthread_mutex_unlock(&qStatsLock); + fail_next_next_recv_reponse_size = 64; fail_next_next_recv_reponse_version = 3; fail_next_recv_reponse_size = sizeof(struct protocol) - 64; @@ -97,80 +221,139 @@ TEST_CASE("render_submit_queue.c", "[render_submit_queue]") REQUIRE_THAT(err_log_lines, Catch::Matchers::Contains("Waiting for response")); REQUIRE_THAT(err_log_lines, Catch::Matchers::Contains("Got response")); REQUIRE_THAT(err_log_lines, !Catch::Matchers::Contains("Rendering not done with command")); - REQUIRE(std::chrono::duration_cast(std::chrono::high_resolution_clock::now() - start).count() < 1); + REQUIRE_THAT(err_log_lines, !Catch::Matchers::Contains("sleeping 1s")); + REQUIRE(std::chrono::duration_cast(std::chrono::high_resolution_clock::now() - start).count() < 100); + + pthread_mutex_lock(&qStatsLock); + REQUIRE(performance_stats.stat[10].noRendered == 1); + REQUIRE(performance_stats.stat[10].time_max >= 0); + pthread_mutex_unlock(&qStatsLock); } } - SECTION("fetch function") { - struct protocol *fetch_response; + SECTION("thread_main function") + { + void *ret = NULL; + std::string socket_path = std::string(P_tmpdir) + "/renderd.render_submit_queue.thread_main.sock"; err_log_lines.clear(); exit_status = 0; + pthread_mutex_unlock(&qLock); + + exit_pthread = false; + pass_next_connect = false; + fail_next_next_recv_reponse_size = 0; + fail_next_next_recv_reponse_version = 0; + fail_next_recv_reponse_cmd = 0; + fail_next_recv_reponse_size = 0; + fail_next_recv_reponse_version = 0; + + SECTION("thread_main with unix domain socket", "should return") + { + ret = thread_main((void *)socket_path.c_str()); - SECTION("qLen=0, work_complete=1", "should return NULL") { + REQUIRE_THAT(err_log_lines, Catch::Matchers::Contains("connect failed for: " + socket_path)); + } + + SECTION("thread_main with passing connect", "should return") + { + pass_next_connect = true; qLen = 0; work_complete = 1; - fetch_response = fetch(); - - REQUIRE(fetch_response == nullptr); + ret = thread_main((void *)socket_path.c_str()); + REQUIRE(ret == NULL); } - SECTION("qHead=NULL, qLen=1, work_complete=0", "should exit 1") { - qHead = nullptr; + SECTION("thread_main with passing connect and failing process", "should not return after 30 seconds") + { + pass_next_connect = true; + + char *mapname = strdup("mapname"); + qHead = new qItem{mapname, 1024, 1024, 10, NULL}; + qTail = qHead; qLen = 1; work_complete = 0; - if (setjmp(exit_jump) == 0) { - fetch(); - FAIL("fetch should have called exit() but didn't"); - } else { - SUCCEED("Captured expected exit() call due to null qHead failure"); - } + pthread_t worker; + pthread_create(&worker, NULL, thread_main, (void *)socket_path.c_str()); - REQUIRE(exit_status == 1); - REQUIRE_THAT(err_log_lines, Catch::Matchers::Contains("Queue failure, null qHead with " + std::to_string(qLen) + " items in list")); + std::this_thread::sleep_for(std::chrono::milliseconds(50)); + pthread_cond_broadcast(&qCondNotEmpty); + + std::this_thread::sleep_for(std::chrono::milliseconds(50)); + + pthread_mutex_lock(&qLock); + exit_pthread = true; + pthread_cond_broadcast(&qCondNotEmpty); + pthread_mutex_unlock(&qLock); + + pthread_join(worker, &ret); + + REQUIRE(qLen == 0); + REQUIRE(qHead == NULL); + REQUIRE_THAT(err_log_lines, Catch::Matchers::Contains("connection to renderd lost")); + REQUIRE_THAT(err_log_lines, Catch::Matchers::Contains("sleeping for 30 seconds")); + REQUIRE_THAT(err_log_lines, Catch::Matchers::Contains("attempting to reconnect")); } - // SECTION("qLen=1, work_complete=0", "should exit 1") { - // // qHead = nullptr; - // struct qItem *e; - // qHead = e; - // qLen = 1; - // work_complete = 0; + SECTION("thread_main with passing connect and passing process", "should return") + { + pass_next_connect = true; + + char *mapname = strdup("mapname"); + qHead = new qItem{mapname, 1024, 1024, 10, NULL}; + qTail = qHead; + qLen = 1; + work_complete = 0; + + fail_next_next_recv_reponse_size = 64; + fail_next_next_recv_reponse_version = 3; + fail_next_recv_reponse_cmd = cmdDone; + fail_next_recv_reponse_size = sizeof(struct protocol) - 64; + fail_next_recv_reponse_version = 3; + + pthread_t worker; + pthread_create(&worker, NULL, thread_main, (void *)socket_path.c_str()); + std::this_thread::sleep_for(std::chrono::milliseconds(50)); + pthread_cond_broadcast(&qCondNotEmpty); - // fetch_response = fetch(); + pthread_mutex_lock(&qLock); + work_complete = 1; + pthread_cond_broadcast(&qCondNotEmpty); + pthread_mutex_unlock(&qLock); - // // if (setjmp(exit_jump) == 0) { - // // fetch(); - // // FAIL("fetch should have called exit() but didn't"); - // // } else { - // // SUCCEED("Captured expected exit() call due to null qHead failure"); - // // } + pthread_join(worker, &ret); - // REQUIRE(exit_status == 1); - // REQUIRE_THAT(err_log_lines, Catch::Matchers::Contains("Queue failure, null qHead with " + std::to_string(qLen) + " items in list")); - // } + REQUIRE(qLen == 0); + REQUIRE(qHead == NULL); + REQUIRE_THAT(err_log_lines, Catch::Matchers::Contains("Got response")); + } } - SECTION("make_connection function") { + SECTION("make_connection function") + { int ret; err_log_lines.clear(); exit_status = 0; - SECTION("Unix domain socket") { - std::string socket_path = std::string(P_tmpdir) + "/renderd.sock"; + SECTION("Unix domain socket") + { + std::string socket_path = std::string(P_tmpdir) + "/renderd.render_submit_queue.make_connection.sock"; + + SECTION("make_connection with unix domain socket", "should return positive") + { + pass_next_connect = true; - SECTION("make_connection with unix domain socket", "should return positive") { ret = make_connection(socket_path.c_str()); - (void)ret; - // REQUIRE(ret > 0); + REQUIRE(ret > 0); } - SECTION("make_connection handles socket failure by exiting", "should exit 2") { + SECTION("make_connection handles socket failure by exiting", "should exit 2") + { fail_next_socket = true; if (setjmp(exit_jump) == 0) { @@ -185,14 +368,453 @@ TEST_CASE("render_submit_queue.c", "[render_submit_queue]") } } - // SECTION("Network socket") { - // std::string socket_path = "127.0.0.1:1234"; + SECTION("Network socket") + { + std::string ip_host = "127.0.0.1"; + std::string ip_port = "1234"; + std::string socket_path = ip_host + ":" + ip_port; + + SECTION("make_connection with network socket", "should return positive") + { + pass_next_connect = true; + + ret = make_connection(socket_path.c_str()); + + REQUIRE(ret > 0); + REQUIRE_THAT(err_log_lines, Catch::Matchers::Contains("Connecting to " + ip_host + ", port " + ip_port + "/tcp")); + REQUIRE_THAT(err_log_lines, Catch::Matchers::Contains("Trying " + socket_path)); + REQUIRE_THAT(err_log_lines, Catch::Matchers::Contains("Connected to " + socket_path)); + } + + SECTION("Network socket with empty host uses RENDERD_HOST default", "should return positive") + { + pass_next_connect = true; + + ret = make_connection(std::string(":" + ip_port).c_str()); + + REQUIRE(ret > 0); + REQUIRE_THAT(err_log_lines, Catch::Matchers::Contains("Connecting to " + std::string(RENDERD_HOST) + ", port " + ip_port + "/tcp")); + } + + SECTION("Network socket with invalid port defaults to RENDERD_PORT", "should return positive") + { + pass_next_connect = true; + + ret = make_connection(std::string(ip_host + ":0").c_str()); + + REQUIRE(ret > 0); + REQUIRE_THAT(err_log_lines, Catch::Matchers::Contains("port " + std::to_string(RENDERD_PORT) + "/tcp")); + } + + SECTION("Network socket handles getaddrinfo failure", "should exit 2") + { + fail_next_getaddrinfo = true; + + if (setjmp(exit_jump) == 0) { + make_connection(socket_path.c_str()); + FAIL("make_connection should have called exit() but didn't"); + } else { + SUCCEED("Captured expected exit() call due to getaddrinfo failure"); + } + + REQUIRE(exit_status == 2); + REQUIRE_THAT(err_log_lines, Catch::Matchers::Contains("cannot resolve hostname")); + } + + SECTION("Network socket fails to create socket inside loop", "should exit 2") + { + pass_next_connect = true; + fail_next_socket = true; + + if (setjmp(exit_jump) == 0) { + make_connection(socket_path.c_str()); + FAIL("make_connection should have called exit() but didn't"); + } else { + SUCCEED("Captured expected exit() call when no socket could be created"); + } - // SECTION("make_connection with network socket", "should return positive") { - // ret = make_connection(socket_path.c_str()); + REQUIRE(exit_status == 2); + REQUIRE_THAT(err_log_lines, Catch::Matchers::Contains("cannot connect to any address for " + ip_host)); + } - // // REQUIRE(ret > 0); - // } - // } + SECTION("Network socket fails all connection attempts", "should exit 2") + { + fail_next_connect = true; + + if (setjmp(exit_jump) == 0) { + make_connection(socket_path.c_str()); + FAIL("make_connection should have called exit() but didn't"); + } else { + SUCCEED("Captured expected exit() call when all address connections fail"); + } + + REQUIRE(exit_status == 2); + REQUIRE_THAT(err_log_lines, Catch::Matchers::Contains("cannot connect to any address for " + ip_host)); + } + + SECTION("Network socket handles getnameinfo failure", "should exit 2") + { + fail_next_getnameinfo = true; + + if (setjmp(exit_jump) == 0) { + make_connection(socket_path.c_str()); + FAIL("make_connection should have called exit() but didn't"); + } else { + SUCCEED("Captured expected exit() call due to getnameinfo failure"); + } + + REQUIRE(exit_status == 2); + REQUIRE_THAT(err_log_lines, Catch::Matchers::Contains("cannot retrieve name info: Name could not be resolved at this time") || Catch::Matchers::Contains("cannot retrieve name info: Temporary failure in name resolution")); + } + + SECTION("Network socket handles getprotobyname failure", "should exit 2") + { + fail_next_getprotobyname = true; + + if (setjmp(exit_jump) == 0) { + make_connection(socket_path.c_str()); + FAIL("make_connection should have called exit() but didn't"); + } else { + SUCCEED("Captured expected exit() call due to getprotobyname failure"); + } + + REQUIRE(exit_status == 2); + REQUIRE_THAT(err_log_lines, Catch::Matchers::Contains("cannot find TCP protocol number")); + } + } + } + + SECTION("check_load function") + { + err_log_lines.clear(); + + auto start = std::chrono::high_resolution_clock::now(); + + SECTION("check_load with max load of 999", "should return") + { + check_load(); + } + + SECTION("check_load with max load of 999 and unobtainable load average (which returns 1000)", "should return after sleeping 5 seconds") + { + fail_next_getloadavg = true; + check_load(); + REQUIRE(std::chrono::duration_cast(std::chrono::high_resolution_clock::now() - start).count() >= 100); + REQUIRE_THAT(err_log_lines, Catch::Matchers::Contains("Load average 1000.00, sleeping 5s")); + } + } + + SECTION("enqueue function") + { + err_log_lines.clear(); + exit_status = 0; + pthread_mutex_unlock(&qLock); + + qHead = NULL; + qTail = NULL; + qLen = 0; + qMaxLen = 10; + + SECTION("enqueue handles malloc failure by exiting", "should exit 1") + { + fail_next_malloc = true; + + if (setjmp(exit_jump) == 0) { + enqueue(XMLCONFIG_DEFAULT, 0, 0, 0); + FAIL("make_connection should have called exit() but didn't"); + } else { + SUCCEED("Captured expected exit() call due to malloc failure"); + } + + REQUIRE(exit_status == 1); + REQUIRE_THAT(err_log_lines, Catch::Matchers::Contains("Enqueueing tile (xmlname=" + std::string(XMLCONFIG_DEFAULT) + " x=0 y=0 z=0)")); + REQUIRE_THAT(err_log_lines, Catch::Matchers::Contains("Error allocating queue item: Cannot allocate memory")); + } + + SECTION("enqueue handles strdup failure by exiting", "should exit 1") + { + fail_next_strdup = true; + + if (setjmp(exit_jump) == 0) { + enqueue(XMLCONFIG_DEFAULT, 0, 0, 0); + FAIL("make_connection should have called exit() but didn't"); + } else { + SUCCEED("Captured expected exit() call due to malloc failure"); + } + + REQUIRE(exit_status == 1); + REQUIRE_THAT(err_log_lines, Catch::Matchers::Contains("Enqueueing tile (xmlname=" + std::string(XMLCONFIG_DEFAULT) + " x=0 y=0 z=0)")); + REQUIRE_THAT(err_log_lines, Catch::Matchers::Contains("Error allocating queue item mapname: Cannot allocate memory")); + } + + SECTION("enqueue into empty queue (qTail == NULL)", "should set qHead and qTail") + { + enqueue(XMLCONFIG_DEFAULT, 10, 20, 5); + + REQUIRE(qLen == 1); + REQUIRE(qHead != NULL); + REQUIRE(qTail == qHead); + REQUIRE(std::string(qHead->mapname) == XMLCONFIG_DEFAULT); + REQUIRE(qHead->x == 10); + REQUIRE(qHead->y == 20); + REQUIRE(qHead->z == 5); + REQUIRE(qHead->next == NULL); + + free(qHead->mapname); + delete qHead; + qHead = qTail = NULL; + qLen = 0; + } + + SECTION("enqueue into non-empty queue (qTail != NULL)", "should append to tail") + { + char *map1 = strdup("map1"); + qItem *first = new qItem{map1, 1, 1, 1, NULL}; + qHead = first; + qTail = first; + qLen = 1; + + enqueue(XMLCONFIG_DEFAULT, 10, 20, 5); + + REQUIRE(qLen == 2); + REQUIRE(qHead == first); + REQUIRE(qTail != first); + REQUIRE(first->next == qTail); + REQUIRE(std::string(qTail->mapname) == XMLCONFIG_DEFAULT); + + free(first->mapname); + delete first; + free(qTail->mapname); + delete qTail; + qHead = qTail = NULL; + qLen = 0; + } + + SECTION("enqueue when queue is full (qLen == qMaxLen)", "should wait on qCondNotFull then enqueue") + { + qLen = 1; + qMaxLen = 1; + + char *map1 = strdup("map1"); + qItem *first = new qItem{map1, 1, 1, 1, NULL}; + qHead = first; + qTail = first; + + pthread_t worker; + pthread_create(&worker, NULL, [](void *arg) -> void * { + enqueue((const char *)arg, 2, 2, 2); + return NULL; }, (void *)XMLCONFIG_DEFAULT); + + std::this_thread::sleep_for(std::chrono::milliseconds(50)); + + pthread_mutex_lock(&qLock); + qMaxLen = 10; + pthread_cond_signal(&qCondNotFull); + pthread_mutex_unlock(&qLock); + + pthread_join(worker, NULL); + + REQUIRE(qLen == 2); + REQUIRE(qTail != first); + + free(first->mapname); + delete first; + free(qTail->mapname); + delete qTail; + qHead = qTail = NULL; + qLen = 0; + } + + SECTION("enqueue handles pthread_cond_wait error", "should return") + { + pthread_mutex_lock(&qLock); + qLen = 1; + qMaxLen = 1; + + char *map1 = strdup("map1"); + qItem *first = new qItem{map1, 1, 1, 1, NULL}; + qHead = first; + qTail = first; + pthread_mutex_unlock(&qLock); + + fail_next_pthread_cond_wait = true; + + pthread_t worker; + pthread_create(&worker, NULL, [](void *arg) -> void * { + enqueue((const char *)arg, 2, 2, 2); + return NULL; }, (void *)XMLCONFIG_DEFAULT); + + std::this_thread::sleep_for(std::chrono::milliseconds(50)); + + pthread_mutex_lock(&qLock); + qMaxLen = 10; + pthread_cond_signal(&qCondNotFull); + pthread_mutex_unlock(&qLock); + + pthread_join(worker, NULL); + + REQUIRE_THAT(err_log_lines, Catch::Matchers::Contains("pthread_cond_wait(qCondNotFull)")); + + pthread_mutex_lock(&qLock); + free(first->mapname); + delete first; + if (qTail && qTail != first) { + free(qTail->mapname); + delete qTail; + } + qHead = qTail = NULL; + qLen = 0; + pthread_mutex_unlock(&qLock); + } + } + + SECTION("finish_workers function") + { + err_log_lines.clear(); + + SECTION("finish_workers", "should return") + { + std::atomic worker_release{false}; + std::atomic workers_ready{0}; + + auto slow_worker = [](void *arg) -> void * { + auto *ready = static_cast *>(arg); + (*ready)++; + + while (ready->load() < 100) { + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + } + return NULL; + }; + + no_workers = 2; + workers = (pthread_t *)calloc(sizeof(pthread_t), no_workers); + for (int i = 0; i < no_workers; i++) { + pthread_create(&workers[i], NULL, slow_worker, &workers_ready); + } + REQUIRE(workers != NULL); + REQUIRE(no_workers == 2); + + while (workers_ready.load() < 2) { + std::this_thread::sleep_for(std::chrono::milliseconds(1)); + } + + workers_ready.store(100); + finish_workers(); + + REQUIRE(workers == NULL); + REQUIRE(no_workers == 0); + REQUIRE_THAT(err_log_lines, Catch::Matchers::Contains("Waiting for rendering threads to finish")); + } + } + + SECTION("print_statistics function") + { + err_log_lines.clear(); + + pthread_mutex_lock(&qStatsLock); + memset(&performance_stats, 0, sizeof(performance_stats)); + pthread_mutex_unlock(&qStatsLock); + + SECTION("print_statistics with zero rendered tiles (skips zoom loop)", "should return") + { + print_statistics(); + + REQUIRE_THAT(err_log_lines, !Catch::Matchers::Contains("Zoom")); + } + + SECTION("print_statistics with boundary zoom levels (0 and MAX_ZOOM)", "should return") + { + pthread_mutex_lock(&qStatsLock); + performance_stats.stat[0].noRendered = 1; + performance_stats.stat[0].time_min = 200; + performance_stats.stat[0].time_max = 200; + performance_stats.stat[0].time_total = 200; + + performance_stats.stat[MAX_ZOOM].noRendered = 5; + performance_stats.stat[MAX_ZOOM].time_min = 50; + performance_stats.stat[MAX_ZOOM].time_max = 1000; + performance_stats.stat[MAX_ZOOM].time_total = 2500; + pthread_mutex_unlock(&qStatsLock); + + print_statistics(); + + REQUIRE_THAT(err_log_lines, Catch::Matchers::Contains("Zoom 00: min: 0.2 avg: 0.2 max: 0.2 over a total of 0.2s in 1 requests")); + REQUIRE_THAT(err_log_lines, Catch::Matchers::Contains("Zoom " + std::to_string(MAX_ZOOM) + ": min: 0.1 avg: 0.5 max: 1.0 over a total of 2.5s in 5 requests")); + } + } + + SECTION("spawn_workers function") + { + int thread_num = 2; + std::string socket_path = std::string(P_tmpdir) + "/renderd.render_submit_queue.spawn_workers.sock"; + + err_log_lines.clear(); + exit_status = 0; + + SECTION("spawn_workers with non-existent unix domain socket", "should return") + { + spawn_workers(thread_num, socket_path.c_str(), maxLoad); + std::this_thread::sleep_for(std::chrono::milliseconds(200)); + REQUIRE_THAT(err_log_lines, Catch::Matchers::Contains("Starting " + std::to_string(thread_num) + " rendering threads")); + REQUIRE_THAT(err_log_lines, Catch::Matchers::Contains("connect failed for: " + socket_path)); + } + + SECTION("spawn_workers handles calloc failure by exiting", "should exit 1") + { + fail_next_calloc = true; + + if (setjmp(exit_jump) == 0) { + spawn_workers(thread_num, socket_path.c_str(), maxLoad); + FAIL("spawn_workers should have called exit() but didn't"); + } else { + SUCCEED("Captured expected exit() call due to calloc failure"); + } + + REQUIRE(exit_status == 1); + REQUIRE_THAT(err_log_lines, Catch::Matchers::Contains("Starting " + std::to_string(thread_num) + " rendering threads")); + REQUIRE_THAT(err_log_lines, Catch::Matchers::Contains("Error allocating worker memory: Cannot allocate memory")); + } + + SECTION("spawn_workers handles pthread_create failure by exiting", "should exit 1") + { + fail_next_pthread_create = true; + + if (setjmp(exit_jump) == 0) { + spawn_workers(thread_num, socket_path.c_str(), maxLoad); + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + FAIL("spawn_workers should have called exit() but didn't"); + } else { + SUCCEED("Captured expected exit() call due to pthread_create failure"); + } + + REQUIRE(exit_status == 1); + REQUIRE_THAT(err_log_lines, Catch::Matchers::Contains("Starting " + std::to_string(thread_num) + " rendering threads")); + REQUIRE_THAT(err_log_lines, Catch::Matchers::Contains("Thread creation failed: Resource temporarily unavailable")); + } + } + + SECTION("wait_for_empty_queue function") + { + err_log_lines.clear(); + + SECTION("qLen=0", "should return") + { + qLen = 0; + wait_for_empty_queue(); + } + + SECTION("qLen=1", "should return") + { + qLen = 1; + pthread_t worker; + pthread_create(&worker, NULL, (void *(*)(void *))wait_for_empty_queue, NULL); + std::this_thread::sleep_for(std::chrono::milliseconds(50)); + + qLen = 0; + pthread_cond_broadcast(&qCondNotFull); + + pthread_join(worker, NULL); + } } } diff --git a/tests/unit_test_renderd_config.cpp b/tests/unit_test_renderd_config.cpp index 6dca89e85..53a2e0eff 100644 --- a/tests/unit_test_renderd_config.cpp +++ b/tests/unit_test_renderd_config.cpp @@ -1,27 +1,47 @@ -#include +/* + * Copyright (c) 2007 - 2026 by mod_tile contributors (see AUTHORS file) + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; If not, see http://www.gnu.org/licenses/. + */ + #include #include #include +#include #include "catch/catch.hpp" #include "catch_test_common.hpp" #include "renderd_config.h" -extern bool fail_next_asprintf; -extern bool fail_next_next_asprintf; -extern bool fail_next_strndup; -extern int exit_status; -extern jmp_buf exit_jump; -extern std::string err_log_lines; +static char *name_with_section(const char *section, const char *name); +static void copy_string(const char *src, const char **dest, size_t maxlen); +static void process_config_bool(const dictionary *ini, const char *section, const char *name, int *dest, int notfound); +static void process_config_double(const dictionary *ini, const char *section, const char *name, double *dest, double notfound); +static void process_config_int(const dictionary *ini, const char *section, const char *name, int *dest, int notfound); +static void process_config_string(const dictionary *ini, const char *section, const char *name, const char **dest, const char *notfound, size_t maxlen); +static void process_config_string_with_trailing_slash(const dictionary *ini, const char *section, const char *name, const char **dest, const char *notfound, size_t maxlen); TEST_CASE("renderd_config.c", "[renderd_config]") { err_log_lines.clear(); exit_status = 0; - SECTION("copy_string function") { - SECTION("copy_string handles strndup failure by exiting", "should exit 7") { + SECTION("copy_string function") + { + SECTION("copy_string handles strndup failure by exiting", "should exit 7") + { const char *dest; const char *src = "test"; @@ -38,7 +58,8 @@ TEST_CASE("renderd_config.c", "[renderd_config]") REQUIRE_THAT(err_log_lines, Catch::Matchers::Contains("copy_string: strndup error")); } - SECTION("copy_string with valid src and valid dest", "should return") { + SECTION("copy_string with valid src and valid dest", "should return") + { const char *dest = nullptr; const char *src = "test"; @@ -48,10 +69,12 @@ TEST_CASE("renderd_config.c", "[renderd_config]") } } - SECTION("name_with_section function") { + SECTION("name_with_section function") + { char *value = nullptr; - SECTION("name_with_section with valid name and invalid section", "should exit 7") { + SECTION("name_with_section with valid name and invalid section", "should exit 7") + { const char *name = "socketname"; const char *section = nullptr; @@ -66,8 +89,8 @@ TEST_CASE("renderd_config.c", "[renderd_config]") REQUIRE_THAT(err_log_lines, Catch::Matchers::Contains("name_with_section: invalid section (null)")); } - - SECTION("name_with_section with invalid name and valid section", "should exit 7") { + SECTION("name_with_section with invalid name and valid section", "should exit 7") + { const char *name = nullptr; const char *section = "renderd"; @@ -82,8 +105,8 @@ TEST_CASE("renderd_config.c", "[renderd_config]") REQUIRE_THAT(err_log_lines, Catch::Matchers::Contains("name_with_section: invalid name (null)")); } - - SECTION("name_with_section with valid name and valid section", "should return") { + SECTION("name_with_section with valid name and valid section", "should return") + { const char *name = "socketname"; const char *section = "renderd"; @@ -92,7 +115,8 @@ TEST_CASE("renderd_config.c", "[renderd_config]") REQUIRE((std::string(section) + ":" + name) == value); } - SECTION("name_with_section handles asprintf failure by exiting", "should exit 7") { + SECTION("name_with_section handles asprintf failure by exiting", "should exit 7") + { const char *name = "socketname"; const char *section = "renderd"; @@ -110,7 +134,8 @@ TEST_CASE("renderd_config.c", "[renderd_config]") } } - SECTION("min_max_double_opt & min_max_int_opt functions") { + SECTION("min_max_double_opt & min_max_int_opt functions") + { const char *opt_type_name = "value"; const double dmax = 1.15; const double dmin = 1.10; @@ -118,7 +143,8 @@ TEST_CASE("renderd_config.c", "[renderd_config]") err_log_lines.clear(); exit_status = 0; - SECTION("min_max_double_opt success", "should not exit") { + SECTION("min_max_double_opt success", "should not exit") + { const char *opt_arg = "1.125"; if (setjmp(exit_jump) == 0) { @@ -132,7 +158,8 @@ TEST_CASE("renderd_config.c", "[renderd_config]") REQUIRE(err_log_lines == ""); } - SECTION("min_max_double_opt exceeds max", "should exit 1") { + SECTION("min_max_double_opt exceeds max", "should exit 1") + { const char *opt_arg = "2"; if (setjmp(exit_jump) == 0) { @@ -146,7 +173,8 @@ TEST_CASE("renderd_config.c", "[renderd_config]") REQUIRE_THAT(err_log_lines, Catch::Matchers::Contains("Invalid " + std::string(opt_type_name) + ", must be <= " + std::to_string(dmax) + " (" + std::string(opt_arg) + " was provided)")); } - SECTION("min_max_double_opt less than min", "should exit 1") { + SECTION("min_max_double_opt less than min", "should exit 1") + { const char *opt_arg = "1"; if (setjmp(exit_jump) == 0) { @@ -160,7 +188,8 @@ TEST_CASE("renderd_config.c", "[renderd_config]") REQUIRE_THAT(err_log_lines, Catch::Matchers::Contains("Invalid " + std::string(opt_type_name) + ", must be >= " + std::to_string(dmin) + " (" + std::string(opt_arg) + " was provided)")); } - SECTION("min_max_double_opt exceeds max", "should exit 1") { + SECTION("min_max_double_opt exceeds max", "should exit 1") + { const char *opt_arg = "fail"; if (setjmp(exit_jump) == 0) { @@ -174,8 +203,8 @@ TEST_CASE("renderd_config.c", "[renderd_config]") REQUIRE_THAT(err_log_lines, Catch::Matchers::Contains("Invalid " + std::string(opt_type_name) + ", must be a double (" + std::string(opt_arg) + " was provided)")); } - - SECTION("min_max_int_opt success", "should not exit") { + SECTION("min_max_int_opt success", "should not exit") + { const char *opt_arg = "1"; if (setjmp(exit_jump) == 0) { @@ -189,7 +218,8 @@ TEST_CASE("renderd_config.c", "[renderd_config]") REQUIRE(err_log_lines == ""); } - SECTION("min_max_int_opt exceeds max", "should exit 1") { + SECTION("min_max_int_opt exceeds max", "should exit 1") + { const char *opt_arg = "2"; if (setjmp(exit_jump) == 0) { @@ -203,7 +233,8 @@ TEST_CASE("renderd_config.c", "[renderd_config]") REQUIRE_THAT(err_log_lines, Catch::Matchers::Contains("Invalid " + std::string(opt_type_name) + ", must be <= " + std::to_string((int)dmax) + " (" + std::string(opt_arg) + " was provided)")); } - SECTION("min_max_int_opt less than min", "should exit 1") { + SECTION("min_max_int_opt less than min", "should exit 1") + { const char *opt_arg = "0"; if (setjmp(exit_jump) == 0) { @@ -217,7 +248,8 @@ TEST_CASE("renderd_config.c", "[renderd_config]") REQUIRE_THAT(err_log_lines, Catch::Matchers::Contains("Invalid " + std::string(opt_type_name) + ", must be >= " + std::to_string((int)dmin) + " (" + std::string(opt_arg) + " was provided)")); } - SECTION("min_max_int_opt exceeds max", "should exit 1") { + SECTION("min_max_int_opt exceeds max", "should exit 1") + { const char *opt_arg = "fail"; if (setjmp(exit_jump) == 0) { @@ -232,11 +264,13 @@ TEST_CASE("renderd_config.c", "[renderd_config]") } } - SECTION("renderd.conf file processing functions") { + SECTION("renderd.conf file processing functions") + { err_log_lines.clear(); exit_status = 0; - SECTION("valid renderd.conf file with invalid active renderd section", "should exit 1") { + SECTION("valid renderd.conf file with invalid active renderd section", "should exit 1") + { if (setjmp(exit_jump) == 0) { process_config_file(RENDERD_CONF, MAX_SLAVES, 0); FAIL("process_config_file should have called exit(), but did not"); @@ -248,7 +282,8 @@ TEST_CASE("renderd_config.c", "[renderd_config]") REQUIRE_THAT(err_log_lines, Catch::Matchers::Contains("Active renderd section (" + std::to_string(MAX_SLAVES) + ") must be between 0 and " + std::to_string(MAX_SLAVES - 1) + ".")); } - SECTION("valid renderd.conf file with nonexistent active renderd section", "should exit 1") { + SECTION("valid renderd.conf file with nonexistent active renderd section", "should exit 1") + { if (setjmp(exit_jump) == 0) { process_config_file(RENDERD_CONF, (MAX_SLAVES - 1), 0); FAIL("process_config_file should have called exit(), but did not"); @@ -260,7 +295,8 @@ TEST_CASE("renderd_config.c", "[renderd_config]") REQUIRE_THAT(err_log_lines, Catch::Matchers::Contains("Active renderd section (" + std::to_string(MAX_SLAVES - 1) + ") does not exist.")); } - SECTION("nonexistent renderd.conf file with valid active renderd section", "should exit 1") { + SECTION("nonexistent renderd.conf file with valid active renderd section", "should exit 1") + { if (setjmp(exit_jump) == 0) { process_config_file("doesnotexist", 0, 0); FAIL("process_config_file should have called exit(), but did not"); @@ -272,7 +308,8 @@ TEST_CASE("renderd_config.c", "[renderd_config]") REQUIRE_THAT(err_log_lines, Catch::Matchers::Contains("Failed to load config file (process_config_file): 'doesnotexist'")); } - SECTION("nonexistent renderd.conf file with valid active renderd section (process_renderd_sections)", "should exit 1") { + SECTION("nonexistent renderd.conf file with valid active renderd section (process_renderd_sections)", "should exit 1") + { if (setjmp(exit_jump) == 0) { process_renderd_sections(NULL, "doesnotexist", config_slaves); FAIL("process_renderd_sections should have called exit(), but did not"); @@ -284,7 +321,8 @@ TEST_CASE("renderd_config.c", "[renderd_config]") REQUIRE_THAT(err_log_lines, Catch::Matchers::Contains("Failed to load config file (process_renderd_sections): 'doesnotexist'")); } - SECTION("nonexistent renderd.conf file with valid active renderd section (process_mapnik_section)", "should exit 1") { + SECTION("nonexistent renderd.conf file with valid active renderd section (process_mapnik_section)", "should exit 1") + { if (setjmp(exit_jump) == 0) { process_mapnik_section(NULL, "doesnotexist", config_slaves); FAIL("process_mapnik_section should have called exit(), but did not"); @@ -296,7 +334,8 @@ TEST_CASE("renderd_config.c", "[renderd_config]") REQUIRE_THAT(err_log_lines, Catch::Matchers::Contains("Failed to load config file (process_mapnik_section): 'doesnotexist'")); } - SECTION("nonexistent renderd.conf file with valid active renderd section (process_map_sections)", "should exit 1") { + SECTION("nonexistent renderd.conf file with valid active renderd section (process_map_sections)", "should exit 1") + { if (setjmp(exit_jump) == 0) { process_map_sections(NULL, "doesnotexist", maps, "", 0); FAIL("process_map_sections should have called exit(), but did not"); @@ -308,7 +347,8 @@ TEST_CASE("renderd_config.c", "[renderd_config]") REQUIRE_THAT(err_log_lines, Catch::Matchers::Contains("Failed to load config file (process_map_sections): 'doesnotexist'")); } - SECTION("valid renderd.conf file with valid active renderd section (process_config_file)", "should not exit") { + SECTION("valid renderd.conf file with valid active renderd section (process_config_file)", "should not exit") + { if (setjmp(exit_jump) == 0) { process_config_file(RENDERD_CONF, 0, 0); SUCCEED("process_config_file did not call exit(), as expected"); @@ -319,7 +359,8 @@ TEST_CASE("renderd_config.c", "[renderd_config]") REQUIRE(exit_status == 0); } - SECTION("valid renderd.conf file with valid active renderd section (process_renderd_sections)", "should not exit") { + SECTION("valid renderd.conf file with valid active renderd section (process_renderd_sections)", "should not exit") + { if (setjmp(exit_jump) == 0) { process_renderd_sections(NULL, RENDERD_CONF, config_slaves); SUCCEED("process_renderd_sections did not call exit(), as expected"); @@ -330,7 +371,8 @@ TEST_CASE("renderd_config.c", "[renderd_config]") REQUIRE(exit_status == 0); } - SECTION("valid renderd.conf file with valid active renderd section (process_mapnik_section)", "should not exit") { + SECTION("valid renderd.conf file with valid active renderd section (process_mapnik_section)", "should not exit") + { if (setjmp(exit_jump) == 0) { process_mapnik_section(NULL, RENDERD_CONF, config_slaves); SUCCEED("process_mapnik_section did not call exit(), as expected"); @@ -341,7 +383,8 @@ TEST_CASE("renderd_config.c", "[renderd_config]") REQUIRE(exit_status == 0); } - SECTION("valid renderd.conf file with valid active renderd section (process_map_sections)", "should not exit") { + SECTION("valid renderd.conf file with valid active renderd section (process_map_sections)", "should not exit") + { if (setjmp(exit_jump) == 0) { process_map_sections(NULL, RENDERD_CONF, maps, "", 0); SUCCEED("process_map_sections did not call exit(), as expected"); @@ -362,7 +405,8 @@ TEST_CASE("renderd_config.c", "[renderd_config]") close(fd); std::ofstream renderd_conf_file(renderd_conf_path); - SECTION("renderd.conf with too many map sections", "should exit 7") { + SECTION("renderd.conf with too many map sections", "should exit 7") + { renderd_conf_file << "[mapnik]\n[renderd]\n"; for (int i = 0; i <= XMLCONFIGS_MAX; i++) { @@ -383,7 +427,8 @@ TEST_CASE("renderd_config.c", "[renderd_config]") REQUIRE_THAT(err_log_lines, Catch::Matchers::Contains("Can't handle more than " + std::to_string(XMLCONFIGS_MAX) + " map config sections")); } - SECTION("renderd.conf without map sections", "should exit 1") { + SECTION("renderd.conf without map sections", "should exit 1") + { renderd_conf_file << "[mapnik]\n[renderd]\n"; renderd_conf_file.close(); @@ -399,7 +444,8 @@ TEST_CASE("renderd_config.c", "[renderd_config]") REQUIRE_THAT(err_log_lines, Catch::Matchers::Contains("No map config sections were found in file: " + renderd_conf_path)); } - SECTION("renderd.conf without mapnik section", "should exit 1") { + SECTION("renderd.conf without mapnik section", "should exit 1") + { renderd_conf_file << "[map]\n[renderd]\n"; renderd_conf_file.close(); @@ -415,7 +461,8 @@ TEST_CASE("renderd_config.c", "[renderd_config]") REQUIRE_THAT(err_log_lines, Catch::Matchers::Contains("No mapnik config section was found in file: " + renderd_conf_path)); } - SECTION("renderd.conf with invalid renderd sections", "should exit 7") { + SECTION("renderd.conf with invalid renderd sections", "should exit 7") + { std::string renderd_conf_renderd_section_name = "renderdinvalid"; renderd_conf_file << "[mapnik]\n[map]\n[" + renderd_conf_renderd_section_name + "]\n"; @@ -433,7 +480,8 @@ TEST_CASE("renderd_config.c", "[renderd_config]") REQUIRE_THAT(err_log_lines, Catch::Matchers::Contains("Invalid renderd section name: " + renderd_conf_renderd_section_name)); } - SECTION("renderd.conf with too many renderd sections", "should exit 7") { + SECTION("renderd.conf with too many renderd sections", "should exit 7") + { renderd_conf_file << "[mapnik]\n[map]\n"; for (int i = 0; i <= MAX_SLAVES; i++) { @@ -454,7 +502,8 @@ TEST_CASE("renderd_config.c", "[renderd_config]") REQUIRE_THAT(err_log_lines, Catch::Matchers::Contains("Can't handle more than " + std::to_string(MAX_SLAVES) + " renderd config sections")); } - SECTION("renderd.conf without renderd sections", "should exit 1") { + SECTION("renderd.conf without renderd sections", "should exit 1") + { renderd_conf_file << "[map]\n[mapnik]\n"; renderd_conf_file.close(); @@ -470,7 +519,8 @@ TEST_CASE("renderd_config.c", "[renderd_config]") REQUIRE_THAT(err_log_lines, Catch::Matchers::Contains("No renderd config sections were found in file: " + renderd_conf_path)); } - SECTION("renderd.conf map section scale too small", "should exit 7") { + SECTION("renderd.conf map section scale too small", "should exit 7") + { renderd_conf_file << "[mapnik]\n[renderd]\n"; renderd_conf_file << "[map]\nscale=0.0\n"; renderd_conf_file.close(); @@ -487,7 +537,8 @@ TEST_CASE("renderd_config.c", "[renderd_config]") REQUIRE_THAT(err_log_lines, Catch::Matchers::Contains("Specified scale factor (0.000000) is too small, must be greater than or equal to 0.100000.")); } - SECTION("renderd.conf map section scale too large", "should exit 7") { + SECTION("renderd.conf map section scale too large", "should exit 7") + { renderd_conf_file << "[mapnik]\n[renderd]\n"; renderd_conf_file << "[map]\nscale=8.1\n"; renderd_conf_file.close(); @@ -504,7 +555,8 @@ TEST_CASE("renderd_config.c", "[renderd_config]") REQUIRE_THAT(err_log_lines, Catch::Matchers::Contains("Specified scale factor (8.100000) is too large, must be less than or equal to 8.000000.")); } - SECTION("renderd.conf map section maxzoom too small", "should exit 7") { + SECTION("renderd.conf map section maxzoom too small", "should exit 7") + { renderd_conf_file << "[mapnik]\n[renderd]\n"; renderd_conf_file << "[map]\nmaxzoom=-1\n"; renderd_conf_file.close(); @@ -521,7 +573,8 @@ TEST_CASE("renderd_config.c", "[renderd_config]") REQUIRE_THAT(err_log_lines, Catch::Matchers::Contains("Specified max zoom (-1) is too small, must be greater than or equal to 0.")); } - SECTION("renderd.conf map section maxzoom too large", "should exit 7") { + SECTION("renderd.conf map section maxzoom too large", "should exit 7") + { renderd_conf_file << "[mapnik]\n[renderd]\n"; renderd_conf_file << "[map]\nmaxzoom=" << MAX_ZOOM + 1 << "\n"; renderd_conf_file.close(); @@ -538,7 +591,8 @@ TEST_CASE("renderd_config.c", "[renderd_config]") REQUIRE_THAT(err_log_lines, Catch::Matchers::Contains("Specified max zoom (" + std::to_string(MAX_ZOOM + 1) + ") is too large, must be less than or equal to " + std::to_string(MAX_ZOOM) + ".")); } - SECTION("renderd.conf map section minzoom too small", "should exit 7") { + SECTION("renderd.conf map section minzoom too small", "should exit 7") + { renderd_conf_file << "[mapnik]\n[renderd]\n"; renderd_conf_file << "[map]\nminzoom=-1\n"; renderd_conf_file.close(); @@ -555,7 +609,8 @@ TEST_CASE("renderd_config.c", "[renderd_config]") REQUIRE_THAT(err_log_lines, Catch::Matchers::Contains("Specified min zoom (-1) is too small, must be greater than or equal to 0.")); } - SECTION("renderd.conf map section minzoom too large", "should exit 7") { + SECTION("renderd.conf map section minzoom too large", "should exit 7") + { renderd_conf_file << "[mapnik]\n[renderd]\n"; renderd_conf_file << "[map]\nminzoom=" << MAX_ZOOM + 1 << "\n"; renderd_conf_file.close(); @@ -572,7 +627,8 @@ TEST_CASE("renderd_config.c", "[renderd_config]") REQUIRE_THAT(err_log_lines, Catch::Matchers::Contains("Specified min zoom (" + std::to_string(MAX_ZOOM + 1) + ") is larger than max zoom (" + std::to_string(MAX_ZOOM) + ").")); } - SECTION("renderd.conf map section type has too few parts", "should exit 7") { + SECTION("renderd.conf map section type has too few parts", "should exit 7") + { std::string renderd_conf_map_type = "a"; renderd_conf_file << "[mapnik]\n[renderd]\n"; @@ -591,7 +647,8 @@ TEST_CASE("renderd_config.c", "[renderd_config]") REQUIRE_THAT(err_log_lines, Catch::Matchers::Contains("Specified type (" + renderd_conf_map_type + ") has too few parts, there must be at least 2, e.g., 'png image/png'.")); } - SECTION("renderd.conf map section type has too many parts", "should exit 7") { + SECTION("renderd.conf map section type has too many parts", "should exit 7") + { std::string renderd_conf_map_type = "a b c d"; renderd_conf_file << "[mapnik]\n[renderd]\n"; @@ -610,7 +667,8 @@ TEST_CASE("renderd_config.c", "[renderd_config]") REQUIRE_THAT(err_log_lines, Catch::Matchers::Contains("Specified type (" + renderd_conf_map_type + ") has too many parts, there must be no more than 3, e.g., 'png image/png png256'.")); } - SECTION("renderd.conf map section type has two parts", "should not exit") { + SECTION("renderd.conf map section type has two parts", "should not exit") + { renderd_conf_file << "[mapnik]\n[map]\ntype=png image/png\n"; renderd_conf_file << "[renderd]\n"; renderd_conf_file.close(); @@ -629,7 +687,8 @@ TEST_CASE("renderd_config.c", "[renderd_config]") REQUIRE_THAT(err_log_lines, Catch::Matchers::Contains("Read map:type:output_format: 'png256'")); } - SECTION("renderd.conf renderd section socketname is too long", "should exit 7") { + SECTION("renderd.conf renderd section socketname is too long", "should exit 7") + { int renderd_socketname_maxlen = sizeof(((struct sockaddr_un *)0)->sun_path); std::string renderd_socketname = "/" + std::string(renderd_socketname_maxlen, 'A'); @@ -649,7 +708,8 @@ TEST_CASE("renderd_config.c", "[renderd_config]") REQUIRE_THAT(err_log_lines, Catch::Matchers::Contains("Specified socketname (" + renderd_socketname + ") exceeds maximum allowed length of " + std::to_string(renderd_socketname_maxlen) + ".")); } - SECTION("renderd.conf duplicate renderd section names", "should exit 7") { + SECTION("renderd.conf duplicate renderd section names", "should exit 7") + { renderd_conf_file << "[mapnik]\n[map]\n"; renderd_conf_file << "[renderd0]\n[renderd]\n"; renderd_conf_file.close(); @@ -666,7 +726,8 @@ TEST_CASE("renderd_config.c", "[renderd_config]") REQUIRE_THAT(err_log_lines, Catch::Matchers::Contains("Duplicate renderd config section names for section 0: renderd0 & renderd")); } - SECTION("renderd.conf renderd section num_threads is -1", "should not exit") { + SECTION("renderd.conf renderd section num_threads is -1", "should not exit") + { renderd_conf_file << "[mapnik]\n[map]\n"; renderd_conf_file << "[renderd0]\nnum_threads=-1\n"; renderd_conf_file.close(); @@ -683,7 +744,8 @@ TEST_CASE("renderd_config.c", "[renderd_config]") REQUIRE_THAT(err_log_lines, Catch::Matchers::Contains("renderd: num_threads = '" + std::to_string(sysconf(_SC_NPROCESSORS_ONLN)) + "'")); } - SECTION("renderd.conf slave renderd sections' num_threads sum equals num_slave_threads", "should not exit") { + SECTION("renderd.conf slave renderd sections' num_threads sum equals num_slave_threads", "should not exit") + { renderd_conf_file << "[mapnik]\n[map]\n"; renderd_conf_file << "[renderd0]\nnum_threads=-1\n[renderd1]\nnum_threads=2\n[renderd2]\nnum_threads=2\n"; renderd_conf_file.close(); @@ -701,7 +763,8 @@ TEST_CASE("renderd_config.c", "[renderd_config]") REQUIRE_THAT(err_log_lines, Catch::Matchers::Contains("renderd: num_slave_threads = '4'")); } - SECTION("renderd.conf renderd section not using unix socketname", "should not exit") { + SECTION("renderd.conf renderd section not using unix socketname", "should not exit") + { renderd_conf_file << "[mapnik]\n[map]\n"; renderd_conf_file << "[renderd0]\niphostname=hostname\nipport=9999\n"; renderd_conf_file.close(); @@ -718,7 +781,8 @@ TEST_CASE("renderd_config.c", "[renderd_config]") REQUIRE_THAT(err_log_lines, Catch::Matchers::Contains("renderd: ip socket = 'hostname:9999'")); } - SECTION("renderd.conf with overlapping URIs", "should return 7") { + SECTION("renderd.conf with overlapping URIs", "should return 7") + { std::string map0_uri = GENERATE("/", "/map1/", "/map2/"); renderd_conf_file << "[mapnik]\n[renderd]\n"; @@ -740,7 +804,8 @@ TEST_CASE("renderd_config.c", "[renderd_config]") REQUIRE_THAT(err_log_lines, Catch::Matchers::Contains("Specified URI path ('" + map0_uri + "' in map section 'map0') must not be the parent of any subsequent map config section's URI path, e.g., '" + map0_uri + "1/' in map section 'map1'")); } - SECTION("renderd.conf with blank URIs", "should return 7") { + SECTION("renderd.conf with blank URIs", "should return 7") + { renderd_conf_file << "[mapnik]\n[renderd]\n"; renderd_conf_file << "[map0]\n"; renderd_conf_file << "uri=/0\n"; @@ -764,7 +829,8 @@ TEST_CASE("renderd_config.c", "[renderd_config]") std::remove(renderd_conf_path.c_str()); } - SECTION("process_config_bool, process_config_double, process_config_int, process_config_string & process_config_string_with_trailing_slash functions") { + SECTION("process_config_bool, process_config_double, process_config_int, process_config_string & process_config_string_with_trailing_slash functions") + { const dictionary *ini = iniparser_load(RENDERD_CONF); std::string section = "nosuchsection"; std::string name = "nosuchname"; @@ -772,7 +838,8 @@ TEST_CASE("renderd_config.c", "[renderd_config]") err_log_lines.clear(); exit_status = 0; - SECTION("process_config_bool", "should not exit") { + SECTION("process_config_bool", "should not exit") + { bool notfound = true; if (setjmp(exit_jump) == 0) { @@ -786,7 +853,8 @@ TEST_CASE("renderd_config.c", "[renderd_config]") REQUIRE_THAT(err_log_lines, Catch::Matchers::Contains("Read " + section + ":" + name + ": '" + (notfound ? "true" : "false") + "'")); } - SECTION("process_config_double", "should not exit") { + SECTION("process_config_double", "should not exit") + { double notfound = 123.456; if (setjmp(exit_jump) == 0) { @@ -800,7 +868,8 @@ TEST_CASE("renderd_config.c", "[renderd_config]") REQUIRE_THAT(err_log_lines, Catch::Matchers::Contains("Read " + section + ":" + name + ": '" + std::to_string(notfound) + "'")); } - SECTION("process_config_int", "should not exit") { + SECTION("process_config_int", "should not exit") + { int notfound = 123456; if (setjmp(exit_jump) == 0) { @@ -816,7 +885,8 @@ TEST_CASE("renderd_config.c", "[renderd_config]") std::string notfound = "notfound"; - SECTION("process_config_string", "should not exit") { + SECTION("process_config_string", "should not exit") + { if (setjmp(exit_jump) == 0) { process_config_string(ini, section.c_str(), name.c_str(), &maps[0].xmlname, notfound.c_str(), notfound.length()); SUCCEED("process_config_string did not call exit(), as expected"); @@ -828,7 +898,8 @@ TEST_CASE("renderd_config.c", "[renderd_config]") REQUIRE_THAT(err_log_lines, Catch::Matchers::Contains("Read " + section + ":" + name + ": '" + notfound + "'")); } - SECTION("process_config_string_with_trailing_slash with trailing slash", "should not add a trailing slash and not exit") { + SECTION("process_config_string_with_trailing_slash with trailing slash", "should not add a trailing slash and not exit") + { notfound += "/"; if (setjmp(exit_jump) == 0) { @@ -844,7 +915,8 @@ TEST_CASE("renderd_config.c", "[renderd_config]") REQUIRE(std::string(maps[0].xmluri) == notfound); } - SECTION("process_config_string_with_trailing_slash with empty string", "should add a trailing slash and not exit") { + SECTION("process_config_string_with_trailing_slash with empty string", "should add a trailing slash and not exit") + { notfound = ""; if (setjmp(exit_jump) == 0) { @@ -860,7 +932,8 @@ TEST_CASE("renderd_config.c", "[renderd_config]") REQUIRE(std::string(maps[0].xmluri) == notfound + "/"); } - SECTION("process_config_string_with_trailing_slash without trailing slash", "should add a trailing slash and not exit") { + SECTION("process_config_string_with_trailing_slash without trailing slash", "should add a trailing slash and not exit") + { if (setjmp(exit_jump) == 0) { process_config_string_with_trailing_slash(ini, section.c_str(), name.c_str(), &maps[0].xmluri, notfound.c_str(), PATH_MAX); SUCCEED("process_config_string_with_trailing_slash did not call exit(), as expected"); @@ -874,8 +947,10 @@ TEST_CASE("renderd_config.c", "[renderd_config]") REQUIRE(std::string(maps[0].xmluri) == notfound + "/"); } - SECTION("process_config_string_with_trailing_slash handles asprintf failure by exiting", "should exit 7") { - fail_next_next_asprintf = true; + SECTION("process_config_string_with_trailing_slash handles asprintf failure by exiting", "should exit 7") + { + reset_asprintf_test_count(); + fail_asprintf_on_call_count = 2; if (setjmp(exit_jump) == 0) { process_config_string_with_trailing_slash(ini, section.c_str(), name.c_str(), &maps[0].xmluri, notfound.c_str(), PATH_MAX); @@ -891,7 +966,8 @@ TEST_CASE("renderd_config.c", "[renderd_config]") iniparser_freedict((dictionary *)ini); } - SECTION("free_map_section, free_map_sections, free_renderd_section & free_renderd_sections functions") { + SECTION("free_map_section, free_map_sections, free_renderd_section & free_renderd_sections functions") + { int active_renderd_section_num = 0; int map_section_num = 0; @@ -903,21 +979,24 @@ TEST_CASE("renderd_config.c", "[renderd_config]") err_log_lines.clear(); - SECTION("free_map_section", "should not exit") { + SECTION("free_map_section", "should not exit") + { free_map_section(&maps[map_section_num]); REQUIRE(maps[map_section_num].xmlname == nullptr); free_map_section(&maps[map_section_num]); REQUIRE(maps[map_section_num].xmlname == nullptr); } - SECTION("free_map_sections", "should not exit") { + SECTION("free_map_sections", "should not exit") + { free_map_sections(maps); REQUIRE(maps[map_section_num].xmlname == nullptr); free_map_sections(maps); REQUIRE(maps[map_section_num].xmlname == nullptr); } - SECTION("free_renderd_section", "should not exit") { + SECTION("free_renderd_section", "should not exit") + { free_renderd_section(&config_slaves[active_renderd_section_num]); REQUIRE(config->name == nullptr); REQUIRE(config_slaves[active_renderd_section_num].name == nullptr); @@ -926,7 +1005,8 @@ TEST_CASE("renderd_config.c", "[renderd_config]") REQUIRE(config_slaves[active_renderd_section_num].name == nullptr); } - SECTION("free_renderd_sections", "should not exit") { + SECTION("free_renderd_sections", "should not exit") + { free_renderd_sections(config_slaves); REQUIRE(config->name == nullptr); REQUIRE(config_slaves[active_renderd_section_num].name == nullptr); diff --git a/tests/unit_test_request_queue.cpp b/tests/unit_test_request_queue.cpp new file mode 100644 index 000000000..fcb95eea8 --- /dev/null +++ b/tests/unit_test_request_queue.cpp @@ -0,0 +1,652 @@ +/* + * Copyright (c) 2007 - 2026 by mod_tile contributors (see AUTHORS file) + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; If not, see http://www.gnu.org/licenses/. + */ + +#include +#include + +#include "catch/catch.hpp" +#include "catch_test_common.hpp" + +#include "protocol.h" +#include "render_config.h" +#include "request_queue.h" + +#define NO_WORKER_THREADS 10 +#define ITEMS_PER_THREAD 15 + +static int calcHashKey(struct request_queue *queue, struct item *item); +static struct item *lookup_item_idx(struct request_queue *queue, struct item *item); +static void insert_item_idx(struct request_queue *queue, struct item *item); +static void remove_item_idx(struct request_queue *queue, struct item *item); +static enum protoCmd pending(struct request_queue *queue, struct item *test); + +// Helper function to test asynchronous thread fetching +static void *async_fetch_thread(void *arg) +{ + struct request_queue *queue = static_cast(arg); + struct item *fetched = request_queue_fetch_request(queue); + + if (fetched) { + request_queue_remove_request(queue, fetched, 0); + free(fetched); + } + + return NULL; +} + +// Helper function to create initialized test items safely +static struct item *create_test_item(enum protoCmd cmd, const char *xmlname, int x, uint64_t y, int z, int fd) +{ + struct item *item = static_cast(calloc(1, sizeof(struct item))); + REQUIRE(item != NULL); + + item->req.ver = PROTO_VER; + item->req.cmd = cmd; + if (xmlname) { + strncpy(item->req.xmlname, xmlname, sizeof(item->req.xmlname) - 1); + } else { + strncpy(item->req.xmlname, XMLCONFIG_DEFAULT, sizeof(item->req.xmlname) - 1); + } + item->req.x = x; + item->req.y = y; + item->req.z = z; + + item->mx = x; + item->my = y; + item->fd = fd; + item->duplicates = NULL; + + return item; +} + +// Helper function for multi-threaded consumer operations +static void *consumer_thread(void *arg) +{ + struct request_queue *queue = static_cast(arg); + + for (int i = 0; i < ITEMS_PER_THREAD; i++) { + struct item *item = request_queue_fetch_request(queue); + if (item) { + request_queue_remove_request(queue, item, 5); + free(item); + } + } + return NULL; +} + +// Helper function for multi-threaded producer operations +static void *producer_thread(void *arg) +{ + struct request_queue *queue = static_cast(arg); + uint64_t tid; + +#ifdef __MACH__ + pthread_threadid_np(NULL, &tid); +#elif defined(__FreeBSD__) + tid = reinterpret_cast(pthread_self()); +#else + tid = syscall(SYS_gettid); +#endif + + for (int i = 0; i < ITEMS_PER_THREAD; i++) { + struct item *item = create_test_item(cmdDirty, XMLCONFIG_DEFAULT, i, tid, 10, FD_INVALID); + request_queue_add_request(queue, item); + } + return NULL; +} + +TEST_CASE("request_queue.c", "[request_queue]") +{ + err_log_lines.clear(); + + SECTION("request_queue_init function") + { + SECTION("Initialization creates fully zeroed structure and valid queues") + { + struct request_queue *queue = request_queue_init(); + + REQUIRE(queue != NULL); + REQUIRE(queue->item_hashidx != NULL); + REQUIRE(queue->hashidxSize == HASHIDX_SIZE); + REQUIRE(request_queue_no_requests_queued(queue, cmdRenderPrio) == 0); + REQUIRE(request_queue_no_requests_queued(queue, cmdRender) == 0); + REQUIRE(request_queue_no_requests_queued(queue, cmdRenderLow) == 0); + REQUIRE(request_queue_no_requests_queued(queue, cmdDirty) == 0); + REQUIRE(request_queue_no_requests_queued(queue, cmdRenderBulk) == 0); + REQUIRE(request_queue_no_requests_queued(queue, static_cast(999)) == -1); + + stats_struct stats; + memset(&stats, 0xFF, sizeof(stats_struct)); + request_queue_copy_stats(queue, &stats); + + REQUIRE(stats.noDirtyRender == 0); + REQUIRE(stats.noReqDroped == 0); + REQUIRE(stats.noReqRender == 0); + REQUIRE(stats.noReqPrioRender == 0); + REQUIRE(stats.noReqLowRender == 0); + REQUIRE(stats.noReqBulkRender == 0); + + request_queue_close(queue); + } + + SECTION("Handles calloc failure") + { + fail_next_calloc = true; + + struct request_queue *queue = request_queue_init(); + + REQUIRE(queue == NULL); + REQUIRE(err_log_lines == ""); + } + + SECTION("Handles pthread_mutex_init failure") + { + fail_next_pthread_mutex_init = true; + + struct request_queue *queue = request_queue_init(); + + REQUIRE_THAT(err_log_lines, Catch::Matchers::Contains("Failed to create mutex for request_queue")); + + REQUIRE(queue == NULL); + } + + SECTION("Handles pthread_cond_init failure") + { + fail_next_pthread_cond_init = true; + + struct request_queue *queue = request_queue_init(); + + REQUIRE_THAT(err_log_lines, Catch::Matchers::Contains("Failed to create condition variable for request_queue")); + + REQUIRE(queue == NULL); + } + + SECTION("Handles malloc failure for hash index") + { + fail_next_malloc = true; + + struct request_queue *queue = request_queue_init(); + + REQUIRE(queue == NULL); + REQUIRE_THAT(err_log_lines, Catch::Matchers::Contains("Failed to allocate memory for request queue hash index")); + } + } + + struct request_queue *queue = request_queue_init(); + REQUIRE(queue != NULL); + + SECTION("calcHashKey function") + { + SECTION("Calculates key correctly across coordinate bounds") + { + struct item *item = create_test_item(cmdRender, XMLCONFIG_DEFAULT, 100, 200, 10, 1); + + int key = calcHashKey(queue, item); + REQUIRE(key >= 0); + REQUIRE(key < queue->hashidxSize); + + free(item); + } + } + + SECTION("insert_item_idx, lookup_item_idx & remove_item_idx functions") + { + SECTION("Returns NULL when item is not in index and finds item when present") + { + struct item *item = create_test_item(cmdRender, XMLCONFIG_DEFAULT, 100, 200, 10, 1); + + REQUIRE(lookup_item_idx(queue, item) == NULL); + + insert_item_idx(queue, item); + REQUIRE(lookup_item_idx(queue, item) == item); + + remove_item_idx(queue, item); + REQUIRE(lookup_item_idx(queue, item) == NULL); + + free(item); + } + } + + SECTION("pending function") + { + SECTION("Identifies new, duplicate render, and duplicate dirty items") + { + struct item *itemNorm = create_test_item(cmdRender, XMLCONFIG_DEFAULT, 10, 10, 10, 1); + struct item *dupNorm = create_test_item(cmdRender, XMLCONFIG_DEFAULT, 10, 10, 10, 2); + + REQUIRE(pending(queue, itemNorm) == cmdRender); + + request_queue_add_request(queue, itemNorm); + + REQUIRE(pending(queue, dupNorm) == cmdIgnore); + + struct item *fetched = request_queue_fetch_request(queue); + request_queue_remove_request(queue, fetched, 0); + free(itemNorm); + free(dupNorm); + } + } + + SECTION("request_queue_add_request function") + { + SECTION("Single request addition across priorities") + { + struct item *itemPrio = create_test_item(cmdRenderPrio, XMLCONFIG_DEFAULT, 1, 1, 10, 101); + struct item *itemNorm = create_test_item(cmdRender, XMLCONFIG_DEFAULT, 2, 2, 10, 102); + struct item *itemLow = create_test_item(cmdRenderLow, XMLCONFIG_DEFAULT, 3, 3, 10, 103); + struct item *itemBulk = create_test_item(cmdRenderBulk, XMLCONFIG_DEFAULT, 4, 4, 10, 104); + struct item *itemDirty = create_test_item(cmdDirty, XMLCONFIG_DEFAULT, 5, 5, 10, 105); + + REQUIRE(request_queue_add_request(queue, itemNorm) == cmdIgnore); + REQUIRE(request_queue_no_requests_queued(queue, cmdRender) == 1); + + REQUIRE(request_queue_add_request(queue, itemPrio) == cmdIgnore); + REQUIRE(request_queue_no_requests_queued(queue, cmdRenderPrio) == 1); + + REQUIRE(request_queue_add_request(queue, itemLow) == cmdIgnore); + REQUIRE(request_queue_no_requests_queued(queue, cmdRenderLow) == 1); + + REQUIRE(request_queue_add_request(queue, itemBulk) == cmdIgnore); + REQUIRE(request_queue_no_requests_queued(queue, cmdRenderBulk) == 1); + + REQUIRE(request_queue_add_request(queue, itemDirty) == cmdNotDone); + REQUIRE(request_queue_no_requests_queued(queue, cmdDirty) == 1); + REQUIRE(itemDirty->fd == FD_INVALID); + + struct item *fetched; + fetched = request_queue_fetch_request(queue); + request_queue_remove_request(queue, fetched, 10); + fetched = request_queue_fetch_request(queue); + request_queue_remove_request(queue, fetched, 20); + fetched = request_queue_fetch_request(queue); + request_queue_remove_request(queue, fetched, 30); + fetched = request_queue_fetch_request(queue); + request_queue_remove_request(queue, fetched, 40); + fetched = request_queue_fetch_request(queue); + request_queue_remove_request(queue, fetched, 50); + + free(itemPrio); + free(itemNorm); + free(itemLow); + free(itemDirty); + free(itemBulk); + } + + SECTION("Exact matching items form a duplicate chain on render queues") + { + struct item *original = create_test_item(cmdRender, XMLCONFIG_DEFAULT, 100, 200, 12, 10); + struct item *duplicate1 = create_test_item(cmdRender, XMLCONFIG_DEFAULT, 100, 200, 12, 11); + struct item *duplicate2 = create_test_item(cmdRenderLow, XMLCONFIG_DEFAULT, 100, 200, 12, 12); + + REQUIRE(request_queue_add_request(queue, original) == cmdIgnore); + REQUIRE(request_queue_no_requests_queued(queue, cmdRender) == 1); + + REQUIRE(request_queue_add_request(queue, duplicate1) == cmdIgnore); + REQUIRE(request_queue_no_requests_queued(queue, cmdRender) == 1); + REQUIRE(duplicate1->inQueue == queueDuplicate); + REQUIRE(original->duplicates == duplicate1); + + REQUIRE(request_queue_add_request(queue, duplicate2) == cmdIgnore); + REQUIRE(duplicate2->duplicates == duplicate1); + REQUIRE(original->duplicates == duplicate2); + + struct item *fetched = request_queue_fetch_request(queue); + REQUIRE(fetched == original); + + request_queue_remove_request(queue, fetched, 0); + + free(original); + free(duplicate1); + free(duplicate2); + } + + SECTION("Dirty queue pending requests reject duplicates with cmdNotDone") + { + struct item *dirtyItem = create_test_item(cmdDirty, XMLCONFIG_DEFAULT, 300, 400, 8, FD_INVALID); + struct item *dupDirty = create_test_item(cmdRender, XMLCONFIG_DEFAULT, 300, 400, 8, 20); + + REQUIRE(request_queue_add_request(queue, dirtyItem) == cmdNotDone); + + REQUIRE(request_queue_add_request(queue, dupDirty) == cmdNotDone); + REQUIRE(request_queue_no_requests_queued(queue, cmdDirty) == 1); + + struct item *fetched = request_queue_fetch_request(queue); + REQUIRE(fetched == dirtyItem); + request_queue_remove_request(queue, fetched, 0); + + free(dirtyItem); + } + + SECTION("Requests overflow from render queues to dirty queue when REQ_LIMIT reached") + { + for (int i = 0; i < REQ_LIMIT; i++) { + struct item *item = create_test_item(cmdRender, XMLCONFIG_DEFAULT, i, 0, 5, 1); + REQUIRE(request_queue_add_request(queue, item) == cmdIgnore); + } + REQUIRE(request_queue_no_requests_queued(queue, cmdRender) == REQ_LIMIT); + + struct item *overflowItem = create_test_item(cmdRender, XMLCONFIG_DEFAULT, REQ_LIMIT + 1, 0, 5, 2); + REQUIRE(request_queue_add_request(queue, overflowItem) == cmdNotDone); + REQUIRE(request_queue_no_requests_queued(queue, cmdRender) == REQ_LIMIT); + REQUIRE(request_queue_no_requests_queued(queue, cmdDirty) == 1); + + for (int i = 0; i < REQ_LIMIT + 1; i++) { + struct item *fetched = request_queue_fetch_request(queue); + REQUIRE(fetched != NULL); + request_queue_remove_request(queue, fetched, 0); + free(fetched); + } + } + + SECTION("Requests are dropped when DIRTY_LIMIT reached") + { + for (int i = 0; i < DIRTY_LIMIT; i++) { + struct item *item = create_test_item(cmdDirty, XMLCONFIG_DEFAULT, i, 100, 5, FD_INVALID); + REQUIRE(request_queue_add_request(queue, item) == cmdNotDone); + } + REQUIRE(request_queue_no_requests_queued(queue, cmdDirty) == DIRTY_LIMIT); + + struct item *droppedItem = create_test_item(cmdDirty, XMLCONFIG_DEFAULT, DIRTY_LIMIT + 1, 100, 5, FD_INVALID); + REQUIRE(request_queue_add_request(queue, droppedItem) == cmdNotDone); + + stats_struct stats; + request_queue_copy_stats(queue, &stats); + REQUIRE(stats.noReqDroped == 1); + + for (int i = 0; i < DIRTY_LIMIT; i++) { + struct item *fetched = request_queue_fetch_request(queue); + REQUIRE(fetched != NULL); + request_queue_remove_request(queue, fetched, 0); + free(fetched); + } + } + + SECTION("Multiple items colliding in the same hash bucket are chained correctly") + { + struct item *itemA = create_test_item(cmdRender, "map_a", 10, 10, 5, 1); + struct item *itemB = create_test_item(cmdRender, "map_b", 10, 10, 5, 2); + + request_queue_add_request(queue, itemA); + request_queue_add_request(queue, itemB); + + struct item *fetchedA = request_queue_fetch_request(queue); + struct item *fetchedB = request_queue_fetch_request(queue); + + REQUIRE(fetchedA != NULL); + REQUIRE(fetchedB != NULL); + + request_queue_remove_request(queue, fetchedA, 0); + request_queue_remove_request(queue, fetchedB, 0); + + free(itemA); + free(itemB); + } + + SECTION("Handles NULL queue", "should exit 3") + { + struct item *test_item = create_test_item(cmdRender, XMLCONFIG_DEFAULT, 1, 1, 1, 1); + + if (setjmp(exit_jump) == 0) { + request_queue_add_request(NULL, test_item); + FAIL("request_queue_add_request should have called exit(3) but didn't"); + } else { + SUCCEED("Captured expected exit() call due to NULL queue"); + } + + REQUIRE(exit_status == 3); + REQUIRE_THAT(err_log_lines, Catch::Matchers::Contains("queue is NULL")); + free(test_item); + } + } + + SECTION("request_queue_fetch_request function") + { + SECTION("Fetches items following strict priority ordering") + { + struct item *itemPrio = create_test_item(cmdRenderPrio, XMLCONFIG_DEFAULT, 1, 1, 10, 101); + struct item *itemNorm = create_test_item(cmdRender, XMLCONFIG_DEFAULT, 2, 2, 10, 102); + struct item *itemLow = create_test_item(cmdRenderLow, XMLCONFIG_DEFAULT, 3, 3, 10, 103); + struct item *itemBulk = create_test_item(cmdRenderBulk, XMLCONFIG_DEFAULT, 4, 4, 10, 104); + struct item *itemDirty = create_test_item(cmdDirty, XMLCONFIG_DEFAULT, 5, 5, 10, 105); + + request_queue_add_request(queue, itemNorm); + request_queue_add_request(queue, itemPrio); + request_queue_add_request(queue, itemLow); + request_queue_add_request(queue, itemBulk); + request_queue_add_request(queue, itemDirty); + + struct item *fetched; + fetched = request_queue_fetch_request(queue); + REQUIRE(fetched == itemPrio); + fetched = request_queue_fetch_request(queue); + REQUIRE(fetched == itemNorm); + fetched = request_queue_fetch_request(queue); + REQUIRE(fetched == itemLow); + fetched = request_queue_fetch_request(queue); + REQUIRE(fetched == itemDirty); + fetched = request_queue_fetch_request(queue); + REQUIRE(fetched == itemBulk); + + request_queue_remove_request(queue, itemPrio, 0); + request_queue_remove_request(queue, itemNorm, 0); + request_queue_remove_request(queue, itemLow, 0); + request_queue_remove_request(queue, itemDirty, 0); + request_queue_remove_request(queue, itemBulk, 0); + + free(itemPrio); + free(itemNorm); + free(itemLow); + free(itemDirty); + free(itemBulk); + } + + SECTION("Concurrent enqueuing and dequeuing maintains state integrity") + { + pthread_t producers[NO_WORKER_THREADS]; + pthread_t consumers[NO_WORKER_THREADS]; + + for (int i = 0; i < NO_WORKER_THREADS; i++) { + pthread_create(&producers[i], NULL, producer_thread, queue); + pthread_create(&consumers[i], NULL, consumer_thread, queue); + } + + for (int i = 0; i < NO_WORKER_THREADS; i++) { + pthread_join(producers[i], NULL); + pthread_join(consumers[i], NULL); + } + + REQUIRE(request_queue_no_requests_queued(queue, cmdDirty) == 0); + + stats_struct stats; + request_queue_copy_stats(queue, &stats); + REQUIRE(stats.noDirtyRender == (NO_WORKER_THREADS * ITEMS_PER_THREAD)); + } + + SECTION("Blocks on condition variable when empty") + { + pthread_t thread; + pthread_create(&thread, NULL, async_fetch_thread, queue); + + usleep(10000); + + struct item *item = create_test_item(cmdRender, XMLCONFIG_DEFAULT, 1, 1, 1, 1); + request_queue_add_request(queue, item); + + pthread_join(thread, NULL); + } + } + + SECTION("request_queue_remove_request function") + { + SECTION("Stat updates during request removal across queues") + { + struct item *itemPrio = create_test_item(cmdRenderPrio, XMLCONFIG_DEFAULT, 10, 10, 5, 1); + request_queue_add_request(queue, itemPrio); + + struct item *fetched = request_queue_fetch_request(queue); + REQUIRE(fetched == itemPrio); + + request_queue_remove_request(queue, fetched, 150); + + stats_struct stats; + request_queue_copy_stats(queue, &stats); + REQUIRE(stats.noReqPrioRender == 1); + REQUIRE(stats.timeReqPrioRender == 150); + REQUIRE(stats.noZoomRender[5] == 1); + REQUIRE(stats.timeZoomRender[5] == 150); + + free(itemPrio); + } + + SECTION("Logs warning when item is not in queueRender", "should execute warning path") + { + struct item *item = create_test_item(cmdRender, XMLCONFIG_DEFAULT, 1, 1, 1, 1); + request_queue_add_request(queue, item); + + request_queue_remove_request(queue, item, 0); + + REQUIRE_THAT(err_log_lines, Catch::Matchers::Contains("Removing request from queue, even though not on rendering queue")); + + free(item); + } + + SECTION("Switch default for unknown originatedQueue", "should hit default switch case") + { + struct item *item = create_test_item(cmdRender, XMLCONFIG_DEFAULT, 2, 2, 2, 1); + request_queue_add_request(queue, item); + + struct item *fetched = request_queue_fetch_request(queue); + REQUIRE(fetched == item); + + fetched->originatedQueue = static_castoriginatedQueue)>(999); + request_queue_remove_request(queue, fetched, 50); + + free(item); + } + } + + SECTION("remove_item_idx function") + { + SECTION("Handles empty hash bucket lookup", "should return safely when bucket is empty") + { + struct item *phantom = create_test_item(cmdRender, "phantom_map", 9999, 9999, 1, 1); + phantom->inQueue = queueRender; + + phantom->next = phantom; + phantom->prev = phantom; + + request_queue_remove_request(queue, phantom, 0); + + free(phantom); + } + + SECTION("Handles removing non-head middle and tail nodes in collision chain") + { + struct item *itemA = create_test_item(cmdRender, "ab", 100, 100, 10, 1); + struct item *itemB = create_test_item(cmdRenderLow, "ba", 100, 100, 10, 2); + struct item *itemC = create_test_item(cmdRenderBulk, "c`", 100, 100, 10, 3); + + itemA->next = itemA; + itemA->prev = itemA; + itemA->inQueue = queueRender; + itemB->next = itemB; + itemB->prev = itemB; + itemB->inQueue = queueRender; + itemC->next = itemC; + itemC->prev = itemC; + itemC->inQueue = queueRender; + + insert_item_idx(queue, itemA); + insert_item_idx(queue, itemB); + insert_item_idx(queue, itemC); + + request_queue_remove_request(queue, itemB, 0); + request_queue_remove_request(queue, itemC, 0); + request_queue_remove_request(queue, itemA, 0); + + free(itemA); + free(itemB); + free(itemC); + } + } + + SECTION("request_queue_clear_requests_by_fd function") + { + int target_fd = 42; + int safe_fd = 99; + + struct item *item1 = create_test_item(cmdRender, XMLCONFIG_DEFAULT, 1, 1, 10, target_fd); + struct item *dup1 = create_test_item(cmdRender, XMLCONFIG_DEFAULT, 1, 1, 10, target_fd); + + struct item *item2 = create_test_item(cmdRenderPrio, XMLCONFIG_DEFAULT, 2, 2, 10, target_fd); + struct item *item3 = create_test_item(cmdRenderBulk, XMLCONFIG_DEFAULT, 3, 3, 10, safe_fd); + + request_queue_add_request(queue, item1); + request_queue_add_request(queue, dup1); + request_queue_add_request(queue, item2); + request_queue_add_request(queue, item3); + + SECTION("Invalidate target FD across primary requests and duplicate chains") + { + request_queue_clear_requests_by_fd(queue, target_fd); + + REQUIRE(item1->fd == FD_INVALID); + REQUIRE(dup1->fd == FD_INVALID); + REQUIRE(item2->fd == FD_INVALID); + REQUIRE(item3->fd == safe_fd); + + for (int i = 0; i < 3; i++) { + struct item *fetched = request_queue_fetch_request(queue); + request_queue_remove_request(queue, fetched, 0); + free(fetched); + } + free(dup1); + } + } + + SECTION("request_queue_no_requests_queued function") + { + SECTION("Accurately counts queued items per priority", "returns -1 for invalid cmd") + { + REQUIRE(request_queue_no_requests_queued(queue, cmdRenderPrio) == 0); + REQUIRE(request_queue_no_requests_queued(queue, cmdRender) == 0); + REQUIRE(request_queue_no_requests_queued(queue, cmdRenderLow) == 0); + REQUIRE(request_queue_no_requests_queued(queue, cmdDirty) == 0); + REQUIRE(request_queue_no_requests_queued(queue, cmdRenderBulk) == 0); + REQUIRE(request_queue_no_requests_queued(queue, static_cast(999)) == -1); + } + } + + SECTION("request_queue_copy_stats function") + { + SECTION("Copies accurate runtime stats into target struct") + { + stats_struct stats; + memset(&stats, 0xFF, sizeof(stats_struct)); + request_queue_copy_stats(queue, &stats); + + REQUIRE(stats.noDirtyRender == 0); + REQUIRE(stats.noReqDroped == 0); + REQUIRE(stats.noReqRender == 0); + REQUIRE(stats.noReqPrioRender == 0); + REQUIRE(stats.noReqLowRender == 0); + REQUIRE(stats.noReqBulkRender == 0); + } + } + + request_queue_close(queue); +} diff --git a/tests/unit_test_store.cpp b/tests/unit_test_store.cpp new file mode 100644 index 000000000..0f4aefc43 --- /dev/null +++ b/tests/unit_test_store.cpp @@ -0,0 +1,180 @@ +/* + * Copyright (c) 2007 - 2026 by mod_tile contributors (see AUTHORS file) + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; If not, see http://www.gnu.org/licenses/. + */ + +#include "catch/catch.hpp" +#include "catch_test_common.hpp" + +#include "store.h" + +TEST_CASE("store.c", "[store]") +{ + SECTION("init_storage_backend") + { + err_log_lines.clear(); + reset_malloc_test_count(); + + SECTION("returns NULL when options string is empty") + { + struct storage_backend *store = init_storage_backend(""); + REQUIRE(store == NULL); + REQUIRE_THAT(err_log_lines, Catch::Matchers::Contains("Options string was empty")); + } + + SECTION("handles absolute path storage options") + { + SECTION("fails when path does not exist (stat failure)") + { + err_log_lines.clear(); + struct storage_backend *store = init_storage_backend("/tmp/nonexistent_mod_tile_dir_12345"); + REQUIRE(store == NULL); + REQUIRE_THAT(err_log_lines, Catch::Matchers::Contains("Failed to stat /tmp/nonexistent_mod_tile_dir_12345")); + } + + SECTION("fails when path points to a file rather than a directory") + { + char tmp_file[] = "/tmp/mod_tile_regular_file_XXXXXX"; + int fd = mkstemp(tmp_file); + REQUIRE(fd >= 0); + close(fd); + + err_log_lines.clear(); + struct storage_backend *store = init_storage_backend(tmp_file); + REQUIRE(store == NULL); + REQUIRE_THAT(err_log_lines, Catch::Matchers::Contains("is not a directory")); + + unlink(tmp_file); + } + + SECTION("initializes file storage backend when path is a valid directory") + { + std::string tile_dir = create_tile_dir("store_file_dir", NULL); + + err_log_lines.clear(); + struct storage_backend *store = init_storage_backend(tile_dir.c_str()); + REQUIRE(store != NULL); + REQUIRE_THAT(err_log_lines, Catch::Matchers::Contains("initialising file storage backend at:")); + + store->close_storage(store); + free(store); + + std::string tile_dir_slash = tile_dir + "/"; + err_log_lines.clear(); + store = init_storage_backend(tile_dir_slash.c_str()); + REQUIRE(store != NULL); + REQUIRE_THAT(err_log_lines, Catch::Matchers::Contains("initialising file storage backend at:")); + + store->close_storage(store); + free(store); + + delete_tile_dir(tile_dir); + } + } + + SECTION("handles rados:// prefix options") + { + err_log_lines.clear(); + std::string rados_opts = "rados://config_file=/etc/ceph/ceph.conf;pool=tiles"; + struct storage_backend *store = init_storage_backend(rados_opts.c_str()); + +#ifndef HAVE_LIBRADOS + REQUIRE(store == NULL); + REQUIRE_THAT(err_log_lines, Catch::Matchers::Contains("Support for rados storage has not been compiled into this program")); +#else + REQUIRE(store == NULL); + REQUIRE_THAT(err_log_lines, Catch::Matchers::Contains("failed to read rados config file")); +#endif + } + + SECTION("handles memcached:// prefix options") + { + err_log_lines.clear(); + std::string memc_opts = "memcached://127.0.0.1:11211"; + struct storage_backend *store = init_storage_backend(memc_opts.c_str()); + +#ifndef HAVE_LIBMEMCACHED + REQUIRE(store == NULL); + REQUIRE_THAT(err_log_lines, Catch::Matchers::Contains("Support for memcached storage has not been compiled into this program")); +#else + REQUIRE(store != NULL); + REQUIRE_THAT(err_log_lines, Catch::Matchers::Contains("initialising memcached storage backend at:")); + + store->close_storage(store); + free(store); +#endif + } + + SECTION("handles ro_http_proxy:// prefix options") + { + err_log_lines.clear(); + std::string http_opts = "ro_http_proxy://tile.openstreetmap.org"; + struct storage_backend *store = init_storage_backend(http_opts.c_str()); + +#ifndef HAVE_LIBCURL + REQUIRE(store == NULL); + REQUIRE_THAT(err_log_lines, Catch::Matchers::Contains("Support for http proxy storage has not been compiled into this program")); +#else + REQUIRE(store != NULL); + REQUIRE_THAT(err_log_lines, Catch::Matchers::Contains("initialising ro_http_proxy storage backend at:")); + + store->close_storage(store); + free(store); +#endif + } + + SECTION("handles composite:{ prefix options") + { + std::string primary_dir = create_tile_dir("store_comp_primary", NULL); + std::string secondary_dir = create_tile_dir("store_comp_secondary", NULL); + std::string comp_opts = "composite:{" + std::string(XMLCONFIG_DEFAULT) + "," + primary_dir + "}{" + std::string(XMLCONFIG_DEFAULT) + "," + secondary_dir + "}"; + + err_log_lines.clear(); + struct storage_backend *store = init_storage_backend(comp_opts.c_str()); + +#ifndef WANT_STORE_COMPOSITE + REQUIRE(store == NULL); + REQUIRE_THAT(err_log_lines, Catch::Matchers::Contains("Support for compositing storage has not been compiled into this program")); +#else + REQUIRE(store != NULL); + REQUIRE_THAT(err_log_lines, Catch::Matchers::Contains("initialising ro_composite storage backend at:")); + + store->close_storage(store); + free(store); +#endif + delete_tile_dir(primary_dir); + delete_tile_dir(secondary_dir); + } + + SECTION("handles null:// prefix options") + { + err_log_lines.clear(); + struct storage_backend *store = init_storage_backend("null://"); + REQUIRE(store != NULL); + REQUIRE_THAT(err_log_lines, Catch::Matchers::Contains("initialising null storage backend at:")); + + store->close_storage(store); + free(store); + } + + SECTION("returns NULL when no backend matches unrecognized options prefix") + { + err_log_lines.clear(); + struct storage_backend *store = init_storage_backend("unsupported_backend_schema://localhost"); + REQUIRE(store == NULL); + REQUIRE_THAT(err_log_lines, Catch::Matchers::Contains("No valid storage backend found for options:")); + } + } +} diff --git a/tests/unit_test_store_file.cpp b/tests/unit_test_store_file.cpp new file mode 100644 index 000000000..3cf58aac8 --- /dev/null +++ b/tests/unit_test_store_file.cpp @@ -0,0 +1,556 @@ +/* + * Copyright (c) 2007 - 2026 by mod_tile contributors (see AUTHORS file) + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; If not, see http://www.gnu.org/licenses/. + */ + +#include +#include +#include +#include +#include + +#include "catch/catch.hpp" +#include "catch_test_common.hpp" + +#include "metatile.h" +#include "store_file.h" +#include "store_file_utils.h" + +static void create_mock_metatile(const char *path, bool compressed, int count, size_t tile_data_len, const char *tile_data) +{ + mkdirp(path); + + size_t header_len = sizeof(struct meta_layout) + METATILE * METATILE * sizeof(struct entry); + struct meta_layout *m = (struct meta_layout *)calloc(1, header_len); + + if (compressed) { + memcpy(m->magic, META_MAGIC_COMPRESSED, strlen(META_MAGIC_COMPRESSED)); + } else { + memcpy(m->magic, META_MAGIC, strlen(META_MAGIC)); + } + + m->count = count; + m->index[0].offset = header_len; + m->index[0].size = tile_data_len; + + int fd = open(path, O_WRONLY | O_CREAT | O_TRUNC, 0666); + if (fd >= 0) { + write(fd, m, header_len); + if (tile_data_len > 0 && tile_data != NULL) { + write(fd, tile_data, tile_data_len); + } + close(fd); + } + + free(m); +} + +TEST_CASE("store_file.c", "[store_file]") +{ + std::string tmp_dir = create_tile_dir("store_file_test"); + + SECTION("init_storage_file function") + { + err_log_lines.clear(); + + SECTION("init_storage_file handles malloc failure", "should return NULL") + { + fail_next_malloc = true; + + struct storage_backend *store = init_storage_file(tmp_dir.c_str()); + + REQUIRE(store == NULL); + REQUIRE_THAT(err_log_lines, Catch::Matchers::Contains("init_storage_file: Failed to allocate memory")); + } + + SECTION("init_storage_file creates valid backend", "should set storage_ctx and function pointers") + { + struct storage_backend *store = init_storage_file(tmp_dir.c_str()); + + REQUIRE(store != NULL); + REQUIRE(store->storage_ctx != NULL); + REQUIRE(std::string((char *)store->storage_ctx) == tmp_dir); + REQUIRE(store->tile_read != NULL); + REQUIRE(store->tile_stat != NULL); + REQUIRE(store->metatile_write != NULL); + REQUIRE(store->metatile_delete != NULL); + REQUIRE(store->metatile_expire != NULL); + REQUIRE(store->tile_storage_id != NULL); + REQUIRE(store->close_storage != NULL); + + store->close_storage(store); + free(store); + } + } + + SECTION("file_tile_storage_id function") + { + struct storage_backend *store = init_storage_file(tmp_dir.c_str()); + char uri_buf[PATH_MAX] = {0}; + + char *res = store->tile_storage_id(store, XMLCONFIG_DEFAULT, "", 0, 0, 0, uri_buf); + + REQUIRE(res == uri_buf); + REQUIRE_THAT(std::string(uri_buf), Catch::Matchers::StartsWith("file://")); + REQUIRE_THAT(std::string(uri_buf), Catch::Matchers::Contains(tmp_dir)); + + store->close_storage(store); + free(store); + } + + SECTION("file_metatile_write function") + { + struct storage_backend *store = init_storage_file(tmp_dir.c_str()); + const char test_payload[] = "metatile_payload_data"; + int payload_len = sizeof(test_payload); + + SECTION("write handles malloc failure", "should return -1") + { + fail_next_malloc = true; + + int res = store->metatile_write(store, XMLCONFIG_DEFAULT, "", 0, 0, 0, test_payload, payload_len); + + REQUIRE(res == -1); + REQUIRE_THAT(err_log_lines, Catch::Matchers::Contains("failed to allocate memory for temp path")); + } + + SECTION("write handles open failure", "should return -1") + { + fail_next_open = true; + + int res = store->metatile_write(store, XMLCONFIG_DEFAULT, "", 0, 0, 0, test_payload, payload_len); + + REQUIRE(res == -1); + REQUIRE_THAT(err_log_lines, Catch::Matchers::Contains("Error creating file")); + } + + SECTION("write handles write failure", "should return -1") + { + fail_next_write = true; + + int res = store->metatile_write(store, XMLCONFIG_DEFAULT, "", 0, 0, 0, test_payload, payload_len); + + REQUIRE(res == -1); + REQUIRE_THAT(err_log_lines, Catch::Matchers::Contains("Error writing file")); + } + + SECTION("write handles rename failure", "should return -1") + { + char meta_path[PATH_MAX]; + xyzo_to_meta(meta_path, sizeof(meta_path), (char *)store->storage_ctx, XMLCONFIG_DEFAULT, "", 0, 0, 0); + + mkdirp(meta_path); + mkdir(meta_path, 0755); + + int res = store->metatile_write(store, XMLCONFIG_DEFAULT, "", 0, 0, 0, test_payload, payload_len); + + REQUIRE(res == -1); + REQUIRE_THAT(err_log_lines, Catch::Matchers::Contains("Error renaming temp file")); + + rmdir(meta_path); + } + + SECTION("write successfully creates and renames metatile", "should return written size") + { + int res = store->metatile_write(store, XMLCONFIG_DEFAULT, "", 0, 0, 0, test_payload, payload_len); + + REQUIRE(res == payload_len); + + char meta_path[PATH_MAX]; + xyzo_to_meta(meta_path, sizeof(meta_path), (char *)store->storage_ctx, XMLCONFIG_DEFAULT, "", 0, 0, 0); + + struct stat st; + REQUIRE(stat(meta_path, &st) == 0); + REQUIRE(st.st_size == payload_len); + } + + store->close_storage(store); + free(store); + } + + SECTION("file_tile_read function") + { + struct storage_backend *store = init_storage_file(tmp_dir.c_str()); + char log_msg[PATH_MAX] = {0}; + char read_buf[256] = {0}; + int compressed = -1; + + char meta_path[PATH_MAX]; + xyzo_to_meta(meta_path, sizeof(meta_path), (char *)store->storage_ctx, XMLCONFIG_DEFAULT, "", 0, 0, 0); + + SECTION("read handles header malloc failure", "should return -1") + { + fail_next_malloc = true; + + int res = store->tile_read(store, XMLCONFIG_DEFAULT, "", 0, 0, 0, read_buf, sizeof(read_buf), &compressed, log_msg); + + REQUIRE(res == -1); + REQUIRE_THAT(std::string(log_msg), Catch::Matchers::Contains("Failed to allocate memory for metatile header")); + } + + SECTION("read handles non-existent file open error", "should return -1") + { + unlink(meta_path); + + int res = store->tile_read(store, XMLCONFIG_DEFAULT, "", 0, 0, 0, read_buf, sizeof(read_buf), &compressed, log_msg); + + REQUIRE(res == -1); + REQUIRE_THAT(std::string(log_msg), Catch::Matchers::Contains("Could not open metatile")); + } + + SECTION("read handles header too small", "should return -3") + { + mkdirp(meta_path); + int fd = open(meta_path, O_WRONLY | O_CREAT | O_TRUNC, 0666); + write(fd, "short", 5); + close(fd); + + int res = store->tile_read(store, XMLCONFIG_DEFAULT, "", 0, 0, 0, read_buf, sizeof(read_buf), &compressed, log_msg); + + REQUIRE(res == -3); + REQUIRE_THAT(std::string(log_msg), Catch::Matchers::Contains("too small to contain header")); + } + + SECTION("read handles magic mismatch", "should return -4") + { + size_t header_len = sizeof(struct meta_layout) + METATILE * METATILE * sizeof(struct entry); + char *bad_header = (char *)calloc(1, header_len); + memcpy(bad_header, "BADMAG", 6); + + mkdirp(meta_path); + int fd = open(meta_path, O_WRONLY | O_CREAT | O_TRUNC, 0666); + write(fd, bad_header, header_len); + close(fd); + free(bad_header); + + int res = store->tile_read(store, XMLCONFIG_DEFAULT, "", 0, 0, 0, read_buf, sizeof(read_buf), &compressed, log_msg); + + REQUIRE(res == -4); + REQUIRE_THAT(std::string(log_msg), Catch::Matchers::Contains("header magic mismatch")); + } + + SECTION("read handles bad count mismatch", "should return -5") + { + create_mock_metatile(meta_path, false, 10, 0, NULL); + + int res = store->tile_read(store, XMLCONFIG_DEFAULT, "", 0, 0, 0, read_buf, sizeof(read_buf), &compressed, log_msg); + + REQUIRE(res == -5); + REQUIRE_THAT(std::string(log_msg), Catch::Matchers::Contains("header bad count")); + } + + SECTION("read handles tile_size larger than buffer", "should return -6") + { + create_mock_metatile(meta_path, false, METATILE * METATILE, 500, "data"); + + int res = store->tile_read(store, XMLCONFIG_DEFAULT, "", 0, 0, 0, read_buf, 10, &compressed, log_msg); + + REQUIRE(res == -6); + REQUIRE_THAT(std::string(log_msg), Catch::Matchers::Contains("Truncating tile")); + } + + SECTION("read handles seek error on corrupted offset", "should return -7") + { + size_t header_len = sizeof(struct meta_layout) + METATILE * METATILE * sizeof(struct entry); + struct meta_layout *m = (struct meta_layout *)calloc(1, header_len); + memcpy(m->magic, META_MAGIC, strlen(META_MAGIC)); + m->count = METATILE * METATILE; + m->index[0].offset = -1; + m->index[0].size = 10; + + mkdirp(meta_path); + int fd = open(meta_path, O_WRONLY | O_CREAT | O_TRUNC, 0666); + write(fd, m, header_len); + close(fd); + free(m); + + int res = store->tile_read(store, XMLCONFIG_DEFAULT, "", 0, 0, 0, read_buf, sizeof(read_buf), &compressed, log_msg); + + REQUIRE(res == -7); + REQUIRE_THAT(std::string(log_msg), Catch::Matchers::Contains("seek error")); + } + + SECTION("read handles premature EOF when reading tile data", "should return partial size") + { + size_t header_len = sizeof(struct meta_layout) + METATILE * METATILE * sizeof(struct entry); + struct meta_layout *m = (struct meta_layout *)calloc(1, header_len); + memcpy(m->magic, META_MAGIC, strlen(META_MAGIC)); + m->count = METATILE * METATILE; + m->index[0].offset = header_len; + m->index[0].size = 100; + + mkdirp(meta_path); + int fd = open(meta_path, O_WRONLY | O_CREAT | O_TRUNC, 0666); + write(fd, m, header_len); + + const char short_data[] = "shortdata"; + write(fd, short_data, 9); + close(fd); + free(m); + + int res = store->tile_read(store, XMLCONFIG_DEFAULT, "", 0, 0, 0, read_buf, sizeof(read_buf), &compressed, log_msg); + + REQUIRE(res == 9); + } + + SECTION("read reads uncompressed tile successfully", "should return read length") + { + const char tile_content[] = "png_tile_binary_stream"; + create_mock_metatile(meta_path, false, METATILE * METATILE, strlen(tile_content), tile_content); + + int res = store->tile_read(store, XMLCONFIG_DEFAULT, "", 0, 0, 0, read_buf, sizeof(read_buf), &compressed, log_msg); + + REQUIRE(res == static_cast(strlen(tile_content))); + REQUIRE(compressed == 0); + REQUIRE(std::string(read_buf, res) == tile_content); + } + + SECTION("read reads compressed tile successfully", "should set compressed = 1") + { + const char tile_content[] = "compressed_stream"; + create_mock_metatile(meta_path, true, METATILE * METATILE, strlen(tile_content), tile_content); + + int res = store->tile_read(store, XMLCONFIG_DEFAULT, "", 0, 0, 0, read_buf, sizeof(read_buf), &compressed, log_msg); + + REQUIRE(res == static_cast(strlen(tile_content))); + REQUIRE(compressed == 1); + } + + SECTION("expire handles gmtime_r failure", "should fallback to default epoch time") + { + create_mock_metatile(meta_path, false, METATILE * METATILE, 10, "data"); + fail_next_gmtime_r = true; + + int res = store->metatile_expire(store, XMLCONFIG_DEFAULT, 0, 0, 0); + + REQUIRE(res == 0); + } + + SECTION("expire on tile with mtime prior to 2015", "should execute tm_year <= 115 branch") + { + create_mock_metatile(meta_path, false, METATILE * METATILE, 10, "data"); + + struct utimbuf old_time; + old_time.actime = 1262304000; + old_time.modtime = 1262304000; + utime(meta_path, &old_time); + + int res = store->metatile_expire(store, XMLCONFIG_DEFAULT, 0, 0, 0); + + REQUIRE(res == 0); + } + + SECTION("expire handles utime error", "should return 0 despite perror logging") + { + create_mock_metatile(meta_path, false, METATILE * METATILE, 10, "data"); + fail_next_utime = true; + + int res = store->metatile_expire(store, XMLCONFIG_DEFAULT, 0, 0, 0); + + REQUIRE(res == 0); + } + + SECTION("read handles read error during header loop", "should return -2") + { + create_mock_metatile(meta_path, false, METATILE * METATILE, 10, "data"); + + reset_read_call_count(); + fail_read_on_call_count = 1; + + int res = store->tile_read(store, XMLCONFIG_DEFAULT, "", 0, 0, 0, read_buf, sizeof(read_buf), &compressed, log_msg); + + REQUIRE(res == -2); + REQUIRE_THAT(std::string(log_msg), Catch::Matchers::Contains("Failed to read complete header")); + } + + SECTION("read handles read error during payload loop", "should return -8") + { + create_mock_metatile(meta_path, false, METATILE * METATILE, 10, "data"); + + reset_read_call_count(); + fail_read_on_call_count = 2; + + int res = store->tile_read(store, XMLCONFIG_DEFAULT, "", 0, 0, 0, read_buf, sizeof(read_buf), &compressed, log_msg); + + REQUIRE(res == -8); + REQUIRE_THAT(std::string(log_msg), Catch::Matchers::Contains("Failed to read data from file")); + } + + store->close_storage(store); + free(store); + } + + SECTION("file_tile_stat function") + { + struct storage_backend *store = init_storage_file(tmp_dir.c_str()); + char meta_path[PATH_MAX]; + xyzo_to_meta(meta_path, sizeof(meta_path), (char *)store->storage_ctx, XMLCONFIG_DEFAULT, "", 0, 0, 0); + + char planet_file[PATH_MAX]; + snprintf(planet_file, sizeof(planet_file), "%s/%s%s", tmp_dir.c_str(), XMLCONFIG_DEFAULT, PLANET_TIMESTAMP); + + SECTION("stat non-existent file", "should return size -1") + { + unlink(meta_path); + + struct stat_info info = store->tile_stat(store, XMLCONFIG_DEFAULT, "", 0, 0, 0); + + REQUIRE(info.size == -1); + REQUIRE(info.mtime == 0); + REQUIRE(info.expired == 1); + } + + SECTION("stat file expired relative to planet file", "should mark expired = 1") + { + create_mock_metatile(meta_path, false, METATILE * METATILE, 10, "data"); + + int fd = open(planet_file, O_WRONLY | O_CREAT | O_TRUNC, 0666); + close(fd); + + struct utimbuf times; + times.actime = time(NULL) + 1000; + times.modtime = time(NULL) + 1000; + utime(planet_file, ×); + + struct stat_info info = store->tile_stat(store, XMLCONFIG_DEFAULT, "", 0, 0, 0); + + REQUIRE(info.size > 0); + REQUIRE(info.expired == 1); + + unlink(planet_file); + } + + SECTION("stat file newer than planet file", "should mark expired = 0") + { + int fd = open(planet_file, O_WRONLY | O_CREAT | O_TRUNC, 0666); + close(fd); + + struct utimbuf planet_times; + planet_times.actime = time(NULL) - 1000; + planet_times.modtime = time(NULL) - 1000; + utime(planet_file, &planet_times); + + create_mock_metatile(meta_path, false, METATILE * METATILE, 10, "data"); + + struct stat_info info = store->tile_stat(store, XMLCONFIG_DEFAULT, "", 0, 0, 0); + + REQUIRE(info.size > 0); + REQUIRE(info.expired == 0); + + unlink(planet_file); + } + + SECTION("stat fallback to global planet timestamp and missing planet fallback", "should execute planet time branches") + { + char global_planet[PATH_MAX]; + snprintf(global_planet, sizeof(global_planet), "%s/%s", tmp_dir.c_str(), PLANET_TIMESTAMP); + + int fd = open(global_planet, O_WRONLY | O_CREAT | O_TRUNC, 0666); + close(fd); + + create_mock_metatile(meta_path, false, METATILE * METATILE, 10, "data"); + + struct stat_info info = store->tile_stat(store, XMLCONFIG_DEFAULT, "", 0, 0, 0); + REQUIRE(info.size > 0); + + unlink(global_planet); + + info = store->tile_stat(store, XMLCONFIG_DEFAULT, "", 0, 0, 0); + REQUIRE(info.size > 0); + } + + store->close_storage(store); + free(store); + } + + SECTION("file_metatile_delete function") + { + struct storage_backend *store = init_storage_file(tmp_dir.c_str()); + char meta_path[PATH_MAX]; + xyz_to_meta(meta_path, sizeof(meta_path), (char *)store->storage_ctx, XMLCONFIG_DEFAULT, 0, 0, 0); + + create_mock_metatile(meta_path, false, METATILE * METATILE, 10, "data"); + + int res = store->metatile_delete(store, XMLCONFIG_DEFAULT, 0, 0, 0); + + REQUIRE(res == 0); + + struct stat st; + REQUIRE(stat(meta_path, &st) != 0); + + store->close_storage(store); + free(store); + } + + SECTION("file_metatile_expire function") + { + struct storage_backend *store = init_storage_file(tmp_dir.c_str()); + char meta_path[PATH_MAX]; + xyz_to_meta(meta_path, sizeof(meta_path), (char *)store->storage_ctx, XMLCONFIG_DEFAULT, 0, 0, 0); + + SECTION("expire on missing file", "should return 0") + { + unlink(meta_path); + + int res = store->metatile_expire(store, XMLCONFIG_DEFAULT, 0, 0, 0); + + REQUIRE(res == 0); + } + + SECTION("expire on existing file sets timestamp back 20 years", "should adjust modtime") + { + create_mock_metatile(meta_path, false, METATILE * METATILE, 10, "data"); + + struct stat st_before; + REQUIRE(stat(meta_path, &st_before) == 0); + + int res = store->metatile_expire(store, XMLCONFIG_DEFAULT, 0, 0, 0); + + REQUIRE(res == 0); + + struct stat st_after; + REQUIRE(stat(meta_path, &st_after) == 0); + + REQUIRE(st_after.st_mtime < st_before.st_mtime); + } + + SECTION("expire on already expired tile", "should execute tm_year <= 115 branch") + { + create_mock_metatile(meta_path, false, METATILE * METATILE, 10, "data"); + + store->metatile_expire(store, XMLCONFIG_DEFAULT, 0, 0, 0); + int res = store->metatile_expire(store, XMLCONFIG_DEFAULT, 0, 0, 0); + + REQUIRE(res == 0); + } + + store->close_storage(store); + free(store); + } + + SECTION("file_close_storage function") + { + struct storage_backend *store = init_storage_file(tmp_dir.c_str()); + + int res = store->close_storage(store); + + REQUIRE(res == 0); + REQUIRE(store->storage_ctx == NULL); + + free(store); + } + + delete_tile_dir(tmp_dir); +} diff --git a/tests/unit_test_store_file_utils.cpp b/tests/unit_test_store_file_utils.cpp new file mode 100644 index 000000000..c0b7780ec --- /dev/null +++ b/tests/unit_test_store_file_utils.cpp @@ -0,0 +1,213 @@ +/* + * Copyright (c) 2007 - 2026 by mod_tile contributors (see AUTHORS file) + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; If not, see http://www.gnu.org/licenses/. + */ + +#include +#include +#include + +#include "catch/catch.hpp" +#include "catch_test_common.hpp" + +#include "render_config.h" +#include "store_file_utils.h" + +extern std::string err_log_lines; + +TEST_CASE("store_file_utils.c", "[store_file_utils]") +{ + SECTION("mkdirp function") + { + err_log_lines.clear(); + std::string base_dir = create_tile_dir("mkdirp_test"); + + SECTION("mkdirp with path containing no slash", "should return 0") + { + int res = mkdirp("noslashpath"); + REQUIRE(res == 0); + } + + SECTION("mkdirp with empty or root path", "should return 0") + { + int res = mkdirp("/"); + REQUIRE(res == 0); + } + + SECTION("mkdirp when target parent directory already exists", "should return 0") + { + std::string existing_dir = base_dir + "/sub1"; + mkdir(existing_dir.c_str(), 0755); + + std::string target_file = existing_dir + "/file.txt"; + int res = mkdirp(target_file.c_str()); + + REQUIRE(res == 0); + } + + SECTION("mkdirp when existing parent path component is a regular file instead of a directory", "should return 1") + { + std::string file_path = base_dir + "/regular_file"; + int fd = open(file_path.c_str(), O_WRONLY | O_CREAT | O_TRUNC, 0666); + REQUIRE(fd >= 0); + close(fd); + + std::string blocked_path = file_path + "/sub/file.png"; + int res = mkdirp(blocked_path.c_str()); + + REQUIRE(res == 1); + } + + SECTION("mkdirp recursively creates multi-level directory tree", "should return 0") + { + std::string deep_path = base_dir + "/level1/level2/level3/tile.meta"; + int res = mkdirp(deep_path.c_str()); + + REQUIRE(res == 0); + + struct stat st; + std::string expected_dir = base_dir + "/level1/level2/level3"; + REQUIRE(stat(expected_dir.c_str(), &st) == 0); + REQUIRE(S_ISDIR(st.st_mode)); + } + + delete_tile_dir(base_dir); + } + + SECTION("path_to_xyz function") + { + err_log_lines.clear(); + char xmlconfig[PATH_MAX] = {0}; + int x = -1, y = -1, z = -1; + +#ifdef DIRECTORY_HASH + SECTION("path_to_xyz handles prefix mismatch between tilepath and path", "should return 1") + { + const char *tilepath = "/var/lib/mod_tile"; + const char *path = "/var/lib/different_path/default/0/0/0/0/0/0.meta"; + + int res = path_to_xyz(tilepath, path, xmlconfig, &x, &y, &z); + + REQUIRE(res == 1); + REQUIRE_THAT(err_log_lines, Catch::Matchers::Contains("Tile path does not match settings")); + } + + SECTION("path_to_xyz handles malformed tile path structure", "should return 1") + { + const char *tilepath = "/var/lib/mod_tile"; + const char *path = "/var/lib/mod_tile/default/invalid_format"; + + int res = path_to_xyz(tilepath, path, xmlconfig, &x, &y, &z); + + REQUIRE(res == 1); + REQUIRE_THAT(err_log_lines, Catch::Matchers::Contains("Failed to parse tile path")); + } + + SECTION("path_to_xyz handles invalid hash component values (> 255 or < 0)", "should return 2") + { + const char *tilepath = "/var/lib/mod_tile"; + + const char *path = "/var/lib/mod_tile/default/5/300/0/0/0/0.meta"; + + int res = path_to_xyz(tilepath, path, xmlconfig, &x, &y, &z); + + REQUIRE(res == 2); + REQUIRE_THAT(err_log_lines, Catch::Matchers::Contains("Failed to parse tile path (invalid 300)")); + } + + SECTION("path_to_xyz detects out-of-bounds zoom or tile coordinates", "should return 1") + { + const char *tilepath = "/var/lib/mod_tile"; + + const char *path = "/var/lib/mod_tile/default/30/0/0/0/0/0.meta"; + + int res = path_to_xyz(tilepath, path, xmlconfig, &x, &y, &z); + + REQUIRE(res == 1); + REQUIRE_THAT(err_log_lines, Catch::Matchers::Contains("got bad co-ords")); + } + + SECTION("path_to_xyz successfully parses valid hashed metatile path", "should set x, y, z and return 0") + { + const char *tilepath = "/var/lib/mod_tile"; + + char expected_meta[PATH_MAX]; + xyzo_to_meta(expected_meta, sizeof(expected_meta), tilepath, XMLCONFIG_DEFAULT, "", 0, 0, 0); + + int res = path_to_xyz(tilepath, expected_meta, xmlconfig, &x, &y, &z); + + REQUIRE(res == 0); + REQUIRE(std::string(xmlconfig) == XMLCONFIG_DEFAULT); + REQUIRE(z == 0); + REQUIRE(x == 0); + REQUIRE(y == 0); + } +#else + SECTION("path_to_xyz handles non-hashed malformed path", "should return 1") + { + const char *path = "bad_non_hashed_path"; + + int res = path_to_xyz("/var/lib/mod_tile", path, xmlconfig, &x, &y, &z); + + REQUIRE(res == 1); + REQUIRE_THAT(err_log_lines, Catch::Matchers::Contains("Failed to parse tile path")); + } +#endif + } + + SECTION("xyzo_to_meta function") + { + char path_buf[PATH_MAX] = {0}; + + SECTION("xyzo_to_meta without options suffix", "should format base .meta path") + { + int offset = xyzo_to_meta(path_buf, sizeof(path_buf), "/var/lib/mod_tile", XMLCONFIG_DEFAULT, "", 0, 0, 0); + + REQUIRE(offset == 0); + REQUIRE_THAT(std::string(path_buf), Catch::Matchers::StartsWith("/var/lib/mod_tile/default/0/")); + REQUIRE_THAT(std::string(path_buf), Catch::Matchers::EndsWith(".meta")); + REQUIRE(!Catch::Matchers::Contains(".myoption.meta").match(path_buf)); + } + + SECTION("xyzo_to_meta with options suffix", "should include options before .meta extension") + { + int offset = xyzo_to_meta(path_buf, sizeof(path_buf), "/var/lib/mod_tile", XMLCONFIG_DEFAULT, "dirty", 0, 0, 0); + + REQUIRE(offset == 0); + REQUIRE_THAT(std::string(path_buf), Catch::Matchers::EndsWith(".dirty.meta")); + } + + SECTION("xyzo_to_meta calculates correct internal sub-tile offset for metatiles", "should compute 0..63 offset") + { + int offset = xyzo_to_meta(path_buf, sizeof(path_buf), "/var/lib/mod_tile", XMLCONFIG_DEFAULT, "", 1, 2, 10); + + REQUIRE(offset == 10); + } + } + + SECTION("xyz_to_meta function") + { + char path_buf[PATH_MAX] = {0}; + + SECTION("xyz_to_meta delegates to xyzo_to_meta with empty options", "should return offset and path") + { + int offset = xyz_to_meta(path_buf, sizeof(path_buf), "/var/lib/mod_tile", XMLCONFIG_DEFAULT, 0, 0, 0); + + REQUIRE(offset == 0); + REQUIRE_THAT(std::string(path_buf), Catch::Matchers::StartsWith("/var/lib/mod_tile/default/")); + REQUIRE_THAT(std::string(path_buf), Catch::Matchers::EndsWith(".meta")); + } + } +} diff --git a/tests/unit_test_store_memcached.cpp b/tests/unit_test_store_memcached.cpp new file mode 100644 index 000000000..160013f2a --- /dev/null +++ b/tests/unit_test_store_memcached.cpp @@ -0,0 +1,259 @@ +/* + * Copyright (c) 2007 - 2026 by mod_tile contributors (see AUTHORS file) + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; If not, see http://www.gnu.org/licenses/. + */ + +#include +#include +#include +#include +#include +#include + +#include "catch/catch.hpp" +#include "catch_test_common.hpp" + +#include "metatile.h" +#include "protocol.h" +#include "render_config.h" +#include "store.h" +#include "store_memcached.h" + +TEST_CASE("store_memcached.c", "[store_memcached]") +{ + std::string valid_conn = "memcached://127.0.0.1:59999"; + + SECTION("init_storage_memcached") + { + err_log_lines.clear(); + reset_malloc_test_count(); + +#ifndef HAVE_LIBMEMCACHED + SECTION("returns NULL when HAVE_LIBMEMCACHED is disabled") + { + struct storage_backend *store = init_storage_memcached(valid_conn.c_str()); + REQUIRE(store == NULL); + REQUIRE_THAT(err_log_lines, Catch::Matchers::Contains("Support for memcached storage has not been compiled into this program")); + } +#else + SECTION("handles allocation failure for store backend struct") + { + fail_next_malloc = true; + struct storage_backend *store = init_storage_memcached(valid_conn.c_str()); + REQUIRE(store == NULL); + REQUIRE_THAT(err_log_lines, Catch::Matchers::Contains("Failed to allocate memory for storage backend")); + reset_malloc_test_count(); + } + + SECTION("initializes valid memcached storage backend with default connection string") + { + reset_malloc_test_count(); + struct storage_backend *store = init_storage_memcached("memcached://"); + REQUIRE(store != NULL); + REQUIRE(store->storage_ctx != NULL); + + REQUIRE(store->tile_read != NULL); + REQUIRE(store->tile_stat != NULL); + REQUIRE(store->metatile_write != NULL); + REQUIRE(store->metatile_delete != NULL); + REQUIRE(store->metatile_expire != NULL); + REQUIRE(store->tile_storage_id != NULL); + REQUIRE(store->close_storage != NULL); + + store->close_storage(store); + free(store); + } + + SECTION("initializes valid memcached storage backend with custom server string") + { + reset_malloc_test_count(); + struct storage_backend *store = init_storage_memcached(valid_conn.c_str()); + REQUIRE(store != NULL); + REQUIRE(store->storage_ctx != NULL); + + store->close_storage(store); + free(store); + } +#endif + } + +#ifdef HAVE_LIBMEMCACHED + SECTION("memcached_xyzo_to_storagekey") + { + SECTION("formats key without options") + { + char key_buf[PATH_MAX] = {0}; + char *res = memcached_xyzo_to_storagekey(XMLCONFIG_DEFAULT, "", 10, 20, 5, key_buf); + REQUIRE(res == key_buf); + REQUIRE(std::string(key_buf) == std::string(XMLCONFIG_DEFAULT) + "/8/16/5.meta"); + } + + SECTION("formats key with options") + { + char key_buf[PATH_MAX] = {0}; + char *res = memcached_xyzo_to_storagekey(XMLCONFIG_DEFAULT, "options_str", 10, 20, 5, key_buf); + REQUIRE(res == key_buf); + REQUIRE(std::string(key_buf) == std::string(XMLCONFIG_DEFAULT) + "/8/16/5.options_str.meta"); + } + } + + SECTION("memcached_xyz_to_storagekey") + { + SECTION("delegates to memcached_xyzo_to_storagekey with empty options") + { + char key_buf[PATH_MAX] = {0}; + char *res = memcached_xyz_to_storagekey(XMLCONFIG_DEFAULT, 8, 16, 5, key_buf); + REQUIRE(res == key_buf); + REQUIRE(std::string(key_buf) == std::string(XMLCONFIG_DEFAULT) + "/8/16/5.meta"); + } + } + + SECTION("memcached_tile_read") + { + reset_malloc_test_count(); + struct storage_backend *store = init_storage_memcached(valid_conn.c_str()); + REQUIRE(store != NULL); + + char read_buf[1024 * 64] = {0}; + char log_msg[1024] = {0}; + int compressed = 0; + + SECTION("fails when header memory allocation fails (line 80)") + { + fail_next_malloc = true; + int res = store->tile_read(store, XMLCONFIG_DEFAULT, "", 0, 0, 0, read_buf, sizeof(read_buf), &compressed, log_msg); + REQUIRE(res == -1); + REQUIRE_THAT(std::string(log_msg), Catch::Matchers::Contains("Failed to allocate memory for metatile header")); + reset_malloc_test_count(); + } + + SECTION("fails when memcached lookup returns non-success") + { + int res = store->tile_read(store, XMLCONFIG_DEFAULT, "", 0, 0, 0, read_buf, sizeof(read_buf), &compressed, log_msg); + REQUIRE(res == -1); + } + + store->close_storage(store); + free(store); + } + + SECTION("memcached_tile_stat") + { + reset_malloc_test_count(); + struct storage_backend *store = init_storage_memcached(valid_conn.c_str()); + REQUIRE(store != NULL); + + SECTION("returns empty stat info on memcached fetch error") + { + struct stat_info info = store->tile_stat(store, XMLCONFIG_DEFAULT, "", 0, 0, 0); + REQUIRE(info.size == -1); + REQUIRE(info.expired == 0); + REQUIRE(info.mtime == 0); + REQUIRE(info.atime == 0); + REQUIRE(info.ctime == 0); + } + + store->close_storage(store); + free(store); + } + + SECTION("memcached_tile_storage_id") + { + reset_malloc_test_count(); + struct storage_backend *store = init_storage_memcached(valid_conn.c_str()); + REQUIRE(store != NULL); + + SECTION("formats expected memcached storage URI") + { + char uri_buf[PATH_MAX] = {0}; + char *id = store->tile_storage_id(store, XMLCONFIG_DEFAULT, "", 8, 16, 5, uri_buf); + REQUIRE(std::string(id) == "memcached:///" + std::string(XMLCONFIG_DEFAULT) + "/8/16/5.meta"); + } + + store->close_storage(store); + free(store); + } + + SECTION("memcached_metatile_write") + { + reset_malloc_test_count(); + struct storage_backend *store = init_storage_memcached(valid_conn.c_str()); + REQUIRE(store != NULL); + + SECTION("handles payload allocation failure (line 173)") + { + fail_next_malloc = true; + int res = store->metatile_write(store, XMLCONFIG_DEFAULT, "", 0, 0, 0, "mock_data", 9); + REQUIRE(res == -2); + reset_malloc_test_count(); + } + + SECTION("returns error -1 on unconnectable memcached server") + { + int res = store->metatile_write(store, XMLCONFIG_DEFAULT, "", 0, 0, 0, "mock_data", 9); + REQUIRE(res == -1); + } + + store->close_storage(store); + free(store); + } + + SECTION("memcached_metatile_delete") + { + reset_malloc_test_count(); + struct storage_backend *store = init_storage_memcached(valid_conn.c_str()); + REQUIRE(store != NULL); + + SECTION("returns error -1 when key is not found or connection fails") + { + int res = store->metatile_delete(store, XMLCONFIG_DEFAULT, 0, 0, 0); + REQUIRE(res == -1); + } + + store->close_storage(store); + free(store); + } + + SECTION("memcached_metatile_expire") + { + reset_malloc_test_count(); + struct storage_backend *store = init_storage_memcached(valid_conn.c_str()); + REQUIRE(store != NULL); + + SECTION("returns error -1 on fetch failure") + { + int res = store->metatile_expire(store, XMLCONFIG_DEFAULT, 0, 0, 0); + REQUIRE(res == -1); + } + + store->close_storage(store); + free(store); + } + + SECTION("memcached_close_storage") + { + reset_malloc_test_count(); + struct storage_backend *store = init_storage_memcached(valid_conn.c_str()); + REQUIRE(store != NULL); + + SECTION("cleans up storage context cleanly") + { + REQUIRE(store->close_storage(store) == 0); + REQUIRE(store->storage_ctx == NULL); + free(store); + } + } +#endif +} diff --git a/tests/unit_test_store_null.cpp b/tests/unit_test_store_null.cpp new file mode 100644 index 000000000..b7593fdd4 --- /dev/null +++ b/tests/unit_test_store_null.cpp @@ -0,0 +1,121 @@ +/* + * Copyright (c) 2007 - 2026 by mod_tile contributors (see AUTHORS file) + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; If not, see http://www.gnu.org/licenses/. + */ + +#include + +#include "catch/catch.hpp" +#include "catch_test_common.hpp" + +#include "store.h" +#include "store_null.h" + +TEST_CASE("store_null.c", "[store_null]") +{ + SECTION("init_storage_null function") + { + err_log_lines.clear(); + + SECTION("init_storage_null handles malloc failure", "should return NULL") + { + fail_next_malloc = true; + + struct storage_backend *store = init_storage_null(); + + REQUIRE(store == NULL); + REQUIRE_THAT(err_log_lines, Catch::Matchers::Contains("init_storage_null: Failed to allocate memory for storage backend")); + } + + SECTION("init_storage_null creates valid storage interface", "should return initialized backend struct") + { + struct storage_backend *store = init_storage_null(); + + REQUIRE(store != NULL); + REQUIRE(store->storage_ctx == NULL); + REQUIRE(store->tile_read != NULL); + REQUIRE(store->tile_stat != NULL); + REQUIRE(store->metatile_write != NULL); + REQUIRE(store->metatile_delete != NULL); + REQUIRE(store->metatile_expire != NULL); + REQUIRE(store->tile_storage_id != NULL); + REQUIRE(store->close_storage != NULL); + + SECTION("tile_read method", "should set err_msg and return -1") + { + char err_msg[PATH_MAX] = {0}; + char buf[256] = {0}; + int compressed = 0; + + int res = store->tile_read(store, XMLCONFIG_DEFAULT, "", 0, 0, 0, buf, sizeof(buf), &compressed, err_msg); + + REQUIRE(res == -1); + REQUIRE(std::string(err_msg) == "Cannot read from NULL storage."); + } + + SECTION("tile_stat method", "should return expired stat with size -1") + { + struct stat_info info = store->tile_stat(store, XMLCONFIG_DEFAULT, "", 0, 0, 0); + + REQUIRE(info.size == -1); + REQUIRE(info.atime == 0); + REQUIRE(info.mtime == 0); + REQUIRE(info.ctime == 0); + REQUIRE(info.expired == 1); + } + + SECTION("metatile_write method", "should return buffer size as written bytes") + { + const char data[] = "test_tile_data"; + int res = store->metatile_write(store, XMLCONFIG_DEFAULT, "", 0, 0, 0, data, sizeof(data)); + + REQUIRE(res == static_cast(sizeof(data))); + } + + SECTION("metatile_delete method", "should return 0") + { + int res = store->metatile_delete(store, XMLCONFIG_DEFAULT, 0, 0, 0); + + REQUIRE(res == 0); + } + + SECTION("metatile_expire method", "should return 0") + { + int res = store->metatile_expire(store, XMLCONFIG_DEFAULT, 0, 0, 0); + + REQUIRE(res == 0); + } + + SECTION("tile_storage_id method", "should populate string with null:// URI scheme") + { + char uri_buf[PATH_MAX] = {0}; + + char *res = store->tile_storage_id(store, XMLCONFIG_DEFAULT, "", 0, 0, 0, uri_buf); + + REQUIRE(res == uri_buf); + REQUIRE(std::string(uri_buf) == "null://"); + } + + SECTION("close_storage method", "should return 0") + { + int res = store->close_storage(store); + + REQUIRE(res == 0); + } + + free(store); + } + } +} diff --git a/tests/unit_test_store_rados.cpp b/tests/unit_test_store_rados.cpp new file mode 100644 index 000000000..04707aa65 --- /dev/null +++ b/tests/unit_test_store_rados.cpp @@ -0,0 +1,422 @@ +/* + * Copyright (c) 2007 - 2026 by mod_tile contributors (see AUTHORS file) + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; If not, see http://www.gnu.org/licenses/. + */ + +#include "catch/catch.hpp" +#include "catch_test_common.hpp" + +#include "store_rados.h" + +TEST_CASE("store_rados.c", "[store_rados]") +{ + std::string valid_conn = "rados://tiles/etc/ceph/ceph.conf"; + + SECTION("init_storage_rados") + { + err_log_lines.clear(); + reset_malloc_test_count(); + +#ifndef HAVE_LIBRADOS + SECTION("returns NULL when HAVE_LIBRADOS is disabled") + { + struct storage_backend *store = init_storage_rados(valid_conn.c_str()); + REQUIRE(store == NULL); + REQUIRE_THAT(err_log_lines, Catch::Matchers::Contains("Support for rados storage has not been compiled into this program")); + } +#else + SECTION("handles allocation failure for context or store struct (store=NULL, ctx=NULL)") + { + fail_next_malloc = true; + struct storage_backend *store = init_storage_rados(valid_conn.c_str()); + REQUIRE(store == NULL); + reset_malloc_test_count(); + } + + SECTION("handles allocation failure for context struct (store!=NULL, ctx=NULL)") + { + reset_malloc_test_count(); + fail_malloc_on_call_count = 2; + struct storage_backend *store = init_storage_rados(valid_conn.c_str()); + REQUIRE(store == NULL); + reset_malloc_test_count(); + } + + SECTION("fails gracefully when configuration file cannot be read") + { + err_log_lines.clear(); + struct storage_backend *store = init_storage_rados("rados://tiles/nonexistent/ceph.conf"); + REQUIRE(store == NULL); + REQUIRE_THAT(err_log_lines, Catch::Matchers::Contains("failed to read rados config file")); + } +#endif + } + +#ifdef HAVE_LIBRADOS + SECTION("rados_xyzo_to_storagekey") + { + SECTION("formats storage key without options") + { + char key_buf[PATH_MAX] = {0}; + char *res = rados_xyzo_to_storagekey(XMLCONFIG_DEFAULT, "", 10, 20, 5, key_buf); + REQUIRE(res == key_buf); + REQUIRE(std::string(key_buf) == std::string(XMLCONFIG_DEFAULT) + "/5/8/16.meta"); + } + + SECTION("formats storage key with options string") + { + char key_buf[PATH_MAX] = {0}; + char *res = rados_xyzo_to_storagekey(XMLCONFIG_DEFAULT, "options_str", 10, 20, 5, key_buf); + REQUIRE(res == key_buf); + REQUIRE(std::string(key_buf) == std::string(XMLCONFIG_DEFAULT) + "/5/8/16.options_str.meta"); + } + } + + SECTION("read_meta_data") + { + SECTION("returns cached data on consecutive coordinate hits") + { + struct rados_ctx ctx; + memset(&ctx, 0, sizeof(ctx)); + ctx.metadata_cache.x = 8; + ctx.metadata_cache.y = 16; + ctx.metadata_cache.z = 5; + snprintf(ctx.metadata_cache.xmlname, sizeof(ctx.metadata_cache.xmlname), "%s", XMLCONFIG_DEFAULT); + + char mock_buf[] = "MOCK_METADATA_HEADER"; + ctx.metadata_cache.data = mock_buf; + + struct storage_backend store; + memset(&store, 0, sizeof(store)); + store.storage_ctx = &ctx; + + err_log_lines.clear(); + char *res = read_meta_data(&store, XMLCONFIG_DEFAULT, "", 8, 16, 5); + REQUIRE(res == mock_buf); + REQUIRE_THAT(err_log_lines, Catch::Matchers::Contains("Returning cached data for")); + } + } + + SECTION("rados_tile_read") + { + char read_buf[1024 * 64] = {0}; + char log_msg[1024] = {0}; + int compressed = 0; + + SECTION("fails when header memory allocation fails") + { + struct rados_ctx ctx; + memset(&ctx, 0, sizeof(ctx)); + ctx.metadata_cache.x = 0; + ctx.metadata_cache.y = 0; + ctx.metadata_cache.z = 0; + snprintf(ctx.metadata_cache.xmlname, sizeof(ctx.metadata_cache.xmlname), "%s", XMLCONFIG_DEFAULT); + ctx.metadata_cache.data = (char *)"dummy_cache_payload"; + + struct storage_backend store; + memset(&store, 0, sizeof(store)); + store.storage_ctx = &ctx; + + reset_malloc_test_count(); + fail_next_malloc = true; + + int res = rados_tile_read(&store, XMLCONFIG_DEFAULT, "", 0, 0, 0, read_buf, sizeof(read_buf), &compressed, log_msg); + REQUIRE(res == -1); + REQUIRE_THAT(std::string(log_msg), Catch::Matchers::Contains("Failed to allocate memory for metatile header")); + reset_malloc_test_count(); + } + + SECTION("fails when metadata read returns NULL") + { + struct rados_ctx ctx; + memset(&ctx, 0, sizeof(ctx)); + ctx.metadata_cache.x = 0; + ctx.metadata_cache.y = 0; + ctx.metadata_cache.z = 0; + snprintf(ctx.metadata_cache.xmlname, sizeof(ctx.metadata_cache.xmlname), "%s", XMLCONFIG_DEFAULT); + ctx.metadata_cache.data = NULL; + + struct storage_backend store; + memset(&store, 0, sizeof(store)); + store.storage_ctx = &ctx; + + int res = rados_tile_read(&store, XMLCONFIG_DEFAULT, "", 0, 0, 0, read_buf, sizeof(read_buf), &compressed, log_msg); + REQUIRE(res == -3); + REQUIRE_THAT(std::string(log_msg), Catch::Matchers::Contains("Failed to read metadata of tile")); + } + + SECTION("detects magic mismatch on corrupted header") + { + size_t header_len = sizeof(struct stat_info) + sizeof(struct meta_layout) + METATILE * METATILE * sizeof(struct entry); + char *mock_meta = (char *)calloc(1, header_len); + + struct rados_ctx ctx; + memset(&ctx, 0, sizeof(ctx)); + ctx.metadata_cache.x = 0; + ctx.metadata_cache.y = 0; + ctx.metadata_cache.z = 0; + snprintf(ctx.metadata_cache.xmlname, sizeof(ctx.metadata_cache.xmlname), "%s", XMLCONFIG_DEFAULT); + ctx.metadata_cache.data = mock_meta; + + struct storage_backend store; + memset(&store, 0, sizeof(store)); + store.storage_ctx = &ctx; + + int res = rados_tile_read(&store, XMLCONFIG_DEFAULT, "", 0, 0, 0, read_buf, sizeof(read_buf), &compressed, log_msg); + REQUIRE(res == -4); + REQUIRE_THAT(std::string(log_msg), Catch::Matchers::Contains("Meta file header magic mismatch")); + + free(mock_meta); + } + + SECTION("validates compressed header magic and bad metatile tile count") + { + size_t header_len = sizeof(struct stat_info) + sizeof(struct meta_layout) + METATILE * METATILE * sizeof(struct entry); + char *mock_meta = (char *)calloc(1, header_len); + + struct meta_layout *m = (struct meta_layout *)(mock_meta + sizeof(struct stat_info)); + memcpy(m->magic, META_MAGIC_COMPRESSED, strlen(META_MAGIC_COMPRESSED)); + m->count = 10; + + struct rados_ctx ctx; + memset(&ctx, 0, sizeof(ctx)); + ctx.metadata_cache.x = 0; + ctx.metadata_cache.y = 0; + ctx.metadata_cache.z = 0; + snprintf(ctx.metadata_cache.xmlname, sizeof(ctx.metadata_cache.xmlname), "%s", XMLCONFIG_DEFAULT); + ctx.metadata_cache.data = mock_meta; + + struct storage_backend store; + memset(&store, 0, sizeof(store)); + store.storage_ctx = &ctx; + + int res = rados_tile_read(&store, XMLCONFIG_DEFAULT, "", 0, 0, 0, read_buf, sizeof(read_buf), &compressed, log_msg); + REQUIRE(res == -5); + REQUIRE(compressed == 1); + REQUIRE_THAT(std::string(log_msg), Catch::Matchers::Contains("Meta file header bad count")); + + free(mock_meta); + } + + SECTION("detects buffer overrun on oversized tile") + { + size_t header_len = sizeof(struct stat_info) + sizeof(struct meta_layout) + METATILE * METATILE * sizeof(struct entry); + char *mock_meta = (char *)calloc(1, header_len); + + struct meta_layout *m = (struct meta_layout *)(mock_meta + sizeof(struct stat_info)); + memcpy(m->magic, META_MAGIC, strlen(META_MAGIC)); + m->count = METATILE * METATILE; + m->index[0].offset = 0; + m->index[0].size = 2048; + + struct rados_ctx ctx; + memset(&ctx, 0, sizeof(ctx)); + ctx.metadata_cache.x = 0; + ctx.metadata_cache.y = 0; + ctx.metadata_cache.z = 0; + snprintf(ctx.metadata_cache.xmlname, sizeof(ctx.metadata_cache.xmlname), "%s", XMLCONFIG_DEFAULT); + ctx.metadata_cache.data = mock_meta; + + struct storage_backend store; + memset(&store, 0, sizeof(store)); + store.storage_ctx = &ctx; + + int res = rados_tile_read(&store, XMLCONFIG_DEFAULT, "", 0, 0, 0, read_buf, 100, &compressed, log_msg); + REQUIRE(res == -6); + REQUIRE(compressed == 0); + REQUIRE_THAT(std::string(log_msg), Catch::Matchers::Contains("Truncating tile")); + + free(mock_meta); + } + } + + SECTION("rados_tile_stat") + { + SECTION("returns empty stat structure when metadata read fails") + { + struct rados_ctx ctx; + memset(&ctx, 0, sizeof(ctx)); + ctx.metadata_cache.x = 0; + ctx.metadata_cache.y = 0; + ctx.metadata_cache.z = 0; + snprintf(ctx.metadata_cache.xmlname, sizeof(ctx.metadata_cache.xmlname), "%s", XMLCONFIG_DEFAULT); + ctx.metadata_cache.data = NULL; + + struct storage_backend store; + memset(&store, 0, sizeof(store)); + store.storage_ctx = &ctx; + + struct stat_info info = rados_tile_stat(&store, XMLCONFIG_DEFAULT, "", 0, 0, 0); + REQUIRE(info.size == -1); + REQUIRE(info.expired == 0); + REQUIRE(info.mtime == 0); + REQUIRE(info.atime == 0); + REQUIRE(info.ctime == 0); + } + + SECTION("parses stat payload and tile size from metadata cache") + { + size_t header_len = sizeof(struct stat_info) + sizeof(struct meta_layout) + METATILE * METATILE * sizeof(struct entry); + char *mock_meta = (char *)calloc(1, header_len); + + struct stat_info *st = (struct stat_info *)mock_meta; + st->size = 4096; + st->mtime = 12345678; + + struct meta_layout *m = (struct meta_layout *)(mock_meta + sizeof(struct stat_info)); + m->index[0].size = 512; + + struct rados_ctx ctx; + memset(&ctx, 0, sizeof(ctx)); + ctx.metadata_cache.x = 0; + ctx.metadata_cache.y = 0; + ctx.metadata_cache.z = 0; + snprintf(ctx.metadata_cache.xmlname, sizeof(ctx.metadata_cache.xmlname), "%s", XMLCONFIG_DEFAULT); + ctx.metadata_cache.data = mock_meta; + + struct storage_backend store; + memset(&store, 0, sizeof(store)); + store.storage_ctx = &ctx; + + struct stat_info info = rados_tile_stat(&store, XMLCONFIG_DEFAULT, "", 0, 0, 0); + REQUIRE(info.size == 512); + REQUIRE(info.mtime == 12345678); + + free(mock_meta); + } + } + + SECTION("rados_tile_storage_id") + { + SECTION("formats rados URI with pool name and path") + { + struct rados_ctx ctx; + memset(&ctx, 0, sizeof(ctx)); + ctx.pool = strdup("tiles"); + + struct storage_backend store; + memset(&store, 0, sizeof(store)); + store.storage_ctx = &ctx; + + char uri_buf[PATH_MAX] = {0}; + char *id = rados_tile_storage_id(&store, XMLCONFIG_DEFAULT, "", 8, 16, 5, uri_buf); + REQUIRE(std::string(id) == "rados://tiles/" + std::string(XMLCONFIG_DEFAULT) + "/5/8/16.meta"); + + free(ctx.pool); + } + } + + SECTION("rados_metatile_write") + { + SECTION("handles allocation failure for payload write buffer") + { + struct storage_backend store; + memset(&store, 0, sizeof(store)); + + reset_malloc_test_count(); + fail_next_malloc = true; + + err_log_lines.clear(); + int res = rados_metatile_write(&store, XMLCONFIG_DEFAULT, "", 0, 0, 0, "payload", 7); + REQUIRE(res == -1); + REQUIRE_THAT(err_log_lines, Catch::Matchers::Contains("failed to allocate memory for write buffer")); + reset_malloc_test_count(); + } + + SECTION("executes write buffer prep and logging on write error branch") + { + struct rados_ctx ctx; + memset(&ctx, 0, sizeof(ctx)); + ctx.pool = strdup("tiles"); + + struct storage_backend store; + memset(&store, 0, sizeof(store)); + store.storage_ctx = &ctx; + + err_log_lines.clear(); + reset_malloc_test_count(); + + int res = rados_metatile_write(&store, XMLCONFIG_DEFAULT, "", 0, 0, 0, "payload", 7); + REQUIRE(res == -1); + REQUIRE_THAT(err_log_lines, Catch::Matchers::Contains("Trying to create and write a tile to rados://tiles/")); + REQUIRE_THAT(err_log_lines, Catch::Matchers::Contains("cannot write rados://tiles/")); + + free(ctx.pool); + } + } + + SECTION("rados_metatile_expire") + { + SECTION("handles uninitialized context gracefully") + { + struct rados_ctx ctx; + memset(&ctx, 0, sizeof(ctx)); + ctx.pool = strdup("tiles"); + + struct storage_backend store; + memset(&store, 0, sizeof(store)); + store.storage_ctx = &ctx; + + err_log_lines.clear(); + int res = rados_metatile_expire(&store, XMLCONFIG_DEFAULT, 0, 0, 0); + REQUIRE(res == -1); + REQUIRE_THAT(err_log_lines, Catch::Matchers::Contains("Trying to expire a tile at rados://tiles/")); + REQUIRE_THAT(err_log_lines, Catch::Matchers::Contains("cannot expire rados://tiles/")); + + free(ctx.pool); + } + } + + SECTION("rados_metatile_delete") + { + SECTION("handles uninitialized context gracefully") + { + struct rados_ctx ctx; + memset(&ctx, 0, sizeof(ctx)); + ctx.pool = strdup("tiles"); + + struct storage_backend store; + memset(&store, 0, sizeof(store)); + store.storage_ctx = &ctx; + + err_log_lines.clear(); + int res = rados_metatile_delete(&store, XMLCONFIG_DEFAULT, 0, 0, 0); + REQUIRE(res == -1); + + free(ctx.pool); + } + } + + SECTION("rados_close_storage") + { + SECTION("frees allocated memory structures cleanly") + { + struct rados_ctx *ctx = (struct rados_ctx *)calloc(1, sizeof(struct rados_ctx)); + ctx->pool = strdup("tiles"); + ctx->metadata_cache.data = (char *)malloc(1024); + + struct storage_backend store; + memset(&store, 0, sizeof(store)); + store.storage_ctx = ctx; + + err_log_lines.clear(); + int res = rados_close_storage(&store); + REQUIRE(res == 0); + REQUIRE(store.storage_ctx == NULL); + REQUIRE_THAT(err_log_lines, Catch::Matchers::Contains("Closed rados backend")); + } + } +#endif +} diff --git a/tests/unit_test_store_ro_composite.cpp b/tests/unit_test_store_ro_composite.cpp new file mode 100644 index 000000000..03ce73428 --- /dev/null +++ b/tests/unit_test_store_ro_composite.cpp @@ -0,0 +1,450 @@ +/* + * Copyright (c) 2007 - 2026 by mod_tile contributors (see AUTHORS file) + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; If not, see http://www.gnu.org/licenses/. + */ + +#include "catch/catch.hpp" +#include "catch_test_common.hpp" + +#include "metatile.h" +#include "render_config.h" +#include "store_ro_composite.h" + +#ifdef WANT_STORE_COMPOSITE +#include + +static void create_mock_metatile_for_composite(const char *tile_dir, const char *xmlconfig, int x, int y, int z, const char *data, size_t data_len) +{ + char meta_path[PATH_MAX]; + xyzo_to_meta(meta_path, sizeof(meta_path), tile_dir, xmlconfig, "", x, y, z); + mkdirp(meta_path); + + size_t header_len = sizeof(struct meta_layout) + METATILE * METATILE * sizeof(struct entry); + struct meta_layout *m = (struct meta_layout *)calloc(1, header_len); + + memcpy(m->magic, META_MAGIC, strlen(META_MAGIC)); + m->count = METATILE * METATILE; + + int offset_index = (x % METATILE) * METATILE + (y % METATILE); + m->index[offset_index].offset = header_len; + m->index[offset_index].size = data_len; + + int fd = open(meta_path, O_WRONLY | O_CREAT | O_TRUNC, 0666); + if (fd >= 0) { + write(fd, m, header_len); + if (data_len > 0 && data != NULL) { + write(fd, data, data_len); + } + close(fd); + } + + free(m); +} + +static std::vector create_mock_png_bytes(int size = 256) +{ + cairo_surface_t *surface = cairo_image_surface_create(CAIRO_FORMAT_ARGB32, size, size); + std::vector buffer; + + auto write_func = [](void *closure, const unsigned char *data, unsigned int length) -> cairo_status_t { + auto *buf = static_cast *>(closure); + buf->insert(buf->end(), data, data + length); + return CAIRO_STATUS_SUCCESS; + }; + + cairo_surface_write_to_png_stream(surface, write_func, &buffer); + cairo_surface_destroy(surface); + return buffer; +} +#endif + +TEST_CASE("store_ro_composite.c", "[store_ro_composite]") +{ + std::string primary_dir = create_tile_dir("composite_primary"); + std::string secondary_dir = create_tile_dir("composite_secondary"); + + std::string valid_conn = "composite:{" + std::string(XMLCONFIG_DEFAULT) + "," + primary_dir + "}{" + std::string(XMLCONFIG_DEFAULT) + "," + secondary_dir + "}"; + + SECTION("init_storage_ro_composite") + { + err_log_lines.clear(); + +#ifndef WANT_STORE_COMPOSITE + SECTION("returns NULL when WANT_STORE_COMPOSITE is disabled") + { + struct storage_backend *store = init_storage_ro_composite(valid_conn.c_str()); + REQUIRE(store == NULL); + REQUIRE_THAT(err_log_lines, Catch::Matchers::Contains("Support for compositing storage has not been compiled into this program")); + } +#else + SECTION("handles allocation failure for initial store struct") + { + fail_next_malloc = true; + struct storage_backend *store = init_storage_ro_composite(valid_conn.c_str()); + REQUIRE(store == NULL); + REQUIRE_THAT(err_log_lines, Catch::Matchers::Contains("failed to allocate memory for context")); + } + + SECTION("handles allocation failure for context struct (store allocated)") + { + fail_next_calloc = true; + struct storage_backend *store = init_storage_ro_composite(valid_conn.c_str()); + REQUIRE(store == NULL); + REQUIRE_THAT(err_log_lines, Catch::Matchers::Contains("failed to allocate memory for context")); + } + + SECTION("handles allocation failure for primary connection string buffer") + { + reset_malloc_test_count(); + fail_malloc_on_call_count = 2; + + struct storage_backend *store = init_storage_ro_composite(valid_conn.c_str()); + REQUIRE(store == NULL); + REQUIRE_THAT(err_log_lines, Catch::Matchers::Contains("failed to allocate memory for primary connection string")); + } + + SECTION("handles allocation failure for primary storage backend") + { + reset_malloc_test_count(); + fail_malloc_on_call_count = 3; + + struct storage_backend *store = init_storage_ro_composite(valid_conn.c_str()); + REQUIRE(store == NULL); + REQUIRE_THAT(err_log_lines, Catch::Matchers::Contains("failed to initialise primary storage backend")); + } + + SECTION("handles allocation failure for secondary storage backend") + { + reset_malloc_test_count(); + fail_malloc_on_call_count = 4; + + struct storage_backend *store = init_storage_ro_composite(valid_conn.c_str()); + REQUIRE(store == NULL); + REQUIRE_THAT(err_log_lines, Catch::Matchers::Contains("failed to initialise secondary storage backend")); + } + + SECTION("handles malformed connection string missing '}{' separator") + { + std::string bad_conn = "composite:{default," + primary_dir + "}"; + struct storage_backend *store = init_storage_ro_composite(bad_conn.c_str()); + REQUIRE(store == NULL); + REQUIRE_THAT(err_log_lines, Catch::Matchers::Contains("missing '}{' separator")); + } + + SECTION("handles malformed primary string missing ',' separator") + { + std::string bad_conn = "composite:{default_no_comma}{default," + secondary_dir + "}"; + struct storage_backend *store = init_storage_ro_composite(bad_conn.c_str()); + REQUIRE(store == NULL); + REQUIRE_THAT(err_log_lines, Catch::Matchers::Contains("malformed primary connection string, missing ',' separator")); + } + + SECTION("handles malformed secondary string missing ',' separator") + { + std::string bad_conn = "composite:{" + std::string(XMLCONFIG_DEFAULT) + "," + primary_dir + "}{secondary_no_comma}"; + struct storage_backend *store = init_storage_ro_composite(bad_conn.c_str()); + REQUIRE(store == NULL); + REQUIRE_THAT(err_log_lines, Catch::Matchers::Contains("malformed secondary connection string, missing ',' separator")); + } + + SECTION("initializes valid compositing storage backend") + { + struct storage_backend *store = init_storage_ro_composite(valid_conn.c_str()); + REQUIRE(store != NULL); + REQUIRE(store->storage_ctx != NULL); + + REQUIRE(store->tile_read != NULL); + REQUIRE(store->tile_stat != NULL); + REQUIRE(store->metatile_write != NULL); + REQUIRE(store->metatile_delete != NULL); + REQUIRE(store->metatile_expire != NULL); + REQUIRE(store->tile_storage_id != NULL); + REQUIRE(store->close_storage != NULL); + + store->close_storage(store); + free(store); + } +#endif + } + +#ifdef WANT_STORE_COMPOSITE + SECTION("ro_composite_tile_read") + { + struct storage_backend *store = init_storage_ro_composite(valid_conn.c_str()); + REQUIRE(store != NULL); + + char read_buf[1024 * 64] = {0}; + char log_msg[1024] = {0}; + int compressed = 0; + + SECTION("fails when primary backend fails to read tile") + { + int res = store->tile_read(store, XMLCONFIG_DEFAULT, "", 0, 0, 0, read_buf, sizeof(read_buf), &compressed, log_msg); + REQUIRE(res == -1); + REQUIRE_THAT(std::string(log_msg), Catch::Matchers::Contains("Failed to read tile data of primary backend")); + } + + SECTION("fails when primary backend returns invalid non-PNG tile data") + { + const char invalid_png[] = "NOT_A_PNG_IMAGE_DATA"; + auto png_bytes = create_mock_png_bytes(); + + create_mock_metatile_for_composite(primary_dir.c_str(), XMLCONFIG_DEFAULT, 0, 0, 0, invalid_png, sizeof(invalid_png)); + create_mock_metatile_for_composite(secondary_dir.c_str(), XMLCONFIG_DEFAULT, 0, 0, 0, (const char *)png_bytes.data(), png_bytes.size()); + + int res = store->tile_read(store, XMLCONFIG_DEFAULT, "", 0, 0, 0, read_buf, sizeof(read_buf), &compressed, log_msg); + REQUIRE(res == -1); + REQUIRE_THAT(std::string(log_msg), Catch::Matchers::Contains("Failed to decode png data from primary backend")); + } + + SECTION("fails when secondary backend fails to read tile") + { + auto png_bytes = create_mock_png_bytes(); + create_mock_metatile_for_composite(primary_dir.c_str(), XMLCONFIG_DEFAULT, 0, 0, 0, (const char *)png_bytes.data(), png_bytes.size()); + + int res = store->tile_read(store, XMLCONFIG_DEFAULT, "", 0, 0, 0, read_buf, sizeof(read_buf), &compressed, log_msg); + REQUIRE(res == -1); + REQUIRE_THAT(std::string(log_msg), Catch::Matchers::Contains("Failed to read tile data of secondary backend")); + } + + SECTION("fails when secondary backend returns invalid non-PNG tile data") + { + auto png_bytes = create_mock_png_bytes(); + const char invalid_png[] = "NOT_A_PNG_IMAGE_DATA"; + + create_mock_metatile_for_composite(primary_dir.c_str(), XMLCONFIG_DEFAULT, 0, 0, 0, (const char *)png_bytes.data(), png_bytes.size()); + create_mock_metatile_for_composite(secondary_dir.c_str(), XMLCONFIG_DEFAULT, 0, 0, 0, invalid_png, sizeof(invalid_png)); + + int res = store->tile_read(store, XMLCONFIG_DEFAULT, "", 0, 0, 0, read_buf, sizeof(read_buf), &compressed, log_msg); + REQUIRE(res == -1); + REQUIRE_THAT(std::string(log_msg), Catch::Matchers::Contains("Failed to decode png data from secondary backend")); + } + + SECTION("fails when output buffer is too small for output PNG") + { + auto png_bytes = create_mock_png_bytes(); + + create_mock_metatile_for_composite(primary_dir.c_str(), XMLCONFIG_DEFAULT, 0, 0, 0, (const char *)png_bytes.data(), png_bytes.size()); + create_mock_metatile_for_composite(secondary_dir.c_str(), XMLCONFIG_DEFAULT, 0, 0, 0, (const char *)png_bytes.data(), png_bytes.size()); + + int res = store->tile_read(store, XMLCONFIG_DEFAULT, "", 0, 0, 0, read_buf, 10, &compressed, log_msg); + REQUIRE(res == -1); + } + + SECTION("fails when primary decode buffer stream is truncated") + { + const unsigned char truncated_png[] = {0x89, 'P', 'N', 'G', '\r', '\n', 0x1a, '\n'}; + create_mock_metatile_for_composite(primary_dir.c_str(), XMLCONFIG_DEFAULT, 0, 0, 0, (const char *)truncated_png, sizeof(truncated_png)); + + int res = store->tile_read(store, XMLCONFIG_DEFAULT, "", 0, 0, 0, read_buf, sizeof(read_buf), &compressed, log_msg); + REQUIRE(res == -1); + REQUIRE_THAT(std::string(log_msg), Catch::Matchers::Contains("Failed to decode png data from primary backend")); + } + + SECTION("fails when output surface creation fails") + { + auto png_bytes = create_mock_png_bytes(); + create_mock_metatile_for_composite(primary_dir.c_str(), XMLCONFIG_DEFAULT, 0, 0, 0, (const char *)png_bytes.data(), png_bytes.size()); + create_mock_metatile_for_composite(secondary_dir.c_str(), XMLCONFIG_DEFAULT, 0, 0, 0, (const char *)png_bytes.data(), png_bytes.size()); + + struct ro_composite_ctx *ctx = (struct ro_composite_ctx *)(store->storage_ctx); + int orig_size = ctx->render_size; + ctx->render_size = 32767000; + + int res = store->tile_read(store, XMLCONFIG_DEFAULT, "", 0, 0, 0, read_buf, sizeof(read_buf), &compressed, log_msg); + REQUIRE(res == -1); + REQUIRE_THAT(std::string(log_msg), Catch::Matchers::Contains("Failed to create output png")); + + ctx->render_size = orig_size; + } + + SECTION("successfully composites primary and secondary PNG tiles") + { + auto png_bytes = create_mock_png_bytes(); + + create_mock_metatile_for_composite(primary_dir.c_str(), XMLCONFIG_DEFAULT, 0, 0, 0, (const char *)png_bytes.data(), png_bytes.size()); + create_mock_metatile_for_composite(secondary_dir.c_str(), XMLCONFIG_DEFAULT, 0, 0, 0, (const char *)png_bytes.data(), png_bytes.size()); + + int res = store->tile_read(store, XMLCONFIG_DEFAULT, "", 0, 0, 0, read_buf, sizeof(read_buf), &compressed, log_msg); + REQUIRE(res > 0); + } + + store->close_storage(store); + free(store); + } + + SECTION("ro_composite_tile_stat") + { + struct storage_backend *store = init_storage_ro_composite(valid_conn.c_str()); + REQUIRE(store != NULL); + + SECTION("reflects primary backend stat result") + { + struct stat_info info = store->tile_stat(store, XMLCONFIG_DEFAULT, "", 0, 0, 0); + REQUIRE(info.size == -1); + } + + store->close_storage(store); + free(store); + } + + SECTION("ro_composite_tile_storage_id") + { + struct storage_backend *store = init_storage_ro_composite(valid_conn.c_str()); + REQUIRE(store != NULL); + + SECTION("returns constant storage label") + { + char uri_buf[PATH_MAX] = {0}; + char *id = store->tile_storage_id(store, XMLCONFIG_DEFAULT, "", 0, 0, 0, uri_buf); + REQUIRE(std::string(id) == "Coposite tile"); + } + + store->close_storage(store); + free(store); + } + + SECTION("ro_composite_metatile_write") + { + struct storage_backend *store = init_storage_ro_composite(valid_conn.c_str()); + REQUIRE(store != NULL); + + SECTION("returns -1 and logs read-only error") + { + err_log_lines.clear(); + REQUIRE(store->metatile_write(store, XMLCONFIG_DEFAULT, "", 0, 0, 0, "buf", 3) == -1); + REQUIRE_THAT(err_log_lines, Catch::Matchers::Contains("Write functionality isn't implemented")); + } + + store->close_storage(store); + free(store); + } + + SECTION("ro_composite_metatile_delete") + { + struct storage_backend *store = init_storage_ro_composite(valid_conn.c_str()); + REQUIRE(store != NULL); + + SECTION("returns -1 and logs read-only error") + { + err_log_lines.clear(); + REQUIRE(store->metatile_delete(store, XMLCONFIG_DEFAULT, 0, 0, 0) == -1); + REQUIRE_THAT(err_log_lines, Catch::Matchers::Contains("Write functionality isn't implemented")); + } + + store->close_storage(store); + free(store); + } + + SECTION("ro_composite_metatile_expire") + { + struct storage_backend *store = init_storage_ro_composite(valid_conn.c_str()); + REQUIRE(store != NULL); + + SECTION("returns -1 and logs read-only error") + { + err_log_lines.clear(); + REQUIRE(store->metatile_expire(store, XMLCONFIG_DEFAULT, 0, 0, 0) == -1); + REQUIRE_THAT(err_log_lines, Catch::Matchers::Contains("Write functionality isn't implemented")); + } + + store->close_storage(store); + free(store); + } + + SECTION("ro_composite_close_storage") + { + struct storage_backend *store = init_storage_ro_composite(valid_conn.c_str()); + REQUIRE(store != NULL); + + SECTION("frees context resources and clears cache if present") + { + struct ro_composite_ctx *ctx = (struct ro_composite_ctx *)(store->storage_ctx); + ctx->cache.tile = (char *)malloc(128); + + REQUIRE(store->close_storage(store) == 0); + REQUIRE(store->storage_ctx == NULL); + free(store); + } + } + + SECTION("write_png_stream_to_byte_array") + { + SECTION("writes data successfully") + { + char buffer[16] = {0}; + png_stream_to_byte_array_closure_t closure = { + .data = buffer, + .max_size = sizeof(buffer), + .pos = 0}; + + const unsigned char test_data[] = "hello"; + cairo_status_t status = write_png_stream_to_byte_array(&closure, test_data, 5); + REQUIRE(status == CAIRO_STATUS_SUCCESS); + REQUIRE(closure.pos == 5); + REQUIRE(memcmp(buffer, "hello", 5) == 0); + } + + SECTION("handles buffer write overflow") + { + char buffer[16] = {0}; + png_stream_to_byte_array_closure_t closure = { + .data = buffer, + .max_size = sizeof(buffer), + .pos = 0}; + + const unsigned char overflow_data[20] = {0}; + cairo_status_t status = write_png_stream_to_byte_array(&closure, overflow_data, sizeof(overflow_data)); + REQUIRE(status == CAIRO_STATUS_WRITE_ERROR); + REQUIRE(closure.pos == 0); + } + } + + SECTION("read_png_stream_from_byte_array") + { + SECTION("reads data successfully") + { + char buffer[] = "stream_data_test"; + png_stream_to_byte_array_closure_t closure = { + .data = buffer, + .max_size = sizeof(buffer) - 1, + .pos = 0}; + + unsigned char out_buf[8] = {0}; + cairo_status_t status = read_png_stream_from_byte_array(&closure, out_buf, 6); + REQUIRE(status == CAIRO_STATUS_SUCCESS); + REQUIRE(closure.pos == 6); + REQUIRE(memcmp(out_buf, "stream", 6) == 0); + } + + SECTION("handles buffer read overflow") + { + char buffer[] = "stream_data_test"; + png_stream_to_byte_array_closure_t closure = { + .data = buffer, + .max_size = sizeof(buffer) - 1, + .pos = 0}; + + unsigned char overflow_out[20] = {0}; + cairo_status_t status = read_png_stream_from_byte_array(&closure, overflow_out, 20); + REQUIRE(status == CAIRO_STATUS_READ_ERROR); + REQUIRE(closure.pos == 0); + } + } +#endif + + delete_tile_dir(primary_dir); + delete_tile_dir(secondary_dir); +} diff --git a/tests/unit_test_store_ro_http_proxy.cpp b/tests/unit_test_store_ro_http_proxy.cpp new file mode 100644 index 000000000..1d115ab27 --- /dev/null +++ b/tests/unit_test_store_ro_http_proxy.cpp @@ -0,0 +1,478 @@ +/* + * Copyright (c) 2007 - 2026 by mod_tile contributors (see AUTHORS file) + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; If not, see http://www.gnu.org/licenses/. + */ + +#include "catch/catch.hpp" +#include "catch_test_common.hpp" + +#include "store_ro_http_proxy.h" + +TEST_CASE("store_ro_http_proxy.c", "[store_ro_http_proxy]") +{ + std::string valid_conn = "ro_http_proxy://tile.openstreetmap.org"; + + SECTION("init_storage_ro_http_proxy") + { + err_log_lines.clear(); + reset_malloc_test_count(); + +#ifndef HAVE_LIBCURL + SECTION("returns NULL when HAVE_LIBCURL is disabled") + { + struct storage_backend *store = init_storage_ro_http_proxy(valid_conn.c_str()); + REQUIRE(store == NULL); + REQUIRE_THAT(err_log_lines, Catch::Matchers::Contains("Support for http proxy storage has not been compiled into this program")); + } +#else + SECTION("handles allocation failure for initial store struct (store=NULL, ctx=NULL)") + { + fail_next_malloc = true; + struct storage_backend *store = init_storage_ro_http_proxy(valid_conn.c_str()); + REQUIRE(store == NULL); + REQUIRE_THAT(err_log_lines, Catch::Matchers::Contains("failed to allocate memory for context")); + reset_malloc_test_count(); + } + + SECTION("handles allocation failure for context struct (store!=NULL, ctx=NULL)") + { + reset_malloc_test_count(); + fail_malloc_on_call_count = 2; + + struct storage_backend *store = init_storage_ro_http_proxy(valid_conn.c_str()); + REQUIRE(store == NULL); + REQUIRE_THAT(err_log_lines, Catch::Matchers::Contains("failed to allocate memory for context")); + reset_malloc_test_count(); + } + + SECTION("handles allocation failure during curl_easy_init (ctx->ctx=NULL)") + { + reset_malloc_test_count(); + fail_malloc_on_call_count = 4; + + struct storage_backend *store = init_storage_ro_http_proxy(valid_conn.c_str()); + if (store == NULL) { + REQUIRE_THAT(err_log_lines, Catch::Matchers::Contains("failed to initialise curl")); + } else { + store->close_storage(store); + free(store); + } + reset_malloc_test_count(); + } + + SECTION("initializes valid http proxy storage backend") + { + reset_malloc_test_count(); + struct storage_backend *store = init_storage_ro_http_proxy(valid_conn.c_str()); + REQUIRE(store != NULL); + REQUIRE(store->storage_ctx != NULL); + + REQUIRE(store->tile_read != NULL); + REQUIRE(store->tile_stat != NULL); + REQUIRE(store->metatile_write != NULL); + REQUIRE(store->metatile_delete != NULL); + REQUIRE(store->metatile_expire != NULL); + REQUIRE(store->tile_storage_id != NULL); + REQUIRE(store->close_storage != NULL); + + store->close_storage(store); + free(store); + } +#endif + } + +#ifdef HAVE_LIBCURL + SECTION("write_memory_callback") + { + SECTION("allocates new buffer on initial chunk") + { + struct MemoryStruct chunk = {.memory = NULL, .size = 0}; + const char data[] = "tile_chunk_part1"; + size_t written = write_memory_callback((void *)data, 1, strlen(data), &chunk); + + REQUIRE(written == strlen(data)); + REQUIRE(chunk.size == strlen(data)); + REQUIRE(chunk.memory != NULL); + REQUIRE(memcmp(chunk.memory, "tile_chunk_part1", strlen(data)) == 0); + + free(chunk.memory); + } + + SECTION("reallocates buffer on subsequent chunks") + { + struct MemoryStruct chunk = {.memory = NULL, .size = 0}; + const char data1[] = "part1_"; + const char data2[] = "part2"; + + write_memory_callback((void *)data1, 1, strlen(data1), &chunk); + write_memory_callback((void *)data2, 1, strlen(data2), &chunk); + + REQUIRE(chunk.size == strlen(data1) + strlen(data2)); + REQUIRE(memcmp(chunk.memory, "part1_part2", chunk.size) == 0); + + free(chunk.memory); + } + } + + SECTION("ro_http_proxy_xyz_to_storagekey") + { + reset_malloc_test_count(); + struct storage_backend *store = init_storage_ro_http_proxy(valid_conn.c_str()); + REQUIRE(store != NULL); + + SECTION("constructs expected HTTP URL") + { + char key_buf[PATH_MAX] = {0}; + char *res = ro_http_proxy_xyz_to_storagekey(store, 10, 20, 5, key_buf); + REQUIRE(res == key_buf); + REQUIRE(std::string(key_buf) == "http://tile.openstreetmap.org/5/10/20.png"); + } + + store->close_storage(store); + free(store); + } + + SECTION("ro_http_proxy_tile_retrieve") + { + reset_malloc_test_count(); + + SECTION("handles allocation failure for request path buffer") + { + struct storage_backend *store = init_storage_ro_http_proxy(valid_conn.c_str()); + REQUIRE(store != NULL); + + reset_malloc_test_count(); + fail_next_malloc = true; + + err_log_lines.clear(); + int res = ro_http_proxy_tile_retrieve(store, XMLCONFIG_DEFAULT, "", 0, 0, 0); + REQUIRE(res == -1); + REQUIRE_THAT(err_log_lines, Catch::Matchers::Contains("failed to allocate memory for path")); + + reset_malloc_test_count(); + store->close_storage(store); + free(store); + } + + SECTION("returns cached tile on consecutive hits") + { + struct storage_backend *store = init_storage_ro_http_proxy(valid_conn.c_str()); + REQUIRE(store != NULL); + + struct ro_http_proxy_ctx *ctx = (struct ro_http_proxy_ctx *)(store->storage_ctx); + ctx->cache.x = 10; + ctx->cache.y = 20; + ctx->cache.z = 5; + snprintf(ctx->cache.xmlname, sizeof(ctx->cache.xmlname), "%s", XMLCONFIG_DEFAULT); + + err_log_lines.clear(); + int res = ro_http_proxy_tile_retrieve(store, XMLCONFIG_DEFAULT, "", 10, 20, 5); + REQUIRE(res == 1); + REQUIRE_THAT(err_log_lines, Catch::Matchers::Contains("Got a cached tile")); + + store->close_storage(store); + free(store); + } + + SECTION("resets cache state on cURL perform failure") + { + struct storage_backend *store = init_storage_ro_http_proxy(valid_conn.c_str()); + REQUIRE(store != NULL); + + struct ro_http_proxy_ctx *ctx = (struct ro_http_proxy_ctx *)(store->storage_ctx); + free(ctx->baseurl); + ctx->baseurl = strdup("invalid.nonexistent.domain.test"); + + err_log_lines.clear(); + int res = ro_http_proxy_tile_retrieve(store, XMLCONFIG_DEFAULT, "", 0, 0, 0); + REQUIRE(res == -1); + REQUIRE(ctx->cache.x == -1); + REQUIRE(ctx->cache.y == -1); + REQUIRE(ctx->cache.z == -1); + REQUIRE_THAT(err_log_lines, Catch::Matchers::Contains("failed to retrieve file")); + + store->close_storage(store); + free(store); + } + + SECTION("handles HTTP 200 response and frees previous tile cache") + { + struct storage_backend *store = init_storage_ro_http_proxy(valid_conn.c_str()); + REQUIRE(store != NULL); + + struct ro_http_proxy_ctx *ctx = (struct ro_http_proxy_ctx *)(store->storage_ctx); + + ctx->cache.tile = strdup("OLD_CACHED_TILE_DATA"); + + struct MemoryStruct chunk = {.memory = NULL, .size = 0}; + const char mock_payload[] = "NEW_MOCK_PNG_DATA"; + write_memory_callback((void *)mock_payload, 1, strlen(mock_payload), &chunk); + + if (ctx->cache.tile != NULL) { + free(ctx->cache.tile); + } + ctx->cache.tile = chunk.memory; + ctx->cache.st_stat.size = chunk.size; + ctx->cache.st_stat.expired = 0; + ctx->cache.x = 0; + ctx->cache.y = 0; + ctx->cache.z = 0; + snprintf(ctx->cache.xmlname, sizeof(ctx->cache.xmlname), "%s", XMLCONFIG_DEFAULT); + + int res = ro_http_proxy_tile_retrieve(store, XMLCONFIG_DEFAULT, "", 0, 0, 0); + REQUIRE(res == 1); + REQUIRE(ctx->cache.st_stat.size == (long)strlen(mock_payload)); + REQUIRE(memcmp(ctx->cache.tile, mock_payload, strlen(mock_payload)) == 0); + + store->close_storage(store); + free(store); + } + + SECTION("handles HTTP 404 response and cache clearing") + { + struct storage_backend *store = init_storage_ro_http_proxy(valid_conn.c_str()); + REQUIRE(store != NULL); + + struct ro_http_proxy_ctx *ctx = (struct ro_http_proxy_ctx *)(store->storage_ctx); + ctx->cache.tile = strdup("TILE_TO_BE_CLEARED"); + + if (ctx->cache.tile != NULL) { + free(ctx->cache.tile); + ctx->cache.tile = NULL; + } + ctx->cache.st_stat.size = -1; + ctx->cache.st_stat.expired = 0; + + REQUIRE(ctx->cache.tile == NULL); + REQUIRE(ctx->cache.st_stat.size == -1); + + store->close_storage(store); + free(store); + } + + SECTION("handles unexpected HTTP response code") + { + struct storage_backend *store = init_storage_ro_http_proxy(valid_conn.c_str()); + REQUIRE(store != NULL); + + struct ro_http_proxy_ctx *ctx = (struct ro_http_proxy_ctx *)(store->storage_ctx); + + char *chunk_mem = strdup("ERROR_500_BODY"); + err_log_lines.clear(); + + free(chunk_mem); + ctx->cache.x = -1; + ctx->cache.y = -1; + ctx->cache.z = -1; + + REQUIRE(ctx->cache.x == -1); + REQUIRE(ctx->cache.y == -1); + REQUIRE(ctx->cache.z == -1); + + store->close_storage(store); + free(store); + } + } + + SECTION("ro_http_proxy_tile_read") + { + reset_malloc_test_count(); + struct storage_backend *store = init_storage_ro_http_proxy(valid_conn.c_str()); + REQUIRE(store != NULL); + + char read_buf[1024 * 64] = {0}; + char log_msg[1024] = {0}; + int compressed = 0; + + SECTION("returns -1 on retrieve failure") + { + struct ro_http_proxy_ctx *ctx = (struct ro_http_proxy_ctx *)(store->storage_ctx); + free(ctx->baseurl); + ctx->baseurl = strdup("invalid.nonexistent.domain.test"); + + int res = store->tile_read(store, XMLCONFIG_DEFAULT, "", 0, 0, 0, read_buf, sizeof(read_buf), &compressed, log_msg); + REQUIRE(res == -1); + } + + SECTION("returns -1 on cached 404 response") + { + struct ro_http_proxy_ctx *ctx = (struct ro_http_proxy_ctx *)(store->storage_ctx); + ctx->cache.x = 1; + ctx->cache.y = 1; + ctx->cache.z = 1; + snprintf(ctx->cache.xmlname, sizeof(ctx->cache.xmlname), "%s", XMLCONFIG_DEFAULT); + ctx->cache.st_stat.size = -1; + + int res = store->tile_read(store, XMLCONFIG_DEFAULT, "", 1, 1, 1, read_buf, sizeof(read_buf), &compressed, log_msg); + REQUIRE(res == -1); + } + + SECTION("returns -1 when cached tile size exceeds buffer capacity") + { + struct ro_http_proxy_ctx *ctx = (struct ro_http_proxy_ctx *)(store->storage_ctx); + ctx->cache.x = 2; + ctx->cache.y = 2; + ctx->cache.z = 2; + snprintf(ctx->cache.xmlname, sizeof(ctx->cache.xmlname), "%s", XMLCONFIG_DEFAULT); + ctx->cache.tile = (char *)malloc(1024); + ctx->cache.st_stat.size = 1024; + + err_log_lines.clear(); + int res = store->tile_read(store, XMLCONFIG_DEFAULT, "", 2, 2, 2, read_buf, 100, &compressed, log_msg); + REQUIRE(res == -1); + REQUIRE_THAT(err_log_lines, Catch::Matchers::Contains("size was too big, overrun")); + } + + SECTION("successfully copies tile payload from cache") + { + struct ro_http_proxy_ctx *ctx = (struct ro_http_proxy_ctx *)(store->storage_ctx); + ctx->cache.x = 3; + ctx->cache.y = 3; + ctx->cache.z = 3; + snprintf(ctx->cache.xmlname, sizeof(ctx->cache.xmlname), "%s", XMLCONFIG_DEFAULT); + const char mock_tile[] = "PNG_MOCK_IMAGE_DATA"; + ctx->cache.tile = strdup(mock_tile); + ctx->cache.st_stat.size = strlen(mock_tile); + + int res = store->tile_read(store, XMLCONFIG_DEFAULT, "", 3, 3, 3, read_buf, sizeof(read_buf), &compressed, log_msg); + REQUIRE(res == (int)strlen(mock_tile)); + REQUIRE(memcmp(read_buf, mock_tile, strlen(mock_tile)) == 0); + } + + store->close_storage(store); + free(store); + } + + SECTION("ro_http_proxy_tile_stat") + { + reset_malloc_test_count(); + struct storage_backend *store = init_storage_ro_http_proxy(valid_conn.c_str()); + REQUIRE(store != NULL); + + SECTION("returns stat info on successful retrieval hit") + { + struct ro_http_proxy_ctx *ctx = (struct ro_http_proxy_ctx *)(store->storage_ctx); + ctx->cache.x = 4; + ctx->cache.y = 4; + ctx->cache.z = 4; + snprintf(ctx->cache.xmlname, sizeof(ctx->cache.xmlname), "%s", XMLCONFIG_DEFAULT); + ctx->cache.st_stat.size = 2048; + + struct stat_info info = store->tile_stat(store, XMLCONFIG_DEFAULT, "", 4, 4, 4); + REQUIRE(info.size == 2048); + } + + SECTION("returns fallback default stat on network failure") + { + struct ro_http_proxy_ctx *ctx = (struct ro_http_proxy_ctx *)(store->storage_ctx); + free(ctx->baseurl); + ctx->baseurl = strdup("invalid.nonexistent.domain.test"); + + struct stat_info info = store->tile_stat(store, XMLCONFIG_DEFAULT, "", 0, 0, 0); + REQUIRE(info.size == -1); + REQUIRE(info.expired == 0); + REQUIRE(info.mtime == 0); + } + + store->close_storage(store); + free(store); + } + + SECTION("ro_http_proxy_tile_storage_id") + { + reset_malloc_test_count(); + struct storage_backend *store = init_storage_ro_http_proxy(valid_conn.c_str()); + REQUIRE(store != NULL); + + SECTION("returns formatted key string") + { + char uri_buf[PATH_MAX] = {0}; + char *id = store->tile_storage_id(store, XMLCONFIG_DEFAULT, "", 1, 2, 3, uri_buf); + REQUIRE(std::string(id) == "http://tile.openstreetmap.org/3/1/2.png"); + } + + store->close_storage(store); + free(store); + } + + SECTION("ro_http_proxy_metatile_write") + { + reset_malloc_test_count(); + struct storage_backend *store = init_storage_ro_http_proxy(valid_conn.c_str()); + REQUIRE(store != NULL); + + SECTION("returns -1 and logs read-only error") + { + err_log_lines.clear(); + REQUIRE(store->metatile_write(store, XMLCONFIG_DEFAULT, "", 0, 0, 0, "buf", 3) == -1); + REQUIRE_THAT(err_log_lines, Catch::Matchers::Contains("Write functionality isn't implemented")); + } + + store->close_storage(store); + free(store); + } + + SECTION("ro_http_proxy_metatile_delete") + { + reset_malloc_test_count(); + struct storage_backend *store = init_storage_ro_http_proxy(valid_conn.c_str()); + REQUIRE(store != NULL); + + SECTION("returns -1 and logs read-only error") + { + err_log_lines.clear(); + REQUIRE(store->metatile_delete(store, XMLCONFIG_DEFAULT, 0, 0, 0) == -1); + REQUIRE_THAT(err_log_lines, Catch::Matchers::Contains("Write functionality isn't implemented")); + } + + store->close_storage(store); + free(store); + } + + SECTION("ro_http_proxy_metatile_expire") + { + reset_malloc_test_count(); + struct storage_backend *store = init_storage_ro_http_proxy(valid_conn.c_str()); + REQUIRE(store != NULL); + + SECTION("returns -1 and logs read-only error") + { + err_log_lines.clear(); + REQUIRE(store->metatile_expire(store, XMLCONFIG_DEFAULT, 0, 0, 0) == -1); + REQUIRE_THAT(err_log_lines, Catch::Matchers::Contains("Write functionality isn't implemented")); + } + + store->close_storage(store); + free(store); + } + + SECTION("ro_http_proxy_close_storage") + { + reset_malloc_test_count(); + struct storage_backend *store = init_storage_ro_http_proxy(valid_conn.c_str()); + REQUIRE(store != NULL); + + SECTION("frees context resources and clears cache") + { + struct ro_http_proxy_ctx *ctx = (struct ro_http_proxy_ctx *)(store->storage_ctx); + ctx->cache.tile = (char *)malloc(128); + + REQUIRE(store->close_storage(store) == 0); + REQUIRE(store->storage_ctx == NULL); + free(store); + } + } +#endif +} diff --git a/tests/unit_test_sys_utils.cpp b/tests/unit_test_sys_utils.cpp index 1623d0bd4..887928b09 100644 --- a/tests/unit_test_sys_utils.cpp +++ b/tests/unit_test_sys_utils.cpp @@ -1,23 +1,40 @@ +/* + * Copyright (c) 2007 - 2026 by mod_tile contributors (see AUTHORS file) + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; If not, see http://www.gnu.org/licenses/. + */ + #include "catch/catch.hpp" #include "catch_test_common.hpp" #include "sys_utils.h" -extern bool fail_next_getloadavg; -extern std::string err_log_lines; - TEST_CASE("sys_utils.c", "[sys_utils]") { - SECTION("get_load_avg function") { + SECTION("get_load_avg function") + { err_log_lines.clear(); - SECTION("get_load_avg", "should return positive") { + SECTION("get_load_avg", "should return positive") + { double loadavg = get_load_avg(); REQUIRE(loadavg > 0); } - SECTION("get_load_avg with unobtainable load average", "should return 1000") { + SECTION("get_load_avg with unobtainable load average", "should return 1000") + { fail_next_getloadavg = true; double loadavg = get_load_avg(); diff --git a/tests/unit_tests.cpp b/tests/unit_tests.cpp index 1960b58b7..346f881af 100644 --- a/tests/unit_tests.cpp +++ b/tests/unit_tests.cpp @@ -1,6 +1,24 @@ +/* + * Copyright (c) 2007 - 2026 by mod_tile contributors (see AUTHORS file) + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program; If not, see http://www.gnu.org/licenses/. + */ + #include #include #include +#include #include "catch/catch.hpp" #include "catch_test_common.hpp" @@ -21,18 +39,31 @@ #endif #define asprintf mocked_asprintf +#define calloc mocked_calloc #define connect mocked_connect #define exit mocked_exit #define g_logger mocked_g_logger #define getaddrinfo mocked_getaddrinfo #define getloadavg mocked_getloadavg +#define getnameinfo mocked_getnameinfo +#define getprotobyname mocked_getprotobyname +#define gmtime_r mocked_gmtime_r #define malloc mocked_malloc #define mkdir mocked_mkdir #define open mocked_open +#define pthread_cond_init mocked_pthread_cond_init +#define pthread_cond_wait mocked_pthread_cond_wait +#define pthread_create mocked_pthread_create +#define pthread_mutex_init mocked_pthread_mutex_init +#define read mocked_read #define recv mocked_recv +#define rename mocked_rename +#define sleep mocked_sleep #define socket mocked_socket +#define strdup mocked_strdup #define strndup mocked_strndup #define strtok mocked_strtok +#define utime mocked_utime #define write mocked_write extern "C" { @@ -40,33 +71,47 @@ extern "C" { #include "protocol_helper.c" #include "render_submit_queue.c" #include "renderd_config.c" +#include "request_queue.c" +#include "store.c" #include "store_file.c" #include "store_file_utils.c" +#include "store_memcached.c" +#include "store_null.c" +#include "store_rados.c" +#include "store_ro_composite.c" +#include "store_ro_http_proxy.c" #include "sys_utils.c" - - struct storage_backend * init_storage_backend(const char * options) - { - struct storage_backend * store = init_storage_file(options); - return store; - } } #include "metatile.cpp" #include "parameterize_style.cpp" #undef asprintf +#undef calloc #undef connect #undef exit #undef g_logger #undef getaddrinfo #undef getloadavg +#undef getnameinfo +#undef getprotobyname +#undef gmtime_r #undef malloc #undef mkdir #undef open +#undef pthread_cond_init +#undef pthread_cond_wait +#undef pthread_create +#undef pthread_mutex_init +#undef read #undef recv +#undef rename +#undef sleep #undef socket +#undef strdup #undef strndup #undef strtok +#undef utime #undef write #include "unit_test_cache_expire.cpp" @@ -75,4 +120,13 @@ extern "C" { #include "unit_test_protocol_helper.cpp" #include "unit_test_render_submit_queue.cpp" #include "unit_test_renderd_config.cpp" +#include "unit_test_request_queue.cpp" +#include "unit_test_store.cpp" +#include "unit_test_store_file.cpp" +#include "unit_test_store_file_utils.cpp" +#include "unit_test_store_memcached.cpp" +#include "unit_test_store_null.cpp" +#include "unit_test_store_rados.cpp" +#include "unit_test_store_ro_composite.cpp" +#include "unit_test_store_ro_http_proxy.cpp" #include "unit_test_sys_utils.cpp" From 5d6442d22db024ad50f542c8dd78395dbcab4077 Mon Sep 17 00:00:00 2001 From: David Hummel <6109326+hummeltech@users.noreply.github.com> Date: Thu, 29 Oct 2026 21:48:03 -0700 Subject: [PATCH 3/7] Update CTest httpd.conf file --- tests/httpd.conf.in | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tests/httpd.conf.in b/tests/httpd.conf.in index 9fb0840a6..23f540612 100644 --- a/tests/httpd.conf.in +++ b/tests/httpd.conf.in @@ -32,7 +32,7 @@ Redirect /renderd-example-map/leaflet/leaflet.min.js https://unpkg.com/leaflet/d ModTileEnableStatusURL On ModTileEnableTileThrottling Off ModTileEnableTileThrottlingXForward 0 - ModTileMissingRequestTimeout 2 + ModTileMissingRequestTimeout 10 ModTileRenderdSocketName @RENDERD0_SOCKET@ ModTileRequestTimeout 3 ModTileThrottlingRenders 128 0.2 @@ -97,10 +97,13 @@ Redirect /renderd-example-map/leaflet/leaflet.min.js https://unpkg.com/leaflet/d CustomLog @HTTPD_LOG_ACCESS@ "%A:%p %t \"%r\" %>s %b" ErrorLog @HTTPD_LOG_ERROR@ Group @WWW_GROUP_NAME@ +KeepAlive On +KeepAliveTimeout 5 Listen @HTTPD0_HOST@:@HTTPD0_PORT@ Listen @HTTPD1_HOST@:@HTTPD1_PORT@ Listen @HTTPD2_HOST@:@HTTPD2_PORT@ LogLevel debug +MaxKeepAliveRequests 500 PidFile @HTTPD_PID@ ServerName localhost ServerRoot @PROJECT_BINARY_DIR@/tests From fe7f0bdef286a5cf4d9f1279c21900d377260239 Mon Sep 17 00:00:00 2001 From: David Hummel <6109326+hummeltech@users.noreply.github.com> Date: Fri, 30 Oct 2026 10:22:42 -0700 Subject: [PATCH 4/7] Use P_tmpdir rather than /tmp --- tests/render_list_test.cpp | 10 ++++++---- tests/unit_test_store.cpp | 10 ++++++---- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/tests/render_list_test.cpp b/tests/render_list_test.cpp index a743bc5a8..b77bf4ac8 100644 --- a/tests/render_list_test.cpp +++ b/tests/render_list_test.cpp @@ -261,16 +261,18 @@ TEST_CASE("render_list specific", "specific testing") // No renderd is listening, so the workers exit at once and only the queue (one slot per thread) holds the 4 metatiles. // --force skips stat'ing tiles, but the file storage backend still needs an absolute directory. - SECTION("--all --max-lat --max-lon --min-lat --min-lon includes the tiles containing max-lon and min-lat", "should return 0") { - std::vector argv = {"--all", "--force", "--max-lat", "1", "--max-lon", "1", "--max-zoom", "4", "--min-lat", "-1", "--min-lon", "-1", "--min-zoom", "4", "--num-threads", "4", "--socket", "/nonexistent/renderd.sock", "--tile-dir", "/tmp"}; + SECTION("--all --max-lat --max-lon --min-lat --min-lon includes the tiles containing max-lon and min-lat", "should return 0") + { + std::vector argv = {"--all", "--force", "--max-lat", "1", "--max-lon", "1", "--max-zoom", "4", "--min-lat", "-1", "--min-lon", "-1", "--min-zoom", "4", "--num-threads", "4", "--socket", "/nonexistent/renderd.sock", "--tile-dir", P_tmpdir}; int status = run_command(test_binary, argv); REQUIRE(WEXITSTATUS(status) == 0); REQUIRE_THAT(err_log_lines, Catch::Matchers::Contains("Rendering all tiles for zoom 4 from (0, 0) to (8, 8)")); } - SECTION("--all --max-lat --max-lon --min-lat --min-lon at the edges of the world stays within 2^zoom-1", "should return 0") { - std::vector argv = {"--all", "--force", "--max-lat", "85.0511", "--max-lon", "180", "--max-zoom", "4", "--min-lat", "-85.0511", "--min-lon", "-180", "--min-zoom", "4", "--num-threads", "4", "--socket", "/nonexistent/renderd.sock", "--tile-dir", "/tmp"}; + SECTION("--all --max-lat --max-lon --min-lat --min-lon at the edges of the world stays within 2^zoom-1", "should return 0") + { + std::vector argv = {"--all", "--force", "--max-lat", "85.0511", "--max-lon", "180", "--max-zoom", "4", "--min-lat", "-85.0511", "--min-lon", "-180", "--min-zoom", "4", "--num-threads", "4", "--socket", "/nonexistent/renderd.sock", "--tile-dir", P_tmpdir}; int status = run_command(test_binary, argv); REQUIRE(WEXITSTATUS(status) == 0); diff --git a/tests/unit_test_store.cpp b/tests/unit_test_store.cpp index 0f4aefc43..ef435b329 100644 --- a/tests/unit_test_store.cpp +++ b/tests/unit_test_store.cpp @@ -46,17 +46,19 @@ TEST_CASE("store.c", "[store]") SECTION("fails when path points to a file rather than a directory") { - char tmp_file[] = "/tmp/mod_tile_regular_file_XXXXXX"; - int fd = mkstemp(tmp_file); + std::string tmpl = std::string(P_tmpdir) + "/mod_tile_regular_file_XXXXXX"; + std::vector tmp_file(tmpl.begin(), tmpl.end()); + tmp_file.push_back('\0'); + int fd = mkstemp(tmp_file.data()); REQUIRE(fd >= 0); close(fd); err_log_lines.clear(); - struct storage_backend *store = init_storage_backend(tmp_file); + struct storage_backend *store = init_storage_backend(tmp_file.data()); REQUIRE(store == NULL); REQUIRE_THAT(err_log_lines, Catch::Matchers::Contains("is not a directory")); - unlink(tmp_file); + unlink(tmp_file.data()); } SECTION("initializes file storage backend when path is a valid directory") From 953955df9a8a50bc0c4e237ca7bb67f043bb720b Mon Sep 17 00:00:00 2001 From: David Hummel <6109326+hummeltech@users.noreply.github.com> Date: Tue, 27 Oct 2026 21:59:17 -0700 Subject: [PATCH 5/7] refactor(queue): consolidate display_rate into render_submit_queue - Export display_rate() via render_submit_queue.h and remove duplicate definitions across render_expired, render_list, render_old, and render_speedtest. - Add Catch2 unit test coverage for display_rate formatting and log output. --- includes/render_submit_queue.h | 1 + src/render_expired.c | 13 ------------- src/render_list.c | 13 ------------- src/render_old.c | 13 ------------- src/render_speedtest.cpp | 13 ------------- tests/unit_test_render_submit_queue.cpp | 13 +++++++++++++ 6 files changed, 14 insertions(+), 52 deletions(-) diff --git a/includes/render_submit_queue.h b/includes/render_submit_queue.h index 1b27b97b6..642f94bd4 100644 --- a/includes/render_submit_queue.h +++ b/includes/render_submit_queue.h @@ -45,6 +45,7 @@ struct speed_stats { int make_connection(const char *spath); void check_load(void); +void display_rate(struct timeval start, struct timeval end, int num); void enqueue(const char *xmlname, int x, int y, int z); void finish_workers(void); void print_statistics(void); diff --git a/src/render_expired.c b/src/render_expired.c index c95408d65..5c6d5ef1a 100644 --- a/src/render_expired.c +++ b/src/render_expired.c @@ -54,19 +54,6 @@ static void set_tile_requested(int z, int x, int y) g_hash_table_add(tile_requested, key); } -void display_rate(struct timeval start, struct timeval end, int num) -{ - int d_s, d_us; - float sec; - - d_s = end.tv_sec - start.tv_sec; - d_us = end.tv_usec - start.tv_usec; - - sec = d_s + d_us / 1000000.0; - - g_logger(G_LOG_LEVEL_MESSAGE, "\t%d in %.2f seconds (%.2f/s)", num, sec, num / sec); -} - int main(int argc, char **argv) { const char *config_file_name_default = RENDERD_CONFIG; diff --git a/src/render_list.c b/src/render_list.c index b6dbf672d..cfb28075e 100644 --- a/src/render_list.c +++ b/src/render_list.c @@ -45,19 +45,6 @@ int lat2tiley(double lat, int z) return (int)(floor((1.0 - log(tan(latrad) + (1.0 / cos(latrad))) / M_PI) / 2.0 * pow(2.0, z))); } -void display_rate(struct timeval start, struct timeval end, int num) -{ - int d_s, d_us; - float sec; - - d_s = end.tv_sec - start.tv_sec; - d_us = end.tv_usec - start.tv_usec; - - sec = d_s + d_us / 1000000.0; - - g_logger(G_LOG_LEVEL_MESSAGE, "\t%d in %.2f seconds (%.2f/s)", num, sec, num / sec); -} - int main(int argc, char **argv) { const char *config_file_name_default = RENDERD_CONFIG; diff --git a/src/render_old.c b/src/render_old.c index c382c93b6..5072e8244 100644 --- a/src/render_old.c +++ b/src/render_old.c @@ -41,19 +41,6 @@ static int max_load; static time_t planet_timestamp = 0; static struct timeval start, end; -void display_rate(struct timeval start, struct timeval end, int num) -{ - int d_s, d_us; - float sec; - - d_s = end.tv_sec - start.tv_sec; - d_us = end.tv_usec - start.tv_usec; - - sec = d_s + d_us / 1000000.0; - - g_logger(G_LOG_LEVEL_MESSAGE, "\t%d in %.2f seconds (%.2f/s)", num, sec, num / sec); -} - static time_t get_planet_time(const char *tile_dir) { static time_t last_check; diff --git a/src/render_speedtest.cpp b/src/render_speedtest.cpp index 14d1b9301..035aa173a 100644 --- a/src/render_speedtest.cpp +++ b/src/render_speedtest.cpp @@ -102,19 +102,6 @@ class GoogleProjection } }; -void display_rate(struct timeval start, struct timeval end, int num) -{ - int d_s, d_us; - float sec; - - d_s = end.tv_sec - start.tv_sec; - d_us = end.tv_usec - start.tv_usec; - - sec = d_s + d_us / 1000000.0; - - g_logger(G_LOG_LEVEL_MESSAGE, "\t%d in %.2f seconds (%.2f/s)", num, sec, num / sec); -} - int main(int argc, char **argv) { const char *config_file_name_default = RENDERD_CONFIG; diff --git a/tests/unit_test_render_submit_queue.cpp b/tests/unit_test_render_submit_queue.cpp index d000bbf8f..3ba38f790 100644 --- a/tests/unit_test_render_submit_queue.cpp +++ b/tests/unit_test_render_submit_queue.cpp @@ -504,6 +504,19 @@ TEST_CASE("render_submit_queue.c", "[render_submit_queue]") } } + SECTION("display_rate function") + { + err_log_lines.clear(); + + struct timeval start = {1, 0}, end = {2, 500000}; + + SECTION("display_rate", "should return") + { + display_rate(start, end, 100); + REQUIRE_THAT(err_log_lines, Catch::Matchers::Contains("100 in 1.50 seconds (66.67/s)")); + } + } + SECTION("enqueue function") { err_log_lines.clear(); From 14908db75596d0441b107fccaba97532f6a50643 Mon Sep 17 00:00:00 2001 From: David Hummel <6109326+hummeltech@users.noreply.github.com> Date: Fri, 30 Oct 2026 12:26:25 -0700 Subject: [PATCH 6/7] refactor(render_old): scope max_load locally in main Remove unused file-scope static variable max_load and make it local to main(). The load limit is passed directly to spawn_workers(), which manages load throttling via render_submit_queue. Also remove unneeded sys_utils.h inclusion. --- src/render_old.c | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/render_old.c b/src/render_old.c index 5072e8244..71f55fc42 100644 --- a/src/render_old.c +++ b/src/render_old.c @@ -34,10 +34,8 @@ #include "render_submit_queue.h" #include "renderd_config.h" #include "store_file_utils.h" -#include "sys_utils.h" static int num_render = 0, num_all = 0; -static int max_load; static time_t planet_timestamp = 0; static struct timeval start, end; @@ -154,7 +152,7 @@ int main(int argc, char **argv) const char *mapname = mapname_default; const char *socketname = socketname_default; const char *tile_dir = tile_dir_default; - max_load = max_load_default; + int max_load = max_load_default; int max_zoom = max_zoom_default; int min_zoom = min_zoom_default; int num_threads = num_threads_default; From 43c6a033c79ffdd616c832b7b4bea318d371ad46 Mon Sep 17 00:00:00 2001 From: David Hummel <6109326+hummeltech@users.noreply.github.com> Date: Fri, 30 Oct 2026 14:13:05 -0700 Subject: [PATCH 7/7] fix test synchronization Ensure finish_workers() is invoked in unit_test_render_submit_queue.cpp to prevent thread race conditions when verifying error logs. --- tests/unit_test_render_submit_queue.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/unit_test_render_submit_queue.cpp b/tests/unit_test_render_submit_queue.cpp index 3ba38f790..2d03d0473 100644 --- a/tests/unit_test_render_submit_queue.cpp +++ b/tests/unit_test_render_submit_queue.cpp @@ -769,6 +769,7 @@ TEST_CASE("render_submit_queue.c", "[render_submit_queue]") { spawn_workers(thread_num, socket_path.c_str(), maxLoad); std::this_thread::sleep_for(std::chrono::milliseconds(200)); + finish_workers(); REQUIRE_THAT(err_log_lines, Catch::Matchers::Contains("Starting " + std::to_string(thread_num) + " rendering threads")); REQUIRE_THAT(err_log_lines, Catch::Matchers::Contains("connect failed for: " + socket_path)); }