Wrote test suites for the kofoto.dag, kofoto.search and kofoto.shelf
authorJoel Rosdahl <joel@rosdahl.net>
Sat, 31 Jan 2004 17:57:18 +0000 (17:57 +0000)
committerJoel Rosdahl <joel@rosdahl.net>
Sat, 31 Jan 2004 17:57:18 +0000 (17:57 +0000)
modules.

Makefile
doc/todo.txt
src/test/alltests.py [new file with mode: 0755]
src/test/dagtests.py [new file with mode: 0755]
src/test/searchtests.py [new file with mode: 0755]
src/test/shelftests.py [new file with mode: 0755]

index 17d24b0..3f2b780 100644 (file)
--- a/Makefile
+++ b/Makefile
@@ -12,3 +12,6 @@ clean:
 
 install:
        python setup.py install --prefix=$(PREFIX)
+
+check:
+       python src/test/alltests.py
index bda0256..36c6b1d 100644 (file)
@@ -48,7 +48,7 @@
   imagecache (copies on Windows). A third mode that makes image copies
   regardless of platform could also be useful.
 
-* Write a test suite.
+* Write test suites for modules that miss one.
 
 * When generating an album, store images in per-month directories
   instead of the current two-level hash-based directory structure.
diff --git a/src/test/alltests.py b/src/test/alltests.py
new file mode 100755 (executable)
index 0000000..07df1c2
--- /dev/null
@@ -0,0 +1,20 @@
+import os
+import sys
+import unittest
+
+testModules = ["dagtests", "shelftests", "searchtests"]
+
+cwd = os.getcwd()
+libdir = unicode(os.path.realpath(
+    os.path.join(os.path.dirname(sys.argv[0]), "..", "lib")))
+os.chdir(libdir)
+sys.path.insert(0, libdir)
+
+def suite():
+    alltests = unittest.TestSuite()
+    for module in [__import__(x) for x in testModules]:
+        alltests.addTest(unittest.findTestCases(module))
+    return alltests
+
+if __name__ == "__main__":
+    unittest.main(defaultTest="suite")
diff --git a/src/test/dagtests.py b/src/test/dagtests.py
new file mode 100755 (executable)
index 0000000..617987c
--- /dev/null
@@ -0,0 +1,138 @@
+#! /usr/bin/env python
+
+import gc
+import os
+import sys
+import unittest
+
+if __name__ == "__main__":
+    cwd = os.getcwd()
+    libdir = unicode(os.path.realpath(
+        os.path.join(os.path.dirname(sys.argv[0]), "..", "lib")))
+    os.chdir(libdir)
+    sys.path.insert(0, libdir)
+from kofoto.dag import *
+
+PICDIR = unicode(os.path.realpath(
+    os.path.join("..", "reference_pictures", "working")))
+
+def sorted(x):
+    y = x[:]
+    y.sort()
+    return y
+
+class TestDAG(unittest.TestCase):
+    def setUp(self):
+        self.dag = DAG()
+        for x in [1, 2, 3, 4, 5, 6]:
+            self.dag.add(x)
+        for x, y in [(1, 3), (1, 4), (2, 3), (3, 5), (4, 5), (5, 6)]:
+            self.dag.connect(x, y)
+
+    def tearDown(self):
+        del self.dag
+
+    def test_iter(self):
+        assert sorted(list(self.dag)) == [1, 2, 3, 4, 5, 6]
+
+    def test_contains(self):
+        assert 3 in self.dag
+
+    def test_negative_contains(self):
+        assert not 4711 in self.dag
+
+    def test_redundant_add(self):
+        self.dag.add(1)
+        assert sorted(list(self.dag)) == [1, 2, 3, 4, 5, 6]
+
+    def test_redundant_connect(self):
+        assert self.dag.reachable(1, 3)
+        self.dag.connect(1, 3)
+        assert self.dag.reachable(1, 3)
+
+    def test_connect_loop(self):
+        try:
+            self.dag.connect(6, 1)
+        except LoopError:
+            pass
+        else:
+            assert False
+
+    def test_connected(self):
+        assert self.dag.connected(1, 3)
+        assert self.dag.connected(1, 4)
+        assert not self.dag.connected(1, 2)
+        assert not self.dag.connected(1, 5)
+
+    def test_disconnect(self):
+        assert self.dag.reachable(1, 3)
+        self.dag.disconnect(1, 3)
+        assert not self.dag.reachable(1, 3)
+
+    def test_idempotent_disconnect(self):
+        self.dag.disconnect(3, 1)
+        assert self.dag.reachable(1, 3)
+        assert not self.dag.reachable(3, 1)
+
+    def test_getAncestors(self):
+        for x, y in [(1, [1]),
+                     (2, [2]),
+                     (3, [1, 2, 3]),
+                     (4, [1, 4]),
+                     (5, [1, 2, 3, 4, 5]),
+                     (6, [1, 2, 3, 4, 5, 6])]:
+            assert sorted(list(self.dag.getAncestors(x))) == sorted(y)
+
+    def test_getChildren(self):
+        for x, y in [(1, [3, 4]),
+                     (2, [3]),
+                     (3, [5]),
+                     (4, [5]),
+                     (5, [6]),
+                     (6, [])]:
+            assert sorted(list(self.dag.getChildren(x))) == sorted(y)
+
+    def test_getDescendants(self):
+        for x, y in [(1, [1, 3, 4, 5, 6]),
+                     (2, [2, 3, 5, 6]),
+                     (3, [3, 5, 6]),
+                     (4, [4, 5, 6]),
+                     (5, [5, 6]),
+                     (6, [6])]:
+            assert sorted(list(self.dag.getDescendants(x))) == sorted(y)
+
+    def test_getParents(self):
+        for x, y in [(1, []),
+                     (2, []),
+                     (3, [1, 2]),
+                     (4, [1]),
+                     (5, [3, 4]),
+                     (6, [5])]:
+            assert sorted(list(self.dag.getParents(x))) == sorted(y)
+
+    def test_getRoots(self):
+        assert sorted(list(self.dag.getRoots())) == [1, 2]
+
+    def test_reachable(self):
+        assert self.dag.reachable(1, 3)
+        assert self.dag.reachable(1, 6)
+        assert not self.dag.reachable(1, 2)
+        assert not self.dag.reachable(1, 4711)
+
+    def test_remove(self):
+        assert self.dag.reachable(1, 6)
+        self.dag.remove(5)
+        assert not self.dag.reachable(1, 6)
+
+    def test_negative_remove(self):
+        try:
+            self.dag.remove(4711)
+        except:
+            pass
+        else:
+            assert False
+
+######################################################################
+
+if __name__ == "__main__":
+    unittest.main()
diff --git a/src/test/searchtests.py b/src/test/searchtests.py
new file mode 100755 (executable)
index 0000000..1280563
--- /dev/null
@@ -0,0 +1,84 @@
+#! /usr/bin/env python
+
+import gc
+import os
+import sys
+import unittest
+
+if __name__ == "__main__":
+    cwd = os.getcwd()
+    libdir = unicode(os.path.realpath(
+        os.path.join(os.path.dirname(sys.argv[0]), "..", "lib")))
+    os.chdir(libdir)
+    sys.path.insert(0, libdir)
+from kofoto.shelf import *
+from kofoto.search import *
+
+PICDIR = unicode(os.path.realpath(
+    os.path.join("..", "reference_pictures", "working")))
+
+######################################################################
+
+db = "shelf.tmp"
+codeset = "latin1"
+
+def removeTmpDb():
+    for x in [db, db + "-journal"]:
+        if os.path.exists(x):
+            os.unlink(x)
+
+from shelftests import TestShelfFixture
+
+class TestSearch(TestShelfFixture):
+    def setUp(self):
+        TestShelfFixture.setUp(self)
+        images = list(self.shelf.getAllImages())
+        self.image1, self.image2, self.image3 = images[0:3]
+        cat_a = self.shelf.getCategory(u"a")
+        cat_b = self.shelf.getCategory(u"b")
+        cat_c = self.shelf.getCategory(u"c")
+        cat_d = self.shelf.getCategory(u"d")
+        self.image1.addCategory(cat_a)
+        self.image1.addCategory(cat_b)
+        self.image2.addCategory(cat_c)
+        self.image1.setAttribute(u"foo", u"abc")
+        self.image1.setAttribute(u"bar", u"17")
+        self.image2.setAttribute(u"foo", u"xyz")
+        self.image3.setAttribute(u"fie", u"fum")
+
+    def tearDown(self):
+        TestShelfFixture.tearDown(self)
+
+    def test_search(self):
+        tests = [
+            (u"a", [self.image2, self.image1]),
+            (u"b", [self.image1]),
+            (u"c", [self.image2]),
+            (u"d", []),
+            (u"(((b)))", [self.image1]),
+            (u'@foo >= "b\'ar"', [self.image2]),
+            (u"a and b", [self.image1]),
+            (u'a and @foo = "abc"', [self.image1]),
+            (u'a and @foo = xyz', [self.image2]),
+            (u'@foo = "abc" and @bar = 17', [self.image1]),
+            (u'a and b and @bar = "17" and @foo = abc', [self.image1]),
+            (u"not a and c", []),
+            (u"not exactly a and c", [self.image2]),
+            (u"not (a and b) and a", [self.image2]),
+            (u"not a and not b and @fie=fum", [self.image3]),
+            (u"a and b or c", [self.image2, self.image1]),
+            (u"b or c and d", [self.image1]),
+            (u"a or b or c or d", [self.image2, self.image1]),
+            (ur' ((a and not b) or @gazonk != "hej \"ju\"") and c ', [self.image2])]
+        parser = Parser(self.shelf)
+        for expression, expectedResult in tests:
+            parseTree = parser.parse(expression)
+            result = list(self.shelf.search(parseTree))
+            result.sort(lambda x, y: cmp(x.getLocation(), y.getLocation()))
+            assert result == expectedResult, (expression, expectedResult, result)
+
+######################################################################
+
+if __name__ == "__main__":
+    removeTmpDb()
+    unittest.main()
diff --git a/src/test/shelftests.py b/src/test/shelftests.py
new file mode 100755 (executable)
index 0000000..ed06372
--- /dev/null
@@ -0,0 +1,712 @@
+#! /usr/bin/env python
+
+import gc
+import os
+import sys
+import unittest
+
+if __name__ == "__main__":
+    cwd = os.getcwd()
+    libdir = unicode(os.path.realpath(
+        os.path.join(os.path.dirname(sys.argv[0]), "..", "lib")))
+    os.chdir(libdir)
+    sys.path.insert(0, libdir)
+from kofoto.shelf import *
+
+PICDIR = unicode(os.path.realpath(
+    os.path.join("..", "reference_pictures", "working")))
+
+######################################################################
+
+db = "shelf.tmp"
+codeset = "latin1"
+
+def removeTmpDb():
+    for x in [db, db + "-journal"]:
+        if os.path.exists(x):
+            os.unlink(x)
+
+class TestPublicShelfFunctions(unittest.TestCase):
+    def test_computeImageHash(self):
+        s = computeImageHash(os.path.join(PICDIR, "arlaharen.png"))
+        assert s == "39a1266d2689f53d48b09a5e0ca0af1f"
+        try:
+            computeImageHash("nonexisting")
+        except IOError:
+            pass
+        else:
+            assert False, s
+
+    def test_verifyValidAlbumTag(self):
+        # Valid tags.
+        for x in ("foo", "1foo", "and", "exactly", "not", "or"):
+            try:
+                verifyValidAlbumTag(x)
+            except:
+                assert False, x
+        # Invalid tags.
+        for x in (None, 1, 1L, "1" "foo " "@foo"):
+            try:
+                verifyValidAlbumTag(x)
+            except BadAlbumTagError:
+                pass
+            else:
+                assert False, x
+
+    def test_verifyValidCategoryTag(self):
+        # Valid tags.
+        for x in ("foo", "1foo"):
+            try:
+                verifyValidCategoryTag(x)
+            except:
+                assert False, x
+        # Invalid tags.
+        for x in (None, 1, 1L, "1" "foo " "@foo",
+                  "and", "exactly", "not", "or"):
+            try:
+                verifyValidCategoryTag(x)
+            except BadCategoryTagError:
+                pass
+            else:
+                assert False, x
+
+    def test_makeValidTag(self):
+        for tag, validTag in [("", "_"),
+                              ("@", "_"),
+                              (" ", "_"),
+                              ("1", "1_"),
+                              ("@foo_", "foo_"),
+                              ("fo@o __", "fo@o__")]:
+            assert makeValidTag(tag) == validTag, (tag, validTag)
+
+class TestNegativeShelfOpens(unittest.TestCase):
+    def tearDown(self):
+        removeTmpDb()
+
+    def test_NonexistingShelf(self):
+        for params, kwparams in [((db, codeset), {}),
+                                 ((db, codeset, False), {}),
+                                 ((db, codeset), {"create": False})]:
+            try:
+                Shelf(*params, **kwparams)
+            except ShelfNotFoundError:
+                pass
+            else:
+                assert False, (params, kwparams)
+            assert not os.path.exists(db)
+
+    def test_BadShelf(self):
+        file(db, "w") # Create empty file.
+        try:
+            Shelf(db, codeset)
+        except UnsupportedShelfError:
+            pass
+        else:
+            assert False
+
+class TestShelfCreation(unittest.TestCase):
+    def tearDown(self):
+        removeTmpDb()
+
+    def test_CreateShelf1(self):
+        assert Shelf(db, codeset, True)
+        assert os.path.exists(db)
+
+    def test_CreateShelf2(self):
+        assert Shelf(db, codeset, create=True)
+        assert os.path.exists(db)
+
+class TestShelfOpen(unittest.TestCase):
+    def tearDown(self):
+        removeTmpDb()
+
+    def test_CreateShelf(self):
+        assert Shelf(db, codeset, True)
+        assert os.path.exists(db)
+
+    def test_CreateShelf2(self):
+        assert Shelf(db, codeset, create=True)
+        assert os.path.exists(db)
+
+class TestShelfMemoryLeakage(unittest.TestCase):
+    def tearDown(self):
+        removeTmpDb()
+
+    def test_MemoryLeak1(self):
+        Shelf(db, codeset, True)
+        assert gc.collect() == 0
+
+    def test_MemoryLeak2(self):
+        s = Shelf(db, codeset, True)
+        s.begin()
+        s.getObject(0)
+        s.rollback()
+        assert gc.collect() == 0
+
+    def test_MemoryLeak3(self):
+        s = Shelf(db, codeset, True)
+        s.begin()
+        s.getObject(0)
+        s.commit()
+        assert gc.collect() == 0
+
+class TestShelfTransactions(unittest.TestCase):
+    def tearDown(self):
+        removeTmpDb()
+
+    def test_commit(self):
+        s = Shelf(db, codeset, True)
+        s.begin()
+        s.createAlbum(u"foo")
+        assert s.getAlbum(u"foo")
+        s.commit()
+        s = Shelf(db, codeset)
+        s.begin()
+        assert s.getAlbum(u"foo")
+        s.rollback()
+
+    def test_rollback(self):
+        s = Shelf(db, codeset, True)
+        s.begin()
+        s.createAlbum(u"foo")
+        s.rollback()
+        s.begin()
+        try:
+            s.getAlbum(u"foo")
+        except AlbumDoesNotExistError:
+            pass
+        else:
+            assert False
+
+    def test_isModified(self):
+        s = Shelf(db, codeset, True)
+        s.begin()
+        assert not s.isModified()
+        s.createAlbum(u"foo")
+        assert s.isModified()
+        s.rollback()
+        s.begin()
+        assert not s.isModified()
+        s.rollback()
+
+    def test_registerModificationCallback(self):
+        res = [False]
+        def f(x):
+            res[0] = True
+        s = Shelf(db, codeset, True)
+        s.begin()
+        s.registerModificationCallback(f)
+        assert not res[0]
+        s.createAlbum(u"foo")
+        assert res[0]
+        s.rollback()
+        res[0] = False
+        s.begin()
+        assert not res[0]
+        s.unregisterModificationCallback(f)
+        s.createAlbum(u"foo")
+        assert not res[0]
+        s.rollback()
+
+class TestShelfFixture(unittest.TestCase):
+    def setUp(self):
+        self.shelf = Shelf(db, codeset, True)
+        self.shelf.begin()
+        root = self.shelf.getRootAlbum()
+        alpha = self.shelf.createAlbum(u"alpha")
+        beta = self.shelf.createAlbum(u"beta")
+        children = [alpha, beta]
+        for x in os.listdir(PICDIR):
+            loc = os.path.join(PICDIR, x)
+            if not os.path.isfile(loc):
+                continue
+            children.append(self.shelf.createImage(loc))
+        del children[-1] # The last image becomes orphaned.
+        alpha.setChildren(children) # This creates a cycle.
+        beta.setChildren(list(beta.getChildren()) + [children[-1]])
+        root.setChildren(list(root.getChildren()) + [
+            alpha,
+            beta,
+            self.shelf.createAlbum(u"gamma", u"allalbums"),
+            self.shelf.createAlbum(u"delta", u"allimages")])
+        self.shelf.createAlbum(u"epsilon", u"plain") # Orphan album.
+
+        cat_a = self.shelf.createCategory(u"a", u"A")
+        cat_b = self.shelf.createCategory(u"b", u"B")
+        cat_c = self.shelf.createCategory(u"c", u"C")
+        cat_d = self.shelf.createCategory(u"d", u"D")
+        cat_a.connectChild(cat_b)
+        cat_a.connectChild(cat_c)
+        cat_b.connectChild(cat_d)
+        cat_c.connectChild(cat_d)
+
+    def tearDown(self):
+        # Break cycle mentioned in setUp above.
+        self.shelf.getAlbum(u"alpha").setChildren([])
+        self.shelf.rollback()
+        removeTmpDb()
+
+class TestShelfMethods(TestShelfFixture):
+    def test_flushCaches(self):
+        self.shelf.flushCategoryCache()
+        self.shelf.flushObjectCache()
+
+    def test_getStatistics(self):
+        s = self.shelf.getStatistics()
+        assert s["nalbums"] == 7
+        assert s["nimages"] == 11
+
+    def test_createdObjects(self):
+        root = self.shelf.getRootAlbum()
+        children = list(root.getChildren())
+        assert len(children) == 5
+        orphans, alpha, beta, gamma, delta = children
+        assert self.shelf.getObject(u"alpha") == alpha
+        assert self.shelf.getAlbum(u"beta") == beta
+        assert len(list(alpha.getChildren())) == 12
+        assert len(list(beta.getChildren())) == 1
+        assert len(list(gamma.getChildren())) == 7
+        assert len(list(delta.getChildren())) == 11
+
+    def test_createdAttributes(self):
+        for image in self.shelf.getAllImages():
+            assert image.getAttribute(u"registered")
+        image = self.shelf.getImage(
+            os.path.join(PICDIR, "Canon_Digital_IXUS.jpg"))
+        assert image.getAttribute(u"captured") == "2002-02-02 22:20:51"
+        assert image.getAttribute(u"cameramake") == "Canon"
+        assert image.getAttribute(u"cameramodel") == "Canon DIGITAL IXUS"
+
+    def test_negativeAlbumCreation(self):
+        try:
+            self.shelf.createAlbum(u"beta")
+        except AlbumExistsError:
+            pass
+        else:
+            assert False
+
+    def test_getAlbum(self):
+        album = self.shelf.getAlbum(u"alpha")
+        album = self.shelf.getAlbum(album.getTag())
+        album = self.shelf.getAlbum(album.getId())
+
+    def test_negativeGetAlbum(self):
+        try:
+            self.shelf.getAlbum(u"nonexisting")
+        except AlbumDoesNotExistError:
+            pass
+        else:
+            assert False
+
+    def test_getRootAlbum(self):
+        root = self.shelf.getRootAlbum()
+        assert root == self.shelf.getAlbum(u"root")
+
+    def test_getAllAlbums(self):
+        albums = list(self.shelf.getAllAlbums())
+        assert len(albums) == 7
+
+    def test_getAllImages(self):
+        images = list(self.shelf.getAllImages())
+        assert len(images) == 11
+
+    def test_getImagesInDirectory(self):
+        images = list(self.shelf.getImagesInDirectory(u"."))
+        assert len(images) == 0
+        images = list(self.shelf.getImagesInDirectory(PICDIR))
+        assert len(images) == 11
+
+    def test_deleteAlbum(self):
+        self.shelf.deleteAlbum(u"beta")
+
+    def test_negativeRootAlbumDeletion(self):
+        try:
+            self.shelf.deleteAlbum(u"root")
+        except UndeletableAlbumError:
+            pass
+        else:
+            assert False
+
+    def test_negativeAlbumDeletion(self):
+        try:
+            self.shelf.deleteAlbum(u"nonexisting")
+        except AlbumDoesNotExistError:
+            pass
+        else:
+            assert False
+
+    def test_negativeImageCreation(self):
+        try:
+            self.shelf.createImage(os.path.join(PICDIR, "arlaharen.png"))
+        except ImageExistsError:
+            pass
+        else:
+            assert False
+
+    def test_getImage(self):
+        image = self.shelf.getImage(os.path.join(PICDIR, "arlaharen.png"))
+        image = self.shelf.getImage(image.getHash())
+        image = self.shelf.getImage(image.getId())
+
+    def test_negativeGetImage(self):
+        try:
+            self.shelf.getImage(u"nonexisting")
+        except ImageDoesNotExistError:
+            pass
+        else:
+            assert False
+
+    def test_deleteImage(self):
+        self.shelf.deleteImage(os.path.join(PICDIR, "arlaharen.png"))
+
+    def test_negativeImageDeletion(self):
+        try:
+            self.shelf.deleteImage(u"nonexisting")
+        except ImageDoesNotExistError:
+            pass
+        else:
+            assert False
+
+    def test_getObject(self):
+        album = self.shelf.getObject(u"alpha")
+        album = self.shelf.getObject(album.getTag())
+        album = self.shelf.getObject(album.getId())
+        image = self.shelf.getObject(os.path.join(PICDIR, "arlaharen.png"))
+        image = self.shelf.getObject(image.getHash())
+        image = self.shelf.getObject(image.getId())
+
+    def test_deleteObject(self):
+        self.shelf.deleteObject(u"beta")
+        self.shelf.deleteObject(os.path.join(PICDIR, "arlaharen.png"))
+
+    def test_getAllAttributeNames(self):
+        attrnames = list(self.shelf.getAllAttributeNames())
+        attrnames.sort()
+        assert attrnames == [
+            "cameramake", "cameramodel", "captured", "description", "height",
+            "orientation", "registered", "title", "width"
+            ]
+
+    def test_negativeCreateCategory(self):
+        try:
+            self.shelf.createCategory(u"a", u"Foo")
+        except CategoryExistsError:
+            pass
+        else:
+            assert False
+
+    def test_deleteCategory(self):
+        self.shelf.deleteCategory(u"a")
+
+    def test_negativeDeleteCategory(self):
+        try:
+            self.shelf.deleteCategory(u"nonexisting")
+        except CategoryDoesNotExistError:
+            pass
+        else:
+            assert False
+
+    def test_getRootCategories(self):
+        categories = list(self.shelf.getRootCategories())
+        cat_a = self.shelf.getCategory(u"a")
+        assert categories == [cat_a]
+
+class TestCategory(TestShelfFixture):
+    def test_categoryMethods(self):
+        cat_a = self.shelf.getCategory(u"a")
+        cat_b = self.shelf.getCategory(u"b")
+        cat_c = self.shelf.getCategory(u"c")
+        cat_d = self.shelf.getCategory(u"d")
+
+        assert self.shelf.getCategory(cat_a.getTag()) == cat_a
+        assert self.shelf.getCategory(cat_a.getId()) == cat_a
+        cat_a.setTag(u"foo")
+        assert self.shelf.getCategory(u"foo") == cat_a
+
+        assert cat_a.getDescription() == "A"
+        cat_a.setDescription(u"foo")
+        assert cat_a.getDescription() == "foo"
+
+        a_children = list(cat_a.getChildren())
+        a_children.sort(lambda x, y: cmp(x.getId(), y.getId()))
+        assert a_children == [cat_b, cat_c]
+        b_children = list(cat_b.getChildren())
+        assert b_children == [cat_d]
+        d_children = list(cat_d.getChildren())
+        assert d_children == []
+
+        a_parents = list(cat_a.getParents())
+        assert a_parents == []
+        b_parents = list(cat_b.getParents())
+        assert b_parents == [cat_a]
+        d_parents = list(cat_d.getParents())
+        d_parents.sort(lambda x, y: cmp(x.getTag(), y.getTag()))
+        assert d_parents == [cat_b, cat_c]
+
+        assert not cat_a.isChildOf(cat_a)
+        assert cat_b.isChildOf(cat_a)
+        assert cat_c.isChildOf(cat_a)
+        assert not cat_d.isChildOf(cat_a)
+        assert cat_a.isChildOf(cat_a, recursive=True)
+        assert cat_b.isChildOf(cat_a, recursive=True)
+        assert cat_c.isChildOf(cat_a, recursive=True)
+        assert cat_d.isChildOf(cat_a, recursive=True)
+
+        assert not cat_d.isParentOf(cat_d)
+        assert cat_b.isParentOf(cat_d)
+        assert cat_c.isParentOf(cat_d)
+        assert not cat_a.isParentOf(cat_d)
+        assert cat_d.isParentOf(cat_d, recursive=True)
+        assert cat_b.isParentOf(cat_d, recursive=True)
+        assert cat_c.isParentOf(cat_d, recursive=True)
+        assert cat_a.isParentOf(cat_d, recursive=True)
+
+    def test_negativeCategoryConnectChild(self):
+        cat_a = self.shelf.getCategory(u"a")
+        cat_b = self.shelf.getCategory(u"b")
+        try:
+            cat_a.connectChild(cat_b)
+        except CategoriesAlreadyConnectedError:
+            pass
+        else:
+            assert False
+        try:
+            cat_b.connectChild(cat_a)
+        except CategoryLoopError:
+            pass
+        else:
+            assert False
+
+    def test_categoryDisconnectChild(self):
+        cat_a = self.shelf.getCategory(u"a")
+        cat_b = self.shelf.getCategory(u"b")
+        cat_a.disconnectChild(cat_b)
+        assert not cat_a.isParentOf(cat_b)
+
+    def test_negativeCategoryDisconnectChild(self):
+        cat_a = self.shelf.getCategory(u"a")
+        cat_d = self.shelf.getCategory(u"d")
+        cat_a.disconnectChild(cat_d) # No exception.
+
+class TestObject(TestShelfFixture):
+    def test_getParents(self):
+        root = self.shelf.getRootAlbum()
+        alpha = self.shelf.getAlbum(u"alpha")
+        beta = self.shelf.getAlbum(u"beta")
+        parents = list(beta.getParents())
+        parents.sort(lambda x, y: cmp(x.getTag(), y.getTag()))
+        assert parents == [alpha, root]
+
+    def test_getAttribute(self):
+        orphans = self.shelf.getAlbum(u"orphans")
+        assert orphans.getAttribute(u"title")
+        assert orphans.getAttribute(u"description")
+        assert not orphans.getAttribute(u"nonexisting")
+
+    def test_getAttributeMap(self):
+        orphans = self.shelf.getAlbum(u"orphans")
+        map = orphans.getAttributeMap()
+        assert "description" in map
+        assert "title" in map
+
+    def test_getAttributeNames(self):
+        orphans = self.shelf.getAlbum(u"orphans")
+        names = list(orphans.getAttributeNames())
+        names.sort()
+        assert names == ["description", "title"]
+
+    def test_setAttribute(self):
+        orphans = self.shelf.getAlbum(u"orphans")
+        orphans.setAttribute(u"foo", u"fie") # New.
+        assert orphans.getAttribute(u"foo") == u"fie"
+        assert u"foo" in orphans.getAttributeMap()
+        assert u"foo" in orphans.getAttributeNames()
+        assert orphans.getAttribute(u"title")
+        orphans.setAttribute(u"title", u"gazonk") # Existing
+        assert orphans.getAttribute(u"title") == u"gazonk"
+        assert u"foo" in orphans.getAttributeMap()
+        assert u"foo" in orphans.getAttributeNames()
+
+    def test_deleteAttribute(self):
+        orphans = self.shelf.getAlbum(u"orphans")
+        orphans.deleteAttribute(u"nonexisting") # No exception.
+        assert orphans.getAttribute(u"title")
+        orphans.deleteAttribute(u"title")
+        assert not orphans.getAttribute(u"title")
+
+    def test_addCategory(self):
+        orphans = self.shelf.getAlbum(u"orphans")
+        cat_a = self.shelf.getCategory(u"a")
+        assert list(orphans.getCategories()) == []
+        orphans.addCategory(cat_a)
+        assert list(orphans.getCategories()) == [cat_a]
+        try:
+            orphans.addCategory(cat_a)
+        except CategoryPresentError:
+            pass
+        else:
+            assert False
+        assert list(orphans.getCategories()) == [cat_a]
+
+    def test_removeCategory(self):
+        orphans = self.shelf.getAlbum(u"orphans")
+        assert list(orphans.getCategories()) == []
+        cat_a = self.shelf.getCategory(u"a")
+        orphans.addCategory(cat_a)
+        assert list(orphans.getCategories()) == [cat_a]
+        orphans.removeCategory(cat_a)
+        assert list(orphans.getCategories()) == []
+
+class TestAlbum(TestShelfFixture):
+    def test_getType(self):
+        alpha = self.shelf.getAlbum(u"alpha")
+        assert alpha.getType()
+
+    def test_getTag(self):
+        alpha = self.shelf.getAlbum(u"alpha")
+        assert alpha.getTag() == u"alpha"
+
+    def test_setTag(self):
+        alpha = self.shelf.getAlbum(u"alpha")
+        alpha.setTag(u"alfa")
+        assert alpha.getTag() == u"alfa"
+
+    def test_getAlbumParents(self):
+        root = self.shelf.getRootAlbum()
+        alpha = self.shelf.getAlbum(u"alpha")
+        parents = list(alpha.getAlbumParents())
+        parents.sort(lambda x, y: cmp(x.getTag(), y.getTag()))
+        assert parents == [alpha, root]
+
+    def test_isAlbum(self):
+        assert self.shelf.getRootAlbum().isAlbum()
+
+class TestPlainAlbum(TestShelfFixture):
+    def test_getChildren(self):
+        epsilon = self.shelf.getAlbum(u"epsilon")
+        alpha = self.shelf.getAlbum(u"alpha")
+        beta = self.shelf.getAlbum(u"beta")
+        assert list(epsilon.getChildren()) == []
+        alphaChildren = list(alpha.getChildren())
+        assert list(beta.getChildren()) == [alphaChildren[-1]]
+
+    def test_getAlbumChildren(self):
+        alpha = self.shelf.getAlbum(u"alpha")
+        beta = self.shelf.getAlbum(u"beta")
+        epsilon = self.shelf.getAlbum(u"epsilon")
+        alphaAlbumChildren = list(alpha.getAlbumChildren())
+        assert alphaAlbumChildren == [alpha, beta]
+        assert list(epsilon.getAlbumChildren()) == []
+
+    def test_setChildren(self):
+        root = self.shelf.getRootAlbum()
+        beta = self.shelf.getAlbum(u"beta")
+        assert list(beta.getChildren()) != []
+        beta.setChildren([beta, root])
+        assert list(beta.getChildren()) == [beta, root]
+        beta.setChildren([]) # Break the cycle.
+        assert list(beta.getChildren()) == []
+
+class TestImage(TestShelfFixture):
+    def test_getLocation(self):
+        location = os.path.join(PICDIR, "arlaharen.png")
+        image = self.shelf.getImage(location)
+        assert image.getLocation() == os.path.realpath(location)
+
+    def test_setLocation(self):
+        location = os.path.join(PICDIR, "arlaharen.png")
+        image = self.shelf.getImage(location)
+        image.setLocation(u"/foo/../bar")
+        assert image.getLocation() == "/bar"
+
+    def test_getHash(self):
+        image = self.shelf.getImage(os.path.join(PICDIR, "arlaharen.png"))
+        assert image.getHash() == "39a1266d2689f53d48b09a5e0ca0af1f"
+
+    def test_setHash(self):
+        image1 = self.shelf.getImage(os.path.join(PICDIR, "arlaharen.png"))
+        image2 = self.shelf.getImage(
+            os.path.join(PICDIR, "Canon_Digital_IXUS.jpg"))
+        image2.setLocation(os.path.join(PICDIR, "arlaharen.png"))
+        h = image1.getHash()
+        self.shelf.deleteImage(h)
+        image2.setHash(h)
+        assert image2.getHash() == "39a1266d2689f53d48b09a5e0ca0af1f"
+
+    def test_isAlbum(self):
+        image = self.shelf.getImage(os.path.join(PICDIR, "arlaharen.png"))
+        assert not image.isAlbum()
+
+    def test_importExifTags(self):
+        image = self.shelf.getImage(os.path.join(PICDIR, "arlaharen.png"))
+        image.importExifTags() # TODO: Test more.
+
+class TestAllAlbumsAlbum(TestShelfFixture):
+    def test_getChildren(self):
+        gamma = self.shelf.getAlbum(u"gamma")
+        assert len(list(gamma.getChildren())) == 7
+
+    def test_getAlbumChildren(self):
+        gamma = self.shelf.getAlbum(u"gamma")
+        assert list(gamma.getAlbumChildren()) == list(gamma.getChildren())
+
+    def test_setChildren(self):
+        gamma = self.shelf.getAlbum(u"gamma")
+        try:
+            gamma.setChildren([])
+        except UnsettableChildrenError:
+            pass
+        else:
+            assert False
+
+    def test_isAlbum(self):
+        assert self.shelf.getAlbum(u"gamma").isAlbum()
+
+class TestAllImagesAlbum(TestShelfFixture):
+    def test_getChildren(self):
+        delta = self.shelf.getAlbum(u"delta")
+        assert len(list(delta.getChildren())) == 11
+
+    def test_getAlbumChildren(self):
+        delta = self.shelf.getAlbum(u"delta")
+        assert list(delta.getAlbumChildren()) == []
+
+    def test_setChildren(self):
+        delta = self.shelf.getAlbum(u"delta")
+        try:
+            delta.setChildren([])
+        except UnsettableChildrenError:
+            pass
+        else:
+            assert False
+
+    def test_isAlbum(self):
+        assert self.shelf.getAlbum(u"delta").isAlbum()
+
+class TestOrphansAlbum(TestShelfFixture):
+    def test_getChildren(self):
+        orphans = self.shelf.getAlbum(u"orphans")
+        assert len(list(orphans.getChildren())) == 2
+
+    def test_getAlbumChildren(self):
+        orphans = self.shelf.getAlbum(u"orphans")
+        epsilon = self.shelf.getAlbum(u"epsilon")
+        assert list(orphans.getAlbumChildren()) == [epsilon]
+
+    def test_setChildren(self):
+        orphans = self.shelf.getAlbum(u"orphans")
+        try:
+            orphans.setChildren([])
+        except UnsettableChildrenError:
+            pass
+        else:
+            assert False
+
+    def test_isAlbum(self):
+        assert self.shelf.getAlbum(u"orphans").isAlbum()
+
+######################################################################
+
+if __name__ == "__main__":
+    removeTmpDb()
+    unittest.main()