1 /* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
2
3 #include "test-utils.h"
4
5 static SoupBuffer *correct_response;
6
7 static void
authenticate(SoupSession * session,SoupMessage * msg,SoupAuth * auth,gboolean retrying,gpointer data)8 authenticate (SoupSession *session, SoupMessage *msg,
9 SoupAuth *auth, gboolean retrying, gpointer data)
10 {
11 if (!retrying)
12 soup_auth_authenticate (auth, "user2", "realm2");
13 }
14
15 #ifdef HAVE_APACHE
16 static void
get_correct_response(const char * uri)17 get_correct_response (const char *uri)
18 {
19 SoupSession *session;
20 SoupMessage *msg;
21
22 session = soup_test_session_new (SOUP_TYPE_SESSION_ASYNC, NULL);
23 msg = soup_message_new (SOUP_METHOD_GET, uri);
24 soup_session_send_message (session, msg);
25 if (msg->status_code != SOUP_STATUS_OK) {
26 g_printerr ("Could not fetch %s: %d %s\n", uri,
27 msg->status_code, msg->reason_phrase);
28 exit (1);
29 }
30
31 correct_response = soup_message_body_flatten (msg->response_body);
32
33 g_object_unref (msg);
34 soup_test_session_abort_unref (session);
35 }
36 #endif
37
38 /* Pull API version 1: fully-async. More like a "poke" API. Rather
39 * than having SoupMessage emit "got_chunk" signals whenever it wants,
40 * we stop it after it finishes reading the message headers, and then
41 * tell it when we want to hear about new chunks.
42 */
43
44 typedef struct {
45 GMainLoop *loop;
46 SoupSession *session;
47 SoupMessage *msg;
48 guint timeout;
49 gboolean chunks_ready;
50 gboolean chunk_wanted;
51 gboolean did_first_timeout;
52 gsize read_so_far;
53 guint expected_status;
54 } FullyAsyncData;
55
56 static void fully_async_got_headers (SoupMessage *msg, gpointer user_data);
57 static void fully_async_got_chunk (SoupMessage *msg, SoupBuffer *chunk,
58 gpointer user_data);
59 static void fully_async_finished (SoupSession *session, SoupMessage *msg,
60 gpointer user_data);
61 static gboolean fully_async_request_chunk (gpointer user_data);
62
63 static void
do_fully_async_test(SoupSession * session,const char * base_uri,const char * sub_uri,gboolean fast_request,guint expected_status)64 do_fully_async_test (SoupSession *session,
65 const char *base_uri, const char *sub_uri,
66 gboolean fast_request, guint expected_status)
67 {
68 GMainLoop *loop;
69 FullyAsyncData ad;
70 SoupMessage *msg;
71 char *uri;
72
73 loop = g_main_loop_new (NULL, FALSE);
74
75 uri = g_build_filename (base_uri, sub_uri, NULL);
76 debug_printf (1, "GET %s\n", uri);
77
78 msg = soup_message_new (SOUP_METHOD_GET, uri);
79 g_free (uri);
80
81 ad.loop = loop;
82 ad.session = session;
83 ad.msg = msg;
84 ad.chunks_ready = FALSE;
85 ad.chunk_wanted = FALSE;
86 ad.did_first_timeout = FALSE;
87 ad.read_so_far = 0;
88 ad.expected_status = expected_status;
89
90 /* Since we aren't going to look at the final value of
91 * msg->response_body, we tell libsoup to not even bother
92 * generating it.
93 */
94 soup_message_body_set_accumulate (msg->response_body, FALSE);
95
96 /* Connect to "got_headers", from which we'll decide where to
97 * go next.
98 */
99 g_signal_connect (msg, "got_headers",
100 G_CALLBACK (fully_async_got_headers), &ad);
101
102 /* Queue the request */
103 soup_session_queue_message (session, msg, fully_async_finished, &ad);
104
105 /* In a real program, we'd probably just return at this point.
106 * Eventually the caller would return all the way to the main
107 * loop, and then eventually, some event would cause the
108 * application to request a chunk of data from the message
109 * response.
110 *
111 * In our test program, there is no "real" main loop, so we
112 * had to create our own. We use a timeout to represent the
113 * event that causes the app to decide to request another body
114 * chunk. We use short timeouts in one set of tests, and long
115 * ones in another, to test both the
116 * chunk-requested-before-its-been-read and
117 * chunk-read-before-its-been-requested cases.
118 */
119 ad.timeout = g_timeout_add (fast_request ? 0 : 100,
120 fully_async_request_chunk, &ad);
121 g_main_loop_run (ad.loop);
122 g_main_loop_unref (ad.loop);
123 }
124
125 static gboolean
fully_async_request_chunk(gpointer user_data)126 fully_async_request_chunk (gpointer user_data)
127 {
128 FullyAsyncData *ad = user_data;
129
130 if (!ad->did_first_timeout) {
131 debug_printf (1, " first timeout\n");
132 ad->did_first_timeout = TRUE;
133 } else
134 debug_printf (2, " timeout\n");
135 ad->timeout = 0;
136
137 /* ad->chunks_ready and ad->chunk_wanted are used because
138 * there's a race condition between the application requesting
139 * the first chunk, and the message reaching a point where
140 * it's actually ready to read chunks. If chunks_ready has
141 * been set, we can just call soup_session_unpause_message() to
142 * cause the first chunk to be read. But if it's not, we just
143 * set chunk_wanted, to let the got_headers handler below know
144 * that a chunk has already been requested.
145 */
146 if (ad->chunks_ready)
147 soup_session_unpause_message (ad->session, ad->msg);
148 else
149 ad->chunk_wanted = TRUE;
150
151 return FALSE;
152 }
153
154 static void
fully_async_got_headers(SoupMessage * msg,gpointer user_data)155 fully_async_got_headers (SoupMessage *msg, gpointer user_data)
156 {
157 FullyAsyncData *ad = user_data;
158
159 debug_printf (1, " %d %s\n", msg->status_code, msg->reason_phrase);
160 if (msg->status_code == SOUP_STATUS_UNAUTHORIZED) {
161 /* Let soup handle this one; this got_headers handler
162 * will get called again next time around.
163 */
164 return;
165 } else if (msg->status_code != SOUP_STATUS_OK) {
166 soup_test_assert_message_status (msg, SOUP_STATUS_OK);
167 return;
168 }
169
170 /* OK, we're happy with the response. So, we connect to
171 * "got_chunk". If there has already been a chunk requested,
172 * we let I/O continue; but if there hasn't, we pause now
173 * until one is requested.
174 */
175 ad->chunks_ready = TRUE;
176 g_signal_connect (msg, "got_chunk",
177 G_CALLBACK (fully_async_got_chunk), ad);
178 if (!ad->chunk_wanted)
179 soup_session_pause_message (ad->session, msg);
180 }
181
182 static void
fully_async_got_chunk(SoupMessage * msg,SoupBuffer * chunk,gpointer user_data)183 fully_async_got_chunk (SoupMessage *msg, SoupBuffer *chunk, gpointer user_data)
184 {
185 FullyAsyncData *ad = user_data;
186
187 debug_printf (2, " got chunk from %lu - %lu\n",
188 (unsigned long) ad->read_so_far,
189 (unsigned long) ad->read_so_far + chunk->length);
190
191 /* We've got a chunk, let's process it. In the case of the
192 * test program, that means comparing it against
193 * correct_response to make sure that we got the right data.
194 */
195 g_assert_cmpint (ad->read_so_far + chunk->length, <=, correct_response->length);
196 soup_assert_cmpmem (chunk->data, chunk->length,
197 correct_response->data + ad->read_so_far,
198 chunk->length);
199 ad->read_so_far += chunk->length;
200
201 /* Now pause I/O, and prepare to read another chunk later.
202 * (Again, the timeout just abstractly represents the idea of
203 * the application requesting another chunk at some random
204 * point in the future. You wouldn't be using a timeout in a
205 * real program.)
206 */
207 soup_session_pause_message (ad->session, msg);
208 ad->chunk_wanted = FALSE;
209
210 ad->timeout = g_timeout_add (10, fully_async_request_chunk, ad);
211 }
212
213 static void
fully_async_finished(SoupSession * session,SoupMessage * msg,gpointer user_data)214 fully_async_finished (SoupSession *session, SoupMessage *msg,
215 gpointer user_data)
216 {
217 FullyAsyncData *ad = user_data;
218
219 soup_test_assert_message_status (msg, ad->expected_status);
220
221 if (ad->timeout != 0)
222 g_source_remove (ad->timeout);
223
224 /* Since our test program is only running the loop for the
225 * purpose of this one test, we quit the loop once the
226 * test is done.
227 */
228 g_main_loop_quit (ad->loop);
229 }
230
231 static void
do_fast_async_test(gconstpointer data)232 do_fast_async_test (gconstpointer data)
233 {
234 const char *base_uri = data;
235 SoupSession *session;
236
237 SOUP_TEST_SKIP_IF_NO_APACHE;
238
239 session = soup_test_session_new (SOUP_TYPE_SESSION_ASYNC, NULL);
240 g_signal_connect (session, "authenticate",
241 G_CALLBACK (authenticate), NULL);
242 do_fully_async_test (session, base_uri, "/",
243 TRUE, SOUP_STATUS_OK);
244 do_fully_async_test (session, base_uri, "/Basic/realm1/",
245 TRUE, SOUP_STATUS_UNAUTHORIZED);
246 do_fully_async_test (session, base_uri, "/Basic/realm2/",
247 TRUE, SOUP_STATUS_OK);
248 soup_test_session_abort_unref (session);
249 }
250
251 static void
do_slow_async_test(gconstpointer data)252 do_slow_async_test (gconstpointer data)
253 {
254 const char *base_uri = data;
255 SoupSession *session;
256
257 SOUP_TEST_SKIP_IF_NO_APACHE;
258
259 session = soup_test_session_new (SOUP_TYPE_SESSION_ASYNC, NULL);
260 g_signal_connect (session, "authenticate",
261 G_CALLBACK (authenticate), NULL);
262 do_fully_async_test (session, base_uri, "/",
263 FALSE, SOUP_STATUS_OK);
264 do_fully_async_test (session, base_uri, "/Basic/realm1/",
265 FALSE, SOUP_STATUS_UNAUTHORIZED);
266 do_fully_async_test (session, base_uri, "/Basic/realm2/",
267 FALSE, SOUP_STATUS_OK);
268 soup_test_session_abort_unref (session);
269 }
270
271 /* Pull API version 2: synchronous pull API via async I/O. */
272
273 typedef struct {
274 GMainLoop *loop;
275 SoupSession *session;
276 SoupBuffer *chunk;
277 } SyncAsyncData;
278
279 static void sync_async_send (SoupSession *session,
280 SoupMessage *msg);
281 static gboolean sync_async_is_finished(SoupMessage *msg);
282 static SoupBuffer *sync_async_read_chunk (SoupMessage *msg);
283 static void sync_async_cleanup (SoupMessage *msg);
284
285 static void sync_async_got_headers (SoupMessage *msg, gpointer user_data);
286 static void sync_async_copy_chunk (SoupMessage *msg, SoupBuffer *chunk,
287 gpointer user_data);
288 static void sync_async_finished (SoupSession *session, SoupMessage *msg,
289 gpointer user_data);
290
291 static void
do_synchronously_async_test(SoupSession * session,const char * base_uri,const char * sub_uri,guint expected_status)292 do_synchronously_async_test (SoupSession *session,
293 const char *base_uri, const char *sub_uri,
294 guint expected_status)
295 {
296 SoupMessage *msg;
297 char *uri;
298 gsize read_so_far;
299 SoupBuffer *chunk;
300
301 uri = g_build_filename (base_uri, sub_uri, NULL);
302 debug_printf (1, "GET %s\n", uri);
303
304 msg = soup_message_new (SOUP_METHOD_GET, uri);
305 g_free (uri);
306
307 /* As in the fully-async case, we turn off accumulate, as an
308 * optimization.
309 */
310 soup_message_body_set_accumulate (msg->response_body, FALSE);
311
312 /* Send the message, get back headers */
313 sync_async_send (session, msg);
314 if (expected_status == SOUP_STATUS_OK) {
315 soup_test_assert (!sync_async_is_finished (msg),
316 "finished without reading response");
317 } else {
318 soup_test_assert (sync_async_is_finished (msg),
319 "request failed to fail");
320 }
321
322 /* Now we're ready to read the response body (though we could
323 * put that off until later if we really wanted).
324 */
325 read_so_far = 0;
326 while ((chunk = sync_async_read_chunk (msg))) {
327 debug_printf (2, " read chunk from %lu - %lu\n",
328 (unsigned long) read_so_far,
329 (unsigned long) read_so_far + chunk->length);
330
331 g_assert_cmpint (read_so_far + chunk->length, <=, correct_response->length);
332 soup_assert_cmpmem (chunk->data, chunk->length,
333 correct_response->data + read_so_far,
334 chunk->length);
335
336 read_so_far += chunk->length;
337 soup_buffer_free (chunk);
338 }
339
340 g_assert_true (sync_async_is_finished (msg));
341 soup_test_assert_message_status (msg, expected_status);
342 if (msg->status_code == SOUP_STATUS_OK)
343 g_assert_cmpint (read_so_far, ==, correct_response->length);
344
345 sync_async_cleanup (msg);
346 g_object_unref (msg);
347 }
348
349 /* Sends @msg on async session @session and returns after the headers
350 * of a successful response (or the complete body of a failed
351 * response) have been read.
352 */
353 static void
sync_async_send(SoupSession * session,SoupMessage * msg)354 sync_async_send (SoupSession *session, SoupMessage *msg)
355 {
356 SyncAsyncData *ad;
357
358 ad = g_new0 (SyncAsyncData, 1);
359 g_object_set_data (G_OBJECT (msg), "SyncAsyncData", ad);
360
361 /* In this case, unlike the fully-async case, the loop
362 * actually belongs to us, not the application; it will only
363 * be run when we're waiting for chunks, not at other times.
364 *
365 * If session has an async_context associated with it, we'd
366 * want to pass that, rather than NULL, here.
367 */
368 ad->loop = g_main_loop_new (NULL, FALSE);
369 ad->session = session;
370
371 g_signal_connect (msg, "got_headers",
372 G_CALLBACK (sync_async_got_headers), ad);
373
374 /* Start the request by queuing it and then running our main
375 * loop. Note: we have to use soup_session_queue_message()
376 * here; soup_session_send_message() won't work, for several
377 * reasons. Also, since soup_session_queue_message() steals a
378 * ref to the message and then unrefs it after invoking the
379 * callback, we have to add an extra ref before calling it.
380 */
381 g_object_ref (msg);
382 soup_session_queue_message (session, msg, sync_async_finished, ad);
383 g_main_loop_run (ad->loop);
384
385 /* At this point, one of two things has happened; either the
386 * got_headers handler got headers it liked, and so stopped
387 * the loop, or else the message was fully processed without
388 * the got_headers handler interrupting it, and so the final
389 * callback (sync_async_finished) was invoked, and stopped the
390 * loop.
391 *
392 * Either way, we're done, so we return to the caller.
393 */
394 }
395
396 static void
sync_async_got_headers(SoupMessage * msg,gpointer user_data)397 sync_async_got_headers (SoupMessage *msg, gpointer user_data)
398 {
399 SyncAsyncData *ad = user_data;
400
401 debug_printf (1, " %d %s\n", msg->status_code, msg->reason_phrase);
402 if (msg->status_code == SOUP_STATUS_UNAUTHORIZED) {
403 /* Let soup handle this one; this got_headers handler
404 * will get called again next time around.
405 */
406 return;
407 } else if (msg->status_code != SOUP_STATUS_OK) {
408 soup_test_assert_message_status (msg, SOUP_STATUS_OK);
409 return;
410 }
411
412 /* Stop I/O and return to the caller */
413 soup_session_pause_message (ad->session, msg);
414 g_main_loop_quit (ad->loop);
415 }
416
417 static gboolean
sync_async_is_finished(SoupMessage * msg)418 sync_async_is_finished (SoupMessage *msg)
419 {
420 SyncAsyncData *ad = g_object_get_data (G_OBJECT (msg), "SyncAsyncData");
421
422 /* sync_async_finished clears ad->loop */
423 return ad->loop == NULL;
424 }
425
426 /* Tries to read a chunk. Returns %NULL on error/end-of-response. */
427 static SoupBuffer *
sync_async_read_chunk(SoupMessage * msg)428 sync_async_read_chunk (SoupMessage *msg)
429 {
430 SyncAsyncData *ad = g_object_get_data (G_OBJECT (msg), "SyncAsyncData");
431 guint handler;
432
433 if (sync_async_is_finished (msg))
434 return NULL;
435
436 ad->chunk = NULL;
437 handler = g_signal_connect (msg, "got_chunk",
438 G_CALLBACK (sync_async_copy_chunk),
439 ad);
440 soup_session_unpause_message (ad->session, msg);
441 g_main_loop_run (ad->loop);
442 g_signal_handler_disconnect (msg, handler);
443
444 return ad->chunk;
445 }
446
447 static void
sync_async_copy_chunk(SoupMessage * msg,SoupBuffer * chunk,gpointer user_data)448 sync_async_copy_chunk (SoupMessage *msg, SoupBuffer *chunk, gpointer user_data)
449 {
450 SyncAsyncData *ad = user_data;
451
452 ad->chunk = soup_buffer_copy (chunk);
453
454 /* Now pause and return from the g_main_loop_run() call in
455 * sync_async_read_chunk().
456 */
457 soup_session_pause_message (ad->session, msg);
458 g_main_loop_quit (ad->loop);
459 }
460
461 static void
sync_async_finished(SoupSession * session,SoupMessage * msg,gpointer user_data)462 sync_async_finished (SoupSession *session, SoupMessage *msg, gpointer user_data)
463 {
464 SyncAsyncData *ad = user_data;
465
466 /* Unlike in the fully_async_case, we don't need to do much
467 * here, because control will return to
468 * do_synchronously_async_test() when we're done, and we do
469 * the final tests there.
470 */
471 g_main_loop_quit (ad->loop);
472 g_main_loop_unref (ad->loop);
473 ad->loop = NULL;
474 }
475
476 static void
sync_async_cleanup(SoupMessage * msg)477 sync_async_cleanup (SoupMessage *msg)
478 {
479 SyncAsyncData *ad = g_object_get_data (G_OBJECT (msg), "SyncAsyncData");
480
481 if (ad->loop)
482 g_main_loop_unref (ad->loop);
483 g_free (ad);
484 }
485
486 static void
do_sync_async_test(gconstpointer data)487 do_sync_async_test (gconstpointer data)
488 {
489 const char *base_uri = data;
490 SoupSession *session;
491
492 SOUP_TEST_SKIP_IF_NO_APACHE;
493
494 session = soup_test_session_new (SOUP_TYPE_SESSION_ASYNC, NULL);
495 g_signal_connect (session, "authenticate",
496 G_CALLBACK (authenticate), NULL);
497 do_synchronously_async_test (session, base_uri, "/",
498 SOUP_STATUS_OK);
499 do_synchronously_async_test (session, base_uri, "/Basic/realm1/",
500 SOUP_STATUS_UNAUTHORIZED);
501 do_synchronously_async_test (session, base_uri, "/Basic/realm2/",
502 SOUP_STATUS_OK);
503 soup_test_session_abort_unref (session);
504 }
505
506
507 int
main(int argc,char ** argv)508 main (int argc, char **argv)
509 {
510 const char *base_uri;
511 int ret;
512
513 test_init (argc, argv, NULL);
514 apache_init ();
515
516 base_uri = "http://127.0.0.1:47524/";
517 #ifdef HAVE_APACHE
518 get_correct_response (base_uri);
519 #endif
520
521 g_test_add_data_func ("/pull-api/async/fast", base_uri, do_fast_async_test);
522 g_test_add_data_func ("/pull-api/async/slow", base_uri, do_slow_async_test);
523 g_test_add_data_func ("/pull-api/sync-async", base_uri, do_sync_async_test);
524
525 ret = g_test_run ();
526
527 #ifdef HAVE_APACHE
528 soup_buffer_free (correct_response);
529 #endif
530
531 test_cleanup ();
532 return ret;
533 }
534