# -*- 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 . 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.