VirtualBox

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

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

testmanager: new result groupings; tab order hack.

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