From f215e02bf85f68d3a6106c2a1f4f7f063f819064 Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Thu, 11 Apr 2024 10:17:27 +0200 Subject: Adding upstream version 7.0.14-dfsg. Signed-off-by: Daniel Baumann --- .../testmanager/core/failurereason.py | 580 +++++++++++++++++++++ 1 file changed, 580 insertions(+) create mode 100755 src/VBox/ValidationKit/testmanager/core/failurereason.py (limited to 'src/VBox/ValidationKit/testmanager/core/failurereason.py') diff --git a/src/VBox/ValidationKit/testmanager/core/failurereason.py b/src/VBox/ValidationKit/testmanager/core/failurereason.py new file mode 100755 index 00000000..0d9294b0 --- /dev/null +++ b/src/VBox/ValidationKit/testmanager/core/failurereason.py @@ -0,0 +1,580 @@ +# -*- coding: utf-8 -*- +# $Id: failurereason.py $ + +""" +Test Manager - Failure Reasons. +""" + +__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; + +# Validation Kit imports. +from testmanager.core.base import ModelDataBase, ModelLogicBase, TMRowNotFound, TMInvalidData, TMRowInUse, \ + AttributeChangeEntry, ChangeLogEntry; +from testmanager.core.useraccount import UserAccountLogic; + +# Python 3 hacks: +if sys.version_info[0] >= 3: + xrange = range; # pylint: disable=redefined-builtin,invalid-name + + +class FailureReasonData(ModelDataBase): + """ + Failure Reason Data. + """ + + ksIdAttr = 'idFailureReason'; + + ksParam_idFailureReason = 'FailureReasonData_idFailureReason' + ksParam_tsEffective = 'FailureReasonData_tsEffective' + ksParam_tsExpire = 'FailureReasonData_tsExpire' + ksParam_uidAuthor = 'FailureReasonData_uidAuthor' + ksParam_idFailureCategory = 'FailureReasonData_idFailureCategory' + ksParam_sShort = 'FailureReasonData_sShort' + ksParam_sFull = 'FailureReasonData_sFull' + ksParam_iTicket = 'FailureReasonData_iTicket' + ksParam_asUrls = 'FailureReasonData_asUrls' + + kasAllowNullAttributes = [ 'idFailureReason', 'tsEffective', 'tsExpire', + 'uidAuthor', 'iTicket', 'asUrls' ] + + def __init__(self): + ModelDataBase.__init__(self); + + # + # Initialize with defaults. + # See the database for explanations of each of these fields. + # + + self.idFailureReason = None + self.tsEffective = None + self.tsExpire = None + self.uidAuthor = None + self.idFailureCategory = None + self.sShort = None + self.sFull = None + self.iTicket = None + self.asUrls = None + + def initFromDbRow(self, aoRow): + """ + Re-initializes the data with a row from a SELECT * FROM FailureReasons. + + Returns self. Raises exception if the row is None or otherwise invalid. + """ + + if aoRow is None: + raise TMRowNotFound('Failure Reason not found.'); + + self.idFailureReason = aoRow[0] + self.tsEffective = aoRow[1] + self.tsExpire = aoRow[2] + self.uidAuthor = aoRow[3] + self.idFailureCategory = aoRow[4] + self.sShort = aoRow[5] + self.sFull = aoRow[6] + self.iTicket = aoRow[7] + self.asUrls = aoRow[8] + + return self; + + def initFromDbWithId(self, oDb, idFailureReason, tsNow = None, sPeriodBack = None): + """ + Initialize from the database, given the ID of a row. + """ + oDb.execute(self.formatSimpleNowAndPeriodQuery(oDb, + 'SELECT *\n' + 'FROM FailureReasons\n' + 'WHERE idFailureReason = %s\n' + , ( idFailureReason,), tsNow, sPeriodBack)); + aoRow = oDb.fetchOne() + if aoRow is None: + raise TMRowNotFound('idFailureReason=%s not found (tsNow=%s sPeriodBack=%s)' + % (idFailureReason, tsNow, sPeriodBack,)); + return self.initFromDbRow(aoRow); + + +class FailureReasonDataEx(FailureReasonData): + """ + Failure Reason Data, extended version that includes the category. + """ + + def __init__(self): + FailureReasonData.__init__(self); + self.oCategory = None; + self.oAuthor = None; + + def initFromDbRowEx(self, aoRow, oCategoryLogic, oUserAccountLogic): + """ + Re-initializes the data with a row from a SELECT * FROM FailureReasons. + + Returns self. Raises exception if the row is None or otherwise invalid. + """ + + self.initFromDbRow(aoRow); + self.oCategory = oCategoryLogic.cachedLookup(self.idFailureCategory); + self.oAuthor = oUserAccountLogic.cachedLookup(self.uidAuthor); + + return self; + + +class FailureReasonLogic(ModelLogicBase): # pylint: disable=too-few-public-methods + """ + Failure Reason logic. + """ + + def __init__(self, oDb): + ModelLogicBase.__init__(self, oDb) + self.dCache = None; + self.dCacheNameAndCat = None; + self.oCategoryLogic = None; + self.oUserAccountLogic = None; + + def fetchForListing(self, iStart, cMaxRows, tsNow, aiSortColumns = None): + """ + Fetches Failure Category records. + + Returns an array (list) of FailureReasonDataEx items, empty list if none. + Raises exception on error. + """ + _ = aiSortColumns; + self._ensureCachesPresent(); + + if tsNow is None: + self._oDb.execute('SELECT FailureReasons.*,\n' + ' FailureCategories.sShort AS sCategory\n' + 'FROM FailureReasons,\n' + ' FailureCategories\n' + 'WHERE FailureReasons.tsExpire = \'infinity\'::TIMESTAMP\n' + ' AND FailureCategories.idFailureCategory = FailureReasons.idFailureCategory\n' + ' AND FailureCategories.tsExpire = \'infinity\'::TIMESTAMP\n' + 'ORDER BY sCategory ASC, sShort ASC\n' + 'LIMIT %s OFFSET %s\n' + , (cMaxRows, iStart,)); + else: + self._oDb.execute('SELECT FailureReasons.*,\n' + ' FailureCategories.sShort AS sCategory\n' + 'FROM FailureReasons,\n' + ' FailureCategories\n' + 'WHERE FailureReasons.tsExpire > %s\n' + ' AND FailureReasons.tsEffective <= %s\n' + ' AND FailureCategories.idFailureCategory = FailureReasons.idFailureCategory\n' + ' AND FailureReasons.tsExpire > %s\n' + ' AND FailureReasons.tsEffective <= %s\n' + 'ORDER BY sCategory ASC, sShort ASC\n' + 'LIMIT %s OFFSET %s\n' + , (tsNow, tsNow, tsNow, tsNow, cMaxRows, iStart,)); + + aoRows = [] + for aoRow in self._oDb.fetchAll(): + aoRows.append(FailureReasonDataEx().initFromDbRowEx(aoRow, self.oCategoryLogic, self.oUserAccountLogic)); + return aoRows + + def fetchForListingInCategory(self, iStart, cMaxRows, tsNow, idFailureCategory, aiSortColumns = None): + """ + Fetches Failure Category records. + + Returns an array (list) of FailureReasonDataEx items, empty list if none. + Raises exception on error. + """ + _ = aiSortColumns; + self._ensureCachesPresent(); + + if tsNow is None: + self._oDb.execute('SELECT *\n' + 'FROM FailureReasons\n' + 'WHERE tsExpire = \'infinity\'::TIMESTAMP\n' + ' AND idFailureCategory = %s\n' + 'ORDER BY sShort ASC\n' + 'LIMIT %s OFFSET %s\n' + , ( idFailureCategory, cMaxRows, iStart,)); + else: + self._oDb.execute('SELECT *\n' + 'FROM FailureReasons\n' + 'WHERE idFailureCategory = %s\n' + ' AND tsExpire > %s\n' + ' AND tsEffective <= %s\n' + 'ORDER BY sShort ASC\n' + 'LIMIT %s OFFSET %s\n' + , ( idFailureCategory, tsNow, tsNow, cMaxRows, iStart,)); + + aoRows = [] + for aoRow in self._oDb.fetchAll(): + aoRows.append(FailureReasonDataEx().initFromDbRowEx(aoRow, self.oCategoryLogic, self.oUserAccountLogic)); + return aoRows + + + def fetchForSheriffByNamedCategory(self, sFailureCategory): + """ + Fetches the short names of the reasons in the named category. + + Returns array of strings. + Raises exception on error. + """ + self._oDb.execute('SELECT FailureReasons.sShort\n' + 'FROM FailureReasons,\n' + ' FailureCategories\n' + 'WHERE FailureReasons.tsExpire = \'infinity\'::TIMESTAMP\n' + ' AND FailureReasons.idFailureCategory = FailureCategories.idFailureCategory\n' + ' AND FailureCategories.sShort = %s\n' + 'ORDER BY FailureReasons.sShort ASC\n' + , ( sFailureCategory,)); + return [aoRow[0] for aoRow in self._oDb.fetchAll()]; + + + def fetchForCombo(self, sFirstEntry = 'Select a failure reason', tsEffective = None): + """ + Gets the list of Failure Reasons for a combo box. + Returns an array of (value [idFailureReason], drop-down-name [sShort], + hover-text [sFull]) tuples. + """ + if tsEffective is None: + self._oDb.execute('SELECT fr.idFailureReason, CONCAT(fc.sShort, \' / \', fr.sShort) as sComboText, fr.sFull\n' + 'FROM FailureReasons fr,\n' + ' FailureCategories fc\n' + 'WHERE fr.idFailureCategory = fc.idFailureCategory\n' + ' AND fr.tsExpire = \'infinity\'::TIMESTAMP\n' + ' AND fc.tsExpire = \'infinity\'::TIMESTAMP\n' + 'ORDER BY sComboText') + else: + self._oDb.execute('SELECT fr.idFailureReason, CONCAT(fc.sShort, \' / \', fr.sShort) as sComboText, fr.sFull\n' + 'FROM FailureReasons fr,\n' + ' FailureCategories fc\n' + 'WHERE fr.idFailureCategory = fc.idFailureCategory\n' + ' AND fr.tsExpire > %s\n' + ' AND fr.tsEffective <= %s\n' + ' AND fc.tsExpire > %s\n' + ' AND fc.tsEffective <= %s\n' + 'ORDER BY sComboText' + , (tsEffective, tsEffective, tsEffective, tsEffective)); + aoRows = self._oDb.fetchAll(); + return [(-1, sFirstEntry, '')] + aoRows; + + + def fetchForChangeLog(self, idFailureReason, iStart, cMaxRows, tsNow): # pylint: disable=too-many-locals + """ + Fetches change log entries for a failure reason. + + Returns an array of ChangeLogEntry instance and an indicator whether + there are more entries. + Raises exception on error. + """ + self._ensureCachesPresent(); + + if tsNow is None: + tsNow = self._oDb.getCurrentTimestamp(); + + # 1. Get a list of the relevant changes. + self._oDb.execute('SELECT * FROM FailureReasons WHERE idFailureReason = %s AND tsEffective <= %s\n' + 'ORDER BY tsEffective DESC\n' + 'LIMIT %s OFFSET %s\n' + , ( idFailureReason, tsNow, cMaxRows + 1, iStart, )); + aoRows = []; + for aoChange in self._oDb.fetchAll(): + aoRows.append(FailureReasonData().initFromDbRow(aoChange)); + + # 2. Calculate the changes. + aoEntries = []; + for i in xrange(0, len(aoRows) - 1): + oNew = aoRows[i]; + oOld = aoRows[i + 1]; + + aoChanges = []; + for sAttr in oNew.getDataAttributes(): + if sAttr not in [ 'tsEffective', 'tsExpire', 'uidAuthor', ]: + oOldAttr = getattr(oOld, sAttr); + oNewAttr = getattr(oNew, sAttr); + if oOldAttr != oNewAttr: + if sAttr == 'idFailureCategory': + oCat = self.oCategoryLogic.cachedLookup(oOldAttr); + if oCat is not None: + oOldAttr = '%s (%s)' % (oOldAttr, oCat.sShort, ); + oCat = self.oCategoryLogic.cachedLookup(oNewAttr); + if oCat is not None: + oNewAttr = '%s (%s)' % (oNewAttr, oCat.sShort, ); + aoChanges.append(AttributeChangeEntry(sAttr, oNewAttr, oOldAttr, str(oNewAttr), str(oOldAttr))); + + aoEntries.append(ChangeLogEntry(oNew.uidAuthor, None, oNew.tsEffective, oNew.tsExpire, oNew, oOld, aoChanges)); + + # If we're at the end of the log, add the initial entry. + if len(aoRows) <= cMaxRows and aoRows: + oNew = aoRows[-1]; + aoEntries.append(ChangeLogEntry(oNew.uidAuthor, None, oNew.tsEffective, oNew.tsExpire, oNew, None, [])); + + return (UserAccountLogic(self._oDb).resolveChangeLogAuthors(aoEntries), len(aoRows) > cMaxRows); + + + def getById(self, idFailureReason): + """Get Failure Reason data by idFailureReason""" + + self._oDb.execute('SELECT *\n' + 'FROM FailureReasons\n' + 'WHERE tsExpire = \'infinity\'::timestamp\n' + ' AND idFailureReason = %s;', (idFailureReason,)) + aRows = self._oDb.fetchAll() + if len(aRows) not in (0, 1): + raise self._oDb.integrityException( + 'Found more than one failure reasons with the same credentials. Database structure is corrupted.') + try: + return FailureReasonData().initFromDbRow(aRows[0]) + except IndexError: + return None + + + def addEntry(self, oData, uidAuthor, fCommit = False): + """ + Add a failure reason. + """ + # + # Validate. + # + dErrors = oData.validateAndConvert(self._oDb, oData.ksValidateFor_Add); + if dErrors: + raise TMInvalidData('addEntry invalid input: %s' % (dErrors,)); + + # + # Add the record. + # + self._readdEntry(uidAuthor, oData); + self._oDb.maybeCommit(fCommit); + return True; + + + def editEntry(self, oData, uidAuthor, fCommit = False): + """ + Modifies a failure reason. + """ + + # + # Validate inputs and read in the old(/current) data. + # + assert isinstance(oData, FailureReasonData); + dErrors = oData.validateAndConvert(self._oDb, oData.ksValidateFor_Edit); + if dErrors: + raise TMInvalidData('editEntry invalid input: %s' % (dErrors,)); + + oOldData = FailureReasonData().initFromDbWithId(self._oDb, oData.idFailureReason); + + # + # Update the data that needs updating. + # + if not oData.isEqualEx(oOldData, [ 'tsEffective', 'tsExpire', 'uidAuthor', ]): + self._historizeEntry(oData.idFailureReason); + self._readdEntry(uidAuthor, oData); + self._oDb.maybeCommit(fCommit); + return True; + + + def removeEntry(self, uidAuthor, idFailureReason, fCascade = False, fCommit = False): + """ + Deletes a failure reason. + """ + _ = fCascade; # too complicated for now. + + # + # Check whether it's being used by other tables and bitch if it is . + # We currently do not implement cascading. + # + self._oDb.execute('SELECT CONCAT(idBlacklisting, \' - blacklisting\')\n' + 'FROM BuildBlacklist\n' + 'WHERE idFailureReason = %s\n' + ' AND tsExpire = \'infinity\'::TIMESTAMP\n' + 'UNION\n' + 'SELECT CONCAT(idTestResult, \' - test result failure reason\')\n' + 'FROM TestResultFailures\n' + 'WHERE idFailureReason = %s\n' + ' AND tsExpire = \'infinity\'::TIMESTAMP\n' + , (idFailureReason, idFailureReason,)); + aaoRows = self._oDb.fetchAll(); + if aaoRows: + raise TMRowInUse('Cannot remove failure reason %u because its being used by: %s' + % (idFailureReason, ', '.join(aoRow[0] for aoRow in aaoRows),)); + + # + # Do the job. + # + oData = FailureReasonData().initFromDbWithId(self._oDb, idFailureReason); + assert oData.idFailureReason == idFailureReason; + (tsCur, tsCurMinusOne) = self._oDb.getCurrentTimestamps(); + if oData.tsEffective not in (tsCur, tsCurMinusOne): + self._historizeEntry(idFailureReason, tsCurMinusOne); + self._readdEntry(uidAuthor, oData, tsCurMinusOne); + self._historizeEntry(idFailureReason); + self._oDb.maybeCommit(fCommit); + return True; + + + def cachedLookup(self, idFailureReason): + """ + Looks up the most recent FailureReasonDataEx object for idFailureReason + via an object cache. + + Returns a shared FailureReasonData object. None if not found. + Raises exception on DB error. + """ + if self.dCache is None: + self.dCache = self._oDb.getCache('FailureReasonDataEx'); + oEntry = self.dCache.get(idFailureReason, None); + if oEntry is None: + self._oDb.execute('SELECT *\n' + 'FROM FailureReasons\n' + 'WHERE idFailureReason = %s\n' + ' AND tsExpire = \'infinity\'::TIMESTAMP\n' + , (idFailureReason, )); + if self._oDb.getRowCount() == 0: + # Maybe it was deleted, try get the last entry. + self._oDb.execute('SELECT *\n' + 'FROM FailureReasons\n' + 'WHERE idFailureReason = %s\n' + 'ORDER BY tsExpire DESC\n' + 'LIMIT 1\n' + , (idFailureReason, )); + elif self._oDb.getRowCount() > 1: + raise self._oDb.integrityException('%s infinity rows for %s' % (self._oDb.getRowCount(), idFailureReason)); + + if self._oDb.getRowCount() == 1: + self._ensureCachesPresent(); + oEntry = FailureReasonDataEx().initFromDbRowEx(self._oDb.fetchOne(), self.oCategoryLogic, + self.oUserAccountLogic); + self.dCache[idFailureReason] = oEntry; + return oEntry; + + + def cachedLookupByNameAndCategory(self, sName, sCategory): + """ + Looks up a failure reason by it's name and category. + + Should the request be ambigiuos, we'll return the oldest one. + + Returns a shared FailureReasonData object. None if not found. + Raises exception on DB error. + """ + if self.dCacheNameAndCat is None: + self.dCacheNameAndCat = self._oDb.getCache('FailureReasonDataEx-By-Name-And-Category'); + sKey = '%s:::%s' % (sName, sCategory,); + oEntry = self.dCacheNameAndCat.get(sKey, None); + if oEntry is None: + self._oDb.execute('SELECT *\n' + 'FROM FailureReasons,\n' + ' FailureCategories\n' + 'WHERE FailureReasons.sShort = %s\n' + ' AND FailureReasons.tsExpire = \'infinity\'::TIMESTAMP\n' + ' AND FailureReasons.idFailureCategory = FailureCategories.idFailureCategory ' + ' AND FailureCategories.sShort = %s\n' + ' AND FailureCategories.tsExpire = \'infinity\'::TIMESTAMP\n' + 'ORDER BY FailureReasons.tsEffective\n' + , ( sName, sCategory)); + if self._oDb.getRowCount() == 0: + sLikeSucks = self._oDb.formatBindArgs( + 'SELECT *\n' + 'FROM FailureReasons,\n' + ' FailureCategories\n' + 'WHERE ( FailureReasons.sShort ILIKE @@@@@@@! %s !@@@@@@@\n' + ' OR FailureReasons.sFull ILIKE @@@@@@@! %s !@@@@@@@)\n' + ' AND FailureCategories.tsExpire = \'infinity\'::TIMESTAMP\n' + ' AND FailureReasons.idFailureCategory = FailureCategories.idFailureCategory\n' + ' AND ( FailureCategories.sShort = %s\n' + ' OR FailureCategories.sFull = %s)\n' + ' AND FailureReasons.tsExpire = \'infinity\'::TIMESTAMP\n' + 'ORDER BY FailureReasons.tsEffective\n' + , ( sName, sName, sCategory, sCategory )); + sLikeSucks = sLikeSucks.replace('LIKE @@@@@@@! \'', 'LIKE \'%').replace('\' !@@@@@@@', '%\''); + self._oDb.execute(sLikeSucks); + if self._oDb.getRowCount() > 0: + self._ensureCachesPresent(); + oEntry = FailureReasonDataEx().initFromDbRowEx(self._oDb.fetchOne(), self.oCategoryLogic, + self.oUserAccountLogic); + self.dCacheNameAndCat[sKey] = oEntry; + if sName != oEntry.sShort or sCategory != oEntry.oCategory.sShort: + sKey2 = '%s:::%s' % (oEntry.sShort, oEntry.oCategory.sShort,); + self.dCacheNameAndCat[sKey2] = oEntry; + return oEntry; + + + # + # Helpers. + # + + def _readdEntry(self, uidAuthor, oData, tsEffective = None): + """ + Re-adds the FailureReasons entry. Used by addEntry, editEntry and removeEntry. + """ + if tsEffective is None: + tsEffective = self._oDb.getCurrentTimestamp(); + self._oDb.execute('INSERT INTO FailureReasons (\n' + ' uidAuthor,\n' + ' tsEffective,\n' + ' idFailureReason,\n' + ' idFailureCategory,\n' + ' sShort,\n' + ' sFull,\n' + ' iTicket,\n' + ' asUrls)\n' + 'VALUES (%s, %s, ' + + ( 'DEFAULT' if oData.idFailureReason is None else str(oData.idFailureReason) ) + + ', %s, %s, %s, %s, %s)\n' + , ( uidAuthor, + tsEffective, + oData.idFailureCategory, + oData.sShort, + oData.sFull, + oData.iTicket, + oData.asUrls,) ); + return True; + + + def _historizeEntry(self, idFailureReason, tsExpire = None): + """ Historizes the current entry. """ + if tsExpire is None: + tsExpire = self._oDb.getCurrentTimestamp(); + self._oDb.execute('UPDATE FailureReasons\n' + 'SET tsExpire = %s\n' + 'WHERE idFailureReason = %s\n' + ' AND tsExpire = \'infinity\'::TIMESTAMP\n' + , (tsExpire, idFailureReason,)); + return True; + + + def _ensureCachesPresent(self): + """ Ensures we've got the cache references resolved. """ + if self.oCategoryLogic is None: + from testmanager.core.failurecategory import FailureCategoryLogic; + self.oCategoryLogic = FailureCategoryLogic(self._oDb); + if self.oUserAccountLogic is None: + self.oUserAccountLogic = UserAccountLogic(self._oDb); + return True; + -- cgit v1.2.3