[libsoup/carlosgc/preconnect: 11/11] Add new api to preconnect to a given uri




commit a1644f3d50ed6f5cdbf099099b657df44fd30521
Author: Carlos Garcia Campos <cgarcia igalia com>
Date:   Fri Sep 4 07:14:39 2020 +0200

    Add new api to preconnect to a given uri
    
    A new connection is started that can be reused by a future request for
    the same host. This is needed by WebKit to implement link preconnect
    https://w3c.github.io/resource-hints/#preconnect

 docs/reference/libsoup-2.4-sections.txt |   2 +
 libsoup/soup-connection.c               |  11 +
 libsoup/soup-connection.h               |   3 +
 libsoup/soup-message-queue.c            |  19 ++
 libsoup/soup-message-queue.h            |   2 +
 libsoup/soup-session.c                  | 188 ++++++++++++++++-
 libsoup/soup-session.h                  |  12 ++
 libsoup/soup-version.h.in               |   7 +
 tests/connection-test.c                 | 354 ++++++++++++++++++++++++++++++--
 9 files changed, 579 insertions(+), 19 deletions(-)
---
diff --git a/docs/reference/libsoup-2.4-sections.txt b/docs/reference/libsoup-2.4-sections.txt
index 26ddfbb2..10207eb2 100644
--- a/docs/reference/libsoup-2.4-sections.txt
+++ b/docs/reference/libsoup-2.4-sections.txt
@@ -488,6 +488,8 @@ soup_session_steal_connection
 SoupSessionConnectProgressCallback
 soup_session_connect_async
 soup_session_connect_finish
+soup_session_preconnect_async
+soup_session_preconnect_finish
 <SUBSECTION>
 SOUP_SESSION_PROXY_URI
 SOUP_SESSION_PROXY_RESOLVER
diff --git a/libsoup/soup-connection.c b/libsoup/soup-connection.c
index 1b9a4fee..4214ff45 100644
--- a/libsoup/soup-connection.c
+++ b/libsoup/soup-connection.c
@@ -725,6 +725,17 @@ soup_connection_set_state (SoupConnection *conn, SoupConnectionState state)
        g_object_thaw_notify (G_OBJECT (conn));
 }
 
