VirtualBox

source: vbox/trunk/src/VBox/ValidationKit/testmanager/core/testresults.py@ 65053

Last change on this file since 65053 was 65053, checked in by vboxsync, 8 years ago

testmanager: More test result filtering.

  • Property svn:eol-style set to native
  • Property svn:keywords set to Author Date Id Revision
File size: 110.6 KB
Line 
1# -*- coding: utf-8 -*-
2# $Id: testresults.py 65053 2017-01-02 16:43:09Z vboxsync $
3# pylint: disable=C0302
4
5## @todo Rename this file to testresult.py!
6
7"""
8Test Manager - Fetch test results.
9"""
10
11__copyright__ = \
12"""
13Copyright (C) 2012-2016 Oracle Corporation
14
15This file is part of VirtualBox Open Source Edition (OSE), as
16available from http://www.215389.xyz. This file is free software;
17you can redistribute it and/or modify it under the terms of the GNU
18General Public License (GPL) as published by the Free Software
19Foundation, in version 2 as it comes in the "COPYING" file of the
20VirtualBox OSE distribution. VirtualBox OSE is distributed in the
21hope that it will be useful, but WITHOUT ANY WARRANTY of any kind.
22
23The contents of this file may alternatively be used under the terms
24of the Common Development and Distribution License Version 1.0
25(CDDL) only, as it comes in the "COPYING.CDDL" file of the
26VirtualBox OSE distribution, in which case the provisions of the
27CDDL are applicable instead of those of the GPL.
28
29You may elect to license modified versions of this file under the
30terms and conditions of either the GPL or the CDDL or both.
31"""
32__version__ = "$Revision: 65053 $"
33# Standard python imports.
34import unittest;
35
36# Validation Kit imports.
37from common import constants;
38from testmanager import config;
39from testmanager.core.base import ModelDataBase, ModelLogicBase, ModelDataBaseTestCase, ModelFilterBase, \
40 FilterCriterion, FilterCriterionValueAndDescription, \
41 TMExceptionBase, TMTooManyRows, TMRowNotFound;
42from testmanager.core.testgroup import TestGroupData;
43from testmanager.core.build import BuildDataEx, BuildCategoryData, BuildLogic, BuildCategoryLogic;
44from testmanager.core.failurereason import FailureReasonLogic;
45from testmanager.core.testbox import TestBoxData, TestBoxLogic;
46from testmanager.core.testcase import TestCaseData, TestCaseLogic;
47from testmanager.core.schedgroup import SchedGroupData, SchedGroupLogic;
48from testmanager.core.systemlog import SystemLogData, SystemLogLogic;
49from testmanager.core.testresultfailures import TestResultFailureDataEx;
50from testmanager.core.useraccount import UserAccountLogic;
51
52
53class TestResultData(ModelDataBase):
54 """
55 Test case execution result data
56 """
57
58 ## @name TestStatus_T
59 # @{
60 ksTestStatus_Running = 'running';
61 ksTestStatus_Success = 'success';
62 ksTestStatus_Skipped = 'skipped';
63 ksTestStatus_BadTestBox = 'bad-testbox';
64 ksTestStatus_Aborted = 'aborted';
65 ksTestStatus_Failure = 'failure';
66 ksTestStatus_TimedOut = 'timed-out';
67 ksTestStatus_Rebooted = 'rebooted';
68 ## @}
69
70 ## List of relatively harmless (to testgroup/case) statuses.
71 kasHarmlessTestStatuses = [ ksTestStatus_Skipped, ksTestStatus_BadTestBox, ksTestStatus_Aborted, ];
72 ## List of bad statuses.
73 kasBadTestStatuses = [ ksTestStatus_Failure, ksTestStatus_TimedOut, ksTestStatus_Rebooted, ];
74
75
76 ksIdAttr = 'idTestResult';
77
78 ksParam_idTestResult = 'TestResultData_idTestResult';
79 ksParam_idTestResultParent = 'TestResultData_idTestResultParent';
80 ksParam_idTestSet = 'TestResultData_idTestSet';
81 ksParam_tsCreated = 'TestResultData_tsCreated';
82 ksParam_tsElapsed = 'TestResultData_tsElapsed';
83 ksParam_idStrName = 'TestResultData_idStrName';
84 ksParam_cErrors = 'TestResultData_cErrors';
85 ksParam_enmStatus = 'TestResultData_enmStatus';
86 ksParam_iNestingDepth = 'TestResultData_iNestingDepth';
87 kasValidValues_enmStatus = [
88 ksTestStatus_Running,
89 ksTestStatus_Success,
90 ksTestStatus_Skipped,
91 ksTestStatus_BadTestBox,
92 ksTestStatus_Aborted,
93 ksTestStatus_Failure,
94 ksTestStatus_TimedOut,
95 ksTestStatus_Rebooted
96 ];
97
98
99 def __init__(self):
100 ModelDataBase.__init__(self)
101 self.idTestResult = None
102 self.idTestResultParent = None
103 self.idTestSet = None
104 self.tsCreated = None
105 self.tsElapsed = None
106 self.idStrName = None
107 self.cErrors = 0;
108 self.enmStatus = None
109 self.iNestingDepth = None
110
111 def initFromDbRow(self, aoRow):
112 """
113 Reinitialize from a SELECT * FROM TestResults.
114 Return self. Raises exception if no row.
115 """
116 if aoRow is None:
117 raise TMRowNotFound('Test result record not found.')
118
119 self.idTestResult = aoRow[0]
120 self.idTestResultParent = aoRow[1]
121 self.idTestSet = aoRow[2]
122 self.tsCreated = aoRow[3]
123 self.tsElapsed = aoRow[4]
124 self.idStrName = aoRow[5]
125 self.cErrors = aoRow[6]
126 self.enmStatus = aoRow[7]
127 self.iNestingDepth = aoRow[8]
128 return self;
129
130 def initFromDbWithId(self, oDb, idTestResult, tsNow = None, sPeriodBack = None):
131 """
132 Initialize from the database, given the ID of a row.
133 """
134 _ = tsNow;
135 _ = sPeriodBack;
136 oDb.execute('SELECT *\n'
137 'FROM TestResults\n'
138 'WHERE idTestResult = %s\n'
139 , ( idTestResult,));
140 aoRow = oDb.fetchOne()
141 if aoRow is None:
142 raise TMRowNotFound('idTestResult=%s not found' % (idTestResult,));
143 return self.initFromDbRow(aoRow);
144
145 def isFailure(self):
146 """ Check if it's a real failure. """
147 return self.enmStatus in self.kasBadTestStatuses;
148
149
150class TestResultDataEx(TestResultData):
151 """
152 Extended test result data class.
153
154 This is intended for use as a node in a result tree. This is not intended
155 for serialization to parameters or vice versa. Use TestResultLogic to
156 construct the tree.
157 """
158
159 def __init__(self):
160 TestResultData.__init__(self)
161 self.sName = None; # idStrName resolved.
162 self.oParent = None; # idTestResultParent within the tree.
163
164 self.aoChildren = []; # TestResultDataEx;
165 self.aoValues = []; # TestResultValueDataEx;
166 self.aoMsgs = []; # TestResultMsgDataEx;
167 self.aoFiles = []; # TestResultFileDataEx;
168 self.oReason = None; # TestResultReasonDataEx;
169
170 def initFromDbRow(self, aoRow):
171 """
172 Initialize from a query like this:
173 SELECT TestResults.*, TestResultStrTab.sValue
174 FROM TestResults, TestResultStrTab
175 WHERE TestResultStrTab.idStr = TestResults.idStrName
176
177 Note! The caller is expected to fetch children, values, failure
178 details, and files.
179 """
180 self.sName = None;
181 self.oParent = None;
182 self.aoChildren = [];
183 self.aoValues = [];
184 self.aoMsgs = [];
185 self.aoFiles = [];
186 self.oReason = None;
187
188 TestResultData.initFromDbRow(self, aoRow);
189
190 self.sName = aoRow[9];
191 return self;
192
193 def deepCountErrorContributers(self):
194 """
195 Counts how many test result instances actually contributed to cErrors.
196 """
197
198 # Check each child (if any).
199 cChanges = 0;
200 cChildErrors = 0;
201 for oChild in self.aoChildren:
202 if oChild.cErrors > 0:
203 cChildErrors += oChild.cErrors;
204 cChanges += oChild.deepCountErrorContributers();
205
206 # Did we contribute as well?
207 if self.cErrors > cChildErrors:
208 cChanges += 1;
209 return cChanges;
210
211 def getListOfFailures(self):
212 """
213 Get a list of test results insances actually contributing to cErrors.
214
215 Returns a list of TestResultDataEx insance from this tree. (shared!)
216 """
217 # Check each child (if any).
218 aoRet = [];
219 cChildErrors = 0;
220 for oChild in self.aoChildren:
221 if oChild.cErrors > 0:
222 cChildErrors += oChild.cErrors;
223 aoRet.extend(oChild.getListOfFailures());
224
225 # Did we contribute as well?
226 if self.cErrors > cChildErrors:
227 aoRet.append(self);
228
229 return aoRet;
230
231 def getFullName(self):
232 """ Constructs the full name of this test result. """
233 if self.oParent is None:
234 return self.sName;
235 return self.oParent.getFullName() + ' / ' + self.sName;
236
237
238
239class TestResultValueData(ModelDataBase):
240 """
241 Test result value data.
242 """
243
244 ksIdAttr = 'idTestResultValue';
245
246 ksParam_idTestResultValue = 'TestResultValue_idTestResultValue';
247 ksParam_idTestResult = 'TestResultValue_idTestResult';
248 ksParam_idTestSet = 'TestResultValue_idTestSet';
249 ksParam_tsCreated = 'TestResultValue_tsCreated';
250 ksParam_idStrName = 'TestResultValue_idStrName';
251 ksParam_lValue = 'TestResultValue_lValue';
252 ksParam_iUnit = 'TestResultValue_iUnit';
253
254 kasAllowNullAttributes = [ 'idTestSet', ];
255
256 def __init__(self):
257 ModelDataBase.__init__(self)
258 self.idTestResultValue = None;
259 self.idTestResult = None;
260 self.idTestSet = None;
261 self.tsCreated = None;
262 self.idStrName = None;
263 self.lValue = None;
264 self.iUnit = 0;
265
266 def initFromDbRow(self, aoRow):
267 """
268 Reinitialize from a SELECT * FROM TestResultValues.
269 Return self. Raises exception if no row.
270 """
271 if aoRow is None:
272 raise TMRowNotFound('Test result value record not found.')
273
274 self.idTestResultValue = aoRow[0];
275 self.idTestResult = aoRow[1];
276 self.idTestSet = aoRow[2];
277 self.tsCreated = aoRow[3];
278 self.idStrName = aoRow[4];
279 self.lValue = aoRow[5];
280 self.iUnit = aoRow[6];
281 return self;
282
283
284class TestResultValueDataEx(TestResultValueData):
285 """
286 Extends TestResultValue by resolving the value name and unit string.
287 """
288
289 def __init__(self):
290 TestResultValueData.__init__(self)
291 self.sName = None;
292 self.sUnit = '';
293
294 def initFromDbRow(self, aoRow):
295 """
296 Reinitialize from a query like this:
297 SELECT TestResultValues.*, TestResultStrTab.sValue
298 FROM TestResultValues, TestResultStrTab
299 WHERE TestResultStrTab.idStr = TestResultValues.idStrName
300
301 Return self. Raises exception if no row.
302 """
303 TestResultValueData.initFromDbRow(self, aoRow);
304 self.sName = aoRow[7];
305 if self.iUnit < len(constants.valueunit.g_asNames):
306 self.sUnit = constants.valueunit.g_asNames[self.iUnit];
307 else:
308 self.sUnit = '<%d>' % (self.iUnit,);
309 return self;
310
311class TestResultMsgData(ModelDataBase):
312 """
313 Test result message data.
314 """
315
316 ksIdAttr = 'idTestResultMsg';
317
318 ksParam_idTestResultMsg = 'TestResultValue_idTestResultMsg';
319 ksParam_idTestResult = 'TestResultValue_idTestResult';
320 ksParam_idTestSet = 'TestResultValue_idTestSet';
321 ksParam_tsCreated = 'TestResultValue_tsCreated';
322 ksParam_idStrMsg = 'TestResultValue_idStrMsg';
323 ksParam_enmLevel = 'TestResultValue_enmLevel';
324
325 kasAllowNullAttributes = [ 'idTestSet', ];
326
327 kcDbColumns = 6
328
329 def __init__(self):
330 ModelDataBase.__init__(self)
331 self.idTestResultMsg = None;
332 self.idTestResult = None;
333 self.idTestSet = None;
334 self.tsCreated = None;
335 self.idStrMsg = None;
336 self.enmLevel = None;
337
338 def initFromDbRow(self, aoRow):
339 """
340 Reinitialize from a SELECT * FROM TestResultMsgs.
341 Return self. Raises exception if no row.
342 """
343 if aoRow is None:
344 raise TMRowNotFound('Test result value record not found.')
345
346 self.idTestResultMsg = aoRow[0];
347 self.idTestResult = aoRow[1];
348 self.idTestSet = aoRow[2];
349 self.tsCreated = aoRow[3];
350 self.idStrMsg = aoRow[4];
351 self.enmLevel = aoRow[5];
352 return self;
353
354class TestResultMsgDataEx(TestResultMsgData):
355 """
356 Extends TestResultMsg by resolving the message string.
357 """
358
359 def __init__(self):
360 TestResultMsgData.__init__(self)
361 self.sMsg = None;
362
363 def initFromDbRow(self, aoRow):
364 """
365 Reinitialize from a query like this:
366 SELECT TestResultMsg.*, TestResultStrTab.sValue
367 FROM TestResultMsg, TestResultStrTab
368 WHERE TestResultStrTab.idStr = TestResultMsgs.idStrName
369
370 Return self. Raises exception if no row.
371 """
372 TestResultMsgData.initFromDbRow(self, aoRow);
373 self.sMsg = aoRow[self.kcDbColumns];
374 return self;
375
376
377class TestResultFileData(ModelDataBase):
378 """
379 Test result message data.
380 """
381
382 ksIdAttr = 'idTestResultFile';
383
384 ksParam_idTestResultFile = 'TestResultFile_idTestResultFile';
385 ksParam_idTestResult = 'TestResultFile_idTestResult';
386 ksParam_tsCreated = 'TestResultFile_tsCreated';
387 ksParam_idStrFile = 'TestResultFile_idStrFile';
388 ksParam_idStrDescription = 'TestResultFile_idStrDescription';
389 ksParam_idStrKind = 'TestResultFile_idStrKind';
390 ksParam_idStrMime = 'TestResultFile_idStrMime';
391
392 ## @name Kind of files.
393 ## @{
394 ksKind_LogReleaseVm = 'log/release/vm';
395 ksKind_LogDebugVm = 'log/debug/vm';
396 ksKind_LogReleaseSvc = 'log/release/svc';
397 ksKind_LogRebugSvc = 'log/debug/svc';
398 ksKind_LogReleaseClient = 'log/release/client';
399 ksKind_LogDebugClient = 'log/debug/client';
400 ksKind_LogInstaller = 'log/installer';
401 ksKind_LogUninstaller = 'log/uninstaller';
402 ksKind_LogGuestKernel = 'log/guest/kernel';
403 ksKind_CrashReportVm = 'crash/report/vm';
404 ksKind_CrashDumpVm = 'crash/dump/vm';
405 ksKind_CrashReportSvc = 'crash/report/svc';
406 ksKind_CrashDumpSvc = 'crash/dump/svc';
407 ksKind_CrashReportClient = 'crash/report/client';
408 ksKind_CrashDumpClient = 'crash/dump/client';
409 ksKind_InfoCollection = 'info/collection';
410 ksKind_InfoVgaText = 'info/vgatext';
411 ksKind_MiscOther = 'misc/other';
412 ksKind_ScreenshotFailure = 'screenshot/failure';
413 ksKind_ScreenshotSuccesss = 'screenshot/success';
414 #kSkind_ScreenCaptureFailure = 'screencapture/failure';
415 ## @}
416
417 kasAllowNullAttributes = [ 'idTestSet', ];
418
419 kcDbColumns = 8
420
421 def __init__(self):
422 ModelDataBase.__init__(self)
423 self.idTestResultFile = None;
424 self.idTestResult = None;
425 self.idTestSet = None;
426 self.tsCreated = None;
427 self.idStrFile = None;
428 self.idStrDescription = None;
429 self.idStrKind = None;
430 self.idStrMime = None;
431
432 def initFromDbRow(self, aoRow):
433 """
434 Reinitialize from a SELECT * FROM TestResultFiles.
435 Return self. Raises exception if no row.
436 """
437 if aoRow is None:
438 raise TMRowNotFound('Test result file record not found.')
439
440 self.idTestResultFile = aoRow[0];
441 self.idTestResult = aoRow[1];
442 self.idTestSet = aoRow[2];
443 self.tsCreated = aoRow[3];
444 self.idStrFile = aoRow[4];
445 self.idStrDescription = aoRow[5];
446 self.idStrKind = aoRow[6];
447 self.idStrMime = aoRow[7];
448 return self;
449
450class TestResultFileDataEx(TestResultFileData):
451 """
452 Extends TestResultFile by resolving the strings.
453 """
454
455 def __init__(self):
456 TestResultFileData.__init__(self)
457 self.sFile = None;
458 self.sDescription = None;
459 self.sKind = None;
460 self.sMime = None;
461
462 def initFromDbRow(self, aoRow):
463 """
464 Reinitialize from a query like this:
465 SELECT TestResultFiles.*,
466 StrTabFile.sValue AS sFile,
467 StrTabDesc.sValue AS sDescription
468 StrTabKind.sValue AS sKind,
469 StrTabMime.sValue AS sMime,
470 FROM ...
471
472 Return self. Raises exception if no row.
473 """
474 TestResultFileData.initFromDbRow(self, aoRow);
475 self.sFile = aoRow[self.kcDbColumns];
476 self.sDescription = aoRow[self.kcDbColumns + 1];
477 self.sKind = aoRow[self.kcDbColumns + 2];
478 self.sMime = aoRow[self.kcDbColumns + 3];
479 return self;
480
481 def initFakeMainLog(self, oTestSet):
482 """
483 Reinitializes to represent the main.log object (not in DB).
484
485 Returns self.
486 """
487 self.idTestResultFile = 0;
488 self.idTestResult = oTestSet.idTestResult;
489 self.tsCreated = oTestSet.tsCreated;
490 self.idStrFile = None;
491 self.idStrDescription = None;
492 self.idStrKind = None;
493 self.idStrMime = None;
494
495 self.sFile = 'main.log';
496 self.sDescription = '';
497 self.sKind = 'log/main';
498 self.sMime = 'text/plain';
499 return self;
500
501 def isProbablyUtf8Encoded(self):
502 """
503 Checks if the file is likely to be UTF-8 encoded.
504 """
505 if self.sMime in [ 'text/plain', 'text/html' ]:
506 return True;
507 return False;
508
509 def getMimeWithEncoding(self):
510 """
511 Gets the MIME type with encoding if likely to be UTF-8.
512 """
513 if self.isProbablyUtf8Encoded():
514 return '%s; charset=utf-8' % (self.sMime,);
515 return self.sMime;
516
517
518
519class TestResultListingData(ModelDataBase): # pylint: disable=R0902
520 """
521 Test case result data representation for table listing
522 """
523
524 class FailureReasonListingData(object):
525 """ Failure reason listing data """
526 def __init__(self):
527 self.oFailureReason = None;
528 self.oFailureReasonAssigner = None;
529 self.tsFailureReasonAssigned = None;
530 self.sFailureReasonComment = None;
531
532 def __init__(self):
533 """Initialize"""
534 ModelDataBase.__init__(self)
535
536 self.idTestSet = None
537
538 self.idBuildCategory = None;
539 self.sProduct = None
540 self.sRepository = None;
541 self.sBranch = None
542 self.sType = None
543 self.idBuild = None;
544 self.sVersion = None;
545 self.iRevision = None
546
547 self.sOs = None;
548 self.sOsVersion = None;
549 self.sArch = None;
550 self.sCpuVendor = None;
551 self.sCpuName = None;
552 self.cCpus = None;
553 self.fCpuHwVirt = None;
554 self.fCpuNestedPaging = None;
555 self.fCpu64BitGuest = None;
556 self.idTestBox = None
557 self.sTestBoxName = None
558
559 self.tsCreated = None
560 self.tsElapsed = None
561 self.enmStatus = None
562 self.cErrors = None;
563
564 self.idTestCase = None
565 self.sTestCaseName = None
566 self.sBaseCmd = None
567 self.sArgs = None
568 self.sSubName = None;
569
570 self.idBuildTestSuite = None;
571 self.iRevisionTestSuite = None;
572
573 self.aoFailureReasons = [];
574
575 def initFromDbRowEx(self, aoRow, oFailureReasonLogic, oUserAccountLogic):
576 """
577 Reinitialize from a database query.
578 Return self. Raises exception if no row.
579 """
580 if aoRow is None:
581 raise TMRowNotFound('Test result record not found.')
582
583 self.idTestSet = aoRow[0];
584
585 self.idBuildCategory = aoRow[1];
586 self.sProduct = aoRow[2];
587 self.sRepository = aoRow[3];
588 self.sBranch = aoRow[4];
589 self.sType = aoRow[5];
590 self.idBuild = aoRow[6];
591 self.sVersion = aoRow[7];
592 self.iRevision = aoRow[8];
593
594 self.sOs = aoRow[9];
595 self.sOsVersion = aoRow[10];
596 self.sArch = aoRow[11];
597 self.sCpuVendor = aoRow[12];
598 self.sCpuName = aoRow[13];
599 self.cCpus = aoRow[14];
600 self.fCpuHwVirt = aoRow[15];
601 self.fCpuNestedPaging = aoRow[16];
602 self.fCpu64BitGuest = aoRow[17];
603 self.idTestBox = aoRow[18];
604 self.sTestBoxName = aoRow[19];
605
606 self.tsCreated = aoRow[20];
607 self.tsElapsed = aoRow[21];
608 self.enmStatus = aoRow[22];
609 self.cErrors = aoRow[23];
610
611 self.idTestCase = aoRow[24];
612 self.sTestCaseName = aoRow[25];
613 self.sBaseCmd = aoRow[26];
614 self.sArgs = aoRow[27];
615 self.sSubName = aoRow[28];
616
617 self.idBuildTestSuite = aoRow[29];
618 self.iRevisionTestSuite = aoRow[30];
619
620 self.aoFailureReasons = [];
621 for i, _ in enumerate(aoRow[31]):
622 if aoRow[31][i] is not None \
623 or aoRow[32][i] is not None \
624 or aoRow[33][i] is not None \
625 or aoRow[34][i] is not None:
626 oReason = self.FailureReasonListingData();
627 if aoRow[31][i] is not None:
628 oReason.oFailureReason = oFailureReasonLogic.cachedLookup(aoRow[31][i]);
629 if aoRow[32][i] is not None:
630 oReason.oFailureReasonAssigner = oUserAccountLogic.cachedLookup(aoRow[32][i]);
631 oReason.tsFailureReasonAssigned = aoRow[33][i];
632 oReason.sFailureReasonComment = aoRow[34][i];
633 self.aoFailureReasons.append(oReason);
634
635 return self
636
637
638class TestResultHangingOffence(TMExceptionBase):
639 """Hanging offence committed by test case."""
640 pass;
641
642
643class TestResultFilter(ModelFilterBase):
644 """
645 Test result filter.
646 """
647
648 kiTestStatus = 0;
649 kiBranches = 1;
650 kiSchedGroups = 2;
651 kiTestBoxes = 3;
652 kiTestCases = 4;
653 kiRevisions = 5;
654 kiCpuArches = 6;
655 kiCpuVendors = 7;
656 kiOses = 8;
657 kiOsVersions = 9;
658 kiFailReasons = 10;
659
660 def __init__(self):
661 ModelFilterBase.__init__(self);
662 oCrit = FilterCriterion('Test status', sVarNm = 'ts', sType = FilterCriterion.ksType_String,
663 sTable = 'TestSets', sColumn = 'enmStatus');
664 oCrit.aoPossible = (
665 FilterCriterionValueAndDescription(TestResultData.ksTestStatus_Success, 'Success'),
666 FilterCriterionValueAndDescription(TestResultData.ksTestStatus_Running, 'Running'),
667 FilterCriterionValueAndDescription(TestResultData.ksTestStatus_Skipped, 'Skipped'),
668 FilterCriterionValueAndDescription(TestResultData.ksTestStatus_Aborted, 'Aborted'),
669 FilterCriterionValueAndDescription(TestResultData.ksTestStatus_Failure, 'Failure'),
670 FilterCriterionValueAndDescription(TestResultData.ksTestStatus_TimedOut, 'Timed out'),
671 FilterCriterionValueAndDescription(TestResultData.ksTestStatus_Rebooted, 'Rebooted'),
672 );
673 self.aCriteria.append(oCrit);
674 assert self.aCriteria[self.kiTestStatus] is oCrit;
675
676 oCrit = FilterCriterion('Branches', sVarNm = 'br', sType = FilterCriterion.ksType_String,
677 sTable = 'BuildCategories', sColumn = 'sBranch');
678 self.aCriteria.append(oCrit);
679 assert self.aCriteria[self.kiBranches] is oCrit;
680
681 oCrit = FilterCriterion('Sched groups', sVarNm = 'sg', sTable = 'TestSets', sColumn = 'idSchedGroup');
682 self.aCriteria.append(oCrit);
683 assert self.aCriteria[self.kiSchedGroups] is oCrit;
684
685 oCrit = FilterCriterion('Testboxes', sVarNm = 'tb', sTable = 'TestSets', sColumn = 'idTestBox');
686 self.aCriteria.append(oCrit);
687 assert self.aCriteria[self.kiTestBoxes] is oCrit;
688
689 oCrit = FilterCriterion('Test cases', sVarNm = 'tc', sTable = 'TestSets', sColumn = 'idTestCase');
690 self.aCriteria.append(oCrit);
691 assert self.aCriteria[self.kiTestCases] is oCrit;
692
693 oCrit = FilterCriterion('Revisions', sVarNm = 'rv', sTable = 'Builds', sColumn = 'iRevision');
694 self.aCriteria.append(oCrit);
695 assert self.aCriteria[self.kiRevisions] is oCrit;
696
697 oCrit = FilterCriterion('CPU Arches', sVarNm = 'ca', sTable = 'TestBoxesWithStrings', sColumn = 'idStrCpuArch');
698 self.aCriteria.append(oCrit);
699 assert self.aCriteria[self.kiCpuArches] is oCrit;
700
701 oCrit = FilterCriterion('CPU Vendor', sVarNm = 'cv', sTable = 'TestBoxesWithStrings', sColumn = 'idStrCpuVendor');
702 self.aCriteria.append(oCrit);
703 assert self.aCriteria[self.kiCpuVendors] is oCrit;
704
705 oCrit = FilterCriterion('OSes', sVarNm = 'os', sTable = 'TestBoxesWithStrings', sColumn = 'idStrOs');
706 self.aCriteria.append(oCrit);
707 assert self.aCriteria[self.kiOses] is oCrit;
708
709 oCrit = FilterCriterion('OS Versions', sVarNm = 'ov', sTable = 'TestBoxesWithStrings', sColumn = 'idStrOsVersion');
710 self.aCriteria.append(oCrit);
711 assert self.aCriteria[self.kiOsVersions] is oCrit;
712
713 oCrit = FilterCriterion('Failure Reasons', sVarNm = 'fr', sTable = 'TestResultFailures', sColumn = 'idFailureReason');
714 self.aCriteria.append(oCrit);
715 assert self.aCriteria[self.kiFailReasons] is oCrit;
716
717
718 def getWhereConditions(self, sExtraIndent = '', iOmit = -1):
719 """
720 Construct the WHERE conditions for the filter, optionally omitting one
721 criterion.
722 """
723 sQuery = '';
724 for iCrit, oCrit in enumerate(self.aCriteria):
725 if oCrit.sState == FilterCriterion.ksState_Selected and iCrit != iOmit:
726 sQuery += '%s AND %s.%s IN (' % (sExtraIndent, oCrit.sTable, oCrit.sColumn,);
727 if oCrit.sType == FilterCriterion.ksType_String:
728 sQuery += ', '.join('\'%s\'' % (sValue,) for sValue in oCrit.aoSelected) + ')\n';
729 else:
730 sQuery += ', '.join(str(iValue) for iValue in oCrit.aoSelected) + ')\n';
731 return sQuery;
732
733 def getTableJoins(self, sExtraIndent = '', iOmit = -1):
734 """
735 Construct the WHERE conditions for the filter, optionally omitting one
736 criterion.
737 """
738 sQuery = '';
739 afDone = { 'TestSets': True, };
740 for iCrit, oCrit in enumerate(self.aCriteria):
741 if oCrit.sState == FilterCriterion.ksState_Selected \
742 and oCrit.sTable not in afDone \
743 and iCrit != iOmit:
744 afDone[oCrit.sTable] = True;
745 if oCrit.sTable == 'Builds':
746 sQuery += '%sINNER JOIN Builds\n' \
747 '%s ON Builds.idBuild = TestSets.idBuild\n' \
748 '%s AND Builds.tsExpire > TestSets.tsCreated\n' \
749 '%s AND Builds.tsEffective <= TestSets.tsCreated\n' \
750 % ( sExtraIndent, sExtraIndent, sExtraIndent, sExtraIndent, );
751 elif oCrit.sTable == 'TestResultFailures':
752 sQuery += '%sLEFT OUTER JOIN TestResultFailures\n' \
753 '%s ON TestResultFailures.idTestSet = TestSets.idTestSet\n' \
754 '%s AND TestResultFailures.tsExpire = \'infinity\'::TIMESTAMP\n' \
755 % ( sExtraIndent, sExtraIndent, sExtraIndent, );
756 elif oCrit.sTable == 'TestBoxesWithStrings':
757 sQuery += '%sLEFT OUTER JOIN TestBoxesWithStrings\n' \
758 '%s ON TestBoxesWithStrings.idGenTestBox = TestSets.idGenTestBox\n' \
759 % ( sExtraIndent, sExtraIndent, );
760 elif oCrit.sTable == 'BuildCategories':
761 sQuery += '%sINNER JOIN BuildCategories\n' \
762 '%s ON BuildCategories.idBuildCategory = TestSets.idBuildCategory\n' \
763 % ( sExtraIndent, sExtraIndent, );
764 else:
765 assert False, oCrit.sTable;
766 return sQuery;
767
768 def isJoiningWithTable(self, sTable):
769 """ Checks whether getTableJoins already joins with TestResultFailures. """
770 for oCrit in self.aCriteria:
771 if oCrit.sTable == sTable and oCrit.sState == FilterCriterion.ksState_Selected:
772 return True;
773 return False
774
775
776
777class TestResultLogic(ModelLogicBase): # pylint: disable=R0903
778 """
779 Results grouped by scheduling group.
780 """
781
782 #
783 # Result grinding for displaying in the WUI.
784 #
785
786 ksResultsGroupingTypeNone = 'ResultsGroupingTypeNone';
787 ksResultsGroupingTypeTestGroup = 'ResultsGroupingTypeTestGroup';
788 ksResultsGroupingTypeBuildCat = 'ResultsGroupingTypeBuildCat';
789 ksResultsGroupingTypeBuildRev = 'ResultsGroupingTypeBuildRev';
790 ksResultsGroupingTypeTestBox = 'ResultsGroupingTypeTestBox';
791 ksResultsGroupingTypeTestCase = 'ResultsGroupingTypeTestCase';
792 ksResultsGroupingTypeOS = 'ResultsGroupingTypeOS';
793 ksResultsGroupingTypeArch = 'ResultsGroupingTypeArch';
794 ksResultsGroupingTypeSchedGroup = 'ResultsGroupingTypeSchedGroup';
795
796 ## @name Result sorting options.
797 ## @{
798 ksResultsSortByRunningAndStart = 'ResultsSortByRunningAndStart'; ##< Default
799 ksResultsSortByBuildRevision = 'ResultsSortByBuildRevision';
800 ksResultsSortByTestBoxName = 'ResultsSortByTestBoxName';
801 ksResultsSortByTestBoxOs = 'ResultsSortByTestBoxOs';
802 ksResultsSortByTestBoxOsVersion = 'ResultsSortByTestBoxOsVersion';
803 ksResultsSortByTestBoxOsArch = 'ResultsSortByTestBoxOsArch';
804 ksResultsSortByTestBoxArch = 'ResultsSortByTestBoxArch';
805 ksResultsSortByTestBoxCpuVendor = 'ResultsSortByTestBoxCpuVendor';
806 ksResultsSortByTestBoxCpuName = 'ResultsSortByTestBoxCpuName';
807 ksResultsSortByTestBoxCpuRev = 'ResultsSortByTestBoxCpuRev';
808 ksResultsSortByTestBoxCpuFeatures = 'ResultsSortByTestBoxCpuFeatures';
809 ksResultsSortByTestCaseName = 'ResultsSortByTestCaseName';
810 ksResultsSortByFailureReason = 'ResultsSortByFailureReason';
811 kasResultsSortBy = {
812 ksResultsSortByRunningAndStart,
813 ksResultsSortByBuildRevision,
814 ksResultsSortByTestBoxName,
815 ksResultsSortByTestBoxOs,
816 ksResultsSortByTestBoxOsVersion,
817 ksResultsSortByTestBoxOsArch,
818 ksResultsSortByTestBoxArch,
819 ksResultsSortByTestBoxCpuVendor,
820 ksResultsSortByTestBoxCpuName,
821 ksResultsSortByTestBoxCpuRev,
822 ksResultsSortByTestBoxCpuFeatures,
823 ksResultsSortByTestCaseName,
824 ksResultsSortByFailureReason,
825 };
826 ## Used by the WUI for generating the drop down.
827 kaasResultsSortByTitles = (
828 ( ksResultsSortByRunningAndStart, 'Running & Start TS' ),
829 ( ksResultsSortByBuildRevision, 'Build Revision' ),
830 ( ksResultsSortByTestBoxName, 'TestBox Name' ),
831 ( ksResultsSortByTestBoxOs, 'O/S' ),
832 ( ksResultsSortByTestBoxOsVersion, 'O/S Version' ),
833 ( ksResultsSortByTestBoxOsArch, 'O/S & Architecture' ),
834 ( ksResultsSortByTestBoxArch, 'Architecture' ),
835 ( ksResultsSortByTestBoxCpuVendor, 'CPU Vendor' ),
836 ( ksResultsSortByTestBoxCpuName, 'CPU Vendor & Name' ),
837 ( ksResultsSortByTestBoxCpuRev, 'CPU Vendor & Revision' ),
838 ( ksResultsSortByTestBoxCpuFeatures, 'CPU Features' ),
839 ( ksResultsSortByTestCaseName, 'Test Case Name' ),
840 ( ksResultsSortByFailureReason, 'Failure Reason' ),
841 );
842 ## @}
843
844 ## Default sort by map.
845 kdResultSortByMap = {
846 ksResultsSortByRunningAndStart: ( (), None, None, '', '' ),
847 ksResultsSortByBuildRevision: (
848 # Sorting tables.
849 ('Builds',),
850 # Sorting table join(s).
851 ' AND TestSets.idBuild = Builds.idBuild'
852 ' AND Builds.tsExpire >= TestSets.tsCreated'
853 ' AND Builds.tsEffective <= TestSets.tsCreated',
854 # Start of ORDER BY statement.
855 ' Builds.iRevision DESC',
856 # Extra columns to fetch for the above ORDER BY to work in a SELECT DISTINCT statement.
857 '',
858 # Columns for the GROUP BY
859 ''),
860 ksResultsSortByTestBoxName: (
861 ('TestBoxes',),
862 ' AND TestSets.idGenTestBox = TestBoxes.idGenTestBox',
863 ' TestBoxes.sName DESC',
864 '', '' ),
865 ksResultsSortByTestBoxOsArch: (
866 ('TestBoxesWithStrings',),
867 ' AND TestSets.idGenTestBox = TestBoxesWithStrings.idGenTestBox',
868 ' TestBoxesWithStrings.sOs, TestBoxesWithStrings.sCpuArch',
869 '', '' ),
870 ksResultsSortByTestBoxOs: (
871 ('TestBoxesWithStrings',),
872 ' AND TestSets.idGenTestBox = TestBoxesWithStrings.idGenTestBox',
873 ' TestBoxesWithStrings.sOs',
874 '', '' ),
875 ksResultsSortByTestBoxOsVersion: (
876 ('TestBoxesWithStrings',),
877 ' AND TestSets.idGenTestBox = TestBoxesWithStrings.idGenTestBox',
878 ' TestBoxesWithStrings.sOs, TestBoxesWithStrings.sOsVersion DESC',
879 '', '' ),
880 ksResultsSortByTestBoxArch: (
881 ('TestBoxesWithStrings',),
882 ' AND TestSets.idGenTestBox = TestBoxesWithStrings.idGenTestBox',
883 ' TestBoxesWithStrings.sCpuArch',
884 '', '' ),
885 ksResultsSortByTestBoxCpuVendor: (
886 ('TestBoxesWithStrings',),
887 ' AND TestSets.idGenTestBox = TestBoxesWithStrings.idGenTestBox',
888 ' TestBoxesWithStrings.sCpuVendor',
889 '', '' ),
890 ksResultsSortByTestBoxCpuName: (
891 ('TestBoxesWithStrings',),
892 ' AND TestSets.idGenTestBox = TestBoxesWithStrings.idGenTestBox',
893 ' TestBoxesWithStrings.sCpuVendor, TestBoxesWithStrings.sCpuName',
894 '', '' ),
895 ksResultsSortByTestBoxCpuRev: (
896 ('TestBoxesWithStrings',),
897 ' AND TestSets.idGenTestBox = TestBoxesWithStrings.idGenTestBox',
898 ' TestBoxesWithStrings.sCpuVendor, TestBoxesWithStrings.lCpuRevision DESC',
899 ', TestBoxesWithStrings.lCpuRevision',
900 ', TestBoxesWithStrings.lCpuRevision' ),
901 ksResultsSortByTestBoxCpuFeatures: (
902 ('TestBoxes',),
903 ' AND TestSets.idGenTestBox = TestBoxes.idGenTestBox',
904 ' TestBoxes.fCpuHwVirt DESC, TestBoxes.fCpuNestedPaging DESC, TestBoxes.fCpu64BitGuest DESC, TestBoxes.cCpus DESC',
905 '',
906 '' ),
907 ksResultsSortByTestCaseName: (
908 ('TestCases',),
909 ' AND TestSets.idGenTestCase = TestCases.idGenTestCase',
910 ' TestCases.sName',
911 '', '' ),
912 ksResultsSortByFailureReason: (
913 (), '',
914 'asSortByFailureReason ASC',
915 ', array_agg(FailureReasons.sShort ORDER BY TestResultFailures.idTestResult) AS asSortByFailureReason',
916 '' ),
917 };
918
919 kdResultGroupingMap = {
920 ksResultsGroupingTypeNone: (
921 # Grouping tables;
922 (),
923 # Grouping field;
924 None,
925 # Grouping where addition.
926 None,
927 # Sort by overrides.
928 {},
929 ),
930 ksResultsGroupingTypeTestGroup: ('', 'TestSets.idTestGroup', None, {},),
931 ksResultsGroupingTypeTestBox: ('', 'TestSets.idTestBox', None, {},),
932 ksResultsGroupingTypeTestCase: ('', 'TestSets.idTestCase', None, {},),
933 ksResultsGroupingTypeOS: (
934 ('TestBoxes',),
935 'TestBoxes.idStrOs',
936 ' AND TestBoxes.idGenTestBox = TestSets.idGenTestBox',
937 {},
938 ),
939 ksResultsGroupingTypeArch: (
940 ('TestBoxes',),
941 'TestBoxes.idStrCpuArch',
942 ' AND TestBoxes.idGenTestBox = TestSets.idGenTestBox',
943 {},
944 ),
945 ksResultsGroupingTypeBuildCat: ('', 'TestSets.idBuildCategory', None, {},),
946 ksResultsGroupingTypeBuildRev: (
947 ('Builds',),
948 'Builds.iRevision',
949 ' AND Builds.idBuild = TestSets.idBuild'
950 ' AND Builds.tsExpire > TestSets.tsCreated'
951 ' AND Builds.tsEffective <= TestSets.tsCreated',
952 { ksResultsSortByBuildRevision: ( (), None, ' Builds.iRevision DESC' ), }
953 ),
954 ksResultsGroupingTypeSchedGroup: ( '', 'TestSets.idSchedGroup', None, {},),
955 };
956
957
958 def __init__(self, oDb):
959 ModelLogicBase.__init__(self, oDb)
960 self.oFailureReasonLogic = None;
961 self.oUserAccountLogic = None;
962
963 def _getTimePeriodQueryPart(self, tsNow, sInterval, sExtraIndent = ''):
964 """
965 Get part of SQL query responsible for SELECT data within
966 specified period of time.
967 """
968 assert sInterval is not None; # too many rows.
969
970 cMonthsMourningPeriod = 2; # Stop reminding everyone about testboxes after 2 months. (May also speed up the query.)
971 if tsNow is None:
972 sRet = '(TestSets.tsDone IS NULL OR TestSets.tsDone >= (CURRENT_TIMESTAMP - \'%s\'::interval))\n' \
973 '%s AND TestSets.tsCreated >= (CURRENT_TIMESTAMP - \'%s\'::interval - \'%u months\'::interval)\n' \
974 % ( sInterval,
975 sExtraIndent, sInterval, cMonthsMourningPeriod);
976 else:
977 sTsNow = '\'%s\'::TIMESTAMP' % (tsNow,); # It's actually a string already. duh.
978 sRet = 'TestSets.tsCreated <= %s\n' \
979 '%s AND TestSets.tsCreated >= (%s - \'%s\'::interval - \'%u months\'::interval)\n' \
980 '%s AND (TestSets.tsDone IS NULL OR TestSets.tsDone >= (%s - \'%s\'::interval))\n' \
981 % ( sTsNow,
982 sExtraIndent, sTsNow, sInterval, cMonthsMourningPeriod,
983 sExtraIndent, sTsNow, sInterval );
984 return sRet
985
986 def fetchResultsForListing(self, iStart, cMaxRows, tsNow, sInterval, oFilter, enmResultSortBy, # pylint: disable=R0913
987 enmResultsGroupingType, iResultsGroupingValue, fOnlyFailures, fOnlyNeedingReason):
988 """
989 Fetches TestResults table content.
990
991 If @param enmResultsGroupingType and @param iResultsGroupingValue
992 are not None, then resulting (returned) list contains only records
993 that match specified @param enmResultsGroupingType.
994
995 If @param enmResultsGroupingType is None, then
996 @param iResultsGroupingValue is ignored.
997
998 Returns an array (list) of TestResultData items, empty list if none.
999 Raises exception on error.
1000 """
1001
1002 _ = oFilter;
1003
1004 #
1005 # Get SQL query parameters
1006 #
1007 if enmResultsGroupingType is None or enmResultsGroupingType not in self.kdResultGroupingMap:
1008 raise TMExceptionBase('Unknown grouping type');
1009 if enmResultSortBy is None or enmResultSortBy not in self.kasResultsSortBy:
1010 raise TMExceptionBase('Unknown sorting');
1011 asGroupingTables, sGroupingField, sGroupingCondition, dSortOverrides = self.kdResultGroupingMap[enmResultsGroupingType];
1012 if enmResultSortBy in dSortOverrides:
1013 asSortTables, sSortWhere, sSortOrderBy, sSortColumns, sSortGroupBy = dSortOverrides[enmResultSortBy];
1014 else:
1015 asSortTables, sSortWhere, sSortOrderBy, sSortColumns, sSortGroupBy = self.kdResultSortByMap[enmResultSortBy];
1016
1017 #
1018 # Construct the query.
1019 #
1020 sQuery = 'SELECT DISTINCT TestSets.idTestSet,\n' \
1021 ' BuildCategories.idBuildCategory,\n' \
1022 ' BuildCategories.sProduct,\n' \
1023 ' BuildCategories.sRepository,\n' \
1024 ' BuildCategories.sBranch,\n' \
1025 ' BuildCategories.sType,\n' \
1026 ' Builds.idBuild,\n' \
1027 ' Builds.sVersion,\n' \
1028 ' Builds.iRevision,\n' \
1029 ' TestBoxesWithStrings.sOs,\n' \
1030 ' TestBoxesWithStrings.sOsVersion,\n' \
1031 ' TestBoxesWithStrings.sCpuArch,\n' \
1032 ' TestBoxesWithStrings.sCpuVendor,\n' \
1033 ' TestBoxesWithStrings.sCpuName,\n' \
1034 ' TestBoxesWithStrings.cCpus,\n' \
1035 ' TestBoxesWithStrings.fCpuHwVirt,\n' \
1036 ' TestBoxesWithStrings.fCpuNestedPaging,\n' \
1037 ' TestBoxesWithStrings.fCpu64BitGuest,\n' \
1038 ' TestBoxesWithStrings.idTestBox,\n' \
1039 ' TestBoxesWithStrings.sName,\n' \
1040 ' TestResults.tsCreated,\n' \
1041 ' COALESCE(TestResults.tsElapsed, CURRENT_TIMESTAMP - TestResults.tsCreated) AS tsElapsedTestResult,\n' \
1042 ' TestSets.enmStatus,\n' \
1043 ' TestResults.cErrors,\n' \
1044 ' TestCases.idTestCase,\n' \
1045 ' TestCases.sName,\n' \
1046 ' TestCases.sBaseCmd,\n' \
1047 ' TestCaseArgs.sArgs,\n' \
1048 ' TestCaseArgs.sSubName,\n' \
1049 ' TestSuiteBits.idBuild AS idBuildTestSuite,\n' \
1050 ' TestSuiteBits.iRevision AS iRevisionTestSuite,\n' \
1051 ' array_agg(TestResultFailures.idFailureReason ORDER BY TestResultFailures.idTestResult),\n' \
1052 ' array_agg(TestResultFailures.uidAuthor ORDER BY TestResultFailures.idTestResult),\n' \
1053 ' array_agg(TestResultFailures.tsEffective ORDER BY TestResultFailures.idTestResult),\n' \
1054 ' array_agg(TestResultFailures.sComment ORDER BY TestResultFailures.idTestResult),\n' \
1055 ' (TestSets.tsDone IS NULL) SortRunningFirst' + sSortColumns + '\n' \
1056 'FROM ( SELECT TestSets.idTestSet AS idTestSet,\n' \
1057 ' TestSets.tsDone AS tsDone,\n' \
1058 ' TestSets.tsCreated AS tsCreated,\n' \
1059 ' TestSets.enmStatus AS enmStatus,\n' \
1060 ' TestSets.idBuild AS idBuild,\n' \
1061 ' TestSets.idBuildTestSuite AS idBuildTestSuite,\n' \
1062 ' TestSets.idGenTestBox AS idGenTestBox,\n' \
1063 ' TestSets.idGenTestCase AS idGenTestCase,\n' \
1064 ' TestSets.idGenTestCaseArgs AS idGenTestCaseArgs\n' \
1065 ' FROM TestSets\n';
1066 sQuery += oFilter.getTableJoins(' ');
1067 if fOnlyNeedingReason and not oFilter.isJoiningWithTable('TestResultFailures'):
1068 sQuery += '\n' \
1069 ' LEFT OUTER JOIN TestResultFailures\n' \
1070 ' ON TestSets.idTestSet = TestResultFailures.idTestSet\n' \
1071 ' AND TestResultFailures.tsExpire = \'infinity\'::TIMESTAMP';
1072 for asTables in [asGroupingTables, asSortTables]:
1073 for sTable in asTables:
1074 if not oFilter.isJoiningWithTable(sTable):
1075 sQuery = sQuery[:-1] + ',\n ' + sTable + '\n';
1076
1077 sQuery += ' WHERE ' + self._getTimePeriodQueryPart(tsNow, sInterval, ' ') + \
1078 oFilter.getWhereConditions(' ');
1079 if fOnlyFailures or fOnlyNeedingReason:
1080 sQuery += ' AND TestSets.enmStatus != \'success\'::TestStatus_T\n' \
1081 ' AND TestSets.enmStatus != \'running\'::TestStatus_T\n';
1082 if fOnlyNeedingReason:
1083 sQuery += ' AND TestResultFailures.idTestSet IS NULL\n';
1084 if sGroupingField is not None:
1085 sQuery += ' AND %s = %d\n' % (sGroupingField, iResultsGroupingValue,);
1086 if sGroupingCondition is not None:
1087 sQuery += sGroupingCondition.replace(' AND ', ' AND ');
1088 if sSortWhere is not None:
1089 sQuery += sSortWhere.replace(' AND ', ' AND ');
1090 sQuery += ' ORDER BY ';
1091 if sSortOrderBy is not None and sSortOrderBy.find('FailureReason') < 0:
1092 sQuery += sSortOrderBy + ',\n ';
1093 sQuery += '(TestSets.tsDone IS NULL) DESC, TestSets.idTestSet DESC\n' \
1094 ' LIMIT %s OFFSET %s\n' % (cMaxRows, iStart,);
1095
1096 # Note! INNER JOIN TestBoxesWithStrings performs miserable compared to LEFT OUTER JOIN. Doesn't matter for the result
1097 # because TestSets.idGenTestBox is a foreign key and unique in TestBoxes. So, let's do what ever is faster.
1098 sQuery += ' ) AS TestSets\n' \
1099 ' LEFT OUTER JOIN TestBoxesWithStrings\n' \
1100 ' ON TestSets.idGenTestBox = TestBoxesWithStrings.idGenTestBox' \
1101 ' LEFT OUTER JOIN Builds AS TestSuiteBits\n' \
1102 ' ON TestSets.idBuildTestSuite = TestSuiteBits.idBuild\n' \
1103 ' LEFT OUTER JOIN TestResultFailures\n' \
1104 ' ON TestSets.idTestSet = TestResultFailures.idTestSet\n' \
1105 ' AND TestResultFailures.tsExpire = \'infinity\'::TIMESTAMP';
1106 if sSortOrderBy is not None and sSortOrderBy.find('FailureReason') >= 0:
1107 sQuery += '\n' \
1108 ' LEFT OUTER JOIN FailureReasons\n' \
1109 ' ON TestResultFailures.idFailureReason = FailureReasons.idFailureReason\n' \
1110 ' AND FailureReasons.tsExpire = \'infinity\'::TIMESTAMP';
1111 sQuery += ',\n' \
1112 ' BuildCategories,\n' \
1113 ' Builds,\n' \
1114 ' TestResults,\n' \
1115 ' TestCases,\n' \
1116 ' TestCaseArgs\n';
1117 sQuery += 'WHERE TestSets.idTestSet = TestResults.idTestSet\n' \
1118 ' AND TestResults.idTestResultParent is NULL\n' \
1119 ' AND TestSets.idBuild = Builds.idBuild\n' \
1120 ' AND Builds.tsExpire > TestSets.tsCreated\n' \
1121 ' AND Builds.tsEffective <= TestSets.tsCreated\n' \
1122 ' AND Builds.idBuildCategory = BuildCategories.idBuildCategory\n' \
1123 ' AND TestSets.idGenTestCase = TestCases.idGenTestCase\n' \
1124 ' AND TestSets.idGenTestCaseArgs = TestCaseArgs.idGenTestCaseArgs\n';
1125 sQuery += 'GROUP BY TestSets.idTestSet,\n' \
1126 ' BuildCategories.idBuildCategory,\n' \
1127 ' BuildCategories.sProduct,\n' \
1128 ' BuildCategories.sRepository,\n' \
1129 ' BuildCategories.sBranch,\n' \
1130 ' BuildCategories.sType,\n' \
1131 ' Builds.idBuild,\n' \
1132 ' Builds.sVersion,\n' \
1133 ' Builds.iRevision,\n' \
1134 ' TestBoxesWithStrings.sOs,\n' \
1135 ' TestBoxesWithStrings.sOsVersion,\n' \
1136 ' TestBoxesWithStrings.sCpuArch,\n' \
1137 ' TestBoxesWithStrings.sCpuVendor,\n' \
1138 ' TestBoxesWithStrings.sCpuName,\n' \
1139 ' TestBoxesWithStrings.cCpus,\n' \
1140 ' TestBoxesWithStrings.fCpuHwVirt,\n' \
1141 ' TestBoxesWithStrings.fCpuNestedPaging,\n' \
1142 ' TestBoxesWithStrings.fCpu64BitGuest,\n' \
1143 ' TestBoxesWithStrings.idTestBox,\n' \
1144 ' TestBoxesWithStrings.sName,\n' \
1145 ' TestResults.tsCreated,\n' \
1146 ' tsElapsedTestResult,\n' \
1147 ' TestSets.enmStatus,\n' \
1148 ' TestResults.cErrors,\n' \
1149 ' TestCases.idTestCase,\n' \
1150 ' TestCases.sName,\n' \
1151 ' TestCases.sBaseCmd,\n' \
1152 ' TestCaseArgs.sArgs,\n' \
1153 ' TestCaseArgs.sSubName,\n' \
1154 ' TestSuiteBits.idBuild,\n' \
1155 ' TestSuiteBits.iRevision,\n' \
1156 ' SortRunningFirst' + sSortGroupBy + '\n';
1157 sQuery += 'ORDER BY ';
1158 if sSortOrderBy is not None:
1159 sQuery += sSortOrderBy.replace('TestBoxes.', 'TestBoxesWithStrings.') + ',\n ';
1160 sQuery += '(TestSets.tsDone IS NULL) DESC, TestSets.idTestSet DESC\n';
1161
1162 #
1163 # Execute the query and return the wrapped results.
1164 #
1165 self._oDb.execute(sQuery);
1166
1167 if self.oFailureReasonLogic is None:
1168 self.oFailureReasonLogic = FailureReasonLogic(self._oDb);
1169 if self.oUserAccountLogic is None:
1170 self.oUserAccountLogic = UserAccountLogic(self._oDb);
1171
1172 aoRows = [];
1173 for aoRow in self._oDb.fetchAll():
1174 aoRows.append(TestResultListingData().initFromDbRowEx(aoRow, self.oFailureReasonLogic, self.oUserAccountLogic));
1175
1176 return aoRows
1177
1178
1179 def fetchTimestampsForLogViewer(self, idTestSet):
1180 """
1181 Returns an ordered list with all the test result timestamps, both start
1182 and end.
1183
1184 The log viewer create anchors in the log text so we can jump directly to
1185 the log lines relevant for a test event.
1186 """
1187 self._oDb.execute('(\n'
1188 'SELECT tsCreated\n'
1189 'FROM TestResults\n'
1190 'WHERE idTestSet = %s\n'
1191 ') UNION (\n'
1192 'SELECT tsCreated + tsElapsed\n'
1193 'FROM TestResults\n'
1194 'WHERE idTestSet = %s\n'
1195 ' AND tsElapsed IS NOT NULL\n'
1196 ') UNION (\n'
1197 'SELECT TestResultFiles.tsCreated\n'
1198 'FROM TestResultFiles\n'
1199 'WHERE idTestSet = %s\n'
1200 ') UNION (\n'
1201 'SELECT tsCreated\n'
1202 'FROM TestResultValues\n'
1203 'WHERE idTestSet = %s\n'
1204 ') UNION (\n'
1205 'SELECT TestResultMsgs.tsCreated\n'
1206 'FROM TestResultMsgs\n'
1207 'WHERE idTestSet = %s\n'
1208 ') ORDER by 1'
1209 , ( idTestSet, idTestSet, idTestSet, idTestSet, idTestSet, ));
1210 return [aoRow[0] for aoRow in self._oDb.fetchAll()];
1211
1212
1213 def getEntriesCount(self, tsNow, sInterval, oFilter, enmResultsGroupingType, iResultsGroupingValue,
1214 fOnlyFailures, fOnlyNeedingReason):
1215 """
1216 Get number of table records.
1217
1218 If @param enmResultsGroupingType and @param iResultsGroupingValue
1219 are not None, then we count only only those records
1220 that match specified @param enmResultsGroupingType.
1221
1222 If @param enmResultsGroupingType is None, then
1223 @param iResultsGroupingValue is ignored.
1224 """
1225 _ = oFilter;
1226
1227 #
1228 # Get SQL query parameters
1229 #
1230 if enmResultsGroupingType is None:
1231 raise TMExceptionBase('Unknown grouping type')
1232
1233 if enmResultsGroupingType not in self.kdResultGroupingMap:
1234 raise TMExceptionBase('Unknown grouping type')
1235 asGroupingTables, sGroupingField, sGroupingCondition, _ = self.kdResultGroupingMap[enmResultsGroupingType];
1236
1237 #
1238 # Construct the query.
1239 #
1240 sQuery = 'SELECT COUNT(TestSets.idTestSet)\n' \
1241 'FROM TestSets\n';
1242 sQuery += oFilter.getTableJoins();
1243 if fOnlyNeedingReason and not oFilter.isJoiningWithTable('TestResultFailures'):
1244 sQuery += ' LEFT OUTER JOIN TestResultFailures\n' \
1245 ' ON TestSets.idTestSet = TestResultFailures.idTestSet\n' \
1246 ' AND TestResultFailures.tsExpire = \'infinity\'::TIMESTAMP\n';
1247 for sTable in asGroupingTables:
1248 if not oFilter.isJoiningWithTable(sTable):
1249 sQuery = sQuery[:-1] + ',\n ' + sTable + '\n';
1250 sQuery += 'WHERE ' + self._getTimePeriodQueryPart(tsNow, sInterval) + \
1251 oFilter.getWhereConditions();
1252 if fOnlyFailures or fOnlyNeedingReason:
1253 sQuery += ' AND TestSets.enmStatus != \'success\'::TestStatus_T\n' \
1254 ' AND TestSets.enmStatus != \'running\'::TestStatus_T\n';
1255 if fOnlyNeedingReason:
1256 sQuery += ' AND TestResultFailures.idTestSet IS NULL\n';
1257 if sGroupingField is not None:
1258 sQuery += ' AND %s = %d\n' % (sGroupingField, iResultsGroupingValue,);
1259 if sGroupingCondition is not None:
1260 sQuery += sGroupingCondition.replace(' AND ', ' AND ');
1261
1262 #
1263 # Execute the query and return the result.
1264 #
1265 self._oDb.execute(sQuery)
1266 return self._oDb.fetchOne()[0]
1267
1268 def getTestGroups(self, tsNow, sPeriod):
1269 """
1270 Get list of uniq TestGroupData objects which
1271 found in all test results.
1272 """
1273
1274 self._oDb.execute('SELECT DISTINCT TestGroups.*\n'
1275 'FROM TestGroups, TestSets\n'
1276 'WHERE TestSets.idTestGroup = TestGroups.idTestGroup\n'
1277 ' AND TestGroups.tsExpire > TestSets.tsCreated\n'
1278 ' AND TestGroups.tsEffective <= TestSets.tsCreated'
1279 ' AND ' + self._getTimePeriodQueryPart(tsNow, sPeriod))
1280 aaoRows = self._oDb.fetchAll()
1281 aoRet = []
1282 for aoRow in aaoRows:
1283 aoRet.append(TestGroupData().initFromDbRow(aoRow))
1284 return aoRet
1285
1286 def getBuilds(self, tsNow, sPeriod):
1287 """
1288 Get list of uniq BuildDataEx objects which
1289 found in all test results.
1290 """
1291
1292 self._oDb.execute('SELECT DISTINCT Builds.*, BuildCategories.*\n'
1293 'FROM Builds, BuildCategories, TestSets\n'
1294 'WHERE TestSets.idBuild = Builds.idBuild\n'
1295 ' AND Builds.idBuildCategory = BuildCategories.idBuildCategory\n'
1296 ' AND Builds.tsExpire > TestSets.tsCreated\n'
1297 ' AND Builds.tsEffective <= TestSets.tsCreated'
1298 ' AND ' + self._getTimePeriodQueryPart(tsNow, sPeriod))
1299 aaoRows = self._oDb.fetchAll()
1300 aoRet = []
1301 for aoRow in aaoRows:
1302 aoRet.append(BuildDataEx().initFromDbRow(aoRow))
1303 return aoRet
1304
1305 def getTestBoxes(self, tsNow, sPeriod):
1306 """
1307 Get list of uniq TestBoxData objects which
1308 found in all test results.
1309 """
1310 # Note! INNER JOIN TestBoxesWithStrings performs miserable compared to LEFT OUTER JOIN. Doesn't matter for the result
1311 # because TestSets.idGenTestBox is a foreign key and unique in TestBoxes. So, let's do what ever is faster.
1312 self._oDb.execute('SELECT TestBoxesWithStrings.*\n'
1313 'FROM ( SELECT idTestBox AS idTestBox,\n'
1314 ' MAX(idGenTestBox) AS idGenTestBox\n'
1315 ' FROM TestSets\n'
1316 ' WHERE ' + self._getTimePeriodQueryPart(tsNow, sPeriod, ' ') +
1317 ' GROUP BY idTestBox\n'
1318 ' ) AS TestBoxIDs\n'
1319 ' LEFT OUTER JOIN TestBoxesWithStrings\n'
1320 ' ON TestBoxesWithStrings.idGenTestBox = TestBoxIDs.idGenTestBox\n'
1321 'ORDER BY TestBoxesWithStrings.sName\n' );
1322 aoRet = []
1323 for aoRow in self._oDb.fetchAll():
1324 aoRet.append(TestBoxData().initFromDbRow(aoRow));
1325 return aoRet
1326
1327 def getTestCases(self, tsNow, sPeriod):
1328 """
1329 Get a list of unique TestCaseData objects which is appears in the test
1330 specified result period.
1331 """
1332
1333 # Using LEFT OUTER JOIN instead of INNER JOIN in case it performs better, doesn't matter for the result.
1334 self._oDb.execute('SELECT TestCases.*\n'
1335 'FROM ( SELECT idTestCase AS idTestCase,\n'
1336 ' MAX(idGenTestCase) AS idGenTestCase\n'
1337 ' FROM TestSets\n'
1338 ' WHERE ' + self._getTimePeriodQueryPart(tsNow, sPeriod, ' ') +
1339 ' GROUP BY idTestCase\n'
1340 ' ) AS TestCasesIDs\n'
1341 ' LEFT OUTER JOIN TestCases ON TestCases.idGenTestCase = TestCasesIDs.idGenTestCase\n'
1342 'ORDER BY TestCases.sName\n' );
1343
1344 aoRet = [];
1345 for aoRow in self._oDb.fetchAll():
1346 aoRet.append(TestCaseData().initFromDbRow(aoRow));
1347 return aoRet
1348
1349 def getOSes(self, tsNow, sPeriod):
1350 """
1351 Get a list of [idStrOs, sOs] tuples of the OSes that appears in the specified result period.
1352 """
1353
1354 # Note! INNER JOIN TestBoxesWithStrings performs miserable compared to LEFT OUTER JOIN. Doesn't matter for the result
1355 # because TestSets.idGenTestBox is a foreign key and unique in TestBoxes. So, let's do what ever is faster.
1356 self._oDb.execute('SELECT DISTINCT TestBoxesWithStrings.idStrOs, TestBoxesWithStrings.sOs\n'
1357 'FROM ( SELECT idTestBox AS idTestBox,\n'
1358 ' MAX(idGenTestBox) AS idGenTestBox\n'
1359 ' FROM TestSets\n'
1360 ' WHERE ' + self._getTimePeriodQueryPart(tsNow, sPeriod, ' ') +
1361 ' GROUP BY idTestBox\n'
1362 ' ) AS TestBoxIDs\n'
1363 ' LEFT OUTER JOIN TestBoxesWithStrings\n'
1364 ' ON TestBoxesWithStrings.idGenTestBox = TestBoxIDs.idGenTestBox\n'
1365 'ORDER BY TestBoxesWithStrings.sOs\n' );
1366 return self._oDb.fetchAll();
1367
1368 def getArchitectures(self, tsNow, sPeriod):
1369 """
1370 Get a list of [idStrCpuArch, sCpuArch] tuples of the architecutres
1371 that appears in the specified result period.
1372 """
1373
1374 # Note! INNER JOIN TestBoxesWithStrings performs miserable compared to LEFT OUTER JOIN. Doesn't matter for the result
1375 # because TestSets.idGenTestBox is a foreign key and unique in TestBoxes. So, let's do what ever is faster.
1376 self._oDb.execute('SELECT DISTINCT TestBoxesWithStrings.idStrCpuArch, TestBoxesWithStrings.sCpuArch\n'
1377 'FROM ( SELECT idTestBox AS idTestBox,\n'
1378 ' MAX(idGenTestBox) AS idGenTestBox\n'
1379 ' FROM TestSets\n'
1380 ' WHERE ' + self._getTimePeriodQueryPart(tsNow, sPeriod, ' ') +
1381 ' GROUP BY idTestBox\n'
1382 ' ) AS TestBoxIDs\n'
1383 ' LEFT OUTER JOIN TestBoxesWithStrings\n'
1384 ' ON TestBoxesWithStrings.idGenTestBox = TestBoxIDs.idGenTestBox\n'
1385 'ORDER BY TestBoxesWithStrings.sCpuArch\n' );
1386 return self._oDb.fetchAll();
1387
1388 def getBuildCategories(self, tsNow, sPeriod):
1389 """
1390 Get a list of BuildCategoryData that appears in the specified result period.
1391 """
1392
1393 self._oDb.execute('SELECT DISTINCT BuildCategories.*\n'
1394 'FROM ( SELECT DISTINCT idBuildCategory AS idBuildCategory\n'
1395 ' FROM TestSets\n'
1396 ' WHERE ' + self._getTimePeriodQueryPart(tsNow, sPeriod, ' ') +
1397 ' ) AS BuildCategoryIDs\n'
1398 ' LEFT OUTER JOIN BuildCategories\n'
1399 ' ON BuildCategories.idBuildCategory = BuildCategoryIDs.idBuildCategory\n'
1400 'ORDER BY BuildCategories.sProduct, BuildCategories.sBranch, BuildCategories.sType\n');
1401 aoRet = [];
1402 for aoRow in self._oDb.fetchAll():
1403 aoRet.append(BuildCategoryData().initFromDbRow(aoRow));
1404 return aoRet;
1405
1406 def getSchedGroups(self, tsNow, sPeriod):
1407 """
1408 Get list of uniq SchedGroupData objects which
1409 found in all test results.
1410 """
1411
1412 self._oDb.execute('SELECT SchedGroups.*\n'
1413 'FROM ( SELECT idSchedGroup,\n'
1414 ' MAX(TestSets.tsCreated) AS tsNow\n'
1415 ' FROM TestSets\n'
1416 ' WHERE ' + self._getTimePeriodQueryPart(tsNow, sPeriod, ' ') +
1417 ' GROUP BY idSchedGroup\n'
1418 ' ) AS SchedGroupIDs\n'
1419 ' INNER JOIN SchedGroups\n'
1420 ' ON SchedGroups.idSchedGroup = SchedGroupIDs.idSchedGroup\n'
1421 ' AND SchedGroups.tsExpire > SchedGroupIDs.tsNow\n'
1422 ' AND SchedGroups.tsEffective <= SchedGroupIDs.tsNow\n'
1423 'ORDER BY SchedGroups.sName\n' );
1424 aoRet = []
1425 for aoRow in self._oDb.fetchAll():
1426 aoRet.append(SchedGroupData().initFromDbRow(aoRow));
1427 return aoRet
1428
1429 def getById(self, idTestResult):
1430 """
1431 Get build record by its id
1432 """
1433 self._oDb.execute('SELECT *\n'
1434 'FROM TestResults\n'
1435 'WHERE idTestResult = %s\n',
1436 (idTestResult,))
1437
1438 aRows = self._oDb.fetchAll()
1439 if len(aRows) not in (0, 1):
1440 raise TMTooManyRows('Found more than one test result with the same credentials. Database structure is corrupted.')
1441 try:
1442 return TestResultData().initFromDbRow(aRows[0])
1443 except IndexError:
1444 return None
1445
1446 def fetchPossibleFilterOptions(self, oFilter, tsNow, sPeriod):
1447 """
1448 Fetches the available filter criteria, given the current filtering.
1449
1450 Returns oFilter.
1451 """
1452 assert isinstance(oFilter, TestResultFilter);
1453
1454 def workerDoFetch(oMissingLogicType, sNameAttr = 'sName', fIdIsName = False):
1455 """ Does the tedious result fetching and handling of missing bits. """
1456 dLeft = { oValue: 1 for oValue in oCrit.aoSelected };
1457 for aoRow in self._oDb.fetchAll():
1458 oCrit.aoPossible.append(FilterCriterionValueAndDescription(aoRow[0], aoRow[1]));
1459 if aoRow[0] in dLeft:
1460 del dLeft[aoRow[0]];
1461 if len(dLeft) > 0:
1462 if fIdIsName:
1463 for idMissing in dLeft:
1464 oCrit.aoPossible.append(FilterCriterionValueAndDescription(idMissing, idMissing, fIrrelevant = True));
1465 else:
1466 oMissingLogic = oMissingLogicType(self._oDb);
1467 for idMissing in dLeft:
1468 oMissing = oMissingLogic.cachedLookup(idMissing);
1469 if oMissing is not None:
1470 oCrit.aoPossible.append(FilterCriterionValueAndDescription(idMissing,
1471 getattr(oMissing, sNameAttr),
1472 fIrrelevant = True));
1473
1474 # Scheduling groups (see getSchedGroups).
1475 oCrit = oFilter.aCriteria[TestResultFilter.kiSchedGroups];
1476 self._oDb.execute('SELECT SchedGroups.idSchedGroup, SchedGroups.sName\n'
1477 'FROM ( SELECT TestSets.idSchedGroup,\n'
1478 ' MAX(TestSets.tsCreated) AS tsNow\n'
1479 ' FROM TestSets\n' + oFilter.getTableJoins(iOmit = TestResultFilter.kiSchedGroups) +
1480 ' WHERE ' + self._getTimePeriodQueryPart(tsNow, sPeriod, ' ') +
1481 oFilter.getWhereConditions(iOmit = TestResultFilter.kiSchedGroups) +
1482 ' GROUP BY TestSets.idSchedGroup\n'
1483 ' ) AS SchedGroupIDs\n'
1484 ' INNER JOIN SchedGroups\n'
1485 ' ON SchedGroups.idSchedGroup = SchedGroupIDs.idSchedGroup\n'
1486 ' AND SchedGroups.tsExpire > SchedGroupIDs.tsNow\n'
1487 ' AND SchedGroups.tsEffective <= SchedGroupIDs.tsNow\n'
1488 'ORDER BY SchedGroups.sName\n' );
1489 workerDoFetch(SchedGroupLogic);
1490
1491 # Testboxes (see getTestBoxes).
1492 oCrit = oFilter.aCriteria[TestResultFilter.kiTestBoxes];
1493 self._oDb.execute('SELECT TestBoxesWithStrings.idTestBox, TestBoxesWithStrings.sName\n'
1494 'FROM ( SELECT TestSets.idTestBox AS idTestBox,\n'
1495 ' MAX(TestSets.idGenTestBox) AS idGenTestBox\n'
1496 ' FROM TestSets\n' + oFilter.getTableJoins(iOmit = TestResultFilter.kiTestBoxes) +
1497 ' WHERE ' + self._getTimePeriodQueryPart(tsNow, sPeriod, ' ') +
1498 oFilter.getWhereConditions(iOmit = TestResultFilter.kiTestBoxes) +
1499 ' GROUP BY TestSets.idTestBox\n'
1500 ' ) AS TestBoxIDs\n'
1501 ' LEFT OUTER JOIN TestBoxesWithStrings\n'
1502 ' ON TestBoxesWithStrings.idGenTestBox = TestBoxIDs.idGenTestBox\n'
1503 'ORDER BY TestBoxesWithStrings.sName\n' );
1504 workerDoFetch(TestBoxLogic);
1505
1506 # Testbox OSes.
1507 oCrit = oFilter.aCriteria[TestResultFilter.kiOses];
1508 self._oDb.execute('SELECT DISTINCT TestBoxesWithStrings.idStrOs, TestBoxesWithStrings.sOs\n'
1509 'FROM ( SELECT DISTINCT TestSets.idGenTestBox\n'
1510 ' FROM TestSets\n' + oFilter.getTableJoins(iOmit = TestResultFilter.kiOses) +
1511 ' WHERE ' + self._getTimePeriodQueryPart(tsNow, sPeriod, ' ') +
1512 oFilter.getWhereConditions(iOmit = TestResultFilter.kiOses) +
1513 ' ) AS TestBoxGenIDs\n'
1514 ' LEFT OUTER JOIN TestBoxesWithStrings\n'
1515 ' ON TestBoxesWithStrings.idGenTestBox = TestBoxGenIDs.idGenTestBox\n'
1516 'ORDER BY TestBoxesWithStrings.sOs\n' );
1517 workerDoFetch(TestBoxLogic, 'sOs');
1518
1519 # Testbox OS versions .
1520 oCrit = oFilter.aCriteria[TestResultFilter.kiOsVersions];
1521 self._oDb.execute('SELECT DISTINCT TestBoxesWithStrings.idStrOsVersion, TestBoxesWithStrings.sOsVersion\n'
1522 'FROM ( SELECT DISTINCT TestSets.idGenTestBox AS idGenTestBox\n'
1523 ' FROM TestSets\n' + oFilter.getTableJoins(iOmit = TestResultFilter.kiOsVersions) +
1524 ' WHERE ' + self._getTimePeriodQueryPart(tsNow, sPeriod, ' ') +
1525 oFilter.getWhereConditions(iOmit = TestResultFilter.kiOsVersions) +
1526 ' ) AS TestBoxGenIDs\n'
1527 ' LEFT OUTER JOIN TestBoxesWithStrings\n'
1528 ' ON TestBoxesWithStrings.idGenTestBox = TestBoxGenIDs.idGenTestBox\n'
1529 'ORDER BY TestBoxesWithStrings.sOsVersion\n' );
1530 workerDoFetch(TestBoxLogic, 'sOsVersion');
1531
1532 # Testbox CPU/OS architectures.
1533 oCrit = oFilter.aCriteria[TestResultFilter.kiCpuArches];
1534 self._oDb.execute('SELECT DISTINCT TestBoxesWithStrings.idStrCpuArch, TestBoxesWithStrings.sCpuArch\n'
1535 'FROM ( SELECT DISTINCT TestSets.idGenTestBox\n'
1536 ' FROM TestSets\n' + oFilter.getTableJoins(iOmit = TestResultFilter.kiCpuArches) +
1537 ' WHERE ' + self._getTimePeriodQueryPart(tsNow, sPeriod, ' ') +
1538 oFilter.getWhereConditions(iOmit = TestResultFilter.kiCpuArches) +
1539 ' ) AS TestBoxGenIDs\n'
1540 ' LEFT OUTER JOIN TestBoxesWithStrings\n'
1541 ' ON TestBoxesWithStrings.idGenTestBox = TestBoxGenIDs.idGenTestBox\n'
1542 'ORDER BY TestBoxesWithStrings.sCpuArch\n' );
1543 workerDoFetch(TestBoxLogic, 'sCpuArch');
1544
1545 # Testbox CPU vendors.
1546 oCrit = oFilter.aCriteria[TestResultFilter.kiCpuVendors];
1547 self._oDb.execute('SELECT DISTINCT TestBoxesWithStrings.idStrCpuVendor, TestBoxesWithStrings.sCpuVendor\n'
1548 'FROM ( SELECT DISTINCT TestSets.idGenTestBox\n'
1549 ' FROM TestSets\n' + oFilter.getTableJoins(iOmit = TestResultFilter.kiCpuVendors) +
1550 ' WHERE ' + self._getTimePeriodQueryPart(tsNow, sPeriod, ' ') +
1551 oFilter.getWhereConditions(iOmit = TestResultFilter.kiCpuVendors) +
1552 ' ) AS TestBoxGenIDs\n'
1553 ' LEFT OUTER JOIN TestBoxesWithStrings\n'
1554 ' ON TestBoxesWithStrings.idGenTestBox = TestBoxGenIDs.idGenTestBox\n'
1555 'ORDER BY TestBoxesWithStrings.sCpuVendor\n' );
1556 workerDoFetch(TestBoxLogic, 'sCpuVendor');
1557
1558 # Testcases (see getTestCases).
1559 oCrit = oFilter.aCriteria[TestResultFilter.kiTestCases];
1560 self._oDb.execute('SELECT TestCases.idTestCase, TestCases.sName\n'
1561 'FROM ( SELECT TestSets.idTestCase AS idTestCase,\n'
1562 ' MAX(TestSets.idGenTestCase) AS idGenTestCase\n'
1563 ' FROM TestSets\n' + oFilter.getTableJoins(iOmit = TestResultFilter.kiTestCases) +
1564 ' WHERE ' + self._getTimePeriodQueryPart(tsNow, sPeriod, ' ') +
1565 oFilter.getWhereConditions(iOmit = TestResultFilter.kiTestCases) +
1566 ' GROUP BY TestSets.idTestCase\n'
1567 ' ) AS TestCasesIDs\n'
1568 ' LEFT OUTER JOIN TestCases ON TestCases.idGenTestCase = TestCasesIDs.idGenTestCase\n'
1569 'ORDER BY TestCases.sName\n' );
1570 workerDoFetch(TestCaseLogic);
1571
1572 # Build revisions.
1573 oCrit = oFilter.aCriteria[TestResultFilter.kiRevisions];
1574 self._oDb.execute('SELECT Builds.iRevision, CONCAT(\'r\', Builds.iRevision)\n'
1575 'FROM ( SELECT TestSets.idBuild AS idBuild,\n'
1576 ' MAX(TestSets.tsCreated) AS tsNow\n'
1577 ' FROM TestSets\n' + oFilter.getTableJoins(iOmit = TestResultFilter.kiRevisions) +
1578 ' WHERE ' + self._getTimePeriodQueryPart(tsNow, sPeriod, ' ') +
1579 oFilter.getWhereConditions(iOmit = TestResultFilter.kiRevisions) +
1580 ' GROUP BY TestSets.idBuild\n'
1581 ' ) AS BuildIDs\n'
1582 ' INNER JOIN Builds\n'
1583 ' ON Builds.idBuild = BuildIDs.idBuild\n'
1584 ' AND Builds.tsExpire > BuildIDs.tsNow\n'
1585 ' AND Builds.tsEffective <= BuildIDs.tsNow\n'
1586 'GROUP BY Builds.iRevision\n'
1587 'ORDER BY Builds.iRevision DESC\n' );
1588 workerDoFetch(BuildLogic);
1589
1590 # Build branches.
1591 oCrit = oFilter.aCriteria[TestResultFilter.kiBranches];
1592 self._oDb.execute('SELECT DISTINCT BuildCategories.sBranch, BuildCategories.sBranch\n'
1593 'FROM ( SELECT DISTINCT TestSets.idBuildCategory\n'
1594 ' FROM TestSets\n' + oFilter.getTableJoins(iOmit = TestResultFilter.kiBranches) +
1595 ' WHERE ' + self._getTimePeriodQueryPart(tsNow, sPeriod, ' ') +
1596 oFilter.getWhereConditions(iOmit = TestResultFilter.kiBranches) +
1597 ' ) AS BuildCategoryIDs\n'
1598 ' INNER JOIN BuildCategories\n'
1599 ' ON BuildCategories.idBuildCategory = BuildCategoryIDs.idBuildCategory\n'
1600 'ORDER BY BuildCategories.sBranch DESC\n' );
1601 workerDoFetch(BuildCategoryLogic, fIdIsName = True);
1602
1603 # Failure reasons.
1604 oCrit = oFilter.aCriteria[TestResultFilter.kiFailReasons];
1605 self._oDb.execute('SELECT FailureReasons.idFailureReason, FailureReasons.sShort\n'
1606 'FROM ( SELECT TestResultFailures.idFailureReason\n'
1607 ' FROM TestSets\n'
1608 ' INNER JOIN TestResultFailures\n'
1609 ' ON TestResultFailures.idTestSet = TestSets.idTestSet\n'
1610 ' AND TestResultFailures.tsExpire = \'infinity\'::TIMESTAMP\n' +
1611 oFilter.getTableJoins(iOmit = TestResultFilter.kiFailReasons) +
1612 ' WHERE ' + self._getTimePeriodQueryPart(tsNow, sPeriod, ' ') +
1613 oFilter.getWhereConditions(iOmit = TestResultFilter.kiFailReasons) +
1614 ' GROUP BY TestResultFailures.idFailureReason\n'
1615 ' ) AS FailureReasonIDs\n'
1616 ' INNER JOIN FailureReasons\n'
1617 ' ON FailureReasons.idFailureReason = FailureReasonIDs.idFailureReason\n'
1618 ' AND FailureReasons.tsExpire = \'infinity\'::TIMESTAMP\n'
1619 'ORDER BY FailureReasons.sShort\n' );
1620 workerDoFetch(FailureReasonLogic, 'sShort');
1621
1622 return oFilter;
1623
1624
1625 #
1626 # Details view and interface.
1627 #
1628
1629 def fetchResultTree(self, idTestSet, cMaxDepth = None):
1630 """
1631 Fetches the result tree for the given test set.
1632
1633 Returns a tree of TestResultDataEx nodes.
1634 Raises exception on invalid input and database issues.
1635 """
1636 # Depth first, i.e. just like the XML added them.
1637 ## @todo this still isn't performing extremely well, consider optimizations.
1638 sQuery = self._oDb.formatBindArgs(
1639 'SELECT TestResults.*,\n'
1640 ' TestResultStrTab.sValue,\n'
1641 ' EXISTS ( SELECT idTestResultValue\n'
1642 ' FROM TestResultValues\n'
1643 ' WHERE TestResultValues.idTestResult = TestResults.idTestResult ) AS fHasValues,\n'
1644 ' EXISTS ( SELECT idTestResultMsg\n'
1645 ' FROM TestResultMsgs\n'
1646 ' WHERE TestResultMsgs.idTestResult = TestResults.idTestResult ) AS fHasMsgs,\n'
1647 ' EXISTS ( SELECT idTestResultFile\n'
1648 ' FROM TestResultFiles\n'
1649 ' WHERE TestResultFiles.idTestResult = TestResults.idTestResult ) AS fHasFiles,\n'
1650 ' EXISTS ( SELECT idTestResult\n'
1651 ' FROM TestResultFailures\n'
1652 ' WHERE TestResultFailures.idTestResult = TestResults.idTestResult ) AS fHasReasons\n'
1653 'FROM TestResults, TestResultStrTab\n'
1654 'WHERE TestResults.idTestSet = %s\n'
1655 ' AND TestResults.idStrName = TestResultStrTab.idStr\n'
1656 , ( idTestSet, ));
1657 if cMaxDepth is not None:
1658 sQuery += self._oDb.formatBindArgs(' AND TestResults.iNestingDepth <= %s\n', (cMaxDepth,));
1659 sQuery += 'ORDER BY idTestResult ASC\n'
1660
1661 self._oDb.execute(sQuery);
1662 cRows = self._oDb.getRowCount();
1663 if cRows > 65536:
1664 raise TMTooManyRows('Too many rows returned for idTestSet=%d: %d' % (idTestSet, cRows,));
1665
1666 aaoRows = self._oDb.fetchAll();
1667 if len(aaoRows) == 0:
1668 raise TMRowNotFound('No test results for idTestSet=%d.' % (idTestSet,));
1669
1670 # Set up the root node first.
1671 aoRow = aaoRows[0];
1672 oRoot = TestResultDataEx().initFromDbRow(aoRow);
1673 if oRoot.idTestResultParent is not None:
1674 raise self._oDb.integrityException('The root TestResult (#%s) has a parent (#%s)!'
1675 % (oRoot.idTestResult, oRoot.idTestResultParent));
1676 self._fetchResultTreeNodeExtras(oRoot, aoRow[-4], aoRow[-3], aoRow[-2], aoRow[-1]);
1677
1678 # The chilren (if any).
1679 dLookup = { oRoot.idTestResult: oRoot };
1680 oParent = oRoot;
1681 for iRow in range(1, len(aaoRows)):
1682 aoRow = aaoRows[iRow];
1683 oCur = TestResultDataEx().initFromDbRow(aoRow);
1684 self._fetchResultTreeNodeExtras(oCur, aoRow[-4], aoRow[-3], aoRow[-2], aoRow[-1]);
1685
1686 # Figure out and vet the parent.
1687 if oParent.idTestResult != oCur.idTestResultParent:
1688 oParent = dLookup.get(oCur.idTestResultParent, None);
1689 if oParent is None:
1690 raise self._oDb.integrityException('TestResult #%d is orphaned from its parent #%s.'
1691 % (oCur.idTestResult, oCur.idTestResultParent,));
1692 if oParent.iNestingDepth + 1 != oCur.iNestingDepth:
1693 raise self._oDb.integrityException('TestResult #%d has incorrect nesting depth (%d instead of %d)'
1694 % (oCur.idTestResult, oCur.iNestingDepth, oParent.iNestingDepth + 1,));
1695
1696 # Link it up.
1697 oCur.oParent = oParent;
1698 oParent.aoChildren.append(oCur);
1699 dLookup[oCur.idTestResult] = oCur;
1700
1701 return (oRoot, dLookup);
1702
1703 def _fetchResultTreeNodeExtras(self, oCurNode, fHasValues, fHasMsgs, fHasFiles, fHasReasons):
1704 """
1705 fetchResultTree worker that fetches values, message and files for the
1706 specified node.
1707 """
1708 assert(oCurNode.aoValues == []);
1709 assert(oCurNode.aoMsgs == []);
1710 assert(oCurNode.aoFiles == []);
1711 assert(oCurNode.oReason is None);
1712
1713 if fHasValues:
1714 self._oDb.execute('SELECT TestResultValues.*,\n'
1715 ' TestResultStrTab.sValue\n'
1716 'FROM TestResultValues, TestResultStrTab\n'
1717 'WHERE TestResultValues.idTestResult = %s\n'
1718 ' AND TestResultValues.idStrName = TestResultStrTab.idStr\n'
1719 'ORDER BY idTestResultValue ASC\n'
1720 , ( oCurNode.idTestResult, ));
1721 for aoRow in self._oDb.fetchAll():
1722 oCurNode.aoValues.append(TestResultValueDataEx().initFromDbRow(aoRow));
1723
1724 if fHasMsgs:
1725 self._oDb.execute('SELECT TestResultMsgs.*,\n'
1726 ' TestResultStrTab.sValue\n'
1727 'FROM TestResultMsgs, TestResultStrTab\n'
1728 'WHERE TestResultMsgs.idTestResult = %s\n'
1729 ' AND TestResultMsgs.idStrMsg = TestResultStrTab.idStr\n'
1730 'ORDER BY idTestResultMsg ASC\n'
1731 , ( oCurNode.idTestResult, ));
1732 for aoRow in self._oDb.fetchAll():
1733 oCurNode.aoMsgs.append(TestResultMsgDataEx().initFromDbRow(aoRow));
1734
1735 if fHasFiles:
1736 self._oDb.execute('SELECT TestResultFiles.*,\n'
1737 ' StrTabFile.sValue AS sFile,\n'
1738 ' StrTabDesc.sValue AS sDescription,\n'
1739 ' StrTabKind.sValue AS sKind,\n'
1740 ' StrTabMime.sValue AS sMime\n'
1741 'FROM TestResultFiles,\n'
1742 ' TestResultStrTab AS StrTabFile,\n'
1743 ' TestResultStrTab AS StrTabDesc,\n'
1744 ' TestResultStrTab AS StrTabKind,\n'
1745 ' TestResultStrTab AS StrTabMime\n'
1746 'WHERE TestResultFiles.idTestResult = %s\n'
1747 ' AND TestResultFiles.idStrFile = StrTabFile.idStr\n'
1748 ' AND TestResultFiles.idStrDescription = StrTabDesc.idStr\n'
1749 ' AND TestResultFiles.idStrKind = StrTabKind.idStr\n'
1750 ' AND TestResultFiles.idStrMime = StrTabMime.idStr\n'
1751 'ORDER BY idTestResultFile ASC\n'
1752 , ( oCurNode.idTestResult, ));
1753 for aoRow in self._oDb.fetchAll():
1754 oCurNode.aoFiles.append(TestResultFileDataEx().initFromDbRow(aoRow));
1755
1756 if fHasReasons or True:
1757 if self.oFailureReasonLogic is None:
1758 self.oFailureReasonLogic = FailureReasonLogic(self._oDb);
1759 if self.oUserAccountLogic is None:
1760 self.oUserAccountLogic = UserAccountLogic(self._oDb);
1761 self._oDb.execute('SELECT *\n'
1762 'FROM TestResultFailures\n'
1763 'WHERE idTestResult = %s\n'
1764 ' AND tsExpire = \'infinity\'::TIMESTAMP\n'
1765 , ( oCurNode.idTestResult, ));
1766 if self._oDb.getRowCount() > 0:
1767 oCurNode.oReason = TestResultFailureDataEx().initFromDbRowEx(self._oDb.fetchOne(), self.oFailureReasonLogic,
1768 self.oUserAccountLogic);
1769
1770 return True;
1771
1772
1773
1774 #
1775 # TestBoxController interface(s).
1776 #
1777
1778 def _inhumeTestResults(self, aoStack, idTestSet, sError):
1779 """
1780 The test produces too much output, kill and bury it.
1781
1782 Note! We leave the test set open, only the test result records are
1783 completed. Thus, _getResultStack will return an empty stack and
1784 cause XML processing to fail immediately, while we can still
1785 record when it actually completed in the test set the normal way.
1786 """
1787 self._oDb.dprint('** _inhumeTestResults: idTestSet=%d\n%s' % (idTestSet, self._stringifyStack(aoStack),));
1788
1789 #
1790 # First add a message.
1791 #
1792 self._newFailureDetails(aoStack[0].idTestResult, idTestSet, sError, None);
1793
1794 #
1795 # The complete all open test results.
1796 #
1797 for oTestResult in aoStack:
1798 oTestResult.cErrors += 1;
1799 self._completeTestResults(oTestResult, None, TestResultData.ksTestStatus_Failure, oTestResult.cErrors);
1800
1801 # A bit of paranoia.
1802 self._oDb.execute('UPDATE TestResults\n'
1803 'SET cErrors = cErrors + 1,\n'
1804 ' enmStatus = \'failure\'::TestStatus_T,\n'
1805 ' tsElapsed = CURRENT_TIMESTAMP - tsCreated\n'
1806 'WHERE idTestSet = %s\n'
1807 ' AND enmStatus = \'running\'::TestStatus_T\n'
1808 , ( idTestSet, ));
1809 self._oDb.commit();
1810
1811 return None;
1812
1813 def strTabString(self, sString, fCommit = False):
1814 """
1815 Gets the string table id for the given string, adding it if new.
1816
1817 Note! A copy of this code is also in TestSetLogic.
1818 """
1819 ## @todo move this and make a stored procedure for it.
1820 self._oDb.execute('SELECT idStr\n'
1821 'FROM TestResultStrTab\n'
1822 'WHERE sValue = %s'
1823 , (sString,));
1824 if self._oDb.getRowCount() == 0:
1825 self._oDb.execute('INSERT INTO TestResultStrTab (sValue)\n'
1826 'VALUES (%s)\n'
1827 'RETURNING idStr\n'
1828 , (sString,));
1829 if fCommit:
1830 self._oDb.commit();
1831 return self._oDb.fetchOne()[0];
1832
1833 @staticmethod
1834 def _stringifyStack(aoStack):
1835 """Returns a string rep of the stack."""
1836 sRet = '';
1837 for i, _ in enumerate(aoStack):
1838 sRet += 'aoStack[%d]=%s\n' % (i, aoStack[i]);
1839 return sRet;
1840
1841 def _getResultStack(self, idTestSet):
1842 """
1843 Gets the current stack of result sets.
1844 """
1845 self._oDb.execute('SELECT *\n'
1846 'FROM TestResults\n'
1847 'WHERE idTestSet = %s\n'
1848 ' AND enmStatus = \'running\'::TestStatus_T\n'
1849 'ORDER BY idTestResult DESC'
1850 , ( idTestSet, ));
1851 aoStack = [];
1852 for aoRow in self._oDb.fetchAll():
1853 aoStack.append(TestResultData().initFromDbRow(aoRow));
1854
1855 for i, _ in enumerate(aoStack):
1856 assert aoStack[i].iNestingDepth == len(aoStack) - i - 1, self._stringifyStack(aoStack);
1857
1858 return aoStack;
1859
1860 def _newTestResult(self, idTestResultParent, idTestSet, iNestingDepth, tsCreated, sName, dCounts, fCommit = False):
1861 """
1862 Creates a new test result.
1863 Returns the TestResultData object for the new record.
1864 May raise exception on database error.
1865 """
1866 assert idTestResultParent is not None;
1867 assert idTestResultParent > 1;
1868
1869 #
1870 # This isn't necessarily very efficient, but it's necessary to prevent
1871 # a wild test or testbox from filling up the database.
1872 #
1873 sCountName = 'cTestResults';
1874 if sCountName not in dCounts:
1875 self._oDb.execute('SELECT COUNT(idTestResult)\n'
1876 'FROM TestResults\n'
1877 'WHERE idTestSet = %s\n'
1878 , ( idTestSet,));
1879 dCounts[sCountName] = self._oDb.fetchOne()[0];
1880 dCounts[sCountName] += 1;
1881 if dCounts[sCountName] > config.g_kcMaxTestResultsPerTS:
1882 raise TestResultHangingOffence('Too many sub-tests in total!');
1883
1884 sCountName = 'cTestResultsIn%d' % (idTestResultParent,);
1885 if sCountName not in dCounts:
1886 self._oDb.execute('SELECT COUNT(idTestResult)\n'
1887 'FROM TestResults\n'
1888 'WHERE idTestResultParent = %s\n'
1889 , ( idTestResultParent,));
1890 dCounts[sCountName] = self._oDb.fetchOne()[0];
1891 dCounts[sCountName] += 1;
1892 if dCounts[sCountName] > config.g_kcMaxTestResultsPerTR:
1893 raise TestResultHangingOffence('Too many immediate sub-tests!');
1894
1895 # This is also a hanging offence.
1896 if iNestingDepth > config.g_kcMaxTestResultDepth:
1897 raise TestResultHangingOffence('To deep sub-test nesting!');
1898
1899 # Ditto.
1900 if len(sName) > config.g_kcchMaxTestResultName:
1901 raise TestResultHangingOffence('Test name is too long: %d chars - "%s"' % (len(sName), sName));
1902
1903 #
1904 # Within bounds, do the job.
1905 #
1906 idStrName = self.strTabString(sName, fCommit);
1907 self._oDb.execute('INSERT INTO TestResults (\n'
1908 ' idTestResultParent,\n'
1909 ' idTestSet,\n'
1910 ' tsCreated,\n'
1911 ' idStrName,\n'
1912 ' iNestingDepth )\n'
1913 'VALUES (%s, %s, TIMESTAMP WITH TIME ZONE %s, %s, %s)\n'
1914 'RETURNING *\n'
1915 , ( idTestResultParent, idTestSet, tsCreated, idStrName, iNestingDepth) )
1916 oData = TestResultData().initFromDbRow(self._oDb.fetchOne());
1917
1918 self._oDb.maybeCommit(fCommit);
1919 return oData;
1920
1921 def _newTestValue(self, idTestResult, idTestSet, sName, lValue, sUnit, dCounts, tsCreated = None, fCommit = False):
1922 """
1923 Creates a test value.
1924 May raise exception on database error.
1925 """
1926
1927 #
1928 # Bounds checking.
1929 #
1930 sCountName = 'cTestValues';
1931 if sCountName not in dCounts:
1932 self._oDb.execute('SELECT COUNT(idTestResultValue)\n'
1933 'FROM TestResultValues, TestResults\n'
1934 'WHERE TestResultValues.idTestResult = TestResults.idTestResult\n'
1935 ' AND TestResults.idTestSet = %s\n'
1936 , ( idTestSet,));
1937 dCounts[sCountName] = self._oDb.fetchOne()[0];
1938 dCounts[sCountName] += 1;
1939 if dCounts[sCountName] > config.g_kcMaxTestValuesPerTS:
1940 raise TestResultHangingOffence('Too many values in total!');
1941
1942 sCountName = 'cTestValuesIn%d' % (idTestResult,);
1943 if sCountName not in dCounts:
1944 self._oDb.execute('SELECT COUNT(idTestResultValue)\n'
1945 'FROM TestResultValues\n'
1946 'WHERE idTestResult = %s\n'
1947 , ( idTestResult,));
1948 dCounts[sCountName] = self._oDb.fetchOne()[0];
1949 dCounts[sCountName] += 1;
1950 if dCounts[sCountName] > config.g_kcMaxTestValuesPerTR:
1951 raise TestResultHangingOffence('Too many immediate values for one test result!');
1952
1953 if len(sName) > config.g_kcchMaxTestValueName:
1954 raise TestResultHangingOffence('Value name is too long: %d chars - "%s"' % (len(sName), sName));
1955
1956 #
1957 # Do the job.
1958 #
1959 iUnit = constants.valueunit.g_kdNameToConst.get(sUnit, constants.valueunit.NONE);
1960
1961 idStrName = self.strTabString(sName, fCommit);
1962 if tsCreated is None:
1963 self._oDb.execute('INSERT INTO TestResultValues (\n'
1964 ' idTestResult,\n'
1965 ' idTestSet,\n'
1966 ' idStrName,\n'
1967 ' lValue,\n'
1968 ' iUnit)\n'
1969 'VALUES ( %s, %s, %s, %s, %s )\n'
1970 , ( idTestResult, idTestSet, idStrName, lValue, iUnit,) );
1971 else:
1972 self._oDb.execute('INSERT INTO TestResultValues (\n'
1973 ' idTestResult,\n'
1974 ' idTestSet,\n'
1975 ' tsCreated,\n'
1976 ' idStrName,\n'
1977 ' lValue,\n'
1978 ' iUnit)\n'
1979 'VALUES ( %s, %s, TIMESTAMP WITH TIME ZONE %s, %s, %s, %s )\n'
1980 , ( idTestResult, idTestSet, tsCreated, idStrName, lValue, iUnit,) );
1981 self._oDb.maybeCommit(fCommit);
1982 return True;
1983
1984 def _newFailureDetails(self, idTestResult, idTestSet, sText, dCounts, tsCreated = None, fCommit = False):
1985 """
1986 Creates a record detailing cause of failure.
1987 May raise exception on database error.
1988 """
1989
1990 #
1991 # Overflow protection.
1992 #
1993 if dCounts is not None:
1994 sCountName = 'cTestMsgsIn%d' % (idTestResult,);
1995 if sCountName not in dCounts:
1996 self._oDb.execute('SELECT COUNT(idTestResultMsg)\n'
1997 'FROM TestResultMsgs\n'
1998 'WHERE idTestResult = %s\n'
1999 , ( idTestResult,));
2000 dCounts[sCountName] = self._oDb.fetchOne()[0];
2001 dCounts[sCountName] += 1;
2002 if dCounts[sCountName] > config.g_kcMaxTestMsgsPerTR:
2003 raise TestResultHangingOffence('Too many messages under for one test result!');
2004
2005 if len(sText) > config.g_kcchMaxTestMsg:
2006 raise TestResultHangingOffence('Failure details message is too long: %d chars - "%s"' % (len(sText), sText));
2007
2008 #
2009 # Do the job.
2010 #
2011 idStrMsg = self.strTabString(sText, fCommit);
2012 if tsCreated is None:
2013 self._oDb.execute('INSERT INTO TestResultMsgs (\n'
2014 ' idTestResult,\n'
2015 ' idTestSet,\n'
2016 ' idStrMsg,\n'
2017 ' enmLevel)\n'
2018 'VALUES ( %s, %s, %s, %s)\n'
2019 , ( idTestResult, idTestSet, idStrMsg, 'failure',) );
2020 else:
2021 self._oDb.execute('INSERT INTO TestResultMsgs (\n'
2022 ' idTestResult,\n'
2023 ' idTestSet,\n'
2024 ' tsCreated,\n'
2025 ' idStrMsg,\n'
2026 ' enmLevel)\n'
2027 'VALUES ( %s, %s, TIMESTAMP WITH TIME ZONE %s, %s, %s)\n'
2028 , ( idTestResult, idTestSet, tsCreated, idStrMsg, 'failure',) );
2029
2030 self._oDb.maybeCommit(fCommit);
2031 return True;
2032
2033
2034 def _completeTestResults(self, oTestResult, tsDone, enmStatus, cErrors = 0, fCommit = False):
2035 """
2036 Completes a test result. Updates the oTestResult object.
2037 May raise exception on database error.
2038 """
2039 self._oDb.dprint('** _completeTestResults: cErrors=%s tsDone=%s enmStatus=%s oTestResults=\n%s'
2040 % (cErrors, tsDone, enmStatus, oTestResult,));
2041
2042 #
2043 # Sanity check: No open sub tests (aoStack should make sure about this!).
2044 #
2045 self._oDb.execute('SELECT COUNT(idTestResult)\n'
2046 'FROM TestResults\n'
2047 'WHERE idTestResultParent = %s\n'
2048 ' AND enmStatus = %s\n'
2049 , ( oTestResult.idTestResult, TestResultData.ksTestStatus_Running,));
2050 cOpenSubTest = self._oDb.fetchOne()[0];
2051 assert cOpenSubTest == 0, 'cOpenSubTest=%d - %s' % (cOpenSubTest, oTestResult,);
2052 assert oTestResult.enmStatus == TestResultData.ksTestStatus_Running;
2053
2054 #
2055 # Make sure the reporter isn't lying about successes or error counts.
2056 #
2057 self._oDb.execute('SELECT COALESCE(SUM(cErrors), 0)\n'
2058 'FROM TestResults\n'
2059 'WHERE idTestResultParent = %s\n'
2060 , ( oTestResult.idTestResult, ));
2061 cMinErrors = self._oDb.fetchOne()[0] + oTestResult.cErrors;
2062 if cErrors < cMinErrors:
2063 cErrors = cMinErrors;
2064 if cErrors > 0 and enmStatus == TestResultData.ksTestStatus_Success:
2065 enmStatus = TestResultData.ksTestStatus_Failure
2066
2067 #
2068 # Do the update.
2069 #
2070 if tsDone is None:
2071 self._oDb.execute('UPDATE TestResults\n'
2072 'SET cErrors = %s,\n'
2073 ' enmStatus = %s,\n'
2074 ' tsElapsed = CURRENT_TIMESTAMP - tsCreated\n'
2075 'WHERE idTestResult = %s\n'
2076 'RETURNING tsElapsed'
2077 , ( cErrors, enmStatus, oTestResult.idTestResult,) );
2078 else:
2079 self._oDb.execute('UPDATE TestResults\n'
2080 'SET cErrors = %s,\n'
2081 ' enmStatus = %s,\n'
2082 ' tsElapsed = TIMESTAMP WITH TIME ZONE %s - tsCreated\n'
2083 'WHERE idTestResult = %s\n'
2084 'RETURNING tsElapsed'
2085 , ( cErrors, enmStatus, tsDone, oTestResult.idTestResult,) );
2086
2087 oTestResult.tsElapsed = self._oDb.fetchOne()[0];
2088 oTestResult.enmStatus = enmStatus;
2089 oTestResult.cErrors = cErrors;
2090
2091 self._oDb.maybeCommit(fCommit);
2092 return None;
2093
2094 def _doPopHint(self, aoStack, cStackEntries, dCounts, idTestSet):
2095 """ Executes a PopHint. """
2096 assert cStackEntries >= 0;
2097 while len(aoStack) > cStackEntries:
2098 if aoStack[0].enmStatus == TestResultData.ksTestStatus_Running:
2099 self._newFailureDetails(aoStack[0].idTestResult, idTestSet, 'XML error: Missing </Test>', dCounts);
2100 self._completeTestResults(aoStack[0], tsDone = None, cErrors = 1,
2101 enmStatus = TestResultData.ksTestStatus_Failure, fCommit = True);
2102 aoStack.pop(0);
2103 return True;
2104
2105
2106 @staticmethod
2107 def _validateElement(sName, dAttribs, fClosed):
2108 """
2109 Validates an element and its attributes.
2110 """
2111
2112 #
2113 # Validate attributes by name.
2114 #
2115
2116 # Validate integer attributes.
2117 for sAttr in [ 'errors', 'testdepth' ]:
2118 if sAttr in dAttribs:
2119 try:
2120 _ = int(dAttribs[sAttr]);
2121 except:
2122 return 'Element %s has an invalid %s attribute value: %s.' % (sName, sAttr, dAttribs[sAttr],);
2123
2124 # Validate long attributes.
2125 for sAttr in [ 'value', ]:
2126 if sAttr in dAttribs:
2127 try:
2128 _ = long(dAttribs[sAttr]); # pylint: disable=R0204
2129 except:
2130 return 'Element %s has an invalid %s attribute value: %s.' % (sName, sAttr, dAttribs[sAttr],);
2131
2132 # Validate string attributes.
2133 for sAttr in [ 'name', 'text' ]: # 'unit' can be zero length.
2134 if sAttr in dAttribs and len(dAttribs[sAttr]) == 0:
2135 return 'Element %s has an empty %s attribute value.' % (sName, sAttr,);
2136
2137 # Validate the timestamp attribute.
2138 if 'timestamp' in dAttribs:
2139 (dAttribs['timestamp'], sError) = ModelDataBase.validateTs(dAttribs['timestamp'], fAllowNull = False);
2140 if sError is not None:
2141 return 'Element %s has an invalid timestamp ("%s"): %s' % (sName, dAttribs['timestamp'], sError,);
2142
2143
2144 #
2145 # Check that attributes that are required are present.
2146 # We ignore extra attributes.
2147 #
2148 dElementAttribs = \
2149 {
2150 'Test': [ 'timestamp', 'name', ],
2151 'Value': [ 'timestamp', 'name', 'unit', 'value', ],
2152 'FailureDetails': [ 'timestamp', 'text', ],
2153 'Passed': [ 'timestamp', ],
2154 'Skipped': [ 'timestamp', ],
2155 'Failed': [ 'timestamp', 'errors', ],
2156 'TimedOut': [ 'timestamp', 'errors', ],
2157 'End': [ 'timestamp', ],
2158 'PushHint': [ 'testdepth', ],
2159 'PopHint': [ 'testdepth', ],
2160 };
2161 if sName not in dElementAttribs:
2162 return 'Unknown element "%s".' % (sName,);
2163 for sAttr in dElementAttribs[sName]:
2164 if sAttr not in dAttribs:
2165 return 'Element %s requires attribute "%s".' % (sName, sAttr);
2166
2167 #
2168 # Only the Test element can (and must) remain open.
2169 #
2170 if sName == 'Test' and fClosed:
2171 return '<Test/> is not allowed.';
2172 if sName != 'Test' and not fClosed:
2173 return 'All elements except <Test> must be closed.';
2174
2175 return None;
2176
2177 @staticmethod
2178 def _parseElement(sElement):
2179 """
2180 Parses an element.
2181
2182 """
2183 #
2184 # Element level bits.
2185 #
2186 sName = sElement.split()[0];
2187 sElement = sElement[len(sName):];
2188
2189 fClosed = sElement[-1] == '/';
2190 if fClosed:
2191 sElement = sElement[:-1];
2192
2193 #
2194 # Attributes.
2195 #
2196 sError = None;
2197 dAttribs = {};
2198 sElement = sElement.strip();
2199 while len(sElement) > 0:
2200 # Extract attribute name.
2201 off = sElement.find('=');
2202 if off < 0 or not sElement[:off].isalnum():
2203 sError = 'Attributes shall have alpha numberical names and have values.';
2204 break;
2205 sAttr = sElement[:off];
2206
2207 # Extract attribute value.
2208 if off + 2 >= len(sElement) or sElement[off + 1] != '"':
2209 sError = 'Attribute (%s) value is missing or not in double quotes.' % (sAttr,);
2210 break;
2211 off += 2;
2212 offEndQuote = sElement.find('"', off);
2213 if offEndQuote < 0:
2214 sError = 'Attribute (%s) value is missing end quotation mark.' % (sAttr,);
2215 break;
2216 sValue = sElement[off:offEndQuote];
2217
2218 # Check for duplicates.
2219 if sAttr in dAttribs:
2220 sError = 'Attribute "%s" appears more than once.' % (sAttr,);
2221 break;
2222
2223 # Unescape the value.
2224 sValue = sValue.replace('&lt;', '<');
2225 sValue = sValue.replace('&gt;', '>');
2226 sValue = sValue.replace('&apos;', '\'');
2227 sValue = sValue.replace('&quot;', '"');
2228 sValue = sValue.replace('&#xA;', '\n');
2229 sValue = sValue.replace('&#xD;', '\r');
2230 sValue = sValue.replace('&amp;', '&'); # last
2231
2232 # Done.
2233 dAttribs[sAttr] = sValue;
2234
2235 # advance
2236 sElement = sElement[offEndQuote + 1:];
2237 sElement = sElement.lstrip();
2238
2239 #
2240 # Validate the element before we return.
2241 #
2242 if sError is None:
2243 sError = TestResultLogic._validateElement(sName, dAttribs, fClosed);
2244
2245 return (sName, dAttribs, sError)
2246
2247 def _handleElement(self, sName, dAttribs, idTestSet, aoStack, aaiHints, dCounts):
2248 """
2249 Worker for processXmlStream that handles one element.
2250
2251 Returns None on success, error string on bad XML or similar.
2252 Raises exception on hanging offence and on database error.
2253 """
2254 if sName == 'Test':
2255 iNestingDepth = aoStack[0].iNestingDepth + 1 if len(aoStack) > 0 else 0;
2256 aoStack.insert(0, self._newTestResult(idTestResultParent = aoStack[0].idTestResult, idTestSet = idTestSet,
2257 tsCreated = dAttribs['timestamp'], sName = dAttribs['name'],
2258 iNestingDepth = iNestingDepth, dCounts = dCounts, fCommit = True) );
2259
2260 elif sName == 'Value':
2261 self._newTestValue(idTestResult = aoStack[0].idTestResult, idTestSet = idTestSet, tsCreated = dAttribs['timestamp'],
2262 sName = dAttribs['name'], sUnit = dAttribs['unit'], lValue = long(dAttribs['value']),
2263 dCounts = dCounts, fCommit = True);
2264
2265 elif sName == 'FailureDetails':
2266 self._newFailureDetails(idTestResult = aoStack[0].idTestResult, idTestSet = idTestSet,
2267 tsCreated = dAttribs['timestamp'], sText = dAttribs['text'], dCounts = dCounts,
2268 fCommit = True);
2269
2270 elif sName == 'Passed':
2271 self._completeTestResults(aoStack[0], tsDone = dAttribs['timestamp'],
2272 enmStatus = TestResultData.ksTestStatus_Success, fCommit = True);
2273
2274 elif sName == 'Skipped':
2275 self._completeTestResults(aoStack[0], tsDone = dAttribs['timestamp'],
2276 enmStatus = TestResultData.ksTestStatus_Skipped, fCommit = True);
2277
2278 elif sName == 'Failed':
2279 self._completeTestResults(aoStack[0], tsDone = dAttribs['timestamp'], cErrors = int(dAttribs['errors']),
2280 enmStatus = TestResultData.ksTestStatus_Failure, fCommit = True);
2281
2282 elif sName == 'TimedOut':
2283 self._completeTestResults(aoStack[0], tsDone = dAttribs['timestamp'], cErrors = int(dAttribs['errors']),
2284 enmStatus = TestResultData.ksTestStatus_TimedOut, fCommit = True);
2285
2286 elif sName == 'End':
2287 self._completeTestResults(aoStack[0], tsDone = dAttribs['timestamp'],
2288 cErrors = int(dAttribs.get('errors', '1')),
2289 enmStatus = TestResultData.ksTestStatus_Success, fCommit = True);
2290
2291 elif sName == 'PushHint':
2292 if len(aaiHints) > 1:
2293 return 'PushHint cannot be nested.'
2294
2295 aaiHints.insert(0, [len(aoStack), int(dAttribs['testdepth'])]);
2296
2297 elif sName == 'PopHint':
2298 if len(aaiHints) < 1:
2299 return 'No hint to pop.'
2300
2301 iDesiredTestDepth = int(dAttribs['testdepth']);
2302 cStackEntries, iTestDepth = aaiHints.pop(0);
2303 self._doPopHint(aoStack, cStackEntries, dCounts, idTestSet); # Fake the necessary '<End/></Test>' tags.
2304 if iDesiredTestDepth != iTestDepth:
2305 return 'PopHint tag has different testdepth: %d, on stack %d.' % (iDesiredTestDepth, iTestDepth);
2306 else:
2307 return 'Unexpected element "%s".' % (sName,);
2308 return None;
2309
2310
2311 def processXmlStream(self, sXml, idTestSet):
2312 """
2313 Processes the "XML" stream section given in sXml.
2314
2315 The sXml isn't a complete XML document, even should we save up all sXml
2316 for a given set, they may not form a complete and well formed XML
2317 document since the test may be aborted, abend or simply be buggy. We
2318 therefore do our own parsing and treat the XML tags as commands more
2319 than anything else.
2320
2321 Returns (sError, fUnforgivable), where sError is None on success.
2322 May raise database exception.
2323 """
2324 aoStack = self._getResultStack(idTestSet); # [0] == top; [-1] == bottom.
2325 if len(aoStack) == 0:
2326 return ('No open results', True);
2327 self._oDb.dprint('** processXmlStream len(aoStack)=%s' % (len(aoStack),));
2328 #self._oDb.dprint('processXmlStream: %s' % (self._stringifyStack(aoStack),));
2329 #self._oDb.dprint('processXmlStream: sXml=%s' % (sXml,));
2330
2331 dCounts = {};
2332 aaiHints = [];
2333 sError = None;
2334
2335 fExpectCloseTest = False;
2336 sXml = sXml.strip();
2337 while len(sXml) > 0:
2338 if sXml.startswith('</Test>'): # Only closing tag.
2339 offNext = len('</Test>');
2340 if len(aoStack) <= 1:
2341 sError = 'Trying to close the top test results.'
2342 break;
2343 # ASSUMES that we've just seen an <End/>, <Passed/>, <Failed/>,
2344 # <TimedOut/> or <Skipped/> tag earlier in this call!
2345 if aoStack[0].enmStatus == TestResultData.ksTestStatus_Running or not fExpectCloseTest:
2346 sError = 'Missing <End/>, <Passed/>, <Failed/>, <TimedOut/> or <Skipped/> tag.';
2347 break;
2348 aoStack.pop(0);
2349 fExpectCloseTest = False;
2350
2351 elif fExpectCloseTest:
2352 sError = 'Expected </Test>.'
2353 break;
2354
2355 elif sXml.startswith('<?xml '): # Ignore (included files).
2356 offNext = sXml.find('?>');
2357 if offNext < 0:
2358 sError = 'Unterminated <?xml ?> element.';
2359 break;
2360 offNext += 2;
2361
2362 elif sXml[0] == '<':
2363 # Parse and check the tag.
2364 if not sXml[1].isalpha():
2365 sError = 'Malformed element.';
2366 break;
2367 offNext = sXml.find('>')
2368 if offNext < 0:
2369 sError = 'Unterminated element.';
2370 break;
2371 (sName, dAttribs, sError) = self._parseElement(sXml[1:offNext]);
2372 offNext += 1;
2373 if sError is not None:
2374 break;
2375
2376 # Handle it.
2377 try:
2378 sError = self._handleElement(sName, dAttribs, idTestSet, aoStack, aaiHints, dCounts);
2379 except TestResultHangingOffence as oXcpt:
2380 self._inhumeTestResults(aoStack, idTestSet, str(oXcpt));
2381 return (str(oXcpt), True);
2382
2383
2384 fExpectCloseTest = sName in [ 'End', 'Passed', 'Failed', 'TimedOut', 'Skipped', ];
2385 else:
2386 sError = 'Unexpected content.';
2387 break;
2388
2389 # Advance.
2390 sXml = sXml[offNext:];
2391 sXml = sXml.lstrip();
2392
2393 #
2394 # Post processing checks.
2395 #
2396 if sError is None and fExpectCloseTest:
2397 sError = 'Expected </Test> before the end of the XML section.'
2398 elif sError is None and len(aaiHints) > 0:
2399 sError = 'Expected </PopHint> before the end of the XML section.'
2400 if len(aaiHints) > 0:
2401 self._doPopHint(aoStack, aaiHints[-1][0], dCounts, idTestSet);
2402
2403 #
2404 # Log the error.
2405 #
2406 if sError is not None:
2407 SystemLogLogic(self._oDb).addEntry(SystemLogData.ksEvent_XmlResultMalformed,
2408 'idTestSet=%s idTestResult=%s XML="%s" %s'
2409 % ( idTestSet,
2410 aoStack[0].idTestResult if len(aoStack) > 0 else -1,
2411 sXml[:30 if len(sXml) >= 30 else len(sXml)],
2412 sError, ),
2413 cHoursRepeat = 6, fCommit = True);
2414 return (sError, False);
2415
2416
2417
2418
2419
2420#
2421# Unit testing.
2422#
2423
2424# pylint: disable=C0111
2425class TestResultDataTestCase(ModelDataBaseTestCase):
2426 def setUp(self):
2427 self.aoSamples = [TestResultData(),];
2428
2429class TestResultValueDataTestCase(ModelDataBaseTestCase):
2430 def setUp(self):
2431 self.aoSamples = [TestResultValueData(),];
2432
2433if __name__ == '__main__':
2434 unittest.main();
2435 # not reached.
2436
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