# -*- 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 . 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 = ''): """ 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 ', 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 ' is not allowed.'; if sName != 'Test' and not fClosed: return 'All elements except 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 '' 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(''): # Only closing tag. offNext = len(''); if len(aoStack) <= 1: sError = 'Trying to close the top test results.' break; # ASSUMES that we've just seen an , , , # or tag earlier in this call! if aoStack[0].enmStatus == TestResultData.ksTestStatus_Running or not fExpectCloseTest: sError = 'Missing , , , or tag.'; break; aoStack.pop(0); fExpectCloseTest = False; elif fExpectCloseTest: sError = 'Expected .' break; elif sXml.startswith(''); if offNext < 0: sError = 'Unterminated 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 before the end of the XML section.' elif sError is None and aaiHints: sError = 'Expected 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.