Created and fixed bug 496edad5-1484-413a-bc68-4b01274a65eb.
[be.git] / libbe / bug.py
1 # Copyright (C) 2005 Aaron Bentley and Panometrics, Inc.
2 # <abentley@panoramicfeedback.com>
3 #
4 #    This program is free software; you can redistribute it and/or modify
5 #    it under the terms of the GNU General Public License as published by
6 #    the Free Software Foundation; either version 2 of the License, or
7 #    (at your option) any later version.
8 #
9 #    This program is distributed in the hope that it will be useful,
10 #    but WITHOUT ANY WARRANTY; without even the implied warranty of
11 #    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12 #    GNU General Public License for more details.
13 #
14 #    You should have received a copy of the GNU General Public License
15 #    along with this program; if not, write to the Free Software
16 #    Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
17 import os
18 import os.path
19 import errno
20 import time
21 import doctest
22
23 from beuuid import uuid_gen
24 import mapfile
25 import comment
26 import utility
27
28
29 ### Define and describe valid bug categories
30 # Use a tuple of (category, description) tuples since we don't have
31 # ordered dicts in Python yet http://www.python.org/dev/peps/pep-0372/
32
33 # in order of increasing severity
34 severity_level_def = (
35   ("wishlist","A feature that could improve usefullness, but not a bug."),
36   ("minor","The standard bug level."),
37   ("serious","A bug that requires workarounds."),
38   ("critical","A bug that prevents some features from working at all."),
39   ("fatal","A bug that makes the package unusable."))
40
41 # in order of increasing resolution
42 # roughly following http://www.bugzilla.org/docs/3.2/en/html/lifecycle.html
43 active_status_def = (
44   ("unconfirmed","A possible bug which lacks independent existance confirmation."),
45   ("open","A working bug that has not been assigned to a developer."),
46   ("assigned","A working bug that has been assigned to a developer."),
47   ("test","The code has been adjusted, but the fix is still being tested."))
48 inactive_status_def = (
49   ("closed", "The bug is no longer relevant."),
50   ("fixed", "The bug should no longer occur."),
51   ("wontfix","It's not a bug, it's a feature."),
52   ("disabled", "?"))
53
54
55 ### Convert the description tuples to more useful formats
56
57 severity_values = tuple([val for val,description in severity_level_def])
58 severity_description = dict(severity_level_def)
59 severity_index = {}
60 for i in range(len(severity_values)):
61     severity_index[severity_values[i]] = i
62
63 active_status_values = tuple(val for val,description in active_status_def)
64 inactive_status_values = tuple(val for val,description in inactive_status_def)
65 status_values = active_status_values + inactive_status_values
66 status_description = dict(active_status_def+inactive_status_def)
67 status_index = {}
68 for i in range(len(status_values)):
69     status_index[status_values[i]] = i
70
71
72 def checked_property(name, valid):
73     """
74     Provide access to an attribute name, testing for valid values.
75     """
76     def getter(self):
77         value = getattr(self, "_"+name)
78         if value not in valid:
79             raise InvalidValue(name, value)
80         return value
81
82     def setter(self, value):
83         if value not in valid:
84             raise InvalidValue(name, value)
85         return setattr(self, "_"+name, value)
86     return property(getter, setter)
87
88
89 class Bug(object):
90     severity = checked_property("severity", severity_values)
91     status = checked_property("status", status_values)
92
93     def _get_active(self):
94         return self.status in active_status_values
95
96     active = property(_get_active)
97
98     def __init__(self, bugdir=None, uuid=None, loadNow=False, summary=None):
99         self.bugdir = bugdir
100         if bugdir != None:
101             self.rcs = bugdir.rcs
102         else:
103             self.rcs = None
104         if loadNow == True:
105             self.uuid = uuid
106             self.load()
107         else:
108             # Note: defaults should match those in Bug.load()
109             if uuid != None:
110                 self.uuid = uuid
111             else:
112                 self.uuid = uuid_gen()
113             self.summary = summary
114             if self.rcs != None:
115                 self.creator = self.rcs.get_user_id()
116             else:
117                 self.creator = None
118             self.target = None
119             self.status = "open"
120             self.severity = "minor"
121             self.assigned = None
122             self.time = int(time.time()) # only save to second precision
123             self.comment_root = comment.Comment(self, uuid=comment.INVALID_UUID)
124
125     def __repr__(self):
126         return "Bug(uuid=%r)" % self.uuid
127
128     def string(self, shortlist=False, show_comments=False):
129         if self.bugdir == None:
130             shortname = self.uuid
131         else:
132             shortname = self.bugdir.bug_shortname(self)
133         if shortlist == False:
134             if self.time == None:
135                 timestring = ""
136             else:
137                 htime = utility.handy_time(self.time)
138                 ftime = utility.time_to_str(self.time)
139                 timestring = "%s (%s)" % (htime, ftime)
140             info = [("ID", self.uuid),
141                     ("Short name", shortname),
142                     ("Severity", self.severity),
143                     ("Status", self.status),
144                     ("Assigned", self.assigned),
145                     ("Target", self.target),
146                     ("Creator", self.creator),
147                     ("Created", timestring)]
148             newinfo = []
149             for k,v in info:
150                 if v == None:
151                     newinfo.append((k,""))
152                 else:
153                     newinfo.append((k,v))
154             info = newinfo
155             longest_key_len = max([len(k) for k,v in info])
156             infolines = ["  %*s : %s\n" %(longest_key_len,k,v) for k,v in info]
157             bugout = "".join(infolines) + "%s" % self.summary.rstrip('\n')
158         else:
159             statuschar = self.status[0]
160             severitychar = self.severity[0]
161             chars = "%c%c" % (statuschar, severitychar)
162             bugout = "%s:%s: %s" % (shortname, chars, self.summary.rstrip('\n'))
163         
164         if show_comments == True:
165             comout = self.comment_root.string_thread(auto_name_map=True,
166                                                      bug_shortname=shortname)
167             output = bugout + '\n' + comout.rstrip('\n')
168         else :
169             output = bugout
170         return output
171
172     def __str__(self):
173         return self.string(shortlist=True)
174
175     def __cmp__(self, other):
176         return cmp_full(self, other)
177
178     def get_path(self, name=None):
179         my_dir = os.path.join(self.bugdir.get_path("bugs"), self.uuid)
180         if name is None:
181             return my_dir
182         assert name in ["values", "comments"]
183         return os.path.join(my_dir, name)
184
185     def load(self):
186         map = mapfile.map_load(self.get_path("values"))
187         self.summary = map.get("summary")
188         self.creator = map.get("creator")
189         self.target = map.get("target")
190         self.status = map.get("status", "open")
191         self.severity = map.get("severity", "minor")
192         self.assigned = map.get("assigned")
193         self.time = map.get("time")
194         if self.time is not None:
195             self.time = utility.str_to_time(self.time)
196         
197         self.comment_root = comment.loadComments(self)
198
199     def _add_attr(self, map, name):
200         value = getattr(self, name)
201         if value is not None:
202             map[name] = value
203
204     def save(self):
205         assert self.summary != None, "Can't save blank bug"
206         map = {}
207         self._add_attr(map, "assigned")
208         self._add_attr(map, "summary")
209         self._add_attr(map, "creator")
210         self._add_attr(map, "target")
211         self._add_attr(map, "status")
212         self._add_attr(map, "severity")
213         if self.time is not None:
214             map["time"] = utility.time_to_str(self.time)
215
216         self.rcs.mkdir(self.get_path())
217         path = self.get_path("values")
218         mapfile.map_save(self.rcs, path, map)
219
220         if len(self.comment_root) > 0:
221             self.rcs.mkdir(self.get_path("comments"))
222             comment.saveComments(self)
223
224     def remove(self):
225         self.comment_root.remove()
226         path = self.get_path()
227         self.rcs.recursive_remove(path)
228     
229     def new_comment(self, body=None):
230         comm = comment.comment_root.new_reply(body=body)
231         return comm
232
233     def comment_from_shortname(self, shortname, *args, **kwargs):
234         return self.comment_root.comment_from_shortname(shortname, *args, **kwargs)
235
236     def comment_from_uuid(self, uuid):
237         return self.comment_root.comment_from_uuid(uuid)
238
239
240 # the general rule for bug sorting is that "more important" bugs are
241 # less than "less important" bugs.  This way sorting a list of bugs
242 # will put the most important bugs first in the list.  When relative
243 # importance is unclear, the sorting follows some arbitrary convention
244 # (i.e. dictionary order).
245
246 def cmp_severity(bug_1, bug_2):
247     """
248     Compare the severity levels of two bugs, with more severe bugs
249     comparing as less.
250     >>> bugA = Bug()
251     >>> bugB = Bug()
252     >>> bugA.severity = bugB.severity = "wishlist"
253     >>> cmp_severity(bugA, bugB) == 0
254     True
255     >>> bugB.severity = "minor"
256     >>> cmp_severity(bugA, bugB) > 0
257     True
258     >>> bugA.severity = "critical"
259     >>> cmp_severity(bugA, bugB) < 0
260     True
261     """
262     if not hasattr(bug_2, "severity") :
263         return 1
264     return -cmp(severity_index[bug_1.severity], severity_index[bug_2.severity])
265
266 def cmp_status(bug_1, bug_2):
267     """
268     Compare the status levels of two bugs, with more 'open' bugs
269     comparing as less.
270     >>> bugA = Bug()
271     >>> bugB = Bug()
272     >>> bugA.status = bugB.status = "open"
273     >>> cmp_status(bugA, bugB) == 0
274     True
275     >>> bugB.status = "closed"
276     >>> cmp_status(bugA, bugB) < 0
277     True
278     >>> bugA.status = "fixed"
279     >>> cmp_status(bugA, bugB) > 0
280     True
281     """
282     if not hasattr(bug_2, "status") :
283         return 1
284     val_2 = status_index[bug_2.status]
285     return cmp(status_index[bug_1.status], status_index[bug_2.status])
286
287 def cmp_attr(bug_1, bug_2, attr, invert=False):
288     """
289     Compare a general attribute between two bugs using the conventional
290     comparison rule for that attribute type.  If invert == True, sort
291     *against* that convention.
292     >>> attr="severity"
293     >>> bugA = Bug()
294     >>> bugB = Bug()
295     >>> bugA.severity = "critical"
296     >>> bugB.severity = "wishlist"
297     >>> cmp_attr(bugA, bugB, attr) < 0
298     True
299     >>> cmp_attr(bugA, bugB, attr, invert=True) > 0
300     True
301     >>> bugB.severity = "critical"
302     >>> cmp_attr(bugA, bugB, attr) == 0
303     True
304     """
305     if not hasattr(bug_2, attr) :
306         return 1
307     if invert == True :
308         return -cmp(getattr(bug_1, attr), getattr(bug_2, attr))
309     else :
310         return cmp(getattr(bug_1, attr), getattr(bug_2, attr))
311
312 # alphabetical rankings (a < z)
313 cmp_creator = lambda bug_1, bug_2 : cmp_attr(bug_1, bug_2, "creator")
314 cmp_assigned = lambda bug_1, bug_2 : cmp_attr(bug_1, bug_2, "assigned")
315 # chronological rankings (newer < older)
316 cmp_time = lambda bug_1, bug_2 : cmp_attr(bug_1, bug_2, "time", invert=True)
317
318 def cmp_full(bug_1, bug_2, cmp_list=(cmp_status,cmp_severity,cmp_assigned,
319                                      cmp_time,cmp_creator)):
320     for comparison in cmp_list :
321         val = comparison(bug_1, bug_2)
322         if val != 0 :
323             return val
324     return 0
325
326 class InvalidValue(ValueError):
327     def __init__(self, name, value):
328         msg = "Cannot assign value %s to %s" % (value, name)
329         Exception.__init__(self, msg)
330         self.name = name
331         self.value = value
332
333 suite = doctest.DocTestSuite()