VirtualBox

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

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

testmanager: Adding sComment and fRawMode fields to TestBoxes and moves the strings into a separate shared string table.

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