Mercurial > p > roundup > code
comparison test/memorydb.py @ 4342:94c992852f12
add in-memory hyperdb implementation to speed up testing
| author | Richard Jones <richard@users.sourceforge.net> |
|---|---|
| date | Tue, 02 Feb 2010 04:44:18 +0000 |
| parents | |
| children | 4dc575b33712 |
comparison
equal
deleted
inserted
replaced
| 4341:7f67092fe03d | 4342:94c992852f12 |
|---|---|
| 1 '''Implement an in-memory hyperdb for testing purposes. | |
| 2 ''' | |
| 3 | |
| 4 import shutil | |
| 5 | |
| 6 from roundup import hyperdb | |
| 7 from roundup import roundupdb | |
| 8 from roundup import security | |
| 9 from roundup import password | |
| 10 from roundup import configuration | |
| 11 from roundup.backends import back_anydbm | |
| 12 from roundup.backends import indexer_dbm | |
| 13 from roundup.backends import indexer_common | |
| 14 from roundup.hyperdb import * | |
| 15 | |
| 16 def new_config(): | |
| 17 config = configuration.CoreConfig() | |
| 18 config.DATABASE = "db" | |
| 19 #config.logging = MockNull() | |
| 20 # these TRACKER_WEB and MAIL_DOMAIN values are used in mailgw tests | |
| 21 config.MAIL_DOMAIN = "your.tracker.email.domain.example" | |
| 22 config.TRACKER_WEB = "http://tracker.example/cgi-bin/roundup.cgi/bugs/" | |
| 23 return config | |
| 24 | |
| 25 def create(journaltag, create=True): | |
| 26 db = Database(new_config(), journaltag) | |
| 27 | |
| 28 # load standard schema | |
| 29 schema = os.path.join(os.path.dirname(__file__), | |
| 30 '../share/roundup/templates/classic/schema.py') | |
| 31 vars = dict(globals()) | |
| 32 vars['db'] = db | |
| 33 execfile(schema, vars) | |
| 34 initial_data = os.path.join(os.path.dirname(__file__), | |
| 35 '../share/roundup/templates/classic/initial_data.py') | |
| 36 vars = dict(db=db, admin_email='admin@test.com', | |
| 37 adminpw=password.Password('sekrit')) | |
| 38 execfile(initial_data, vars) | |
| 39 | |
| 40 # load standard detectors | |
| 41 dirname = os.path.join(os.path.dirname(__file__), | |
| 42 '../share/roundup/templates/classic/detectors') | |
| 43 for fn in os.listdir(dirname): | |
| 44 if not fn.endswith('.py'): continue | |
| 45 vars = {} | |
| 46 execfile(os.path.join(dirname, fn), vars) | |
| 47 vars['init'](db) | |
| 48 | |
| 49 ''' | |
| 50 status = Class(db, "status", name=String()) | |
| 51 status.setkey("name") | |
| 52 priority = Class(db, "priority", name=String(), order=String()) | |
| 53 priority.setkey("name") | |
| 54 keyword = Class(db, "keyword", name=String(), order=String()) | |
| 55 keyword.setkey("name") | |
| 56 user = Class(db, "user", username=String(), password=Password(), | |
| 57 assignable=Boolean(), age=Number(), roles=String(), address=String(), | |
| 58 supervisor=Link('user'),realname=String(),alternate_addresses=String()) | |
| 59 user.setkey("username") | |
| 60 file = FileClass(db, "file", name=String(), type=String(), | |
| 61 comment=String(indexme="yes"), fooz=Password()) | |
| 62 file_nidx = FileClass(db, "file_nidx", content=String(indexme='no')) | |
| 63 issue = IssueClass(db, "issue", title=String(indexme="yes"), | |
| 64 status=Link("status"), nosy=Multilink("user"), deadline=Date(), | |
| 65 foo=Interval(), files=Multilink("file"), assignedto=Link('user'), | |
| 66 priority=Link('priority'), spam=Multilink('msg'), | |
| 67 feedback=Link('msg')) | |
| 68 stuff = Class(db, "stuff", stuff=String()) | |
| 69 session = Class(db, 'session', title=String()) | |
| 70 msg = FileClass(db, "msg", date=Date(), | |
| 71 author=Link("user", do_journal='no'), | |
| 72 files=Multilink('file'), inreplyto=String(), | |
| 73 messageid=String(), summary=String(), | |
| 74 content=String(), | |
| 75 recipients=Multilink("user", do_journal='no') | |
| 76 ) | |
| 77 ''' | |
| 78 if create: | |
| 79 db.user.create(username="fred", roles='User', | |
| 80 password=password.Password('sekrit'), address='fred@example.com') | |
| 81 | |
| 82 db.security.addPermissionToRole('User', 'Email Access') | |
| 83 ''' | |
| 84 db.security.addPermission(name='Register', klass='user') | |
| 85 db.security.addPermissionToRole('User', 'Web Access') | |
| 86 db.security.addPermissionToRole('Anonymous', 'Email Access') | |
| 87 db.security.addPermissionToRole('Anonymous', 'Register', 'user') | |
| 88 for cl in 'issue', 'file', 'msg', 'keyword': | |
| 89 db.security.addPermissionToRole('User', 'View', cl) | |
| 90 db.security.addPermissionToRole('User', 'Edit', cl) | |
| 91 db.security.addPermissionToRole('User', 'Create', cl) | |
| 92 for cl in 'priority', 'status': | |
| 93 db.security.addPermissionToRole('User', 'View', cl) | |
| 94 ''' | |
| 95 return db | |
| 96 | |
| 97 class cldb(dict): | |
| 98 def close(self): | |
| 99 pass | |
| 100 | |
| 101 class BasicDatabase(dict): | |
| 102 ''' Provide a nice encapsulation of an anydbm store. | |
| 103 | |
| 104 Keys are id strings, values are automatically marshalled data. | |
| 105 ''' | |
| 106 def __getitem__(self, key): | |
| 107 if key not in self: | |
| 108 d = self[key] = {} | |
| 109 return d | |
| 110 return super(BasicDatabase, self).__getitem__(key) | |
| 111 def exists(self, infoid): | |
| 112 return infoid in self | |
| 113 def get(self, infoid, value, default=None): | |
| 114 return self[infoid].get(value, default) | |
| 115 def getall(self, infoid): | |
| 116 return self[infoid] | |
| 117 def set(self, infoid, **newvalues): | |
| 118 self[infoid].update(newvalues) | |
| 119 def list(self): | |
| 120 return self.keys() | |
| 121 def destroy(self, infoid): | |
| 122 del self[infoid] | |
| 123 def commit(self): | |
| 124 pass | |
| 125 def close(self): | |
| 126 pass | |
| 127 def updateTimestamp(self, sessid): | |
| 128 pass | |
| 129 def clean(self): | |
| 130 pass | |
| 131 | |
| 132 class Sessions(BasicDatabase): | |
| 133 name = 'sessions' | |
| 134 | |
| 135 class OneTimeKeys(BasicDatabase): | |
| 136 name = 'otks' | |
| 137 | |
| 138 class Indexer(indexer_dbm.Indexer): | |
| 139 def __init__(self, db): | |
| 140 indexer_common.Indexer.__init__(self, db) | |
| 141 self.reindex = 0 | |
| 142 self.quiet = 9 | |
| 143 self.changed = 0 | |
| 144 | |
| 145 def load_index(self, reload=0, wordlist=None): | |
| 146 # Unless reload is indicated, do not load twice | |
| 147 if self.index_loaded() and not reload: | |
| 148 return 0 | |
| 149 self.words = {} | |
| 150 self.files = {'_TOP':(0,None)} | |
| 151 self.fileids = {} | |
| 152 self.changed = 0 | |
| 153 | |
| 154 def save_index(self): | |
| 155 pass | |
| 156 | |
| 157 class Database(hyperdb.Database, roundupdb.Database): | |
| 158 """A database for storing records containing flexible data types. | |
| 159 | |
| 160 Transaction stuff TODO: | |
| 161 | |
| 162 - check the timestamp of the class file and nuke the cache if it's | |
| 163 modified. Do some sort of conflict checking on the dirty stuff. | |
| 164 - perhaps detect write collisions (related to above)? | |
| 165 """ | |
| 166 def __init__(self, config, journaltag=None): | |
| 167 self.config, self.journaltag = config, journaltag | |
| 168 self.classes = {} | |
| 169 self.items = {} | |
| 170 self.ids = {} | |
| 171 self.journals = {} | |
| 172 self.files = {} | |
| 173 self.security = security.Security(self) | |
| 174 self.stats = {'cache_hits': 0, 'cache_misses': 0, 'get_items': 0, | |
| 175 'filtering': 0} | |
| 176 self.sessions = Sessions() | |
| 177 self.otks = OneTimeKeys() | |
| 178 self.indexer = Indexer(self) | |
| 179 | |
| 180 | |
| 181 def filename(self, classname, nodeid, property=None, create=0): | |
| 182 shutil.copyfile(__file__, __file__+'.dummy') | |
| 183 return __file__+'.dummy' | |
| 184 | |
| 185 def post_init(self): | |
| 186 pass | |
| 187 | |
| 188 def refresh_database(self): | |
| 189 pass | |
| 190 | |
| 191 def getSessionManager(self): | |
| 192 return self.sessions | |
| 193 | |
| 194 def getOTKManager(self): | |
| 195 return self.otks | |
| 196 | |
| 197 def reindex(self, classname=None, show_progress=False): | |
| 198 pass | |
| 199 | |
| 200 def __repr__(self): | |
| 201 return '<memorydb instance at %x>'%id(self) | |
| 202 | |
| 203 def storefile(self, classname, nodeid, property, content): | |
| 204 self.files[classname, nodeid, property] = content | |
| 205 | |
| 206 def getfile(self, classname, nodeid, property): | |
| 207 return self.files[classname, nodeid, property] | |
| 208 | |
| 209 def numfiles(self): | |
| 210 return len(self.files) | |
| 211 | |
| 212 # | |
| 213 # Classes | |
| 214 # | |
| 215 def __getattr__(self, classname): | |
| 216 """A convenient way of calling self.getclass(classname).""" | |
| 217 if self.classes.has_key(classname): | |
| 218 return self.classes[classname] | |
| 219 raise AttributeError, classname | |
| 220 | |
| 221 def addclass(self, cl): | |
| 222 cn = cl.classname | |
| 223 if self.classes.has_key(cn): | |
| 224 raise ValueError, cn | |
| 225 self.classes[cn] = cl | |
| 226 self.items[cn] = cldb() | |
| 227 self.ids[cn] = 0 | |
| 228 | |
| 229 # add default Edit and View permissions | |
| 230 self.security.addPermission(name="Create", klass=cn, | |
| 231 description="User is allowed to create "+cn) | |
| 232 self.security.addPermission(name="Edit", klass=cn, | |
| 233 description="User is allowed to edit "+cn) | |
| 234 self.security.addPermission(name="View", klass=cn, | |
| 235 description="User is allowed to access "+cn) | |
| 236 | |
| 237 def getclasses(self): | |
| 238 """Return a list of the names of all existing classes.""" | |
| 239 l = self.classes.keys() | |
| 240 l.sort() | |
| 241 return l | |
| 242 | |
| 243 def getclass(self, classname): | |
| 244 """Get the Class object representing a particular class. | |
| 245 | |
| 246 If 'classname' is not a valid class name, a KeyError is raised. | |
| 247 """ | |
| 248 try: | |
| 249 return self.classes[classname] | |
| 250 except KeyError: | |
| 251 raise KeyError, 'There is no class called "%s"'%classname | |
| 252 | |
| 253 # | |
| 254 # Class DBs | |
| 255 # | |
| 256 def clear(self): | |
| 257 self.items = {} | |
| 258 | |
| 259 def getclassdb(self, classname): | |
| 260 """ grab a connection to the class db that will be used for | |
| 261 multiple actions | |
| 262 """ | |
| 263 return self.items[classname] | |
| 264 | |
| 265 # | |
| 266 # Node IDs | |
| 267 # | |
| 268 def newid(self, classname): | |
| 269 self.ids[classname] += 1 | |
| 270 return str(self.ids[classname]) | |
| 271 | |
| 272 # | |
| 273 # Nodes | |
| 274 # | |
| 275 def addnode(self, classname, nodeid, node): | |
| 276 self.getclassdb(classname)[nodeid] = node | |
| 277 | |
| 278 def setnode(self, classname, nodeid, node): | |
| 279 self.getclassdb(classname)[nodeid] = node | |
| 280 | |
| 281 def getnode(self, classname, nodeid, cldb=None): | |
| 282 if cldb is not None: | |
| 283 return cldb[nodeid] | |
| 284 return self.getclassdb(classname)[nodeid] | |
| 285 | |
| 286 def destroynode(self, classname, nodeid): | |
| 287 del self.getclassdb(classname)[nodeid] | |
| 288 | |
| 289 def hasnode(self, classname, nodeid): | |
| 290 return nodeid in self.getclassdb(classname) | |
| 291 | |
| 292 def countnodes(self, classname, db=None): | |
| 293 return len(self.getclassdb(classname)) | |
| 294 | |
| 295 # | |
| 296 # Journal | |
| 297 # | |
| 298 def addjournal(self, classname, nodeid, action, params, creator=None, | |
| 299 creation=None): | |
| 300 if creator is None: | |
| 301 creator = self.getuid() | |
| 302 if creation is None: | |
| 303 creation = date.Date() | |
| 304 self.journals.setdefault(classname, {}).setdefault(nodeid, | |
| 305 []).append((nodeid, creation, creator, action, params)) | |
| 306 | |
| 307 def setjournal(self, classname, nodeid, journal): | |
| 308 self.journals.setdefault(classname, {})[nodeid] = journal | |
| 309 | |
| 310 def getjournal(self, classname, nodeid): | |
| 311 return self.journals.get(classname, {}).get(nodeid, []) | |
| 312 | |
| 313 def pack(self, pack_before): | |
| 314 TODO | |
| 315 | |
| 316 # | |
| 317 # Basic transaction support | |
| 318 # | |
| 319 def commit(self, fail_ok=False): | |
| 320 pass | |
| 321 | |
| 322 def rollback(self): | |
| 323 TODO | |
| 324 | |
| 325 def close(self): | |
| 326 pass | |
| 327 | |
| 328 class Class(back_anydbm.Class): | |
| 329 def getnodeids(self, db=None, retired=None): | |
| 330 return self.db.getclassdb(self.classname).keys() | |
| 331 | |
| 332 class FileClass(back_anydbm.Class): | |
| 333 def __init__(self, db, classname, **properties): | |
| 334 if not properties.has_key('content'): | |
| 335 properties['content'] = hyperdb.String(indexme='yes') | |
| 336 if not properties.has_key('type'): | |
| 337 properties['type'] = hyperdb.String() | |
| 338 back_anydbm.Class.__init__(self, db, classname, **properties) | |
| 339 | |
| 340 def getnodeids(self, db=None, retired=None): | |
| 341 return self.db.getclassdb(self.classname).keys() | |
| 342 | |
| 343 # deviation from spec - was called ItemClass | |
| 344 class IssueClass(Class, roundupdb.IssueClass): | |
| 345 # Overridden methods: | |
| 346 def __init__(self, db, classname, **properties): | |
| 347 """The newly-created class automatically includes the "messages", | |
| 348 "files", "nosy", and "superseder" properties. If the 'properties' | |
| 349 dictionary attempts to specify any of these properties or a | |
| 350 "creation" or "activity" property, a ValueError is raised. | |
| 351 """ | |
| 352 if not properties.has_key('title'): | |
| 353 properties['title'] = hyperdb.String(indexme='yes') | |
| 354 if not properties.has_key('messages'): | |
| 355 properties['messages'] = hyperdb.Multilink("msg") | |
| 356 if not properties.has_key('files'): | |
| 357 properties['files'] = hyperdb.Multilink("file") | |
| 358 if not properties.has_key('nosy'): | |
| 359 # note: journalling is turned off as it really just wastes | |
| 360 # space. this behaviour may be overridden in an instance | |
| 361 properties['nosy'] = hyperdb.Multilink("user", do_journal="no") | |
| 362 if not properties.has_key('superseder'): | |
| 363 properties['superseder'] = hyperdb.Multilink(classname) | |
| 364 Class.__init__(self, db, classname, **properties) | |
| 365 | |
| 366 # vim: set et sts=4 sw=4 : |
