[folks] Add a generic caching object to the core of folks



commit 776adae7b0e61ef5d542d992d5819bcacadce70d
Author: Philip Withnall <philip tecnocode co uk>
Date:   Thu Jun 16 18:33:26 2011 +0100

    Add a generic caching object to the core of folks
    
    This adds the ObjectCache API.
    
    Helps: bgo#652643

 NEWS                          |    1 +
 folks/Makefile.am             |    1 +
 folks/object-cache.vala       |  403 ++++++++++++++++++++++++++++++
 tests/folks/Makefile.am       |    6 +
 tests/folks/object-cache.vala |  544 +++++++++++++++++++++++++++++++++++++++++
 5 files changed, 955 insertions(+), 0 deletions(-)
---
diff --git a/NEWS b/NEWS
index 07c095b..9c05d18 100644
--- a/NEWS
+++ b/NEWS
@@ -38,6 +38,7 @@ API changes:
 * Make Folks.Utils public and add Gee structure equality functions
 * AvatarDetails.avatar is now of type LoadableIcon?
 * Add AvatarCache class
+* Add ObjectCache class
 
 Overview of changes from libfolks 0.5.1 to libfolks 0.5.2
 =========================================================
diff --git a/folks/Makefile.am b/folks/Makefile.am
index c145087..82d9044 100644
--- a/folks/Makefile.am
+++ b/folks/Makefile.am
@@ -41,6 +41,7 @@ libfolks_la_SOURCES = \
 	utils.vala \
 	potential-match.vala \
 	avatar-cache.vala \
+	object-cache.vala \
 	$(NULL)
 
 libfolks_la_VALAFLAGS = \
