VirtualBox

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

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

testmanager: Test result filtering - work in progress.

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

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