1 # Copyright (C) 2005-2012 Aaron Bentley <abentley@panoramicfeedback.com>
2 # Alexander Belchenko <bialix@ukr.net>
3 # Chris Ball <cjb@laptop.org>
4 # Gianluca Montecchi <gian@grys.it>
5 # Oleg Romanyshyn <oromanyshyn@panoramicfeedback.com>
6 # W. Trevor King <wking@tremily.us>
8 # This file is part of Bugs Everywhere.
10 # Bugs Everywhere is free software: you can redistribute it and/or modify it
11 # under the terms of the GNU General Public License as published by the Free
12 # Software Foundation, either version 2 of the License, or (at your option) any
15 # Bugs Everywhere is distributed in the hope that it will be useful, but
16 # WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
17 # FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
20 # You should have received a copy of the GNU General Public License along with
21 # Bugs Everywhere. If not, see <http://www.gnu.org/licenses/>.
23 """Define :py:class:`BugDir` for storing a collection of bugs.
32 try: # import core module, Python >= 2.5
33 from xml.etree import ElementTree
34 except ImportError: # look for non-core module
35 from elementtree import ElementTree
36 import xml.sax.saxutils
39 import libbe.storage as storage
40 from libbe.storage.util.properties import Property, doc_property, \
41 local_property, defaulting_property, checked_property, \
42 fn_checked_property, cached_property, primed_property, \
43 change_hook_property, settings_property
44 import libbe.storage.util.settings_object as settings_object
45 import libbe.storage.util.mapfile as mapfile
46 import libbe.bug as bug
47 import libbe.util.utility as utility
50 if libbe.TESTING == True:
55 import libbe.storage.base
58 class NoBugMatches(libbe.util.id.NoIDMatches):
59 def __init__(self, *args, **kwargs):
60 libbe.util.id.NoIDMatches.__init__(self, *args, **kwargs)
63 return 'No bug matches %s' % self.id
67 class BugDir (list, settings_object.SavedSettingsObject):
68 """A BugDir is a container for :py:class:`~libbe.bug.Bug`\s, with some
69 additional attributes.
73 storage : :py:class:`~libbe.storage.base.Storage`
74 Storage instance containing the bug directory. If
75 `from_storage` is `False`, `storage` may be `None`.
77 Set the bugdir UUID (see :py:mod:`libbe.util.id`).
78 Useful if you are loading one of several bugdirs
79 stored in a single Storage instance.
80 from_storage : bool, optional
81 If `True`, attempt to load from storage. Otherwise,
82 setup in memory, saving to `storage` if it is not `None`.
86 SimpleBugDir : bugdir manipulation exampes.
89 settings_properties = []
90 required_saved_properties = []
91 _prop_save_settings = settings_object.prop_save_settings
92 _prop_load_settings = settings_object.prop_load_settings
93 def _versioned_property(settings_properties=settings_properties,
94 required_saved_properties=required_saved_properties,
96 if "settings_properties" not in kwargs:
97 kwargs["settings_properties"] = settings_properties
98 if "required_saved_properties" not in kwargs:
99 kwargs["required_saved_properties"]=required_saved_properties
100 return settings_object.versioned_property(**kwargs)
102 @_versioned_property(name="target",
103 doc="The current project development target.")
104 def target(): return {}
106 def _setup_severities(self, severities):
107 if severities not in [None, settings_object.EMPTY]:
108 bug.load_severities(severities)
109 def _set_severities(self, old_severities, new_severities):
110 self._setup_severities(new_severities)
111 self._prop_save_settings(old_severities, new_severities)
112 @_versioned_property(name="severities",
113 doc="The allowed bug severities and their descriptions.",
114 change_hook=_set_severities)
115 def severities(): return {}
117 def _setup_status(self, active_status, inactive_status):
118 bug.load_status(active_status, inactive_status)
119 def _set_active_status(self, old_active_status, new_active_status):
120 self._setup_status(new_active_status, self.inactive_status)
121 self._prop_save_settings(old_active_status, new_active_status)
122 @_versioned_property(name="active_status",
123 doc="The allowed active bug states and their descriptions.",
124 change_hook=_set_active_status)
125 def active_status(): return {}
127 def _set_inactive_status(self, old_inactive_status, new_inactive_status):
128 self._setup_status(self.active_status, new_inactive_status)
129 self._prop_save_settings(old_inactive_status, new_inactive_status)
130 @_versioned_property(name="inactive_status",
131 doc="The allowed inactive bug states and their descriptions.",
132 change_hook=_set_inactive_status)
133 def inactive_status(): return {}
135 def _extra_strings_check_fn(value):
136 return utility.iterable_full_of_strings(value, \
137 alternative=settings_object.EMPTY)
138 def _extra_strings_change_hook(self, old, new):
139 self.extra_strings.sort() # to make merging easier
140 self._prop_save_settings(old, new)
141 @_versioned_property(name="extra_strings",
142 doc="Space for an array of extra strings. Useful for storing state for functionality implemented purely in becommands/<some_function>.py.",
144 check_fn=_extra_strings_check_fn,
145 change_hook=_extra_strings_change_hook,
147 def extra_strings(): return {}
149 def _bug_map_gen(self):
153 for uuid in self.uuids():
156 self._bug_map_value = map # ._bug_map_value used by @local_property
159 @primed_property(primer=_bug_map_gen)
160 @local_property("bug_map")
161 @doc_property(doc="A dict of (bug-uuid, bug-instance) pairs.")
162 def _bug_map(): return {}
164 def __init__(self, storage, uuid=None, from_storage=False):
166 settings_object.SavedSettingsObject.__init__(self)
167 self.storage = storage
168 self.id = libbe.util.id.ID(self, 'bugdir')
170 if from_storage == True:
171 if self.uuid == None:
172 self.uuid = [c for c in self.storage.children()
173 if c != 'version'][0]
176 if self.uuid == None:
177 self.uuid = libbe.util.id.uuid_gen()
178 if self.storage != None and self.storage.is_writeable():
181 # methods for saving/loading/accessing settings and properties.
183 def load_settings(self, settings_mapfile=None):
184 if settings_mapfile == None:
186 self.storage.get(self.id.storage('settings'), default='{}\n')
188 settings = mapfile.parse(settings_mapfile)
189 except mapfile.InvalidMapfileContents, e:
190 raise Exception('Invalid settings file for bugdir %s\n'
191 '(BE version missmatch?)' % self.id.user())
192 self._setup_saved_settings(settings)
193 self._setup_severities(self.severities)
194 self._setup_status(self.active_status, self.inactive_status)
196 def save_settings(self):
197 mf = mapfile.generate(self._get_saved_settings())
198 self.storage.set(self.id.storage('settings'), mf)
200 def load_all_bugs(self):
202 Warning: this could take a while.
205 for uuid in self.uuids():
210 Save any loaded contents to storage. Because of lazy loading
211 of bugs and comments, this is actually not too inefficient.
213 However, if self.storage.is_writeable() == True, then any
214 changes are automatically written to storage as soon as they
215 happen, so calling this method will just waste time (unless
216 something else has been messing with your stored files).
218 self.storage.add(self.id.storage(), directory=True)
219 self.storage.add(self.id.storage('settings'), parent=self.id.storage(),
224 bug.storage = self.storage
227 # methods for managing bugs
229 def uuids(self, use_cached_disk_uuids=True):
230 if use_cached_disk_uuids==False or not hasattr(self, '_uuids_cache'):
231 self._refresh_uuid_cache()
232 self._uuids_cache = self._uuids_cache.union([bug.uuid for bug in self])
233 return self._uuids_cache
235 def _refresh_uuid_cache(self):
236 self._uuids_cache = set()
237 # list bugs that are in storage
238 if self.storage != None and self.storage.is_readable():
239 child_uuids = libbe.util.id.child_uuids(
240 self.storage.children(self.id.storage()))
241 for id in child_uuids:
242 self._uuids_cache.add(id)
244 def _clear_bugs(self):
247 if hasattr(self, '_uuids_cache'):
248 del(self._uuids_cache)
251 def _load_bug(self, uuid):
252 bg = bug.Bug(bugdir=self, uuid=uuid, from_storage=True)
257 def new_bug(self, summary=None, _uuid=None):
258 bg = bug.Bug(bugdir=self, uuid=_uuid, summary=summary,
260 self.append(bg, update=True)
263 def append(self, bug, update=False):
264 super(BugDir, self).append(bug)
267 bug.storage = self.storage
269 if (hasattr(self, '_uuids_cache') and
270 not bug.uuid in self._uuids_cache):
271 self._uuids_cache.add(bug.uuid)
273 def remove_bug(self, bug):
274 if hasattr(self, '_uuids_cache') and bug.uuid in self._uuids_cache:
275 self._uuids_cache.remove(bug.uuid)
277 if self.storage != None and self.storage.is_writeable():
280 def bug_from_uuid(self, uuid):
281 if not self.has_bug(uuid):
284 'No bug matches %s in %s' % (uuid, self.storage))
285 if self._bug_map[uuid] == None:
287 return self._bug_map[uuid]
289 def has_bug(self, bug_uuid):
290 if bug_uuid not in self._bug_map:
292 if bug_uuid not in self._bug_map:
296 def xml(self, indent=0, show_bugs=False, show_comments=False):
298 >>> bug.load_severities(bug.severity_def)
300 ... active_status_def=bug.active_status_def,
301 ... inactive_status_def=bug.inactive_status_def)
302 >>> bugdirA = SimpleBugDir(memory=True)
303 >>> bugdirA.severities
304 >>> bugdirA.severities = (('minor', 'The standard bug level.'),)
305 >>> bugdirA.inactive_status = (
306 ... ('closed', 'The bug is no longer relevant.'),)
307 >>> bugA = bugdirA.bug_from_uuid('a')
308 >>> commA = bugA.comment_root.new_reply(body='comment A')
309 >>> commA.uuid = 'commA'
310 >>> commA.date = 'Thu, 01 Jan 1970 00:03:00 +0000'
311 >>> print(bugdirA.xml(show_bugs=True, show_comments=True))
312 ... # doctest: +REPORT_UDIFF
315 <short-name>abc</short-name>
319 <value>The standard bug level.</value>
325 <value>The bug is no longer relevant.</value>
330 <short-name>abc/a</short-name>
331 <severity>minor</severity>
332 <status>open</status>
333 <creator>John Doe <jdoe@example.com></creator>
334 <created>Thu, 01 Jan 1970 00:00:00 +0000</created>
335 <summary>Bug A</summary>
338 <short-name>abc/a/com</short-name>
340 <date>Thu, 01 Jan 1970 00:03:00 +0000</date>
341 <content-type>text/plain</content-type>
342 <body>comment A</body>
347 <short-name>abc/b</short-name>
348 <severity>minor</severity>
349 <status>closed</status>
350 <creator>Jane Doe <jdoe@example.com></creator>
351 <created>Thu, 01 Jan 1970 00:00:00 +0000</created>
352 <summary>Bug B</summary>
355 >>> bug.load_severities(bug.severity_def)
357 ... active_status_def=bug.active_status_def,
358 ... inactive_status_def=bug.inactive_status_def)
359 >>> bugdirA.cleanup()
361 info = [('uuid', self.uuid),
362 ('short-name', self.id.user()),
363 ('target', self.target),
364 ('severities', self.severities),
365 ('active-status', self.active_status),
366 ('inactive-status', self.inactive_status),
371 if k in ['severities', 'active-status', 'inactive-status']:
372 lines.append(' <{}>'.format(k))
376 ' <key>{}</key>'.format(
377 xml.sax.saxutils.escape(vk)),
378 ' <value>{}</value>'.format(
379 xml.sax.saxutils.escape(vv)),
382 lines.append(' </{}>'.format(k))
384 v = xml.sax.saxutils.escape(v)
385 lines.append(' <{0}>{1}</{0}>'.format(k, v))
386 for estr in self.extra_strings:
387 lines.append(' <extra-string>{}</extra-string>'.format(estr))
390 bug_xml = bug.xml(indent=indent+2, show_comments=show_comments)
392 bug_xml = bug_xml[indent:] # strip leading indent spaces
393 lines.append(bug_xml)
394 lines.append('</bugdir>')
397 return istring + sep.join(lines).rstrip('\n')
399 def from_xml(self, xml_string, preserve_uuids=False, verbose=True):
401 Note: If a bugdir uuid is given, set .alt_id to it's value.
402 >>> bug.load_severities(bug.severity_def)
404 ... active_status_def=bug.active_status_def,
405 ... inactive_status_def=bug.inactive_status_def)
406 >>> bugdirA = SimpleBugDir(memory=True)
407 >>> bugdirA.severities = (('minor', 'The standard bug level.'),)
408 >>> bugdirA.inactive_status = (
409 ... ('closed', 'The bug is no longer relevant.'),)
410 >>> bugA = bugdirA.bug_from_uuid('a')
411 >>> commA = bugA.comment_root.new_reply(body='comment A')
412 >>> commA.uuid = 'commA'
413 >>> xml = bugdirA.xml(show_bugs=True, show_comments=True)
414 >>> bugdirB = BugDir(storage=None)
415 >>> bugdirB.from_xml(xml)
416 >>> bugdirB.xml(show_bugs=True, show_comments=True) == xml
418 >>> bugdirB.uuid = bugdirB.alt_id
419 >>> for bug_ in bugdirB:
420 ... bug_.uuid = bug_.alt_id
421 ... bug_.alt_id = None
422 ... for comm in bug_.comments():
423 ... comm.uuid = comm.alt_id
424 ... comm.alt_id = None
425 >>> bugdirB.xml(show_bugs=True, show_comments=True) == xml
427 >>> bugdirB.explicit_attrs # doctest: +NORMALIZE_WHITESPACE
428 ['severities', 'inactive_status']
429 >>> bugdirC = BugDir(storage=None)
430 >>> bugdirC.from_xml(xml, preserve_uuids=True)
431 >>> bugdirC.uuid == bugdirA.uuid
433 >>> bugdirC.xml(show_bugs=True, show_comments=True) == xml
435 >>> bug.load_severities(bug.severity_def)
437 ... active_status_def=bug.active_status_def,
438 ... inactive_status_def=bug.inactive_status_def)
439 >>> bugdirA.cleanup()
441 if type(xml_string) == types.UnicodeType:
442 xml_string = xml_string.strip().encode('unicode_escape')
443 if hasattr(xml_string, 'getchildren'): # already an ElementTree Element
446 bugdir = ElementTree.XML(xml_string)
447 if bugdir.tag != 'bugdir':
448 raise utility.InvalidXML(
449 'bugdir', bugdir, 'root element must be <bugdir>')
450 tags = ['uuid', 'short-name', 'target', 'severities', 'active-status',
451 'inactive-status', 'extra-string']
452 self.explicit_attrs = []
455 for child in bugdir.getchildren():
456 if child.tag == 'short-name':
458 elif child.tag == 'bug':
459 bg = bug.Bug(bugdir=self)
461 child, preserve_uuids=preserve_uuids, verbose=verbose)
462 self.append(bg, update=True)
464 elif child.tag in tags:
465 if child.text == None or len(child.text) == 0:
466 text = settings_object.EMPTY
467 elif child.tag in ['severities', 'active-status',
470 for entry in child.getchildren():
471 if entry.tag != 'entry':
472 raise utility.InvalidXML(
473 '{} child element {} must be <entry>'.format(
476 for kv in entry.getchildren():
479 raise utility.InvalidXML(
480 ('duplicate keys ({} and {}) in {}'
481 ).format(key, kv.text, child.tag))
482 key = xml.sax.saxutils.unescape(kv.text)
483 elif kv.tag == 'value':
484 if value is not None:
485 raise utility.InvalidXML(
486 ('duplicate values ({} and {}) in {}'
488 value, kv.text, child.tag))
489 value = xml.sax.saxutils.unescape(kv.text)
491 raise utility.InvalidXML(
492 ('{} child element {} must be <key> or '
493 '<value>').format(child.tag, kv))
495 raise utility.InvalidXML(
496 'no key for {}'.format(child.tag))
498 raise utility.InvalidXML(
499 'no key for {}'.format(child.tag))
500 entries.append((key, value))
503 text = xml.sax.saxutils.unescape(child.text)
504 if not isinstance(text, unicode):
505 text = text.decode('unicode_escape')
507 if child.tag == 'uuid' and not preserve_uuids:
509 continue # don't set the bug's uuid tag.
510 elif child.tag == 'extra-string':
512 continue # don't set the bug's extra_string yet.
513 attr_name = child.tag.replace('-','_')
514 self.explicit_attrs.append(attr_name)
515 setattr(self, attr_name, text)
516 elif verbose == True:
517 sys.stderr.write('Ignoring unknown tag {} in {}\n'.format(
518 child.tag, bugdir.tag))
519 if uuid != self.uuid:
520 if not hasattr(self, 'alt_id') or self.alt_id == None:
522 self.extra_strings = estrs
524 def merge(self, other, accept_changes=True,
525 accept_extra_strings=True, accept_bugs=True,
526 accept_comments=True, change_exception=False):
527 """Merge info from other into this bugdir.
529 Overrides any attributes in self that are listed in
530 other.explicit_attrs.
532 >>> bugdirA = SimpleBugDir()
533 >>> bugdirA.extra_strings += ['TAG: favorite']
534 >>> bugdirB = SimpleBugDir()
535 >>> bugdirB.explicit_attrs = ['target']
536 >>> bugdirB.target = '1234'
537 >>> bugdirB.extra_strings += ['TAG: very helpful']
538 >>> bugdirB.extra_strings += ['TAG: useful']
539 >>> bugA = bugdirB.bug_from_uuid('a')
540 >>> commA = bugA.comment_root.new_reply(body='comment A')
541 >>> commA.uuid = 'uuid-commA'
542 >>> commA.date = 'Thu, 01 Jan 1970 00:01:00 +0000'
543 >>> bugC = bugdirB.new_bug(summary='bug C', _uuid='c')
544 >>> bugC.alt_id = 'alt-c'
545 >>> bugC.time_string = 'Thu, 01 Jan 1970 00:02:00 +0000'
547 ... bugdirB, accept_changes=False, accept_extra_strings=False,
548 ... accept_bugs=False, change_exception=False)
549 >>> print(bugdirA.target)
552 ... bugdirB, accept_changes=False, accept_extra_strings=False,
553 ... accept_bugs=False, change_exception=True)
554 Traceback (most recent call last):
556 ValueError: Merge would change target "None"->"1234" for bugdir abc123
557 >>> print(bugdirA.target)
560 ... bugdirB, accept_changes=True, accept_extra_strings=False,
561 ... accept_bugs=False, change_exception=True)
562 Traceback (most recent call last):
564 ValueError: Merge would add extra string "TAG: useful" for bugdir abc123
565 >>> print(bugdirA.target)
567 >>> print(bugdirA.extra_strings)
570 ... bugdirB, accept_changes=True, accept_extra_strings=True,
571 ... accept_bugs=False, change_exception=True)
572 Traceback (most recent call last):
574 ValueError: Merge would add bug c (alt: alt-c) to bugdir abc123
575 >>> print(bugdirA.extra_strings)
576 ['TAG: favorite', 'TAG: useful', 'TAG: very helpful']
578 ... bugdirB, accept_changes=True, accept_extra_strings=True,
579 ... accept_bugs=True, change_exception=True)
580 >>> print(bugdirA.xml(show_bugs=True, show_comments=True))
581 ... # doctest: +ELLIPSIS, +REPORT_UDIFF
584 <short-name>abc</short-name>
585 <target>1234</target>
586 <extra-string>TAG: favorite</extra-string>
587 <extra-string>TAG: useful</extra-string>
588 <extra-string>TAG: very helpful</extra-string>
591 <short-name>abc/a</short-name>
592 <severity>minor</severity>
593 <status>open</status>
594 <creator>John Doe <jdoe@example.com></creator>
595 <created>Thu, 01 Jan 1970 00:00:00 +0000</created>
596 <summary>Bug A</summary>
598 <uuid>uuid-commA</uuid>
599 <short-name>abc/a/uui</short-name>
601 <date>Thu, 01 Jan 1970 00:01:00 +0000</date>
602 <content-type>text/plain</content-type>
603 <body>comment A</body>
608 <short-name>abc/b</short-name>
609 <severity>minor</severity>
610 <status>closed</status>
611 <creator>Jane Doe <jdoe@example.com></creator>
612 <created>Thu, 01 Jan 1970 00:00:00 +0000</created>
613 <summary>Bug B</summary>
617 <short-name>abc/c</short-name>
618 <severity>minor</severity>
619 <status>open</status>
620 <created>Thu, 01 Jan 1970 00:02:00 +0000</created>
621 <summary>bug C</summary>
624 >>> bugdirA.cleanup()
625 >>> bugdirB.cleanup()
627 if hasattr(other, 'explicit_attrs'):
628 for attr in other.explicit_attrs:
629 old = getattr(self, attr)
630 new = getattr(other, attr)
633 setattr(self, attr, new)
634 elif change_exception:
636 ('Merge would change {} "{}"->"{}" for bugdir {}'
637 ).format(attr, old, new, self.uuid))
638 for estr in other.extra_strings:
639 if not estr in self.extra_strings:
640 if accept_extra_strings:
641 self.extra_strings += [estr]
642 elif change_exception:
644 ('Merge would add extra string "{}" for bugdir {}'
645 ).format(estr, self.uuid))
648 s_bug = self.bug_from_uuid(o_bug.uuid)
649 except KeyError as e:
651 s_bug = self.bug_from_uuid(o_bug.alt_id)
652 except KeyError as e:
656 o_bug_copy = copy.copy(o_bug)
657 o_bug_copy.bugdir = self
658 o_bug_copy.id = libbe.util.id.ID(o_bug_copy, 'bug')
659 self.append(o_bug_copy)
660 elif change_exception:
662 ('Merge would add bug {} (alt: {}) to bugdir {}'
663 ).format(o_bug.uuid, o_bug.alt_id, self.uuid))
665 s_bug.merge(o_bug, accept_changes=accept_changes,
666 accept_extra_strings=accept_extra_strings,
667 change_exception=change_exception)
669 # methods for id generation
671 def sibling_uuids(self):
674 class RevisionedBugDir (BugDir):
676 RevisionedBugDirs are read-only copies used for generating
677 diffs between revisions.
679 def __init__(self, bugdir, revision):
680 storage_version = bugdir.storage.storage_version(revision)
681 if storage_version != libbe.storage.STORAGE_VERSION:
682 raise libbe.storage.InvalidStorageVersion(storage_version)
683 s = copy.deepcopy(bugdir.storage)
685 class RevisionedStorage (object):
686 def __init__(self, storage, default_revision):
688 self.sget = self.s.get
689 self.sancestors = self.s.ancestors
690 self.schildren = self.s.children
691 self.schanged = self.s.changed
692 self.r = default_revision
693 def get(self, *args, **kwargs):
694 if not 'revision' in kwargs or kwargs['revision'] == None:
695 kwargs['revision'] = self.r
696 return self.sget(*args, **kwargs)
697 def ancestors(self, *args, **kwargs):
698 print 'getting ancestors', args, kwargs
699 if not 'revision' in kwargs or kwargs['revision'] == None:
700 kwargs['revision'] = self.r
701 ret = self.sancestors(*args, **kwargs)
702 print 'got ancestors', ret
704 def children(self, *args, **kwargs):
705 if not 'revision' in kwargs or kwargs['revision'] == None:
706 kwargs['revision'] = self.r
707 return self.schildren(*args, **kwargs)
708 def changed(self, *args, **kwargs):
709 if not 'revision' in kwargs or kwargs['revision'] == None:
710 kwargs['revision'] = self.r
711 return self.schanged(*args, **kwargs)
712 rs = RevisionedStorage(s, revision)
714 s.ancestors = rs.ancestors
715 s.children = rs.children
716 s.changed = rs.changed
717 BugDir.__init__(self, s, from_storage=True)
718 self.revision = revision
720 return self.storage.changed()
723 if libbe.TESTING == True:
724 class SimpleBugDir (BugDir):
726 For testing. Set ``memory=True`` for a memory-only bugdir.
728 >>> bugdir = SimpleBugDir()
729 >>> uuids = list(bugdir.uuids())
735 def __init__(self, memory=True, versioned=False):
740 self._dir_ref = dir # postpone cleanup since dir.cleanup() removes dir.
741 if versioned == False:
742 storage = libbe.storage.base.Storage(dir.path)
744 storage = libbe.storage.base.VersionedStorage(dir.path)
747 BugDir.__init__(self, storage=storage, uuid='abc123')
748 bug_a = self.new_bug(summary='Bug A', _uuid='a')
749 bug_a.creator = 'John Doe <jdoe@example.com>'
751 bug_b = self.new_bug(summary='Bug B', _uuid='b')
752 bug_b.creator = 'Jane Doe <jdoe@example.com>'
754 bug_b.status = 'closed'
755 if self.storage != None:
756 self.storage.disconnect() # flush to storage
757 self.storage.connect()
760 if self.storage != None:
761 self.storage.writeable = True
762 self.storage.disconnect()
763 self.storage.destroy()
764 if hasattr(self, '_dir_ref'):
765 self._dir_ref.cleanup()
767 def flush_reload(self):
768 if self.storage != None:
769 self.storage.disconnect()
770 self.storage.connect()
773 # class BugDirTestCase(unittest.TestCase):
775 # self.dir = utility.Dir()
776 # self.bugdir = BugDir(self.dir.path, sink_to_existing_root=False,
777 # allow_storage_init=True)
778 # self.storage = self.bugdir.storage
779 # def tearDown(self):
780 # self.bugdir.cleanup()
782 # def fullPath(self, path):
783 # return os.path.join(self.dir.path, path)
784 # def assertPathExists(self, path):
785 # fullpath = self.fullPath(path)
786 # self.failUnless(os.path.exists(fullpath)==True,
787 # "path %s does not exist" % fullpath)
788 # self.assertRaises(AlreadyInitialized, BugDir,
789 # self.dir.path, assertNewBugDir=True)
790 # def versionTest(self):
791 # if self.storage != None and self.storage.versioned == False:
793 # original = self.bugdir.storage.commit("Began versioning")
794 # bugA = self.bugdir.bug_from_uuid("a")
795 # bugA.status = "fixed"
797 # new = self.storage.commit("Fixed bug a")
798 # dupdir = self.bugdir.duplicate_bugdir(original)
799 # self.failUnless(dupdir.root != self.bugdir.root,
800 # "%s, %s" % (dupdir.root, self.bugdir.root))
801 # bugAorig = dupdir.bug_from_uuid("a")
802 # self.failUnless(bugA != bugAorig,
803 # "\n%s\n%s" % (bugA.string(), bugAorig.string()))
804 # bugAorig.status = "fixed"
805 # self.failUnless(bug.cmp_status(bugA, bugAorig)==0,
806 # "%s, %s" % (bugA.status, bugAorig.status))
807 # self.failUnless(bug.cmp_severity(bugA, bugAorig)==0,
808 # "%s, %s" % (bugA.severity, bugAorig.severity))
809 # self.failUnless(bug.cmp_assigned(bugA, bugAorig)==0,
810 # "%s, %s" % (bugA.assigned, bugAorig.assigned))
811 # self.failUnless(bug.cmp_time(bugA, bugAorig)==0,
812 # "%s, %s" % (bugA.time, bugAorig.time))
813 # self.failUnless(bug.cmp_creator(bugA, bugAorig)==0,
814 # "%s, %s" % (bugA.creator, bugAorig.creator))
815 # self.failUnless(bugA == bugAorig,
816 # "\n%s\n%s" % (bugA.string(), bugAorig.string()))
817 # self.bugdir.remove_duplicate_bugdir()
818 # self.failUnless(os.path.exists(dupdir.root)==False,
821 # self.bugdir.new_bug(uuid="a", summary="Ant")
822 # self.bugdir.new_bug(uuid="b", summary="Cockroach")
823 # self.bugdir.new_bug(uuid="c", summary="Praying mantis")
824 # length = len(self.bugdir)
825 # self.failUnless(length == 3, "%d != 3 bugs" % length)
826 # uuids = list(self.bugdir.uuids())
827 # self.failUnless(len(uuids) == 3, "%d != 3 uuids" % len(uuids))
828 # self.failUnless(uuids == ["a","b","c"], str(uuids))
829 # bugA = self.bugdir.bug_from_uuid("a")
830 # bugAprime = self.bugdir.bug_from_shortname("a")
831 # self.failUnless(bugA == bugAprime, "%s != %s" % (bugA, bugAprime))
834 # def testComments(self, sync_with_disk=False):
835 # if sync_with_disk == True:
836 # self.bugdir.set_sync_with_disk(True)
837 # self.bugdir.new_bug(uuid="a", summary="Ant")
838 # bug = self.bugdir.bug_from_uuid("a")
839 # comm = bug.comment_root
840 # rep = comm.new_reply("Ants are small.")
841 # rep.new_reply("And they have six legs.")
842 # if sync_with_disk == False:
844 # self.bugdir.set_sync_with_disk(True)
845 # self.bugdir._clear_bugs()
846 # bug = self.bugdir.bug_from_uuid("a")
847 # bug.load_comments()
848 # if sync_with_disk == False:
849 # self.bugdir.set_sync_with_disk(False)
850 # self.failUnless(len(bug.comment_root)==1, len(bug.comment_root))
851 # for index,comment in enumerate(bug.comments()):
853 # repLoaded = comment
854 # self.failUnless(repLoaded.uuid == rep.uuid, repLoaded.uuid)
855 # self.failUnless(comment.sync_with_disk == sync_with_disk,
856 # comment.sync_with_disk)
857 # self.failUnless(comment.content_type == "text/plain",
858 # comment.content_type)
859 # self.failUnless(repLoaded.settings["Content-type"] == \
861 # repLoaded.settings)
862 # self.failUnless(repLoaded.body == "Ants are small.",
865 # self.failUnless(comment.in_reply_to == repLoaded.uuid,
867 # self.failUnless(comment.body == "And they have six legs.",
871 # "Invalid comment: %d\n%s" % (index, comment))
872 # def testSyncedComments(self):
873 # self.testComments(sync_with_disk=True)
875 class SimpleBugDirTestCase (unittest.TestCase):
877 # create a pre-existing bugdir in a temporary directory
878 self.dir = utility.Dir()
879 self.storage = libbe.storage.base.Storage(self.dir.path)
881 self.storage.connect()
882 self.bugdir = BugDir(self.storage)
883 self.bugdir.new_bug(summary="Hopefully not imported",
885 self.storage.disconnect()
886 self.storage.connect()
888 if self.storage != None:
889 self.storage.disconnect()
890 self.storage.destroy()
892 def testOnDiskCleanLoad(self):
894 SimpleBugDir(memory==False) should not import
897 bugdir = SimpleBugDir(memory=False)
898 self.failUnless(bugdir.storage.is_readable() == True,
899 bugdir.storage.is_readable())
900 self.failUnless(bugdir.storage.is_writeable() == True,
901 bugdir.storage.is_writeable())
902 uuids = sorted([bug.uuid for bug in bugdir])
903 self.failUnless(uuids == ['a', 'b'], uuids)
904 bugdir.flush_reload()
905 uuids = sorted(bugdir.uuids())
906 self.failUnless(uuids == ['a', 'b'], uuids)
907 uuids = sorted([bug.uuid for bug in bugdir])
908 self.failUnless(uuids == [], uuids)
909 bugdir.load_all_bugs()
910 uuids = sorted([bug.uuid for bug in bugdir])
911 self.failUnless(uuids == ['a', 'b'], uuids)
913 def testInMemoryCleanLoad(self):
915 SimpleBugDir(memory==True) should not import
918 bugdir = SimpleBugDir(memory=True)
919 self.failUnless(bugdir.storage == None, bugdir.storage)
920 uuids = sorted([bug.uuid for bug in bugdir])
921 self.failUnless(uuids == ['a', 'b'], uuids)
922 uuids = sorted([bug.uuid for bug in bugdir])
923 self.failUnless(uuids == ['a', 'b'], uuids)
925 uuids = sorted(bugdir.uuids())
926 self.failUnless(uuids == [], uuids)
927 uuids = sorted([bug.uuid for bug in bugdir])
928 self.failUnless(uuids == [], uuids)
931 unitsuite =unittest.TestLoader().loadTestsFromModule(sys.modules[__name__])
932 suite = unittest.TestSuite([unitsuite, doctest.DocTestSuite()])
934 # def _get_settings(self, settings_path, for_duplicate_bugdir=False):
935 # allow_no_storage = not self.storage.path_in_root(settings_path)
936 # if allow_no_storage == True:
937 # assert for_duplicate_bugdir == True
938 # if self.sync_with_disk == False and for_duplicate_bugdir == False:
939 # # duplicates can ignore this bugdir's .sync_with_disk status
940 # raise DiskAccessRequired("_get settings")
942 # settings = mapfile.map_load(self.storage, settings_path, allow_no_storage)
943 # except storage.NoSuchFile:
944 # settings = {"storage_name": "None"}
947 # def _save_settings(self, settings_path, settings,
948 # for_duplicate_bugdir=False):
949 # allow_no_storage = not self.storage.path_in_root(settings_path)
950 # if allow_no_storage == True:
951 # assert for_duplicate_bugdir == True
952 # if self.sync_with_disk == False and for_duplicate_bugdir == False:
953 # # duplicates can ignore this bugdir's .sync_with_disk status
954 # raise DiskAccessRequired("_save settings")
955 # self.storage.mkdir(self.get_path(), allow_no_storage)
956 # mapfile.map_save(self.storage, settings_path, settings, allow_no_storage)