[libgdata] Bug 637664 — Fix GSeekable interface implementation in GDataDownloadStream



commit a7597f401019391724eb81d75ab0ee501f67b3df
Author: Philip Withnall <philip tecnocode co uk>
Date:   Tue Jun 21 13:35:45 2011 +0100

    Bug 637664 â Fix GSeekable interface implementation in GDataDownloadStream
    
    Add a few seeking test cases to test the new implementation of seek().
    g_input_stream_truncate() isn't supported by GDataDownloadStream, since it's
    an input stream, not an output stream.
    
    Closes: bgo#637664

 gdata/gdata-buffer.c          |   13 +-
 gdata/gdata-download-stream.c |  184 ++++++++++++++++++-----
 gdata/tests/streams.c         |  332 ++++++++++++++++++++++++++++++++++++++++-
 3 files changed, 481 insertions(+), 48 deletions(-)
---
diff --git a/gdata/gdata-buffer.c b/gdata/gdata-buffer.c
index fe03441..3175dde 100644
--- a/gdata/gdata-buffer.c
+++ b/gdata/gdata-buffer.c
@@ -172,7 +172,7 @@ pop_cancelled_cb (GCancellable *cancellable, CancelledData *data)
 /**
  * gdata_buffer_pop_data:
  * @self: a #GDataBuffer
- * @data: return location for the popped data
+ * @data: (allow-none): return location for the popped data, or %NULL to just drop the data
  * @length_requested: the number of bytes of data requested
  * @reached_eof: return location for a value which is %TRUE when we've reached EOF, %FALSE otherwise, or %NULL
  * @cancellable: (allow-none): a #GCancellable, or %NULL
@@ -204,7 +204,6 @@ gdata_buffer_pop_data (GDataBuffer *self, guint8 *data, gsize length_requested,
 	gboolean cancelled = FALSE;
 
 	g_return_val_if_fail (self != NULL, 0);
-	g_return_val_if_fail (data != NULL, 0);
 	g_return_val_if_fail (cancellable == NULL || G_IS_CANCELLABLE (cancellable), 0);
 
 	/* In the case:
@@ -272,8 +271,10 @@ gdata_buffer_pop_data (GDataBuffer *self, guint8 *data, gsize length_requested,
 
 		/* Copy the data to the output */
 		length_remaining -= chunk_length;
-		memcpy (data, chunk->data + self->head_read_offset, chunk_length);
-		data += chunk_length;
+		if (data != NULL) {
+			memcpy (data, chunk->data + self->head_read_offset, chunk_length);
+			data += chunk_length;
+		}
 
 		/* Free the chunk and move on */
 		next_chunk = chunk->next;
@@ -290,7 +291,9 @@ gdata_buffer_pop_data (GDataBuffer *self, guint8 *data, gsize length_requested,
 		g_assert (chunk != NULL);
 
 		/* Copy the requested data to the output */
-		memcpy (data, chunk->data + self->head_read_offset, length_remaining);
+		if (data != NULL) {
+			memcpy (data, chunk->data + self->head_read_offset, length_remaining);
+		}
 		self->head_read_offset += length_remaining;
 	}
 
diff --git a/gdata/gdata-download-stream.c b/gdata/gdata-download-stream.c
index d19fcfb..ac14a85 100644
--- a/gdata/gdata-download-stream.c
+++ b/gdata/gdata-download-stream.c
@@ -125,19 +125,28 @@ static gboolean gdata_download_stream_can_truncate (GSeekable *seekable);
 static gboolean gdata_download_stream_truncate (GSeekable *seekable, goffset offset, GCancellable *cancellable, GError **error);
 
 static void create_network_thread (GDataDownloadStream *self, GError **error);
