VirtualBox

source: vbox/trunk/src/VBox/ValidationKit/testmanager/core/failurereason.py@ 61267

Last change on this file since 61267 was 61250, checked in by vboxsync, 9 years ago

testmanager: Some failure reason improvments.

  • Property svn:eol-style set to native
  • Property svn:keywords set to Author Date Id Revision
File size: 20.6 KB
Line 
1# -*- coding: utf-8 -*-
2# $Id: failurereason.py 61250 2016-05-27 18:00:16Z vboxsync $
3
4"""
5Test Manager - Failure Reasons.
6"""
7
8__copyright__ = \
9"""
10Copyright (C) 2012-2015 Oracle Corporation
11
12This file is part of VirtualBox Open Source Edition (OSE), as
13available from http://www.virtualbox.org. This file is free software;
14you can redistribute it and/or modify it under the terms of the GNU
15General Public License (GPL) as published by the Free Software
16Foundation, in version 2 as it comes in the "COPYING" file of the
17VirtualBox OSE distribution. VirtualBox OSE is distributed in the
18hope that it will be useful, but WITHOUT ANY WARRANTY of any kind.
19
20The contents of this file may alternatively be used under the terms
21of the Common Development and Distribution License Version 1.0
22(CDDL) only, as it comes in the "COPYING.CDDL" file of the
23VirtualBox OSE distribution, in which case the provisions of the
24CDDL are applicable instead of those of the GPL.
25
26You may elect to license modified versions of this file under the
27terms and conditions of either the GPL or the CDDL or both.
28"""
29__version__ = "$Revision: 61250 $"
30
31
32# Validation Kit imports.
33from testmanager.core.base import ModelDataBase, ModelLogicBase, TMRowNotFound, TMInvalidData, TMRowInUse, \
34 AttributeChangeEntry, ChangeLogEntry;
35from testmanager.core.useraccount import UserAccountLogic;
36
37
38
39class FailureReasonData(ModelDataBase):
40 """
41 Failure Reason Data.
42 """
43
44 ksIdAttr = 'idFailureReason';
45
46 ksParam_idFailureReason = 'FailureReasonData_idFailureReason'
47 ksParam_tsEffective = 'FailureReasonData_tsEffective'
48 ksParam_tsExpire = 'FailureReasonData_tsExpire'
49 ksParam_uidAuthor = 'FailureReasonData_uidAuthor'
50 ksParam_idFailureCategory = 'FailureReasonData_idFailureCategory'
51 ksParam_sShort = 'FailureReasonData_sShort'
52 ksParam_sFull = 'FailureReasonData_sFull'
53 ksParam_iTicket = 'FailureReasonData_iTicket'
54 ksParam_asUrls = 'FailureReasonData_asUrls'
55
56 kasAllowNullAttributes = [ 'idFailureReason', 'tsEffective', 'tsExpire',
57 'uidAuthor', 'iTicket', 'asUrls' ]
58
59 def __init__(self):
60 ModelDataBase.__init__(self);
61
62 #
63 # Initialize with defaults.
64 # See the database for explanations of each of these fields.
65 #
66
67 self.idFailureReason = None
68 self.tsEffective = None
69 self.tsExpire = None
70 self.uidAuthor = None
71 self.idFailureCategory = None
72 self.sShort = None
73 self.sFull = None
74 self.iTicket = None
75 self.asUrls = None
76
77 def initFromDbRow(self, aoRow):
78 """
79 Re-initializes the data with a row from a SELECT * FROM FailureReasons.
80
81 Returns self. Raises exception if the row is None or otherwise invalid.
82 """
83
84 if aoRow is None:
85 raise TMRowNotFound('Failure Reason not found.');
86
87 self.idFailureReason = aoRow[0]
88 self.tsEffective = aoRow[1]
89 self.tsExpire = aoRow[2]
90 self.uidAuthor = aoRow[3]
91 self.idFailureCategory = aoRow[4]
92 self.sShort = aoRow[5]
93 self.sFull = aoRow[6]
94 self.iTicket = aoRow[7]
95 self.asUrls = aoRow[8]
96
97 return self;
98
99 def initFromDbWithId(self, oDb, idFailureReason, tsNow = None, sPeriodBack = None):
100 """
101 Initialize from the database, given the ID of a row.
102 """
103 oDb.execute(self.formatSimpleNowAndPeriodQuery(oDb,
104 'SELECT *\n'
105 'FROM FailureReasons\n'
106 'WHERE idFailureReason = %s\n'
107 , ( idFailureReason,), tsNow, sPeriodBack));
108 aoRow = oDb.fetchOne()
109 if aoRow is None:
110 raise TMRowNotFound('idFailureReason=%s not found (tsNow=%s sPeriodBack=%s)'
111 % (idFailureReason, tsNow, sPeriodBack,));
112 return self.initFromDbRow(aoRow);
113
114
115class FailureReasonDataEx(FailureReasonData):
116 """
117 Failure Reason Data, extended version that includes the category.
118 """
119
120 def __init__(self):
121 FailureReasonData.__init__(self);
122 self.oCategory = None;
123 self.oAuthor = None;
124
125 def initFromDbRowEx(self, aoRow, oCategoryLogic, oUserAccountLogic):
126 """
127 Re-initializes the data with a row from a SELECT * FROM FailureReasons.
128
129 Returns self. Raises exception if the row is None or otherwise invalid.
130 """
131
132 self.initFromDbRow(aoRow);
133 self.oCategory = oCategoryLogic.cachedLookup(self.idFailureCategory);
134 self.oAuthor = oUserAccountLogic.cachedLookup(self.uidAuthor);
135
136 return self;
137
138
139class FailureReasonLogic(ModelLogicBase): # pylint: disable=R0903
140 """
141 Failure Reason logic.
142 """
143
144 def __init__(self, oDb):
145 ModelLogicBase.__init__(self, oDb)
146 self.ahCache = None;
147 self.oCategoryLogic = None;
148 self.oUserAccountLogic = None;
149
150 def fetchForListing(self, iStart, cMaxRows, tsNow):
151 """
152 Fetches Failure Category records.
153
154 Returns an array (list) of FailureReasonDataEx items, empty list if none.
155 Raises exception on error.
156 """
157 self._ensureCachesPresent();
158
159 if tsNow is None:
160 self._oDb.execute('SELECT FailureReasons.*,\n'
161 ' FailureCategories.sShort AS sCategory\n'
162 'FROM FailureReasons,\n'
163 ' FailureCategories\n'
164 'WHERE FailureReasons.tsExpire = \'infinity\'::TIMESTAMP\n'
165 ' AND FailureCategories.idFailureCategory = FailureReasons.idFailureCategory\n'
166 ' AND FailureCategories.tsExpire = \'infinity\'::TIMESTAMP\n'
167 'ORDER BY sCategory ASC, sShort ASC\n'
168 'LIMIT %s OFFSET %s\n'
169 , (cMaxRows, iStart,));
170 else:
171 self._oDb.execute('SELECT FailureReasons.*,\n'
172 ' FailureCategories.sShort AS sCategory\n'
173 'FROM FailureReasons,\n'
174 ' FailureCategories\n'
175 'WHERE FailureReasons.tsExpire > %s\n'
176 ' AND FailureReasons.tsEffective <= %s\n'
177 ' AND FailureCategories.idFailureCategory = FailureReasons.idFailureCategory\n'
178 ' AND FailureReasons.tsExpire > %s\n'
179 ' AND FailureReasons.tsEffective <= %s\n'
180 'ORDER BY sCategory ASC, sShort ASC\n'
181 'LIMIT %s OFFSET %s\n'
182 , (tsNow, tsNow, tsNow, tsNow, cMaxRows, iStart,));
183
184 aoRows = []
185 for aoRow in self._oDb.fetchAll():
186 aoRows.append(FailureReasonDataEx().initFromDbRowEx(aoRow, self.oCategoryLogic, self.oUserAccountLogic));
187 return aoRows
188
189 def fetchForListingInCategory(self, iStart, cMaxRows, tsNow, idFailureCategory):
190 """
191 Fetches Failure Category records.
192
193 Returns an array (list) of FailureReasonDataEx items, empty list if none.
194 Raises exception on error.
195 """
196 self._ensureCachesPresent();
197
198 if tsNow is None:
199 self._oDb.execute('SELECT *\n'
200 'FROM FailureReasons\n'
201 'WHERE tsExpire = \'infinity\'::TIMESTAMP\n'
202 ' AND idFailureCategory = %s\n'
203 'ORDER BY sShort ASC\n'
204 'LIMIT %s OFFSET %s\n'
205 , ( idFailureCategory, cMaxRows, iStart,));
206 else:
207 self._oDb.execute('SELECT *\n'
208 'FROM FailureReasons\n'
209 'WHERE idFailureCategory = %s\n'
210 ' AND tsExpire > %s\n'
211 ' AND tsEffective <= %s\n'
212 'ORDER BY sShort ASC\n'
213 'LIMIT %s OFFSET %s\n'
214 , ( tsNow, tsNow, idFailureCategory, cMaxRows, iStart,));
215
216 aoRows = []
217 for aoRow in self._oDb.fetchAll():
218 aoRows.append(FailureReasonDataEx().initFromDbRowEx(aoRow, self.oCategoryLogic, self.oUserAccountLogic));
219 return aoRows
220
221 def fetchForCombo(self, sFirstEntry = 'Select a failure reason', tsEffective = None):
222 """
223 Gets the list of Failure Reasons for a combo box.
224 Returns an array of (value [idFailureReason], drop-down-name [sShort],
225 hover-text [sFull]) tuples.
226 """
227 if tsEffective is None:
228 self._oDb.execute('SELECT fr.idFailureReason, CONCAT(fc.sShort, \' / \', fr.sShort) as sComboText, fr.sFull\n'
229 'FROM FailureReasons fr,\n'
230 ' FailureCategories fc\n'
231 'WHERE fr.idFailureCategory = fc.idFailureCategory\n'
232 ' AND fr.tsExpire = \'infinity\'::TIMESTAMP\n'
233 ' AND fc.tsExpire = \'infinity\'::TIMESTAMP\n'
234 'ORDER BY sComboText')
235 else:
236 self._oDb.execute('SELECT fr.idFailureReason, CONCAT(fc.sShort, \' / \', fr.sShort) as sComboText, fr.sFull\n'
237 'FROM FailureReasons fr,\n'
238 ' FailureCategories fc\n'
239 'WHERE fr.idFailureCategory = fc.idFailureCategory\n'
240 ' AND fr.tsExpire > %s\n'
241 ' AND fr.tsEffective <= %s\n'
242 ' AND fc.tsExpire > %s\n'
243 ' AND fc.tsEffective <= %s\n'
244 'ORDER BY sComboText'
245 , (tsEffective, tsEffective, tsEffective, tsEffective));
246 aoRows = self._oDb.fetchAll();
247 return [(-1, sFirstEntry, '')] + aoRows;
248
249
250 def fetchForChangeLog(self, idFailureReason, iStart, cMaxRows, tsNow): # pylint: disable=R0914
251 """
252 Fetches change log entries for a failure reason.
253
254 Returns an array of ChangeLogEntry instance and an indicator whether
255 there are more entries.
256 Raises exception on error.
257 """
258 self._ensureCachesPresent();
259
260 if tsNow is None:
261 tsNow = self._oDb.getCurrentTimestamp();
262
263 # 1. Get a list of the relevant changes.
264 self._oDb.execute('SELECT * FROM FailureReasons WHERE idFailureReason = %s AND tsEffective <= %s\n'
265 'ORDER BY tsEffective DESC\n'
266 'LIMIT %s OFFSET %s\n'
267 , ( idFailureReason, tsNow, cMaxRows + 1, iStart, ));
268 aoRows = [];
269 for aoChange in self._oDb.fetchAll():
270 aoRows.append(FailureReasonData().initFromDbRow(aoChange));
271
272 # 2. Calculate the changes.
273 aoEntries = [];
274 for i in xrange(0, len(aoRows) - 1):
275 oNew = aoRows[i];
276 oOld = aoRows[i + 1];
277
278 aoChanges = [];
279 for sAttr in oNew.getDataAttributes():
280 if sAttr not in [ 'tsEffective', 'tsExpire', 'uidAuthor', ]:
281 oOldAttr = getattr(oOld, sAttr);
282 oNewAttr = getattr(oNew, sAttr);
283 if oOldAttr != oNewAttr:
284 if sAttr == 'idFailureCategory':
285 oCat = self.oCategoryLogic.cachedLookup(oOldAttr);
286 if oCat is not None:
287 oOldAttr = '%s (%s)' % (oOldAttr, oCat.sShort, );
288 oCat = self.oCategoryLogic.cachedLookup(oNewAttr);
289 if oCat is not None:
290 oNewAttr = '%s (%s)' % (oNewAttr, oCat.sShort, );
291 aoChanges.append(AttributeChangeEntry(sAttr, oNewAttr, oOldAttr, str(oNewAttr), str(oOldAttr)));
292
293 aoEntries.append(ChangeLogEntry(oNew.uidAuthor, None, oNew.tsEffective, oNew.tsExpire, oNew, oOld, aoChanges));
294
295 # If we're at the end of the log, add the initial entry.
296 if len(aoRows) <= cMaxRows and len(aoRows) > 0:
297 oNew = aoRows[-1];
298 aoEntries.append(ChangeLogEntry(oNew.uidAuthor, None, oNew.tsEffective, oNew.tsExpire, oNew, None, []));
299
300 return (UserAccountLogic(self._oDb).resolveChangeLogAuthors(aoEntries), len(aoRows) > cMaxRows);
301
302
303 def getById(self, idFailureReason):
304 """Get Failure Reason data by idFailureReason"""
305
306 self._oDb.execute('SELECT *\n'
307 'FROM FailureReasons\n'
308 'WHERE tsExpire = \'infinity\'::timestamp\n'
309 ' AND idFailureReason = %s;', (idFailureReason,))
310 aRows = self._oDb.fetchAll()
311 if len(aRows) not in (0, 1):
312 raise self._oDb.integrityException(
313 'Found more than one failure reasons with the same credentials. Database structure is corrupted.')
314 try:
315 return FailureReasonData().initFromDbRow(aRows[0])
316 except IndexError:
317 return None
318
319
320 def addEntry(self, oData, uidAuthor, fCommit = False):
321 """
322 Add a failure reason.
323 """
324 #
325 # Validate.
326 #
327 dErrors = oData.validateAndConvert(self._oDb, oData.ksValidateFor_Add);
328 if len(dErrors) > 0:
329 raise TMInvalidData('addEntry invalid input: %s' % (dErrors,));
330
331 #
332 # Add the record.
333 #
334 self._readdEntry(uidAuthor, oData);
335 self._oDb.maybeCommit(fCommit);
336 return True;
337
338
339 def editEntry(self, oData, uidAuthor, fCommit = False):
340 """
341 Modifies a failure reason.
342 """
343
344 #
345 # Validate inputs and read in the old(/current) data.
346 #
347 assert isinstance(oData, FailureReasonData);
348 dErrors = oData.validateAndConvert(self._oDb, oData.ksValidateFor_Edit);
349 if len(dErrors) > 0:
350 raise TMInvalidData('editEntry invalid input: %s' % (dErrors,));
351
352 oOldData = FailureReasonData().initFromDbWithId(self._oDb, oData.idFailureReason);
353
354 #
355 # Update the data that needs updating.
356 #
357 if not oData.isEqualEx(oOldData, [ 'tsEffective', 'tsExpire', 'uidAuthor', ]):
358 self._historizeEntry(oData.idFailureReason);
359 self._readdEntry(uidAuthor, oData);
360 self._oDb.maybeCommit(fCommit);
361 return True;
362
363
364 def removeEntry(self, uidAuthor, idFailureReason, fCascade = False, fCommit = False):
365 """
366 Deletes a failure reason.
367 """
368 _ = fCascade; # too complicated for now.
369
370 #
371 # Check whether it's being used by other tables and bitch if it is .
372 # We currently do not implement cascading.
373 #
374 self._oDb.execute('SELECT CONCAT(idBlacklisting, \' - blacklisting\')\n'
375 'FROM BuildBlacklist\n'
376 'WHERE idFailureReason = %s\n'
377 ' AND tsExpire = \'infinity\'::TIMESTAMP\n'
378 'UNION\n'
379 'SELECT CONCAT(idTestResult, \' - test result failure reason\')\n'
380 'FROM TestResultFailures\n'
381 'WHERE idFailureReason = %s\n'
382 ' AND tsExpire = \'infinity\'::TIMESTAMP\n'
383 , (idFailureReason, idFailureReason,));
384 aaoRows = self._oDb.fetchAll();
385 if len(aaoRows) > 0:
386 raise TMRowInUse('Cannot remove failure reason %u because its being used by: %s'
387 % (idFailureReason, ', '.join(aoRow[0] for aoRow in aaoRows),));
388
389 #
390 # Do the job.
391 #
392 oData = FailureReasonData().initFromDbWithId(self._oDb, idFailureReason);
393 assert oData.idFailureReason == idFailureReason;
394 (tsCur, tsCurMinusOne) = self._oDb.getCurrentTimestamps();
395 if oData.tsEffective != tsCur and oData.tsEffective != tsCurMinusOne:
396 self._historizeEntry(idFailureReason, tsCurMinusOne);
397 self._readdEntry(uidAuthor, oData, tsCurMinusOne);
398 self._historizeEntry(idFailureReason);
399 self._oDb.maybeCommit(fCommit);
400 return True;
401
402
403 def cachedLookup(self, idFailureReason):
404 """
405 Looks up the most recent FailureReasonDataEx object for uid idFailureReason
406 an object cache.
407
408 Returns a shared FailureReasonData object. None if not found.
409 Raises exception on DB error.
410 """
411 if self.ahCache is None:
412 self.ahCache = self._oDb.getCache('FailureReasonDataEx');
413 oEntry = self.ahCache.get(idFailureReason, None);
414 if oEntry is None:
415 self._oDb.execute('SELECT *\n'
416 'FROM FailureReasons\n'
417 'WHERE idFailureReason = %s\n'
418 ' AND tsExpire = \'infinity\'::TIMESTAMP\n'
419 , (idFailureReason, ));
420 if self._oDb.getRowCount() == 0:
421 # Maybe it was deleted, try get the last entry.
422 self._oDb.execute('SELECT *\n'
423 'FROM FailureReasons\n'
424 'WHERE idFailureReason = %s\n'
425 'ORDER BY tsExpire\n'
426 'LIMIT 1\n'
427 , (idFailureReason, ));
428 elif self._oDb.getRowCount() > 1:
429 raise self._oDb.integrityException('%s infinity rows for %s' % (self._oDb.getRowCount(), idFailureReason));
430
431 if self._oDb.getRowCount() == 1:
432 self._ensureCachesPresent();
433 oEntry = FailureReasonDataEx().initFromDbRowEx(self._oDb.fetchOne(), self.oCategoryLogic,
434 self.oUserAccountLogic);
435 self.ahCache[idFailureReason] = oEntry;
436 return oEntry;
437
438 #
439 # Helpers.
440 #
441
442 def _readdEntry(self, uidAuthor, oData, tsEffective = None):
443 """
444 Re-adds the FailureReasons entry. Used by addEntry, editEntry and removeEntry.
445 """
446 if tsEffective is None:
447 tsEffective = self._oDb.getCurrentTimestamp();
448 self._oDb.execute('INSERT INTO FailureReasons (\n'
449 ' uidAuthor,\n'
450 ' tsEffective,\n'
451 ' idFailureReason,\n'
452 ' idFailureCategory,\n'
453 ' sShort,\n'
454 ' sFull,\n'
455 ' iTicket,\n'
456 ' asUrls)\n'
457 'VALUES (%s, %s, '
458 + ( 'DEFAULT' if oData.idFailureReason is None else str(oData.idFailureReason) )
459 + ', %s, %s, %s, %s, %s)\n'
460 , ( uidAuthor,
461 tsEffective,
462 oData.idFailureCategory,
463 oData.sShort,
464 oData.sFull,
465 oData.iTicket,
466 oData.asUrls,) );
467 return True;
468
469
470 def _historizeEntry(self, idFailureReason, tsExpire = None):
471 """ Historizes the current entry. """
472 if tsExpire is None:
473 tsExpire = self._oDb.getCurrentTimestamp();
474 self._oDb.execute('UPDATE FailureReasons\n'
475 'SET tsExpire = %s\n'
476 'WHERE idFailureReason = %s\n'
477 ' AND tsExpire = \'infinity\'::TIMESTAMP\n'
478 , (tsExpire, idFailureReason,));
479 return True;
480
481
482 def _ensureCachesPresent(self):
483 """ Ensures we've got the cache references resolved. """
484 if self.oCategoryLogic is None:
485 from testmanager.core.failurecategory import FailureCategoryLogic;
486 self.oCategoryLogic = FailureCategoryLogic(self._oDb);
487 if self.oUserAccountLogic is None:
488 self.oUserAccountLogic = UserAccountLogic(self._oDb);
489 return True;
490
491
Note: See TracBrowser for help on using the repository browser.

© 2025 Oracle Support Privacy / Do Not Sell My Info Terms of Use Trademark Policy Automated Access Etiquette