Changeset 61278 in vbox for trunk/src/VBox/ValidationKit
- Timestamp:
- May 29, 2016 4:52:40 PM (9 years ago)
- Location:
- trunk/src/VBox/ValidationKit/testmanager
- Files:
-
- 6 edited
- 1 copied
Legend:
- Unmodified
- Added
- Removed
-
trunk/src/VBox/ValidationKit/testmanager/core/report.py
r61272 r61278 177 177 """ 178 178 if iPeriod == 0: 179 return 'now' ;179 return 'now' if self.tsNow is None else 'then'; 180 180 if self.cHoursPerPeriod == 24: 181 181 return '%dd ago' % (iPeriod, ); 182 if (iPeriod * self.cHoursPerPeriod) % 24 == 0: 183 return '%dd ago' % (iPeriod * self.cHoursPerPeriod / 24, ); 182 184 return '%dh ago' % (iPeriod * self.cHoursPerPeriod, ); 183 185 … … 748 750 749 751 oSet.appendPeriod(oPeriod); 750 cDeleted =oSet.pruneRowsWithZeroSumHits();752 oSet.pruneRowsWithZeroSumHits(); 751 753 752 754 -
trunk/src/VBox/ValidationKit/testmanager/core/testresultfailures.py
r61268 r61278 6 6 7 7 """ 8 Test Manager - Fetch test results.8 Test Manager - Test result failures. 9 9 """ 10 10 … … 35 35 36 36 # Validation Kit imports. 37 from common import constants; 38 from testmanager import config; 39 from testmanager.core.base import ModelDataBase, ModelLogicBase, ModelDataBaseTestCase, TMExceptionBase, \ 40 TMTooManyRows, TMInvalidData, TMRowNotFound, TMRowAlreadyExists, \ 41 ChangeLogEntry, AttributeChangeEntry; 42 from testmanager.core.testgroup import TestGroupData; 43 from testmanager.core.build import BuildDataEx; 44 from testmanager.core.failurereason import FailureReasonLogic, FailureReasonData; 45 from testmanager.core.testbox import TestBoxData; 46 from testmanager.core.testcase import TestCaseData; 47 from testmanager.core.schedgroup import SchedGroupData; 48 from testmanager.core.systemlog import SystemLogData, SystemLogLogic; 37 from testmanager.core.base import ModelDataBase, ModelLogicBase, ModelDataBaseTestCase, TMInvalidData, TMRowNotFound, \ 38 TMRowAlreadyExists, ChangeLogEntry, AttributeChangeEntry; 39 from testmanager.core.failurereason import FailureReasonData; 49 40 from testmanager.core.useraccount import UserAccountLogic; 50 41 51 52 class TestResultData(ModelDataBase):53 """54 Test case execution result data55 """56 57 ## @name TestStatus_T58 # @{59 ksTestStatus_Running = 'running';60 ksTestStatus_Success = 'success';61 ksTestStatus_Skipped = 'skipped';62 ksTestStatus_BadTestBox = 'bad-testbox';63 ksTestStatus_Aborted = 'aborted';64 ksTestStatus_Failure = 'failure';65 ksTestStatus_TimedOut = 'timed-out';66 ksTestStatus_Rebooted = 'rebooted';67 ## @}68 69 ## List of relatively harmless (to testgroup/case) statuses.70 kasHarmlessTestStatuses = [ ksTestStatus_Skipped, ksTestStatus_BadTestBox, ksTestStatus_Aborted, ];71 ## List of bad statuses.72 kasBadTestStatuses = [ ksTestStatus_Failure, ksTestStatus_TimedOut, ksTestStatus_Rebooted, ];73 74 75 ksIdAttr = 'idTestResult';76 77 ksParam_idTestResult = 'TestResultData_idTestResult';78 ksParam_idTestResultParent = 'TestResultData_idTestResultParent';79 ksParam_idTestSet = 'TestResultData_idTestSet';80 ksParam_tsCreated = 'TestResultData_tsCreated';81 ksParam_tsElapsed = 'TestResultData_tsElapsed';82 ksParam_idStrName = 'TestResultData_idStrName';83 ksParam_cErrors = 'TestResultData_cErrors';84 ksParam_enmStatus = 'TestResultData_enmStatus';85 ksParam_iNestingDepth = 'TestResultData_iNestingDepth';86 kasValidValues_enmStatus = [87 ksTestStatus_Running,88 ksTestStatus_Success,89 ksTestStatus_Skipped,90 ksTestStatus_BadTestBox,91 ksTestStatus_Aborted,92 ksTestStatus_Failure,93 ksTestStatus_TimedOut,94 ksTestStatus_Rebooted95 ];96 97 98 def __init__(self):99 ModelDataBase.__init__(self)100 self.idTestResult = None101 self.idTestResultParent = None102 self.idTestSet = None103 self.tsCreated = None104 self.tsElapsed = None105 self.idStrName = None106 self.cErrors = 0;107 self.enmStatus = None108 self.iNestingDepth = None109 110 def initFromDbRow(self, aoRow):111 """112 Reinitialize from a SELECT * FROM TestResults.113 Return self. Raises exception if no row.114 """115 if aoRow is None:116 raise TMRowNotFound('Test result record not found.')117 118 self.idTestResult = aoRow[0]119 self.idTestResultParent = aoRow[1]120 self.idTestSet = aoRow[2]121 self.tsCreated = aoRow[3]122 self.tsElapsed = aoRow[4]123 self.idStrName = aoRow[5]124 self.cErrors = aoRow[6]125 self.enmStatus = aoRow[7]126 self.iNestingDepth = aoRow[8]127 return self;128 129 def initFromDbWithId(self, oDb, idTestResult, tsNow = None, sPeriodBack = None):130 """131 Initialize from the database, given the ID of a row.132 """133 _ = tsNow;134 _ = sPeriodBack;135 oDb.execute('SELECT *\n'136 'FROM TestResults\n'137 'WHERE idTestResult = %s\n'138 , ( idTestResult,));139 aoRow = oDb.fetchOne()140 if aoRow is None:141 raise TMRowNotFound('idTestResult=%s not found' % (idTestResult,));142 return self.initFromDbRow(aoRow);143 144 def isFailure(self):145 """ Check if it's a real failure. """146 return self.enmStatus in self.kasBadTestStatuses;147 148 149 class TestResultDataEx(TestResultData):150 """151 Extended test result data class.152 153 This is intended for use as a node in a result tree. This is not intended154 for serialization to parameters or vice versa. Use TestResultLogic to155 construct the tree.156 """157 158 def __init__(self):159 TestResultData.__init__(self)160 self.sName = None; # idStrName resolved.161 self.oParent = None; # idTestResultParent within the tree.162 163 self.aoChildren = []; # TestResultDataEx;164 self.aoValues = []; # TestResultValueDataEx;165 self.aoMsgs = []; # TestResultMsgDataEx;166 self.aoFiles = []; # TestResultFileDataEx;167 self.oReason = None; # TestResultReasonDataEx;168 169 def initFromDbRow(self, aoRow):170 """171 Initialize from a query like this:172 SELECT TestResults.*, TestResultStrTab.sValue173 FROM TestResults, TestResultStrTab174 WHERE TestResultStrTab.idStr = TestResults.idStrName175 176 Note! The caller is expected to fetch children, values, failure177 details, and files.178 """179 self.sName = None;180 self.oParent = None;181 self.aoChildren = [];182 self.aoValues = [];183 self.aoMsgs = [];184 self.aoFiles = [];185 self.oReason = None;186 187 TestResultData.initFromDbRow(self, aoRow);188 189 self.sName = aoRow[9];190 return self;191 192 193 class TestResultValueData(ModelDataBase):194 """195 Test result value data.196 """197 198 ksIdAttr = 'idTestResultValue';199 200 ksParam_idTestResultValue = 'TestResultValue_idTestResultValue';201 ksParam_idTestResult = 'TestResultValue_idTestResult';202 ksParam_idTestSet = 'TestResultValue_idTestSet';203 ksParam_tsCreated = 'TestResultValue_tsCreated';204 ksParam_idStrName = 'TestResultValue_idStrName';205 ksParam_lValue = 'TestResultValue_lValue';206 ksParam_iUnit = 'TestResultValue_iUnit';207 208 def __init__(self):209 ModelDataBase.__init__(self)210 self.idTestResultValue = None;211 self.idTestResult = None;212 self.idTestSet = None;213 self.tsCreated = None;214 self.idStrName = None;215 self.lValue = None;216 self.iUnit = 0;217 218 def initFromDbRow(self, aoRow):219 """220 Reinitialize from a SELECT * FROM TestResultValues.221 Return self. Raises exception if no row.222 """223 if aoRow is None:224 raise TMRowNotFound('Test result value record not found.')225 226 self.idTestResultValue = aoRow[0];227 self.idTestResult = aoRow[1];228 self.idTestSet = aoRow[2];229 self.tsCreated = aoRow[3];230 self.idStrName = aoRow[4];231 self.lValue = aoRow[5];232 self.iUnit = aoRow[6];233 return self;234 235 236 class TestResultValueDataEx(TestResultValueData):237 """238 Extends TestResultValue by resolving the value name and unit string.239 """240 241 def __init__(self):242 TestResultValueData.__init__(self)243 self.sName = None;244 self.sUnit = '';245 246 def initFromDbRow(self, aoRow):247 """248 Reinitialize from a query like this:249 SELECT TestResultValues.*, TestResultStrTab.sValue250 FROM TestResultValues, TestResultStrTab251 WHERE TestResultStrTab.idStr = TestResultValues.idStrName252 253 Return self. Raises exception if no row.254 """255 TestResultValueData.initFromDbRow(self, aoRow);256 self.sName = aoRow[7];257 if self.iUnit < len(constants.valueunit.g_asNames):258 self.sUnit = constants.valueunit.g_asNames[self.iUnit];259 else:260 self.sUnit = '<%d>' % (self.iUnit,);261 return self;262 263 class TestResultMsgData(ModelDataBase):264 """265 Test result message data.266 """267 268 ksIdAttr = 'idTestResultMsg';269 270 ksParam_idTestResultMsg = 'TestResultValue_idTestResultMsg';271 ksParam_idTestResult = 'TestResultValue_idTestResult';272 ksParam_tsCreated = 'TestResultValue_tsCreated';273 ksParam_idStrMsg = 'TestResultValue_idStrMsg';274 ksParam_enmLevel = 'TestResultValue_enmLevel';275 276 def __init__(self):277 ModelDataBase.__init__(self)278 self.idTestResultMsg = None;279 self.idTestResult = None;280 self.tsCreated = None;281 self.idStrMsg = None;282 self.enmLevel = None;283 284 def initFromDbRow(self, aoRow):285 """286 Reinitialize from a SELECT * FROM TestResultMsgs.287 Return self. Raises exception if no row.288 """289 if aoRow is None:290 raise TMRowNotFound('Test result value record not found.')291 292 self.idTestResultMsg = aoRow[0];293 self.idTestResult = aoRow[1];294 self.tsCreated = aoRow[2];295 self.idStrMsg = aoRow[3];296 self.enmLevel = aoRow[4];297 return self;298 299 class TestResultMsgDataEx(TestResultMsgData):300 """301 Extends TestResultMsg by resolving the message string.302 """303 304 def __init__(self):305 TestResultMsgData.__init__(self)306 self.sMsg = None;307 308 def initFromDbRow(self, aoRow):309 """310 Reinitialize from a query like this:311 SELECT TestResultMsg.*, TestResultStrTab.sValue312 FROM TestResultMsg, TestResultStrTab313 WHERE TestResultStrTab.idStr = TestResultMsgs.idStrName314 315 Return self. Raises exception if no row.316 """317 TestResultMsgData.initFromDbRow(self, aoRow);318 self.sMsg = aoRow[5];319 return self;320 321 class TestResultFileData(ModelDataBase):322 """323 Test result message data.324 """325 326 ksIdAttr = 'idTestResultFile';327 328 ksParam_idTestResultFile = 'TestResultFile_idTestResultFile';329 ksParam_idTestResult = 'TestResultFile_idTestResult';330 ksParam_tsCreated = 'TestResultFile_tsCreated';331 ksParam_idStrFile = 'TestResultFile_idStrFile';332 ksParam_idStrDescription = 'TestResultFile_idStrDescription';333 ksParam_idStrKind = 'TestResultFile_idStrKind';334 ksParam_idStrMime = 'TestResultFile_idStrMime';335 336 def __init__(self):337 ModelDataBase.__init__(self)338 self.idTestResultFile = None;339 self.idTestResult = None;340 self.tsCreated = None;341 self.idStrFile = None;342 self.idStrDescription = None;343 self.idStrKind = None;344 self.idStrMime = None;345 346 def initFromDbRow(self, aoRow):347 """348 Reinitialize from a SELECT * FROM TestResultFiles.349 Return self. Raises exception if no row.350 """351 if aoRow is None:352 raise TMRowNotFound('Test result file record not found.')353 354 self.idTestResultFile = aoRow[0];355 self.idTestResult = aoRow[1];356 self.tsCreated = aoRow[2];357 self.idStrFile = aoRow[3];358 self.idStrDescription = aoRow[4];359 self.idStrKind = aoRow[5];360 self.idStrMime = aoRow[6];361 return self;362 363 class TestResultFileDataEx(TestResultFileData):364 """365 Extends TestResultFile by resolving the strings.366 """367 368 def __init__(self):369 TestResultFileData.__init__(self)370 self.sFile = None;371 self.sDescription = None;372 self.sKind = None;373 self.sMime = None;374 375 def initFromDbRow(self, aoRow):376 """377 Reinitialize from a query like this:378 SELECT TestResultFiles.*,379 StrTabFile.sValue AS sFile,380 StrTabDesc.sValue AS sDescription381 StrTabKind.sValue AS sKind,382 StrTabMime.sValue AS sMime,383 FROM ...384 385 Return self. Raises exception if no row.386 """387 TestResultFileData.initFromDbRow(self, aoRow);388 self.sFile = aoRow[7];389 self.sDescription = aoRow[8];390 self.sKind = aoRow[9];391 self.sMime = aoRow[10];392 return self;393 394 def initFakeMainLog(self, oTestSet):395 """396 Reinitializes to represent the main.log object (not in DB).397 398 Returns self.399 """400 self.idTestResultFile = 0;401 self.idTestResult = oTestSet.idTestResult;402 self.tsCreated = oTestSet.tsCreated;403 self.idStrFile = None;404 self.idStrDescription = None;405 self.idStrKind = None;406 self.idStrMime = None;407 408 self.sFile = 'main.log';409 self.sDescription = '';410 self.sKind = 'log/main';411 self.sMime = 'text/plain';412 return self;413 414 def isProbablyUtf8Encoded(self):415 """416 Checks if the file is likely to be UTF-8 encoded.417 """418 if self.sMime in [ 'text/plain', 'text/html' ]:419 return True;420 return False;421 422 def getMimeWithEncoding(self):423 """424 Gets the MIME type with encoding if likely to be UTF-8.425 """426 if self.isProbablyUtf8Encoded():427 return '%s; charset=utf-8' % (self.sMime,);428 return self.sMime;429 42 430 43 … … 626 239 627 240 return self 628 629 630 class TestResultHangingOffence(TMExceptionBase):631 """Hanging offence committed by test case."""632 pass;633 634 635 class TestResultLogic(ModelLogicBase): # pylint: disable=R0903636 """637 Results grouped by scheduling group.638 """639 640 #641 # Result grinding for displaying in the WUI.642 #643 644 ksResultsGroupingTypeNone = 'ResultsGroupingTypeNone';645 ksResultsGroupingTypeTestGroup = 'ResultsGroupingTypeTestGroup';646 ksResultsGroupingTypeBuildRev = 'ResultsGroupingTypeBuild';647 ksResultsGroupingTypeTestBox = 'ResultsGroupingTypeTestBox';648 ksResultsGroupingTypeTestCase = 'ResultsGroupingTypeTestCase';649 ksResultsGroupingTypeSchedGroup = 'ResultsGroupingTypeSchedGroup';650 651 ## @name Result sorting options.652 ## @{653 ksResultsSortByRunningAndStart = 'ResultsSortByRunningAndStart'; ##< Default654 ksResultsSortByBuildRevision = 'ResultsSortByBuildRevision';655 ksResultsSortByTestBoxName = 'ResultsSortByTestBoxName';656 ksResultsSortByTestBoxOs = 'ResultsSortByTestBoxOs';657 ksResultsSortByTestBoxOsVersion = 'ResultsSortByTestBoxOsVersion';658 ksResultsSortByTestBoxOsArch = 'ResultsSortByTestBoxOsArch';659 ksResultsSortByTestBoxArch = 'ResultsSortByTestBoxArch';660 ksResultsSortByTestBoxCpuVendor = 'ResultsSortByTestBoxCpuVendor';661 ksResultsSortByTestBoxCpuName = 'ResultsSortByTestBoxCpuName';662 ksResultsSortByTestBoxCpuRev = 'ResultsSortByTestBoxCpuRev';663 ksResultsSortByTestBoxCpuFeatures = 'ResultsSortByTestBoxCpuFeatures';664 ksResultsSortByTestCaseName = 'ResultsSortByTestCaseName';665 ksResultsSortByFailureReason = 'ResultsSortByFailureReason';666 kasResultsSortBy = {667 ksResultsSortByRunningAndStart,668 ksResultsSortByBuildRevision,669 ksResultsSortByTestBoxName,670 ksResultsSortByTestBoxOs,671 ksResultsSortByTestBoxOsVersion,672 ksResultsSortByTestBoxOsArch,673 ksResultsSortByTestBoxArch,674 ksResultsSortByTestBoxCpuVendor,675 ksResultsSortByTestBoxCpuName,676 ksResultsSortByTestBoxCpuRev,677 ksResultsSortByTestBoxCpuFeatures,678 ksResultsSortByTestCaseName,679 ksResultsSortByFailureReason,680 };681 ## Used by the WUI for generating the drop down.682 kaasResultsSortByTitles = (683 ( ksResultsSortByRunningAndStart, 'Running & Start TS' ),684 ( ksResultsSortByBuildRevision, 'Build Revision' ),685 ( ksResultsSortByTestBoxName, 'TestBox Name' ),686 ( ksResultsSortByTestBoxOs, 'O/S' ),687 ( ksResultsSortByTestBoxOsVersion, 'O/S Version' ),688 ( ksResultsSortByTestBoxOsArch, 'O/S & Architecture' ),689 ( ksResultsSortByTestBoxArch, 'Architecture' ),690 ( ksResultsSortByTestBoxCpuVendor, 'CPU Vendor' ),691 ( ksResultsSortByTestBoxCpuName, 'CPU Vendor & Name' ),692 ( ksResultsSortByTestBoxCpuRev, 'CPU Vendor & Revision' ),693 ( ksResultsSortByTestBoxCpuFeatures, 'CPU Features' ),694 ( ksResultsSortByTestCaseName, 'Test Case Name' ),695 ( ksResultsSortByFailureReason, 'Failure Reason' ),696 );697 ## @}698 699 ## Default sort by map.700 kdResultSortByMap = {701 ksResultsSortByRunningAndStart: ('', None, None, ''),702 ksResultsSortByBuildRevision: (703 # Sorting tables.704 ', Builds',705 # Sorting table join(s).706 ' AND TestSets.idBuild = Builds.idBuild'707 ' AND Builds.tsExpire >= TestSets.tsCreated'708 ' AND Builds.tsEffective <= TestSets.tsCreated',709 # Start of ORDER BY statement.710 ' Builds.iRevision DESC',711 # Extra columns to fetch for the above ORDER BY to work in a SELECT DISTINCT statement.712 '' ),713 ksResultsSortByTestBoxName: (714 ', TestBoxes',715 ' AND TestSets.idGenTestBox = TestBoxes.idGenTestBox',716 ' TestBoxes.sName DESC',717 '' ),718 ksResultsSortByTestBoxOsArch: (719 ', TestBoxes',720 ' AND TestSets.idGenTestBox = TestBoxes.idGenTestBox',721 ' TestBoxes.sOs, TestBoxes.sCpuArch',722 '' ),723 ksResultsSortByTestBoxOs: (724 ', TestBoxes',725 ' AND TestSets.idGenTestBox = TestBoxes.idGenTestBox',726 ' TestBoxes.sOs',727 '' ),728 ksResultsSortByTestBoxOsVersion: (729 ', TestBoxes',730 ' AND TestSets.idGenTestBox = TestBoxes.idGenTestBox',731 ' TestBoxes.sOs, TestBoxes.sOsVersion DESC',732 '' ),733 ksResultsSortByTestBoxArch: (734 ', TestBoxes',735 ' AND TestSets.idGenTestBox = TestBoxes.idGenTestBox',736 ' TestBoxes.sCpuArch',737 '' ),738 ksResultsSortByTestBoxCpuVendor: (739 ', TestBoxes',740 ' AND TestSets.idGenTestBox = TestBoxes.idGenTestBox',741 ' TestBoxes.sCpuVendor',742 '' ),743 ksResultsSortByTestBoxCpuName: (744 ', TestBoxes',745 ' AND TestSets.idGenTestBox = TestBoxes.idGenTestBox',746 ' TestBoxes.sCpuVendor, TestBoxes.sCpuName',747 '' ),748 ksResultsSortByTestBoxCpuRev: (749 ', TestBoxes',750 ' AND TestSets.idGenTestBox = TestBoxes.idGenTestBox',751 ' TestBoxes.sCpuVendor, TestBoxes.lCpuRevision DESC',752 ', TestBoxes.lCpuRevision' ),753 ksResultsSortByTestBoxCpuFeatures: (754 ', TestBoxes',755 ' AND TestSets.idGenTestBox = TestBoxes.idGenTestBox',756 ' TestBoxes.fCpuHwVirt DESC, TestBoxes.fCpuNestedPaging DESC, TestBoxes.fCpu64BitGuest DESC, TestBoxes.cCpus DESC',757 ', TestBoxes.cCpus' ),758 ksResultsSortByTestCaseName: (759 ', TestCases',760 ' AND TestSets.idGenTestCase = TestCases.idGenTestCase',761 ' TestCases.sName',762 '' ),763 ksResultsSortByFailureReason: (764 '', '',765 'sSortByFailureReason ASC',766 ', FailureReasons.sShort AS sSortByFailureReason' ),767 };768 769 kdResultGroupingMap = {770 ksResultsGroupingTypeNone: (771 # Grouping tables; # Grouping field; # Grouping where addition. # Sort by overrides.772 '', None, None, {}773 ),774 ksResultsGroupingTypeTestGroup: ('', 'TestSets.idTestGroup', None, {}),775 ksResultsGroupingTypeTestBox: ('', 'TestSets.idTestBox', None, {}),776 ksResultsGroupingTypeTestCase: ('', 'TestSets.idTestCase', None, {}),777 ksResultsGroupingTypeBuildRev: (778 ', Builds',779 'Builds.iRevision',780 ' AND Builds.idBuild = TestSets.idBuild'781 ' AND Builds.tsExpire > TestSets.tsCreated'782 ' AND Builds.tsEffective <= TestSets.tsCreated',783 { ksResultsSortByBuildRevision: ( '', None, ' Builds.iRevision DESC' ), }784 ),785 ksResultsGroupingTypeSchedGroup: (786 ', TestBoxes',787 'TestBoxes.idSchedGroup',788 ' AND TestSets.idGenTestBox = TestBoxes.idGenTestBox',789 { ksResultsSortByTestBoxName: ( '', None, ' TestBoxes.sName DESC', '' ),790 ksResultsSortByTestBoxOsArch: ( '', None, ' TestBoxes.sOs, TestBoxes.sCpuArch', '' ),791 ksResultsSortByTestBoxOs: ( '', None, ' TestBoxes.sOs', '' ),792 ksResultsSortByTestBoxOsVersion: ( '', None, ' TestBoxes.sOs, TestBoxes.sOsVersion DESC', '' ),793 ksResultsSortByTestBoxArch: ( '', None, ' TestBoxes.sCpuArch', '' ),794 ksResultsSortByTestBoxCpuVendor: ( '', None, ' TestBoxes.sCpuVendor', '' ),795 ksResultsSortByTestBoxCpuName: ( '', None, ' TestBoxes.sCpuVendor, TestBoxes.sCpuName', '' ),796 ksResultsSortByTestBoxCpuRev: (797 '', None, ' TestBoxes.sCpuVendor, TestBoxes.lCpuRevision DESC', ', TestBoxes.lCpuRevision' ),798 ksResultsSortByTestBoxCpuFeatures: (799 ' TestBoxes.fCpuHwVirt DESC, TestBoxes.fCpuNestedPaging DESC, TestBoxes.fCpu64BitGuest DESC, '800 + 'TestBoxes.cCpus DESC',801 ', TestBoxes.cCpus' ), }802 ),803 };804 805 806 def __init__(self, oDb):807 ModelLogicBase.__init__(self, oDb)808 self.oFailureReasonLogic = None;809 self.oUserAccountLogic = None;810 811 def _getTimePeriodQueryPart(self, tsNow, sInterval, sExtraIndent = ''):812 """813 Get part of SQL query responsible for SELECT data within814 specified period of time.815 """816 assert sInterval is not None; # too many rows.817 818 cMonthsMourningPeriod = 2; # Stop reminding everyone about testboxes after 2 months. (May also speed up the query.)819 if tsNow is None:820 sRet = '(TestSets.tsDone IS NULL OR TestSets.tsDone >= (CURRENT_TIMESTAMP - \'%s\'::interval))\n' \821 '%s AND TestSets.tsCreated >= (CURRENT_TIMESTAMP - \'%s\'::interval - \'%u months\'::interval)\n' \822 % ( sInterval,823 sExtraIndent, sInterval, cMonthsMourningPeriod);824 else:825 sTsNow = '\'%s\'::TIMESTAMP' % (tsNow,); # It's actually a string already. duh.826 sRet = 'TestSets.tsCreated <= %s\n' \827 '%s AND TestSets.tsCreated >= (%s - \'%s\'::interval - \'%u months\'::interval)\n' \828 '%s AND (TestSets.tsDone IS NULL OR TestSets.tsDone >= (%s - \'%s\'::interval))\n' \829 % ( sTsNow,830 sExtraIndent, sTsNow, sInterval, cMonthsMourningPeriod,831 sExtraIndent, sTsNow, sInterval );832 return sRet833 834 def fetchResultsForListing(self, iStart, cMaxRows, tsNow, sInterval, enmResultSortBy, # pylint: disable=R0913835 enmResultsGroupingType, iResultsGroupingValue, fOnlyFailures, fOnlyNeedingReason):836 """837 Fetches TestResults table content.838 839 If @param enmResultsGroupingType and @param iResultsGroupingValue840 are not None, then resulting (returned) list contains only records841 that match specified @param enmResultsGroupingType.842 843 If @param enmResultsGroupingType is None, then844 @param iResultsGroupingValue is ignored.845 846 Returns an array (list) of TestResultData items, empty list if none.847 Raises exception on error.848 """849 850 #851 # Get SQL query parameters852 #853 if enmResultsGroupingType is None or enmResultsGroupingType not in self.kdResultGroupingMap:854 raise TMExceptionBase('Unknown grouping type');855 if enmResultSortBy is None or enmResultSortBy not in self.kasResultsSortBy:856 raise TMExceptionBase('Unknown sorting');857 sGroupingTables, sGroupingField, sGroupingCondition, dSortingOverrides = self.kdResultGroupingMap[enmResultsGroupingType];858 if enmResultSortBy in dSortingOverrides:859 sSortingTables, sSortingWhere, sSortingOrderBy, sSortingColumns = dSortingOverrides[enmResultSortBy];860 else:861 sSortingTables, sSortingWhere, sSortingOrderBy, sSortingColumns = self.kdResultSortByMap[enmResultSortBy];862 863 #864 # Construct the query.865 #866 sQuery = 'SELECT DISTINCT TestSets.idTestSet,\n' \867 ' BuildCategories.idBuildCategory,\n' \868 ' BuildCategories.sProduct,\n' \869 ' BuildCategories.sRepository,\n' \870 ' BuildCategories.sBranch,\n' \871 ' BuildCategories.sType,\n' \872 ' Builds.idBuild,\n' \873 ' Builds.sVersion,\n' \874 ' Builds.iRevision,\n' \875 ' TestBoxes.sOs,\n' \876 ' TestBoxes.sOsVersion,\n' \877 ' TestBoxes.sCpuArch,\n' \878 ' TestBoxes.sCpuVendor,\n' \879 ' TestBoxes.sCpuName,\n' \880 ' TestBoxes.cCpus,\n' \881 ' TestBoxes.fCpuHwVirt,\n' \882 ' TestBoxes.fCpuNestedPaging,\n' \883 ' TestBoxes.fCpu64BitGuest,\n' \884 ' TestBoxes.idTestBox,\n' \885 ' TestBoxes.sName,\n' \886 ' TestResults.tsCreated,\n' \887 ' COALESCE(TestResults.tsElapsed, CURRENT_TIMESTAMP - TestResults.tsCreated),\n' \888 ' TestSets.enmStatus,\n' \889 ' TestResults.cErrors,\n' \890 ' TestCases.idTestCase,\n' \891 ' TestCases.sName,\n' \892 ' TestCases.sBaseCmd,\n' \893 ' TestCaseArgs.sArgs,\n' \894 ' TestCaseArgs.sSubName,\n' \895 ' TestSuiteBits.idBuild AS idBuildTestSuite,\n' \896 ' TestSuiteBits.iRevision AS iRevisionTestSuite,\n' \897 ' TestResultFailures.idFailureReason as idFailureReason,\n' \898 ' TestResultFailures.uidAuthor as uidFailureReasonAssigner,\n' \899 ' TestResultFailures.tsEffective as tsFailureReasonAssigned,\n' \900 ' TestResultFailures.sComment as sFailureReasonComment,\n' \901 ' (TestSets.tsDone IS NULL) SortRunningFirst' + sSortingColumns + '\n' \902 'FROM BuildCategories,\n' \903 ' Builds,\n' \904 ' TestBoxes,\n' \905 ' TestResults\n' \906 ' LEFT OUTER JOIN TestResultFailures\n' \907 ' ON TestResults.idTestResult = TestResultFailures.idTestResult\n' \908 ' AND TestResultFailures.tsExpire = \'infinity\'::TIMESTAMP';909 if sSortingOrderBy is not None and sSortingOrderBy.find('FailureReason') >= 0:910 sQuery += '\n' \911 ' LEFT OUTER JOIN FailureReasons\n' \912 ' ON TestResultFailures.idFailureReason = FailureReasons.idFailureReason\n' \913 ' AND FailureReasons.tsExpire = \'infinity\'::TIMESTAMP';914 sQuery += ',\n'\915 ' TestCases,\n' \916 ' TestCaseArgs,\n' \917 ' ( SELECT TestSets.idTestSet AS idTestSet,\n' \918 ' TestSets.tsDone AS tsDone,\n' \919 ' TestSets.tsCreated AS tsCreated,\n' \920 ' TestSets.enmStatus AS enmStatus,\n' \921 ' TestSets.idBuild AS idBuild,\n' \922 ' TestSets.idBuildTestSuite AS idBuildTestSuite,\n' \923 ' TestSets.idGenTestBox AS idGenTestBox,\n' \924 ' TestSets.idGenTestCase AS idGenTestCase,\n' \925 ' TestSets.idGenTestCaseArgs AS idGenTestCaseArgs\n' \926 ' FROM TestSets';927 if fOnlyNeedingReason:928 sQuery += '\n' \929 ' LEFT OUTER JOIN TestResultFailures\n' \930 ' ON TestSets.idTestSet = TestResultFailures.idTestSet\n' \931 ' AND TestResultFailures.tsExpire = \'infinity\'::TIMESTAMP';932 sQuery += sGroupingTables.replace(',', ',\n ');933 sQuery += sSortingTables.replace( ',', ',\n ');934 sQuery += '\n' \935 ' WHERE ' + self._getTimePeriodQueryPart(tsNow, sInterval, ' ');936 if fOnlyFailures or fOnlyNeedingReason:937 sQuery += ' AND TestSets.enmStatus != \'success\'::TestStatus_T\n' \938 ' AND TestSets.enmStatus != \'running\'::TestStatus_T\n';939 if fOnlyNeedingReason:940 sQuery += ' AND TestResultFailures.idTestSet IS NULL\n';941 if sGroupingField is not None:942 sQuery += ' AND %s = %d\n' % (sGroupingField, iResultsGroupingValue,);943 if sGroupingCondition is not None:944 sQuery += sGroupingCondition.replace(' AND ', ' AND ');945 if sSortingWhere is not None:946 sQuery += sSortingWhere.replace(' AND ', ' AND ');947 sQuery += ' ORDER BY ';948 if sSortingOrderBy is not None and sSortingOrderBy.find('FailureReason') < 0:949 sQuery += sSortingOrderBy + ',\n ';950 sQuery += '(TestSets.tsDone IS NULL) DESC, TestSets.idTestSet DESC\n' \951 ' LIMIT %s OFFSET %s\n' % (cMaxRows, iStart,);952 953 sQuery += ' ) AS TestSets\n' \954 ' LEFT OUTER JOIN Builds AS TestSuiteBits\n' \955 ' ON TestSets.idBuildTestSuite = TestSuiteBits.idBuild\n' \956 'WHERE TestSets.idTestSet = TestResults.idTestSet\n' \957 ' AND TestResults.idTestResultParent is NULL\n' \958 ' AND TestSets.idBuild = Builds.idBuild\n' \959 ' AND Builds.tsExpire > TestSets.tsCreated\n' \960 ' AND Builds.tsEffective <= TestSets.tsCreated\n' \961 ' AND Builds.idBuildCategory = BuildCategories.idBuildCategory\n' \962 ' AND TestSets.idGenTestBox = TestBoxes.idGenTestBox\n' \963 ' AND TestSets.idGenTestCase = TestCases.idGenTestCase\n' \964 ' AND TestSets.idGenTestCaseArgs = TestCaseArgs.idGenTestCaseArgs\n' \965 'ORDER BY ';966 if sSortingOrderBy is not None:967 sQuery += sSortingOrderBy + ',\n ';968 sQuery += '(TestSets.tsDone IS NULL) DESC, TestSets.idTestSet DESC\n';969 970 #971 # Execute the query and return the wrapped results.972 #973 self._oDb.execute(sQuery);974 975 if self.oFailureReasonLogic is None:976 self.oFailureReasonLogic = FailureReasonLogic(self._oDb);977 if self.oUserAccountLogic is None:978 self.oUserAccountLogic = UserAccountLogic(self._oDb);979 980 aoRows = [];981 for aoRow in self._oDb.fetchAll():982 aoRows.append(TestResultListingData().initFromDbRowEx(aoRow, self.oFailureReasonLogic, self.oUserAccountLogic));983 984 return aoRows985 986 def getEntriesCount(self, tsNow, sInterval, enmResultsGroupingType, iResultsGroupingValue, fOnlyFailures, fOnlyNeedingReason):987 """988 Get number of table records.989 990 If @param enmResultsGroupingType and @param iResultsGroupingValue991 are not None, then we count only only those records992 that match specified @param enmResultsGroupingType.993 994 If @param enmResultsGroupingType is None, then995 @param iResultsGroupingValue is ignored.996 """997 998 #999 # Get SQL query parameters1000 #1001 if enmResultsGroupingType is None:1002 raise TMExceptionBase('Unknown grouping type')1003 1004 if enmResultsGroupingType not in self.kdResultGroupingMap:1005 raise TMExceptionBase('Unknown grouping type')1006 sGroupingTables, sGroupingField, sGroupingCondition, _ = self.kdResultGroupingMap[enmResultsGroupingType];1007 1008 #1009 # Construct the query.1010 #1011 sQuery = 'SELECT COUNT(TestSets.idTestSet)\n' \1012 'FROM TestSets';1013 if fOnlyNeedingReason:1014 sQuery += '\n' \1015 ' LEFT OUTER JOIN TestResultFailures\n' \1016 ' ON TestSets.idTestSet = TestResultFailures.idTestSet\n' \1017 ' AND TestResultFailures.tsExpire = \'infinity\'::TIMESTAMP';1018 sQuery += sGroupingTables.replace(',', ',\n ');1019 sQuery += '\n' \1020 'WHERE ' + self._getTimePeriodQueryPart(tsNow, sInterval);1021 if fOnlyFailures or fOnlyNeedingReason:1022 sQuery += ' AND TestSets.enmStatus != \'success\'::TestStatus_T\n' \1023 ' AND TestSets.enmStatus != \'running\'::TestStatus_T\n';1024 if fOnlyNeedingReason:1025 sQuery += ' AND TestResultFailures.idTestSet IS NULL\n';1026 if sGroupingField is not None:1027 sQuery += ' AND %s = %d\n' % (sGroupingField, iResultsGroupingValue,);1028 if sGroupingCondition is not None:1029 sQuery += sGroupingCondition.replace(' AND ', ' AND ');1030 1031 #1032 # Execute the query and return the result.1033 #1034 self._oDb.execute(sQuery)1035 return self._oDb.fetchOne()[0]1036 1037 def getTestGroups(self, tsNow, sPeriod):1038 """1039 Get list of uniq TestGroupData objects which1040 found in all test results.1041 """1042 1043 self._oDb.execute('SELECT DISTINCT TestGroups.*\n'1044 'FROM TestGroups, TestSets\n'1045 'WHERE TestSets.idTestGroup = TestGroups.idTestGroup\n'1046 ' AND TestGroups.tsExpire > TestSets.tsCreated\n'1047 ' AND TestGroups.tsEffective <= TestSets.tsCreated'1048 ' AND ' + self._getTimePeriodQueryPart(tsNow, sPeriod))1049 aaoRows = self._oDb.fetchAll()1050 aoRet = []1051 for aoRow in aaoRows:1052 aoRet.append(TestGroupData().initFromDbRow(aoRow))1053 return aoRet1054 1055 def getBuilds(self, tsNow, sPeriod):1056 """1057 Get list of uniq BuildDataEx objects which1058 found in all test results.1059 """1060 1061 self._oDb.execute('SELECT DISTINCT Builds.*, BuildCategories.*\n'1062 'FROM Builds, BuildCategories, TestSets\n'1063 'WHERE TestSets.idBuild = Builds.idBuild\n'1064 ' AND Builds.idBuildCategory = BuildCategories.idBuildCategory\n'1065 ' AND Builds.tsExpire > TestSets.tsCreated\n'1066 ' AND Builds.tsEffective <= TestSets.tsCreated'1067 ' AND ' + self._getTimePeriodQueryPart(tsNow, sPeriod))1068 aaoRows = self._oDb.fetchAll()1069 aoRet = []1070 for aoRow in aaoRows:1071 aoRet.append(BuildDataEx().initFromDbRow(aoRow))1072 return aoRet1073 1074 def getTestBoxes(self, tsNow, sPeriod):1075 """1076 Get list of uniq TestBoxData objects which1077 found in all test results.1078 """1079 1080 self._oDb.execute('SELECT TestBoxes.*\n'1081 'FROM TestBoxes,\n'1082 ' ( SELECT idTestBox AS idTestBox,\n'1083 ' MAX(idGenTestBox) AS idGenTestBox\n'1084 ' FROM TestSets\n'1085 ' WHERE ' + self._getTimePeriodQueryPart(tsNow, sPeriod, ' ') +1086 ' GROUP BY idTestBox\n'1087 ' ) AS TestBoxIDs\n'1088 'WHERE TestBoxes.idGenTestBox = TestBoxIDs.idGenTestBox\n'1089 'ORDER BY TestBoxes.sName\n' );1090 aoRet = []1091 for aoRow in self._oDb.fetchAll():1092 aoRet.append(TestBoxData().initFromDbRow(aoRow));1093 return aoRet1094 1095 def getTestCases(self, tsNow, sPeriod):1096 """1097 Get a list of unique TestCaseData objects which is appears in the test1098 specified result period.1099 """1100 1101 self._oDb.execute('SELECT TestCases.*\n'1102 'FROM TestCases,\n'1103 ' ( SELECT idTestCase AS idTestCase,\n'1104 ' MAX(idGenTestCase) AS idGenTestCase\n'1105 ' FROM TestSets\n'1106 ' WHERE ' + self._getTimePeriodQueryPart(tsNow, sPeriod, ' ') +1107 ' GROUP BY idTestCase\n'1108 ' ) AS TestCasesIDs\n'1109 'WHERE TestCases.idGenTestCase = TestCasesIDs.idGenTestCase\n'1110 'ORDER BY TestCases.sName\n' );1111 aoRet = [];1112 for aoRow in self._oDb.fetchAll():1113 aoRet.append(TestCaseData().initFromDbRow(aoRow));1114 return aoRet1115 1116 def getSchedGroups(self, tsNow, sPeriod):1117 """1118 Get list of uniq SchedGroupData objects which1119 found in all test results.1120 """1121 1122 self._oDb.execute('SELECT SchedGroups.*\n'1123 'FROM SchedGroups,\n'1124 ' ( SELECT TestBoxes.idSchedGroup AS idSchedGroup,\n'1125 ' MAX(TestSets.tsCreated) AS tsNow\n'1126 ' FROM TestSets,\n'1127 ' TestBoxes\n'1128 ' WHERE TestSets.idGenTestBox = TestBoxes.idGenTestBox\n'1129 ' AND ' + self._getTimePeriodQueryPart(tsNow, sPeriod, ' ') +1130 ' GROUP BY idSchedGroup\n'1131 ' ) AS SchedGroupIDs\n'1132 'WHERE SchedGroups.idSchedGroup = SchedGroupIDs.idSchedGroup\n'1133 ' AND SchedGroups.tsExpire > SchedGroupIDs.tsNow\n'1134 ' AND SchedGroups.tsEffective <= SchedGroupIDs.tsNow\n'1135 'ORDER BY SchedGroups.sName\n' );1136 aoRet = []1137 for aoRow in self._oDb.fetchAll():1138 aoRet.append(SchedGroupData().initFromDbRow(aoRow));1139 return aoRet1140 1141 def getById(self, idTestResult):1142 """1143 Get build record by its id1144 """1145 self._oDb.execute('SELECT *\n'1146 'FROM TestResults\n'1147 'WHERE idTestResult = %s\n',1148 (idTestResult,))1149 1150 aRows = self._oDb.fetchAll()1151 if len(aRows) not in (0, 1):1152 raise TMTooManyRows('Found more than one test result with the same credentials. Database structure is corrupted.')1153 try:1154 return TestResultData().initFromDbRow(aRows[0])1155 except IndexError:1156 return None1157 1158 1159 #1160 # Details view and interface.1161 #1162 1163 def fetchResultTree(self, idTestSet, cMaxDepth = None):1164 """1165 Fetches the result tree for the given test set.1166 1167 Returns a tree of TestResultDataEx nodes.1168 Raises exception on invalid input and database issues.1169 """1170 # Depth first, i.e. just like the XML added them.1171 ## @todo this still isn't performing extremely well, consider optimizations.1172 sQuery = self._oDb.formatBindArgs(1173 'SELECT TestResults.*,\n'1174 ' TestResultStrTab.sValue,\n'1175 ' EXISTS ( SELECT idTestResultValue\n'1176 ' FROM TestResultValues\n'1177 ' WHERE TestResultValues.idTestResult = TestResults.idTestResult ) AS fHasValues,\n'1178 ' EXISTS ( SELECT idTestResultMsg\n'1179 ' FROM TestResultMsgs\n'1180 ' WHERE TestResultMsgs.idTestResult = TestResults.idTestResult ) AS fHasMsgs,\n'1181 ' EXISTS ( SELECT idTestResultFile\n'1182 ' FROM TestResultFiles\n'1183 ' WHERE TestResultFiles.idTestResult = TestResults.idTestResult ) AS fHasFiles,\n'1184 ' EXISTS ( SELECT idTestResult\n'1185 ' FROM TestResultFailures\n'1186 ' WHERE TestResultFailures.idTestResult = TestResults.idTestResult ) AS fHasReasons\n'1187 'FROM TestResults, TestResultStrTab\n'1188 'WHERE TestResults.idTestSet = %s\n'1189 ' AND TestResults.idStrName = TestResultStrTab.idStr\n'1190 , ( idTestSet, ));1191 if cMaxDepth is not None:1192 sQuery += self._oDb.formatBindArgs(' AND TestResults.iNestingDepth <= %s\n', (cMaxDepth,));1193 sQuery += 'ORDER BY idTestResult ASC\n'1194 1195 self._oDb.execute(sQuery);1196 cRows = self._oDb.getRowCount();1197 if cRows > 65536:1198 raise TMTooManyRows('Too many rows returned for idTestSet=%d: %d' % (idTestSet, cRows,));1199 1200 aaoRows = self._oDb.fetchAll();1201 if len(aaoRows) == 0:1202 raise TMRowNotFound('No test results for idTestSet=%d.' % (idTestSet,));1203 1204 # Set up the root node first.1205 aoRow = aaoRows[0];1206 oRoot = TestResultDataEx().initFromDbRow(aoRow);1207 if oRoot.idTestResultParent is not None:1208 raise self._oDb.integrityException('The root TestResult (#%s) has a parent (#%s)!'1209 % (oRoot.idTestResult, oRoot.idTestResultParent));1210 self._fetchResultTreeNodeExtras(oRoot, aoRow[-4], aoRow[-3], aoRow[-2], aoRow[-1]);1211 1212 # The chilren (if any).1213 dLookup = { oRoot.idTestResult: oRoot };1214 oParent = oRoot;1215 for iRow in range(1, len(aaoRows)):1216 aoRow = aaoRows[iRow];1217 oCur = TestResultDataEx().initFromDbRow(aoRow);1218 self._fetchResultTreeNodeExtras(oCur, aoRow[-4], aoRow[-3], aoRow[-2], aoRow[-1]);1219 1220 # Figure out and vet the parent.1221 if oParent.idTestResult != oCur.idTestResultParent:1222 oParent = dLookup.get(oCur.idTestResultParent, None);1223 if oParent is None:1224 raise self._oDb.integrityException('TestResult #%d is orphaned from its parent #%s.'1225 % (oCur.idTestResult, oCur.idTestResultParent,));1226 if oParent.iNestingDepth + 1 != oCur.iNestingDepth:1227 raise self._oDb.integrityException('TestResult #%d has incorrect nesting depth (%d instead of %d)'1228 % (oCur.idTestResult, oCur.iNestingDepth, oParent.iNestingDepth + 1,));1229 1230 # Link it up.1231 oCur.oParent = oParent;1232 oParent.aoChildren.append(oCur);1233 dLookup[oCur.idTestResult] = oCur;1234 1235 return (oRoot, dLookup);1236 1237 def _fetchResultTreeNodeExtras(self, oCurNode, fHasValues, fHasMsgs, fHasFiles, fHasReasons):1238 """1239 fetchResultTree worker that fetches values, message and files for the1240 specified node.1241 """1242 assert(oCurNode.aoValues == []);1243 assert(oCurNode.aoMsgs == []);1244 assert(oCurNode.aoFiles == []);1245 assert(oCurNode.oReason is None);1246 1247 if fHasValues:1248 self._oDb.execute('SELECT TestResultValues.*,\n'1249 ' TestResultStrTab.sValue\n'1250 'FROM TestResultValues, TestResultStrTab\n'1251 'WHERE TestResultValues.idTestResult = %s\n'1252 ' AND TestResultValues.idStrName = TestResultStrTab.idStr\n'1253 'ORDER BY idTestResultValue ASC\n'1254 , ( oCurNode.idTestResult, ));1255 for aoRow in self._oDb.fetchAll():1256 oCurNode.aoValues.append(TestResultValueDataEx().initFromDbRow(aoRow));1257 1258 if fHasMsgs:1259 self._oDb.execute('SELECT TestResultMsgs.*,\n'1260 ' TestResultStrTab.sValue\n'1261 'FROM TestResultMsgs, TestResultStrTab\n'1262 'WHERE TestResultMsgs.idTestResult = %s\n'1263 ' AND TestResultMsgs.idStrMsg = TestResultStrTab.idStr\n'1264 'ORDER BY idTestResultMsg ASC\n'1265 , ( oCurNode.idTestResult, ));1266 for aoRow in self._oDb.fetchAll():1267 oCurNode.aoMsgs.append(TestResultMsgDataEx().initFromDbRow(aoRow));1268 1269 if fHasFiles:1270 self._oDb.execute('SELECT TestResultFiles.*,\n'1271 ' StrTabFile.sValue AS sFile,\n'1272 ' StrTabDesc.sValue AS sDescription,\n'1273 ' StrTabKind.sValue AS sKind,\n'1274 ' StrTabMime.sValue AS sMime\n'1275 'FROM TestResultFiles,\n'1276 ' TestResultStrTab AS StrTabFile,\n'1277 ' TestResultStrTab AS StrTabDesc,\n'1278 ' TestResultStrTab AS StrTabKind,\n'1279 ' TestResultStrTab AS StrTabMime\n'1280 'WHERE TestResultFiles.idTestResult = %s\n'1281 ' AND TestResultFiles.idStrFile = StrTabFile.idStr\n'1282 ' AND TestResultFiles.idStrDescription = StrTabDesc.idStr\n'1283 ' AND TestResultFiles.idStrKind = StrTabKind.idStr\n'1284 ' AND TestResultFiles.idStrMime = StrTabMime.idStr\n'1285 'ORDER BY idTestResultFile ASC\n'1286 , ( oCurNode.idTestResult, ));1287 for aoRow in self._oDb.fetchAll():1288 oCurNode.aoFiles.append(TestResultFileDataEx().initFromDbRow(aoRow));1289 1290 if fHasReasons or True:1291 if self.oFailureReasonLogic is None:1292 self.oFailureReasonLogic = FailureReasonLogic(self._oDb);1293 if self.oUserAccountLogic is None:1294 self.oUserAccountLogic = UserAccountLogic(self._oDb);1295 self._oDb.execute('SELECT *\n'1296 'FROM TestResultFailures\n'1297 'WHERE idTestResult = %s\n'1298 ' AND tsExpire = \'infinity\'::TIMESTAMP\n'1299 , ( oCurNode.idTestResult, ));1300 if self._oDb.getRowCount() > 0:1301 oCurNode.oReason = TestResultFailureDataEx().initFromDbRowEx(self._oDb.fetchOne(), self.oFailureReasonLogic,1302 self.oUserAccountLogic);1303 1304 return True;1305 1306 1307 1308 #1309 # TestBoxController interface(s).1310 #1311 1312 def _inhumeTestResults(self, aoStack, idTestSet, sError):1313 """1314 The test produces too much output, kill and bury it.1315 1316 Note! We leave the test set open, only the test result records are1317 completed. Thus, _getResultStack will return an empty stack and1318 cause XML processing to fail immediately, while we can still1319 record when it actually completed in the test set the normal way.1320 """1321 self._oDb.dprint('** _inhumeTestResults: idTestSet=%d\n%s' % (idTestSet, self._stringifyStack(aoStack),));1322 1323 #1324 # First add a message.1325 #1326 self._newFailureDetails(aoStack[0].idTestResult, sError, None);1327 1328 #1329 # The complete all open test results.1330 #1331 for oTestResult in aoStack:1332 oTestResult.cErrors += 1;1333 self._completeTestResults(oTestResult, None, TestResultData.ksTestStatus_Failure, oTestResult.cErrors);1334 1335 # A bit of paranoia.1336 self._oDb.execute('UPDATE TestResults\n'1337 'SET cErrors = cErrors + 1,\n'1338 ' enmStatus = \'failure\'::TestStatus_T,\n'1339 ' tsElapsed = CURRENT_TIMESTAMP - tsCreated\n'1340 'WHERE idTestSet = %s\n'1341 ' AND enmStatus = \'running\'::TestStatus_T\n'1342 , ( idTestSet, ));1343 self._oDb.commit();1344 1345 return None;1346 1347 def strTabString(self, sString, fCommit = False):1348 """1349 Gets the string table id for the given string, adding it if new.1350 1351 Note! A copy of this code is also in TestSetLogic.1352 """1353 ## @todo move this and make a stored procedure for it.1354 self._oDb.execute('SELECT idStr\n'1355 'FROM TestResultStrTab\n'1356 'WHERE sValue = %s'1357 , (sString,));1358 if self._oDb.getRowCount() == 0:1359 self._oDb.execute('INSERT INTO TestResultStrTab (sValue)\n'1360 'VALUES (%s)\n'1361 'RETURNING idStr\n'1362 , (sString,));1363 if fCommit:1364 self._oDb.commit();1365 return self._oDb.fetchOne()[0];1366 1367 @staticmethod1368 def _stringifyStack(aoStack):1369 """Returns a string rep of the stack."""1370 sRet = '';1371 for i, _ in enumerate(aoStack):1372 sRet += 'aoStack[%d]=%s\n' % (i, aoStack[i]);1373 return sRet;1374 1375 def _getResultStack(self, idTestSet):1376 """1377 Gets the current stack of result sets.1378 """1379 self._oDb.execute('SELECT *\n'1380 'FROM TestResults\n'1381 'WHERE idTestSet = %s\n'1382 ' AND enmStatus = \'running\'::TestStatus_T\n'1383 'ORDER BY idTestResult DESC'1384 , ( idTestSet, ));1385 aoStack = [];1386 for aoRow in self._oDb.fetchAll():1387 aoStack.append(TestResultData().initFromDbRow(aoRow));1388 1389 for i, _ in enumerate(aoStack):1390 assert aoStack[i].iNestingDepth == len(aoStack) - i - 1, self._stringifyStack(aoStack);1391 1392 return aoStack;1393 1394 def _newTestResult(self, idTestResultParent, idTestSet, iNestingDepth, tsCreated, sName, dCounts, fCommit = False):1395 """1396 Creates a new test result.1397 Returns the TestResultData object for the new record.1398 May raise exception on database error.1399 """1400 assert idTestResultParent is not None;1401 assert idTestResultParent > 1;1402 1403 #1404 # This isn't necessarily very efficient, but it's necessary to prevent1405 # a wild test or testbox from filling up the database.1406 #1407 sCountName = 'cTestResults';1408 if sCountName not in dCounts:1409 self._oDb.execute('SELECT COUNT(idTestResult)\n'1410 'FROM TestResults\n'1411 'WHERE idTestSet = %s\n'1412 , ( idTestSet,));1413 dCounts[sCountName] = self._oDb.fetchOne()[0];1414 dCounts[sCountName] += 1;1415 if dCounts[sCountName] > config.g_kcMaxTestResultsPerTS:1416 raise TestResultHangingOffence('Too many sub-tests in total!');1417 1418 sCountName = 'cTestResultsIn%d' % (idTestResultParent,);1419 if sCountName not in dCounts:1420 self._oDb.execute('SELECT COUNT(idTestResult)\n'1421 'FROM TestResults\n'1422 'WHERE idTestResultParent = %s\n'1423 , ( idTestResultParent,));1424 dCounts[sCountName] = self._oDb.fetchOne()[0];1425 dCounts[sCountName] += 1;1426 if dCounts[sCountName] > config.g_kcMaxTestResultsPerTR:1427 raise TestResultHangingOffence('Too many immediate sub-tests!');1428 1429 # This is also a hanging offence.1430 if iNestingDepth > config.g_kcMaxTestResultDepth:1431 raise TestResultHangingOffence('To deep sub-test nesting!');1432 1433 # Ditto.1434 if len(sName) > config.g_kcchMaxTestResultName:1435 raise TestResultHangingOffence('Test name is too long: %d chars - "%s"' % (len(sName), sName));1436 1437 #1438 # Within bounds, do the job.1439 #1440 idStrName = self.strTabString(sName, fCommit);1441 self._oDb.execute('INSERT INTO TestResults (\n'1442 ' idTestResultParent,\n'1443 ' idTestSet,\n'1444 ' tsCreated,\n'1445 ' idStrName,\n'1446 ' iNestingDepth )\n'1447 'VALUES (%s, %s, TIMESTAMP WITH TIME ZONE %s, %s, %s)\n'1448 'RETURNING *\n'1449 , ( idTestResultParent, idTestSet, tsCreated, idStrName, iNestingDepth) )1450 oData = TestResultData().initFromDbRow(self._oDb.fetchOne());1451 1452 self._oDb.maybeCommit(fCommit);1453 return oData;1454 1455 def _newTestValue(self, idTestResult, idTestSet, sName, lValue, sUnit, dCounts, tsCreated = None, fCommit = False):1456 """1457 Creates a test value.1458 May raise exception on database error.1459 """1460 1461 #1462 # Bounds checking.1463 #1464 sCountName = 'cTestValues';1465 if sCountName not in dCounts:1466 self._oDb.execute('SELECT COUNT(idTestResultValue)\n'1467 'FROM TestResultValues, TestResults\n'1468 'WHERE TestResultValues.idTestResult = TestResults.idTestResult\n'1469 ' AND TestResults.idTestSet = %s\n'1470 , ( idTestSet,));1471 dCounts[sCountName] = self._oDb.fetchOne()[0];1472 dCounts[sCountName] += 1;1473 if dCounts[sCountName] > config.g_kcMaxTestValuesPerTS:1474 raise TestResultHangingOffence('Too many values in total!');1475 1476 sCountName = 'cTestValuesIn%d' % (idTestResult,);1477 if sCountName not in dCounts:1478 self._oDb.execute('SELECT COUNT(idTestResultValue)\n'1479 'FROM TestResultValues\n'1480 'WHERE idTestResult = %s\n'1481 , ( idTestResult,));1482 dCounts[sCountName] = self._oDb.fetchOne()[0];1483 dCounts[sCountName] += 1;1484 if dCounts[sCountName] > config.g_kcMaxTestValuesPerTR:1485 raise TestResultHangingOffence('Too many immediate values for one test result!');1486 1487 if len(sName) > config.g_kcchMaxTestValueName:1488 raise TestResultHangingOffence('Value name is too long: %d chars - "%s"' % (len(sName), sName));1489 1490 #1491 # Do the job.1492 #1493 iUnit = constants.valueunit.g_kdNameToConst.get(sUnit, constants.valueunit.NONE);1494 1495 idStrName = self.strTabString(sName, fCommit);1496 if tsCreated is None:1497 self._oDb.execute('INSERT INTO TestResultValues (\n'1498 ' idTestResult,\n'1499 ' idTestSet,\n'1500 ' idStrName,\n'1501 ' lValue,\n'1502 ' iUnit)\n'1503 'VALUES ( %s, %s, %s, %s, %s )\n'1504 , ( idTestResult, idTestSet, idStrName, lValue, iUnit,) );1505 else:1506 self._oDb.execute('INSERT INTO TestResultValues (\n'1507 ' idTestResult,\n'1508 ' idTestSet,\n'1509 ' tsCreated,\n'1510 ' idStrName,\n'1511 ' lValue,\n'1512 ' iUnit)\n'1513 'VALUES ( %s, %s, TIMESTAMP WITH TIME ZONE %s, %s, %s, %s )\n'1514 , ( idTestResult, idTestSet, tsCreated, idStrName, lValue, iUnit,) );1515 self._oDb.maybeCommit(fCommit);1516 return True;1517 1518 def _newFailureDetails(self, idTestResult, sText, dCounts, tsCreated = None, fCommit = False):1519 """1520 Creates a record detailing cause of failure.1521 May raise exception on database error.1522 """1523 1524 #1525 # Overflow protection.1526 #1527 if dCounts is not None:1528 sCountName = 'cTestMsgsIn%d' % (idTestResult,);1529 if sCountName not in dCounts:1530 self._oDb.execute('SELECT COUNT(idTestResultMsg)\n'1531 'FROM TestResultMsgs\n'1532 'WHERE idTestResult = %s\n'1533 , ( idTestResult,));1534 dCounts[sCountName] = self._oDb.fetchOne()[0];1535 dCounts[sCountName] += 1;1536 if dCounts[sCountName] > config.g_kcMaxTestMsgsPerTR:1537 raise TestResultHangingOffence('Too many messages under for one test result!');1538 1539 if len(sText) > config.g_kcchMaxTestMsg:1540 raise TestResultHangingOffence('Failure details message is too long: %d chars - "%s"' % (len(sText), sText));1541 1542 #1543 # Do the job.1544 #1545 idStrMsg = self.strTabString(sText, fCommit);1546 if tsCreated is None:1547 self._oDb.execute('INSERT INTO TestResultMsgs (\n'1548 ' idTestResult,\n'1549 ' idStrMsg,\n'1550 ' enmLevel)\n'1551 'VALUES ( %s, %s, %s)\n'1552 , ( idTestResult, idStrMsg, 'failure',) );1553 else:1554 self._oDb.execute('INSERT INTO TestResultMsgs (\n'1555 ' idTestResult,\n'1556 ' tsCreated,\n'1557 ' idStrMsg,\n'1558 ' enmLevel)\n'1559 'VALUES ( %s, TIMESTAMP WITH TIME ZONE %s, %s, %s)\n'1560 , ( idTestResult, tsCreated, idStrMsg, 'failure',) );1561 1562 self._oDb.maybeCommit(fCommit);1563 return True;1564 1565 1566 def _completeTestResults(self, oTestResult, tsDone, enmStatus, cErrors = 0, fCommit = False):1567 """1568 Completes a test result. Updates the oTestResult object.1569 May raise exception on database error.1570 """1571 self._oDb.dprint('** _completeTestResults: cErrors=%s tsDone=%s enmStatus=%s oTestResults=\n%s'1572 % (cErrors, tsDone, enmStatus, oTestResult,));1573 1574 #1575 # Sanity check: No open sub tests (aoStack should make sure about this!).1576 #1577 self._oDb.execute('SELECT COUNT(idTestResult)\n'1578 'FROM TestResults\n'1579 'WHERE idTestResultParent = %s\n'1580 ' AND enmStatus = %s\n'1581 , ( oTestResult.idTestResult, TestResultData.ksTestStatus_Running,));1582 cOpenSubTest = self._oDb.fetchOne()[0];1583 assert cOpenSubTest == 0, 'cOpenSubTest=%d - %s' % (cOpenSubTest, oTestResult,);1584 assert oTestResult.enmStatus == TestResultData.ksTestStatus_Running;1585 1586 #1587 # Make sure the reporter isn't lying about successes or error counts.1588 #1589 self._oDb.execute('SELECT COALESCE(SUM(cErrors), 0)\n'1590 'FROM TestResults\n'1591 'WHERE idTestResultParent = %s\n'1592 , ( oTestResult.idTestResult, ));1593 cMinErrors = self._oDb.fetchOne()[0] + oTestResult.cErrors;1594 if cErrors < cMinErrors:1595 cErrors = cMinErrors;1596 if cErrors > 0 and enmStatus == TestResultData.ksTestStatus_Success:1597 enmStatus = TestResultData.ksTestStatus_Failure1598 1599 #1600 # Do the update.1601 #1602 if tsDone is None:1603 self._oDb.execute('UPDATE TestResults\n'1604 'SET cErrors = %s,\n'1605 ' enmStatus = %s,\n'1606 ' tsElapsed = CURRENT_TIMESTAMP - tsCreated\n'1607 'WHERE idTestResult = %s\n'1608 'RETURNING tsElapsed'1609 , ( cErrors, enmStatus, oTestResult.idTestResult,) );1610 else:1611 self._oDb.execute('UPDATE TestResults\n'1612 'SET cErrors = %s,\n'1613 ' enmStatus = %s,\n'1614 ' tsElapsed = TIMESTAMP WITH TIME ZONE %s - tsCreated\n'1615 'WHERE idTestResult = %s\n'1616 'RETURNING tsElapsed'1617 , ( cErrors, enmStatus, tsDone, oTestResult.idTestResult,) );1618 1619 oTestResult.tsElapsed = self._oDb.fetchOne()[0];1620 oTestResult.enmStatus = enmStatus;1621 oTestResult.cErrors = cErrors;1622 1623 self._oDb.maybeCommit(fCommit);1624 return None;1625 1626 def _doPopHint(self, aoStack, cStackEntries, dCounts):1627 """ Executes a PopHint. """1628 assert cStackEntries >= 0;1629 while len(aoStack) > cStackEntries:1630 if aoStack[0].enmStatus == TestResultData.ksTestStatus_Running:1631 self._newFailureDetails(aoStack[0].idTestResult, 'XML error: Missing </Test>', dCounts);1632 self._completeTestResults(aoStack[0], tsDone = None, cErrors = 1,1633 enmStatus = TestResultData.ksTestStatus_Failure, fCommit = True);1634 aoStack.pop(0);1635 return True;1636 1637 1638 @staticmethod1639 def _validateElement(sName, dAttribs, fClosed):1640 """1641 Validates an element and its attributes.1642 """1643 1644 #1645 # Validate attributes by name.1646 #1647 1648 # Validate integer attributes.1649 for sAttr in [ 'errors', 'testdepth' ]:1650 if sAttr in dAttribs:1651 try:1652 _ = int(dAttribs[sAttr]);1653 except:1654 return 'Element %s has an invalid %s attribute value: %s.' % (sName, sAttr, dAttribs[sAttr],);1655 1656 # Validate long attributes.1657 for sAttr in [ 'value', ]:1658 if sAttr in dAttribs:1659 try:1660 _ = long(dAttribs[sAttr]); # pylint: disable=R02041661 except:1662 return 'Element %s has an invalid %s attribute value: %s.' % (sName, sAttr, dAttribs[sAttr],);1663 1664 # Validate string attributes.1665 for sAttr in [ 'name', 'text' ]: # 'unit' can be zero length.1666 if sAttr in dAttribs and len(dAttribs[sAttr]) == 0:1667 return 'Element %s has an empty %s attribute value.' % (sName, sAttr,);1668 1669 # Validate the timestamp attribute.1670 if 'timestamp' in dAttribs:1671 (dAttribs['timestamp'], sError) = ModelDataBase.validateTs(dAttribs['timestamp'], fAllowNull = False);1672 if sError is not None:1673 return 'Element %s has an invalid timestamp ("%s"): %s' % (sName, dAttribs['timestamp'], sError,);1674 1675 1676 #1677 # Check that attributes that are required are present.1678 # We ignore extra attributes.1679 #1680 dElementAttribs = \1681 {1682 'Test': [ 'timestamp', 'name', ],1683 'Value': [ 'timestamp', 'name', 'unit', 'value', ],1684 'FailureDetails': [ 'timestamp', 'text', ],1685 'Passed': [ 'timestamp', ],1686 'Skipped': [ 'timestamp', ],1687 'Failed': [ 'timestamp', 'errors', ],1688 'TimedOut': [ 'timestamp', 'errors', ],1689 'End': [ 'timestamp', ],1690 'PushHint': [ 'testdepth', ],1691 'PopHint': [ 'testdepth', ],1692 };1693 if sName not in dElementAttribs:1694 return 'Unknown element "%s".' % (sName,);1695 for sAttr in dElementAttribs[sName]:1696 if sAttr not in dAttribs:1697 return 'Element %s requires attribute "%s".' % (sName, sAttr);1698 1699 #1700 # Only the Test element can (and must) remain open.1701 #1702 if sName == 'Test' and fClosed:1703 return '<Test/> is not allowed.';1704 if sName != 'Test' and not fClosed:1705 return 'All elements except <Test> must be closed.';1706 1707 return None;1708 1709 @staticmethod1710 def _parseElement(sElement):1711 """1712 Parses an element.1713 1714 """1715 #1716 # Element level bits.1717 #1718 sName = sElement.split()[0];1719 sElement = sElement[len(sName):];1720 1721 fClosed = sElement[-1] == '/';1722 if fClosed:1723 sElement = sElement[:-1];1724 1725 #1726 # Attributes.1727 #1728 sError = None;1729 dAttribs = {};1730 sElement = sElement.strip();1731 while len(sElement) > 0:1732 # Extract attribute name.1733 off = sElement.find('=');1734 if off < 0 or not sElement[:off].isalnum():1735 sError = 'Attributes shall have alpha numberical names and have values.';1736 break;1737 sAttr = sElement[:off];1738 1739 # Extract attribute value.1740 if off + 2 >= len(sElement) or sElement[off + 1] != '"':1741 sError = 'Attribute (%s) value is missing or not in double quotes.' % (sAttr,);1742 break;1743 off += 2;1744 offEndQuote = sElement.find('"', off);1745 if offEndQuote < 0:1746 sError = 'Attribute (%s) value is missing end quotation mark.' % (sAttr,);1747 break;1748 sValue = sElement[off:offEndQuote];1749 1750 # Check for duplicates.1751 if sAttr in dAttribs:1752 sError = 'Attribute "%s" appears more than once.' % (sAttr,);1753 break;1754 1755 # Unescape the value.1756 sValue = sValue.replace('<', '<');1757 sValue = sValue.replace('>', '>');1758 sValue = sValue.replace(''', '\'');1759 sValue = sValue.replace('"', '"');1760 sValue = sValue.replace('
', '\n');1761 sValue = sValue.replace('
', '\r');1762 sValue = sValue.replace('&', '&'); # last1763 1764 # Done.1765 dAttribs[sAttr] = sValue;1766 1767 # advance1768 sElement = sElement[offEndQuote + 1:];1769 sElement = sElement.lstrip();1770 1771 #1772 # Validate the element before we return.1773 #1774 if sError is None:1775 sError = TestResultLogic._validateElement(sName, dAttribs, fClosed);1776 1777 return (sName, dAttribs, sError)1778 1779 def _handleElement(self, sName, dAttribs, idTestSet, aoStack, aaiHints, dCounts):1780 """1781 Worker for processXmlStream that handles one element.1782 1783 Returns None on success, error string on bad XML or similar.1784 Raises exception on hanging offence and on database error.1785 """1786 if sName == 'Test':1787 iNestingDepth = aoStack[0].iNestingDepth + 1 if len(aoStack) > 0 else 0;1788 aoStack.insert(0, self._newTestResult(idTestResultParent = aoStack[0].idTestResult, idTestSet = idTestSet,1789 tsCreated = dAttribs['timestamp'], sName = dAttribs['name'],1790 iNestingDepth = iNestingDepth, dCounts = dCounts, fCommit = True) );1791 1792 elif sName == 'Value':1793 self._newTestValue(idTestResult = aoStack[0].idTestResult, idTestSet = idTestSet, tsCreated = dAttribs['timestamp'],1794 sName = dAttribs['name'], sUnit = dAttribs['unit'], lValue = long(dAttribs['value']),1795 dCounts = dCounts, fCommit = True);1796 1797 elif sName == 'FailureDetails':1798 self._newFailureDetails(idTestResult = aoStack[0].idTestResult, tsCreated = dAttribs['timestamp'],1799 sText = dAttribs['text'], dCounts = dCounts, fCommit = True);1800 1801 elif sName == 'Passed':1802 self._completeTestResults(aoStack[0], tsDone = dAttribs['timestamp'],1803 enmStatus = TestResultData.ksTestStatus_Success, fCommit = True);1804 1805 elif sName == 'Skipped':1806 self._completeTestResults(aoStack[0], tsDone = dAttribs['timestamp'],1807 enmStatus = TestResultData.ksTestStatus_Skipped, fCommit = True);1808 1809 elif sName == 'Failed':1810 self._completeTestResults(aoStack[0], tsDone = dAttribs['timestamp'], cErrors = int(dAttribs['errors']),1811 enmStatus = TestResultData.ksTestStatus_Failure, fCommit = True);1812 1813 elif sName == 'TimedOut':1814 self._completeTestResults(aoStack[0], tsDone = dAttribs['timestamp'], cErrors = int(dAttribs['errors']),1815 enmStatus = TestResultData.ksTestStatus_TimedOut, fCommit = True);1816 1817 elif sName == 'End':1818 self._completeTestResults(aoStack[0], tsDone = dAttribs['timestamp'],1819 cErrors = int(dAttribs.get('errors', '1')),1820 enmStatus = TestResultData.ksTestStatus_Success, fCommit = True);1821 1822 elif sName == 'PushHint':1823 if len(aaiHints) > 1:1824 return 'PushHint cannot be nested.'1825 1826 aaiHints.insert(0, [len(aoStack), int(dAttribs['testdepth'])]);1827 1828 elif sName == 'PopHint':1829 if len(aaiHints) < 1:1830 return 'No hint to pop.'1831 1832 iDesiredTestDepth = int(dAttribs['testdepth']);1833 cStackEntries, iTestDepth = aaiHints.pop(0);1834 self._doPopHint(aoStack, cStackEntries, dCounts); # Fake the necessary '<End/></Test>' tags.1835 if iDesiredTestDepth != iTestDepth:1836 return 'PopHint tag has different testdepth: %d, on stack %d.' % (iDesiredTestDepth, iTestDepth);1837 else:1838 return 'Unexpected element "%s".' % (sName,);1839 return None;1840 1841 1842 def processXmlStream(self, sXml, idTestSet):1843 """1844 Processes the "XML" stream section given in sXml.1845 1846 The sXml isn't a complete XML document, even should we save up all sXml1847 for a given set, they may not form a complete and well formed XML1848 document since the test may be aborted, abend or simply be buggy. We1849 therefore do our own parsing and treat the XML tags as commands more1850 than anything else.1851 1852 Returns (sError, fUnforgivable), where sError is None on success.1853 May raise database exception.1854 """1855 aoStack = self._getResultStack(idTestSet); # [0] == top; [-1] == bottom.1856 if len(aoStack) == 0:1857 return ('No open results', True);1858 self._oDb.dprint('** processXmlStream len(aoStack)=%s' % (len(aoStack),));1859 #self._oDb.dprint('processXmlStream: %s' % (self._stringifyStack(aoStack),));1860 #self._oDb.dprint('processXmlStream: sXml=%s' % (sXml,));1861 1862 dCounts = {};1863 aaiHints = [];1864 sError = None;1865 1866 fExpectCloseTest = False;1867 sXml = sXml.strip();1868 while len(sXml) > 0:1869 if sXml.startswith('</Test>'): # Only closing tag.1870 offNext = len('</Test>');1871 if len(aoStack) <= 1:1872 sError = 'Trying to close the top test results.'1873 break;1874 # ASSUMES that we've just seen an <End/>, <Passed/>, <Failed/>,1875 # <TimedOut/> or <Skipped/> tag earlier in this call!1876 if aoStack[0].enmStatus == TestResultData.ksTestStatus_Running or not fExpectCloseTest:1877 sError = 'Missing <End/>, <Passed/>, <Failed/>, <TimedOut/> or <Skipped/> tag.';1878 break;1879 aoStack.pop(0);1880 fExpectCloseTest = False;1881 1882 elif fExpectCloseTest:1883 sError = 'Expected </Test>.'1884 break;1885 1886 elif sXml.startswith('<?xml '): # Ignore (included files).1887 offNext = sXml.find('?>');1888 if offNext < 0:1889 sError = 'Unterminated <?xml ?> element.';1890 break;1891 offNext += 2;1892 1893 elif sXml[0] == '<':1894 # Parse and check the tag.1895 if not sXml[1].isalpha():1896 sError = 'Malformed element.';1897 break;1898 offNext = sXml.find('>')1899 if offNext < 0:1900 sError = 'Unterminated element.';1901 break;1902 (sName, dAttribs, sError) = self._parseElement(sXml[1:offNext]);1903 offNext += 1;1904 if sError is not None:1905 break;1906 1907 # Handle it.1908 try:1909 sError = self._handleElement(sName, dAttribs, idTestSet, aoStack, aaiHints, dCounts);1910 except TestResultHangingOffence as oXcpt:1911 self._inhumeTestResults(aoStack, idTestSet, str(oXcpt));1912 return (str(oXcpt), True);1913 1914 1915 fExpectCloseTest = sName in [ 'End', 'Passed', 'Failed', 'TimedOut', 'Skipped', ];1916 else:1917 sError = 'Unexpected content.';1918 break;1919 1920 # Advance.1921 sXml = sXml[offNext:];1922 sXml = sXml.lstrip();1923 1924 #1925 # Post processing checks.1926 #1927 if sError is None and fExpectCloseTest:1928 sError = 'Expected </Test> before the end of the XML section.'1929 elif sError is None and len(aaiHints) > 0:1930 sError = 'Expected </PopHint> before the end of the XML section.'1931 if len(aaiHints) > 0:1932 self._doPopHint(aoStack, aaiHints[-1][0], dCounts);1933 1934 #1935 # Log the error.1936 #1937 if sError is not None:1938 SystemLogLogic(self._oDb).addEntry(SystemLogData.ksEvent_XmlResultMalformed,1939 'idTestSet=%s idTestResult=%s XML="%s" %s'1940 % ( idTestSet,1941 aoStack[0].idTestResult if len(aoStack) > 0 else -1,1942 sXml[:30 if len(sXml) >= 30 else len(sXml)],1943 sError, ),1944 cHoursRepeat = 6, fCommit = True);1945 return (sError, False);1946 241 1947 242 … … 2202 497 2203 498 # pylint: disable=C0111 2204 class TestResult DataTestCase(ModelDataBaseTestCase):499 class TestResultFailureDataTestCase(ModelDataBaseTestCase): 2205 500 def setUp(self): 2206 self.aoSamples = [TestResultData(),]; 2207 2208 class TestResultValueDataTestCase(ModelDataBaseTestCase): 2209 def setUp(self): 2210 self.aoSamples = [TestResultValueData(),]; 501 self.aoSamples = [TestResultFailureData(),]; 2211 502 2212 503 if __name__ == '__main__': -
trunk/src/VBox/ValidationKit/testmanager/core/testresults.py
r61268 r61278 35 35 36 36 # Validation Kit imports. 37 from common import constants;38 from testmanager import config;39 from testmanager.core.base import ModelDataBase, ModelLogicBase, ModelDataBaseTestCase, TMExceptionBase, \40 TMTooManyRows, TMInvalidData, TMRowNotFound, TMRowAlreadyExists, \41 ChangeLogEntry, AttributeChangeEntry;42 from testmanager.core. testgroup import TestGroupData;43 from testmanager.core. build import BuildDataEx;44 from testmanager.core. failurereason import FailureReasonLogic, FailureReasonData;45 from testmanager.core.test box import TestBoxData;46 from testmanager.core. testcase import TestCaseData;47 from testmanager.core.s chedgroup import SchedGroupData;48 from testmanager.core. systemlog import SystemLogData, SystemLogLogic;49 from testmanager.core.useraccount import UserAccountLogic;37 from common import constants; 38 from testmanager import config; 39 from testmanager.core.base import ModelDataBase, ModelLogicBase, ModelDataBaseTestCase, TMExceptionBase, \ 40 TMTooManyRows, TMRowNotFound; 41 from testmanager.core.testgroup import TestGroupData; 42 from testmanager.core.build import BuildDataEx; 43 from testmanager.core.failurereason import FailureReasonLogic; 44 from testmanager.core.testbox import TestBoxData; 45 from testmanager.core.testcase import TestCaseData; 46 from testmanager.core.schedgroup import SchedGroupData; 47 from testmanager.core.systemlog import SystemLogData, SystemLogLogic; 48 from testmanager.core.testresultfailures import TestResultFailureDataEx; 49 from testmanager.core.useraccount import UserAccountLogic; 50 50 51 51 … … 428 428 return self.sMime; 429 429 430 431 class TestResultFailureData(ModelDataBase):432 """433 Test result failure reason data.434 """435 436 ksIdAttr = 'idTestResult';437 kfIdAttrIsForForeign = True; # Modifies the 'add' validation.438 439 ksParam_idTestResult = 'TestResultFailure_idTestResult';440 ksParam_tsEffective = 'TestResultFailure_tsEffective';441 ksParam_tsExpire = 'TestResultFailure_tsExpire';442 ksParam_uidAuthor = 'TestResultFailure_uidAuthor';443 ksParam_idTestSet = 'TestResultFailure_idTestSet';444 ksParam_idFailureReason = 'TestResultFailure_idFailureReason';445 ksParam_sComment = 'TestResultFailure_sComment';446 447 kasAllowNullAttributes = ['tsEffective', 'tsExpire', 'uidAuthor', 'sComment', 'idTestSet' ];448 449 kcDbColumns = 7;450 451 def __init__(self):452 ModelDataBase.__init__(self)453 self.idTestResult = None;454 self.tsEffective = None;455 self.tsExpire = None;456 self.uidAuthor = None;457 self.idTestSet = None;458 self.idFailureReason = None;459 self.sComment = None;460 461 def initFromDbRow(self, aoRow):462 """463 Reinitialize from a SELECT * FROM TestResultFailures.464 Return self. Raises exception if no row.465 """466 if aoRow is None:467 raise TMRowNotFound('Test result file record not found.')468 469 self.idTestResult = aoRow[0];470 self.tsEffective = aoRow[1];471 self.tsExpire = aoRow[2];472 self.uidAuthor = aoRow[3];473 self.idTestSet = aoRow[4];474 self.idFailureReason = aoRow[5];475 self.sComment = aoRow[6];476 return self;477 478 def initFromDbWithId(self, oDb, idTestResult, tsNow = None, sPeriodBack = None):479 """480 Initialize the object from the database.481 """482 oDb.execute(self.formatSimpleNowAndPeriodQuery(oDb,483 'SELECT *\n'484 'FROM TestResultFailures\n'485 'WHERE idTestResult = %s\n'486 , ( idTestResult,), tsNow, sPeriodBack));487 aoRow = oDb.fetchOne()488 if aoRow is None:489 raise TMRowNotFound('idTestResult=%s not found (tsNow=%s, sPeriodBack=%s)' % (idTestResult, tsNow, sPeriodBack));490 assert len(aoRow) == self.kcDbColumns;491 return self.initFromDbRow(aoRow);492 493 494 class TestResultFailureDataEx(TestResultFailureData):495 """496 Extends TestResultFailureData by resolving reasons and user.497 """498 499 def __init__(self):500 TestResultFailureData.__init__(self);501 self.oFailureReason = None;502 self.oAuthor = None;503 504 def initFromDbRowEx(self, aoRow, oFailureReasonLogic, oUserAccountLogic):505 """506 Reinitialize from a query like this:507 SELECT TestResultFiles.*,508 StrTabFile.sValue AS sFile,509 StrTabDesc.sValue AS sDescription510 StrTabKind.sValue AS sKind,511 StrTabMime.sValue AS sMime,512 FROM ...513 514 Return self. Raises exception if no row.515 """516 self.initFromDbRow(aoRow);517 self.oFailureReason = oFailureReasonLogic.cachedLookup(self.idFailureReason);518 self.oAuthor = oUserAccountLogic.cachedLookup(self.uidAuthor);519 return self;520 430 521 431 … … 1947 1857 1948 1858 1949 class TestResultFailureLogic(ModelLogicBase): # pylint: disable=R09031950 """1951 Test result failure reason logic.1952 """1953 1954 def __init__(self, oDb):1955 ModelLogicBase.__init__(self, oDb)1956 1957 def fetchForChangeLog(self, idTestResult, iStart, cMaxRows, tsNow): # pylint: disable=R09141958 """1959 Fetches change log entries for a failure reason.1960 1961 Returns an array of ChangeLogEntry instance and an indicator whether1962 there are more entries.1963 Raises exception on error.1964 """1965 1966 if tsNow is None:1967 tsNow = self._oDb.getCurrentTimestamp();1968 1969 # 1. Get a list of the changes from both TestResultFailures and assoicated1970 # FailureReasons. The latter is useful since the failure reason1971 # description may evolve along side the invidiual failure analysis.1972 self._oDb.execute('( SELECT trf.tsEffective AS tsEffectiveChangeLog,\n'1973 ' trf.uidAuthor AS uidAuthorChangeLog,\n'1974 ' trf.*,\n'1975 ' fr.*\n'1976 ' FROM TestResultFailures trf,\n'1977 ' FailureReasons fr\n'1978 ' WHERE trf.idTestResult = %s\n'1979 ' AND trf.tsEffective <= %s\n'1980 ' AND trf.idFailureReason = fr.idFailureReason\n'1981 ' AND fr.tsEffective <= trf.tsEffective\n'1982 ' AND fr.tsExpire > trf.tsEffective\n'1983 ')\n'1984 'UNION\n'1985 '( SELECT fr.tsEffective AS tsEffectiveChangeLog,\n'1986 ' fr.uidAuthor AS uidAuthorChangeLog,\n'1987 ' trf.*,\n'1988 ' fr.*\n'1989 ' FROM TestResultFailures trf,\n'1990 ' FailureReasons fr\n'1991 ' WHERE trf.idTestResult = %s\n'1992 ' AND trf.tsEffective <= %s\n'1993 ' AND trf.idFailureReason = fr.idFailureReason\n'1994 ' AND fr.tsEffective > trf.tsEffective\n'1995 ' AND fr.tsEffective < trf.tsExpire\n'1996 ')\n'1997 'ORDER BY tsEffectiveChangeLog DESC\n'1998 'LIMIT %s OFFSET %s\n'1999 , ( idTestResult, tsNow, idTestResult, tsNow, cMaxRows + 1, iStart, ));2000 2001 aaoRows = [];2002 for aoChange in self._oDb.fetchAll():2003 oTrf = TestResultFailureDataEx().initFromDbRow(aoChange[2:]);2004 oFr = FailureReasonData().initFromDbRow(aoChange[(2+TestResultFailureData.kcDbColumns):]);2005 oTrf.oFailureReason = oFr;2006 aaoRows.append([aoChange[0], aoChange[1], oTrf, oFr]);2007 2008 # 2. Calculate the changes.2009 oFailureCategoryLogic = None;2010 aoEntries = [];2011 for i in xrange(0, len(aaoRows) - 1):2012 aoNew = aaoRows[i];2013 aoOld = aaoRows[i + 1];2014 2015 aoChanges = [];2016 oNew = aoNew[2];2017 oOld = aoOld[2];2018 for sAttr in oNew.getDataAttributes():2019 if sAttr not in [ 'tsEffective', 'tsExpire', 'uidAuthor', 'oFailureReason', 'oAuthor' ]:2020 oOldAttr = getattr(oOld, sAttr);2021 oNewAttr = getattr(oNew, sAttr);2022 if oOldAttr != oNewAttr:2023 if sAttr == 'idFailureReason':2024 oNewAttr = '%s (%s)' % (oNewAttr, oNew.oFailureReason.sShort, );2025 oOldAttr = '%s (%s)' % (oOldAttr, oOld.oFailureReason.sShort, );2026 aoChanges.append(AttributeChangeEntry(sAttr, oNewAttr, oOldAttr, str(oNewAttr), str(oOldAttr)));2027 if oOld.idFailureReason == oNew.idFailureReason:2028 oNew = aoNew[3];2029 oOld = aoOld[3];2030 for sAttr in oNew.getDataAttributes():2031 if sAttr not in [ 'tsEffective', 'tsExpire', 'uidAuthor', ]:2032 oOldAttr = getattr(oOld, sAttr);2033 oNewAttr = getattr(oNew, sAttr);2034 if oOldAttr != oNewAttr:2035 if sAttr == 'idFailureCategory':2036 if oFailureCategoryLogic is None:2037 from testmanager.core.failurecategory import FailureCategoryLogic;2038 oFailureCategoryLogic = FailureCategoryLogic(self._oDb);2039 oCat = oFailureCategoryLogic.cachedLookup(oNewAttr);2040 if oCat is not None:2041 oNewAttr = '%s (%s)' % (oNewAttr, oCat.sShort, );2042 oCat = oFailureCategoryLogic.cachedLookup(oOldAttr);2043 if oCat is not None:2044 oOldAttr = '%s (%s)' % (oOldAttr, oCat.sShort, );2045 aoChanges.append(AttributeChangeEntry(sAttr, oNewAttr, oOldAttr, str(oNewAttr), str(oOldAttr)));2046 2047 2048 tsExpire = aaoRows[i - 1][0] if i > 0 else aoNew[2].tsExpire;2049 aoEntries.append(ChangeLogEntry(aoNew[1], None, aoNew[0], tsExpire, aoNew[2], aoOld[2], aoChanges));2050 2051 # If we're at the end of the log, add the initial entry.2052 if len(aaoRows) <= cMaxRows and len(aaoRows) > 0:2053 aoNew = aaoRows[-1];2054 tsExpire = aaoRows[-1 - 1][0] if len(aaoRows) > 1 else aoNew[2].tsExpire;2055 aoEntries.append(ChangeLogEntry(aoNew[1], None, aoNew[0], tsExpire, aoNew[2], None, []));2056 2057 return (UserAccountLogic(self._oDb).resolveChangeLogAuthors(aoEntries), len(aaoRows) > cMaxRows);2058 2059 2060 def getById(self, idTestResult):2061 """Get Test result failure reason data by idTestResult"""2062 2063 self._oDb.execute('SELECT *\n'2064 'FROM TestResultFailures\n'2065 'WHERE tsExpire = \'infinity\'::timestamp\n'2066 ' AND idTestResult = %s;', (idTestResult,))2067 aRows = self._oDb.fetchAll()2068 if len(aRows) not in (0, 1):2069 raise self._oDb.integrityException(2070 'Found more than one failure reasons with the same credentials. Database structure is corrupted.')2071 try:2072 return TestResultFailureData().initFromDbRow(aRows[0])2073 except IndexError:2074 return None2075 2076 def addEntry(self, oData, uidAuthor, fCommit = False):2077 """2078 Add a test result failure reason record.2079 """2080 2081 #2082 # Validate inputs and read in the old(/current) data.2083 #2084 assert isinstance(oData, TestResultFailureData);2085 dErrors = oData.validateAndConvert(self._oDb, oData.ksValidateFor_AddForeignId);2086 if len(dErrors) > 0:2087 raise TMInvalidData('editEntry invalid input: %s' % (dErrors,));2088 2089 # Check if it exist first (we're adding, not editing, collisions not allowed).2090 oOldData = self.getById(oData.idTestResult);2091 if oOldData is not None:2092 raise TMRowAlreadyExists('TestResult %d already have a failure reason associated with it:'2093 '%s\n'2094 'Perhaps someone else beat you to it? Or did you try resubmit?'2095 % (oData.idTestResult, oOldData));2096 oData = self._resolveSetTestIdIfMissing(oData);2097 2098 #2099 # Add record.2100 #2101 self._readdEntry(uidAuthor, oData);2102 self._oDb.maybeCommit(fCommit);2103 return True;2104 2105 def editEntry(self, oData, uidAuthor, fCommit = False):2106 """2107 Modifies a test result failure reason.2108 """2109 2110 #2111 # Validate inputs and read in the old(/current) data.2112 #2113 assert isinstance(oData, TestResultFailureData);2114 dErrors = oData.validateAndConvert(self._oDb, oData.ksValidateFor_Edit);2115 if len(dErrors) > 0:2116 raise TMInvalidData('editEntry invalid input: %s' % (dErrors,));2117 2118 oOldData = self.getById(oData.idTestResult)2119 oData.idTestSet = oOldData.idTestSet;2120 2121 #2122 # Update the data that needs updating.2123 #2124 if not oData.isEqualEx(oOldData, [ 'tsEffective', 'tsExpire', 'uidAuthor', ]):2125 self._historizeEntry(oData.idTestResult);2126 self._readdEntry(uidAuthor, oData);2127 self._oDb.maybeCommit(fCommit);2128 return True;2129 2130 2131 def removeEntry(self, uidAuthor, idTestResult, fCascade = False, fCommit = False):2132 """2133 Deletes a test result failure reason.2134 """2135 _ = fCascade; # Not applicable.2136 2137 oData = self.getById(idTestResult)2138 (tsCur, tsCurMinusOne) = self._oDb.getCurrentTimestamps();2139 if oData.tsEffective != tsCur and oData.tsEffective != tsCurMinusOne:2140 self._historizeEntry(idTestResult, tsCurMinusOne);2141 self._readdEntry(uidAuthor, oData, tsCurMinusOne);2142 self._historizeEntry(idTestResult);2143 self._oDb.execute('UPDATE TestResultFailures\n'2144 'SET tsExpire = CURRENT_TIMESTAMP\n'2145 'WHERE idTestResult = %s\n'2146 ' AND tsExpire = \'infinity\'::TIMESTAMP\n'2147 , (idTestResult,));2148 self._oDb.maybeCommit(fCommit);2149 return True;2150 2151 #2152 # Helpers.2153 #2154 2155 def _readdEntry(self, uidAuthor, oData, tsEffective = None):2156 """2157 Re-adds the TestResultFailure entry. Used by addEntry, editEntry and removeEntry.2158 """2159 if tsEffective is None:2160 tsEffective = self._oDb.getCurrentTimestamp();2161 self._oDb.execute('INSERT INTO TestResultFailures (\n'2162 ' uidAuthor,\n'2163 ' tsEffective,\n'2164 ' idTestResult,\n'2165 ' idTestSet,\n'2166 ' idFailureReason,\n'2167 ' sComment)\n'2168 'VALUES (%s, %s, %s, %s, %s, %s)\n'2169 , ( uidAuthor,2170 tsEffective,2171 oData.idTestResult,2172 oData.idTestSet,2173 oData.idFailureReason,2174 oData.sComment,) );2175 return True;2176 2177 2178 def _historizeEntry(self, idTestResult, tsExpire = None):2179 """ Historizes the current entry. """2180 if tsExpire is None:2181 tsExpire = self._oDb.getCurrentTimestamp();2182 self._oDb.execute('UPDATE TestResultFailures\n'2183 'SET tsExpire = %s\n'2184 'WHERE idTestResult = %s\n'2185 ' AND tsExpire = \'infinity\'::TIMESTAMP\n'2186 , (tsExpire, idTestResult,));2187 return True;2188 2189 2190 def _resolveSetTestIdIfMissing(self, oData):2191 """ Resolve any missing idTestSet reference (it's a duplicate for speed efficiency). """2192 if oData.idTestSet is None and oData.idTestResult is not None:2193 self._oDb.execute('SELECT idTestSet FROM TestResults WHERE idTestResult = %s', (oData.idTestResult,));2194 oData.idTestSet = self._oDb.fetchOne()[0];2195 return oData;2196 2197 2198 2199 1859 # 2200 1860 # Unit testing. -
trunk/src/VBox/ValidationKit/testmanager/webui/wuihlpform.py
r61259 r61278 452 452 sHtml += u'}\n'; 453 453 sHtml += u'\n'; 454 sHtml += u'function %s_extendListEx(sSubName, cGangMembers, cSecTimeout, sArgs, sTestBoxReqExpr, sBuildReqExpr)\n' % (sName,); 454 sHtml += u'function %s_extendListEx(sSubName, cGangMembers, cSecTimeout, sArgs, sTestBoxReqExpr, sBuildReqExpr)\n' \ 455 % (sName,); 455 456 sHtml += u'{\n'; 456 457 sHtml += u' var oElement = document.getElementById(\'%s\');\n' % (sTableId,); -
trunk/src/VBox/ValidationKit/testmanager/webui/wuimain.py
r61270 r61278 920 920 def _actionTestResultFailureAdd(self): 921 921 """ Pro forma. """ 922 from testmanager.core.testresult s import TestResultFailureData;922 from testmanager.core.testresultfailures import TestResultFailureData; 923 923 from testmanager.webui.wuitestresultfailure import WuiTestResultFailure; 924 924 return self._actionGenericFormAdd(TestResultFailureData, WuiTestResultFailure); … … 926 926 def _actionTestResultFailureAddPost(self): 927 927 """Add test result failure result""" 928 from testmanager.core.testresult s import TestResultFailureLogic, TestResultFailureData;928 from testmanager.core.testresultfailures import TestResultFailureLogic, TestResultFailureData; 929 929 from testmanager.webui.wuitestresultfailure import WuiTestResultFailure; 930 930 if self.ksParamRedirectTo not in self._dParams: … … 936 936 def _actionTestResultFailureDetails(self): 937 937 """ Pro forma. """ 938 from testmanager.core.testresult s import TestResultFailureLogic, TestResultFailureData;938 from testmanager.core.testresultfailures import TestResultFailureLogic, TestResultFailureData; 939 939 from testmanager.webui.wuitestresultfailure import WuiTestResultFailure; 940 940 return self._actionGenericFormDetails(TestResultFailureData, TestResultFailureLogic, … … 943 943 def _actionTestResultFailureEdit(self): 944 944 """ Pro forma. """ 945 from testmanager.core.testresult s import TestResultFailureData;945 from testmanager.core.testresultfailures import TestResultFailureData; 946 946 from testmanager.webui.wuitestresultfailure import WuiTestResultFailure; 947 947 return self._actionGenericFormEdit(TestResultFailureData, WuiTestResultFailure, … … 950 950 def _actionTestResultFailureEditPost(self): 951 951 """Edit test result failure result""" 952 from testmanager.core.testresult s import TestResultFailureLogic, TestResultFailureData;952 from testmanager.core.testresultfailures import TestResultFailureLogic, TestResultFailureData; 953 953 from testmanager.webui.wuitestresultfailure import WuiTestResultFailure; 954 954 return self._actionGenericFormEditPost(TestResultFailureData, TestResultFailureLogic, -
trunk/src/VBox/ValidationKit/testmanager/webui/wuitestresult.py
r61272 r61278 46 46 from testmanager.core.testset import TestSetData; 47 47 from testmanager.core.testgroup import TestGroupData; 48 from testmanager.core.testresult simport TestResultFailureData;48 from testmanager.core.testresultfailures import TestResultFailureData; 49 49 from testmanager.core.build import BuildData; 50 50 from testmanager.core import db; -
trunk/src/VBox/ValidationKit/testmanager/webui/wuitestresultfailure.py
r61255 r61278 33 33 from testmanager.webui.wuimain import WuiMain; 34 34 from testmanager.webui.wuiadminfailurereason import WuiFailureReasonDetailsLink, WuiFailureReasonAddLink; 35 from testmanager.core.testresult simport TestResultFailureData;35 from testmanager.core.testresultfailures import TestResultFailureData; 36 36 from testmanager.core.testset import TestSetData; 37 37 from testmanager.core.failurereason import FailureReasonLogic;
Note:
See TracChangeset
for help on using the changeset viewer.