diff options
Diffstat (limited to '')
-rwxr-xr-x | src/VBox/ValidationKit/testmanager/core/testset.py | 869 |
1 files changed, 869 insertions, 0 deletions
diff --git a/src/VBox/ValidationKit/testmanager/core/testset.py b/src/VBox/ValidationKit/testmanager/core/testset.py new file mode 100755 index 00000000..e9e408fe --- /dev/null +++ b/src/VBox/ValidationKit/testmanager/core/testset.py @@ -0,0 +1,869 @@ +# -*- coding: utf-8 -*- +# $Id: testset.py $ + +""" +Test Manager - TestSet. +""" + +__copyright__ = \ +""" +Copyright (C) 2012-2023 Oracle and/or its affiliates. + +This file is part of VirtualBox base platform packages, as +available from https://www.virtualbox.org. + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation, in version 3 of the +License. + +This program is distributed in the hope that it will be useful, but +WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, see <https://www.gnu.org/licenses>. + +The contents of this file may alternatively be used under the terms +of the Common Development and Distribution License Version 1.0 +(CDDL), a copy of it is provided in the "COPYING.CDDL" file included +in the VirtualBox distribution, in which case the provisions of the +CDDL are applicable instead of those of the GPL. + +You may elect to license modified versions of this file under the +terms and conditions of either the GPL or the CDDL or both. + +SPDX-License-Identifier: GPL-3.0-only OR CDDL-1.0 +""" +__version__ = "$Revision: 155244 $" + + +# Standard python imports. +import os; +import zipfile; +import unittest; + +# Validation Kit imports. +from common import utils; +from testmanager import config; +from testmanager.core import db; +from testmanager.core.base import ModelDataBase, ModelDataBaseTestCase, ModelLogicBase, \ + TMExceptionBase, TMTooManyRows, TMRowNotFound; +from testmanager.core.testbox import TestBoxData; +from testmanager.core.testresults import TestResultFileDataEx; + + +class TestSetData(ModelDataBase): + """ + TestSet 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 = 'idTestSet'; + + ksParam_idTestSet = 'TestSet_idTestSet'; + ksParam_tsConfig = 'TestSet_tsConfig'; + ksParam_tsCreated = 'TestSet_tsCreated'; + ksParam_tsDone = 'TestSet_tsDone'; + ksParam_enmStatus = 'TestSet_enmStatus'; + ksParam_idBuild = 'TestSet_idBuild'; + ksParam_idBuildCategory = 'TestSet_idBuildCategory'; + ksParam_idBuildTestSuite = 'TestSet_idBuildTestSuite'; + ksParam_idGenTestBox = 'TestSet_idGenTestBox'; + ksParam_idTestBox = 'TestSet_idTestBox'; + ksParam_idSchedGroup = 'TestSet_idSchedGroup'; + ksParam_idTestGroup = 'TestSet_idTestGroup'; + ksParam_idGenTestCase = 'TestSet_idGenTestCase'; + ksParam_idTestCase = 'TestSet_idTestCase'; + ksParam_idGenTestCaseArgs = 'TestSet_idGenTestCaseArgs'; + ksParam_idTestCaseArgs = 'TestSet_idTestCaseArgs'; + ksParam_idTestResult = 'TestSet_idTestResult'; + ksParam_sBaseFilename = 'TestSet_sBaseFilename'; + ksParam_iGangMemberNo = 'TestSet_iGangMemberNo'; + ksParam_idTestSetGangLeader = 'TestSet_idTestSetGangLeader'; + + kasAllowNullAttributes = [ 'tsDone', 'idBuildTestSuite', 'idTestSetGangLeader' ]; + kasValidValues_enmStatus = [ + ksTestStatus_Running, + ksTestStatus_Success, + ksTestStatus_Skipped, + ksTestStatus_BadTestBox, + ksTestStatus_Aborted, + ksTestStatus_Failure, + ksTestStatus_TimedOut, + ksTestStatus_Rebooted, + ]; + kiMin_iGangMemberNo = 0; + kiMax_iGangMemberNo = 1023; + + + kcDbColumns = 20; + + def __init__(self): + ModelDataBase.__init__(self); + + # + # Initialize with defaults. + # See the database for explanations of each of these fields. + # + self.idTestSet = None; + self.tsConfig = None; + self.tsCreated = None; + self.tsDone = None; + self.enmStatus = 'running'; + self.idBuild = None; + self.idBuildCategory = None; + self.idBuildTestSuite = None; + self.idGenTestBox = None; + self.idTestBox = None; + self.idSchedGroup = None; + self.idTestGroup = None; + self.idGenTestCase = None; + self.idTestCase = None; + self.idGenTestCaseArgs = None; + self.idTestCaseArgs = None; + self.idTestResult = None; + self.sBaseFilename = None; + self.iGangMemberNo = 0; + self.idTestSetGangLeader = None; + + def initFromDbRow(self, aoRow): + """ + Internal worker for initFromDbWithId and initFromDbWithGenId as well as + TestBoxSetLogic. + """ + + if aoRow is None: + raise TMRowNotFound('TestSet not found.'); + + self.idTestSet = aoRow[0]; + self.tsConfig = aoRow[1]; + self.tsCreated = aoRow[2]; + self.tsDone = aoRow[3]; + self.enmStatus = aoRow[4]; + self.idBuild = aoRow[5]; + self.idBuildCategory = aoRow[6]; + self.idBuildTestSuite = aoRow[7]; + self.idGenTestBox = aoRow[8]; + self.idTestBox = aoRow[9]; + self.idSchedGroup = aoRow[10]; + self.idTestGroup = aoRow[11]; + self.idGenTestCase = aoRow[12]; + self.idTestCase = aoRow[13]; + self.idGenTestCaseArgs = aoRow[14]; + self.idTestCaseArgs = aoRow[15]; + self.idTestResult = aoRow[16]; + self.sBaseFilename = aoRow[17]; + self.iGangMemberNo = aoRow[18]; + self.idTestSetGangLeader = aoRow[19]; + return self; + + + def initFromDbWithId(self, oDb, idTestSet): + """ + Initialize the object from the database. + """ + oDb.execute('SELECT *\n' + 'FROM TestSets\n' + 'WHERE idTestSet = %s\n' + , (idTestSet, ) ); + aoRow = oDb.fetchOne() + if aoRow is None: + raise TMRowNotFound('idTestSet=%s not found' % (idTestSet,)); + return self.initFromDbRow(aoRow); + + + def openFile(self, sFilename, sMode = 'rb'): + """ + Opens a file. + + Returns (oFile, cbFile, fIsStream) on success. + Returns (None, sErrorMsg, None) on failure. + Will not raise exceptions, unless the class instance is invalid. + """ + assert sMode in [ 'rb', 'r', 'rU' ]; + + # Try raw file first. + sFile1 = os.path.join(config.g_ksFileAreaRootDir, self.sBaseFilename + '-' + sFilename); + try: + oFile = open(sFile1, sMode); # pylint: disable=consider-using-with,unspecified-encoding + return (oFile, os.fstat(oFile.fileno()).st_size, False); + except Exception as oXcpt1: + # Try the zip archive next. + sFile2 = os.path.join(config.g_ksZipFileAreaRootDir, self.sBaseFilename + '.zip'); + try: + oZipFile = zipfile.ZipFile(sFile2, 'r'); # pylint: disable=consider-using-with + oFile = oZipFile.open(sFilename, sMode if sMode != 'rb' else 'r'); # pylint: disable=consider-using-with + cbFile = oZipFile.getinfo(sFilename).file_size; + return (oFile, cbFile, True); + except Exception as oXcpt2: + # Construct a meaningful error message. + try: + if os.path.exists(sFile1): + return (None, 'Error opening "%s": %s' % (sFile1, oXcpt1), None); + if not os.path.exists(sFile2): + return (None, 'File "%s" not found. [%s, %s]' % (sFilename, sFile1, sFile2,), None); + return (None, 'Error opening "%s" inside "%s": %s' % (sFilename, sFile2, oXcpt2), None); + except Exception as oXcpt3: + return (None, 'OMG! %s; %s; %s' % (oXcpt1, oXcpt2, oXcpt3,), None); + return (None, 'Code not reachable!', None); + + def createFile(self, sFilename, sMode = 'wb'): + """ + Creates a new file. + + Returns oFile on success. + Returns sErrorMsg on failure. + """ + assert sMode in [ 'wb', 'w', 'wU' ]; + + # Try raw file first. + sFile1 = os.path.join(config.g_ksFileAreaRootDir, self.sBaseFilename + '-' + sFilename); + try: + if not os.path.exists(os.path.dirname(sFile1)): + os.makedirs(os.path.dirname(sFile1), 0o755); + oFile = open(sFile1, sMode); # pylint: disable=consider-using-with,unspecified-encoding + except Exception as oXcpt1: + return str(oXcpt1); + return oFile; + + @staticmethod + def findLogOffsetForTimestamp(sLogContent, tsTimestamp, offStart = 0, fAfter = False): + """ + Log parsing utility function for finding the offset for the given timestamp. + + We ASSUME the log lines are prefixed with UTC timestamps on the format + '09:43:55.789353'. + + Return index into the sLogContent string, 0 if not found. + """ + # Turn tsTimestamp into a string compatible with what we expect to find in the log. + oTsZulu = db.dbTimestampToZuluDatetime(tsTimestamp); + sWantedTs = oTsZulu.strftime('%H:%M:%S.%f'); + assert len(sWantedTs) == 15; + + # Now loop thru the string, line by line. + offRet = offStart; + off = offStart; + while True: + sThisTs = sLogContent[off : off + 15]; + if len(sThisTs) >= 15 \ + and sThisTs[2] == ':' \ + and sThisTs[5] == ':' \ + and sThisTs[8] == '.' \ + and sThisTs[14] in '0123456789': + if sThisTs < sWantedTs: + offRet = off; + elif sThisTs == sWantedTs: + if not fAfter: + return off; + offRet = off; + else: + if fAfter: + offRet = off; + break; + + # next line. + off = sLogContent.find('\n', off); + if off < 0: + if fAfter: + offRet = len(sLogContent); + break; + off += 1; + + return offRet; + + @staticmethod + def extractLogSection(sLogContent, tsStart, tsLast): + """ + Returns log section from tsStart to tsLast (or all if we cannot make sense of it). + """ + offStart = TestSetData.findLogOffsetForTimestamp(sLogContent, tsStart); + offEnd = TestSetData.findLogOffsetForTimestamp(sLogContent, tsLast, offStart, fAfter = True); + return sLogContent[offStart : offEnd]; + + @staticmethod + def extractLogSectionElapsed(sLogContent, tsStart, tsElapsed): + """ + Returns log section from tsStart and tsElapsed forward (or all if we cannot make sense of it). + """ + tsStart = db.dbTimestampToZuluDatetime(tsStart); + tsLast = tsStart + tsElapsed; + return TestSetData.extractLogSection(sLogContent, tsStart, tsLast); + + + +class TestSetLogic(ModelLogicBase): + """ + TestSet logic. + """ + + + def __init__(self, oDb): + ModelLogicBase.__init__(self, oDb); + + + def tryFetch(self, idTestSet): + """ + Attempts to fetch a test set. + + Returns a TestSetData object on success. + Returns None if no status was found. + Raises exception on other errors. + """ + self._oDb.execute('SELECT *\n' + 'FROM TestSets\n' + 'WHERE idTestSet = %s\n', + (idTestSet,)); + if self._oDb.getRowCount() == 0: + return None; + oData = TestSetData(); + return oData.initFromDbRow(self._oDb.fetchOne()); + + def strTabString(self, sString, fCommit = False): + """ + Gets the string table id for the given string, adding it if new. + """ + ## @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]; + + def complete(self, idTestSet, sStatus, fCommit = False): + """ + Completes the testset. + Returns the test set ID of the gang leader, None if no gang involvement. + Raises exceptions on database errors and invalid input. + """ + + assert sStatus != TestSetData.ksTestStatus_Running; + + # + # Get the basic test set data and check if there is anything to do here. + # + oData = TestSetData().initFromDbWithId(self._oDb, idTestSet); + if oData.enmStatus != TestSetData.ksTestStatus_Running: + raise TMExceptionBase('TestSet %s is already completed as %s.' % (idTestSet, oData.enmStatus)); + if oData.idTestResult is None: + raise self._oDb.integrityException('idTestResult is NULL for TestSet %u' % (idTestSet,)); + + # + # Close open sub test results, count these as errors. + # Note! No need to propagate error counts here. Only one tree line will + # have open sets, and it will go all the way to the root. + # + self._oDb.execute('SELECT idTestResult\n' + 'FROM TestResults\n' + 'WHERE idTestSet = %s\n' + ' AND enmStatus = %s\n' + ' AND idTestResult <> %s\n' + 'ORDER BY idTestResult DESC\n' + , (idTestSet, TestSetData.ksTestStatus_Running, oData.idTestResult)); + aaoRows = self._oDb.fetchAll(); + if aaoRows: + idStr = self.strTabString('Unclosed test result', fCommit = fCommit); + for aoRow in aaoRows: + self._oDb.execute('UPDATE TestResults\n' + 'SET enmStatus = \'failure\',\n' + ' tsElapsed = CURRENT_TIMESTAMP - tsCreated,\n' + ' cErrors = cErrors + 1\n' + 'WHERE idTestResult = %s\n' + , (aoRow[0],)); + self._oDb.execute('INSERT INTO TestResultMsgs (idTestResult, idTestSet, idStrMsg, enmLevel)\n' + 'VALUES ( %s, %s, %s, \'failure\'::TestResultMsgLevel_T)\n' + , (aoRow[0], idTestSet, idStr,)); + + # + # If it's a success result, check it against error counters. + # + if sStatus not in TestSetData.kasBadTestStatuses: + self._oDb.execute('SELECT COUNT(*)\n' + 'FROM TestResults\n' + 'WHERE idTestSet = %s\n' + ' AND cErrors > 0\n' + , (idTestSet,)); + cErrors = self._oDb.fetchOne()[0]; + if cErrors > 0: + sStatus = TestSetData.ksTestStatus_Failure; + + # + # If it's an pure 'failure', check for timeouts and propagate it. + # + if sStatus == TestSetData.ksTestStatus_Failure: + self._oDb.execute('SELECT COUNT(*)\n' + 'FROM TestResults\n' + 'WHERE idTestSet = %s\n' + ' AND enmStatus = %s\n' + , ( idTestSet, TestSetData.ksTestStatus_TimedOut, )); + if self._oDb.fetchOne()[0] > 0: + sStatus = TestSetData.ksTestStatus_TimedOut; + + # + # Complete the top level test result and then the test set. + # + self._oDb.execute('UPDATE TestResults\n' + 'SET cErrors = (SELECT COALESCE(SUM(cErrors), 0)\n' + ' FROM TestResults\n' + ' WHERE idTestResultParent = %s)\n' + 'WHERE idTestResult = %s\n' + 'RETURNING cErrors\n' + , (oData.idTestResult, oData.idTestResult)); + cErrors = self._oDb.fetchOne()[0]; + if cErrors == 0 and sStatus in TestSetData.kasBadTestStatuses: + self._oDb.execute('UPDATE TestResults\n' + 'SET cErrors = 1\n' + 'WHERE idTestResult = %s\n' + , (oData.idTestResult,)); + elif cErrors > 0 and sStatus not in TestSetData.kasBadTestStatuses: + sStatus = TestSetData.ksTestStatus_Failure; # Impossible. + self._oDb.execute('UPDATE TestResults\n' + 'SET enmStatus = %s,\n' + ' tsElapsed = CURRENT_TIMESTAMP - tsCreated\n' + 'WHERE idTestResult = %s\n' + , (sStatus, oData.idTestResult,)); + + self._oDb.execute('UPDATE TestSets\n' + 'SET enmStatus = %s,\n' + ' tsDone = CURRENT_TIMESTAMP\n' + 'WHERE idTestSet = %s\n' + , (sStatus, idTestSet,)); + + self._oDb.maybeCommit(fCommit); + return oData.idTestSetGangLeader; + + def completeAsAbandoned(self, idTestSet, fCommit = False): + """ + Completes the testset as abandoned if necessary. + + See scenario #9: + file://../../docs/AutomaticTestingRevamp.html#cleaning-up-abandond-testcase + + Returns True if successfully completed as abandond, False if it's already + completed, and raises exceptions under exceptional circumstances. + """ + + # + # Get the basic test set data and check if there is anything to do here. + # + oData = self.tryFetch(idTestSet); + if oData is None: + return False; + if oData.enmStatus != TestSetData.ksTestStatus_Running: + return False; + + if oData.idTestResult is not None: + # + # Clean up test results, adding a message why they failed. + # + self._oDb.execute('UPDATE TestResults\n' + 'SET enmStatus = \'failure\',\n' + ' tsElapsed = CURRENT_TIMESTAMP - tsCreated,\n' + ' cErrors = cErrors + 1\n' + 'WHERE idTestSet = %s\n' + ' AND enmStatus = \'running\'::TestStatus_T\n' + , (idTestSet,)); + + idStr = self.strTabString('The test was abandond by the testbox', fCommit = fCommit); + self._oDb.execute('INSERT INTO TestResultMsgs (idTestResult, idTestSet, idStrMsg, enmLevel)\n' + 'VALUES ( %s, %s, %s, \'failure\'::TestResultMsgLevel_T)\n' + , (oData.idTestResult, idTestSet, idStr,)); + + # + # Complete the testset. + # + self._oDb.execute('UPDATE TestSets\n' + 'SET enmStatus = \'failure\',\n' + ' tsDone = CURRENT_TIMESTAMP\n' + 'WHERE idTestSet = %s\n' + ' AND enmStatus = \'running\'::TestStatus_T\n' + , (idTestSet,)); + + self._oDb.maybeCommit(fCommit); + return True; + + def completeAsGangGatheringTimeout(self, idTestSet, fCommit = False): + """ + Completes the testset with a gang-gathering timeout. + Raises exceptions on database errors and invalid input. + """ + # + # Get the basic test set data and check if there is anything to do here. + # + oData = TestSetData().initFromDbWithId(self._oDb, idTestSet); + if oData.enmStatus != TestSetData.ksTestStatus_Running: + raise TMExceptionBase('TestSet %s is already completed as %s.' % (idTestSet, oData.enmStatus)); + if oData.idTestResult is None: + raise self._oDb.integrityException('idTestResult is NULL for TestSet %u' % (idTestSet,)); + + # + # Complete the top level test result and then the test set. + # + self._oDb.execute('UPDATE TestResults\n' + 'SET enmStatus = \'failure\',\n' + ' tsElapsed = CURRENT_TIMESTAMP - tsCreated,\n' + ' cErrors = cErrors + 1\n' + 'WHERE idTestSet = %s\n' + ' AND enmStatus = \'running\'::TestStatus_T\n' + , (idTestSet,)); + + idStr = self.strTabString('Gang gathering timed out', fCommit = fCommit); + self._oDb.execute('INSERT INTO TestResultMsgs (idTestResult, idTestSet, idStrMsg, enmLevel)\n' + 'VALUES ( %s, %s, %s, \'failure\'::TestResultMsgLevel_T)\n' + , (oData.idTestResult, idTestSet, idStr,)); + + self._oDb.execute('UPDATE TestSets\n' + 'SET enmStatus = \'failure\',\n' + ' tsDone = CURRENT_TIMESTAMP\n' + 'WHERE idTestSet = %s\n' + , (idTestSet,)); + + self._oDb.maybeCommit(fCommit); + return True; + + def createFile(self, oTestSet, sName, sMime, sKind, sDesc, cbFile, fCommit = False): # pylint: disable=too-many-locals + """ + Creates a file and associating with the current test result record in + the test set. + + Returns file object that the file content can be written to. + Raises exception on database error, I/O errors, if there are too many + files in the test set or if they take up too much disk space. + + The caller (testboxdisp.py) is expected to do basic input validation, + so we skip that and get on with the bits only we can do. + """ + + # + # Furhter input and limit checks. + # + if oTestSet.enmStatus != TestSetData.ksTestStatus_Running: + raise TMExceptionBase('Cannot create files on a test set with status "%s".' % (oTestSet.enmStatus,)); + + self._oDb.execute('SELECT TestResultStrTab.sValue\n' + 'FROM TestResultFiles,\n' + ' TestResults,\n' + ' TestResultStrTab\n' + 'WHERE TestResults.idTestSet = %s\n' + ' AND TestResultFiles.idTestResult = TestResults.idTestResult\n' + ' AND TestResultStrTab.idStr = TestResultFiles.idStrFile\n' + , ( oTestSet.idTestSet,)); + if self._oDb.getRowCount() + 1 > config.g_kcMaxUploads: + raise TMExceptionBase('Uploaded too many files already (%d).' % (self._oDb.getRowCount(),)); + + dFiles = {} + cbTotalFiles = 0; + for aoRow in self._oDb.fetchAll(): + dFiles[aoRow[0].lower()] = 1; # For determining a unique filename further down. + sFile = os.path.join(config.g_ksFileAreaRootDir, oTestSet.sBaseFilename + '-' + aoRow[0]); + try: + cbTotalFiles += os.path.getsize(sFile); + except: + cbTotalFiles += config.g_kcMbMaxUploadSingle * 1048576; + if (cbTotalFiles + cbFile + 1048575) / 1048576 > config.g_kcMbMaxUploadTotal: + raise TMExceptionBase('Will exceed total upload limit: %u bytes + %u bytes > %s MiB.' \ + % (cbTotalFiles, cbFile, config.g_kcMbMaxUploadTotal)); + + # + # Create a new file. + # + self._oDb.execute('SELECT idTestResult\n' + 'FROM TestResults\n' + 'WHERE idTestSet = %s\n' + ' AND enmStatus = \'running\'::TestStatus_T\n' + 'ORDER BY idTestResult DESC\n' + 'LIMIT 1\n' + % ( oTestSet.idTestSet, )); + if self._oDb.getRowCount() < 1: + raise TMExceptionBase('No open test results - someone committed a capital offence or we ran into a race.'); + idTestResult = self._oDb.fetchOne()[0]; + + if sName.lower() in dFiles: + # Note! There is in theory a race here, but that's something the + # test driver doing parallel upload with non-unique names + # should worry about. The TD should always avoid this path. + sOrgName = sName; + for i in range(2, config.g_kcMaxUploads + 6): + sName = '%s-%s' % (i, sName,); + if sName not in dFiles: + break; + sName = None; + if sName is None: + raise TMExceptionBase('Failed to find unique name for %s.' % (sOrgName,)); + + self._oDb.execute('INSERT INTO TestResultFiles(idTestResult, idTestSet, idStrFile, idStrDescription,\n' + ' idStrKind, idStrMime)\n' + 'VALUES (%s, %s, %s, %s, %s, %s)\n' + , ( idTestResult, + oTestSet.idTestSet, + self.strTabString(sName), + self.strTabString(sDesc), + self.strTabString(sKind), + self.strTabString(sMime), + )); + + oFile = oTestSet.createFile(sName, 'wb'); + if utils.isString(oFile): + raise TMExceptionBase('Error creating "%s": %s' % (sName, oFile)); + self._oDb.maybeCommit(fCommit); + return oFile; + + def getGang(self, idTestSetGangLeader): + """ + Returns an array of TestBoxData object representing the gang for the given testset. + """ + self._oDb.execute('SELECT TestBoxesWithStrings.*\n' + 'FROM TestBoxesWithStrings,\n' + ' TestSets' + 'WHERE TestSets.idTestSetGangLeader = %s\n' + ' AND TestSets.idGenTestBox = TestBoxesWithStrings.idGenTestBox\n' + 'ORDER BY iGangMemberNo ASC\n' + , ( idTestSetGangLeader,)); + aaoRows = self._oDb.fetchAll(); + aoTestBoxes = []; + for aoRow in aaoRows: + aoTestBoxes.append(TestBoxData().initFromDbRow(aoRow)); + return aoTestBoxes; + + def getFile(self, idTestSet, idTestResultFile): + """ + Gets the TestResultFileEx corresponding to idTestResultFile. + + Raises an exception if the file wasn't found, doesn't belong to + idTestSet, and on DB error. + """ + 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' + ' TestResults\n' + 'WHERE TestResultFiles.idTestResultFile = %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' + ' AND TestResults.idTestResult = TestResultFiles.idTestResult\n' + ' AND TestResults.idTestSet = %s\n' + , ( idTestResultFile, idTestSet, )); + return TestResultFileDataEx().initFromDbRow(self._oDb.fetchOne()); + + + def getById(self, idTestSet): + """ + Get TestSet table record by its id + """ + self._oDb.execute('SELECT *\n' + 'FROM TestSets\n' + 'WHERE idTestSet=%s\n', + (idTestSet,)) + + aRows = self._oDb.fetchAll() + if len(aRows) not in (0, 1): + raise TMTooManyRows('Found more than one test sets with the same credentials. Database structure is corrupted.') + try: + return TestSetData().initFromDbRow(aRows[0]) + except IndexError: + return None + + + def fetchOrphaned(self): + """ + Returns a list of TestSetData objects of orphaned test sets. + + A test set is orphaned if tsDone is NULL and the testbox has created + one or more newer testsets. + """ + + self._oDb.execute('SELECT TestSets.*\n' + 'FROM TestSets,\n' + ' (SELECT idTestSet, idTestBox FROM TestSets WHERE tsDone is NULL) AS t\n' + 'WHERE TestSets.idTestSet = t.idTestSet\n' + ' AND EXISTS(SELECT 1 FROM TestSets st\n' + ' WHERE st.idTestBox = t.idTestBox AND st.idTestSet > t.idTestSet)\n' + ' AND NOT EXISTS(SELECT 1 FROM TestBoxStatuses tbs\n' + ' WHERE tbs.idTestBox = t.idTestBox AND tbs.idTestSet = t.idTestSet)\n' + 'ORDER by TestSets.idTestBox, TestSets.idTestSet' + ); + aoRet = []; + for aoRow in self._oDb.fetchAll(): + aoRet.append(TestSetData().initFromDbRow(aoRow)); + return aoRet; + + def fetchByAge(self, tsNow = None, cHoursBack = 24): + """ + Returns a list of TestSetData objects of a given time period (default is 24 hours). + + Returns None if no testsets stored, + Returns an empty list if no testsets found with given criteria. + """ + if tsNow is None: + tsNow = self._oDb.getCurrentTimestamp(); + + if self._oDb.getRowCount() == 0: + return None; + + self._oDb.execute('(SELECT *\n' + ' FROM TestSets\n' + ' WHERE tsDone <= %s\n' + ' AND tsDone > (%s - interval \'%s hours\')\n' + ')\n' + , ( tsNow, tsNow, cHoursBack, )); + + aoRet = []; + for aoRow in self._oDb.fetchAll(): + aoRet.append(TestSetData().initFromDbRow(aoRow)); + return aoRet; + + def isTestBoxExecutingTooRapidly(self, idTestBox): ## s/To/Too/ + """ + Checks whether the specified test box is executing tests too rapidly. + + The parameters defining too rapid execution are defined in config.py. + + Returns True if it does, False if it doesn't. + May raise database problems. + """ + + self._oDb.execute('(\n' + 'SELECT tsCreated\n' + 'FROM TestSets\n' + 'WHERE idTestBox = %s\n' + ' AND tsCreated >= (CURRENT_TIMESTAMP - interval \'%s seconds\')\n' + ') UNION (\n' + 'SELECT tsCreated\n' + 'FROM TestSets\n' + 'WHERE idTestBox = %s\n' + ' AND tsCreated >= (CURRENT_TIMESTAMP - interval \'%s seconds\')\n' + ' AND enmStatus >= \'failure\'\n' + ')' + , ( idTestBox, config.g_kcSecMinSinceLastTask, + idTestBox, config.g_kcSecMinSinceLastFailedTask, )); + return self._oDb.getRowCount() > 0; + + + # + # The virtual test sheriff interface. + # + + def fetchBadTestBoxIds(self, cHoursBack = 2, tsNow = None, aidFailureReasons = None): + """ + Fetches a list of test box IDs which returned bad-testbox statuses in the + given period (tsDone). + """ + if tsNow is None: + tsNow = self._oDb.getCurrentTimestamp(); + if aidFailureReasons is None: + aidFailureReasons = [ -1, ]; + self._oDb.execute('(SELECT idTestBox\n' + ' FROM TestSets\n' + ' WHERE TestSets.enmStatus = \'bad-testbox\'\n' + ' AND tsDone <= %s\n' + ' AND tsDone > (%s - interval \'%s hours\')\n' + ') UNION (\n' + ' SELECT TestSets.idTestBox\n' + ' FROM TestSets,\n' + ' TestResultFailures\n' + ' WHERE TestSets.tsDone <= %s\n' + ' AND TestSets.tsDone > (%s - interval \'%s hours\')\n' + ' AND TestSets.enmStatus >= \'failure\'::TestStatus_T\n' + ' AND TestSets.idTestSet = TestResultFailures.idTestSet\n' + ' AND TestResultFailures.tsExpire = \'infinity\'::TIMESTAMP\n' + ' AND TestResultFailures.idFailureReason IN (' + + ', '.join([str(i) for i in aidFailureReasons]) + ')\n' + ')\n' + , ( tsNow, tsNow, cHoursBack, + tsNow, tsNow, cHoursBack, )); + return [aoRow[0] for aoRow in self._oDb.fetchAll()]; + + def fetchSetsForTestBox(self, idTestBox, cHoursBack = 2, tsNow = None): + """ + Fetches the TestSet rows for idTestBox for the given period (tsDone), w/o running ones. + + Returns list of TestSetData sorted by tsDone in descending order. + """ + if tsNow is None: + tsNow = self._oDb.getCurrentTimestamp(); + self._oDb.execute('SELECT *\n' + 'FROM TestSets\n' + 'WHERE TestSets.idTestBox = %s\n' + ' AND tsDone IS NOT NULL\n' + ' AND tsDone <= %s\n' + ' AND tsDone > (%s - interval \'%s hours\')\n' + 'ORDER by tsDone DESC\n' + , ( idTestBox, tsNow, tsNow, cHoursBack,)); + return self._dbRowsToModelDataList(TestSetData); + + def fetchFailedSetsWithoutReason(self, cHoursBack = 2, tsNow = None): + """ + Fetches the TestSet failure rows without any currently (CURRENT_TIMESTAMP + not tsNow) assigned failure reason. + + Returns list of TestSetData sorted by tsDone in descending order. + + Note! Includes bad-testbox sets too as it can be useful to analyze these + too even if we normally count them in the 'skipped' category. + """ + if tsNow is None: + tsNow = self._oDb.getCurrentTimestamp(); + self._oDb.execute('SELECT TestSets.*\n' + 'FROM TestSets\n' + ' LEFT OUTER JOIN TestResultFailures\n' + ' ON TestResultFailures.idTestSet = TestSets.idTestSet\n' + ' AND TestResultFailures.tsExpire = \'infinity\'::TIMESTAMP\n' + 'WHERE TestSets.tsDone IS NOT NULL\n' + ' AND TestSets.enmStatus IN ( %s, %s, %s, %s )\n' + ' AND TestSets.tsDone <= %s\n' + ' AND TestSets.tsDone > (%s - interval \'%s hours\')\n' + ' AND TestResultFailures.idTestSet IS NULL\n' + 'ORDER by tsDone DESC\n' + , ( TestSetData.ksTestStatus_Failure, TestSetData.ksTestStatus_TimedOut, + TestSetData.ksTestStatus_Rebooted, TestSetData.ksTestStatus_BadTestBox, + tsNow, + tsNow, cHoursBack,)); + return self._dbRowsToModelDataList(TestSetData); + + + +# +# Unit testing. +# + +# pylint: disable=missing-docstring +class TestSetDataTestCase(ModelDataBaseTestCase): + def setUp(self): + self.aoSamples = [TestSetData(),]; + +if __name__ == '__main__': + unittest.main(); + # not reached. |