VirtualBox

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

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

testmanager: Adding idTestSet to TestResultFiles and TestResultMsgs. Also adding proper indexes to these two tables.

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