VirtualBox

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

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

testmanager: Preparting TestSets for TestBoxes belonging to more than one scheduling group by storing the idSchedGroup in TestSets as well (this has a slight speed up effect on grouping results by scheduling group too of course). The idSchedGroup column in TestBoxes will be changed into a M:N table later some time. Also corrected a typo regarding orphaned tests.

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

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