VirtualBox

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

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

testmanager,testdriver: Add a bunch of 'info' items to the failed tests in case they contain anything useful. vgatext is separate for simplified (virtual) sheriffing.

  • Property svn:eol-style set to native
  • Property svn:keywords set to Author Date Id Revision
File size: 87.0 KB
Line 
1# -*- coding: utf-8 -*-
2# $Id: testresults.py 61511 2016-06-06 23:51:45Z 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-2015 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: 61511 $"
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;
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 ksResultsGroupingTypeBuildRev = 'ResultsGroupingTypeBuild';
654 ksResultsGroupingTypeTestBox = 'ResultsGroupingTypeTestBox';
655 ksResultsGroupingTypeTestCase = 'ResultsGroupingTypeTestCase';
656 ksResultsGroupingTypeSchedGroup = 'ResultsGroupingTypeSchedGroup';
657
658 ## @name Result sorting options.
659 ## @{
660 ksResultsSortByRunningAndStart = 'ResultsSortByRunningAndStart'; ##< Default
661 ksResultsSortByBuildRevision = 'ResultsSortByBuildRevision';
662 ksResultsSortByTestBoxName = 'ResultsSortByTestBoxName';
663 ksResultsSortByTestBoxOs = 'ResultsSortByTestBoxOs';
664 ksResultsSortByTestBoxOsVersion = 'ResultsSortByTestBoxOsVersion';
665 ksResultsSortByTestBoxOsArch = 'ResultsSortByTestBoxOsArch';
666 ksResultsSortByTestBoxArch = 'ResultsSortByTestBoxArch';
667 ksResultsSortByTestBoxCpuVendor = 'ResultsSortByTestBoxCpuVendor';
668 ksResultsSortByTestBoxCpuName = 'ResultsSortByTestBoxCpuName';
669 ksResultsSortByTestBoxCpuRev = 'ResultsSortByTestBoxCpuRev';
670 ksResultsSortByTestBoxCpuFeatures = 'ResultsSortByTestBoxCpuFeatures';
671 ksResultsSortByTestCaseName = 'ResultsSortByTestCaseName';
672 ksResultsSortByFailureReason = 'ResultsSortByFailureReason';
673 kasResultsSortBy = {
674 ksResultsSortByRunningAndStart,
675 ksResultsSortByBuildRevision,
676 ksResultsSortByTestBoxName,
677 ksResultsSortByTestBoxOs,
678 ksResultsSortByTestBoxOsVersion,
679 ksResultsSortByTestBoxOsArch,
680 ksResultsSortByTestBoxArch,
681 ksResultsSortByTestBoxCpuVendor,
682 ksResultsSortByTestBoxCpuName,
683 ksResultsSortByTestBoxCpuRev,
684 ksResultsSortByTestBoxCpuFeatures,
685 ksResultsSortByTestCaseName,
686 ksResultsSortByFailureReason,
687 };
688 ## Used by the WUI for generating the drop down.
689 kaasResultsSortByTitles = (
690 ( ksResultsSortByRunningAndStart, 'Running & Start TS' ),
691 ( ksResultsSortByBuildRevision, 'Build Revision' ),
692 ( ksResultsSortByTestBoxName, 'TestBox Name' ),
693 ( ksResultsSortByTestBoxOs, 'O/S' ),
694 ( ksResultsSortByTestBoxOsVersion, 'O/S Version' ),
695 ( ksResultsSortByTestBoxOsArch, 'O/S & Architecture' ),
696 ( ksResultsSortByTestBoxArch, 'Architecture' ),
697 ( ksResultsSortByTestBoxCpuVendor, 'CPU Vendor' ),
698 ( ksResultsSortByTestBoxCpuName, 'CPU Vendor & Name' ),
699 ( ksResultsSortByTestBoxCpuRev, 'CPU Vendor & Revision' ),
700 ( ksResultsSortByTestBoxCpuFeatures, 'CPU Features' ),
701 ( ksResultsSortByTestCaseName, 'Test Case Name' ),
702 ( ksResultsSortByFailureReason, 'Failure Reason' ),
703 );
704 ## @}
705
706 ## Default sort by map.
707 kdResultSortByMap = {
708 ksResultsSortByRunningAndStart: ( '', None, None, '', '' ),
709 ksResultsSortByBuildRevision: (
710 # Sorting tables.
711 ', Builds',
712 # Sorting table join(s).
713 ' AND TestSets.idBuild = Builds.idBuild'
714 ' AND Builds.tsExpire >= TestSets.tsCreated'
715 ' AND Builds.tsEffective <= TestSets.tsCreated',
716 # Start of ORDER BY statement.
717 ' Builds.iRevision DESC',
718 # Extra columns to fetch for the above ORDER BY to work in a SELECT DISTINCT statement.
719 '',
720 # Columns for the GROUP BY
721 ''),
722 ksResultsSortByTestBoxName: (
723 ', TestBoxes',
724 ' AND TestSets.idGenTestBox = TestBoxes.idGenTestBox',
725 ' TestBoxes.sName DESC',
726 '', '' ),
727 ksResultsSortByTestBoxOsArch: (
728 ', TestBoxesWithStrings',
729 ' AND TestSets.idGenTestBox = TestBoxesWithStrings.idGenTestBox',
730 ' TestBoxesWithStrings.sOs, TestBoxesWithStrings.sCpuArch',
731 '', '' ),
732 ksResultsSortByTestBoxOs: (
733 ', TestBoxesWithStrings',
734 ' AND TestSets.idGenTestBox = TestBoxesWithStrings.idGenTestBox',
735 ' TestBoxesWithStrings.sOs',
736 '', '' ),
737 ksResultsSortByTestBoxOsVersion: (
738 ', TestBoxesWithStrings',
739 ' AND TestSets.idGenTestBox = TestBoxesWithStrings.idGenTestBox',
740 ' TestBoxesWithStrings.sOs, TestBoxesWithStrings.sOsVersion DESC',
741 '', '' ),
742 ksResultsSortByTestBoxArch: (
743 ', TestBoxesWithStrings',
744 ' AND TestSets.idGenTestBox = TestBoxesWithStrings.idGenTestBox',
745 ' TestBoxesWithStrings.sCpuArch',
746 '', '' ),
747 ksResultsSortByTestBoxCpuVendor: (
748 ', TestBoxesWithStrings',
749 ' AND TestSets.idGenTestBox = TestBoxesWithStrings.idGenTestBox',
750 ' TestBoxesWithStrings.sCpuVendor',
751 '', '' ),
752 ksResultsSortByTestBoxCpuName: (
753 ', TestBoxesWithStrings',
754 ' AND TestSets.idGenTestBox = TestBoxesWithStrings.idGenTestBox',
755 ' TestBoxesWithStrings.sCpuVendor, TestBoxesWithStrings.sCpuName',
756 '', '' ),
757 ksResultsSortByTestBoxCpuRev: (
758 ', TestBoxesWithStrings',
759 ' AND TestSets.idGenTestBox = TestBoxesWithStrings.idGenTestBox',
760 ' TestBoxesWithStrings.sCpuVendor, TestBoxesWithStrings.lCpuRevision DESC',
761 ', TestBoxesWithStrings.lCpuRevision',
762 ', TestBoxesWithStrings.lCpuRevision' ),
763 ksResultsSortByTestBoxCpuFeatures: (
764 ', TestBoxes',
765 ' AND TestSets.idGenTestBox = TestBoxes.idGenTestBox',
766 ' TestBoxes.fCpuHwVirt DESC, TestBoxes.fCpuNestedPaging DESC, TestBoxes.fCpu64BitGuest DESC, TestBoxes.cCpus DESC',
767 '',
768 '' ),
769 ksResultsSortByTestCaseName: (
770 ', TestCases',
771 ' AND TestSets.idGenTestCase = TestCases.idGenTestCase',
772 ' TestCases.sName',
773 '', '' ),
774 ksResultsSortByFailureReason: (
775 '', '',
776 'asSortByFailureReason ASC',
777 ', array_agg(FailureReasons.sShort ORDER BY TestResultFailures.idTestResult) AS asSortByFailureReason',
778 '' ),
779 };
780
781 kdResultGroupingMap = {
782 ksResultsGroupingTypeNone: (
783 # Grouping tables;
784 '',
785 # Grouping field;
786 None,
787 # Grouping where addition.
788 None,
789 # Sort by overrides.
790 {},
791 ),
792 ksResultsGroupingTypeTestGroup: ('', 'TestSets.idTestGroup', None, {},),
793 ksResultsGroupingTypeTestBox: ('', 'TestSets.idTestBox', None, {},),
794 ksResultsGroupingTypeTestCase: ('', 'TestSets.idTestCase', None, {},),
795 ksResultsGroupingTypeBuildRev: (
796 ', Builds',
797 'Builds.iRevision',
798 ' AND Builds.idBuild = TestSets.idBuild'
799 ' AND Builds.tsExpire > TestSets.tsCreated'
800 ' AND Builds.tsEffective <= TestSets.tsCreated',
801 { ksResultsSortByBuildRevision: ( '', None, ' Builds.iRevision DESC' ), }
802 ),
803 ksResultsGroupingTypeSchedGroup: ( '', 'TestSets.idSchedGroup', None, {},),
804 };
805
806
807 def __init__(self, oDb):
808 ModelLogicBase.__init__(self, oDb)
809 self.oFailureReasonLogic = None;
810 self.oUserAccountLogic = None;
811
812 def _getTimePeriodQueryPart(self, tsNow, sInterval, sExtraIndent = ''):
813 """
814 Get part of SQL query responsible for SELECT data within
815 specified period of time.
816 """
817 assert sInterval is not None; # too many rows.
818
819 cMonthsMourningPeriod = 2; # Stop reminding everyone about testboxes after 2 months. (May also speed up the query.)
820 if tsNow is None:
821 sRet = '(TestSets.tsDone IS NULL OR TestSets.tsDone >= (CURRENT_TIMESTAMP - \'%s\'::interval))\n' \
822 '%s AND TestSets.tsCreated >= (CURRENT_TIMESTAMP - \'%s\'::interval - \'%u months\'::interval)\n' \
823 % ( sInterval,
824 sExtraIndent, sInterval, cMonthsMourningPeriod);
825 else:
826 sTsNow = '\'%s\'::TIMESTAMP' % (tsNow,); # It's actually a string already. duh.
827 sRet = 'TestSets.tsCreated <= %s\n' \
828 '%s AND TestSets.tsCreated >= (%s - \'%s\'::interval - \'%u months\'::interval)\n' \
829 '%s AND (TestSets.tsDone IS NULL OR TestSets.tsDone >= (%s - \'%s\'::interval))\n' \
830 % ( sTsNow,
831 sExtraIndent, sTsNow, sInterval, cMonthsMourningPeriod,
832 sExtraIndent, sTsNow, sInterval );
833 return sRet
834
835 def fetchResultsForListing(self, iStart, cMaxRows, tsNow, sInterval, enmResultSortBy, # pylint: disable=R0913
836 enmResultsGroupingType, iResultsGroupingValue, fOnlyFailures, fOnlyNeedingReason):
837 """
838 Fetches TestResults table content.
839
840 If @param enmResultsGroupingType and @param iResultsGroupingValue
841 are not None, then resulting (returned) list contains only records
842 that match specified @param enmResultsGroupingType.
843
844 If @param enmResultsGroupingType is None, then
845 @param iResultsGroupingValue is ignored.
846
847 Returns an array (list) of TestResultData items, empty list if none.
848 Raises exception on error.
849 """
850
851 #
852 # Get SQL query parameters
853 #
854 if enmResultsGroupingType is None or enmResultsGroupingType not in self.kdResultGroupingMap:
855 raise TMExceptionBase('Unknown grouping type');
856 if enmResultSortBy is None or enmResultSortBy not in self.kasResultsSortBy:
857 raise TMExceptionBase('Unknown sorting');
858 sGroupingTables, sGroupingField, sGroupingCondition, dSortingOverrides = self.kdResultGroupingMap[enmResultsGroupingType];
859 if enmResultSortBy in dSortingOverrides:
860 sSortTables, sSortWhere, sSortOrderBy, sSortColumns, sSortGroupBy = dSortingOverrides[enmResultSortBy];
861 else:
862 sSortTables, sSortWhere, sSortOrderBy, sSortColumns, sSortGroupBy = self.kdResultSortByMap[enmResultSortBy];
863
864 #
865 # Construct the query.
866 #
867 sQuery = 'SELECT DISTINCT TestSets.idTestSet,\n' \
868 ' BuildCategories.idBuildCategory,\n' \
869 ' BuildCategories.sProduct,\n' \
870 ' BuildCategories.sRepository,\n' \
871 ' BuildCategories.sBranch,\n' \
872 ' BuildCategories.sType,\n' \
873 ' Builds.idBuild,\n' \
874 ' Builds.sVersion,\n' \
875 ' Builds.iRevision,\n' \
876 ' TestBoxesWithStrings.sOs,\n' \
877 ' TestBoxesWithStrings.sOsVersion,\n' \
878 ' TestBoxesWithStrings.sCpuArch,\n' \
879 ' TestBoxesWithStrings.sCpuVendor,\n' \
880 ' TestBoxesWithStrings.sCpuName,\n' \
881 ' TestBoxesWithStrings.cCpus,\n' \
882 ' TestBoxesWithStrings.fCpuHwVirt,\n' \
883 ' TestBoxesWithStrings.fCpuNestedPaging,\n' \
884 ' TestBoxesWithStrings.fCpu64BitGuest,\n' \
885 ' TestBoxesWithStrings.idTestBox,\n' \
886 ' TestBoxesWithStrings.sName,\n' \
887 ' TestResults.tsCreated,\n' \
888 ' COALESCE(TestResults.tsElapsed, CURRENT_TIMESTAMP - TestResults.tsCreated) AS tsElapsedTestResult,\n' \
889 ' TestSets.enmStatus,\n' \
890 ' TestResults.cErrors,\n' \
891 ' TestCases.idTestCase,\n' \
892 ' TestCases.sName,\n' \
893 ' TestCases.sBaseCmd,\n' \
894 ' TestCaseArgs.sArgs,\n' \
895 ' TestCaseArgs.sSubName,\n' \
896 ' TestSuiteBits.idBuild AS idBuildTestSuite,\n' \
897 ' TestSuiteBits.iRevision AS iRevisionTestSuite,\n' \
898 ' array_agg(TestResultFailures.idFailureReason ORDER BY TestResultFailures.idTestResult),\n' \
899 ' array_agg(TestResultFailures.uidAuthor ORDER BY TestResultFailures.idTestResult),\n' \
900 ' array_agg(TestResultFailures.tsEffective ORDER BY TestResultFailures.idTestResult),\n' \
901 ' array_agg(TestResultFailures.sComment ORDER BY TestResultFailures.idTestResult),\n' \
902 ' (TestSets.tsDone IS NULL) SortRunningFirst' + sSortColumns + '\n' \
903 'FROM ( SELECT TestSets.idTestSet AS idTestSet,\n' \
904 ' TestSets.tsDone AS tsDone,\n' \
905 ' TestSets.tsCreated AS tsCreated,\n' \
906 ' TestSets.enmStatus AS enmStatus,\n' \
907 ' TestSets.idBuild AS idBuild,\n' \
908 ' TestSets.idBuildTestSuite AS idBuildTestSuite,\n' \
909 ' TestSets.idGenTestBox AS idGenTestBox,\n' \
910 ' TestSets.idGenTestCase AS idGenTestCase,\n' \
911 ' TestSets.idGenTestCaseArgs AS idGenTestCaseArgs\n' \
912 ' FROM TestSets';
913 if fOnlyNeedingReason:
914 sQuery += '\n' \
915 ' LEFT OUTER JOIN TestResultFailures\n' \
916 ' ON TestSets.idTestSet = TestResultFailures.idTestSet\n' \
917 ' AND TestResultFailures.tsExpire = \'infinity\'::TIMESTAMP';
918 sQuery += sGroupingTables.replace(',', ',\n ');
919 sQuery += sSortTables.replace( ',', ',\n ');
920 sQuery += '\n' \
921 ' WHERE ' + self._getTimePeriodQueryPart(tsNow, sInterval, ' ');
922 if fOnlyFailures or fOnlyNeedingReason:
923 sQuery += ' AND TestSets.enmStatus != \'success\'::TestStatus_T\n' \
924 ' AND TestSets.enmStatus != \'running\'::TestStatus_T\n';
925 if fOnlyNeedingReason:
926 sQuery += ' AND TestResultFailures.idTestSet IS NULL\n';
927 if sGroupingField is not None:
928 sQuery += ' AND %s = %d\n' % (sGroupingField, iResultsGroupingValue,);
929 if sGroupingCondition is not None:
930 sQuery += sGroupingCondition.replace(' AND ', ' AND ');
931 if sSortWhere is not None:
932 sQuery += sSortWhere.replace(' AND ', ' AND ');
933 sQuery += ' ORDER BY ';
934 if sSortOrderBy is not None and sSortOrderBy.find('FailureReason') < 0:
935 sQuery += sSortOrderBy + ',\n ';
936 sQuery += '(TestSets.tsDone IS NULL) DESC, TestSets.idTestSet DESC\n' \
937 ' LIMIT %s OFFSET %s\n' % (cMaxRows, iStart,);
938
939 # Note! INNER JOIN TestBoxesWithStrings performs miserable compared to LEFT OUTER JOIN. Doesn't matter for the result
940 # because TestSets.idGenTestBox is a foreign key and unique in TestBoxes. So, let's do what ever is faster.
941 sQuery += ' ) AS TestSets\n' \
942 ' LEFT OUTER JOIN TestBoxesWithStrings\n' \
943 ' ON TestSets.idGenTestBox = TestBoxesWithStrings.idGenTestBox' \
944 ' LEFT OUTER JOIN Builds AS TestSuiteBits\n' \
945 ' ON TestSets.idBuildTestSuite = TestSuiteBits.idBuild\n' \
946 ' LEFT OUTER JOIN TestResultFailures\n' \
947 ' ON TestSets.idTestSet = TestResultFailures.idTestSet\n' \
948 ' AND TestResultFailures.tsExpire = \'infinity\'::TIMESTAMP';
949 if sSortOrderBy is not None and sSortOrderBy.find('FailureReason') >= 0:
950 sQuery += '\n' \
951 ' LEFT OUTER JOIN FailureReasons\n' \
952 ' ON TestResultFailures.idFailureReason = FailureReasons.idFailureReason\n' \
953 ' AND FailureReasons.tsExpire = \'infinity\'::TIMESTAMP';
954 sQuery += ',\n' \
955 ' BuildCategories,\n' \
956 ' Builds,\n' \
957 ' TestResults,\n' \
958 ' TestCases,\n' \
959 ' TestCaseArgs\n';
960 sQuery += 'WHERE TestSets.idTestSet = TestResults.idTestSet\n' \
961 ' AND TestResults.idTestResultParent is NULL\n' \
962 ' AND TestSets.idBuild = Builds.idBuild\n' \
963 ' AND Builds.tsExpire > TestSets.tsCreated\n' \
964 ' AND Builds.tsEffective <= TestSets.tsCreated\n' \
965 ' AND Builds.idBuildCategory = BuildCategories.idBuildCategory\n' \
966 ' AND TestSets.idGenTestCase = TestCases.idGenTestCase\n' \
967 ' AND TestSets.idGenTestCaseArgs = TestCaseArgs.idGenTestCaseArgs\n';
968 sQuery += 'GROUP BY TestSets.idTestSet,\n' \
969 ' BuildCategories.idBuildCategory,\n' \
970 ' BuildCategories.sProduct,\n' \
971 ' BuildCategories.sRepository,\n' \
972 ' BuildCategories.sBranch,\n' \
973 ' BuildCategories.sType,\n' \
974 ' Builds.idBuild,\n' \
975 ' Builds.sVersion,\n' \
976 ' Builds.iRevision,\n' \
977 ' TestBoxesWithStrings.sOs,\n' \
978 ' TestBoxesWithStrings.sOsVersion,\n' \
979 ' TestBoxesWithStrings.sCpuArch,\n' \
980 ' TestBoxesWithStrings.sCpuVendor,\n' \
981 ' TestBoxesWithStrings.sCpuName,\n' \
982 ' TestBoxesWithStrings.cCpus,\n' \
983 ' TestBoxesWithStrings.fCpuHwVirt,\n' \
984 ' TestBoxesWithStrings.fCpuNestedPaging,\n' \
985 ' TestBoxesWithStrings.fCpu64BitGuest,\n' \
986 ' TestBoxesWithStrings.idTestBox,\n' \
987 ' TestBoxesWithStrings.sName,\n' \
988 ' TestResults.tsCreated,\n' \
989 ' tsElapsedTestResult,\n' \
990 ' TestSets.enmStatus,\n' \
991 ' TestResults.cErrors,\n' \
992 ' TestCases.idTestCase,\n' \
993 ' TestCases.sName,\n' \
994 ' TestCases.sBaseCmd,\n' \
995 ' TestCaseArgs.sArgs,\n' \
996 ' TestCaseArgs.sSubName,\n' \
997 ' TestSuiteBits.idBuild,\n' \
998 ' TestSuiteBits.iRevision,\n' \
999 ' SortRunningFirst' + sSortGroupBy + '\n';
1000 sQuery += 'ORDER BY ';
1001 if sSortOrderBy is not None:
1002 sQuery += sSortOrderBy.replace('TestBoxes.', 'TestBoxesWithStrings.') + ',\n ';
1003 sQuery += '(TestSets.tsDone IS NULL) DESC, TestSets.idTestSet DESC\n';
1004
1005 #
1006 # Execute the query and return the wrapped results.
1007 #
1008 self._oDb.execute(sQuery);
1009
1010 if self.oFailureReasonLogic is None:
1011 self.oFailureReasonLogic = FailureReasonLogic(self._oDb);
1012 if self.oUserAccountLogic is None:
1013 self.oUserAccountLogic = UserAccountLogic(self._oDb);
1014
1015 aoRows = [];
1016 for aoRow in self._oDb.fetchAll():
1017 aoRows.append(TestResultListingData().initFromDbRowEx(aoRow, self.oFailureReasonLogic, self.oUserAccountLogic));
1018
1019 return aoRows
1020
1021
1022 def fetchTimestampsForLogViewer(self, idTestSet):
1023 """
1024 Returns an ordered list with all the test result timestamps, both start
1025 and end.
1026
1027 The log viewer create anchors in the log text so we can jump directly to
1028 the log lines relevant for a test event.
1029 """
1030 self._oDb.execute('(\n'
1031 'SELECT tsCreated\n'
1032 'FROM TestResults\n'
1033 'WHERE idTestSet = %s\n'
1034 ') UNION (\n'
1035 'SELECT tsCreated + tsElapsed\n'
1036 'FROM TestResults\n'
1037 'WHERE idTestSet = %s\n'
1038 ' AND tsElapsed IS NOT NULL\n'
1039 ') UNION (\n'
1040 'SELECT TestResultFiles.tsCreated\n'
1041 'FROM TestResultFiles\n'
1042 'WHERE idTestSet = %s\n'
1043 ') UNION (\n'
1044 'SELECT tsCreated\n'
1045 'FROM TestResultValues\n'
1046 'WHERE idTestSet = %s\n'
1047 ') UNION (\n'
1048 'SELECT TestResultMsgs.tsCreated\n'
1049 'FROM TestResultMsgs\n'
1050 'WHERE idTestSet = %s\n'
1051 ') ORDER by 1'
1052 , ( idTestSet, idTestSet, idTestSet, idTestSet, idTestSet, ));
1053 return [aoRow[0] for aoRow in self._oDb.fetchAll()];
1054
1055
1056 def getEntriesCount(self, tsNow, sInterval, enmResultsGroupingType, iResultsGroupingValue, fOnlyFailures, fOnlyNeedingReason):
1057 """
1058 Get number of table records.
1059
1060 If @param enmResultsGroupingType and @param iResultsGroupingValue
1061 are not None, then we count only only those records
1062 that match specified @param enmResultsGroupingType.
1063
1064 If @param enmResultsGroupingType is None, then
1065 @param iResultsGroupingValue is ignored.
1066 """
1067
1068 #
1069 # Get SQL query parameters
1070 #
1071 if enmResultsGroupingType is None:
1072 raise TMExceptionBase('Unknown grouping type')
1073
1074 if enmResultsGroupingType not in self.kdResultGroupingMap:
1075 raise TMExceptionBase('Unknown grouping type')
1076 sGroupingTables, sGroupingField, sGroupingCondition, _ = self.kdResultGroupingMap[enmResultsGroupingType];
1077
1078 #
1079 # Construct the query.
1080 #
1081 sQuery = 'SELECT COUNT(TestSets.idTestSet)\n' \
1082 'FROM TestSets';
1083 if fOnlyNeedingReason:
1084 sQuery += '\n' \
1085 ' LEFT OUTER JOIN TestResultFailures\n' \
1086 ' ON TestSets.idTestSet = TestResultFailures.idTestSet\n' \
1087 ' AND TestResultFailures.tsExpire = \'infinity\'::TIMESTAMP';
1088 sQuery += sGroupingTables.replace(',', ',\n ');
1089 sQuery += '\n' \
1090 'WHERE ' + self._getTimePeriodQueryPart(tsNow, sInterval);
1091 if fOnlyFailures or fOnlyNeedingReason:
1092 sQuery += ' AND TestSets.enmStatus != \'success\'::TestStatus_T\n' \
1093 ' AND TestSets.enmStatus != \'running\'::TestStatus_T\n';
1094 if fOnlyNeedingReason:
1095 sQuery += ' AND TestResultFailures.idTestSet IS NULL\n';
1096 if sGroupingField is not None:
1097 sQuery += ' AND %s = %d\n' % (sGroupingField, iResultsGroupingValue,);
1098 if sGroupingCondition is not None:
1099 sQuery += sGroupingCondition.replace(' AND ', ' AND ');
1100
1101 #
1102 # Execute the query and return the result.
1103 #
1104 self._oDb.execute(sQuery)
1105 return self._oDb.fetchOne()[0]
1106
1107 def getTestGroups(self, tsNow, sPeriod):
1108 """
1109 Get list of uniq TestGroupData objects which
1110 found in all test results.
1111 """
1112
1113 self._oDb.execute('SELECT DISTINCT TestGroups.*\n'
1114 'FROM TestGroups, TestSets\n'
1115 'WHERE TestSets.idTestGroup = TestGroups.idTestGroup\n'
1116 ' AND TestGroups.tsExpire > TestSets.tsCreated\n'
1117 ' AND TestGroups.tsEffective <= TestSets.tsCreated'
1118 ' AND ' + self._getTimePeriodQueryPart(tsNow, sPeriod))
1119 aaoRows = self._oDb.fetchAll()
1120 aoRet = []
1121 for aoRow in aaoRows:
1122 aoRet.append(TestGroupData().initFromDbRow(aoRow))
1123 return aoRet
1124
1125 def getBuilds(self, tsNow, sPeriod):
1126 """
1127 Get list of uniq BuildDataEx objects which
1128 found in all test results.
1129 """
1130
1131 self._oDb.execute('SELECT DISTINCT Builds.*, BuildCategories.*\n'
1132 'FROM Builds, BuildCategories, TestSets\n'
1133 'WHERE TestSets.idBuild = Builds.idBuild\n'
1134 ' AND Builds.idBuildCategory = BuildCategories.idBuildCategory\n'
1135 ' AND Builds.tsExpire > TestSets.tsCreated\n'
1136 ' AND Builds.tsEffective <= TestSets.tsCreated'
1137 ' AND ' + self._getTimePeriodQueryPart(tsNow, sPeriod))
1138 aaoRows = self._oDb.fetchAll()
1139 aoRet = []
1140 for aoRow in aaoRows:
1141 aoRet.append(BuildDataEx().initFromDbRow(aoRow))
1142 return aoRet
1143
1144 def getTestBoxes(self, tsNow, sPeriod):
1145 """
1146 Get list of uniq TestBoxData objects which
1147 found in all test results.
1148 """
1149 # Note! INNER JOIN TestBoxesWithStrings performs miserable compared to LEFT OUTER JOIN. Doesn't matter for the result
1150 # because TestSets.idGenTestBox is a foreign key and unique in TestBoxes. So, let's do what ever is faster.
1151 self._oDb.execute('SELECT TestBoxesWithStrings.*\n'
1152 'FROM ( SELECT idTestBox AS idTestBox,\n'
1153 ' MAX(idGenTestBox) AS idGenTestBox\n'
1154 ' FROM TestSets\n'
1155 ' WHERE ' + self._getTimePeriodQueryPart(tsNow, sPeriod, ' ') +
1156 ' GROUP BY idTestBox\n'
1157 ' ) AS TestBoxIDs\n'
1158 ' LEFT OUTER JOIN TestBoxesWithStrings\n'
1159 ' ON TestBoxesWithStrings.idGenTestBox = TestBoxIDs.idGenTestBox\n'
1160 'ORDER BY TestBoxesWithStrings.sName\n' );
1161 aoRet = []
1162 for aoRow in self._oDb.fetchAll():
1163 aoRet.append(TestBoxData().initFromDbRow(aoRow));
1164 return aoRet
1165
1166 def getTestCases(self, tsNow, sPeriod):
1167 """
1168 Get a list of unique TestCaseData objects which is appears in the test
1169 specified result period.
1170 """
1171
1172 # Using LEFT OUTER JOIN instead of INNER JOIN in case it performs better, doesn't matter for the result.
1173 self._oDb.execute('SELECT TestCases.*\n'
1174 'FROM ( SELECT idTestCase AS idTestCase,\n'
1175 ' MAX(idGenTestCase) AS idGenTestCase\n'
1176 ' FROM TestSets\n'
1177 ' WHERE ' + self._getTimePeriodQueryPart(tsNow, sPeriod, ' ') +
1178 ' GROUP BY idTestCase\n'
1179 ' ) AS TestCasesIDs\n'
1180 ' LEFT OUTER JOIN TestCases ON TestCases.idGenTestCase = TestCasesIDs.idGenTestCase\n'
1181 'ORDER BY TestCases.sName\n' );
1182
1183 aoRet = [];
1184 for aoRow in self._oDb.fetchAll():
1185 aoRet.append(TestCaseData().initFromDbRow(aoRow));
1186 return aoRet
1187
1188 def getSchedGroups(self, tsNow, sPeriod):
1189 """
1190 Get list of uniq SchedGroupData objects which
1191 found in all test results.
1192 """
1193
1194 self._oDb.execute('SELECT SchedGroups.*\n'
1195 'FROM ( SELECT idSchedGroup,\n'
1196 ' MAX(TestSets.tsCreated) AS tsNow\n'
1197 ' FROM TestSets\n'
1198 ' WHERE ' + self._getTimePeriodQueryPart(tsNow, sPeriod, ' ') +
1199 ' GROUP BY idSchedGroup\n'
1200 ' ) AS SchedGroupIDs\n'
1201 ' INNER JOIN SchedGroups\n'
1202 ' ON SchedGroups.idSchedGroup = SchedGroupIDs.idSchedGroup\n'
1203 ' AND SchedGroups.tsExpire > SchedGroupIDs.tsNow\n'
1204 ' AND SchedGroups.tsEffective <= SchedGroupIDs.tsNow\n'
1205 'ORDER BY SchedGroups.sName\n' );
1206 aoRet = []
1207 for aoRow in self._oDb.fetchAll():
1208 aoRet.append(SchedGroupData().initFromDbRow(aoRow));
1209 return aoRet
1210
1211 def getById(self, idTestResult):
1212 """
1213 Get build record by its id
1214 """
1215 self._oDb.execute('SELECT *\n'
1216 'FROM TestResults\n'
1217 'WHERE idTestResult = %s\n',
1218 (idTestResult,))
1219
1220 aRows = self._oDb.fetchAll()
1221 if len(aRows) not in (0, 1):
1222 raise TMTooManyRows('Found more than one test result with the same credentials. Database structure is corrupted.')
1223 try:
1224 return TestResultData().initFromDbRow(aRows[0])
1225 except IndexError:
1226 return None
1227
1228
1229 #
1230 # Details view and interface.
1231 #
1232
1233 def fetchResultTree(self, idTestSet, cMaxDepth = None):
1234 """
1235 Fetches the result tree for the given test set.
1236
1237 Returns a tree of TestResultDataEx nodes.
1238 Raises exception on invalid input and database issues.
1239 """
1240 # Depth first, i.e. just like the XML added them.
1241 ## @todo this still isn't performing extremely well, consider optimizations.
1242 sQuery = self._oDb.formatBindArgs(
1243 'SELECT TestResults.*,\n'
1244 ' TestResultStrTab.sValue,\n'
1245 ' EXISTS ( SELECT idTestResultValue\n'
1246 ' FROM TestResultValues\n'
1247 ' WHERE TestResultValues.idTestResult = TestResults.idTestResult ) AS fHasValues,\n'
1248 ' EXISTS ( SELECT idTestResultMsg\n'
1249 ' FROM TestResultMsgs\n'
1250 ' WHERE TestResultMsgs.idTestResult = TestResults.idTestResult ) AS fHasMsgs,\n'
1251 ' EXISTS ( SELECT idTestResultFile\n'
1252 ' FROM TestResultFiles\n'
1253 ' WHERE TestResultFiles.idTestResult = TestResults.idTestResult ) AS fHasFiles,\n'
1254 ' EXISTS ( SELECT idTestResult\n'
1255 ' FROM TestResultFailures\n'
1256 ' WHERE TestResultFailures.idTestResult = TestResults.idTestResult ) AS fHasReasons\n'
1257 'FROM TestResults, TestResultStrTab\n'
1258 'WHERE TestResults.idTestSet = %s\n'
1259 ' AND TestResults.idStrName = TestResultStrTab.idStr\n'
1260 , ( idTestSet, ));
1261 if cMaxDepth is not None:
1262 sQuery += self._oDb.formatBindArgs(' AND TestResults.iNestingDepth <= %s\n', (cMaxDepth,));
1263 sQuery += 'ORDER BY idTestResult ASC\n'
1264
1265 self._oDb.execute(sQuery);
1266 cRows = self._oDb.getRowCount();
1267 if cRows > 65536:
1268 raise TMTooManyRows('Too many rows returned for idTestSet=%d: %d' % (idTestSet, cRows,));
1269
1270 aaoRows = self._oDb.fetchAll();
1271 if len(aaoRows) == 0:
1272 raise TMRowNotFound('No test results for idTestSet=%d.' % (idTestSet,));
1273
1274 # Set up the root node first.
1275 aoRow = aaoRows[0];
1276 oRoot = TestResultDataEx().initFromDbRow(aoRow);
1277 if oRoot.idTestResultParent is not None:
1278 raise self._oDb.integrityException('The root TestResult (#%s) has a parent (#%s)!'
1279 % (oRoot.idTestResult, oRoot.idTestResultParent));
1280 self._fetchResultTreeNodeExtras(oRoot, aoRow[-4], aoRow[-3], aoRow[-2], aoRow[-1]);
1281
1282 # The chilren (if any).
1283 dLookup = { oRoot.idTestResult: oRoot };
1284 oParent = oRoot;
1285 for iRow in range(1, len(aaoRows)):
1286 aoRow = aaoRows[iRow];
1287 oCur = TestResultDataEx().initFromDbRow(aoRow);
1288 self._fetchResultTreeNodeExtras(oCur, aoRow[-4], aoRow[-3], aoRow[-2], aoRow[-1]);
1289
1290 # Figure out and vet the parent.
1291 if oParent.idTestResult != oCur.idTestResultParent:
1292 oParent = dLookup.get(oCur.idTestResultParent, None);
1293 if oParent is None:
1294 raise self._oDb.integrityException('TestResult #%d is orphaned from its parent #%s.'
1295 % (oCur.idTestResult, oCur.idTestResultParent,));
1296 if oParent.iNestingDepth + 1 != oCur.iNestingDepth:
1297 raise self._oDb.integrityException('TestResult #%d has incorrect nesting depth (%d instead of %d)'
1298 % (oCur.idTestResult, oCur.iNestingDepth, oParent.iNestingDepth + 1,));
1299
1300 # Link it up.
1301 oCur.oParent = oParent;
1302 oParent.aoChildren.append(oCur);
1303 dLookup[oCur.idTestResult] = oCur;
1304
1305 return (oRoot, dLookup);
1306
1307 def _fetchResultTreeNodeExtras(self, oCurNode, fHasValues, fHasMsgs, fHasFiles, fHasReasons):
1308 """
1309 fetchResultTree worker that fetches values, message and files for the
1310 specified node.
1311 """
1312 assert(oCurNode.aoValues == []);
1313 assert(oCurNode.aoMsgs == []);
1314 assert(oCurNode.aoFiles == []);
1315 assert(oCurNode.oReason is None);
1316
1317 if fHasValues:
1318 self._oDb.execute('SELECT TestResultValues.*,\n'
1319 ' TestResultStrTab.sValue\n'
1320 'FROM TestResultValues, TestResultStrTab\n'
1321 'WHERE TestResultValues.idTestResult = %s\n'
1322 ' AND TestResultValues.idStrName = TestResultStrTab.idStr\n'
1323 'ORDER BY idTestResultValue ASC\n'
1324 , ( oCurNode.idTestResult, ));
1325 for aoRow in self._oDb.fetchAll():
1326 oCurNode.aoValues.append(TestResultValueDataEx().initFromDbRow(aoRow));
1327
1328 if fHasMsgs:
1329 self._oDb.execute('SELECT TestResultMsgs.*,\n'
1330 ' TestResultStrTab.sValue\n'
1331 'FROM TestResultMsgs, TestResultStrTab\n'
1332 'WHERE TestResultMsgs.idTestResult = %s\n'
1333 ' AND TestResultMsgs.idStrMsg = TestResultStrTab.idStr\n'
1334 'ORDER BY idTestResultMsg ASC\n'
1335 , ( oCurNode.idTestResult, ));
1336 for aoRow in self._oDb.fetchAll():
1337 oCurNode.aoMsgs.append(TestResultMsgDataEx().initFromDbRow(aoRow));
1338
1339 if fHasFiles:
1340 self._oDb.execute('SELECT TestResultFiles.*,\n'
1341 ' StrTabFile.sValue AS sFile,\n'
1342 ' StrTabDesc.sValue AS sDescription,\n'
1343 ' StrTabKind.sValue AS sKind,\n'
1344 ' StrTabMime.sValue AS sMime\n'
1345 'FROM TestResultFiles,\n'
1346 ' TestResultStrTab AS StrTabFile,\n'
1347 ' TestResultStrTab AS StrTabDesc,\n'
1348 ' TestResultStrTab AS StrTabKind,\n'
1349 ' TestResultStrTab AS StrTabMime\n'
1350 'WHERE TestResultFiles.idTestResult = %s\n'
1351 ' AND TestResultFiles.idStrFile = StrTabFile.idStr\n'
1352 ' AND TestResultFiles.idStrDescription = StrTabDesc.idStr\n'
1353 ' AND TestResultFiles.idStrKind = StrTabKind.idStr\n'
1354 ' AND TestResultFiles.idStrMime = StrTabMime.idStr\n'
1355 'ORDER BY idTestResultFile ASC\n'
1356 , ( oCurNode.idTestResult, ));
1357 for aoRow in self._oDb.fetchAll():
1358 oCurNode.aoFiles.append(TestResultFileDataEx().initFromDbRow(aoRow));
1359
1360 if fHasReasons or True:
1361 if self.oFailureReasonLogic is None:
1362 self.oFailureReasonLogic = FailureReasonLogic(self._oDb);
1363 if self.oUserAccountLogic is None:
1364 self.oUserAccountLogic = UserAccountLogic(self._oDb);
1365 self._oDb.execute('SELECT *\n'
1366 'FROM TestResultFailures\n'
1367 'WHERE idTestResult = %s\n'
1368 ' AND tsExpire = \'infinity\'::TIMESTAMP\n'
1369 , ( oCurNode.idTestResult, ));
1370 if self._oDb.getRowCount() > 0:
1371 oCurNode.oReason = TestResultFailureDataEx().initFromDbRowEx(self._oDb.fetchOne(), self.oFailureReasonLogic,
1372 self.oUserAccountLogic);
1373
1374 return True;
1375
1376
1377
1378 #
1379 # TestBoxController interface(s).
1380 #
1381
1382 def _inhumeTestResults(self, aoStack, idTestSet, sError):
1383 """
1384 The test produces too much output, kill and bury it.
1385
1386 Note! We leave the test set open, only the test result records are
1387 completed. Thus, _getResultStack will return an empty stack and
1388 cause XML processing to fail immediately, while we can still
1389 record when it actually completed in the test set the normal way.
1390 """
1391 self._oDb.dprint('** _inhumeTestResults: idTestSet=%d\n%s' % (idTestSet, self._stringifyStack(aoStack),));
1392
1393 #
1394 # First add a message.
1395 #
1396 self._newFailureDetails(aoStack[0].idTestResult, idTestSet, sError, None);
1397
1398 #
1399 # The complete all open test results.
1400 #
1401 for oTestResult in aoStack:
1402 oTestResult.cErrors += 1;
1403 self._completeTestResults(oTestResult, None, TestResultData.ksTestStatus_Failure, oTestResult.cErrors);
1404
1405 # A bit of paranoia.
1406 self._oDb.execute('UPDATE TestResults\n'
1407 'SET cErrors = cErrors + 1,\n'
1408 ' enmStatus = \'failure\'::TestStatus_T,\n'
1409 ' tsElapsed = CURRENT_TIMESTAMP - tsCreated\n'
1410 'WHERE idTestSet = %s\n'
1411 ' AND enmStatus = \'running\'::TestStatus_T\n'
1412 , ( idTestSet, ));
1413 self._oDb.commit();
1414
1415 return None;
1416
1417 def strTabString(self, sString, fCommit = False):
1418 """
1419 Gets the string table id for the given string, adding it if new.
1420
1421 Note! A copy of this code is also in TestSetLogic.
1422 """
1423 ## @todo move this and make a stored procedure for it.
1424 self._oDb.execute('SELECT idStr\n'
1425 'FROM TestResultStrTab\n'
1426 'WHERE sValue = %s'
1427 , (sString,));
1428 if self._oDb.getRowCount() == 0:
1429 self._oDb.execute('INSERT INTO TestResultStrTab (sValue)\n'
1430 'VALUES (%s)\n'
1431 'RETURNING idStr\n'
1432 , (sString,));
1433 if fCommit:
1434 self._oDb.commit();
1435 return self._oDb.fetchOne()[0];
1436
1437 @staticmethod
1438 def _stringifyStack(aoStack):
1439 """Returns a string rep of the stack."""
1440 sRet = '';
1441 for i, _ in enumerate(aoStack):
1442 sRet += 'aoStack[%d]=%s\n' % (i, aoStack[i]);
1443 return sRet;
1444
1445 def _getResultStack(self, idTestSet):
1446 """
1447 Gets the current stack of result sets.
1448 """
1449 self._oDb.execute('SELECT *\n'
1450 'FROM TestResults\n'
1451 'WHERE idTestSet = %s\n'
1452 ' AND enmStatus = \'running\'::TestStatus_T\n'
1453 'ORDER BY idTestResult DESC'
1454 , ( idTestSet, ));
1455 aoStack = [];
1456 for aoRow in self._oDb.fetchAll():
1457 aoStack.append(TestResultData().initFromDbRow(aoRow));
1458
1459 for i, _ in enumerate(aoStack):
1460 assert aoStack[i].iNestingDepth == len(aoStack) - i - 1, self._stringifyStack(aoStack);
1461
1462 return aoStack;
1463
1464 def _newTestResult(self, idTestResultParent, idTestSet, iNestingDepth, tsCreated, sName, dCounts, fCommit = False):
1465 """
1466 Creates a new test result.
1467 Returns the TestResultData object for the new record.
1468 May raise exception on database error.
1469 """
1470 assert idTestResultParent is not None;
1471 assert idTestResultParent > 1;
1472
1473 #
1474 # This isn't necessarily very efficient, but it's necessary to prevent
1475 # a wild test or testbox from filling up the database.
1476 #
1477 sCountName = 'cTestResults';
1478 if sCountName not in dCounts:
1479 self._oDb.execute('SELECT COUNT(idTestResult)\n'
1480 'FROM TestResults\n'
1481 'WHERE idTestSet = %s\n'
1482 , ( idTestSet,));
1483 dCounts[sCountName] = self._oDb.fetchOne()[0];
1484 dCounts[sCountName] += 1;
1485 if dCounts[sCountName] > config.g_kcMaxTestResultsPerTS:
1486 raise TestResultHangingOffence('Too many sub-tests in total!');
1487
1488 sCountName = 'cTestResultsIn%d' % (idTestResultParent,);
1489 if sCountName not in dCounts:
1490 self._oDb.execute('SELECT COUNT(idTestResult)\n'
1491 'FROM TestResults\n'
1492 'WHERE idTestResultParent = %s\n'
1493 , ( idTestResultParent,));
1494 dCounts[sCountName] = self._oDb.fetchOne()[0];
1495 dCounts[sCountName] += 1;
1496 if dCounts[sCountName] > config.g_kcMaxTestResultsPerTR:
1497 raise TestResultHangingOffence('Too many immediate sub-tests!');
1498
1499 # This is also a hanging offence.
1500 if iNestingDepth > config.g_kcMaxTestResultDepth:
1501 raise TestResultHangingOffence('To deep sub-test nesting!');
1502
1503 # Ditto.
1504 if len(sName) > config.g_kcchMaxTestResultName:
1505 raise TestResultHangingOffence('Test name is too long: %d chars - "%s"' % (len(sName), sName));
1506
1507 #
1508 # Within bounds, do the job.
1509 #
1510 idStrName = self.strTabString(sName, fCommit);
1511 self._oDb.execute('INSERT INTO TestResults (\n'
1512 ' idTestResultParent,\n'
1513 ' idTestSet,\n'
1514 ' tsCreated,\n'
1515 ' idStrName,\n'
1516 ' iNestingDepth )\n'
1517 'VALUES (%s, %s, TIMESTAMP WITH TIME ZONE %s, %s, %s)\n'
1518 'RETURNING *\n'
1519 , ( idTestResultParent, idTestSet, tsCreated, idStrName, iNestingDepth) )
1520 oData = TestResultData().initFromDbRow(self._oDb.fetchOne());
1521
1522 self._oDb.maybeCommit(fCommit);
1523 return oData;
1524
1525 def _newTestValue(self, idTestResult, idTestSet, sName, lValue, sUnit, dCounts, tsCreated = None, fCommit = False):
1526 """
1527 Creates a test value.
1528 May raise exception on database error.
1529 """
1530
1531 #
1532 # Bounds checking.
1533 #
1534 sCountName = 'cTestValues';
1535 if sCountName not in dCounts:
1536 self._oDb.execute('SELECT COUNT(idTestResultValue)\n'
1537 'FROM TestResultValues, TestResults\n'
1538 'WHERE TestResultValues.idTestResult = TestResults.idTestResult\n'
1539 ' AND TestResults.idTestSet = %s\n'
1540 , ( idTestSet,));
1541 dCounts[sCountName] = self._oDb.fetchOne()[0];
1542 dCounts[sCountName] += 1;
1543 if dCounts[sCountName] > config.g_kcMaxTestValuesPerTS:
1544 raise TestResultHangingOffence('Too many values in total!');
1545
1546 sCountName = 'cTestValuesIn%d' % (idTestResult,);
1547 if sCountName not in dCounts:
1548 self._oDb.execute('SELECT COUNT(idTestResultValue)\n'
1549 'FROM TestResultValues\n'
1550 'WHERE idTestResult = %s\n'
1551 , ( idTestResult,));
1552 dCounts[sCountName] = self._oDb.fetchOne()[0];
1553 dCounts[sCountName] += 1;
1554 if dCounts[sCountName] > config.g_kcMaxTestValuesPerTR:
1555 raise TestResultHangingOffence('Too many immediate values for one test result!');
1556
1557 if len(sName) > config.g_kcchMaxTestValueName:
1558 raise TestResultHangingOffence('Value name is too long: %d chars - "%s"' % (len(sName), sName));
1559
1560 #
1561 # Do the job.
1562 #
1563 iUnit = constants.valueunit.g_kdNameToConst.get(sUnit, constants.valueunit.NONE);
1564
1565 idStrName = self.strTabString(sName, fCommit);
1566 if tsCreated is None:
1567 self._oDb.execute('INSERT INTO TestResultValues (\n'
1568 ' idTestResult,\n'
1569 ' idTestSet,\n'
1570 ' idStrName,\n'
1571 ' lValue,\n'
1572 ' iUnit)\n'
1573 'VALUES ( %s, %s, %s, %s, %s )\n'
1574 , ( idTestResult, idTestSet, idStrName, lValue, iUnit,) );
1575 else:
1576 self._oDb.execute('INSERT INTO TestResultValues (\n'
1577 ' idTestResult,\n'
1578 ' idTestSet,\n'
1579 ' tsCreated,\n'
1580 ' idStrName,\n'
1581 ' lValue,\n'
1582 ' iUnit)\n'
1583 'VALUES ( %s, %s, TIMESTAMP WITH TIME ZONE %s, %s, %s, %s )\n'
1584 , ( idTestResult, idTestSet, tsCreated, idStrName, lValue, iUnit,) );
1585 self._oDb.maybeCommit(fCommit);
1586 return True;
1587
1588 def _newFailureDetails(self, idTestResult, idTestSet, sText, dCounts, tsCreated = None, fCommit = False):
1589 """
1590 Creates a record detailing cause of failure.
1591 May raise exception on database error.
1592 """
1593
1594 #
1595 # Overflow protection.
1596 #
1597 if dCounts is not None:
1598 sCountName = 'cTestMsgsIn%d' % (idTestResult,);
1599 if sCountName not in dCounts:
1600 self._oDb.execute('SELECT COUNT(idTestResultMsg)\n'
1601 'FROM TestResultMsgs\n'
1602 'WHERE idTestResult = %s\n'
1603 , ( idTestResult,));
1604 dCounts[sCountName] = self._oDb.fetchOne()[0];
1605 dCounts[sCountName] += 1;
1606 if dCounts[sCountName] > config.g_kcMaxTestMsgsPerTR:
1607 raise TestResultHangingOffence('Too many messages under for one test result!');
1608
1609 if len(sText) > config.g_kcchMaxTestMsg:
1610 raise TestResultHangingOffence('Failure details message is too long: %d chars - "%s"' % (len(sText), sText));
1611
1612 #
1613 # Do the job.
1614 #
1615 idStrMsg = self.strTabString(sText, fCommit);
1616 if tsCreated is None:
1617 self._oDb.execute('INSERT INTO TestResultMsgs (\n'
1618 ' idTestResult,\n'
1619 ' idTestSet,\n'
1620 ' idStrMsg,\n'
1621 ' enmLevel)\n'
1622 'VALUES ( %s, %s, %s, %s)\n'
1623 , ( idTestResult, idTestSet, idStrMsg, 'failure',) );
1624 else:
1625 self._oDb.execute('INSERT INTO TestResultMsgs (\n'
1626 ' idTestResult,\n'
1627 ' idTestSet,\n'
1628 ' tsCreated,\n'
1629 ' idStrMsg,\n'
1630 ' enmLevel)\n'
1631 'VALUES ( %s, %s, TIMESTAMP WITH TIME ZONE %s, %s, %s)\n'
1632 , ( idTestResult, idTestSet, tsCreated, idStrMsg, 'failure',) );
1633
1634 self._oDb.maybeCommit(fCommit);
1635 return True;
1636
1637
1638 def _completeTestResults(self, oTestResult, tsDone, enmStatus, cErrors = 0, fCommit = False):
1639 """
1640 Completes a test result. Updates the oTestResult object.
1641 May raise exception on database error.
1642 """
1643 self._oDb.dprint('** _completeTestResults: cErrors=%s tsDone=%s enmStatus=%s oTestResults=\n%s'
1644 % (cErrors, tsDone, enmStatus, oTestResult,));
1645
1646 #
1647 # Sanity check: No open sub tests (aoStack should make sure about this!).
1648 #
1649 self._oDb.execute('SELECT COUNT(idTestResult)\n'
1650 'FROM TestResults\n'
1651 'WHERE idTestResultParent = %s\n'
1652 ' AND enmStatus = %s\n'
1653 , ( oTestResult.idTestResult, TestResultData.ksTestStatus_Running,));
1654 cOpenSubTest = self._oDb.fetchOne()[0];
1655 assert cOpenSubTest == 0, 'cOpenSubTest=%d - %s' % (cOpenSubTest, oTestResult,);
1656 assert oTestResult.enmStatus == TestResultData.ksTestStatus_Running;
1657
1658 #
1659 # Make sure the reporter isn't lying about successes or error counts.
1660 #
1661 self._oDb.execute('SELECT COALESCE(SUM(cErrors), 0)\n'
1662 'FROM TestResults\n'
1663 'WHERE idTestResultParent = %s\n'
1664 , ( oTestResult.idTestResult, ));
1665 cMinErrors = self._oDb.fetchOne()[0] + oTestResult.cErrors;
1666 if cErrors < cMinErrors:
1667 cErrors = cMinErrors;
1668 if cErrors > 0 and enmStatus == TestResultData.ksTestStatus_Success:
1669 enmStatus = TestResultData.ksTestStatus_Failure
1670
1671 #
1672 # Do the update.
1673 #
1674 if tsDone is None:
1675 self._oDb.execute('UPDATE TestResults\n'
1676 'SET cErrors = %s,\n'
1677 ' enmStatus = %s,\n'
1678 ' tsElapsed = CURRENT_TIMESTAMP - tsCreated\n'
1679 'WHERE idTestResult = %s\n'
1680 'RETURNING tsElapsed'
1681 , ( cErrors, enmStatus, oTestResult.idTestResult,) );
1682 else:
1683 self._oDb.execute('UPDATE TestResults\n'
1684 'SET cErrors = %s,\n'
1685 ' enmStatus = %s,\n'
1686 ' tsElapsed = TIMESTAMP WITH TIME ZONE %s - tsCreated\n'
1687 'WHERE idTestResult = %s\n'
1688 'RETURNING tsElapsed'
1689 , ( cErrors, enmStatus, tsDone, oTestResult.idTestResult,) );
1690
1691 oTestResult.tsElapsed = self._oDb.fetchOne()[0];
1692 oTestResult.enmStatus = enmStatus;
1693 oTestResult.cErrors = cErrors;
1694
1695 self._oDb.maybeCommit(fCommit);
1696 return None;
1697
1698 def _doPopHint(self, aoStack, cStackEntries, dCounts, idTestSet):
1699 """ Executes a PopHint. """
1700 assert cStackEntries >= 0;
1701 while len(aoStack) > cStackEntries:
1702 if aoStack[0].enmStatus == TestResultData.ksTestStatus_Running:
1703 self._newFailureDetails(aoStack[0].idTestResult, idTestSet, 'XML error: Missing </Test>', dCounts);
1704 self._completeTestResults(aoStack[0], tsDone = None, cErrors = 1,
1705 enmStatus = TestResultData.ksTestStatus_Failure, fCommit = True);
1706 aoStack.pop(0);
1707 return True;
1708
1709
1710 @staticmethod
1711 def _validateElement(sName, dAttribs, fClosed):
1712 """
1713 Validates an element and its attributes.
1714 """
1715
1716 #
1717 # Validate attributes by name.
1718 #
1719
1720 # Validate integer attributes.
1721 for sAttr in [ 'errors', 'testdepth' ]:
1722 if sAttr in dAttribs:
1723 try:
1724 _ = int(dAttribs[sAttr]);
1725 except:
1726 return 'Element %s has an invalid %s attribute value: %s.' % (sName, sAttr, dAttribs[sAttr],);
1727
1728 # Validate long attributes.
1729 for sAttr in [ 'value', ]:
1730 if sAttr in dAttribs:
1731 try:
1732 _ = long(dAttribs[sAttr]); # pylint: disable=R0204
1733 except:
1734 return 'Element %s has an invalid %s attribute value: %s.' % (sName, sAttr, dAttribs[sAttr],);
1735
1736 # Validate string attributes.
1737 for sAttr in [ 'name', 'text' ]: # 'unit' can be zero length.
1738 if sAttr in dAttribs and len(dAttribs[sAttr]) == 0:
1739 return 'Element %s has an empty %s attribute value.' % (sName, sAttr,);
1740
1741 # Validate the timestamp attribute.
1742 if 'timestamp' in dAttribs:
1743 (dAttribs['timestamp'], sError) = ModelDataBase.validateTs(dAttribs['timestamp'], fAllowNull = False);
1744 if sError is not None:
1745 return 'Element %s has an invalid timestamp ("%s"): %s' % (sName, dAttribs['timestamp'], sError,);
1746
1747
1748 #
1749 # Check that attributes that are required are present.
1750 # We ignore extra attributes.
1751 #
1752 dElementAttribs = \
1753 {
1754 'Test': [ 'timestamp', 'name', ],
1755 'Value': [ 'timestamp', 'name', 'unit', 'value', ],
1756 'FailureDetails': [ 'timestamp', 'text', ],
1757 'Passed': [ 'timestamp', ],
1758 'Skipped': [ 'timestamp', ],
1759 'Failed': [ 'timestamp', 'errors', ],
1760 'TimedOut': [ 'timestamp', 'errors', ],
1761 'End': [ 'timestamp', ],
1762 'PushHint': [ 'testdepth', ],
1763 'PopHint': [ 'testdepth', ],
1764 };
1765 if sName not in dElementAttribs:
1766 return 'Unknown element "%s".' % (sName,);
1767 for sAttr in dElementAttribs[sName]:
1768 if sAttr not in dAttribs:
1769 return 'Element %s requires attribute "%s".' % (sName, sAttr);
1770
1771 #
1772 # Only the Test element can (and must) remain open.
1773 #
1774 if sName == 'Test' and fClosed:
1775 return '<Test/> is not allowed.';
1776 if sName != 'Test' and not fClosed:
1777 return 'All elements except <Test> must be closed.';
1778
1779 return None;
1780
1781 @staticmethod
1782 def _parseElement(sElement):
1783 """
1784 Parses an element.
1785
1786 """
1787 #
1788 # Element level bits.
1789 #
1790 sName = sElement.split()[0];
1791 sElement = sElement[len(sName):];
1792
1793 fClosed = sElement[-1] == '/';
1794 if fClosed:
1795 sElement = sElement[:-1];
1796
1797 #
1798 # Attributes.
1799 #
1800 sError = None;
1801 dAttribs = {};
1802 sElement = sElement.strip();
1803 while len(sElement) > 0:
1804 # Extract attribute name.
1805 off = sElement.find('=');
1806 if off < 0 or not sElement[:off].isalnum():
1807 sError = 'Attributes shall have alpha numberical names and have values.';
1808 break;
1809 sAttr = sElement[:off];
1810
1811 # Extract attribute value.
1812 if off + 2 >= len(sElement) or sElement[off + 1] != '"':
1813 sError = 'Attribute (%s) value is missing or not in double quotes.' % (sAttr,);
1814 break;
1815 off += 2;
1816 offEndQuote = sElement.find('"', off);
1817 if offEndQuote < 0:
1818 sError = 'Attribute (%s) value is missing end quotation mark.' % (sAttr,);
1819 break;
1820 sValue = sElement[off:offEndQuote];
1821
1822 # Check for duplicates.
1823 if sAttr in dAttribs:
1824 sError = 'Attribute "%s" appears more than once.' % (sAttr,);
1825 break;
1826
1827 # Unescape the value.
1828 sValue = sValue.replace('&lt;', '<');
1829 sValue = sValue.replace('&gt;', '>');
1830 sValue = sValue.replace('&apos;', '\'');
1831 sValue = sValue.replace('&quot;', '"');
1832 sValue = sValue.replace('&#xA;', '\n');
1833 sValue = sValue.replace('&#xD;', '\r');
1834 sValue = sValue.replace('&amp;', '&'); # last
1835
1836 # Done.
1837 dAttribs[sAttr] = sValue;
1838
1839 # advance
1840 sElement = sElement[offEndQuote + 1:];
1841 sElement = sElement.lstrip();
1842
1843 #
1844 # Validate the element before we return.
1845 #
1846 if sError is None:
1847 sError = TestResultLogic._validateElement(sName, dAttribs, fClosed);
1848
1849 return (sName, dAttribs, sError)
1850
1851 def _handleElement(self, sName, dAttribs, idTestSet, aoStack, aaiHints, dCounts):
1852 """
1853 Worker for processXmlStream that handles one element.
1854
1855 Returns None on success, error string on bad XML or similar.
1856 Raises exception on hanging offence and on database error.
1857 """
1858 if sName == 'Test':
1859 iNestingDepth = aoStack[0].iNestingDepth + 1 if len(aoStack) > 0 else 0;
1860 aoStack.insert(0, self._newTestResult(idTestResultParent = aoStack[0].idTestResult, idTestSet = idTestSet,
1861 tsCreated = dAttribs['timestamp'], sName = dAttribs['name'],
1862 iNestingDepth = iNestingDepth, dCounts = dCounts, fCommit = True) );
1863
1864 elif sName == 'Value':
1865 self._newTestValue(idTestResult = aoStack[0].idTestResult, idTestSet = idTestSet, tsCreated = dAttribs['timestamp'],
1866 sName = dAttribs['name'], sUnit = dAttribs['unit'], lValue = long(dAttribs['value']),
1867 dCounts = dCounts, fCommit = True);
1868
1869 elif sName == 'FailureDetails':
1870 self._newFailureDetails(idTestResult = aoStack[0].idTestResult, idTestSet = idTestSet,
1871 tsCreated = dAttribs['timestamp'], sText = dAttribs['text'], dCounts = dCounts,
1872 fCommit = True);
1873
1874 elif sName == 'Passed':
1875 self._completeTestResults(aoStack[0], tsDone = dAttribs['timestamp'],
1876 enmStatus = TestResultData.ksTestStatus_Success, fCommit = True);
1877
1878 elif sName == 'Skipped':
1879 self._completeTestResults(aoStack[0], tsDone = dAttribs['timestamp'],
1880 enmStatus = TestResultData.ksTestStatus_Skipped, fCommit = True);
1881
1882 elif sName == 'Failed':
1883 self._completeTestResults(aoStack[0], tsDone = dAttribs['timestamp'], cErrors = int(dAttribs['errors']),
1884 enmStatus = TestResultData.ksTestStatus_Failure, fCommit = True);
1885
1886 elif sName == 'TimedOut':
1887 self._completeTestResults(aoStack[0], tsDone = dAttribs['timestamp'], cErrors = int(dAttribs['errors']),
1888 enmStatus = TestResultData.ksTestStatus_TimedOut, fCommit = True);
1889
1890 elif sName == 'End':
1891 self._completeTestResults(aoStack[0], tsDone = dAttribs['timestamp'],
1892 cErrors = int(dAttribs.get('errors', '1')),
1893 enmStatus = TestResultData.ksTestStatus_Success, fCommit = True);
1894
1895 elif sName == 'PushHint':
1896 if len(aaiHints) > 1:
1897 return 'PushHint cannot be nested.'
1898
1899 aaiHints.insert(0, [len(aoStack), int(dAttribs['testdepth'])]);
1900
1901 elif sName == 'PopHint':
1902 if len(aaiHints) < 1:
1903 return 'No hint to pop.'
1904
1905 iDesiredTestDepth = int(dAttribs['testdepth']);
1906 cStackEntries, iTestDepth = aaiHints.pop(0);
1907 self._doPopHint(aoStack, cStackEntries, dCounts, idTestSet); # Fake the necessary '<End/></Test>' tags.
1908 if iDesiredTestDepth != iTestDepth:
1909 return 'PopHint tag has different testdepth: %d, on stack %d.' % (iDesiredTestDepth, iTestDepth);
1910 else:
1911 return 'Unexpected element "%s".' % (sName,);
1912 return None;
1913
1914
1915 def processXmlStream(self, sXml, idTestSet):
1916 """
1917 Processes the "XML" stream section given in sXml.
1918
1919 The sXml isn't a complete XML document, even should we save up all sXml
1920 for a given set, they may not form a complete and well formed XML
1921 document since the test may be aborted, abend or simply be buggy. We
1922 therefore do our own parsing and treat the XML tags as commands more
1923 than anything else.
1924
1925 Returns (sError, fUnforgivable), where sError is None on success.
1926 May raise database exception.
1927 """
1928 aoStack = self._getResultStack(idTestSet); # [0] == top; [-1] == bottom.
1929 if len(aoStack) == 0:
1930 return ('No open results', True);
1931 self._oDb.dprint('** processXmlStream len(aoStack)=%s' % (len(aoStack),));
1932 #self._oDb.dprint('processXmlStream: %s' % (self._stringifyStack(aoStack),));
1933 #self._oDb.dprint('processXmlStream: sXml=%s' % (sXml,));
1934
1935 dCounts = {};
1936 aaiHints = [];
1937 sError = None;
1938
1939 fExpectCloseTest = False;
1940 sXml = sXml.strip();
1941 while len(sXml) > 0:
1942 if sXml.startswith('</Test>'): # Only closing tag.
1943 offNext = len('</Test>');
1944 if len(aoStack) <= 1:
1945 sError = 'Trying to close the top test results.'
1946 break;
1947 # ASSUMES that we've just seen an <End/>, <Passed/>, <Failed/>,
1948 # <TimedOut/> or <Skipped/> tag earlier in this call!
1949 if aoStack[0].enmStatus == TestResultData.ksTestStatus_Running or not fExpectCloseTest:
1950 sError = 'Missing <End/>, <Passed/>, <Failed/>, <TimedOut/> or <Skipped/> tag.';
1951 break;
1952 aoStack.pop(0);
1953 fExpectCloseTest = False;
1954
1955 elif fExpectCloseTest:
1956 sError = 'Expected </Test>.'
1957 break;
1958
1959 elif sXml.startswith('<?xml '): # Ignore (included files).
1960 offNext = sXml.find('?>');
1961 if offNext < 0:
1962 sError = 'Unterminated <?xml ?> element.';
1963 break;
1964 offNext += 2;
1965
1966 elif sXml[0] == '<':
1967 # Parse and check the tag.
1968 if not sXml[1].isalpha():
1969 sError = 'Malformed element.';
1970 break;
1971 offNext = sXml.find('>')
1972 if offNext < 0:
1973 sError = 'Unterminated element.';
1974 break;
1975 (sName, dAttribs, sError) = self._parseElement(sXml[1:offNext]);
1976 offNext += 1;
1977 if sError is not None:
1978 break;
1979
1980 # Handle it.
1981 try:
1982 sError = self._handleElement(sName, dAttribs, idTestSet, aoStack, aaiHints, dCounts);
1983 except TestResultHangingOffence as oXcpt:
1984 self._inhumeTestResults(aoStack, idTestSet, str(oXcpt));
1985 return (str(oXcpt), True);
1986
1987
1988 fExpectCloseTest = sName in [ 'End', 'Passed', 'Failed', 'TimedOut', 'Skipped', ];
1989 else:
1990 sError = 'Unexpected content.';
1991 break;
1992
1993 # Advance.
1994 sXml = sXml[offNext:];
1995 sXml = sXml.lstrip();
1996
1997 #
1998 # Post processing checks.
1999 #
2000 if sError is None and fExpectCloseTest:
2001 sError = 'Expected </Test> before the end of the XML section.'
2002 elif sError is None and len(aaiHints) > 0:
2003 sError = 'Expected </PopHint> before the end of the XML section.'
2004 if len(aaiHints) > 0:
2005 self._doPopHint(aoStack, aaiHints[-1][0], dCounts, idTestSet);
2006
2007 #
2008 # Log the error.
2009 #
2010 if sError is not None:
2011 SystemLogLogic(self._oDb).addEntry(SystemLogData.ksEvent_XmlResultMalformed,
2012 'idTestSet=%s idTestResult=%s XML="%s" %s'
2013 % ( idTestSet,
2014 aoStack[0].idTestResult if len(aoStack) > 0 else -1,
2015 sXml[:30 if len(sXml) >= 30 else len(sXml)],
2016 sError, ),
2017 cHoursRepeat = 6, fCommit = True);
2018 return (sError, False);
2019
2020
2021
2022
2023
2024#
2025# Unit testing.
2026#
2027
2028# pylint: disable=C0111
2029class TestResultDataTestCase(ModelDataBaseTestCase):
2030 def setUp(self):
2031 self.aoSamples = [TestResultData(),];
2032
2033class TestResultValueDataTestCase(ModelDataBaseTestCase):
2034 def setUp(self):
2035 self.aoSamples = [TestResultValueData(),];
2036
2037if __name__ == '__main__':
2038 unittest.main();
2039 # not reached.
2040
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