[z3-checkins] r24030 - in z3/sqlos/trunk/src/sqlos/file: . tests
jinty at codespeak.net
jinty at codespeak.net
Mon Mar 6 19:11:20 CET 2006
Author: jinty
Date: Mon Mar 6 19:11:09 2006
New Revision: 24030
Added:
z3/sqlos/trunk/src/sqlos/file/
z3/sqlos/trunk/src/sqlos/file/README.txt (contents, props changed)
z3/sqlos/trunk/src/sqlos/file/__init__.py (contents, props changed)
z3/sqlos/trunk/src/sqlos/file/fsutility.py (contents, props changed)
z3/sqlos/trunk/src/sqlos/file/interfaces.py (contents, props changed)
z3/sqlos/trunk/src/sqlos/file/tests/
z3/sqlos/trunk/src/sqlos/file/tests/__init__.py (contents, props changed)
z3/sqlos/trunk/src/sqlos/file/tests/test_fsutility.py (contents, props changed)
Log:
introducing sqlos.file, a prototype of a transaction safe way of storing files on the filesystem and metadata in the RDB.
Added: z3/sqlos/trunk/src/sqlos/file/README.txt
==============================================================================
--- (empty file)
+++ z3/sqlos/trunk/src/sqlos/file/README.txt Mon Mar 6 19:11:09 2006
@@ -0,0 +1,39 @@
+sqlos.file - Storing files on the filesystem and metadata in the database
+=========================================================================
+
+Objectives
+----------
+
+* Files to be stored on the filesystem with metadata a Relational or Object
+ database.
+* Files on the filesystem must be stored with their real name so that they
+ can be served by other servers (apache, ftp)
+* Must be transaction safe.
+
+Design decisions
+================
+
+sqlos.file.interfaces.IFileStorage
+----------------------------------
+
+This is a generic interface for a utility which can store files related to id's
+and filenames.
+
+ * Very primitive interface for simplicity.
+ * File ids are positive integers, this allows the conflation of these id's with
+ row ids in a relational database.
+ * Filenames can be passed to most methods to optimize the number of disk reads
+ if the files are stored on the filesystem. Methods must be robust if this name
+ turns out to be incorrect
+ * It only deals in file like objects and thus doesn't care about the encoding of
+ the contents.
+
+sqlos.file.fsutility.FileSystemFileStorage
+------------------------------------------
+
+ * Single instance represents one directory on the filesystem used for storage.
+ * Transactions don't conflict. At the moment of the commit the file is
+ either moved into place or deleted. It is up to other components of the
+ system to conflict during the prepare phase of the two phase commit.
+ * A tree structure is used to prevent having too many files in a single
+ directory.
Added: z3/sqlos/trunk/src/sqlos/file/__init__.py
==============================================================================
Added: z3/sqlos/trunk/src/sqlos/file/fsutility.py
==============================================================================
--- (empty file)
+++ z3/sqlos/trunk/src/sqlos/file/fsutility.py Mon Mar 6 19:11:09 2006
@@ -0,0 +1,372 @@
+import os
+import shutil
+import threading
+
+import transaction
+from zope.interface import implements
+from transaction.interfaces import IDataManager
+from zope.thread import local
+
+from sqlos.file.interfaces import IFileStorage
+
+#
+# Globals
+#
+
+NOTFOUND = 'NOTFOUND'
+DELETED = 'DELETED'
+CHANGED = 'CHANGED'
+READ = 'READ'
+
+FS_USE_LOCK = threading.Lock() # One great big inefficient (but safe) lock for
+ # whenever we do something not thread safe on
+ # the filesystem
+
+class FileSystemFileStorage:
+ """Stores files keyed by integer ids on the filesystem.
+
+ First of all, setup the environment:
+
+ >>> import tempfile
+ >>> datadir = tempfile.mkdtemp()
+
+ Create a utility:
+
+ >>> fs = FileSystemFileStorage(datadir)
+ >>> from zope.interface.verify import verifyObject
+ >>> verifyObject(IFileStorage, fs)
+ True
+
+ and a file:
+
+ >>> from StringIO import StringIO
+ >>> teststring = "This is a test"
+ >>> testfile = StringIO(teststring)
+
+ we can store the test file in the utility:
+
+ >>> fs.putFile(5, 'testfile.txt', testfile)
+
+ and get it:
+
+ >>> fs.queryFile(1) is None
+ True
+ >>> name, file = fs.queryFile('5')
+ >>> (name, file.read()) == ('testfile.txt', teststring)
+ True
+ >>> name, file = fs.queryFile(5, filename='testfile.txt')
+ >>> (name, file.read()) == ('testfile.txt', teststring)
+ True
+ >>> name, file = fs.queryFile(5, filename='wrong')
+ >>> (name, file.read()) == ('testfile.txt', teststring)
+ True
+
+ Find out the size:
+
+ >>> fs.statFile(1) is None
+ True
+ >>> len(teststring) == fs.statFile('5').st_size
+ True
+ >>> len(teststring) == fs.statFile(5, filename='testfile.txt').st_size
+ True
+ >>> len(teststring) == fs.statFile(5, filename='wrong').st_size
+ True
+
+ Delete it:
+
+ >>> fs.deleteFile(1)
+ Traceback (most recent call last):
+ ...
+ KeyError: 1
+ >>> fs.deleteFile(5)
+ >>> fs.deleteFile(5)
+ Traceback (most recent call last):
+ ...
+ KeyError: 5
+ >>> fs.queryFile('5') is None
+ True
+
+ id's must be integers:
+
+ >>> fs.putFile('a', 'bbb', StringIO('a'))
+ Traceback (most recent call last):
+ ...
+ TypeError: a is not an integer
+ >>> fs.queryFile('a') == None
+ Traceback (most recent call last):
+ ...
+ TypeError: a is not an integer
+ >>> fs.statFile('a') == None
+ Traceback (most recent call last):
+ ...
+ TypeError: a is not an integer
+ >>> fs.deleteFile('a')
+ Traceback (most recent call last):
+ ...
+ TypeError: a is not an integer
+
+ id's must be greater or equal to 0:
+
+ >>> fs.putFile(-1, 'bbb', StringIO('a'))
+ Traceback (most recent call last):
+ ...
+ TypeError: -1 is less than 0
+ >>> fs.queryFile(-1) == None
+ Traceback (most recent call last):
+ ...
+ TypeError: -1 is less than 0
+ >>> fs.statFile(-1) == None
+ Traceback (most recent call last):
+ ...
+ TypeError: -1 is less than 0
+ >>> fs.deleteFile(-1)
+ Traceback (most recent call last):
+ ...
+ TypeError: -1 is less than 0
+
+ >>> fs.putFile(0, 'bbb', StringIO('a'))
+
+ Tear Down:
+
+ >>> transaction.get().commit()
+ >>> shutil.rmtree(datadir)
+ """
+
+ implements(IFileStorage)
+
+ def __init__(self, data_root):
+ self.data_root = data_root
+ self._tmpdir = os.path.join(self.data_root, 'tmp')
+ if os.path.exists(self._tmpdir):
+ # Remove stale temporary files dir in case of a non-clean shutdown
+ # XXX: are people going to try create more than on of this utility???
+ shutil.rmtree(self._tmpdir)
+ os.mkdir(self._tmpdir)
+ self._dmlocal = local()
+
+ def _getDataManager(self):
+ dm = getattr(self._dmlocal, 'dm', None)
+ if dm is None:
+ self._dmlocal.dm = FileSystemDataManager(self.data_root, self._tmpdir)
+ dm = self._dmlocal.dm
+ return dm
+
+ def _checkId(self, id):
+ try:
+ id = int(id) # parameter typecheck
+ except ValueError:
+ raise TypeError('%s is not an integer' % id)
+ if id < 0:
+ raise TypeError('%s is less than 0' % id)
+ return id
+
+ def statFile(self, id, filename=None):
+ id = self._checkId(id) # parameter typecheck
+ dm = self._getDataManager()
+ return dm.statFile(id, filename=filename)
+
+ def putFile(self, id, filename, file):
+ id = self._checkId(id) # parameter typecheck
+ dm = self._getDataManager()
+ return dm.putFile(id, filename, file)
+
+ def queryFile(self, id, filename=None):
+ id = self._checkId(id) # parameter typecheck
+ dm = self._getDataManager()
+ return dm.queryFile(id, filename=filename)
+
+ def deleteFile(self, id, filename=None):
+ id = self._checkId(id) # parameter typecheck
+ dm = self._getDataManager()
+ return dm.deleteFile(id, filename=filename)
+
+
+class FileSystemDataManager:
+
+ implements(IDataManager)
+
+ def __init__(self, data_root, data_tmp):
+ self._state = {}
+ self._filenames = {}
+ self._joined_txn = False
+ self.data_root = data_root
+ self.data_tmp = data_tmp
+
+ def _joinTransaction(self):
+ if not self._joined_txn:
+ txn = transaction.get()
+ txn.join(self)
+ self._joined_txn = True
+
+ def _calcFilePath(self, id):
+ """Calculate the file path for an id.
+
+ This chops the id up into a directory path. We need to put the file in
+ it's own directory as the file can have any filename. Also, with many
+ file names we want to create a tree structure so as not to have too
+ many entries in one directory. This is semi-randomly chosen as 100.
+
+ The entries are reversed so we can build it up as we go along
+
+ Like this:
+
+ >>> data_root = 'r'
+ >>> dm = FileSystemDataManager(data_root, '//b')
+ >>> dm._calcFilePath(00) == os.path.join(data_root, '00', 'data')
+ True
+ >>> dm._calcFilePath(23) == os.path.join(data_root, '23', 'data')
+ True
+ >>> dm._calcFilePath(100) == os.path.join(data_root, '00', '01', 'data')
+ True
+ >>> dm._calcFilePath(1234) == os.path.join(data_root, '34', '12', 'data')
+ True
+ >>> dm._calcFilePath(10001) == os.path.join(data_root, '01', '00', '01', 'data')
+ True
+ >>> dm._calcFilePath(123456) == os.path.join(data_root, '56', '34', '12', 'data')
+ True
+ """
+ # pad with a zero in the front if not even
+ id = str(id)
+ if len(id) % 2 == 1:
+ id = '0' + id
+ # divide into a list of groups of 2
+ tmp = None
+ path = []
+ for i in id:
+ if tmp is None:
+ tmp = i
+ else:
+ path.append(tmp + i)
+ tmp = None
+ # add on the non id parts and reverse
+ path.append(self.data_root)
+ path.reverse()
+ path.append('data')
+ return os.path.join(*path)
+
+ def _calcTmpFileName(self, id):
+ tid = threading.currentThread().getName()
+ filename = '%s-%s' % (tid, id)
+ return os.path.join(self.data_tmp, filename)
+
+ def _setTmpFile(self, id, filename):
+ assert id not in self._state
+ self._filenames[id] = filename
+ tmpfile = self._calcTmpFileName(id)
+ fullfilename = os.path.join(self._calcFilePath(id), filename)
+ # NOTE: Filesystem operation is thread safe as the thread id is a
+ # component of the filename
+ os.link(fullfilename, tmpfile)
+
+ def _getTmpFile(self, id):
+ assert id in self._state
+ tmpfile = self._calcTmpFileName(id)
+ return (self._filenames[id], open(tmpfile, 'r'))
+
+ def _delTmpFile(self, id):
+ tmpfile = self._calcTmpFileName(id)
+ # NOTE: Filesystem operation is thread safe as the thread id is a
+ # component of the filename
+ if os.path.exists(tmpfile):
+ os.remove(tmpfile)
+
+ def prepare(self, txn):
+ return True
+
+ def abort(self, txn):
+ for id in self._state:
+ self._delTmpFile(id)
+ self._state = {}
+ self._joined_txn = False
+
+ def commit(self, txn):
+ # XXX we need to thread lock those pieces dealing with the filesystem
+ for id, state in self._state.items():
+ if state == DELETED:
+ #delete if exists
+ dir = self._calcFilePath(id)
+ FS_USE_LOCK.acquire()
+ if os.path.exists(dir):
+ shutil.rmtree(dir)
+ FS_USE_LOCK.release()
+ elif state == CHANGED:
+ filename = self._filenames[id]
+ tmpfile = self._calcTmpFileName(id)
+ filedir = self._calcFilePath(id)
+ fullfilename = os.path.join(filedir, filename)
+ FS_USE_LOCK.acquire()
+ if os.path.exists(filedir):
+ shutil.rmtree(filedir)
+ os.makedirs(filedir)
+ os.link(tmpfile, fullfilename)
+ FS_USE_LOCK.release()
+ self._delTmpFile(id)
+ self._state = {}
+ self._joined_txn = False
+
+ def _queryFileName(self, id):
+ """Try to get a path to a file.
+
+ returns the path to the file, or None if not found.
+ """
+ filedir = self._calcFilePath(id)
+ try:
+ filenamelist = os.listdir(filedir)
+ assert len(filenamelist) == 1
+ except (AssertionError, OSError):
+ return None
+ return filenamelist[0]
+
+ def statFile(self, id, filename=None):
+ if id not in self._state:
+ # Never seen this file before
+ filename = self._queryFileName(id)
+ if filename is None:
+ self._state[id] = NOTFOUND
+ return None
+ self._setTmpFile(id, filename)
+ self._state[id] = READ
+ if self._state[id] in [NOTFOUND, DELETED]:
+ return None
+ tmpfile = self._calcTmpFileName(id)
+ return os.stat(tmpfile)
+
+ def queryFile(self, id, filename=None):
+ if id not in self._state:
+ # Never seen this file before
+ filename = self._queryFileName(id)
+ if filename is None:
+ self._state[id] = NOTFOUND
+ return None
+ self._setTmpFile(id, filename)
+ self._state[id] = READ
+ if self._state[id] in [NOTFOUND, DELETED]:
+ return None
+ return self._getTmpFile(id)
+
+ def deleteFile(self, id, filename=None):
+ if id not in self._state:
+ # Never seen this file before
+ filename = self._queryFileName(id)
+ if filename is None:
+ self._state[id] = NOTFOUND
+ raise KeyError(id)
+ self._joinTransaction()
+ self._state[id] = DELETED
+ return
+ if self._state[id] in [NOTFOUND, DELETED]:
+ raise KeyError(id)
+ self._joinTransaction()
+ self._state[id] = DELETED
+
+ def putFile(self, id, filename, file):
+ # XXX - this is really crappilly inefficient.
+ tmpfile = open(self._calcTmpFileName(id), 'w')
+ tmpfile.write(file.read())
+ file.close()
+ self._filenames[id] = filename
+ self._joinTransaction()
+ self._state[id] = CHANGED
+
+ def sortKey(self):
+ return str(id(self))
Added: z3/sqlos/trunk/src/sqlos/file/interfaces.py
==============================================================================
--- (empty file)
+++ z3/sqlos/trunk/src/sqlos/file/interfaces.py Mon Mar 6 19:11:09 2006
@@ -0,0 +1,51 @@
+from zope.interface import Interface, Attribute
+from zope.schema.interfaces import IField
+from zope.schema import TextLine, Bytes
+
+class IFileStorage(Interface):
+
+ def statFile(id, filename=None):
+ """Return the results of calling os.stat on the file or None.
+
+ raises TypeError if the id is not a positive integer (i.e. => 0)
+
+ id - positive integer.
+ filename - a hint used for optimization or None for no hint,
+ the hint is not trusted.
+
+ returns None if no file exists at that id.
+
+ Probably this is only useful for finding the size of the file.
+ """
+
+ def queryFile(id, filename=None):
+ """Return a tuple of (filename, file) or None.
+
+ raises TypeError if the id is not a positive integer (i.e. => 0)
+
+ id - positive integer.
+ filename - a hint used for optimization or None for no hint,
+ the hint is not trusted.
+ """
+
+ def putFile(id, filename, file):
+ """Store a file, overwriting any existing file.
+
+ raises TypeError if the id is not a positive integer (i.e. => 0)
+
+ id - positive integer
+ filename - string.
+ file - file like object, seek may or may not be called,
+ so make sure that the current position is 0.
+ """
+
+ def deleteFile(id, filename=None):
+ """Delete the stored file.
+
+ raises a KeyError if no file by that id exists.
+ raises TypeError if the id is not a positive integer (i.e. => 0)
+
+ id - positive integer.
+ filename - a hint used for optimization or None for no hint,
+ the hint is not trusted.
+ """
Added: z3/sqlos/trunk/src/sqlos/file/tests/__init__.py
==============================================================================
Added: z3/sqlos/trunk/src/sqlos/file/tests/test_fsutility.py
==============================================================================
--- (empty file)
+++ z3/sqlos/trunk/src/sqlos/file/tests/test_fsutility.py Mon Mar 6 19:11:09 2006
@@ -0,0 +1,357 @@
+"""
+$Id: test_doctests.py 20906 2005-12-08 20:56:12Z jinty $
+"""
+
+import unittest
+import doctest
+import tempfile
+import shutil
+import threading
+from StringIO import StringIO
+
+import transaction
+from zope.testing.doctestunit import DocTestSuite
+from zope.app.testing.placelesssetup import setUp, tearDown
+from zope.app.testing import ztapi
+from zope import component
+
+from sqlos.file.interfaces import IFileStorage
+from sqlos.file.fsutility import FileSystemFileStorage
+
+class FileSystemFileStorageSetUp(object):
+
+ datadir = None
+
+ @classmethod
+ def setUp(cls):
+ assert cls.datadir is None
+ cls.datadir = tempfile.mkdtemp()
+ fs = FileSystemFileStorage(cls.datadir)
+ ztapi.provideUtility(IFileStorage, fs)
+
+ @classmethod
+ def tearDown(cls):
+ assert cls.datadir is not None
+ shutil.rmtree(cls.datadir)
+ cls.datadir = None
+ ztapi.unprovideUtility(IFileStorage)
+
+def test_transaction_read_isolation():
+ r"""
+ Set Up:
+
+ >>> setUp()
+ >>> FileSystemFileStorageSetUp.setUp()
+ >>> fileid = 5
+
+ Begin a transaction:
+
+ >>> transaction.get().commit()
+ >>> txn = transaction.begin()
+
+ Create a function to query the file in a different thread:
+
+ >>> log = []
+ >>> def queryFile():
+ ... transaction.begin()
+ ... fs = component.getUtility(IFileStorage)
+ ... file = fs.queryFile(fileid)
+ ... if file is not None:
+ ... name = file[0]
+ ... content = file[1].read()
+ ... log.append('FILENAME:\n%s\nCONTENTS:\n%s' % (name, content))
+ ... else:
+ ... log.append(file)
+ ... transaction.get().commit()
+
+ >>> def runInThread(target):
+ ... thread = threading.Thread(target=target)
+ ... thread.start()
+ ... thread.join()
+ ... print log.pop()
+
+ Try to get the file in a different thread:
+
+ >>> runInThread(queryFile)
+ None
+
+ Create a file:
+
+ >>> f = StringIO('File')
+ >>> fs = component.getUtility(IFileStorage)
+ >>> fs.putFile(fileid, 'file.txt', f)
+
+ Try to get the file in a different thread before comitting:
+
+ >>> runInThread(queryFile)
+ None
+
+ Commit the transaction
+
+ >>> transaction.get().commit()
+
+ Try to get the file in a different thread:
+
+ >>> runInThread(queryFile)
+ FILENAME:
+ file.txt
+ CONTENTS:
+ File
+
+ TearDown:
+
+ >>> FileSystemFileStorageSetUp.tearDown()
+ >>> tearDown()
+ """
+
+def test_transaction_conflict_write():
+ r"""Test what should happen when we get competing write/deletes.
+
+ The strategy here is an overly optimistic one. Writes always succeed.
+ Files are moved into place on transaction commit.
+
+ Set Up:
+
+ >>> setUp()
+ >>> FileSystemFileStorageSetUp.setUp()
+ >>> fileid = 5
+
+ Begin a transaction:
+
+ >>> transaction.get().commit()
+ >>> txn = transaction.begin()
+
+ Create a function to put or delete the file in a different thread:
+
+ >>> log = []
+ >>> def queryFile():
+ ... transaction.begin()
+ ... fs = component.getUtility(IFileStorage)
+ ... file = fs.queryFile(fileid)
+ ... if file is not None:
+ ... name = file[0]
+ ... content = file[1].read()
+ ... log.append('FILENAME:\n%s\nCONTENTS:\n%s' % (name, content))
+ ... else:
+ ... log.append(file)
+ ... transaction.get().commit()
+ >>> def putFile():
+ ... transaction.begin()
+ ... f = StringIO('eliF')
+ ... fs = component.getUtility(IFileStorage)
+ ... try:
+ ... fs.putFile(fileid, 'txt.file', f)
+ ... log.append('SUCCESS')
+ ... except WriteConflictError:
+ ... log.append('CONFLICT')
+ ... transaction.get().commit()
+ >>> def deleteFile():
+ ... transaction.begin()
+ ... f = StringIO('File')
+ ... fs = component.getUtility(IFileStorage)
+ ... try:
+ ... fs.deleteFile(fileid)
+ ... log.append('SUCCESS')
+ ... except KeyError:
+ ... log.append('NOT FOUND')
+ ... except WriteConflictError:
+ ... log.append('CONFLICT')
+ ... transaction.get().commit()
+
+ >>> def runInThread(target):
+ ... thread = threading.Thread(target=target)
+ ... thread.start()
+ ... thread.join()
+ ... print log.pop()
+
+ Try to put and delete in a different thread:
+
+ >>> runInThread(putFile)
+ SUCCESS
+ >>> runInThread(deleteFile)
+ SUCCESS
+
+ Create a file:
+
+ >>> f = StringIO('File')
+ >>> fs = component.getUtility(IFileStorage)
+ >>> fs.putFile(fileid, 'file.txt', f)
+
+ Try _really_ hard to break the transaction isolation before committing:
+
+ >>> runInThread(deleteFile)
+ NOT FOUND
+
+ >>> runInThread(queryFile)
+ None
+
+ >>> (name, file) = fs.queryFile(fileid)
+ >>> print 'FILENAME:\n%s\nCONTENTS:\n%s' % (name, file.read())
+ FILENAME:
+ file.txt
+ CONTENTS:
+ File
+
+ >>> runInThread(putFile)
+ SUCCESS
+
+ >>> (name, file) = fs.queryFile(fileid)
+ >>> print 'FILENAME:\n%s\nCONTENTS:\n%s' % (name, file.read())
+ FILENAME:
+ file.txt
+ CONTENTS:
+ File
+
+ >>> runInThread(queryFile)
+ FILENAME:
+ txt.file
+ CONTENTS:
+ eliF
+
+ >>> runInThread(deleteFile)
+ SUCCESS
+
+ >>> runInThread(queryFile)
+ None
+
+ >>> (name, file) = fs.queryFile(fileid)
+ >>> print 'FILENAME:\n%s\nCONTENTS:\n%s' % (name, file.read())
+ FILENAME:
+ file.txt
+ CONTENTS:
+ File
+
+ Commit the transaction
+
+ >>> transaction.get().commit()
+
+ Check that the file was created, overwriting the previous file:
+
+ >>> runInThread(queryFile)
+ FILENAME:
+ file.txt
+ CONTENTS:
+ File
+
+ TearDown:
+
+ >>> FileSystemFileStorageSetUp.tearDown()
+ >>> tearDown()
+ """
+
+def test_transaction_conflict_delete():
+ r"""Test what should happen when we get competing write/deletes.
+
+ The strategy here is an overly optimistic one. Writes always succeed.
+ Files are moved into place on transaction commit.
+
+ Set Up:
+
+ >>> setUp()
+ >>> FileSystemFileStorageSetUp.setUp()
+ >>> fileid = 6
+
+ Begin a transaction:
+
+ >>> transaction.get().commit()
+ >>> txn = transaction.begin()
+ >>> f = StringIO('File')
+ >>> fs = component.getUtility(IFileStorage)
+ >>> fs.putFile(fileid, 'file.txt', f)
+ >>> transaction.get().commit()
+ >>> txn = transaction.begin()
+
+ Create a function to put or delete the file in a different thread:
+
+ >>> log = []
+ >>> def queryFile():
+ ... transaction.begin()
+ ... fs = component.getUtility(IFileStorage)
+ ... file = fs.queryFile(fileid)
+ ... if file is not None:
+ ... name = file[0]
+ ... content = file[1].read()
+ ... log.append('FILENAME:\n%s\nCONTENTS:\n%s' % (name, content))
+ ... else:
+ ... log.append(file)
+ ... transaction.get().commit()
+ >>> def putFile():
+ ... transaction.begin()
+ ... f = StringIO('eliF')
+ ... fs = component.getUtility(IFileStorage)
+ ... fs.putFile(fileid, 'txt.file', f)
+ ... log.append('SUCCESS')
+ ... transaction.get().commit()
+ >>> def deleteFile():
+ ... transaction.begin()
+ ... fs = component.getUtility(IFileStorage)
+ ... try:
+ ... fs.deleteFile(fileid)
+ ... log.append('SUCCESS')
+ ... except KeyError:
+ ... log.append('NOT FOUND')
+ ... transaction.get().commit()
+
+ >>> def runInThread(target):
+ ... thread = threading.Thread(target=target)
+ ... thread.start()
+ ... thread.join()
+ ... print log.pop()
+
+ Try to put and delete in a different thread:
+
+ >>> runInThread(putFile) #a
+ SUCCESS
+ >>> runInThread(deleteFile) #b
+ SUCCESS
+ >>> runInThread(putFile) #c
+ SUCCESS
+
+ Delete the file:
+
+ >>> fs = component.getUtility(IFileStorage)
+ >>> fs.deleteFile(fileid)
+
+ Try _really_ hard to break the transaction isolation before committing:
+
+ >>> runInThread(deleteFile) #e
+ SUCCESS
+
+ >>> runInThread(queryFile) #f
+ None
+
+ >>> fs.queryFile(fileid) == None
+ True
+
+ >>> runInThread(putFile) #g
+ SUCCESS
+
+ >>> fs.queryFile(fileid) == None
+ True
+
+ >>> runInThread(queryFile) #h
+ FILENAME:
+ txt.file
+ CONTENTS:
+ eliF
+
+ Commit the transaction
+
+ >>> transaction.get().commit()
+
+ Check that the file was deleted, removing the previous file:
+
+ >>> runInThread(queryFile)
+ None
+
+ TearDown:
+
+ >>> FileSystemFileStorageSetUp.tearDown()
+ >>> tearDown()
+ """
+
+def test_suite():
+ return unittest.TestSuite([
+ DocTestSuite('sqlos.file.fsutility'),
+ DocTestSuite()
+ ])
More information about the z3-checkins
mailing list