diff --git a/folks/object-cache.vala b/folks/object-cache.vala
new file mode 100644
index 0000000..3724f9e
--- /dev/null
+++ b/folks/object-cache.vala
@@ -0,0 +1,403 @@
+/*
+ * Copyright (C) 2011 Collabora Ltd.
+ *
+ * This library is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as published by
+ * the Free Software Foundation, either version 2.1 of the License, or
+ * (at your option) any later version.
+ *
+ * This library is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public License
+ * along with this library.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * Authors:
+ *       Philip Withnall <philip withnall collabora co uk>
+ */
+
+using GLib;
+using Gee;
+
+/**
+ * A generic abstract cache for sets of objects. This can be used by subclasses
+ * to implement caching of homogeneous sets of objects. Subclasses simply have
+ * to implement serialisation and deserialisation of the objects to and from
+ * { link Variant}s.
+ *
+ * It's intended that this class be used for providing caching layers for
+ * { link PersonaStore}s, for example.
+ *
+ * @since UNRELEASED
+ */
+public abstract class Folks.ObjectCache<T> : Object
+{
+  /* The version number of the header/wrapper for a cache file. When accompanied
+   * by a version number for the serialised object type, this unambiguously
+   * keys the variant type describing an entire cache file.
+   *
+   * The wrapper and object version numbers are stored as the first two bytes
+   * of a cache file. They can't be stored as part of the Variant which forms
+   * the rest of the file, as to interpret the Variant its entire type has to
+   * be known â which depends on the version numbers. */
+  private static const uint8 _FILE_FORMAT_VERSION = 1;
+
+  /* The length of the version header at the beginning of the file. This has
+   * to be a multiple of 8 to keep Variant's alignment code happy.
+   * As documented above, currently only the first two bytes of this header
+   * are used (for version numbers). */
+  private static const size_t _HEADER_WIDTH = 8; /* bytes */
+
+  private string _type_id;
+  private string _id;
+  private File _cache_directory;
+  private File _cache_file;
+
+  /**
+   * Get the { link VariantType} of the serialised form of an object stored
+   * in this cache.
+   *
+   * If a smooth upgrade path is needed in future due to cache file format
+   * changes, this may be modified to take a version parameter.
+   *
+   * @since UNRELEASED
+   */
+  protected abstract VariantType get_serialised_object_type ();
+
+  /**
+   * Get the version of the variant type returned by
+   * { link ObjectCache.get_serialised_object_type}. This must be incremented
+   * every time the variant type changes so that old cache files aren't
+   * misinterpreted.
+   *
+   * @since UNRELEASED
+   */
+  protected abstract uint8 get_serialised_object_version ();
+
+  /**
+   * Serialise the given `object` to a { link Variant} and return the variant.
+   * The variant must be of the type returned by
+   * { link ObjectCache.get_serialised_object_type()}.
+   *
+   * @since UNRELEASED
+   */
+  protected abstract Variant serialise_object (T object);
+
+  /**
+   * Deserialise the given `variant` to a new instance of an object. The variant
+   * is guaranteed to have the type returned by
+   * { link ObjectCache.get_serialised_object_type()}.
+   *
+   * @since UNRELEASED
+   */
+  protected abstract T deserialise_object (Variant variant);
+
+  /**
+   * Create a new cache instance using the given type ID and ID. This is
+   * protected as the `type_id` will typically be set statically by subclasses.
+   *
+   * @param type_id A string identifying the type of object being cached. This
+   * has to be suitable for use as a directory name; i.e. lower case,
+   * hyphen-separated.
+   * @param id A string identifying the particular cache instance. This will
+   * form the file name of the cache file, but will be escaped beforehand, so
+   * can be an arbitrary non-empty string.
+   * @return A new cache instance
+   *
+   * @since UNRELEASED
+   */
+  protected ObjectCache (string type_id, string id)
+    {
+      assert (id != "");
+
+      this._type_id = type_id;
+      this._id = id;
+
+      debug ("Creating object cache for type ID '%s' with ID '%s'.",
+          type_id, id);
+
+      this._cache_directory =
+          File.new_for_path (Environment.get_user_cache_dir ())
+              .get_child ("folks")
+              .get_child (type_id);
+      this._cache_file =
+          this._cache_directory.get_child (Uri.escape_string (id, "", false));
+    }
+
+  /**
+   * Load a set of objects from the cache and return them as a new set. If the
+   * cache file doesn't exist, `null` will be returned. An empty set will be
+   * returned if the cache file existed but was empty (i.e. was stored with
+   * an empty set originally).
+   *
+   * Loading the objects can be cancelled using `cancellable`. If it is, `null`
+   * will be returned.
+   *
+   * If any errors are encountered while loading the objects, warnings will be
+   * logged as appropriate and `null` will be returned.
+   *
+   * @param cancellable A { link Cancellable} for the operation, or `null`.
+   * @return A set of objects from the cache, or `null`.
+   *
+   * @since UNRELEASED
+   */
+  public async Set<T>? load_objects (Cancellable? cancellable = null)
+    {
+      debug ("Loading cache (type ID '%s', ID '%s') from file '%s'.",
+          this._type_id, this._id, this._cache_file.get_path ());
+
+      // Read in the file
+      uint8[] data;
+
+      try
+        {
+          yield this._cache_file.load_contents_async (cancellable, out data);
+        }
+      catch (Error e)
+        {
+          if (e is IOError.CANCELLED)
+            {
+              /* not a true error */
+            }
+          else if (e is IOError.NOT_FOUND)
+            {
+              debug ("Couldn't load cache file '%s': %s",
+                  this._cache_file.get_path (), e.message);
+            }
+          else
+            {
+              warning ("Couldn't load cache file '%s': %s",
+                  this._cache_file.get_path (), e.message);
+            }
+
+          return null;
+        }
+
+      // Check the length
+      if (data.length < this._HEADER_WIDTH)
+        {
+          warning ("Cache file '%s' was too small. The file was deleted.",
+              this._cache_file.get_path ());
+          yield this.clear_cache ();
+
+          return null;
+        }
+
+      // Check the version
+      var wrapper_version = data[0];
+      var object_version = data[1];
+
+      if (wrapper_version != this._FILE_FORMAT_VERSION)
+        {
+          warning ("Cache file '%s' was version %u of the file format, " +
+              "but only version %u is supported. The file was deleted.",
+              this._cache_file.get_path (), wrapper_version,
+              this._FILE_FORMAT_VERSION);
+          yield this.clear_cache ();
+
+          return null;
+        }
+
+      unowned uint8[] variant_data = data[this._HEADER_WIDTH:data.length];
+
+      // Deserialise the variant according to the given version numbers
+      var variant_type =
+          this._get_cache_file_variant_type (wrapper_version, object_version);
+      var variant =
+          Variant.new_from_data<uint8[]> (variant_type, variant_data, false,
+              data);
+
+      // Check the variant was deserialised correctly
+      if (variant.is_normal_form () == false)
+        {
+          warning ("Cache file '%s' was corrupt and was deleted.",
+              this._cache_file.get_path ());
+          yield this.clear_cache ();
+
+          return null;
+        }
+
+      // Unpack the stored data
+      var type_id = variant.get_child_value (0).get_string ();
+
+      if (type_id != this._type_id)
+        {
+          warning ("Cache file '%s' had type ID '%s', but '%s' was expected." +
+              "The file was deleted.", this._cache_file.get_path (), type_id,
+              this._type_id);
+          yield this.clear_cache ();
+
+          return null;
+        }
+
+      var id = variant.get_child_value (1).get_string ();
+
+      if (id != this._id)
+        {
+          warning ("Cache file '%s' had ID '%s', but '%s' was expected." +
+              "The file was deleted.", this._cache_file.get_path (), id,
+              this._id);
+          yield this.clear_cache ();
+
+          return null;
+        }
+
+      var objects_variant = variant.get_child_value (2);
+
+      var objects = new HashSet<T> ();
+
+      for (uint i = 0; i < objects_variant.n_children (); i++)
+        {
+          var object_variant = objects_variant.get_child_value (i);
+          var object = this.deserialise_object (object_variant);
+
+          objects.add (object);
+        }
+
+      return objects;
+    }
+
+  /**
+   * Store a set of objects to the cache file, overwriting any existing set of
+   * objects in the cache, or creating the cache file from scratch if it didn't
+   * previously exist.
+   *
+   * Storing the objects can be cancelled using `cancellable`. If it is, the
+   * cache will be left in a consistent state, but may be storing the old set
+   * of objects or the new set.
+   *
+   * @param objects A set of objects to store. This may be empty, but may not
+   * be `null`.
+   * @cancellable A { link Cancellable} for the operation, or `null`.
+   *
+   * @since UNRELEASED
+   */
+  public async void store_objects (Set<T> objects,
+      Cancellable? cancellable = null)
+    {
+      debug ("Storing cache (type ID '%s', ID '%s') to file '%s'.",
+          this._type_id, this._id, this._cache_file.get_path ());
+
+      var child_type = this.get_serialised_object_type ();
+      Variant[] children = new Variant[objects.size];
+
+      // Serialise all the objects in the set
+      uint i = 0;
+      foreach (var object in objects)
+        {
+          children[i++] = this.serialise_object (object);
+        }
+
+      // File format
+      var wrapper_version = this._FILE_FORMAT_VERSION;
+      var object_version = this.get_serialised_object_version ();
+
+      var variant = new Variant.tuple ({
+        new Variant.string (this._type_id), // Type ID
+        new Variant.string (this._id), // ID
+        new Variant.array (child_type, children) // Array of objects
+      });
+
+      assert (variant.get_type ().equal (
+          this._get_cache_file_variant_type (wrapper_version, object_version)));
+
+      // Prepend the version numbers to the data
+      uint8[] data = new uint8[this._HEADER_WIDTH + variant.get_size ()];
+      data[0] = wrapper_version;
+      data[1] = object_version;
+      variant.store (data[this._HEADER_WIDTH:data.length]);
+
+      // Write the data out to the file
+      while (true)
+        {
+          try
+            {
+              yield this._cache_file.replace_contents_async (
+                  (string) data, data.length, null, false,
+                  FileCreateFlags.PRIVATE, cancellable);
+              break;
+            }
+          catch (Error e)
+            {
+              if (e is IOError.NOT_FOUND)
+                {
+                  try
+                    {
+                      yield this._create_cache_directory ();
+                      continue;
+                    }
+                  catch (Error e2)
+                    {
+                      warning ("Couldn't create cache directory '%s': %s",
+                          this._cache_directory.get_path (), e.message);
+                      return;
+                    }
+                }
+              else if (e is IOError.CANCELLED)
+                {
+                  /* We assume the replace_contents_async() call is atomic,
+                   * so cancelling it is atomic as well. */
+                  return;
+                }
+
+              /* Print a warning and delete the cache file so we don't leave
+               * stale cached objects lying around. */
+              warning ("Couldn't write to cache file '%s', so deleting it: %s",
+                  this._cache_file.get_path (), e.message);
+              yield this.clear_cache ();
+
+              return;
+            }
+        }
+    }
+
+  /**
+   * Clear this cache object, deleting its backing file.
+   *
+   * @since UNRELEASED
+   */
+  public async void clear_cache ()
+    {
+      debug ("Clearing cache (type ID '%s', ID '%s'); deleting file '%s'.",
+          this._type_id, this._id, this._cache_file.get_path ());
+
+      try
+        {
+          this._cache_file.delete ();
+        }
+      catch (Error e)
+        {
+          // Ignore errors
+        }
+    }
+
+  private VariantType _get_cache_file_variant_type (uint8 wrapper_version,
+      uint8 object_version)
+    {
+      return new VariantType.tuple ({
+        VariantType.STRING, // Type ID
+        VariantType.STRING, // ID
+        new VariantType.array (this.get_serialised_object_type ()) // Objects
+      });
+    }
+
+  private async void _create_cache_directory () throws Error
+    {
+      try
+        {
+          this._cache_directory.make_directory_with_parents ();
+        }
+      catch (Error e)
+        {
+          // Ignore errors caused by the directory existing already
+          if (!(e is IOError.EXISTS))
+            {
+              throw e;
+            }
+        }
+    }
+}
+
+/* vim: filetype=vala textwidth=80 tabstop=2 expandtab: */
diff --git a/tests/folks/Makefile.am b/tests/folks/Makefile.am
index 8723a28..0d24b12 100644
--- a/tests/folks/Makefile.am
+++ b/tests/folks/Makefile.am
@@ -44,6 +44,7 @@ noinst_PROGRAMS = \
 	backend-loading \
 	aggregation \
 	avatar-cache \
