Blob Blame History Raw
/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */

#include "test-utils.h"

static SoupBuffer *correct_response;

static void
authenticate (SoupSession *session, SoupMessage *msg,
	      SoupAuth *auth, gboolean retrying, gpointer data)
{
	if (!retrying)
		soup_auth_authenticate (auth, "user2", "realm2");
}

#if HAVE_APACHE
static void
get_correct_response (const char *uri)
{
	SoupSession *session;
	SoupMessage *msg;

	session = soup_test_session_new (SOUP_TYPE_SESSION_ASYNC, NULL);
	msg = soup_message_new (SOUP_METHOD_GET, uri);
	soup_session_send_message (session, msg);
	if (msg->status_code != SOUP_STATUS_OK) {
		g_printerr ("Could not fetch %s: %d %s\n", uri,
			    msg->status_code, msg->reason_phrase);
		exit (1);
	}

	correct_response = soup_message_body_flatten (msg->response_body);

	g_object_unref (msg);
	soup_test_session_abort_unref (session);
}
#endif

/* Pull API version 1: fully-async. More like a "poke" API. Rather
 * than having SoupMessage emit "got_chunk" signals whenever it wants,
 * we stop it after it finishes reading the message headers, and then
 * tell it when we want to hear about new chunks.
 */

typedef struct {
	GMainLoop *loop;
	SoupSession *session;
	SoupMessage *msg;
	guint timeout;
	gboolean chunks_ready;
	gboolean chunk_wanted;
	gboolean did_first_timeout;
	gsize read_so_far;
	guint expected_status;
} FullyAsyncData;

static void fully_async_got_headers (SoupMessage *msg, gpointer user_data);
static void fully_async_got_chunk   (SoupMessage *msg, SoupBuffer *chunk,
				     gpointer user_data);
static void fully_async_finished    (SoupSession *session, SoupMessage *msg,
				     gpointer user_data);
static gboolean fully_async_request_chunk (gpointer user_data);

static void
do_fully_async_test (SoupSession *session,
		     const char *base_uri, const char *sub_uri,
		     gboolean fast_request, guint expected_status)
{
	GMainLoop *loop;
	FullyAsyncData ad;
	SoupMessage *msg;
	char *uri;

	loop = g_main_loop_new (NULL, FALSE);

	uri = g_build_filename (base_uri, sub_uri, NULL);
	debug_printf (1, "GET %s\n", uri);

	msg = soup_message_new (SOUP_METHOD_GET, uri);
	g_free (uri);

	ad.loop = loop;
	ad.session = session;
	ad.msg = msg;
	ad.chunks_ready = FALSE;
	ad.chunk_wanted = FALSE;
	ad.did_first_timeout = FALSE;
	ad.read_so_far = 0;
	ad.expected_status = expected_status;

	/* Since we aren't going to look at the final value of
	 * msg->response_body, we tell libsoup to not even bother
	 * generating it.
	 */
	soup_message_body_set_accumulate (msg->response_body, FALSE);

	/* Connect to "got_headers", from which we'll decide where to
	 * go next.
	 */
	g_signal_connect (msg, "got_headers",
			  G_CALLBACK (fully_async_got_headers), &ad);

	/* Queue the request */
	soup_session_queue_message (session, msg, fully_async_finished, &ad);

	/* In a real program, we'd probably just return at this point.
	 * Eventually the caller would return all the way to the main
	 * loop, and then eventually, some event would cause the
	 * application to request a chunk of data from the message
	 * response.
	 *
	 * In our test program, there is no "real" main loop, so we
	 * had to create our own. We use a timeout to represent the
	 * event that causes the app to decide to request another body
	 * chunk. We use short timeouts in one set of tests, and long
	 * ones in another, to test both the
	 * chunk-requested-before-its-been-read and
	 * chunk-read-before-its-been-requested cases.
	 */
	ad.timeout = g_timeout_add (fast_request ? 0 : 100,
				    fully_async_request_chunk, &ad);
	g_main_loop_run (ad.loop);
	g_main_loop_unref (ad.loop);
}

