Просмотр исходного кода

first test against test framework

Christian Grothoff 1 год назад
Родитель
Сommit
45fb407b49

+ 17 - 1
src/include/microhttpd2.h

@@ -5515,6 +5515,22 @@ MHD_response_from_buffer (
 MHD_FN_PAR_IN_SIZE_ (3,2);
 
 
+/**
+ * Create a response object with body that is a
+ * statically allocated buffer that never needs to
+ * be freed as its lifetime exceeds that of the
+ * daemon.
+ *
+ * The response object can be extended with header information and then be used
+ * any number of times.
+ * @param sc status code to use for the response
+ * @param len number of bytes in @a buf
+ * @param buf buffer with response payload
+ */
+#define MHD_response_from_buffer_static(sc, len, buf)       \
+        MHD_response_from_buffer (sc, len, buf, NULL, NULL)
+
+
 /**
  * Create a response object with empty (zero size) body.
  *
@@ -5523,7 +5539,7 @@ MHD_FN_PAR_IN_SIZE_ (3,2);
  * @param sc status code to use for the response
  */
 #define MHD_response_from_empty(sc) \
-        MHD_response_from_buffer (sc, 0, "", NULL, NULL)
+        MHD_response_from_buffer_static (sc, 0, "")
 
 
 /**

+ 18 - 0
src/tests/basic/Makefile.am

@@ -3,6 +3,7 @@ EMPTY_ITEM =
 
 AM_CPPFLAGS = \
   -I$(top_srcdir)/src/include \
+  -I$(top_srcdir)/src/mhd2 \
   -DMHD_CPU_COUNT=$(CPU_COUNT) \
   $(CPPFLAGS_ac)
 
@@ -16,6 +17,16 @@ if USE_COVERAGE
   AM_CFLAGS += -fprofile-arcs -ftest-coverage
 endif
 
+noinst_LTLIBRARIES = \
+  libmhdt.la
+
+libmhdt_la_SOURCES = \
+  libtest.c libtest.h \
+  libtest_convenience.c
+libmhdt_la_LIBADD = \
+  -lpthread \
+  -lcurl
+
 LDADD = $(top_builddir)/src/mhd2/libmicrohttpd2.la
 
 $(top_builddir)/src/mhd2/libmicrohttpd2.la: $(top_builddir)/src/mhd2/Makefile
@@ -23,6 +34,7 @@ $(top_builddir)/src/mhd2/libmicrohttpd2.la: $(top_builddir)/src/mhd2/Makefile
 	$(am__cd) $(top_builddir)/src/mhd2 && $(MAKE) $(AM_MAKEFLAGS) libmicrohttpd2.la
 
 check_PROGRAMS = \
+  test_client_server \
   test_create_destroy \
   test_create_start_destroy \
   test_create_destroy_ipv4 \
@@ -103,6 +115,12 @@ endif
 
 TESTS = $(check_PROGRAMS)
 
+test_client_server_SOURCES = \
+  test_client_server.c
+test_client_server_LDADD = \
+  libmhdt.la \
+  $(top_builddir)/src/mhd2/libmicrohttpd2.la
+
 # The universal sources used in all tests
 basic_test_sources = test_basic_checks.c
 

+ 583 - 0
src/tests/basic/libtest.c

@@ -0,0 +1,583 @@
+/*
+  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;
+
+  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));
+  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);
+  for (i = 0; i<num_clients; i++)
+  {
+    cctxs[i].phase = phase;
+    cctxs[i].pc = *pc;
+    cctxs[i].pc.client_id = i;
+    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
+    };
+    char c;
+
+    nanosleep (&ms,
+               NULL);
+    /* 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]));
+  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++)
+  {
+    semaphore_down (&ctx->client_sem);
+    ctx->phase++;
+    semaphore_up (&ctx->server_sem);
+  }
+  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;
+  enum MHD_StatusCode sc;
+  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;
+  }
+  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;
+  }
+  // FIXME: start some thread to run the actual server!
+
+  for (i = 0; NULL == phases[i].label; i++)
+  {
+    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 *res;
+
+    test_check (1 ==
+                write (p[1],
+                       "e",
+                       1));
+    test_check (0 ==
+                pthread_join (server_run_thr,
+                              &res));
+  }
+  {
+    void *res;
+
+    /* 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,
+                              &res));
+  }
+  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;
+}

+ 223 - 0
src/tests/basic/libtest.h

@@ -0,0 +1,223 @@
+/*
+  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);
+
+
+/**
+ * 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);
+
+
+/**
+ * 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 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

+ 124 - 0
src/tests/basic/libtest_convenience.c

@@ -0,0 +1,124 @@
+/*
+  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)
+{
+  if (MHD_SC_OK !=
+      MHD_DAEMON_SET_OPTIONS (d,
+                              MHD_D_OPTION_BIND_PORT (MHD_AF_DUAL,
+                                                      0)))
+    return "Failed to bind to port 0!";
+  return NULL;
+}
+
+
+void
+MHDT_server_run_blocking (void *cls,
+                          int finsig,
+                          struct MHD_Daemon *d)
+{
+  char e;
+
+  while (-1 ==
+         read (finsig,
+               &e,
+               1))
+  {
+    if (EAGAIN != errno)
+    {
+      fprintf (stderr,
+               "Failure reading termination signal: %s\n",
+               strerror (errno));
+      break;
+    }
+    if (MHD_SC_OK !=
+        MHD_daemon_process_blocking (d,
+                                     1000))
+    {
+      fprintf (stderr,
+               "Failure running MHD_daemon_process_blocking()\n");
+      break;
+    }
+  }
+}
+
+
+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 char *
+MHDT_client_get_root (
+  void *cls,
+  const struct MHDT_PhaseContext *pc)
+{
+  const char *text = cls;
+  CURL *c;
+  CURLcode res;
+
+  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";
+  }
+  res = curl_easy_perform (c);
+  curl_easy_cleanup (c);
+  if (CURLE_OK != res)
+    return "Failed to fetch URL";
+  (void) text; // FIXME: check data returned was 'text'
+  return NULL;
+}

+ 1 - 1
src/tests/basic/test_basic_checks.c

@@ -19,7 +19,7 @@
 */
 
 /**
- * @file test_start_destroy.c
+ * @file test_basic_checks.c
  * @brief  test for create, start and destroy
  * @author Karlson2k (Evgeny Grin)
  */

+ 55 - 0
src/tests/basic/test_client_server.c

@@ -0,0 +1,55 @@
+/*
+  This file is part of GNU libmicrohttpd
+  Copyright (C) 2016, 2024 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 "libtest.h"
+
+
+int
+main (int argc, char *argv[])
+{
+  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 = 5,
+      .num_clients = 10
+    },
+    {
+      .label = NULL,
+    },
+  };
+  (void) argc; /* Unused. Silence compiler warning. */
+  (void) argv; /* Unused. Silence compiler warning. */
+
+  return MHDT_test (&MHDT_server_setup_minimal,
+                    NULL,
+                    &MHDT_server_run_blocking,
+                    NULL,
+                    phases);
+}