VirtualBox

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

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

ValidationKit: Updated (C) year.

  • Property svn:eol-style set to native
  • Property svn:keywords set to Author Date Id Revision
File size: 65.0 KB
Line 
1# -*- coding: utf-8 -*-
2# $Id: testresults.py 56295 2015-06-09 14:29:55Z vboxsync $
3# pylint: disable=C0302
4
5## @todo Rename this file to testresult.py!
6
7"""
8Test Manager - Fetch test results.
9"""
10
11__copyright__ = \
12"""
13Copyright (C) 2012-2015 Oracle Corporation
14
15This file is part of VirtualBox Open Source Edition (OSE), as
16available from http://www.virtualbox.org. This file is free software;
17you can redistribute it and/or modify it under the terms of the GNU
18General Public License (GPL) as published by the Free Software
19Foundation, in version 2 as it comes in the "COPYING" file of the
20VirtualBox OSE distribution. VirtualBox OSE is distributed in the
21hope that it will be useful, but WITHOUT ANY WARRANTY of any kind.
22
23The contents of this file may alternatively be used under the terms
24of the Common Development and Distribution License Version 1.0
25(CDDL) only, as it comes in the "COPYING.CDDL" file of the
26VirtualBox OSE distribution, in which case the provisions of the
27CDDL are applicable instead of those of the GPL.
28
29You may elect to license modified versions of this file under the
30terms and conditions of either the GPL or the CDDL or both.
31"""
32__version__ = "$Revision: 56295 $"
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 def initFromDbRow(self, aoRow):
456 """
457 Reinitialize from a database query.
458 Return self. Raises exception if no row.
459 """
460 if aoRow is None:
461 raise TMExceptionBase('Test result record not found.')
462
463 self.idTestSet = aoRow[0];
464
465 self.idBuildCategory = aoRow[1];
466 self.sProduct = aoRow[2];
467 self.sRepository = aoRow[3];
468 self.sBranch = aoRow[4];
469 self.sType = aoRow[5];
470 self.idBuild = aoRow[6];
471 self.sVersion = aoRow[7];
472 self.iRevision = aoRow[8];
473
474 self.sOs = aoRow[9];
475 self.sOsVersion = aoRow[10];
476 self.sArch = aoRow[11];
477 self.sCpuVendor = aoRow[12];
478 self.sCpuName = aoRow[13];
479 self.cCpus = aoRow[14];
480 self.fCpuHwVirt = aoRow[15];
481 self.fCpuNestedPaging = aoRow[16];
482 self.fCpu64BitGuest = aoRow[17];
483 self.idTestBox = aoRow[18];
484 self.sTestBoxName = aoRow[19];
485
486 self.tsCreated = aoRow[20];
487 self.tsElapsed = aoRow[21];
488 self.enmStatus = aoRow[22];
489 self.cErrors = aoRow[23];
490
491 self.idTestCase = aoRow[24];
492 self.sTestCaseName = aoRow[25];
493 self.sBaseCmd = aoRow[26];
494 self.sArgs = aoRow[27];
495
496 self.idBuildTestSuite = aoRow[28];
497 self.iRevisionTestSuite = aoRow[29];
498
499 return self
500
501
502class TestResultHangingOffence(TMExceptionBase):
503 """Hanging offence committed by test case."""
504 pass;
505
506class TestResultLogic(ModelLogicBase): # pylint: disable=R0903
507 """
508 Results grouped by scheduling group.
509 """
510
511 #
512 # Result grinding for displaying in the WUI.
513 #
514
515 ksResultsGroupingTypeNone = 'ResultsGroupingTypeNone'
516 ksResultsGroupingTypeTestGroup = 'ResultsGroupingTypeTestGroup'
517 ksResultsGroupingTypeBuildRev = 'ResultsGroupingTypeBuild'
518 ksResultsGroupingTypeTestBox = 'ResultsGroupingTypeTestBox'
519 ksResultsGroupingTypeTestCase = 'ResultsGroupingTypeTestCase'
520 ksResultsGroupingTypeSchedGroup = 'ResultsGroupingTypeSchedGroup'
521
522 ksBaseTables = 'BuildCategories, Builds, TestBoxes, TestResults, TestCases, TestCaseArgs,\n' \
523 + ' TestSets LEFT OUTER JOIN Builds AS TestSuiteBits\n' \
524 ' ON TestSets.idBuildTestSuite = TestSuiteBits.idBuild\n';
525
526 ksBasePreCondition = 'TestSets.idTestSet = TestResults.idTestSet\n' \
527 + ' AND TestResults.idTestResultParent is NULL\n' \
528 + ' AND TestSets.idBuild = Builds.idBuild\n' \
529 + ' AND Builds.tsExpire > TestSets.tsCreated\n' \
530 + ' AND Builds.tsEffective <= TestSets.tsCreated\n' \
531 + ' AND Builds.idBuildCategory = BuildCategories.idBuildCategory\n' \
532 + ' AND TestSets.idGenTestBox = TestBoxes.idGenTestBox\n' \
533 + ' AND TestSets.idGenTestCase = TestCases.idGenTestCase\n' \
534 + ' AND TestSets.idGenTestCaseArgs = TestCaseArgs.idGenTestCaseArgs\n'
535 kdResultGroupingMap = {
536 ksResultsGroupingTypeNone: (ksBaseTables,
537 ksBasePreCondition,),
538
539 ksResultsGroupingTypeTestGroup: (ksBaseTables,
540 ksBasePreCondition + ' AND TestSets.idTestGroup',),
541
542 ksResultsGroupingTypeBuildRev: (ksBaseTables,
543 ksBasePreCondition + ' AND Builds.iRevision',),
544
545 ksResultsGroupingTypeTestBox: (ksBaseTables,
546 ksBasePreCondition + ' AND TestSets.idTestBox',),
547
548 ksResultsGroupingTypeTestCase: (ksBaseTables,
549 ksBasePreCondition + ' AND TestSets.idTestCase',),
550
551 ksResultsGroupingTypeSchedGroup: (ksBaseTables,
552 ksBasePreCondition + ' AND TestBoxes.idSchedGroup',),
553 }
554
555 def _getTimePeriodQueryPart(self, tsNow, sInterval):
556 """
557 Get part of SQL query responsible for SELECT data within
558 specified period of time.
559 """
560 assert sInterval is not None; # too many rows.
561
562 cMonthsMourningPeriod = 2; # Stop reminding everyone about testboxes after 2 months. (May also speed up the query.)
563 if tsNow is None:
564 sRet = '(TestSets.tsDone IS NULL OR TestSets.tsDone >= (CURRENT_TIMESTAMP - \'%s\'::interval))\n' \
565 ' AND TestSets.tsCreated >= (CURRENT_TIMESTAMP - \'%s\'::interval - \'%u months\'::interval)\n' \
566 % (sInterval, sInterval, cMonthsMourningPeriod);
567 else:
568 sTsNow = '\'%s\'::TIMESTAMP' % (tsNow,); # It's actually a string already. duh.
569 sRet = 'TestSets.tsCreated <= %s\n' \
570 ' AND TestSets.tsCreated >= (%s - \'%s\'::interval - \'%u months\'::interval)\n' \
571 ' AND (TestSets.tsDone IS NULL OR TestSets.tsDone >= (%s - \'%s\'::interval))\n' \
572 % ( sTsNow,
573 sTsNow, sInterval, cMonthsMourningPeriod,
574 sTsNow, sInterval );
575 return sRet
576
577 def _getSqlQueryForGroupSearch(self, sWhat, tsNow, sInterval, enmResultsGroupingType, iResultsGroupingValue, fOnlyFailures):
578 """
579 Returns an SQL query that limits SELECT result
580 in order to satisfy @param enmResultsGroupingType.
581 """
582
583 if enmResultsGroupingType is None:
584 raise TMExceptionBase('Unknown grouping type')
585
586 if enmResultsGroupingType not in self.kdResultGroupingMap:
587 raise TMExceptionBase('Unknown grouping type')
588
589 # Get SQL query parameters
590 sTables, sCondition = self.kdResultGroupingMap[enmResultsGroupingType]
591
592 # Extend SQL query with time period limitation
593 sTimePeriodQuery = self._getTimePeriodQueryPart(tsNow, sInterval)
594
595 if iResultsGroupingValue is not None:
596 sCondition += ' = %d' % iResultsGroupingValue + '\n';
597 sCondition += ' AND ' + sTimePeriodQuery
598
599 # Extend the condition with test status limitations if requested.
600 if fOnlyFailures:
601 sCondition += '\n AND TestSets.enmStatus != \'success\'::TestStatus_T' \
602 '\n AND TestSets.enmStatus != \'running\'::TestStatus_T';
603
604 # Assemble the query.
605 sQuery = 'SELECT DISTINCT %s\n' % sWhat
606 sQuery += 'FROM %s\n' % sTables
607 sQuery += 'WHERE %s\n' % sCondition
608
609 return sQuery
610
611 def fetchResultsForListing(self, iStart, cMaxRows, tsNow, sInterval, enmResultsGroupingType, iResultsGroupingValue,
612 fOnlyFailures):
613 """
614 Fetches TestResults table content.
615
616 If @param enmResultsGroupingType and @param iResultsGroupingValue
617 are not None, then resulting (returned) list contains only records
618 that match specified @param enmResultsGroupingType.
619
620 If @param enmResultsGroupingType is None, then
621 @param iResultsGroupingValue is ignored.
622
623 Returns an array (list) of TestResultData items, empty list if none.
624 Raises exception on error.
625 """
626
627 sWhat = 'TestSets.idTestSet,\n' \
628 ' BuildCategories.idBuildCategory,\n' \
629 ' BuildCategories.sProduct,\n' \
630 ' BuildCategories.sRepository,\n' \
631 ' BuildCategories.sBranch,\n' \
632 ' BuildCategories.sType,\n' \
633 ' Builds.idBuild,\n' \
634 ' Builds.sVersion,\n' \
635 ' Builds.iRevision,\n' \
636 ' TestBoxes.sOs,\n' \
637 ' TestBoxes.sOsVersion,\n' \
638 ' TestBoxes.sCpuArch,\n' \
639 ' TestBoxes.sCpuVendor,\n' \
640 ' TestBoxes.sCpuName,\n' \
641 ' TestBoxes.cCpus,\n' \
642 ' TestBoxes.fCpuHwVirt,\n' \
643 ' TestBoxes.fCpuNestedPaging,\n' \
644 ' TestBoxes.fCpu64BitGuest,\n' \
645 ' TestBoxes.idTestBox,\n' \
646 ' TestBoxes.sName,\n' \
647 ' TestResults.tsCreated,\n' \
648 ' COALESCE(TestResults.tsElapsed, CURRENT_TIMESTAMP - TestResults.tsCreated),\n' \
649 ' TestSets.enmStatus,\n' \
650 ' TestResults.cErrors,\n' \
651 ' TestCases.idTestCase,\n' \
652 ' TestCases.sName,\n' \
653 ' TestCases.sBaseCmd,\n' \
654 ' TestCaseArgs.sArgs,\n' \
655 ' TestSuiteBits.idBuild AS idBuildTestSuite,\n' \
656 ' TestSuiteBits.iRevision AS iRevisionTestSuite,\n' \
657 ' (TestSets.tsDone IS NULL) SortRunningFirst' \
658 ;
659
660 sSqlQuery = self._getSqlQueryForGroupSearch(sWhat, tsNow, sInterval, enmResultsGroupingType, iResultsGroupingValue,
661 fOnlyFailures);
662
663 sSqlQuery += 'ORDER BY SortRunningFirst DESC, TestSets.idTestSet DESC\n';
664 sSqlQuery += 'LIMIT %s OFFSET %s\n' % (cMaxRows, iStart,);
665
666 self._oDb.execute(sSqlQuery);
667
668 aoRows = [];
669 for aoRow in self._oDb.fetchAll():
670 aoRows.append(TestResultListingData().initFromDbRow(aoRow))
671
672 return aoRows
673
674 def getEntriesCount(self, tsNow, sInterval, enmResultsGroupingType, iResultsGroupingValue, fOnlyFailures):
675 """
676 Get number of table records.
677
678 If @param enmResultsGroupingType and @param iResultsGroupingValue
679 are not None, then we count only only those records
680 that match specified @param enmResultsGroupingType.
681
682 If @param enmResultsGroupingType is None, then
683 @param iResultsGroupingValue is ignored.
684 """
685
686 sSqlQuery = self._getSqlQueryForGroupSearch('COUNT(TestSets.idTestSet)', tsNow, sInterval,
687 enmResultsGroupingType, iResultsGroupingValue, fOnlyFailures)
688 self._oDb.execute(sSqlQuery)
689 return self._oDb.fetchOne()[0]
690
691 def getTestGroups(self, tsNow, sPeriod):
692 """
693 Get list of uniq TestGroupData objects which
694 found in all test results.
695 """
696
697 self._oDb.execute('SELECT DISTINCT TestGroups.*\n'
698 'FROM TestGroups, TestSets\n'
699 'WHERE TestSets.idTestGroup = TestGroups.idTestGroup\n'
700 ' AND TestGroups.tsExpire > TestSets.tsCreated\n'
701 ' AND TestGroups.tsEffective <= TestSets.tsCreated'
702 ' AND ' + self._getTimePeriodQueryPart(tsNow, sPeriod))
703
704 aaoRows = self._oDb.fetchAll()
705 aoRet = []
706 for aoRow in aaoRows:
707 ## @todo Need to take time into consideration. Will go belly up if we delete a testgroup.
708 aoRet.append(TestGroupData().initFromDbRow(aoRow))
709
710 return aoRet
711
712 def getBuilds(self, tsNow, sPeriod):
713 """
714 Get list of uniq BuildDataEx objects which
715 found in all test results.
716 """
717
718 self._oDb.execute('SELECT DISTINCT Builds.*, BuildCategories.*\n'
719 'FROM Builds, BuildCategories, TestSets\n'
720 'WHERE TestSets.idBuild = Builds.idBuild\n'
721 ' AND Builds.idBuildCategory = BuildCategories.idBuildCategory\n'
722 ' AND Builds.tsExpire > TestSets.tsCreated\n'
723 ' AND Builds.tsEffective <= TestSets.tsCreated'
724 ' AND ' + self._getTimePeriodQueryPart(tsNow, sPeriod))
725
726 aaoRows = self._oDb.fetchAll()
727 aoRet = []
728 for aoRow in aaoRows:
729 aoRet.append(BuildDataEx().initFromDbRow(aoRow))
730
731 return aoRet
732
733 def getTestBoxes(self, tsNow, sPeriod):
734 """
735 Get list of uniq TestBoxData objects which
736 found in all test results.
737 """
738
739 ## @todo do all in one query.
740 self._oDb.execute('SELECT DISTINCT TestBoxes.idTestBox, TestBoxes.idGenTestBox\n'
741 'FROM TestBoxes, TestSets\n'
742 'WHERE TestSets.idGenTestBox = TestBoxes.idGenTestBox\n'
743 ' AND ' + self._getTimePeriodQueryPart(tsNow, sPeriod) +
744 'ORDER BY TestBoxes.idTestBox, TestBoxes.idGenTestBox DESC' );
745 idPrevTestBox = -1;
746 asIdGenTestBoxes = [];
747 for aoRow in self._oDb.fetchAll():
748 if aoRow[0] != idPrevTestBox:
749 idPrevTestBox = aoRow[0];
750 asIdGenTestBoxes.append(str(aoRow[1]));
751
752 aoRet = []
753 if len(asIdGenTestBoxes) > 0:
754 self._oDb.execute('SELECT *\n'
755 'FROM TestBoxes\n'
756 'WHERE idGenTestBox IN (' + ','.join(asIdGenTestBoxes) + ')\n'
757 'ORDER BY sName');
758 for aoRow in self._oDb.fetchAll():
759 aoRet.append(TestBoxData().initFromDbRow(aoRow));
760 return aoRet
761
762 def getTestCases(self, tsNow, sPeriod):
763 """
764 Get a list of unique TestCaseData objects which is appears in the test
765 specified result period.
766 """
767
768 self._oDb.execute('SELECT DISTINCT TestCases.idTestCase, TestCases.idGenTestCase, TestSets.tsConfig\n'
769 'FROM TestCases, TestSets\n'
770 'WHERE TestSets.idTestCase = TestCases.idTestCase\n'
771 ' AND TestCases.tsExpire > TestSets.tsCreated\n'
772 ' AND TestCases.tsEffective <= TestSets.tsCreated\n'
773 ' AND ' + self._getTimePeriodQueryPart(tsNow, sPeriod) +
774 'ORDER BY TestCases.idTestCase, TestCases.idGenTestCase DESC\n');
775
776 aaoRows = self._oDb.fetchAll()
777 aoRet = []
778 idPrevTestCase = -1;
779 for aoRow in aaoRows:
780 ## @todo reduce subqueries
781 if aoRow[0] != idPrevTestCase:
782 idPrevTestCase = aoRow[0];
783 aoRet.append(TestCaseData().initFromDbWithGenId(self._oDb, aoRow[1], aoRow[2]))
784
785 return aoRet
786
787 def getSchedGroups(self, tsNow, sPeriod):
788 """
789 Get list of uniq SchedGroupData objects which
790 found in all test results.
791 """
792
793 self._oDb.execute('SELECT DISTINCT TestBoxes.idSchedGroup\n'
794 'FROM TestBoxes, TestSets\n'
795 'WHERE TestSets.idGenTestBox = TestBoxes.idGenTestBox\n'
796 ' AND TestBoxes.tsExpire > TestSets.tsCreated\n'
797 ' AND TestBoxes.tsEffective <= TestSets.tsCreated'
798 ' AND ' + self._getTimePeriodQueryPart(tsNow, sPeriod))
799
800 aiRows = self._oDb.fetchAll()
801 aoRet = []
802 for iRow in aiRows:
803 ## @todo reduce subqueries
804 aoRet.append(SchedGroupData().initFromDbWithId(self._oDb, iRow))
805
806 return aoRet
807
808 def getById(self, idTestResult):
809 """
810 Get build record by its id
811 """
812 self._oDb.execute('SELECT *\n'
813 'FROM TestResults\n'
814 'WHERE idTestResult = %s\n',
815 (idTestResult,))
816
817 aRows = self._oDb.fetchAll()
818 if len(aRows) not in (0, 1):
819 raise TMExceptionBase('Found more than one test result with the same credentials. Database structure is corrupted.')
820 try:
821 return TestResultData().initFromDbRow(aRows[0])
822 except IndexError:
823 return None
824
825
826 #
827 # Details view and interface.
828 #
829
830 def fetchResultTree(self, idTestSet, cMaxDepth = None):
831 """
832 Fetches the result tree for the given test set.
833
834 Returns a tree of TestResultDataEx nodes.
835 Raises exception on invalid input and database issues.
836 """
837 # Depth first, i.e. just like the XML added them.
838 ## @todo this still isn't performing extremely well, consider optimizations.
839 sQuery = self._oDb.formatBindArgs(
840 'SELECT TestResults.*,\n'
841 ' TestResultStrTab.sValue,\n'
842 ' EXISTS ( SELECT idTestResultValue\n'
843 ' FROM TestResultValues\n'
844 ' WHERE TestResultValues.idTestResult = TestResults.idTestResult ) AS fHasValues,\n'
845 ' EXISTS ( SELECT idTestResultMsg\n'
846 ' FROM TestResultMsgs\n'
847 ' WHERE TestResultMsgs.idTestResult = TestResults.idTestResult ) AS fHasMsgs,\n'
848 ' EXISTS ( SELECT idTestResultFile\n'
849 ' FROM TestResultFiles\n'
850 ' WHERE TestResultFiles.idTestResult = TestResults.idTestResult ) AS fHasFiles\n'
851 'FROM TestResults, TestResultStrTab\n'
852 'WHERE TestResults.idTestSet = %s\n'
853 ' AND TestResults.idStrName = TestResultStrTab.idStr\n'
854 , ( idTestSet, ));
855 if cMaxDepth is not None:
856 sQuery += self._oDb.formatBindArgs(' AND TestResults.iNestingDepth <= %s\n', (cMaxDepth,));
857 sQuery += 'ORDER BY idTestResult ASC\n'
858
859 self._oDb.execute(sQuery);
860 cRows = self._oDb.getRowCount();
861 if cRows > 65536:
862 raise TMTooManyRows('Too many rows returned for idTestSet=%d: %d' % (idTestSet, cRows,));
863
864 aaoRows = self._oDb.fetchAll();
865 if len(aaoRows) == 0:
866 raise TMExceptionBase('No test results for idTestSet=%d.' % (idTestSet,));
867
868 # Set up the root node first.
869 aoRow = aaoRows[0];
870 oRoot = TestResultDataEx().initFromDbRow(aoRow);
871 if oRoot.idTestResultParent is not None:
872 raise self._oDb.integrityException('The root TestResult (#%s) has a parent (#%s)!'
873 % (oRoot.idTestResult, oRoot.idTestResultParent));
874 self._fetchResultTreeNodeExtras(oRoot, aoRow[-3], aoRow[-2], aoRow[-1]);
875
876 # The chilren (if any).
877 dLookup = { oRoot.idTestResult: oRoot };
878 oParent = oRoot;
879 for iRow in range(1, len(aaoRows)):
880 aoRow = aaoRows[iRow];
881 oCur = TestResultDataEx().initFromDbRow(aoRow);
882 self._fetchResultTreeNodeExtras(oCur, aoRow[-3], aoRow[-2], aoRow[-1]);
883
884 # Figure out and vet the parent.
885 if oParent.idTestResult != oCur.idTestResultParent:
886 oParent = dLookup.get(oCur.idTestResultParent, None);
887 if oParent is None:
888 raise self._oDb.integrityException('TestResult #%d is orphaned from its parent #%s.'
889 % (oCur.idTestResult, oCur.idTestResultParent,));
890 if oParent.iNestingDepth + 1 != oCur.iNestingDepth:
891 raise self._oDb.integrityException('TestResult #%d has incorrect nesting depth (%d instead of %d)'
892 % (oCur.idTestResult, oCur.iNestingDepth, oParent.iNestingDepth + 1,));
893
894 # Link it up.
895 oCur.oParent = oParent;
896 oParent.aoChildren.append(oCur);
897 dLookup[oCur.idTestResult] = oCur;
898
899 return (oRoot, dLookup);
900
901 def _fetchResultTreeNodeExtras(self, oCurNode, fHasValues, fHasMsgs, fHasFiles):
902 """
903 fetchResultTree worker that fetches values, message and files for the
904 specified node.
905 """
906 assert(oCurNode.aoValues == []);
907 assert(oCurNode.aoMsgs == []);
908 assert(oCurNode.aoFiles == []);
909
910 if fHasValues:
911 self._oDb.execute('SELECT TestResultValues.*,\n'
912 ' TestResultStrTab.sValue\n'
913 'FROM TestResultValues, TestResultStrTab\n'
914 'WHERE TestResultValues.idTestResult = %s\n'
915 ' AND TestResultValues.idStrName = TestResultStrTab.idStr\n'
916 'ORDER BY idTestResultValue ASC\n'
917 , ( oCurNode.idTestResult, ));
918 for aoRow in self._oDb.fetchAll():
919 oCurNode.aoValues.append(TestResultValueDataEx().initFromDbRow(aoRow));
920
921 if fHasMsgs:
922 self._oDb.execute('SELECT TestResultMsgs.*,\n'
923 ' TestResultStrTab.sValue\n'
924 'FROM TestResultMsgs, TestResultStrTab\n'
925 'WHERE TestResultMsgs.idTestResult = %s\n'
926 ' AND TestResultMsgs.idStrMsg = TestResultStrTab.idStr\n'
927 'ORDER BY idTestResultMsg ASC\n'
928 , ( oCurNode.idTestResult, ));
929 for aoRow in self._oDb.fetchAll():
930 oCurNode.aoMsgs.append(TestResultMsgDataEx().initFromDbRow(aoRow));
931
932 if fHasFiles:
933 self._oDb.execute('SELECT TestResultFiles.*,\n'
934 ' StrTabFile.sValue AS sFile,\n'
935 ' StrTabDesc.sValue AS sDescription,\n'
936 ' StrTabKind.sValue AS sKind,\n'
937 ' StrTabMime.sValue AS sMime\n'
938 'FROM TestResultFiles,\n'
939 ' TestResultStrTab AS StrTabFile,\n'
940 ' TestResultStrTab AS StrTabDesc,\n'
941 ' TestResultStrTab AS StrTabKind,\n'
942 ' TestResultStrTab AS StrTabMime\n'
943 'WHERE TestResultFiles.idTestResult = %s\n'
944 ' AND TestResultFiles.idStrFile = StrTabFile.idStr\n'
945 ' AND TestResultFiles.idStrDescription = StrTabDesc.idStr\n'
946 ' AND TestResultFiles.idStrKind = StrTabKind.idStr\n'
947 ' AND TestResultFiles.idStrMime = StrTabMime.idStr\n'
948 'ORDER BY idTestResultFile ASC\n'
949 , ( oCurNode.idTestResult, ));
950 for aoRow in self._oDb.fetchAll():
951 oCurNode.aoFiles.append(TestResultFileDataEx().initFromDbRow(aoRow));
952
953 return True;
954
955
956
957 #
958 # TestBoxController interface(s).
959 #
960
961 def _inhumeTestResults(self, aoStack, idTestSet, sError):
962 """
963 The test produces too much output, kill and bury it.
964
965 Note! We leave the test set open, only the test result records are
966 completed. Thus, _getResultStack will return an empty stack and
967 cause XML processing to fail immediately, while we can still
968 record when it actually completed in the test set the normal way.
969 """
970 self._oDb.dprint('** _inhumeTestResults: idTestSet=%d\n%s' % (idTestSet, self._stringifyStack(aoStack),));
971
972 #
973 # First add a message.
974 #
975 self._newFailureDetails(aoStack[0].idTestResult, sError, None);
976
977 #
978 # The complete all open test results.
979 #
980 for oTestResult in aoStack:
981 oTestResult.cErrors += 1;
982 self._completeTestResults(oTestResult, None, TestResultData.ksTestStatus_Failure, oTestResult.cErrors);
983
984 # A bit of paranoia.
985 self._oDb.execute('UPDATE TestResults\n'
986 'SET cErrors = cErrors + 1,\n'
987 ' enmStatus = \'failure\'::TestStatus_T,\n'
988 ' tsElapsed = CURRENT_TIMESTAMP - tsCreated\n'
989 'WHERE idTestSet = %s\n'
990 ' AND enmStatus = \'running\'::TestStatus_T\n'
991 , ( idTestSet, ));
992 self._oDb.commit();
993
994 return None;
995
996 def strTabString(self, sString, fCommit = False):
997 """
998 Gets the string table id for the given string, adding it if new.
999
1000 Note! A copy of this code is also in TestSetLogic.
1001 """
1002 ## @todo move this and make a stored procedure for it.
1003 self._oDb.execute('SELECT idStr\n'
1004 'FROM TestResultStrTab\n'
1005 'WHERE sValue = %s'
1006 , (sString,));
1007 if self._oDb.getRowCount() == 0:
1008 self._oDb.execute('INSERT INTO TestResultStrTab (sValue)\n'
1009 'VALUES (%s)\n'
1010 'RETURNING idStr\n'
1011 , (sString,));
1012 if fCommit:
1013 self._oDb.commit();
1014 return self._oDb.fetchOne()[0];
1015
1016 @staticmethod
1017 def _stringifyStack(aoStack):
1018 """Returns a string rep of the stack."""
1019 sRet = '';
1020 for i in range(len(aoStack)):
1021 sRet += 'aoStack[%d]=%s\n' % (i, aoStack[i]);
1022 return sRet;
1023
1024 def _getResultStack(self, idTestSet):
1025 """
1026 Gets the current stack of result sets.
1027 """
1028 self._oDb.execute('SELECT *\n'
1029 'FROM TestResults\n'
1030 'WHERE idTestSet = %s\n'
1031 ' AND enmStatus = \'running\'::TestStatus_T\n'
1032 'ORDER BY idTestResult DESC'
1033 , ( idTestSet, ));
1034 aoStack = [];
1035 for aoRow in self._oDb.fetchAll():
1036 aoStack.append(TestResultData().initFromDbRow(aoRow));
1037
1038 for i in range(len(aoStack)):
1039 assert aoStack[i].iNestingDepth == len(aoStack) - i - 1, self._stringifyStack(aoStack);
1040
1041 return aoStack;
1042
1043 def _newTestResult(self, idTestResultParent, idTestSet, iNestingDepth, tsCreated, sName, dCounts, fCommit = False):
1044 """
1045 Creates a new test result.
1046 Returns the TestResultData object for the new record.
1047 May raise exception on database error.
1048 """
1049 assert idTestResultParent is not None;
1050 assert idTestResultParent > 1;
1051
1052 #
1053 # This isn't necessarily very efficient, but it's necessary to prevent
1054 # a wild test or testbox from filling up the database.
1055 #
1056 sCountName = 'cTestResults';
1057 if sCountName not in dCounts:
1058 self._oDb.execute('SELECT COUNT(idTestResult)\n'
1059 'FROM TestResults\n'
1060 'WHERE idTestSet = %s\n'
1061 , ( idTestSet,));
1062 dCounts[sCountName] = self._oDb.fetchOne()[0];
1063 dCounts[sCountName] += 1;
1064 if dCounts[sCountName] > config.g_kcMaxTestResultsPerTS:
1065 raise TestResultHangingOffence('Too many sub-tests in total!');
1066
1067 sCountName = 'cTestResultsIn%d' % (idTestResultParent,);
1068 if sCountName not in dCounts:
1069 self._oDb.execute('SELECT COUNT(idTestResult)\n'
1070 'FROM TestResults\n'
1071 'WHERE idTestResultParent = %s\n'
1072 , ( idTestResultParent,));
1073 dCounts[sCountName] = self._oDb.fetchOne()[0];
1074 dCounts[sCountName] += 1;
1075 if dCounts[sCountName] > config.g_kcMaxTestResultsPerTR:
1076 raise TestResultHangingOffence('Too many immediate sub-tests!');
1077
1078 # This is also a hanging offence.
1079 if iNestingDepth > config.g_kcMaxTestResultDepth:
1080 raise TestResultHangingOffence('To deep sub-test nesting!');
1081
1082 # Ditto.
1083 if len(sName) > config.g_kcchMaxTestResultName:
1084 raise TestResultHangingOffence('Test name is too long: %d chars - "%s"' % (len(sName), sName));
1085
1086 #
1087 # Within bounds, do the job.
1088 #
1089 idStrName = self.strTabString(sName, fCommit);
1090 self._oDb.execute('INSERT INTO TestResults (\n'
1091 ' idTestResultParent,\n'
1092 ' idTestSet,\n'
1093 ' tsCreated,\n'
1094 ' idStrName,\n'
1095 ' iNestingDepth )\n'
1096 'VALUES (%s, %s, TIMESTAMP WITH TIME ZONE %s, %s, %s)\n'
1097 'RETURNING *\n'
1098 , ( idTestResultParent, idTestSet, tsCreated, idStrName, iNestingDepth) )
1099 oData = TestResultData().initFromDbRow(self._oDb.fetchOne());
1100
1101 self._oDb.maybeCommit(fCommit);
1102 return oData;
1103
1104 def _newTestValue(self, idTestResult, idTestSet, sName, lValue, sUnit, dCounts, tsCreated = None, fCommit = False):
1105 """
1106 Creates a test value.
1107 May raise exception on database error.
1108 """
1109
1110 #
1111 # Bounds checking.
1112 #
1113 sCountName = 'cTestValues';
1114 if sCountName not in dCounts:
1115 self._oDb.execute('SELECT COUNT(idTestResultValue)\n'
1116 'FROM TestResultValues, TestResults\n'
1117 'WHERE TestResultValues.idTestResult = TestResults.idTestResult\n'
1118 ' AND TestResults.idTestSet = %s\n'
1119 , ( idTestSet,));
1120 dCounts[sCountName] = self._oDb.fetchOne()[0];
1121 dCounts[sCountName] += 1;
1122 if dCounts[sCountName] > config.g_kcMaxTestValuesPerTS:
1123 raise TestResultHangingOffence('Too many values in total!');
1124
1125 sCountName = 'cTestValuesIn%d' % (idTestResult,);
1126 if sCountName not in dCounts:
1127 self._oDb.execute('SELECT COUNT(idTestResultValue)\n'
1128 'FROM TestResultValues\n'
1129 'WHERE idTestResult = %s\n'
1130 , ( idTestResult,));
1131 dCounts[sCountName] = self._oDb.fetchOne()[0];
1132 dCounts[sCountName] += 1;
1133 if dCounts[sCountName] > config.g_kcMaxTestValuesPerTR:
1134 raise TestResultHangingOffence('Too many immediate values for one test result!');
1135
1136 if len(sName) > config.g_kcchMaxTestValueName:
1137 raise TestResultHangingOffence('Value name is too long: %d chars - "%s"' % (len(sName), sName));
1138
1139 #
1140 # Do the job.
1141 #
1142 iUnit = constants.valueunit.g_kdNameToConst.get(sUnit, constants.valueunit.NONE);
1143
1144 idStrName = self.strTabString(sName, fCommit);
1145 if tsCreated is None:
1146 self._oDb.execute('INSERT INTO TestResultValues (\n'
1147 ' idTestResult,\n'
1148 ' idTestSet,\n'
1149 ' idStrName,\n'
1150 ' lValue,\n'
1151 ' iUnit)\n'
1152 'VALUES ( %s, %s, %s, %s, %s )\n'
1153 , ( idTestResult, idTestSet, idStrName, lValue, iUnit,) );
1154 else:
1155 self._oDb.execute('INSERT INTO TestResultValues (\n'
1156 ' idTestResult,\n'
1157 ' idTestSet,\n'
1158 ' tsCreated,\n'
1159 ' idStrName,\n'
1160 ' lValue,\n'
1161 ' iUnit)\n'
1162 'VALUES ( %s, %s, TIMESTAMP WITH TIME ZONE %s, %s, %s, %s )\n'
1163 , ( idTestResult, idTestSet, tsCreated, idStrName, lValue, iUnit,) );
1164 self._oDb.maybeCommit(fCommit);
1165 return True;
1166
1167 def _newFailureDetails(self, idTestResult, sText, dCounts, tsCreated = None, fCommit = False):
1168 """
1169 Creates a record detailing cause of failure.
1170 May raise exception on database error.
1171 """
1172
1173 #
1174 # Overflow protection.
1175 #
1176 if dCounts is not None:
1177 sCountName = 'cTestMsgsIn%d' % (idTestResult,);
1178 if sCountName not in dCounts:
1179 self._oDb.execute('SELECT COUNT(idTestResultMsg)\n'
1180 'FROM TestResultMsgs\n'
1181 'WHERE idTestResult = %s\n'
1182 , ( idTestResult,));
1183 dCounts[sCountName] = self._oDb.fetchOne()[0];
1184 dCounts[sCountName] += 1;
1185 if dCounts[sCountName] > config.g_kcMaxTestMsgsPerTR:
1186 raise TestResultHangingOffence('Too many messages under for one test result!');
1187
1188 if len(sText) > config.g_kcchMaxTestMsg:
1189 raise TestResultHangingOffence('Failure details message is too long: %d chars - "%s"' % (len(sText), sText));
1190
1191 #
1192 # Do the job.
1193 #
1194 idStrMsg = self.strTabString(sText, fCommit);
1195 if tsCreated is None:
1196 self._oDb.execute('INSERT INTO TestResultMsgs (\n'
1197 ' idTestResult,\n'
1198 ' idStrMsg,\n'
1199 ' enmLevel)\n'
1200 'VALUES ( %s, %s, %s)\n'
1201 , ( idTestResult, idStrMsg, 'failure',) );
1202 else:
1203 self._oDb.execute('INSERT INTO TestResultMsgs (\n'
1204 ' idTestResult,\n'
1205 ' tsCreated,\n'
1206 ' idStrMsg,\n'
1207 ' enmLevel)\n'
1208 'VALUES ( %s, TIMESTAMP WITH TIME ZONE %s, %s, %s)\n'
1209 , ( idTestResult, tsCreated, idStrMsg, 'failure',) );
1210
1211 self._oDb.maybeCommit(fCommit);
1212 return True;
1213
1214
1215 def _completeTestResults(self, oTestResult, tsDone, enmStatus, cErrors = 0, fCommit = False):
1216 """
1217 Completes a test result. Updates the oTestResult object.
1218 May raise exception on database error.
1219 """
1220 self._oDb.dprint('** _completeTestResults: cErrors=%s tsDone=%s enmStatus=%s oTestResults=\n%s'
1221 % (cErrors, tsDone, enmStatus, oTestResult,));
1222
1223 #
1224 # Sanity check: No open sub tests (aoStack should make sure about this!).
1225 #
1226 self._oDb.execute('SELECT COUNT(idTestResult)\n'
1227 'FROM TestResults\n'
1228 'WHERE idTestResultParent = %s\n'
1229 ' AND enmStatus = %s\n'
1230 , ( oTestResult.idTestResult, TestResultData.ksTestStatus_Running,));
1231 cOpenSubTest = self._oDb.fetchOne()[0];
1232 assert cOpenSubTest == 0, 'cOpenSubTest=%d - %s' % (cOpenSubTest, oTestResult,);
1233 assert oTestResult.enmStatus == TestResultData.ksTestStatus_Running;
1234
1235 #
1236 # Make sure the reporter isn't lying about successes or error counts.
1237 #
1238 self._oDb.execute('SELECT COALESCE(SUM(cErrors), 0)\n'
1239 'FROM TestResults\n'
1240 'WHERE idTestResultParent = %s\n'
1241 , ( oTestResult.idTestResult, ));
1242 cMinErrors = self._oDb.fetchOne()[0] + oTestResult.cErrors;
1243 if cErrors < cMinErrors:
1244 cErrors = cMinErrors;
1245 if cErrors > 0 and enmStatus == TestResultData.ksTestStatus_Success:
1246 enmStatus = TestResultData.ksTestStatus_Failure
1247
1248 #
1249 # Do the update.
1250 #
1251 if tsDone is None:
1252 self._oDb.execute('UPDATE TestResults\n'
1253 'SET cErrors = %s,\n'
1254 ' enmStatus = %s,\n'
1255 ' tsElapsed = CURRENT_TIMESTAMP - tsCreated\n'
1256 'WHERE idTestResult = %s\n'
1257 'RETURNING tsElapsed'
1258 , ( cErrors, enmStatus, oTestResult.idTestResult,) );
1259 else:
1260 self._oDb.execute('UPDATE TestResults\n'
1261 'SET cErrors = %s,\n'
1262 ' enmStatus = %s,\n'
1263 ' tsElapsed = TIMESTAMP WITH TIME ZONE %s - tsCreated\n'
1264 'WHERE idTestResult = %s\n'
1265 'RETURNING tsElapsed'
1266 , ( cErrors, enmStatus, tsDone, oTestResult.idTestResult,) );
1267
1268 oTestResult.tsElapsed = self._oDb.fetchOne()[0];
1269 oTestResult.enmStatus = enmStatus;
1270 oTestResult.cErrors = cErrors;
1271
1272 self._oDb.maybeCommit(fCommit);
1273 return None;
1274
1275 def _doPopHint(self, aoStack, cStackEntries, dCounts):
1276 """ Executes a PopHint. """
1277 assert cStackEntries >= 0;
1278 while len(aoStack) > cStackEntries:
1279 if aoStack[0].enmStatus == TestResultData.ksTestStatus_Running:
1280 self._newFailureDetails(aoStack[0].idTestResult, 'XML error: Missing </Test>', dCounts);
1281 self._completeTestResults(aoStack[0], tsDone = None, cErrors = 1,
1282 enmStatus = TestResultData.ksTestStatus_Failure, fCommit = True);
1283 aoStack.pop(0);
1284 return True;
1285
1286
1287 @staticmethod
1288 def _validateElement(sName, dAttribs, fClosed):
1289 """
1290 Validates an element and its attributes.
1291 """
1292
1293 #
1294 # Validate attributes by name.
1295 #
1296
1297 # Validate integer attributes.
1298 for sAttr in [ 'errors', 'testdepth' ]:
1299 if sAttr in dAttribs:
1300 try:
1301 _ = int(dAttribs[sAttr]);
1302 except:
1303 return 'Element %s has an invalid %s attribute value: %s.' % (sName, sAttr, dAttribs[sAttr],);
1304
1305 # Validate long attributes.
1306 for sAttr in [ 'value', ]:
1307 if sAttr in dAttribs:
1308 try:
1309 _ = long(dAttribs[sAttr]);
1310 except:
1311 return 'Element %s has an invalid %s attribute value: %s.' % (sName, sAttr, dAttribs[sAttr],);
1312
1313 # Validate string attributes.
1314 for sAttr in [ 'name', 'unit', 'text' ]:
1315 if sAttr in dAttribs and len(dAttribs[sAttr]) == 0:
1316 return 'Element %s has an empty %s attribute value.' % (sName, sAttr,);
1317
1318 # Validate the timestamp attribute.
1319 if 'timestamp' in dAttribs:
1320 (dAttribs['timestamp'], sError) = ModelDataBase.validateTs(dAttribs['timestamp'], fAllowNull = False);
1321 if sError is not None:
1322 return 'Element %s has an invalid timestamp ("%s"): %s' % (sName, dAttribs['timestamp'], sError,);
1323
1324
1325 #
1326 # Check that attributes that are required are present.
1327 # We ignore extra attributes.
1328 #
1329 dElementAttribs = \
1330 {
1331 'Test': [ 'timestamp', 'name', ],
1332 'Value': [ 'timestamp', 'name', 'unit', 'value', ],
1333 'FailureDetails': [ 'timestamp', 'text', ],
1334 'Passed': [ 'timestamp', ],
1335 'Skipped': [ 'timestamp', ],
1336 'Failed': [ 'timestamp', 'errors', ],
1337 'TimedOut': [ 'timestamp', 'errors', ],
1338 'End': [ 'timestamp', ],
1339 'PushHint': [ 'testdepth', ],
1340 'PopHint': [ 'testdepth', ],
1341 };
1342 if sName not in dElementAttribs:
1343 return 'Unknown element "%s".' % (sName,);
1344 for sAttr in dElementAttribs[sName]:
1345 if sAttr not in dAttribs:
1346 return 'Element %s requires attribute "%s".' % (sName, sAttr);
1347
1348 #
1349 # Only the Test element can (and must) remain open.
1350 #
1351 if sName == 'Test' and fClosed:
1352 return '<Test/> is not allowed.';
1353 if sName != 'Test' and not fClosed:
1354 return 'All elements except <Test> must be closed.';
1355
1356 return None;
1357
1358 @staticmethod
1359 def _parseElement(sElement):
1360 """
1361 Parses an element.
1362
1363 """
1364 #
1365 # Element level bits.
1366 #
1367 sName = sElement.split()[0];
1368 sElement = sElement[len(sName):];
1369
1370 fClosed = sElement[-1] == '/';
1371 if fClosed:
1372 sElement = sElement[:-1];
1373
1374 #
1375 # Attributes.
1376 #
1377 sError = None;
1378 dAttribs = {};
1379 sElement = sElement.strip();
1380 while len(sElement) > 0:
1381 # Extract attribute name.
1382 off = sElement.find('=');
1383 if off < 0 or not sElement[:off].isalnum():
1384 sError = 'Attributes shall have alpha numberical names and have values.';
1385 break;
1386 sAttr = sElement[:off];
1387
1388 # Extract attribute value.
1389 if off + 2 >= len(sElement) or sElement[off + 1] != '"':
1390 sError = 'Attribute (%s) value is missing or not in double quotes.' % (sAttr,);
1391 break;
1392 off += 2;
1393 offEndQuote = sElement.find('"', off);
1394 if offEndQuote < 0:
1395 sError = 'Attribute (%s) value is missing end quotation mark.' % (sAttr,);
1396 break;
1397 sValue = sElement[off:offEndQuote];
1398
1399 # Check for duplicates.
1400 if sAttr in dAttribs:
1401 sError = 'Attribute "%s" appears more than once.' % (sAttr,);
1402 break;
1403
1404 # Unescape the value.
1405 sValue = sValue.replace('&lt;', '<');
1406 sValue = sValue.replace('&gt;', '>');
1407 sValue = sValue.replace('&apos;', '\'');
1408 sValue = sValue.replace('&quot;', '"');
1409 sValue = sValue.replace('&#xA;', '\n');
1410 sValue = sValue.replace('&#xD;', '\r');
1411 sValue = sValue.replace('&amp;', '&'); # last
1412
1413 # Done.
1414 dAttribs[sAttr] = sValue;
1415
1416 # advance
1417 sElement = sElement[offEndQuote + 1:];
1418 sElement = sElement.lstrip();
1419
1420 #
1421 # Validate the element before we return.
1422 #
1423 if sError is None:
1424 sError = TestResultLogic._validateElement(sName, dAttribs, fClosed);
1425
1426 return (sName, dAttribs, sError)
1427
1428 def _handleElement(self, sName, dAttribs, idTestSet, aoStack, aaiHints, dCounts):
1429 """
1430 Worker for processXmlStream that handles one element.
1431
1432 Returns None on success, error string on bad XML or similar.
1433 Raises exception on hanging offence and on database error.
1434 """
1435 if sName == 'Test':
1436 iNestingDepth = aoStack[0].iNestingDepth + 1 if len(aoStack) > 0 else 0;
1437 aoStack.insert(0, self._newTestResult(idTestResultParent = aoStack[0].idTestResult, idTestSet = idTestSet,
1438 tsCreated = dAttribs['timestamp'], sName = dAttribs['name'],
1439 iNestingDepth = iNestingDepth, dCounts = dCounts, fCommit = True) );
1440
1441 elif sName == 'Value':
1442 self._newTestValue(idTestResult = aoStack[0].idTestResult, idTestSet = idTestSet, tsCreated = dAttribs['timestamp'],
1443 sName = dAttribs['name'], sUnit = dAttribs['unit'], lValue = long(dAttribs['value']),
1444 dCounts = dCounts, fCommit = True);
1445
1446 elif sName == 'FailureDetails':
1447 self._newFailureDetails(idTestResult = aoStack[0].idTestResult, tsCreated = dAttribs['timestamp'],
1448 sText = dAttribs['text'], dCounts = dCounts, fCommit = True);
1449
1450 elif sName == 'Passed':
1451 self._completeTestResults(aoStack[0], tsDone = dAttribs['timestamp'],
1452 enmStatus = TestResultData.ksTestStatus_Success, fCommit = True);
1453
1454 elif sName == 'Skipped':
1455 self._completeTestResults(aoStack[0], tsDone = dAttribs['timestamp'],
1456 enmStatus = TestResultData.ksTestStatus_Skipped, fCommit = True);
1457
1458 elif sName == 'Failed':
1459 self._completeTestResults(aoStack[0], tsDone = dAttribs['timestamp'], cErrors = int(dAttribs['errors']),
1460 enmStatus = TestResultData.ksTestStatus_Failure, fCommit = True);
1461
1462 elif sName == 'TimedOut':
1463 self._completeTestResults(aoStack[0], tsDone = dAttribs['timestamp'], cErrors = int(dAttribs['errors']),
1464 enmStatus = TestResultData.ksTestStatus_TimedOut, fCommit = True);
1465
1466 elif sName == 'End':
1467 self._completeTestResults(aoStack[0], tsDone = dAttribs['timestamp'],
1468 cErrors = int(dAttribs.get('errors', '1')),
1469 enmStatus = TestResultData.ksTestStatus_Success, fCommit = True);
1470
1471 elif sName == 'PushHint':
1472 if len(aaiHints) > 1:
1473 return 'PushHint cannot be nested.'
1474
1475 aaiHints.insert(0, [len(aoStack), int(dAttribs['testdepth'])]);
1476
1477 elif sName == 'PopHint':
1478 if len(aaiHints) < 1:
1479 return 'No hint to pop.'
1480
1481 iDesiredTestDepth = int(dAttribs['testdepth']);
1482 cStackEntries, iTestDepth = aaiHints.pop(0);
1483 self._doPopHint(aoStack, cStackEntries, dCounts); # Fake the necessary '<End/></Test>' tags.
1484 if iDesiredTestDepth != iTestDepth:
1485 return 'PopHint tag has different testdepth: %d, on stack %d.' % (iDesiredTestDepth, iTestDepth);
1486 else:
1487 return 'Unexpected element "%s".' % (sName,);
1488 return None;
1489
1490
1491 def processXmlStream(self, sXml, idTestSet):
1492 """
1493 Processes the "XML" stream section given in sXml.
1494
1495 The sXml isn't a complete XML document, even should we save up all sXml
1496 for a given set, they may not form a complete and well formed XML
1497 document since the test may be aborted, abend or simply be buggy. We
1498 therefore do our own parsing and treat the XML tags as commands more
1499 than anything else.
1500
1501 Returns (sError, fUnforgivable), where sError is None on success.
1502 May raise database exception.
1503 """
1504 aoStack = self._getResultStack(idTestSet); # [0] == top; [-1] == bottom.
1505 if len(aoStack) == 0:
1506 return ('No open results', True);
1507 self._oDb.dprint('** processXmlStream len(aoStack)=%s' % (len(aoStack),));
1508 #self._oDb.dprint('processXmlStream: %s' % (self._stringifyStack(aoStack),));
1509 #self._oDb.dprint('processXmlStream: sXml=%s' % (sXml,));
1510
1511 dCounts = {};
1512 aaiHints = [];
1513 sError = None;
1514
1515 fExpectCloseTest = False;
1516 sXml = sXml.strip();
1517 while len(sXml) > 0:
1518 if sXml.startswith('</Test>'): # Only closing tag.
1519 offNext = len('</Test>');
1520 if len(aoStack) <= 1:
1521 sError = 'Trying to close the top test results.'
1522 break;
1523 # ASSUMES that we've just seen an <End/>, <Passed/>, <Failed/>,
1524 # <TimedOut/> or <Skipped/> tag earlier in this call!
1525 if aoStack[0].enmStatus == TestResultData.ksTestStatus_Running or not fExpectCloseTest:
1526 sError = 'Missing <End/>, <Passed/>, <Failed/>, <TimedOut/> or <Skipped/> tag.';
1527 break;
1528 aoStack.pop(0);
1529 fExpectCloseTest = False;
1530
1531 elif fExpectCloseTest:
1532 sError = 'Expected </Test>.'
1533 break;
1534
1535 elif sXml.startswith('<?xml '): # Ignore (included files).
1536 offNext = sXml.find('?>');
1537 if offNext < 0:
1538 sError = 'Unterminated <?xml ?> element.';
1539 break;
1540 offNext += 2;
1541
1542 elif sXml[0] == '<':
1543 # Parse and check the tag.
1544 if not sXml[1].isalpha():
1545 sError = 'Malformed element.';
1546 break;
1547 offNext = sXml.find('>')
1548 if offNext < 0:
1549 sError = 'Unterminated element.';
1550 break;
1551 (sName, dAttribs, sError) = self._parseElement(sXml[1:offNext]);
1552 offNext += 1;
1553 if sError is not None:
1554 break;
1555
1556 # Handle it.
1557 try:
1558 sError = self._handleElement(sName, dAttribs, idTestSet, aoStack, aaiHints, dCounts);
1559 except TestResultHangingOffence as oXcpt:
1560 self._inhumeTestResults(aoStack, idTestSet, str(oXcpt));
1561 return (str(oXcpt), True);
1562
1563
1564 fExpectCloseTest = sName in [ 'End', 'Passed', 'Failed', 'TimedOut', 'Skipped', ];
1565 else:
1566 sError = 'Unexpected content.';
1567 break;
1568
1569 # Advance.
1570 sXml = sXml[offNext:];
1571 sXml = sXml.lstrip();
1572
1573 #
1574 # Post processing checks.
1575 #
1576 if sError is None and fExpectCloseTest:
1577 sError = 'Expected </Test> before the end of the XML section.'
1578 elif sError is None and len(aaiHints) > 0:
1579 sError = 'Expected </PopHint> before the end of the XML section.'
1580 if len(aaiHints) > 0:
1581 self._doPopHint(aoStack, aaiHints[-1][0], dCounts);
1582
1583 #
1584 # Log the error.
1585 #
1586 if sError is not None:
1587 SystemLogLogic(self._oDb).addEntry(SystemLogData.ksEvent_XmlResultMalformed,
1588 'idTestSet=%s idTestResult=%s XML="%s" %s'
1589 % ( idTestSet,
1590 aoStack[0].idTestResult if len(aoStack) > 0 else -1,
1591 sXml[:30 if len(sXml) >= 30 else len(sXml)],
1592 sError, ),
1593 cHoursRepeat = 6, fCommit = True);
1594 return (sError, False);
1595
1596
1597#
1598# Unit testing.
1599#
1600
1601# pylint: disable=C0111
1602class TestResultDataTestCase(ModelDataBaseTestCase):
1603 def setUp(self):
1604 self.aoSamples = [TestResultData(),];
1605
1606class TestResultValueDataTestCase(ModelDataBaseTestCase):
1607 def setUp(self):
1608 self.aoSamples = [TestResultValueData(),];
1609
1610if __name__ == '__main__':
1611 unittest.main();
1612 # not reached.
1613
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