static gboolean
fully_async_request_chunk (gpointer user_data)
{
	FullyAsyncData *ad = user_data;

	if (!ad->did_first_timeout) {
		debug_printf (1, "  first timeout\n");
		ad->did_first_timeout = TRUE;
	} else
		debug_printf (2, "  timeout\n");
	ad->timeout = 0;

	/* ad->chunks_ready and ad->chunk_wanted are used because
	 * there's a race condition between the application requesting
	 * the first chunk, and the message reaching a point where
	 * it's actually ready to read chunks. If chunks_ready has
	 * been set, we can just call soup_session_unpause_message() to
	 * cause the first chunk to be read. But if it's not, we just
	 * set chunk_wanted, to let the got_headers handler below know
	 * that a chunk has already been requested.
	 */
	if (ad->chunks_ready)
		soup_session_unpause_message (ad->session, ad->msg);
	else
		ad->chunk_wanted = TRUE;

	return FALSE;
}

static void
fully_async_got_headers (SoupMessage *msg, gpointer user_data)
{
	FullyAsyncData *ad = user_data;

	debug_printf (1, "  %d %s\n", msg->status_code, msg->reason_phrase);
	if (msg->status_code == SOUP_STATUS_UNAUTHORIZED) {
		/* Let soup handle this one; this got_headers handler
		 * will get called again next time around.
		 */
		return;
	} else if (msg->status_code != SOUP_STATUS_OK) {
		soup_test_assert_message_status (msg, SOUP_STATUS_OK);
		return;
	}

	/* OK, we're happy with the response. So, we connect to
	 * "got_chunk". If there has already been a chunk requested,
	 * we let I/O continue; but if there hasn't, we pause now
	 * until one is requested.
	 */
	ad->chunks_ready = TRUE;
	g_signal_connect (msg, "got_chunk",
			  G_CALLBACK (fully_async_got_chunk), ad);
	if (!ad->chunk_wanted)
		soup_session_pause_message (ad->session, msg);
}

static void
fully_async_got_chunk (SoupMessage *msg, SoupBuffer *chunk, gpointer user_data)
{
	FullyAsyncData *ad = user_data;

	debug_printf (2, "  got chunk from %lu - %lu\n",
		      (unsigned long) ad->read_so_far,
		      (unsigned long) ad->read_so_far + chunk->length);

	/* We've got a chunk, let's process it. In the case of the
	 * test program, that means comparing it against
	 * correct_response to make sure that we got the right data.
	 */
	g_assert_cmpint (ad->read_so_far + chunk->length, <=, correct_response->length);
	soup_assert_cmpmem (chunk->data, chunk->length,
			    correct_response->data + ad->read_so_far,
			    chunk->length);
	ad->read_so_far += chunk->length;

	/* Now pause I/O, and prepare to read another chunk later.
	 * (Again, the timeout just abstractly represents the idea of
	 * the application requesting another chunk at some random
	 * point in the future. You wouldn't be using a timeout in a
	 * real program.)
	 */
	soup_session_pause_message (ad->session, msg);
	ad->chunk_wanted = FALSE;

	ad->timeout = g_timeout_add (10, fully_async_request_chunk, ad);
}

static void
fully_async_finished (SoupSession *session, SoupMessage *msg,
		      gpointer user_data)
{
	FullyAsyncData *ad = user_data;

	soup_test_assert_message_status (msg, ad->expected_status);

	if (ad->timeout != 0)
		g_source_remove (ad->timeout);

	/* Since our test program is only running the loop for the
	 * purpose of this one test, we quit the loop once the
	 * test is done.
	 */
	g_main_loop_quit (ad->loop);
}

static void
do_fast_async_test (gconstpointer data)
{
	const char *base_uri = data;
	SoupSession *session;

	SOUP_TEST_SKIP_IF_NO_APACHE;

	session = soup_test_session_new (SOUP_TYPE_SESSION_ASYNC, NULL);
	g_signal_connect (session, "authenticate",
			  G_CALLBACK (authenticate), NULL);
	do_fully_async_test (session, base_uri, "/",
			     TRUE, SOUP_STATUS_OK);
	do_fully_async_test (session, base_uri, "/Basic/realm1/",
			     TRUE, SOUP_STATUS_UNAUTHORIZED);
	do_fully_async_test (session, base_uri, "/Basic/realm2/",
			     TRUE, SOUP_STATUS_OK);
	soup_test_session_abort_unref (session);
}

static void
do_slow_async_test (gconstpointer data)
{
	const char *base_uri = data;
	SoupSession *session;

	SOUP_TEST_SKIP_IF_NO_APACHE;

	session = soup_test_session_new (SOUP_TYPE_SESSION_ASYNC, NULL);
	g_signal_connect (session, "authenticate",
			  G_CALLBACK (authenticate), NULL);
	do_fully_async_test (session, base_uri, "/",
			     FALSE, SOUP_STATUS_OK);
	do_fully_async_test (session, base_uri, "/Basic/realm1/",
			     FALSE, SOUP_STATUS_UNAUTHORIZED);
	do_fully_async_test (session, base_uri, "/Basic/realm2/",
			     FALSE, SOUP_STATUS_OK);
	soup_test_session_abort_unref (session);
}

