VirtualBox

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

Last change on this file since 52776 was 52776, checked in by vboxsync, 10 years ago

fix OSE

  • Property svn:eol-style set to native
  • Property svn:keywords set to Author Date Id Revision
File size: 66.3 KB
Line 
1# -*- coding: utf-8 -*-
2# $Id: testresults.py 52776 2014-09-17 14:51:43Z 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-2014 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: 52776 $"
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, TMTooManyRows;
40from testmanager.core.testgroup import TestGroupData
41from testmanager.core.build import BuildDataEx
42from testmanager.core.testbox import TestBoxData
43from testmanager.core.testcase import TestCaseData
44from testmanager.core.schedgroup import SchedGroupData
45from testmanager.core.systemlog import SystemLogData, SystemLogLogic;
46
47
48class TestResultData(ModelDataBase):
49 """
50 Test case execution result data
51 """
52
53 ## @name TestStatus_T
54 # @{
55 ksTestStatus_Running = 'running';
56 ksTestStatus_Success = 'success';
57 ksTestStatus_Skipped = 'skipped';
58 ksTestStatus_BadTestBox = 'bad-testbox';
59 ksTestStatus_Aborted = 'aborted';
60 ksTestStatus_Failure = 'failure';
61 ksTestStatus_TimedOut = 'timed-out';
62 ksTestStatus_Rebooted = 'rebooted';
63 ## @}
64
65 ## List of relatively harmless (to testgroup/case) statuses.
66 kasHarmlessTestStatuses = [ ksTestStatus_Skipped, ksTestStatus_BadTestBox, ksTestStatus_Aborted, ];
67 ## List of bad statuses.
68 kasBadTestStatuses = [ ksTestStatus_Failure, ksTestStatus_TimedOut, ksTestStatus_Rebooted, ];
69
70
71 ksIdAttr = 'idTestResult';
72
73 ksParam_idTestResult = 'TestResultData_idTestResult';
74 ksParam_idTestResultParent = 'TestResultData_idTestResultParent';
75 ksParam_idTestSet = 'TestResultData_idTestSet';
76 ksParam_tsCreated = 'TestResultData_tsCreated';
77 ksParam_tsElapsed = 'TestResultData_tsElapsed';
78 ksParam_idStrName = 'TestResultData_idStrName';
79 ksParam_cErrors = 'TestResultData_cErrors';
80 ksParam_enmStatus = 'TestResultData_enmStatus';
81 ksParam_iNestingDepth = 'TestResultData_iNestingDepth';
82 kasValidValues_enmStatus = [
83 ksTestStatus_Running,
84 ksTestStatus_Success,
85 ksTestStatus_Skipped,
86 ksTestStatus_BadTestBox,
87 ksTestStatus_Aborted,
88 ksTestStatus_Failure,
89 ksTestStatus_TimedOut,
90 ksTestStatus_Rebooted
91 ];
92
93
94 def __init__(self):
95 ModelDataBase.__init__(self)
96 self.idTestResult = None
97 self.idTestResultParent = None
98 self.idTestSet = None
99 self.tsCreated = None
100 self.tsElapsed = None
101 self.idStrName = None
102 self.cErrors = 0;
103 self.enmStatus = None
104 self.iNestingDepth = None
105
106 def initFromDbRow(self, aoRow):
107 """
108 Reinitialize from a SELECT * FROM TestResults.
109 Return self. Raises exception if no row.
110 """
111 if aoRow is None:
112 raise TMExceptionBase('Test result record not found.')
113
114 self.idTestResult = aoRow[0]
115 self.idTestResultParent = aoRow[1]
116 self.idTestSet = aoRow[2]
117 self.tsCreated = aoRow[3]
118 self.tsElapsed = aoRow[4]
119 self.idStrName = aoRow[5]
120 self.cErrors = aoRow[6]
121 self.enmStatus = aoRow[7]
122 self.iNestingDepth = aoRow[8]
123 return self;
124
125 def isFailure(self):
126 """ Check if it's a real failure. """
127 return self.enmStatus in self.kasBadTestStatuses;
128
129
130class TestResultDataEx(TestResultData):
131 """
132 Extended test result data class.
133
134 This is intended for use as a node in a result tree. This is not intended
135 for serialization to parameters or vice versa. Use TestResultLogic to
136 construct the tree.
137 """
138
139 def __init__(self):
140 TestResultData.__init__(self)
141 self.sName = None; # idStrName resolved.
142 self.oParent = None; # idTestResultParent within the tree.
143
144 self.aoChildren = []; # TestResultDataEx;
145 self.aoValues = []; # TestResultValue;
146 self.aoMsgs = []; # TestResultMsg;
147 self.aoFiles = []; # TestResultFile;
148
149 def initFromDbRow(self, aoRow):
150 """
151 Initialize from a query like this:
152 SELECT TestResults.*, TestResultStrTab.sValue
153 FROM TestResults, TestResultStrTab
154 WHERE TestResultStrTab.idStr = TestResults.idStrName
155
156 Note! The caller is expected to fetch children, values, failure
157 details, and files.
158 """
159 self.sName = None;
160 self.oParent = None;
161 self.aoChildren = [];
162 self.aoValues = [];
163 self.aoMsgs = [];
164 self.aoFiles = [];
165
166 TestResultData.initFromDbRow(self, aoRow);
167
168 self.sName = aoRow[9];
169 return self;
170
171
172class TestResultValueData(ModelDataBase):
173 """
174 Test result value data.
175 """
176
177 ksIdAttr = 'idTestResultValue';
178
179 ksParam_idTestResultValue = 'TestResultValue_idTestResultValue';
180 ksParam_idTestResult = 'TestResultValue_idTestResult';
181 ksParam_idTestSet = 'TestResultValue_idTestSet';
182 ksParam_tsCreated = 'TestResultValue_tsCreated';
183 ksParam_idStrName = 'TestResultValue_idStrName';
184 ksParam_lValue = 'TestResultValue_lValue';
185 ksParam_iUnit = 'TestResultValue_iUnit';
186
187 def __init__(self):
188 ModelDataBase.__init__(self)
189 self.idTestResultValue = None;
190 self.idTestResult = None;
191 self.idTestSet = None;
192 self.tsCreated = None;
193 self.idStrName = None;
194 self.lValue = None;
195 self.iUnit = 0;
196
197 def initFromDbRow(self, aoRow):
198 """
199 Reinitialize from a SELECT * FROM TestResultValues.
200 Return self. Raises exception if no row.
201 """
202 if aoRow is None:
203 raise TMExceptionBase('Test result value record not found.')
204
205 self.idTestResultValue = aoRow[0];
206 self.idTestResult = aoRow[1];
207 self.idTestSet = aoRow[2];
208 self.tsCreated = aoRow[3];
209 self.idStrName = aoRow[4];
210 self.lValue = aoRow[5];
211 self.iUnit = aoRow[6];
212 return self;
213
214
215class TestResultValueDataEx(TestResultValueData):
216 """
217 Extends TestResultValue by resolving the value name and unit string.
218 """
219
220 def __init__(self):
221 TestResultValueData.__init__(self)
222 self.sName = None;
223 self.sUnit = '';
224
225 def initFromDbRow(self, aoRow):
226 """
227 Reinitialize from a query like this:
228 SELECT TestResultValues.*, TestResultStrTab.sValue
229 FROM TestResultValues, TestResultStrTab
230 WHERE TestResultStrTab.idStr = TestResultValues.idStrName
231
232 Return self. Raises exception if no row.
233 """
234 TestResultValueData.initFromDbRow(self, aoRow);
235 self.sName = aoRow[7];
236 if self.iUnit < len(constants.valueunit.g_asNames):
237 self.sUnit = constants.valueunit.g_asNames[self.iUnit];
238 else:
239 self.sUnit = '<%d>' % (self.iUnit,);
240 return self;
241
242class TestResultMsgData(ModelDataBase):
243 """
244 Test result message data.
245 """
246
247 ksIdAttr = 'idTestResultMsg';
248
249 ksParam_idTestResultMsg = 'TestResultValue_idTestResultMsg';
250 ksParam_idTestResult = 'TestResultValue_idTestResult';
251 ksParam_tsCreated = 'TestResultValue_tsCreated';
252 ksParam_idStrMsg = 'TestResultValue_idStrMsg';
253 ksParam_enmLevel = 'TestResultValue_enmLevel';
254
255 def __init__(self):
256 ModelDataBase.__init__(self)
257 self.idTestResultMsg = None;
258 self.idTestResult = None;
259 self.tsCreated = None;
260 self.idStrMsg = None;
261 self.enmLevel = None;
262
263 def initFromDbRow(self, aoRow):
264 """
265 Reinitialize from a SELECT * FROM TestResultMsgs.
266 Return self. Raises exception if no row.
267 """
268 if aoRow is None:
269 raise TMExceptionBase('Test result value record not found.')
270
271 self.idTestResultMsg = aoRow[0];
272 self.idTestResult = aoRow[1];
273 self.tsCreated = aoRow[2];
274 self.idStrMsg = aoRow[3];
275 self.enmLevel = aoRow[4];
276 return self;
277
278class TestResultMsgDataEx(TestResultMsgData):
279 """
280 Extends TestResultMsg by resolving the message string.
281 """
282
283 def __init__(self):
284 TestResultMsgData.__init__(self)
285 self.sMsg = None;
286
287 def initFromDbRow(self, aoRow):
288 """
289 Reinitialize from a query like this:
290 SELECT TestResultMsg.*, TestResultStrTab.sValue
291 FROM TestResultMsg, TestResultStrTab
292 WHERE TestResultStrTab.idStr = TestResultMsgs.idStrName
293
294 Return self. Raises exception if no row.
295 """
296 TestResultMsgData.initFromDbRow(self, aoRow);
297 self.sMsg = aoRow[5];
298 return self;
299
300class TestResultFileData(ModelDataBase):
301 """
302 Test result message data.
303 """
304
305 ksIdAttr = 'idTestResultFile';
306
307 ksParam_idTestResultFile = 'TestResultFile_idTestResultFile';
308 ksParam_idTestResult = 'TestResultFile_idTestResult';
309 ksParam_tsCreated = 'TestResultFile_tsCreated';
310 ksParam_idStrFile = 'TestResultFile_idStrFile';
311 ksParam_idStrDescription = 'TestResultFile_idStrDescription';
312 ksParam_idStrKind = 'TestResultFile_idStrKind';
313 ksParam_idStrMime = 'TestResultFile_idStrMime';
314
315 def __init__(self):
316 ModelDataBase.__init__(self)
317 self.idTestResultFile = None;
318 self.idTestResult = None;
319 self.tsCreated = None;
320 self.idStrFile = None;
321 self.idStrDescription = None;
322 self.idStrKind = None;
323 self.idStrMime = None;
324
325 def initFromDbRow(self, aoRow):
326 """
327 Reinitialize from a SELECT * FROM TestResultFiles.
328 Return self. Raises exception if no row.
329 """
330 if aoRow is None:
331 raise TMExceptionBase('Test result file record not found.')
332
333 self.idTestResultFile = aoRow[0];
334 self.idTestResult = aoRow[1];
335 self.tsCreated = aoRow[2];
336 self.idStrFile = aoRow[3];
337 self.idStrDescription = aoRow[4];
338 self.idStrKind = aoRow[5];
339 self.idStrMime = aoRow[6];
340 return self;
341
342class TestResultFileDataEx(TestResultFileData):
343 """
344 Extends TestResultFile by resolving the strings.
345 """
346
347 def __init__(self):
348 TestResultFileData.__init__(self)
349 self.sFile = None;
350 self.sDescription = None;
351 self.sKind = None;
352 self.sMime = None;
353
354 def initFromDbRow(self, aoRow):
355 """
356 Reinitialize from a query like this:
357 SELECT TestResultFiles.*,
358 StrTabFile.sValue AS sFile,
359 StrTabDesc.sValue AS sDescription
360 StrTabKind.sValue AS sKind,
361 StrTabMime.sValue AS sMime,
362 FROM ...
363
364 Return self. Raises exception if no row.
365 """
366 TestResultFileData.initFromDbRow(self, aoRow);
367 self.sFile = aoRow[7];
368 self.sDescription = aoRow[8];
369 self.sKind = aoRow[9];
370 self.sMime = aoRow[10];
371 return self;
372
373 def initFakeMainLog(self, oTestSet):
374 """
375 Reinitializes to represent the main.log object (not in DB).
376
377 Returns self.
378 """
379 self.idTestResultFile = 0;
380 self.idTestResult = oTestSet.idTestResult;
381 self.tsCreated = oTestSet.tsCreated;
382 self.idStrFile = None;
383 self.idStrDescription = None;
384 self.idStrKind = None;
385 self.idStrMime = None;
386
387 self.sFile = 'main.log';
388 self.sDescription = '';
389 self.sKind = 'log/main';
390 self.sMime = 'text/plain';
391 return self;
392
393 def isProbablyUtf8Encoded(self):
394 """
395 Checks if the file is likely to be UTF-8 encoded.
396 """
397 if self.sMime in [ 'text/plain', 'text/html' ]:
398 return True;
399 return False;
400
401 def getMimeWithEncoding(self):
402 """
403 Gets the MIME type with encoding if likely to be UTF-8.
404 """
405 if self.isProbablyUtf8Encoded():
406 return '%s; charset=utf-8' % (self.sMime,);
407 return self.sMime;
408
409
410class TestResultListingData(ModelDataBase): # pylint: disable=R0902
411 """
412 Test case result data representation for table listing
413 """
414
415 def __init__(self):
416 """Initialize"""
417 ModelDataBase.__init__(self)
418
419 self.idTestSet = None
420
421 self.idBuildCategory = None;
422 self.sProduct = None
423 self.sRepository = None;
424 self.sBranch = None
425 self.sType = None
426 self.idBuild = None;
427 self.sVersion = None;
428 self.iRevision = None
429
430 self.sOs = None;
431 self.sOsVersion = None;
432 self.sArch = None;
433 self.sCpuVendor = None;
434 self.sCpuName = None;
435 self.cCpus = None;
436 self.fCpuHwVirt = None;
437 self.fCpuNestedPaging = None;
438 self.fCpu64BitGuest = None;
439 self.idTestBox = None
440 self.sTestBoxName = None
441
442 self.tsCreated = None
443 self.tsElapsed = None
444 self.enmStatus = None
445 self.cErrors = None;
446
447 self.idTestCase = None
448 self.sTestCaseName = None
449 self.sBaseCmd = None
450 self.sArgs = None
451
452 self.idBuildTestSuite = None;
453 self.iRevisionTestSuite = None;
454
455 self.asMsgs = None;
456
457 def initFromDbRow(self, aoRow):
458 """
459 Reinitialize from a database query.
460 Return self. Raises exception if no row.
461 """
462 if aoRow is None:
463 raise TMExceptionBase('Test result record not found.')
464
465 self.idTestSet = aoRow[0];
466
467 self.idBuildCategory = aoRow[1];
468 self.sProduct = aoRow[2];
469 self.sRepository = aoRow[3];
470 self.sBranch = aoRow[4];
471 self.sType = aoRow[5];
472 self.idBuild = aoRow[6];
473 self.sVersion = aoRow[7];
474 self.iRevision = aoRow[8];
475
476 self.sOs = aoRow[9];
477 self.sOsVersion = aoRow[10];
478 self.sArch = aoRow[11];
479 self.sCpuVendor = aoRow[12];
480 self.sCpuName = aoRow[13];
481 self.cCpus = aoRow[14];
482 self.fCpuHwVirt = aoRow[15];
483 self.fCpuNestedPaging = aoRow[16];
484 self.fCpu64BitGuest = aoRow[17];
485 self.idTestBox = aoRow[18];
486 self.sTestBoxName = aoRow[19];
487
488 self.tsCreated = aoRow[20];
489 self.tsElapsed = aoRow[21];
490 self.enmStatus = aoRow[22];
491 self.cErrors = aoRow[23];
492
493 self.idTestCase = aoRow[24];
494 self.sTestCaseName = aoRow[25];
495 self.sBaseCmd = aoRow[26];
496 self.sArgs = aoRow[27];
497
498 self.idBuildTestSuite = aoRow[28];
499 self.iRevisionTestSuite = aoRow[29];
500
501 self.asMsgs = aoRow[31];
502
503 return self
504
505
506class TestResultHangingOffence(TMExceptionBase):
507 """Hanging offence committed by test case."""
508 pass;
509
510class TestResultLogic(ModelLogicBase): # pylint: disable=R0903
511 """
512 Results grouped by scheduling group.
513 """
514
515 #
516 # Result grinding for displaying in the WUI.
517 #
518
519 ksResultsGroupingTypeNone = 'ResultsGroupingTypeNone'
520 ksResultsGroupingTypeTestGroup = 'ResultsGroupingTypeTestGroup'
521 ksResultsGroupingTypeBuildRev = 'ResultsGroupingTypeBuild'
522 ksResultsGroupingTypeTestBox = 'ResultsGroupingTypeTestBox'
523 ksResultsGroupingTypeTestCase = 'ResultsGroupingTypeTestCase'
524 ksResultsGroupingTypeSchedGroup = 'ResultsGroupingTypeSchedGroup'
525
526 ksBaseTables = 'BuildCategories, Builds, TestBoxes, TestCases, TestCaseArgs,\n' \
527 ' TestSets LEFT OUTER JOIN Builds AS TestSuiteBits\n' \
528 ' ON TestSets.idBuildTestSuite = TestSuiteBits.idBuild,\n' \
529 'TestResults LEFT OUTER JOIN TestResults as MsgTab ON TestResults.idTestResult=MsgTab.idTestResultParent\n' \
530 ' LEFT OUTER JOIN TestResultMsgs as MsgTab1 ON MsgTab.idTestResult = MsgTab1.idTestResult\n' \
531 ' LEFT OUTER JOIN TestResultStrTab as MsgTab2 ON MsgTab1.idStrMsg = MsgTab2.idStr\n'
532
533 ksBasePreCondition = 'TestSets.idTestSet = TestResults.idTestSet\n' \
534 + ' AND TestResults.idTestResultParent is NULL\n' \
535 + ' AND TestSets.idBuild = Builds.idBuild\n' \
536 + ' AND Builds.tsExpire > TestSets.tsCreated\n' \
537 + ' AND Builds.tsEffective <= TestSets.tsCreated\n' \
538 + ' AND Builds.idBuildCategory = BuildCategories.idBuildCategory\n' \
539 + ' AND TestSets.idGenTestBox = TestBoxes.idGenTestBox\n' \
540 + ' AND TestSets.idGenTestCase = TestCases.idGenTestCase\n' \
541 + ' AND TestSets.idGenTestCaseArgs = TestCaseArgs.idGenTestCaseArgs\n'
542 kdResultGroupingMap = {
543 ksResultsGroupingTypeNone: (ksBaseTables,
544 ksBasePreCondition,),
545
546 ksResultsGroupingTypeTestGroup: (ksBaseTables,
547 ksBasePreCondition + ' AND TestSets.idTestGroup',),
548
549 ksResultsGroupingTypeBuildRev: (ksBaseTables,
550 ksBasePreCondition + ' AND Builds.iRevision',),
551
552 ksResultsGroupingTypeTestBox: (ksBaseTables,
553 ksBasePreCondition + ' AND TestSets.idTestBox',),
554
555 ksResultsGroupingTypeTestCase: (ksBaseTables,
556 ksBasePreCondition + ' AND TestSets.idTestCase',),
557
558 ksResultsGroupingTypeSchedGroup: (ksBaseTables,
559 ksBasePreCondition + ' AND TestBoxes.idSchedGroup',),
560 }
561
562 ksGroupByPreCondition = 'TestResults.idTestResult, TestSets.idTestSet, TestResults.cErrors,\n' \
563 'TestCases.idTestCase, TestCases.sName, TestSuiteBits.idBuild, TestSuiteBits.iRevision,\n' \
564 'BuildCategories.idBuildCategory, Builds.idBuild, Builds.sVersion, Builds.iRevision,\n' \
565 'TestBoxes.sOs, TestBoxes.sOsVersion, TestBoxes.sCpuArch, TestBoxes.sCpuVendor,\n' \
566 'TestBoxes.sCpuName, TestBoxes.cCpus, TestBoxes.fCpuHwVirt, TestBoxes.fCpuNestedPaging,\n' \
567 'TestBoxes.fCpu64BitGuest, TestBoxes.idTestBox, TestBoxes.sName, TestCases.sBaseCmd,\n' \
568 'TestCaseArgs.sArgs'
569
570 def _getTimePeriodQueryPart(self, tsNow, sInterval):
571 """
572 Get part of SQL query responsible for SELECT data within
573 specified period of time.
574 """
575 assert sInterval is not None; # too many rows.
576
577 cMonthsMourningPeriod = 2; # Stop reminding everyone about testboxes after 2 months. (May also speed up the query.)
578 if tsNow is None:
579 sRet = '(TestSets.tsDone IS NULL OR TestSets.tsDone >= (CURRENT_TIMESTAMP - \'%s\'::interval))\n' \
580 ' AND TestSets.tsCreated >= (CURRENT_TIMESTAMP - \'%s\'::interval - \'%u months\'::interval)\n' \
581 % (sInterval, sInterval, cMonthsMourningPeriod);
582 else:
583 sTsNow = '\'%s\'::TIMESTAMP' % (tsNow,); # It's actually a string already. duh.
584 sRet = 'TestSets.tsCreated <= %s\n' \
585 ' AND TestSets.tsCreated >= (%s - \'%s\'::interval - \'%u months\'::interval)\n' \
586 ' AND (TestSets.tsDone IS NULL OR TestSets.tsDone >= (%s - \'%s\'::interval))\n' \
587 % ( sTsNow,
588 sTsNow, sInterval, cMonthsMourningPeriod,
589 sTsNow, sInterval );
590 return sRet
591
592 def _getSqlQueryForGroupSearch(self, sWhat, tsNow, sInterval, enmResultsGroupingType, iResultsGroupingValue, fOnlyFailures):
593 """
594 Returns an SQL query that limits SELECT result
595 in order to satisfy @param enmResultsGroupingType.
596 """
597
598 if enmResultsGroupingType is None:
599 raise TMExceptionBase('Unknown grouping type')
600
601 if enmResultsGroupingType not in self.kdResultGroupingMap:
602 raise TMExceptionBase('Unknown grouping type')
603
604 # Get SQL query parameters
605 sTables, sCondition = self.kdResultGroupingMap[enmResultsGroupingType]
606
607 # Extend SQL query with time period limitation
608 sTimePeriodQuery = self._getTimePeriodQueryPart(tsNow, sInterval)
609
610 if iResultsGroupingValue is not None:
611 sCondition += ' = %d' % iResultsGroupingValue + '\n';
612 sCondition += ' AND ' + sTimePeriodQuery
613
614 # Extend the condition with test status limitations if requested.
615 if fOnlyFailures:
616 sCondition += '\n AND TestSets.enmStatus != \'success\'::TestStatus_T' \
617 '\n AND TestSets.enmStatus != \'running\'::TestStatus_T';
618
619 # Assemble the query.
620 sQuery = 'SELECT DISTINCT %s\n' % sWhat
621 sQuery += 'FROM %s\n' % sTables
622 sQuery += 'WHERE %s\n' % sCondition
623 sQuery += 'GROUP BY %s\n' % self.ksGroupByPreCondition
624
625 return sQuery
626
627 def fetchResultsForListing(self, iStart, cMaxRows, tsNow, sInterval, enmResultsGroupingType, iResultsGroupingValue,
628 fOnlyFailures):
629 """
630 Fetches TestResults table content.
631
632 If @param enmResultsGroupingType and @param iResultsGroupingValue
633 are not None, then resulting (returned) list contains only records
634 that match specified @param enmResultsGroupingType.
635
636 If @param enmResultsGroupingType is None, then
637 @param iResultsGroupingValue is ignored.
638
639 Returns an array (list) of TestResultData items, empty list if none.
640 Raises exception on error.
641 """
642
643 sWhat = 'TestSets.idTestSet,\n' \
644 ' BuildCategories.idBuildCategory,\n' \
645 ' BuildCategories.sProduct,\n' \
646 ' BuildCategories.sRepository,\n' \
647 ' BuildCategories.sBranch,\n' \
648 ' BuildCategories.sType,\n' \
649 ' Builds.idBuild,\n' \
650 ' Builds.sVersion,\n' \
651 ' Builds.iRevision,\n' \
652 ' TestBoxes.sOs,\n' \
653 ' TestBoxes.sOsVersion,\n' \
654 ' TestBoxes.sCpuArch,\n' \
655 ' TestBoxes.sCpuVendor,\n' \
656 ' TestBoxes.sCpuName,\n' \
657 ' TestBoxes.cCpus,\n' \
658 ' TestBoxes.fCpuHwVirt,\n' \
659 ' TestBoxes.fCpuNestedPaging,\n' \
660 ' TestBoxes.fCpu64BitGuest,\n' \
661 ' TestBoxes.idTestBox,\n' \
662 ' TestBoxes.sName,\n' \
663 ' TestResults.tsCreated,\n' \
664 ' COALESCE(TestResults.tsElapsed, CURRENT_TIMESTAMP - TestResults.tsCreated),\n' \
665 ' TestSets.enmStatus,\n' \
666 ' TestResults.cErrors,\n' \
667 ' TestCases.idTestCase,\n' \
668 ' TestCases.sName,\n' \
669 ' TestCases.sBaseCmd,\n' \
670 ' TestCaseArgs.sArgs,\n' \
671 ' TestSuiteBits.idBuild AS idBuildTestSuite,\n' \
672 ' TestSuiteBits.iRevision AS iRevisionTestSuite,\n' \
673 ' (TestSets.tsDone IS NULL) SortRunningFirst,' \
674 ' array_agg(MsgTab2.sValue) AS asMsgs' \
675 ;
676
677 sSqlQuery = self._getSqlQueryForGroupSearch(sWhat, tsNow, sInterval, enmResultsGroupingType, iResultsGroupingValue,
678 fOnlyFailures);
679
680 sSqlQuery += 'ORDER BY SortRunningFirst DESC, TestSets.idTestSet DESC\n';
681 sSqlQuery += 'LIMIT %s OFFSET %s\n' % (cMaxRows, iStart,);
682
683 self._oDb.execute(sSqlQuery);
684
685 aoRows = [];
686 for aoRow in self._oDb.fetchAll():
687 aoRows.append(TestResultListingData().initFromDbRow(aoRow))
688
689 return aoRows
690
691 def getEntriesCount(self, tsNow, sInterval, enmResultsGroupingType, iResultsGroupingValue, fOnlyFailures):
692 """
693 Get number of table records.
694
695 If @param enmResultsGroupingType and @param iResultsGroupingValue
696 are not None, then we count only only those records
697 that match specified @param enmResultsGroupingType.
698
699 If @param enmResultsGroupingType is None, then
700 @param iResultsGroupingValue is ignored.
701 """
702
703 sSqlQuery = self._getSqlQueryForGroupSearch('COUNT(TestSets.idTestSet)', tsNow, sInterval,
704 enmResultsGroupingType, iResultsGroupingValue, fOnlyFailures)
705 self._oDb.execute(sSqlQuery)
706 return self._oDb.fetchOne()[0]
707
708 def getTestGroups(self, tsNow, sPeriod):
709 """
710 Get list of uniq TestGroupData objects which
711 found in all test results.
712 """
713
714 self._oDb.execute('SELECT DISTINCT TestGroups.*\n'
715 'FROM TestGroups, TestSets\n'
716 'WHERE TestSets.idTestGroup = TestGroups.idTestGroup\n'
717 ' AND TestGroups.tsExpire > TestSets.tsCreated\n'
718 ' AND TestGroups.tsEffective <= TestSets.tsCreated'
719 ' AND ' + self._getTimePeriodQueryPart(tsNow, sPeriod))
720
721 aaoRows = self._oDb.fetchAll()
722 aoRet = []
723 for aoRow in aaoRows:
724 ## @todo Need to take time into consideration. Will go belly up if we delete a testgroup.
725 aoRet.append(TestGroupData().initFromDbRow(aoRow))
726
727 return aoRet
728
729 def getBuilds(self, tsNow, sPeriod):
730 """
731 Get list of uniq BuildDataEx objects which
732 found in all test results.
733 """
734
735 self._oDb.execute('SELECT DISTINCT Builds.*, BuildCategories.*\n'
736 'FROM Builds, BuildCategories, TestSets\n'
737 'WHERE TestSets.idBuild = Builds.idBuild\n'
738 ' AND Builds.idBuildCategory = BuildCategories.idBuildCategory\n'
739 ' AND Builds.tsExpire > TestSets.tsCreated\n'
740 ' AND Builds.tsEffective <= TestSets.tsCreated'
741 ' AND ' + self._getTimePeriodQueryPart(tsNow, sPeriod))
742
743 aaoRows = self._oDb.fetchAll()
744 aoRet = []
745 for aoRow in aaoRows:
746 aoRet.append(BuildDataEx().initFromDbRow(aoRow))
747
748 return aoRet
749
750 def getTestBoxes(self, tsNow, sPeriod):
751 """
752 Get list of uniq TestBoxData objects which
753 found in all test results.
754 """
755
756 ## @todo do all in one query.
757 self._oDb.execute('SELECT DISTINCT TestBoxes.idTestBox, TestBoxes.idGenTestBox\n'
758 'FROM TestBoxes, TestSets\n'
759 'WHERE TestSets.idGenTestBox = TestBoxes.idGenTestBox\n'
760 ' AND ' + self._getTimePeriodQueryPart(tsNow, sPeriod) +
761 'ORDER BY TestBoxes.idTestBox, TestBoxes.idGenTestBox DESC' );
762 idPrevTestBox = -1;
763 asIdGenTestBoxes = [];
764 for aoRow in self._oDb.fetchAll():
765 if aoRow[0] != idPrevTestBox:
766 idPrevTestBox = aoRow[0];
767 asIdGenTestBoxes.append(str(aoRow[1]));
768
769 aoRet = []
770 if len(asIdGenTestBoxes) > 0:
771 self._oDb.execute('SELECT *\n'
772 'FROM TestBoxes\n'
773 'WHERE idGenTestBox IN (' + ','.join(asIdGenTestBoxes) + ')\n'
774 'ORDER BY sName');
775 for aoRow in self._oDb.fetchAll():
776 aoRet.append(TestBoxData().initFromDbRow(aoRow));
777 return aoRet
778
779 def getTestCases(self, tsNow, sPeriod):
780 """
781 Get a list of unique TestCaseData objects which is appears in the test
782 specified result period.
783 """
784
785 self._oDb.execute('SELECT DISTINCT TestCases.idTestCase, TestCases.idGenTestCase, TestSets.tsConfig\n'
786 'FROM TestCases, TestSets\n'
787 'WHERE TestSets.idTestCase = TestCases.idTestCase\n'
788 ' AND TestCases.tsExpire > TestSets.tsCreated\n'
789 ' AND TestCases.tsEffective <= TestSets.tsCreated\n'
790 ' AND ' + self._getTimePeriodQueryPart(tsNow, sPeriod) +
791 'ORDER BY TestCases.idTestCase, TestCases.idGenTestCase DESC\n');
792
793 aaoRows = self._oDb.fetchAll()
794 aoRet = []
795 idPrevTestCase = -1;
796 for aoRow in aaoRows:
797 ## @todo reduce subqueries
798 if aoRow[0] != idPrevTestCase:
799 idPrevTestCase = aoRow[0];
800 aoRet.append(TestCaseData().initFromDbWithGenId(self._oDb, aoRow[1], aoRow[2]))
801
802 return aoRet
803
804 def getSchedGroups(self, tsNow, sPeriod):
805 """
806 Get list of uniq SchedGroupData objects which
807 found in all test results.
808 """
809
810 self._oDb.execute('SELECT DISTINCT TestBoxes.idSchedGroup\n'
811 'FROM TestBoxes, TestSets\n'
812 'WHERE TestSets.idGenTestBox = TestBoxes.idGenTestBox\n'
813 ' AND TestBoxes.tsExpire > TestSets.tsCreated\n'
814 ' AND TestBoxes.tsEffective <= TestSets.tsCreated'
815 ' AND ' + self._getTimePeriodQueryPart(tsNow, sPeriod))
816
817 aiRows = self._oDb.fetchAll()
818 aoRet = []
819 for iRow in aiRows:
820 ## @todo reduce subqueries
821 aoRet.append(SchedGroupData().initFromDbWithId(self._oDb, iRow))
822
823 return aoRet
824
825 def getById(self, idTestResult):
826 """
827 Get build record by its id
828 """
829 self._oDb.execute('SELECT *\n'
830 'FROM TestResults\n'
831 'WHERE idTestResult = %s\n',
832 (idTestResult,))
833
834 aRows = self._oDb.fetchAll()
835 if len(aRows) not in (0, 1):
836 raise TMExceptionBase('Found more than one test result with the same credentials. Database structure is corrupted.')
837 try:
838 return TestResultData().initFromDbRow(aRows[0])
839 except IndexError:
840 return None
841
842
843 #
844 # Details view and interface.
845 #
846
847 def fetchResultTree(self, idTestSet, cMaxDepth = None):
848 """
849 Fetches the result tree for the given test set.
850
851 Returns a tree of TestResultDataEx nodes.
852 Raises exception on invalid input and database issues.
853 """
854 # Depth first, i.e. just like the XML added them.
855 ## @todo this still isn't performing extremely well, consider optimizations.
856 sQuery = self._oDb.formatBindArgs(
857 'SELECT TestResults.*,\n'
858 ' TestResultStrTab.sValue,\n'
859 ' EXISTS ( SELECT idTestResultValue\n'
860 ' FROM TestResultValues\n'
861 ' WHERE TestResultValues.idTestResult = TestResults.idTestResult ) AS fHasValues,\n'
862 ' EXISTS ( SELECT idTestResultMsg\n'
863 ' FROM TestResultMsgs\n'
864 ' WHERE TestResultMsgs.idTestResult = TestResults.idTestResult ) AS fHasMsgs,\n'
865 ' EXISTS ( SELECT idTestResultFile\n'
866 ' FROM TestResultFiles\n'
867 ' WHERE TestResultFiles.idTestResult = TestResults.idTestResult ) AS fHasFiles\n'
868 'FROM TestResults, TestResultStrTab\n'
869 'WHERE TestResults.idTestSet = %s\n'
870 ' AND TestResults.idStrName = TestResultStrTab.idStr\n'
871 , ( idTestSet, ));
872 if cMaxDepth is not None:
873 sQuery += self._oDb.formatBindArgs(' AND TestResults.iNestingDepth <= %s\n', (cMaxDepth,));
874 sQuery += 'ORDER BY idTestResult ASC\n'
875
876 self._oDb.execute(sQuery);
877 cRows = self._oDb.getRowCount();
878 if cRows > 65536:
879 raise TMTooManyRows('Too many rows returned for idTestSet=%d: %d' % (idTestSet, cRows,));
880
881 aaoRows = self._oDb.fetchAll();
882 if len(aaoRows) == 0:
883 raise TMExceptionBase('No test results for idTestSet=%d.' % (idTestSet,));
884
885 # Set up the root node first.
886 aoRow = aaoRows[0];
887 oRoot = TestResultDataEx().initFromDbRow(aoRow);
888 if oRoot.idTestResultParent is not None:
889 raise self._oDb.integrityException('The root TestResult (#%s) has a parent (#%s)!'
890 % (oRoot.idTestResult, oRoot.idTestResultParent));
891 self._fetchResultTreeNodeExtras(oRoot, aoRow[-3], aoRow[-2], aoRow[-1]);
892
893 # The chilren (if any).
894 dLookup = { oRoot.idTestResult: oRoot };
895 oParent = oRoot;
896 for iRow in range(1, len(aaoRows)):
897 aoRow = aaoRows[iRow];
898 oCur = TestResultDataEx().initFromDbRow(aoRow);
899 self._fetchResultTreeNodeExtras(oCur, aoRow[-3], aoRow[-2], aoRow[-1]);
900
901 # Figure out and vet the parent.
902 if oParent.idTestResult != oCur.idTestResultParent:
903 oParent = dLookup.get(oCur.idTestResultParent, None);
904 if oParent is None:
905 raise self._oDb.integrityException('TestResult #%d is orphaned from its parent #%s.'
906 % (oCur.idTestResult, oCur.idTestResultParent,));
907 if oParent.iNestingDepth + 1 != oCur.iNestingDepth:
908 raise self._oDb.integrityException('TestResult #%d has incorrect nesting depth (%d instead of %d)'
909 % (oCur.idTestResult, oCur.iNestingDepth, oParent.iNestingDepth + 1,));
910
911 # Link it up.
912 oCur.oParent = oParent;
913 oParent.aoChildren.append(oCur);
914 dLookup[oCur.idTestResult] = oCur;
915
916 return (oRoot, dLookup);
917
918 def _fetchResultTreeNodeExtras(self, oCurNode, fHasValues, fHasMsgs, fHasFiles):
919 """
920 fetchResultTree worker that fetches values, message and files for the
921 specified node.
922 """
923 assert(oCurNode.aoValues == []);
924 assert(oCurNode.aoMsgs == []);
925 assert(oCurNode.aoFiles == []);
926
927 if fHasValues:
928 self._oDb.execute('SELECT TestResultValues.*,\n'
929 ' TestResultStrTab.sValue\n'
930 'FROM TestResultValues, TestResultStrTab\n'
931 'WHERE TestResultValues.idTestResult = %s\n'
932 ' AND TestResultValues.idStrName = TestResultStrTab.idStr\n'
933 'ORDER BY idTestResultValue ASC\n'
934 , ( oCurNode.idTestResult, ));
935 for aoRow in self._oDb.fetchAll():
936 oCurNode.aoValues.append(TestResultValueDataEx().initFromDbRow(aoRow));
937
938 if fHasMsgs:
939 self._oDb.execute('SELECT TestResultMsgs.*,\n'
940 ' TestResultStrTab.sValue\n'
941 'FROM TestResultMsgs, TestResultStrTab\n'
942 'WHERE TestResultMsgs.idTestResult = %s\n'
943 ' AND TestResultMsgs.idStrMsg = TestResultStrTab.idStr\n'
944 'ORDER BY idTestResultMsg ASC\n'
945 , ( oCurNode.idTestResult, ));
946 for aoRow in self._oDb.fetchAll():
947 oCurNode.aoMsgs.append(TestResultMsgDataEx().initFromDbRow(aoRow));
948
949 if fHasFiles:
950 self._oDb.execute('SELECT TestResultFiles.*,\n'
951 ' StrTabFile.sValue AS sFile,\n'
952 ' StrTabDesc.sValue AS sDescription,\n'
953 ' StrTabKind.sValue AS sKind,\n'
954 ' StrTabMime.sValue AS sMime\n'
955 'FROM TestResultFiles,\n'
956 ' TestResultStrTab AS StrTabFile,\n'
957 ' TestResultStrTab AS StrTabDesc,\n'
958 ' TestResultStrTab AS StrTabKind,\n'
959 ' TestResultStrTab AS StrTabMime\n'
960 'WHERE TestResultFiles.idTestResult = %s\n'
961 ' AND TestResultFiles.idStrFile = StrTabFile.idStr\n'
962 ' AND TestResultFiles.idStrDescription = StrTabDesc.idStr\n'
963 ' AND TestResultFiles.idStrKind = StrTabKind.idStr\n'
964 ' AND TestResultFiles.idStrMime = StrTabMime.idStr\n'
965 'ORDER BY idTestResultFile ASC\n'
966 , ( oCurNode.idTestResult, ));
967 for aoRow in self._oDb.fetchAll():
968 oCurNode.aoFiles.append(TestResultFileDataEx().initFromDbRow(aoRow));
969
970 return True;
971
972
973
974 #
975 # TestBoxController interface(s).
976 #
977
978 def _inhumeTestResults(self, aoStack, idTestSet, sError):
979 """
980 The test produces too much output, kill and bury it.
981
982 Note! We leave the test set open, only the test result records are
983 completed. Thus, _getResultStack will return an empty stack and
984 cause XML processing to fail immediately, while we can still
985 record when it actually completed in the test set the normal way.
986 """
987 self._oDb.dprint('** _inhumeTestResults: idTestSet=%d\n%s' % (idTestSet, self._stringifyStack(aoStack),));
988
989 #
990 # First add a message.
991 #
992 self._newFailureDetails(aoStack[0].idTestResult, sError, None);
993
994 #
995 # The complete all open test results.
996 #
997 for oTestResult in aoStack:
998 oTestResult.cErrors += 1;
999 self._completeTestResults(oTestResult, None, TestResultData.ksTestStatus_Failure, oTestResult.cErrors);
1000
1001 # A bit of paranoia.
1002 self._oDb.execute('UPDATE TestResults\n'
1003 'SET cErrors = cErrors + 1,\n'
1004 ' enmStatus = \'failure\'::TestStatus_T,\n'
1005 ' tsElapsed = CURRENT_TIMESTAMP - tsCreated\n'
1006 'WHERE idTestSet = %s\n'
1007 ' AND enmStatus = \'running\'::TestStatus_T\n'
1008 , ( idTestSet, ));
1009 self._oDb.commit();
1010
1011 return None;
1012
1013 def strTabString(self, sString, fCommit = False):
1014 """
1015 Gets the string table id for the given string, adding it if new.
1016
1017 Note! A copy of this code is also in TestSetLogic.
1018 """
1019 ## @todo move this and make a stored procedure for it.
1020 self._oDb.execute('SELECT idStr\n'
1021 'FROM TestResultStrTab\n'
1022 'WHERE sValue = %s'
1023 , (sString,));
1024 if self._oDb.getRowCount() == 0:
1025 self._oDb.execute('INSERT INTO TestResultStrTab (sValue)\n'
1026 'VALUES (%s)\n'
1027 'RETURNING idStr\n'
1028 , (sString,));
1029 if fCommit:
1030 self._oDb.commit();
1031 return self._oDb.fetchOne()[0];
1032
1033 @staticmethod
1034 def _stringifyStack(aoStack):
1035 """Returns a string rep of the stack."""
1036 sRet = '';
1037 for i in range(len(aoStack)):
1038 sRet += 'aoStack[%d]=%s\n' % (i, aoStack[i]);
1039 return sRet;
1040
1041 def _getResultStack(self, idTestSet):
1042 """
1043 Gets the current stack of result sets.
1044 """
1045 self._oDb.execute('SELECT *\n'
1046 'FROM TestResults\n'
1047 'WHERE idTestSet = %s\n'
1048 ' AND enmStatus = \'running\'::TestStatus_T\n'
1049 'ORDER BY idTestResult DESC'
1050 , ( idTestSet, ));
1051 aoStack = [];
1052 for aoRow in self._oDb.fetchAll():
1053 aoStack.append(TestResultData().initFromDbRow(aoRow));
1054
1055 for i in range(len(aoStack)):
1056 assert aoStack[i].iNestingDepth == len(aoStack) - i - 1, self._stringifyStack(aoStack);
1057
1058 return aoStack;
1059
1060 def _newTestResult(self, idTestResultParent, idTestSet, iNestingDepth, tsCreated, sName, dCounts, fCommit = False):
1061 """
1062 Creates a new test result.
1063 Returns the TestResultData object for the new record.
1064 May raise exception on database error.
1065 """
1066 assert idTestResultParent is not None;
1067 assert idTestResultParent > 1;
1068
1069 #
1070 # This isn't necessarily very efficient, but it's necessary to prevent
1071 # a wild test or testbox from filling up the database.
1072 #
1073 sCountName = 'cTestResults';
1074 if sCountName not in dCounts:
1075 self._oDb.execute('SELECT COUNT(idTestResult)\n'
1076 'FROM TestResults\n'
1077 'WHERE idTestSet = %s\n'
1078 , ( idTestSet,));
1079 dCounts[sCountName] = self._oDb.fetchOne()[0];
1080 dCounts[sCountName] += 1;
1081 if dCounts[sCountName] > config.g_kcMaxTestResultsPerTS:
1082 raise TestResultHangingOffence('Too many sub-tests in total!');
1083
1084 sCountName = 'cTestResultsIn%d' % (idTestResultParent,);
1085 if sCountName not in dCounts:
1086 self._oDb.execute('SELECT COUNT(idTestResult)\n'
1087 'FROM TestResults\n'
1088 'WHERE idTestResultParent = %s\n'
1089 , ( idTestResultParent,));
1090 dCounts[sCountName] = self._oDb.fetchOne()[0];
1091 dCounts[sCountName] += 1;
1092 if dCounts[sCountName] > config.g_kcMaxTestResultsPerTR:
1093 raise TestResultHangingOffence('Too many immediate sub-tests!');
1094
1095 # This is also a hanging offence.
1096 if iNestingDepth > config.g_kcMaxTestResultDepth:
1097 raise TestResultHangingOffence('To deep sub-test nesting!');
1098
1099 # Ditto.
1100 if len(sName) > config.g_kcchMaxTestResultName:
1101 raise TestResultHangingOffence('Test name is too long: %d chars - "%s"' % (len(sName), sName));
1102
1103 #
1104 # Within bounds, do the job.
1105 #
1106 idStrName = self.strTabString(sName, fCommit);
1107 self._oDb.execute('INSERT INTO TestResults (\n'
1108 ' idTestResultParent,\n'
1109 ' idTestSet,\n'
1110 ' tsCreated,\n'
1111 ' idStrName,\n'
1112 ' iNestingDepth )\n'
1113 'VALUES (%s, %s, TIMESTAMP WITH TIME ZONE %s, %s, %s)\n'
1114 'RETURNING *\n'
1115 , ( idTestResultParent, idTestSet, tsCreated, idStrName, iNestingDepth) )
1116 oData = TestResultData().initFromDbRow(self._oDb.fetchOne());
1117
1118 self._oDb.maybeCommit(fCommit);
1119 return oData;
1120
1121 def _newTestValue(self, idTestResult, idTestSet, sName, lValue, sUnit, dCounts, tsCreated = None, fCommit = False):
1122 """
1123 Creates a test value.
1124 May raise exception on database error.
1125 """
1126
1127 #
1128 # Bounds checking.
1129 #
1130 sCountName = 'cTestValues';
1131 if sCountName not in dCounts:
1132 self._oDb.execute('SELECT COUNT(idTestResultValue)\n'
1133 'FROM TestResultValues, TestResults\n'
1134 'WHERE TestResultValues.idTestResult = TestResults.idTestResult\n'
1135 ' AND TestResults.idTestSet = %s\n'
1136 , ( idTestSet,));
1137 dCounts[sCountName] = self._oDb.fetchOne()[0];
1138 dCounts[sCountName] += 1;
1139 if dCounts[sCountName] > config.g_kcMaxTestValuesPerTS:
1140 raise TestResultHangingOffence('Too many values in total!');
1141
1142 sCountName = 'cTestValuesIn%d' % (idTestResult,);
1143 if sCountName not in dCounts:
1144 self._oDb.execute('SELECT COUNT(idTestResultValue)\n'
1145 'FROM TestResultValues\n'
1146 'WHERE idTestResult = %s\n'
1147 , ( idTestResult,));
1148 dCounts[sCountName] = self._oDb.fetchOne()[0];
1149 dCounts[sCountName] += 1;
1150 if dCounts[sCountName] > config.g_kcMaxTestValuesPerTR:
1151 raise TestResultHangingOffence('Too many immediate values for one test result!');
1152
1153 if len(sName) > config.g_kcchMaxTestValueName:
1154 raise TestResultHangingOffence('Value name is too long: %d chars - "%s"' % (len(sName), sName));
1155
1156 #
1157 # Do the job.
1158 #
1159 iUnit = constants.valueunit.g_kdNameToConst.get(sUnit, constants.valueunit.NONE);
1160
1161 idStrName = self.strTabString(sName, fCommit);
1162 if tsCreated is None:
1163 self._oDb.execute('INSERT INTO TestResultValues (\n'
1164 ' idTestResult,\n'
1165 ' idTestSet,\n'
1166 ' idStrName,\n'
1167 ' lValue,\n'
1168 ' iUnit)\n'
1169 'VALUES ( %s, %s, %s, %s, %s )\n'
1170 , ( idTestResult, idTestSet, idStrName, lValue, iUnit,) );
1171 else:
1172 self._oDb.execute('INSERT INTO TestResultValues (\n'
1173 ' idTestResult,\n'
1174 ' idTestSet,\n'
1175 ' tsCreated,\n'
1176 ' idStrName,\n'
1177 ' lValue,\n'
1178 ' iUnit)\n'
1179 'VALUES ( %s, %s, TIMESTAMP WITH TIME ZONE %s, %s, %s, %s )\n'
1180 , ( idTestResult, idTestSet, tsCreated, idStrName, lValue, iUnit,) );
1181 self._oDb.maybeCommit(fCommit);
1182 return True;
1183
1184 def _newFailureDetails(self, idTestResult, sText, dCounts, tsCreated = None, fCommit = False):
1185 """
1186 Creates a record detailing cause of failure.
1187 May raise exception on database error.
1188 """
1189
1190 #
1191 # Overflow protection.
1192 #
1193 if dCounts is not None:
1194 sCountName = 'cTestMsgsIn%d' % (idTestResult,);
1195 if sCountName not in dCounts:
1196 self._oDb.execute('SELECT COUNT(idTestResultMsg)\n'
1197 'FROM TestResultMsgs\n'
1198 'WHERE idTestResult = %s\n'
1199 , ( idTestResult,));
1200 dCounts[sCountName] = self._oDb.fetchOne()[0];
1201 dCounts[sCountName] += 1;
1202 if dCounts[sCountName] > config.g_kcMaxTestMsgsPerTR:
1203 raise TestResultHangingOffence('Too many messages under for one test result!');
1204
1205 if len(sText) > config.g_kcchMaxTestMsg:
1206 raise TestResultHangingOffence('Failure details message is too long: %d chars - "%s"' % (len(sText), sText));
1207
1208 #
1209 # Do the job.
1210 #
1211 idStrMsg = self.strTabString(sText, fCommit);
1212 if tsCreated is None:
1213 self._oDb.execute('INSERT INTO TestResultMsgs (\n'
1214 ' idTestResult,\n'
1215 ' idStrMsg,\n'
1216 ' enmLevel)\n'
1217 'VALUES ( %s, %s, %s)\n'
1218 , ( idTestResult, idStrMsg, 'failure',) );
1219 else:
1220 self._oDb.execute('INSERT INTO TestResultMsgs (\n'
1221 ' idTestResult,\n'
1222 ' tsCreated,\n'
1223 ' idStrMsg,\n'
1224 ' enmLevel)\n'
1225 'VALUES ( %s, TIMESTAMP WITH TIME ZONE %s, %s, %s)\n'
1226 , ( idTestResult, tsCreated, idStrMsg, 'failure',) );
1227
1228 self._oDb.maybeCommit(fCommit);
1229 return True;
1230
1231
1232 def _completeTestResults(self, oTestResult, tsDone, enmStatus, cErrors = 0, fCommit = False):
1233 """
1234 Completes a test result. Updates the oTestResult object.
1235 May raise exception on database error.
1236 """
1237 self._oDb.dprint('** _completeTestResults: cErrors=%s tsDone=%s enmStatus=%s oTestResults=\n%s'
1238 % (cErrors, tsDone, enmStatus, oTestResult,));
1239
1240 #
1241 # Sanity check: No open sub tests (aoStack should make sure about this!).
1242 #
1243 self._oDb.execute('SELECT COUNT(idTestResult)\n'
1244 'FROM TestResults\n'
1245 'WHERE idTestResultParent = %s\n'
1246 ' AND enmStatus = %s\n'
1247 , ( oTestResult.idTestResult, TestResultData.ksTestStatus_Running,));
1248 cOpenSubTest = self._oDb.fetchOne()[0];
1249 assert cOpenSubTest == 0, 'cOpenSubTest=%d - %s' % (cOpenSubTest, oTestResult,);
1250 assert oTestResult.enmStatus == TestResultData.ksTestStatus_Running;
1251
1252 #
1253 # Make sure the reporter isn't lying about successes or error counts.
1254 #
1255 self._oDb.execute('SELECT COALESCE(SUM(cErrors), 0)\n'
1256 'FROM TestResults\n'
1257 'WHERE idTestResultParent = %s\n'
1258 , ( oTestResult.idTestResult, ));
1259 cMinErrors = self._oDb.fetchOne()[0] + oTestResult.cErrors;
1260 if cErrors < cMinErrors:
1261 cErrors = cMinErrors;
1262 if cErrors > 0 and enmStatus == TestResultData.ksTestStatus_Success:
1263 enmStatus = TestResultData.ksTestStatus_Failure
1264
1265 #
1266 # Do the update.
1267 #
1268 if tsDone is None:
1269 self._oDb.execute('UPDATE TestResults\n'
1270 'SET cErrors = %s,\n'
1271 ' enmStatus = %s,\n'
1272 ' tsElapsed = CURRENT_TIMESTAMP - tsCreated\n'
1273 'WHERE idTestResult = %s\n'
1274 'RETURNING tsElapsed'
1275 , ( cErrors, enmStatus, oTestResult.idTestResult,) );
1276 else:
1277 self._oDb.execute('UPDATE TestResults\n'
1278 'SET cErrors = %s,\n'
1279 ' enmStatus = %s,\n'
1280 ' tsElapsed = TIMESTAMP WITH TIME ZONE %s - tsCreated\n'
1281 'WHERE idTestResult = %s\n'
1282 'RETURNING tsElapsed'
1283 , ( cErrors, enmStatus, tsDone, oTestResult.idTestResult,) );
1284
1285 oTestResult.tsElapsed = self._oDb.fetchOne()[0];
1286 oTestResult.enmStatus = enmStatus;
1287 oTestResult.cErrors = cErrors;
1288
1289 self._oDb.maybeCommit(fCommit);
1290 return None;
1291
1292 def _doPopHint(self, aoStack, cStackEntries, dCounts):
1293 """ Executes a PopHint. """
1294 assert cStackEntries >= 0;
1295 while len(aoStack) > cStackEntries:
1296 if aoStack[0].enmStatus == TestResultData.ksTestStatus_Running:
1297 self._newFailureDetails(aoStack[0].idTestResult, 'XML error: Missing </Test>', dCounts);
1298 self._completeTestResults(aoStack[0], tsDone = None, cErrors = 1,
1299 enmStatus = TestResultData.ksTestStatus_Failure, fCommit = True);
1300 aoStack.pop(0);
1301 return True;
1302
1303
1304 @staticmethod
1305 def _validateElement(sName, dAttribs, fClosed):
1306 """
1307 Validates an element and its attributes.
1308 """
1309
1310 #
1311 # Validate attributes by name.
1312 #
1313
1314 # Validate integer attributes.
1315 for sAttr in [ 'errors', 'testdepth' ]:
1316 if sAttr in dAttribs:
1317 try:
1318 _ = int(dAttribs[sAttr]);
1319 except:
1320 return 'Element %s has an invalid %s attribute value: %s.' % (sName, sAttr, dAttribs[sAttr],);
1321
1322 # Validate long attributes.
1323 for sAttr in [ 'value', ]:
1324 if sAttr in dAttribs:
1325 try:
1326 _ = long(dAttribs[sAttr]);
1327 except:
1328 return 'Element %s has an invalid %s attribute value: %s.' % (sName, sAttr, dAttribs[sAttr],);
1329
1330 # Validate string attributes.
1331 for sAttr in [ 'name', 'unit', 'text' ]:
1332 if sAttr in dAttribs and len(dAttribs[sAttr]) == 0:
1333 return 'Element %s has an empty %s attribute value.' % (sName, sAttr,);
1334
1335 # Validate the timestamp attribute.
1336 if 'timestamp' in dAttribs:
1337 (dAttribs['timestamp'], sError) = ModelDataBase.validateTs(dAttribs['timestamp'], fAllowNull = False);
1338 if sError is not None:
1339 return 'Element %s has an invalid timestamp ("%s"): %s' % (sName, dAttribs['timestamp'], sError,);
1340
1341
1342 #
1343 # Check that attributes that are required are present.
1344 # We ignore extra attributes.
1345 #
1346 dElementAttribs = \
1347 {
1348 'Test': [ 'timestamp', 'name', ],
1349 'Value': [ 'timestamp', 'name', 'unit', 'value', ],
1350 'FailureDetails': [ 'timestamp', 'text', ],
1351 'Passed': [ 'timestamp', ],
1352 'Skipped': [ 'timestamp', ],
1353 'Failed': [ 'timestamp', 'errors', ],
1354 'TimedOut': [ 'timestamp', 'errors', ],
1355 'End': [ 'timestamp', ],
1356 'PushHint': [ 'testdepth', ],
1357 'PopHint': [ 'testdepth', ],
1358 };
1359 if sName not in dElementAttribs:
1360 return 'Unknown element "%s".' % (sName,);
1361 for sAttr in dElementAttribs[sName]:
1362 if sAttr not in dAttribs:
1363 return 'Element %s requires attribute "%s".' % (sName, sAttr);
1364
1365 #
1366 # Only the Test element can (and must) remain open.
1367 #
1368 if sName == 'Test' and fClosed:
1369 return '<Test/> is not allowed.';
1370 if sName != 'Test' and not fClosed:
1371 return 'All elements except <Test> must be closed.';
1372
1373 return None;
1374
1375 @staticmethod
1376 def _parseElement(sElement):
1377 """
1378 Parses an element.
1379
1380 """
1381 #
1382 # Element level bits.
1383 #
1384 sName = sElement.split()[0];
1385 sElement = sElement[len(sName):];
1386
1387 fClosed = sElement[-1] == '/';
1388 if fClosed:
1389 sElement = sElement[:-1];
1390
1391 #
1392 # Attributes.
1393 #
1394 sError = None;
1395 dAttribs = {};
1396 sElement = sElement.strip();
1397 while len(sElement) > 0:
1398 # Extract attribute name.
1399 off = sElement.find('=');
1400 if off < 0 or not sElement[:off].isalnum():
1401 sError = 'Attributes shall have alpha numberical names and have values.';
1402 break;
1403 sAttr = sElement[:off];
1404
1405 # Extract attribute value.
1406 if off + 2 >= len(sElement) or sElement[off + 1] != '"':
1407 sError = 'Attribute (%s) value is missing or not in double quotes.' % (sAttr,);
1408 break;
1409 off += 2;
1410 offEndQuote = sElement.find('"', off);
1411 if offEndQuote < 0:
1412 sError = 'Attribute (%s) value is missing end quotation mark.' % (sAttr,);
1413 break;
1414 sValue = sElement[off:offEndQuote];
1415
1416 # Check for duplicates.
1417 if sAttr in dAttribs:
1418 sError = 'Attribute "%s" appears more than once.' % (sAttr,);
1419 break;
1420
1421 # Unescape the value.
1422 sValue = sValue.replace('&lt;', '<');
1423 sValue = sValue.replace('&gt;', '>');
1424 sValue = sValue.replace('&apos;', '\'');
1425 sValue = sValue.replace('&quot;', '"');
1426 sValue = sValue.replace('&#xA;', '\n');
1427 sValue = sValue.replace('&#xD;', '\r');
1428 sValue = sValue.replace('&amp;', '&'); # last
1429
1430 # Done.
1431 dAttribs[sAttr] = sValue;
1432
1433 # advance
1434 sElement = sElement[offEndQuote + 1:];
1435 sElement = sElement.lstrip();
1436
1437 #
1438 # Validate the element before we return.
1439 #
1440 if sError is None:
1441 sError = TestResultLogic._validateElement(sName, dAttribs, fClosed);
1442
1443 return (sName, dAttribs, sError)
1444
1445 def _handleElement(self, sName, dAttribs, idTestSet, aoStack, aaiHints, dCounts):
1446 """
1447 Worker for processXmlStream that handles one element.
1448
1449 Returns None on success, error string on bad XML or similar.
1450 Raises exception on hanging offence and on database error.
1451 """
1452 if sName == 'Test':
1453 iNestingDepth = aoStack[0].iNestingDepth + 1 if len(aoStack) > 0 else 0;
1454 aoStack.insert(0, self._newTestResult(idTestResultParent = aoStack[0].idTestResult, idTestSet = idTestSet,
1455 tsCreated = dAttribs['timestamp'], sName = dAttribs['name'],
1456 iNestingDepth = iNestingDepth, dCounts = dCounts, fCommit = True) );
1457
1458 elif sName == 'Value':
1459 self._newTestValue(idTestResult = aoStack[0].idTestResult, idTestSet = idTestSet, tsCreated = dAttribs['timestamp'],
1460 sName = dAttribs['name'], sUnit = dAttribs['unit'], lValue = long(dAttribs['value']),
1461 dCounts = dCounts, fCommit = True);
1462
1463 elif sName == 'FailureDetails':
1464 self._newFailureDetails(idTestResult = aoStack[0].idTestResult, tsCreated = dAttribs['timestamp'],
1465 sText = dAttribs['text'], dCounts = dCounts, fCommit = True);
1466
1467 elif sName == 'Passed':
1468 self._completeTestResults(aoStack[0], tsDone = dAttribs['timestamp'],
1469 enmStatus = TestResultData.ksTestStatus_Success, fCommit = True);
1470
1471 elif sName == 'Skipped':
1472 self._completeTestResults(aoStack[0], tsDone = dAttribs['timestamp'],
1473 enmStatus = TestResultData.ksTestStatus_Skipped, fCommit = True);
1474
1475 elif sName == 'Failed':
1476 self._completeTestResults(aoStack[0], tsDone = dAttribs['timestamp'], cErrors = int(dAttribs['errors']),
1477 enmStatus = TestResultData.ksTestStatus_Failure, fCommit = True);
1478
1479 elif sName == 'TimedOut':
1480 self._completeTestResults(aoStack[0], tsDone = dAttribs['timestamp'], cErrors = int(dAttribs['errors']),
1481 enmStatus = TestResultData.ksTestStatus_TimedOut, fCommit = True);
1482
1483 elif sName == 'End':
1484 self._completeTestResults(aoStack[0], tsDone = dAttribs['timestamp'],
1485 cErrors = int(dAttribs.get('errors', '1')),
1486 enmStatus = TestResultData.ksTestStatus_Success, fCommit = True);
1487
1488 elif sName == 'PushHint':
1489 if len(aaiHints) > 1:
1490 return 'PushHint cannot be nested.'
1491
1492 aaiHints.insert(0, [len(aoStack), int(dAttribs['testdepth'])]);
1493
1494 elif sName == 'PopHint':
1495 if len(aaiHints) < 1:
1496 return 'No hint to pop.'
1497
1498 iDesiredTestDepth = int(dAttribs['testdepth']);
1499 cStackEntries, iTestDepth = aaiHints.pop(0);
1500 self._doPopHint(aoStack, cStackEntries, dCounts); # Fake the necessary '<End/></Test>' tags.
1501 if iDesiredTestDepth != iTestDepth:
1502 return 'PopHint tag has different testdepth: %d, on stack %d.' % (iDesiredTestDepth, iTestDepth);
1503 else:
1504 return 'Unexpected element "%s".' % (sName,);
1505 return None;
1506
1507
1508 def processXmlStream(self, sXml, idTestSet):
1509 """
1510 Processes the "XML" stream section given in sXml.
1511
1512 The sXml isn't a complete XML document, even should we save up all sXml
1513 for a given set, they may not form a complete and well formed XML
1514 document since the test may be aborted, abend or simply be buggy. We
1515 therefore do our own parsing and treat the XML tags as commands more
1516 than anything else.
1517
1518 Returns (sError, fUnforgivable), where sError is None on success.
1519 May raise database exception.
1520 """
1521 aoStack = self._getResultStack(idTestSet); # [0] == top; [-1] == bottom.
1522 if len(aoStack) == 0:
1523 return ('No open results', True);
1524 self._oDb.dprint('** processXmlStream len(aoStack)=%s' % (len(aoStack),));
1525 #self._oDb.dprint('processXmlStream: %s' % (self._stringifyStack(aoStack),));
1526 #self._oDb.dprint('processXmlStream: sXml=%s' % (sXml,));
1527
1528 dCounts = {};
1529 aaiHints = [];
1530 sError = None;
1531
1532 fExpectCloseTest = False;
1533 sXml = sXml.strip();
1534 while len(sXml) > 0:
1535 if sXml.startswith('</Test>'): # Only closing tag.
1536 offNext = len('</Test>');
1537 if len(aoStack) <= 1:
1538 sError = 'Trying to close the top test results.'
1539 break;
1540 # ASSUMES that we've just seen an <End/>, <Passed/>, <Failed/>,
1541 # <TimedOut/> or <Skipped/> tag earlier in this call!
1542 if aoStack[0].enmStatus == TestResultData.ksTestStatus_Running or not fExpectCloseTest:
1543 sError = 'Missing <End/>, <Passed/>, <Failed/>, <TimedOut/> or <Skipped/> tag.';
1544 break;
1545 aoStack.pop(0);
1546 fExpectCloseTest = False;
1547
1548 elif fExpectCloseTest:
1549 sError = 'Expected </Test>.'
1550 break;
1551
1552 elif sXml.startswith('<?xml '): # Ignore (included files).
1553 offNext = sXml.find('?>');
1554 if offNext < 0:
1555 sError = 'Unterminated <?xml ?> element.';
1556 break;
1557 offNext += 2;
1558
1559 elif sXml[0] == '<':
1560 # Parse and check the tag.
1561 if not sXml[1].isalpha():
1562 sError = 'Malformed element.';
1563 break;
1564 offNext = sXml.find('>')
1565 if offNext < 0:
1566 sError = 'Unterminated element.';
1567 break;
1568 (sName, dAttribs, sError) = self._parseElement(sXml[1:offNext]);
1569 offNext += 1;
1570 if sError is not None:
1571 break;
1572
1573 # Handle it.
1574 try:
1575 sError = self._handleElement(sName, dAttribs, idTestSet, aoStack, aaiHints, dCounts);
1576 except TestResultHangingOffence as oXcpt:
1577 self._inhumeTestResults(aoStack, idTestSet, str(oXcpt));
1578 return (str(oXcpt), True);
1579
1580
1581 fExpectCloseTest = sName in [ 'End', 'Passed', 'Failed', 'TimedOut', 'Skipped', ];
1582 else:
1583 sError = 'Unexpected content.';
1584 break;
1585
1586 # Advance.
1587 sXml = sXml[offNext:];
1588 sXml = sXml.lstrip();
1589
1590 #
1591 # Post processing checks.
1592 #
1593 if sError is None and fExpectCloseTest:
1594 sError = 'Expected </Test> before the end of the XML section.'
1595 elif sError is None and len(aaiHints) > 0:
1596 sError = 'Expected </PopHint> before the end of the XML section.'
1597 if len(aaiHints) > 0:
1598 self._doPopHint(aoStack, aaiHints[-1][0], dCounts);
1599
1600 #
1601 # Log the error.
1602 #
1603 if sError is not None:
1604 SystemLogLogic(self._oDb).addEntry(SystemLogData.ksEvent_XmlResultMalformed,
1605 'idTestSet=%s idTestResult=%s XML="%s" %s'
1606 % ( idTestSet,
1607 aoStack[0].idTestResult if len(aoStack) > 0 else -1,
1608 sXml[:30 if len(sXml) >= 30 else len(sXml)],
1609 sError, ),
1610 cHoursRepeat = 6, fCommit = True);
1611 return (sError, False);
1612
1613
1614#
1615# Unit testing.
1616#
1617
1618# pylint: disable=C0111
1619class TestResultDataTestCase(ModelDataBaseTestCase):
1620 def setUp(self):
1621 self.aoSamples = [TestResultData(),];
1622
1623class TestResultValueDataTestCase(ModelDataBaseTestCase):
1624 def setUp(self):
1625 self.aoSamples = [TestResultValueData(),];
1626
1627if __name__ == '__main__':
1628 unittest.main();
1629 # not reached.
1630
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