# -*- coding: utf-8 -*-
# $Id$
# pylint: disable=C0302
"""
Test Manager - Test Case.
"""
__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;
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.
"""
# Standard python imports.
import copy;
import unittest;
# Validation Kit imports.
from testmanager.core.base import ModelDataBase, ModelDataBaseTestCase, ModelLogicBase, TMExceptionBase, \
"""
Test case dependency on a global resource - data.
"""
#
# Initialize with defaults.
# See the database for explanations of each of these fields.
#
self.idTestCase = None;
self.idGlobalRsrc = None;
self.tsEffective = None;
"""
Reinitialize from a SELECT * FROM TestCaseDeps row.
"""
if aoRow is None:
raise TMExceptionBase('Test case not found.');
return self;
"""
Test case dependency on a global resources - logic.
"""
"""
Returns an array of (TestCaseGlobalRsrcDepData, GlobalResourceData)
with the global resources required by idTestCase.
Returns empty array if none found. Raises exception on database error.
Note! Maybe a bit overkill...
"""
## @todo This code isn't entirely kosher... Should use a DataEx with a oGlobalRsrc = GlobalResourceData().
if tsNow is not None:
'FROM TestCaseGlobalRsrcDeps, GlobalResources\n'
'WHERE TestCaseGlobalRsrcDeps.idTestCase = %s\n'
' AND TestCaseGlobalRsrcDeps.tsExpire > %s\n'
' AND TestCaseGlobalRsrcDeps.tsEffective <= %s\n'
' AND GlobalResources.idGlobalRsrc = TestCaseGlobalRsrcDeps.idGlobalRsrc\n'
' AND GlobalResources.tsExpire > %s\n'
' AND GlobalResources.tsEffective <= %s\n'
else:
'FROM TestCaseGlobalRsrcDeps, GlobalResources\n'
'WHERE TestCaseGlobalRsrcDeps.idTestCase = %s\n'
' AND GlobalResources.idGlobalRsrc = TestCaseGlobalRsrcDeps.idGlobalRsrc\n'
' AND TestCaseGlobalRsrcDeps.tsExpire = \'infinity\'::TIMESTAMP\n'
' AND GlobalResources.tsExpire = \'infinity\'::TIMESTAMP\n'
, (idTestCase,))
aoRet = []
return aoRet
"""
Returns an array of global resources that idTestCase require.
Returns empty array if none found. Raises exception on database error.
"""
if tsNow is not None:
'FROM TestCaseGlobalRsrcDeps\n'
'WHERE TestCaseGlobalRsrcDeps.idTestCase = %s\n'
' AND TestCaseGlobalRsrcDeps.tsExpire > %s\n'
' AND TestCaseGlobalRsrcDeps.tsEffective <= %s\n'
else:
'FROM TestCaseGlobalRsrcDeps\n'
'WHERE TestCaseGlobalRsrcDeps.idTestCase = %s\n'
' AND TestCaseGlobalRsrcDeps.tsExpire = \'infinity\'::TIMESTAMP\n'
, (idTestCase,))
aidGlobalRsrcs = []
return aidGlobalRsrcs;
"""
Returns an array of objects of type GlobalResourceData on which the
specified test case depends on.
"""
if tsNow is None :
'FROM TestCaseGlobalRsrcDeps, GlobalResources\n'
'WHERE TestCaseGlobalRsrcDeps.idTestCase = %s\n'
' AND GlobalResources.idGlobalRsrc = TestCaseGlobalRsrcDeps.idGlobalRsrc\n'
' AND TestCaseGlobalRsrcDeps.tsExpire = \'infinity\'::TIMESTAMP\n'
' AND GlobalResources.tsExpire = \'infinity\'::TIMESTAMP\n'
'ORDER BY GlobalResources.idGlobalRsrc\n'
, (idTestCase,))
else:
'FROM TestCaseGlobalRsrcDeps, GlobalResources\n'
'WHERE TestCaseGlobalRsrcDeps.idTestCase = %s\n'
' AND GlobalResources.idGlobalRsrc = TestCaseGlobalRsrcDeps.idGlobalRsrc\n'
' AND TestCaseGlobalRsrcDeps.tsExpire > %s\n'
' AND TestCaseGlobalRsrcDeps.tsExpire <= %s\n'
' AND GlobalResources.tsExpire > %s\n'
' AND GlobalResources.tsEffective <= %s\n'
'ORDER BY GlobalResources.idGlobalRsrc\n'
aoRet = []
return aoRet
"""
Test case dependency data
"""
#
# Initialize with defaults.
# See the database for explanations of each of these fields.
#
self.idTestCase = None;
self.idTestCasePreReq = None;
self.tsEffective = None;
"""
Reinitialize from a SELECT * FROM TestCaseDeps row.
"""
if aoRow is None:
raise TMExceptionBase('Test case not found.');
return self;
"""
Initialize the object from parameters.
The input is not validated at all, except that all parameters must be
present when fStrict is True.
Note! Returns parameter NULL values, not database ones.
"""
self.idTestCasePreReq = fn(self.ksParam_idTestCasePreReq, None, None if fStrict else self.idTestCasePreReq);
return True
"""
Validates the input and converts valid fields to their right type.
Returns a dictionary with per field reports, only invalid fields will
be returned, so an empty dictionary means that the data is valid.
The dictionary keys are ksParam_*.
"""
self.idTestCasePreReq = self._validateInt( dErrors, self.ksParam_idTestCasePreReq, self.idTestCasePreReq);
_ = oDb;
return dErrors
"""
Converts from parameter NULL values to database NULL values (None).
"""
return True;
"""
Converts from database NULL values (None) to special values we can
pass thru parameters list.
"""
return True;
""" Compares two instances. """
"""
Get list of Test Case IDs which current
Test Case depends on
"""
return []
aoRet = []
return aoRet
"""Test case dependency management logic"""
"""
Returns an array of TestCaseDependencyData with the prerequisites of
idTestCase.
Returns empty array if none found. Raises exception on database error.
"""
if tsEffective is not None:
'FROM TestCaseDeps\n'
'WHERE idTestCase = %s\n'
' AND tsExpire > %s\n'
' AND tsEffective <= %s\n'
else:
'FROM TestCaseDeps\n'
'WHERE idTestCase = %s\n'
' AND tsExpire = \'infinity\'::TIMESTAMP\n'
, (idTestCase, ) );
aoRet = [];
return aoRet
"""
Returns an array of test case IDs of the prerequisites of idTestCase.
Returns empty array if none found. Raises exception on database error.
"""
if tsNow is not None:
'FROM TestCaseDeps\n'
'WHERE idTestCase = %s\n'
' AND tsExpire > %s\n'
' AND tsEffective <= %s\n'
else:
'FROM TestCaseDeps\n'
'WHERE idTestCase = %s\n'
' AND tsExpire = \'infinity\'::TIMESTAMP\n'
, (idTestCase, ) );
aidPreReqs = [];
return aidPreReqs;
"""
Returns an array of objects of type TestCaseData2 on which
specified test case depends on
"""
if tsNow is None:
'FROM TestCases, TestCaseDeps\n'
'WHERE TestCaseDeps.idTestCase = %s\n'
' AND TestCaseDeps.idTestCasePreReq = TestCases.idTestCase\n'
' AND TestCaseDeps.tsExpire = \'infinity\'::TIMESTAMP\n'
' AND TestCases.tsExpire = \'infinity\'::TIMESTAMP\n'
'ORDER BY TestCases.idTestCase\n'
, (idTestCase, ) );
else:
'FROM TestCases, TestCaseDeps\n'
'WHERE TestCaseDeps.idTestCase = %s\n'
' AND TestCaseDeps.idTestCasePreReq = TestCases.idTestCase\n'
' AND TestCaseDeps.tsExpire > %s\n'
' AND TestCaseDeps.tsEffective <= %s\n'
' AND TestCases.tsExpire > %s\n'
' AND TestCases.tsEffective <= %s\n'
'ORDER BY TestCases.idTestCase\n'
aoRet = []
return aoRet
"""
Returns an array of objects of type TestCaseData on which
specified test case might depends on (all test
cases except the specified one and those testcases which are
depend on idTestCase)
"""
'FROM TestCases\n'
'WHERE idTestCase <> %s\n'
' AND idTestCase NOT IN (SELECT idTestCase\n'
' FROM TestCaseDeps\n'
' WHERE idTestCasePreReq=%s\n'
' AND tsExpire = \'infinity\'::TIMESTAMP)\n'
' AND tsExpire = \'infinity\'::TIMESTAMP\n'
, (idTestCase, idTestCase) )
aoRet = []
return aoRet
"""
Test case data
"""
kasAllowNullAttributes = [ 'idTestCase', 'tsEffective', 'tsExpire', 'uidAuthor', 'idGenTestCase', 'sDescription',
'sTestBoxReqExpr', 'sBuildReqExpr', 'sValidationKitZips', ];
#
# Initialize with defaults.
# See the database for explanations of each of these fields.
#
self.idTestCase = None;
self.tsEffective = None;
self.idGenTestCase = None;
self.sDescription = None;
self.sTestBoxReqExpr = None;
self.sBuildReqExpr = None;
self.sValidationKitZips = None;
"""
Reinitialize from a SELECT * FROM TestCases row.
Returns self. Raises exception if no row.
"""
if aoRow is None:
raise TMExceptionBase('Test case not found.');
return self;
"""
Initialize the object from the database.
"""
'SELECT *\n'
'FROM TestCases\n'
'WHERE idTestCase = %s\n'
if aoRow is None:
raise TMExceptionBase('idTestCase=%s not found (tsNow=%s sPeriodBack=%s)' % (idTestCase, tsNow, sPeriodBack,));
"""
Initialize the object from the database.
"""
_ = tsNow; # For relevant for the TestCaseDataEx version only.
'FROM TestCases\n'
'WHERE idGenTestCase = %s\n'
, (idGenTestCase, ) );
(oValue, sError) = ModelDataBase._validateAndConvertAttribute(self, sAttr, sParam, oValue, aoNilValues, fAllowNull, oDb);
if sError is None:
if sAttr == 'sTestBoxReqExpr':
elif sAttr == 'sBuildReqExpr':
elif sAttr == 'sBaseCmd':
#
# Misc.
#
"""
Predicate method for checking whether a validation kit build is required.
"""
return self.sValidationKitZips is None \
"""
Checks if the all of the testbox related test requirements matches the
given testbox.
Returns True or False according to the expression, None on exception or
non-boolean expression result.
"""
"""
Checks if the all of the build related test requirements matches the
given build.
Returns True or False according to the expression, None on exception or
non-boolean expression result.
"""
#
# Expression validation code shared with TestCaseArgsDataEx.
#
"""
Safely evaluate requirment expression given a set of locals.
Returns True or False according to the expression. If the expression
causes an exception to be raised or does not return a boolean result,
None will be returned.
"""
return True;
dGlobals = \
{
'__builtins__': None,
'long': long,
'int': int,
'bool': bool,
'True': True,
'False': False,
'len': len,
'isinstance': isinstance,
'type': type,
'dict': dict,
'dir': dir,
'list': list,
};
try:
except:
if fMayRaiseXcpt:
raise;
return None;
if fMayRaiseXcpt:
return None;
return fRc;
"""
Validates a requirement expression using the given sets of locals,
returning None on success and an error string on failure.
"""
try:
return None;
"""
Validates a testbox expression, returning None on success and an error
string on failure.
"""
adTestBoxes = \
[
{
'sOs': 'win',
'sOsVersion': '3.1',
'sCpuVendor': 'VirtualBox',
'sCpuArch': 'x86',
'cCpus': 1,
'fCpuHwVirt': False,
'fCpuNestedPaging': False,
'fCpu64BitGuest': False,
'fChipsetIoMmu': False,
'cMbMemory': 985034,
'cMbScratch': 1234089,
'iTestBoxScriptRev': 1,
'sName': 'emanon',
'uuidSystem': '8FF81BE5-3901-4AB1-8A65-B48D511C0321',
},
{
'sOs': 'linux',
'sOsVersion': '3.1',
'sCpuVendor': 'VirtualBox',
'sCpuArch': 'amd64',
'cCpus': 8191,
'fCpuHwVirt': True,
'fCpuNestedPaging': True,
'fCpu64BitGuest': True,
'fChipsetIoMmu': True,
'cMbMemory': 9999999999,
'cMbScratch': 9999999999999,
'iTestBoxScriptRev': 9999999,
'sName': 'emanon',
'uuidSystem': '00000000-0000-0000-0000-000000000000',
},
];
""" Worker for TestCaseData.matchesTestBoxProps and TestCaseArgsDataEx.matchesTestBoxProps. """
if sExpr is None:
return True;
dLocals = \
{
};
"""
Validates a testbox expression, returning None on success and an error
string on failure.
"""
adBuilds = \
[
{
'sProduct': 'VirtualBox',
'sBranch': 'trunk',
'sType': 'release',
'asOsArches': ['win.amd64', 'win.x86'],
'sVersion': '1.0',
'iRevision': 1234,
'uidAuthor': None,
'idBuild': 953,
},
{
'sProduct': 'VirtualBox',
'sBranch': 'VBox-4.1',
'sType': 'release',
'asOsArches': ['linux.x86',],
'sVersion': '4.2.15',
'iRevision': 89876,
'uidAuthor': None,
'idBuild': 945689,
},
{
'sProduct': 'VirtualBox',
'sBranch': 'VBox-4.1',
'sType': 'strict',
'asOsArches': ['solaris.x86', 'solaris.amd64',],
'sVersion': '4.3.0_RC3',
'iRevision': 97939,
'uidAuthor': 33,
'idBuild': 9456893,
},
];
"""
Checks if the all of the build related test requirements matches the
given build.
"""
if sExpr is None:
return True;
dLocals = \
{
};
"""
Test case data.
"""
# Use [] instead of None.
# List of objects of type TestCaseData (or TestCaseDataEx, we don't
# care) on which current Test Case depends.
self.aoDepTestCases = [];
# List of objects of type GlobalResourceData on which current Test Case depends.
self.aoDepGlobalResources = [];
# List of objects of type TestCaseArgsData.
self.aoTestCaseArgs = [];
"""
Worker shared by the initFromDb* methods.
Returns self. Raises exception if no row or database error.
"""
_ = sPeriodBack; ## @todo sPeriodBack
self.aoDepGlobalResources = TestCaseGlobalRsrcDepLogic(oDb).getDepGlobalResourceData(self.idTestCase, tsNow);
# Note! The above arrays are sorted by their relvant IDs for fetchForChangeLog's sake.
return self;
"""
Reinitialize from a SELECT * FROM TestCases row. Will query the
necessary additional data from oDb using tsNow.
Returns self. Raises exception if no row or database error.
"""
"""
Initialize the object from the database.
"""
"""
Initialize the object from the database.
"""
return [[], ''];
"""For dealing with the arrays."""
aoNewValues = [];
if sAttr == 'aoDepTestCases':
oDep = TestCaseData();
elif sAttr == 'aoDepGlobalResources':
elif sAttr == 'aoTestCaseArgs':
for sArgKey in oDisp.getStringParam(TestCaseDataEx.ksParam_aoTestCaseArgs, sDefault = '').split(','):
oDispWrapper = self.DispWrapper(oDisp, '%s[%s][%%s]' % (TestCaseDataEx.ksParam_aoTestCaseArgs, sArgKey,))
return aoNewValues;
def _validateAndConvertAttribute(self, sAttr, sParam, oValue, aoNilValues, fAllowNull, oDb): # pylint: disable=R0914
"""
Validate special arrays and requirement expressions.
For the two dependency arrays we have to supply missing bits by
looking them up in the database. In the argument variation case we
need to validate each item.
"""
return TestCaseData._validateAndConvertAttribute(self, sAttr, sParam, oValue, aoNilValues, fAllowNull, oDb);
asErrors = [];
aoNewValues = [];
if sAttr == 'aoDepTestCases':
try:
elif sAttr == 'aoDepGlobalResources':
try:
else:
assert sAttr == 'aoTestCaseArgs';
return (None, 'The testcase requires at least one argument variation to be valid.');
# Note! We'll be returning an error dictionary instead of an string here.
dErrors = {};
pass; ## @todo figure out the ID?
else:
asErrors = [];
for sKey in dCurErrors:
break;
# Validate dependencies a wee bit for paranoid reasons. The scheduler
# queue generation code does the real validation here!
else:
return dErrors;
"""
Test case management logic.
"""
"""
Fetches all test case records from DB (TestCaseData).
"""
'FROM TestCases\n'
'WHERE tsExpire = \'infinity\'::TIMESTAMP\n'
'ORDER BY idTestCase ASC;')
aoRet = [];
return aoRet
"""
Fetches test cases.
Returns an array (list) of TestCaseDataEx items, empty list if none.
Raises exception on error.
"""
if tsNow is None:
'FROM TestCases\n'
'WHERE tsExpire = \'infinity\'::TIMESTAMP\n'
'ORDER BY sName ASC\n'
'LIMIT %s OFFSET %s\n'
else:
'FROM TestCases\n'
'WHERE tsExpire > %s\n'
' AND tsEffective <= %s\n'
'ORDER BY sName ASC\n'
'LIMIT %s OFFSET %s\n'
aoRows = [];
return aoRows;
"""
Fetches change log entries for a testbox.
Returns an array of ChangeLogEntry instance and an indicator whether
there are more entries.
Raises exception on error.
"""
if tsNow is None:
# 1. Get a list of the relevant change times.
self._oDb.execute('( SELECT tsEffective, uidAuthor FROM TestCases WHERE idTestCase = %s AND tsEffective <= %s )\n'
'UNION\n'
'( SELECT tsEffective, uidAuthor FROM TestCaseArgs WHERE idTestCase = %s AND tsEffective <= %s )\n'
'UNION\n'
'( SELECT tsEffective, uidAuthor FROM TestCaseDeps WHERE idTestCase = %s AND tsEffective <= %s )\n'
'UNION\n'
'( SELECT tsEffective, uidAuthor FROM TestCaseGlobalRsrcDeps \n' \
' WHERE idTestCase = %s AND tsEffective <= %s )\n'
'ORDER BY tsEffective DESC\n'
'LIMIT %s OFFSET %s\n'
, ( idTestCase, tsNow,
# 2. Collect data sets for each of those points.
# (Doing it the lazy + inefficient way for now.)
aoRows = [];
for aoChange in aaoChanges:
# 3. Calculate the changes.
aoEntries = [];
assert self._oDb.isTsInfinity(tsEffective) != self._oDb.isTsInfinity(tsExpire) or tsEffective < tsExpire, \
aoChanges = [];
# The testcase object.
'aoTestCaseArgs', 'aoDepTestCases', 'aoDepGlobalResources']:
# The argument variations.
iChildOld = 0;
# Locate the old entry, emitting removed markers for old items we have to skip.
iChildOld += 1;
iChildOld += 1;
else:
oChildNew, None,
# The testcase dependencies.
iChildOld = 0;
# Locate the old entry, emitting removed markers for old items we have to skip.
None, oChildOld, 'Removed',
iChildOld += 1;
iChildOld += 1;
else:
oChildNew, None,
'Did not exist'));
# The global resource dependencies.
iChildOld = 0;
# Locate the old entry, emitting removed markers for old items we have to skip.
None, oChildOld, 'Removed',
iChildOld += 1;
iChildOld += 1;
else:
oChildNew, None,
'Did not exist'));
# Done.
# If we're at the end of the log, add the initial entry.
oNew, None, []));
"""
Add a new testcase to the DB.
"""
#
# Validate the input first.
#
#
# Add the testcase.
#
# Add testcase dependencies.
self._oDb.execute('INSERT INTO TestCaseDeps (idTestCase, idTestCasePreReq, uidAuthor) VALUES (%s, %s, %s)'
# Add global resource dependencies.
self._oDb.execute('INSERT INTO TestCaseGlobalRsrcDeps (idTestCase, idGlobalRsrc, uidAuthor) VALUES (%s, %s, %s)'
# Set Test Case Arguments variations
' idTestCase, uidAuthor, sArgs, cSecTimeout,\n'
' sTestBoxReqExpr, sBuildReqExpr, cGangMembers)\n'
'VALUES (%s, %s, %s, %s, %s, %s, %s)'
return True;
"""
Edit a testcase entry (extended).
Caller is expected to rollback the database transactions on exception.
"""
#
# Validate the input.
#
#
# Did anything change? If not return straight away.
#
return True;
#
# Make the necessary changes.
#
# The test case itself.
self._oDb.callProc('TestCaseLogic_editEntry', ( uidAuthor, oData.idTestCase, oData.sName, oData.sDescription,
#
# Its dependencies on other testcases.
#
'SET tsExpire = CURRENT_TIMESTAMP\n'
'WHERE idTestCase = %s\n'
' AND tsExpire = \'infinity\'::timestamp\n'
, (oData.idTestCase,));
asKeepers = [];
for idDep in aidOldDeps:
if idDep in aidNewDeps:
for idDep in aidNewDeps:
if idDep not in aidOldDeps:
'VALUES (%s, %s, %s)\n'
#
# Its dependencies on global resources.
#
'SET tsExpire = CURRENT_TIMESTAMP\n'
'WHERE idTestCase = %s\n'
' AND tsExpire = \'infinity\'::timestamp\n'
, (oData.idTestCase,));
asKeepers = [];
for idDep in aidOldDeps:
if idDep in aidNewDeps:
for idDep in aidNewDeps:
if idDep not in aidOldDeps:
'VALUES (%s, %s, %s)\n'
#
# Update Test Case Args
# Note! Primary key is idTestCase, tsExpire, sArgs.
#
# Historize rows that have been removed.
'SET tsExpire = CURRENT_TIMESTAMP\n'
'WHERE idTestCase = %s\n'
' AND tsExpire = \'infinity\'::TIMESTAMP'
, (oData.idTestCase, ));
# Add new TestCaseArgs records if necessary, reusing old IDs when possible.
'FROM TestCaseArgs\n'
'WHERE idTestCase = %s\n'
' AND sArgs = %s\n'
'ORDER BY tsExpire DESC\n'
'LIMIT 1\n'
if aoRow is None:
# New
' idTestCase, uidAuthor, sArgs, cSecTimeout,\n'
' sTestBoxReqExpr, sBuildReqExpr, cGangMembers)\n'
'VALUES (%s, %s, %s, %s, %s, %s, %s)'
else:
# Existing current entry, updated if changed.
continue; # Unchanged.
self._oDb.execute('UPDATE TestCaseArgs SET tsExpire = CURRENT_TIMESTAMP WHERE idGenTestCaseArgs = %s\n'
, (oCurVar.idGenTestCaseArgs, ));
else:
# Existing old entry, re-use the ID.
pass;
' idTestCaseArgs, idTestCase, uidAuthor, sArgs, cSecTimeout,\n'
' sTestBoxReqExpr, sBuildReqExpr, cGangMembers)\n'
'VALUES (%s, %s, %s, %s, %s, %s, %s, %s)\n'
'RETURNING idGenTestCaseArgs\n'
return True;
""" Deletes the test case if possible. """
return True
"""
Returns an array of prerequisite testcases (IDs) for the given testcase.
May raise exception on database error or if the result exceeds cMax.
"""
if tsEffective is None:
'FROM TestCaseDeps\n'
'WHERE idTestCase = %s\n'
' AND tsExpire = \'infinity\'::TIMESTAMP\n'
'ORDER BY idTestCasePreReq\n'
, (idTestCase,) );
else:
'FROM TestCaseDeps\n'
'WHERE idTestCase = %s\n'
' AND tsExpire > %s\n'
' AND tsEffective <= %s\n'
'ORDER BY idTestCasePreReq\n'
raise TMExceptionBase('Too many prerequisites for testcase %s: %s, max %s'
aidPreReqs = [];
return aidPreReqs;
#
# Unit testing.
#
# pylint: disable=C0111
self.assertNotEqual(TestCaseData.validateTestBoxReqExpr('this is an bad expression, surely it must be'), None);
self.assertNotEqual(TestCaseData.validateTestBoxReqExpr('__import__(\'os\').unlink(\'/tmp/no/such/file\')'), None);
if __name__ == '__main__':
# not reached.