+void
+soup_connection_set_reusable (SoupConnection *conn,
+                              gboolean        reusable)
+{
+        SoupConnectionPrivate *priv = soup_connection_get_instance_private (conn);
+
+        g_return_if_fail (SOUP_IS_CONNECTION (conn));
+
+        priv->reusable = TRUE;
+}
+
 gboolean
 soup_connection_get_ever_used (SoupConnection *conn)
 {
diff --git a/libsoup/soup-connection.h b/libsoup/soup-connection.h
index 45e80b2c..cd6bb612 100644
--- a/libsoup/soup-connection.h
+++ b/libsoup/soup-connection.h
@@ -73,6 +73,9 @@ SoupConnectionState soup_connection_get_state  (SoupConnection   *conn);
 void                soup_connection_set_state  (SoupConnection   *conn,
                                                SoupConnectionState state);
 
+void            soup_connection_set_reusable   (SoupConnection   *conn,
+                                                gboolean          reusable);
+
 gboolean        soup_connection_get_ever_used  (SoupConnection   *conn);
 
 void            soup_connection_send_request   (SoupConnection          *conn,
diff --git a/libsoup/soup-message-queue.c b/libsoup/soup-message-queue.c
index 436f3eb5..b671b4b8 100644
--- a/libsoup/soup-message-queue.c
+++ b/libsoup/soup-message-queue.c
@@ -224,6 +224,25 @@ soup_message_queue_lookup (SoupMessageQueue *queue, SoupMessage *msg)
        return item;
 }
 
+SoupMessageQueueItem *
+soup_message_queue_lookup_by_connection (SoupMessageQueue *queue,
+                                         SoupConnection   *conn)
+{
+        SoupMessageQueueItem *item;
+
+        g_mutex_lock (&queue->mutex);
+
+        item = queue->tail;
+        while (item && (item->removed || item->conn != conn))
+                item = item->prev;
+
+        if (item)
+                item->ref_count++;
+
+        g_mutex_unlock (&queue->mutex);
+        return item;
+}
+
 /**
  * soup_message_queue_first:
  * @queue: a #SoupMessageQueue
diff --git a/libsoup/soup-message-queue.h b/libsoup/soup-message-queue.h
index 2b33a451..f37073c8 100644
--- a/libsoup/soup-message-queue.h
+++ b/libsoup/soup-message-queue.h
@@ -71,6 +71,8 @@ SoupMessageQueueItem *soup_message_queue_append     (SoupMessageQueue     *queue
 
 SoupMessageQueueItem *soup_message_queue_lookup     (SoupMessageQueue     *queue,
                                                     SoupMessage          *msg);
+SoupMessageQueueItem *soup_message_queue_lookup_by_connection (SoupMessageQueue *queue,
+                                                               SoupConnection   *conn);
 
 SoupMessageQueueItem *soup_message_queue_first      (SoupMessageQueue     *queue);
 SoupMessageQueueItem *soup_message_queue_next       (SoupMessageQueue     *queue,
diff --git a/libsoup/soup-session.c b/libsoup/soup-session.c
index dd3cdc46..183c8d29 100644
--- a/libsoup/soup-session.c
+++ b/libsoup/soup-session.c
@@ -1778,6 +1778,17 @@ connect_async_complete (GObject      *object,
        GError *error = NULL;
 
        soup_connection_connect_finish (conn, result, &error);
+       if (item->related) {
+               SoupMessageQueueItem *new_item = item->related;
+
+               /* Complete the preconnect successfully, since it was stolen. */
+               item->state = SOUP_MESSAGE_FINISHING;
+               item->related = NULL;
+               soup_session_process_queue_item (item->session, item, NULL, FALSE);
+               soup_message_queue_item_unref (item);
+
+               item = new_item;
+       }
        connect_complete (item, conn, error);
 
        if (item->state == SOUP_MESSAGE_CONNECTED ||
@@ -1789,6 +1800,35 @@ connect_async_complete (GObject      *object,
        soup_message_queue_item_unref (item);
 }
 
+static gboolean
+steal_preconnection (SoupSession          *session,
+                     SoupMessageQueueItem *item,
+                     SoupConnection       *conn)
+{
+        SoupSessionPrivate *priv = soup_session_get_instance_private (session);
+        SoupMessageQueueItem *preconnect_item;
+
+        if (!item->async)
+                return FALSE;
+
+        preconnect_item = soup_message_queue_lookup_by_connection (priv->queue, conn);
+        if (!preconnect_item)
+                return FALSE;
+
+        if (!preconnect_item->connect_only || preconnect_item->state != SOUP_MESSAGE_CONNECTING) {
+                soup_message_queue_item_unref (preconnect_item);
+                return FALSE;
+        }
+
+        soup_session_set_item_connection (session, preconnect_item, NULL);
+        g_assert (preconnect_item->related == NULL);
+        preconnect_item->related = item;
+        soup_message_queue_item_ref (item);
+        soup_message_queue_item_unref (preconnect_item);
+
+        return TRUE;
+}
+
 /* requires conn_lock */
 static SoupConnection *
 get_connection_for_host (SoupSession *session,
@@ -1815,11 +1855,22 @@ get_connection_for_host (SoupSession *session,
        for (conns = host->connections; conns; conns = conns->next) {
                conn = conns->data;
 
-               if (!need_new_connection && soup_connection_get_state (conn) == SOUP_CONNECTION_IDLE) {
-                       soup_connection_set_state (conn, SOUP_CONNECTION_IN_USE);
-                       return conn;
-               } else if (soup_connection_get_state (conn) == SOUP_CONNECTION_CONNECTING)
+               switch (soup_connection_get_state (conn)) {
+               case SOUP_CONNECTION_IDLE:
+                       if (!need_new_connection) {
+                               soup_connection_set_state (conn, SOUP_CONNECTION_IN_USE);
+                               return conn;
+                       }
+                       break;
+               case SOUP_CONNECTION_CONNECTING:
+                       if (steal_preconnection (session, item, conn)) {
+                               return conn;
+                       }
                        num_pending++;
+                       break;
+               default:
+                       break;
+               }
        }
 
        /* Limit the number of pending connections; num_messages / 2
@@ -1940,10 +1991,20 @@ get_connection (SoupMessageQueueItem *item, gboolean *should_cleanup)
        soup_session_set_item_connection (session, item, conn);
        item->conn_is_dedicated = is_dedicated_connection;
 
-       if (soup_connection_get_state (item->conn) != SOUP_CONNECTION_NEW) {
+       switch (soup_connection_get_state (item->conn)) {
+       case SOUP_CONNECTION_IN_USE:
                item->state = SOUP_MESSAGE_READY;
                soup_message_set_https_status (item->msg, item->conn);
                return TRUE;
+       case SOUP_CONNECTION_CONNECTING:
+               item->state = SOUP_MESSAGE_CONNECTING;
+               return FALSE;
+       case SOUP_CONNECTION_NEW:
+               break;
+       case SOUP_CONNECTION_IDLE:
+       case SOUP_CONNECTION_REMOTE_DISCONNECTED:
+       case SOUP_CONNECTION_DISCONNECTED:
+               g_assert_not_reached ();
        }
 
        item->state = SOUP_MESSAGE_CONNECTING;
@@ -5003,7 +5064,8 @@ soup_session_websocket_connect_finish (SoupSession      *session,
  * @connection: the current state of the network connection
  * @user_data: the data passed to soup_session_connect_async().
  *
- * Prototype for the progress callback passed to soup_session_connect_async().
+ * Prototype for the progress callback passed to soup_session_connect_async() and
+ * soup_session_preconnect_async().
  *
  * Since: 2.62
  */
@@ -5149,3 +5211,117 @@ soup_session_connect_finish (SoupSession  *session,
 
         return g_task_propagate_pointer (G_TASK (result), error);
 }
+
+static void
+preconnect_async_message_finished (SoupMessage *msg,
+                                   GTask       *task)
+{
+        ConnectAsyncData *data = g_task_get_task_data (task);
+        SoupMessageQueueItem *item = data->item;
+
+        if (item->conn && !item->error)
+                soup_connection_set_reusable (item->conn, TRUE);
+}
+
+static void
+preconnect_async_complete (SoupSession *session,
+                           SoupMessage *msg,
+                           GTask       *task)
+{
+        ConnectAsyncData *data = g_task_get_task_data (task);
+        SoupMessageQueueItem *item = data->item;
+
+        if (item->error)
+                g_task_return_error (task, g_error_copy (item->error));
+        else
+                g_task_return_boolean (task, TRUE);
+        g_object_unref (task);
+}
+
+/**
+ * soup_session_preconnect_async:
+ * @session: a #SoupSession
+ * @uri: a #SoupURI to preconnect to
+ * @cancellable: a #GCancellable
+ * @progress_callback: (allow-none) (scope async): a #SoupSessionConnectProgressCallback which
+ * will be called for every network event that occurs during the connection.
+ * @callback: (allow-none) (scope async): the callback to invoke when the operation finishes
+ * @user_data: data for @progress_callback and @callback
+ *
+ * Start a preconnection to @uri. Once the connection is done, it will remain in idle state so that
+ * it can be reused by future requests. If there's already an idle connection for the given @uri
+ * host, the operation finishes successfully without creating a new connection. If a new request
+ * for the given @uri host is made while the preconnect is still ongoing, the request will take
+ * the ownership of the connection and the preconnect operation will finish successfully (if
+ * there's a connection error it will be handled by the request).
+ *
+ * The operation can be monitored by providing a @progress_callback and finishes when the connection
+ * is done or an error ocurred.
+ *
+ * Since: 2.74
+ */
+void
+soup_session_preconnect_async (SoupSession                       *session,
+                               SoupURI                           *uri,
+                               GCancellable                      *cancellable,
+                               SoupSessionConnectProgressCallback progress_callback,
+                               GAsyncReadyCallback                callback,
+                               gpointer                           user_data)
+{
+        SoupSessionPrivate *priv;
+        SoupMessage *msg;
+        SoupMessageQueueItem *item;
+        ConnectAsyncData *data;
+        GTask *task;
+
+        g_return_if_fail (SOUP_IS_SESSION (session));
+        g_return_if_fail (!SOUP_IS_SESSION_SYNC (session));
+        priv = soup_session_get_instance_private (session);
+        g_return_if_fail (priv->use_thread_context);
+        g_return_if_fail (uri != NULL);
+
+        task = g_task_new (session, cancellable, callback, user_data);
+
+        msg = soup_message_new_from_uri (SOUP_METHOD_HEAD, uri);
+        g_signal_connect_object (msg, "finished",
+                                 G_CALLBACK (preconnect_async_message_finished),
+                                 task, 0);
+        if (progress_callback) {
+                g_signal_connect_object (msg, "network-event",
+                                         G_CALLBACK (connect_async_message_network_event),
+                                         task, 0);
+        }
+
+        item = soup_session_append_queue_item (session, msg, TRUE, FALSE,
+                                               (SoupSessionCallback) preconnect_async_complete,
+                                               task);
+        item->connect_only = TRUE;
+        data = connect_async_data_new (item, progress_callback, user_data);
+        g_task_set_task_data (task, data, (GDestroyNotify) connect_async_data_free);
+        soup_session_kick_queue (session);
+        soup_message_queue_item_unref (item);
+        g_object_unref (msg);
+}
+
+/**
+ * soup_session_preconnect_finish:
+ * @session: a #SoupSession
+ * @result: the #GAsyncResult passed to your callback
+ * @error: return location for a #GError, or %NULL
+ *
+ * Complete a preconnect async operation started with soup_session_preconnect_async().
+ *
+ * Return value: %TRUE if the preconnect succeeded, or %FALSE in case of error.
+ *
+ * Since: 2.74
+ */
+gboolean
+soup_session_preconnect_finish (SoupSession  *session,
+                                GAsyncResult *result,
+                                GError      **error)
+{
+        g_return_val_if_fail (SOUP_IS_SESSION (session), FALSE);
+        g_return_val_if_fail (g_task_is_valid (result, session), FALSE);
+
+        return g_task_propagate_boolean (G_TASK (result), error);
+}
diff --git a/libsoup/soup-session.h b/libsoup/soup-session.h
index 46e4e154..3e23393b 100644
--- a/libsoup/soup-session.h
+++ b/libsoup/soup-session.h
@@ -252,6 +252,18 @@ GIOStream *soup_session_connect_finish (SoupSession                       *sessi
                                        GAsyncResult                      *result,
                                        GError                           **error);
 
+SOUP_AVAILABLE_IN_2_74
+void       soup_session_preconnect_async  (SoupSession                       *session,
+                                          SoupURI                           *uri,
+                                          GCancellable                      *cancellable,
+                                          SoupSessionConnectProgressCallback progress_callback,
+                                          GAsyncReadyCallback                callback,
+                                          gpointer                           user_data);
+SOUP_AVAILABLE_IN_2_74
+gboolean   soup_session_preconnect_finish (SoupSession                       *session,
+                                          GAsyncResult                      *result,
+                                          GError                           **error);
+
 G_END_DECLS
 
 #endif /* __SOUP_SESSION_H__ */
diff --git a/libsoup/soup-version.h.in b/libsoup/soup-version.h.in
index 07bed7fd..ad64721a 100644
--- a/libsoup/soup-version.h.in
+++ b/libsoup/soup-version.h.in
@@ -70,6 +70,7 @@ G_BEGIN_DECLS
 #define SOUP_VERSION_2_68 (G_ENCODE_VERSION (2, 68))
 #define SOUP_VERSION_2_70 (G_ENCODE_VERSION (2, 70))
 #define SOUP_VERSION_2_72 (G_ENCODE_VERSION (2, 72))
+#define SOUP_VERSION_2_74 (G_ENCODE_VERSION (2, 74))
 
 /* evaluates to the current stable version; for development cycles,
  * this means the next stable target
@@ -418,6 +419,12 @@ G_BEGIN_DECLS
 # define SOUP_AVAILABLE_IN_2_72                 _SOUP_EXTERN
 #endif
 
+#if SOUP_VERSION_MAX_ALLOWED < SOUP_VERSION_2_74
+# define SOUP_AVAILABLE_IN_2_74                 G_UNAVAILABLE(2, 74) _SOUP_EXTERN
+#else
+# define SOUP_AVAILABLE_IN_2_74                 _SOUP_EXTERN
+#endif
+
 SOUP_AVAILABLE_IN_2_42
 guint    soup_get_major_version (void);
 
diff --git a/tests/connection-test.c b/tests/connection-test.c
index 7e430801..1ceb09e4 100644
--- a/tests/connection-test.c
+++ b/tests/connection-test.c
@@ -752,16 +752,6 @@ do_non_idempotent_connection_test (void)
 #define HTTPS_SERVER "https://127.0.0.1:47525";
 #define HTTP_PROXY   "http://127.0.0.1:47526";
 
-static SoupConnectionState state_transitions[] = {
-       /* NEW -> */        SOUP_CONNECTION_CONNECTING,
-       /* CONNECTING -> */ SOUP_CONNECTION_IN_USE,
-       /* IDLE -> */       SOUP_CONNECTION_DISCONNECTED,
-       /* IN_USE -> */     SOUP_CONNECTION_IDLE,
-
-       /* REMOTE_DISCONNECTED */ -1,
-       /* DISCONNECTED */        -1,
-};
-
 static const char *state_names[] = {
        "NEW", "CONNECTING", "IDLE", "IN_USE",
        "REMOTE_DISCONNECTED", "DISCONNECTED"
@@ -777,9 +767,34 @@ connection_state_changed (GObject *object, GParamSpec *param,
        g_object_get (object, "state", &new_state, NULL);
        debug_printf (2, "      %s -> %s\n",
                      state_names[*state], state_names[new_state]);
-       soup_test_assert (state_transitions[*state] == new_state,
-                         "Unexpected transition: %s -> %s\n",
-                         state_names[*state], state_names[new_state]);
+       switch (*state) {
+       case SOUP_CONNECTION_NEW:
+               soup_test_assert (new_state == SOUP_CONNECTION_CONNECTING,
+                                 "Unexpected transition: %s -> %s\n",
+                                 state_names[*state], state_names[new_state]);
+               break;
+       case SOUP_CONNECTION_CONNECTING:
+               soup_test_assert (new_state == SOUP_CONNECTION_IN_USE || new_state == 
SOUP_CONNECTION_DISCONNECTED,
+                                 "Unexpected transition: %s -> %s\n",
+                                 state_names[*state], state_names[new_state]);
+               break;
+       case SOUP_CONNECTION_IDLE:
+               soup_test_assert (new_state == SOUP_CONNECTION_IN_USE || new_state == 
SOUP_CONNECTION_DISCONNECTED,
+                                 "Unexpected transition: %s -> %s\n",
+                                 state_names[*state], state_names[new_state]);
+               break;
+       case SOUP_CONNECTION_IN_USE:
+               soup_test_assert (new_state == SOUP_CONNECTION_IDLE,
+                                 "Unexpected transition: %s -> %s\n",
+                                 state_names[*state], state_names[new_state]);
+               break;
+       case SOUP_CONNECTION_REMOTE_DISCONNECTED:
+       case SOUP_CONNECTION_DISCONNECTED:
+               soup_test_assert (FALSE,
+                                 "Unexpected transition: %s -> %s\n",
+                                 state_names[*state], state_names[new_state]);
+               break;
+       }
        *state = new_state;
 }
 
@@ -1187,6 +1202,318 @@ do_connection_connect_test (void)
         soup_test_session_abort_unref (session);
 }
 
+typedef struct {
+        GMainLoop *loop;
+        GError *error;
+        const char *events;
+        SoupConnectionState state;
+        GObject *conn;
+        gboolean quit_on_preconnect;
+} PreconnectTestData;
+
+static void
+preconnection_created (SoupSession        *session,
+                       GObject            *conn,
+                       PreconnectTestData *data)
+{
+        g_object_get (conn, "state", &data->state, NULL);
+        g_assert_cmpint (data->state, ==, SOUP_CONNECTION_NEW);
+
+        g_assert_null (data->conn);
+        data->conn = g_object_ref (conn);
+
+        g_signal_connect (conn, "notify::state",
+                          G_CALLBACK (connection_state_changed),
+                          &data->state);
+}
+
+static void
+preconnect_progress (SoupSession        *session,
+                     GSocketClientEvent  event,
+                     GIOStream          *connection,
+                     PreconnectTestData *data)
+{
+        soup_test_assert (*data->events == event_abbrevs[event],
+                          "Unexpected event: %s (expected %s)",
+                          event_names[event],
+                          event_name_from_abbrev (*data->events));
+        data->events = data->events + 1;
+}
+
+static void
+preconnect_finished (SoupSession        *session,
+                     GAsyncResult       *result,
+                     PreconnectTestData *data)
+{
+        soup_session_preconnect_finish (session, result, &data->error);
+        if (data->quit_on_preconnect)
+                g_main_loop_quit (data->loop);
+}
+
+static void
+do_idle_connection_preconnect_test (SoupURI *uri, SoupURI *proxy_uri, const char *events)
+{
+        SoupSession *session;
+        PreconnectTestData data = { NULL, NULL, events, SOUP_CONNECTION_DISCONNECTED, NULL, TRUE };
+        GObject *conn;
+        SoupMessage *msg;
+
+        session = soup_test_session_new (SOUP_TYPE_SESSION_ASYNC,
+                                         SOUP_SESSION_USE_THREAD_CONTEXT, TRUE,
+                                         NULL);
+
+        if (proxy_uri)
+                g_object_set (session, SOUP_SESSION_PROXY_URI, proxy_uri, NULL);
+
+        data.loop = g_main_loop_new (NULL, FALSE);
+        g_signal_connect (session, "connection-created",
+                          G_CALLBACK (preconnection_created),
+                          &data);
+        soup_session_preconnect_async (session, uri, NULL,
+                                       (SoupSessionConnectProgressCallback)preconnect_progress,
+                                       (GAsyncReadyCallback)preconnect_finished,
+                                       &data);
+        g_main_loop_run (data.loop);
+        g_assert_no_error (data.error);
+        g_assert_nonnull (data.conn);
+        g_assert_cmpint (data.state, ==, SOUP_CONNECTION_IDLE);
+
+        while (*data.events) {
+                soup_test_assert (!*data.events,
+                                  "Expected %s",
+                                  event_name_from_abbrev (*data.events));
+                data.events++;
+        }
+
+        conn = data.conn;
+        data.conn = NULL;
+        msg = soup_message_new_from_uri ("GET", uri);
+        soup_session_send_message (session, msg);
+        soup_test_assert_message_status (msg, SOUP_STATUS_OK);
+        g_object_unref (msg);
+
+        /* connection-created hasn't been called. */
+        g_assert_null (data.conn);
+        g_assert_cmpint (data.state, ==, SOUP_CONNECTION_IDLE);
+
+        /* Preconnect again does nothing because there's already an idle connection ready. */
+        soup_session_preconnect_async (session, uri, NULL,
+                                       (SoupSessionConnectProgressCallback)preconnect_progress,
+                                       (GAsyncReadyCallback)preconnect_finished,
+                                       &data);
+        g_main_loop_run (data.loop);
+        g_assert_no_error (data.error);
+        g_assert_null (data.conn);
+        g_assert_cmpint (data.state, ==, SOUP_CONNECTION_IDLE);
+
+        soup_session_abort (session);
+        g_assert_cmpint (data.state, ==, SOUP_CONNECTION_DISCONNECTED);
+        g_object_unref (conn);
+
+        g_main_loop_unref (data.loop);
+
+        soup_test_session_abort_unref (session);
+}
+
+static void
+do_idle_connection_preconnect_fail_test (SoupURI *uri, GQuark domain, gint code, const char *events)
+{
+        SoupSession *session;
+        PreconnectTestData data = { NULL, NULL, events, SOUP_CONNECTION_DISCONNECTED, NULL, TRUE };
+
+        session = soup_test_session_new (SOUP_TYPE_SESSION_ASYNC,
+                                         SOUP_SESSION_USE_THREAD_CONTEXT, TRUE,
+                                         NULL);
+
+        if (tls_available)
+               g_object_set (session, SOUP_SESSION_SSL_USE_SYSTEM_CA_FILE, TRUE, NULL);
+
+        data.loop = g_main_loop_new (NULL, FALSE);
+        g_signal_connect (session, "connection-created",
+                          G_CALLBACK (preconnection_created),
+                          &data);
+        soup_session_preconnect_async (session, uri, NULL,
+                                       (SoupSessionConnectProgressCallback)preconnect_progress,
+                                       (GAsyncReadyCallback)preconnect_finished,
+                                       &data);
+        g_main_loop_run (data.loop);
+        g_assert_error (data.error, domain, code);
+        g_error_free (data.error);
+        g_assert_nonnull (data.conn);
+        g_assert_cmpint (data.state, ==, SOUP_CONNECTION_DISCONNECTED);
+        g_object_unref (data.conn);
+
+        while (*data.events) {
+                soup_test_assert (!*data.events,
+                                  "Expected %s",
+                                  event_name_from_abbrev (*data.events));
+                data.events++;
+        }
+
+        g_main_loop_unref (data.loop);
+
+        soup_test_session_abort_unref (session);
+}
+
+static void
+preconnect_message_completed (SoupSession        *session,
+                              SoupMessage        *msg,
+                              PreconnectTestData *data)
+{
+        g_main_loop_quit (data->loop);
+}
+
+static void
+do_steal_connection_preconnect_test (SoupURI *uri, SoupURI *proxy_uri, const char *events)
+{
+        SoupSession *session;
+        PreconnectTestData data = { NULL, NULL, events, SOUP_CONNECTION_DISCONNECTED, NULL, FALSE };
+        SoupMessage *msg;
+
+        session = soup_test_session_new (SOUP_TYPE_SESSION_ASYNC,
+                                         SOUP_SESSION_USE_THREAD_CONTEXT, TRUE,
+                                         NULL);
+
+        if (proxy_uri)
+                g_object_set (session, SOUP_SESSION_PROXY_URI, proxy_uri, NULL);
+
+        data.loop = g_main_loop_new (NULL, FALSE);
+        g_signal_connect (session, "connection-created",
+                          G_CALLBACK (preconnection_created),
+                          &data);
+        soup_session_preconnect_async (session, uri, NULL,
+                                       (SoupSessionConnectProgressCallback)preconnect_progress,
+                                       (GAsyncReadyCallback)preconnect_finished,
+                                       &data);
+        msg = soup_message_new_from_uri ("GET", uri);
+        soup_session_queue_message (session, msg,
+                                    (SoupSessionCallback)preconnect_message_completed,
+                                    &data);
+        g_main_loop_run (data.loop);
+        g_assert_no_error (data.error);
+        g_assert_nonnull (data.conn);
+        g_assert_cmpint (data.state, ==, SOUP_CONNECTION_IDLE);
+
+        while (*data.events) {
+                soup_test_assert (!*data.events,
+                                  "Expected %s",
+                                  event_name_from_abbrev (*data.events));
+                data.events++;
+        }
+
+        soup_session_abort (session);
+        g_assert_cmpint (data.state, ==, SOUP_CONNECTION_DISCONNECTED);
+        g_object_unref (data.conn);
+
+        g_main_loop_unref (data.loop);
+
+        soup_test_session_abort_unref (session);
+}
+
+static void
+do_steal_connection_preconnect_fail_test (SoupURI *uri, SoupStatus status, const char *events)
+{
+        SoupSession *session;
+        PreconnectTestData data = { NULL, NULL, events, SOUP_CONNECTION_DISCONNECTED, NULL, FALSE };
+        SoupMessage *msg;
+        SoupStatus status_code;
+
+        session = soup_test_session_new (SOUP_TYPE_SESSION_ASYNC,
+                                         SOUP_SESSION_USE_THREAD_CONTEXT, TRUE,
+                                         NULL);
+
+        if (tls_available)
+               g_object_set (session, SOUP_SESSION_SSL_USE_SYSTEM_CA_FILE, TRUE, NULL);
+
+        data.loop = g_main_loop_new (NULL, FALSE);
+        g_signal_connect (session, "connection-created",
+                          G_CALLBACK (preconnection_created),
+                          &data);
+        soup_session_preconnect_async (session, uri, NULL,
+                                       (SoupSessionConnectProgressCallback)preconnect_progress,
+                                       (GAsyncReadyCallback)preconnect_finished,
+                                       &data);
+        msg = soup_message_new_from_uri ("GET", uri);
+        soup_session_queue_message (session, g_object_ref (msg),
+                                    (SoupSessionCallback)preconnect_message_completed,
+                                    &data);
+        g_main_loop_run (data.loop);
+        g_assert_no_error (data.error);
+        g_assert_nonnull (data.conn);
+        g_assert_cmpint (data.state, ==, SOUP_CONNECTION_DISCONNECTED);
+        g_object_unref (data.conn);
+        g_object_get (msg, "status-code", &status_code, NULL);
+        g_assert_cmpint (status, ==, status_code);
+        g_object_unref (msg);
+
+        while (*data.events) {
+                soup_test_assert (!*data.events,
+                                  "Expected %s",
+                                  event_name_from_abbrev (*data.events));
+                data.events++;
+        }
+
+        g_main_loop_unref (data.loop);
+
+        soup_test_session_abort_unref (session);
+}
+
+static void
+do_connection_preconnect_test (void)
+{
+        SoupURI *http_uri;
+        SoupURI *proxy_uri;
+
+        SOUP_TEST_SKIP_IF_NO_APACHE;
+
+        debug_printf (1, "    http\n");
+        http_uri = soup_uri_new (HTTP_SERVER);
+        do_idle_connection_preconnect_test (http_uri, NULL, "rRcCx");
+        do_steal_connection_preconnect_test (http_uri, NULL, "r");
+
+        debug_printf (1, "    http with proxy\n");
+        proxy_uri = soup_uri_new (HTTP_PROXY);
+        do_idle_connection_preconnect_test (http_uri, proxy_uri, "rRcCx");
+        do_steal_connection_preconnect_test (http_uri, proxy_uri, "r");
+        soup_uri_free (http_uri);
+
+        debug_printf (1, "    wrong http (invalid port)\n");
+        http_uri = soup_uri_new (HTTP_SERVER);
+        http_uri->port = 1234;
+        do_idle_connection_preconnect_fail_test (http_uri,
+                                                 G_IO_ERROR, G_IO_ERROR_CONNECTION_REFUSED,
+                                                 "rRcr"); /* FIXME: why r again? GLib bug? */
+        do_steal_connection_preconnect_fail_test (http_uri,
+                                                  SOUP_STATUS_CANT_CONNECT,
+                                                  "r");
+        soup_uri_free (http_uri);
+
+        if (tls_available) {
+                SoupURI *https_uri;
+
+                debug_printf (1, "    https\n");
+                https_uri = soup_uri_new (HTTPS_SERVER);
+                do_idle_connection_preconnect_test (https_uri, NULL, "rRcCtTx");
+                do_steal_connection_preconnect_test (https_uri, NULL, "r");
+
+                debug_printf (1, "    https with proxy\n");
+                do_idle_connection_preconnect_test (https_uri, proxy_uri, "rRcCpPtTx");
+                do_steal_connection_preconnect_test (https_uri, proxy_uri, "r");
+
+                debug_printf (1, "    wrong https (invalid certificate)\n");
+                do_idle_connection_preconnect_fail_test (https_uri,
+                                                         G_TLS_ERROR, G_TLS_ERROR_BAD_CERTIFICATE,
+                                                         "rRcCt");
+                do_steal_connection_preconnect_fail_test (https_uri,
+                                                          SOUP_STATUS_SSL_FAILED,
+                                                          "r");
+                soup_uri_free (https_uri);
+        } else
+                debug_printf (1, "    https -- SKIPPING\n");
+
+        soup_uri_free (proxy_uri);
+}
+
 int
 main (int argc, char **argv)
 {
@@ -1209,6 +1536,7 @@ main (int argc, char **argv)
        g_test_add_func ("/connection/state", do_connection_state_test);
        g_test_add_func ("/connection/event", do_connection_event_test);
        g_test_add_func ("/connection/connect", do_connection_connect_test);
+       g_test_add_func ("/connection/preconnect", do_connection_preconnect_test);
 
        ret = g_test_run ();
 


[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]