/* Pull API version 2: synchronous pull API via async I/O. */

typedef struct {
	GMainLoop *loop;
	SoupSession *session;
	SoupBuffer *chunk;
} SyncAsyncData;

static void        sync_async_send       (SoupSession *session,
					  SoupMessage *msg);
static gboolean    sync_async_is_finished(SoupMessage *msg);
static SoupBuffer *sync_async_read_chunk (SoupMessage *msg);
static void        sync_async_cleanup    (SoupMessage *msg);

static void sync_async_got_headers (SoupMessage *msg, gpointer user_data);
static void sync_async_copy_chunk  (SoupMessage *msg, SoupBuffer *chunk,
				    gpointer user_data);
static void sync_async_finished    (SoupSession *session, SoupMessage *msg,
				    gpointer user_data);

static void
do_synchronously_async_test (SoupSession *session,
			     const char *base_uri, const char *sub_uri,
			     guint expected_status)
{
	SoupMessage *msg;
	char *uri;
	gsize read_so_far;
	SoupBuffer *chunk;

	uri = g_build_filename (base_uri, sub_uri, NULL);
	debug_printf (1, "GET %s\n", uri);

	msg = soup_message_new (SOUP_METHOD_GET, uri);
	g_free (uri);

	/* As in the fully-async case, we turn off accumulate, as an
	 * optimization.
	 */
	soup_message_body_set_accumulate (msg->response_body, FALSE);

	/* Send the message, get back headers */
	sync_async_send (session, msg);
	if (expected_status == SOUP_STATUS_OK) {
		soup_test_assert (!sync_async_is_finished (msg),
				  "finished without reading response");
	} else {
		soup_test_assert (sync_async_is_finished (msg),
				  "request failed to fail");
	}

	/* Now we're ready to read the response body (though we could
	 * put that off until later if we really wanted).
	 */
	read_so_far = 0;
	while ((chunk = sync_async_read_chunk (msg))) {
		debug_printf (2, "  read chunk from %lu - %lu\n",
			      (unsigned long) read_so_far,
			      (unsigned long) read_so_far + chunk->length);

		g_assert_cmpint (read_so_far + chunk->length, <=, correct_response->length);
		soup_assert_cmpmem (chunk->data, chunk->length,
				    correct_response->data + read_so_far,
				    chunk->length);

		read_so_far += chunk->length;
		soup_buffer_free (chunk);
	}

	g_assert_true (sync_async_is_finished (msg));
	soup_test_assert_message_status (msg, expected_status);
	if (msg->status_code == SOUP_STATUS_OK)
		g_assert_cmpint (read_so_far, ==, correct_response->length);

	sync_async_cleanup (msg);
	g_object_unref (msg);
}

/* Sends @msg on async session @session and returns after the headers
 * of a successful response (or the complete body of a failed
 * response) have been read.
 */
static void
sync_async_send (SoupSession *session, SoupMessage *msg)
{
	SyncAsyncData *ad;

	ad = g_new0 (SyncAsyncData, 1);
	g_object_set_data (G_OBJECT (msg), "SyncAsyncData", ad);

	/* In this case, unlike the fully-async case, the loop
	 * actually belongs to us, not the application; it will only
	 * be run when we're waiting for chunks, not at other times.
	 *
	 * If session has an async_context associated with it, we'd
	 * want to pass that, rather than NULL, here.
	 */
	ad->loop = g_main_loop_new (NULL, FALSE);
	ad->session = session;

	g_signal_connect (msg, "got_headers",
			  G_CALLBACK (sync_async_got_headers), ad);

	/* Start the request by queuing it and then running our main
	 * loop. Note: we have to use soup_session_queue_message()
	 * here; soup_session_send_message() won't work, for several
	 * reasons. Also, since soup_session_queue_message() steals a
	 * ref to the message and then unrefs it after invoking the
	 * callback, we have to add an extra ref before calling it.
	 */
	g_object_ref (msg);
	soup_session_queue_message (session, msg, sync_async_finished, ad);
	g_main_loop_run (ad->loop);

	/* At this point, one of two things has happened; either the
	 * got_headers handler got headers it liked, and so stopped
	 * the loop, or else the message was fully processed without
	 * the got_headers handler interrupting it, and so the final
	 * callback (sync_async_finished) was invoked, and stopped the
	 * loop.
	 *
	 * Either way, we're done, so we return to the caller.
	 */
}

