Skip to content

Commit 45130c9

Browse files
author
Richard Jones
committed
add in-memory hyperdb implementation to speed up testing
1 parent ac75b59 commit 45130c9

File tree

4 files changed

+428
-60
lines changed

4 files changed

+428
-60
lines changed

roundup/backends/back_anydbm.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -950,7 +950,7 @@ def get(self, nodeid, propname, default=_marker, cache=1):
950950
raise ValueError, 'Journalling is disabled for this class'
951951
journal = self.db.getjournal(self.classname, nodeid)
952952
if journal:
953-
return self.db.getjournal(self.classname, nodeid)[0][1]
953+
return journal[0][1]
954954
else:
955955
# on the strange chance that there's no journal
956956
return date.Date()

roundup/configuration.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1249,6 +1249,14 @@ def __init__(self, home_dir=None, settings={}):
12491249
if home_dir is None:
12501250
self.init_logging()
12511251

1252+
def copy(self):
1253+
new = CoreConfig()
1254+
new.sections = list(self.sections)
1255+
new.section_descriptions = dict(self.section_descriptions)
1256+
new.section_options = dict(self.section_options)
1257+
new.options = dict(self.options)
1258+
return new
1259+
12521260
def _get_unset_options(self):
12531261
need_set = Config._get_unset_options(self)
12541262
# remove MAIL_PASSWORD if MAIL_USER is empty

test/memorydb.py

Lines changed: 366 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,366 @@
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='[email protected]',
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='[email protected]')
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 :

0 commit comments

Comments
 (0)