+static void reset_network_thread (GDataDownloadStream *self);
 
 /*
  * The GDataDownloadStream can be in one of several states:
  *  1. Pre-network activity. This is the state that the stream is created in. @network_thread and @cancellable are both %NULL, and @finished is %FALSE.
  *     The stream will remain in this state until gdata_download_stream_read() or gdata_download_stream_seek() are called for the first time.
  *     @content_type and @content_length are at their default values (NULL and -1, respectively).
- *  2. Network activity. This state is entered when gdata_download_stream_read() or gdata_download_stream_seek() are called for the first time.
- *     @network_thread and @cancellable are created, while @finished remains %FALSE.
+ *  2. Network activity. This state is entered when gdata_download_stream_read() is called for the first time.
+ *     @network_thread, @buffer and @cancellable are created, while @finished remains %FALSE.
  *     As soon as the headers are downloaded, which is guaranteed to be before the first call to gdata_download_stream_read() returns, @content_type
  *     and @content_length are set from the headers. From this point onwards, they are immutable.
- *  3. Post-network activity. This state is reached once the download thread finishes downloading, either due to having downloaded everything, or due
- *     to being cancelled by gdata_download_stream_close(). @network_thread is non-%NULL, but meaningless; @cancellable is still a valid #GCancellable
- *     instance; and @finished is set to %TRUE. At the same time, @finished_cond is signalled. The stream remains in this state until it's destroyed.
+ *  3. Reset network activity. This state is entered only if case 3 is encountered in a call to gdata_download_stream_seek(): a seek to an offset which
+ *     has already been read out of the buffer. In this state, @buffer is freed and set to %NULL, @network_thread is cancelled (then set to %NULL),
+ *     and @offset is set to the seeked-to offset. @finished remains at %FALSE.
+ *     When the next call to gdata_download_stream_read() is made, the download stream will go back to state 2 as if this was the first call to
+ *     gdata_download_stream_read().
+ *  4. Post-network activity. This state is reached once the download thread finishes downloading, due to having downloaded everything.
+ *     @buffer is non-%NULL, @network_thread is non-%NULL, but meaningless; @cancellable is still a valid #GCancellable instance; and @finished is set
+ *     to %TRUE. At the same time, @finished_cond is signalled.
+ *     This state can be exited either by making a call to gdata_download_stream_seek(), in which case the stream will go back to state 3; or by
+ *     calling gdata_download_stream_close(), in which case the stream will return errors for all operations, as the underlying %GInputStream will be
+ *     marked as closed.
  */
 struct _GDataDownloadStreamPrivate {
 	gchar *download_uri;
@@ -146,7 +155,7 @@ struct _GDataDownloadStreamPrivate {
 	SoupSession *session;
 	SoupMessage *message;
 	GDataBuffer *buffer;
-	goffset offset; /* seek offset */
+	goffset offset; /* current position in the stream */
 
 	GThread *network_thread;
 	GCancellable *cancellable;
@@ -305,7 +314,7 @@ static void
 gdata_download_stream_init (GDataDownloadStream *self)
 {
 	self->priv = G_TYPE_INSTANCE_GET_PRIVATE (self, GDATA_TYPE_DOWNLOAD_STREAM, GDataDownloadStreamPrivate);
-	self->priv->buffer = gdata_buffer_new ();
+	self->priv->buffer = NULL; /* created when the network thread is started and destroyed when the stream is closed */
 
 	self->priv->finished = FALSE;
 	self->priv->finished_cond = g_cond_new ();
@@ -398,11 +407,13 @@ gdata_download_stream_finalize (GObject *object)
 {
 	GDataDownloadStreamPrivate *priv = GDATA_DOWNLOAD_STREAM (object)->priv;
 
+	reset_network_thread (GDATA_DOWNLOAD_STREAM (object));
+
 	g_cond_free (priv->finished_cond);
 	g_static_mutex_free (&(priv->finished_mutex));
 
 	g_static_mutex_free (&(priv->content_mutex));
-	gdata_buffer_free (priv->buffer);
+
 	g_free (priv->download_uri);
 	g_free (priv->content_type);
 
@@ -515,6 +526,7 @@ gdata_download_stream_read (GInputStream *stream, void *buffer, gsize count, GCa
 
 	/* Read the data off the buffer. If the operation is cancelled, it'll probably still return a positive number of bytes read â if it does, we
 	 * can return without error. Iff it returns a non-positive number of bytes should we return an error. */
+	g_assert (priv->buffer != NULL);
 	length_read = (gssize) gdata_buffer_pop_data (priv->buffer, buffer, count, &reached_eof, child_cancellable);
 
 	if (length_read < 1 && g_cancellable_set_error_if_cancelled (child_cancellable, &child_error) == TRUE) {
@@ -550,6 +562,11 @@ done:
 	if (child_error != NULL)
 		g_propagate_error (error, child_error);
 
+	/* Update our internal offset */
+	if (length_read > 0) {
+		priv->offset += length_read;
+	}
+
 	return length_read;
 }
 
@@ -593,17 +610,9 @@ gdata_download_stream_close (GInputStream *stream, GCancellable *cancellable, GE
 	GError *child_error = NULL;
 
 	/* If the operation was never started, return successfully immediately */
-	if (priv->network_thread == NULL)
-		return TRUE;
-
-	/* If we've already closed the stream, return G_IO_ERROR_CLOSED */
-	g_static_mutex_lock (&(priv->finished_mutex));
-	if (priv->finished == FALSE) {
-		g_static_mutex_unlock (&(priv->finished_mutex));
-		g_set_error_literal (error, G_IO_ERROR, G_IO_ERROR_CLOSED, _("Stream is already closed"));
-		return FALSE;
+	if (priv->network_thread == NULL) {
+		goto done;
 	}
-	g_static_mutex_unlock (&(priv->finished_mutex));
 
 	/* Allow cancellation */
 	data.download_stream = GDATA_DOWNLOAD_STREAM (stream);
@@ -644,6 +653,16 @@ gdata_download_stream_close (GInputStream *stream, GCancellable *cancellable, GE
 	if (global_cancelled_signal != 0)
 		g_cancellable_disconnect (priv->cancellable, global_cancelled_signal);
 
+done:
+	/* If we were successful, tidy up various bits of state */
+	g_static_mutex_lock (&(priv->finished_mutex));
+
+	if (success == TRUE && priv->finished == TRUE) {
+		reset_network_thread (GDATA_DOWNLOAD_STREAM (stream));
+	}
+
+	g_static_mutex_unlock (&(priv->finished_mutex));
+
 	g_assert ((success == TRUE && child_error == NULL) || (success == FALSE && child_error != NULL));
 
 	if (child_error != NULL)
@@ -666,56 +685,104 @@ gdata_download_stream_can_seek (GSeekable *seekable)
 
 extern void soup_message_io_cleanup (SoupMessage *msg);
 
-/* Copied from SoupInputStream */
 static gboolean
 gdata_download_stream_seek (GSeekable *seekable, goffset offset, GSeekType type, GCancellable *cancellable, GError **error)
 {
 	GDataDownloadStreamPrivate *priv = GDATA_DOWNLOAD_STREAM (seekable)->priv;
-	gchar *range = NULL;
+	GError *child_error = NULL;
 
-	if (type == G_SEEK_END) {
-		/* FIXME: we could send "bytes=-offset", but unless we know the
-		 * Content-Length, we wouldn't be able to answer a tell() properly.
-		 * We could find the Content-Length by doing a HEAD...
-		*/
+	if (type == G_SEEK_END && priv->content_length == -1) {
+		/* If we don't have the Content-Length, we can't calculate the offset from the start of the stream properly, so just give up.
+		 * We could technically use a HEAD request to get the Content-Length, but this hasn't been tried yet (FIXME). */
 		g_set_error_literal (error, G_IO_ERROR, G_IO_ERROR_NOT_SUPPORTED, "G_SEEK_END not currently supported");
 		return FALSE;
 	}
 
-	if (g_input_stream_set_pending (G_INPUT_STREAM (seekable), error) == FALSE)
+	if (g_input_stream_set_pending (G_INPUT_STREAM (seekable), error) == FALSE) {
 		return FALSE;
+	}
 
-	/* Cancel the current network thread if it exists */
-	if (gdata_download_stream_close (G_INPUT_STREAM (seekable), NULL, error) == FALSE)
-		goto done;
-	soup_message_io_cleanup (priv->message);
-
+	/* Ensure that offset is relative to the start of the stream. */
 	switch (type) {
 		case G_SEEK_CUR:
 			offset += priv->offset;
-			/* fall through */
+			break;
 		case G_SEEK_SET:
-			range = g_strdup_printf ("bytes=%" G_GUINT64_FORMAT "-", (guint64) offset);
-			priv->offset = offset;
+			/* Nothing needs doing */
 			break;
 		case G_SEEK_END:
+			offset += priv->content_length;
+			break;
 		default:
 			g_assert_not_reached ();
 	}
 
-	/* Change the Range header and re-send the message */
-	soup_message_headers_remove (priv->message->request_headers, "Range");
-	soup_message_headers_append (priv->message->request_headers, "Range", range);
-	g_free (range);
+	/* There are three cases to consider:
+	 *  1. The network thread hasn't been started. In this case, we need to set the offset and do nothing. When the network thread is started
+	 *     (in the next read() call), a Range header will be set on it which will give the correct seek.
+	 *  2. The network thread has been started and the seek is to a position greater than our current position (i.e. one which already does, or
+	 *     will soon, exist in the buffer). In this case, we need to pop the intervening bytes off the buffer (which may block) and update the
+	 *     offset.
+	 *  3. The network thread has been started and the seek is to a position which has already been popped off the buffer. In this case, we need
+	 *     to set the offset and cancel the network thread. When the network thread is restarted (in the next read() call), a Range header will
+	 *     be set on it which will give the correct seek.
+	 */
+
+	if (priv->network_thread == NULL) {
+		/* Case 1. Set the offset and we're done. */
+		priv->offset = offset;
+
+		goto done;
+	}
+
+	/* Cases 2 and 3. The network thread has already been started. */
+	if (offset >= priv->offset) {
+		goffset num_intervening_bytes;
+		gssize length_read;
+
+		/* Case 2. Pop off the intervening bytes and update the offset. If we can't pop enough bytes off, we throw an error. */
+		num_intervening_bytes = offset - priv->offset;
+		g_assert (priv->buffer != NULL);
+		length_read = (gssize) gdata_buffer_pop_data (priv->buffer, NULL, num_intervening_bytes, NULL, cancellable);
 
-	/* Launch a new thread with the modified message */
-	create_network_thread (GDATA_DOWNLOAD_STREAM (seekable), error);
+		if (length_read != num_intervening_bytes) {
+			if (g_cancellable_set_error_if_cancelled (cancellable, &child_error) == FALSE) {
+				/* Tried to seek too far */
+				g_set_error_literal (&child_error, G_IO_ERROR, G_IO_ERROR_INVALID_ARGUMENT, _("Invalid seek request"));
+			}
+
+			goto done;
+		}
+
+		/* Update the offset */
+		priv->offset = offset;
+
+		goto done;
+	} else {
+		/* Case 3. Cancel the current network thread. Note that we don't allow cancellation of this call, as we depend on it waiting for
+		 * the network thread to join. */
+		if (gdata_download_stream_close (G_INPUT_STREAM (seekable), NULL, &child_error) == FALSE) {
+			goto done;
+		}
+
+		/* Update the offset */
+		priv->offset = offset;
+
+		/* Mark the thread as unfinished */
+		g_static_mutex_lock (&(priv->finished_mutex));
+		priv->finished = FALSE;
+		g_static_mutex_unlock (&(priv->finished_mutex));
+
+		goto done;
+	}
 
 done:
 	g_input_stream_clear_pending (G_INPUT_STREAM (seekable));
 
-	if (priv->network_thread == NULL)
+	if (child_error != NULL) {
+		g_propagate_error (error, child_error);
 		return FALSE;
+	}
 
 	return TRUE;
 }
@@ -761,6 +828,7 @@ got_chunk_cb (SoupMessage *message, SoupBuffer *buffer, GDataDownloadStream *sel
 		return;
 
 	/* Push the data onto the buffer immediately */
+	g_assert (self->priv->buffer != NULL);
 	gdata_buffer_push_data (self->priv->buffer, (const guint8*) buffer->data, buffer->length);
 }
 
@@ -777,9 +845,17 @@ download_thread (GDataDownloadStream *self)
 	g_signal_connect (priv->message, "got-headers", (GCallback) got_headers_cb, self);
 	g_signal_connect (priv->message, "got-chunk", (GCallback) got_chunk_cb, self);
 
+	/* Set a Range header if our starting offset is non-zero */
+	if (priv->offset > 0) {
+		soup_message_headers_set_range (priv->message->request_headers, priv->offset, -1);
+	} else {
+		soup_message_headers_remove (priv->message->request_headers, "Range");
+	}
+
 	_gdata_service_actually_send_message (priv->session, priv->message, priv->network_cancellable, NULL);
 
 	/* Mark the buffer as having reached EOF */
+	g_assert (priv->buffer != NULL);
 	gdata_buffer_push_data (priv->buffer, NULL, 0);
 
 	/* Mark the download as finished */
@@ -798,10 +874,36 @@ create_network_thread (GDataDownloadStream *self, GError **error)
 {
 	GDataDownloadStreamPrivate *priv = self->priv;
 
+	g_assert (priv->buffer == NULL);
+	priv->buffer = gdata_buffer_new ();
+
 	g_assert (priv->network_thread == NULL);
 	priv->network_thread = g_thread_create ((GThreadFunc) download_thread, self, TRUE, error);
 }
 
+static void
+reset_network_thread (GDataDownloadStream *self)
+{
+	GDataDownloadStreamPrivate *priv = self->priv;
+
+	priv->network_thread = NULL;
+
+	if (priv->buffer != NULL) {
+		gdata_buffer_free (priv->buffer);
+		priv->buffer = NULL;
+	}
+
+	if (priv->message != NULL) {
+		soup_message_io_cleanup (priv->message);
+	}
+
+	priv->offset = 0;
+
+	if (priv->network_cancellable != NULL) {
+		g_cancellable_reset (priv->network_cancellable);
+	}
+}
+
 /**
  * gdata_download_stream_new:
  * @service: a #GDataService
diff --git a/gdata/tests/streams.c b/gdata/tests/streams.c
index 330791c..ae8300d 100644
--- a/gdata/tests/streams.c
+++ b/gdata/tests/streams.c
@@ -55,7 +55,7 @@ get_test_string (guint start_num, guint end_num)
 }
 
 static void
-test_download_stream_download_content_length_server_handler_cb (SoupServer *server, SoupMessage *message, const char *path, GHashTable *query,
+test_download_stream_download_server_content_length_handler_cb (SoupServer *server, SoupMessage *message, const char *path, GHashTable *query,
                                                                 SoupClientContext *client, gpointer user_data)
 {
 	gchar *test_string;
@@ -97,7 +97,7 @@ test_download_stream_download_content_length (void)
 	server = soup_server_new (SOUP_SERVER_INTERFACE, addr,
 	                          SOUP_SERVER_ASYNC_CONTEXT, async_context,
 	                          NULL);
-	soup_server_add_handler (server, NULL, (SoupServerCallback) test_download_stream_download_content_length_server_handler_cb, NULL, NULL);
+	soup_server_add_handler (server, NULL, (SoupServerCallback) test_download_stream_download_server_content_length_handler_cb, NULL, NULL);
 
 	g_object_unref (addr);
 
@@ -152,6 +152,331 @@ test_download_stream_download_content_length (void)
 }
 
 static void
+test_download_stream_download_server_seek_handler_cb (SoupServer *server, SoupMessage *message, const char *path, GHashTable *query,
+                                                      SoupClientContext *client, gpointer user_data)
+{
+	gchar *test_string;
+	goffset test_string_length;
+
+	test_string = get_test_string (1, 1000);
+	test_string_length = strlen (test_string) + 1;
+
+	/* Add some response headers */
+	soup_message_set_status (message, SOUP_STATUS_OK);
+	soup_message_body_append (message->response_body, SOUP_MEMORY_TAKE, test_string, test_string_length);
+}
+
+/* Test seeking before the first read */
+static void
+test_download_stream_download_seek_before_start (void)
+{
+	SoupServer *server;
+	GMainContext *async_context;
+	SoupAddress *addr;
+	GThread *thread;
+	gchar *download_uri, *test_string;
+	goffset test_string_offset = 0;
+	guint test_string_length;
+	GDataService *service;
+	GInputStream *download_stream;
+	gssize length_read;
+	guint8 buffer[20];
+	gboolean success;
+	GError *error = NULL;
+
+	/* Create the server */
+	async_context = g_main_context_new ();
+	addr = soup_address_new ("127.0.0.1", SOUP_ADDRESS_ANY_PORT);
+	soup_address_resolve_sync (addr, NULL);
+
+	server = soup_server_new (SOUP_SERVER_INTERFACE, addr,
+	                          SOUP_SERVER_ASYNC_CONTEXT, async_context,
+	                          NULL);
+	soup_server_add_handler (server, NULL, (SoupServerCallback) test_download_stream_download_server_seek_handler_cb, NULL, NULL);
+
+	g_object_unref (addr);
+
+	g_assert (server != NULL);
+
+	/* Create a thread for the server */
+	thread = g_thread_create ((GThreadFunc) run_server_thread, server, TRUE, &error);
+	g_assert_no_error (error);
+	g_assert (thread != NULL);
+
+	/* Create a new download stream connected to the server */
+	download_uri = g_strdup_printf ("http://127.0.0.1:%u/";, soup_server_get_port (server));
+	service = GDATA_SERVICE (gdata_youtube_service_new ("developer-key", NULL));
+	download_stream = gdata_download_stream_new (service, NULL, download_uri, NULL);
+	g_object_unref (service);
+	g_free (download_uri);
+
+	/* Read alternating blocks into a string and compare with what we expect as we go. i.e. Skip 20 bytes, then read 20 bytes, etc. */
+	test_string = get_test_string (1, 1000);
+	test_string_length = strlen (test_string) + 1;
+
+	while (TRUE) {
+		/* Check the seek offset */
+		g_assert_cmpint (g_seekable_tell (G_SEEKABLE (download_stream)), ==, test_string_offset);
+
+		/* Seek forward a buffer length */
+		if (g_seekable_seek (G_SEEKABLE (download_stream), sizeof (buffer), G_SEEK_CUR, NULL, &error) == FALSE) {
+			/* Tried to seek past the end of the stream */
+			g_assert_error (error, G_IO_ERROR, G_IO_ERROR_INVALID_ARGUMENT);
+			g_clear_error (&error);
+			break;
+		}
+
+		test_string_offset += sizeof (buffer);
+		g_assert_no_error (error);
+
+		/* Check the seek offset again */
+		g_assert_cmpint (g_seekable_tell (G_SEEKABLE (download_stream)), ==, test_string_offset);
+
+		/* Read a buffer-load */
+		length_read = g_input_stream_read (download_stream, buffer, sizeof (buffer), NULL, &error);
+
+		g_assert_no_error (error);
+		g_assert_cmpint (length_read, <=, sizeof (buffer));
+
+		/* Check the buffer-load against the test string */
+		g_assert (memcmp (buffer, test_string + test_string_offset, length_read) == 0);
+		test_string_offset += length_read;
+
+		/* Check the seek offset again */
+		g_assert_cmpint (g_seekable_tell (G_SEEKABLE (download_stream)), ==, test_string_offset);
+
+		if (length_read < (gssize) sizeof (buffer)) {
+			/* Done */
+			break;
+		}
+	}
+
+	g_free (test_string);
+
+	/* Check the seek offset is within one buffer-load of the end */
+	g_assert_cmpint (g_seekable_tell (G_SEEKABLE (download_stream)), >, test_string_length - sizeof (buffer));
+	g_assert_cmpint (g_seekable_tell (G_SEEKABLE (download_stream)), <=, test_string_length);
+
+	/* Close the stream */
+	success = g_input_stream_close (download_stream, NULL, &error);
+	g_assert_no_error (error);
+	g_assert (success == TRUE);
+
+	/* Kill the server and wait for it to die */
+	soup_add_completion (async_context, (GSourceFunc) quit_server_cb, server);
+	g_thread_join (thread);
+
+	g_object_unref (download_stream);
+	g_object_unref (server);
+	g_main_context_unref (async_context);
+}
+
+/* Test seeking forwards after the first read */
+static void
+test_download_stream_download_seek_after_start_forwards (void)
+{
+	SoupServer *server;
+	GMainContext *async_context;
+	SoupAddress *addr;
+	GThread *thread;
+	gchar *download_uri, *test_string;
+	goffset test_string_offset = 0;
+	guint test_string_length;
+	GDataService *service;
+	GInputStream *download_stream;
+	gssize length_read;
+	guint8 buffer[20];
+	gboolean success;
+	GError *error = NULL;
+
+	/* Create the server */
+	async_context = g_main_context_new ();
+	addr = soup_address_new ("127.0.0.1", SOUP_ADDRESS_ANY_PORT);
+	soup_address_resolve_sync (addr, NULL);
+
+	server = soup_server_new (SOUP_SERVER_INTERFACE, addr,
+	                          SOUP_SERVER_ASYNC_CONTEXT, async_context,
+	                          NULL);
+	soup_server_add_handler (server, NULL, (SoupServerCallback) test_download_stream_download_server_seek_handler_cb, NULL, NULL);
+
+	g_object_unref (addr);
+
+	g_assert (server != NULL);
+
+	/* Create a thread for the server */
+	thread = g_thread_create ((GThreadFunc) run_server_thread, server, TRUE, &error);
+	g_assert_no_error (error);
+	g_assert (thread != NULL);
+
+	/* Create a new download stream connected to the server */
+	download_uri = g_strdup_printf ("http://127.0.0.1:%u/";, soup_server_get_port (server));
+	service = GDATA_SERVICE (gdata_youtube_service_new ("developer-key", NULL));
+	download_stream = gdata_download_stream_new (service, NULL, download_uri, NULL);
+	g_object_unref (service);
+	g_free (download_uri);
+
+	/* Read alternating blocks into a string and compare with what we expect as we go. i.e. Read 20 bytes, then skip 20 bytes, etc. */
+	test_string = get_test_string (1, 1000);
+	test_string_length = strlen (test_string) + 1;
+
+	while (TRUE) {
+		/* Check the seek offset */
+		g_assert_cmpint (g_seekable_tell (G_SEEKABLE (download_stream)), ==, test_string_offset);
+
+		/* Read a buffer-load */
+		length_read = g_input_stream_read (download_stream, buffer, sizeof (buffer), NULL, &error);
+
+		g_assert_no_error (error);
+		g_assert_cmpint (length_read, <=, sizeof (buffer));
+
+		/* Check the buffer-load against the test string */
+		g_assert (memcmp (buffer, test_string + test_string_offset, length_read) == 0);
+		test_string_offset += length_read;
+
+		/* Check the seek offset again */
+		g_assert_cmpint (g_seekable_tell (G_SEEKABLE (download_stream)), ==, test_string_offset);
+
+		if (length_read < (gssize) sizeof (buffer)) {
+			/* Done */
+			break;
+		}
+
+		/* Seek forward a buffer length */
+		if (g_seekable_seek (G_SEEKABLE (download_stream), sizeof (buffer), G_SEEK_CUR, NULL, &error) == FALSE) {
+			/* Tried to seek past the end of the stream */
+			g_assert_error (error, G_IO_ERROR, G_IO_ERROR_INVALID_ARGUMENT);
+			g_clear_error (&error);
+			break;
+		}
+
+		test_string_offset += sizeof (buffer);
+		g_assert_no_error (error);
+
+		/* Check the seek offset again */
+		g_assert_cmpint (g_seekable_tell (G_SEEKABLE (download_stream)), ==, test_string_offset);
+	}
+
+	g_free (test_string);
+
+	/* Check the seek offset is within one buffer-load of the end */
+	g_assert_cmpint (g_seekable_tell (G_SEEKABLE (download_stream)), >, test_string_length - sizeof (buffer));
+	g_assert_cmpint (g_seekable_tell (G_SEEKABLE (download_stream)), <=, test_string_length);
+
+	/* Close the stream */
+	success = g_input_stream_close (download_stream, NULL, &error);
+	g_assert_no_error (error);
+	g_assert (success == TRUE);
+
+	/* Kill the server and wait for it to die */
+	soup_add_completion (async_context, (GSourceFunc) quit_server_cb, server);
+	g_thread_join (thread);
+
+	g_object_unref (download_stream);
+	g_object_unref (server);
+	g_main_context_unref (async_context);
+}
+
+/* Test seeking backwards after the first read */
+static void
+test_download_stream_download_seek_after_start_backwards (void)
+{
+	SoupServer *server;
+	GMainContext *async_context;
+	SoupAddress *addr;
+	GThread *thread;
+	gchar *download_uri, *test_string;
+	goffset test_string_offset = 0;
+	guint repeat_count;
+	GDataService *service;
+	GInputStream *download_stream;
+	gssize length_read;
+	guint8 buffer[20];
+	gboolean success;
+	GError *error = NULL;
+
+	/* Create the server */
+	async_context = g_main_context_new ();
+	addr = soup_address_new ("127.0.0.1", SOUP_ADDRESS_ANY_PORT);
+	soup_address_resolve_sync (addr, NULL);
+
+	server = soup_server_new (SOUP_SERVER_INTERFACE, addr,
+	                          SOUP_SERVER_ASYNC_CONTEXT, async_context,
+	                          NULL);
+	soup_server_add_handler (server, NULL, (SoupServerCallback) test_download_stream_download_server_seek_handler_cb, NULL, NULL);
+
+	g_object_unref (addr);
+
+	g_assert (server != NULL);
+
+	/* Create a thread for the server */
+	thread = g_thread_create ((GThreadFunc) run_server_thread, server, TRUE, &error);
+	g_assert_no_error (error);
+	g_assert (thread != NULL);
+
+	/* Create a new download stream connected to the server */
+	download_uri = g_strdup_printf ("http://127.0.0.1:%u/";, soup_server_get_port (server));
+	service = GDATA_SERVICE (gdata_youtube_service_new ("developer-key", NULL));
+	download_stream = gdata_download_stream_new (service, NULL, download_uri, NULL);
+	g_object_unref (service);
+	g_free (download_uri);
+
+	/* Read a block in, then skip back over the block again. i.e. Read the first block, read the second block, skip back over the second block,
+	 * read the second block again, skip back over it, etc. Close the stream after doing this several times. */
+	test_string = get_test_string (1, 1000);
+
+	/* Read a buffer-load to begin with */
+	length_read = g_input_stream_read (download_stream, buffer, sizeof (buffer), NULL, &error);
+
+	g_assert_no_error (error);
+	test_string_offset += length_read;
+
+	for (repeat_count = 6; repeat_count > 0; repeat_count--) {
+		/* Check the seek offset */
+		g_assert_cmpint (g_seekable_tell (G_SEEKABLE (download_stream)), ==, test_string_offset);
+
+		/* Read a buffer-load */
+		length_read = g_input_stream_read (download_stream, buffer, sizeof (buffer), NULL, &error);
+
+		g_assert_no_error (error);
+		g_assert_cmpint (length_read, <=, sizeof (buffer));
+
+		/* Check the buffer-load against the test string */
+		g_assert (memcmp (buffer, test_string + test_string_offset, length_read) == 0);
+		test_string_offset += length_read;
+
+		/* Check the seek offset again */
+		g_assert_cmpint (g_seekable_tell (G_SEEKABLE (download_stream)), ==, test_string_offset);
+
+		/* Seek backwards a buffer length */
+		success = g_seekable_seek (G_SEEKABLE (download_stream), -length_read, G_SEEK_CUR, NULL, &error);
+		g_assert_no_error (error);
+		g_assert (success == TRUE);
+		test_string_offset -= length_read;
+
+		/* Check the seek offset again */
+		g_assert_cmpint (g_seekable_tell (G_SEEKABLE (download_stream)), ==, test_string_offset);
+	}
+
+	g_free (test_string);
+
+	/* Check the seek offset is at the end of the first buffer-load */
+	g_assert_cmpint (g_seekable_tell (G_SEEKABLE (download_stream)), ==, sizeof (buffer));
+
+	/* Close the stream */
+	success = g_input_stream_close (download_stream, NULL, &error);
+	g_assert_no_error (error);
+	g_assert (success == TRUE);
+
+	/* Kill the server and wait for it to die */
+	soup_add_completion (async_context, (GSourceFunc) quit_server_cb, server);
+	g_thread_join (thread);
+
+	g_object_unref (download_stream);
+	g_object_unref (server);
+	g_main_context_unref (async_context);
+}
+
+static void
 test_upload_stream_upload_no_entry_content_length_server_handler_cb (SoupServer *server, SoupMessage *message, const char *path, GHashTable *query,
                                                                      SoupClientContext *client, gpointer user_data)
 {
@@ -259,6 +584,9 @@ main (int argc, char *argv[])
 	gdata_test_init (argc, argv);
 
 	g_test_add_func ("/download-stream/download_content_length", test_download_stream_download_content_length);
+	g_test_add_func ("/download-stream/download_seek/before_start", test_download_stream_download_seek_before_start);
+	g_test_add_func ("/download-stream/download_seek/after_start_forwards", test_download_stream_download_seek_after_start_forwards);
+	g_test_add_func ("/download-stream/download_seek/after_start_backwards", test_download_stream_download_seek_after_start_backwards);
 
 	g_test_add_func ("/upload-stream/upload_no_entry_content_length", test_upload_stream_upload_no_entry_content_length);
 



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