[libgdata] Bug 637664 — Fix GSeekable interface implementation in GDataDownloadStream
- From: Philip Withnall <pwithnall src gnome org>
- To: commits-list gnome org
- Cc:
- Subject: [libgdata] Bug 637664 — Fix GSeekable interface implementation in GDataDownloadStream
- Date: Wed, 22 Jun 2011 22:37:24 +0000 (UTC)
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]