diff options
Diffstat (limited to 'src/VBox/ValidationKit/testmanager/core/testresults.py')
-rwxr-xr-x | src/VBox/ValidationKit/testmanager/core/testresults.py | 2926 |
1 files changed, 2926 insertions, 0 deletions
diff --git a/src/VBox/ValidationKit/testmanager/core/testresults.py b/src/VBox/ValidationKit/testmanager/core/testresults.py new file mode 100755 index 00000000..58f056d2 --- /dev/null +++ b/src/VBox/ValidationKit/testmanager/core/testresults.py @@ -0,0 +1,2926 @@ +# -*- coding: utf-8 -*- +# $Id: testresults.py $ +# pylint: disable=too-many-lines + +## @todo Rename this file to testresult.py! + +""" +Test Manager - Fetch test results. +""" + +__copyright__ = \ +""" +Copyright (C) 2012-2023 Oracle and/or its affiliates. + +This file is part of VirtualBox base platform packages, as +available from https://www.virtualbox.org. + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation, in version 3 of the +License. + +This program is distributed in the hope that it will be useful, but +WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, see <https://www.gnu.org/licenses>. + +The contents of this file may alternatively be used under the terms +of the Common Development and Distribution License Version 1.0 +(CDDL), a copy of it is provided in the "COPYING.CDDL" file included +in the VirtualBox distribution, in which case the provisions of the +CDDL are applicable instead of those of the GPL. + +You may elect to license modified versions of this file under the +terms and conditions of either the GPL or the CDDL or both. + +SPDX-License-Identifier: GPL-3.0-only OR CDDL-1.0 +""" +__version__ = "$Revision: 155244 $" + + +# Standard python imports. +import sys; +import unittest; + +# Validation Kit imports. +from common import constants; +from testmanager import config; +from testmanager.core.base import ModelDataBase, ModelLogicBase, ModelDataBaseTestCase, ModelFilterBase, \ + FilterCriterion, FilterCriterionValueAndDescription, \ + TMExceptionBase, TMTooManyRows, TMRowNotFound; +from testmanager.core.testgroup import TestGroupData; +from testmanager.core.build import BuildDataEx, BuildCategoryData; +from testmanager.core.failurereason import FailureReasonLogic; +from testmanager.core.testbox import TestBoxData, TestBoxLogic; +from testmanager.core.testcase import TestCaseData; +from testmanager.core.schedgroup import SchedGroupData, SchedGroupLogic; +from testmanager.core.systemlog import SystemLogData, SystemLogLogic; +from testmanager.core.testresultfailures import TestResultFailureDataEx; +from testmanager.core.useraccount import UserAccountLogic; + +# Python 3 hacks: +if sys.version_info[0] >= 3: + long = int; # pylint: disable=redefined-builtin,invalid-name + + +class TestResultData(ModelDataBase): + """ + Test case execution result data + """ + + ## @name TestStatus_T + # @{ + ksTestStatus_Running = 'running'; + ksTestStatus_Success = 'success'; + ksTestStatus_Skipped = 'skipped'; + ksTestStatus_BadTestBox = 'bad-testbox'; + ksTestStatus_Aborted = 'aborted'; + ksTestStatus_Failure = 'failure'; + ksTestStatus_TimedOut = 'timed-out'; + ksTestStatus_Rebooted = 'rebooted'; + ## @} + + ## List of relatively harmless (to testgroup/case) statuses. + kasHarmlessTestStatuses = [ ksTestStatus_Skipped, ksTestStatus_BadTestBox, ksTestStatus_Aborted, ]; + ## List of bad statuses. + kasBadTestStatuses = [ ksTestStatus_Failure, ksTestStatus_TimedOut, ksTestStatus_Rebooted, ]; + + + ksIdAttr = 'idTestResult'; + + ksParam_idTestResult = 'TestResultData_idTestResult'; + ksParam_idTestResultParent = 'TestResultData_idTestResultParent'; + ksParam_idTestSet = 'TestResultData_idTestSet'; + ksParam_tsCreated = 'TestResultData_tsCreated'; + ksParam_tsElapsed = 'TestResultData_tsElapsed'; + ksParam_idStrName = 'TestResultData_idStrName'; + ksParam_cErrors = 'TestResultData_cErrors'; + ksParam_enmStatus = 'TestResultData_enmStatus'; + ksParam_iNestingDepth = 'TestResultData_iNestingDepth'; + kasValidValues_enmStatus = [ + ksTestStatus_Running, + ksTestStatus_Success, + ksTestStatus_Skipped, + ksTestStatus_BadTestBox, + ksTestStatus_Aborted, + ksTestStatus_Failure, + ksTestStatus_TimedOut, + ksTestStatus_Rebooted + ]; + + + def __init__(self): + ModelDataBase.__init__(self) + self.idTestResult = None + self.idTestResultParent = None + self.idTestSet = None + self.tsCreated = None + self.tsElapsed = None + self.idStrName = None + self.cErrors = 0; + self.enmStatus = None + self.iNestingDepth = None + + def initFromDbRow(self, aoRow): + """ + Reinitialize from a SELECT * FROM TestResults. + Return self. Raises exception if no row. + """ + if aoRow is None: + raise TMRowNotFound('Test result record not found.') + + self.idTestResult = aoRow[0] + self.idTestResultParent = aoRow[1] + self.idTestSet = aoRow[2] + self.tsCreated = aoRow[3] + self.tsElapsed = aoRow[4] + self.idStrName = aoRow[5] + self.cErrors = aoRow[6] + self.enmStatus = aoRow[7] + self.iNestingDepth = aoRow[8] + return self; + + def initFromDbWithId(self, oDb, idTestResult, tsNow = None, sPeriodBack = None): + """ + Initialize from the database, given the ID of a row. + """ + _ = tsNow; + _ = sPeriodBack; + oDb.execute('SELECT *\n' + 'FROM TestResults\n' + 'WHERE idTestResult = %s\n' + , ( idTestResult,)); + aoRow = oDb.fetchOne() + if aoRow is None: + raise TMRowNotFound('idTestResult=%s not found' % (idTestResult,)); + return self.initFromDbRow(aoRow); + + def isFailure(self): + """ Check if it's a real failure. """ + return self.enmStatus in self.kasBadTestStatuses; + + +class TestResultDataEx(TestResultData): + """ + Extended test result data class. + + This is intended for use as a node in a result tree. This is not intended + for serialization to parameters or vice versa. Use TestResultLogic to + construct the tree. + """ + + def __init__(self): + TestResultData.__init__(self) + self.sName = None; # idStrName resolved. + self.oParent = None; # idTestResultParent within the tree. + + self.aoChildren = []; # TestResultDataEx; + self.aoValues = []; # TestResultValueDataEx; + self.aoMsgs = []; # TestResultMsgDataEx; + self.aoFiles = []; # TestResultFileDataEx; + self.oReason = None; # TestResultReasonDataEx; + + def initFromDbRow(self, aoRow): + """ + Initialize from a query like this: + SELECT TestResults.*, TestResultStrTab.sValue + FROM TestResults, TestResultStrTab + WHERE TestResultStrTab.idStr = TestResults.idStrName + + Note! The caller is expected to fetch children, values, failure + details, and files. + """ + self.sName = None; + self.oParent = None; + self.aoChildren = []; + self.aoValues = []; + self.aoMsgs = []; + self.aoFiles = []; + self.oReason = None; + + TestResultData.initFromDbRow(self, aoRow); + + self.sName = aoRow[9]; + return self; + + def deepCountErrorContributers(self): + """ + Counts how many test result instances actually contributed to cErrors. + """ + + # Check each child (if any). + cChanges = 0; + cChildErrors = 0; + for oChild in self.aoChildren: + if oChild.cErrors > 0: + cChildErrors += oChild.cErrors; + cChanges += oChild.deepCountErrorContributers(); + + # Did we contribute as well? + if self.cErrors > cChildErrors: + cChanges += 1; + return cChanges; + + def getListOfFailures(self): + """ + Get a list of test results instances actually contributing to cErrors. + + Returns a list of TestResultDataEx instances from this tree. (shared!) + """ + # Check each child (if any). + aoRet = []; + cChildErrors = 0; + for oChild in self.aoChildren: + if oChild.cErrors > 0: + cChildErrors += oChild.cErrors; + aoRet.extend(oChild.getListOfFailures()); + + # Did we contribute as well? + if self.cErrors > cChildErrors: + aoRet.append(self); + + return aoRet; + + def getListOfLogFilesByKind(self, asKinds): + """ + Get a list of test results instances actually contributing to cErrors. + + Returns a list of TestResultFileDataEx instances from this tree. (shared!) + """ + aoRet = []; + + # Check the children first. + for oChild in self.aoChildren: + aoRet.extend(oChild.getListOfLogFilesByKind(asKinds)); + + # Check our own files next. + for oFile in self.aoFiles: + if oFile.sKind in asKinds: + aoRet.append(oFile); + + return aoRet; + + def getFullName(self): + """ Constructs the full name of this test result. """ + if self.oParent is None: + return self.sName; + return self.oParent.getFullName() + ' / ' + self.sName; + + + +class TestResultValueData(ModelDataBase): + """ + Test result value data. + """ + + ksIdAttr = 'idTestResultValue'; + + ksParam_idTestResultValue = 'TestResultValue_idTestResultValue'; + ksParam_idTestResult = 'TestResultValue_idTestResult'; + ksParam_idTestSet = 'TestResultValue_idTestSet'; + ksParam_tsCreated = 'TestResultValue_tsCreated'; + ksParam_idStrName = 'TestResultValue_idStrName'; + ksParam_lValue = 'TestResultValue_lValue'; + ksParam_iUnit = 'TestResultValue_iUnit'; + + kasAllowNullAttributes = [ 'idTestSet', ]; + + def __init__(self): + ModelDataBase.__init__(self) + self.idTestResultValue = None; + self.idTestResult = None; + self.idTestSet = None; + self.tsCreated = None; + self.idStrName = None; + self.lValue = None; + self.iUnit = 0; + + def initFromDbRow(self, aoRow): + """ + Reinitialize from a SELECT * FROM TestResultValues. + Return self. Raises exception if no row. + """ + if aoRow is None: + raise TMRowNotFound('Test result value record not found.') + + self.idTestResultValue = aoRow[0]; + self.idTestResult = aoRow[1]; + self.idTestSet = aoRow[2]; + self.tsCreated = aoRow[3]; + self.idStrName = aoRow[4]; + self.lValue = aoRow[5]; + self.iUnit = aoRow[6]; + return self; + + +class TestResultValueDataEx(TestResultValueData): + """ + Extends TestResultValue by resolving the value name and unit string. + """ + + def __init__(self): + TestResultValueData.__init__(self) + self.sName = None; + self.sUnit = ''; + + def initFromDbRow(self, aoRow): + """ + Reinitialize from a query like this: + SELECT TestResultValues.*, TestResultStrTab.sValue + FROM TestResultValues, TestResultStrTab + WHERE TestResultStrTab.idStr = TestResultValues.idStrName + + Return self. Raises exception if no row. + """ + TestResultValueData.initFromDbRow(self, aoRow); + self.sName = aoRow[7]; + if self.iUnit < len(constants.valueunit.g_asNames): + self.sUnit = constants.valueunit.g_asNames[self.iUnit]; + else: + self.sUnit = '<%d>' % (self.iUnit,); + return self; + +class TestResultMsgData(ModelDataBase): + """ + Test result message data. + """ + + ksIdAttr = 'idTestResultMsg'; + + ksParam_idTestResultMsg = 'TestResultValue_idTestResultMsg'; + ksParam_idTestResult = 'TestResultValue_idTestResult'; + ksParam_idTestSet = 'TestResultValue_idTestSet'; + ksParam_tsCreated = 'TestResultValue_tsCreated'; + ksParam_idStrMsg = 'TestResultValue_idStrMsg'; + ksParam_enmLevel = 'TestResultValue_enmLevel'; + + kasAllowNullAttributes = [ 'idTestSet', ]; + + kcDbColumns = 6 + + def __init__(self): + ModelDataBase.__init__(self) + self.idTestResultMsg = None; + self.idTestResult = None; + self.idTestSet = None; + self.tsCreated = None; + self.idStrMsg = None; + self.enmLevel = None; + + def initFromDbRow(self, aoRow): + """ + Reinitialize from a SELECT * FROM TestResultMsgs. + Return self. Raises exception if no row. + """ + if aoRow is None: + raise TMRowNotFound('Test result value record not found.') + + self.idTestResultMsg = aoRow[0]; + self.idTestResult = aoRow[1]; + self.idTestSet = aoRow[2]; + self.tsCreated = aoRow[3]; + self.idStrMsg = aoRow[4]; + self.enmLevel = aoRow[5]; + return self; + +class TestResultMsgDataEx(TestResultMsgData): + """ + Extends TestResultMsg by resolving the message string. + """ + + def __init__(self): + TestResultMsgData.__init__(self) + self.sMsg = None; + + def initFromDbRow(self, aoRow): + """ + Reinitialize from a query like this: + SELECT TestResultMsg.*, TestResultStrTab.sValue + FROM TestResultMsg, TestResultStrTab + WHERE TestResultStrTab.idStr = TestResultMsgs.idStrName + + Return self. Raises exception if no row. + """ + TestResultMsgData.initFromDbRow(self, aoRow); + self.sMsg = aoRow[self.kcDbColumns]; + return self; + + +class TestResultFileData(ModelDataBase): + """ + Test result message data. + """ + + ksIdAttr = 'idTestResultFile'; + + ksParam_idTestResultFile = 'TestResultFile_idTestResultFile'; + ksParam_idTestResult = 'TestResultFile_idTestResult'; + ksParam_tsCreated = 'TestResultFile_tsCreated'; + ksParam_idStrFile = 'TestResultFile_idStrFile'; + ksParam_idStrDescription = 'TestResultFile_idStrDescription'; + ksParam_idStrKind = 'TestResultFile_idStrKind'; + ksParam_idStrMime = 'TestResultFile_idStrMime'; + + ## @name Kind of files. + ## @{ + ksKind_LogReleaseVm = 'log/release/vm'; + ksKind_LogDebugVm = 'log/debug/vm'; + ksKind_LogReleaseSvc = 'log/release/svc'; + ksKind_LogDebugSvc = 'log/debug/svc'; + ksKind_LogReleaseClient = 'log/release/client'; + ksKind_LogDebugClient = 'log/debug/client'; + ksKind_LogInstaller = 'log/installer'; + ksKind_LogUninstaller = 'log/uninstaller'; + ksKind_LogGuestKernel = 'log/guest/kernel'; + ksKind_ProcessReportVm = 'process/report/vm'; + ksKind_CrashReportVm = 'crash/report/vm'; + ksKind_CrashDumpVm = 'crash/dump/vm'; + ksKind_CrashReportSvc = 'crash/report/svc'; + ksKind_CrashDumpSvc = 'crash/dump/svc'; + ksKind_CrashReportClient = 'crash/report/client'; + ksKind_CrashDumpClient = 'crash/dump/client'; + ksKind_InfoCollection = 'info/collection'; + ksKind_InfoVgaText = 'info/vgatext'; + ksKind_MiscOther = 'misc/other'; + ksKind_ScreenshotFailure = 'screenshot/failure'; + ksKind_ScreenshotSuccesss = 'screenshot/success'; + ksKind_ScreenRecordingFailure = 'screenrecording/failure'; + ksKind_ScreenRecordingSuccess = 'screenrecording/success'; + ## @} + + kasKinds = [ + ksKind_LogReleaseVm, + ksKind_LogDebugVm, + ksKind_LogReleaseSvc, + ksKind_LogDebugSvc, + ksKind_LogReleaseClient, + ksKind_LogDebugClient, + ksKind_LogInstaller, + ksKind_LogUninstaller, + ksKind_LogGuestKernel, + ksKind_ProcessReportVm, + ksKind_CrashReportVm, + ksKind_CrashDumpVm, + ksKind_CrashReportSvc, + ksKind_CrashDumpSvc, + ksKind_CrashReportClient, + ksKind_CrashDumpClient, + ksKind_InfoCollection, + ksKind_InfoVgaText, + ksKind_MiscOther, + ksKind_ScreenshotFailure, + ksKind_ScreenshotSuccesss, + ksKind_ScreenRecordingFailure, + ksKind_ScreenRecordingSuccess, + ]; + + kasAllowNullAttributes = [ 'idTestSet', ]; + + kcDbColumns = 8 + + def __init__(self): + ModelDataBase.__init__(self) + self.idTestResultFile = None; + self.idTestResult = None; + self.idTestSet = None; + self.tsCreated = None; + self.idStrFile = None; + self.idStrDescription = None; + self.idStrKind = None; + self.idStrMime = None; + + def initFromDbRow(self, aoRow): + """ + Reinitialize from a SELECT * FROM TestResultFiles. + Return self. Raises exception if no row. + """ + if aoRow is None: + raise TMRowNotFound('Test result file record not found.') + + self.idTestResultFile = aoRow[0]; + self.idTestResult = aoRow[1]; + self.idTestSet = aoRow[2]; + self.tsCreated = aoRow[3]; + self.idStrFile = aoRow[4]; + self.idStrDescription = aoRow[5]; + self.idStrKind = aoRow[6]; + self.idStrMime = aoRow[7]; + return self; + +class TestResultFileDataEx(TestResultFileData): + """ + Extends TestResultFile by resolving the strings. + """ + + def __init__(self): + TestResultFileData.__init__(self) + self.sFile = None; + self.sDescription = None; + self.sKind = None; + self.sMime = None; + + def initFromDbRow(self, aoRow): + """ + Reinitialize from a query like this: + SELECT TestResultFiles.*, + StrTabFile.sValue AS sFile, + StrTabDesc.sValue AS sDescription + StrTabKind.sValue AS sKind, + StrTabMime.sValue AS sMime, + FROM ... + + Return self. Raises exception if no row. + """ + TestResultFileData.initFromDbRow(self, aoRow); + self.sFile = aoRow[self.kcDbColumns]; + self.sDescription = aoRow[self.kcDbColumns + 1]; + self.sKind = aoRow[self.kcDbColumns + 2]; + self.sMime = aoRow[self.kcDbColumns + 3]; + return self; + + def initFakeMainLog(self, oTestSet): + """ + Reinitializes to represent the main.log object (not in DB). + + Returns self. + """ + self.idTestResultFile = 0; + self.idTestResult = oTestSet.idTestResult; + self.tsCreated = oTestSet.tsCreated; + self.idStrFile = None; + self.idStrDescription = None; + self.idStrKind = None; + self.idStrMime = None; + + self.sFile = 'main.log'; + self.sDescription = ''; + self.sKind = 'log/main'; + self.sMime = 'text/plain'; + return self; + + def isProbablyUtf8Encoded(self): + """ + Checks if the file is likely to be UTF-8 encoded. + """ + if self.sMime in [ 'text/plain', 'text/html' ]: + return True; + return False; + + def getMimeWithEncoding(self): + """ + Gets the MIME type with encoding if likely to be UTF-8. + """ + if self.isProbablyUtf8Encoded(): + return '%s; charset=utf-8' % (self.sMime,); + return self.sMime; + + + +class TestResultListingData(ModelDataBase): # pylint: disable=too-many-instance-attributes + """ + Test case result data representation for table listing + """ + + class FailureReasonListingData(object): + """ Failure reason listing data """ + def __init__(self): + self.oFailureReason = None; + self.oFailureReasonAssigner = None; + self.tsFailureReasonAssigned = None; + self.sFailureReasonComment = None; + + def __init__(self): + """Initialize""" + ModelDataBase.__init__(self) + + self.idTestSet = None + + self.idBuildCategory = None; + self.sProduct = None + self.sRepository = None; + self.sBranch = None + self.sType = None + self.idBuild = None; + self.sVersion = None; + self.iRevision = None + + self.sOs = None; + self.sOsVersion = None; + self.sArch = None; + self.sCpuVendor = None; + self.sCpuName = None; + self.cCpus = None; + self.fCpuHwVirt = None; + self.fCpuNestedPaging = None; + self.fCpu64BitGuest = None; + self.idTestBox = None + self.sTestBoxName = None + + self.tsCreated = None + self.tsElapsed = None + self.enmStatus = None + self.cErrors = None; + + self.idTestCase = None + self.sTestCaseName = None + self.sBaseCmd = None + self.sArgs = None + self.sSubName = None; + + self.idBuildTestSuite = None; + self.iRevisionTestSuite = None; + + self.aoFailureReasons = []; + + def initFromDbRowEx(self, aoRow, oFailureReasonLogic, oUserAccountLogic): + """ + Reinitialize from a database query. + Return self. Raises exception if no row. + """ + if aoRow is None: + raise TMRowNotFound('Test result record not found.') + + self.idTestSet = aoRow[0]; + + self.idBuildCategory = aoRow[1]; + self.sProduct = aoRow[2]; + self.sRepository = aoRow[3]; + self.sBranch = aoRow[4]; + self.sType = aoRow[5]; + self.idBuild = aoRow[6]; + self.sVersion = aoRow[7]; + self.iRevision = aoRow[8]; + + self.sOs = aoRow[9]; + self.sOsVersion = aoRow[10]; + self.sArch = aoRow[11]; + self.sCpuVendor = aoRow[12]; + self.sCpuName = aoRow[13]; + self.cCpus = aoRow[14]; + self.fCpuHwVirt = aoRow[15]; + self.fCpuNestedPaging = aoRow[16]; + self.fCpu64BitGuest = aoRow[17]; + self.idTestBox = aoRow[18]; + self.sTestBoxName = aoRow[19]; + + self.tsCreated = aoRow[20]; + self.tsElapsed = aoRow[21]; + self.enmStatus = aoRow[22]; + self.cErrors = aoRow[23]; + + self.idTestCase = aoRow[24]; + self.sTestCaseName = aoRow[25]; + self.sBaseCmd = aoRow[26]; + self.sArgs = aoRow[27]; + self.sSubName = aoRow[28]; + + self.idBuildTestSuite = aoRow[29]; + self.iRevisionTestSuite = aoRow[30]; + + self.aoFailureReasons = []; + for i, _ in enumerate(aoRow[31]): + if aoRow[31][i] is not None \ + or aoRow[32][i] is not None \ + or aoRow[33][i] is not None \ + or aoRow[34][i] is not None: + oReason = self.FailureReasonListingData(); + if aoRow[31][i] is not None: + oReason.oFailureReason = oFailureReasonLogic.cachedLookup(aoRow[31][i]); + if aoRow[32][i] is not None: + oReason.oFailureReasonAssigner = oUserAccountLogic.cachedLookup(aoRow[32][i]); + oReason.tsFailureReasonAssigned = aoRow[33][i]; + oReason.sFailureReasonComment = aoRow[34][i]; + self.aoFailureReasons.append(oReason); + + return self + + +class TestResultHangingOffence(TMExceptionBase): + """Hanging offence committed by test case.""" + pass; # pylint: disable=unnecessary-pass + + +class TestResultFilter(ModelFilterBase): + """ + Test result filter. + """ + + kiTestStatus = 0; + kiErrorCounts = 1; + kiBranches = 2; + kiBuildTypes = 3; + kiRevisions = 4; + kiRevisionRange = 5; + kiFailReasons = 6; + kiTestCases = 7; + kiTestCaseMisc = 8; + kiTestBoxes = 9; + kiOses = 10; + kiCpuArches = 11; + kiCpuVendors = 12; + kiCpuCounts = 13; + kiMemory = 14; + kiTestboxMisc = 15; + kiPythonVersions = 16; + kiSchedGroups = 17; + + ## Misc test case / variation name filters. + ## Presented in table order. The first sub element is the presistent ID. + kaTcMisc = ( + ( 1, 'x86', ), + ( 2, 'amd64', ), + ( 3, 'uni', ), + ( 4, 'smp', ), + ( 5, 'raw', ), + ( 6, 'hw', ), + ( 7, 'np', ), + ( 8, 'Install', ), + ( 20, 'UInstall', ), # NB. out of order. + ( 9, 'Benchmark', ), + ( 18, 'smoke', ), # NB. out of order. + ( 19, 'unit', ), # NB. out of order. + ( 10, 'USB', ), + ( 11, 'Debian', ), + ( 12, 'Fedora', ), + ( 13, 'Oracle', ), + ( 14, 'RHEL', ), + ( 15, 'SUSE', ), + ( 16, 'Ubuntu', ), + ( 17, 'Win', ), + ); + + kiTbMisc_NestedPaging = 0; + kiTbMisc_NoNestedPaging = 1; + kiTbMisc_RawMode = 2; + kiTbMisc_NoRawMode = 3; + kiTbMisc_64BitGuest = 4; + kiTbMisc_No64BitGuest = 5; + kiTbMisc_HwVirt = 6; + kiTbMisc_NoHwVirt = 7; + kiTbMisc_IoMmu = 8; + kiTbMisc_NoIoMmu = 9; + + def __init__(self): + ModelFilterBase.__init__(self); + + # Test statuses + oCrit = FilterCriterion('Test statuses', sVarNm = 'ts', sType = FilterCriterion.ksType_String, + sTable = 'TestSets', sColumn = 'enmStatus'); + self.aCriteria.append(oCrit); + assert self.aCriteria[self.kiTestStatus] is oCrit; + + # Error counts + oCrit = FilterCriterion('Error counts', sVarNm = 'ec', sTable = 'TestResults', sColumn = 'cErrors'); + self.aCriteria.append(oCrit); + assert self.aCriteria[self.kiErrorCounts] is oCrit; + + # Branches + oCrit = FilterCriterion('Branches', sVarNm = 'br', sType = FilterCriterion.ksType_String, + sTable = 'BuildCategories', sColumn = 'sBranch'); + self.aCriteria.append(oCrit); + assert self.aCriteria[self.kiBranches] is oCrit; + + # Build types + oCrit = FilterCriterion('Build types', sVarNm = 'bt', sType = FilterCriterion.ksType_String, + sTable = 'BuildCategories', sColumn = 'sType'); + self.aCriteria.append(oCrit); + assert self.aCriteria[self.kiBuildTypes] is oCrit; + + # Revisions + oCrit = FilterCriterion('Revisions', sVarNm = 'rv', sTable = 'Builds', sColumn = 'iRevision'); + self.aCriteria.append(oCrit); + assert self.aCriteria[self.kiRevisions] is oCrit; + + # Revision Range + oCrit = FilterCriterion('Revision Range', sVarNm = 'rr', sType = FilterCriterion.ksType_Ranges, + sKind = FilterCriterion.ksKind_ElementOfOrNot, sTable = 'Builds', sColumn = 'iRevision'); + self.aCriteria.append(oCrit); + assert self.aCriteria[self.kiRevisionRange] is oCrit; + + # Failure reasons + oCrit = FilterCriterion('Failure reasons', sVarNm = 'fr', sType = FilterCriterion.ksType_UIntNil, + sTable = 'TestResultFailures', sColumn = 'idFailureReason'); + self.aCriteria.append(oCrit); + assert self.aCriteria[self.kiFailReasons] is oCrit; + + # Test cases and variations. + oCrit = FilterCriterion('Test case / var', sVarNm = 'tc', sTable = 'TestSets', sColumn = 'idTestCase', + oSub = FilterCriterion('Test variations', sVarNm = 'tv', + sTable = 'TestSets', sColumn = 'idTestCaseArgs')); + self.aCriteria.append(oCrit); + assert self.aCriteria[self.kiTestCases] is oCrit; + + # Special test case and varation name sub string matching. + oCrit = FilterCriterion('Test case name', sVarNm = 'cm', sKind = FilterCriterion.ksKind_Special, + asTables = ('TestCases', 'TestCaseArgs')); + oCrit.aoPossible = [ + FilterCriterionValueAndDescription(aoCur[0], 'Include %s' % (aoCur[1],)) for aoCur in self.kaTcMisc + ]; + oCrit.aoPossible.extend([ + FilterCriterionValueAndDescription(aoCur[0] + 32, 'Exclude %s' % (aoCur[1],)) for aoCur in self.kaTcMisc + ]); + self.aCriteria.append(oCrit); + assert self.aCriteria[self.kiTestCaseMisc] is oCrit; + + # Testboxes + oCrit = FilterCriterion('Testboxes', sVarNm = 'tb', sTable = 'TestSets', sColumn = 'idTestBox'); + self.aCriteria.append(oCrit); + assert self.aCriteria[self.kiTestBoxes] is oCrit; + + # Testbox OS and OS version. + oCrit = FilterCriterion('OS / version', sVarNm = 'os', sTable = 'TestBoxesWithStrings', sColumn = 'idStrOs', + oSub = FilterCriterion('OS Versions', sVarNm = 'ov', + sTable = 'TestBoxesWithStrings', sColumn = 'idStrOsVersion')); + self.aCriteria.append(oCrit); + assert self.aCriteria[self.kiOses] is oCrit; + + # Testbox CPU architectures. + oCrit = FilterCriterion('CPU arches', sVarNm = 'ca', sTable = 'TestBoxesWithStrings', sColumn = 'idStrCpuArch'); + self.aCriteria.append(oCrit); + assert self.aCriteria[self.kiCpuArches] is oCrit; + + # Testbox CPU vendors and revisions. + oCrit = FilterCriterion('CPU vendor / rev', sVarNm = 'cv', sTable = 'TestBoxesWithStrings', sColumn = 'idStrCpuVendor', + oSub = FilterCriterion('CPU revisions', sVarNm = 'cr', + sTable = 'TestBoxesWithStrings', sColumn = 'lCpuRevision')); + self.aCriteria.append(oCrit); + assert self.aCriteria[self.kiCpuVendors] is oCrit; + + # Testbox CPU (thread) count + oCrit = FilterCriterion('CPU counts', sVarNm = 'cc', sTable = 'TestBoxesWithStrings', sColumn = 'cCpus'); + self.aCriteria.append(oCrit); + assert self.aCriteria[self.kiCpuCounts] is oCrit; + + # Testbox memory sizes. + oCrit = FilterCriterion('Memory', sVarNm = 'mb', sTable = 'TestBoxesWithStrings', sColumn = 'cMbMemory'); + self.aCriteria.append(oCrit); + assert self.aCriteria[self.kiMemory] is oCrit; + + # Testbox features. + oCrit = FilterCriterion('Testbox features', sVarNm = 'tm', sKind = FilterCriterion.ksKind_Special, + sTable = 'TestBoxesWithStrings'); + oCrit.aoPossible = [ + FilterCriterionValueAndDescription(self.kiTbMisc_NestedPaging, "req nested paging"), + FilterCriterionValueAndDescription(self.kiTbMisc_NoNestedPaging, "w/o nested paging"), + #FilterCriterionValueAndDescription(self.kiTbMisc_RawMode, "req raw-mode"), - not implemented yet. + #FilterCriterionValueAndDescription(self.kiTbMisc_NoRawMode, "w/o raw-mode"), - not implemented yet. + FilterCriterionValueAndDescription(self.kiTbMisc_64BitGuest, "req 64-bit guests"), + FilterCriterionValueAndDescription(self.kiTbMisc_No64BitGuest, "w/o 64-bit guests"), + FilterCriterionValueAndDescription(self.kiTbMisc_HwVirt, "req VT-x / AMD-V"), + FilterCriterionValueAndDescription(self.kiTbMisc_NoHwVirt, "w/o VT-x / AMD-V"), + #FilterCriterionValueAndDescription(self.kiTbMisc_IoMmu, "req I/O MMU"), - not implemented yet. + #FilterCriterionValueAndDescription(self.kiTbMisc_NoIoMmu, "w/o I/O MMU"), - not implemented yet. + ]; + self.aCriteria.append(oCrit); + assert self.aCriteria[self.kiTestboxMisc] is oCrit; + + # Testbox python versions. + oCrit = FilterCriterion('Python', sVarNm = 'py', sTable = 'TestBoxesWithStrings', sColumn = 'iPythonHexVersion'); + self.aCriteria.append(oCrit); + assert self.aCriteria[self.kiPythonVersions] is oCrit; + + # Scheduling groups. + oCrit = FilterCriterion('Sched groups', sVarNm = 'sg', sTable = 'TestSets', sColumn = 'idSchedGroup'); + self.aCriteria.append(oCrit); + assert self.aCriteria[self.kiSchedGroups] is oCrit; + + + kdTbMiscConditions = { + kiTbMisc_NestedPaging: 'TestBoxesWithStrings.fCpuNestedPaging IS TRUE', + kiTbMisc_NoNestedPaging: 'TestBoxesWithStrings.fCpuNestedPaging IS FALSE', + kiTbMisc_RawMode: 'TestBoxesWithStrings.fRawMode IS TRUE', + kiTbMisc_NoRawMode: 'TestBoxesWithStrings.fRawMode IS NOT TRUE', + kiTbMisc_64BitGuest: 'TestBoxesWithStrings.fCpu64BitGuest IS TRUE', + kiTbMisc_No64BitGuest: 'TestBoxesWithStrings.fCpu64BitGuest IS FALSE', + kiTbMisc_HwVirt: 'TestBoxesWithStrings.fCpuHwVirt IS TRUE', + kiTbMisc_NoHwVirt: 'TestBoxesWithStrings.fCpuHwVirt IS FALSE', + kiTbMisc_IoMmu: 'TestBoxesWithStrings.fChipsetIoMmu IS TRUE', + kiTbMisc_NoIoMmu: 'TestBoxesWithStrings.fChipsetIoMmu IS FALSE', + }; + + def _getWhereWorker(self, iCrit, oCrit, sExtraIndent, iOmit): + """ Formats one - main or sub. """ + sQuery = ''; + if oCrit.sState == FilterCriterion.ksState_Selected and iCrit != iOmit: + if iCrit == self.kiTestCaseMisc: + for iValue, sLike in self.kaTcMisc: + if iValue in oCrit.aoSelected: sNot = ''; + elif iValue + 32 in oCrit.aoSelected: sNot = 'NOT '; + else: continue; + sQuery += '%s AND %s (' % (sExtraIndent, sNot,); + if len(sLike) <= 3: # do word matching for small substrings (hw, np, smp, uni, ++). + sQuery += 'TestCases.sName ~ \'.*\\y%s\\y.*\' ' \ + 'OR COALESCE(TestCaseArgs.sSubName, \'\') ~ \'.*\\y%s\\y.*\')\n' \ + % ( sLike, sLike,); + else: + sQuery += 'TestCases.sName LIKE \'%%%s%%\' ' \ + 'OR COALESCE(TestCaseArgs.sSubName, \'\') LIKE \'%%%s%%\')\n' \ + % ( sLike, sLike,); + elif iCrit == self.kiTestboxMisc: + dConditions = self.kdTbMiscConditions; + for iValue in oCrit.aoSelected: + if iValue in dConditions: + sQuery += '%s AND %s\n' % (sExtraIndent, dConditions[iValue],); + elif oCrit.sType == FilterCriterion.ksType_Ranges: + assert not oCrit.aoPossible; + if oCrit.aoSelected: + asConditions = []; + for tRange in oCrit.aoSelected: + if tRange[0] == tRange[1]: + asConditions.append('%s.%s = %s' % (oCrit.asTables[0], oCrit.sColumn, tRange[0])); + elif tRange[1] is None: # 9999- + asConditions.append('%s.%s >= %s' % (oCrit.asTables[0], oCrit.sColumn, tRange[0])); + elif tRange[0] is None: # -9999 + asConditions.append('%s.%s <= %s' % (oCrit.asTables[0], oCrit.sColumn, tRange[1])); + else: + asConditions.append('%s.%s BETWEEN %s AND %s' % (oCrit.asTables[0], oCrit.sColumn, + tRange[0], tRange[1])); + if not oCrit.fInverted: + sQuery += '%s AND (%s)\n' % (sExtraIndent, ' OR '.join(asConditions)); + else: + sQuery += '%s AND NOT (%s)\n' % (sExtraIndent, ' OR '.join(asConditions)); + else: + assert len(oCrit.asTables) == 1; + sQuery += '%s AND (' % (sExtraIndent,); + + if oCrit.sType != FilterCriterion.ksType_UIntNil or max(oCrit.aoSelected) != -1: + if iCrit == self.kiMemory: + sQuery += '(%s.%s / 1024)' % (oCrit.asTables[0], oCrit.sColumn,); + else: + sQuery += '%s.%s' % (oCrit.asTables[0], oCrit.sColumn,); + if not oCrit.fInverted: + sQuery += ' IN ('; + else: + sQuery += ' NOT IN ('; + if oCrit.sType == FilterCriterion.ksType_String: + sQuery += ', '.join('\'%s\'' % (sValue,) for sValue in oCrit.aoSelected) + ')'; + else: + sQuery += ', '.join(str(iValue) for iValue in oCrit.aoSelected if iValue != -1) + ')'; + + if oCrit.sType == FilterCriterion.ksType_UIntNil \ + and -1 in oCrit.aoSelected: + if sQuery[-1] != '(': sQuery += ' OR '; + sQuery += '%s.%s IS NULL' % (oCrit.asTables[0], oCrit.sColumn,); + + if iCrit == self.kiFailReasons: + if oCrit.fInverted: + sQuery += '%s OR TestResultFailures.idFailureReason IS NULL\n' % (sExtraIndent,); + else: + sQuery += '%s AND TestSets.enmStatus >= \'failure\'::TestStatus_T\n' % (sExtraIndent,); + sQuery += ')\n'; + if oCrit.oSub is not None: + sQuery += self._getWhereWorker(iCrit | (((iCrit >> 8) + 1) << 8), oCrit.oSub, sExtraIndent, iOmit); + return sQuery; + + def getWhereConditions(self, sExtraIndent = '', iOmit = -1): + """ + Construct the WHERE conditions for the filter, optionally omitting one + criterion. + """ + sQuery = ''; + for iCrit, oCrit in enumerate(self.aCriteria): + sQuery += self._getWhereWorker(iCrit, oCrit, sExtraIndent, iOmit); + return sQuery; + + def getTableJoins(self, sExtraIndent = '', iOmit = -1, dOmitTables = None): + """ + Construct the WHERE conditions for the filter, optionally omitting one + criterion. + """ + afDone = { 'TestSets': True, }; + if dOmitTables is not None: + afDone.update(dOmitTables); + + sQuery = ''; + for iCrit, oCrit in enumerate(self.aCriteria): + if oCrit.sState == FilterCriterion.ksState_Selected \ + and iCrit != iOmit: + for sTable in oCrit.asTables: + if sTable not in afDone: + afDone[sTable] = True; + if sTable == 'Builds': + sQuery += '%sINNER JOIN Builds\n' \ + '%s ON Builds.idBuild = TestSets.idBuild\n' \ + '%s AND Builds.tsExpire > TestSets.tsCreated\n' \ + '%s AND Builds.tsEffective <= TestSets.tsCreated\n' \ + % ( sExtraIndent, sExtraIndent, sExtraIndent, sExtraIndent, ); + elif sTable == 'BuildCategories': + sQuery += '%sINNER JOIN BuildCategories\n' \ + '%s ON BuildCategories.idBuildCategory = TestSets.idBuildCategory\n' \ + % ( sExtraIndent, sExtraIndent, ); + elif sTable == 'TestBoxesWithStrings': + sQuery += '%sLEFT OUTER JOIN TestBoxesWithStrings\n' \ + '%s ON TestBoxesWithStrings.idGenTestBox = TestSets.idGenTestBox\n' \ + % ( sExtraIndent, sExtraIndent, ); + elif sTable == 'TestCases': + sQuery += '%sINNER JOIN TestCases\n' \ + '%s ON TestCases.idGenTestCase = TestSets.idGenTestCase\n' \ + % ( sExtraIndent, sExtraIndent, ); + elif sTable == 'TestCaseArgs': + sQuery += '%sINNER JOIN TestCaseArgs\n' \ + '%s ON TestCaseArgs.idGenTestCaseArgs = TestSets.idGenTestCaseArgs\n' \ + % ( sExtraIndent, sExtraIndent, ); + elif sTable == 'TestResults': + sQuery += '%sINNER JOIN TestResults\n' \ + '%s ON TestResults.idTestResult = TestSets.idTestResult\n' \ + % ( sExtraIndent, sExtraIndent, ); + elif sTable == 'TestResultFailures': + sQuery += '%sLEFT OUTER JOIN TestResultFailures\n' \ + '%s ON TestResultFailures.idTestSet = TestSets.idTestSet\n' \ + '%s AND TestResultFailures.tsExpire = \'infinity\'::TIMESTAMP\n' \ + % ( sExtraIndent, sExtraIndent, sExtraIndent, ); + else: + assert False, sTable; + return sQuery; + + def isJoiningWithTable(self, sTable): + """ Checks whether getTableJoins already joins with TestResultFailures. """ + for oCrit in self.aCriteria: + if oCrit.sState == FilterCriterion.ksState_Selected and sTable in oCrit.asTables: + return True; + return False + + + +class TestResultLogic(ModelLogicBase): # pylint: disable=too-few-public-methods + """ + Results grouped by scheduling group. + """ + + # + # Result grinding for displaying in the WUI. + # + + ksResultsGroupingTypeNone = 'ResultsGroupingTypeNone'; + ksResultsGroupingTypeTestGroup = 'ResultsGroupingTypeTestGroup'; + ksResultsGroupingTypeBuildCat = 'ResultsGroupingTypeBuildCat'; + ksResultsGroupingTypeBuildRev = 'ResultsGroupingTypeBuildRev'; + ksResultsGroupingTypeTestBox = 'ResultsGroupingTypeTestBox'; + ksResultsGroupingTypeTestCase = 'ResultsGroupingTypeTestCase'; + ksResultsGroupingTypeOS = 'ResultsGroupingTypeOS'; + ksResultsGroupingTypeArch = 'ResultsGroupingTypeArch'; + ksResultsGroupingTypeSchedGroup = 'ResultsGroupingTypeSchedGroup'; + + ## @name Result sorting options. + ## @{ + ksResultsSortByRunningAndStart = 'ResultsSortByRunningAndStart'; ##< Default + ksResultsSortByBuildRevision = 'ResultsSortByBuildRevision'; + ksResultsSortByTestBoxName = 'ResultsSortByTestBoxName'; + ksResultsSortByTestBoxOs = 'ResultsSortByTestBoxOs'; + ksResultsSortByTestBoxOsVersion = 'ResultsSortByTestBoxOsVersion'; + ksResultsSortByTestBoxOsArch = 'ResultsSortByTestBoxOsArch'; + ksResultsSortByTestBoxArch = 'ResultsSortByTestBoxArch'; + ksResultsSortByTestBoxCpuVendor = 'ResultsSortByTestBoxCpuVendor'; + ksResultsSortByTestBoxCpuName = 'ResultsSortByTestBoxCpuName'; + ksResultsSortByTestBoxCpuRev = 'ResultsSortByTestBoxCpuRev'; + ksResultsSortByTestBoxCpuFeatures = 'ResultsSortByTestBoxCpuFeatures'; + ksResultsSortByTestCaseName = 'ResultsSortByTestCaseName'; + ksResultsSortByFailureReason = 'ResultsSortByFailureReason'; + kasResultsSortBy = { + ksResultsSortByRunningAndStart, + ksResultsSortByBuildRevision, + ksResultsSortByTestBoxName, + ksResultsSortByTestBoxOs, + ksResultsSortByTestBoxOsVersion, + ksResultsSortByTestBoxOsArch, + ksResultsSortByTestBoxArch, + ksResultsSortByTestBoxCpuVendor, + ksResultsSortByTestBoxCpuName, + ksResultsSortByTestBoxCpuRev, + ksResultsSortByTestBoxCpuFeatures, + ksResultsSortByTestCaseName, + ksResultsSortByFailureReason, + }; + ## Used by the WUI for generating the drop down. + kaasResultsSortByTitles = ( + ( ksResultsSortByRunningAndStart, 'Running & Start TS' ), + ( ksResultsSortByBuildRevision, 'Build Revision' ), + ( ksResultsSortByTestBoxName, 'TestBox Name' ), + ( ksResultsSortByTestBoxOs, 'O/S' ), + ( ksResultsSortByTestBoxOsVersion, 'O/S Version' ), + ( ksResultsSortByTestBoxOsArch, 'O/S & Architecture' ), + ( ksResultsSortByTestBoxArch, 'Architecture' ), + ( ksResultsSortByTestBoxCpuVendor, 'CPU Vendor' ), + ( ksResultsSortByTestBoxCpuName, 'CPU Vendor & Name' ), + ( ksResultsSortByTestBoxCpuRev, 'CPU Vendor & Revision' ), + ( ksResultsSortByTestBoxCpuFeatures, 'CPU Features' ), + ( ksResultsSortByTestCaseName, 'Test Case Name' ), + ( ksResultsSortByFailureReason, 'Failure Reason' ), + ); + ## @} + + ## Default sort by map. + kdResultSortByMap = { + ksResultsSortByRunningAndStart: ( (), None, None, '', '' ), + ksResultsSortByBuildRevision: ( + # Sorting tables. + ('Builds',), + # Sorting table join(s). + ' AND TestSets.idBuild = Builds.idBuild' + ' AND Builds.tsExpire >= TestSets.tsCreated' + ' AND Builds.tsEffective <= TestSets.tsCreated', + # Start of ORDER BY statement. + ' Builds.iRevision DESC', + # Extra columns to fetch for the above ORDER BY to work in a SELECT DISTINCT statement. + '', + # Columns for the GROUP BY + ''), + ksResultsSortByTestBoxName: ( + ('TestBoxes',), + ' AND TestSets.idGenTestBox = TestBoxes.idGenTestBox', + ' TestBoxes.sName DESC', + '', '' ), + ksResultsSortByTestBoxOsArch: ( + ('TestBoxesWithStrings',), + ' AND TestSets.idGenTestBox = TestBoxesWithStrings.idGenTestBox', + ' TestBoxesWithStrings.sOs, TestBoxesWithStrings.sCpuArch', + '', '' ), + ksResultsSortByTestBoxOs: ( + ('TestBoxesWithStrings',), + ' AND TestSets.idGenTestBox = TestBoxesWithStrings.idGenTestBox', + ' TestBoxesWithStrings.sOs', + '', '' ), + ksResultsSortByTestBoxOsVersion: ( + ('TestBoxesWithStrings',), + ' AND TestSets.idGenTestBox = TestBoxesWithStrings.idGenTestBox', + ' TestBoxesWithStrings.sOs, TestBoxesWithStrings.sOsVersion DESC', + '', '' ), + ksResultsSortByTestBoxArch: ( + ('TestBoxesWithStrings',), + ' AND TestSets.idGenTestBox = TestBoxesWithStrings.idGenTestBox', + ' TestBoxesWithStrings.sCpuArch', + '', '' ), + ksResultsSortByTestBoxCpuVendor: ( + ('TestBoxesWithStrings',), + ' AND TestSets.idGenTestBox = TestBoxesWithStrings.idGenTestBox', + ' TestBoxesWithStrings.sCpuVendor', + '', '' ), + ksResultsSortByTestBoxCpuName: ( + ('TestBoxesWithStrings',), + ' AND TestSets.idGenTestBox = TestBoxesWithStrings.idGenTestBox', + ' TestBoxesWithStrings.sCpuVendor, TestBoxesWithStrings.sCpuName', + '', '' ), + ksResultsSortByTestBoxCpuRev: ( + ('TestBoxesWithStrings',), + ' AND TestSets.idGenTestBox = TestBoxesWithStrings.idGenTestBox', + ' TestBoxesWithStrings.sCpuVendor, TestBoxesWithStrings.lCpuRevision DESC', + ', TestBoxesWithStrings.lCpuRevision', + ', TestBoxesWithStrings.lCpuRevision' ), + ksResultsSortByTestBoxCpuFeatures: ( + ('TestBoxes',), + ' AND TestSets.idGenTestBox = TestBoxes.idGenTestBox', + ' TestBoxes.fCpuHwVirt DESC, TestBoxes.fCpuNestedPaging DESC, TestBoxes.fCpu64BitGuest DESC, TestBoxes.cCpus DESC', + '', + '' ), + ksResultsSortByTestCaseName: ( + ('TestCases',), + ' AND TestSets.idGenTestCase = TestCases.idGenTestCase', + ' TestCases.sName', + '', '' ), + ksResultsSortByFailureReason: ( + (), '', + 'asSortByFailureReason ASC', + ', array_agg(FailureReasons.sShort ORDER BY TestResultFailures.idTestResult) AS asSortByFailureReason', + '' ), + }; + + kdResultGroupingMap = { + ksResultsGroupingTypeNone: ( + # Grouping tables; + (), + # Grouping field; + None, + # Grouping where addition. + None, + # Sort by overrides. + {}, + ), + ksResultsGroupingTypeTestGroup: ('', 'TestSets.idTestGroup', None, {},), + ksResultsGroupingTypeTestBox: ('', 'TestSets.idTestBox', None, {},), + ksResultsGroupingTypeTestCase: ('', 'TestSets.idTestCase', None, {},), + ksResultsGroupingTypeOS: ( + ('TestBoxes',), + 'TestBoxes.idStrOs', + ' AND TestBoxes.idGenTestBox = TestSets.idGenTestBox', + {}, + ), + ksResultsGroupingTypeArch: ( + ('TestBoxes',), + 'TestBoxes.idStrCpuArch', + ' AND TestBoxes.idGenTestBox = TestSets.idGenTestBox', + {}, + ), + ksResultsGroupingTypeBuildCat: ('', 'TestSets.idBuildCategory', None, {},), + ksResultsGroupingTypeBuildRev: ( + ('Builds',), + 'Builds.iRevision', + ' AND Builds.idBuild = TestSets.idBuild' + ' AND Builds.tsExpire > TestSets.tsCreated' + ' AND Builds.tsEffective <= TestSets.tsCreated', + { ksResultsSortByBuildRevision: ( (), None, ' Builds.iRevision DESC' ), } + ), + ksResultsGroupingTypeSchedGroup: ( '', 'TestSets.idSchedGroup', None, {},), + }; + + + def __init__(self, oDb): + ModelLogicBase.__init__(self, oDb) + self.oFailureReasonLogic = None; + self.oUserAccountLogic = None; + + def _getTimePeriodQueryPart(self, tsNow, sInterval, sExtraIndent = ''): + """ + Get part of SQL query responsible for SELECT data within + specified period of time. + """ + assert sInterval is not None; # too many rows. + + cMonthsMourningPeriod = 2; # Stop reminding everyone about testboxes after 2 months. (May also speed up the query.) + if tsNow is None: + sRet = '(TestSets.tsDone IS NULL OR TestSets.tsDone >= (CURRENT_TIMESTAMP - \'%s\'::interval))\n' \ + '%s AND TestSets.tsCreated >= (CURRENT_TIMESTAMP - \'%s\'::interval - \'%u months\'::interval)\n' \ + % ( sInterval, + sExtraIndent, sInterval, cMonthsMourningPeriod); + else: + sTsNow = '\'%s\'::TIMESTAMP' % (tsNow,); # It's actually a string already. duh. + sRet = 'TestSets.tsCreated <= %s\n' \ + '%s AND TestSets.tsCreated >= (%s - \'%s\'::interval - \'%u months\'::interval)\n' \ + '%s AND (TestSets.tsDone IS NULL OR TestSets.tsDone >= (%s - \'%s\'::interval))\n' \ + % ( sTsNow, + sExtraIndent, sTsNow, sInterval, cMonthsMourningPeriod, + sExtraIndent, sTsNow, sInterval ); + return sRet + + def fetchResultsForListing(self, iStart, cMaxRows, tsNow, sInterval, oFilter, enmResultSortBy, # pylint: disable=too-many-arguments + enmResultsGroupingType, iResultsGroupingValue, fOnlyFailures, fOnlyNeedingReason): + """ + Fetches TestResults table content. + + If @param enmResultsGroupingType and @param iResultsGroupingValue + are not None, then resulting (returned) list contains only records + that match specified @param enmResultsGroupingType. + + If @param enmResultsGroupingType is None, then + @param iResultsGroupingValue is ignored. + + Returns an array (list) of TestResultData items, empty list if none. + Raises exception on error. + """ + + _ = oFilter; + + # + # Get SQL query parameters + # + if enmResultsGroupingType is None or enmResultsGroupingType not in self.kdResultGroupingMap: + raise TMExceptionBase('Unknown grouping type'); + if enmResultSortBy is None or enmResultSortBy not in self.kasResultsSortBy: + raise TMExceptionBase('Unknown sorting'); + asGroupingTables, sGroupingField, sGroupingCondition, dSortOverrides = self.kdResultGroupingMap[enmResultsGroupingType]; + if enmResultSortBy in dSortOverrides: + asSortTables, sSortWhere, sSortOrderBy, sSortColumns, sSortGroupBy = dSortOverrides[enmResultSortBy]; + else: + asSortTables, sSortWhere, sSortOrderBy, sSortColumns, sSortGroupBy = self.kdResultSortByMap[enmResultSortBy]; + + # + # Construct the query. + # + sQuery = 'SELECT DISTINCT TestSets.idTestSet,\n' \ + ' BuildCategories.idBuildCategory,\n' \ + ' BuildCategories.sProduct,\n' \ + ' BuildCategories.sRepository,\n' \ + ' BuildCategories.sBranch,\n' \ + ' BuildCategories.sType,\n' \ + ' Builds.idBuild,\n' \ + ' Builds.sVersion,\n' \ + ' Builds.iRevision,\n' \ + ' TestBoxesWithStrings.sOs,\n' \ + ' TestBoxesWithStrings.sOsVersion,\n' \ + ' TestBoxesWithStrings.sCpuArch,\n' \ + ' TestBoxesWithStrings.sCpuVendor,\n' \ + ' TestBoxesWithStrings.sCpuName,\n' \ + ' TestBoxesWithStrings.cCpus,\n' \ + ' TestBoxesWithStrings.fCpuHwVirt,\n' \ + ' TestBoxesWithStrings.fCpuNestedPaging,\n' \ + ' TestBoxesWithStrings.fCpu64BitGuest,\n' \ + ' TestBoxesWithStrings.idTestBox,\n' \ + ' TestBoxesWithStrings.sName,\n' \ + ' TestResults.tsCreated,\n' \ + ' COALESCE(TestResults.tsElapsed, CURRENT_TIMESTAMP - TestResults.tsCreated) AS tsElapsedTestResult,\n' \ + ' TestSets.enmStatus,\n' \ + ' TestResults.cErrors,\n' \ + ' TestCases.idTestCase,\n' \ + ' TestCases.sName,\n' \ + ' TestCases.sBaseCmd,\n' \ + ' TestCaseArgs.sArgs,\n' \ + ' TestCaseArgs.sSubName,\n' \ + ' TestSuiteBits.idBuild AS idBuildTestSuite,\n' \ + ' TestSuiteBits.iRevision AS iRevisionTestSuite,\n' \ + ' array_agg(TestResultFailures.idFailureReason ORDER BY TestResultFailures.idTestResult),\n' \ + ' array_agg(TestResultFailures.uidAuthor ORDER BY TestResultFailures.idTestResult),\n' \ + ' array_agg(TestResultFailures.tsEffective ORDER BY TestResultFailures.idTestResult),\n' \ + ' array_agg(TestResultFailures.sComment ORDER BY TestResultFailures.idTestResult),\n' \ + ' (TestSets.tsDone IS NULL) SortRunningFirst' + sSortColumns + '\n' \ + 'FROM ( SELECT TestSets.idTestSet AS idTestSet,\n' \ + ' TestSets.tsDone AS tsDone,\n' \ + ' TestSets.tsCreated AS tsCreated,\n' \ + ' TestSets.enmStatus AS enmStatus,\n' \ + ' TestSets.idBuild AS idBuild,\n' \ + ' TestSets.idBuildTestSuite AS idBuildTestSuite,\n' \ + ' TestSets.idGenTestBox AS idGenTestBox,\n' \ + ' TestSets.idGenTestCase AS idGenTestCase,\n' \ + ' TestSets.idGenTestCaseArgs AS idGenTestCaseArgs\n' \ + ' FROM TestSets\n'; + sQuery += oFilter.getTableJoins(' '); + if fOnlyNeedingReason and not oFilter.isJoiningWithTable('TestResultFailures'): + sQuery += '\n' \ + ' LEFT OUTER JOIN TestResultFailures\n' \ + ' ON TestSets.idTestSet = TestResultFailures.idTestSet\n' \ + ' AND TestResultFailures.tsExpire = \'infinity\'::TIMESTAMP'; + for asTables in [asGroupingTables, asSortTables]: + for sTable in asTables: + if not oFilter.isJoiningWithTable(sTable): + sQuery = sQuery[:-1] + ',\n ' + sTable + '\n'; + + sQuery += ' WHERE ' + self._getTimePeriodQueryPart(tsNow, sInterval, ' ') + \ + oFilter.getWhereConditions(' '); + if fOnlyFailures or fOnlyNeedingReason: + sQuery += ' AND TestSets.enmStatus != \'success\'::TestStatus_T\n' \ + ' AND TestSets.enmStatus != \'running\'::TestStatus_T\n'; + if fOnlyNeedingReason: + sQuery += ' AND TestResultFailures.idTestSet IS NULL\n'; + if sGroupingField is not None: + sQuery += ' AND %s = %d\n' % (sGroupingField, iResultsGroupingValue,); + if sGroupingCondition is not None: + sQuery += sGroupingCondition.replace(' AND ', ' AND '); + if sSortWhere is not None: + sQuery += sSortWhere.replace(' AND ', ' AND '); + sQuery += ' ORDER BY '; + if sSortOrderBy is not None and sSortOrderBy.find('FailureReason') < 0: + sQuery += sSortOrderBy + ',\n '; + sQuery += '(TestSets.tsDone IS NULL) DESC, TestSets.idTestSet DESC\n' \ + ' LIMIT %s OFFSET %s\n' % (cMaxRows, iStart,); + + # Note! INNER JOIN TestBoxesWithStrings performs miserable compared to LEFT OUTER JOIN. Doesn't matter for the result + # because TestSets.idGenTestBox is a foreign key and unique in TestBoxes. So, let's do what ever is faster. + sQuery += ' ) AS TestSets\n' \ + ' LEFT OUTER JOIN TestBoxesWithStrings\n' \ + ' ON TestSets.idGenTestBox = TestBoxesWithStrings.idGenTestBox' \ + ' LEFT OUTER JOIN Builds AS TestSuiteBits\n' \ + ' ON TestSuiteBits.idBuild = TestSets.idBuildTestSuite\n' \ + ' AND TestSuiteBits.tsExpire > TestSets.tsCreated\n' \ + ' AND TestSuiteBits.tsEffective <= TestSets.tsCreated\n' \ + ' LEFT OUTER JOIN TestResultFailures\n' \ + ' ON TestSets.idTestSet = TestResultFailures.idTestSet\n' \ + ' AND TestResultFailures.tsExpire = \'infinity\'::TIMESTAMP'; + if sSortOrderBy is not None and sSortOrderBy.find('FailureReason') >= 0: + sQuery += '\n' \ + ' LEFT OUTER JOIN FailureReasons\n' \ + ' ON TestResultFailures.idFailureReason = FailureReasons.idFailureReason\n' \ + ' AND FailureReasons.tsExpire = \'infinity\'::TIMESTAMP'; + sQuery += ',\n' \ + ' BuildCategories,\n' \ + ' Builds,\n' \ + ' TestResults,\n' \ + ' TestCases,\n' \ + ' TestCaseArgs\n'; + sQuery += 'WHERE TestSets.idTestSet = TestResults.idTestSet\n' \ + ' AND TestResults.idTestResultParent is NULL\n' \ + ' AND TestSets.idBuild = Builds.idBuild\n' \ + ' AND Builds.tsExpire > TestSets.tsCreated\n' \ + ' AND Builds.tsEffective <= TestSets.tsCreated\n' \ + ' AND Builds.idBuildCategory = BuildCategories.idBuildCategory\n' \ + ' AND TestSets.idGenTestCase = TestCases.idGenTestCase\n' \ + ' AND TestSets.idGenTestCaseArgs = TestCaseArgs.idGenTestCaseArgs\n'; + sQuery += 'GROUP BY TestSets.idTestSet,\n' \ + ' BuildCategories.idBuildCategory,\n' \ + ' BuildCategories.sProduct,\n' \ + ' BuildCategories.sRepository,\n' \ + ' BuildCategories.sBranch,\n' \ + ' BuildCategories.sType,\n' \ + ' Builds.idBuild,\n' \ + ' Builds.sVersion,\n' \ + ' Builds.iRevision,\n' \ + ' TestBoxesWithStrings.sOs,\n' \ + ' TestBoxesWithStrings.sOsVersion,\n' \ + ' TestBoxesWithStrings.sCpuArch,\n' \ + ' TestBoxesWithStrings.sCpuVendor,\n' \ + ' TestBoxesWithStrings.sCpuName,\n' \ + ' TestBoxesWithStrings.cCpus,\n' \ + ' TestBoxesWithStrings.fCpuHwVirt,\n' \ + ' TestBoxesWithStrings.fCpuNestedPaging,\n' \ + ' TestBoxesWithStrings.fCpu64BitGuest,\n' \ + ' TestBoxesWithStrings.idTestBox,\n' \ + ' TestBoxesWithStrings.sName,\n' \ + ' TestResults.tsCreated,\n' \ + ' tsElapsedTestResult,\n' \ + ' TestSets.enmStatus,\n' \ + ' TestResults.cErrors,\n' \ + ' TestCases.idTestCase,\n' \ + ' TestCases.sName,\n' \ + ' TestCases.sBaseCmd,\n' \ + ' TestCaseArgs.sArgs,\n' \ + ' TestCaseArgs.sSubName,\n' \ + ' TestSuiteBits.idBuild,\n' \ + ' TestSuiteBits.iRevision,\n' \ + ' SortRunningFirst' + sSortGroupBy + '\n'; + sQuery += 'ORDER BY '; + if sSortOrderBy is not None: + sQuery += sSortOrderBy.replace('TestBoxes.', 'TestBoxesWithStrings.') + ',\n '; + sQuery += '(TestSets.tsDone IS NULL) DESC, TestSets.idTestSet DESC\n'; + + # + # Execute the query and return the wrapped results. + # + self._oDb.execute(sQuery); + + if self.oFailureReasonLogic is None: + self.oFailureReasonLogic = FailureReasonLogic(self._oDb); + if self.oUserAccountLogic is None: + self.oUserAccountLogic = UserAccountLogic(self._oDb); + + aoRows = []; + for aoRow in self._oDb.fetchAll(): + aoRows.append(TestResultListingData().initFromDbRowEx(aoRow, self.oFailureReasonLogic, self.oUserAccountLogic)); + + return aoRows + + + def fetchTimestampsForLogViewer(self, idTestSet): + """ + Returns an ordered list with all the test result timestamps, both start + and end. + + The log viewer create anchors in the log text so we can jump directly to + the log lines relevant for a test event. + """ + self._oDb.execute('(\n' + 'SELECT tsCreated\n' + 'FROM TestResults\n' + 'WHERE idTestSet = %s\n' + ') UNION (\n' + 'SELECT tsCreated + tsElapsed\n' + 'FROM TestResults\n' + 'WHERE idTestSet = %s\n' + ' AND tsElapsed IS NOT NULL\n' + ') UNION (\n' + 'SELECT TestResultFiles.tsCreated\n' + 'FROM TestResultFiles\n' + 'WHERE idTestSet = %s\n' + ') UNION (\n' + 'SELECT tsCreated\n' + 'FROM TestResultValues\n' + 'WHERE idTestSet = %s\n' + ') UNION (\n' + 'SELECT TestResultMsgs.tsCreated\n' + 'FROM TestResultMsgs\n' + 'WHERE idTestSet = %s\n' + ') ORDER by 1' + , ( idTestSet, idTestSet, idTestSet, idTestSet, idTestSet, )); + return [aoRow[0] for aoRow in self._oDb.fetchAll()]; + + + def getEntriesCount(self, tsNow, sInterval, oFilter, enmResultsGroupingType, iResultsGroupingValue, + fOnlyFailures, fOnlyNeedingReason): + """ + Get number of table records. + + If @param enmResultsGroupingType and @param iResultsGroupingValue + are not None, then we count only only those records + that match specified @param enmResultsGroupingType. + + If @param enmResultsGroupingType is None, then + @param iResultsGroupingValue is ignored. + """ + _ = oFilter; + + # + # Get SQL query parameters + # + if enmResultsGroupingType is None: + raise TMExceptionBase('Unknown grouping type') + + if enmResultsGroupingType not in self.kdResultGroupingMap: + raise TMExceptionBase('Unknown grouping type') + asGroupingTables, sGroupingField, sGroupingCondition, _ = self.kdResultGroupingMap[enmResultsGroupingType]; + + # + # Construct the query. + # + sQuery = 'SELECT COUNT(TestSets.idTestSet)\n' \ + 'FROM TestSets\n'; + sQuery += oFilter.getTableJoins(); + if fOnlyNeedingReason and not oFilter.isJoiningWithTable('TestResultFailures'): + sQuery += ' LEFT OUTER JOIN TestResultFailures\n' \ + ' ON TestSets.idTestSet = TestResultFailures.idTestSet\n' \ + ' AND TestResultFailures.tsExpire = \'infinity\'::TIMESTAMP\n'; + for sTable in asGroupingTables: + if not oFilter.isJoiningWithTable(sTable): + sQuery = sQuery[:-1] + ',\n ' + sTable + '\n'; + sQuery += 'WHERE ' + self._getTimePeriodQueryPart(tsNow, sInterval) + \ + oFilter.getWhereConditions(); + if fOnlyFailures or fOnlyNeedingReason: + sQuery += ' AND TestSets.enmStatus != \'success\'::TestStatus_T\n' \ + ' AND TestSets.enmStatus != \'running\'::TestStatus_T\n'; + if fOnlyNeedingReason: + sQuery += ' AND TestResultFailures.idTestSet IS NULL\n'; + if sGroupingField is not None: + sQuery += ' AND %s = %d\n' % (sGroupingField, iResultsGroupingValue,); + if sGroupingCondition is not None: + sQuery += sGroupingCondition.replace(' AND ', ' AND '); + + # + # Execute the query and return the result. + # + self._oDb.execute(sQuery) + return self._oDb.fetchOne()[0] + + def getTestGroups(self, tsNow, sPeriod): + """ + Get list of uniq TestGroupData objects which + found in all test results. + """ + + self._oDb.execute('SELECT DISTINCT TestGroups.*\n' + 'FROM TestGroups, TestSets\n' + 'WHERE TestSets.idTestGroup = TestGroups.idTestGroup\n' + ' AND TestGroups.tsExpire > TestSets.tsCreated\n' + ' AND TestGroups.tsEffective <= TestSets.tsCreated' + ' AND ' + self._getTimePeriodQueryPart(tsNow, sPeriod)) + aaoRows = self._oDb.fetchAll() + aoRet = [] + for aoRow in aaoRows: + aoRet.append(TestGroupData().initFromDbRow(aoRow)) + return aoRet + + def getBuilds(self, tsNow, sPeriod): + """ + Get list of uniq BuildDataEx objects which + found in all test results. + """ + + self._oDb.execute('SELECT DISTINCT Builds.*, BuildCategories.*\n' + 'FROM Builds, BuildCategories, TestSets\n' + 'WHERE TestSets.idBuild = Builds.idBuild\n' + ' AND Builds.idBuildCategory = BuildCategories.idBuildCategory\n' + ' AND Builds.tsExpire > TestSets.tsCreated\n' + ' AND Builds.tsEffective <= TestSets.tsCreated' + ' AND ' + self._getTimePeriodQueryPart(tsNow, sPeriod)) + aaoRows = self._oDb.fetchAll() + aoRet = [] + for aoRow in aaoRows: + aoRet.append(BuildDataEx().initFromDbRow(aoRow)) + return aoRet + + def getTestBoxes(self, tsNow, sPeriod): + """ + Get list of uniq TestBoxData objects which + found in all test results. + """ + # Note! INNER JOIN TestBoxesWithStrings performs miserable compared to LEFT OUTER JOIN. Doesn't matter for the result + # because TestSets.idGenTestBox is a foreign key and unique in TestBoxes. So, let's do what ever is faster. + self._oDb.execute('SELECT TestBoxesWithStrings.*\n' + 'FROM ( SELECT idTestBox AS idTestBox,\n' + ' MAX(idGenTestBox) AS idGenTestBox\n' + ' FROM TestSets\n' + ' WHERE ' + self._getTimePeriodQueryPart(tsNow, sPeriod, ' ') + + ' GROUP BY idTestBox\n' + ' ) AS TestBoxIDs\n' + ' LEFT OUTER JOIN TestBoxesWithStrings\n' + ' ON TestBoxesWithStrings.idGenTestBox = TestBoxIDs.idGenTestBox\n' + 'ORDER BY TestBoxesWithStrings.sName\n' ); + aoRet = [] + for aoRow in self._oDb.fetchAll(): + aoRet.append(TestBoxData().initFromDbRow(aoRow)); + return aoRet + + def getTestCases(self, tsNow, sPeriod): + """ + Get a list of unique TestCaseData objects which is appears in the test + specified result period. + """ + + # Using LEFT OUTER JOIN instead of INNER JOIN in case it performs better, doesn't matter for the result. + self._oDb.execute('SELECT TestCases.*\n' + 'FROM ( SELECT idTestCase AS idTestCase,\n' + ' MAX(idGenTestCase) AS idGenTestCase\n' + ' FROM TestSets\n' + ' WHERE ' + self._getTimePeriodQueryPart(tsNow, sPeriod, ' ') + + ' GROUP BY idTestCase\n' + ' ) AS TestCasesIDs\n' + ' LEFT OUTER JOIN TestCases ON TestCases.idGenTestCase = TestCasesIDs.idGenTestCase\n' + 'ORDER BY TestCases.sName\n' ); + + aoRet = []; + for aoRow in self._oDb.fetchAll(): + aoRet.append(TestCaseData().initFromDbRow(aoRow)); + return aoRet + + def getOSes(self, tsNow, sPeriod): + """ + Get a list of [idStrOs, sOs] tuples of the OSes that appears in the specified result period. + """ + + # Note! INNER JOIN TestBoxesWithStrings performs miserable compared to LEFT OUTER JOIN. Doesn't matter for the result + # because TestSets.idGenTestBox is a foreign key and unique in TestBoxes. So, let's do what ever is faster. + self._oDb.execute('SELECT DISTINCT TestBoxesWithStrings.idStrOs, TestBoxesWithStrings.sOs\n' + 'FROM ( SELECT idTestBox AS idTestBox,\n' + ' MAX(idGenTestBox) AS idGenTestBox\n' + ' FROM TestSets\n' + ' WHERE ' + self._getTimePeriodQueryPart(tsNow, sPeriod, ' ') + + ' GROUP BY idTestBox\n' + ' ) AS TestBoxIDs\n' + ' LEFT OUTER JOIN TestBoxesWithStrings\n' + ' ON TestBoxesWithStrings.idGenTestBox = TestBoxIDs.idGenTestBox\n' + 'ORDER BY TestBoxesWithStrings.sOs\n' ); + return self._oDb.fetchAll(); + + def getArchitectures(self, tsNow, sPeriod): + """ + Get a list of [idStrCpuArch, sCpuArch] tuples of the architecutres + that appears in the specified result period. + """ + + # Note! INNER JOIN TestBoxesWithStrings performs miserable compared to LEFT OUTER JOIN. Doesn't matter for the result + # because TestSets.idGenTestBox is a foreign key and unique in TestBoxes. So, let's do what ever is faster. + self._oDb.execute('SELECT DISTINCT TestBoxesWithStrings.idStrCpuArch, TestBoxesWithStrings.sCpuArch\n' + 'FROM ( SELECT idTestBox AS idTestBox,\n' + ' MAX(idGenTestBox) AS idGenTestBox\n' + ' FROM TestSets\n' + ' WHERE ' + self._getTimePeriodQueryPart(tsNow, sPeriod, ' ') + + ' GROUP BY idTestBox\n' + ' ) AS TestBoxIDs\n' + ' LEFT OUTER JOIN TestBoxesWithStrings\n' + ' ON TestBoxesWithStrings.idGenTestBox = TestBoxIDs.idGenTestBox\n' + 'ORDER BY TestBoxesWithStrings.sCpuArch\n' ); + return self._oDb.fetchAll(); + + def getBuildCategories(self, tsNow, sPeriod): + """ + Get a list of BuildCategoryData that appears in the specified result period. + """ + + self._oDb.execute('SELECT DISTINCT BuildCategories.*\n' + 'FROM ( SELECT DISTINCT idBuildCategory AS idBuildCategory\n' + ' FROM TestSets\n' + ' WHERE ' + self._getTimePeriodQueryPart(tsNow, sPeriod, ' ') + + ' ) AS BuildCategoryIDs\n' + ' LEFT OUTER JOIN BuildCategories\n' + ' ON BuildCategories.idBuildCategory = BuildCategoryIDs.idBuildCategory\n' + 'ORDER BY BuildCategories.sProduct, BuildCategories.sBranch, BuildCategories.sType\n'); + aoRet = []; + for aoRow in self._oDb.fetchAll(): + aoRet.append(BuildCategoryData().initFromDbRow(aoRow)); + return aoRet; + + def getSchedGroups(self, tsNow, sPeriod): + """ + Get list of uniq SchedGroupData objects which + found in all test results. + """ + + self._oDb.execute('SELECT SchedGroups.*\n' + 'FROM ( SELECT idSchedGroup,\n' + ' MAX(TestSets.tsCreated) AS tsNow\n' + ' FROM TestSets\n' + ' WHERE ' + self._getTimePeriodQueryPart(tsNow, sPeriod, ' ') + + ' GROUP BY idSchedGroup\n' + ' ) AS SchedGroupIDs\n' + ' INNER JOIN SchedGroups\n' + ' ON SchedGroups.idSchedGroup = SchedGroupIDs.idSchedGroup\n' + ' AND SchedGroups.tsExpire > SchedGroupIDs.tsNow\n' + ' AND SchedGroups.tsEffective <= SchedGroupIDs.tsNow\n' + 'ORDER BY SchedGroups.sName\n' ); + aoRet = [] + for aoRow in self._oDb.fetchAll(): + aoRet.append(SchedGroupData().initFromDbRow(aoRow)); + return aoRet + + def getById(self, idTestResult): + """ + Get build record by its id + """ + self._oDb.execute('SELECT *\n' + 'FROM TestResults\n' + 'WHERE idTestResult = %s\n', + (idTestResult,)) + + aRows = self._oDb.fetchAll() + if len(aRows) not in (0, 1): + raise TMTooManyRows('Found more than one test result with the same credentials. Database structure is corrupted.') + try: + return TestResultData().initFromDbRow(aRows[0]) + except IndexError: + return None + + def fetchPossibleFilterOptions(self, oFilter, tsNow, sPeriod, oReportModel = None): + """ + Fetches the available filter criteria, given the current filtering. + + Returns oFilter. + """ + assert isinstance(oFilter, TestResultFilter); + + # Hack to avoid lot's of conditionals or duplicate this code. + if oReportModel is None: + class DummyReportModel(object): + """ Dummy """ + def getExtraSubjectTables(self): + """ Dummy """ + return []; + def getExtraSubjectWhereExpr(self): + """ Dummy """ + return ''; + oReportModel = DummyReportModel(); + + def workerDoFetch(oMissingLogicType, sNameAttr = 'sName', fIdIsName = False, idxHover = -1, + idNull = -1, sNullDesc = '<NULL>'): + """ Does the tedious result fetching and handling of missing bits. """ + dLeft = { oValue: 1 for oValue in oCrit.aoSelected }; + oCrit.aoPossible = []; + for aoRow in self._oDb.fetchAll(): + oCrit.aoPossible.append(FilterCriterionValueAndDescription(aoRow[0] if aoRow[0] is not None else idNull, + aoRow[1] if aoRow[1] is not None else sNullDesc, + aoRow[2], + aoRow[idxHover] if idxHover >= 0 else None)); + if aoRow[0] in dLeft: + del dLeft[aoRow[0]]; + if dLeft: + if fIdIsName: + for idMissing in dLeft: + oCrit.aoPossible.append(FilterCriterionValueAndDescription(idMissing, str(idMissing), + fIrrelevant = True)); + else: + oMissingLogic = oMissingLogicType(self._oDb); + for idMissing in dLeft: + oMissing = oMissingLogic.cachedLookup(idMissing); + if oMissing is not None: + oCrit.aoPossible.append(FilterCriterionValueAndDescription(idMissing, + getattr(oMissing, sNameAttr), + fIrrelevant = True)); + + def workerDoFetchNested(): + """ Does the tedious result fetching and handling of missing bits. """ + oCrit.aoPossible = []; + oCrit.oSub.aoPossible = []; + dLeft = { oValue: 1 for oValue in oCrit.aoSelected }; + dSubLeft = { oValue: 1 for oValue in oCrit.oSub.aoSelected }; + oMain = None; + for aoRow in self._oDb.fetchAll(): + if oMain is None or oMain.oValue != aoRow[0]: + oMain = FilterCriterionValueAndDescription(aoRow[0], aoRow[1], 0); + oCrit.aoPossible.append(oMain); + if aoRow[0] in dLeft: + del dLeft[aoRow[0]]; + oCurSub = FilterCriterionValueAndDescription(aoRow[2], aoRow[3], aoRow[4]); + oCrit.oSub.aoPossible.append(oCurSub); + if aoRow[2] in dSubLeft: + del dSubLeft[aoRow[2]]; + + oMain.aoSubs.append(oCurSub); + oMain.cTimes += aoRow[4]; + + if dLeft: + pass; ## @todo + + # Statuses. + oCrit = oFilter.aCriteria[TestResultFilter.kiTestStatus]; + self._oDb.execute('SELECT TestSets.enmStatus, TestSets.enmStatus, COUNT(TestSets.idTestSet)\n' + 'FROM TestSets\n' + oFilter.getTableJoins(iOmit = TestResultFilter.kiTestStatus) + + ''.join(' , %s\n' % (sTable,) for sTable in oReportModel.getExtraSubjectTables()) + + 'WHERE ' + self._getTimePeriodQueryPart(tsNow, sPeriod) + + oFilter.getWhereConditions(iOmit = TestResultFilter.kiTestStatus) + + oReportModel.getExtraSubjectWhereExpr() + + 'GROUP BY TestSets.enmStatus\n' + 'ORDER BY TestSets.enmStatus\n'); + workerDoFetch(None, fIdIsName = True); + + # Scheduling groups (see getSchedGroups). + oCrit = oFilter.aCriteria[TestResultFilter.kiSchedGroups]; + self._oDb.execute('SELECT SchedGroups.idSchedGroup, SchedGroups.sName, SchedGroupIDs.cTimes\n' + 'FROM ( SELECT TestSets.idSchedGroup,\n' + ' MAX(TestSets.tsCreated) AS tsNow,\n' + ' COUNT(TestSets.idTestSet) AS cTimes\n' + ' FROM TestSets\n' + oFilter.getTableJoins(iOmit = TestResultFilter.kiSchedGroups) + + ''.join(' , %s\n' % (sTable,) for sTable in oReportModel.getExtraSubjectTables()) + + ' WHERE ' + self._getTimePeriodQueryPart(tsNow, sPeriod, ' ') + + oFilter.getWhereConditions(iOmit = TestResultFilter.kiSchedGroups) + + oReportModel.getExtraSubjectWhereExpr() + + ' GROUP BY TestSets.idSchedGroup\n' + ' ) AS SchedGroupIDs\n' + ' INNER JOIN SchedGroups\n' + ' ON SchedGroups.idSchedGroup = SchedGroupIDs.idSchedGroup\n' + ' AND SchedGroups.tsExpire > SchedGroupIDs.tsNow\n' + ' AND SchedGroups.tsEffective <= SchedGroupIDs.tsNow\n' + 'ORDER BY SchedGroups.sName\n' ); + workerDoFetch(SchedGroupLogic); + + # Testboxes (see getTestBoxes). + oCrit = oFilter.aCriteria[TestResultFilter.kiTestBoxes]; + self._oDb.execute('SELECT TestBoxesWithStrings.idTestBox,\n' + ' TestBoxesWithStrings.sName,\n' + ' TestBoxIDs.cTimes\n' + 'FROM ( SELECT TestSets.idTestBox AS idTestBox,\n' + ' MAX(TestSets.idGenTestBox) AS idGenTestBox,\n' + ' COUNT(TestSets.idTestSet) AS cTimes\n' + ' FROM TestSets\n' + oFilter.getTableJoins(iOmit = TestResultFilter.kiTestBoxes) + + ''.join(' , %s\n' % (sTable,) for sTable in oReportModel.getExtraSubjectTables()) + + ' WHERE ' + self._getTimePeriodQueryPart(tsNow, sPeriod, ' ') + + oFilter.getWhereConditions(iOmit = TestResultFilter.kiTestBoxes) + + oReportModel.getExtraSubjectWhereExpr() + + ' GROUP BY TestSets.idTestBox\n' + ' ) AS TestBoxIDs\n' + ' LEFT OUTER JOIN TestBoxesWithStrings\n' + ' ON TestBoxesWithStrings.idGenTestBox = TestBoxIDs.idGenTestBox\n' + 'ORDER BY TestBoxesWithStrings.sName\n' ); + workerDoFetch(TestBoxLogic); + + # Testbox OSes and versions. + oCrit = oFilter.aCriteria[TestResultFilter.kiOses]; + self._oDb.execute('SELECT TestBoxesWithStrings.idStrOs,\n' + ' TestBoxesWithStrings.sOs,\n' + ' TestBoxesWithStrings.idStrOsVersion,\n' + ' TestBoxesWithStrings.sOsVersion,\n' + ' SUM(TestBoxGenIDs.cTimes)\n' + 'FROM ( SELECT TestSets.idGenTestBox,\n' + ' COUNT(TestSets.idTestSet) AS cTimes\n' + ' FROM TestSets\n' + oFilter.getTableJoins(iOmit = TestResultFilter.kiOses) + + ''.join(' , %s\n' % (sTable,) for sTable in oReportModel.getExtraSubjectTables()) + + ' WHERE ' + self._getTimePeriodQueryPart(tsNow, sPeriod, ' ') + + oFilter.getWhereConditions(iOmit = TestResultFilter.kiOses) + + oReportModel.getExtraSubjectWhereExpr() + + ' GROUP BY TestSets.idGenTestBox\n' + ' ) AS TestBoxGenIDs\n' + ' LEFT OUTER JOIN TestBoxesWithStrings\n' + ' ON TestBoxesWithStrings.idGenTestBox = TestBoxGenIDs.idGenTestBox\n' + 'GROUP BY TestBoxesWithStrings.idStrOs,\n' + ' TestBoxesWithStrings.sOs,\n' + ' TestBoxesWithStrings.idStrOsVersion,\n' + ' TestBoxesWithStrings.sOsVersion\n' + 'ORDER BY TestBoxesWithStrings.sOs,\n' + ' TestBoxesWithStrings.sOs = \'win\' AND TestBoxesWithStrings.sOsVersion = \'10\' DESC,\n' + ' TestBoxesWithStrings.sOsVersion DESC\n' + ); + workerDoFetchNested(); + + # Testbox CPU(/OS) architectures. + oCrit = oFilter.aCriteria[TestResultFilter.kiCpuArches]; + self._oDb.execute('SELECT TestBoxesWithStrings.idStrCpuArch,\n' + ' TestBoxesWithStrings.sCpuArch,\n' + ' SUM(TestBoxGenIDs.cTimes)\n' + 'FROM ( SELECT TestSets.idGenTestBox,\n' + ' COUNT(TestSets.idTestSet) AS cTimes\n' + ' FROM TestSets\n' + oFilter.getTableJoins(iOmit = TestResultFilter.kiCpuArches) + + ''.join(' , %s\n' % (sTable,) for sTable in oReportModel.getExtraSubjectTables()) + + ' WHERE ' + self._getTimePeriodQueryPart(tsNow, sPeriod, ' ') + + oFilter.getWhereConditions(iOmit = TestResultFilter.kiCpuArches) + + oReportModel.getExtraSubjectWhereExpr() + + ' GROUP BY TestSets.idGenTestBox\n' + ' ) AS TestBoxGenIDs\n' + ' LEFT OUTER JOIN TestBoxesWithStrings\n' + ' ON TestBoxesWithStrings.idGenTestBox = TestBoxGenIDs.idGenTestBox\n' + 'GROUP BY TestBoxesWithStrings.idStrCpuArch, TestBoxesWithStrings.sCpuArch\n' + 'ORDER BY TestBoxesWithStrings.sCpuArch\n' ); + workerDoFetch(None, fIdIsName = True); + + # Testbox CPU revisions. + oCrit = oFilter.aCriteria[TestResultFilter.kiCpuVendors]; + self._oDb.execute('SELECT TestBoxesWithStrings.idStrCpuVendor,\n' + ' TestBoxesWithStrings.sCpuVendor,\n' + ' TestBoxesWithStrings.lCpuRevision,\n' + ' TestBoxesWithStrings.sCpuVendor,\n' + ' SUM(TestBoxGenIDs.cTimes)\n' + 'FROM ( SELECT TestSets.idGenTestBox,\n' + ' COUNT(TestSets.idTestSet) AS cTimes\n' + ' FROM TestSets\n' + oFilter.getTableJoins(iOmit = TestResultFilter.kiCpuVendors) + + ''.join(' , %s\n' % (sTable,) for sTable in oReportModel.getExtraSubjectTables()) + + ' WHERE ' + self._getTimePeriodQueryPart(tsNow, sPeriod, ' ') + + oFilter.getWhereConditions(iOmit = TestResultFilter.kiCpuVendors) + + oReportModel.getExtraSubjectWhereExpr() + + ' GROUP BY TestSets.idGenTestBox' + ' ) AS TestBoxGenIDs\n' + ' LEFT OUTER JOIN TestBoxesWithStrings\n' + ' ON TestBoxesWithStrings.idGenTestBox = TestBoxGenIDs.idGenTestBox\n' + 'GROUP BY TestBoxesWithStrings.idStrCpuVendor,\n' + ' TestBoxesWithStrings.sCpuVendor,\n' + ' TestBoxesWithStrings.lCpuRevision,\n' + ' TestBoxesWithStrings.sCpuVendor\n' + 'ORDER BY TestBoxesWithStrings.sCpuVendor DESC,\n' + ' TestBoxesWithStrings.sCpuVendor = \'GenuineIntel\'\n' + ' AND (TestBoxesWithStrings.lCpuRevision >> 24) = 15,\n' # P4 at the bottom is a start... + ' TestBoxesWithStrings.lCpuRevision DESC\n' + ); + workerDoFetchNested(); + for oCur in oCrit.oSub.aoPossible: + oCur.sDesc = TestBoxData.getPrettyCpuVersionEx(oCur.oValue, oCur.sDesc).replace('_', ' '); + + # Testbox CPU core/thread counts. + oCrit = oFilter.aCriteria[TestResultFilter.kiCpuCounts]; + self._oDb.execute('SELECT TestBoxesWithStrings.cCpus,\n' + ' CAST(TestBoxesWithStrings.cCpus AS TEXT),\n' + ' SUM(TestBoxGenIDs.cTimes)\n' + 'FROM ( SELECT TestSets.idGenTestBox,\n' + ' COUNT(TestSets.idTestSet) AS cTimes\n' + ' FROM TestSets\n' + oFilter.getTableJoins(iOmit = TestResultFilter.kiCpuCounts) + + ''.join(' , %s\n' % (sTable,) for sTable in oReportModel.getExtraSubjectTables()) + + ' WHERE ' + self._getTimePeriodQueryPart(tsNow, sPeriod, ' ') + + oFilter.getWhereConditions(iOmit = TestResultFilter.kiCpuCounts) + + oReportModel.getExtraSubjectWhereExpr() + + ' GROUP BY TestSets.idGenTestBox' + ' ) AS TestBoxGenIDs\n' + ' LEFT OUTER JOIN TestBoxesWithStrings\n' + ' ON TestBoxesWithStrings.idGenTestBox = TestBoxGenIDs.idGenTestBox\n' + 'GROUP BY TestBoxesWithStrings.cCpus\n' + 'ORDER BY TestBoxesWithStrings.cCpus\n' ); + workerDoFetch(None, fIdIsName = True); + + # Testbox memory. + oCrit = oFilter.aCriteria[TestResultFilter.kiMemory]; + self._oDb.execute('SELECT TestBoxesWithStrings.cMbMemory / 1024,\n' + ' NULL,\n' + ' SUM(TestBoxGenIDs.cTimes)\n' + 'FROM ( SELECT TestSets.idGenTestBox,\n' + ' COUNT(TestSets.idTestSet) AS cTimes\n' + ' FROM TestSets\n' + oFilter.getTableJoins(iOmit = TestResultFilter.kiMemory) + + ''.join(' , %s\n' % (sTable,) for sTable in oReportModel.getExtraSubjectTables()) + + ' WHERE ' + self._getTimePeriodQueryPart(tsNow, sPeriod, ' ') + + oFilter.getWhereConditions(iOmit = TestResultFilter.kiMemory) + + oReportModel.getExtraSubjectWhereExpr() + + ' GROUP BY TestSets.idGenTestBox' + ' ) AS TestBoxGenIDs\n' + ' LEFT OUTER JOIN TestBoxesWithStrings\n' + ' ON TestBoxesWithStrings.idGenTestBox = TestBoxGenIDs.idGenTestBox\n' + 'GROUP BY TestBoxesWithStrings.cMbMemory / 1024\n' + 'ORDER BY 1\n' ); + workerDoFetch(None, fIdIsName = True); + for oCur in oCrit.aoPossible: + oCur.sDesc = '%u GB' % (oCur.oValue,); + + # Testbox python versions . + oCrit = oFilter.aCriteria[TestResultFilter.kiPythonVersions]; + self._oDb.execute('SELECT TestBoxesWithStrings.iPythonHexVersion,\n' + ' NULL,\n' + ' SUM(TestBoxGenIDs.cTimes)\n' + 'FROM ( SELECT TestSets.idGenTestBox AS idGenTestBox,\n' + ' COUNT(TestSets.idTestSet) AS cTimes\n' + ' FROM TestSets\n' + oFilter.getTableJoins(iOmit = TestResultFilter.kiPythonVersions) + + ''.join(' , %s\n' % (sTable,) for sTable in oReportModel.getExtraSubjectTables()) + + ' WHERE ' + self._getTimePeriodQueryPart(tsNow, sPeriod, ' ') + + oFilter.getWhereConditions(iOmit = TestResultFilter.kiPythonVersions) + + oReportModel.getExtraSubjectWhereExpr() + + ' GROUP BY TestSets.idGenTestBox\n' + ' ) AS TestBoxGenIDs\n' + ' LEFT OUTER JOIN TestBoxesWithStrings\n' + ' ON TestBoxesWithStrings.idGenTestBox = TestBoxGenIDs.idGenTestBox\n' + 'GROUP BY TestBoxesWithStrings.iPythonHexVersion\n' + 'ORDER BY TestBoxesWithStrings.iPythonHexVersion\n' ); + workerDoFetch(None, fIdIsName = True); + for oCur in oCrit.aoPossible: + oCur.sDesc = TestBoxData.formatPythonVersionEx(oCur.oValue); # pylint: disable=redefined-variable-type + + # Testcase with variation. + oCrit = oFilter.aCriteria[TestResultFilter.kiTestCases]; + self._oDb.execute('SELECT TestCaseArgsIDs.idTestCase,\n' + ' TestCases.sName,\n' + ' TestCaseArgsIDs.idTestCaseArgs,\n' + ' CASE WHEN TestCaseArgs.sSubName IS NULL OR TestCaseArgs.sSubName = \'\' THEN\n' + ' CONCAT(\'/ #\', TestCaseArgs.idTestCaseArgs)\n' + ' ELSE\n' + ' TestCaseArgs.sSubName\n' + ' END,' + ' TestCaseArgsIDs.cTimes\n' + 'FROM ( SELECT TestSets.idTestCase AS idTestCase,\n' + ' TestSets.idTestCaseArgs AS idTestCaseArgs,\n' + ' MAX(TestSets.idGenTestCase) AS idGenTestCase,\n' + ' MAX(TestSets.idGenTestCaseArgs) AS idGenTestCaseArgs,\n' + ' COUNT(TestSets.idTestSet) AS cTimes\n' + ' FROM TestSets\n' + oFilter.getTableJoins(iOmit = TestResultFilter.kiTestCases) + + ''.join(' , %s\n' % (sTable,) for sTable in oReportModel.getExtraSubjectTables()) + + ' WHERE ' + self._getTimePeriodQueryPart(tsNow, sPeriod, ' ') + + oFilter.getWhereConditions(iOmit = TestResultFilter.kiTestCases) + + oReportModel.getExtraSubjectWhereExpr() + + ' GROUP BY TestSets.idTestCase, TestSets.idTestCaseArgs\n' + ' ) AS TestCaseArgsIDs\n' + ' LEFT OUTER JOIN TestCases ON TestCases.idGenTestCase = TestCaseArgsIDs.idGenTestCase\n' + ' LEFT OUTER JOIN TestCaseArgs\n' + ' ON TestCaseArgs.idGenTestCaseArgs = TestCaseArgsIDs.idGenTestCaseArgs\n' + 'ORDER BY TestCases.sName, 4\n' ); + workerDoFetchNested(); + + # Build revisions. + oCrit = oFilter.aCriteria[TestResultFilter.kiRevisions]; + self._oDb.execute('SELECT Builds.iRevision, CONCAT(\'r\', Builds.iRevision), SUM(BuildIDs.cTimes)\n' + 'FROM ( SELECT TestSets.idBuild AS idBuild,\n' + ' MAX(TestSets.tsCreated) AS tsNow,\n' + ' COUNT(TestSets.idBuild) AS cTimes\n' + ' FROM TestSets\n' + oFilter.getTableJoins(iOmit = TestResultFilter.kiRevisions) + + ''.join(' , %s\n' % (sTable,) for sTable in oReportModel.getExtraSubjectTables()) + + ' WHERE ' + self._getTimePeriodQueryPart(tsNow, sPeriod, ' ') + + oFilter.getWhereConditions(iOmit = TestResultFilter.kiRevisions) + + oReportModel.getExtraSubjectWhereExpr() + + ' GROUP BY TestSets.idBuild\n' + ' ) AS BuildIDs\n' + ' INNER JOIN Builds\n' + ' ON Builds.idBuild = BuildIDs.idBuild\n' + ' AND Builds.tsExpire > BuildIDs.tsNow\n' + ' AND Builds.tsEffective <= BuildIDs.tsNow\n' + 'GROUP BY Builds.iRevision\n' + 'ORDER BY Builds.iRevision DESC\n' ); + workerDoFetch(None, fIdIsName = True); + + # Build branches. + oCrit = oFilter.aCriteria[TestResultFilter.kiBranches]; + self._oDb.execute('SELECT BuildCategories.sBranch, BuildCategories.sBranch, SUM(BuildCategoryIDs.cTimes)\n' + 'FROM ( SELECT TestSets.idBuildCategory,\n' + ' COUNT(TestSets.idTestSet) AS cTimes\n' + ' FROM TestSets\n' + oFilter.getTableJoins(iOmit = TestResultFilter.kiBranches) + + ''.join(' , %s\n' % (sTable,) for sTable in oReportModel.getExtraSubjectTables()) + + ' WHERE ' + self._getTimePeriodQueryPart(tsNow, sPeriod, ' ') + + oFilter.getWhereConditions(iOmit = TestResultFilter.kiBranches) + + oReportModel.getExtraSubjectWhereExpr() + + ' GROUP BY TestSets.idBuildCategory\n' + ' ) AS BuildCategoryIDs\n' + ' INNER JOIN BuildCategories\n' + ' ON BuildCategories.idBuildCategory = BuildCategoryIDs.idBuildCategory\n' + 'GROUP BY BuildCategories.sBranch\n' + 'ORDER BY BuildCategories.sBranch DESC\n' ); + workerDoFetch(None, fIdIsName = True); + + # Build types. + oCrit = oFilter.aCriteria[TestResultFilter.kiBuildTypes]; + self._oDb.execute('SELECT BuildCategories.sType, BuildCategories.sType, SUM(BuildCategoryIDs.cTimes)\n' + 'FROM ( SELECT TestSets.idBuildCategory,\n' + ' COUNT(TestSets.idTestSet) AS cTimes\n' + ' FROM TestSets\n' + oFilter.getTableJoins(iOmit = TestResultFilter.kiBuildTypes) + + ''.join(' , %s\n' % (sTable,) for sTable in oReportModel.getExtraSubjectTables()) + + ' WHERE ' + self._getTimePeriodQueryPart(tsNow, sPeriod, ' ') + + oFilter.getWhereConditions(iOmit = TestResultFilter.kiBuildTypes) + + oReportModel.getExtraSubjectWhereExpr() + + ' GROUP BY TestSets.idBuildCategory\n' + ' ) AS BuildCategoryIDs\n' + ' INNER JOIN BuildCategories\n' + ' ON BuildCategories.idBuildCategory = BuildCategoryIDs.idBuildCategory\n' + 'GROUP BY BuildCategories.sType\n' + 'ORDER BY BuildCategories.sType DESC\n' ); + workerDoFetch(None, fIdIsName = True); + + # Failure reasons. + oCrit = oFilter.aCriteria[TestResultFilter.kiFailReasons]; + self._oDb.execute('SELECT FailureReasons.idFailureReason, FailureReasons.sShort, FailureReasonIDs.cTimes\n' + 'FROM ( SELECT TestResultFailures.idFailureReason,\n' + ' COUNT(TestSets.idTestSet) as cTimes\n' + ' FROM TestSets\n' + ' LEFT OUTER JOIN TestResultFailures\n' + ' ON TestResultFailures.idTestSet = TestSets.idTestSet\n' + ' AND TestResultFailures.tsExpire = \'infinity\'::TIMESTAMP\n' + + oFilter.getTableJoins(iOmit = TestResultFilter.kiFailReasons) + + ''.join(' , %s\n' % (sTable,) for sTable in oReportModel.getExtraSubjectTables()) + + ' WHERE ' + self._getTimePeriodQueryPart(tsNow, sPeriod, ' ') + + ' AND TestSets.enmStatus >= \'failure\'::TestStatus_T\n' + + oFilter.getWhereConditions(iOmit = TestResultFilter.kiFailReasons) + + oReportModel.getExtraSubjectWhereExpr() + + ' GROUP BY TestResultFailures.idFailureReason\n' + ' ) AS FailureReasonIDs\n' + ' LEFT OUTER JOIN FailureReasons\n' + ' ON FailureReasons.idFailureReason = FailureReasonIDs.idFailureReason\n' + ' AND FailureReasons.tsExpire = \'infinity\'::TIMESTAMP\n' + 'ORDER BY FailureReasons.idFailureReason IS NULL DESC,\n' + ' FailureReasons.sShort\n' ); + workerDoFetch(FailureReasonLogic, 'sShort', sNullDesc = 'Not given'); + + # Error counts. + oCrit = oFilter.aCriteria[TestResultFilter.kiErrorCounts]; + self._oDb.execute('SELECT TestResults.cErrors, CAST(TestResults.cErrors AS TEXT), COUNT(TestResults.idTestResult)\n' + 'FROM ( SELECT TestSets.idTestResult AS idTestResult\n' + ' FROM TestSets\n' + + oFilter.getTableJoins(iOmit = TestResultFilter.kiFailReasons) + + ''.join(' , %s\n' % (sTable,) for sTable in oReportModel.getExtraSubjectTables()) + + ' WHERE ' + self._getTimePeriodQueryPart(tsNow, sPeriod, ' ') + + oFilter.getWhereConditions(iOmit = TestResultFilter.kiFailReasons) + + oReportModel.getExtraSubjectWhereExpr() + + ' ) AS TestSetIDs\n' + ' INNER JOIN TestResults\n' + ' ON TestResults.idTestResult = TestSetIDs.idTestResult\n' + 'GROUP BY TestResults.cErrors\n' + 'ORDER BY TestResults.cErrors\n'); + + workerDoFetch(None, fIdIsName = True); + + return oFilter; + + + # + # Details view and interface. + # + + def fetchResultTree(self, idTestSet, cMaxDepth = None): + """ + Fetches the result tree for the given test set. + + Returns a tree of TestResultDataEx nodes. + Raises exception on invalid input and database issues. + """ + # Depth first, i.e. just like the XML added them. + ## @todo this still isn't performing extremely well, consider optimizations. + sQuery = self._oDb.formatBindArgs( + 'SELECT TestResults.*,\n' + ' TestResultStrTab.sValue,\n' + ' EXISTS ( SELECT idTestResultValue\n' + ' FROM TestResultValues\n' + ' WHERE TestResultValues.idTestResult = TestResults.idTestResult ) AS fHasValues,\n' + ' EXISTS ( SELECT idTestResultMsg\n' + ' FROM TestResultMsgs\n' + ' WHERE TestResultMsgs.idTestResult = TestResults.idTestResult ) AS fHasMsgs,\n' + ' EXISTS ( SELECT idTestResultFile\n' + ' FROM TestResultFiles\n' + ' WHERE TestResultFiles.idTestResult = TestResults.idTestResult ) AS fHasFiles,\n' + ' EXISTS ( SELECT idTestResult\n' + ' FROM TestResultFailures\n' + ' WHERE TestResultFailures.idTestResult = TestResults.idTestResult ) AS fHasReasons\n' + 'FROM TestResults, TestResultStrTab\n' + 'WHERE TestResults.idTestSet = %s\n' + ' AND TestResults.idStrName = TestResultStrTab.idStr\n' + , ( idTestSet, )); + if cMaxDepth is not None: + sQuery += self._oDb.formatBindArgs(' AND TestResults.iNestingDepth <= %s\n', (cMaxDepth,)); + sQuery += 'ORDER BY idTestResult ASC\n' + + self._oDb.execute(sQuery); + cRows = self._oDb.getRowCount(); + if cRows > 65536: + raise TMTooManyRows('Too many rows returned for idTestSet=%d: %d' % (idTestSet, cRows,)); + + aaoRows = self._oDb.fetchAll(); + if not aaoRows: + raise TMRowNotFound('No test results for idTestSet=%d.' % (idTestSet,)); + + # Set up the root node first. + aoRow = aaoRows[0]; + oRoot = TestResultDataEx().initFromDbRow(aoRow); + if oRoot.idTestResultParent is not None: + raise self._oDb.integrityException('The root TestResult (#%s) has a parent (#%s)!' + % (oRoot.idTestResult, oRoot.idTestResultParent)); + self._fetchResultTreeNodeExtras(oRoot, aoRow[-4], aoRow[-3], aoRow[-2], aoRow[-1]); + + # The children (if any). + dLookup = { oRoot.idTestResult: oRoot }; + oParent = oRoot; + for iRow in range(1, len(aaoRows)): + aoRow = aaoRows[iRow]; + oCur = TestResultDataEx().initFromDbRow(aoRow); + self._fetchResultTreeNodeExtras(oCur, aoRow[-4], aoRow[-3], aoRow[-2], aoRow[-1]); + + # Figure out and vet the parent. + if oParent.idTestResult != oCur.idTestResultParent: + oParent = dLookup.get(oCur.idTestResultParent, None); + if oParent is None: + raise self._oDb.integrityException('TestResult #%d is orphaned from its parent #%s.' + % (oCur.idTestResult, oCur.idTestResultParent,)); + if oParent.iNestingDepth + 1 != oCur.iNestingDepth: + raise self._oDb.integrityException('TestResult #%d has incorrect nesting depth (%d instead of %d)' + % (oCur.idTestResult, oCur.iNestingDepth, oParent.iNestingDepth + 1,)); + + # Link it up. + oCur.oParent = oParent; + oParent.aoChildren.append(oCur); + dLookup[oCur.idTestResult] = oCur; + + return (oRoot, dLookup); + + def _fetchResultTreeNodeExtras(self, oCurNode, fHasValues, fHasMsgs, fHasFiles, fHasReasons): + """ + fetchResultTree worker that fetches values, message and files for the + specified node. + """ + assert(oCurNode.aoValues == []); + assert(oCurNode.aoMsgs == []); + assert(oCurNode.aoFiles == []); + assert(oCurNode.oReason is None); + + if fHasValues: + self._oDb.execute('SELECT TestResultValues.*,\n' + ' TestResultStrTab.sValue\n' + 'FROM TestResultValues, TestResultStrTab\n' + 'WHERE TestResultValues.idTestResult = %s\n' + ' AND TestResultValues.idStrName = TestResultStrTab.idStr\n' + 'ORDER BY idTestResultValue ASC\n' + , ( oCurNode.idTestResult, )); + for aoRow in self._oDb.fetchAll(): + oCurNode.aoValues.append(TestResultValueDataEx().initFromDbRow(aoRow)); + + if fHasMsgs: + self._oDb.execute('SELECT TestResultMsgs.*,\n' + ' TestResultStrTab.sValue\n' + 'FROM TestResultMsgs, TestResultStrTab\n' + 'WHERE TestResultMsgs.idTestResult = %s\n' + ' AND TestResultMsgs.idStrMsg = TestResultStrTab.idStr\n' + 'ORDER BY idTestResultMsg ASC\n' + , ( oCurNode.idTestResult, )); + for aoRow in self._oDb.fetchAll(): + oCurNode.aoMsgs.append(TestResultMsgDataEx().initFromDbRow(aoRow)); + + if fHasFiles: + self._oDb.execute('SELECT TestResultFiles.*,\n' + ' StrTabFile.sValue AS sFile,\n' + ' StrTabDesc.sValue AS sDescription,\n' + ' StrTabKind.sValue AS sKind,\n' + ' StrTabMime.sValue AS sMime\n' + 'FROM TestResultFiles,\n' + ' TestResultStrTab AS StrTabFile,\n' + ' TestResultStrTab AS StrTabDesc,\n' + ' TestResultStrTab AS StrTabKind,\n' + ' TestResultStrTab AS StrTabMime\n' + 'WHERE TestResultFiles.idTestResult = %s\n' + ' AND TestResultFiles.idStrFile = StrTabFile.idStr\n' + ' AND TestResultFiles.idStrDescription = StrTabDesc.idStr\n' + ' AND TestResultFiles.idStrKind = StrTabKind.idStr\n' + ' AND TestResultFiles.idStrMime = StrTabMime.idStr\n' + 'ORDER BY idTestResultFile ASC\n' + , ( oCurNode.idTestResult, )); + for aoRow in self._oDb.fetchAll(): + oCurNode.aoFiles.append(TestResultFileDataEx().initFromDbRow(aoRow)); + + if fHasReasons: + if self.oFailureReasonLogic is None: + self.oFailureReasonLogic = FailureReasonLogic(self._oDb); + if self.oUserAccountLogic is None: + self.oUserAccountLogic = UserAccountLogic(self._oDb); + self._oDb.execute('SELECT *\n' + 'FROM TestResultFailures\n' + 'WHERE idTestResult = %s\n' + ' AND tsExpire = \'infinity\'::TIMESTAMP\n' + , ( oCurNode.idTestResult, )); + if self._oDb.getRowCount() > 0: + oCurNode.oReason = TestResultFailureDataEx().initFromDbRowEx(self._oDb.fetchOne(), self.oFailureReasonLogic, + self.oUserAccountLogic); + + return True; + + + + # + # TestBoxController interface(s). + # + + def _inhumeTestResults(self, aoStack, idTestSet, sError): + """ + The test produces too much output, kill and bury it. + + Note! We leave the test set open, only the test result records are + completed. Thus, _getResultStack will return an empty stack and + cause XML processing to fail immediately, while we can still + record when it actually completed in the test set the normal way. + """ + self._oDb.dprint('** _inhumeTestResults: idTestSet=%d\n%s' % (idTestSet, self._stringifyStack(aoStack),)); + + # + # First add a message. + # + self._newFailureDetails(aoStack[0].idTestResult, idTestSet, sError, None); + + # + # The complete all open test results. + # + for oTestResult in aoStack: + oTestResult.cErrors += 1; + self._completeTestResults(oTestResult, None, TestResultData.ksTestStatus_Failure, oTestResult.cErrors); + + # A bit of paranoia. + self._oDb.execute('UPDATE TestResults\n' + 'SET cErrors = cErrors + 1,\n' + ' enmStatus = \'failure\'::TestStatus_T,\n' + ' tsElapsed = CURRENT_TIMESTAMP - tsCreated\n' + 'WHERE idTestSet = %s\n' + ' AND enmStatus = \'running\'::TestStatus_T\n' + , ( idTestSet, )); + self._oDb.commit(); + + return None; + + def strTabString(self, sString, fCommit = False): + """ + Gets the string table id for the given string, adding it if new. + + Note! A copy of this code is also in TestSetLogic. + """ + ## @todo move this and make a stored procedure for it. + self._oDb.execute('SELECT idStr\n' + 'FROM TestResultStrTab\n' + 'WHERE sValue = %s' + , (sString,)); + if self._oDb.getRowCount() == 0: + self._oDb.execute('INSERT INTO TestResultStrTab (sValue)\n' + 'VALUES (%s)\n' + 'RETURNING idStr\n' + , (sString,)); + if fCommit: + self._oDb.commit(); + return self._oDb.fetchOne()[0]; + + @staticmethod + def _stringifyStack(aoStack): + """Returns a string rep of the stack.""" + sRet = ''; + for i, _ in enumerate(aoStack): + sRet += 'aoStack[%d]=%s\n' % (i, aoStack[i]); + return sRet; + + def _getResultStack(self, idTestSet): + """ + Gets the current stack of result sets. + """ + self._oDb.execute('SELECT *\n' + 'FROM TestResults\n' + 'WHERE idTestSet = %s\n' + ' AND enmStatus = \'running\'::TestStatus_T\n' + 'ORDER BY idTestResult DESC' + , ( idTestSet, )); + aoStack = []; + for aoRow in self._oDb.fetchAll(): + aoStack.append(TestResultData().initFromDbRow(aoRow)); + + for i, _ in enumerate(aoStack): + assert aoStack[i].iNestingDepth == len(aoStack) - i - 1, self._stringifyStack(aoStack); + + return aoStack; + + def _newTestResult(self, idTestResultParent, idTestSet, iNestingDepth, tsCreated, sName, dCounts, fCommit = False): + """ + Creates a new test result. + Returns the TestResultData object for the new record. + May raise exception on database error. + """ + assert idTestResultParent is not None; + assert idTestResultParent > 1; + + # + # This isn't necessarily very efficient, but it's necessary to prevent + # a wild test or testbox from filling up the database. + # + sCountName = 'cTestResults'; + if sCountName not in dCounts: + self._oDb.execute('SELECT COUNT(idTestResult)\n' + 'FROM TestResults\n' + 'WHERE idTestSet = %s\n' + , ( idTestSet,)); + dCounts[sCountName] = self._oDb.fetchOne()[0]; + dCounts[sCountName] += 1; + if dCounts[sCountName] > config.g_kcMaxTestResultsPerTS: + raise TestResultHangingOffence('Too many sub-tests in total!'); + + sCountName = 'cTestResultsIn%d' % (idTestResultParent,); + if sCountName not in dCounts: + self._oDb.execute('SELECT COUNT(idTestResult)\n' + 'FROM TestResults\n' + 'WHERE idTestResultParent = %s\n' + , ( idTestResultParent,)); + dCounts[sCountName] = self._oDb.fetchOne()[0]; + dCounts[sCountName] += 1; + if dCounts[sCountName] > config.g_kcMaxTestResultsPerTR: + raise TestResultHangingOffence('Too many immediate sub-tests!'); + + # This is also a hanging offence. + if iNestingDepth > config.g_kcMaxTestResultDepth: + raise TestResultHangingOffence('To deep sub-test nesting!'); + + # Ditto. + if len(sName) > config.g_kcchMaxTestResultName: + raise TestResultHangingOffence('Test name is too long: %d chars - "%s"' % (len(sName), sName)); + + # + # Within bounds, do the job. + # + idStrName = self.strTabString(sName, fCommit); + self._oDb.execute('INSERT INTO TestResults (\n' + ' idTestResultParent,\n' + ' idTestSet,\n' + ' tsCreated,\n' + ' idStrName,\n' + ' iNestingDepth )\n' + 'VALUES (%s, %s, TIMESTAMP WITH TIME ZONE %s, %s, %s)\n' + 'RETURNING *\n' + , ( idTestResultParent, idTestSet, tsCreated, idStrName, iNestingDepth) ) + oData = TestResultData().initFromDbRow(self._oDb.fetchOne()); + + self._oDb.maybeCommit(fCommit); + return oData; + + def _newTestValue(self, idTestResult, idTestSet, sName, lValue, sUnit, dCounts, tsCreated = None, fCommit = False): + """ + Creates a test value. + May raise exception on database error. + """ + + # + # Bounds checking. + # + sCountName = 'cTestValues'; + if sCountName not in dCounts: + self._oDb.execute('SELECT COUNT(idTestResultValue)\n' + 'FROM TestResultValues, TestResults\n' + 'WHERE TestResultValues.idTestResult = TestResults.idTestResult\n' + ' AND TestResults.idTestSet = %s\n' + , ( idTestSet,)); + dCounts[sCountName] = self._oDb.fetchOne()[0]; + dCounts[sCountName] += 1; + if dCounts[sCountName] > config.g_kcMaxTestValuesPerTS: + raise TestResultHangingOffence('Too many values in total!'); + + sCountName = 'cTestValuesIn%d' % (idTestResult,); + if sCountName not in dCounts: + self._oDb.execute('SELECT COUNT(idTestResultValue)\n' + 'FROM TestResultValues\n' + 'WHERE idTestResult = %s\n' + , ( idTestResult,)); + dCounts[sCountName] = self._oDb.fetchOne()[0]; + dCounts[sCountName] += 1; + if dCounts[sCountName] > config.g_kcMaxTestValuesPerTR: + raise TestResultHangingOffence('Too many immediate values for one test result!'); + + if len(sName) > config.g_kcchMaxTestValueName: + raise TestResultHangingOffence('Value name is too long: %d chars - "%s"' % (len(sName), sName)); + + # + # Do the job. + # + iUnit = constants.valueunit.g_kdNameToConst.get(sUnit, constants.valueunit.NONE); + + idStrName = self.strTabString(sName, fCommit); + if tsCreated is None: + self._oDb.execute('INSERT INTO TestResultValues (\n' + ' idTestResult,\n' + ' idTestSet,\n' + ' idStrName,\n' + ' lValue,\n' + ' iUnit)\n' + 'VALUES ( %s, %s, %s, %s, %s )\n' + , ( idTestResult, idTestSet, idStrName, lValue, iUnit,) ); + else: + self._oDb.execute('INSERT INTO TestResultValues (\n' + ' idTestResult,\n' + ' idTestSet,\n' + ' tsCreated,\n' + ' idStrName,\n' + ' lValue,\n' + ' iUnit)\n' + 'VALUES ( %s, %s, TIMESTAMP WITH TIME ZONE %s, %s, %s, %s )\n' + , ( idTestResult, idTestSet, tsCreated, idStrName, lValue, iUnit,) ); + self._oDb.maybeCommit(fCommit); + return True; + + def _newFailureDetails(self, idTestResult, idTestSet, sText, dCounts, tsCreated = None, fCommit = False): + """ + Creates a record detailing cause of failure. + May raise exception on database error. + """ + + # + # Overflow protection. + # + if dCounts is not None: + sCountName = 'cTestMsgsIn%d' % (idTestResult,); + if sCountName not in dCounts: + self._oDb.execute('SELECT COUNT(idTestResultMsg)\n' + 'FROM TestResultMsgs\n' + 'WHERE idTestResult = %s\n' + , ( idTestResult,)); + dCounts[sCountName] = self._oDb.fetchOne()[0]; + dCounts[sCountName] += 1; + if dCounts[sCountName] > config.g_kcMaxTestMsgsPerTR: + raise TestResultHangingOffence('Too many messages under for one test result!'); + + if len(sText) > config.g_kcchMaxTestMsg: + raise TestResultHangingOffence('Failure details message is too long: %d chars - "%s"' % (len(sText), sText)); + + # + # Do the job. + # + idStrMsg = self.strTabString(sText, fCommit); + if tsCreated is None: + self._oDb.execute('INSERT INTO TestResultMsgs (\n' + ' idTestResult,\n' + ' idTestSet,\n' + ' idStrMsg,\n' + ' enmLevel)\n' + 'VALUES ( %s, %s, %s, %s)\n' + , ( idTestResult, idTestSet, idStrMsg, 'failure',) ); + else: + self._oDb.execute('INSERT INTO TestResultMsgs (\n' + ' idTestResult,\n' + ' idTestSet,\n' + ' tsCreated,\n' + ' idStrMsg,\n' + ' enmLevel)\n' + 'VALUES ( %s, %s, TIMESTAMP WITH TIME ZONE %s, %s, %s)\n' + , ( idTestResult, idTestSet, tsCreated, idStrMsg, 'failure',) ); + + self._oDb.maybeCommit(fCommit); + return True; + + + def _completeTestResults(self, oTestResult, tsDone, enmStatus, cErrors = 0, fCommit = False): + """ + Completes a test result. Updates the oTestResult object. + May raise exception on database error. + """ + self._oDb.dprint('** _completeTestResults: cErrors=%s tsDone=%s enmStatus=%s oTestResults=\n%s' + % (cErrors, tsDone, enmStatus, oTestResult,)); + + # + # Sanity check: No open sub tests (aoStack should make sure about this!). + # + self._oDb.execute('SELECT COUNT(idTestResult)\n' + 'FROM TestResults\n' + 'WHERE idTestResultParent = %s\n' + ' AND enmStatus = %s\n' + , ( oTestResult.idTestResult, TestResultData.ksTestStatus_Running,)); + cOpenSubTest = self._oDb.fetchOne()[0]; + assert cOpenSubTest == 0, 'cOpenSubTest=%d - %s' % (cOpenSubTest, oTestResult,); + assert oTestResult.enmStatus == TestResultData.ksTestStatus_Running; + + # + # Make sure the reporter isn't lying about successes or error counts. + # + self._oDb.execute('SELECT COALESCE(SUM(cErrors), 0)\n' + 'FROM TestResults\n' + 'WHERE idTestResultParent = %s\n' + , ( oTestResult.idTestResult, )); + cMinErrors = self._oDb.fetchOne()[0] + oTestResult.cErrors; + cErrors = max(cErrors, cMinErrors); + if cErrors > 0 and enmStatus == TestResultData.ksTestStatus_Success: + enmStatus = TestResultData.ksTestStatus_Failure + + # + # Do the update. + # + if tsDone is None: + self._oDb.execute('UPDATE TestResults\n' + 'SET cErrors = %s,\n' + ' enmStatus = %s,\n' + ' tsElapsed = CURRENT_TIMESTAMP - tsCreated\n' + 'WHERE idTestResult = %s\n' + 'RETURNING tsElapsed' + , ( cErrors, enmStatus, oTestResult.idTestResult,) ); + else: + self._oDb.execute('UPDATE TestResults\n' + 'SET cErrors = %s,\n' + ' enmStatus = %s,\n' + ' tsElapsed = TIMESTAMP WITH TIME ZONE %s - tsCreated\n' + 'WHERE idTestResult = %s\n' + 'RETURNING tsElapsed' + , ( cErrors, enmStatus, tsDone, oTestResult.idTestResult,) ); + + oTestResult.tsElapsed = self._oDb.fetchOne()[0]; + oTestResult.enmStatus = enmStatus; + oTestResult.cErrors = cErrors; + + self._oDb.maybeCommit(fCommit); + return None; + + def _doPopHint(self, aoStack, cStackEntries, dCounts, idTestSet): + """ Executes a PopHint. """ + assert cStackEntries >= 0; + while len(aoStack) > cStackEntries: + if aoStack[0].enmStatus == TestResultData.ksTestStatus_Running: + self._newFailureDetails(aoStack[0].idTestResult, idTestSet, 'XML error: Missing </Test>', dCounts); + self._completeTestResults(aoStack[0], tsDone = None, cErrors = 1, + enmStatus = TestResultData.ksTestStatus_Failure, fCommit = True); + aoStack.pop(0); + return True; + + + @staticmethod + def _validateElement(sName, dAttribs, fClosed): + """ + Validates an element and its attributes. + """ + + # + # Validate attributes by name. + # + + # Validate integer attributes. + for sAttr in [ 'errors', 'testdepth' ]: + if sAttr in dAttribs: + try: + _ = int(dAttribs[sAttr]); + except: + return 'Element %s has an invalid %s attribute value: %s.' % (sName, sAttr, dAttribs[sAttr],); + + # Validate long attributes. + for sAttr in [ 'value', ]: + if sAttr in dAttribs: + try: + _ = long(dAttribs[sAttr]); # pylint: disable=redefined-variable-type + except: + return 'Element %s has an invalid %s attribute value: %s.' % (sName, sAttr, dAttribs[sAttr],); + + # Validate string attributes. + for sAttr in [ 'name', 'text' ]: # 'unit' can be zero length. + if sAttr in dAttribs and not dAttribs[sAttr]: + return 'Element %s has an empty %s attribute value.' % (sName, sAttr,); + + # Validate the timestamp attribute. + if 'timestamp' in dAttribs: + (dAttribs['timestamp'], sError) = ModelDataBase.validateTs(dAttribs['timestamp'], fAllowNull = False); + if sError is not None: + return 'Element %s has an invalid timestamp ("%s"): %s' % (sName, dAttribs['timestamp'], sError,); + + + # + # Check that attributes that are required are present. + # We ignore extra attributes. + # + dElementAttribs = \ + { + 'Test': [ 'timestamp', 'name', ], + 'Value': [ 'timestamp', 'name', 'unit', 'value', ], + 'FailureDetails': [ 'timestamp', 'text', ], + 'Passed': [ 'timestamp', ], + 'Skipped': [ 'timestamp', ], + 'Failed': [ 'timestamp', 'errors', ], + 'TimedOut': [ 'timestamp', 'errors', ], + 'End': [ 'timestamp', ], + 'PushHint': [ 'testdepth', ], + 'PopHint': [ 'testdepth', ], + }; + if sName not in dElementAttribs: + return 'Unknown element "%s".' % (sName,); + for sAttr in dElementAttribs[sName]: + if sAttr not in dAttribs: + return 'Element %s requires attribute "%s".' % (sName, sAttr); + + # + # Only the Test element can (and must) remain open. + # + if sName == 'Test' and fClosed: + return '<Test/> is not allowed.'; + if sName != 'Test' and not fClosed: + return 'All elements except <Test> must be closed.'; + + return None; + + @staticmethod + def _parseElement(sElement): + """ + Parses an element. + + """ + # + # Element level bits. + # + sName = sElement.split()[0]; + sElement = sElement[len(sName):]; + + fClosed = sElement[-1] == '/'; + if fClosed: + sElement = sElement[:-1]; + + # + # Attributes. + # + sError = None; + dAttribs = {}; + sElement = sElement.strip(); + while sElement: + # Extract attribute name. + off = sElement.find('='); + if off < 0 or not sElement[:off].isalnum(): + sError = 'Attributes shall have alpha numberical names and have values.'; + break; + sAttr = sElement[:off]; + + # Extract attribute value. + if off + 2 >= len(sElement) or sElement[off + 1] != '"': + sError = 'Attribute (%s) value is missing or not in double quotes.' % (sAttr,); + break; + off += 2; + offEndQuote = sElement.find('"', off); + if offEndQuote < 0: + sError = 'Attribute (%s) value is missing end quotation mark.' % (sAttr,); + break; + sValue = sElement[off:offEndQuote]; + + # Check for duplicates. + if sAttr in dAttribs: + sError = 'Attribute "%s" appears more than once.' % (sAttr,); + break; + + # Unescape the value. + sValue = sValue.replace('<', '<'); + sValue = sValue.replace('>', '>'); + sValue = sValue.replace(''', '\''); + sValue = sValue.replace('"', '"'); + sValue = sValue.replace('
', '\n'); + sValue = sValue.replace('
', '\r'); + sValue = sValue.replace('&', '&'); # last + + # Done. + dAttribs[sAttr] = sValue; + + # advance + sElement = sElement[offEndQuote + 1:]; + sElement = sElement.lstrip(); + + # + # Validate the element before we return. + # + if sError is None: + sError = TestResultLogic._validateElement(sName, dAttribs, fClosed); + + return (sName, dAttribs, sError) + + def _handleElement(self, sName, dAttribs, idTestSet, aoStack, aaiHints, dCounts): + """ + Worker for processXmlStream that handles one element. + + Returns None on success, error string on bad XML or similar. + Raises exception on hanging offence and on database error. + """ + if sName == 'Test': + iNestingDepth = aoStack[0].iNestingDepth + 1 if aoStack else 0; + aoStack.insert(0, self._newTestResult(idTestResultParent = aoStack[0].idTestResult, idTestSet = idTestSet, + tsCreated = dAttribs['timestamp'], sName = dAttribs['name'], + iNestingDepth = iNestingDepth, dCounts = dCounts, fCommit = True) ); + + elif sName == 'Value': + self._newTestValue(idTestResult = aoStack[0].idTestResult, idTestSet = idTestSet, tsCreated = dAttribs['timestamp'], + sName = dAttribs['name'], sUnit = dAttribs['unit'], lValue = long(dAttribs['value']), + dCounts = dCounts, fCommit = True); + + elif sName == 'FailureDetails': + self._newFailureDetails(idTestResult = aoStack[0].idTestResult, idTestSet = idTestSet, + tsCreated = dAttribs['timestamp'], sText = dAttribs['text'], dCounts = dCounts, + fCommit = True); + + elif sName == 'Passed': + self._completeTestResults(aoStack[0], tsDone = dAttribs['timestamp'], + enmStatus = TestResultData.ksTestStatus_Success, fCommit = True); + + elif sName == 'Skipped': + self._completeTestResults(aoStack[0], tsDone = dAttribs['timestamp'], + enmStatus = TestResultData.ksTestStatus_Skipped, fCommit = True); + + elif sName == 'Failed': + self._completeTestResults(aoStack[0], tsDone = dAttribs['timestamp'], cErrors = int(dAttribs['errors']), + enmStatus = TestResultData.ksTestStatus_Failure, fCommit = True); + + elif sName == 'TimedOut': + self._completeTestResults(aoStack[0], tsDone = dAttribs['timestamp'], cErrors = int(dAttribs['errors']), + enmStatus = TestResultData.ksTestStatus_TimedOut, fCommit = True); + + elif sName == 'End': + self._completeTestResults(aoStack[0], tsDone = dAttribs['timestamp'], + cErrors = int(dAttribs.get('errors', '1')), + enmStatus = TestResultData.ksTestStatus_Success, fCommit = True); + + elif sName == 'PushHint': + if len(aaiHints) > 1: + return 'PushHint cannot be nested.' + + aaiHints.insert(0, [len(aoStack), int(dAttribs['testdepth'])]); + + elif sName == 'PopHint': + if not aaiHints: + return 'No hint to pop.' + + iDesiredTestDepth = int(dAttribs['testdepth']); + cStackEntries, iTestDepth = aaiHints.pop(0); + self._doPopHint(aoStack, cStackEntries, dCounts, idTestSet); # Fake the necessary '<End/></Test>' tags. + if iDesiredTestDepth != iTestDepth: + return 'PopHint tag has different testdepth: %d, on stack %d.' % (iDesiredTestDepth, iTestDepth); + else: + return 'Unexpected element "%s".' % (sName,); + return None; + + + def processXmlStream(self, sXml, idTestSet): + """ + Processes the "XML" stream section given in sXml. + + The sXml isn't a complete XML document, even should we save up all sXml + for a given set, they may not form a complete and well formed XML + document since the test may be aborted, abend or simply be buggy. We + therefore do our own parsing and treat the XML tags as commands more + than anything else. + + Returns (sError, fUnforgivable), where sError is None on success. + May raise database exception. + """ + aoStack = self._getResultStack(idTestSet); # [0] == top; [-1] == bottom. + if not aoStack: + return ('No open results', True); + self._oDb.dprint('** processXmlStream len(aoStack)=%s' % (len(aoStack),)); + #self._oDb.dprint('processXmlStream: %s' % (self._stringifyStack(aoStack),)); + #self._oDb.dprint('processXmlStream: sXml=%s' % (sXml,)); + + dCounts = {}; + aaiHints = []; + sError = None; + + fExpectCloseTest = False; + sXml = sXml.strip(); + while sXml: + if sXml.startswith('</Test>'): # Only closing tag. + offNext = len('</Test>'); + if len(aoStack) <= 1: + sError = 'Trying to close the top test results.' + break; + # ASSUMES that we've just seen an <End/>, <Passed/>, <Failed/>, + # <TimedOut/> or <Skipped/> tag earlier in this call! + if aoStack[0].enmStatus == TestResultData.ksTestStatus_Running or not fExpectCloseTest: + sError = 'Missing <End/>, <Passed/>, <Failed/>, <TimedOut/> or <Skipped/> tag.'; + break; + aoStack.pop(0); + fExpectCloseTest = False; + + elif fExpectCloseTest: + sError = 'Expected </Test>.' + break; + + elif sXml.startswith('<?xml '): # Ignore (included files). + offNext = sXml.find('?>'); + if offNext < 0: + sError = 'Unterminated <?xml ?> element.'; + break; + offNext += 2; + + elif sXml[0] == '<': + # Parse and check the tag. + if not sXml[1].isalpha(): + sError = 'Malformed element.'; + break; + offNext = sXml.find('>') + if offNext < 0: + sError = 'Unterminated element.'; + break; + (sName, dAttribs, sError) = self._parseElement(sXml[1:offNext]); + offNext += 1; + if sError is not None: + break; + + # Handle it. + try: + sError = self._handleElement(sName, dAttribs, idTestSet, aoStack, aaiHints, dCounts); + except TestResultHangingOffence as oXcpt: + self._inhumeTestResults(aoStack, idTestSet, str(oXcpt)); + return (str(oXcpt), True); + + + fExpectCloseTest = sName in [ 'End', 'Passed', 'Failed', 'TimedOut', 'Skipped', ]; + else: + sError = 'Unexpected content.'; + break; + + # Advance. + sXml = sXml[offNext:]; + sXml = sXml.lstrip(); + + # + # Post processing checks. + # + if sError is None and fExpectCloseTest: + sError = 'Expected </Test> before the end of the XML section.' + elif sError is None and aaiHints: + sError = 'Expected </PopHint> before the end of the XML section.' + if aaiHints: + self._doPopHint(aoStack, aaiHints[-1][0], dCounts, idTestSet); + + # + # Log the error. + # + if sError is not None: + SystemLogLogic(self._oDb).addEntry(SystemLogData.ksEvent_XmlResultMalformed, + 'idTestSet=%s idTestResult=%s XML="%s" %s' + % ( idTestSet, + aoStack[0].idTestResult if aoStack else -1, + sXml[:min(len(sXml), 30)], + sError, ), + cHoursRepeat = 6, fCommit = True); + return (sError, False); + + + + + +# +# Unit testing. +# + +# pylint: disable=missing-docstring +class TestResultDataTestCase(ModelDataBaseTestCase): + def setUp(self): + self.aoSamples = [TestResultData(),]; + +class TestResultValueDataTestCase(ModelDataBaseTestCase): + def setUp(self): + self.aoSamples = [TestResultValueData(),]; + +if __name__ == '__main__': + unittest.main(); + # not reached. + |