build.py revision cf22150eaeeb72431bf1cf65c309a431454fb22b
# -*- coding: utf-8 -*-
# $Id$
"""
Test Manager - Builds.
"""
__copyright__ = \
"""
Copyright (C) 2012-2014 Oracle Corporation
This file is part of VirtualBox Open Source Edition (OSE), as
available from http://www.virtualbox.org. This file is free software;
you can redistribute it and/or modify it under the terms of the GNU
General Public License (GPL) as published by the Free Software
Foundation, in version 2 as it comes in the "COPYING" file of the
VirtualBox OSE distribution. VirtualBox OSE is distributed in the
hope that it will be useful, but WITHOUT ANY WARRANTY of any kind.
The contents of this file may alternatively be used under the terms
of the Common Development and Distribution License Version 1.0
(CDDL) only, as it comes in the "COPYING.CDDL" file of the
VirtualBox OSE 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.
"""
__version__ = "$Revision$"
# 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;
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 TMExceptionBase('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 TMExceptionBase('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=R0903
"""
Build categories database logic.
"""
def fetchForListing(self, iStart, cMaxRows, tsNow):
"""
Fetches testboxes for listing.
Returns an array (list) of UserAccountData items, empty list if none.
Raises exception on error.
"""
_ = tsNow;
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 TMExceptionBase('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;
#
# 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 len(aaoRows) == 0:
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 len(aaoRows) == 0:
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);
if len(dDataErrors) > 0:
raise TMExceptionBase('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 TMExceptionBase('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 TMExceptionBase('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 len(sBinary) == 0:
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.
"""
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 TMExceptionBase('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 TMExceptionBase('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=R0903
"""
Build database logic (covers build categories as well as builds).
"""
#
# Standard methods.
#
def fetchForListing(self, iStart, cMaxRows, tsNow):
"""
Fetches builds for listing.
Returns an array (list) of BuildDataEx items, empty list if none.
Raises exception on error.
"""
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);
if len(dErrors) > 0:
raise TMExceptionBase('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;
#
# 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(self, 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 i in range(len(oBuildEx.oCat.asOsArches)):
asParts = oBuildEx.oCat.asOsArches[i].split('.');
if len(asParts) != 2 or len(asParts[0]) == 0 or len(asParts[1]) == 0:
raise self._oDb.integrityException('Bad build asOsArches value: %s (idBuild=%s idBuildCategory=%s)'
% (oBuildEx.asOsArches[i], 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 TMExceptionBase('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=C0111
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.