VirtualBox

source: vbox/trunk/src/VBox/ValidationKit/testmanager/core/testresultfailures.py@ 96407

Last change on this file since 96407 was 96407, checked in by vboxsync, 2 years ago

scm copyright and license note update

  • Property svn:eol-style set to native
  • Property svn:keywords set to Author Date Id Revision
File size: 21.3 KB
Line 
1# -*- coding: utf-8 -*-
2# $Id: testresultfailures.py 96407 2022-08-22 17:43:14Z vboxsync $
3# pylint: disable=too-many-lines
4
5## @todo Rename this file to testresult.py!
6
7"""
8Test Manager - Test result failures.
9"""
10
11__copyright__ = \
12"""
13Copyright (C) 2012-2022 Oracle and/or its affiliates.
14
15This file is part of VirtualBox base platform packages, as
16available from https://www.virtualbox.org.
17
18This program is free software; you can redistribute it and/or
19modify it under the terms of the GNU General Public License
20as published by the Free Software Foundation, in version 3 of the
21License.
22
23This program is distributed in the hope that it will be useful, but
24WITHOUT ANY WARRANTY; without even the implied warranty of
25MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
26General Public License for more details.
27
28You should have received a copy of the GNU General Public License
29along with this program; if not, see <https://www.gnu.org/licenses>.
30
31The contents of this file may alternatively be used under the terms
32of the Common Development and Distribution License Version 1.0
33(CDDL), a copy of it is provided in the "COPYING.CDDL" file included
34in the VirtualBox distribution, in which case the provisions of the
35CDDL are applicable instead of those of the GPL.
36
37You may elect to license modified versions of this file under the
38terms and conditions of either the GPL or the CDDL or both.
39
40SPDX-License-Identifier: GPL-3.0-only OR CDDL-1.0
41"""
42__version__ = "$Revision: 96407 $"
43
44# Standard python imports.
45import sys;
46import unittest;
47
48# Validation Kit imports.
49from testmanager.core.base import ModelDataBase, ModelLogicBase, ModelDataBaseTestCase, TMInvalidData, TMRowNotFound, \
50 TMRowAlreadyExists, ChangeLogEntry, AttributeChangeEntry;
51from testmanager.core.failurereason import FailureReasonData;
52from testmanager.core.useraccount import UserAccountLogic;
53
54# Python 3 hacks:
55if sys.version_info[0] >= 3:
56 xrange = range; # pylint: disable=redefined-builtin,invalid-name
57
58
59class TestResultFailureData(ModelDataBase):
60 """
61 Test result failure reason data.
62 """
63
64 ksIdAttr = 'idTestResult';
65 kfIdAttrIsForForeign = True; # Modifies the 'add' validation.
66
67 ksParam_idTestResult = 'TestResultFailure_idTestResult';
68 ksParam_tsEffective = 'TestResultFailure_tsEffective';
69 ksParam_tsExpire = 'TestResultFailure_tsExpire';
70 ksParam_uidAuthor = 'TestResultFailure_uidAuthor';
71 ksParam_idTestSet = 'TestResultFailure_idTestSet';
72 ksParam_idFailureReason = 'TestResultFailure_idFailureReason';
73 ksParam_sComment = 'TestResultFailure_sComment';
74
75 kasAllowNullAttributes = ['tsEffective', 'tsExpire', 'uidAuthor', 'sComment', 'idTestSet' ];
76
77 kcDbColumns = 7;
78
79 def __init__(self):
80 ModelDataBase.__init__(self)
81 self.idTestResult = None;
82 self.tsEffective = None;
83 self.tsExpire = None;
84 self.uidAuthor = None;
85 self.idTestSet = None;
86 self.idFailureReason = None;
87 self.sComment = None;
88
89 def initFromDbRow(self, aoRow):
90 """
91 Reinitialize from a SELECT * FROM TestResultFailures.
92 Return self. Raises exception if no row.
93 """
94 if aoRow is None:
95 raise TMRowNotFound('Test result file record not found.')
96
97 self.idTestResult = aoRow[0];
98 self.tsEffective = aoRow[1];
99 self.tsExpire = aoRow[2];
100 self.uidAuthor = aoRow[3];
101 self.idTestSet = aoRow[4];
102 self.idFailureReason = aoRow[5];
103 self.sComment = aoRow[6];
104 return self;
105
106 def initFromDbWithId(self, oDb, idTestResult, tsNow = None, sPeriodBack = None):
107 """
108 Initialize the object from the database.
109 """
110 oDb.execute(self.formatSimpleNowAndPeriodQuery(oDb,
111 'SELECT *\n'
112 'FROM TestResultFailures\n'
113 'WHERE idTestResult = %s\n'
114 , ( idTestResult,), tsNow, sPeriodBack));
115 aoRow = oDb.fetchOne()
116 if aoRow is None:
117 raise TMRowNotFound('idTestResult=%s not found (tsNow=%s, sPeriodBack=%s)' % (idTestResult, tsNow, sPeriodBack));
118 assert len(aoRow) == self.kcDbColumns;
119 return self.initFromDbRow(aoRow);
120
121 def initFromValues(self, idTestResult, idFailureReason, uidAuthor,
122 tsExpire = None, tsEffective = None, idTestSet = None, sComment = None):
123 """
124 Initialize from values.
125 """
126 self.idTestResult = idTestResult;
127 self.tsEffective = tsEffective;
128 self.tsExpire = tsExpire;
129 self.uidAuthor = uidAuthor;
130 self.idTestSet = idTestSet;
131 self.idFailureReason = idFailureReason;
132 self.sComment = sComment;
133 return self;
134
135
136
137class TestResultFailureDataEx(TestResultFailureData):
138 """
139 Extends TestResultFailureData by resolving reasons and user.
140 """
141
142 def __init__(self):
143 TestResultFailureData.__init__(self);
144 self.oFailureReason = None;
145 self.oAuthor = None;
146
147 def initFromDbRowEx(self, aoRow, oFailureReasonLogic, oUserAccountLogic):
148 """
149 Reinitialize from a SELECT * FROM TestResultFailures.
150 Return self. Raises exception if no row.
151 """
152 self.initFromDbRow(aoRow);
153 self.oFailureReason = oFailureReasonLogic.cachedLookup(self.idFailureReason);
154 self.oAuthor = oUserAccountLogic.cachedLookup(self.uidAuthor);
155 return self;
156
157
158class TestResultListingData(ModelDataBase): # pylint: disable=too-many-instance-attributes
159 """
160 Test case result data representation for table listing
161 """
162
163 def __init__(self):
164 """Initialize"""
165 ModelDataBase.__init__(self)
166
167 self.idTestSet = None
168
169 self.idBuildCategory = None;
170 self.sProduct = None
171 self.sRepository = None;
172 self.sBranch = None
173 self.sType = None
174 self.idBuild = None;
175 self.sVersion = None;
176 self.iRevision = None
177
178 self.sOs = None;
179 self.sOsVersion = None;
180 self.sArch = None;
181 self.sCpuVendor = None;
182 self.sCpuName = None;
183 self.cCpus = None;
184 self.fCpuHwVirt = None;
185 self.fCpuNestedPaging = None;
186 self.fCpu64BitGuest = None;
187 self.idTestBox = None
188 self.sTestBoxName = None
189
190 self.tsCreated = None
191 self.tsElapsed = None
192 self.enmStatus = None
193 self.cErrors = None;
194
195 self.idTestCase = None
196 self.sTestCaseName = None
197 self.sBaseCmd = None
198 self.sArgs = None
199 self.sSubName = None;
200
201 self.idBuildTestSuite = None;
202 self.iRevisionTestSuite = None;
203
204 self.oFailureReason = None;
205 self.oFailureReasonAssigner = None;
206 self.tsFailureReasonAssigned = None;
207 self.sFailureReasonComment = None;
208
209 def initFromDbRowEx(self, aoRow, oFailureReasonLogic, oUserAccountLogic):
210 """
211 Reinitialize from a database query.
212 Return self. Raises exception if no row.
213 """
214 if aoRow is None:
215 raise TMRowNotFound('Test result record not found.')
216
217 self.idTestSet = aoRow[0];
218
219 self.idBuildCategory = aoRow[1];
220 self.sProduct = aoRow[2];
221 self.sRepository = aoRow[3];
222 self.sBranch = aoRow[4];
223 self.sType = aoRow[5];
224 self.idBuild = aoRow[6];
225 self.sVersion = aoRow[7];
226 self.iRevision = aoRow[8];
227
228 self.sOs = aoRow[9];
229 self.sOsVersion = aoRow[10];
230 self.sArch = aoRow[11];
231 self.sCpuVendor = aoRow[12];
232 self.sCpuName = aoRow[13];
233 self.cCpus = aoRow[14];
234 self.fCpuHwVirt = aoRow[15];
235 self.fCpuNestedPaging = aoRow[16];
236 self.fCpu64BitGuest = aoRow[17];
237 self.idTestBox = aoRow[18];
238 self.sTestBoxName = aoRow[19];
239
240 self.tsCreated = aoRow[20];
241 self.tsElapsed = aoRow[21];
242 self.enmStatus = aoRow[22];
243 self.cErrors = aoRow[23];
244
245 self.idTestCase = aoRow[24];
246 self.sTestCaseName = aoRow[25];
247 self.sBaseCmd = aoRow[26];
248 self.sArgs = aoRow[27];
249 self.sSubName = aoRow[28];
250
251 self.idBuildTestSuite = aoRow[29];
252 self.iRevisionTestSuite = aoRow[30];
253
254 self.oFailureReason = None;
255 if aoRow[31] is not None:
256 self.oFailureReason = oFailureReasonLogic.cachedLookup(aoRow[31]);
257 self.oFailureReasonAssigner = None;
258 if aoRow[32] is not None:
259 self.oFailureReasonAssigner = oUserAccountLogic.cachedLookup(aoRow[32]);
260 self.tsFailureReasonAssigned = aoRow[33];
261 self.sFailureReasonComment = aoRow[34];
262
263 return self
264
265
266
267class TestResultFailureLogic(ModelLogicBase): # pylint: disable=too-few-public-methods
268 """
269 Test result failure reason logic.
270 """
271
272 def __init__(self, oDb):
273 ModelLogicBase.__init__(self, oDb)
274
275 def fetchForChangeLog(self, idTestResult, iStart, cMaxRows, tsNow): # pylint: disable=too-many-locals
276 """
277 Fetches change log entries for a failure reason.
278
279 Returns an array of ChangeLogEntry instance and an indicator whether
280 there are more entries.
281 Raises exception on error.
282 """
283
284 if tsNow is None:
285 tsNow = self._oDb.getCurrentTimestamp();
286
287 # 1. Get a list of the changes from both TestResultFailures and assoicated
288 # FailureReasons. The latter is useful since the failure reason
289 # description may evolve along side the invidiual failure analysis.
290 self._oDb.execute('( SELECT trf.tsEffective AS tsEffectiveChangeLog,\n'
291 ' trf.uidAuthor AS uidAuthorChangeLog,\n'
292 ' trf.*,\n'
293 ' fr.*\n'
294 ' FROM TestResultFailures trf,\n'
295 ' FailureReasons fr\n'
296 ' WHERE trf.idTestResult = %s\n'
297 ' AND trf.tsEffective <= %s\n'
298 ' AND trf.idFailureReason = fr.idFailureReason\n'
299 ' AND fr.tsEffective <= trf.tsEffective\n'
300 ' AND fr.tsExpire > trf.tsEffective\n'
301 ')\n'
302 'UNION\n'
303 '( SELECT fr.tsEffective AS tsEffectiveChangeLog,\n'
304 ' fr.uidAuthor AS uidAuthorChangeLog,\n'
305 ' trf.*,\n'
306 ' fr.*\n'
307 ' FROM TestResultFailures trf,\n'
308 ' FailureReasons fr\n'
309 ' WHERE trf.idTestResult = %s\n'
310 ' AND trf.tsEffective <= %s\n'
311 ' AND trf.idFailureReason = fr.idFailureReason\n'
312 ' AND fr.tsEffective > trf.tsEffective\n'
313 ' AND fr.tsEffective < trf.tsExpire\n'
314 ')\n'
315 'ORDER BY tsEffectiveChangeLog DESC\n'
316 'LIMIT %s OFFSET %s\n'
317 , ( idTestResult, tsNow, idTestResult, tsNow, cMaxRows + 1, iStart, ));
318
319 aaoRows = [];
320 for aoChange in self._oDb.fetchAll():
321 oTrf = TestResultFailureDataEx().initFromDbRow(aoChange[2:]);
322 oFr = FailureReasonData().initFromDbRow(aoChange[(2+TestResultFailureData.kcDbColumns):]);
323 oTrf.oFailureReason = oFr;
324 aaoRows.append([aoChange[0], aoChange[1], oTrf, oFr]);
325
326 # 2. Calculate the changes.
327 oFailureCategoryLogic = None;
328 aoEntries = [];
329 for i in xrange(0, len(aaoRows) - 1):
330 aoNew = aaoRows[i];
331 aoOld = aaoRows[i + 1];
332
333 aoChanges = [];
334 oNew = aoNew[2];
335 oOld = aoOld[2];
336 for sAttr in oNew.getDataAttributes():
337 if sAttr not in [ 'tsEffective', 'tsExpire', 'uidAuthor', 'oFailureReason', 'oAuthor' ]:
338 oOldAttr = getattr(oOld, sAttr);
339 oNewAttr = getattr(oNew, sAttr);
340 if oOldAttr != oNewAttr:
341 if sAttr == 'idFailureReason':
342 oNewAttr = '%s (%s)' % (oNewAttr, oNew.oFailureReason.sShort, );
343 oOldAttr = '%s (%s)' % (oOldAttr, oOld.oFailureReason.sShort, );
344 aoChanges.append(AttributeChangeEntry(sAttr, oNewAttr, oOldAttr, str(oNewAttr), str(oOldAttr)));
345 if oOld.idFailureReason == oNew.idFailureReason:
346 oNew = aoNew[3];
347 oOld = aoOld[3];
348 for sAttr in oNew.getDataAttributes():
349 if sAttr not in [ 'tsEffective', 'tsExpire', 'uidAuthor', ]:
350 oOldAttr = getattr(oOld, sAttr);
351 oNewAttr = getattr(oNew, sAttr);
352 if oOldAttr != oNewAttr:
353 if sAttr == 'idFailureCategory':
354 if oFailureCategoryLogic is None:
355 from testmanager.core.failurecategory import FailureCategoryLogic;
356 oFailureCategoryLogic = FailureCategoryLogic(self._oDb);
357 oCat = oFailureCategoryLogic.cachedLookup(oNewAttr);
358 if oCat is not None:
359 oNewAttr = '%s (%s)' % (oNewAttr, oCat.sShort, );
360 oCat = oFailureCategoryLogic.cachedLookup(oOldAttr);
361 if oCat is not None:
362 oOldAttr = '%s (%s)' % (oOldAttr, oCat.sShort, );
363 aoChanges.append(AttributeChangeEntry(sAttr, oNewAttr, oOldAttr, str(oNewAttr), str(oOldAttr)));
364
365
366 tsExpire = aaoRows[i - 1][0] if i > 0 else aoNew[2].tsExpire;
367 aoEntries.append(ChangeLogEntry(aoNew[1], None, aoNew[0], tsExpire, aoNew[2], aoOld[2], aoChanges));
368
369 # If we're at the end of the log, add the initial entry.
370 if len(aaoRows) <= cMaxRows and aaoRows:
371 aoNew = aaoRows[-1];
372 tsExpire = aaoRows[-1 - 1][0] if len(aaoRows) > 1 else aoNew[2].tsExpire;
373 aoEntries.append(ChangeLogEntry(aoNew[1], None, aoNew[0], tsExpire, aoNew[2], None, []));
374
375 return (UserAccountLogic(self._oDb).resolveChangeLogAuthors(aoEntries), len(aaoRows) > cMaxRows);
376
377
378 def getById(self, idTestResult):
379 """Get Test result failure reason data by idTestResult"""
380
381 self._oDb.execute('SELECT *\n'
382 'FROM TestResultFailures\n'
383 'WHERE tsExpire = \'infinity\'::timestamp\n'
384 ' AND idTestResult = %s;', (idTestResult,))
385 aRows = self._oDb.fetchAll()
386 if len(aRows) not in (0, 1):
387 raise self._oDb.integrityException(
388 'Found more than one failure reasons with the same credentials. Database structure is corrupted.')
389 try:
390 return TestResultFailureData().initFromDbRow(aRows[0])
391 except IndexError:
392 return None
393
394 def addEntry(self, oData, uidAuthor, fCommit = False):
395 """
396 Add a test result failure reason record.
397 """
398
399 #
400 # Validate inputs and read in the old(/current) data.
401 #
402 assert isinstance(oData, TestResultFailureData);
403 dErrors = oData.validateAndConvert(self._oDb, oData.ksValidateFor_AddForeignId);
404 if dErrors:
405 raise TMInvalidData('editEntry invalid input: %s' % (dErrors,));
406
407 # Check if it exist first (we're adding, not editing, collisions not allowed).
408 oOldData = self.getById(oData.idTestResult);
409 if oOldData is not None:
410 raise TMRowAlreadyExists('TestResult %d already have a failure reason associated with it:'
411 '%s\n'
412 'Perhaps someone else beat you to it? Or did you try resubmit?'
413 % (oData.idTestResult, oOldData));
414 oData = self._resolveSetTestIdIfMissing(oData);
415
416 #
417 # Add record.
418 #
419 self._readdEntry(uidAuthor, oData);
420 self._oDb.maybeCommit(fCommit);
421 return True;
422
423 def editEntry(self, oData, uidAuthor, fCommit = False):
424 """
425 Modifies a test result failure reason.
426 """
427
428 #
429 # Validate inputs and read in the old(/current) data.
430 #
431 assert isinstance(oData, TestResultFailureData);
432 dErrors = oData.validateAndConvert(self._oDb, oData.ksValidateFor_Edit);
433 if dErrors:
434 raise TMInvalidData('editEntry invalid input: %s' % (dErrors,));
435
436 oOldData = self.getById(oData.idTestResult)
437 oData.idTestSet = oOldData.idTestSet;
438
439 #
440 # Update the data that needs updating.
441 #
442 if not oData.isEqualEx(oOldData, [ 'tsEffective', 'tsExpire', 'uidAuthor', ]):
443 self._historizeEntry(oData.idTestResult);
444 self._readdEntry(uidAuthor, oData);
445 self._oDb.maybeCommit(fCommit);
446 return True;
447
448
449 def removeEntry(self, uidAuthor, idTestResult, fCascade = False, fCommit = False):
450 """
451 Deletes a test result failure reason.
452 """
453 _ = fCascade; # Not applicable.
454
455 oData = self.getById(idTestResult)
456 (tsCur, tsCurMinusOne) = self._oDb.getCurrentTimestamps();
457 if oData.tsEffective != tsCur and oData.tsEffective != tsCurMinusOne:
458 self._historizeEntry(idTestResult, tsCurMinusOne);
459 self._readdEntry(uidAuthor, oData, tsCurMinusOne);
460 self._historizeEntry(idTestResult);
461 self._oDb.execute('UPDATE TestResultFailures\n'
462 'SET tsExpire = CURRENT_TIMESTAMP\n'
463 'WHERE idTestResult = %s\n'
464 ' AND tsExpire = \'infinity\'::TIMESTAMP\n'
465 , (idTestResult,));
466 self._oDb.maybeCommit(fCommit);
467 return True;
468
469 #
470 # Helpers.
471 #
472
473 def _readdEntry(self, uidAuthor, oData, tsEffective = None):
474 """
475 Re-adds the TestResultFailure entry. Used by addEntry, editEntry and removeEntry.
476 """
477 if tsEffective is None:
478 tsEffective = self._oDb.getCurrentTimestamp();
479 self._oDb.execute('INSERT INTO TestResultFailures (\n'
480 ' uidAuthor,\n'
481 ' tsEffective,\n'
482 ' idTestResult,\n'
483 ' idTestSet,\n'
484 ' idFailureReason,\n'
485 ' sComment)\n'
486 'VALUES (%s, %s, %s, %s, %s, %s)\n'
487 , ( uidAuthor,
488 tsEffective,
489 oData.idTestResult,
490 oData.idTestSet,
491 oData.idFailureReason,
492 oData.sComment,) );
493 return True;
494
495
496 def _historizeEntry(self, idTestResult, tsExpire = None):
497 """ Historizes the current entry. """
498 if tsExpire is None:
499 tsExpire = self._oDb.getCurrentTimestamp();
500 self._oDb.execute('UPDATE TestResultFailures\n'
501 'SET tsExpire = %s\n'
502 'WHERE idTestResult = %s\n'
503 ' AND tsExpire = \'infinity\'::TIMESTAMP\n'
504 , (tsExpire, idTestResult,));
505 return True;
506
507
508 def _resolveSetTestIdIfMissing(self, oData):
509 """ Resolve any missing idTestSet reference (it's a duplicate for speed efficiency). """
510 if oData.idTestSet is None and oData.idTestResult is not None:
511 self._oDb.execute('SELECT idTestSet FROM TestResults WHERE idTestResult = %s', (oData.idTestResult,));
512 oData.idTestSet = self._oDb.fetchOne()[0];
513 return oData;
514
515
516
517#
518# Unit testing.
519#
520
521# pylint: disable=missing-docstring
522class TestResultFailureDataTestCase(ModelDataBaseTestCase):
523 def setUp(self):
524 self.aoSamples = [TestResultFailureData(),];
525
526if __name__ == '__main__':
527 unittest.main();
528 # not reached.
529
Note: See TracBrowser for help on using the repository browser.

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