commit 9873cb011aa46b45ea25c0930cce85cdd1afd8a0
parent c744d067ebcec0677cf7fb6c769b5bd72a679143
Author: Christian Grothoff <grothoff@gnunet.org>
Date: Mon, 29 Jul 2024 14:51:58 +0200
test framework + tests against test framework
Diffstat:
9 files changed, 2594 insertions(+), 1 deletion(-)
diff --git a/configure.ac b/configure.ac
@@ -7206,6 +7206,7 @@ src/include/Makefile
src/mhd2/Makefile
src/tests/Makefile
src/tests/basic/Makefile
+src/tests/client_server/Makefile
src/examples2/Makefile
])
AC_OUTPUT
diff --git a/src/tests/Makefile.am b/src/tests/Makefile.am
@@ -1,5 +1,5 @@
# This Makefile.am is in the public domain
-SUBDIRS = basic
+SUBDIRS = basic client_server
.NOTPARALLEL:
diff --git a/src/tests/client_server/Makefile.am b/src/tests/client_server/Makefile.am
@@ -0,0 +1,49 @@
+# This Makefile.am is in the public domain
+EMPTY_ITEM =
+
+AM_CPPFLAGS = \
+ -I$(top_srcdir)/src/include \
+ -I$(top_srcdir)/src/mhd2 \
+ -DMHD_CPU_COUNT=$(CPU_COUNT) \
+ $(CPPFLAGS_ac)
+
+AM_CFLAGS = $(CFLAGS_ac)
+
+AM_LDFLAGS = $(LDFLAGS_ac)
+
+AM_TESTS_ENVIRONMENT = $(TESTS_ENVIRONMENT_ac)
+
+if USE_COVERAGE
+ AM_CFLAGS += -fprofile-arcs -ftest-coverage
+endif
+
+LDADD = $(top_builddir)/src/mhd2/libmicrohttpd2.la
+LIBADD = $(top_builddir)/src/mhd2/libmicrohttpd2.la
+
+$(top_builddir)/src/mhd2/libmicrohttpd2.la: $(top_builddir)/src/mhd2/Makefile
+ @echo ' cd $(top_builddir)/src/mhd2 && $(MAKE) $(AM_MAKEFLAGS) libmicrohttpd2.la'; \
+ $(am__cd) $(top_builddir)/src/mhd2 && $(MAKE) $(AM_MAKEFLAGS) libmicrohttpd2.la
+
+check_PROGRAMS = \
+ test_client_server
+
+TESTS = $(check_PROGRAMS)
+
+
+noinst_LTLIBRARIES = \
+ libmhdt.la
+
+libmhdt_la_SOURCES = \
+ libtest.c libtest.h \
+ libtest_convenience.c \
+ libtest_convenience_client_request.c \
+ libtest_convenience_server_reply.c
+libmhdt_la_LIBADD = \
+ -lpthread \
+ -lcurl \
+ $(LIBADD)
+
+test_client_server_SOURCES = \
+ test_client_server.c
+test_client_server_LDADD = \
+ libmhdt.la
diff --git a/src/tests/client_server/libtest.c b/src/tests/client_server/libtest.c
@@ -0,0 +1,616 @@
+/*
+ This file is part of GNU libmicrohttpd
+ Copyright (C) 2024 Christian Grothoff
+
+ GNU libmicrohttpd is free software; you can redistribute it and/or
+ modify it under the terms of the GNU Lesser General Public
+ License as published by the Free Software Foundation; either
+ version 2.1 of the License, or (at your option) any later version.
+
+ GNU libmicrohttpd 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
+ Lesser General Public License for more details.
+
+ You should have received a copy of the GNU Lesser General Public
+ License along with this library; if not, write to the Free Software
+ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+
+*/
+
+/**
+ * @file libtest.c
+ * @brief testing harness with clients against server
+ * @author Christian Grothoff
+ */
+#include <pthread.h>
+#include <stdbool.h>
+#include <fcntl.h>
+#include <unistd.h>
+#include <errno.h>
+#include "microhttpd2.h"
+#include "libtest.h"
+
+/**
+ * A semaphore.
+ */
+struct Semaphore
+{
+ /**
+ * Mutex for the semaphore.
+ */
+ pthread_mutex_t mutex;
+
+ /**
+ * Condition variable for the semaphore.
+ */
+ pthread_cond_t cv;
+
+ /**
+ * Counter of the semaphore.
+ */
+ unsigned int ctr;
+};
+
+
+/**
+ * Check that @a cond is true, otherwise abort().
+ *
+ * @param cond condition to check
+ * @param filename filename to log
+ * @param line line number to log
+ */
+static void
+test_check_ (bool cond,
+ const char *filename,
+ unsigned int line)
+{
+ if (! cond)
+ {
+ fprintf (stderr,
+ "Assertion failed at %s:%u\n",
+ filename,
+ line);
+ abort ();
+ }
+}
+
+
+/**
+ * Checks that @a cond is true and otherwise aborts.
+ *
+ * @param cond condition to check
+ */
+#define test_check(cond) \
+ test_check_ (cond, __FILE__, __LINE__)
+
+
+/**
+ * Initialize a semaphore @a sem with a value of @a val.
+ *
+ * @param[out] sem semaphore to initialize
+ * @param val initial value of the semaphore
+ */
+static void
+semaphore_create (struct Semaphore *sem,
+ unsigned int val)
+{
+ test_check (0 ==
+ pthread_mutex_init (&sem->mutex,
+ NULL));
+ test_check (0 ==
+ pthread_cond_init (&sem->cv,
+ NULL));
+ sem->ctr = val;
+}
+
+
+/**
+ * Decrement semaphore, blocks until this is possible.
+ *
+ * @param[in,out] sem semaphore to decrement
+ */
+static void
+semaphore_down (struct Semaphore *sem)
+{
+ test_check (0 == pthread_mutex_lock (&sem->mutex));
+ while (0 == sem->ctr)
+ {
+ pthread_cond_wait (&sem->cv,
+ &sem->mutex);
+ }
+ sem->ctr--;
+ test_check (0 == pthread_mutex_unlock (&sem->mutex));
+}
+
+
+/**
+ * Increment semaphore, blocks until this is possible.
+ *
+ * @param[in,out] sem semaphore to decrement
+ */
+static void
+semaphore_up (struct Semaphore *sem)
+{
+ test_check (0 == pthread_mutex_lock (&sem->mutex));
+ sem->ctr++;
+ test_check (0 == pthread_mutex_unlock (&sem->mutex));
+ pthread_cond_signal (&sem->cv);
+}
+
+
+/**
+ * Release resources used by @a sem.
+ *
+ * @param[in] sem semaphore to release (except the memory itself)
+ */
+static void
+semaphore_destroy (struct Semaphore *sem)
+{
+ test_check (0 == pthread_cond_destroy (&sem->cv));
+ test_check (0 == pthread_mutex_destroy (&sem->mutex));
+}
+
+
+/**
+ * Context for the implementation of the HTTP server.
+ */
+struct ServerContext
+{
+ /**
+ * Semaphore the client raises when it goes into the
+ * next phase.
+ */
+ struct Semaphore client_sem;
+
+ /**
+ * Semaphore the server raises when it goes into the
+ * next phase.
+ */
+ struct Semaphore server_sem;
+
+ /**
+ * Current phase of the server.
+ */
+ const struct MHDT_Phase *phase;
+
+ /**
+ * Main function to run the server.
+ */
+ MHDT_ServerRunner run_cb;
+
+ /**
+ * Closure for @e run_cb.
+ */
+ void *run_cb_cls;
+
+ /**
+ * The daemon we are running.
+ */
+ struct MHD_Daemon *d;
+
+ /**
+ * Signal for server termination.
+ */
+ int finsig;
+};
+
+
+/**
+ * A client has requested the given url using the given method
+ * (#MHD_HTTP_METHOD_GET, #MHD_HTTP_METHOD_PUT,
+ * #MHD_HTTP_METHOD_DELETE, #MHD_HTTP_METHOD_POST, etc).
+ * If @a upload_size is not zero and response action is provided by this
+ * callback, then upload will be discarded and the stream (the connection for
+ * HTTP/1.1) will be closed after sending the response.
+ *
+ * @param cls argument given together with the function
+ * pointer when the handler was registered with MHD
+ * @param request the request object
+ * @param path the requested uri (without arguments after "?")
+ * @param method the HTTP method used (#MHD_HTTP_METHOD_GET,
+ * #MHD_HTTP_METHOD_PUT, etc.)
+ * @param upload_size the size of the message upload content payload,
+ * #MHD_SIZE_UNKNOWN for chunked uploads (if the
+ * final chunk has not been processed yet)
+ * @return action how to proceed, NULL
+ * if the request must be aborted due to a serious
+ * error while handling the request (implies closure
+ * of underling data stream, for HTTP/1.1 it means
+ * socket closure).
+ */
+static const struct MHD_Action *
+server_req_cb (void *cls,
+ struct MHD_Request *MHD_RESTRICT request,
+ const struct MHD_String *MHD_RESTRICT path,
+ enum MHD_HTTP_Method method,
+ uint_fast64_t upload_size)
+{
+ struct ServerContext *sc = cls;
+
+ if (NULL == sc->phase->label)
+ return NULL;
+ return sc->phase->server_cb (sc->phase->server_cb_cls,
+ request,
+ path,
+ method,
+ upload_size);
+}
+
+
+/**
+ * Closure for run_single_client()
+ */
+struct ClientContext
+{
+ /**
+ * Test phase to run.
+ */
+ const struct MHDT_Phase *phase;
+
+ /**
+ * Phase and client specific context.
+ */
+ struct MHDT_PhaseContext pc;
+
+ /**
+ * Pipe to use to signal that the thread has
+ * finished.
+ */
+ int p2;
+
+ /**
+ * Set to true on success.
+ */
+ bool status;
+};
+
+
+/**
+ * Runs the logic for a single client in a thread.
+ *
+ * @param cls a `struct ClientContext`
+ * @return NULL
+ */
+static void *
+run_single_client (void *cls)
+{
+ struct ClientContext *cc = cls;
+ const char *err;
+
+ fprintf (stderr,
+ "Client %u started in phase `%s'\n",
+ cc->pc.client_id,
+ cc->phase->label);
+ err = cc->phase->client_cb (cc->phase->client_cb_cls,
+ &cc->pc);
+ if (NULL != err)
+ {
+ fprintf (stderr,
+ "Client %u failed in phase `%s': %s\n",
+ cc->pc.client_id,
+ cc->phase->label,
+ err);
+ /* This is a blocking write, thus must succeed */
+ test_check (1 ==
+ write (cc->p2,
+ "e",
+ 1));
+ return NULL;
+ }
+ cc->status = true;
+ /* This is a blocking write, thus must succeed */
+ test_check (1 ==
+ write (cc->p2,
+ "s",
+ 1));
+ fprintf (stderr,
+ "Client %u finished in phase `%s'\n",
+ cc->pc.client_id,
+ cc->phase->label);
+ return NULL;
+}
+
+
+/**
+ * Creates a pipe with a non-blocking read end.
+ *
+ * @param p pipe to initialize
+ */
+static void
+make_pipe (int p[2])
+{
+ int flags;
+
+ test_check (0 ==
+ pipe (p));
+ flags = fcntl (p[0],
+ F_GETFL);
+ flags |= O_NONBLOCK;
+ test_check (0 ==
+ fcntl (p[0],
+ F_SETFL,
+ flags));
+}
+
+
+/**
+ * Run client processes for the given test @a phase
+ *
+ * @param phase test phase to run
+ * @param pc context to give to clients
+ */
+static bool
+run_client_phase (const struct MHDT_Phase *phase,
+ const struct MHDT_PhaseContext *pc)
+{
+ unsigned int num_clients
+ = (0 == phase->num_clients)
+ ? 1
+ : phase->num_clients;
+ unsigned int clients_left = 0;
+ struct ClientContext cctxs[num_clients];
+ pthread_t clients[num_clients];
+ int p[2];
+ unsigned int i;
+ bool ret = true;
+
+ make_pipe (p);
+ fprintf (stderr,
+ "Starting phase `%s'\n",
+ phase->label);
+ for (i = 0; i<num_clients; i++)
+ {
+ cctxs[i].phase = phase;
+ cctxs[i].pc = *pc;
+ cctxs[i].pc.client_id = i;
+ cctxs[i].p2 = p[1];
+ cctxs[i].status = false;
+ if (0 !=
+ pthread_create (&clients[i],
+ NULL,
+ &run_single_client,
+ &cctxs[i]))
+ goto cleanup;
+ clients_left++;
+ }
+
+ /* 0 for timeout_ms means no timeout, we deliberately
+ underflow to MAX_UINT in this case... */
+ for (i = phase->timeout_ms - 1; i>0; i--)
+ {
+ struct timespec ms = {
+ .tv_nsec = 1000 * 1000
+ };
+ struct timespec rem;
+ char c;
+
+ if (0 != nanosleep (&ms,
+ &rem))
+ {
+ fprintf (stderr,
+ "nanosleep() interrupted (%s), trying again\n",
+ strerror (errno));
+ i++;
+ }
+ /* This is a non-blocking read */
+ while (1 == read (p[0],
+ &c,
+ 1))
+ clients_left--;
+ if (0 == clients_left)
+ break;
+ }
+ if (0 != clients_left)
+ {
+ fprintf (stderr,
+ "Timeout (%u ms) in phase `%s': %u clients still running\n",
+ phase->timeout_ms,
+ phase->label,
+ clients_left);
+ exit (1);
+ }
+cleanup:
+ for (i = 0; i<num_clients; i++)
+ {
+ void *res;
+
+ test_check (0 ==
+ pthread_join (clients[i],
+ &res));
+ if (! cctxs[i].status)
+ ret = false;
+ }
+ test_check (0 == close (p[0]));
+ test_check (0 == close (p[1]));
+ fprintf (stderr,
+ "Finished phase `%s' with %s\n",
+ phase->label,
+ ret ? "success" : "FAILURE");
+ return ret;
+}
+
+
+/**
+ * Thread that switches the server to the next phase
+ * as needed.
+ *
+ * @param cls a `struct ServerContext`
+ * @return NULL
+ */
+static void *
+server_phase_logic (void *cls)
+{
+ struct ServerContext *ctx = cls;
+ unsigned int i;
+
+ for (i = 0; NULL != ctx->phase->label; i++)
+ {
+ fprintf (stderr,
+ "Running server phase `%s'\n",
+ ctx->phase->label);
+ semaphore_down (&ctx->client_sem);
+ ctx->phase++;
+ semaphore_up (&ctx->server_sem);
+ }
+ fprintf (stderr,
+ "Server terminating\n");
+ return NULL;
+}
+
+
+/**
+ * Thread that runs the MHD daemon.
+ *
+ * @param cls a `struct ServerContext`
+ * @return NULL
+ */
+static void *
+server_run_logic (void *cls)
+{
+ struct ServerContext *ctx = cls;
+
+ ctx->run_cb (ctx->run_cb_cls,
+ ctx->finsig,
+ ctx->d);
+ return NULL;
+}
+
+
+int
+MHDT_test (MHDT_ServerSetup ss_cb,
+ void *ss_cb_cls,
+ MHDT_ServerRunner run_cb,
+ void *run_cb_cls,
+ const struct MHDT_Phase *phases)
+{
+ struct ServerContext ctx = {
+ .run_cb = run_cb,
+ .run_cb_cls = run_cb_cls,
+ .phase = &phases[0]
+ };
+ struct MHD_Daemon *d;
+ int res;
+ const char *err;
+ pthread_t server_phase_thr;
+ pthread_t server_run_thr;
+ struct MHDT_PhaseContext pc;
+ char base_url[128];
+ unsigned int i;
+ int p[2];
+
+ make_pipe (p);
+ semaphore_create (&ctx.server_sem,
+ 0);
+ semaphore_create (&ctx.client_sem,
+ 0);
+ d = MHD_daemon_create (&server_req_cb,
+ &ctx);
+ if (NULL == d)
+ exit (77);
+ err = ss_cb (ss_cb_cls,
+ d);
+ if (NULL != err)
+ {
+ fprintf (stderr,
+ "Failed to setup server: %s\n",
+ err);
+ return 1;
+ }
+ {
+ enum MHD_StatusCode sc;
+
+ sc = MHD_daemon_start (d);
+ if (MHD_SC_OK != sc)
+ {
+ fprintf (stderr,
+ "Failed to start server: %s\n",
+ err);
+ return 1;
+ }
+ }
+ {
+ union MHD_DaemonInfoFixedData info;
+ enum MHD_StatusCode sc;
+
+ sc = MHD_daemon_get_info_fixed (
+ d,
+ MHD_DAEMON_INFO_FIXED_BIND_PORT,
+ &info);
+ test_check (MHD_SC_OK == sc);
+ snprintf (base_url,
+ sizeof (base_url),
+ "http://localhost:%u/",
+ (unsigned int) info.v_port);
+ pc.base_url = base_url;
+ }
+ if (0 != pthread_create (&server_phase_thr,
+ NULL,
+ &server_phase_logic,
+ &ctx))
+ {
+ fprintf (stderr,
+ "Failed to start server phase thread: %s\n",
+ strerror (errno));
+ return 77;
+ }
+ ctx.finsig = p[0];
+ ctx.d = d;
+ if (0 != pthread_create (&server_run_thr,
+ NULL,
+ &server_run_logic,
+ &ctx))
+ {
+ fprintf (stderr,
+ "Failed to start server run thread: %s\n",
+ strerror (errno));
+ return 77;
+ }
+ for (i = 0; NULL != phases[i].label; i++)
+ {
+ fprintf (stderr,
+ "Running test phase `%s'\n",
+ phases[i].label);
+ if (! run_client_phase (&phases[i],
+ &pc))
+ {
+ res = 1;
+ goto cleanup;
+ }
+ /* client is done with phase */
+ semaphore_up (&ctx.client_sem);
+ /* wait for server to have moved to new phase */
+ semaphore_down (&ctx.server_sem);
+ }
+ res = 0;
+cleanup:
+ /* stop thread that runs the actual server */
+ {
+ void *pres;
+
+ test_check (1 ==
+ write (p[1],
+ "e",
+ 1));
+ test_check (0 ==
+ pthread_join (server_run_thr,
+ &pres));
+ }
+ {
+ void *pres;
+
+ /* Unblock the #server_phase_logic() even if we had
+ an error */
+ for (i = 0; NULL != phases[i].label; i++)
+ semaphore_up (&ctx.client_sem);
+ test_check (0 ==
+ pthread_join (server_phase_thr,
+ &pres));
+ }
+ MHD_daemon_destroy (d);
+ semaphore_destroy (&ctx.client_sem);
+ semaphore_destroy (&ctx.server_sem);
+ test_check (0 == close (p[0]));
+ test_check (0 == close (p[1]));
+ return res;
+}
diff --git a/src/tests/client_server/libtest.h b/src/tests/client_server/libtest.h
@@ -0,0 +1,478 @@
+/*
+ This file is part of GNU libmicrohttpd
+ Copyright (C) 2024 Christian Grothoff
+
+ GNU libmicrohttpd is free software; you can redistribute it and/or
+ modify it under the terms of the GNU Lesser General Public
+ License as published by the Free Software Foundation; either
+ version 2.1 of the License, or (at your option) any later version.
+
+ GNU libmicrohttpd 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
+ Lesser General Public License for more details.
+
+ You should have received a copy of the GNU Lesser General Public
+ License along with this library; if not, write to the Free Software
+ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+
+*/
+
+/**
+ * @file libtest.h
+ * @brief testing harness with clients against server
+ * @author Christian Grothoff
+ */
+#ifndef LIBTEST_H
+#define LIBTEST_H
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+#include <microhttpd2.h>
+
+/**
+ * Information about the current phase.
+ */
+struct MHDT_PhaseContext
+{
+ /**
+ * Base URL of the server
+ */
+ const char *base_url;
+
+ /**
+ * Specific client we are running.
+ */
+ unsigned int client_id;
+};
+
+
+/**
+ * Function called to run some client logic against
+ * the server.
+ *
+ * @param cls closure
+ * @param pc context for the client
+ * @return error message, NULL on success
+ */
+typedef const char *
+(*MHDT_ClientLogic)(void *cls,
+ const struct MHDT_PhaseContext *pc);
+
+
+/**
+ * Run request against the base URL and expect the
+ * string in @a cls to be returned
+ *
+ * @param cls closure with text string to be returned
+ * @param pc context for the client
+ * @return error message, NULL on success
+ */
+const char *
+MHDT_client_get_root (void *cls,
+ const struct MHDT_PhaseContext *pc);
+
+
+/**
+ * Run request against the base URL with the
+ * query arguments from @a cls appended to it.
+ * Expect the server to return a 200 OK response.
+ *
+ * @param cls closure with query parameters to append
+ * to the base URL of the server
+ * @param pc context for the client
+ * @return error message, NULL on success
+ */
+const char *
+MHDT_client_get_with_query (void *cls,
+ const struct MHDT_PhaseContext *pc);
+
+
+/**
+ * Run request against the base URL with the
+ * custom header from @a cls set.
+ * Expect the server to return a 204 No content response.
+ *
+ * @param cls closure with custom header to set
+ * @param pc context for the client
+ * @return error message, NULL on success
+ */
+const char *
+MHDT_client_set_header (void *cls,
+ const struct MHDT_PhaseContext *pc);
+
+
+/**
+ * Run request against the base URL and expect the header from @a cls to be
+ * set in the 204 No content response.
+ *
+ * @param cls closure with custom header to set,
+ * must be of the format "$KEY:$VALUE"
+ * without space before the "$VALUE".
+ * @param pc context for the client
+ * @return error message, NULL on success
+ */
+const char *
+MHDT_client_expect_header (void *cls,
+ const struct MHDT_PhaseContext *pc);
+
+
+/**
+ * Run simple upload against the base URL and expect a
+ * 204 No Content response.
+ *
+ * @param cls 0-terminated string with data to PUT
+ * @param pc context for the client
+ * @return error message, NULL on success
+ */
+const char *
+MHDT_client_put_data (void *cls,
+ const struct MHDT_PhaseContext *pc);
+
+
+/**
+ * Run chunked upload against the base URL and expect a
+ * 204 No Content response.
+ *
+ * @param cls 0-terminated string with data to PUT
+ * @param pc context for the client
+ * @return error message, NULL on success
+ */
+const char *
+MHDT_client_chunk_data (void *cls,
+ const struct MHDT_PhaseContext *pc);
+
+
+/**
+ * A phase defines some server and client-side
+ * behaviors to execute.
+ */
+struct MHDT_Phase
+{
+
+ /**
+ * Name of the phase, for debugging/logging.
+ */
+ const char *label;
+
+ /**
+ * Logic for the MHD server for this phase.
+ */
+ MHD_RequestCallback server_cb;
+
+ /**
+ * Closure for @e server_cb.
+ */
+ void *server_cb_cls;
+
+ /**
+ * Logic for the CURL client for this phase.
+ */
+ MHDT_ClientLogic client_cb;
+
+ /**
+ * Closure for @e client_cb.
+ */
+ void *client_cb_cls;
+
+ /**
+ * How long is the phase allowed to run at most before
+ * timing out. 0 for no timeout.
+ */
+ unsigned int timeout_ms;
+
+ /**
+ * How many clients should be run in parallel.
+ * 0 to run just one client.
+ */
+ unsigned int num_clients;
+};
+
+
+/**
+ * Returns the text from @a cls as the response to any
+ * request.
+ *
+ * @param cls argument given together with the function
+ * pointer when the handler was registered with MHD
+ * @param request the request object
+ * @param path the requested uri (without arguments after "?")
+ * @param method the HTTP method used (#MHD_HTTP_METHOD_GET,
+ * #MHD_HTTP_METHOD_PUT, etc.)
+ * @param upload_size the size of the message upload content payload,
+ * #MHD_SIZE_UNKNOWN for chunked uploads (if the
+ * final chunk has not been processed yet)
+ * @return action how to proceed, NULL
+ * if the request must be aborted due to a serious
+ * error while handling the request (implies closure
+ * of underling data stream, for HTTP/1.1 it means
+ * socket closure).
+ */
+const struct MHD_Action *
+MHDT_server_reply_text (
+ void *cls,
+ struct MHD_Request *MHD_RESTRICT request,
+ const struct MHD_String *MHD_RESTRICT path,
+ enum MHD_HTTP_Method method,
+ uint_fast64_t upload_size);
+
+
+/**
+ * Returns the text from @a cls as the response to any
+ * request, but using chunks by returning @a cls
+ * word-wise (breaking into chunks at spaces).
+ *
+ * @param cls argument given together with the function
+ * pointer when the handler was registered with MHD
+ * @param request the request object
+ * @param path the requested uri (without arguments after "?")
+ * @param method the HTTP method used (#MHD_HTTP_METHOD_GET,
+ * #MHD_HTTP_METHOD_PUT, etc.)
+ * @param upload_size the size of the message upload content payload,
+ * #MHD_SIZE_UNKNOWN for chunked uploads (if the
+ * final chunk has not been processed yet)
+ * @return action how to proceed, NULL
+ * if the request must be aborted due to a serious
+ * error while handling the request (implies closure
+ * of underling data stream, for HTTP/1.1 it means
+ * socket closure).
+ */
+const struct MHD_Action *
+MHDT_server_reply_chunked_text (
+ void *cls,
+ struct MHD_Request *MHD_RESTRICT request,
+ const struct MHD_String *MHD_RESTRICT path,
+ enum MHD_HTTP_Method method,
+ uint_fast64_t upload_size);
+
+
+/**
+ * Returns writes text from @a cls to a temporary file
+ * and then uses the file descriptor to serve the
+ * content to the client.
+ *
+ * @param cls argument given together with the function
+ * pointer when the handler was registered with MHD
+ * @param request the request object
+ * @param path the requested uri (without arguments after "?")
+ * @param method the HTTP method used (#MHD_HTTP_METHOD_GET,
+ * #MHD_HTTP_METHOD_PUT, etc.)
+ * @param upload_size the size of the message upload content payload,
+ * #MHD_SIZE_UNKNOWN for chunked uploads (if the
+ * final chunk has not been processed yet)
+ * @return action how to proceed, NULL
+ * if the request must be aborted due to a serious
+ * error while handling the request (implies closure
+ * of underling data stream, for HTTP/1.1 it means
+ * socket closure).
+ */
+const struct MHD_Action *
+MHDT_server_reply_file (
+ void *cls,
+ struct MHD_Request *MHD_RESTRICT request,
+ const struct MHD_String *MHD_RESTRICT path,
+ enum MHD_HTTP_Method method,
+ uint_fast64_t upload_size);
+
+
+/**
+ * Returns an emtpy response with a custom header
+ * set from @a cls and the #MHD_HTTP_STATUS_NO_CONTENT.
+ *
+ * @param cls header in the format "$NAME:$VALUE"
+ * without a space before "$VALUE".
+ * @param request the request object
+ * @param path the requested uri (without arguments after "?")
+ * @param method the HTTP method used (#MHD_HTTP_METHOD_GET,
+ * #MHD_HTTP_METHOD_PUT, etc.)
+ * @param upload_size the size of the message upload content payload,
+ * #MHD_SIZE_UNKNOWN for chunked uploads (if the
+ * final chunk has not been processed yet)
+ * @return action how to proceed, NULL
+ * if the request must be aborted due to a serious
+ * error while handling the request (implies closure
+ * of underling data stream, for HTTP/1.1 it means
+ * socket closure).
+ */
+const struct MHD_Action *
+MHDT_server_reply_with_header (
+ void *cls,
+ struct MHD_Request *MHD_RESTRICT request,
+ const struct MHD_String *MHD_RESTRICT path,
+ enum MHD_HTTP_Method method,
+ uint_fast64_t upload_size);
+
+
+/**
+ * Checks that the request query arguments match the
+ * arguments given in @a cls.
+ * request.
+ *
+ * @param cls string with expected arguments separated by '&' and '='. URI encoding is NOT supported.
+ * @param request the request object
+ * @param path the requested uri (without arguments after "?")
+ * @param method the HTTP method used (#MHD_HTTP_METHOD_GET,
+ * #MHD_HTTP_METHOD_PUT, etc.)
+ * @param upload_size the size of the message upload content payload,
+ * #MHD_SIZE_UNKNOWN for chunked uploads (if the
+ * final chunk has not been processed yet)
+ * @return action how to proceed, NULL
+ * if the request must be aborted due to a serious
+ * error while handling the request (implies closure
+ * of underling data stream, for HTTP/1.1 it means
+ * socket closure).
+ */
+const struct MHD_Action *
+MHDT_server_reply_check_query (
+ void *cls,
+ struct MHD_Request *MHD_RESTRICT request,
+ const struct MHD_String *MHD_RESTRICT path,
+ enum MHD_HTTP_Method method,
+ uint_fast64_t upload_size);
+
+
+/**
+ * Checks that the client request includes the given
+ * custom header. If so, returns #MHD_HTTP_STATUS_NO_CONTENT.
+ *
+ * @param cls expected header with "$NAME:$VALUE" format.
+ * @param request the request object
+ * @param path the requested uri (without arguments after "?")
+ * @param method the HTTP method used (#MHD_HTTP_METHOD_GET,
+ * #MHD_HTTP_METHOD_PUT, etc.)
+ * @param upload_size the size of the message upload content payload,
+ * #MHD_SIZE_UNKNOWN for chunked uploads (if the
+ * final chunk has not been processed yet)
+ * @return action how to proceed, NULL
+ * if the request must be aborted due to a serious
+ * error while handling the request (implies closure
+ * of underling data stream, for HTTP/1.1 it means
+ * socket closure).
+ */
+const struct MHD_Action *
+MHDT_server_reply_check_header (
+ void *cls,
+ struct MHD_Request *MHD_RESTRICT request,
+ const struct MHD_String *MHD_RESTRICT path,
+ enum MHD_HTTP_Method method,
+ uint_fast64_t upload_size);
+
+
+/**
+ * Checks that the client request includes the given
+ * upload. If so, returns #MHD_HTTP_STATUS_NO_CONTENT.
+ *
+ * @param cls expected upload data as a 0-terminated string.
+ * @param request the request object
+ * @param path the requested uri (without arguments after "?")
+ * @param method the HTTP method used (#MHD_HTTP_METHOD_GET,
+ * #MHD_HTTP_METHOD_PUT, etc.)
+ * @param upload_size the size of the message upload content payload,
+ * #MHD_SIZE_UNKNOWN for chunked uploads (if the
+ * final chunk has not been processed yet)
+ * @return action how to proceed, NULL
+ * if the request must be aborted due to a serious
+ * error while handling the request (implies closure
+ * of underling data stream, for HTTP/1.1 it means
+ * socket closure).
+ */
+const struct MHD_Action *
+MHDT_server_reply_check_upload (
+ void *cls,
+ struct MHD_Request *MHD_RESTRICT request,
+ const struct MHD_String *MHD_RESTRICT path,
+ enum MHD_HTTP_Method method,
+ uint_fast64_t upload_size);
+
+
+/**
+ * Initialize options for an MHD daemon for a test.
+ *
+ * @param cls closure
+ * @param[in,out] d daemon to initialize
+ * @return error message, NULL on success
+ */
+typedef const char *
+(*MHDT_ServerSetup)(void *cls,
+ struct MHD_Daemon *d);
+
+
+/**
+ * Initialize MHD daemon without any special
+ * options, binding to any free port.
+ *
+ * @param cls closure
+ * @param[in,out] d daemon to initialize
+ * @return error message, NULL on success
+ */
+const char *
+MHDT_server_setup_minimal (void *cls,
+ struct MHD_Daemon *d);
+
+
+/**
+ * Function that runs an MHD daemon until
+ * a read() against @a finsig succeeds.
+ *
+ * @param cls closure
+ * @param finsig fd to read from to detect termination request
+ * @param[in,out] d daemon to run
+ */
+typedef void
+(*MHDT_ServerRunner)(void *cls,
+ int finsig,
+ struct MHD_Daemon *d);
+
+
+/**
+ * Function that starts an MHD daemon with the
+ * simple #MHD_daemon_start() method until
+ * a read() against @a finsig succeeds.
+ *
+ * @param cls closure, pass a NULL-terminated (!)
+ * array of `struct MHD_DaemonOptionAndValue` with the
+ * the threading mode to use
+ * @param finsig fd to read from to detect termination request
+ * @param[in,out] d daemon to run
+ */
+void
+MHDT_server_run_minimal (void *cls,
+ int finsig,
+ struct MHD_Daemon *d);
+
+
+/**
+ * Function that runs an MHD daemon in blocking mode until
+ * a read() against @a finsig succeeds.
+ *
+ * @param cls closure
+ * @param finsig fd to read from to detect termination request
+ * @param[in,out] d daemon to run
+ */
+void
+MHDT_server_run_blocking (void *cls,
+ int finsig,
+ struct MHD_Daemon *d);
+
+
+/**
+ * Run test suite with @a phases for a daemon initialized
+ * using @a ss_cb on the local machine.
+ *
+ * @param ss_cb setup logic for the daemon
+ * @param ss_cb_cls closure for @a ss_cb
+ * @param run_cb runs the daemon
+ * @param run_cb_cls closure for @a run_cb
+ * @param phases test phases to run in child processes
+ * @return 0 on success, 77 if test was skipped,
+ * error code otherwise
+ */
+int
+MHDT_test (MHDT_ServerSetup ss_cb,
+ void *ss_cb_cls,
+ MHDT_ServerRunner run_cb,
+ void *run_cb_cls,
+ const struct MHDT_Phase *phases);
+
+#endif
diff --git a/src/tests/client_server/libtest_convenience.c b/src/tests/client_server/libtest_convenience.c
@@ -0,0 +1,150 @@
+/*
+ This file is part of GNU libmicrohttpd
+ Copyright (C) 2024 Christian Grothoff
+
+ GNU libmicrohttpd is free software; you can redistribute it and/or
+ modify it under the terms of the GNU Lesser General Public
+ License as published by the Free Software Foundation; either
+ version 2.1 of the License, or (at your option) any later version.
+
+ GNU libmicrohttpd 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
+ Lesser General Public License for more details.
+
+ You should have received a copy of the GNU Lesser General Public
+ License along with this library; if not, write to the Free Software
+ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+
+*/
+
+/**
+ * @file libtest_convenience.c
+ * @brief convenience functions for libtest users
+ * @author Christian Grothoff
+ */
+#include <pthread.h>
+#include <stdbool.h>
+#include <fcntl.h>
+#include <unistd.h>
+#include <errno.h>
+#include "microhttpd2.h"
+#include "libtest.h"
+#include <curl/curl.h>
+
+
+const char *
+MHDT_server_setup_minimal (void *cls,
+ struct MHD_Daemon *d)
+{
+ const struct MHD_DaemonOptionAndValue *options = cls;
+
+ if (MHD_SC_OK !=
+ MHD_daemon_set_options (
+ d,
+ options,
+ MHD_OPTIONS_ARRAY_MAX_SIZE))
+ return "Failed to configure threading mode!";
+ if (MHD_SC_OK !=
+ MHD_DAEMON_SET_OPTIONS (
+ d,
+ MHD_D_OPTION_BIND_PORT (MHD_AF_AUTO,
+ 0)))
+ return "Failed to bind to port 0!";
+ return NULL;
+}
+
+
+void
+MHDT_server_run_minimal (void *cls,
+ int finsig,
+ struct MHD_Daemon *d)
+{
+ fd_set r;
+ char c;
+
+ FD_ZERO (&r);
+ FD_SET (finsig, &r);
+ while (1)
+ {
+ if ( (-1 ==
+ select (finsig + 1,
+ &r,
+ NULL,
+ NULL,
+ NULL)) &&
+ (EAGAIN != errno) )
+ {
+ fprintf (stderr,
+ "Failure waiting on termination signal: %s\n",
+ strerror (errno));
+ break;
+ }
+ if (FD_ISSET (finsig,
+ &r))
+ break;
+ }
+ if ( (FD_ISSET (finsig,
+ &r)) &&
+ (1 != read (finsig,
+ &c,
+ 1)) )
+ {
+ fprintf (stderr,
+ "Failed to drain termination signal\n");
+ }
+}
+
+
+void
+MHDT_server_run_blocking (void *cls,
+ int finsig,
+ struct MHD_Daemon *d)
+{
+ fd_set r;
+ char c;
+
+ FD_ZERO (&r);
+ FD_SET (finsig, &r);
+ while (1)
+ {
+ struct timeval timeout = {
+ .tv_usec = 1000 /* 1000 microseconds */
+ };
+
+ if ( (-1 ==
+ select (finsig + 1,
+ &r,
+ NULL,
+ NULL,
+ &timeout)) &&
+ (EAGAIN != errno) )
+ {
+ fprintf (stderr,
+ "Failure waiting on termination signal: %s\n",
+ strerror (errno));
+ break;
+ }
+#if FIXME
+ if (MHD_SC_OK !=
+ MHD_daemon_process_blocking (d,
+ 1000))
+ {
+ fprintf (stderr,
+ "Failure running MHD_daemon_process_blocking()\n");
+ break;
+ }
+#else
+ abort ();
+#endif
+ }
+ if ( (FD_ISSET (finsig,
+ &r)) &&
+ (1 != read (finsig,
+ &c,
+ 1)) )
+ {
+ fprintf (stderr,
+ "Failed to drain termination signal\n");
+ }
+}
diff --git a/src/tests/client_server/libtest_convenience_client_request.c b/src/tests/client_server/libtest_convenience_client_request.c
@@ -0,0 +1,568 @@
+/*
+ This file is part of GNU libmicrohttpd
+ Copyright (C) 2024 Christian Grothoff
+
+ GNU libmicrohttpd is free software; you can redistribute it and/or
+ modify it under the terms of the GNU Lesser General Public
+ License as published by the Free Software Foundation; either
+ version 2.1 of the License, or (at your option) any later version.
+
+ GNU libmicrohttpd 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
+ Lesser General Public License for more details.
+
+ You should have received a copy of the GNU Lesser General Public
+ License along with this library; if not, write to the Free Software
+ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+
+*/
+
+/**
+ * @file libtest_convenience.c
+ * @brief convenience functions implementing clients making requests for libtest users
+ * @author Christian Grothoff
+ */
+#include <pthread.h>
+#include <stdbool.h>
+#include <fcntl.h>
+#include <unistd.h>
+#include <errno.h>
+#include "microhttpd2.h"
+#include "libtest.h"
+#include <curl/curl.h>
+
+/**
+ * Closure for the write_cb().
+ */
+struct WriteBuffer
+{
+ /**
+ * Where to store the response.
+ */
+ char *buf;
+
+ /**
+ * Number of bytes in @e buf.
+ */
+ size_t len;
+
+ /**
+ * Current write offset in @e buf.
+ */
+ size_t pos;
+
+ /**
+ * Set to non-zero on errors (buffer full).
+ */
+ int err;
+};
+
+
+/**
+ * Callback for CURLOPT_WRITEFUNCTION processing
+ * data downloaded from the HTTP server.
+ *
+ * @param ptr data uploaded
+ * @param size size of a member
+ * @param nmemb number of members
+ * @param stream must be a `struct WriteBuffer`
+ * @return bytes processed (size*nmemb) or error
+ */
+static size_t
+write_cb (void *ptr,
+ size_t size,
+ size_t nmemb,
+ void *stream)
+{
+ struct WriteBuffer *wb = stream;
+ size_t prod = size * nmemb;
+
+ if ( (prod / size != nmemb) ||
+ (wb->pos + prod < wb->pos) ||
+ (wb->pos + prod > wb->len) )
+ {
+ wb->err = 1;
+ return CURLE_WRITE_ERROR;
+ }
+ memcpy (wb->buf + wb->pos,
+ ptr,
+ prod);
+ wb->pos += prod;
+ return prod;
+}
+
+
+/**
+ * Declare variables needed to check a download.
+ *
+ * @param text text data we expect to receive
+ */
+#define DECLARE_WB(text) \
+ size_t wb_tlen = strlen (text); \
+ char wb_buf[wb_tlen]; \
+ struct WriteBuffer wb = { \
+ .buf = wb_buf, \
+ .len = wb_tlen \
+ }
+
+
+/**
+ * Set CURL options to the write_cb() and wb buffer
+ * to check a download.
+ *
+ * @param c CURL handle
+ */
+#define SETUP_WB(c) do { \
+ if (CURLE_OK != \
+ curl_easy_setopt (c, \
+ CURLOPT_WRITEFUNCTION, \
+ &write_cb)) \
+ { \
+ curl_easy_cleanup (c); \
+ return "Failed to set write callback for curl request"; \
+ } \
+ if (CURLE_OK != \
+ curl_easy_setopt (c, \
+ CURLOPT_WRITEDATA, \
+ &wb)) \
+ { \
+ curl_easy_cleanup (c); \
+ return "Failed to set write buffer for curl request"; \
+ } \
+} while (0)
+
+/**
+ * Check that we received the expected text.
+ *
+ * @param text text we expect to have downloaded
+ */
+#define CHECK_WB(text) do { \
+ if ( (wb_tlen != wb.pos) || \
+ (0 != wb.err) || \
+ (0 != memcmp (text, \
+ wb_buf, \
+ wb_tlen)) ) \
+ return "Downloaded data does not match expectations"; \
+} while (0)
+
+
+/**
+ * Perform the curl request @a c and cleanup and
+ * return an error if the request failed.
+ *
+ * @param c request to perform
+ */
+#define PERFORM_REQUEST(c) do { \
+ CURLcode res; \
+ res = curl_easy_perform (c); \
+ if (CURLE_OK != res) \
+ { \
+ curl_easy_cleanup (c); \
+ return "Failed to fetch URL"; \
+ } \
+} while (0)
+
+/**
+ * Check that the curl request @a c completed
+ * with the @a want status code.
+ * Return an error if the status does not match.
+ *
+ * @param c request to check
+ * @param want desired HTTP status code
+ */
+#define CHECK_STATUS(c,want) do { \
+ if (! check_status (c, want)) \
+ { \
+ curl_easy_cleanup (c); \
+ return "Unexpected HTTP status"; \
+ } \
+} while (0)
+
+/**
+ * Chec that the HTTP status of @a c matches @a expected_status
+ *
+ * @param a completed CURL request
+ * @param expected_status the expected HTTP response code
+ * @return true if the status matches
+ */
+static bool
+check_status (CURL *c,
+ unsigned int expected_status)
+{
+ long status;
+
+ if (CURLE_OK !=
+ curl_easy_getinfo (c,
+ CURLINFO_RESPONSE_CODE,
+ &status))
+ {
+ fprintf (stderr,
+ "Failed to get HTTP status");
+ return false;
+ }
+ if (status != expected_status)
+ {
+ fprintf (stderr,
+ "Expected HTTP status %u, got %ld\n",
+ expected_status,
+ status);
+ return false;
+ }
+ return true;
+}
+
+
+const char *
+MHDT_client_get_root (
+ void *cls,
+ const struct MHDT_PhaseContext *pc)
+{
+ const char *text = cls;
+ CURL *c;
+ DECLARE_WB (text);
+
+ c = curl_easy_init ();
+ if (NULL == c)
+ return "Failed to initialize Curl handle";
+ if (CURLE_OK !=
+ curl_easy_setopt (c,
+ CURLOPT_URL,
+ pc->base_url))
+ {
+ curl_easy_cleanup (c);
+ return "Failed to set URL for curl request";
+ }
+ SETUP_WB (c);
+ PERFORM_REQUEST (c);
+ CHECK_STATUS (c, MHD_HTTP_STATUS_OK);
+ curl_easy_cleanup (c);
+ CHECK_WB (text);
+ return NULL;
+}
+
+
+const char *
+MHDT_client_get_with_query (
+ void *cls,
+ const struct MHDT_PhaseContext *pc)
+{
+ const char *args = cls;
+ size_t alen = strlen (args);
+ CURL *c;
+ size_t blen = strlen (pc->base_url);
+ char u[alen + blen + 1];
+
+ memcpy (u,
+ pc->base_url,
+ blen);
+ memcpy (u + blen,
+ args,
+ alen);
+ u[alen + blen] = '\0';
+ c = curl_easy_init ();
+ if (NULL == c)
+ return "Failed to initialize Curl handle";
+
+ if (CURLE_OK !=
+ curl_easy_setopt (c,
+ CURLOPT_URL,
+ u))
+ {
+ curl_easy_cleanup (c);
+ return "Failed to set URL for curl request";
+ }
+ PERFORM_REQUEST (c);
+ CHECK_STATUS (c,
+ MHD_HTTP_STATUS_NO_CONTENT);
+ curl_easy_cleanup (c);
+ return NULL;
+}
+
+
+const char *
+MHDT_client_set_header (void *cls,
+ const struct MHDT_PhaseContext *pc)
+{
+ const char *hdr = cls;
+ CURL *c;
+ CURLcode res;
+ struct curl_slist *slist;
+
+ c = curl_easy_init ();
+ if (NULL == c)
+ return "Failed to initialize Curl handle";
+ if (CURLE_OK !=
+ curl_easy_setopt (c,
+ CURLOPT_URL,
+ pc->base_url))
+ {
+ curl_easy_cleanup (c);
+ return "Failed to set URL for curl request";
+ }
+ slist = curl_slist_append (NULL,
+ hdr);
+ if (CURLE_OK !=
+ curl_easy_setopt (c,
+ CURLOPT_HTTPHEADER,
+ slist))
+ {
+ curl_easy_cleanup (c);
+ curl_slist_free_all (slist);
+ return "Failed to set custom header for curl request";
+ }
+ res = curl_easy_perform (c);
+ curl_slist_free_all (slist);
+ if (CURLE_OK != res)
+ {
+ curl_easy_cleanup (c);
+ return "Failed to fetch URL";
+ }
+ CHECK_STATUS (c,
+ MHD_HTTP_STATUS_NO_CONTENT);
+ curl_easy_cleanup (c);
+ return NULL;
+}
+
+
+const char *
+MHDT_client_expect_header (void *cls,
+ const struct MHDT_PhaseContext *pc)
+{
+ const char *hdr = cls;
+ size_t hlen = strlen (hdr) + 1;
+ char key[hlen];
+ const char *colon = strchr (hdr, ':');
+ const char *value;
+ CURL *c;
+ bool found = false;
+
+ if (NULL == colon)
+ return "Invalid expected header passed";
+ memcpy (key,
+ hdr,
+ hlen);
+ key[colon - hdr] = '\0';
+ value = &key[colon - hdr + 1];
+ c = curl_easy_init ();
+ if (NULL == c)
+ return "Failed to initialize Curl handle";
+ if (CURLE_OK !=
+ curl_easy_setopt (c,
+ CURLOPT_URL,
+ pc->base_url))
+ {
+ curl_easy_cleanup (c);
+ return "Failed to set URL for curl request";
+ }
+ PERFORM_REQUEST (c);
+ CHECK_STATUS (c,
+ MHD_HTTP_STATUS_NO_CONTENT);
+ for (size_t index = 0; ! found; index++)
+ {
+ CURLHcode rval;
+ struct curl_header *hout;
+
+ rval = curl_easy_header (c,
+ key,
+ index,
+ CURLH_HEADER,
+ -1 /* last request */,
+ &hout);
+ if (CURLHE_BADINDEX == rval)
+ break;
+ found = (0 == strcmp (value,
+ hout->value));
+ }
+ if (! found)
+ {
+ curl_easy_cleanup (c);
+ return "Expected HTTP response header not found";
+ }
+ curl_easy_cleanup (c);
+ return NULL;
+}
+
+
+/**
+ * Closure for the read_cb().
+ */
+struct ReadBuffer
+{
+ /**
+ * Origin of data to upload.
+ */
+ const char *buf;
+
+ /**
+ * Number of bytes in @e buf.
+ */
+ size_t len;
+
+ /**
+ * Current read offset in @e buf.
+ */
+ size_t pos;
+
+ /**
+ * Number of chunks to user when sending.
+ */
+ unsigned int chunks;
+
+};
+
+
+/**
+ * Callback for CURLOPT_READFUNCTION for uploading
+ * data to the HTTP server.
+ *
+ * @param ptr data uploaded
+ * @param size size of a member
+ * @param nmemb number of members
+ * @param stream must be a `struct ReadBuffer`
+ * @return bytes processed (size*nmemb) or error
+ */
+static size_t
+read_cb (void *ptr,
+ size_t size,
+ size_t nmemb,
+ void *stream)
+{
+ struct ReadBuffer *rb = stream;
+ size_t limit = size * nmemb;
+
+ if (limit / size != nmemb)
+ return CURLE_WRITE_ERROR;
+ if (limit > rb->len - rb->pos)
+ limit = rb->len - rb->pos;
+ if ( (rb->chunks > 1) &&
+ (limit > 1) )
+ {
+ limit /= rb->chunks;
+ rb->chunks--;
+ }
+ memcpy (ptr,
+ rb->buf + rb->pos,
+ limit);
+ rb->pos += limit;
+ return limit;
+}
+
+
+const char *
+MHDT_client_put_data (
+ void *cls,
+ const struct MHDT_PhaseContext *pc)
+{
+ const char *text = cls;
+ struct ReadBuffer rb = {
+ .buf = text,
+ .len = strlen (text)
+ };
+ CURL *c;
+
+ c = curl_easy_init ();
+ if (NULL == c)
+ return "Failed to initialize Curl handle";
+ if (CURLE_OK !=
+ curl_easy_setopt (c,
+ CURLOPT_URL,
+ pc->base_url))
+ {
+ curl_easy_cleanup (c);
+ return "Failed to set URL for curl request";
+ }
+ if (CURLE_OK !=
+ curl_easy_setopt (c,
+ CURLOPT_UPLOAD,
+ 1L))
+ {
+ curl_easy_cleanup (c);
+ return "Failed to set PUT method for curl request";
+ }
+ if (CURLE_OK !=
+ curl_easy_setopt (c,
+ CURLOPT_READFUNCTION,
+ &read_cb))
+ {
+ curl_easy_cleanup (c);
+ return "Failed to set READFUNCTION for curl request";
+ }
+ if (CURLE_OK !=
+ curl_easy_setopt (c,
+ CURLOPT_READDATA,
+ &rb))
+ {
+ curl_easy_cleanup (c);
+ return "Failed to set READFUNCTION for curl request";
+ }
+ if (CURLE_OK !=
+ curl_easy_setopt (c,
+ CURLOPT_INFILESIZE_LARGE,
+ (curl_off_t) rb.len))
+ {
+ curl_easy_cleanup (c);
+ return "Failed to set INFILESIZE_LARGE for curl request";
+ }
+ PERFORM_REQUEST (c);
+ CHECK_STATUS (c,
+ MHD_HTTP_STATUS_NO_CONTENT);
+ curl_easy_cleanup (c);
+ return NULL;
+}
+
+
+const char *
+MHDT_client_chunk_data (
+ void *cls,
+ const struct MHDT_PhaseContext *pc)
+{
+ const char *text = cls;
+ struct ReadBuffer rb = {
+ .buf = text,
+ .len = strlen (text),
+ .chunks = 2
+ };
+ CURL *c;
+
+ c = curl_easy_init ();
+ if (NULL == c)
+ return "Failed to initialize Curl handle";
+ if (CURLE_OK !=
+ curl_easy_setopt (c,
+ CURLOPT_URL,
+ pc->base_url))
+ {
+ curl_easy_cleanup (c);
+ return "Failed to set URL for curl request";
+ }
+ if (CURLE_OK !=
+ curl_easy_setopt (c,
+ CURLOPT_UPLOAD,
+ 1L))
+ {
+ curl_easy_cleanup (c);
+ return "Failed to set PUT method for curl request";
+ }
+ if (CURLE_OK !=
+ curl_easy_setopt (c,
+ CURLOPT_READFUNCTION,
+ &read_cb))
+ {
+ curl_easy_cleanup (c);
+ return "Failed to set READFUNCTION for curl request";
+ }
+ if (CURLE_OK !=
+ curl_easy_setopt (c,
+ CURLOPT_READDATA,
+ &rb))
+ {
+ curl_easy_cleanup (c);
+ return "Failed to set READFUNCTION for curl request";
+ }
+ PERFORM_REQUEST (c);
+ CHECK_STATUS (c,
+ MHD_HTTP_STATUS_NO_CONTENT);
+ curl_easy_cleanup (c);
+ return NULL;
+}
diff --git a/src/tests/client_server/libtest_convenience_server_reply.c b/src/tests/client_server/libtest_convenience_server_reply.c
@@ -0,0 +1,450 @@
+/*
+ This file is part of GNU libmicrohttpd
+ Copyright (C) 2024 Christian Grothoff
+
+ GNU libmicrohttpd is free software; you can redistribute it and/or
+ modify it under the terms of the GNU Lesser General Public
+ License as published by the Free Software Foundation; either
+ version 2.1 of the License, or (at your option) any later version.
+
+ GNU libmicrohttpd 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
+ Lesser General Public License for more details.
+
+ You should have received a copy of the GNU Lesser General Public
+ License along with this library; if not, write to the Free Software
+ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+
+*/
+
+/**
+ * @file libtest_convenience_server_reply.c
+ * @brief convenience functions that generate
+ * replies from the server for libtest users
+ * @author Christian Grothoff
+ */
+#include <pthread.h>
+#include <stdbool.h>
+#include <fcntl.h>
+#include <stdio.h>
+#include <unistd.h>
+#include <errno.h>
+#include "microhttpd2.h"
+#include "libtest.h"
+#include <curl/curl.h>
+
+
+const struct MHD_Action *
+MHDT_server_reply_text (
+ void *cls,
+ struct MHD_Request *MHD_RESTRICT request,
+ const struct MHD_String *MHD_RESTRICT path,
+ enum MHD_HTTP_Method method,
+ uint_fast64_t upload_size)
+{
+ const char *text = cls;
+
+ return MHD_action_from_response (
+ request,
+ MHD_response_from_buffer_static (MHD_HTTP_STATUS_OK,
+ strlen (text),
+ text));
+}
+
+
+const struct MHD_Action *
+MHDT_server_reply_file (
+ void *cls,
+ struct MHD_Request *MHD_RESTRICT request,
+ const struct MHD_String *MHD_RESTRICT path,
+ enum MHD_HTTP_Method method,
+ uint_fast64_t upload_size)
+{
+ const char *text = cls;
+ size_t tlen = strlen (text);
+ char fn[] = "/tmp/mhd-test-XXXXXX";
+ int fd;
+
+ fd = mkstemp (fn);
+ if (-1 == fd)
+ {
+ fprintf (stderr,
+ "Failed to mkstemp() temporary file\n");
+ return NULL;
+ }
+ if (tlen != write (fd, text, tlen))
+ {
+ fprintf (stderr,
+ "Failed to write() temporary file in one go: %s\n",
+ strerror (errno));
+ return NULL;
+ }
+ fsync (fd);
+ if (0 != remove (fn))
+ {
+ fprintf (stderr,
+ "Failed to remove() temporary file %s: %s\n",
+ fn,
+ strerror (errno));
+ }
+ return MHD_action_from_response (
+ request,
+ MHD_response_from_fd (MHD_HTTP_STATUS_OK,
+ fd,
+ 0 /* offset */,
+ tlen));
+}
+
+
+const struct MHD_Action *
+MHDT_server_reply_with_header (
+ void *cls,
+ struct MHD_Request *MHD_RESTRICT request,
+ const struct MHD_String *MHD_RESTRICT path,
+ enum MHD_HTTP_Method method,
+ uint_fast64_t upload_size)
+{
+ const char *header = cls;
+ size_t hlen = strlen (header) + 1;
+ char name[hlen];
+ const char *colon = strchr (header, ':');
+ const char *value;
+ struct MHD_Response *resp;
+
+ memcpy (name,
+ header,
+ hlen);
+ name[colon - header] = '\0';
+ value = &name[colon - header + 1];
+
+ resp = MHD_response_from_empty (MHD_HTTP_STATUS_NO_CONTENT);
+ if (MHD_SC_OK !=
+ MHD_response_add_header (resp,
+ name,
+ value))
+ return NULL;
+ return MHD_action_from_response (
+ request,
+ resp);
+}
+
+
+const struct MHD_Action *
+MHDT_server_reply_check_query (
+ void *cls,
+ struct MHD_Request *MHD_RESTRICT request,
+ const struct MHD_String *MHD_RESTRICT path,
+ enum MHD_HTTP_Method method,
+ uint_fast64_t upload_size)
+{
+ const char *equery = cls;
+ size_t qlen = strlen (equery) + 1;
+ char qc[qlen];
+
+ memcpy (qc,
+ equery,
+ qlen);
+ for (const char *tok = strtok (qc, "&");
+ NULL != tok;
+ tok = strtok (NULL, "&"))
+ {
+ const char *end;
+ const struct MHD_StringNullable *sn;
+ const char *val;
+
+ end = strchr (tok, '=');
+ if (NULL == end)
+ {
+ end = &tok[strlen (tok)];
+ val = NULL;
+ }
+ else
+ {
+ val = end + 1;
+ }
+ {
+ size_t alen = end - tok;
+ char arg[alen + 1];
+
+ memcpy (arg,
+ tok,
+ alen);
+ arg[alen] = '\0';
+ sn = MHD_request_get_value (request,
+ MHD_VK_GET_ARGUMENT,
+ arg);
+ if (NULL == sn)
+ {
+ fprintf (stderr,
+ "NULL returned for query key %s\n",
+ arg);
+ return NULL;
+ }
+ if (NULL == val)
+ {
+ if (NULL != sn->cstr)
+ {
+ fprintf (stderr,
+ "NULL expected for value for query key %s, got %s\n",
+ arg,
+ sn->cstr);
+ return NULL;
+ }
+ }
+ else
+ {
+ if (NULL == sn->cstr)
+ {
+ fprintf (stderr,
+ "%s expected for value for query key %s, got NULL\n",
+ val,
+ arg);
+ return NULL;
+ }
+ if (0 != strcmp (val,
+ sn->cstr))
+ {
+ fprintf (stderr,
+ "%s expected for value for query key %s, got %s\n",
+ val,
+ arg,
+ sn->cstr);
+ return NULL;
+ }
+ }
+ }
+ }
+
+ return MHD_action_from_response (
+ request,
+ MHD_response_from_empty (
+ MHD_HTTP_STATUS_NO_CONTENT));
+}
+
+
+const struct MHD_Action *
+MHDT_server_reply_check_header (
+ void *cls,
+ struct MHD_Request *MHD_RESTRICT request,
+ const struct MHD_String *MHD_RESTRICT path,
+ enum MHD_HTTP_Method method,
+ uint_fast64_t upload_size)
+{
+ const char *want = cls;
+ size_t wlen = strlen (want) + 1;
+ char key[wlen];
+ const char *colon = strchr (want, ':');
+ const struct MHD_StringNullable *have;
+ const char *value;
+
+ memcpy (key,
+ want,
+ wlen);
+ if (NULL != colon)
+ {
+ key[colon - want] = '\0';
+ value = &key[colon - want + 1];
+ }
+ else
+ {
+ value = NULL;
+ }
+ have = MHD_request_get_value (request,
+ MHD_VK_HEADER,
+ key);
+ if (NULL == have)
+ {
+ fprintf (stderr,
+ "Missing client header `%s'\n",
+ want);
+ return NULL;
+ }
+ if (NULL == value)
+ {
+ if (NULL != have->cstr)
+ {
+ fprintf (stderr,
+ "Have unexpected client header `%s': `%s'\n",
+ key,
+ have->cstr);
+ return NULL;
+ }
+ }
+ else
+ {
+ if (NULL == have->cstr)
+ {
+ fprintf (stderr,
+ "Missing value for client header `%s'\n",
+ want);
+ return NULL;
+ }
+ if (0 != strcmp (have->cstr,
+ value))
+ {
+ fprintf (stderr,
+ "Client HTTP header `%s' was expected to be `%s' but is `%s'\n",
+ key,
+ value,
+ have->cstr);
+ return NULL;
+ }
+ }
+ return MHD_action_from_response (
+ request,
+ MHD_response_from_empty (
+ MHD_HTTP_STATUS_NO_CONTENT));
+}
+
+
+/**
+ * Function to process data uploaded by a client.
+ *
+ * @param cls the payload we expect to be uploaded as a 0-terminated string
+ * @param request the request is being processed
+ * @param content_data_size the size of the @a content_data,
+ * zero when all data have been processed
+ * @param[in] content_data the uploaded content data,
+ * may be modified in the callback,
+ * valid only until return from the callback,
+ * NULL when all data have been processed
+ * @return action specifying how to proceed:
+ * #MHD_upload_action_continue() to continue upload (for incremental
+ * upload processing only),
+ * #MHD_upload_action_suspend() to stop reading the upload until
+ * the request is resumed,
+ * #MHD_upload_action_abort_request() to close the socket,
+ * or a response to discard the rest of the upload and transmit
+ * the response
+ * @ingroup action
+ */
+static const struct MHD_UploadAction *
+check_upload_cb (void *cls,
+ struct MHD_Request *request,
+ size_t content_data_size,
+ void *content_data)
+{
+ const char *want = cls;
+ size_t wlen = strlen (want);
+
+ if (content_data_size != wlen)
+ {
+ fprintf (stderr,
+ "Invalid body size given to full upload callback\n");
+ return NULL;
+ }
+ if (0 != memcmp (want,
+ content_data,
+ wlen))
+ {
+ fprintf (stderr,
+ "Invalid body data given to full upload callback\n");
+ return NULL;
+ }
+ /* success! */
+ return MHD_upload_action_from_response (
+ request,
+ MHD_response_from_empty (
+ MHD_HTTP_STATUS_NO_CONTENT));
+}
+
+
+const struct MHD_Action *
+MHDT_server_reply_check_upload (
+ void *cls,
+ struct MHD_Request *MHD_RESTRICT request,
+ const struct MHD_String *MHD_RESTRICT path,
+ enum MHD_HTTP_Method method,
+ uint_fast64_t upload_size)
+{
+ const char *want = cls;
+ size_t wlen = strlen (want);
+
+ return MHD_action_process_upload_full (request,
+ wlen,
+ &check_upload_cb,
+ (void *) want);
+}
+
+
+/**
+ * Closure for #chunk_return.
+ */
+struct ChunkContext
+{
+ /**
+ * Where we are in the buffer.
+ */
+ const char *pos;
+};
+
+
+/**
+ * Function that returns a string in chunks.
+ *
+ * @param dyn_cont_cls must be a `struct ChunkContext`
+ * @param ctx the context to produce the action to return,
+ * the pointer is only valid until the callback returns
+ * @param pos position in the datastream to access;
+ * note that if a `struct MHD_Response` object is re-used,
+ * it is possible for the same content reader to
+ * be queried multiple times for the same data;
+ * however, if a `struct MHD_Response` is not re-used,
+ * libmicrohttpd guarantees that "pos" will be
+ * the sum of all data sizes provided by this callback
+ * @param[out] buf where to copy the data
+ * @param max maximum number of bytes to copy to @a buf (size of @a buf)
+ * @return action to use,
+ * NULL in case of any error (the response will be aborted)
+ */
+static const struct MHD_DynamicContentCreatorAction *
+chunk_return (void *cls,
+ struct MHD_DynamicContentCreatorContext *ctx,
+ uint_fast64_t pos,
+ void *buf,
+ size_t max)
+{
+ struct ChunkContext *cc = cls;
+ size_t imax = strlen (cc->pos);
+ const char *space = strchr (cc->pos, ' ');
+
+ if (0 == imax)
+ return MHD_DCC_action_finish (ctx);
+ if (NULL != space)
+ imax = space - cc->pos + 1;
+ if (imax > max)
+ imax = max;
+ memcpy (buf,
+ cc->pos,
+ imax);
+ cc->pos += imax;
+ return MHD_DCC_action_continue (ctx,
+ imax);
+}
+
+
+const struct MHD_Action *
+MHDT_server_reply_chunked_text (
+ void *cls,
+ struct MHD_Request *MHD_RESTRICT request,
+ const struct MHD_String *MHD_RESTRICT path,
+ enum MHD_HTTP_Method method,
+ uint_fast64_t upload_size)
+{
+ const char *text = cls;
+ struct ChunkContext *cc;
+
+ cc = malloc (sizeof (struct ChunkContext));
+ if (NULL == cc)
+ return NULL;
+ cc->pos = text;
+
+ return MHD_action_from_response (
+ request,
+ MHD_response_from_callback (MHD_HTTP_STATUS_OK,
+ MHD_SIZE_UNKNOWN,
+ &chunk_return,
+ cc,
+ &free));
+}
diff --git a/src/tests/client_server/test_client_server.c b/src/tests/client_server/test_client_server.c
@@ -0,0 +1,281 @@
+/*
+ This file is part of GNU libmicrohttpd
+ Copyright (C) 2016, 2024 Christian Grothoff & Evgeny Grin (Karlson2k)
+
+ GNU libmicrohttpd is free software; you can redistribute it and/or
+ modify it under the terms of the GNU Lesser General Public
+ License as published by the Free Software Foundation; either
+ version 2.1 of the License, or (at your option) any later version.
+
+ GNU libmicrohttpd 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
+ Lesser General Public License for more details.
+
+ You should have received a copy of the GNU Lesser General Public
+ License along with this library; if not, write to the Free Software
+ Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+
+*/
+
+/**
+ * @file test_client_server.c
+ * @brief test with client against server
+ * @author Christian Grothoff
+ */
+#include <microhttpd2.h>
+#include "mhd_config.h"
+#include "libtest.h"
+
+
+int
+main (int argc, char *argv[])
+{
+ struct MHD_DaemonOptionAndValue thread1select[] = {
+ MHD_D_OPTION_POLL_SYSCALL (MHD_SPS_SELECT),
+ MHD_D_OPTION_WM_WORKER_THREADS (1),
+ MHD_D_OPTION_TERMINATE ()
+ };
+ struct MHD_DaemonOptionAndValue thread2select[] = {
+ MHD_D_OPTION_POLL_SYSCALL (MHD_SPS_SELECT),
+ MHD_D_OPTION_WM_WORKER_THREADS (2),
+ MHD_D_OPTION_TERMINATE ()
+ };
+ struct MHD_DaemonOptionAndValue thread1poll[] = {
+ MHD_D_OPTION_POLL_SYSCALL (MHD_SPS_POLL),
+ MHD_D_OPTION_WM_WORKER_THREADS (1),
+ MHD_D_OPTION_TERMINATE ()
+ };
+ struct MHD_DaemonOptionAndValue thread2poll[] = {
+ MHD_D_OPTION_POLL_SYSCALL (MHD_SPS_POLL),
+ MHD_D_OPTION_WM_WORKER_THREADS (1),
+ MHD_D_OPTION_TERMINATE ()
+ };
+ struct MHD_DaemonOptionAndValue thread1epoll[] = {
+ MHD_D_OPTION_POLL_SYSCALL (MHD_SPS_EPOLL),
+ MHD_D_OPTION_WM_WORKER_THREADS (1),
+ MHD_D_OPTION_TERMINATE ()
+ };
+ struct MHD_DaemonOptionAndValue thread2epoll[] = {
+ MHD_D_OPTION_POLL_SYSCALL (MHD_SPS_EPOLL),
+ MHD_D_OPTION_WM_WORKER_THREADS (1),
+ MHD_D_OPTION_TERMINATE ()
+ };
+ struct MHD_DaemonOptionAndValue thread1auto[] = {
+ MHD_D_OPTION_POLL_SYSCALL (MHD_SPS_AUTO),
+ MHD_D_OPTION_WM_WORKER_THREADS (1),
+ MHD_D_OPTION_TERMINATE ()
+ };
+ struct MHD_DaemonOptionAndValue external0auto[] = {
+ MHD_D_OPTION_POLL_SYSCALL (MHD_SPS_AUTO),
+ MHD_D_OPTION_WM_EXTERNAL_PERIODIC (),
+ MHD_D_OPTION_TERMINATE ()
+ };
+ struct ServerType
+ {
+ const char *label;
+ MHDT_ServerSetup server_setup;
+ void *server_setup_cls;
+ MHDT_ServerRunner server_runner;
+ void *server_runner_cls;
+ } configs[] = {
+#ifdef MHD_USE_SELECT
+ {
+ .label = "single threaded select",
+ .server_setup = &MHDT_server_setup_minimal,
+ .server_setup_cls = thread1select,
+ .server_runner = &MHDT_server_run_minimal,
+ },
+ {
+ .label = "multi-threaded select",
+ .server_setup = &MHDT_server_setup_minimal,
+ .server_setup_cls = thread2select,
+ .server_runner = &MHDT_server_run_minimal,
+ },
+#endif
+#ifdef MHD_USE_POLL
+ {
+ .label = "single threaded poll",
+ .server_setup = &MHDT_server_setup_minimal,
+ .server_setup_cls = thread1poll,
+ .server_runner = &MHDT_server_run_minimal,
+ },
+ {
+ .label = "multi-threaded poll",
+ .server_setup = &MHDT_server_setup_minimal,
+ .server_setup_cls = thread2poll,
+ .server_runner = &MHDT_server_run_minimal,
+ },
+#endif
+#if MHD_USE_EPOLL
+ {
+ .label = "single threaded epoll",
+ .server_setup = &MHDT_server_setup_minimal,
+ .server_setup_cls = thread1epoll,
+ .server_runner = &MHDT_server_run_minimal,
+ },
+ {
+ .label = "multi-threaded epoll",
+ .server_setup = &MHDT_server_setup_minimal,
+ .server_setup_cls = thread2epoll,
+ .server_runner = &MHDT_server_run_minimal,
+ },
+#endif
+ {
+ .label = "auto-selected mode, single threaded",
+ .server_setup = &MHDT_server_setup_minimal,
+ .server_setup_cls = thread1auto,
+ .server_runner = &MHDT_server_run_minimal,
+ },
+#if 1
+ /* FIXME: remove once MHD_daemon_process_blocking
+ has been implemented */
+ {
+ .label = "END"
+ },
+#endif
+ {
+ .label = "auto-selected external event loop mode, no threads",
+ .server_setup = &MHDT_server_setup_minimal,
+ .server_setup_cls = external0auto,
+ .server_runner = &MHDT_server_run_blocking,
+ },
+ {
+ .label = "END"
+ }
+ };
+ struct MHDT_Phase phases[] = {
+ {
+ .label = "simple get",
+ .server_cb = &MHDT_server_reply_text,
+ .server_cb_cls = "Hello world",
+ .client_cb = &MHDT_client_get_root,
+ .client_cb_cls = "Hello world",
+ .timeout_ms = 2500,
+ },
+ {
+ .label = "GET with sendfile",
+ .server_cb = &MHDT_server_reply_file,
+ .server_cb_cls = "Hello world",
+ .client_cb = &MHDT_client_get_root,
+ .client_cb_cls = "Hello world",
+ .timeout_ms = 2500,
+ },
+ {
+ .label = "client PUT with content-length",
+ .server_cb = &MHDT_server_reply_check_upload,
+ .server_cb_cls = "simple-upload-value",
+ .client_cb = &MHDT_client_put_data,
+ .client_cb_cls = "simple-upload-value",
+ .timeout_ms = 2500,
+ },
+ {
+ .label = "client PUT with 2 chunks",
+ .server_cb = &MHDT_server_reply_check_upload,
+ .server_cb_cls = "chunky-upload-value",
+ .client_cb = &MHDT_client_chunk_data,
+ .client_cb_cls = "chunky-upload-value",
+ .timeout_ms = 2500,
+ },
+ {
+ .label = "client request with custom header",
+ .server_cb = &MHDT_server_reply_check_header,
+ .server_cb_cls = "C-Header:testvalue",
+ .client_cb = &MHDT_client_set_header,
+ .client_cb_cls = "C-Header:testvalue",
+ .timeout_ms = 2500,
+ },
+ {
+ .label = "server response with custom header",
+ .server_cb = &MHDT_server_reply_with_header,
+ .server_cb_cls = "X-Header:testvalue",
+ .client_cb = &MHDT_client_expect_header,
+ .client_cb_cls = "X-Header:testvalue",
+ .timeout_ms = 2500,
+ },
+ {
+ .label = "URL with query parameters 1",
+ .server_cb = &MHDT_server_reply_check_query,
+ .server_cb_cls = "a=b&c",
+ .client_cb = &MHDT_client_get_with_query,
+ .client_cb_cls = "?a=b&c",
+ .timeout_ms = 5000,
+ .num_clients = 4
+ },
+ {
+ .label = "URL with query parameters 2",
+ .server_cb = &MHDT_server_reply_check_query,
+ .server_cb_cls = "a=b&c", /* a => b, c => NULL */
+ .client_cb = &MHDT_client_get_with_query,
+ .client_cb_cls = "?c&a=b",
+ .timeout_ms = 5000,
+ .num_clients = 1
+ },
+ {
+ .label = "URL with query parameters 3",
+ .server_cb = &MHDT_server_reply_check_query,
+ .server_cb_cls = "a=&c", /* a => "", c => NULL */
+ .client_cb = &MHDT_client_get_with_query,
+ .client_cb_cls = "?c&a=",
+ .timeout_ms = 5000,
+ .num_clients = 1
+ },
+ {
+ .label = "URL with query parameters 4",
+ .server_cb = &MHDT_server_reply_check_query,
+ .server_cb_cls = "a=", /* a => "" */
+ .client_cb = &MHDT_client_get_with_query,
+ .client_cb_cls = "?a=",
+ .timeout_ms = 5000,
+ .num_clients = 1
+ },
+ {
+ .label = "URL with query parameters 5",
+ .server_cb = &MHDT_server_reply_check_query,
+ .server_cb_cls = "a=b", /* a => "b" */
+ .client_cb = &MHDT_client_get_with_query,
+ .client_cb_cls = "?a=b",
+ .timeout_ms = 5000,
+ .num_clients = 1
+ },
+ {
+ .label = "chunked response get",
+ .server_cb = &MHDT_server_reply_chunked_text,
+ .server_cb_cls = "Hello world",
+ .client_cb = &MHDT_client_get_root,
+ .client_cb_cls = "Hello world",
+ .timeout_ms = 2500,
+ },
+ // TODO: chunked download
+ {
+ .label = NULL,
+ },
+ };
+ unsigned int i;
+
+ (void) argc; /* Unused. Silence compiler warning. */
+ (void) argv; /* Unused. Silence compiler warning. */
+
+ for (i = 0; NULL != configs[i].server_setup; i++)
+ {
+ int ret;
+
+ fprintf (stderr,
+ "Running tests with server setup `%s'\n",
+ configs[i].label);
+ ret = MHDT_test (configs[i].server_setup,
+ configs[i].server_setup_cls,
+ configs[i].server_runner,
+ configs[i].server_runner_cls,
+ phases);
+ if (0 != ret)
+ {
+ fprintf (stderr,
+ "Test failed with server of type `%s' (%u)\n",
+ configs[i].label,
+ i);
+ return ret;
+ }
+ }
+ return 0;
+}