diff options
Diffstat (limited to '')
-rwxr-xr-x | src/VBox/ValidationKit/testmanager/core/build.py | 891 |
1 files changed, 891 insertions, 0 deletions
diff --git a/src/VBox/ValidationKit/testmanager/core/build.py b/src/VBox/ValidationKit/testmanager/core/build.py new file mode 100755 index 00000000..bfe50ca5 --- /dev/null +++ b/src/VBox/ValidationKit/testmanager/core/build.py @@ -0,0 +1,891 @@ +# -*- coding: utf-8 -*- +# $Id: build.py $ + +""" +Test Manager - Builds. +""" + +__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 unittest; + +# Validation Kit imports. +from testmanager import config; +from testmanager.core import coreconsts; +from testmanager.core.base import ModelDataBase, ModelDataBaseTestCase, ModelLogicBase, TMExceptionBase, \ + TMTooManyRows, TMInvalidData, TMRowNotFound, TMRowInUse; + + +class BuildCategoryData(ModelDataBase): + """ + A build category. + """ + + ksIdAttr = 'idBuildCategory'; + + ksParam_idBuildCategory = 'BuildCategory_idBuildCategory'; + ksParam_sProduct = 'BuildCategory_sProduct'; + ksParam_sRepository = 'BuildCategory_sRepository'; + ksParam_sBranch = 'BuildCategory_sBranch'; + ksParam_sType = 'BuildCategory_sType'; + ksParam_asOsArches = 'BuildCategory_asOsArches'; + + kasAllowNullAttributes = ['idBuildCategory', ]; + + def __init__(self): + ModelDataBase.__init__(self); + + # + # Initialize with defaults. + # See the database for explanations of each of these fields. + # + self.idBuildCategory = None; + self.sProduct = None; + self.sRepository = None; + self.sBranch = None; + self.sType = None; + self.asOsArches = None; + + def initFromDbRow(self, aoRow): + """ + Re-initializes the object from a SELECT * FROM BuildCategories row. + Returns self. Raises exception if aoRow is None. + """ + if aoRow is None: + raise TMRowNotFound('BuildCategory not found.'); + + self.idBuildCategory = aoRow[0]; + self.sProduct = aoRow[1]; + self.sRepository = aoRow[2]; + self.sBranch = aoRow[3]; + self.sType = aoRow[4]; + self.asOsArches = sorted(aoRow[5]); + return self; + + def initFromDbWithId(self, oDb, idBuildCategory, tsNow = None, sPeriodBack = None): + """ + Initialize from the database, given the ID of a row. + """ + _ = tsNow; _ = sPeriodBack; # No history in this table. + oDb.execute('SELECT * FROM BuildCategories WHERE idBuildCategory = %s', (idBuildCategory,)); + aoRow = oDb.fetchOne() + if aoRow is None: + raise TMRowNotFound('idBuildCategory=%s not found' % (idBuildCategory, )); + return self.initFromDbRow(aoRow); + + def initFromValues(self, sProduct, sRepository, sBranch, sType, asOsArches, idBuildCategory = None): + """ + Reinitializes form a set of values. + return self. + """ + self.idBuildCategory = idBuildCategory; + self.sProduct = sProduct; + self.sRepository = sRepository; + self.sBranch = sBranch; + self.sType = sType; + self.asOsArches = asOsArches; + return self; + + def _validateAndConvertAttribute(self, sAttr, sParam, oValue, aoNilValues, fAllowNull, oDb): + # Handle sType and asOsArches specially. + if sAttr == 'sType': + (oNewValue, sError) = ModelDataBase._validateAndConvertAttribute(self, sAttr, sParam, oValue, + aoNilValues, fAllowNull, oDb); + if sError is None and self.sType.lower() != self.sType: + sError = 'Invalid build type value'; + + elif sAttr == 'asOsArches': + (oNewValue, sError) = self.validateListOfStr(oValue, aoNilValues = aoNilValues, fAllowNull = fAllowNull, + asValidValues = coreconsts.g_kasOsDotCpusAll); + if sError is not None and oNewValue is not None: + oNewValue = sorted(oNewValue); # Must be sorted! + + else: + return ModelDataBase._validateAndConvertAttribute(self, sAttr, sParam, oValue, aoNilValues, fAllowNull, oDb); + + return (oNewValue, sError); + + def matchesOsArch(self, sOs, sArch): + """ Checks if the build matches the given OS and architecture. """ + if sOs + '.' + sArch in self.asOsArches: + return True; + if sOs + '.noarch' in self.asOsArches: + return True; + if 'os-agnostic.' + sArch in self.asOsArches: + return True; + if 'os-agnostic.noarch' in self.asOsArches: + return True; + return False; + + +class BuildCategoryLogic(ModelLogicBase): # pylint: disable=too-few-public-methods + """ + Build categories database logic. + """ + + def __init__(self, oDb): + ModelLogicBase.__init__(self, oDb) + self.dCache = None; + + def fetchForListing(self, iStart, cMaxRows, tsNow, aiSortColumns = None): + """ + Fetches testboxes for listing. + + Returns an array (list) of UserAccountData items, empty list if none. + Raises exception on error. + """ + _ = tsNow; _ = aiSortColumns; + self._oDb.execute('SELECT *\n' + 'FROM BuildCategories\n' + 'ORDER BY sProduct, sRepository, sBranch, sType, idBuildCategory\n' + 'LIMIT %s OFFSET %s\n' + , (cMaxRows, iStart,)); + + aoRows = []; + for _ in range(self._oDb.getRowCount()): + aoRows.append(BuildCategoryData().initFromDbRow(self._oDb.fetchOne())); + return aoRows; + + def fetchForCombo(self): + """ + Gets the list of Build Categories for a combo box. + Returns an array of (value [idBuildCategory], drop-down-name [info], + hover-text [info]) tuples. + """ + self._oDb.execute('SELECT *\n' + 'FROM BuildCategories\n' + 'ORDER BY sProduct, sBranch, sType, asOsArches') + + aaoRows = self._oDb.fetchAll() + aoRet = [] + for aoRow in aaoRows: + oData = BuildCategoryData().initFromDbRow(aoRow) + + sInfo = '%s / %s / %s / %s' % \ + (oData.sProduct, + oData.sBranch, + oData.sType, + ', '.join(oData.asOsArches)) + + # Make short info string if necessary + sInfo = sInfo if len(sInfo) < 70 else (sInfo[:70] + '...') + + oInfoItem = (oData.idBuildCategory, sInfo, sInfo) + aoRet.append(oInfoItem) + + return aoRet + + def addEntry(self, oData, uidAuthor = None, fCommit = False): + """ + Standard method for adding a build category. + """ + + # Lazy bird warning! Reuse the soft addBuildCategory method. + self.addBuildCategory(oData, fCommit); + _ = uidAuthor; + return True; + + def removeEntry(self, uidAuthor, idBuildCategory, fCascade = False, fCommit = False): + """ + Tries to delete the build category. + Note! Does not implement cascading. This is intentional! + """ + + # + # Check that the build category isn't used by anyone. + # + self._oDb.execute('SELECT COUNT(idBuild)\n' + 'FROM Builds\n' + 'WHERE idBuildCategory = %s\n' + , (idBuildCategory,)); + cBuilds = self._oDb.fetchOne()[0]; + if cBuilds > 0: + raise TMRowInUse('Build category #%d is used by %d builds and can therefore not be deleted.' + % (idBuildCategory, cBuilds,)); + + # + # Ok, it's not used, so just delete it. + # (No history on this table. This code is for typos.) + # + self._oDb.execute('DELETE FROM Builds\n' + 'WHERE idBuildCategory = %s\n' + , (idBuildCategory,)); + + self._oDb.maybeCommit(fCommit); + _ = uidAuthor; _ = fCascade; + return True; + + def cachedLookup(self, idBuildCategory): + """ + Looks up the most recent BuildCategoryData object for idBuildCategory + via an object cache. + + Returns a shared BuildCategoryData object. None if not found. + Raises exception on DB error. + """ + if self.dCache is None: + self.dCache = self._oDb.getCache('BuildCategoryData'); + oEntry = self.dCache.get(idBuildCategory, None); + if oEntry is None: + self._oDb.execute('SELECT *\n' + 'FROM BuildCategories\n' + 'WHERE idBuildCategory = %s\n' + , (idBuildCategory, )); + if self._oDb.getRowCount() == 1: + aaoRow = self._oDb.fetchOne(); + oEntry = BuildCategoryData(); + oEntry.initFromDbRow(aaoRow); + self.dCache[idBuildCategory] = oEntry; + return oEntry; + + # + # Other methods. + # + + def tryFetch(self, idBuildCategory): + """ + Try fetch the build category with the given ID. + Returns BuildCategoryData instance if found, None if not found. + May raise exception on database error. + """ + self._oDb.execute('SELECT *\n' + 'FROM BuildCategories\n' + 'WHERE idBuildCategory = %s\n' + , (idBuildCategory,)) + aaoRows = self._oDb.fetchAll() + if not aaoRows: + return None; + if len(aaoRows) != 1: + raise self._oDb.integrityException('Duplicates in BuildCategories: %s' % (aaoRows,)); + return BuildCategoryData().initFromDbRow(aaoRows[0]) + + def tryFindByData(self, oData): + """ + Tries to find the matching build category from the sProduct, sBranch, + sType and asOsArches members of oData. + + Returns a valid build category ID and an updated oData object if found. + Returns None and unmodified oData object if not found. + May raise exception on database error. + """ + self._oDb.execute('SELECT *\n' + 'FROM BuildCategories\n' + 'WHERE sProduct = %s\n' + ' AND sRepository = %s\n' + ' AND sBranch = %s\n' + ' AND sType = %s\n' + ' AND asOsArches = %s\n' + , ( oData.sProduct, + oData.sRepository, + oData.sBranch, + oData.sType, + sorted(oData.asOsArches), + )); + aaoRows = self._oDb.fetchAll(); + if not aaoRows: + return None; + if len(aaoRows) > 1: + raise self._oDb.integrityException('Duplicates in BuildCategories: %s' % (aaoRows,)); + + oData.initFromDbRow(aaoRows[0]); + return oData.idBuildCategory; + + def addBuildCategory(self, oData, fCommit = False): + """ + Add Build Category record into the database if needed, returning updated oData. + Raises exception on input and database errors. + """ + + # Check BuildCategoryData before do anything + dDataErrors = oData.validateAndConvert(self._oDb, oData.ksValidateFor_Add); + if dDataErrors: + raise TMInvalidData('Invalid data passed to addBuildCategory(): %s' % (dDataErrors,)); + + # Does it already exist? + if self.tryFindByData(oData) is None: + # No, We'll have to add it. + self._oDb.execute('INSERT INTO BuildCategories (sProduct, sRepository, sBranch, sType, asOsArches)\n' + 'VALUES (%s, %s, %s, %s, %s)\n' + 'RETURNING idBuildCategory' + , ( oData.sProduct, + oData.sRepository, + oData.sBranch, + oData.sType, + sorted(oData.asOsArches), + )); + oData.idBuildCategory = self._oDb.fetchOne()[0]; + + self._oDb.maybeCommit(fCommit); + return oData; + + +class BuildData(ModelDataBase): + """ + A build. + """ + + ksIdAttr = 'idBuild'; + + ksParam_idBuild = 'Build_idBuild'; + ksParam_tsCreated = 'Build_tsCreated'; + ksParam_tsEffective = 'Build_tsEffective'; + ksParam_tsExpire = 'Build_tsExpire'; + ksParam_uidAuthor = 'Build_uidAuthor'; + ksParam_idBuildCategory = 'Build_idBuildCategory'; + ksParam_iRevision = 'Build_iRevision'; + ksParam_sVersion = 'Build_sVersion'; + ksParam_sLogUrl = 'Build_sLogUrl'; + ksParam_sBinaries = 'Build_sBinaries'; + ksParam_fBinariesDeleted = 'Build_fBinariesDeleted'; + + kasAllowNullAttributes = ['idBuild', 'tsCreated', 'tsEffective', 'tsExpire', 'uidAuthor', 'tsCreated', 'sLogUrl']; + + + def __init__(self): + ModelDataBase.__init__(self); + + # + # Initialize with defaults. + # See the database for explanations of each of these fields. + # + self.idBuild = None; + self.tsCreated = None; + self.tsEffective = None; + self.tsExpire = None; + self.uidAuthor = None; + self.idBuildCategory = None; + self.iRevision = None; + self.sVersion = None; + self.sLogUrl = None; + self.sBinaries = None; + self.fBinariesDeleted = False; + + def initFromDbRow(self, aoRow): + """ + Re-initializes the object from a SELECT * FROM Builds row. + Returns self. Raises exception if aoRow is None. + """ + if aoRow is None: + raise TMRowNotFound('Build not found.'); + + self.idBuild = aoRow[0]; + self.tsCreated = aoRow[1]; + self.tsEffective = aoRow[2]; + self.tsExpire = aoRow[3]; + self.uidAuthor = aoRow[4]; + self.idBuildCategory = aoRow[5]; + self.iRevision = aoRow[6]; + self.sVersion = aoRow[7]; + self.sLogUrl = aoRow[8]; + self.sBinaries = aoRow[9]; + self.fBinariesDeleted = aoRow[10]; + return self; + + def initFromDbWithId(self, oDb, idBuild, tsNow = None, sPeriodBack = None): + """ + Initialize from the database, given the ID of a row. + """ + oDb.execute(self.formatSimpleNowAndPeriodQuery(oDb, + 'SELECT *\n' + 'FROM Builds\n' + 'WHERE idBuild = %s\n' + , ( idBuild,), tsNow, sPeriodBack)); + aoRow = oDb.fetchOne() + if aoRow is None: + raise TMRowNotFound('idBuild=%s not found (tsNow=%s sPeriodBack=%s)' % (idBuild, tsNow, sPeriodBack,)); + return self.initFromDbRow(aoRow); + + def areFilesStillThere(self): + """ + Try check if the build files are still there. + + Returns True if they are, None if we cannot tell, and False if one or + more are missing. + """ + if self.fBinariesDeleted: + return False; + + for sBinary in self.sBinaries.split(','): + sBinary = sBinary.strip(); + if not sBinary: + continue; + # Same URL tests as in webutils.downloadFile(). + if sBinary.startswith('http://') \ + or sBinary.startswith('https://') \ + or sBinary.startswith('ftp://'): + # URL - don't bother trying to verify that (we don't use it atm). + fRc = None; + else: + # File. + if config.g_ksBuildBinRootDir is not None: + sFullPath = os.path.join(config.g_ksBuildBinRootDir, sBinary); + fRc = os.path.isfile(sFullPath); + if not fRc \ + and not os.path.isfile(os.path.join(config.g_ksBuildBinRootDir, config.g_ksBuildBinRootFile)): + fRc = None; # Root file missing, so the share might not be mounted correctly. + else: + fRc = None; + if fRc is not True: + return fRc; + + return True; + + +class BuildDataEx(BuildData): + """ + Complete data set. + """ + + kasInternalAttributes = [ 'oCat', ]; + + def __init__(self): + BuildData.__init__(self); + self.oCat = None; + + def initFromDbRow(self, aoRow): + """ + Reinitialize from a SELECT Builds.*, BuildCategories.* FROM Builds, BuildCategories query. + Returns self. Raises exception if aoRow is None. + """ + if aoRow is None: + raise TMRowNotFound('Build not found.'); + BuildData.initFromDbRow(self, aoRow); + self.oCat = BuildCategoryData().initFromDbRow(aoRow[11:]); + return self; + + def initFromDbWithId(self, oDb, idBuild, tsNow = None, sPeriodBack = None): + """ + Reinitialize from database given a row ID. + Returns self. Raises exception on database error or if the ID is invalid. + """ + oDb.execute(self.formatSimpleNowAndPeriodQuery(oDb, + 'SELECT Builds.*, BuildCategories.*\n' + 'FROM Builds, BuildCategories\n' + 'WHERE idBuild = %s\n' + ' AND Builds.idBuildCategory = BuildCategories.idBuildCategory\n' + , ( idBuild,), tsNow, sPeriodBack, 'Builds.')); + aoRow = oDb.fetchOne() + if aoRow is None: + raise TMRowNotFound('idBuild=%s not found (tsNow=%s sPeriodBack=%s)' % (idBuild, tsNow, sPeriodBack,)); + return self.initFromDbRow(aoRow); + + def convertFromParamNull(self): + raise TMExceptionBase('Not implemented'); + + def isEqual(self, oOther): + raise TMExceptionBase('Not implemented'); + + + +class BuildLogic(ModelLogicBase): # pylint: disable=too-few-public-methods + """ + Build database logic (covers build categories as well as builds). + """ + + def __init__(self, oDb): + ModelLogicBase.__init__(self, oDb) + self.dCache = None; + + # + # Standard methods. + # + + def fetchForListing(self, iStart, cMaxRows, tsNow, aiSortColumns = None): + """ + Fetches builds for listing. + + Returns an array (list) of BuildDataEx items, empty list if none. + Raises exception on error. + """ + _ = aiSortColumns; + + if tsNow is None: + self._oDb.execute('SELECT *\n' + 'FROM Builds, BuildCategories\n' + 'WHERE Builds.idBuildCategory = BuildCategories.idBuildCategory\n' + ' AND Builds.tsExpire = \'infinity\'::TIMESTAMP\n' + 'ORDER BY tsCreated DESC\n' + 'LIMIT %s OFFSET %s\n' + , (cMaxRows, iStart,)); + else: + self._oDb.execute('SELECT *\n' + 'FROM Builds, BuildCategories\n' + 'WHERE Builds.idBuildCategory = BuildCategories.idBuildCategory\n' + ' AND Builds.tsExpire > %s\n' + ' AND Builds.tsEffective <= %s\n' + 'ORDER BY tsCreated DESC\n' + 'LIMIT %s OFFSET %s\n' + , (tsNow, tsNow, cMaxRows, iStart,)); + + aoRows = []; + for _ in range(self._oDb.getRowCount()): + aoRows.append(BuildDataEx().initFromDbRow(self._oDb.fetchOne())); + return aoRows; + + def addEntry(self, oBuildData, uidAuthor = None, fCommit = False): + """ + Adds the build to the database, optionally adding the build category if + a BuildDataEx object used and it's necessary. + + Returns updated data object. Raises exception on failure. + """ + + # Find/Add the build category if specified. + if isinstance(oBuildData, BuildDataEx) \ + and oBuildData.idBuildCategory is None: + BuildCategoryLogic(self._oDb).addBuildCategory(oBuildData.oCat, fCommit = False); + oBuildData.idBuildCategory = oBuildData.oCat.idBuildCategory; + + # Add the build. + self._oDb.execute('INSERT INTO Builds (uidAuthor,\n' + ' idBuildCategory,\n' + ' iRevision,\n' + ' sVersion,\n' + ' sLogUrl,\n' + ' sBinaries,\n' + ' fBinariesDeleted)\n' + 'VALUES (%s, %s, %s, %s, %s, %s, %s)\n' + 'RETURNING idBuild, tsCreated\n' + , ( uidAuthor, + oBuildData.idBuildCategory, + oBuildData.iRevision, + oBuildData.sVersion, + oBuildData.sLogUrl, + oBuildData.sBinaries, + oBuildData.fBinariesDeleted, + )); + aoRow = self._oDb.fetchOne(); + oBuildData.idBuild = aoRow[0]; + oBuildData.tsCreated = aoRow[1]; + + self._oDb.maybeCommit(fCommit); + return oBuildData; + + def editEntry(self, oData, uidAuthor = None, fCommit = False): + """Modify database record""" + + # + # Validate input and get current data. + # + dErrors = oData.validateAndConvert(self._oDb, oData.ksValidateFor_Edit); + if dErrors: + raise TMInvalidData('editEntry invalid input: %s' % (dErrors,)); + oOldData = BuildData().initFromDbWithId(self._oDb, oData.idBuild); + + # + # Do the work. + # + if not oData.isEqualEx(oOldData, [ 'tsEffective', 'tsExpire', 'uidAuthor' ]): + self._historizeBuild(oData.idBuild); + self._oDb.execute('INSERT INTO Builds (uidAuthor,\n' + ' idBuild,\n' + ' tsCreated,\n' + ' idBuildCategory,\n' + ' iRevision,\n' + ' sVersion,\n' + ' sLogUrl,\n' + ' sBinaries,\n' + ' fBinariesDeleted)\n' + 'VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s)\n' + 'RETURNING idBuild, tsCreated\n' + , ( uidAuthor, + oData.idBuild, + oData.tsCreated, + oData.idBuildCategory, + oData.iRevision, + oData.sVersion, + oData.sLogUrl, + oData.sBinaries, + oData.fBinariesDeleted, + )); + + self._oDb.maybeCommit(fCommit); + return True; + + def removeEntry(self, uidAuthor, idBuild, fCascade = False, fCommit = False): + """ + Historize record + """ + + # + # No non-historic refs here, so just go ahead and expire the build. + # + _ = fCascade; + _ = uidAuthor; ## @todo record deleter. + + self._historizeBuild(idBuild, None); + + self._oDb.maybeCommit(fCommit); + return True; + + def cachedLookup(self, idBuild): + """ + Looks up the most recent BuildDataEx object for idBuild + via an object cache. + + Returns a shared BuildDataEx object. None if not found. + Raises exception on DB error. + """ + if self.dCache is None: + self.dCache = self._oDb.getCache('BuildDataEx'); + oEntry = self.dCache.get(idBuild, None); + if oEntry is None: + self._oDb.execute('SELECT Builds.*, BuildCategories.*\n' + 'FROM Builds, BuildCategories\n' + 'WHERE Builds.idBuild = %s\n' + ' AND Builds.idBuildCategory = BuildCategories.idBuildCategory\n' + ' AND tsExpire = \'infinity\'::TIMESTAMP\n' + , (idBuild, )); + if self._oDb.getRowCount() == 0: + # Maybe it was deleted, try get the last entry. + self._oDb.execute('SELECT Builds.*, BuildCategories.*\n' + 'FROM Builds, BuildCategories\n' + 'WHERE Builds.idBuild = %s\n' + ' AND Builds.idBuildCategory = BuildCategories.idBuildCategory\n' + 'ORDER BY tsExpire DESC\n' + 'LIMIT 1\n' + , (idBuild, )); + elif self._oDb.getRowCount() > 1: + raise self._oDb.integrityException('%s infinity rows for %s' % (self._oDb.getRowCount(), idBuild)); + + if self._oDb.getRowCount() == 1: + aaoRow = self._oDb.fetchOne(); + oEntry = BuildDataEx(); + oEntry.initFromDbRow(aaoRow); + self.dCache[idBuild] = oEntry; + return oEntry; + + + # + # Other methods. + # + + def tryFindSameBuildForOsArch(self, oBuildEx, sOs, sCpuArch): + """ + Attempts to find a matching build for the given OS.ARCH. May return + the input build if if matches. + + Returns BuildDataEx instance if found, None if none. May raise + exception on database error. + """ + + if oBuildEx.oCat.matchesOsArch(sOs, sCpuArch): + return oBuildEx; + + self._oDb.execute('SELECT Builds.*, BuildCategories.*\n' + 'FROM Builds, BuildCategories\n' + 'WHERE BuildCategories.sProduct = %s\n' + ' AND BuildCategories.sBranch = %s\n' + ' AND BuildCategories.sType = %s\n' + ' AND ( %s = ANY(BuildCategories.asOsArches)\n' + ' OR %s = ANY(BuildCategories.asOsArches)\n' + ' OR %s = ANY(BuildCategories.asOsArches))\n' + ' AND Builds.idBuildCategory = BuildCategories.idBuildCategory\n' + ' AND Builds.tsExpire = \'infinity\'::TIMESTAMP\n' + ' AND Builds.iRevision = %s\n' + ' AND Builds.sRelease = %s\n' + ' AND Builds.fBinariesDeleted IS FALSE\n' + 'ORDER BY tsCreated DESC\n' + 'LIMIT 4096\n' # stay sane. + , (oBuildEx.oCat.sProduct, + oBuildEx.oCat.sBranch, + oBuildEx.oCat.sType, + '%s.%s' % (sOs, sCpuArch), + '%s.noarch' % (sOs,), + 'os-agnostic.%s' % (sCpuArch,), + 'os-agnostic.noarch', + oBuildEx.iRevision, + oBuildEx.sRelease, + ) ); + aaoRows = self._oDb.fetchAll(); + + for aoRow in aaoRows: + oBuildExRet = BuildDataEx().initFromDbRow(aoRow); + if not self.isBuildBlacklisted(oBuildExRet): + return oBuildExRet; + + return None; + + def isBuildBlacklisted(self, oBuildEx): + """ + Checks if the given build is blacklisted + Returns True/False. May raise exception on database error. + """ + + asOsAgnosticArch = []; + asOsNoArch = []; + for sOsArch in oBuildEx.oCat.asOsArches: + asParts = sOsArch.split('.'); + if len(asParts) != 2 or not asParts[0] or not asParts[1]: + raise self._oDb.integrityException('Bad build asOsArches value: %s (idBuild=%s idBuildCategory=%s)' + % (sOsArch, oBuildEx.idBuild, oBuildEx.idBuildCategory)); + asOsNoArch.append(asParts[0] + '.noarch'); + asOsNoArch.append('os-agnostic.' + asParts[1]); + + self._oDb.execute('SELECT COUNT(*)\n' + 'FROM BuildBlacklist\n' + 'WHERE BuildBlacklist.tsExpire > CURRENT_TIMESTAMP\n' + ' AND BuildBlacklist.tsEffective <= CURRENT_TIMESTAMP\n' + ' AND BuildBlacklist.sProduct = %s\n' + ' AND BuildBlacklist.sBranch = %s\n' + ' AND ( BuildBlacklist.asTypes is NULL\n' + ' OR %s = ANY(BuildBlacklist.asTypes))\n' + ' AND ( BuildBlacklist.asOsArches is NULL\n' + ' OR %s && BuildBlacklist.asOsArches\n' ## @todo check array rep! Need overload? + ' OR %s && BuildBlacklist.asOsArches\n' + ' OR %s && BuildBlacklist.asOsArches\n' + ' OR %s = ANY(BuildBlacklist.asOsArches))\n' + ' AND BuildBlacklist.iFirstRevision <= %s\n' + ' AND BuildBlacklist.iLastRevision >= %s\n' + , (oBuildEx.oCat.sProduct, + oBuildEx.oCat.sBranch, + oBuildEx.oCat.sType, + oBuildEx.oCat.asOsArches, + asOsAgnosticArch, + asOsNoArch, + 'os-agnostic.noarch', + oBuildEx.iRevision, + oBuildEx.iRevision, + ) ); + return self._oDb.fetchOne()[0] > 0; + + + def getById(self, idBuild): + """ + Get build record by its id + """ + self._oDb.execute('SELECT Builds.*, BuildCategories.*\n' + 'FROM Builds, BuildCategories\n' + 'WHERE Builds.idBuild=%s\n' + ' AND Builds.idBuildCategory=BuildCategories.idBuildCategory\n' + ' AND Builds.tsExpire = \'infinity\'::TIMESTAMP\n', (idBuild,)) + + aRows = self._oDb.fetchAll() + if len(aRows) not in (0, 1): + raise TMTooManyRows('Found more than one build with the same credentials. Database structure is corrupted.') + try: + return BuildDataEx().initFromDbRow(aRows[0]) + except IndexError: + return None + + + def getAll(self, tsEffective = None): + """ + Gets the list of all builds. + Returns an array of BuildDataEx instances. + """ + if tsEffective is None: + self._oDb.execute('SELECT Builds.*, BuildCategories.*\n' + 'FROM Builds, BuildCategories\n' + 'WHERE Builds.tsExpire = \'infinity\'::TIMESTAMP\n' + ' AND Builds.idBuildCategory=BuildCategories.idBuildCategory') + else: + self._oDb.execute('SELECT Builds.*, BuildCategories.*\n' + 'FROM Builds, BuildCategories\n' + 'WHERE Builds.tsExpire > %s\n' + ' AND Builds.tsEffective <= %s' + ' AND Builds.idBuildCategory=BuildCategories.idBuildCategory' + , (tsEffective, tsEffective)) + aoRet = [] + for aoRow in self._oDb.fetchAll(): + aoRet.append(BuildDataEx().initFromDbRow(aoRow)) + return aoRet + + + def markDeletedByBinaries(self, sBinaries, fCommit = False): + """ + Marks zero or more builds deleted given the build binaries. + + Returns the number of affected builds. + """ + # Fetch a list of affected build IDs (generally 1 build), and used the + # editEntry method to do the rest. This isn't 100% optimal, but it's + # short and simple, the main effort is anyway the first query. + self._oDb.execute('SELECT idBuild\n' + 'FROM Builds\n' + 'WHERE sBinaries = %s\n' + ' AND fBinariesDeleted = FALSE\n' + ' AND tsExpire = \'infinity\'::TIMESTAMP\n' + , (sBinaries,)); + aaoRows = self._oDb.fetchAll(); + for aoRow in aaoRows: + oData = BuildData().initFromDbWithId(self._oDb, aoRow[0]); + assert not oData.fBinariesDeleted; + oData.fBinariesDeleted = True; + self.editEntry(oData, fCommit = False); + self._oDb.maybeCommit(fCommit); + return len(aaoRows); + + + + # + # Internal helpers. + # + + def _historizeBuild(self, idBuild, tsExpire = None): + """ Historizes the current entry for the specified build. """ + if tsExpire is None: + self._oDb.execute('UPDATE Builds\n' + 'SET tsExpire = CURRENT_TIMESTAMP\n' + 'WHERE idBuild = %s\n' + ' AND tsExpire = \'infinity\'::TIMESTAMP\n' + , (idBuild,)); + else: + self._oDb.execute('UPDATE Builds\n' + 'SET tsExpire = %s\n' + 'WHERE idBuild = %s\n' + ' AND tsExpire = \'infinity\'::TIMESTAMP\n' + , (tsExpire, idBuild,)); + return True; + +# +# Unit testing. +# + +# pylint: disable=missing-docstring +class BuildCategoryDataTestCase(ModelDataBaseTestCase): + def setUp(self): + self.aoSamples = [BuildCategoryData(),]; + +class BuildDataTestCase(ModelDataBaseTestCase): + def setUp(self): + self.aoSamples = [BuildData(),]; + +if __name__ == '__main__': + unittest.main(); + # not reached. + |