static void
sync_async_got_headers (SoupMessage *msg, gpointer user_data)
{
	SyncAsyncData *ad = user_data;

	debug_printf (1, "  %d %s\n", msg->status_code, msg->reason_phrase);
	if (msg->status_code == SOUP_STATUS_UNAUTHORIZED) {
		/* Let soup handle this one; this got_headers handler
		 * will get called again next time around.
		 */
		return;
	} else if (msg->status_code != SOUP_STATUS_OK) {
		soup_test_assert_message_status (msg, SOUP_STATUS_OK);
		return;
	}

	/* Stop I/O and return to the caller */
	soup_session_pause_message (ad->session, msg);
	g_main_loop_quit (ad->loop);
}

static gboolean
sync_async_is_finished (SoupMessage *msg)
{
	SyncAsyncData *ad = g_object_get_data (G_OBJECT (msg), "SyncAsyncData");

	/* sync_async_finished clears ad->loop */
	return ad->loop == NULL;
}

/* Tries to read a chunk. Returns %NULL on error/end-of-response. */
static SoupBuffer *
sync_async_read_chunk (SoupMessage *msg)
{
	SyncAsyncData *ad = g_object_get_data (G_OBJECT (msg), "SyncAsyncData");
	guint handler;

	if (sync_async_is_finished (msg))
		return NULL;

	ad->chunk = NULL;
	handler = g_signal_connect (msg, "got_chunk",
				    G_CALLBACK (sync_async_copy_chunk),
				    ad);
	soup_session_unpause_message (ad->session, msg);
	g_main_loop_run (ad->loop);
	g_signal_handler_disconnect (msg, handler);

	return ad->chunk;
}

static void
sync_async_copy_chunk (SoupMessage *msg, SoupBuffer *chunk, gpointer user_data)
{
	SyncAsyncData *ad = user_data;

	ad->chunk = soup_buffer_copy (chunk);

	/* Now pause and return from the g_main_loop_run() call in
	 * sync_async_read_chunk().
	 */
	soup_session_pause_message (ad->session, msg);
	g_main_loop_quit (ad->loop);
}

static void
sync_async_finished (SoupSession *session, SoupMessage *msg, gpointer user_data)
{
	SyncAsyncData *ad = user_data;

	/* Unlike in the fully_async_case, we don't need to do much
	 * here, because control will return to
	 * do_synchronously_async_test() when we're done, and we do
	 * the final tests there.
	 */
	g_main_loop_quit (ad->loop);
	g_main_loop_unref (ad->loop);
	ad->loop = NULL;
}

static void
sync_async_cleanup (SoupMessage *msg)
{
	SyncAsyncData *ad = g_object_get_data (G_OBJECT (msg), "SyncAsyncData");

	if (ad->loop)
		g_main_loop_unref (ad->loop);
	g_free (ad);
}

static void
do_sync_async_test (gconstpointer data)
{
	const char *base_uri = data;
	SoupSession *session;

	SOUP_TEST_SKIP_IF_NO_APACHE;

	session = soup_test_session_new (SOUP_TYPE_SESSION_ASYNC, NULL);
	g_signal_connect (session, "authenticate",
			  G_CALLBACK (authenticate), NULL);
	do_synchronously_async_test (session, base_uri, "/",
				     SOUP_STATUS_OK);
	do_synchronously_async_test (session, base_uri, "/Basic/realm1/",
				     SOUP_STATUS_UNAUTHORIZED);
	do_synchronously_async_test (session, base_uri, "/Basic/realm2/",
				     SOUP_STATUS_OK);
	soup_test_session_abort_unref (session);
}


int
main (int argc, char **argv)
{
	const char *base_uri;
	int ret;

	test_init (argc, argv, NULL);
	apache_init ();

	base_uri = "http://127.0.0.1:47524/";
#if HAVE_APACHE
	get_correct_response (base_uri);
#endif

	g_test_add_data_func ("/pull-api/async/fast", base_uri, do_fast_async_test);
	g_test_add_data_func ("/pull-api/async/slow", base_uri, do_slow_async_test);
	g_test_add_data_func ("/pull-api/sync-async", base_uri, do_sync_async_test);

	ret = g_test_run ();

#if HAVE_APACHE
	soup_buffer_free (correct_response);
#endif

	test_cleanup ();
	return ret;
}