diff options
author | Lars Wirzenius <liw@gytha> | 2008-04-20 15:59:58 +0300 |
---|---|---|
committer | Lars Wirzenius <liw@gytha> | 2008-04-20 15:59:58 +0300 |
commit | 5f90ca5db557bcf5a6ae5387f2d527f7a4207411 (patch) | |
tree | 222c9bd9eee35086286d58958b9c0fdeae72de1c /obnam | |
parent | 323ee15aaa4144cb3a17257ff4707655000f5705 (diff) | |
parent | b2b4b68d111ee60a34e003561c765beafce3b1d1 (diff) | |
download | obnam-5f90ca5db557bcf5a6ae5387f2d527f7a4207411.tar.gz |
Merged branch to create a Store abstraction.
Diffstat (limited to 'obnam')
-rw-r--r-- | obnam/__init__.py | 1 | ||||
-rw-r--r-- | obnam/app.py | 100 | ||||
-rw-r--r-- | obnam/appTests.py | 86 | ||||
-rw-r--r-- | obnam/oper_backup.py | 4 | ||||
-rw-r--r-- | obnam/oper_forget.py | 4 | ||||
-rw-r--r-- | obnam/oper_restore.py | 4 | ||||
-rw-r--r-- | obnam/oper_show_generations.py | 2 | ||||
-rw-r--r-- | obnam/store.py | 228 | ||||
-rw-r--r-- | obnam/storeTests.py | 301 |
9 files changed, 547 insertions, 183 deletions
diff --git a/obnam/__init__.py b/obnam/__init__.py index 50a8ce4f..05d6936c 100644 --- a/obnam/__init__.py +++ b/obnam/__init__.py @@ -45,6 +45,7 @@ import walk from app import Application from oper import Operation, OperationFactory +from store import Store from oper_backup import Backup from oper_forget import Forget diff --git a/obnam/app.py b/obnam/app.py index 834da3c1..3b9a3f95 100644 --- a/obnam/app.py +++ b/obnam/app.py @@ -41,7 +41,7 @@ class Application: self._exclusion_strings = [] self._exclusion_regexps = [] self._filelist = None - self._host = None + self._store = obnam.Store(self._context) # When we traverse the file system tree while making a backup, # we process children before the parent. This is necessary for @@ -60,32 +60,14 @@ class Application: """Get the context for the backup application.""" return self._context - def get_host(self): - """Return currently active host object, or None if none is active.""" - return self._host + def get_store(self): + """Get the Store for the backup application.""" + return self._store def load_host(self): """Load the host block into memory.""" - if not self._host: - host_block = obnam.io.get_host_block(self._context) - if host_block: - self._host = obnam.obj.create_host_from_block(host_block) - else: - id = self._context.config.get("backup", "host-id") - self._host = obnam.obj.HostBlockObject(host_id=id) - return self._host - - def load_maps(self): - """Load non-content map blocks.""" - ids = self._host.get_map_block_ids() - logging.info("Decoding %d mapping blocks" % len(ids)) - obnam.io.load_maps(self._context, self._context.map, ids) - - def load_content_maps(self): - """Load content map blocks.""" - ids = self._host.get_contmap_block_ids() - logging.info("Decoding %d content mapping blocks" % len(ids)) - obnam.io.load_maps(self._context, self._context.contmap, ids) + self.get_store().fetch_host_block() + return self.get_store().get_host_block() def get_exclusion_regexps(self): """Return list of regexp to exclude things from backup.""" @@ -218,13 +200,6 @@ class Application: return None - def enqueue(self, objs): - """Push objects to the object queue.""" - for obj in objs: - obnam.io.enqueue_object(self._context, self._context.oq, - self._context.map, obj.get_id(), - obj.encode(), True) - def compute_signature(self, filename): """Compute rsync signature for a filename. @@ -236,7 +211,7 @@ class Application: sigdata = obnam.rsync.compute_signature(self._context, filename) id = obnam.obj.object_id_new() sig = obnam.obj.SignatureObject(id=id, sigdata=sigdata) - self.enqueue([sig]) + self.get_store().queue_object(sig) return sig def add_to_filegroup(self, fg, filename): @@ -270,7 +245,7 @@ class Application: list.append(obnam.obj.FileGroupObject(id=id)) self.add_to_filegroup(list[-1], filename) - self.enqueue(list) + self.get_store().queue_objects(list) return list def _make_absolute(self, basename, relatives): @@ -299,7 +274,7 @@ class Application: filegrouprefs=filegrouprefs) - self.enqueue([dir]) + self.get_store().queue_object(dir) return dir def backup_one_root(self, root): @@ -353,60 +328,5 @@ class Application: gen = obnam.obj.GenerationObject(id=obnam.obj.object_id_new(), dirrefs=dirrefs, start=start, end=end) - self.enqueue([gen]) + self.get_store().queue_object(gen) return gen - - def _update_map_helper(self, map): - """Create new mapping blocks of a given kind, and upload them. - - Return list of block ids for the new blocks. - - """ - - if obnam.map.get_new(map): - id = self._context.be.generate_block_id() - logging.debug("Creating mapping block %s" % id) - block = obnam.map.encode_new_to_block(map, id) - self._context.be.upload_block(id, block, True) - return [id] - else: - logging.debug("No new mappings, no new mapping block") - return [] - - def update_maps(self): - """Create new object mapping blocks and upload them.""" - logging.debug("Creating new mapping block for normal mappings") - return self._update_map_helper(self._context.map) - - def update_content_maps(self): - """Create new content object mapping blocks and upload them.""" - logging.debug("Creating new mapping block for content mappings") - return self._update_map_helper(self._context.contmap) - - def finish(self, new_gens): - """Finish a backup operation by updating maps and uploading host block. - - This also removes the host block that has been load. In other - words, if you want to continue using the application for anything - that requires the host block, you have to call load_host again. - - """ - - obnam.io.flush_all_object_queues(self._context) - - logging.info("Creating new mapping blocks") - host = self.get_host() - map_ids = host.get_map_block_ids() + self.update_maps() - contmap_ids = (host.get_contmap_block_ids() + - self.update_content_maps()) - - logging.info("Creating new host block") - gen_ids = (host.get_generation_ids() + - [gen.get_id() for gen in new_gens]) - host2 = obnam.obj.HostBlockObject(host_id=host.get_id(), - gen_ids=gen_ids, - map_block_ids=map_ids, - contmap_block_ids=contmap_ids) - obnam.io.upload_host_block(self._context, host2.encode()) - - self._host = None diff --git a/obnam/appTests.py b/obnam/appTests.py index 72a54b7b..7cee6843 100644 --- a/obnam/appTests.py +++ b/obnam/appTests.py @@ -34,9 +34,6 @@ class ApplicationTests(unittest.TestCase): context = obnam.context.Context() self.app = obnam.Application(context) - def testHasNoHostBlockInitially(self): - self.failUnlessEqual(self.app.get_host(), None) - def testReturnsEmptyExclusionListInitially(self): self.failUnlessEqual(self.app.get_exclusion_regexps(), []) @@ -544,86 +541,3 @@ class ApplicationBackupTests(unittest.TestCase): gen = self.app.backup([self.abs("pink"), self.abs("pretty")]) self.failIfEqual(gen.get_start_time(), None) self.failIfEqual(gen.get_end_time(), None) - - -class ApplicationMapTests(unittest.TestCase): - - def setUp(self): - # First, set up two mappings. - - context = obnam.context.Context() - context.cache = obnam.cache.Cache(context.config) - context.be = obnam.backend.init(context.config, context.cache) - - obnam.map.add(context.map, "pink", "pretty") - obnam.map.add(context.contmap, "black", "beautiful") - - map_id = context.be.generate_block_id() - map_block = obnam.map.encode_new_to_block(context.map, map_id) - context.be.upload_block(map_id, map_block, True) - - contmap_id = context.be.generate_block_id() - contmap_block = obnam.map.encode_new_to_block(context.contmap, - contmap_id) - context.be.upload_block(contmap_id, contmap_block, True) - - host_id = context.config.get("backup", "host-id") - host = obnam.obj.HostBlockObject(host_id=host_id, - map_block_ids=[map_id], - contmap_block_ids=[contmap_id]) - obnam.io.upload_host_block(context, host.encode()) - - # Then set up the real context and app. - - self.context = obnam.context.Context() - self.context.cache = obnam.cache.Cache(self.context.config) - self.context.be = obnam.backend.init(self.context.config, - self.context.cache) - self.app = obnam.Application(self.context) - self.app.load_host() - - def testHasNoMapsLoadedByDefault(self): - self.failUnlessEqual(obnam.map.count(self.context.map), 0) - - def testHasNoContentMapsLoadedByDefault(self): - self.failUnlessEqual(obnam.map.count(self.context.contmap), 0) - - def testLoadsMapsWhenRequested(self): - self.app.load_maps() - self.failUnlessEqual(obnam.map.count(self.context.map), 1) - - def testLoadsContentMapsWhenRequested(self): - self.app.load_content_maps() - self.failUnlessEqual(obnam.map.count(self.context.contmap), 1) - - def testAddsNoNewMapsWhenNothingHasChanged(self): - self.app.update_maps() - self.failUnlessEqual(obnam.map.count(self.context.map), 0) - - def testAddsANewMapsWhenSomethingHasChanged(self): - obnam.map.add(self.context.map, "pink", "pretty") - self.app.update_maps() - self.failUnlessEqual(obnam.map.count(self.context.map), 1) - - def testAddsNoNewContentMapsWhenNothingHasChanged(self): - self.app.update_content_maps() - self.failUnlessEqual(obnam.map.count(self.context.contmap), 0) - - def testAddsANewContentMapsWhenSomethingHasChanged(self): - obnam.map.add(self.context.contmap, "pink", "pretty") - self.app.update_content_maps() - self.failUnlessEqual(obnam.map.count(self.context.contmap), 1) - - -class ApplicationFinishTests(unittest.TestCase): - - def testRemovesHostObject(self): - self.context = obnam.context.Context() - self.context.cache = obnam.cache.Cache(self.context.config) - self.context.be = obnam.backend.init(self.context.config, - self.context.cache) - self.app = obnam.Application(self.context) - self.app.load_host() - - self.app.finish([]) - self.failUnlessEqual(self.app.get_host(), None) diff --git a/obnam/oper_backup.py b/obnam/oper_backup.py index 7265c6ef..dd11690f 100644 --- a/obnam/oper_backup.py +++ b/obnam/oper_backup.py @@ -34,12 +34,12 @@ class Backup(obnam.Operation): logging.info("Getting and decoding host block") app = self.get_application() host = app.load_host() - app.load_maps() + app.get_store().load_maps() # We don't need to load in file data, therefore we don't load # the content map blocks. gen = app.backup(roots) - app.finish([gen]) + app.get_store().commit_host_block([gen]) logging.info("Backup done") diff --git a/obnam/oper_forget.py b/obnam/oper_forget.py index b901da5c..40d98f41 100644 --- a/obnam/oper_forget.py +++ b/obnam/oper_forget.py @@ -40,8 +40,8 @@ class Forget(obnam.Operation): map_block_ids = host.get_map_block_ids() contmap_block_ids = host.get_contmap_block_ids() - app.load_maps() - app.load_content_maps() + app.get_store().load_maps() + app.get_store().load_content_maps() logging.debug("forget: Forgetting each id") for id in forgettable_ids: diff --git a/obnam/oper_restore.py b/obnam/oper_restore.py index 31129791..a4a161e1 100644 --- a/obnam/oper_restore.py +++ b/obnam/oper_restore.py @@ -197,8 +197,8 @@ class Restore(obnam.Operation): context = app.get_context() host = app.load_host() - app.load_maps() - app.load_content_maps() + app.get_store().load_maps() + app.get_store().load_content_maps() logging.debug("Getting generation object") gen = obnam.io.get_object(context, gen_id) diff --git a/obnam/oper_show_generations.py b/obnam/oper_show_generations.py index db20eb2f..e22d9510 100644 --- a/obnam/oper_show_generations.py +++ b/obnam/oper_show_generations.py @@ -90,7 +90,7 @@ class ShowGenerations(obnam.Operation): app = self.get_application() context = app.get_context() host = app.load_host() - app.load_maps() + app.get_store().load_maps() for gen_id in gen_ids: gen = obnam.io.get_object(context, gen_id) diff --git a/obnam/store.py b/obnam/store.py new file mode 100644 index 00000000..905e66f8 --- /dev/null +++ b/obnam/store.py @@ -0,0 +1,228 @@ +# Copyright (C) 2008 Lars Wirzenius <liw@iki.fi> +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + + +"""Abstraction for storing backup data, for Obnam.""" + + +import logging +import os + +import obnam + + +class ObjectNotFoundInStore(obnam.exception.ObnamException): + + def __init__(self, id): + self._msg = "Object %s not found in store" % id + + +class Store: + + def __init__(self, context): + self._context = context + self._host = None + + def get_host_block(self): + """Return current host block, or None if one is not known. + + You must call fetch_host_block to fetch the host block first. + + """ + + return self._host + + def fetch_host_block(self): + """Fetch host block from store, if one exists. + + If a host block does not exist, it is not an error. A new + host block is then created. + + """ + + if not self._host: + host_block = obnam.io.get_host_block(self._context) + if host_block: + self._host = obnam.obj.create_host_from_block(host_block) + else: + id = self._context.config.get("backup", "host-id") + self._host = obnam.obj.HostBlockObject(host_id=id) + return self._host + + + def load_maps(self): + """Load non-content map blocks.""" + ids = self._host.get_map_block_ids() + logging.info("Decoding %d mapping blocks" % len(ids)) + obnam.io.load_maps(self._context, self._context.map, ids) + + def load_content_maps(self): + """Load content map blocks.""" + ids = self._host.get_contmap_block_ids() + logging.info("Decoding %d content mapping blocks" % len(ids)) + obnam.io.load_maps(self._context, self._context.contmap, ids) + + def _update_map_helper(self, map): + """Create new mapping blocks of a given kind, and upload them. + + Return list of block ids for the new blocks. + + """ + + if obnam.map.get_new(map): + id = self._context.be.generate_block_id() + logging.debug("Creating mapping block %s" % id) + block = obnam.map.encode_new_to_block(map, id) + self._context.be.upload_block(id, block, True) + return [id] + else: + logging.debug("No new mappings, no new mapping block") + return [] + + def update_maps(self): + """Create new object mapping blocks and upload them.""" + logging.debug("Creating new mapping block for normal mappings") + return self._update_map_helper(self._context.map) + + def update_content_maps(self): + """Create new content object mapping blocks and upload them.""" + logging.debug("Creating new mapping block for content mappings") + return self._update_map_helper(self._context.contmap) + + def commit_host_block(self, new_generations): + """Commit the current host block to the store. + + If no host block exists, create one. If one already exists, + update it with new info. + + NOTE that after this operation the host block has changed, + and you need to call get_host_block again. + + """ + + obnam.io.flush_all_object_queues(self._context) + + logging.info("Creating new mapping blocks") + host = self.get_host_block() + map_ids = host.get_map_block_ids() + self.update_maps() + contmap_ids = (host.get_contmap_block_ids() + + self.update_content_maps()) + + logging.info("Creating new host block") + gen_ids = (host.get_generation_ids() + + [gen.get_id() for gen in new_generations]) + host2 = obnam.obj.HostBlockObject(host_id=host.get_id(), + gen_ids=gen_ids, + map_block_ids=map_ids, + contmap_block_ids=contmap_ids) + obnam.io.upload_host_block(self._context, host2.encode()) + + self._host = host2 + + def queue_object(self, object): + """Queue an object for upload to the store. + + It won't necessarily be committed (i.e., uploaded, etc) until + you call commit_host_block. Until it is committed, you may not + call get_object on it. + + """ + + obnam.io.enqueue_object(self._context, self._context.oq, + self._context.map, object.get_id(), + object.encode(), True) + + def queue_objects(self, objects): + """Queue a list of objects for upload to the store. + + See queue_object for information about what queuing means. + + """ + + for object in objects: + self.queue_object(object) + + def get_object(self, id): + """Get an object from the store. + + If the object cannot be found, raise an exception. + + """ + + object = obnam.io.get_object(self._context, id) + if object: + return object + raise ObjectNotFoundInStore(id) + + def parse_pathname(self, pathname): + """Return list of components in pathname.""" + + list = [] + while pathname: + dirname = os.path.dirname(pathname) + basename = os.path.basename(pathname) + if basename: + list.insert(0, basename) + elif dirname == os.sep: + list.insert(0, "/") + dirname = "" + pathname = dirname + + return list + + def _lookup_dir_from_refs(self, dirrefs, parts): + for ref in dirrefs: + dir = self.get_object(ref) + if dir.get_name() == parts[0]: + parts = parts[1:] + if parts: + dirrefs = dir.get_dirrefs() + return self._lookup_dir_from_refs(dirrefs, parts) + else: + return dir + return None + + def lookup_dir(self, generation, pathname): + """Return a DirObject that corresponds to pathname in a generation. + + Look up the directory in the generation. If it does not exist, + return None. + + """ + + parts = self.parse_pathname(pathname) + return self._lookup_dir_from_refs(generation.get_dirrefs(), parts) + + def lookup_file(self, generation, pathname): + """Find a non-directory thingy in a generation. + + Return a FILE component that corresponds to the filesystem entity + in question. If not found, return None. + + """ + + dirname = os.path.dirname(pathname) + if dirname: + dir = self.lookup_dir(generation, dirname) + if dir: + basename = os.path.basename(pathname) + for id in dir.get_filegrouprefs(): + fg = self.get_object(id) + file = fg.get_file(basename) + if file: + return file + + return None diff --git a/obnam/storeTests.py b/obnam/storeTests.py new file mode 100644 index 00000000..24113268 --- /dev/null +++ b/obnam/storeTests.py @@ -0,0 +1,301 @@ +# Copyright (C) 2008 Lars Wirzenius <liw@iki.fi> +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + + +"""Unit tests for abstraction for storing backup data, for Obnam.""" + + +import os +import shutil +import socket +import tempfile +import unittest + +import obnam + + +class StoreTests(unittest.TestCase): + + def setUp(self): + context = obnam.context.Context() + context.cache = obnam.cache.Cache(context.config) + context.be = obnam.backend.init(context.config, context.cache) + self.store = obnam.Store(context) + + def tearDown(self): + shutil.rmtree(self.store._context.config.get("backup", "store"), + ignore_errors=True) + shutil.rmtree(self.store._context.config.get("backup", "cache"), + ignore_errors=True) + + def testReturnsNoneWhenNoHostBlockExists(self): + self.failUnlessEqual(self.store.get_host_block(), None) + + def testReturnsAnActualHostBlockAfterFetch(self): + self.store.fetch_host_block() + host = self.store.get_host_block() + self.failUnless(isinstance(host, obnam.obj.HostBlockObject)) + + def testReturnsActualHostBlockWhenOneExists(self): + self.store.fetch_host_block() + self.store.commit_host_block([]) + + context = obnam.context.Context() + context.be = obnam.backend.init(context.config, context.cache) + store = obnam.Store(context) + store.fetch_host_block() + host = store.get_host_block() + self.failUnless(isinstance(host, obnam.obj.HostBlockObject)) + + def testReplacesHostObjectInMemory(self): + self.store.fetch_host_block() + host = self.store.get_host_block() + self.store.commit_host_block([]) + self.failIfEqual(self.store.get_host_block(), host) + + def testCreatesNewHostBlockWhenNoneExists(self): + self.store.fetch_host_block() + host = self.store.get_host_block() + self.failUnlessEqual(host.get_id(), socket.gethostname()) + self.failUnlessEqual(host.get_generation_ids(), []) + self.failUnlessEqual(host.get_map_block_ids(), []) + self.failUnlessEqual(host.get_contmap_block_ids(), []) + + def testLoadsActualHostBlockWhenOneExists(self): + context = obnam.context.Context() + cache = obnam.cache.Cache(context.config) + context.be = obnam.backend.init(context.config, context.cache) + host_id = context.config.get("backup", "host-id") + temp = obnam.obj.HostBlockObject(host_id=host_id, + gen_ids=["pink", "pretty"]) + obnam.io.upload_host_block(context, temp.encode()) + + self.store.fetch_host_block() + host = self.store.get_host_block() + self.failUnlessEqual(host.get_generation_ids(), ["pink", "pretty"]) + + def testGettingNonExistentObjectRaisesException(self): + self.failUnlessRaises(obnam.exception.ObnamException, + self.store.get_object, "pink") + + def testAddsObjectToStore(self): + o = obnam.obj.GenerationObject(id="pink") + self.store.fetch_host_block() + self.store.queue_object(o) + self.store.commit_host_block([]) + + context2 = obnam.context.Context() + context2.cache = obnam.cache.Cache(context2.config) + context2.be = obnam.backend.init(context2.config, context2.cache) + store2 = obnam.Store(context2) + store2.fetch_host_block() + store2.load_maps() + self.failUnless(store2.get_object(o.get_id())) + + def mock_queue_object(self, object): + self.queued_objects.append(object) + + def testAddsSeveralObjectsToStore(self): + objs = [None, True, False] + self.queued_objects = [] + self.store.queue_object = self.mock_queue_object + self.store.queue_objects(objs) + self.failUnlessEqual(objs, self.queued_objects) + + +class StoreMapTests(unittest.TestCase): + + def setUp(self): + # First, set up two mappings. + + context = obnam.context.Context() + context.cache = obnam.cache.Cache(context.config) + context.be = obnam.backend.init(context.config, context.cache) + + obnam.map.add(context.map, "pink", "pretty") + obnam.map.add(context.contmap, "black", "beautiful") + + map_id = context.be.generate_block_id() + map_block = obnam.map.encode_new_to_block(context.map, map_id) + context.be.upload_block(map_id, map_block, True) + + contmap_id = context.be.generate_block_id() + contmap_block = obnam.map.encode_new_to_block(context.contmap, + contmap_id) + context.be.upload_block(contmap_id, contmap_block, True) + + host_id = context.config.get("backup", "host-id") + host = obnam.obj.HostBlockObject(host_id=host_id, + map_block_ids=[map_id], + contmap_block_ids=[contmap_id]) + obnam.io.upload_host_block(context, host.encode()) + + # Then set up the real context and app. + + self.context = obnam.context.Context() + self.context.cache = obnam.cache.Cache(self.context.config) + self.context.be = obnam.backend.init(self.context.config, + self.context.cache) + self.store = obnam.Store(self.context) + self.store.fetch_host_block() + + def tearDown(self): + shutil.rmtree(self.store._context.config.get("backup", "store"), + ignore_errors=True) + shutil.rmtree(self.store._context.config.get("backup", "cache"), + ignore_errors=True) + + def testHasNoMapsLoadedByDefault(self): + self.failUnlessEqual(obnam.map.count(self.context.map), 0) + + def testHasNoContentMapsLoadedByDefault(self): + self.failUnlessEqual(obnam.map.count(self.context.contmap), 0) + + def testLoadsMapsWhenRequested(self): + self.store.load_maps() + self.failUnlessEqual(obnam.map.count(self.context.map), 1) + + def testLoadsContentMapsWhenRequested(self): + self.store.load_content_maps() + self.failUnlessEqual(obnam.map.count(self.context.contmap), 1) + + def testAddsNoNewMapsWhenNothingHasChanged(self): + self.store.update_maps() + self.failUnlessEqual(obnam.map.count(self.context.map), 0) + + def testAddsANewMapsWhenSomethingHasChanged(self): + obnam.map.add(self.context.map, "pink", "pretty") + self.store.update_maps() + self.failUnlessEqual(obnam.map.count(self.context.map), 1) + + def testAddsNoNewContentMapsWhenNothingHasChanged(self): + self.store.update_content_maps() + self.failUnlessEqual(obnam.map.count(self.context.contmap), 0) + + def testAddsANewContentMapsWhenSomethingHasChanged(self): + obnam.map.add(self.context.contmap, "pink", "pretty") + self.store.update_content_maps() + self.failUnlessEqual(obnam.map.count(self.context.contmap), 1) + + +class StorePathnameParserTests(unittest.TestCase): + + def setUp(self): + context = obnam.context.Context() + self.store = obnam.Store(context) + + def testReturnsRootForRoot(self): + self.failUnlessEqual(self.store.parse_pathname("/"), ["/"]) + + def testReturnsDotForDot(self): + self.failUnlessEqual(self.store.parse_pathname("."), ["."]) + + def testReturnsItselfForSingleElement(self): + self.failUnlessEqual(self.store.parse_pathname("foo"), ["foo"]) + + def testReturnsListOfPartsForMultipleElements(self): + self.failUnlessEqual(self.store.parse_pathname("foo/bar"), + ["foo", "bar"]) + + def testReturnsListOfPartsFromRootForAbsolutePathname(self): + self.failUnlessEqual(self.store.parse_pathname("/foo/bar"), + ["/", "foo", "bar"]) + + def testIgnoredTrailingSlashIfNotRoot(self): + self.failUnlessEqual(self.store.parse_pathname("foo/bar/"), + ["foo", "bar"]) + + +class StoreLookupTests(unittest.TestCase): + + def create_data_dir(self): + dirname = tempfile.mkdtemp() + file(os.path.join(dirname, "file1"), "w").close() + os.mkdir(os.path.join(dirname, "dir1")) + os.mkdir(os.path.join(dirname, "dir1", "dir2")) + file(os.path.join(dirname, "dir1", "dir2", "file2"), "w").close() + return dirname + + def create_context(self): + context = obnam.context.Context() + context.cache = obnam.cache.Cache(context.config) + context.be = obnam.backend.init(context.config, context.cache) + return context + + def setUp(self): + self.datadir = self.create_data_dir() + self.dirbasename = os.path.basename(self.datadir) + + app = obnam.Application(self.create_context()) + app.load_host() + gen = app.backup([self.datadir]) + app.get_store().commit_host_block([gen]) + + self.store = obnam.Store(self.create_context()) + self.store.fetch_host_block() + self.store.load_maps() + gen_ids = self.store.get_host_block().get_generation_ids() + self.gen = self.store.get_object(gen_ids[0]) + + def tearDown(self): + shutil.rmtree(self.datadir) + shutil.rmtree(self.store._context.config.get("backup", "store")) + + def testFindsBackupRoot(self): + dir = self.store.lookup_dir(self.gen, self.dirbasename) + self.failUnless(dir.get_name(), self.dirbasename) + + def testFindsFirstSubdir(self): + pathname = os.path.join(self.dirbasename, "dir1") + dir = self.store.lookup_dir(self.gen, pathname) + self.failUnless(dir.get_name(), "dir1") + + def testFindsSecondSubdir(self): + pathname = os.path.join(self.dirbasename, "dir1", "dir2") + dir = self.store.lookup_dir(self.gen, pathname) + self.failUnless(dir.get_name(), "dir2") + + def testDoesNotFindNonExistentDir(self): + self.failUnlessEqual(self.store.lookup_dir(self.gen, "notexist"), + None) + + def testDoesNotFindNonExistentFileInSubDirectory(self): + pathname = os.path.join(self.dirbasename, "dir1", "notexist") + file = self.store.lookup_file(self.gen, pathname) + self.failUnlessEqual(file, None) + + def testDoesNotFindNonExistentFileInSubSubDirectory(self): + pathname = os.path.join(self.dirbasename, "dir1", "dir2", "notexist") + file = self.store.lookup_file(self.gen, pathname) + self.failUnlessEqual(file, None) + + def testDoesNotFindNonExistentFileInRoot(self): + pathname = os.path.join(self.dirbasename, "notexist") + file = self.store.lookup_file(self.gen, pathname) + self.failUnlessEqual(file, None) + + def filename(self, file): + return file.first_string_by_kind(obnam.cmp.FILENAME) + + def testFindsFileInRootDirectory(self): + pathname = os.path.join(self.dirbasename, "file1") + file = self.store.lookup_file(self.gen, pathname) + self.failUnlessEqual(self.filename(file), "file1") + + def testFindsFileInSubDirectory(self): + pathname = os.path.join(self.dirbasename, "dir1", "dir2", "file2") + file = self.store.lookup_file(self.gen, pathname) + self.failUnlessEqual(self.filename(file), "file2") |