+	object-cache \
 	$(NULL)
 
 SESSION_CONF = $(top_builddir)/tests/lib/telepathy/contactlist/session.conf
@@ -80,6 +81,10 @@ avatar_cache_SOURCES = \
 	avatar-cache.vala \
 	$(NULL)
 
+object_cache_SOURCES = \
+	object-cache.vala \
+	$(NULL)
+
 CLEANFILES = \
         *.pid \
         *.address \
@@ -93,6 +98,7 @@ MAINTAINERCLEANFILES = \
         field_details_vala.stamp \
         utils_vala.stamp \
         avatar_cache_vala.stamp \
+        object_cache_vala.stamp \
         $(NULL)
 
 EXTRA_DIST = \
diff --git a/tests/folks/object-cache.vala b/tests/folks/object-cache.vala
new file mode 100644
index 0000000..b7cee02
--- /dev/null
+++ b/tests/folks/object-cache.vala
@@ -0,0 +1,544 @@
+using Gee;
+using Folks;
+
+/* Dummy ObjectCache subclass */
+internal class TestObject
+{
+  public string my_string { get; set; }
+  public uint my_int { get; set; }
+
+  public TestObject (string my_string, uint my_int)
+    {
+      this.my_string = my_string;
+      this.my_int = my_int;
+    }
+}
+
+internal class TestCache : Folks.ObjectCache<TestObject>
+{
+  internal TestCache (string id)
+    {
+      base ("test", id);
+    }
+
+  protected override VariantType get_serialised_object_type ()
+    {
+      return new VariantType.tuple ({
+        VariantType.STRING,
+        VariantType.UINT32
+      });
+    }
+
+  protected override uint8 get_serialised_object_version ()
+    {
+      return 1;
+    }
+
+  protected override Variant serialise_object (TestObject obj)
+    {
+      return new Variant.tuple ({
+        new Variant.string (obj.my_string),
+        new Variant.uint32 (obj.my_int)
+      });
+    }
+
+  protected override TestObject deserialise_object (Variant variant)
+    {
+      // Deserialise the persona
+      var my_string = variant.get_child_value (0).get_string ();
+      var my_int = variant.get_child_value (1).get_uint32 ();
+
+      return new TestObject (my_string, my_int);
+    }
+}
+
+/* Test suite */
+public class ObjectCacheTests : Folks.TestCase
+{
+  private File _cache_dir;
+
+  public ObjectCacheTests ()
+    {
+      base ("ObjectCache");
+
+      /* Use a temporary cache directory */
+      this._cache_dir =
+          File.new_for_path (Environment.get_tmp_dir ()).
+              get_child ("folks-object-cache-tests");
+
+      // Basic functionality tests
+      this.add_test ("create", this.test_create);
+      this.add_test ("store-objects", this.test_store_objects);
+      this.add_test ("store-objects-empty", this.test_store_objects_empty);
+      this.add_test ("load-objects", this.test_load_objects);
+      this.add_test ("load-objects-empty", this.test_load_objects_empty);
+      this.add_test ("load-objects-nonexistent",
+          this.test_load_objects_nonexistent);
+      this.add_test ("clear", this.test_clear);
+      this.add_test ("clear-empty", this.test_clear_empty);
+      this.add_test ("clear-nonexistent", this.test_clear_nonexistent);
+
+      // Cancellation tests
+      this.add_test ("store-objects-cancellation",
+          this.test_store_objects_cancellation);
+      this.add_test ("load-objects-cancellation",
+          this.test_load_objects_cancellation);
+
+      // Stress test
+      this.add_test ("stress", this.test_stress);
+    }
+
+  public override void set_up ()
+    {
+      this._delete_cache_directory ();
+      Environment.set_variable ("XDG_CACHE_HOME", this._cache_dir.get_path (),
+          true);
+    }
+
+  public override void tear_down ()
+    {
+      this._delete_cache_directory ();
+    }
+
+  protected void _delete_directory (File dir) throws GLib.Error
+    {
+      // Delete the files in the directory
+      var enumerator =
+          dir.enumerate_children (FILE_ATTRIBUTE_STANDARD_NAME,
+              FileQueryInfoFlags.NONE);
+
+      FileInfo? file_info = enumerator.next_file ();
+      while (file_info != null)
+        {
+          var child_file = dir.get_child (file_info.get_name ());
+
+          if (child_file.query_file_type (FileQueryInfoFlags.NONE) ==
+                  FileType.DIRECTORY)
+            {
+              this._delete_directory (child_file);
+            }
+          else
+            {
+              child_file.delete ();
+            }
+
+          file_info = enumerator.next_file ();
+        }
+      enumerator.close ();
+
+      // Delete the directory itself
+      dir.delete ();
+    }
+
+  protected void _delete_cache_directory ()
+    {
+      try
+        {
+          this._delete_directory (this._cache_dir);
+        }
+      catch (Error e)
+        {
+          // Ignore it
+        }
+    }
+
+  public void test_create ()
+    {
+      // Does creating a cache object work?
+      var cache = new TestCache ("test-create");
+
+      assert (cache != null);
+    }
+
+  public void test_store_objects ()
+    {
+      var main_loop = new GLib.MainLoop (null, false);
+      var cache = new TestCache ("test-store-objects");
+
+      var obj_set = new HashSet<TestObject> ();
+      obj_set.add (new TestObject ("Foo", 1));
+      obj_set.add (new TestObject ("Bar", 2));
+      obj_set.add (new TestObject ("De", 3));
+      obj_set.add (new TestObject ("Baz", 4));
+
+      cache.store_objects.begin (obj_set, null, (o, r) =>
+        {
+          cache.store_objects.end (r);
+          main_loop.quit ();
+        });
+
+      main_loop.run ();
+    }
+
+  public void test_store_objects_empty ()
+    {
+      var main_loop = new GLib.MainLoop (null, false);
+      var cache = new TestCache ("test-store-objects-empty");
+
+      cache.store_objects.begin (new HashSet<TestObject> (), null, (o, r) =>
+        {
+          cache.store_objects.end (r);
+          main_loop.quit ();
+        });
+
+      main_loop.run ();
+    }
+
+  public void test_load_objects ()
+    {
+      var main_loop = new GLib.MainLoop (null, false);
+      var cache = new TestCache ("test-load-objects");
+
+      // Create some objects
+      var obj1 = new TestObject ("Foo", 1);
+      var obj2 = new TestObject ("Bar", 2);
+
+      var obj_set = new HashSet<TestObject> ();
+      obj_set.add (obj1);
+      obj_set.add (obj2);
+
+      // Store the objects
+      cache.store_objects.begin (obj_set, null, (o, r) =>
+        {
+          cache.store_objects.end (r);
+          main_loop.quit ();
+        });
+
+      main_loop.run ();
+
+      // Load the objects
+      Set<TestObject>? new_obj_set = null;
+      cache.load_objects.begin (null, (o, r) =>
+        {
+          new_obj_set = cache.load_objects.end (r);
+          main_loop.quit ();
+        });
+
+      main_loop.run ();
+
+      // Check the objects
+      assert (new_obj_set != null);
+      assert (new_obj_set.size == obj_set.size);
+
+      foreach (var new_obj in new_obj_set)
+        {
+          bool partner_found = false;
+
+          foreach (var original_obj in obj_set)
+            {
+              if (new_obj.my_string == original_obj.my_string &&
+                  new_obj.my_int == original_obj.my_int)
+                {
+                  obj_set.remove (original_obj);
+                  partner_found = true;
+                  break;
+                }
+            }
+
+          assert (partner_found);
+        }
+
+      assert (obj_set.size == 0);
+    }
+
+  public void test_load_objects_empty ()
+    {
+      var main_loop = new GLib.MainLoop (null, false);
+      var cache = new TestCache ("test-load-objects-empty");
+
+      // Store an empty set of objects
+      cache.store_objects.begin (new HashSet<TestObject> (), null, (o, r) =>
+        {
+          cache.store_objects.end (r);
+          main_loop.quit ();
+        });
+
+      main_loop.run ();
+
+      // Load the set
+      Set<TestObject>? new_obj_set = null;
+      cache.load_objects.begin (null, (o, r) =>
+        {
+          new_obj_set = cache.load_objects.end (r);
+          main_loop.quit ();
+        });
+
+      main_loop.run ();
+
+      // Check the set
+      assert (new_obj_set != null);
+      assert (new_obj_set.size == 0);
+    }
+
+  public void test_load_objects_nonexistent ()
+    {
+      var main_loop = new GLib.MainLoop (null, false);
+      var cache = new TestCache ("test-load-objects-nonexistent");
+
+      // Remove the cache directory
+      this._delete_cache_directory ();
+
+      // Load the cache file
+      Set<TestObject>? new_obj_set = null;
+      cache.load_objects.begin (null, (o, r) =>
+        {
+          new_obj_set = cache.load_objects.end (r);
+          main_loop.quit ();
+        });
+
+      main_loop.run ();
+
+      // Check the set is nonexistent
+      assert (new_obj_set == null);
+    }
+
+  public void test_clear ()
+    {
+      var main_loop = new GLib.MainLoop (null, false);
+      var cache = new TestCache ("test-clear");
+
+      // Create some objects
+      var obj1 = new TestObject ("Foo", 1);
+      var obj2 = new TestObject ("Bar", 2);
+
+      var obj_set = new HashSet<TestObject> ();
+      obj_set.add (obj1);
+      obj_set.add (obj2);
+
+      // Store the objects
+      cache.store_objects.begin (obj_set, null, (o, r) =>
+        {
+          cache.store_objects.end (r);
+          main_loop.quit ();
+        });
+
+      main_loop.run ();
+
+      // Clear the cache
+      cache.clear_cache.begin ((o, r) =>
+        {
+          cache.clear_cache.end (r);
+          main_loop.quit ();
+        });
+
+      main_loop.run ();
+
+      // Attempt to load the cache file. This should fail.
+      Set<TestObject>? new_obj_set = null;
+      cache.load_objects.begin (null, (o, r) =>
+        {
+          new_obj_set = cache.load_objects.end (r);
+          main_loop.quit ();
+        });
+
+      main_loop.run ();
+
+      assert (new_obj_set == null);
+    }
+
+  public void test_clear_empty ()
+    {
+      var main_loop = new GLib.MainLoop (null, false);
+      var cache = new TestCache ("test-clear-empty");
+
+      // Store an empty set
+      cache.store_objects.begin (new HashSet<TestObject> (), null, (o, r) =>
+        {
+          cache.store_objects.end (r);
+          main_loop.quit ();
+        });
+
+      main_loop.run ();
+
+      // Clear the cache
+      cache.clear_cache.begin ((o, r) =>
+        {
+          cache.clear_cache.end (r);
+          main_loop.quit ();
+        });
+
+      main_loop.run ();
+
+      // Attempt to load the cache file. This should fail.
+      Set<TestObject>? new_obj_set = null;
+      cache.load_objects.begin (null, (o, r) =>
+        {
+          new_obj_set = cache.load_objects.end (r);
+          main_loop.quit ();
+        });
+
+      main_loop.run ();
+
+      assert (new_obj_set == null);
+    }
+
+  public void test_clear_nonexistent ()
+    {
+      var main_loop = new GLib.MainLoop (null, false);
+      var cache = new TestCache ("test-clear-nonexistent");
+
+      // Remove the cache directory
+      this._delete_cache_directory ();
+
+      // Clear the cache
+      cache.clear_cache.begin ((o, r) =>
+        {
+          cache.clear_cache.end (r);
+          main_loop.quit ();
+        });
+
+      main_loop.run ();
+
+      // Attempt to load the cache file. This should fail.
+      Set<TestObject>? new_obj_set = null;
+      cache.load_objects.begin (null, (o, r) =>
+        {
+          new_obj_set = cache.load_objects.end (r);
+          main_loop.quit ();
+        });
+
+      main_loop.run ();
+
+      assert (new_obj_set == null);
+    }
+
+  public void test_store_objects_cancellation ()
+    {
+      var main_loop = new GLib.MainLoop (null, false);
+      var cache = new TestCache ("test-store-objects-cancellation");
+
+      var obj_set = new HashSet<TestObject> ();
+      obj_set.add (new TestObject ("Foo", 1));
+      obj_set.add (new TestObject ("Bar", 2));
+      obj_set.add (new TestObject ("De", 3));
+      obj_set.add (new TestObject ("Baz", 4));
+
+      var cancellable = new Cancellable ();
+
+      cache.store_objects.begin (obj_set, cancellable, (o, r) =>
+        {
+          cache.store_objects.end (r);
+          main_loop.quit ();
+        });
+
+      // Cancel the operation before running the main loop
+      cancellable.cancel ();
+      main_loop.run ();
+
+      // Check that loading the objects fails (i.e. storing them failed)
+      Set<TestObject>? new_obj_set = null;
+      cache.load_objects.begin (null, (o, r) =>
+        {
+          new_obj_set = cache.load_objects.end (r);
+          main_loop.quit ();
+        });
+
+      main_loop.run ();
+
+      assert (new_obj_set == null);
+    }
+
+  public void test_load_objects_cancellation ()
+    {
+      var main_loop = new GLib.MainLoop (null, false);
+      var cache = new TestCache ("test-load-objects-cancellation");
+
+      // Create some objects
+      var obj1 = new TestObject ("Foo", 1);
+      var obj2 = new TestObject ("Bar", 2);
+
+      var obj_set = new HashSet<TestObject> ();
+      obj_set.add (obj1);
+      obj_set.add (obj2);
+
+      // Store the objects
+      cache.store_objects.begin (obj_set, null, (o, r) =>
+        {
+          cache.store_objects.end (r);
+          main_loop.quit ();
+        });
+
+      main_loop.run ();
+
+      // Load the objects and check that nothing is returned
+      var cancellable = new Cancellable ();
+
+      Set<TestObject>? new_obj_set = null;
+      cache.load_objects.begin (cancellable, (o, r) =>
+        {
+          new_obj_set = cache.load_objects.end (r);
+          main_loop.quit ();
+        });
+
+      // Cancel the operation before running the main loop
+      cancellable.cancel ();
+      main_loop.run ();
+
+      assert (new_obj_set == null);
+    }
+
+  public void test_stress ()
+    {
+      var main_loop = new GLib.MainLoop (null, false);
+      var cache = new TestCache ("test-stress");
+
+      // Create a handful of objects
+      var obj_count = 66666;
+      var obj_set = new HashSet<TestObject> ();
+
+      for (var i = 0; i < obj_count; i++)
+        {
+          obj_set.add (new TestObject ("bizzle", i));
+        }
+
+      // Store the objects
+      Test.timer_start ();
+
+      cache.store_objects.begin (obj_set, null, (o, r) =>
+        {
+          var elapsed_time = Test.timer_elapsed ();
+          message ("Storing %u objects in a cache file took %f seconds.",
+              obj_count, elapsed_time);
+
+          cache.store_objects.end (r);
+          main_loop.quit ();
+        });
+
+      main_loop.run ();
+
+      // Load the objects
+      Test.timer_start ();
+
+      Set<TestObject>? new_obj_set = null;
+      cache.load_objects.begin (null, (o, r) =>
+        {
+          var elapsed_time = Test.timer_elapsed ();
+          message ("Loading %u objects from a cache file took %f seconds.",
+              obj_count, elapsed_time);
+
+          new_obj_set = cache.load_objects.end (r);
+          main_loop.quit ();
+        });
+
+      main_loop.run ();
+
+      /* Check the set is the right size. We don't bother to check that the
+       * objects themselves are OK â the loading tests do that. */
+      assert (new_obj_set != null);
+      assert (new_obj_set.size == obj_set.size);
+    }
+}
+
+public int main (string[] args)
+{
+  Test.init (ref args);
+
+  TestSuite root = TestSuite.get_root ();
+  root.add_suite (new ObjectCacheTests ().get_suite ());
+
+  Test.run ();
+
+  return 0;
+}
+
+/* vim: filetype=vala textwidth=80 tabstop=2 expandtab: */



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