diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-11 08:17:27 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-11 08:17:27 +0000 |
commit | f215e02bf85f68d3a6106c2a1f4f7f063f819064 (patch) | |
tree | 6bb5b92c046312c4e95ac2620b10ddf482d3fa8b /src/VBox/ValidationKit/testmanager | |
parent | Initial commit. (diff) | |
download | virtualbox-c31cb7e733ae8e33d73383e04db264d4293f0b1a.tar.xz virtualbox-c31cb7e733ae8e33d73383e04db264d4293f0b1a.zip |
Adding upstream version 7.0.14-dfsg.upstream/7.0.14-dfsg
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'src/VBox/ValidationKit/testmanager')
235 files changed, 58661 insertions, 0 deletions
diff --git a/src/VBox/ValidationKit/testmanager/Makefile.kmk b/src/VBox/ValidationKit/testmanager/Makefile.kmk new file mode 100644 index 00000000..de7c4cc0 --- /dev/null +++ b/src/VBox/ValidationKit/testmanager/Makefile.kmk @@ -0,0 +1,54 @@ +# $Id: Makefile.kmk $ +## @file +# VirtualBox Validation Kit. +# + +# +# Copyright (C) 2006-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 +# + +SUB_DEPTH = ../../../.. +include $(KBUILD_PATH)/subheader.kmk + +include $(PATH_SUB_CURRENT)/cgi/Makefile.kmk +include $(PATH_SUB_CURRENT)/core/Makefile.kmk +include $(PATH_SUB_CURRENT)/batch/Makefile.kmk +include $(PATH_SUB_CURRENT)/debug/Makefile.kmk +include $(PATH_SUB_CURRENT)/misc/Makefile.kmk +include $(PATH_SUB_CURRENT)/webui/Makefile.kmk + +VBOX_VALIDATIONKIT_PYTHON_SOURCES += $(wildcard $(PATH_SUB_CURRENT)/*.py) +VBOX_VALIDATIONKIT_JS_SOURCES += $(wildcard $(PATH_SUB_CURRENT)/htdocs/js/*.js) + + +$(evalcall def_vbox_validationkit_process_python_sources) +$(evalcall def_vbox_validationkit_process_js_sources) +include $(FILE_KBUILD_SUB_FOOTER) + diff --git a/src/VBox/ValidationKit/testmanager/__init__.py b/src/VBox/ValidationKit/testmanager/__init__.py new file mode 100644 index 00000000..71137716 --- /dev/null +++ b/src/VBox/ValidationKit/testmanager/__init__.py @@ -0,0 +1,40 @@ +# -*- coding: utf-8 -*- +# $Id: __init__.py $ + +""" +Test Manager. +""" + +__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 $" + diff --git a/src/VBox/ValidationKit/testmanager/apache-template-2.2.conf b/src/VBox/ValidationKit/testmanager/apache-template-2.2.conf new file mode 100644 index 00000000..0064cfaa --- /dev/null +++ b/src/VBox/ValidationKit/testmanager/apache-template-2.2.conf @@ -0,0 +1,87 @@ +# $Id: apache-template-2.2.conf $ +## @file +# Test Manager - Apache 2.2 configuration sample. +# +# Requires TestManagerRootDir to be set in the environment (envvars file for instance). +# + +# +# 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 +# + + +<LocationMatch "^/testmanager/logout.py"> + AuthType Basic + AuthName "Test Manager" + AuthUserFile ${TestManagerRootDir}/misc/htpasswd-logout + Require user logout +</LocationMatch> + +<LocationMatch "^/testmanager/(?!(testboxdisp.py|logout.py|/*htdocs/downloads/.*))"> + AuthType Basic + AuthName "Test Manager" + AuthUserFile ${TestManagerRootDir}/misc/htpasswd-sample + Require valid-user +</LocationMatch> + +# These two directives are only for local testing! +Alias /testmanager/htdocs/downloads/VBoxValidationKit.zip ${VBoxBuildOutputDir}/VBoxValidationKit.zip +<Location /testmanager/htdocs/downloads/VBoxValidationKit.zip> + Options Indexes + Order allow,deny + Allow from all +</Location> + +Alias /testmanager/htdocs/ ${TestManagerRootDir}/htdocs/ +<Directory ${TestManagerRootDir}/htdocs/> + AllowOverride None + Options Indexes + Order allow,deny + Allow from all +</Directory> + +Alias /testmanager/logs/ /var/tmp/testmanager/ +<Directory /var/tmp/testmanager/> + AllowOverride None + Options Indexes + Order allow,deny + Allow from all +</Directory> + +Alias /testmanager/ ${TestManagerRootDir}/cgi/ +<Directory ${TestManagerRootDir}/cgi/> + AllowOverride None + Options Indexes ExecCGI + DirectoryIndex index.py + AddHandler cgi-script .py + Order allow,deny + Allow from all +</Directory> + diff --git a/src/VBox/ValidationKit/testmanager/apache-template-2.4.conf b/src/VBox/ValidationKit/testmanager/apache-template-2.4.conf new file mode 100644 index 00000000..e38c5924 --- /dev/null +++ b/src/VBox/ValidationKit/testmanager/apache-template-2.4.conf @@ -0,0 +1,81 @@ +# $Id: apache-template-2.4.conf $ +## @file +# Test Manager - Apache 2.4 configuration sample. +# +# Use the new Define directive to define TestManagerRootDir and +# VBoxBuildOutputDir before including this file. +# + +# +# 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 +# + + +<LocationMatch "^/testmanager/logout.py"> + AuthType Basic + AuthName "Test Manager" + AuthUserFile ${TestManagerRootDir}/misc/htpasswd-logout + Require user logout +</LocationMatch> + +<LocationMatch "^/testmanager/(?!(testboxdisp.py|logout.py|/*htdocs/downloads/.*))"> + AuthType Basic + AuthName "Test Manager" + AuthUserFile ${TestManagerRootDir}/misc/htpasswd-sample + Require valid-user +</LocationMatch> + +# These two directives are only for local testing! +Alias /testmanager/htdocs/downloads/VBoxValidationKit.zip ${VBoxBuildOutputDir}/VBoxValidationKit.zip +<Location /testmanager/htdocs/downloads/VBoxValidationKit.zip> + Options Indexes + Require all granted +</Location> + +Alias /testmanager/htdocs/ ${TestManagerRootDir}/htdocs/ +<Directory ${TestManagerRootDir}/htdocs/> + AllowOverride None + Options Indexes +</Directory> + +Alias /testmanager/logs/ /var/tmp/testmanager/ +<Directory /var/tmp/testmanager/> + AllowOverride None + Options Indexes +</Directory> + +Alias /testmanager/ ${TestManagerRootDir}/cgi/ +<Directory ${TestManagerRootDir}/cgi/> + AllowOverride None + Options Indexes ExecCGI + DirectoryIndex index.py + AddHandler cgi-script .py +</Directory> + diff --git a/src/VBox/ValidationKit/testmanager/batch/Makefile.kmk b/src/VBox/ValidationKit/testmanager/batch/Makefile.kmk new file mode 100644 index 00000000..74d882cc --- /dev/null +++ b/src/VBox/ValidationKit/testmanager/batch/Makefile.kmk @@ -0,0 +1,46 @@ +# $Id: Makefile.kmk $ +## @file +# VirtualBox Validation Kit. +# + +# +# Copyright (C) 2006-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 +# + +SUB_DEPTH = ../../../../.. +include $(KBUILD_PATH)/subheader.kmk + + +VBOX_VALIDATIONKIT_PYTHON_SOURCES += $(wildcard $(PATH_SUB_CURRENT)/*.py) + +$(evalcall def_vbox_validationkit_process_python_sources) +$(evalcall def_vbox_validationkit_process_js_sources) +include $(FILE_KBUILD_SUB_FOOTER) + diff --git a/src/VBox/ValidationKit/testmanager/batch/add_build.py b/src/VBox/ValidationKit/testmanager/batch/add_build.py new file mode 100755 index 00000000..a82da5eb --- /dev/null +++ b/src/VBox/ValidationKit/testmanager/batch/add_build.py @@ -0,0 +1,137 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# $Id: add_build.py $ +# pylint: disable=line-too-long + +""" +Interface used by the tinderbox server side software to add a fresh build. +""" + +__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 sys; +import os; +from optparse import OptionParser; # pylint: disable=deprecated-module + +# Add Test Manager's modules path +g_ksTestManagerDir = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))); +sys.path.append(g_ksTestManagerDir); + +# Test Manager imports +from testmanager.core.db import TMDatabaseConnection; +from testmanager.core.build import BuildDataEx, BuildLogic, BuildCategoryData; + +class Build(object): # pylint: disable=too-few-public-methods + """ + Add build info into Test Manager database. + """ + + def __init__(self): + """ + Parse command line. + """ + + oParser = OptionParser(); + oParser.add_option('-q', '--quiet', dest = 'fQuiet', action = 'store_true', + help = 'Quiet execution'); + oParser.add_option('-b', '--branch', dest = 'sBranch', metavar = '<branch>', + help = 'branch name (default: trunk)', default = 'trunk'); + oParser.add_option('-p', '--product', dest = 'sProductName', metavar = '<name>', + help = 'The product name.'); + oParser.add_option('-r', '--revision', dest = 'iRevision', metavar = '<rev>', + help = 'revision number'); + oParser.add_option('-R', '--repository', dest = 'sRepository', metavar = '<repository>', + help = 'Version control repository name.'); + oParser.add_option('-t', '--type', dest = 'sBuildType', metavar = '<type>', + help = 'build type (debug, release etc.)'); + oParser.add_option('-v', '--version', dest = 'sProductVersion', metavar = '<ver>', + help = 'The product version number (suitable for RTStrVersionCompare)'); + oParser.add_option('-o', '--os-arch', dest = 'asTargetOsArches', metavar = '<os.arch>', action = 'append', + help = 'Target OS and architecture. This option can be repeated.'); + oParser.add_option('-l', '--log', dest = 'sBuildLogPath', metavar = '<url>', + help = 'URL to the build logs (optional).'); + oParser.add_option('-f', '--file', dest = 'asFiles', metavar = '<file|url>', action = 'append', + help = 'URLs or build share relative path to a build output file. This option can be repeated.'); + + (self.oConfig, _) = oParser.parse_args(); + + # Check command line + asMissing = []; + if self.oConfig.sBranch is None: asMissing.append('--branch'); + if self.oConfig.iRevision is None: asMissing.append('--revision'); + if self.oConfig.sProductVersion is None: asMissing.append('--version'); + if self.oConfig.sProductName is None: asMissing.append('--product'); + if self.oConfig.sBuildType is None: asMissing.append('--type'); + if self.oConfig.asTargetOsArches is None: asMissing.append('--os-arch'); + if self.oConfig.asFiles is None: asMissing.append('--file'); + if asMissing: + sys.stderr.write('syntax error: Missing: %s\n' % (asMissing,)); + sys.exit(1); + # Temporary default. + if self.oConfig.sRepository is None: + self.oConfig.sRepository = 'vbox'; + + def add(self): + """ + Add build data record into database. + """ + oDb = TMDatabaseConnection() + + # Assemble the build data. + oBuildData = BuildDataEx() + oBuildData.idBuildCategory = None; + oBuildData.iRevision = self.oConfig.iRevision + oBuildData.sVersion = self.oConfig.sProductVersion + oBuildData.sLogUrl = self.oConfig.sBuildLogPath + oBuildData.sBinaries = ','.join(self.oConfig.asFiles); + oBuildData.oCat = BuildCategoryData().initFromValues(sProduct = self.oConfig.sProductName, + sRepository = self.oConfig.sRepository, + sBranch = self.oConfig.sBranch, + sType = self.oConfig.sBuildType, + asOsArches = self.oConfig.asTargetOsArches); + + # Add record to database + try: + BuildLogic(oDb).addEntry(oBuildData, fCommit = True); + except: + if self.oConfig.fQuiet: + sys.exit(1); + raise; + oDb.close(); + return 0; + +if __name__ == '__main__': + sys.exit(Build().add()); + diff --git a/src/VBox/ValidationKit/testmanager/batch/check_for_deleted_builds.py b/src/VBox/ValidationKit/testmanager/batch/check_for_deleted_builds.py new file mode 100755 index 00000000..1ac5ab95 --- /dev/null +++ b/src/VBox/ValidationKit/testmanager/batch/check_for_deleted_builds.py @@ -0,0 +1,133 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# $Id: check_for_deleted_builds.py $ +# pylint: disable=line-too-long + +""" +Admin job for checking detecting deleted builds. + +This is necessary when the tinderbox <-> test manager interface was +busted and the build info in is out of sync. The result is generally +a lot of skipped tests because of missing builds, typically during +bisecting problems. +""" + +from __future__ import print_function; + +__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 sys; +import os; +from optparse import OptionParser; # pylint: disable=deprecated-module + +# Add Test Manager's modules path +g_ksTestManagerDir = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))); +sys.path.append(g_ksTestManagerDir); + +# Test Manager imports +from testmanager.core.db import TMDatabaseConnection; +from testmanager.core.build import BuildLogic; + + + +class BuildChecker(object): # pylint: disable=too-few-public-methods + """ + Add build info into Test Manager database. + """ + + def __init__(self): + """ + Parse command line. + """ + + oParser = OptionParser(); + oParser.add_option('-q', '--quiet', dest = 'fQuiet', action = 'store_true', default = False, + help = 'Quiet execution'); + oParser.add_option('--dry-run', dest = 'fRealRun', action = 'store_false', default = False, + help = 'Dry run'); + oParser.add_option('--real-run', dest = 'fRealRun', action = 'store_true', default = False, + help = 'Real run'); + + (self.oConfig, _) = oParser.parse_args(); + if not self.oConfig.fQuiet: + if not self.oConfig.fRealRun: + print('Dry run.'); + else: + print('Real run! Will commit findings!'); + + + def checkBuilds(self): + """ + Add build data record into database. + """ + oDb = TMDatabaseConnection(); + oBuildLogic = BuildLogic(oDb); + + tsNow = oDb.getCurrentTimestamp(); + cMaxRows = 1024; + iStart = 0; + while True: + aoBuilds = oBuildLogic.fetchForListing(iStart, cMaxRows, tsNow); + if not self.oConfig.fQuiet and aoBuilds: + print('Processing builds #%s thru #%s' % (aoBuilds[0].idBuild, aoBuilds[-1].idBuild)); + + for oBuild in aoBuilds: + if oBuild.fBinariesDeleted is False: + rc = oBuild.areFilesStillThere(); + if rc is False: + if not self.oConfig.fQuiet: + print('missing files for build #%s / r%s / %s / %s / %s / %s / %s' + % (oBuild.idBuild, oBuild.iRevision, oBuild.sVersion, oBuild.oCat.sType, + oBuild.oCat.sBranch, oBuild.oCat.sProduct, oBuild.oCat.asOsArches,)); + print(' %s' % (oBuild.sBinaries,)); + if self.oConfig.fRealRun is True: + oBuild.fBinariesDeleted = True; + oBuildLogic.editEntry(oBuild, fCommit = True); + elif rc is True and not self.oConfig.fQuiet: + print('build #%s still have its files' % (oBuild.idBuild,)); + elif rc is None and not self.oConfig.fQuiet: + print('Unable to determine state of build #%s' % (oBuild.idBuild,)); + + # advance + if len(aoBuilds) < cMaxRows: + break; + iStart += len(aoBuilds); + + oDb.close(); + return 0; + +if __name__ == '__main__': + sys.exit(BuildChecker().checkBuilds()); + diff --git a/src/VBox/ValidationKit/testmanager/batch/close_orphaned_testsets.py b/src/VBox/ValidationKit/testmanager/batch/close_orphaned_testsets.py new file mode 100755 index 00000000..932122b5 --- /dev/null +++ b/src/VBox/ValidationKit/testmanager/batch/close_orphaned_testsets.py @@ -0,0 +1,105 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# $Id: close_orphaned_testsets.py $ +# pylint: disable=line-too-long + +""" +Maintenance tool for closing orphaned testsets. +""" + +from __future__ import print_function; + +__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 sys +import os +from optparse import OptionParser; # pylint: disable=deprecated-module + +# Add Test Manager's modules path +g_ksTestManagerDir = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) +sys.path.append(g_ksTestManagerDir) + +# Test Manager imports +from testmanager.core.db import TMDatabaseConnection +from testmanager.core.testset import TestSetLogic; + + +class CloseOrphanedTestSets(object): + """ + Finds and closes orphaned testsets. + """ + + def __init__(self): + """ + Parse command line + """ + oParser = OptionParser(); + oParser.add_option('-d', '--just-do-it', dest='fJustDoIt', action='store_true', + help='Do the database changes.'); + + + (self.oConfig, _) = oParser.parse_args(); + + + def main(self): + """ Main method. """ + oDb = TMDatabaseConnection(); + + # Get a list of orphans. + oLogic = TestSetLogic(oDb); + aoOrphans = oLogic.fetchOrphaned(); + if aoOrphans: + # Complete them. + if self.oConfig.fJustDoIt: + print('Completing %u test sets as abandoned:' % (len(aoOrphans),)); + for oTestSet in aoOrphans: + print('#%-7u: idTestBox=%-3u tsCreated=%s tsDone=%s' + % (oTestSet.idTestSet, oTestSet.idTestBox, oTestSet.tsCreated, oTestSet.tsDone)); + oLogic.completeAsAbandoned(oTestSet.idTestSet); + print('Committing...'); + oDb.commit(); + else: + for oTestSet in aoOrphans: + print('#%-7u: idTestBox=%-3u tsCreated=%s tsDone=%s' + % (oTestSet.idTestSet, oTestSet.idTestBox, oTestSet.tsCreated, oTestSet.tsDone)); + print('Not completing any testsets without seeing the --just-do-it option.'); + else: + print('No orphaned test sets.\n'); + return 0; + + +if __name__ == '__main__': + sys.exit(CloseOrphanedTestSets().main()) + diff --git a/src/VBox/ValidationKit/testmanager/batch/del_build.py b/src/VBox/ValidationKit/testmanager/batch/del_build.py new file mode 100755 index 00000000..76e43344 --- /dev/null +++ b/src/VBox/ValidationKit/testmanager/batch/del_build.py @@ -0,0 +1,95 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# $Id: del_build.py $ +# pylint: disable=line-too-long + +""" +Interface used by the tinderbox server side software to mark build binaries +deleted. +""" + +from __future__ import print_function; + +__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 sys +import os +from optparse import OptionParser; # pylint: disable=deprecated-module + +# Add Test Manager's modules path +g_ksTestManagerDir = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) +sys.path.append(g_ksTestManagerDir) + +# Test Manager imports +from testmanager.core.db import TMDatabaseConnection +from testmanager.core.build import BuildLogic + + +def markBuildsDeleted(): + """ + Marks the builds using the specified binaries as deleted. + """ + + oParser = OptionParser() + oParser.add_option('-q', '--quiet', dest='fQuiet', action='store_true', + help='Quiet execution'); + + (oConfig, asArgs) = oParser.parse_args() + if not asArgs: + if not oConfig.fQuiet: + sys.stderr.write('syntax error: No builds binaries specified\n'); + return 1; + + + oDb = TMDatabaseConnection() + oLogic = BuildLogic(oDb) + + for sBuildBin in asArgs: + try: + cBuilds = oLogic.markDeletedByBinaries(sBuildBin, fCommit = True) + except: + if oConfig.fQuiet: + sys.exit(1); + raise; + else: + if not oConfig.fQuiet: + print("del_build.py: Marked %u builds associated with '%s' as deleted." % (cBuilds, sBuildBin,)); + + oDb.close() + return 0; + +if __name__ == '__main__': + sys.exit(markBuildsDeleted()) + diff --git a/src/VBox/ValidationKit/testmanager/batch/filearchiver.py b/src/VBox/ValidationKit/testmanager/batch/filearchiver.py new file mode 100755 index 00000000..10a772ae --- /dev/null +++ b/src/VBox/ValidationKit/testmanager/batch/filearchiver.py @@ -0,0 +1,282 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# $Id: filearchiver.py $ +# pylint: disable=line-too-long + +""" +A cronjob that compresses logs and other files, moving them to the +g_ksZipFileAreaRootDir storage area. +""" + +from __future__ import print_function; + +__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 sys +import os +from optparse import OptionParser; # pylint: disable=deprecated-module +import time; +import zipfile; + +# Add Test Manager's modules path +g_ksTestManagerDir = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) +sys.path.append(g_ksTestManagerDir) + +# Test Manager imports +from common import utils; +from testmanager import config; +from testmanager.core.db import TMDatabaseConnection; +from testmanager.core.testset import TestSetData, TestSetLogic; + + + +class FileArchiverBatchJob(object): # pylint: disable=too-few-public-methods + """ + Log+files comp + """ + + def __init__(self, oOptions): + """ + Parse command line + """ + self.fVerbose = oOptions.fVerbose; + self.sSrcDir = config.g_ksFileAreaRootDir; + self.sDstDir = config.g_ksZipFileAreaRootDir; + #self.oTestSetLogic = TestSetLogic(TMDatabaseConnection(self.dprint if self.fVerbose else None)); + self.oTestSetLogic = TestSetLogic(TMDatabaseConnection(None)); + self.fDryRun = oOptions.fDryRun; + + def dprint(self, sText): + """ Verbose output. """ + if self.fVerbose: + print(sText); + return True; + + def warning(self, sText): + """Prints a warning.""" + print(sText); + return True; + + def _processTestSet(self, idTestSet, asFiles, sCurDir): + """ + Worker for processDir. + Same return codes as processDir. + """ + + sBaseFilename = os.path.join(sCurDir, 'TestSet-%d' % (idTestSet,)); + if sBaseFilename[0:2] == ('.' + os.path.sep): + sBaseFilename = sBaseFilename[2:]; + sSrcFileBase = os.path.join(self.sSrcDir, sBaseFilename + '-'); + + # + # Skip the file if the test set is still running. + # But delete them if the testset is not found. + # + oTestSet = self.oTestSetLogic.tryFetch(idTestSet); + if oTestSet is not None and sBaseFilename != oTestSet.sBaseFilename: + self.warning('TestSet %d: Deleting because sBaseFilename differs: "%s" (disk) vs "%s" (db)' \ + % (idTestSet, sBaseFilename, oTestSet.sBaseFilename,)); + oTestSet = None; + + if oTestSet is not None: + if oTestSet.enmStatus == TestSetData.ksTestStatus_Running: + self.dprint('Skipping test set #%d, still running' % (idTestSet,)); + return True; + + # + # If we have a zip file already, don't try recreate it as we might + # have had trouble removing the source files. + # + sDstDirPath = os.path.join(self.sDstDir, sCurDir); + sZipFileNm = os.path.join(sDstDirPath, 'TestSet-%d.zip' % (idTestSet,)); + if not os.path.exists(sZipFileNm): + # + # Create zip file with all testset files as members. + # + self.dprint('TestSet %d: Creating %s...' % (idTestSet, sZipFileNm,)); + if not self.fDryRun: + + if not os.path.exists(sDstDirPath): + os.makedirs(sDstDirPath, 0o755); + + utils.noxcptDeleteFile(sZipFileNm + '.tmp'); + with zipfile.ZipFile(sZipFileNm + '.tmp', 'w', zipfile.ZIP_DEFLATED, allowZip64 = True) as oZipFile: + for sFile in asFiles: + sSuff = os.path.splitext(sFile)[1]; + if sSuff in [ '.png', '.webm', '.gz', '.bz2', '.zip', '.mov', '.avi', '.mpg', '.gif', '.jpg' ]: + ## @todo Consider storing these files outside the zip if they are a little largish. + self.dprint('TestSet %d: Storing %s...' % (idTestSet, sFile)); + oZipFile.write(sSrcFileBase + sFile, sFile, zipfile.ZIP_STORED); + else: + self.dprint('TestSet %d: Deflating %s...' % (idTestSet, sFile)); + oZipFile.write(sSrcFileBase + sFile, sFile, zipfile.ZIP_DEFLATED); + + # + # .zip.tmp -> .zip. + # + utils.noxcptDeleteFile(sZipFileNm); + os.rename(sZipFileNm + '.tmp', sZipFileNm); + + #else: Dry run. + else: + self.dprint('TestSet %d: zip file exists already (%s)' % (idTestSet, sZipFileNm,)); + + # + # Delete the files. + # + fRc = True; + if self.fVerbose: + self.dprint('TestSet %d: deleting file: %s' % (idTestSet, asFiles)); + if not self.fDryRun: + for sFile in asFiles: + if utils.noxcptDeleteFile(sSrcFileBase + sFile) is False: + self.warning('TestSet %d: Failed to delete "%s" (%s)' % (idTestSet, sFile, sSrcFileBase + sFile,)); + fRc = False; + + return fRc; + + + def processDir(self, sCurDir): + """ + Process the given directory (relative to sSrcDir and sDstDir). + Returns success indicator. + """ + if self.fVerbose: + self.dprint('processDir: %s' % (sCurDir,)); + + # + # Sift thought the directory content, collecting subdirectories and + # sort relevant files by test set. + # Generally there will either be subdirs or there will be files. + # + asSubDirs = []; + dTestSets = {}; + sCurPath = os.path.abspath(os.path.join(self.sSrcDir, sCurDir)); + for sFile in os.listdir(sCurPath): + if os.path.isdir(os.path.join(sCurPath, sFile)): + if sFile not in [ '.', '..' ]: + asSubDirs.append(sFile); + elif sFile.startswith('TestSet-'): + # Parse the file name. ASSUMES 'TestSet-%d-filename' format. + iSlash1 = sFile.find('-'); + iSlash2 = sFile.find('-', iSlash1 + 1); + if iSlash2 <= iSlash1: + self.warning('Bad filename (1): "%s"' % (sFile,)); + continue; + + try: idTestSet = int(sFile[(iSlash1 + 1):iSlash2]); + except: + self.warning('Bad filename (2): "%s"' % (sFile,)); + if self.fVerbose: + self.dprint('\n'.join(utils.getXcptInfo(4))); + continue; + + if idTestSet <= 0: + self.warning('Bad filename (3): "%s"' % (sFile,)); + continue; + + if iSlash2 + 2 >= len(sFile): + self.warning('Bad filename (4): "%s"' % (sFile,)); + continue; + sName = sFile[(iSlash2 + 1):]; + + # Add it. + if idTestSet not in dTestSets: + dTestSets[idTestSet] = []; + asTestSet = dTestSets[idTestSet]; + asTestSet.append(sName); + + # + # Test sets. + # + fRc = True; + for idTestSet, oTestSet in dTestSets.items(): + try: + if self._processTestSet(idTestSet, oTestSet, sCurDir) is not True: + fRc = False; + except: + self.warning('TestSet %d: Exception in _processTestSet:\n%s' % (idTestSet, '\n'.join(utils.getXcptInfo()),)); + fRc = False; + + # + # Sub dirs. + # + for sSubDir in asSubDirs: + if self.processDir(os.path.join(sCurDir, sSubDir)) is not True: + fRc = False; + + # + # Try Remove the directory iff it's not '.' and it's been unmodified + # for the last 24h (race protection). + # + if sCurDir != '.': + try: + fpModTime = float(os.path.getmtime(sCurPath)); + if fpModTime + (24*3600) <= time.time(): + if utils.noxcptRmDir(sCurPath) is True: + self.dprint('Removed "%s".' % (sCurPath,)); + except: + pass; + + return fRc; + + @staticmethod + def main(): + """ C-style main(). """ + # + # Parse options. + # + oParser = OptionParser(); + oParser.add_option('-v', '--verbose', dest = 'fVerbose', action = 'store_true', default = False, + help = 'Verbose output.'); + oParser.add_option('-q', '--quiet', dest = 'fVerbose', action = 'store_false', default = False, + help = 'Quiet operation.'); + oParser.add_option('-d', '--dry-run', dest = 'fDryRun', action = 'store_true', default = False, + help = 'Dry run, do not make any changes.'); + (oOptions, asArgs) = oParser.parse_args() + if asArgs != []: + oParser.print_help(); + return 1; + + # + # Do the work. + # + oBatchJob = FileArchiverBatchJob(oOptions); + fRc = oBatchJob.processDir('.'); + return 0 if fRc is True else 1; + +if __name__ == '__main__': + sys.exit(FileArchiverBatchJob.main()); + diff --git a/src/VBox/ValidationKit/testmanager/batch/quota.py b/src/VBox/ValidationKit/testmanager/batch/quota.py new file mode 100755 index 00000000..e2854881 --- /dev/null +++ b/src/VBox/ValidationKit/testmanager/batch/quota.py @@ -0,0 +1,319 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# $Id: quota.py $ +# pylint: disable=line-too-long + +""" +A cronjob that applies quotas to large files in testsets. +""" + +from __future__ import print_function; + +__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 sys +import os +from optparse import OptionParser; # pylint: disable=deprecated-module +import shutil +import tempfile; +import zipfile; + +# Add Test Manager's modules path +g_ksTestManagerDir = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) +sys.path.append(g_ksTestManagerDir) + +# Test Manager imports +from testmanager import config; +from testmanager.core.db import TMDatabaseConnection; +from testmanager.core.testset import TestSetLogic; + + +class ArchiveDelFilesBatchJob(object): # pylint: disable=too-few-public-methods + """ + Log+files comp + """ + + def __init__(self, oOptions): + """ + Parse command line + """ + self.fDryRun = oOptions.fDryRun; + self.fVerbose = oOptions.fVerbose; + self.sTempDir = tempfile.gettempdir(); + + self.dprint('Connecting to DB ...'); + self.oTestSetLogic = TestSetLogic(TMDatabaseConnection(self.dprint if self.fVerbose else None)); + + ## Fetches (and handles) all testsets up to this age (in hours). + self.uHoursAgeToHandle = 24; + ## Always remove files with these extensions. + self.asRemoveFileExt = [ 'webm' ]; + ## Always remove files which are bigger than this limit. + # Set to 0 to disable. + self.cbRemoveBiggerThan = 128 * 1024 * 1024; + + def dprint(self, sText): + """ Verbose output. """ + if self.fVerbose: + print(sText); + return True; + + def warning(self, sText): + """Prints a warning.""" + print(sText); + return True; + + def _replaceFile(self, sDstFile, sSrcFile, fDryRun = False, fForce = False): + """ + Replaces / moves a file safely by backing up the existing destination file (if any). + + Returns success indicator. + """ + + fRc = True; + + # Rename the destination file first (if any). + sDstFileTmp = None; + if os.path.exists(sDstFile): + sDstFileTmp = sDstFile + ".bak"; + if os.path.exists(sDstFileTmp): + if not fForce: + print('Replace file: Warning: Temporary destination file "%s" already exists, skipping' % (sDstFileTmp,)); + fRc = False; + else: + try: + os.remove(sDstFileTmp); + except Exception as e: + print('Replace file: Error deleting old temporary destination file "%s": %s' % (sDstFileTmp, e)); + fRc = False; + try: + if not fDryRun: + shutil.move(sDstFile, sDstFileTmp); + except Exception as e: + print('Replace file: Error moving old destination file "%s" to temporary file "%s": %s' \ + % (sDstFile, sDstFileTmp, e)); + fRc = False; + + if not fRc: + return False; + + try: + if not fDryRun: + shutil.move(sSrcFile, sDstFile); + except Exception as e: + print('Replace file: Error moving source file "%s" to destination "%s": %s' % (sSrcFile, sDstFile, e,)); + fRc = False; + + if sDstFileTmp: + if fRc: # Move succeeded, remove backup. + try: + if not fDryRun: + os.remove(sDstFileTmp); + except Exception as e: + print('Replace file: Error deleting temporary destination file "%s": %s' % (sDstFileTmp, e)); + fRc = False; + else: # Final move failed, roll back. + try: + if not fDryRun: + shutil.move(sDstFileTmp, sDstFile); + except Exception as e: + print('Replace file: Error restoring old destination file "%s": %s' % (sDstFile, e)); + fRc = False; + return fRc; + + def _processTestSetZip(self, idTestSet, sSrcZipFileAbs): + """ + Worker for processOneTestSet, which processes the testset's ZIP file. + + Returns success indicator. + """ + _ = idTestSet + + with tempfile.NamedTemporaryFile(dir=self.sTempDir, delete=False) as tmpfile: + sDstZipFileAbs = tmpfile.name; + + fRc = True; + + try: + oSrcZipFile = zipfile.ZipFile(sSrcZipFileAbs, 'r'); # pylint: disable=consider-using-with + self.dprint('Processing ZIP archive "%s" ...' % (sSrcZipFileAbs)); + try: + if not self.fDryRun: + oDstZipFile = zipfile.ZipFile(sDstZipFileAbs, 'w'); # pylint: disable=consider-using-with + self.dprint('Using temporary ZIP archive "%s"' % (sDstZipFileAbs)); + try: + # + # First pass: Gather information if we need to do some re-packing. + # + fDoRepack = False; + aoFilesToRepack = []; + for oCurFile in oSrcZipFile.infolist(): + self.dprint('Handling File "%s" ...' % (oCurFile.filename)) + sFileExt = os.path.splitext(oCurFile.filename)[1]; + + if sFileExt \ + and sFileExt[1:] in self.asRemoveFileExt: + self.dprint('\tMatches excluded extensions') + fDoRepack = True; + elif self.cbRemoveBiggerThan \ + and oCurFile.file_size > self.cbRemoveBiggerThan: + self.dprint('\tIs bigger than %d bytes (%d bytes)' % (self.cbRemoveBiggerThan, oCurFile.file_size)) + fDoRepack = True; + else: + aoFilesToRepack.append(oCurFile); + + if not fDoRepack: + oSrcZipFile.close(); + self.dprint('No re-packing necessary, skipping ZIP archive'); + return True; + + # + # Second pass: Re-pack all needed files into our temporary ZIP archive. + # + for oCurFile in aoFilesToRepack: + self.dprint('Re-packing file "%s"' % (oCurFile.filename,)) + if not self.fDryRun: + oBuf = oSrcZipFile.read(oCurFile); + oDstZipFile.writestr(oCurFile, oBuf); + + if not self.fDryRun: + oDstZipFile.close(); + + except Exception as oXcpt4: + print('Error handling file "%s" of archive "%s": %s' % (oCurFile.filename, sSrcZipFileAbs, oXcpt4,)); + return False; + + oSrcZipFile.close(); + + if fRc: + self.dprint('Moving file "%s" to "%s"' % (sDstZipFileAbs, sSrcZipFileAbs)); + fRc = self._replaceFile(sSrcZipFileAbs, sDstZipFileAbs, self.fDryRun); + + except Exception as oXcpt3: + print('Error creating temporary ZIP archive "%s": %s' % (sDstZipFileAbs, oXcpt3,)); + return False; + + except Exception as oXcpt1: + # Construct a meaningful error message. + if os.path.exists(sSrcZipFileAbs): + print('Error: Opening file "%s" failed: %s' % (sSrcZipFileAbs, oXcpt1)); + else: + print('Error: File "%s" not found.' % (sSrcZipFileAbs,)); + return False; + + return fRc; + + + def processOneTestSet(self, idTestSet, sBasename): + """ + Processes one single testset. + + Returns success indicator. + """ + + fRc = True; + self.dprint('Processing testset %d' % (idTestSet,)); + + # Construct absolute ZIP file path. + # ZIP is hardcoded in config, so do here. + sSrcZipFileAbs = os.path.join(config.g_ksZipFileAreaRootDir, sBasename + '.zip'); + + if self._processTestSetZip(idTestSet, sSrcZipFileAbs) is not True: + fRc = False; + + return fRc; + + def processTestSets(self): + """ + Processes all testsets according to the set configuration. + + Returns success indicator. + """ + + aoTestSets = self.oTestSetLogic.fetchByAge(cHoursBack = self.uHoursAgeToHandle); + cTestSets = len(aoTestSets); + print('Found %d entries in DB' % cTestSets); + if not cTestSets: + return True; # Nothing to do (yet). + + fRc = True; + for oTestSet in aoTestSets: + fRc = self.processOneTestSet(oTestSet.idTestSet, oTestSet.sBaseFilename) and fRc; + # Keep going. + + return fRc; + + @staticmethod + def main(): + """ C-style main(). """ + # + # Parse options. + # + + oParser = OptionParser(); + + # Generic options. + oParser.add_option('-v', '--verbose', dest = 'fVerbose', action = 'store_true', default = False, + help = 'Verbose output.'); + oParser.add_option('-q', '--quiet', dest = 'fVerbose', action = 'store_false', default = False, + help = 'Quiet operation.'); + oParser.add_option('-d', '--dry-run', dest = 'fDryRun', action = 'store_true', default = False, + help = 'Dry run, do not make any changes.'); + + (oOptions, asArgs) = oParser.parse_args(sys.argv[1:]); + if asArgs != []: + oParser.print_help(); + return 1; + + if oOptions.fDryRun: + print('***********************************'); + print('*** DRY RUN - NO FILES MODIFIED ***'); + print('***********************************'); + + # + # Do the work. + # + fRc = False; + + oBatchJob = ArchiveDelFilesBatchJob(oOptions); + fRc = oBatchJob.processTestSets(); + + if oOptions.fVerbose: + print('SUCCESS' if fRc else 'FAILURE'); + + return 0 if fRc is True else 1; + +if __name__ == '__main__': + sys.exit(ArchiveDelFilesBatchJob.main()); diff --git a/src/VBox/ValidationKit/testmanager/batch/regen_sched_queues.py b/src/VBox/ValidationKit/testmanager/batch/regen_sched_queues.py new file mode 100755 index 00000000..a20d6cbe --- /dev/null +++ b/src/VBox/ValidationKit/testmanager/batch/regen_sched_queues.py @@ -0,0 +1,132 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# $Id: regen_sched_queues.py $ +# pylint: disable=line-too-long + +""" +Interface used by the admin to regenerate scheduling queues. +""" + +from __future__ import print_function; + +__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 sys; +import os; +from optparse import OptionParser; # pylint: disable=deprecated-module + +# Add Test Manager's modules path +g_ksTestManagerDir = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))); +sys.path.append(g_ksTestManagerDir); + +# Test Manager imports +from testmanager.core.db import TMDatabaseConnection; +from testmanager.core.schedulerbase import SchedulerBase; +from testmanager.core.schedgroup import SchedGroupLogic; + + + +class RegenSchedQueues(object): # pylint: disable=too-few-public-methods + """ + Regenerates all the scheduling queues. + """ + + def __init__(self): + """ + Parse command line. + """ + + oParser = OptionParser(); + oParser.add_option('-q', '--quiet', dest = 'fQuiet', action = 'store_true', default = False, + help = 'Quiet execution'); + oParser.add_option('-u', '--uid', dest = 'uid', action = 'store', type = 'int', default = 1, + help = 'User ID to accredit with this job'); + oParser.add_option('--profile', dest = 'fProfile', action = 'store_true', default = False, + help = 'User ID to accredit with this job'); + + (self.oConfig, _) = oParser.parse_args(); + + + def doIt(self): + """ + Does the job. + """ + oDb = TMDatabaseConnection(); + + aoGroups = SchedGroupLogic(oDb).getAll(); + iRc = 0; + for oGroup in aoGroups: + if not self.oConfig.fQuiet: + print('%s (ID %#d):' % (oGroup.sName, oGroup.idSchedGroup,)); + try: + (aoErrors, asMessages) = SchedulerBase.recreateQueue(oDb, self.oConfig.uid, oGroup.idSchedGroup, 2); + except Exception as oXcpt: + oDb.rollback(); + print(' !!Hit exception processing "%s": %s' % (oGroup.sName, oXcpt,)); + else: + if not aoErrors: + if not self.oConfig.fQuiet: + print(' Successfully regenerated.'); + else: + iRc = 1; + print(' %d errors:' % (len(aoErrors,))); + for oError in aoErrors: + if oError[1] is None: + print(' !!%s' % (oError[0],)); + else: + print(' !!%s (%s)' % (oError[0], oError[1])); + if asMessages and not self.oConfig.fQuiet: + print(' %d messages:' % (len(asMessages),)); + for sMsg in asMessages: + print(' ##%s' % (sMsg,)); + return iRc; + + @staticmethod + def main(): + """ Main function. """ + oMain = RegenSchedQueues(); + if oMain.oConfig.fProfile is not True: + iRc = oMain.doIt(); + else: + import cProfile; + oProfiler = cProfile.Profile(); + iRc = oProfiler.runcall(oMain.doIt); + oProfiler.print_stats(sort = 'time'); + oProfiler = None; + return iRc; + +if __name__ == '__main__': + sys.exit(RegenSchedQueues().main()); + diff --git a/src/VBox/ValidationKit/testmanager/batch/vcs_import.py b/src/VBox/ValidationKit/testmanager/batch/vcs_import.py new file mode 100755 index 00000000..bffa576b --- /dev/null +++ b/src/VBox/ValidationKit/testmanager/batch/vcs_import.py @@ -0,0 +1,205 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# $Id: vcs_import.py $ + +""" +Cron job for importing revision history for a repository. +""" + +from __future__ import print_function; + +__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 sys; +import os; +from optparse import OptionParser; # pylint: disable=deprecated-module +import xml.etree.ElementTree as ET; + +# Add Test Manager's modules path +g_ksTestManagerDir = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))); +sys.path.append(g_ksTestManagerDir); + +# Test Manager imports +from testmanager.config import g_kdBugTrackers; +from testmanager.core.db import TMDatabaseConnection; +from testmanager.core.vcsrevisions import VcsRevisionData, VcsRevisionLogic; +from testmanager.core.vcsbugreference import VcsBugReferenceData, VcsBugReferenceLogic; +from common import utils; + +# Python 3 hacks: +if sys.version_info[0] >= 3: + long = int; # pylint: disable=redefined-builtin,invalid-name + + +class VcsImport(object): # pylint: disable=too-few-public-methods + """ + Imports revision history from a VSC into the Test Manager database. + """ + + class BugTracker(object): + def __init__(self, sDbName, sTag): + self.sDbName = sDbName; + self.sTag = sTag; + + + def __init__(self): + """ + Parse command line. + """ + + oParser = OptionParser() + oParser.add_option('-b', '--only-bug-refs', dest = 'fBugRefsOnly', action = 'store_true', + help = 'Only do bug references, not revisions.'); + oParser.add_option('-e', '--extra-option', dest = 'asExtraOptions', metavar = 'vcsoption', action = 'append', + help = 'Adds a extra option to the command retrieving the log.'); + oParser.add_option('-f', '--full', dest = 'fFull', action = 'store_true', + help = 'Full revision history import.'); + oParser.add_option('-q', '--quiet', dest = 'fQuiet', action = 'store_true', + help = 'Quiet execution'); + oParser.add_option('-R', '--repository', dest = 'sRepository', metavar = '<repository>', + help = 'Version control repository name.'); + oParser.add_option('-s', '--start-revision', dest = 'iStartRevision', metavar = 'start-revision', + type = "int", default = 0, + help = 'The revision to start at when doing a full import.'); + oParser.add_option('-t', '--type', dest = 'sType', metavar = '<type>', + help = 'The VCS type (default: svn)', choices = [ 'svn', ], default = 'svn'); + oParser.add_option('-u', '--url', dest = 'sUrl', metavar = '<url>', + help = 'The VCS URL'); + + (self.oConfig, _) = oParser.parse_args(); + + # Check command line + asMissing = []; + if self.oConfig.sUrl is None: asMissing.append('--url'); + if self.oConfig.sRepository is None: asMissing.append('--repository'); + if asMissing: + sys.stderr.write('syntax error: Missing: %s\n' % (asMissing,)); + sys.exit(1); + + assert self.oConfig.sType == 'svn'; + + def main(self): + """ + Main function. + """ + oDb = TMDatabaseConnection(); + oLogic = VcsRevisionLogic(oDb); + oBugLogic = VcsBugReferenceLogic(oDb); + + # Where to start. + iStartRev = 0; + if not self.oConfig.fFull: + if not self.oConfig.fBugRefsOnly: + iStartRev = oLogic.getLastRevision(self.oConfig.sRepository); + else: + iStartRev = oBugLogic.getLastRevision(self.oConfig.sRepository); + if iStartRev == 0: + iStartRev = self.oConfig.iStartRevision; + + # Construct a command line. + os.environ['LC_ALL'] = 'en_US.utf-8'; + asArgs = [ + 'svn', + 'log', + '--xml', + '--revision', str(iStartRev) + ':HEAD', + ]; + if self.oConfig.asExtraOptions is not None: + asArgs.extend(self.oConfig.asExtraOptions); + asArgs.append(self.oConfig.sUrl); + if not self.oConfig.fQuiet: + print('Executing: %s' % (asArgs,)); + sLogXml = utils.processOutputChecked(asArgs); + + # Parse the XML and add the entries to the database. + oParser = ET.XMLParser(target = ET.TreeBuilder(), encoding = 'utf-8'); + oParser.feed(sLogXml.encode('utf-8')); # Does its own decoding; processOutputChecked always gives us decoded utf-8 now. + oRoot = oParser.close(); + + for oLogEntry in oRoot.findall('logentry'): + iRevision = int(oLogEntry.get('revision')); + sAuthor = oLogEntry.findtext('author', 'unspecified').strip(); # cvs2svn entries doesn't have an author. + sDate = oLogEntry.findtext('date').strip(); + sRawMsg = oLogEntry.findtext('msg', '').strip(); + sMessage = sRawMsg; + if sMessage == '': + sMessage = ' '; + elif len(sMessage) > VcsRevisionData.kcchMax_sMessage: + sMessage = sMessage[:VcsRevisionData.kcchMax_sMessage - 4] + ' ...'; + if not self.oConfig.fQuiet: + utils.printOut(u'sDate=%s iRev=%u sAuthor=%s sMsg[%s]=%s' + % (sDate, iRevision, sAuthor, type(sMessage).__name__, sMessage)); + + if not self.oConfig.fBugRefsOnly: + oData = VcsRevisionData().initFromValues(self.oConfig.sRepository, iRevision, sDate, sAuthor, sMessage); + oLogic.addVcsRevision(oData); + + # Analyze the raw message looking for bug tracker references. + for oBugTracker in g_kdBugTrackers.values(): + for sTag in oBugTracker.asCommitTags: + off = sRawMsg.find(sTag); + while off >= 0: + off += len(sTag); + while off < len(sRawMsg) and sRawMsg[off].isspace(): + off += 1; + + if off < len(sRawMsg) and sRawMsg[off].isdigit(): + offNum = off; + while off < len(sRawMsg) and sRawMsg[off].isdigit(): + off += 1; + try: + iBugNo = long(sRawMsg[offNum:off]); + except Exception as oXcpt: + utils.printErr(u'error! exception(r%s,"%s"): -> %s' % (iRevision, sRawMsg[offNum:off], oXcpt,)); + else: + if not self.oConfig.fQuiet: + utils.printOut(u' r%u -> sBugTracker=%s iBugNo=%s' + % (iRevision, oBugTracker.sDbId, iBugNo,)); + + oBugData = VcsBugReferenceData().initFromValues(self.oConfig.sRepository, iRevision, + oBugTracker.sDbId, iBugNo); + oBugLogic.addVcsBugReference(oBugData); + + # next + off = sRawMsg.find(sTag, off); + + oDb.commit(); + + oDb.close(); + return 0; + +if __name__ == '__main__': + sys.exit(VcsImport().main()); + diff --git a/src/VBox/ValidationKit/testmanager/batch/virtual_test_sheriff.py b/src/VBox/ValidationKit/testmanager/batch/virtual_test_sheriff.py new file mode 100755 index 00000000..51999e21 --- /dev/null +++ b/src/VBox/ValidationKit/testmanager/batch/virtual_test_sheriff.py @@ -0,0 +1,1832 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# $Id: virtual_test_sheriff.py $ +# pylint: disable=line-too-long + +""" +Virtual Test Sheriff. + +Duties: + - Try to a assign failure reasons to recently failed tests. + - Reboot or disable bad test boxes. + +""" + +from __future__ import print_function; + +__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 hashlib; +import os; +import re; +import smtplib; +#import subprocess; +import sys; +from email.mime.multipart import MIMEMultipart; +from email.mime.text import MIMEText; +from email.utils import COMMASPACE; + +if sys.version_info[0] >= 3: + from io import BytesIO as BytesIO; # pylint: disable=import-error,no-name-in-module,useless-import-alias +else: + from StringIO import StringIO as BytesIO; # pylint: disable=import-error,no-name-in-module,useless-import-alias +from optparse import OptionParser; # pylint: disable=deprecated-module +from PIL import Image; # pylint: disable=import-error + +# Add Test Manager's modules path +g_ksTestManagerDir = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))); +sys.path.append(g_ksTestManagerDir); + +# Test Manager imports +from common import utils; +from testmanager.core.db import TMDatabaseConnection; +from testmanager.core.build import BuildDataEx; +from testmanager.core.failurereason import FailureReasonLogic; +from testmanager.core.testbox import TestBoxLogic, TestBoxData; +from testmanager.core.testcase import TestCaseDataEx; +from testmanager.core.testgroup import TestGroupData; +from testmanager.core.testset import TestSetLogic, TestSetData; +from testmanager.core.testresults import TestResultLogic, TestResultFileData; +from testmanager.core.testresultfailures import TestResultFailureLogic, TestResultFailureData; +from testmanager.core.useraccount import UserAccountLogic; +from testmanager.config import g_ksSmtpHost, g_kcSmtpPort, g_ksAlertFrom, \ + g_ksAlertSubject, g_asAlertList #, g_ksLomPassword; + +# Python 3 hacks: +if sys.version_info[0] >= 3: + xrange = range; # pylint: disable=redefined-builtin,invalid-name + + +class VirtualTestSheriffCaseFile(object): + """ + A failure investigation case file. + + """ + + + ## Max log file we'll read into memory. (256 MB) + kcbMaxLogRead = 0x10000000; + + def __init__(self, oSheriff, oTestSet, oTree, oBuild, oTestBox, oTestGroup, oTestCase): + self.oSheriff = oSheriff; + self.oTestSet = oTestSet; # TestSetData + self.oTree = oTree; # TestResultDataEx + self.oBuild = oBuild; # BuildDataEx + self.oTestBox = oTestBox; # TestBoxData + self.oTestGroup = oTestGroup; # TestGroupData + self.oTestCase = oTestCase; # TestCaseDataEx + self.sMainLog = ''; # The main log file. Empty string if not accessible. + self.sSvcLog = ''; # The VBoxSVC log file. Empty string if not accessible. + + # Generate a case file name. + self.sName = '#%u: %s' % (self.oTestSet.idTestSet, self.oTestCase.sName,) + self.sLongName = '#%u: "%s" on "%s" running %s %s (%s), "%s" by %s, using %s %s %s r%u' \ + % ( self.oTestSet.idTestSet, + self.oTestCase.sName, + self.oTestBox.sName, + self.oTestBox.sOs, + self.oTestBox.sOsVersion, + self.oTestBox.sCpuArch, + self.oTestBox.sCpuName, + self.oTestBox.sCpuVendor, + self.oBuild.oCat.sProduct, + self.oBuild.oCat.sBranch, + self.oBuild.oCat.sType, + self.oBuild.iRevision, ); + + # Investigation notes. + self.tReason = None; # None or one of the ktReason_XXX constants. + self.dReasonForResultId = {}; # Reason assignments indexed by idTestResult. + self.dCommentForResultId = {}; # Comment assignments indexed by idTestResult. + + # + # Reason. + # + + def noteReason(self, tReason): + """ Notes down a possible reason. """ + self.oSheriff.dprint(u'noteReason: %s -> %s' % (self.tReason, tReason,)); + self.tReason = tReason; + return True; + + def noteReasonForId(self, tReason, idTestResult, sComment = None): + """ Notes down a possible reason for a specific test result. """ + self.oSheriff.dprint(u'noteReasonForId: %u: %s -> %s%s' + % (idTestResult, self.dReasonForResultId.get(idTestResult, None), tReason, + (u' (%s)' % (sComment,)) if sComment is not None else '')); + self.dReasonForResultId[idTestResult] = tReason; + if sComment is not None: + self.dCommentForResultId[idTestResult] = sComment; + return True; + + + # + # Test classification. + # + + def isVBoxTest(self): + """ Test classification: VirtualBox (using the build) """ + return self.oBuild.oCat.sProduct.lower() in [ 'virtualbox', 'vbox' ]; + + def isVBoxUnitTest(self): + """ Test case classification: The unit test doing all our testcase/*.cpp stuff. """ + return self.isVBoxTest() \ + and (self.oTestCase.sName.lower() == 'unit tests' or self.oTestCase.sName.lower().startswith('misc: unit tests')); + + def isVBoxInstallTest(self): + """ Test case classification: VirtualBox Guest installation test. """ + return self.isVBoxTest() \ + and self.oTestCase.sName.lower().startswith('install:'); + + def isVBoxUnattendedInstallTest(self): + """ Test case classification: VirtualBox Guest installation test. """ + return self.isVBoxTest() \ + and self.oTestCase.sName.lower().startswith('uinstall:'); + + def isVBoxUSBTest(self): + """ Test case classification: VirtualBox USB test. """ + return self.isVBoxTest() \ + and self.oTestCase.sName.lower().startswith('usb:'); + + def isVBoxStorageTest(self): + """ Test case classification: VirtualBox Storage test. """ + return self.isVBoxTest() \ + and self.oTestCase.sName.lower().startswith('storage:'); + + def isVBoxGAsTest(self): + """ Test case classification: VirtualBox Guest Additions test. """ + return self.isVBoxTest() \ + and ( self.oTestCase.sName.lower().startswith('guest additions') + or self.oTestCase.sName.lower().startswith('ga\'s tests')); + + def isVBoxAPITest(self): + """ Test case classification: VirtualBox API test. """ + return self.isVBoxTest() \ + and self.oTestCase.sName.lower().startswith('api:'); + + def isVBoxBenchmarkTest(self): + """ Test case classification: VirtualBox Benchmark test. """ + return self.isVBoxTest() \ + and self.oTestCase.sName.lower().startswith('benchmark:'); + + def isVBoxSmokeTest(self): + """ Test case classification: Smoke test. """ + return self.isVBoxTest() \ + and self.oTestCase.sName.lower().startswith('smoketest'); + + def isVBoxSerialTest(self): + """ Test case classification: Smoke test. """ + return self.isVBoxTest() \ + and self.oTestCase.sName.lower().startswith('serial:'); + + + # + # Utility methods. + # + + def getMainLog(self): + """ + Tries to read the main log file since this will be the first source of information. + """ + if self.sMainLog: + return self.sMainLog; + (oFile, oSizeOrError, _) = self.oTestSet.openFile('main.log', 'rb'); + if oFile is not None: + try: + self.sMainLog = oFile.read(min(self.kcbMaxLogRead, oSizeOrError)).decode('utf-8', 'replace'); + except Exception as oXcpt: + self.oSheriff.vprint(u'Error reading main log file: %s' % (oXcpt,)) + self.sMainLog = ''; + else: + self.oSheriff.vprint(u'Error opening main log file: %s' % (oSizeOrError,)); + return self.sMainLog; + + def getLogFile(self, oFile): + """ + Tries to read the given file as a utf-8 log file. + oFile is a TestFileDataEx instance. + Returns empty string if problems opening or reading the file. + """ + sContent = ''; + (oFile, oSizeOrError, _) = self.oTestSet.openFile(oFile.sFile, 'rb'); + if oFile is not None: + try: + sContent = oFile.read(min(self.kcbMaxLogRead, oSizeOrError)).decode('utf-8', 'replace'); + except Exception as oXcpt: + self.oSheriff.vprint(u'Error reading the "%s" log file: %s' % (oFile.sFile, oXcpt,)) + else: + self.oSheriff.vprint(u'Error opening the "%s" log file: %s' % (oFile.sFile, oSizeOrError,)); + return sContent; + + def getSvcLog(self): + """ + Tries to read the VBoxSVC log file as it typically not associated with a failing test result. + Note! Returns the first VBoxSVC log file we find. + """ + if not self.sSvcLog: + aoSvcLogFiles = self.oTree.getListOfLogFilesByKind(TestResultFileData.ksKind_LogReleaseSvc); + if aoSvcLogFiles: + self.sSvcLog = self.getLogFile(aoSvcLogFiles[0]); + return self.sSvcLog; + + def getScreenshotSha256(self, oFile): + """ + Tries to read the given screenshot file, uncompress it, and do SHA-2 + on the raw pixels. + Returns SHA-2 digest string on success, None on failure. + """ + (oImgFile, _, _) = self.oTestSet.openFile(oFile.sFile, 'rb'); + try: + abImageFile = oImgFile.read(); + except Exception as oXcpt: + self.oSheriff.vprint(u'Error reading the "%s" image file: %s' % (oFile.sFile, oXcpt,)) + else: + try: + oImage = Image.open(BytesIO(abImageFile)); + except Exception as oXcpt: + self.oSheriff.vprint(u'Error opening the "%s" image bytes using PIL.Image.open: %s' % (oFile.sFile, oXcpt,)) + else: + try: + oHash = hashlib.sha256(); + if hasattr(oImage, 'tobytes'): + oHash.update(oImage.tobytes()); + else: + oHash.update(oImage.tostring()); # pylint: disable=no-member + except Exception as oXcpt: + self.oSheriff.vprint(u'Error hashing the uncompressed image bytes for "%s": %s' % (oFile.sFile, oXcpt,)) + else: + return oHash.hexdigest(); + return None; + + + + def isSingleTestFailure(self): + """ + Figure out if this is a single test failing or if it's one of the + more complicated ones. + """ + if self.oTree.cErrors == 1: + return True; + if self.oTree.deepCountErrorContributers() <= 1: + return True; + return False; + + + +class VirtualTestSheriff(object): # pylint: disable=too-few-public-methods + """ + Add build info into Test Manager database. + """ + + ## The user account for the virtual sheriff. + ksLoginName = 'vsheriff'; + + def __init__(self): + """ + Parse command line. + """ + self.oDb = None; + self.tsNow = None; + self.oTestResultLogic = None; + self.oTestSetLogic = None; + self.oFailureReasonLogic = None; # FailureReasonLogic; + self.oTestResultFailureLogic = None; # TestResultFailureLogic + self.oLogin = None; + self.uidSelf = -1; + self.oLogFile = None; + self.asBsodReasons = []; + self.asUnitTestReasons = []; + + oParser = OptionParser(); + oParser.add_option('--start-hours-ago', dest = 'cStartHoursAgo', metavar = '<hours>', default = 0, type = 'int', + help = 'When to start specified as hours relative to current time. Defauls is right now.', ); + oParser.add_option('--hours-period', dest = 'cHoursBack', metavar = '<period-in-hours>', default = 2, type = 'int', + help = 'Work period specified in hours. Defauls is 2 hours.'); + oParser.add_option('--real-run-back', dest = 'fRealRun', action = 'store_true', default = False, + help = 'Whether to commit the findings to the database. Default is a dry run.'); + oParser.add_option('--testset', dest = 'aidTestSets', metavar = '<id>', default = [], type = 'int', action = 'append', + help = 'Only investigate this one. Accumulates IDs when repeated.'); + oParser.add_option('-q', '--quiet', dest = 'fQuiet', action = 'store_true', default = False, + help = 'Quiet execution'); + oParser.add_option('-l', '--log', dest = 'sLogFile', metavar = '<logfile>', default = None, + help = 'Where to log messages.'); + oParser.add_option('--debug', dest = 'fDebug', action = 'store_true', default = False, + help = 'Enables debug mode.'); + + (self.oConfig, _) = oParser.parse_args(); + + if self.oConfig.sLogFile: + self.oLogFile = open(self.oConfig.sLogFile, "a"); # pylint: disable=consider-using-with,unspecified-encoding + self.oLogFile.write('VirtualTestSheriff: $Revision: 155244 $ \n'); + + + def eprint(self, sText): + """ + Prints error messages. + Returns 1 (for exit code usage.) + """ + print('error: %s' % (sText,)); + if self.oLogFile is not None: + if sys.version_info[0] >= 3: + self.oLogFile.write(u'error: %s\n' % (sText,)); + else: + self.oLogFile.write((u'error: %s\n' % (sText,)).encode('utf-8')); + return 1; + + def dprint(self, sText): + """ + Prints debug info. + """ + if self.oConfig.fDebug: + if not self.oConfig.fQuiet: + print('debug: %s' % (sText, )); + if self.oLogFile is not None: + if sys.version_info[0] >= 3: + self.oLogFile.write(u'debug: %s\n' % (sText,)); + else: + self.oLogFile.write((u'debug: %s\n' % (sText,)).encode('utf-8')); + return 0; + + def vprint(self, sText): + """ + Prints verbose info. + """ + if not self.oConfig.fQuiet: + print('info: %s' % (sText,)); + if self.oLogFile is not None: + if sys.version_info[0] >= 3: + self.oLogFile.write(u'info: %s\n' % (sText,)); + else: + self.oLogFile.write((u'info: %s\n' % (sText,)).encode('utf-8')); + return 0; + + def getFailureReason(self, tReason): + """ Gets the failure reason object for tReason. """ + return self.oFailureReasonLogic.cachedLookupByNameAndCategory(tReason[1], tReason[0]); + + def selfCheck(self): + """ Does some self checks, looking up things we expect to be in the database and such. """ + rcExit = 0; + for sAttr in dir(self.__class__): + if sAttr.startswith('ktReason_'): + tReason = getattr(self.__class__, sAttr); + oFailureReason = self.getFailureReason(tReason); + if oFailureReason is None: + rcExit = self.eprint(u'Failed to find failure reason "%s" in category "%s" in the database!' + % (tReason[1], tReason[0],)); + + # Check the user account as well. + if self.oLogin is None: + oLogin = UserAccountLogic(self.oDb).tryFetchAccountByLoginName(VirtualTestSheriff.ksLoginName); + if oLogin is None: + rcExit = self.eprint(u'Cannot find my user account "%s"!' % (VirtualTestSheriff.ksLoginName,)); + return rcExit; + + def sendEmailAlert(self, uidAuthor, sBodyText): + """ + Sends email alert. + """ + + # Get author email + self.oDb.execute('SELECT sEmail FROM Users WHERE uid=%s', (uidAuthor,)); + sFrom = self.oDb.fetchOne(); + if sFrom is not None: + sFrom = sFrom[0]; + else: + sFrom = g_ksAlertFrom; + + # Gather recipient list. + asEmailList = []; + for sUser in g_asAlertList: + self.oDb.execute('SELECT sEmail FROM Users WHERE sUsername=%s', (sUser,)); + sEmail = self.oDb.fetchOne(); + if sEmail: + asEmailList.append(sEmail[0]); + if not asEmailList: + return self.eprint('No email addresses to send alter to!'); + + # Compose the message. + oMsg = MIMEMultipart(); + oMsg['From'] = sFrom; + oMsg['To'] = COMMASPACE.join(asEmailList); + oMsg['Subject'] = g_ksAlertSubject; + oMsg.attach(MIMEText(sBodyText, 'plain')) + + # Try send it. + try: + oSMTP = smtplib.SMTP(g_ksSmtpHost, g_kcSmtpPort); + oSMTP.sendmail(sFrom, asEmailList, oMsg.as_string()) + oSMTP.quit() + except smtplib.SMTPException as oXcpt: + return self.eprint('Failed to send mail: %s' % (oXcpt,)); + + return 0; + + def badTestBoxManagement(self): + """ + Looks for bad test boxes and first tries once to reboot them then disables them. + """ + rcExit = 0; + + # + # We skip this entirely if we're running in the past and not in harmless debug mode. + # + if self.oConfig.cStartHoursAgo != 0 \ + and (not self.oConfig.fDebug or self.oConfig.fRealRun): + return rcExit; + tsNow = self.tsNow if self.oConfig.fDebug else None; + cHoursBack = self.oConfig.cHoursBack if self.oConfig.fDebug else 2; + oTestBoxLogic = TestBoxLogic(self.oDb); + + # + # Generate a list of failures reasons we consider bad-testbox behavior. + # + aidFailureReasons = [ + self.getFailureReason(self.ktReason_Host_DriverNotLoaded).idFailureReason, + self.getFailureReason(self.ktReason_Host_DriverNotUnloading).idFailureReason, + self.getFailureReason(self.ktReason_Host_DriverNotCompilable).idFailureReason, + self.getFailureReason(self.ktReason_Host_InstallationFailed).idFailureReason, + ]; + + # + # Get list of bad test boxes for given period and check them out individually. + # + aidBadTestBoxes = self.oTestSetLogic.fetchBadTestBoxIds(cHoursBack = cHoursBack, tsNow = tsNow, + aidFailureReasons = aidFailureReasons); + for idTestBox in aidBadTestBoxes: + # Skip if the testbox is already disabled or has a pending reboot command. + try: + oTestBox = TestBoxData().initFromDbWithId(self.oDb, idTestBox); + except Exception as oXcpt: + rcExit = self.eprint('Failed to get data for test box #%u in badTestBoxManagement: %s' % (idTestBox, oXcpt,)); + continue; + if not oTestBox.fEnabled: + self.dprint(u'badTestBoxManagement: Skipping test box #%u (%s) as it has been disabled already.' + % ( idTestBox, oTestBox.sName, )); + continue; + if oTestBox.enmPendingCmd != TestBoxData.ksTestBoxCmd_None: + self.dprint(u'badTestBoxManagement: Skipping test box #%u (%s) as it has a command pending: %s' + % ( idTestBox, oTestBox.sName, oTestBox.enmPendingCmd)); + continue; + + # Get the most recent testsets for this box (descending on tsDone) and see how bad it is. + aoSets = self.oTestSetLogic.fetchSetsForTestBox(idTestBox, cHoursBack = cHoursBack, tsNow = tsNow); + cOkay = 0; + cBad = 0; + iFirstOkay = len(aoSets); + for iSet, oSet in enumerate(aoSets): + if oSet.enmStatus == TestSetData.ksTestStatus_BadTestBox: + cBad += 1; + else: + # Check for bad failure reasons. + oFailure = None; + if oSet.enmStatus in TestSetData.kasBadTestStatuses: + (oTree, _ ) = self.oTestResultLogic.fetchResultTree(oSet.idTestSet) + aoFailedResults = oTree.getListOfFailures(); + for oFailedResult in aoFailedResults: + oFailure = self.oTestResultFailureLogic.getById(oFailedResult.idTestResult); + if oFailure is not None and oFailure.idFailureReason in aidFailureReasons: + break; + oFailure = None; + if oFailure is not None: + cBad += 1; + else: + # This is an okay test result then. + ## @todo maybe check the elapsed time here, it could still be a bad run? + cOkay += 1; + iFirstOkay = min(iFirstOkay, iSet); + if iSet > 10: + break; + + # We react if there are two or more bad-testbox statuses at the head of the + # history and at least three in the last 10 results. + if iFirstOkay >= 2 and cBad > 2: + if oTestBoxLogic.hasTestBoxRecentlyBeenRebooted(idTestBox, cHoursBack = cHoursBack, tsNow = tsNow): + sComment = u'Disabling testbox #%u (%s) - iFirstOkay=%u cBad=%u cOkay=%u' \ + % (idTestBox, oTestBox.sName, iFirstOkay, cBad, cOkay); + self.vprint(sComment); + self.sendEmailAlert(self.uidSelf, sComment); + if self.oConfig.fRealRun is True: + try: + oTestBoxLogic.disableTestBox(idTestBox, self.uidSelf, fCommit = True, + sComment = 'Automatically disabled (iFirstOkay=%u cBad=%u cOkay=%u)' + % (iFirstOkay, cBad, cOkay),); + except Exception as oXcpt: + rcExit = self.eprint(u'Error disabling testbox #%u (%u): %s\n' % (idTestBox, oTestBox.sName, oXcpt,)); + else: + sComment = u'Rebooting testbox #%u (%s) - iFirstOkay=%u cBad=%u cOkay=%u' \ + % (idTestBox, oTestBox.sName, iFirstOkay, cBad, cOkay); + self.vprint(sComment); + self.sendEmailAlert(self.uidSelf, sComment); + if self.oConfig.fRealRun is True: + try: + oTestBoxLogic.rebootTestBox(idTestBox, self.uidSelf, fCommit = True, + sComment = 'Automatically rebooted (iFirstOkay=%u cBad=%u cOkay=%u)' + % (iFirstOkay, cBad, cOkay),); + except Exception as oXcpt: + rcExit = self.eprint(u'Error rebooting testbox #%u (%s): %s\n' % (idTestBox, oTestBox.sName, oXcpt,)); + else: + self.dprint(u'badTestBoxManagement: #%u (%s) looks ok: iFirstOkay=%u cBad=%u cOkay=%u' + % ( idTestBox, oTestBox.sName, iFirstOkay, cBad, cOkay)); + + ## @todo r=bird: review + rewrite; + ## - no selecting here, that belongs in the core/*.py files. + ## - preserve existing comments. + ## - doing way too much in the try/except block. + ## - No password quoting in the sshpass command that always fails (127). + ## - Timeout is way to low. testboxmem1 need more than 10 min to take a dump, ages to + ## get thru POST and another 5 just to time out in grub. Should be an hour or so. + ## Besides, it need to be constant elsewhere in the file, not a variable here. + ## + ## + ## Reset hanged testboxes + ## + #cStatusTimeoutMins = 10; + # + #self.oDb.execute('SELECT TestBoxStatuses.idTestBox\n' + # ' FROM TestBoxStatuses, TestBoxes\n' + # ' WHERE TestBoxStatuses.tsUpdated >= (CURRENT_TIMESTAMP - interval \'%s hours\')\n' + # ' AND TestBoxStatuses.tsUpdated < (CURRENT_TIMESTAMP - interval \'%s minutes\')\n' + # ' AND TestBoxStatuses.idTestBox = TestBoxes.idTestBox\n' + # ' AND Testboxes.tsExpire = \'infinity\'::timestamp', (cHoursBack,cStatusTimeoutMins)); + #for idTestBox in self.oDb.fetchAll(): + # idTestBox = idTestBox[0]; + # try: + # oTestBox = TestBoxData().initFromDbWithId(self.oDb, idTestBox); + # except Exception as oXcpt: + # rcExit = self.eprint('Failed to get data for test box #%u in badTestBoxManagement: %s' % (idTestBox, oXcpt,)); + # continue; + # # Skip if the testbox is already disabled, already reset or there's no iLOM + # if not oTestBox.fEnabled or oTestBox.ipLom is None or oTestBox.sComment is not None and oTestBox.sComment.find('Automatically reset') >= 0: + # self.dprint(u'badTestBoxManagement: Skipping test box #%u (%s) as it has been disabled already.' + # % ( idTestBox, oTestBox.sName, )); + # continue; + # ## @todo get iLOM credentials from a table? + # sCmd = 'sshpass -p%s ssh -oStrictHostKeyChecking=no root@%s show /SP && reset /SYS' % (g_ksLomPassword, oTestBox.ipLom,); + # try: + # oPs = subprocess.Popen(sCmd, stdout=subprocess.PIPE, shell=True); + # sStdout = oPs.communicate()[0]; + # iRC = oPs.wait(); + # + # oTestBox.sComment = 'Automatically reset (iRC=%u sStdout=%s)' % (iRC, sStdout,); + # oTestBoxLogic.editEntry(oTestBox, self.uidSelf, fCommit = True); + # + # sComment = u'Reset testbox #%u (%s) - iRC=%u sStduot=%s' % ( idTestBox, oTestBox.sName, iRC, sStdout); + # self.vprint(sComment); + # self.sendEmailAlert(self.uidSelf, sComment); + # + # except Exception as oXcpt: + # rcExit = self.eprint(u'Error resetting testbox #%u (%s): %s\n' % (idTestBox, oTestBox.sName, oXcpt,)); + # + return rcExit; + + + ## @name Failure reasons we know. + ## @{ + + ktReason_Add_Installer_Win_Failed = ( 'Additions', 'Win GA install' ); + ktReason_Add_ShFl_Automount = ( 'Additions', 'Automounting' ); + ktReason_Add_ShFl_FsPerf = ( 'Additions', 'FsPerf' ); + ktReason_Add_ShFl_FsPerf_Abend = ( 'Additions', 'FsPerf abend' ); + ktReason_Add_GstCtl_Preparations = ( 'Additions', 'GstCtl preparations' ); + ktReason_Add_GstCtl_SessionBasics = ( 'Additions', 'Session basics' ); + ktReason_Add_GstCtl_SessionProcRefs = ( 'Additions', 'Session process' ); + ktReason_Add_GstCtl_Session_Reboot = ( 'Additions', 'Session reboot' ); + ktReason_Add_GstCtl_CopyFromGuest_Timeout = ( 'Additions', 'CopyFromGuest timeout' ); + ktReason_Add_GstCtl_CopyToGuest_Timeout = ( 'Additions', 'CopyToGuest timeout' ); + ktReason_Add_GstCtl_CopyToGuest_DstEmpty = ( 'Additions', 'CopyToGuest dst empty' ); + ktReason_Add_GstCtl_CopyToGuest_DstExists = ( 'Additions', 'CopyToGuest dst exists' ); + ktReason_Add_FlushViewOfFile = ( 'Additions', 'FlushViewOfFile' ); + ktReason_Add_Mmap_Coherency = ( 'Additions', 'mmap coherency' ); + ktReason_BSOD_Recovery = ( 'BSOD', 'Recovery' ); + ktReason_BSOD_Automatic_Repair = ( 'BSOD', 'Automatic Repair' ); + ktReason_BSOD_0000007F = ( 'BSOD', '0x0000007F' ); + ktReason_BSOD_000000D1 = ( 'BSOD', '0x000000D1' ); + ktReason_BSOD_C0000225 = ( 'BSOD', '0xC0000225 (boot)' ); + ktReason_Guru_Generic = ( 'Guru Meditations', 'Generic Guru Meditation' ); + ktReason_Guru_VERR_IEM_INSTR_NOT_IMPLEMENTED = ( 'Guru Meditations', 'VERR_IEM_INSTR_NOT_IMPLEMENTED' ); + ktReason_Guru_VERR_IEM_ASPECT_NOT_IMPLEMENTED = ( 'Guru Meditations', 'VERR_IEM_ASPECT_NOT_IMPLEMENTED' ); + ktReason_Guru_VERR_TRPM_DONT_PANIC = ( 'Guru Meditations', 'VERR_TRPM_DONT_PANIC' ); + ktReason_Guru_VERR_PGM_PHYS_PAGE_RESERVED = ( 'Guru Meditations', 'VERR_PGM_PHYS_PAGE_RESERVED' ); + ktReason_Guru_VERR_VMX_INVALID_GUEST_STATE = ( 'Guru Meditations', 'VERR_VMX_INVALID_GUEST_STATE' ); + ktReason_Guru_VINF_EM_TRIPLE_FAULT = ( 'Guru Meditations', 'VINF_EM_TRIPLE_FAULT' ); + ktReason_Host_HostMemoryLow = ( 'Host', 'HostMemoryLow' ); + ktReason_Host_DriverNotLoaded = ( 'Host', 'Driver not loaded' ); + ktReason_Host_DriverNotUnloading = ( 'Host', 'Driver not unloading' ); + ktReason_Host_DriverNotCompilable = ( 'Host', 'Driver not compilable' ); + ktReason_Host_InstallationFailed = ( 'Host', 'Installation failed' ); + ktReason_Host_InstallationWantReboot = ( 'Host', 'Installation want reboot' ); + ktReason_Host_InvalidPackage = ( 'Host', 'ERROR_INSTALL_PACKAGE_INVALID' ); + ktReason_Host_InstallSourceAbsent = ( 'Host', 'ERROR_INSTALL_SOURCE_ABSENT' ); + ktReason_Host_NotSignedWithBuildCert = ( 'Host', 'Not signed with build cert' ); + ktReason_Host_DiskFull = ( 'Host', 'Host disk full' ); + ktReason_Host_DoubleFreeHeap = ( 'Host', 'Double free or corruption' ); + ktReason_Host_LeftoverService = ( 'Host', 'Leftover service' ); + ktReason_Host_win32com_gen_py = ( 'Host', 'win32com.gen_py' ); + ktReason_Host_Reboot_OSX_Watchdog_Timeout = ( 'Host Reboot', 'OSX Watchdog Timeout' ); + ktReason_Host_Modprobe_Failed = ( 'Host', 'Modprobe failed' ); + ktReason_Host_Install_Hang = ( 'Host', 'Install hang' ); + ktReason_Host_NetworkMisconfiguration = ( 'Host', 'Network misconfiguration' ); + ktReason_Host_TSTInfo_Accuracy_OOR = ( 'Host', 'TSTInfo accuracy out of range' ); + ktReason_Networking_Nonexistent_host_nic = ( 'Networking', 'Nonexistent host networking interface' ); + ktReason_Networking_VERR_INTNET_FLT_IF_NOT_FOUND = ( 'Networking', 'VERR_INTNET_FLT_IF_NOT_FOUND' ); + ktReason_OSInstall_GRUB_hang = ( 'O/S Install', 'GRUB hang' ); + ktReason_OSInstall_Udev_hang = ( 'O/S Install', 'udev hang' ); + ktReason_OSInstall_Sata_no_BM = ( 'O/S Install', 'SATA busmaster bit not set' ); + ktReason_Panic_BootManagerC000000F = ( 'Panic', 'Hardware Changed' ); + ktReason_Panic_MP_BIOS_IO_APIC = ( 'Panic', 'MP-BIOS/IO-APIC' ); + ktReason_Panic_HugeMemory = ( 'Panic', 'Huge memory assertion' ); + ktReason_Panic_IOAPICDoesntWork = ( 'Panic', 'IO-APIC and timer does not work' ); + ktReason_Panic_TxUnitHang = ( 'Panic', 'Tx Unit Hang' ); + ktReason_API_std_bad_alloc = ( 'API / (XP)COM', 'std::bad_alloc' ); + ktReason_API_Digest_Mismatch = ( 'API / (XP)COM', 'Digest mismatch' ); + ktReason_API_MoveVM_SharingViolation = ( 'API / (XP)COM', 'MoveVM sharing violation' ); + ktReason_API_MoveVM_InvalidParameter = ( 'API / (XP)COM', 'MoveVM invalid parameter' ); + ktReason_API_Open_Session_Failed = ( 'API / (XP)COM', 'Open session failed' ); + ktReason_XPCOM_Exit_Minus_11 = ( 'API / (XP)COM', 'exit -11' ); + ktReason_XPCOM_VBoxSVC_Hang = ( 'API / (XP)COM', 'VBoxSVC hang' ); + ktReason_XPCOM_VBoxSVC_Hang_Plus_Heap_Corruption = ( 'API / (XP)COM', 'VBoxSVC hang + heap corruption' ); + ktReason_XPCOM_NS_ERROR_CALL_FAILED = ( 'API / (XP)COM', 'NS_ERROR_CALL_FAILED' ); + ktReason_BootManager_Image_corrupt = ( 'Unknown', 'BOOTMGR Image corrupt' ); + ktReason_Unknown_Heap_Corruption = ( 'Unknown', 'Heap corruption' ); + ktReason_Unknown_Reboot_Loop = ( 'Unknown', 'Reboot loop' ); + ktReason_Unknown_File_Not_Found = ( 'Unknown', 'File not found' ); + ktReason_Unknown_HalReturnToFirmware = ( 'Unknown', 'HalReturnToFirmware' ); + ktReason_Unknown_VM_Crash = ( 'Unknown', 'VM crash' ); + ktReason_Unknown_VM_Terminated = ( 'Unknown', 'VM terminated' ); + ktReason_Unknown_VM_Start_Error = ( 'Unknown', 'VM Start Error' ); + ktReason_Unknown_VM_Runtime_Error = ( 'Unknown', 'VM Runtime Error' ); + ktReason_VMM_kvm_lock_spinning = ( 'VMM', 'kvm_lock_spinning' ); + ktReason_Ignore_Buggy_Test_Driver = ( 'Ignore', 'Buggy test driver' ); + ktReason_Ignore_Stale_Files = ( 'Ignore', 'Stale files' ); + ktReason_Buggy_Build_Broken_Build = ( 'Broken Build', 'Buggy build' ); + ktReason_GuestBug_CompizVBoxQt = ( 'Guest Bug', 'Compiz + VirtualBox Qt GUI crash' ); + ## @} + + ## BSOD category. + ksBsodCategory = 'BSOD'; + ## Special reason indicating that the flesh and blood sheriff has work to do. + ksBsodAddNew = 'Add new BSOD'; + + ## Unit test category. + ksUnitTestCategory = 'Unit'; + ## Special reason indicating that the flesh and blood sheriff has work to do. + ksUnitTestAddNew = 'Add new'; + + ## Used for indica that we shouldn't report anything for this test result ID and + ## consider promoting the previous error to test set level if it's the only one. + ktHarmless = ( 'Probably', 'Caused by previous error' ); + + + def caseClosed(self, oCaseFile): + """ + Reports the findings in the case and closes it. + """ + # + # Log it and create a dReasonForReasultId we can use below. + # + dCommentForResultId = oCaseFile.dCommentForResultId; + if oCaseFile.dReasonForResultId: + # Must weed out ktHarmless. + dReasonForResultId = {}; + for idKey, tReason in oCaseFile.dReasonForResultId.items(): + if tReason is not self.ktHarmless: + dReasonForResultId[idKey] = tReason; + if not dReasonForResultId: + self.vprint(u'TODO: Closing %s without a real reason, only %s.' + % (oCaseFile.sName, oCaseFile.dReasonForResultId)); + return False; + + # Try promote to single reason. + atValues = dReasonForResultId.values(); + fSingleReason = True; + if len(dReasonForResultId) == 1 and next(iter(dReasonForResultId.keys())) != oCaseFile.oTestSet.idTestResult: + self.dprint(u'Promoting single reason to whole set: %s' % (next(iter(atValues)),)); + elif len(dReasonForResultId) > 1 and len(atValues) == list(atValues).count(next(iter(atValues))): + self.dprint(u'Merged %d reasons to a single one: %s' % (len(atValues), next(iter(atValues)))); + else: + fSingleReason = False; + if fSingleReason: + dReasonForResultId = { oCaseFile.oTestSet.idTestResult: next(iter(atValues)), }; + if dCommentForResultId: + dCommentForResultId = { oCaseFile.oTestSet.idTestResult: next(iter(dCommentForResultId.values())), }; + elif oCaseFile.tReason is not None: + dReasonForResultId = { oCaseFile.oTestSet.idTestResult: oCaseFile.tReason, }; + else: + self.vprint(u'Closing %s without a reason - this should not happen!' % (oCaseFile.sName,)); + return False; + + self.vprint(u'Closing %s with following reason%s: %s' + % ( oCaseFile.sName, 's' if len(dReasonForResultId) > 1 else '', dReasonForResultId, )); + + # + # Add the test failure reason record(s). + # + for idTestResult, tReason in dReasonForResultId.items(): + oFailureReason = self.getFailureReason(tReason); + if oFailureReason is not None: + sComment = 'Set by $Revision: 155244 $' # Handy for reverting later. + if idTestResult in dCommentForResultId: + sComment += ': ' + dCommentForResultId[idTestResult]; + + oAdd = TestResultFailureData(); + oAdd.initFromValues(idTestResult = idTestResult, + idFailureReason = oFailureReason.idFailureReason, + uidAuthor = self.uidSelf, + idTestSet = oCaseFile.oTestSet.idTestSet, + sComment = sComment,); + if self.oConfig.fRealRun: + try: + self.oTestResultFailureLogic.addEntry(oAdd, self.uidSelf, fCommit = True); + except Exception as oXcpt: + self.eprint(u'caseClosed: Exception "%s" while adding reason %s for %s' + % (oXcpt, oAdd, oCaseFile.sLongName,)); + else: + self.eprint(u'caseClosed: Cannot locate failure reason: %s / %s' % ( tReason[0], tReason[1],)); + return True; + + # + # Tools for assiting log parsing. + # + + @staticmethod + def matchFollowedByLines(sStr, off, asFollowingLines): + """ Worker for isThisFollowedByTheseLines. """ + + # Advance off to the end of the line. + off = sStr.find('\n', off); + if off < 0: + return False; + off += 1; + + # Match each string with the subsequent lines. + for iLine, sLine in enumerate(asFollowingLines): + offEnd = sStr.find('\n', off); + if offEnd < 0: + return iLine + 1 == len(asFollowingLines) and sStr.find(sLine, off) < 0; + if sLine and sStr.find(sLine, off, offEnd) < 0: + return False; + + # next line. + off = offEnd + 1; + + return True; + + @staticmethod + def isThisFollowedByTheseLines(sStr, sFirst, asFollowingLines): + """ + Looks for a line contining sFirst which is then followed by lines + with the strings in asFollowingLines. (No newline chars anywhere!) + Returns True / False. + """ + off = sStr.find(sFirst, 0); + while off >= 0: + if VirtualTestSheriff.matchFollowedByLines(sStr, off, asFollowingLines): + return True; + off = sStr.find(sFirst, off + 1); + return False; + + @staticmethod + def findAndReturnRestOfLine(sHaystack, sNeedle): + """ + Looks for sNeedle in sHaystack. + Returns The text following the needle up to the end of the line. + Returns None if not found. + """ + if sHaystack is None: + return None; + off = sHaystack.find(sNeedle); + if off < 0: + return None; + off += len(sNeedle) + offEol = sHaystack.find('\n', off); + if offEol < 0: + offEol = len(sHaystack); + return sHaystack[off:offEol] + + @staticmethod + def findInAnyAndReturnRestOfLine(asHaystacks, sNeedle): + """ + Looks for sNeedle in zeroe or more haystacks (asHaystack). + Returns The text following the first needed found up to the end of the line. + Returns None if not found. + """ + for sHaystack in asHaystacks: + sRet = VirtualTestSheriff.findAndReturnRestOfLine(sHaystack, sNeedle); + if sRet is not None: + return sRet; + return None; + + + # + # The investigative units. + # + + katSimpleInstallUninstallMainLogReasons = [ + # ( Whether to stop on hit, reason tuple, needle text. ) + ( False, ktReason_Host_LeftoverService, + 'SERVICE_NAME: vbox' ), + ( False, ktReason_Host_LeftoverService, + 'Seems installation was skipped. Old version lurking behind? Not the fault of this build/test run!'), + ]; + + kdatSimpleInstallUninstallMainLogReasonsPerOs = { + 'darwin': [ + # ( Whether to stop on hit, reason tuple, needle text. ) + ( True, ktReason_Host_DriverNotUnloading, + 'Can\'t remove kext org.virtualbox.kext.VBoxDrv; services failed to terminate - 0xe00002c7' ), + ], + 'linux': [ + # ( Whether to stop on hit, reason tuple, needle text. ) + ( True, ktReason_Host_DriverNotCompilable, + 'This system is not currently set up to build kernel modules' ), + ( True, ktReason_Host_DriverNotCompilable, + 'This system is currently not set up to build kernel modules' ), + ( True, ktReason_Host_InstallationFailed, + 'vboxdrv.sh: failed: Look at /var/log/vbox-install.log to find out what went wrong.' ), + ( True, ktReason_Host_DriverNotUnloading, + 'Cannot unload module vboxdrv'), + ], + 'solaris': [ + # ( Whether to stop on hit, reason tuple, needle text. ) + ( True, ktReason_Host_DriverNotUnloading, 'can\'t unload the module: Device busy' ), + ( True, ktReason_Host_DriverNotUnloading, 'Unloading: Host module ...FAILED!' ), + ( True, ktReason_Host_DriverNotUnloading, 'Unloading: NetFilter (Crossbow) module ...FAILED!' ), + ( True, ktReason_Host_InstallationFailed, 'svcadm: Couldn\'t bind to svc.configd.' ), + ( True, ktReason_Host_InstallationFailed, 'pkgadd: ERROR: postinstall script did not complete successfully' ), + ], + 'win': [ + # ( Whether to stop on hit, reason tuple, needle text. ) + ( True, ktReason_Host_InstallationWantReboot, 'ERROR_SUCCESS_REBOOT_REQUIRED' ), + ( False, ktReason_Host_InstallationFailed, 'Installation error.' ), + ( True, ktReason_Host_InvalidPackage, 'Uninstaller failed, exit code: 1620' ), + ( True, ktReason_Host_InstallSourceAbsent, 'Uninstaller failed, exit code: 1612' ), + ], + }; + + + def investigateInstallUninstallFailure(self, oCaseFile, oFailedResult, sResultLog, fInstall): + """ + Investigates an install or uninstall failure. + + We lump the two together since the installation typically also performs + an uninstall first and will be seeing similar issues to the uninstall. + """ + self.dprint(u'%s + %s <<\n%s\n<<' % (oFailedResult.tsCreated, oFailedResult.tsElapsed, sResultLog,)); + + if fInstall and oFailedResult.enmStatus == TestSetData.ksTestStatus_TimedOut: + oCaseFile.noteReasonForId(self.ktReason_Host_Install_Hang, oFailedResult.idTestResult) + return True; + + atSimple = self.katSimpleInstallUninstallMainLogReasons; + if oCaseFile.oTestBox.sOs in self.kdatSimpleInstallUninstallMainLogReasonsPerOs: + atSimple = self.kdatSimpleInstallUninstallMainLogReasonsPerOs[oCaseFile.oTestBox.sOs] + atSimple; + + fFoundSomething = False; + for fStopOnHit, tReason, sNeedle in atSimple: + if sResultLog.find(sNeedle) > 0: + oCaseFile.noteReasonForId(tReason, oFailedResult.idTestResult); + if fStopOnHit: + return True; + fFoundSomething = True; + + return fFoundSomething if fFoundSomething else None; + + + def investigateBadTestBox(self, oCaseFile): + """ + Checks out bad-testbox statuses. + """ + _ = oCaseFile; + return False; + + + def investigateVBoxUnitTest(self, oCaseFile): + """ + Checks out a VBox unittest problem. + """ + + # + # Process simple test case failures first, using their name as reason. + # We do the reason management just like for BSODs. + # + cRelevantOnes = 0; + sMainLog = oCaseFile.getMainLog(); + aoFailedResults = oCaseFile.oTree.getListOfFailures(); + for oFailedResult in aoFailedResults: + if oFailedResult is oCaseFile.oTree: + self.vprint('TODO: toplevel failure'); + cRelevantOnes += 1 + + elif oFailedResult.sName == 'Installing VirtualBox': + sResultLog = TestSetData.extractLogSectionElapsed(sMainLog, oFailedResult.tsCreated, oFailedResult.tsElapsed); + self.investigateInstallUninstallFailure(oCaseFile, oFailedResult, sResultLog, fInstall = True) + cRelevantOnes += 1 + + elif oFailedResult.sName == 'Uninstalling VirtualBox': + sResultLog = TestSetData.extractLogSectionElapsed(sMainLog, oFailedResult.tsCreated, oFailedResult.tsElapsed); + self.investigateInstallUninstallFailure(oCaseFile, oFailedResult, sResultLog, fInstall = False) + cRelevantOnes += 1 + + elif oFailedResult.oParent is not None: + # Get the 2nd level node because that's where we'll find the unit test name. + while oFailedResult.oParent.oParent is not None: + oFailedResult = oFailedResult.oParent; + + # Only report a failure once. + if oFailedResult.idTestResult not in oCaseFile.dReasonForResultId: + sKey = oFailedResult.sName; + if sKey.startswith('testcase/'): + sKey = sKey[9:]; + if sKey in self.asUnitTestReasons: + tReason = ( self.ksUnitTestCategory, sKey ); + oCaseFile.noteReasonForId(tReason, oFailedResult.idTestResult); + else: + self.dprint(u'Unit test failure "%s" not found in %s;' % (sKey, self.asUnitTestReasons)); + tReason = ( self.ksUnitTestCategory, self.ksUnitTestAddNew ); + oCaseFile.noteReasonForId(tReason, oFailedResult.idTestResult, sComment = sKey); + cRelevantOnes += 1 + else: + self.vprint(u'Internal error: expected oParent to NOT be None for %s' % (oFailedResult,)); + + # + # If we've caught all the relevant ones by now, report the result. + # + if len(oCaseFile.dReasonForResultId) >= cRelevantOnes: + return self.caseClosed(oCaseFile); + return False; + + def extractGuestCpuStack(self, sInfoText): + """ + Extracts the guest CPU stacks from the input file. + + Returns a dictionary keyed by the CPU number, value being a list of + raw stack lines (no header). + Returns empty dictionary if no stacks where found. + """ + dRet = {}; + off = 0; + while True: + # Find the stack. + offStart = sInfoText.find('=== start guest stack VCPU ', off); + if offStart < 0: + break; + offEnd = sInfoText.find('=== end guest stack', offStart + 20); + if offEnd >= 0: + offEnd += 3; + else: + offEnd = sInfoText.find('=== start guest stack VCPU', offStart + 20); + if offEnd < 0: + offEnd = len(sInfoText); + + sStack = sInfoText[offStart : offEnd]; + sStack = sStack.replace('\r',''); # paranoia + asLines = sStack.split('\n'); + + # Figure the CPU. + asWords = asLines[0].split(); + if len(asWords) < 6 or not asWords[5].isdigit(): + break; + iCpu = int(asWords[5]); + + # Add it and advance. + dRet[iCpu] = [sLine.rstrip() for sLine in asLines[2:-1]] + off = offEnd; + return dRet; + + def investigateInfoKvmLockSpinning(self, oCaseFile, sInfoText, dLogs): + """ Investigates kvm_lock_spinning deadlocks """ + # + # Extract the stacks. We need more than one CPU to create a deadlock. + # + dStacks = self.extractGuestCpuStack(sInfoText); + self.dprint('kvm_lock_spinning: found %s stacks' % (len(dStacks),)); + if len(dStacks) >= 2: + # + # Examin each of the stacks. Each must have kvm_lock_spinning in + # one of the first three entries. + # + cHits = 0; + for asBacktrace in dStacks.values(): + for iFrame in xrange(min(3, len(asBacktrace))): + if asBacktrace[iFrame].find('kvm_lock_spinning') >= 0: + cHits += 1; + break; + self.dprint('kvm_lock_spinning: %s/%s hits' % (cHits, len(dStacks),)); + if cHits == len(dStacks): + return (True, self.ktReason_VMM_kvm_lock_spinning); + + _ = dLogs; _ = oCaseFile; + return (False, None); + + def investigateInfoHalReturnToFirmware(self, oCaseFile, sInfoText, dLogs): + """ Investigates HalReturnToFirmware hangs """ + del oCaseFile + del sInfoText + del dLogs + # hope that's sufficient + return (True, self.ktReason_Unknown_HalReturnToFirmware); + + ## Things we search a main or VM log for to figure out why something went bust. + ## @note DO NOT ADD MORE STUFF HERE! + ## Please use katSimpleMainLogReasons and katSimpleVmLogReasons instead! + katSimpleMainAndVmLogReasonsDeprecated = [ + # ( Whether to stop on hit, reason tuple, needle text. ) + ( False, ktReason_Guru_Generic, 'GuruMeditation' ), + ( False, ktReason_Guru_Generic, 'Guru Meditation' ), + ( True, ktReason_Guru_VERR_IEM_INSTR_NOT_IMPLEMENTED, 'VERR_IEM_INSTR_NOT_IMPLEMENTED' ), + ( True, ktReason_Guru_VERR_IEM_ASPECT_NOT_IMPLEMENTED, 'VERR_IEM_ASPECT_NOT_IMPLEMENTED' ), + ( True, ktReason_Guru_VERR_TRPM_DONT_PANIC, 'VERR_TRPM_DONT_PANIC' ), + ( True, ktReason_Guru_VERR_PGM_PHYS_PAGE_RESERVED, 'VERR_PGM_PHYS_PAGE_RESERVED' ), + ( True, ktReason_Guru_VERR_VMX_INVALID_GUEST_STATE, 'VERR_VMX_INVALID_GUEST_STATE' ), + ( True, ktReason_Guru_VINF_EM_TRIPLE_FAULT, 'VINF_EM_TRIPLE_FAULT' ), + ( True, ktReason_Networking_Nonexistent_host_nic, + 'rc=E_FAIL text="Nonexistent host networking interface, name \'eth0\' (VERR_INTERNAL_ERROR)"' ), + ( True, ktReason_Networking_VERR_INTNET_FLT_IF_NOT_FOUND, + 'Failed to attach the network LUN (VERR_INTNET_FLT_IF_NOT_FOUND)' ), + ( True, ktReason_Host_Reboot_OSX_Watchdog_Timeout, ': "OSX Watchdog Timeout: ' ), + ( False, ktReason_XPCOM_NS_ERROR_CALL_FAILED, + 'Exception: 0x800706be (Call to remote object failed (NS_ERROR_CALL_FAILED))' ), + ( True, ktReason_API_std_bad_alloc, 'Unexpected exception: std::bad_alloc' ), + ( True, ktReason_Host_HostMemoryLow, 'HostMemoryLow' ), + ( True, ktReason_Host_HostMemoryLow, 'Failed to procure handy pages; rc=VERR_NO_MEMORY' ), + ( True, ktReason_Unknown_File_Not_Found, + 'Error: failed to start machine. Error message: File not found. (VERR_FILE_NOT_FOUND)' ), + ( True, ktReason_Unknown_File_Not_Found, # lump it in with file-not-found for now. + 'Error: failed to start machine. Error message: Not supported. (VERR_NOT_SUPPORTED)' ), + ( False, ktReason_Unknown_VM_Crash, 'txsDoConnectViaTcp: Machine state: Aborted' ), + ( True, ktReason_Host_Modprobe_Failed, 'Kernel driver not installed' ), + ( True, ktReason_OSInstall_Sata_no_BM, 'PCHS=14128/14134/8224' ), + ( True, ktReason_Host_DoubleFreeHeap, 'double free or corruption' ), + #( False, ktReason_Unknown_VM_Start_Error, 'VMSetError: ' ), - false positives for stuff like: + # "VMSetError: VD: Backend 'VBoxIsoMaker' does not support async I/O" + ( False, ktReason_Unknown_VM_Start_Error, 'error: failed to open session for' ), + ( False, ktReason_Unknown_VM_Runtime_Error, 'Console: VM runtime error: fatal=true' ), + ]; + + ## This we search a main log for to figure out why something went bust. + katSimpleMainLogReasons = [ + # ( Whether to stop on hit, reason tuple, needle text. ) + ( False, ktReason_Host_win32com_gen_py, 'ModuleNotFoundError: No module named \'win32com.gen_py' ), + + ]; + + ## This we search a VM log for to figure out why something went bust. + katSimpleVmLogReasons = [ + # ( Whether to stop on hit, reason tuple, needle text. ) + # Note: Works for ATA and VD drivers. + ( False, ktReason_Host_DiskFull, '_DISKFULL' ), + ]; + + ## Things we search a VBoxHardening.log file for to figure out why something went bust. + katSimpleVBoxHardeningLogReasons = [ + # ( Whether to stop on hit, reason tuple, needle text. ) + ( True, ktReason_Host_DriverNotLoaded, 'Error opening VBoxDrvStub: STATUS_OBJECT_NAME_NOT_FOUND' ), + ( True, ktReason_Host_NotSignedWithBuildCert, 'Not signed with the build certificate' ), + ( True, ktReason_Host_TSTInfo_Accuracy_OOR, 'RTCRTSPTSTINFO::Accuracy::Millis: Out of range' ), + ( False, ktReason_Unknown_VM_Crash, 'Quitting: ExitCode=0xc0000005 (rcNtWait=' ), + ]; + + ## Things we search a kernel.log file for to figure out why something went bust. + katSimpleKernelLogReasons = [ + # ( Whether to stop on hit, reason tuple, needle text. ) + ( True, ktReason_Panic_HugeMemory, 'mm/huge_memory.c:1988' ), + ( True, ktReason_Panic_IOAPICDoesntWork, 'IO-APIC + timer doesn\'t work' ), + ( True, ktReason_Panic_TxUnitHang, 'Detected Tx Unit Hang' ), + ( True, ktReason_GuestBug_CompizVBoxQt, 'error 4 in libQt5CoreVBox' ), + ( True, ktReason_GuestBug_CompizVBoxQt, 'error 4 in libgtk-3' ), + ]; + + ## Things we search the _RIGHT_ _STRIPPED_ vgatext for. + katSimpleVgaTextReasons = [ + # ( Whether to stop on hit, reason tuple, needle text. ) + ( True, ktReason_Panic_MP_BIOS_IO_APIC, + "..MP-BIOS bug: 8254 timer not connected to IO-APIC\n\n" ), + ( True, ktReason_Panic_MP_BIOS_IO_APIC, + "..MP-BIOS bug: 8254 timer not connected to IO-APIC\n" + "...trying to set up timer (IRQ0) through the 8259A ... failed.\n" + "...trying to set up timer as Virtual Wire IRQ... failed.\n" + "...trying to set up timer as ExtINT IRQ... failed :(.\n" + "Kernel panic - not syncing: IO-APIC + timer doesn't work! Boot with apic=debug\n" + "and send a report. Then try booting with the 'noapic' option\n" + "\n" ), + ( True, ktReason_OSInstall_GRUB_hang, + "-----\nGRUB Loading stage2..\n\n\n\n" ), + ( True, ktReason_OSInstall_GRUB_hang, + "-----\nGRUB Loading stage2...\n\n\n\n" ), # the 3 dot hang appears to be less frequent + ( True, ktReason_OSInstall_GRUB_hang, + "-----\nGRUB Loading stage2....\n\n\n\n" ), # the 4 dot hang appears to be very infrequent + ( True, ktReason_OSInstall_GRUB_hang, + "-----\nGRUB Loading stage2.....\n\n\n\n" ), # the 5 dot hang appears to be more frequent again + ( True, ktReason_OSInstall_Udev_hang, + "\nStarting udev:\n\n\n\n" ), + ( True, ktReason_OSInstall_Udev_hang, + "\nStarting udev:\n------" ), + ( True, ktReason_Panic_BootManagerC000000F, + "Windows failed to start. A recent hardware or software change might be the" ), + ( True, ktReason_BootManager_Image_corrupt, + "BOOTMGR image is corrupt. The system cannot boot." ), + ]; + + ## Things we search for in the info.txt file. Require handlers for now. + katInfoTextHandlers = [ + # ( Trigger text, handler method ) + ( "kvm_lock_spinning", investigateInfoKvmLockSpinning ), + ( "HalReturnToFirmware", investigateInfoHalReturnToFirmware ), + ]; + + ## Mapping screenshot/failure SHA-256 hashes to failure reasons. + katSimpleScreenshotHashReasons = [ + # ( Whether to stop on hit, reason tuple, lowercased sha-256 of PIL.Image.tostring output ) + ( True, ktReason_BSOD_Recovery, '576f8e38d62b311cac7e3dc3436a0d0b9bd8cfd7fa9c43aafa95631520a45eac' ), + ( True, ktReason_BSOD_Automatic_Repair, 'c6a72076cc619937a7a39cfe9915b36d94cee0d4e3ce5ce061485792dcee2749' ), + ( True, ktReason_BSOD_Automatic_Repair, '26c4d8a724ff2c5e1051f3d5b650dbda7b5fdee0aa3e3c6059797f7484a515df' ), + ( True, ktReason_BSOD_0000007F, '57e1880619e13042a87100e7a38c8974b85ce3866501be621bea0cc696bb2c63' ), + ( True, ktReason_BSOD_000000D1, '134621281f00a3f8aeeb7660064bffbf6187ed56d5852142328d0bcb18ef0ede' ), + ( True, ktReason_BSOD_000000D1, '279f11258150c9d2fef041eca65501f3141da8df39256d8f6377e897e3b45a93' ), + ( True, ktReason_BSOD_C0000225, 'bd13a144be9dcdfb16bc863ff4c8f02a86e263c174f2cd5ffd27ca5f3aa31789' ), + ( True, ktReason_BSOD_C0000225, '8348b465e7ee9e59dd4e785880c57fd8677de05d11ac21e786bfde935307b42f' ), + ( True, ktReason_BSOD_C0000225, '1316e1fc818a73348412788e6910b8c016f237d8b4e15b20caf4a866f7a7840e' ), + ( True, ktReason_BSOD_C0000225, '54e0acbff365ce20a85abbe42bcd53647b8b9e80c68e45b2cd30e86bf177a0b5' ), + ( True, ktReason_BSOD_C0000225, '50fec50b5199923fa48b3f3e782687cc381e1c8a788ebda14e6a355fbe3bb1b3' ), + ]; + + + def scanLog(self, asLogs, atNeedles, oCaseFile, idTestResult): + """ + Scans for atNeedles in sLog. + + Returns True if a stop-on-hit neelde was found. + Returns None if a no-stop reason was found. + Returns False if no hit. + """ + fRet = False; + for fStopOnHit, tReason, oNeedle in atNeedles: + fMatch = False; + if utils.isString(oNeedle): + for sLog in asLogs: + if sLog: + fMatch |= sLog.find(oNeedle) > 0; + else: + for sLog in asLogs: + if sLog: + fMatch |= oNeedle.search(sLog) is not None; + if fMatch: + oCaseFile.noteReasonForId(tReason, idTestResult); + if fStopOnHit: + return True; + fRet = None; + return fRet; + + + def investigateGATest(self, oCaseFile, oFailedResult, sResultLog): + """ + Investigates a failed VM run. + """ + enmReason = None; + sParentName = oFailedResult.oParent.sName if oFailedResult.oParent else ''; + if oFailedResult.sName == 'VBoxWindowsAdditions.exe' or sResultLog.find('VBoxWindowsAdditions.exe" failed with') > 0: + enmReason = self.ktReason_Add_Installer_Win_Failed; + # guest control: + elif sParentName == 'Guest Control' and oFailedResult.sName == 'Preparations': + enmReason = self.ktReason_Add_GstCtl_Preparations; + elif oFailedResult.sName == 'Session Basics': + enmReason = self.ktReason_Add_GstCtl_SessionBasics; + elif oFailedResult.sName == 'Session Process References': + enmReason = self.ktReason_Add_GstCtl_SessionProcRefs; + elif oFailedResult.sName == 'Copy from guest': + if sResultLog.find('*** abort action ***') >= 0: + enmReason = self.ktReason_Add_GstCtl_CopyFromGuest_Timeout; + elif oFailedResult.sName == 'Copy to guest': + off = sResultLog.find('"Guest directory "'); + if off > 0 and sResultLog.find('" already exists"', off, off + 80): + enmReason = self.ktReason_Add_GstCtl_CopyToGuest_DstExists; + elif sResultLog.find('Guest destination must not be empty') >= 0: + enmReason = self.ktReason_Add_GstCtl_CopyToGuest_DstEmpty; + elif sResultLog.find('*** abort action ***') >= 0: + enmReason = self.ktReason_Add_GstCtl_CopyToGuest_Timeout; + elif oFailedResult.sName.find('Session w/ Guest Reboot') >= 0: + enmReason = self.ktReason_Add_GstCtl_Session_Reboot; + # shared folders: + elif sParentName == 'Shared Folders' and oFailedResult.sName == 'Automounting': + enmReason = self.ktReason_Add_ShFl_Automount; + elif oFailedResult.sName == 'mmap': + if sResultLog.find('FsPerf: Flush issue at offset ') >= 0: + enmReason = self.ktReason_Add_Mmap_Coherency; + elif sResultLog.find('FlushViewOfFile') >= 0: + enmReason = self.ktReason_Add_FlushViewOfFile; + elif sParentName == 'Shared Folders' and oFailedResult.sName == 'Running FsPerf': + enmReason = self.ktReason_Add_ShFl_FsPerf; ## Maybe it would be better to be more specific... + + if enmReason is not None: + return oCaseFile.noteReasonForId(enmReason, oFailedResult.idTestResult); + + self.vprint(u'TODO: Cannot place GA failure idTestResult=%u - %s' % (oFailedResult.idTestResult, oFailedResult.sName,)); + self.dprint(u'%s + %s <<\n%s\n<<' % (oFailedResult.tsCreated, oFailedResult.tsElapsed, sResultLog,)); + return False; + + def isResultFromGATest(self, oCaseFile, oFailedResult): + """ + Checks if this result and corresponding log snippet looks like a GA test run. + """ + while oFailedResult is not None: + if oFailedResult.sName in [ 'Guest Control', 'Shared Folders', 'FsPerf', 'VBoxWindowsAdditions.exe' ]: + return True; + if oCaseFile.oTestCase.sName == 'Guest Additions' and oFailedResult.sName in [ 'Install', ]: + return True; + oFailedResult = oFailedResult.oParent; + return False; + + + def investigateVMResult(self, oCaseFile, oFailedResult, sResultLog): + """ + Investigates a failed VM run. + """ + + def investigateLogSet(): + """ + Investigates the current set of VM related logs. + """ + self.dprint('investigateLogSet: log lengths: result %u, VM %u, kernel %u, vga text %u, info text %u, hard %u' + % ( len(sResultLog if sResultLog else ''), + len(sVMLog if sVMLog else ''), + len(sKrnlLog if sKrnlLog else ''), + len(sVgaText if sVgaText else ''), + len(sInfoText if sInfoText else ''), + len(sNtHardLog if sNtHardLog else ''),)); + + #self.dprint(u'main.log<<<\n%s\n<<<\n' % (sResultLog,)); + #self.dprint(u'vbox.log<<<\n%s\n<<<\n' % (sVMLog,)); + #self.dprint(u'krnl.log<<<\n%s\n<<<\n' % (sKrnlLog,)); + #self.dprint(u'vgatext.txt<<<\n%s\n<<<\n' % (sVgaText,)); + #self.dprint(u'info.txt<<<\n%s\n<<<\n' % (sInfoText,)); + #self.dprint(u'hard.txt<<<\n%s\n<<<\n' % (sNtHardLog,)); + + # TODO: more + + # + # Look for BSODs. Some stupid stupid inconsistencies in reason and log messages here, so don't try prettify this. + # + sDetails = self.findInAnyAndReturnRestOfLine([ sVMLog, sResultLog ], + 'GIM: HyperV: Guest indicates a fatal condition! P0='); + if sDetails is not None: + # P0=%#RX64 P1=%#RX64 P2=%#RX64 P3=%#RX64 P4=%#RX64 " + sKey = sDetails.split(' ', 1)[0]; + try: sKey = '0x%08X' % (int(sKey, 16),); + except: pass; + if sKey in self.asBsodReasons: + tReason = ( self.ksBsodCategory, sKey ); + elif sKey.lower() in self.asBsodReasons: # just in case. + tReason = ( self.ksBsodCategory, sKey.lower() ); + else: + self.dprint(u'BSOD "%s" not found in %s;' % (sKey, self.asBsodReasons)); + tReason = ( self.ksBsodCategory, self.ksBsodAddNew ); + return oCaseFile.noteReasonForId(tReason, oFailedResult.idTestResult, sComment = sDetails.strip()); + + fFoundSomething = False; + + # + # Look for linux panic. + # + if sKrnlLog is not None: + fRet = self.scanLog([sKrnlLog,], self.katSimpleKernelLogReasons, oCaseFile, oFailedResult.idTestResult); + if fRet is True: + return fRet; + fFoundSomething |= fRet is None; + + # + # Loop thru the simple stuff. + # + + # Main log. + fRet = self.scanLog([sResultLog,], self.katSimpleMainLogReasons, oCaseFile, oFailedResult.idTestResult); + if fRet is True: + return fRet; + fFoundSomething |= fRet is None; + + # VM log. + fRet = self.scanLog([sVMLog,], self.katSimpleVmLogReasons, oCaseFile, oFailedResult.idTestResult); + if fRet is True: + return fRet; + fFoundSomething |= fRet is None; + + # Old main + vm log. + fRet = self.scanLog([sResultLog, sVMLog], self.katSimpleMainAndVmLogReasonsDeprecated, + oCaseFile, oFailedResult.idTestResult); + if fRet is True: + return fRet; + fFoundSomething |= fRet is None; + + # Continue with vga text. + if sVgaText: + fRet = self.scanLog([sVgaText,], self.katSimpleVgaTextReasons, oCaseFile, oFailedResult.idTestResult); + if fRet is True: + return fRet; + fFoundSomething |= fRet is None; + + # Continue with screen hashes. + if sScreenHash is not None: + for fStopOnHit, tReason, sHash in self.katSimpleScreenshotHashReasons: + if sScreenHash == sHash: + oCaseFile.noteReasonForId(tReason, oFailedResult.idTestResult); + if fStopOnHit: + return True; + fFoundSomething = True; + + # Check VBoxHardening.log. + if sNtHardLog is not None: + fRet = self.scanLog([sNtHardLog,], self.katSimpleVBoxHardeningLogReasons, oCaseFile, oFailedResult.idTestResult); + if fRet is True: + return fRet; + fFoundSomething |= fRet is None; + + # + # Complicated stuff. + # + dLogs = { + 'sVMLog': sVMLog, + 'sNtHardLog': sNtHardLog, + 'sScreenHash': sScreenHash, + 'sKrnlLog': sKrnlLog, + 'sVgaText': sVgaText, + 'sInfoText': sInfoText, + }; + + # info.txt. + if sInfoText: + for sNeedle, fnHandler in self.katInfoTextHandlers: + if sInfoText.find(sNeedle) > 0: + (fStop, tReason) = fnHandler(self, oCaseFile, sInfoText, dLogs); + if tReason is not None: + oCaseFile.noteReasonForId(tReason, oFailedResult.idTestResult); + if fStop: + return True; + fFoundSomething = True; + + # + # Check for repeated reboots... + # + if sVMLog is not None: + cResets = sVMLog.count('Changing the VM state from \'RUNNING\' to \'RESETTING\''); + if cResets > 10: + return oCaseFile.noteReasonForId(self.ktReason_Unknown_Reboot_Loop, oFailedResult.idTestResult, + sComment = 'Counted %s reboots' % (cResets,)); + + return fFoundSomething; + + # + # Check if we got any VM or/and kernel logs. Treat them as sets in + # case we run multiple VMs here (this is of course ASSUMING they + # appear in the order that terminateVmBySession uploads them). + # + cTimes = 0; + sVMLog = None; + sNtHardLog = None; + sScreenHash = None; + sKrnlLog = None; + sVgaText = None; + sInfoText = None; + for oFile in oFailedResult.aoFiles: + if oFile.sKind == TestResultFileData.ksKind_LogReleaseVm: + if 'VBoxHardening.log' not in oFile.sFile: + if sVMLog is not None: + if investigateLogSet() is True: + return True; + cTimes += 1; + sInfoText = None; + sVgaText = None; + sKrnlLog = None; + sScreenHash = None; + sNtHardLog = None; + sVMLog = oCaseFile.getLogFile(oFile); + else: + sNtHardLog = oCaseFile.getLogFile(oFile); + elif oFile.sKind == TestResultFileData.ksKind_LogGuestKernel: + sKrnlLog = oCaseFile.getLogFile(oFile); + elif oFile.sKind == TestResultFileData.ksKind_InfoVgaText: + sVgaText = '\n'.join([sLine.rstrip() for sLine in oCaseFile.getLogFile(oFile).split('\n')]); + elif oFile.sKind == TestResultFileData.ksKind_InfoCollection: + sInfoText = oCaseFile.getLogFile(oFile); + elif oFile.sKind == TestResultFileData.ksKind_ScreenshotFailure: + sScreenHash = oCaseFile.getScreenshotSha256(oFile); + if sScreenHash is not None: + sScreenHash = sScreenHash.lower(); + self.vprint(u'%s %s' % ( sScreenHash, oFile.sFile,)); + + if ( sVMLog is not None \ + or sNtHardLog is not None \ + or cTimes == 0) \ + and investigateLogSet() is True: + return True; + + return None; + + def isResultFromVMRun(self, oFailedResult, sResultLog): + """ + Checks if this result and corresponding log snippet looks like a VM run. + """ + + # Look for startVmEx/ startVmAndConnectToTxsViaTcp and similar output in the log. + if sResultLog.find(' startVm') > 0: + return True; + + # Any other indicators? No? + _ = oFailedResult; + return False; + + + ## Things we search a VBoxSVC log for to figure out why something went bust. + katSimpleSvcLogReasons = [ + # ( Whether to stop on hit, reason tuple, needle text. ) + ( False, ktReason_Unknown_VM_Crash, re.compile(r'Reaper.* exited normally: -1073741819 \(0xc0000005\)') ), + ( False, ktReason_Unknown_VM_Crash, re.compile(r'Reaper.* was signalled: 11 \(0xb\)') ), # For VBox < 6.1. + ( False, ktReason_Unknown_VM_Crash, re.compile(r'Reaper.* was signalled: SIGABRT.*') ), # Since VBox 7.0. + ( False, ktReason_Unknown_VM_Crash, re.compile(r'Reaper.* was signalled: SIGSEGV.*') ), + ( False, ktReason_Unknown_VM_Terminated, re.compile(r'Reaper.* was signalled: SIGTERM.*') ), + ( False, ktReason_Unknown_VM_Terminated, re.compile(r'Reaper.* was signalled: SIGKILL.*') ), + ]; + + def investigateSvcLogForVMRun(self, oCaseFile, sSvcLog): + """ + Check the VBoxSVC log for a single VM run. + """ + if sSvcLog: + fRet = self.scanLog([sSvcLog,], self.katSimpleSvcLogReasons, oCaseFile, oCaseFile.oTree.idTestResult); + if fRet is True or fRet is None: + return True; + return False; + + def investigateNtHardLogForVMRun(self, oCaseFile): + """ + Check if the hardening log for a single VM run contains VM crash indications. + """ + aoLogFiles = oCaseFile.oTree.getListOfLogFilesByKind(TestResultFileData.ksKind_LogReleaseVm); + for oLogFile in aoLogFiles: + if oLogFile.sFile.find('VBoxHardening.log') >= 0: + sLog = oCaseFile.getLogFile(oLogFile); + if sLog.find('Quitting: ExitCode=0xc0000005') >= 0: + return oCaseFile.noteReasonForId(self.ktReason_Unknown_VM_Crash, oCaseFile.oTree.idTestResult); + return False; + + + def investigateVBoxVMTest(self, oCaseFile, fSingleVM): + """ + Checks out a VBox VM test. + + This is generic investigation of a test running one or more VMs, like + for example a smoke test or a guest installation test. + + The fSingleVM parameter is a hint, which probably won't come in useful. + """ + _ = fSingleVM; + + # + # Get a list of test result failures we should be looking into and the main log. + # + aoFailedResults = oCaseFile.oTree.getListOfFailures(); + sMainLog = oCaseFile.getMainLog(); + + # + # There are a set of errors ending up on the top level result record. + # Should deal with these first. + # + if len(aoFailedResults) == 1 and aoFailedResults[0] == oCaseFile.oTree: + # Check if we've just got that XPCOM client smoke test shutdown issue. This will currently always + # be reported on the top result because vboxinstall.py doesn't add an error for it. It is easy to + # ignore other failures in the test if we're not a little bit careful here. + if sMainLog.find('vboxinstaller: Exit code: -11 (') > 0: + oCaseFile.noteReason(self.ktReason_XPCOM_Exit_Minus_11); + return self.caseClosed(oCaseFile); + + # Hang after starting VBoxSVC (e.g. idTestSet=136307258) + if self.isThisFollowedByTheseLines(sMainLog, 'oVBoxMgr=<vboxapi.VirtualBoxManager object at', + (' Timeout: ', ' Attempting to abort child...',) ): + if sMainLog.find('*** glibc detected *** /') > 0: + oCaseFile.noteReason(self.ktReason_XPCOM_VBoxSVC_Hang_Plus_Heap_Corruption); + else: + oCaseFile.noteReason(self.ktReason_XPCOM_VBoxSVC_Hang); + return self.caseClosed(oCaseFile); + + # Look for heap corruption without visible hang. + if sMainLog.find('*** glibc detected *** /') > 0 \ + or sMainLog.find("-1073740940") > 0: # STATUS_HEAP_CORRUPTION / 0xc0000374 + oCaseFile.noteReason(self.ktReason_Unknown_Heap_Corruption); + return self.caseClosed(oCaseFile); + + # Out of memory w/ timeout. + if sMainLog.find('sErrId=HostMemoryLow') > 0: + oCaseFile.noteReason(self.ktReason_Host_HostMemoryLow); + return self.caseClosed(oCaseFile); + + # Stale files like vts_rm.exe (windows). + offEnd = sMainLog.rfind('*** The test driver exits successfully. ***'); + if offEnd > 0 and sMainLog.find('[Error 145] The directory is not empty: ', offEnd) > 0: + oCaseFile.noteReason(self.ktReason_Ignore_Stale_Files); + return self.caseClosed(oCaseFile); + + # + # XPCOM screwup + # + if sMainLog.find('AttributeError: \'NoneType\' object has no attribute \'addObserver\'') > 0: + oCaseFile.noteReason(self.ktReason_Buggy_Build_Broken_Build); + return self.caseClosed(oCaseFile); + + # + # Go thru each failed result. + # + for oFailedResult in aoFailedResults: + self.dprint(u'Looking at test result #%u - %s' % (oFailedResult.idTestResult, oFailedResult.getFullName(),)); + sResultLog = TestSetData.extractLogSectionElapsed(sMainLog, oFailedResult.tsCreated, oFailedResult.tsElapsed); + if oFailedResult.sName == 'Installing VirtualBox': + self.investigateInstallUninstallFailure(oCaseFile, oFailedResult, sResultLog, fInstall = True) + + elif oFailedResult.sName == 'Uninstalling VirtualBox': + self.investigateInstallUninstallFailure(oCaseFile, oFailedResult, sResultLog, fInstall = False) + + elif self.isResultFromVMRun(oFailedResult, sResultLog): + self.investigateVMResult(oCaseFile, oFailedResult, sResultLog); + + elif self.isResultFromGATest(oCaseFile, oFailedResult): + self.investigateGATest(oCaseFile, oFailedResult, sResultLog); + + elif sResultLog.find('most likely not unique') > 0: + oCaseFile.noteReasonForId(self.ktReason_Host_NetworkMisconfiguration, oFailedResult.idTestResult) + elif sResultLog.find('Exception: 0x800706be (Call to remote object failed (NS_ERROR_CALL_FAILED))') > 0: + oCaseFile.noteReasonForId(self.ktReason_XPCOM_NS_ERROR_CALL_FAILED, oFailedResult.idTestResult); + + elif sResultLog.find('The machine is not mutable (state is ') > 0: + self.vprint('Ignoring "machine not mutable" error as it is probably due to an earlier problem'); + oCaseFile.noteReasonForId(self.ktHarmless, oFailedResult.idTestResult); + + elif sResultLog.find('** error: no action was specified') > 0 \ + or sResultLog.find('(len(self._asXml, asText))') > 0: + oCaseFile.noteReasonForId(self.ktReason_Ignore_Buggy_Test_Driver, oFailedResult.idTestResult); + + else: + self.vprint(u'TODO: Cannot place idTestResult=%u - %s' % (oFailedResult.idTestResult, oFailedResult.sName,)); + self.dprint(u'%s + %s <<\n%s\n<<' % (oFailedResult.tsCreated, oFailedResult.tsElapsed, sResultLog,)); + + # + # Windows python/com screwup. + # + if sMainLog.find('ModuleNotFoundError: No module named \'win32com.gen_py') > 0: + oCaseFile.noteReason(self.ktReason_Host_win32com_gen_py); + return self.caseClosed(oCaseFile); + + # + # Check VBoxSVC.log and VBoxHardening.log for VM crashes if inconclusive on single VM runs. + # + if fSingleVM and len(oCaseFile.dReasonForResultId) < len(aoFailedResults): + self.dprint(u'Got %u out of %u - checking VBoxSVC.log...' + % (len(oCaseFile.dReasonForResultId), len(aoFailedResults))); + if self.investigateSvcLogForVMRun(oCaseFile, oCaseFile.getSvcLog()): + return self.caseClosed(oCaseFile); + if self.investigateNtHardLogForVMRun(oCaseFile): + return self.caseClosed(oCaseFile); + + # + # Report home and close the case if we got them all, otherwise log it. + # + if len(oCaseFile.dReasonForResultId) >= len(aoFailedResults): + return self.caseClosed(oCaseFile); + + if oCaseFile.dReasonForResultId: + self.vprint(u'TODO: Got %u out of %u - close, but no cigar. :-/' + % (len(oCaseFile.dReasonForResultId), len(aoFailedResults))); + else: + self.vprint(u'XXX: Could not figure out anything at all! :-('); + return False; + + + ## Things we search a main log for to figure out why something in the API test went bust. + katSimpleApiMainLogReasons = [ + # ( Whether to stop on hit, reason tuple, needle text. ) + ( True, ktReason_Networking_Nonexistent_host_nic, + 'rc=E_FAIL text="Nonexistent host networking interface, name \'eth0\' (VERR_INTERNAL_ERROR)"' ), + ( False, ktReason_XPCOM_NS_ERROR_CALL_FAILED, + 'Exception: 0x800706be (Call to remote object failed (NS_ERROR_CALL_FAILED))' ), + ( True, ktReason_API_std_bad_alloc, 'Unexpected exception: std::bad_alloc' ), + ( True, ktReason_API_Digest_Mismatch, 'Digest mismatch (VERR_NOT_EQUAL)' ), + ( True, ktReason_API_MoveVM_SharingViolation, 'rc=VBOX_E_IPRT_ERROR text="Could not copy the log file ' ), + ( True, ktReason_API_MoveVM_InvalidParameter, + 'rc=VBOX_E_IPRT_ERROR text="Could not copy the setting file ' ), + ( True, ktReason_API_Open_Session_Failed, 'error: failed to open session for' ), + ]; + + def investigateVBoxApiTest(self, oCaseFile): + """ + Checks out a VBox API test. + """ + + # + # Get a list of test result failures we should be looking into and the main log. + # + aoFailedResults = oCaseFile.oTree.getListOfFailures(); + sMainLog = oCaseFile.getMainLog(); + + # + # Go thru each failed result. + # + for oFailedResult in aoFailedResults: + self.dprint(u'Looking at test result #%u - %s' % (oFailedResult.idTestResult, oFailedResult.getFullName(),)); + sResultLog = TestSetData.extractLogSectionElapsed(sMainLog, oFailedResult.tsCreated, oFailedResult.tsElapsed); + if oFailedResult.sName == 'Installing VirtualBox': + self.investigateInstallUninstallFailure(oCaseFile, oFailedResult, sResultLog, fInstall = True) + + elif oFailedResult.sName == 'Uninstalling VirtualBox': + self.investigateInstallUninstallFailure(oCaseFile, oFailedResult, sResultLog, fInstall = False) + + elif sResultLog.find('Exception: 0x800706be (Call to remote object failed (NS_ERROR_CALL_FAILED))') > 0: + oCaseFile.noteReasonForId(self.ktReason_XPCOM_NS_ERROR_CALL_FAILED, oFailedResult.idTestResult); + + else: + fFoundSomething = False; + for fStopOnHit, tReason, sNeedle in self.katSimpleApiMainLogReasons: + if sResultLog.find(sNeedle) > 0: + oCaseFile.noteReasonForId(tReason, oFailedResult.idTestResult); + fFoundSomething = True; + if fStopOnHit: + break; + if fFoundSomething: + self.vprint(u'TODO: Cannot place idTestResult=%u - %s' % (oFailedResult.idTestResult, oFailedResult.sName,)); + self.dprint(u'%s + %s <<\n%s\n<<' % (oFailedResult.tsCreated, oFailedResult.tsElapsed, sResultLog,)); + + # + # Report home and close the case if we got them all, otherwise log it. + # + if len(oCaseFile.dReasonForResultId) >= len(aoFailedResults): + return self.caseClosed(oCaseFile); + + if oCaseFile.dReasonForResultId: + self.vprint(u'TODO: Got %u out of %u - close, but no cigar. :-/' + % (len(oCaseFile.dReasonForResultId), len(aoFailedResults))); + else: + self.vprint(u'XXX: Could not figure out anything at all! :-('); + return False; + + + def reasoningFailures(self): + """ + Guess the reason for failures. + """ + # + # Get a list of failed test sets without any assigned failure reason. + # + cGot = 0; + if not self.oConfig.aidTestSets: + aoTestSets = self.oTestSetLogic.fetchFailedSetsWithoutReason(cHoursBack = self.oConfig.cHoursBack, + tsNow = self.tsNow); + else: + aoTestSets = [self.oTestSetLogic.getById(idTestSet) for idTestSet in self.oConfig.aidTestSets]; + for oTestSet in aoTestSets: + self.dprint(u'----------------------------------- #%u, status %s -----------------------------------' + % ( oTestSet.idTestSet, oTestSet.enmStatus,)); + + # + # Open a case file and assign it to the right investigator. + # + (oTree, _ ) = self.oTestResultLogic.fetchResultTree(oTestSet.idTestSet); + oBuild = BuildDataEx().initFromDbWithId( self.oDb, oTestSet.idBuild, oTestSet.tsCreated); + oTestBox = TestBoxData().initFromDbWithGenId( self.oDb, oTestSet.idGenTestBox); + oTestGroup = TestGroupData().initFromDbWithId( self.oDb, oTestSet.idTestGroup, oTestSet.tsCreated); + oTestCase = TestCaseDataEx().initFromDbWithGenId( self.oDb, oTestSet.idGenTestCase, oTestSet.tsConfig); + + oCaseFile = VirtualTestSheriffCaseFile(self, oTestSet, oTree, oBuild, oTestBox, oTestGroup, oTestCase); + + if oTestSet.enmStatus == TestSetData.ksTestStatus_BadTestBox: + self.dprint(u'investigateBadTestBox is taking over %s.' % (oCaseFile.sLongName,)); + fRc = self.investigateBadTestBox(oCaseFile); + + elif oCaseFile.isVBoxUnitTest(): + self.dprint(u'investigateVBoxUnitTest is taking over %s.' % (oCaseFile.sLongName,)); + fRc = self.investigateVBoxUnitTest(oCaseFile); + + elif oCaseFile.isVBoxInstallTest() or oCaseFile.isVBoxUnattendedInstallTest(): + self.dprint(u'investigateVBoxVMTest is taking over %s.' % (oCaseFile.sLongName,)); + fRc = self.investigateVBoxVMTest(oCaseFile, fSingleVM = True); + + elif oCaseFile.isVBoxUSBTest(): + self.dprint(u'investigateVBoxVMTest is taking over %s.' % (oCaseFile.sLongName,)); + fRc = self.investigateVBoxVMTest(oCaseFile, fSingleVM = True); + + elif oCaseFile.isVBoxStorageTest(): + self.dprint(u'investigateVBoxVMTest is taking over %s.' % (oCaseFile.sLongName,)); + fRc = self.investigateVBoxVMTest(oCaseFile, fSingleVM = True); + + elif oCaseFile.isVBoxGAsTest(): + self.dprint(u'investigateVBoxVMTest is taking over %s.' % (oCaseFile.sLongName,)); + fRc = self.investigateVBoxVMTest(oCaseFile, fSingleVM = True); + + elif oCaseFile.isVBoxAPITest(): + self.dprint(u'investigateVBoxApiTest is taking over %s.' % (oCaseFile.sLongName,)); + fRc = self.investigateVBoxApiTest(oCaseFile); + + elif oCaseFile.isVBoxBenchmarkTest(): + self.dprint(u'investigateVBoxVMTest is taking over %s.' % (oCaseFile.sLongName,)); + fRc = self.investigateVBoxVMTest(oCaseFile, fSingleVM = False); + + elif oCaseFile.isVBoxSmokeTest(): + self.dprint(u'investigateVBoxVMTest is taking over %s.' % (oCaseFile.sLongName,)); + fRc = self.investigateVBoxVMTest(oCaseFile, fSingleVM = False); + + elif oCaseFile.isVBoxSerialTest(): + self.dprint(u'investigateVBoxVMTest is taking over %s.' % (oCaseFile.sLongName,)); + fRc = self.investigateVBoxVMTest(oCaseFile, fSingleVM = False); + + else: + self.vprint(u'reasoningFailures: Unable to classify test set: %s' % (oCaseFile.sLongName,)); + fRc = False; + cGot += fRc is True; + + self.vprint(u'reasoningFailures: Got %u out of %u' % (cGot, len(aoTestSets), )); + return 0; + + + def main(self): + """ + The 'main' function. + Return exit code (0, 1, etc). + """ + # Database stuff. + self.oDb = TMDatabaseConnection() + self.oTestResultLogic = TestResultLogic(self.oDb); + self.oTestSetLogic = TestSetLogic(self.oDb); + self.oFailureReasonLogic = FailureReasonLogic(self.oDb); + self.oTestResultFailureLogic = TestResultFailureLogic(self.oDb); + self.asBsodReasons = self.oFailureReasonLogic.fetchForSheriffByNamedCategory(self.ksBsodCategory); + self.asUnitTestReasons = self.oFailureReasonLogic.fetchForSheriffByNamedCategory(self.ksUnitTestCategory); + + # Get a fix on our 'now' before we do anything.. + self.oDb.execute('SELECT CURRENT_TIMESTAMP - interval \'%s hours\'', (self.oConfig.cStartHoursAgo,)); + self.tsNow = self.oDb.fetchOne(); + + # If we're suppost to commit anything we need to get our user ID. + rcExit = 0; + if self.oConfig.fRealRun: + self.oLogin = UserAccountLogic(self.oDb).tryFetchAccountByLoginName(VirtualTestSheriff.ksLoginName); + if self.oLogin is None: + rcExit = self.eprint('Cannot find my user account "%s"!' % (VirtualTestSheriff.ksLoginName,)); + else: + self.uidSelf = self.oLogin.uid; + + # + # Do the stuff. + # + if rcExit == 0: + rcExit = self.selfCheck(); + if rcExit == 0: + rcExit = self.badTestBoxManagement(); + rcExit2 = self.reasoningFailures(); + if rcExit == 0: + rcExit = rcExit2; + # Redo the bad testbox management after failure reasons have been assigned (got timing issues). + if rcExit == 0: + rcExit = self.badTestBoxManagement(); + + # Cleanup. + self.oFailureReasonLogic = None; + self.oTestResultFailureLogic = None; + self.oTestSetLogic = None; + self.oTestResultLogic = None; + self.oDb.close(); + self.oDb = None; + if self.oLogFile is not None: + self.oLogFile.close(); + self.oLogFile = None; + return rcExit; + +if __name__ == '__main__': + sys.exit(VirtualTestSheriff().main()); diff --git a/src/VBox/ValidationKit/testmanager/cgi/Makefile.kmk b/src/VBox/ValidationKit/testmanager/cgi/Makefile.kmk new file mode 100644 index 00000000..74d882cc --- /dev/null +++ b/src/VBox/ValidationKit/testmanager/cgi/Makefile.kmk @@ -0,0 +1,46 @@ +# $Id: Makefile.kmk $ +## @file +# VirtualBox Validation Kit. +# + +# +# Copyright (C) 2006-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 +# + +SUB_DEPTH = ../../../../.. +include $(KBUILD_PATH)/subheader.kmk + + +VBOX_VALIDATIONKIT_PYTHON_SOURCES += $(wildcard $(PATH_SUB_CURRENT)/*.py) + +$(evalcall def_vbox_validationkit_process_python_sources) +$(evalcall def_vbox_validationkit_process_js_sources) +include $(FILE_KBUILD_SUB_FOOTER) + diff --git a/src/VBox/ValidationKit/testmanager/cgi/admin.py b/src/VBox/ValidationKit/testmanager/cgi/admin.py new file mode 100755 index 00000000..b8c10f76 --- /dev/null +++ b/src/VBox/ValidationKit/testmanager/cgi/admin.py @@ -0,0 +1,77 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# $Id: admin.py $ + +""" +CGI - Administrator Web-UI. +""" + +__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 sys + +# Only the main script needs to modify the path. +g_ksValidationKitDir = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))); +sys.path.append(g_ksValidationKitDir); + +# Validation Kit imports. +from testmanager import config; +from testmanager.core.webservergluecgi import WebServerGlueCgi; +from testmanager.webui.wuiadmin import WuiAdmin; + +def main(): + """ + Main function a la C/C++. Returns exit code. + """ + + oSrvGlue = WebServerGlueCgi(g_ksValidationKitDir, fHtmlOutput = True); + try: + oWui = WuiAdmin(oSrvGlue); + oWui.dispatchRequest(); + oSrvGlue.flush(); + except Exception as oXcpt: + return oSrvGlue.errorPage('Internal error: %s' % (str(oXcpt),), sys.exc_info()); + + return 0; + +if __name__ == '__main__': + if config.g_kfProfileAdmin: + from testmanager.debug import cgiprofiling; + sys.exit(cgiprofiling.profileIt(main)); + else: + sys.exit(main()); + diff --git a/src/VBox/ValidationKit/testmanager/cgi/debuginfo.py b/src/VBox/ValidationKit/testmanager/cgi/debuginfo.py new file mode 100755 index 00000000..a2663436 --- /dev/null +++ b/src/VBox/ValidationKit/testmanager/cgi/debuginfo.py @@ -0,0 +1,71 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# $Id: debuginfo.py $ + +""" +CGI - Debug Info Page. +""" + +__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 sys + +# Only the main script needs to modify the path. +g_ksValidationKitDir = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))); +sys.path.append(g_ksValidationKitDir); + +# Validation Kit imports. +from testmanager.core.webservergluecgi import WebServerGlueCgi; + + +def main(): + """ + Main function a la C/C++. Returns exit code. + """ + + oSrvGlue = WebServerGlueCgi(g_ksValidationKitDir, fHtmlOutput = True); + try: + oSrvGlue.debugInfoPage(); + oSrvGlue.flush(); + except Exception as oXcpt: + return oSrvGlue.errorPage('Internal error: %s' % (str(oXcpt),), sys.exc_info()); + + return 0; + +if __name__ == '__main__': + sys.exit(main()); + diff --git a/src/VBox/ValidationKit/testmanager/cgi/index.py b/src/VBox/ValidationKit/testmanager/cgi/index.py new file mode 100755 index 00000000..b9b546c2 --- /dev/null +++ b/src/VBox/ValidationKit/testmanager/cgi/index.py @@ -0,0 +1,78 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# $Id: index.py $ + +""" +CGI - Web UI - Main (index) page. +""" + +__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 sys + +# Only the main script needs to modify the path. +g_ksValidationKitDir = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))); +sys.path.append(g_ksValidationKitDir); + +# Validation Kit imports. +from testmanager import config; +from testmanager.core.webservergluecgi import WebServerGlueCgi; +from testmanager.webui.wuimain import WuiMain; + + +def main(): + """ + Main function a la C/C++. Returns exit code. + """ + + oSrvGlue = WebServerGlueCgi(g_ksValidationKitDir, fHtmlOutput = False); + try: + oWui = WuiMain(oSrvGlue); + oWui.dispatchRequest(); + oSrvGlue.flush(); + except Exception as oXcpt: + return oSrvGlue.errorPage('Internal error: %s' % (str(oXcpt),), sys.exc_info()); + + return 0; + +if __name__ == '__main__': + if config.g_kfProfileIndex: + from testmanager.debug import cgiprofiling; + sys.exit(cgiprofiling.profileIt(main)); + else: + sys.exit(main()); + diff --git a/src/VBox/ValidationKit/testmanager/cgi/logout.py b/src/VBox/ValidationKit/testmanager/cgi/logout.py new file mode 100755 index 00000000..109bfa86 --- /dev/null +++ b/src/VBox/ValidationKit/testmanager/cgi/logout.py @@ -0,0 +1,79 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# $Id: logout.py $ + +""" +VirtualBox Validation Kit - CGI - Log out page. +""" + +__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 sys + +# Only the main script needs to modify the path. +g_ksValidationKitDir = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))); +sys.path.append(g_ksValidationKitDir); + +# Validation Kit imports. +from testmanager.core.webservergluecgi import WebServerGlueCgi + + +def main(): + """ + Main function a la C/C++. Returns exit code. + """ + + oSrvGlue = WebServerGlueCgi(g_ksValidationKitDir, fHtmlOutput = True) + sUser = oSrvGlue.getLoginName() + if sUser not in (oSrvGlue.ksUnknownUser, 'logout'): + oSrvGlue.write('<p>Broken apache config!\n' + 'The logout.py script should be configured with .htaccess-logout and require user logout!</p>') + else: + oSrvGlue.write('<p>Successfully logged out!</p>') + oSrvGlue.write('<p><a href="%sadmin.py">Log in</a> under another user name.</p>' % + (oSrvGlue.getBaseUrl(),)) + + + oSrvGlue.write('<hr/><p>debug info:</p>') + oSrvGlue.debugInfoPage() + oSrvGlue.flush() + + return 0 + +if __name__ == '__main__': + sys.exit(main()) + diff --git a/src/VBox/ValidationKit/testmanager/cgi/logout2.py b/src/VBox/ValidationKit/testmanager/cgi/logout2.py new file mode 100755 index 00000000..1ab24cbc --- /dev/null +++ b/src/VBox/ValidationKit/testmanager/cgi/logout2.py @@ -0,0 +1,81 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# $Id: logout2.py $ + +""" +VirtualBox Validation Kit - CGI - Log out page for Safari. +""" + +__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 sys + +# Only the main script needs to modify the path. +g_ksValidationKitDir = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))); +sys.path.append(g_ksValidationKitDir); + +# Validation Kit imports. +from testmanager.core.webservergluecgi import WebServerGlueCgi; + + +def main(): + """ + Main function a la C/C++. Returns exit code. + """ + + oSrvGlue = WebServerGlueCgi(g_ksValidationKitDir, fHtmlOutput = True); + sUserAgent = oSrvGlue.getUserAgent(); + oSrvGlue.setHeaderField('Status', '401 Unauthorized to access the document'); + oSrvGlue.setHeaderField('WWW-authenticate', 'Basic realm="Test Manager"'); + if sUserAgent.startswith('Mozilla/') and sUserAgent.find('AppleWebKit/') > 0: + oSrvGlue.write('<p>Attempting to log out an Apple browser...</p>'); + else: + oSrvGlue.write('<p>Sorry, not sure this will work...</p>'); + oSrvGlue.write('<p>User-Agent:' + sUserAgent + '</p>'); + + oSrvGlue.write('<p><a href="%sadmin.py">Log in</a> under another user name.</p>' % + (oSrvGlue.getBaseUrl(),)) + + oSrvGlue.write('<hr/><p>debug info:</p>'); + oSrvGlue.debugInfoPage(); + oSrvGlue.flush(); + + return 0; + +if __name__ == '__main__': + sys.exit(main()); + diff --git a/src/VBox/ValidationKit/testmanager/cgi/rest.py b/src/VBox/ValidationKit/testmanager/cgi/rest.py new file mode 100755 index 00000000..89a5238c --- /dev/null +++ b/src/VBox/ValidationKit/testmanager/cgi/rest.py @@ -0,0 +1,81 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# $Id: rest.py $ + +""" +CGI - REST - sPath=path variant. +""" + +__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 sys + +# Only the main script needs to modify the path. +g_ksValidationKitDir = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))); +sys.path.append(g_ksValidationKitDir); + +# Validation Kit imports. +from testmanager import config; +from testmanager.core.webservergluecgi import WebServerGlueCgi; +from testmanager.core.restdispatcher import RestMain, RestDispException; + + +def main(): + """ + Main function a la C/C++. Returns exit code. + """ + + oSrvGlue = WebServerGlueCgi(g_ksValidationKitDir, fHtmlOutput = False); + try: + oMain = RestMain(oSrvGlue); + oMain.dispatchRequest(); + oSrvGlue.flush(); + except RestDispException as oXcpt: + oSrvGlue.setStatus(oXcpt.iStatus); + oSrvGlue.setHeaderField('tm-error-message', str(oXcpt)); + oSrvGlue.write('error: ' + str(oXcpt)); + oSrvGlue.flush(); + except Exception as oXcpt: + return oSrvGlue.errorPage('Internal error: %s' % (str(oXcpt),), + sys.exc_info(), + config.g_ksTestBoxDispXpctLog); + + return 0; + +if __name__ == '__main__': + sys.exit(main()); + diff --git a/src/VBox/ValidationKit/testmanager/cgi/status.py b/src/VBox/ValidationKit/testmanager/cgi/status.py new file mode 100755 index 00000000..39c8af03 --- /dev/null +++ b/src/VBox/ValidationKit/testmanager/cgi/status.py @@ -0,0 +1,519 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# $Id: status.py $ + +""" +CGI - Administrator Web-UI. +""" + +__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 sys + +# Only the main script needs to modify the path. +g_ksValidationKitDir = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))); +sys.path.append(g_ksValidationKitDir); + +# Validation Kit imports. +from testmanager import config; +from testmanager.core.webservergluecgi import WebServerGlueCgi; + +from common import constants; +from testmanager.core.base import TMExceptionBase; +from testmanager.core.db import TMDatabaseConnection; + + + +def timeDeltaToHours(oTimeDelta): + return oTimeDelta.days * 24 + oTimeDelta.seconds // 3600 + + +def testbox_data_processing(oDb): + testboxes_dict = {} + while True: + line = oDb.fetchOne(); + if line is None: + break; + testbox_name = line[0] + test_result = line[1] + oTimeDeltaSinceStarted = line[2] + test_box_os = line[3] + test_sched_group = line[4] + + # idle testboxes might have an assigned testsets, skipping them + if test_result not in g_kdTestStatuses: + continue + + testboxes_dict = dict_update(testboxes_dict, testbox_name, test_result) + + if "testbox_os" not in testboxes_dict[testbox_name]: + testboxes_dict[testbox_name].update({"testbox_os": test_box_os}) + + if "sched_group" not in testboxes_dict[testbox_name]: + testboxes_dict[testbox_name].update({"sched_group": test_sched_group}) + elif test_sched_group not in testboxes_dict[testbox_name]["sched_group"]: + testboxes_dict[testbox_name]["sched_group"] += "," + test_sched_group + + if test_result == "running": + testboxes_dict[testbox_name].update({"hours_running": timeDeltaToHours(oTimeDeltaSinceStarted)}) + + return testboxes_dict; + + +def os_results_separating(vb_dict, test_name, testbox_os, test_result): + if testbox_os == "linux": + dict_update(vb_dict, test_name + " / linux", test_result) + elif testbox_os == "win": + dict_update(vb_dict, test_name + " / windows", test_result) + elif testbox_os == "darwin": + dict_update(vb_dict, test_name + " / darwin", test_result) + elif testbox_os == "solaris": + dict_update(vb_dict, test_name + " / solaris", test_result) + else: + dict_update(vb_dict, test_name + " / other", test_result) + + +# const/immutable. +g_kdTestStatuses = { + 'running': 0, + 'success': 0, + 'skipped': 0, + 'bad-testbox': 0, + 'aborted': 0, + 'failure': 0, + 'timed-out': 0, + 'rebooted': 0, +} + +def dict_update(target_dict, key_name, test_result): + if key_name not in target_dict: + target_dict.update({key_name: g_kdTestStatuses.copy()}) + if test_result in g_kdTestStatuses: + target_dict[key_name][test_result] += 1 + return target_dict + + +def formatDataEntry(sKey, dEntry): + # There are variations in the first and second "columns". + if "hours_running" in dEntry: + sRet = "%s;%s;%s | running: %s;%s" \ + % (sKey, dEntry["testbox_os"], dEntry["sched_group"], dEntry["running"], dEntry["hours_running"]); + else: + if "testbox_os" in dEntry: + sRet = "%s;%s;%s" % (sKey, dEntry["testbox_os"], dEntry["sched_group"],); + else: + sRet = sKey; + sRet += " | running: %s" % (dEntry["running"],) + + # The rest is currently identical: + sRet += " | success: %s | skipped: %s | bad-testbox: %s | aborted: %s | failure: %s | timed-out: %s | rebooted: %s | \n" \ + % (dEntry["success"], dEntry["skipped"], dEntry["bad-testbox"], dEntry["aborted"], + dEntry["failure"], dEntry["timed-out"], dEntry["rebooted"],); + return sRet; + + +def format_data(dData, fSorted): + sRet = ""; + if not fSorted: + for sKey in dData: + sRet += formatDataEntry(sKey, dData[sKey]); + else: + for sKey in sorted(dData.keys()): + sRet += formatDataEntry(sKey, dData[sKey]); + return sRet; + +###### + +class StatusDispatcherException(TMExceptionBase): + """ + Exception class for TestBoxController. + """ + pass; # pylint: disable=unnecessary-pass + + +class StatusDispatcher(object): # pylint: disable=too-few-public-methods + """ + Status dispatcher class. + """ + + + def __init__(self, oSrvGlue): + """ + Won't raise exceptions. + """ + self._oSrvGlue = oSrvGlue; + self._sAction = None; # _getStandardParams / dispatchRequest sets this later on. + self._dParams = None; # _getStandardParams / dispatchRequest sets this later on. + self._asCheckedParams = []; + self._dActions = \ + { + 'MagicMirrorTestResults': self._actionMagicMirrorTestResults, + 'MagicMirrorTestBoxes': self._actionMagicMirrorTestBoxes, + }; + + def _getStringParam(self, sName, asValidValues = None, fStrip = False, sDefValue = None): + """ + Gets a string parameter (stripped). + + Raises exception if not found and no default is provided, or if the + value isn't found in asValidValues. + """ + if sName not in self._dParams: + if sDefValue is None: + raise StatusDispatcherException('%s parameter %s is missing' % (self._sAction, sName)); + return sDefValue; + sValue = self._dParams[sName]; + if fStrip: + sValue = sValue.strip(); + + if sName not in self._asCheckedParams: + self._asCheckedParams.append(sName); + + if asValidValues is not None and sValue not in asValidValues: + raise StatusDispatcherException('%s parameter %s value "%s" not in %s ' + % (self._sAction, sName, sValue, asValidValues)); + return sValue; + + def _getIntParam(self, sName, iMin = None, iMax = None, iDefValue = None): + """ + Gets a string parameter. + Raises exception if not found, not a valid integer, or if the value + isn't in the range defined by iMin and iMax. + """ + if sName not in self._dParams: + if iDefValue is None: + raise StatusDispatcherException('%s parameter %s is missing' % (self._sAction, sName)); + return iDefValue; + sValue = self._dParams[sName]; + try: + iValue = int(sValue, 0); + except: + raise StatusDispatcherException('%s parameter %s value "%s" cannot be convert to an integer' + % (self._sAction, sName, sValue)); + if sName not in self._asCheckedParams: + self._asCheckedParams.append(sName); + + if (iMin is not None and iValue < iMin) \ + or (iMax is not None and iValue > iMax): + raise StatusDispatcherException('%s parameter %s value %d is out of range [%s..%s]' + % (self._sAction, sName, iValue, iMin, iMax)); + return iValue; + + def _getBoolParam(self, sName, fDefValue = None): + """ + Gets a boolean parameter. + + Raises exception if not found and no default is provided, or if not a + valid boolean. + """ + sValue = self._getStringParam(sName, [ 'True', 'true', '1', 'False', 'false', '0'], sDefValue = str(fDefValue)); + return sValue in ('True', 'true', '1',); + + def _checkForUnknownParameters(self): + """ + Check if we've handled all parameters, raises exception if anything + unknown was found. + """ + + if len(self._asCheckedParams) != len(self._dParams): + sUnknownParams = ''; + for sKey in self._dParams: + if sKey not in self._asCheckedParams: + sUnknownParams += ' ' + sKey + '=' + self._dParams[sKey]; + raise StatusDispatcherException('Unknown parameters: ' + sUnknownParams); + + return True; + + def _connectToDb(self): + """ + Connects to the database. + + Returns (TMDatabaseConnection, (more later perhaps) ) on success. + Returns (None, ) on failure after sending the box an appropriate response. + May raise exception on DB error. + """ + return (TMDatabaseConnection(self._oSrvGlue.dprint),); + + def _actionMagicMirrorTestBoxes(self): + """ + Produces test result status for the magic mirror dashboard + """ + + # + # Parse arguments and connect to the database. + # + cHoursBack = self._getIntParam('cHours', 1, 24*14, 12); + fSorted = self._getBoolParam('fSorted', False); + self._checkForUnknownParameters(); + + # + # Get the data. + # + # Note! We're not joining on TestBoxesWithStrings.idTestBox = + # TestSets.idGenTestBox here because of indexes. This is + # also more consistent with the rest of the query. + # Note! The original SQL is slow because of the 'OR TestSets.tsDone' + # part, using AND and UNION is significatly faster because + # it matches the TestSetsGraphBoxIdx (index). + # + (oDb,) = self._connectToDb(); + if oDb is None: + return False; + + # + # some comments regarding select below: + # first part is about fetching all finished tests for last cHoursBack hours + # second part is fetching all tests which isn't done + # both old (running more than cHoursBack) and fresh (less than cHoursBack) ones + # 'cause we want to know if there's a hanging tests together with currently running + # + # there's also testsets without status at all, likely because disabled testboxes still have an assigned testsets + # + oDb.execute(''' +( SELECT TestBoxesWithStrings.sName, + TestSets.enmStatus, + CURRENT_TIMESTAMP - TestSets.tsCreated, + TestBoxesWithStrings.sOS, + SchedGroupNames.sSchedGroupNames + FROM ( + SELECT TestBoxesInSchedGroups.idTestBox AS idTestBox, + STRING_AGG(SchedGroups.sName, ',') AS sSchedGroupNames + FROM TestBoxesInSchedGroups + INNER JOIN SchedGroups + ON SchedGroups.idSchedGroup = TestBoxesInSchedGroups.idSchedGroup + WHERE TestBoxesInSchedGroups.tsExpire = 'infinity'::TIMESTAMP + AND SchedGroups.tsExpire = 'infinity'::TIMESTAMP + GROUP BY TestBoxesInSchedGroups.idTestBox + ) AS SchedGroupNames, + TestBoxesWithStrings + LEFT OUTER JOIN TestSets + ON TestSets.idTestBox = TestBoxesWithStrings.idTestBox + AND TestSets.tsCreated >= (CURRENT_TIMESTAMP - '%s hours'::interval) + AND TestSets.tsDone IS NOT NULL + WHERE TestBoxesWithStrings.tsExpire = 'infinity'::TIMESTAMP + AND SchedGroupNames.idTestBox = TestBoxesWithStrings.idTestBox +) UNION ( + SELECT TestBoxesWithStrings.sName, + TestSets.enmStatus, + CURRENT_TIMESTAMP - TestSets.tsCreated, + TestBoxesWithStrings.sOS, + SchedGroupNames.sSchedGroupNames + FROM ( + SELECT TestBoxesInSchedGroups.idTestBox AS idTestBox, + STRING_AGG(SchedGroups.sName, ',') AS sSchedGroupNames + FROM TestBoxesInSchedGroups + INNER JOIN SchedGroups + ON SchedGroups.idSchedGroup = TestBoxesInSchedGroups.idSchedGroup + WHERE TestBoxesInSchedGroups.tsExpire = 'infinity'::TIMESTAMP + AND SchedGroups.tsExpire = 'infinity'::TIMESTAMP + GROUP BY TestBoxesInSchedGroups.idTestBox + ) AS SchedGroupNames, + TestBoxesWithStrings + LEFT OUTER JOIN TestSets + ON TestSets.idTestBox = TestBoxesWithStrings.idTestBox + AND TestSets.tsDone IS NULL + WHERE TestBoxesWithStrings.tsExpire = 'infinity'::TIMESTAMP + AND SchedGroupNames.idTestBox = TestBoxesWithStrings.idTestBox +) +''', (cHoursBack, cHoursBack,)); + + + # + # Process, format and output data. + # + dResult = testbox_data_processing(oDb); + self._oSrvGlue.setContentType('text/plain'); + self._oSrvGlue.write(format_data(dResult, fSorted)); + + return True; + + def _actionMagicMirrorTestResults(self): + """ + Produces test result status for the magic mirror dashboard + """ + + # + # Parse arguments and connect to the database. + # + sBranch = self._getStringParam('sBranch'); + cHoursBack = self._getIntParam('cHours', 1, 24*14, 6); ## @todo why 6 hours here and 12 for test boxes? + fSorted = self._getBoolParam('fSorted', False); + self._checkForUnknownParameters(); + + # + # Get the data. + # + # Note! These queries should be joining TestBoxesWithStrings and TestSets + # on idGenTestBox rather than on idTestBox and tsExpire=inf, but + # we don't have any index matching those. So, we'll ignore tests + # performed by deleted testboxes for the present as that doesn't + # happen often and we want the ~1000x speedup. + # + (oDb,) = self._connectToDb(); + if oDb is None: + return False; + + if sBranch == 'all': + oDb.execute(''' +SELECT TestSets.enmStatus, + TestCases.sName, + TestBoxesWithStrings.sOS +FROM TestSets +INNER JOIN TestCases + ON TestCases.idGenTestCase = TestSets.idGenTestCase +INNER JOIN TestBoxesWithStrings + ON TestBoxesWithStrings.idTestBox = TestSets.idTestBox + AND TestBoxesWithStrings.tsExpire = 'infinity'::TIMESTAMP +WHERE TestSets.tsCreated >= (CURRENT_TIMESTAMP - '%s hours'::interval) +''', (cHoursBack,)); + else: + oDb.execute(''' +SELECT TestSets.enmStatus, + TestCases.sName, + TestBoxesWithStrings.sOS +FROM TestSets +INNER JOIN BuildCategories + ON BuildCategories.idBuildCategory = TestSets.idBuildCategory + AND BuildCategories.sBranch = %s +INNER JOIN TestCases + ON TestCases.idGenTestCase = TestSets.idGenTestCase +INNER JOIN TestBoxesWithStrings + ON TestBoxesWithStrings.idTestBox = TestSets.idTestBox + AND TestBoxesWithStrings.tsExpire = 'infinity'::TIMESTAMP +WHERE TestSets.tsCreated >= (CURRENT_TIMESTAMP - '%s hours'::interval) +''', (sBranch, cHoursBack,)); + + # Process the data + dResult = {}; + while True: + aoRow = oDb.fetchOne(); + if aoRow is None: + break; + os_results_separating(dResult, aoRow[1], aoRow[2], aoRow[0]) # save all test results + + # Format and output it. + self._oSrvGlue.setContentType('text/plain'); + self._oSrvGlue.write(format_data(dResult, fSorted)); + + return True; + + def _getStandardParams(self, dParams): + """ + Gets the standard parameters and validates them. + + The parameters are returned as a tuple: sAction, (more later, maybe) + Note! the sTextBoxId can be None if it's a SIGNON request. + + Raises StatusDispatcherException on invalid input. + """ + # + # Get the action parameter and validate it. + # + if constants.tbreq.ALL_PARAM_ACTION not in dParams: + raise StatusDispatcherException('No "%s" parameter in request (params: %s)' + % (constants.tbreq.ALL_PARAM_ACTION, dParams,)); + sAction = dParams[constants.tbreq.ALL_PARAM_ACTION]; + + if sAction not in self._dActions: + raise StatusDispatcherException('Unknown action "%s" in request (params: %s; action: %s)' + % (sAction, dParams, self._dActions)); + # + # Update the list of checked parameters. + # + self._asCheckedParams.extend([constants.tbreq.ALL_PARAM_ACTION,]); + + return (sAction,); + + def dispatchRequest(self): + """ + Dispatches the incoming request. + + Will raise StatusDispatcherException on failure. + """ + + # + # Must be a GET request. + # + try: + sMethod = self._oSrvGlue.getMethod(); + except Exception as oXcpt: + raise StatusDispatcherException('Error retriving request method: %s' % (oXcpt,)); + if sMethod != 'GET': + raise StatusDispatcherException('Error expected POST request not "%s"' % (sMethod,)); + + # + # Get the parameters and checks for duplicates. + # + try: + dParams = self._oSrvGlue.getParameters(); + except Exception as oXcpt: + raise StatusDispatcherException('Error retriving parameters: %s' % (oXcpt,)); + for sKey in dParams.keys(): + if len(dParams[sKey]) > 1: + raise StatusDispatcherException('Parameter "%s" is given multiple times: %s' % (sKey, dParams[sKey])); + dParams[sKey] = dParams[sKey][0]; + self._dParams = dParams; + + # + # Get+validate the standard action parameters and dispatch the request. + # + (self._sAction, ) = self._getStandardParams(dParams); + return self._dActions[self._sAction](); + + +def main(): + """ + Main function a la C/C++. Returns exit code. + """ + + oSrvGlue = WebServerGlueCgi(g_ksValidationKitDir, fHtmlOutput = False); + try: + oDisp = StatusDispatcher(oSrvGlue); + oDisp.dispatchRequest(); + oSrvGlue.flush(); + except Exception as oXcpt: + return oSrvGlue.errorPage('Internal error: %s' % (str(oXcpt),), sys.exc_info()); + + return 0; + +if __name__ == '__main__': + if config.g_kfProfileAdmin: + from testmanager.debug import cgiprofiling; + sys.exit(cgiprofiling.profileIt(main)); + else: + sys.exit(main()); + diff --git a/src/VBox/ValidationKit/testmanager/cgi/testboxdisp.py b/src/VBox/ValidationKit/testmanager/cgi/testboxdisp.py new file mode 100755 index 00000000..c5c704bb --- /dev/null +++ b/src/VBox/ValidationKit/testmanager/cgi/testboxdisp.py @@ -0,0 +1,75 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# $Id: testboxdisp.py $ + +""" +CGI - TestBox Interaction (see testboxscript or the other party). +""" + +__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 sys + +# Only the main script needs to modify the path. +g_ksValidationKitDir = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))); +sys.path.append(g_ksValidationKitDir); + +# Validation Kit imports. +from testmanager import config; +from testmanager.core.webservergluecgi import WebServerGlueCgi; +from testmanager.core.testboxcontroller import TestBoxController; + + +def main(): + """ + Main function a la C/C++. Returns exit code. + """ + + oSrvGlue = WebServerGlueCgi(g_ksValidationKitDir, fHtmlOutput = False); + oCtrl = TestBoxController(oSrvGlue); + try: + oCtrl.dispatchRequest() + oSrvGlue.flush(); + except Exception as oXcpt: + return oSrvGlue.errorPage('Internal error: %s' % (str(oXcpt),), + sys.exc_info(), + config.g_ksTestBoxDispXpctLog); + return 0; + +if __name__ == '__main__': + sys.exit(main()); + diff --git a/src/VBox/ValidationKit/testmanager/config.py b/src/VBox/ValidationKit/testmanager/config.py new file mode 100644 index 00000000..b4ef94cc --- /dev/null +++ b/src/VBox/ValidationKit/testmanager/config.py @@ -0,0 +1,261 @@ +# -*- coding: utf-8 -*- +# $Id: config.py $ + +""" +Test Manager Configuration. +""" + +__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 $" + +import os; + +## Test Manager version string. +g_ksVersion = 'v0.1.0'; +## Test Manager revision string. +g_ksRevision = ('$Revision: 155244 $')[11:-2]; + +## Enable VBox specific stuff. +g_kfVBoxSpecific = True; + + +## @name Used by the TMDatabaseConnection class. +# @{ +g_ksDatabaseName = 'testmanager'; +g_ksDatabaseAddress = None; +g_ksDatabasePort = None; +g_ksDatabaseUser = 'postgres'; +g_ksDatabasePassword = ''; +## @} + + +## @name User handling. +## @{ + +## Whether login names are case insensitive (True) or case sensitive (False). +## @note Implemented by inserting lower case names into DB and lower case +## bind variables in WHERE clauses. +g_kfLoginNameCaseInsensitive = True; + +## @} + + +## @name File locations +## @{ + +## The TestManager directory. +g_ksTestManagerDir = os.path.dirname(os.path.abspath(__file__)); +## The Validation Kit directory. +g_ksValidationKitDir = os.path.dirname(g_ksTestManagerDir); +## The TestManager htdoc directory. +g_ksTmHtDocDir = os.path.join(g_ksTestManagerDir, 'htdocs'); +## The TestManager download directory (under htdoc somewhere), for validationkit zips. +g_ksTmDownloadDir = os.path.join(g_ksTmHtDocDir, 'download'); +## The base URL relative path of the TM download directory (g_ksTmDownloadDir). +g_ksTmDownloadBaseUrlRel = 'htdocs/downloads'; +## The root of the file area (referred to as TM_FILE_DIR in database docs). +g_ksFileAreaRootDir = '/var/tmp/testmanager' +## The root of the file area with the zip files (best put on a big storage server). +g_ksZipFileAreaRootDir = '/var/tmp/testmanager2' +## URL prefix for trac log viewer. +g_ksTracLogUrlPrefix = 'https://linserv.de.oracle.com/vbox/log/' +## URL prefix for trac log viewer. +g_ksTracChangsetUrlFmt = 'https://linserv.de.oracle.com/%(sRepository)s/changeset/%(iRevision)s' +## URL prefix for unprefixed build logs. +g_ksBuildLogUrlPrefix = '' +## URL prefix for unprefixed build binaries. +g_ksBuildBinUrlPrefix = '/builds/' +## The local path prefix for unprefixed build binaries. (Host file system, not web server.) +g_ksBuildBinRootDir = '/mnt/builds/' +## File on the build binary share that can be used to check that it's mounted. +g_ksBuildBinRootFile = 'builds.txt' +## Template for paratial database dump output files. One argument: UID +g_ksTmDbDumpOutFileTmpl = '/var/tmp/tm-partial-db-dump-for-%u.zip' +## Template for paratial database dump temporary files. One argument: UID +g_ksTmDbDumpTmpFileTmpl = '/var/tmp/tm-partial-db-dump-for-%u.pgtxt' +## @} + + +## @name Scheduling parameters +## @{ + +## The time to wait for a gang to gather (in seconds). +g_kcSecGangGathering = 600; +## The max time allowed to spend looking for a new task (in seconds). +g_kcSecMaxNewTask = 60; +## Minimum time since last task started. +g_kcSecMinSinceLastTask = 120; # (2 min) +## Minimum time since last failed task. +g_kcSecMinSinceLastFailedTask = 180; # (3 min) + +## @} + + + +## @name Test result limits. +## In general, we will fail the test when reached and stop accepting further results. +## @{ + +## The max number of test results per test set. +g_kcMaxTestResultsPerTS = 4096; +## The max number of test results (children) per test result. +g_kcMaxTestResultsPerTR = 512; +## The max number of test result values per test set. +g_kcMaxTestValuesPerTS = 4096; +## The max number of test result values per test result. +g_kcMaxTestValuesPerTR = 256; +## The max number of test result message per test result. +g_kcMaxTestMsgsPerTR = 4; +## The max test result nesting depth. +g_kcMaxTestResultDepth = 10; + +## The max length of a test result name. +g_kcchMaxTestResultName = 64; +## The max length of a test result value name. +g_kcchMaxTestValueName = 56; +## The max length of a test result message. +g_kcchMaxTestMsg = 128; + +## The max size of the main log file. +g_kcMbMaxMainLog = 32; +## The max size of an uploaded file (individual). +g_kcMbMaxUploadSingle = 150; +## The max size of all uploaded file. +g_kcMbMaxUploadTotal = 200; +## The max number of files that can be uploaded. +g_kcMaxUploads = 256; +## @} + + +## @name Bug Trackers and VCS reference tags. +## @{ +class BugTrackerConfig(object): + """ Bug tracker config """ + def __init__(self, sDbId, sName, sBugUrl, asCommitTags): + assert len(sDbId) == 4; + self.sDbId = sDbId; + self.sName = sName; + self.sBugUrl = sBugUrl; + self.asCommitTags = asCommitTags; + +## The key is the database table +g_kdBugTrackers = { + 'xtrk': BugTrackerConfig('xtrk', 'xTracker', 'https://linserv.de.oracle.com/vbox/xTracker/index.php?bug=', + ['bugref:', '@bugref{', 'bugef:', 'bugrf:', ], ), + 'bgdb': BugTrackerConfig('bgdb', 'BugDB', 'https://bug.oraclecorp.com/pls/bug/webbug_edit.edit_info_top?rptno=', + ['bugdbref:', '@bugdbref{', 'bugdb:', ], ), + 'vorg': BugTrackerConfig('vorg', 'External Trac', 'https://www.virtualbox.org/ticket/', + ['ticketref:', '@ticketref{', 'ticket:', ], ), +}; +## @} + + + +## @name Virtual Sheriff email alerts +## @{ + +## SMTP server host name. +g_ksSmtpHost = 'internal-mail-router.oracle.com'; +## SMTP server port number. +g_kcSmtpPort = 25; +## Default email 'From' for email alert. +g_ksAlertFrom = 'vsheriff@oracle.com'; +## Subject for email alert. +g_ksAlertSubject = 'Virtual Test Sheriff Alert'; +## List of users to send alerts. +g_asAlertList = ['alertuser1', 'alertuser2']; +## iLOM password. +g_ksLomPassword = 'put_your_ILOM_password_here_if_applicable'; + +## @} + + +## @name Partial Database Dump +## @{ + +## Minimum number of day. Set higher than g_kcTmDbDumpMaxDays to disable. +g_kcTmDbDumpMinDays = 1; +## Maximum number of day. Keep low - consider space and runtime. +g_kcTmDbDumpMaxDays = 31; +## The default number of days. +g_kcTmDbDumpDefaultDays = 14; +## @} + + +## @name Debug Features +## @{ + +## Enables extra DB exception information. +g_kfDebugDbXcpt = True; + +## Where to write the glue debug. +# None indicates apache error log, string indicates a file. +#g_ksSrvGlueDebugLogDst = '/tmp/testmanager-srv-glue.log'; +g_ksSrvGlueDebugLogDst = None; +## Whether to enable CGI trace back in the server glue. +g_kfSrvGlueCgiTb = False; +## Enables glue debug output. +g_kfSrvGlueDebug = False; +## Timestamp and pid prefix the glue debug output. +g_kfSrvGlueDebugTS = True; +## Whether to dumping CGI environment variables. +g_kfSrvGlueCgiDumpEnv = False; +## Whether to dumping CGI script arguments. +g_kfSrvGlueCgiDumpArgs = False; +## Enables task scheduler debug output to g_ksSrvGlueDebugLogDst. +g_kfSrvGlueDebugScheduler = False; + +## Enables the SQL trace back. +g_kfWebUiSqlTrace = False; +## Enables the explain in the SQL trace back. +g_kfWebUiSqlTraceExplain = False; +## Whether the postgresql version supports the TIMING option on EXPLAIN (>= 9.2). +g_kfWebUiSqlTraceExplainTiming = False; +## Display time spent processing the page. +g_kfWebUiProcessedIn = True; +## Enables WebUI debug output. +g_kfWebUiDebug = False; +## Enables WebUI SQL debug output print() calls (requires g_kfWebUiDebug). +g_kfWebUiSqlDebug = False; +## Enables the debug panel at the bottom of the page. +g_kfWebUiDebugPanel = True; + +## Profile cgi/admin.py. +g_kfProfileAdmin = False; +## Profile cgi/index.py. +g_kfProfileIndex = False; + +## When not None, +g_ksTestBoxDispXpctLog = '/tmp/testmanager-testboxdisp-xcpt.log' +## @} + diff --git a/src/VBox/ValidationKit/testmanager/core/Makefile.kmk b/src/VBox/ValidationKit/testmanager/core/Makefile.kmk new file mode 100644 index 00000000..74d882cc --- /dev/null +++ b/src/VBox/ValidationKit/testmanager/core/Makefile.kmk @@ -0,0 +1,46 @@ +# $Id: Makefile.kmk $ +## @file +# VirtualBox Validation Kit. +# + +# +# Copyright (C) 2006-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 +# + +SUB_DEPTH = ../../../../.. +include $(KBUILD_PATH)/subheader.kmk + + +VBOX_VALIDATIONKIT_PYTHON_SOURCES += $(wildcard $(PATH_SUB_CURRENT)/*.py) + +$(evalcall def_vbox_validationkit_process_python_sources) +$(evalcall def_vbox_validationkit_process_js_sources) +include $(FILE_KBUILD_SUB_FOOTER) + diff --git a/src/VBox/ValidationKit/testmanager/core/__init__.py b/src/VBox/ValidationKit/testmanager/core/__init__.py new file mode 100644 index 00000000..bfcc7f8b --- /dev/null +++ b/src/VBox/ValidationKit/testmanager/core/__init__.py @@ -0,0 +1,40 @@ +# -*- coding: utf-8 -*- +# $Id: __init__.py $ + +""" +TestBox Script - Core Logic. +""" + +__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 $" + diff --git a/src/VBox/ValidationKit/testmanager/core/base.py b/src/VBox/ValidationKit/testmanager/core/base.py new file mode 100755 index 00000000..eac3d921 --- /dev/null +++ b/src/VBox/ValidationKit/testmanager/core/base.py @@ -0,0 +1,1514 @@ +# -*- coding: utf-8 -*- +# $Id: base.py $ +# pylint: disable=too-many-lines + +""" +Test Manager Core - Base Class(es). +""" + +__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 copy; +import datetime; +import json; +import re; +import socket; +import sys; +import uuid; +import unittest; + +# Validation Kit imports. +from common import utils; + +# Python 3 hacks: +if sys.version_info[0] >= 3: + long = int # pylint: disable=redefined-builtin,invalid-name + + +class TMExceptionBase(Exception): + """ + For exceptions raised by any TestManager component. + """ + pass; # pylint: disable=unnecessary-pass + + +class TMTooManyRows(TMExceptionBase): + """ + Too many rows in the result. + Used by ModelLogicBase decendants. + """ + pass; # pylint: disable=unnecessary-pass + + +class TMRowNotFound(TMExceptionBase): + """ + Database row not found. + Used by ModelLogicBase decendants. + """ + pass; # pylint: disable=unnecessary-pass + + +class TMRowAlreadyExists(TMExceptionBase): + """ + Database row already exists (typically raised by addEntry). + Used by ModelLogicBase decendants. + """ + pass; # pylint: disable=unnecessary-pass + + +class TMInvalidData(TMExceptionBase): + """ + Data validation failed. + Used by ModelLogicBase decendants. + """ + pass; # pylint: disable=unnecessary-pass + + +class TMRowInUse(TMExceptionBase): + """ + Database row is in use and cannot be deleted. + Used by ModelLogicBase decendants. + """ + pass; # pylint: disable=unnecessary-pass + + +class TMInFligthCollision(TMExceptionBase): + """ + Database update failed because someone else had already made changes to + the data there. + Used by ModelLogicBase decendants. + """ + pass; # pylint: disable=unnecessary-pass + + +class ModelBase(object): # pylint: disable=too-few-public-methods + """ + Something all classes in the logical model inherits from. + + Not sure if 'logical model' is the right term here. + Will see if it has any purpose later on... + """ + + def __init__(self): + pass; + + +class ModelDataBase(ModelBase): # pylint: disable=too-few-public-methods + """ + Something all classes in the data classes in the logical model inherits from. + """ + + ## Child classes can use this to list array attributes which should use + # an empty array ([]) instead of None as database NULL value. + kasAltArrayNull = []; + + ## validate + ## @{ + ksValidateFor_Add = 'add'; + ksValidateFor_AddForeignId = 'add-foreign-id'; + ksValidateFor_Edit = 'edit'; + ksValidateFor_Other = 'other'; + ## @} + + + ## List of internal attributes which should be ignored by + ## getDataAttributes and related machinery + kasInternalAttributes = []; + + def __init__(self): + ModelBase.__init__(self); + + + # + # Standard methods implemented by combining python magic and hungarian prefixes. + # + + def getDataAttributes(self): + """ + Returns a list of data attributes. + """ + asRet = []; + asAttrs = dir(self); + for sAttr in asAttrs: + if sAttr[0] == '_' or sAttr[0] == 'k': + continue; + if sAttr in self.kasInternalAttributes: + continue; + oValue = getattr(self, sAttr); + if callable(oValue): + continue; + asRet.append(sAttr); + return asRet; + + def initFromOther(self, oOther): + """ + Initialize this object with the values from another instance (child + class instance is accepted). + + This serves as a kind of copy constructor. + + Returns self. May raise exception if the type of other object differs + or is damaged. + """ + for sAttr in self.getDataAttributes(): + setattr(self, sAttr, getattr(oOther, sAttr)); + return self; + + @staticmethod + def getHungarianPrefix(sName): + """ + Returns the hungarian prefix of the given name. + """ + for i, _ in enumerate(sName): + if sName[i] not in ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', + 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z']: + assert re.search('^[A-Z][a-zA-Z0-9]*$', sName[i:]) is not None; + return sName[:i]; + return sName; + + def getAttributeParamNullValues(self, sAttr): + """ + Returns a list of parameter NULL values, with the preferred one being + the first element. + + Child classes can override this to handle one or more attributes specially. + """ + sPrefix = self.getHungarianPrefix(sAttr); + if sPrefix in ['id', 'uid', 'i', 'off', 'pct']: + return [-1, '', '-1',]; + if sPrefix in ['l', 'c',]: + return [long(-1), '', '-1',]; + if sPrefix == 'f': + return ['',]; + if sPrefix in ['enm', 'ip', 's', 'ts', 'uuid']: + return ['',]; + if sPrefix in ['ai', 'aid', 'al', 'as']: + return [[], '', None]; ## @todo ?? + if sPrefix == 'bm': + return ['', [],]; ## @todo bitmaps. + raise TMExceptionBase('Unable to classify "%s" (prefix %s)' % (sAttr, sPrefix)); + + def isAttributeNull(self, sAttr, oValue): + """ + Checks if the specified attribute value indicates NULL. + Return True/False. + + Note! This isn't entirely kosher actually. + """ + if oValue is None: + return True; + aoNilValues = self.getAttributeParamNullValues(sAttr); + return oValue in aoNilValues; + + def _convertAttributeFromParamNull(self, sAttr, oValue): + """ + Converts an attribute from parameter NULL to database NULL value. + Returns the new attribute value. + """ + aoNullValues = self.getAttributeParamNullValues(sAttr); + if oValue in aoNullValues: + oValue = None if sAttr not in self.kasAltArrayNull else []; + # + # Perform deep conversion on ModelDataBase object and lists of them. + # + elif isinstance(oValue, list) and oValue and isinstance(oValue[0], ModelDataBase): + oValue = copy.copy(oValue); + for i, _ in enumerate(oValue): + assert isinstance(oValue[i], ModelDataBase); + oValue[i] = copy.copy(oValue[i]); + oValue[i].convertFromParamNull(); + + elif isinstance(oValue, ModelDataBase): + oValue = copy.copy(oValue); + oValue.convertFromParamNull(); + + return oValue; + + def convertFromParamNull(self): + """ + Converts from parameter NULL values to database NULL values (None). + Returns self. + """ + for sAttr in self.getDataAttributes(): + oValue = getattr(self, sAttr); + oNewValue = self._convertAttributeFromParamNull(sAttr, oValue); + if oValue != oNewValue: + setattr(self, sAttr, oNewValue); + return self; + + def _convertAttributeToParamNull(self, sAttr, oValue): + """ + Converts an attribute from database NULL to a sepcial value we can pass + thru parameter list. + Returns the new attribute value. + """ + if oValue is None: + oValue = self.getAttributeParamNullValues(sAttr)[0]; + # + # Perform deep conversion on ModelDataBase object and lists of them. + # + elif isinstance(oValue, list) and oValue and isinstance(oValue[0], ModelDataBase): + oValue = copy.copy(oValue); + for i, _ in enumerate(oValue): + assert isinstance(oValue[i], ModelDataBase); + oValue[i] = copy.copy(oValue[i]); + oValue[i].convertToParamNull(); + + elif isinstance(oValue, ModelDataBase): + oValue = copy.copy(oValue); + oValue.convertToParamNull(); + + return oValue; + + def convertToParamNull(self): + """ + Converts from database NULL values (None) to special values we can + pass thru parameters list. + Returns self. + """ + for sAttr in self.getDataAttributes(): + oValue = getattr(self, sAttr); + oNewValue = self._convertAttributeToParamNull(sAttr, oValue); + if oValue != oNewValue: + setattr(self, sAttr, oNewValue); + return self; + + def _validateAndConvertAttribute(self, sAttr, sParam, oValue, aoNilValues, fAllowNull, oDb): + """ + Validates and convert one attribute. + Returns the converted value. + + Child classes can override this to handle one or more attributes specially. + Note! oDb can be None. + """ + sPrefix = self.getHungarianPrefix(sAttr); + + if sPrefix in ['id', 'uid']: + (oNewValue, sError) = self.validateInt( oValue, aoNilValues = aoNilValues, fAllowNull = fAllowNull); + elif sPrefix in ['i', 'off', 'pct']: + (oNewValue, sError) = self.validateInt( oValue, aoNilValues = aoNilValues, fAllowNull = fAllowNull, + iMin = getattr(self, 'kiMin_' + sAttr, 0), + iMax = getattr(self, 'kiMax_' + sAttr, 0x7ffffffe)); + elif sPrefix in ['l', 'c']: + (oNewValue, sError) = self.validateLong(oValue, aoNilValues = aoNilValues, fAllowNull = fAllowNull, + lMin = getattr(self, 'klMin_' + sAttr, 0), + lMax = getattr(self, 'klMax_' + sAttr, None)); + elif sPrefix == 'f': + if not oValue and not fAllowNull: oValue = '0'; # HACK ALERT! Checkboxes are only added when checked. + (oNewValue, sError) = self.validateBool(oValue, aoNilValues = aoNilValues, fAllowNull = fAllowNull); + elif sPrefix == 'ts': + (oNewValue, sError) = self.validateTs( oValue, aoNilValues = aoNilValues, fAllowNull = fAllowNull); + elif sPrefix == 'ip': + (oNewValue, sError) = self.validateIp( oValue, aoNilValues = aoNilValues, fAllowNull = fAllowNull); + elif sPrefix == 'uuid': + (oNewValue, sError) = self.validateUuid(oValue, aoNilValues = aoNilValues, fAllowNull = fAllowNull); + elif sPrefix == 'enm': + (oNewValue, sError) = self.validateWord(oValue, aoNilValues = aoNilValues, fAllowNull = fAllowNull, + asValid = getattr(self, 'kasValidValues_' + sAttr)); # The list is required. + elif sPrefix == 's': + (oNewValue, sError) = self.validateStr( oValue, aoNilValues = aoNilValues, fAllowNull = fAllowNull, + cchMin = getattr(self, 'kcchMin_' + sAttr, 0), + cchMax = getattr(self, 'kcchMax_' + sAttr, 4096), + fAllowUnicodeSymbols = getattr(self, 'kfAllowUnicode_' + sAttr, False) ); + ## @todo al. + elif sPrefix == 'aid': + (oNewValue, sError) = self.validateListOfInts(oValue, aoNilValues = aoNilValues, fAllowNull = fAllowNull, + iMin = 1, iMax = 0x7ffffffe); + elif sPrefix == 'as': + (oNewValue, sError) = self.validateListOfStr(oValue, aoNilValues = aoNilValues, fAllowNull = fAllowNull, + asValidValues = getattr(self, 'kasValidValues_' + sAttr, None), + cchMin = getattr(self, 'kcchMin_' + sAttr, 0 if fAllowNull else 1), + cchMax = getattr(self, 'kcchMax_' + sAttr, 4096)); + + elif sPrefix == 'bm': + ## @todo figure out bitfields. + (oNewValue, sError) = self.validateListOfStr(oValue, aoNilValues = aoNilValues, fAllowNull = fAllowNull); + else: + raise TMExceptionBase('Unable to classify "%s" (prefix %s)' % (sAttr, sPrefix)); + + _ = sParam; _ = oDb; + return (oNewValue, sError); + + def _validateAndConvertWorker(self, asAllowNullAttributes, oDb, enmValidateFor = ksValidateFor_Other): + """ + Worker for implementing validateAndConvert(). + """ + dErrors = {}; + for sAttr in self.getDataAttributes(): + oValue = getattr(self, sAttr); + sParam = getattr(self, 'ksParam_' + sAttr); + aoNilValues = self.getAttributeParamNullValues(sAttr); + aoNilValues.append(None); + + (oNewValue, sError) = self._validateAndConvertAttribute(sAttr, sParam, oValue, aoNilValues, + sAttr in asAllowNullAttributes, oDb); + if oValue != oNewValue: + setattr(self, sAttr, oNewValue); + if sError is not None: + dErrors[sParam] = sError; + + # Check the NULL requirements of the primary ID(s) for the 'add' and 'edit' actions. + if enmValidateFor in (ModelDataBase.ksValidateFor_Add, + ModelDataBase.ksValidateFor_AddForeignId, + ModelDataBase.ksValidateFor_Edit,): + fMustBeNull = enmValidateFor == ModelDataBase.ksValidateFor_Add; + sAttr = getattr(self, 'ksIdAttr', None); + if sAttr is not None: + oValue = getattr(self, sAttr); + if self.isAttributeNull(sAttr, oValue) != fMustBeNull: + sParam = getattr(self, 'ksParam_' + sAttr); + sErrMsg = 'Must be NULL!' if fMustBeNull else 'Must not be NULL!' + if sParam in dErrors: + dErrors[sParam] += ' ' + sErrMsg; + else: + dErrors[sParam] = sErrMsg; + + return dErrors; + + def validateAndConvert(self, oDb, enmValidateFor = ksValidateFor_Other): + """ + 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_*. + + Child classes can override _validateAndConvertAttribute to handle + selected fields specially. There are also a few class variables that + can be used to advice the validation: kcchMin_sAttr, kcchMax_sAttr, + kiMin_iAttr, kiMax_iAttr, klMin_lAttr, klMax_lAttr, + kasValidValues_enmAttr, and kasAllowNullAttributes. + """ + return self._validateAndConvertWorker(getattr(self, 'kasAllowNullAttributes', []), oDb, + enmValidateFor = enmValidateFor); + + def validateAndConvertEx(self, asAllowNullAttributes, oDb, enmValidateFor = ksValidateFor_Other): + """ + Same as validateAndConvert but with custom allow-null list. + """ + return self._validateAndConvertWorker(asAllowNullAttributes, oDb, enmValidateFor = enmValidateFor); + + def convertParamToAttribute(self, sAttr, sParam, oValue, oDisp, fStrict): + """ + Calculate the attribute value when initialized from a parameter. + + Returns the new value, with parameter NULL values. Raises exception on + invalid parameter value. + + Child classes can override to do special parameter conversion jobs. + """ + sPrefix = self.getHungarianPrefix(sAttr); + asValidValues = getattr(self, 'kasValidValues_' + sAttr, None); + fAllowNull = sAttr in getattr(self, 'kasAllowNullAttributes', []); + if fStrict: + if sPrefix == 'f': + # HACK ALERT! Checkboxes are only present when checked, so we always have to provide a default. + oNewValue = oDisp.getStringParam(sParam, asValidValues, '0'); + elif sPrefix[0] == 'a': + # HACK ALERT! Lists are not present if empty. + oNewValue = oDisp.getListOfStrParams(sParam, []); + else: + oNewValue = oDisp.getStringParam(sParam, asValidValues, None, fAllowNull = fAllowNull); + else: + if sPrefix[0] == 'a': + oNewValue = oDisp.getListOfStrParams(sParam, []); + else: + assert oValue is not None, 'sAttr=%s' % (sAttr,); + oNewValue = oDisp.getStringParam(sParam, asValidValues, oValue, fAllowNull = fAllowNull); + return oNewValue; + + def initFromParams(self, oDisp, fStrict = True): + """ + Initialize the object from parameters. + The input is not validated at all, except that all parameters must be + present when fStrict is True. + + Returns self. Raises exception on invalid parameter value. + + Note! The returned object has parameter NULL values, not database ones! + """ + + self.convertToParamNull() + for sAttr in self.getDataAttributes(): + oValue = getattr(self, sAttr); + oNewValue = self.convertParamToAttribute(sAttr, getattr(self, 'ksParam_' + sAttr), oValue, oDisp, fStrict); + if oNewValue != oValue: + setattr(self, sAttr, oNewValue); + return self; + + def areAttributeValuesEqual(self, sAttr, sPrefix, oValue1, oValue2): + """ + Called to compare two attribute values and python thinks differs. + + Returns True/False. + + Child classes can override this to do special compares of things like arrays. + """ + # Just in case someone uses it directly. + if oValue1 == oValue2: + return True; + + # + # Timestamps can be both string (param) and object (db) + # depending on the data source. Compare string values to make + # sure we're doing the right thing here. + # + if sPrefix == 'ts': + return str(oValue1) == str(oValue2); + + # + # Some generic code handling ModelDataBase children. + # + if isinstance(oValue1, list) and isinstance(oValue2, list): + if len(oValue1) == len(oValue2): + for i, _ in enumerate(oValue1): + if not isinstance(oValue1[i], ModelDataBase) \ + or type(oValue1) is not type(oValue2): + return False; + if not oValue1[i].isEqual(oValue2[i]): + return False; + return True; + + elif isinstance(oValue1, ModelDataBase) \ + and type(oValue1) is type(oValue2): + return oValue1[i].isEqual(oValue2[i]); + + _ = sAttr; + return False; + + def isEqual(self, oOther): + """ Compares two instances. """ + for sAttr in self.getDataAttributes(): + if getattr(self, sAttr) != getattr(oOther, sAttr): + # Delegate the final decision to an overridable method. + if not self.areAttributeValuesEqual(sAttr, self.getHungarianPrefix(sAttr), + getattr(self, sAttr), getattr(oOther, sAttr)): + return False; + return True; + + def isEqualEx(self, oOther, asExcludeAttrs): + """ Compares two instances, omitting the given attributes. """ + for sAttr in self.getDataAttributes(): + if sAttr not in asExcludeAttrs \ + and getattr(self, sAttr) != getattr(oOther, sAttr): + # Delegate the final decision to an overridable method. + if not self.areAttributeValuesEqual(sAttr, self.getHungarianPrefix(sAttr), + getattr(self, sAttr), getattr(oOther, sAttr)): + return False; + return True; + + def reinitToNull(self): + """ + Reinitializes the object to (database) NULL values. + Returns self. + """ + for sAttr in self.getDataAttributes(): + setattr(self, sAttr, None); + return self; + + def toString(self): + """ + Stringifies the object. + Returns string representation. + """ + + sMembers = ''; + for sAttr in self.getDataAttributes(): + oValue = getattr(self, sAttr); + sMembers += ', %s=%s' % (sAttr, oValue); + + oClass = type(self); + if sMembers == '': + return '<%s>' % (oClass.__name__); + return '<%s: %s>' % (oClass.__name__, sMembers[2:]); + + def __str__(self): + return self.toString(); + + + + # + # New validation helpers. + # + # These all return (oValue, sError), where sError is None when the value + # is valid and an error message when not. On success and in case of + # range errors, oValue is converted into the requested type. + # + + @staticmethod + def validateInt(sValue, iMin = 0, iMax = 0x7ffffffe, aoNilValues = tuple([-1, None, '']), fAllowNull = True): + """ Validates an integer field. """ + if sValue in aoNilValues: + if fAllowNull: + return (None if sValue is None else aoNilValues[0], None); + return (sValue, 'Mandatory.'); + + try: + if utils.isString(sValue): + iValue = int(sValue, 0); + else: + iValue = int(sValue); + except: + return (sValue, 'Not an integer'); + + if iValue in aoNilValues: + return (aoNilValues[0], None if fAllowNull else 'Mandatory.'); + + if iValue < iMin: + return (iValue, 'Value too small (min %d)' % (iMin,)); + if iValue > iMax: + return (iValue, 'Value too high (max %d)' % (iMax,)); + return (iValue, None); + + @staticmethod + def validateLong(sValue, lMin = 0, lMax = None, aoNilValues = tuple([long(-1), None, '']), fAllowNull = True): + """ Validates an long integer field. """ + if sValue in aoNilValues: + if fAllowNull: + return (None if sValue is None else aoNilValues[0], None); + return (sValue, 'Mandatory.'); + try: + if utils.isString(sValue): + lValue = long(sValue, 0); + else: + lValue = long(sValue); + except: + return (sValue, 'Not a long integer'); + + if lValue in aoNilValues: + return (aoNilValues[0], None if fAllowNull else 'Mandatory.'); + + if lMin is not None and lValue < lMin: + return (lValue, 'Value too small (min %d)' % (lMin,)); + if lMax is not None and lValue > lMax: + return (lValue, 'Value too high (max %d)' % (lMax,)); + return (lValue, None); + + kdTimestampRegex = { + len('2012-10-08 01:54:06'): r'(\d{4})-([01]\d)-([0123]\d)[ Tt]([012]\d):[0-5]\d:([0-6]\d)$', + len('2012-10-08 01:54:06.00'): r'(\d{4})-([01]\d)-([0123]\d)[ Tt]([012]\d):[0-5]\d:([0-6]\d).\d{2}$', + len('2012-10-08 01:54:06.000'): r'(\d{4})-([01]\d)-([0123]\d)[ Tt]([012]\d):[0-5]\d:([0-6]\d).\d{3}$', + len('999999-12-31 00:00:00.00'): r'(\d{6})-([01]\d)-([0123]\d)[ Tt]([012]\d):[0-5]\d:([0-6]\d).\d{2}$', + len('9999-12-31 23:59:59.999999'): r'(\d{4})-([01]\d)-([0123]\d)[ Tt]([012]\d):[0-5]\d:([0-6]\d).\d{6}$', + len('9999-12-31T23:59:59.999999999'): r'(\d{4})-([01]\d)-([0123]\d)[ Tt]([012]\d):[0-5]\d:([0-6]\d).\d{9}$', + }; + + @staticmethod + def validateTs(sValue, aoNilValues = tuple([None, '']), fAllowNull = True, fRelative = False): + """ Validates a timestamp field. """ + if sValue in aoNilValues: + return (sValue, None if fAllowNull else 'Mandatory.'); + if not utils.isString(sValue): + return (sValue, None); + + # Validate and strip off the timezone stuff. + if sValue[-1] in 'Zz': + sStripped = sValue[:-1]; + sValue = sStripped + 'Z'; + elif len(sValue) >= 19 + 3: + oRes = re.match(r'^.*[+-](\d\d):(\d\d)$', sValue); + if oRes is not None: + if int(oRes.group(1)) > 12 or int(oRes.group(2)) >= 60: + return (sValue, 'Invalid timezone offset.'); + sStripped = sValue[:-6]; + else: + sStripped = sValue; + else: + sStripped = sValue; + + # Used the stripped value length to find regular expression for validating and parsing the timestamp. + sError = None; + sRegExp = ModelDataBase.kdTimestampRegex.get(len(sStripped), None); + if sRegExp: + oRes = re.match(sRegExp, sStripped); + if oRes is not None: + iYear = int(oRes.group(1)); + if iYear % 4 == 0 and (iYear % 100 != 0 or iYear % 400 == 0): + acDaysOfMonth = [31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]; + else: + acDaysOfMonth = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]; + iMonth = int(oRes.group(2)); + iDay = int(oRes.group(3)); + iHour = int(oRes.group(4)); + iSec = int(oRes.group(5)); + if iMonth > 12 or (iMonth <= 0 and not fRelative): + sError = 'Invalid timestamp month.'; + elif iDay > acDaysOfMonth[iMonth - 1]: + sError = 'Invalid timestamp day-of-month (%02d has %d days).' % (iMonth, acDaysOfMonth[iMonth - 1]); + elif iHour > 23: + sError = 'Invalid timestamp hour.' + elif iSec >= 61: + sError = 'Invalid timestamp second.' + elif iSec >= 60: + sError = 'Invalid timestamp: no leap seconds, please.' + else: + sError = 'Invalid timestamp (validation regexp: %s).' % (sRegExp,); + else: + sError = 'Invalid timestamp length.'; + return (sValue, sError); + + @staticmethod + def validateIp(sValue, aoNilValues = tuple([None, '']), fAllowNull = True): + """ Validates an IP address field. """ + if sValue in aoNilValues: + return (sValue, None if fAllowNull else 'Mandatory.'); + + if sValue == '::1': + return (sValue, None); + + try: + socket.inet_pton(socket.AF_INET, sValue); # pylint: disable=no-member + except: + try: + socket.inet_pton(socket.AF_INET6, sValue); # pylint: disable=no-member + except: + return (sValue, 'Not a valid IP address.'); + + return (sValue, None); + + @staticmethod + def validateBool(sValue, aoNilValues = tuple([None, '']), fAllowNull = True): + """ Validates a boolean field. """ + if sValue in aoNilValues: + return (sValue, None if fAllowNull else 'Mandatory.'); + + if sValue in ('True', 'true', '1', True): + return (True, None); + if sValue in ('False', 'false', '0', False): + return (False, None); + return (sValue, 'Invalid boolean value.'); + + @staticmethod + def validateUuid(sValue, aoNilValues = tuple([None, '']), fAllowNull = True): + """ Validates an UUID field. """ + if sValue in aoNilValues: + return (sValue, None if fAllowNull else 'Mandatory.'); + + try: + sValue = str(uuid.UUID(sValue)); + except: + return (sValue, 'Invalid UUID value.'); + return (sValue, None); + + @staticmethod + def validateWord(sValue, cchMin = 1, cchMax = 64, asValid = None, aoNilValues = tuple([None, '']), fAllowNull = True): + """ Validates a word field. """ + if sValue in aoNilValues: + return (sValue, None if fAllowNull else 'Mandatory.'); + + if re.search('[^a-zA-Z0-9_-]', sValue) is not None: + sError = 'Single word ([a-zA-Z0-9_-]), please.'; + elif cchMin is not None and len(sValue) < cchMin: + sError = 'Too short, min %s chars' % (cchMin,); + elif cchMax is not None and len(sValue) > cchMax: + sError = 'Too long, max %s chars' % (cchMax,); + elif asValid is not None and sValue not in asValid: + sError = 'Invalid value "%s", must be one of: %s' % (sValue, asValid); + else: + sError = None; + return (sValue, sError); + + @staticmethod + def validateStr(sValue, cchMin = 0, cchMax = 4096, aoNilValues = tuple([None, '']), fAllowNull = True, + fAllowUnicodeSymbols = False): + """ Validates a string field. """ + if sValue in aoNilValues: + return (sValue, None if fAllowNull else 'Mandatory.'); + + if cchMin is not None and len(sValue) < cchMin: + sError = 'Too short, min %s chars' % (cchMin,); + elif cchMax is not None and len(sValue) > cchMax: + sError = 'Too long, max %s chars' % (cchMax,); + elif fAllowUnicodeSymbols is False and utils.hasNonAsciiCharacters(sValue): + sError = 'Non-ascii characters not allowed' + else: + sError = None; + return (sValue, sError); + + @staticmethod + def validateEmail(sValue, aoNilValues = tuple([None, '']), fAllowNull = True): + """ Validates a email field.""" + if sValue in aoNilValues: + return (sValue, None if fAllowNull else 'Mandatory.'); + + if re.match(r'.+@.+\..+', sValue) is None: + return (sValue,'Invalid e-mail format.'); + return (sValue, None); + + @staticmethod + def validateListOfSomething(asValues, aoNilValues = tuple([[], None]), fAllowNull = True): + """ Validate a list of some uniform values. Returns a copy of the list (if list it is). """ + if asValues in aoNilValues or (not asValues and not fAllowNull): + return (asValues, None if fAllowNull else 'Mandatory.') + + if not isinstance(asValues, list): + return (asValues, 'Invalid data type (%s).' % (type(asValues),)); + + asValues = list(asValues); # copy the list. + if asValues: + oType = type(asValues[0]); + for i in range(1, len(asValues)): + if type(asValues[i]) is not oType: # pylint: disable=unidiomatic-typecheck + return (asValues, 'Invalid entry data type ([0]=%s vs [%d]=%s).' % (oType, i, type(asValues[i])) ); + + return (asValues, None); + + @staticmethod + def validateListOfStr(asValues, cchMin = None, cchMax = None, asValidValues = None, + aoNilValues = tuple([[], None]), fAllowNull = True): + """ Validates a list of text items.""" + (asValues, sError) = ModelDataBase.validateListOfSomething(asValues, aoNilValues, fAllowNull); + + if sError is None and asValues not in aoNilValues and asValues: + if not utils.isString(asValues[0]): + return (asValues, 'Invalid item data type.'); + + if not fAllowNull and cchMin is None: + cchMin = 1; + + for sValue in asValues: + if asValidValues is not None and sValue not in asValidValues: + sThisErr = 'Invalid value "%s".' % (sValue,); + elif cchMin is not None and len(sValue) < cchMin: + sThisErr = 'Value "%s" is too short, min length is %u chars.' % (sValue, cchMin); + elif cchMax is not None and len(sValue) > cchMax: + sThisErr = 'Value "%s" is too long, max length is %u chars.' % (sValue, cchMax); + else: + continue; + + if sError is None: + sError = sThisErr; + else: + sError += ' ' + sThisErr; + + return (asValues, sError); + + @staticmethod + def validateListOfInts(asValues, iMin = 0, iMax = 0x7ffffffe, aoNilValues = tuple([[], None]), fAllowNull = True): + """ Validates a list of integer items.""" + (asValues, sError) = ModelDataBase.validateListOfSomething(asValues, aoNilValues, fAllowNull); + + if sError is None and asValues not in aoNilValues and asValues: + for i, _ in enumerate(asValues): + sValue = asValues[i]; + + sThisErr = ''; + try: + iValue = int(sValue); + except: + sThisErr = 'Invalid integer value "%s".' % (sValue,); + else: + asValues[i] = iValue; + if iValue < iMin: + sThisErr = 'Value %d is too small (min %d)' % (iValue, iMin,); + elif iValue > iMax: + sThisErr = 'Value %d is too high (max %d)' % (iValue, iMax,); + else: + continue; + + if sError is None: + sError = sThisErr; + else: + sError += ' ' + sThisErr; + + return (asValues, sError); + + + + # + # Old validation helpers. + # + + @staticmethod + def _validateInt(dErrors, sName, sValue, iMin = 0, iMax = 0x7ffffffe, aoNilValues = tuple([-1, None, ''])): + """ Validates an integer field. """ + (sValue, sError) = ModelDataBase.validateInt(sValue, iMin, iMax, aoNilValues, fAllowNull = True); + if sError is not None: + dErrors[sName] = sError; + return sValue; + + @staticmethod + def _validateIntNN(dErrors, sName, sValue, iMin = 0, iMax = 0x7ffffffe, aoNilValues = tuple([-1, None, ''])): + """ Validates an integer field, not null. """ + (sValue, sError) = ModelDataBase.validateInt(sValue, iMin, iMax, aoNilValues, fAllowNull = False); + if sError is not None: + dErrors[sName] = sError; + return sValue; + + @staticmethod + def _validateLong(dErrors, sName, sValue, lMin = 0, lMax = None, aoNilValues = tuple([long(-1), None, ''])): + """ Validates an long integer field. """ + (sValue, sError) = ModelDataBase.validateLong(sValue, lMin, lMax, aoNilValues, fAllowNull = False); + if sError is not None: + dErrors[sName] = sError; + return sValue; + + @staticmethod + def _validateLongNN(dErrors, sName, sValue, lMin = 0, lMax = None, aoNilValues = tuple([long(-1), None, ''])): + """ Validates an long integer field, not null. """ + (sValue, sError) = ModelDataBase.validateLong(sValue, lMin, lMax, aoNilValues, fAllowNull = True); + if sError is not None: + dErrors[sName] = sError; + return sValue; + + @staticmethod + def _validateTs(dErrors, sName, sValue): + """ Validates a timestamp field. """ + (sValue, sError) = ModelDataBase.validateTs(sValue, fAllowNull = True); + if sError is not None: + dErrors[sName] = sError; + return sValue; + + @staticmethod + def _validateTsNN(dErrors, sName, sValue): + """ Validates a timestamp field, not null. """ + (sValue, sError) = ModelDataBase.validateTs(sValue, fAllowNull = False); + if sError is not None: + dErrors[sName] = sError; + return sValue; + + @staticmethod + def _validateIp(dErrors, sName, sValue): + """ Validates an IP address field. """ + (sValue, sError) = ModelDataBase.validateIp(sValue, fAllowNull = True); + if sError is not None: + dErrors[sName] = sError; + return sValue; + + @staticmethod + def _validateIpNN(dErrors, sName, sValue): + """ Validates an IP address field, not null. """ + (sValue, sError) = ModelDataBase.validateIp(sValue, fAllowNull = False); + if sError is not None: + dErrors[sName] = sError; + return sValue; + + @staticmethod + def _validateBool(dErrors, sName, sValue): + """ Validates a boolean field. """ + (sValue, sError) = ModelDataBase.validateBool(sValue, fAllowNull = True); + if sError is not None: + dErrors[sName] = sError; + return sValue; + + @staticmethod + def _validateBoolNN(dErrors, sName, sValue): + """ Validates a boolean field, not null. """ + (sValue, sError) = ModelDataBase.validateBool(sValue, fAllowNull = False); + if sError is not None: + dErrors[sName] = sError; + return sValue; + + @staticmethod + def _validateUuid(dErrors, sName, sValue): + """ Validates an UUID field. """ + (sValue, sError) = ModelDataBase.validateUuid(sValue, fAllowNull = True); + if sError is not None: + dErrors[sName] = sError; + return sValue; + + @staticmethod + def _validateUuidNN(dErrors, sName, sValue): + """ Validates an UUID field, not null. """ + (sValue, sError) = ModelDataBase.validateUuid(sValue, fAllowNull = False); + if sError is not None: + dErrors[sName] = sError; + return sValue; + + @staticmethod + def _validateWord(dErrors, sName, sValue, cchMin = 1, cchMax = 64, asValid = None): + """ Validates a word field. """ + (sValue, sError) = ModelDataBase.validateWord(sValue, cchMin, cchMax, asValid, fAllowNull = True); + if sError is not None: + dErrors[sName] = sError; + return sValue; + + @staticmethod + def _validateWordNN(dErrors, sName, sValue, cchMin = 1, cchMax = 64, asValid = None): + """ Validates a boolean field, not null. """ + (sValue, sError) = ModelDataBase.validateWord(sValue, cchMin, cchMax, asValid, fAllowNull = False); + if sError is not None: + dErrors[sName] = sError; + return sValue; + + @staticmethod + def _validateStr(dErrors, sName, sValue, cchMin = 0, cchMax = 4096): + """ Validates a string field. """ + (sValue, sError) = ModelDataBase.validateStr(sValue, cchMin, cchMax, fAllowNull = True); + if sError is not None: + dErrors[sName] = sError; + return sValue; + + @staticmethod + def _validateStrNN(dErrors, sName, sValue, cchMin = 0, cchMax = 4096): + """ Validates a string field, not null. """ + (sValue, sError) = ModelDataBase.validateStr(sValue, cchMin, cchMax, fAllowNull = False); + if sError is not None: + dErrors[sName] = sError; + return sValue; + + @staticmethod + def _validateEmail(dErrors, sName, sValue): + """ Validates a email field.""" + (sValue, sError) = ModelDataBase.validateEmail(sValue, fAllowNull = True); + if sError is not None: + dErrors[sName] = sError; + return sValue; + + @staticmethod + def _validateEmailNN(dErrors, sName, sValue): + """ Validates a email field.""" + (sValue, sError) = ModelDataBase.validateEmail(sValue, fAllowNull = False); + if sError is not None: + dErrors[sName] = sError; + return sValue; + + @staticmethod + def _validateListOfStr(dErrors, sName, asValues, asValidValues = None): + """ Validates a list of text items.""" + (sValue, sError) = ModelDataBase.validateListOfStr(asValues, asValidValues = asValidValues, fAllowNull = True); + if sError is not None: + dErrors[sName] = sError; + return sValue; + + @staticmethod + def _validateListOfStrNN(dErrors, sName, asValues, asValidValues = None): + """ Validates a list of text items, not null and len >= 1.""" + (sValue, sError) = ModelDataBase.validateListOfStr(asValues, asValidValues = asValidValues, fAllowNull = False); + if sError is not None: + dErrors[sName] = sError; + return sValue; + + # + # Various helpers. + # + + @staticmethod + def formatSimpleNowAndPeriod(oDb, tsNow = None, sPeriodBack = None, + sTablePrefix = '', sExpCol = 'tsExpire', sEffCol = 'tsEffective'): + """ + Formats a set of tsNow and sPeriodBack arguments for a standard testmanager + table. + + If sPeriodBack is given, the query is effective for the period + (tsNow - sPeriodBack) thru (tsNow). + + If tsNow isn't given, it defaults to current time. + + Returns the final portion of a WHERE query (start with AND) and maybe an + ORDER BY and LIMIT bit if sPeriodBack is given. + """ + if tsNow is not None: + if sPeriodBack is not None: + sRet = oDb.formatBindArgs(' AND ' + sTablePrefix + sExpCol + ' > (%s::timestamp - %s::interval)\n' + ' AND tsEffective <= %s\n' + 'ORDER BY ' + sTablePrefix + sExpCol + ' DESC\n' + 'LIMIT 1\n' + , ( tsNow, sPeriodBack, tsNow)); + else: + sRet = oDb.formatBindArgs(' AND ' + sTablePrefix + sExpCol + ' > %s\n' + ' AND ' + sTablePrefix + sEffCol + ' <= %s\n' + , ( tsNow, tsNow, )); + else: + if sPeriodBack is not None: + sRet = oDb.formatBindArgs(' AND ' + sTablePrefix + sExpCol + ' > (CURRENT_TIMESTAMP - %s::interval)\n' + ' AND ' + sTablePrefix + sEffCol + ' <= CURRENT_TIMESTAMP\n' + 'ORDER BY ' + sTablePrefix + sExpCol + ' DESC\n' + 'LIMIT 1\n' + , ( sPeriodBack, )); + else: + sRet = ' AND ' + sTablePrefix + sExpCol + ' = \'infinity\'::timestamp\n'; + return sRet; + + @staticmethod + def formatSimpleNowAndPeriodQuery(oDb, sQuery, aBindArgs, tsNow = None, sPeriodBack = None, + sTablePrefix = '', sExpCol = 'tsExpire', sEffCol = 'tsEffective'): + """ + Formats a simple query for a standard testmanager table with optional + tsNow and sPeriodBack arguments. + + The sQuery and sBindArgs are passed along to oDb.formatBindArgs to form + the first part of the query. Must end with an open WHERE statement as + we'll be adding the time part starting with 'AND something...'. + + See formatSimpleNowAndPeriod for tsNow and sPeriodBack description. + + Returns the final portion of a WHERE query (start with AND) and maybe an + ORDER BY and LIMIT bit if sPeriodBack is given. + + """ + return oDb.formatBindArgs(sQuery, aBindArgs) \ + + ModelDataBase.formatSimpleNowAndPeriod(oDb, tsNow, sPeriodBack, sTablePrefix, sExpCol, sEffCol); + + + # + # JSON + # + + @staticmethod + def stringToJson(sString): + """ Converts a string to a JSON value string. """ + if not utils.isString(sString): + sString = utils.toUnicode(sString); + if not utils.isString(sString): + sString = str(sString); + return json.dumps(sString); + + @staticmethod + def dictToJson(dDict, dOptions = None): + """ Converts a dictionary to a JSON string. """ + sJson = u'{ '; + for i, oKey in enumerate(dDict): + if i > 0: + sJson += ', '; + sJson += '%s: %s' % (ModelDataBase.stringToJson(oKey), + ModelDataBase.genericToJson(dDict[oKey], dOptions)); + return sJson + ' }'; + + @staticmethod + def listToJson(aoList, dOptions = None): + """ Converts list of something to a JSON string. """ + sJson = u'[ '; + for i, oValue in enumerate(aoList): + if i > 0: + sJson += u', '; + sJson += ModelDataBase.genericToJson(oValue, dOptions); + return sJson + u' ]'; + + @staticmethod + def datetimeToJson(oDateTime): + """ Converts a datetime instance to a JSON string. """ + return '"%s"' % (oDateTime,); + + + @staticmethod + def genericToJson(oValue, dOptions = None): + """ Converts a generic object to a JSON string. """ + if isinstance(oValue, ModelDataBase): + return oValue.toJson(); + if isinstance(oValue, dict): + return ModelDataBase.dictToJson(oValue, dOptions); + if isinstance(oValue, (list, tuple, set, frozenset)): + return ModelDataBase.listToJson(oValue, dOptions); + if isinstance(oValue, datetime.datetime): + return ModelDataBase.datetimeToJson(oValue) + return json.dumps(oValue); + + def attribValueToJson(self, sAttr, oValue, dOptions = None): + """ + Converts the attribute value to JSON. + Returns JSON (string). + """ + _ = sAttr; + return self.genericToJson(oValue, dOptions); + + def toJson(self, dOptions = None): + """ + Converts the object to JSON. + Returns JSON (string). + """ + sJson = u'{ '; + for iAttr, sAttr in enumerate(self.getDataAttributes()): + oValue = getattr(self, sAttr); + if iAttr > 0: + sJson += ', '; + sJson += u'"%s": ' % (sAttr,); + sJson += self.attribValueToJson(sAttr, oValue, dOptions); + return sJson + u' }'; + + + # + # Sub-classes. + # + + class DispWrapper(object): + """Proxy object.""" + def __init__(self, oDisp, sAttrFmt): + self.oDisp = oDisp; + self.sAttrFmt = sAttrFmt; + def getStringParam(self, sName, asValidValues = None, sDefault = None, fAllowNull = False): + """See WuiDispatcherBase.getStringParam.""" + return self.oDisp.getStringParam(self.sAttrFmt % (sName,), asValidValues, sDefault, fAllowNull = fAllowNull); + def getListOfStrParams(self, sName, asDefaults = None): + """See WuiDispatcherBase.getListOfStrParams.""" + return self.oDisp.getListOfStrParams(self.sAttrFmt % (sName,), asDefaults); + def getListOfIntParams(self, sName, iMin = None, iMax = None, aiDefaults = None): + """See WuiDispatcherBase.getListOfIntParams.""" + return self.oDisp.getListOfIntParams(self.sAttrFmt % (sName,), iMin, iMax, aiDefaults); + + + + +# pylint: disable=no-member,missing-docstring,too-few-public-methods +class ModelDataBaseTestCase(unittest.TestCase): + """ + Base testcase for ModelDataBase decendants. + Derive from this and override setUp. + """ + + def setUp(self): + """ + Override this! Don't call super! + The subclasses are expected to set aoSamples to an array of instance + samples. The first entry must be a default object, the subsequent ones + are optional and their contents freely choosen. + """ + self.aoSamples = [ModelDataBase(),]; + + def testEquality(self): + for oSample in self.aoSamples: + self.assertEqual(oSample.isEqual(copy.copy(oSample)), True); + self.assertIsNotNone(oSample.isEqual(self.aoSamples[0])); + + def testNullConversion(self): + if not self.aoSamples[0].getDataAttributes(): + return; + for oSample in self.aoSamples: + oCopy = copy.copy(oSample); + self.assertEqual(oCopy.convertToParamNull(), oCopy); + self.assertEqual(oCopy.isEqual(oSample), False); + self.assertEqual(oCopy.convertFromParamNull(), oCopy); + self.assertEqual(oCopy.isEqual(oSample), True, '\ngot : %s\nexpected: %s' % (oCopy, oSample,)); + + oCopy = copy.copy(oSample); + self.assertEqual(oCopy.convertToParamNull(), oCopy); + oCopy2 = copy.copy(oCopy); + self.assertEqual(oCopy.convertToParamNull(), oCopy); + self.assertEqual(oCopy.isEqual(oCopy2), True); + self.assertEqual(oCopy.convertToParamNull(), oCopy); + self.assertEqual(oCopy.isEqual(oCopy2), True); + + oCopy = copy.copy(oSample); + self.assertEqual(oCopy.convertFromParamNull(), oCopy); + oCopy2 = copy.copy(oCopy); + self.assertEqual(oCopy.convertFromParamNull(), oCopy); + self.assertEqual(oCopy.isEqual(oCopy2), True); + self.assertEqual(oCopy.convertFromParamNull(), oCopy); + self.assertEqual(oCopy.isEqual(oCopy2), True); + + def testReinitToNull(self): + oFirst = copy.copy(self.aoSamples[0]); + self.assertEqual(oFirst.reinitToNull(), oFirst); + for oSample in self.aoSamples: + oCopy = copy.copy(oSample); + self.assertEqual(oCopy.reinitToNull(), oCopy); + self.assertEqual(oCopy.isEqual(oFirst), True); + + def testValidateAndConvert(self): + for oSample in self.aoSamples: + oCopy = copy.copy(oSample); + oCopy.convertToParamNull(); + dError1 = oCopy.validateAndConvert(None); + + oCopy2 = copy.copy(oCopy); + self.assertEqual(oCopy.validateAndConvert(None), dError1); + self.assertEqual(oCopy.isEqual(oCopy2), True); + + def testInitFromParams(self): + class DummyDisp(object): + def getStringParam(self, sName, asValidValues = None, sDefault = None, fAllowNull = False): + _ = sName; _ = asValidValues; _ = fAllowNull; + return sDefault; + def getListOfStrParams(self, sName, asDefaults = None): + _ = sName; + return asDefaults; + def getListOfIntParams(self, sName, iMin = None, iMax = None, aiDefaults = None): + _ = sName; _ = iMin; _ = iMax; + return aiDefaults; + + for oSample in self.aoSamples: + oCopy = copy.copy(oSample); + self.assertEqual(oCopy.initFromParams(DummyDisp(), fStrict = False), oCopy); + + def testToString(self): + for oSample in self.aoSamples: + self.assertIsNotNone(oSample.toString()); + + +class FilterCriterionValueAndDescription(object): + """ + A filter criterion value and its description. + """ + + def __init__(self, oValue, sDesc, cTimes = None, sHover = None, fIrrelevant = False): + self.oValue = oValue; ##< Typically the ID of something in the database. + self.sDesc = sDesc; ##< What to display. + self.cTimes = cTimes; ##< Number of times the value occurs in the result set. None if not given. + self.sHover = sHover; ##< Optional hover/title string. + self.fIrrelevant = fIrrelevant; ##< Irrelevant filter option, only present because it's selected + self.aoSubs = []; ##< References to FilterCriterion.oSub.aoPossible. + + +class FilterCriterion(object): + """ + A filter criterion. + """ + + ## @name The state. + ## @{ + ksState_NotSelected = 'not-selected'; + ksState_Selected = 'selected'; + ## @} + + ## @name The kind of filtering. + ## @{ + ## 'Element of' by default, 'not an element of' when fInverted is False. + ksKind_ElementOfOrNot = 'element-of-or-not'; + ## The criterion is a special one and cannot be inverted. + ksKind_Special = 'special'; + ## @} + + ## @name The value type. + ## @{ + ksType_UInt = 'uint'; ##< unsigned integer value. + ksType_UIntNil = 'uint-nil'; ##< unsigned integer value, with nil. + ksType_String = 'string'; ##< string value. + ksType_Ranges = 'ranges'; ##< List of (unsigned) integer ranges. + ## @} + + def __init__(self, sName, sVarNm = None, sType = ksType_UInt, # pylint: disable=too-many-arguments + sState = ksState_NotSelected, sKind = ksKind_ElementOfOrNot, + sTable = None, sColumn = None, asTables = None, oSub = None): + assert len(sVarNm) == 2; # required by wuimain.py for filtering. + self.sName = sName; + self.sState = sState; + self.sType = sType; + self.sKind = sKind; + self.sVarNm = sVarNm; + self.aoSelected = []; ##< User input from sVarNm. Single value, type according to sType. + self.sInvVarNm = 'i' + sVarNm if sKind == self.ksKind_ElementOfOrNot else None; + self.fInverted = False; ##< User input from sInvVarNm. Inverts the operation (-> not an element of). + self.aoPossible = []; ##< type: list[FilterCriterionValueAndDescription] + assert (sTable is None and asTables is None) or ((sTable is not None) != (asTables is not None)), \ + '%s %s' % (sTable, asTables); + self.asTables = [sTable,] if sTable is not None else asTables; + assert sColumn is None or len(self.asTables) == 1, '%s %s' % (self.asTables, sColumn); + self.sColumn = sColumn; ##< Normally only applicable if one table. + self.fExpanded = None; ##< Tristate (None, False, True) + self.oSub = oSub; ##< type: FilterCriterion + + +class ModelFilterBase(ModelBase): + """ + Base class for filters. + + Filters are used to narrow down data that is displayed in a list or + report. This class differs a little from ModelDataBase in that it is not + tied to a database table, but one or more database queries that are + typically rather complicated. + + The filter object has two roles: + + 1. It is used by a ModelLogicBase descendant to store the available + filtering options for data begin displayed. + + 2. It decodes and stores the filtering options submitted by the user so + a ModeLogicBase descendant can use it to construct WHERE statements. + + The ModelFilterBase class is related to the ModelDataBase class in that it + decodes user parameters and stores data, however it is not a descendant. + + Note! In order to reduce URL lengths, we use very very brief parameter + names for the filters. + """ + + def __init__(self): + ModelBase.__init__(self); + self.aCriteria = [] # type: list[FilterCriterion] + + def _initFromParamsWorker(self, oDisp, oCriterion): # (,FilterCriterion) + """ Worker for initFromParams. """ + if oCriterion.sType == FilterCriterion.ksType_UInt: + oCriterion.aoSelected = oDisp.getListOfIntParams(oCriterion.sVarNm, iMin = 0, aiDefaults = []); + elif oCriterion.sType == FilterCriterion.ksType_UIntNil: + oCriterion.aoSelected = oDisp.getListOfIntParams(oCriterion.sVarNm, iMin = -1, aiDefaults = []); + elif oCriterion.sType == FilterCriterion.ksType_String: + oCriterion.aoSelected = oDisp.getListOfStrParams(oCriterion.sVarNm, asDefaults = []); + if len(oCriterion.aoSelected) > 100: + raise TMExceptionBase('Variable %s has %u value, max allowed is 100!' + % (oCriterion.sVarNm, len(oCriterion.aoSelected))); + for sValue in oCriterion.aoSelected: + if len(sValue) > 64 \ + or '\'' in sValue \ + or sValue[-1] == '\\': + raise TMExceptionBase('Variable %s has an illegal value "%s"!' % (oCriterion.sVarNm, sValue)); + elif oCriterion.sType == FilterCriterion.ksType_Ranges: + def convertRangeNumber(sValue): + """ Helper """ + sValue = sValue.strip(); + if sValue and sValue not in ('inf', 'Inf', 'INf', 'INF', 'InF', 'iNf', 'iNF', 'inF',): + try: return int(sValue); + except: pass; + return None; + + for sRange in oDisp.getStringParam(oCriterion.sVarNm, sDefault = '').split(','): + sRange = sRange.strip(); + if sRange and sRange != '-' and any(ch.isdigit() for ch in sRange): + asValues = sRange.split('-'); + if len(asValues) == 1: + asValues = [asValues[0], asValues[0]]; + elif len(asValues) > 2: + asValues = [asValues[0], asValues[-1]]; + tTuple = (convertRangeNumber(asValues[0]), convertRangeNumber(asValues[1])); + if tTuple[0] is not None and tTuple[1] is not None and tTuple[0] > tTuple[1]: + tTuple = (tTuple[1], tTuple[0]); + oCriterion.aoSelected.append(tTuple); + else: + assert False; + if oCriterion.aoSelected: + oCriterion.sState = FilterCriterion.ksState_Selected; + else: + oCriterion.sState = FilterCriterion.ksState_NotSelected; + + if oCriterion.sKind == FilterCriterion.ksKind_ElementOfOrNot: + oCriterion.fInverted = oDisp.getBoolParam(oCriterion.sInvVarNm, fDefault = False); + + if oCriterion.oSub is not None: + self._initFromParamsWorker(oDisp, oCriterion.oSub); + return; + + def initFromParams(self, oDisp): # type: (WuiDispatcherBase) -> self + """ + Initialize the object from parameters. + + Returns self. Raises exception on invalid parameter value. + """ + + for oCriterion in self.aCriteria: + self._initFromParamsWorker(oDisp, oCriterion); + return self; + + def strainParameters(self, dParams, aAdditionalParams = None): + """ Filters just the parameters relevant to this filter, returning a copy. """ + + # Collect the parameter names. + dWanted = {}; + for oCrit in self.aCriteria: + dWanted[oCrit.sVarNm] = 1; + if oCrit.sInvVarNm: + dWanted[oCrit.sInvVarNm] = 1; + + # Add additional stuff. + if aAdditionalParams: + for sParam in aAdditionalParams: + dWanted[sParam] = 1; + + # To the straining. + dRet = {}; + for sKey in dParams: + if sKey in dWanted: + dRet[sKey] = dParams[sKey]; + return dRet; + + +class ModelLogicBase(ModelBase): # pylint: disable=too-few-public-methods + """ + Something all classes in the logic classes the logical model inherits from. + """ + + def __init__(self, oDb): + ModelBase.__init__(self); + + # + # Note! Do not create a connection here if None, we need to DB share + # connection with all other logic objects so we can perform half + # complex transactions involving several logic objects. + # + self._oDb = oDb; + + def getDbConnection(self): + """ + Gets the database connection. + This should only be used for instantiating other ModelLogicBase children. + """ + return self._oDb; + + def _dbRowsToModelDataList(self, oModelDataType, aaoRows = None): + """ + Helper for conerting a simple fetch into a list of ModelDataType python objects. + + If aaoRows is None, we'll fetchAll from the database ourselves. + + The oModelDataType must be a class derived from ModelDataBase and implement + the initFormDbRow method. + + Returns a list of oModelDataType instances. + """ + assert issubclass(oModelDataType, ModelDataBase); + aoRet = []; + if aaoRows is None: + aaoRows = self._oDb.fetchAll(); + for aoRow in aaoRows: + aoRet.append(oModelDataType().initFromDbRow(aoRow)); + return aoRet; + + + +class AttributeChangeEntry(object): # pylint: disable=too-few-public-methods + """ + Data class representing the changes made to one attribute. + """ + + def __init__(self, sAttr, oNewRaw, oOldRaw, sNewText, sOldText): + self.sAttr = sAttr; + self.oNewRaw = oNewRaw; + self.oOldRaw = oOldRaw; + self.sNewText = sNewText; + self.sOldText = sOldText; + +class AttributeChangeEntryPre(AttributeChangeEntry): # pylint: disable=too-few-public-methods + """ + AttributeChangeEntry for preformatted values. + """ + + def __init__(self, sAttr, oNewRaw, oOldRaw, sNewText, sOldText): + AttributeChangeEntry.__init__(self, sAttr, oNewRaw, oOldRaw, sNewText, sOldText); + +class ChangeLogEntry(object): # pylint: disable=too-few-public-methods + """ + A change log entry returned by the fetchChangeLog method typically + implemented by ModelLogicBase child classes. + """ + + def __init__(self, uidAuthor, sAuthor, tsEffective, tsExpire, oNewRaw, oOldRaw, aoChanges): + self.uidAuthor = uidAuthor; + self.sAuthor = sAuthor; + self.tsEffective = tsEffective; + self.tsExpire = tsExpire; + self.oNewRaw = oNewRaw; + self.oOldRaw = oOldRaw; # Note! NULL for the last entry. + self.aoChanges = aoChanges; + 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. + diff --git a/src/VBox/ValidationKit/testmanager/core/buildblacklist.py b/src/VBox/ValidationKit/testmanager/core/buildblacklist.py new file mode 100755 index 00000000..487fb9f0 --- /dev/null +++ b/src/VBox/ValidationKit/testmanager/core/buildblacklist.py @@ -0,0 +1,324 @@ +# -*- coding: utf-8 -*- +# $Id: buildblacklist.py $ + +""" +Test Manager - Builds Blacklist. +""" + +__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 $" + + +# Validation Kit imports. +from testmanager.core.base import ModelDataBase, ModelLogicBase, TMInvalidData, TMRowNotFound; + + +class BuildBlacklistData(ModelDataBase): + """ + Build Blacklist Data. + """ + + ksIdAttr = 'idBlacklisting'; + + ksParam_idBlacklisting = 'BuildBlacklist_idBlacklisting' + ksParam_tsEffective = 'BuildBlacklist_tsEffective' + ksParam_tsExpire = 'BuildBlacklist_tsExpire' + ksParam_uidAuthor = 'BuildBlacklist_uidAuthor' + ksParam_idFailureReason = 'BuildBlacklist_idFailureReason' + ksParam_sProduct = 'BuildBlacklist_sProduct' + ksParam_sBranch = 'BuildBlacklist_sBranch' + ksParam_asTypes = 'BuildBlacklist_asTypes' + ksParam_asOsArches = 'BuildBlacklist_asOsArches' + ksParam_iFirstRevision = 'BuildBlacklist_iFirstRevision' + ksParam_iLastRevision = 'BuildBlacklist_iLastRevision' + + kasAllowNullAttributes = [ 'idBlacklisting', + 'tsEffective', + 'tsExpire', + 'uidAuthor', + 'asTypes', + 'asOsArches' ]; + + def __init__(self): + ModelDataBase.__init__(self); + + # + # Initialize with defaults. + # See the database for explanations of each of these fields. + # + self.idBlacklisting = None + self.tsEffective = None + self.tsExpire = None + self.uidAuthor = None + self.idFailureReason = None + self.sProduct = None + self.sBranch = None + self.asTypes = None + self.asOsArches = None + self.iFirstRevision = None + self.iLastRevision = None + + def initFromDbRow(self, aoRow): + """ + Re-initializes the data with a row from a SELECT * FROM BuildBlacklist. + + Returns self. Raises exception if the row is None or otherwise invalid. + """ + + if aoRow is None: + raise TMRowNotFound('Build Blacklist item not found.') + + self.idBlacklisting = aoRow[0] + self.tsEffective = aoRow[1] + self.tsExpire = aoRow[2] + self.uidAuthor = aoRow[3] + self.idFailureReason = aoRow[4] + self.sProduct = aoRow[5] + self.sBranch = aoRow[6] + self.asTypes = aoRow[7] + self.asOsArches = aoRow[8] + self.iFirstRevision = aoRow[9] + self.iLastRevision = aoRow[10] + + return self; + + def initFromDbWithId(self, oDb, idBlacklisting, tsNow = None, sPeriodBack = None): + """ + Initialize the object from the database. + """ + oDb.execute(self.formatSimpleNowAndPeriodQuery(oDb, + 'SELECT *\n' + 'FROM BuildBlacklist\n' + 'WHERE idBlacklisting = %s\n' + , ( idBlacklisting,), tsNow, sPeriodBack)); + aoRow = oDb.fetchOne() + if aoRow is None: + raise TMRowNotFound('idBlacklisting=%s not found (tsNow=%s sPeriodBack=%s)' + % (idBlacklisting, tsNow, sPeriodBack,)); + return self.initFromDbRow(aoRow); + + +class BuildBlacklistLogic(ModelLogicBase): # pylint: disable=too-few-public-methods + """ + Build Back List logic. + """ + + def __init__(self, oDb): + ModelLogicBase.__init__(self, oDb) + self.dCache = None; + + def fetchForListing(self, iStart, cMaxRows, tsNow, aiSortColumns = None): + """ + Fetches Build Blacklist records. + + Returns an array (list) of BuildBlacklistData items, empty list if none. + Raises exception on error. + """ + _ = aiSortColumns; + + if tsNow is None: + self._oDb.execute('SELECT *\n' + 'FROM BuildBlacklist\n' + 'WHERE tsExpire = \'infinity\'::TIMESTAMP\n' + 'ORDER BY idBlacklisting DESC\n' + 'LIMIT %s OFFSET %s\n' + , (cMaxRows, iStart,)); + else: + self._oDb.execute('SELECT *\n' + 'FROM BuildBlacklist\n' + 'WHERE tsExpire > %s\n' + ' AND tsEffective <= %s\n' + 'ORDER BY idBlacklisting DESC\n' + 'LIMIT %s OFFSET %s\n' + , (tsNow, tsNow, cMaxRows, iStart,)); + + aoRows = [] + for aoRow in self._oDb.fetchAll(): + aoRows.append(BuildBlacklistData().initFromDbRow(aoRow)) + return aoRows + + def addEntry(self, oData, uidAuthor, fCommit = False): + """ + Adds a blacklisting to the database. + """ + self._oDb.execute('INSERT INTO BuildBlacklist (\n' + ' uidAuthor,\n' + ' idFailureReason,\n' + ' sProduct,\n' + ' sBranch,\n' + ' asTypes,\n' + ' asOsArches,\n' + ' iFirstRevision,\n' + ' iLastRevision)\n' + 'VALUES (%s, %s, %s, %s, %s, %s, %s, %s)' + , ( uidAuthor, + oData.idFailureReason, + oData.sProduct, + oData.sBranch, + oData.asTypes, + oData.asOsArches, + oData.iFirstRevision, + oData.iLastRevision,) ); + self._oDb.maybeCommit(fCommit); + return True + + def editEntry(self, oData, uidAuthor, fCommit = False): + """ + Modifies a blacklisting. + """ + + # + # Validate inputs and read in the old(/current) data. + # + assert isinstance(oData, BuildBlacklistData); + dErrors = oData.validateAndConvert(self._oDb, oData.ksValidateFor_Edit); + if dErrors: + raise TMInvalidData('editEntry invalid input: %s' % (dErrors,)); + + oOldData = BuildBlacklistData().initFromDbWithId(self._oDb, oData.idBlacklisting); + + # + # Update the data that needs updating. + # + if not oData.isEqualEx(oOldData, [ 'tsEffective', 'tsExpire', 'uidAuthor', ]): + self._historizeEntry(oData.idBlacklisting, None); + self._readdEntry(uidAuthor, oData, None); + self._oDb.maybeCommit(fCommit); + return True; + + + def removeEntry(self, uidAuthor, idBlacklisting, fCascade = False, fCommit = False): + """ + Deletes a test group. + """ + _ = fCascade; # Not applicable. + + oData = BuildBlacklistData().initFromDbWithId(self._oDb, idBlacklisting); + + (tsCur, tsCurMinusOne) = self._oDb.getCurrentTimestamps(); + if oData.tsEffective not in (tsCur, tsCurMinusOne): + self._historizeEntry(idBlacklisting, tsCurMinusOne); + self._readdEntry(uidAuthor, oData, tsCurMinusOne); + self._historizeEntry(idBlacklisting); + self._oDb.execute('UPDATE BuildBlacklist\n' + 'SET tsExpire = CURRENT_TIMESTAMP\n' + 'WHERE idBlacklisting = %s\n' + ' AND tsExpire = \'infinity\'::TIMESTAMP\n' + , (idBlacklisting,)); + self._oDb.maybeCommit(fCommit); + return True; + + + def cachedLookup(self, idBlacklisting): + """ + Looks up the most recent BuildBlacklistData object for idBlacklisting + via an object cache. + + Returns a shared BuildBlacklistData object. None if not found. + Raises exception on DB error. + """ + if self.dCache is None: + self.dCache = self._oDb.getCache('BuildBlacklistData'); + oEntry = self.dCache.get(idBlacklisting, None); + if oEntry is None: + self._oDb.execute('SELECT *\n' + 'FROM BuildBlacklist\n' + 'WHERE idBlacklisting = %s\n' + ' AND tsExpire = \'infinity\'::TIMESTAMP\n' + , (idBlacklisting, )); + if self._oDb.getRowCount() == 0: + # Maybe it was deleted, try get the last entry. + self._oDb.execute('SELECT *\n' + 'FROM BuildBlacklist\n' + 'WHERE idBlacklisting = %s\n' + 'ORDER BY tsExpire DESC\n' + 'LIMIT 1\n' + , (idBlacklisting, )); + elif self._oDb.getRowCount() > 1: + raise self._oDb.integrityException('%s infinity rows for %s' % (self._oDb.getRowCount(), idBlacklisting)); + + if self._oDb.getRowCount() == 1: + aaoRow = self._oDb.fetchOne(); + oEntry = BuildBlacklistData(); + oEntry.initFromDbRow(aaoRow); + self.dCache[idBlacklisting] = oEntry; + return oEntry; + + + # + # Helpers. + # + + def _historizeEntry(self, idBlacklisting, tsExpire = None): + """ + Historizes the current entry for the given backlisting. + """ + if tsExpire is None: + tsExpire = self._oDb.getCurrentTimestamp(); + self._oDb.execute('UPDATE BuildBlacklist\n' + 'SET tsExpire = %s\n' + 'WHERE idBlacklisting = %s\n' + ' AND tsExpire = \'infinity\'::TIMESTAMP\n' + , ( tsExpire, idBlacklisting, )); + return True; + + def _readdEntry(self, uidAuthor, oData, tsEffective = None): + """ + Re-adds the BuildBlacklist entry. Used by editEntry and removeEntry. + """ + if tsEffective is None: + tsEffective = self._oDb.getCurrentTimestamp(); + self._oDb.execute('INSERT INTO BuildBlacklist (\n' + ' uidAuthor,\n' + ' tsEffective,\n' + ' idBlacklisting,\n' + ' idFailureReason,\n' + ' sProduct,\n' + ' sBranch,\n' + ' asTypes,\n' + ' asOsArches,\n' + ' iFirstRevision,\n' + ' iLastRevision)\n' + 'VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s)\n' + , ( uidAuthor, + tsEffective, + oData.idBlacklisting, + oData.idFailureReason, + oData.sProduct, + oData.sBranch, + oData.asTypes, + oData.asOsArches, + oData.iFirstRevision, + oData.iLastRevision,) ); + return True; + diff --git a/src/VBox/ValidationKit/testmanager/core/buildsource.py b/src/VBox/ValidationKit/testmanager/core/buildsource.py new file mode 100755 index 00000000..782b5ee0 --- /dev/null +++ b/src/VBox/ValidationKit/testmanager/core/buildsource.py @@ -0,0 +1,524 @@ +# -*- coding: utf-8 -*- +# $Id: buildsource.py $ + +""" +Test Manager - Build Sources. +""" + +__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 unittest; + +# Validation Kit imports. +from common import utils; +from testmanager.core.base import ModelDataBase, ModelDataBaseTestCase, ModelLogicBase, TMRowAlreadyExists, \ + TMRowInUse, TMInvalidData, TMRowNotFound; +from testmanager.core import coreconsts; + + +class BuildSourceData(ModelDataBase): + """ + A build source. + """ + + ksIdAttr = 'idBuildSrc'; + + ksParam_idBuildSrc = 'BuildSource_idBuildSrc'; + ksParam_tsEffective = 'BuildSource_tsEffective'; + ksParam_tsExpire = 'BuildSource_tsExpire'; + ksParam_uidAuthor = 'BuildSource_uidAuthor'; + ksParam_sName = 'BuildSource_sName'; + ksParam_sDescription = 'BuildSource_sDescription'; + ksParam_sProduct = 'BuildSource_sProduct'; + ksParam_sBranch = 'BuildSource_sBranch'; + ksParam_asTypes = 'BuildSource_asTypes'; + ksParam_asOsArches = 'BuildSource_asOsArches'; + ksParam_iFirstRevision = 'BuildSource_iFirstRevision'; + ksParam_iLastRevision = 'BuildSource_iLastRevision'; + ksParam_cSecMaxAge = 'BuildSource_cSecMaxAge'; + + kasAllowNullAttributes = [ 'idBuildSrc', 'tsEffective', 'tsExpire', 'uidAuthor', 'sDescription', 'asTypes', + 'asOsArches', 'iFirstRevision', 'iLastRevision', 'cSecMaxAge' ]; + + def __init__(self): + ModelDataBase.__init__(self); + + # + # Initialize with defaults. + # See the database for explanations of each of these fields. + # + self.idBuildSrc = None; + self.tsEffective = None; + self.tsExpire = None; + self.uidAuthor = None; + self.sName = None; + self.sDescription = None; + self.sProduct = None; + self.sBranch = None; + self.asTypes = None; + self.asOsArches = None; + self.iFirstRevision = None; + self.iLastRevision = None; + self.cSecMaxAge = None; + + def initFromDbRow(self, aoRow): + """ + Re-initializes the object from a SELECT * FROM BuildSources row. + Returns self. Raises exception if aoRow is None. + """ + if aoRow is None: + raise TMRowNotFound('Build source not found.'); + + self.idBuildSrc = aoRow[0]; + self.tsEffective = aoRow[1]; + self.tsExpire = aoRow[2]; + self.uidAuthor = aoRow[3]; + self.sName = aoRow[4]; + self.sDescription = aoRow[5]; + self.sProduct = aoRow[6]; + self.sBranch = aoRow[7]; + self.asTypes = aoRow[8]; + self.asOsArches = aoRow[9]; + self.iFirstRevision = aoRow[10]; + self.iLastRevision = aoRow[11]; + self.cSecMaxAge = aoRow[12]; + return self; + + def initFromDbWithId(self, oDb, idBuildSrc, tsNow = None, sPeriodBack = None): + """ + Initialize from the database, given the ID of a row. + """ + oDb.execute(self.formatSimpleNowAndPeriodQuery(oDb, + 'SELECT *\n' + 'FROM BuildSources\n' + 'WHERE idBuildSrc = %s\n' + , ( idBuildSrc,), tsNow, sPeriodBack)); + aoRow = oDb.fetchOne() + if aoRow is None: + raise TMRowNotFound('idBuildSrc=%s not found (tsNow=%s sPeriodBack=%s)' % (idBuildSrc, tsNow, sPeriodBack,)); + return self.initFromDbRow(aoRow); + + def _validateAndConvertAttribute(self, sAttr, sParam, oValue, aoNilValues, fAllowNull, oDb): + # Handle asType and asOsArches specially. + if sAttr == 'sType': + (oNewValue, sError) = ModelDataBase._validateAndConvertAttribute(self, sAttr, sParam, oValue, + aoNilValues, fAllowNull, oDb); + if sError is None: + if not self.asTypes: + oNewValue = None; + else: + for sType in oNewValue: + if len(sType) < 2 or sType.lower() != sType: + if sError is None: sError = ''; + else: sError += ', '; + sError += 'invalid value "%s"' % (sType,); + + 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! + + elif sAttr == 'cSecMaxAge' and oValue not in aoNilValues: # Allow human readable interval formats. + (oNewValue, sError) = utils.parseIntervalSeconds(oValue); + else: + return ModelDataBase._validateAndConvertAttribute(self, sAttr, sParam, oValue, aoNilValues, fAllowNull, oDb); + + return (oNewValue, sError); + +class BuildSourceLogic(ModelLogicBase): # pylint: disable=too-few-public-methods + """ + Build source database logic. + """ + + def __init__(self, oDb): + ModelLogicBase.__init__(self, oDb) + self.dCache = None; + + # + # Standard methods. + # + + def fetchForListing(self, iStart, cMaxRows, tsNow, aiSortColumns = None): + """ + Fetches build sources. + + Returns an array (list) of BuildSourceData items, empty list if none. + Raises exception on error. + """ + _ = aiSortColumns; + + if tsNow is None: + self._oDb.execute('SELECT *\n' + 'FROM BuildSources\n' + 'WHERE tsExpire = \'infinity\'::TIMESTAMP\n' + 'ORDER BY idBuildSrc DESC\n' + 'LIMIT %s OFFSET %s\n' + , (cMaxRows, iStart,)); + else: + self._oDb.execute('SELECT *\n' + 'FROM BuildSources\n' + 'WHERE tsExpire > %s\n' + ' AND tsEffective <= %s\n' + 'ORDER BY idBuildSrc DESC\n' + 'LIMIT %s OFFSET %s\n' + , (tsNow, tsNow, cMaxRows, iStart,)); + + aoRows = [] + for aoRow in self._oDb.fetchAll(): + aoRows.append(BuildSourceData().initFromDbRow(aoRow)) + return aoRows + + def fetchForCombo(self): + """Fetch data which is aimed to be passed to HTML form""" + self._oDb.execute('SELECT idBuildSrc, sName, sProduct\n' + 'FROM BuildSources\n' + 'WHERE tsExpire = \'infinity\'::TIMESTAMP\n' + 'ORDER BY idBuildSrc DESC\n') + asRet = self._oDb.fetchAll(); + asRet.insert(0, (-1, 'None', 'None')); + return asRet; + + + def addEntry(self, oData, uidAuthor, fCommit = False): + """ + Add a new build source to the database. + """ + + # + # Validate the input. + # + dErrors = oData.validateAndConvert(self._oDb, oData.ksValidateFor_Add); + if dErrors: + raise TMInvalidData('addEntry invalid input: %s' % (dErrors,)); + self._assertUnique(oData, None); + + # + # Add it. + # + self._oDb.execute('INSERT INTO BuildSources (\n' + ' uidAuthor,\n' + ' sName,\n' + ' sDescription,\n' + ' sProduct,\n' + ' sBranch,\n' + ' asTypes,\n' + ' asOsArches,\n' + ' iFirstRevision,\n' + ' iLastRevision,\n' + ' cSecMaxAge)\n' + 'VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s)\n' + , ( uidAuthor, + oData.sName, + oData.sDescription, + oData.sProduct, + oData.sBranch, + oData.asTypes, + oData.asOsArches, + oData.iFirstRevision, + oData.iLastRevision, + oData.cSecMaxAge, )); + + self._oDb.maybeCommit(fCommit); + return True; + + def editEntry(self, oData, uidAuthor, fCommit = False): + """ + Modifies a build source. + """ + + # + # Validate the input and read the old entry. + # + dErrors = oData.validateAndConvert(self._oDb, oData.ksValidateFor_Edit); + if dErrors: + raise TMInvalidData('addEntry invalid input: %s' % (dErrors,)); + self._assertUnique(oData, oData.idBuildSrc); + oOldData = BuildSourceData().initFromDbWithId(self._oDb, oData.idBuildSrc); + + # + # Make the changes (if something actually changed). + # + if not oData.isEqualEx(oOldData, [ 'tsEffective', 'tsExpire', 'uidAuthor', ]): + self._historizeBuildSource(oData.idBuildSrc); + self._oDb.execute('INSERT INTO BuildSources (\n' + ' uidAuthor,\n' + ' idBuildSrc,\n' + ' sName,\n' + ' sDescription,\n' + ' sProduct,\n' + ' sBranch,\n' + ' asTypes,\n' + ' asOsArches,\n' + ' iFirstRevision,\n' + ' iLastRevision,\n' + ' cSecMaxAge)\n' + 'VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)\n' + , ( uidAuthor, + oData.idBuildSrc, + oData.sName, + oData.sDescription, + oData.sProduct, + oData.sBranch, + oData.asTypes, + oData.asOsArches, + oData.iFirstRevision, + oData.iLastRevision, + oData.cSecMaxAge, )); + self._oDb.maybeCommit(fCommit); + return True; + + def removeEntry(self, uidAuthor, idBuildSrc, fCascade = False, fCommit = False): + """ + Deletes a build sources. + """ + + # + # Check cascading. + # + if fCascade is not True: + self._oDb.execute('SELECT idSchedGroup, sName\n' + 'FROM SchedGroups\n' + 'WHERE idBuildSrc = %s\n' + ' OR idBuildSrcTestSuite = %s\n' + , (idBuildSrc, idBuildSrc,)); + if self._oDb.getRowCount() > 0: + asGroups = []; + for aoRow in self._oDb.fetchAll(): + asGroups.append('%s (#%d)' % (aoRow[1], aoRow[0])); + raise TMRowInUse('Build source #%d is used by one or more scheduling groups: %s' + % (idBuildSrc, ', '.join(asGroups),)); + else: + self._oDb.execute('UPDATE SchedGroups\n' + 'SET idBuildSrc = NULL\n' + 'WHERE idBuildSrc = %s' + , ( idBuildSrc,)); + self._oDb.execute('UPDATE SchedGroups\n' + 'SET idBuildSrcTestSuite = NULL\n' + 'WHERE idBuildSrcTestSuite = %s' + , ( idBuildSrc,)); + + # + # Do the job. + # + self._historizeBuildSource(idBuildSrc, None); + _ = uidAuthor; ## @todo record deleter. + + self._oDb.maybeCommit(fCommit); + return True; + + def cachedLookup(self, idBuildSrc): + """ + Looks up the most recent BuildSourceData object for idBuildSrc + via an object cache. + + Returns a shared BuildSourceData object. None if not found. + Raises exception on DB error. + """ + if self.dCache is None: + self.dCache = self._oDb.getCache('BuildSourceData'); + oEntry = self.dCache.get(idBuildSrc, None); + if oEntry is None: + self._oDb.execute('SELECT *\n' + 'FROM BuildSources\n' + 'WHERE idBuildSrc = %s\n' + ' AND tsExpire = \'infinity\'::TIMESTAMP\n' + , (idBuildSrc, )); + if self._oDb.getRowCount() == 0: + # Maybe it was deleted, try get the last entry. + self._oDb.execute('SELECT *\n' + 'FROM BuildSources\n' + 'WHERE idBuildSrc = %s\n' + 'ORDER BY tsExpire DESC\n' + 'LIMIT 1\n' + , (idBuildSrc, )); + elif self._oDb.getRowCount() > 1: + raise self._oDb.integrityException('%s infinity rows for %s' % (self._oDb.getRowCount(), idBuildSrc)); + + if self._oDb.getRowCount() == 1: + aaoRow = self._oDb.fetchOne(); + oEntry = BuildSourceData(); + oEntry.initFromDbRow(aaoRow); + self.dCache[idBuildSrc] = oEntry; + return oEntry; + + # + # Other methods. + # + + def openBuildCursor(self, oBuildSource, sOs, sCpuArch, tsNow): + """ + Opens a cursor (SELECT) using the criteria found in the build source + and the given OS.CPUARCH. + + Returns database cursor. May raise exception on bad input or logic error. + + Used by SchedulerBase. + """ + + oCursor = self._oDb.openCursor(); + + # + # Construct the extra conditionals. + # + sExtraConditions = ''; + + # Types + if oBuildSource.asTypes is not None and oBuildSource.asTypes: + if len(oBuildSource.asTypes) == 1: + sExtraConditions += oCursor.formatBindArgs(' AND BuildCategories.sType = %s', (oBuildSource.asTypes[0],)); + else: + sExtraConditions += oCursor.formatBindArgs(' AND BuildCategories.sType IN (%s', (oBuildSource.asTypes[0],)) + for i in range(1, len(oBuildSource.asTypes) - 1): + sExtraConditions += oCursor.formatBindArgs(', %s', (oBuildSource.asTypes[i],)); + sExtraConditions += oCursor.formatBindArgs(', %s)\n', (oBuildSource.asTypes[-1],)); + + # BuildSource OSes.ARCHes. (Paranoia: use a dictionary to avoid duplicate values.) + if oBuildSource.asOsArches is not None and oBuildSource.asOsArches: + sExtraConditions += oCursor.formatBindArgs(' AND BuildCategories.asOsArches && %s', (oBuildSource.asOsArches,)); + + # TestBox OSes.ARCHes. (Paranoia: use a dictionary to avoid duplicate values.) + dOsDotArches = {}; + dOsDotArches[sOs + '.' + sCpuArch] = 1; + dOsDotArches[sOs + '.' + coreconsts.g_ksCpuArchAgnostic] = 1; + dOsDotArches[coreconsts.g_ksOsAgnostic + '.' + sCpuArch] = 1; + dOsDotArches[coreconsts.g_ksOsDotArchAgnostic] = 1; + sExtraConditions += oCursor.formatBindArgs(' AND BuildCategories.asOsArches && %s', (list(dOsDotArches.keys()),)); + + # Revision range. + if oBuildSource.iFirstRevision is not None: + sExtraConditions += oCursor.formatBindArgs(' AND Builds.iRevision >= %s\n', (oBuildSource.iFirstRevision,)); + if oBuildSource.iLastRevision is not None: + sExtraConditions += oCursor.formatBindArgs(' AND Builds.iRevision <= %s\n', (oBuildSource.iLastRevision,)); + + # Max age. + if oBuildSource.cSecMaxAge is not None: + sExtraConditions += oCursor.formatBindArgs(' AND Builds.tsCreated >= (%s - \'%s seconds\'::INTERVAL)\n', + (tsNow, oBuildSource.cSecMaxAge,)); + + # + # Execute the query. + # + oCursor.execute('SELECT Builds.*, BuildCategories.*,\n' + ' EXISTS( SELECT tsExpire\n' + ' FROM BuildBlacklist\n' + ' WHERE BuildBlacklist.tsExpire = \'infinity\'::TIMESTAMP\n' + ' AND BuildBlacklist.sProduct = %s\n' + ' AND BuildBlacklist.sBranch = %s\n' + ' AND BuildBlacklist.iFirstRevision <= Builds.iRevision\n' + ' AND BuildBlacklist.iLastRevision >= Builds.iRevision ) AS fMaybeBlacklisted\n' + 'FROM Builds, BuildCategories\n' + 'WHERE Builds.idBuildCategory = BuildCategories.idBuildCategory\n' + ' AND Builds.tsExpire = \'infinity\'::TIMESTAMP\n' + ' AND Builds.tsEffective <= %s\n' + ' AND Builds.fBinariesDeleted is FALSE\n' + ' AND BuildCategories.sProduct = %s\n' + ' AND BuildCategories.sBranch = %s\n' + + sExtraConditions + + 'ORDER BY Builds.idBuild DESC\n' + 'LIMIT 256\n' + , ( oBuildSource.sProduct, oBuildSource.sBranch, + tsNow, oBuildSource.sProduct, oBuildSource.sBranch,)); + + return oCursor; + + + def getById(self, idBuildSrc): + """Get Build Source data by idBuildSrc""" + + self._oDb.execute('SELECT *\n' + 'FROM BuildSources\n' + 'WHERE tsExpire = \'infinity\'::timestamp\n' + ' AND idBuildSrc = %s;', (idBuildSrc,)) + aRows = self._oDb.fetchAll() + if len(aRows) not in (0, 1): + raise self._oDb.integrityException( + 'Found more than one build sources with the same credentials. Database structure is corrupted.') + try: + return BuildSourceData().initFromDbRow(aRows[0]) + except IndexError: + return None + + # + # Internal helpers. + # + + def _assertUnique(self, oData, idBuildSrcIgnore): + """ Checks that the build source name is unique, raises exception if it isn't. """ + self._oDb.execute('SELECT idBuildSrc\n' + 'FROM BuildSources\n' + 'WHERE sName = %s\n' + ' AND tsExpire = \'infinity\'::TIMESTAMP\n' + + ('' if idBuildSrcIgnore is None else ' AND idBuildSrc <> %d\n' % (idBuildSrcIgnore,)) + , ( oData.sName, )) + if self._oDb.getRowCount() > 0: + raise TMRowAlreadyExists('A build source with name "%s" already exist.' % (oData.sName,)); + return True; + + + def _historizeBuildSource(self, idBuildSrc, tsExpire = None): + """ Historizes the current build source entry. """ + if tsExpire is None: + self._oDb.execute('UPDATE BuildSources\n' + 'SET tsExpire = CURRENT_TIMESTAMP\n' + 'WHERE idBuildSrc = %s\n' + ' AND tsExpire = \'infinity\'::TIMESTAMP\n' + , ( idBuildSrc, )); + else: + self._oDb.execute('UPDATE BuildSources\n' + 'SET tsExpire = %s\n' + 'WHERE idBuildSrc = %s\n' + ' AND tsExpire = \'infinity\'::TIMESTAMP\n' + , ( tsExpire, idBuildSrc, )); + return True; + + + + + +# +# Unit testing. +# + +# pylint: disable=missing-docstring +class BuildSourceDataTestCase(ModelDataBaseTestCase): + def setUp(self): + self.aoSamples = [BuildSourceData(),]; + +if __name__ == '__main__': + unittest.main(); + # not reached. + diff --git a/src/VBox/ValidationKit/testmanager/core/coreconsts.py b/src/VBox/ValidationKit/testmanager/core/coreconsts.py new file mode 100644 index 00000000..bbdc7712 --- /dev/null +++ b/src/VBox/ValidationKit/testmanager/core/coreconsts.py @@ -0,0 +1,100 @@ +# -*- coding: utf-8 -*- +# $Id: coreconsts.py $ + +""" +Test Manager - Test Manager Constants (without a more appropriate home). +""" + +__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 $" + +## OS agnostic. +g_ksOsAgnostic = 'os-agnostic'; +## All known OSes, except the agnostic one. +# See KBUILD_OSES in kBuild/header.kmk for reference. +g_kasOses = ['darwin', 'dos', 'dragonfly', 'freebsd', 'haiku', 'l4', 'linux', 'netbsd', 'nt', 'openbsd', 'os2', + 'solaris', 'win']; +## All known OSes, including the agnostic one. +# See KBUILD_OSES in kBuild/header.kmk for reference. +g_kasOsesAll = g_kasOses + [g_ksOsAgnostic,]; + + +## Architecture agnostic. +g_ksCpuArchAgnostic = 'noarch'; +## All known CPU architectures, except the agnostic one. +# See KBUILD_ARCHES in kBuild/header.kmk for reference. +g_kasCpuArches = ['amd64', 'x86', 'sparc32', 'sparc64', 's390', 's390x', 'ppc32', 'ppc64', 'mips32', 'mips64', 'ia64', + 'hppa32', 'hppa64', 'arm', 'alpha']; +## All known CPU architectures, except the agnostic one. +# See KBUILD_ARCHES in kBuild/header.kmk for reference. +g_kasCpuArchesAll = g_kasCpuArches + [g_ksCpuArchAgnostic,]; + +## All known build types +# See KBUILD_TYPE in kBuild/header.kmk for reference. +# @note 'blessed' is a special type used for release builds that has been notarized +# or attestation signed by the OS vendor. +g_kasBuildTypesAll = [ 'release', 'strict', 'profile', 'debug', 'asan', 'blessed' ]; + +## OS and CPU architecture agnostic. +g_ksOsDotArchAgnostic = 'os-agnostic.noarch'; +## Combinations of all OSes and CPU architectures, except the two agnostic ones. +# We do some of them by hand to avoid offering too many choices. +g_kasOsDotCpus = \ +[ + 'darwin.amd64', 'darwin.x86', 'darwin.ppc32', 'darwin.ppc64', 'darwin.arm', + 'dos.x86', + 'dragonfly.amd64', 'dragonfly.x86', + 'freebsd.amd64', 'freebsd.x86', 'freebsd.sparc64', 'freebsd.ia64', 'freebsd.ppc32', 'freebsd.ppc64', 'freebsd.arm', + 'freebsd.mips32', 'freebsd.mips64', + 'haiku.amd64', 'haiku.x86', + 'l4.amd64', 'l4.x86', 'l4.ppc32', 'l4.ppc64', 'l4.arm', + 'nt.amd64', 'nt.x86', 'nt.arm', 'nt.ia64', 'nt.mips32', 'nt.ppc32', 'nt.alpha', + 'win.amd64', 'win.x86', 'win.arm', 'win.ia64', 'win.mips32', 'win.ppc32', 'win.alpha', + 'os2.x86', + 'solaris.amd64', 'solaris.x86', 'solaris.sparc32', 'solaris.sparc64', +]; +for sOs in g_kasOses: + if sOs not in ['darwin', 'dos', 'dragonfly', 'freebsd', 'haiku', 'l4', 'nt', 'win', 'os2', 'solaris']: + for sArch in g_kasCpuArches: + g_kasOsDotCpus.append(sOs + '.' + sArch); +g_kasOsDotCpus.sort(); + +## Combinations of all OSes and CPU architectures, including the two agnostic ones. +g_kasOsDotCpusAll = [g_ksOsDotArchAgnostic] +g_kasOsDotCpusAll.extend(g_kasOsDotCpus); +for sOs in g_kasOsesAll: + g_kasOsDotCpusAll.append(sOs + '.' + g_ksCpuArchAgnostic); +for sArch in g_kasCpuArchesAll: + g_kasOsDotCpusAll.append(g_ksOsAgnostic + '.' + sArch); +g_kasOsDotCpusAll.sort(); + diff --git a/src/VBox/ValidationKit/testmanager/core/db.py b/src/VBox/ValidationKit/testmanager/core/db.py new file mode 100755 index 00000000..915336c3 --- /dev/null +++ b/src/VBox/ValidationKit/testmanager/core/db.py @@ -0,0 +1,745 @@ +# -*- coding: utf-8 -*- +# $Id: db.py $ + +""" +Test Manager - Database Interface. +""" + +__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 datetime; +import os; +import sys; +import psycopg2; # pylint: disable=import-error +import psycopg2.extensions; # pylint: disable=import-error + +# Validation Kit imports. +from common import utils, webutils; +from testmanager import config; + +# Fix psycho unicode handling in psycopg2 with python 2.x. +if sys.version_info[0] < 3: + psycopg2.extensions.register_type(psycopg2.extensions.UNICODE); + psycopg2.extensions.register_type(psycopg2.extensions.UNICODEARRAY); +else: + unicode = str; # pylint: disable=redefined-builtin,invalid-name + + + +def isDbTimestampInfinity(tsValue): + """ + Checks if tsValue is an infinity timestamp. + """ + ## @todo improve this test... + return tsValue.year >= 9999; + +def isDbTimestamp(oValue): + """ + Checks if oValue is a DB timestamp object. + """ + if isinstance(oValue, datetime.datetime): + return True; + if utils.isString(oValue): + ## @todo detect strings as well. + return False; + return getattr(oValue, 'pydatetime', None) is not None; + +def dbTimestampToDatetime(oValue): + """ + Converts a database timestamp to a datetime instance. + """ + if isinstance(oValue, datetime.datetime): + return oValue; + if utils.isString(oValue): + return utils.parseIsoTimestamp(oValue); + return oValue.pydatetime(); + +def dbTimestampToZuluDatetime(oValue): + """ + Converts a database timestamp to a zulu datetime instance. + """ + tsValue = dbTimestampToDatetime(oValue); + + class UTC(datetime.tzinfo): + """UTC TZ Info Class""" + def utcoffset(self, _): + return datetime.timedelta(0); + def tzname(self, _): + return "UTC"; + def dst(self, _): + return datetime.timedelta(0); + if tsValue.tzinfo is not None: + tsValue = tsValue.astimezone(UTC()); + else: + tsValue = tsValue.replace(tzinfo=UTC()); + return tsValue; + +def dbTimestampPythonNow(): + """ + Gets the current python timestamp in a database compatible way. + """ + return dbTimestampToZuluDatetime(datetime.datetime.utcnow()); + +def dbOneTickIntervalString(): + """ + Returns the interval string for one tick. + + Mogrify the return value into the SQL: + "... %s::INTERVAL ..." + or + "INTERVAL %s" + The completed SQL will contain the necessary ticks. + """ + return '1 microsecond'; + +def dbTimestampMinusOneTick(oValue): + """ + Returns a new timestamp that's one tick before the given one. + """ + oValue = dbTimestampToZuluDatetime(oValue); + return oValue - datetime.timedelta(microseconds = 1); + +def dbTimestampPlusOneTick(oValue): + """ + Returns a new timestamp that's one tick after the given one. + """ + oValue = dbTimestampToZuluDatetime(oValue); + return oValue + datetime.timedelta(microseconds = 1); + +def isDbInterval(oValue): + """ + Checks if oValue is a DB interval object. + """ + if isinstance(oValue, datetime.timedelta): + return True; + return False; + + +class TMDatabaseIntegrityException(Exception): + """ + Herolds a database integrity error up the callstack. + + Do NOT use directly, only thru TMDatabaseConnection.integrityException. + Otherwise, we won't be able to log the issue. + """ + pass; # pylint: disable=unnecessary-pass + + +class TMDatabaseCursor(object): + """ Cursor wrapper class. """ + + def __init__(self, oDb, oCursor): + self._oDb = oDb; + self._oCursor = oCursor; + + def execute(self, sOperation, aoArgs = None): + """ See TMDatabaseConnection.execute()""" + return self._oDb.executeInternal(self._oCursor, sOperation, aoArgs, utils.getCallerName()); + + def callProc(self, sProcedure, aoArgs = None): + """ See TMDatabaseConnection.callProc()""" + return self._oDb.callProcInternal(self._oCursor, sProcedure, aoArgs, utils.getCallerName()); + + def insertList(self, sInsertSql, aoList, fnEntryFmt): + """ See TMDatabaseConnection.insertList. """ + return self._oDb.insertListInternal(self._oCursor, sInsertSql, aoList, fnEntryFmt, utils.getCallerName()); + + def fetchOne(self): + """Wrapper around Psycopg2.cursor.fetchone.""" + return self._oCursor.fetchone(); + + def fetchMany(self, cRows = None): + """Wrapper around Psycopg2.cursor.fetchmany.""" + return self._oCursor.fetchmany(cRows if cRows is not None else self._oCursor.arraysize); + + def fetchAll(self): + """Wrapper around Psycopg2.cursor.fetchall.""" + return self._oCursor.fetchall(); + + def getRowCount(self): + """Wrapper around Psycopg2.cursor.rowcount.""" + return self._oCursor.rowcount; + + def formatBindArgs(self, sStatement, aoArgs): + """Wrapper around Psycopg2.cursor.mogrify.""" + oRet = self._oCursor.mogrify(sStatement, aoArgs); + if sys.version_info[0] >= 3 and not isinstance(oRet, str): + oRet = oRet.decode('utf-8'); + return oRet; + + def copyExpert(self, sSqlCopyStmt, oFile, cbBuf = 8192): + """ See TMDatabaseConnection.copyExpert()""" + return self._oCursor.copy_expert(sSqlCopyStmt, oFile, cbBuf); + + @staticmethod + def isTsInfinity(tsValue): + """ Checks if tsValue is an infinity timestamp. """ + return isDbTimestampInfinity(tsValue); + + +class TMDatabaseConnection(object): + """ + Test Manager Database Access class. + + This class contains no logic, just raw access abstraction and utilities, + as well as some debug help and some statistics. + """ + + def __init__(self, fnDPrint = None, oSrvGlue = None): + """ + Database connection wrapper. + The fnDPrint is for debug logging of all database activity. + + Raises an exception on failure. + """ + + sAppName = '%s-%s' % (os.getpid(), os.path.basename(sys.argv[0]),) + if len(sAppName) >= 64: + sAppName = sAppName[:64]; + os.environ['PGAPPNAME'] = sAppName; + + dArgs = \ + { \ + 'database': config.g_ksDatabaseName, + 'user': config.g_ksDatabaseUser, + 'password': config.g_ksDatabasePassword, + # 'application_name': sAppName, - Darn stale debian! :/ + }; + if config.g_ksDatabaseAddress is not None: + dArgs['host'] = config.g_ksDatabaseAddress; + if config.g_ksDatabasePort is not None: + dArgs['port'] = config.g_ksDatabasePort; + self._oConn = psycopg2.connect(**dArgs); # pylint: disable=star-args + self._oConn.set_client_encoding('UTF-8'); + self._oCursor = self._oConn.cursor(); + self._oExplainConn = None; + self._oExplainCursor = None; + if config.g_kfWebUiSqlTraceExplain and config.g_kfWebUiSqlTrace: + self._oExplainConn = psycopg2.connect(**dArgs); # pylint: disable=star-args + self._oExplainConn.set_client_encoding('UTF-8'); + self._oExplainCursor = self._oExplainConn.cursor(); + self._fTransaction = False; + self._tsCurrent = None; + self._tsCurrentMinusOne = None; + + assert self.isAutoCommitting() is False; + + # Debug and introspection. + self._fnDPrint = fnDPrint; + self._aoTraceBack = []; + + # Exception class handles. + self.oXcptError = psycopg2.Error; + + if oSrvGlue is not None: + oSrvGlue.registerDebugInfoCallback(self.debugInfoCallback); + + # Object caches (used by database logic classes). + self.ddCaches = {}; + + def isAutoCommitting(self): + """ Work around missing autocommit attribute in older versions.""" + return getattr(self._oConn, 'autocommit', False); + + def close(self): + """ + Closes the connection and renders all cursors useless. + """ + if self._oCursor is not None: + self._oCursor.close(); + self._oCursor = None; + + if self._oConn is not None: + self._oConn.close(); + self._oConn = None; + + if self._oExplainCursor is not None: + self._oExplainCursor.close(); + self._oExplainCursor = None; + + if self._oExplainConn is not None: + self._oExplainConn.close(); + self._oExplainConn = None; + + + def _startedTransaction(self): + """ + Called to work the _fTransaction and related variables when starting + a transaction. + """ + self._fTransaction = True; + self._tsCurrent = None; + self._tsCurrentMinusOne = None; + return None; + + def _endedTransaction(self): + """ + Called to work the _fTransaction and related variables when ending + a transaction. + """ + self._fTransaction = False; + self._tsCurrent = None; + self._tsCurrentMinusOne = None; + return None; + + def begin(self): + """ + Currently just for marking where a transaction starts in the code. + """ + assert self._oConn is not None; + assert self.isAutoCommitting() is False; + self._aoTraceBack.append([utils.timestampNano(), 'START TRANSACTION', 0, 0, utils.getCallerName(), None]); + self._startedTransaction(); + return True; + + def commit(self, sCallerName = None): + """ Wrapper around Psycopg2.connection.commit.""" + assert self._fTransaction is True; + + nsStart = utils.timestampNano(); + oRc = self._oConn.commit(); + cNsElapsed = utils.timestampNano() - nsStart; + + if sCallerName is None: + sCallerName = utils.getCallerName(); + self._aoTraceBack.append([nsStart, 'COMMIT', cNsElapsed, 0, sCallerName, None]); + self._endedTransaction(); + return oRc; + + def maybeCommit(self, fCommit): + """ + Commits if fCommit is True. + Returns True if committed, False if not. + """ + if fCommit is True: + self.commit(utils.getCallerName()); + return True; + return False; + + def rollback(self): + """ Wrapper around Psycopg2.connection.rollback.""" + nsStart = utils.timestampNano(); + oRc = self._oConn.rollback(); + cNsElapsed = utils.timestampNano() - nsStart; + + self._aoTraceBack.append([nsStart, 'ROLLBACK', cNsElapsed, 0, utils.getCallerName(), None]); + self._endedTransaction(); + return oRc; + + # + # Internal cursor workers. + # + + def executeInternal(self, oCursor, sOperation, aoArgs, sCallerName): + """ + Execute a query or command. + + Mostly a wrapper around the psycopg2 cursor method with the same name, + but collect data for traceback. + """ + if aoArgs is not None: + sBound = oCursor.mogrify(unicode(sOperation), aoArgs); + elif sOperation.find('%') < 0: + sBound = oCursor.mogrify(unicode(sOperation), []); + else: + sBound = unicode(sOperation); + + if sys.version_info[0] >= 3 and not isinstance(sBound, str): + sBound = sBound.decode('utf-8'); # pylint: disable=redefined-variable-type + + aasExplain = None; + if self._oExplainCursor is not None and not sBound.startswith('DROP'): + try: + if config.g_kfWebUiSqlTraceExplainTiming: + self._oExplainCursor.execute('EXPLAIN (ANALYZE, BUFFERS, COSTS, VERBOSE, TIMING) ' + sBound); + else: + self._oExplainCursor.execute('EXPLAIN (ANALYZE, BUFFERS, COSTS, VERBOSE) ' + sBound); + except Exception as oXcpt: + aasExplain = [ ['Explain exception: '], [str(oXcpt)]]; + try: self._oExplainConn.rollback(); + except: pass; + else: + aasExplain = self._oExplainCursor.fetchall(); + + nsStart = utils.timestampNano(); + try: + oRc = oCursor.execute(sBound); + except Exception as oXcpt: + cNsElapsed = utils.timestampNano() - nsStart; + self._aoTraceBack.append([nsStart, 'oXcpt=%s; Statement: %s' % (oXcpt, sBound), cNsElapsed, 0, sCallerName, None]); + if self._fnDPrint is not None: + self._fnDPrint('db::execute %u ns, caller %s: oXcpt=%s; Statement: %s' + % (cNsElapsed, sCallerName, oXcpt, sBound)); + raise; + cNsElapsed = utils.timestampNano() - nsStart; + + if self._fTransaction is False and not self.isAutoCommitting(): # Even SELECTs starts transactions with psycopg2, see FAQ. + self._aoTraceBack.append([nsStart, '[START TRANSACTION]', 0, 0, sCallerName, None]); + self._startedTransaction(); + self._aoTraceBack.append([nsStart, sBound, cNsElapsed, oCursor.rowcount, sCallerName, aasExplain]); + if self._fnDPrint is not None: + self._fnDPrint('db::execute %u ns, caller %s: "\n%s"' % (cNsElapsed, sCallerName, sBound)); + if self.isAutoCommitting(): + self._aoTraceBack.append([nsStart, '[AUTO COMMIT]', 0, 0, sCallerName, None]); + + return oRc; + + def callProcInternal(self, oCursor, sProcedure, aoArgs, sCallerName): + """ + Call a stored procedure. + + Mostly a wrapper around the psycopg2 cursor method 'callproc', but + collect data for traceback. + """ + if aoArgs is None: + aoArgs = []; + + nsStart = utils.timestampNano(); + try: + oRc = oCursor.callproc(sProcedure, aoArgs); + except Exception as oXcpt: + cNsElapsed = utils.timestampNano() - nsStart; + self._aoTraceBack.append([nsStart, 'oXcpt=%s; Calling: %s(%s)' % (oXcpt, sProcedure, aoArgs), + cNsElapsed, 0, sCallerName, None]); + if self._fnDPrint is not None: + self._fnDPrint('db::callproc %u ns, caller %s: oXcpt=%s; Calling: %s(%s)' + % (cNsElapsed, sCallerName, oXcpt, sProcedure, aoArgs)); + raise; + cNsElapsed = utils.timestampNano() - nsStart; + + if self._fTransaction is False and not self.isAutoCommitting(): # Even SELECTs starts transactions with psycopg2, see FAQ. + self._aoTraceBack.append([nsStart, '[START TRANSACTION]', 0, 0, sCallerName, None]); + self._startedTransaction(); + self._aoTraceBack.append([nsStart, '%s(%s)' % (sProcedure, aoArgs), cNsElapsed, oCursor.rowcount, sCallerName, None]); + if self._fnDPrint is not None: + self._fnDPrint('db::callproc %u ns, caller %s: "%s(%s)"' % (cNsElapsed, sCallerName, sProcedure, aoArgs)); + if self.isAutoCommitting(): + self._aoTraceBack.append([nsStart, '[AUTO COMMIT]', 0, 0, sCallerName, sCallerName, None]); + + return oRc; + + def insertListInternal(self, oCursor, sInsertSql, aoList, fnEntryFmt, sCallerName): + """ + Optimizes the insertion of a list of values. + """ + oRc = None; + asValues = []; + for aoEntry in aoList: + asValues.append(fnEntryFmt(aoEntry)); + if len(asValues) > 256: + oRc = self.executeInternal(oCursor, sInsertSql + 'VALUES' + ', '.join(asValues), None, sCallerName); + asValues = []; + if asValues: + oRc = self.executeInternal(oCursor, sInsertSql + 'VALUES' + ', '.join(asValues), None, sCallerName); + return oRc + + def _fetchOne(self, oCursor): + """Wrapper around Psycopg2.cursor.fetchone.""" + oRow = oCursor.fetchone() + if self._fnDPrint is not None: + self._fnDPrint('db:fetchOne returns: %s' % (oRow,)); + return oRow; + + def _fetchMany(self, oCursor, cRows): + """Wrapper around Psycopg2.cursor.fetchmany.""" + return oCursor.fetchmany(cRows if cRows is not None else oCursor.arraysize); + + def _fetchAll(self, oCursor): + """Wrapper around Psycopg2.cursor.fetchall.""" + return oCursor.fetchall() + + def _getRowCountWorker(self, oCursor): + """Wrapper around Psycopg2.cursor.rowcount.""" + return oCursor.rowcount; + + + # + # Default cursor access. + # + + def execute(self, sOperation, aoArgs = None): + """ + Execute a query or command. + + Mostly a wrapper around the psycopg2 cursor method with the same name, + but collect data for traceback. + """ + return self.executeInternal(self._oCursor, sOperation, aoArgs, utils.getCallerName()); + + def callProc(self, sProcedure, aoArgs = None): + """ + Call a stored procedure. + + Mostly a wrapper around the psycopg2 cursor method 'callproc', but + collect data for traceback. + """ + return self.callProcInternal(self._oCursor, sProcedure, aoArgs, utils.getCallerName()); + + def insertList(self, sInsertSql, aoList, fnEntryFmt): + """ + Optimizes the insertion of a list of values. + """ + return self.insertListInternal(self._oCursor, sInsertSql, aoList, fnEntryFmt, utils.getCallerName()); + + def fetchOne(self): + """Wrapper around Psycopg2.cursor.fetchone.""" + return self._oCursor.fetchone(); + + def fetchMany(self, cRows = None): + """Wrapper around Psycopg2.cursor.fetchmany.""" + return self._oCursor.fetchmany(cRows if cRows is not None else self._oCursor.arraysize); + + def fetchAll(self): + """Wrapper around Psycopg2.cursor.fetchall.""" + return self._oCursor.fetchall(); + + def getRowCount(self): + """Wrapper around Psycopg2.cursor.rowcount.""" + return self._oCursor.rowcount; + + def formatBindArgs(self, sStatement, aoArgs): + """Wrapper around Psycopg2.cursor.mogrify.""" + oRet = self._oCursor.mogrify(sStatement, aoArgs); + if sys.version_info[0] >= 3 and not isinstance(oRet, str): + oRet = oRet.decode('utf-8'); + return oRet; + + def copyExpert(self, sSqlCopyStmt, oFile, cbBuf = 8192): + """ Wrapper around Psycopg2.cursor.copy_expert. """ + return self._oCursor.copy_expert(sSqlCopyStmt, oFile, cbBuf); + + def getCurrentTimestamps(self): + """ + Returns the current timestamp and the current timestamp minus one tick. + This will start a transaction if necessary. + """ + if self._tsCurrent is None: + self.execute('SELECT CURRENT_TIMESTAMP, CURRENT_TIMESTAMP - INTERVAL \'1 microsecond\''); + (self._tsCurrent, self._tsCurrentMinusOne) = self.fetchOne(); + return (self._tsCurrent, self._tsCurrentMinusOne); + + def getCurrentTimestamp(self): + """ + Returns the current timestamp. + This will start a transaction if necessary. + """ + if self._tsCurrent is None: + self.getCurrentTimestamps(); + return self._tsCurrent; + + def getCurrentTimestampMinusOne(self): + """ + Returns the current timestamp minus one tick. + This will start a transaction if necessary. + """ + if self._tsCurrentMinusOne is None: + self.getCurrentTimestamps(); + return self._tsCurrentMinusOne; + + + # + # Additional cursors. + # + def openCursor(self): + """ + Opens a new cursor (TMDatabaseCursor). + """ + oCursor = self._oConn.cursor(); + return TMDatabaseCursor(self, oCursor); + + # + # Cache support. + # + def getCache(self, sType): + """ Returns the cache dictionary for this data type. """ + dRet = self.ddCaches.get(sType, None); + if dRet is None: + dRet = {}; + self.ddCaches[sType] = dRet; + return dRet; + + + # + # Utilities. + # + + @staticmethod + def isTsInfinity(tsValue): + """ Checks if tsValue is an infinity timestamp. """ + return isDbTimestampInfinity(tsValue); + + # + # Error stuff. + # + def integrityException(self, sMessage): + """ + Database integrity reporter and exception factory. + Returns an TMDatabaseIntegrityException which the caller can raise. + """ + ## @todo Create a new database connection and log the issue in the SystemLog table. + ## Alternatively, rollback whatever is going on and do it using the current one. + return TMDatabaseIntegrityException(sMessage); + + + # + # Debugging. + # + + def dprint(self, sText): + """ + Debug output. + """ + if not self._fnDPrint: + return False; + self._fnDPrint(sText); + return True; + + def debugHtmlReport(self, tsStart = 0): + """ + Used to get a SQL activity dump as HTML, usually for WuiBase._sDebug. + """ + cNsElapsed = 0; + for aEntry in self._aoTraceBack: + cNsElapsed += aEntry[2]; + + sDebug = '<h3>SQL Debug Log (total time %s ns):</h3>\n' \ + '<table class="tmsqltable">\n' \ + ' <tr>\n' \ + ' <th>No.</th>\n' \ + ' <th>Timestamp (ns)</th>\n' \ + ' <th>Elapsed (ns)</th>\n' \ + ' <th>Rows Returned</th>\n' \ + ' <th>Command</th>\n' \ + ' <th>Caller</th>\n' \ + ' </tr>\n' \ + % (utils.formatNumber(cNsElapsed, ' '),); + + iEntry = 0; + for aEntry in self._aoTraceBack: + iEntry += 1; + sDebug += ' <tr>\n' \ + ' <td>%s</td>\n' \ + ' <td>%s</td>\n' \ + ' <td>%s</td>\n' \ + ' <td>%s</td>\n' \ + ' <td><pre>%s</pre></td>\n' \ + ' <td>%s</td>\n' \ + ' </tr>\n' \ + % (iEntry, + utils.formatNumber(aEntry[0] - tsStart, ' '), + utils.formatNumber(aEntry[2], ' '), + utils.formatNumber(aEntry[3], ' '), + webutils.escapeElem(aEntry[1]), + webutils.escapeElem(aEntry[4]), + ); + if aEntry[5] is not None: + sDebug += ' <tr>\n' \ + ' <td colspan="6"><pre style="white-space: pre-wrap;">%s</pre></td>\n' \ + ' </tr>\n' \ + % (webutils.escapeElem('\n'.join([aoRow[0] for aoRow in aEntry[5]])),); + + sDebug += '</table>'; + return sDebug; + + def debugTextReport(self, tsStart = 0): + """ + Used to get a SQL activity dump as text. + """ + cNsElapsed = 0; + for aEntry in self._aoTraceBack: + cNsElapsed += aEntry[2]; + + sHdr = 'SQL Debug Log (total time %s ns)' % (utils.formatNumber(cNsElapsed),); + sDebug = sHdr + '\n' + '-' * len(sHdr) + '\n'; + + iEntry = 0; + for aEntry in self._aoTraceBack: + iEntry += 1; + sHdr = 'Query #%s Timestamp: %s ns Elapsed: %s ns Rows: %s Caller: %s' \ + % ( iEntry, + utils.formatNumber(aEntry[0] - tsStart), + utils.formatNumber(aEntry[2]), + utils.formatNumber(aEntry[3]), + aEntry[4], ); + sDebug += '\n' + sHdr + '\n' + '-' * len(sHdr) + '\n'; + + sDebug += aEntry[1]; + if sDebug[-1] != '\n': + sDebug += '\n'; + + if aEntry[5] is not None: + sDebug += 'Explain:\n' \ + ' %s\n' \ + % ( '\n'.join([aoRow[0] for aoRow in aEntry[5]]),); + + return sDebug; + + def debugInfoCallback(self, oGlue, fHtml): + """ Called back by the glue code on error. """ + oGlue.write('\n'); + if not fHtml: oGlue.write(self.debugTextReport()); + else: oGlue.write(self.debugHtmlReport()); + oGlue.write('\n'); + return True; + + def debugEnableExplain(self): + """ Enabled explain. """ + if self._oExplainConn is None: + dArgs = \ + { \ + 'database': config.g_ksDatabaseName, + 'user': config.g_ksDatabaseUser, + 'password': config.g_ksDatabasePassword, + # 'application_name': sAppName, - Darn stale debian! :/ + }; + if config.g_ksDatabaseAddress is not None: + dArgs['host'] = config.g_ksDatabaseAddress; + if config.g_ksDatabasePort is not None: + dArgs['port'] = config.g_ksDatabasePort; + self._oExplainConn = psycopg2.connect(**dArgs); # pylint: disable=star-args + self._oExplainCursor = self._oExplainConn.cursor(); + return True; + + def debugDisableExplain(self): + """ Disables explain. """ + self._oExplainCursor = None; + self._oExplainConn = None + return True; + + def debugIsExplainEnabled(self): + """ Check if explaining of SQL statements is enabled. """ + return self._oExplainConn is not None; + diff --git a/src/VBox/ValidationKit/testmanager/core/dbobjcache.py b/src/VBox/ValidationKit/testmanager/core/dbobjcache.py new file mode 100755 index 00000000..b059dccd --- /dev/null +++ b/src/VBox/ValidationKit/testmanager/core/dbobjcache.py @@ -0,0 +1,200 @@ +# -*- coding: utf-8 -*- +# $Id: dbobjcache.py $ + +""" +Test Manager - Database object cache. +""" + +__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 $" + + +# Validation Kit imports. +from testmanager.core.base import ModelLogicBase; + + +class DatabaseObjCache(ModelLogicBase): + """ + Database object cache. + + This is mainly for reports and test results where we wish to get further + information on a data series or similar. The cache should reduce database + lookups as well as pyhon memory footprint. + + Note! Dependecies are imported when needed to avoid potential cylic dependency issues. + """ + + ## @name Cache object types. + ## @{ + ksObjType_TestResultStrTab_idStrName = 0; + ksObjType_BuildCategory_idBuildCategory = 1; + ksObjType_TestBox_idTestBox = 2; + ksObjType_TestBox_idGenTestBox = 3; + ksObjType_TestCase_idTestCase = 4; + ksObjType_TestCase_idGenTestCase = 5; + ksObjType_TestCaseArgs_idTestCaseArgs = 6; + ksObjType_TestCaseArgs_idGenTestCaseArgs = 7; + ksObjType_VcsRevision_sRepository_iRevision = 8; + ksObjType_End = 9; + ## @} + + def __init__(self, oDb, tsNow = None, sPeriodBack = None, cHoursBack = None): + ModelLogicBase.__init__(self, oDb); + + self.tsNow = tsNow; + self.sPeriodBack = sPeriodBack; + if sPeriodBack is None and cHoursBack is not None: + self.sPeriodBack = '%u hours' % cHoursBack; + + self._adCache = ( + {}, {}, {}, {}, + {}, {}, {}, {}, + {}, + ); + assert(len(self._adCache) == self.ksObjType_End); + + def _handleDbException(self): + """ Deals with database exceptions. """ + #self._oDb.rollback(); + return False; + + def getTestResultString(self, idStrName): + """ Gets a string from the TestResultStrTab. """ + sRet = self._adCache[self.ksObjType_TestResultStrTab_idStrName].get(idStrName); + if sRet is None: + # Load cache entry. + self._oDb.execute('SELECT sValue FROM TestResultStrTab WHERE idStr = %s', (idStrName,)); + sRet = self._oDb.fetchOne()[0]; + self._adCache[self.ksObjType_TestResultStrTab_idStrName][idStrName] = sRet + return sRet; + + def getBuildCategory(self, idBuildCategory): + """ Gets the corresponding BuildCategoryData object. """ + oRet = self._adCache[self.ksObjType_BuildCategory_idBuildCategory].get(idBuildCategory); + if oRet is None: + # Load cache entry. + from testmanager.core.build import BuildCategoryData; + oRet = BuildCategoryData(); + try: oRet.initFromDbWithId(self._oDb, idBuildCategory); + except: self._handleDbException(); raise; + self._adCache[self.ksObjType_BuildCategory_idBuildCategory][idBuildCategory] = oRet; + return oRet; + + def getTestBox(self, idTestBox): + """ Gets the corresponding TestBoxData object. """ + oRet = self._adCache[self.ksObjType_TestBox_idTestBox].get(idTestBox); + if oRet is None: + # Load cache entry. + from testmanager.core.testbox import TestBoxData; + oRet = TestBoxData(); + try: oRet.initFromDbWithId(self._oDb, idTestBox, self.tsNow, self.sPeriodBack); + except: self._handleDbException(); raise; + else: self._adCache[self.ksObjType_TestBox_idGenTestBox][oRet.idGenTestBox] = oRet; + self._adCache[self.ksObjType_TestBox_idTestBox][idTestBox] = oRet; + return oRet; + + def getTestCase(self, idTestCase): + """ Gets the corresponding TestCaseData object. """ + oRet = self._adCache[self.ksObjType_TestCase_idTestCase].get(idTestCase); + if oRet is None: + # Load cache entry. + from testmanager.core.testcase import TestCaseData; + oRet = TestCaseData(); + try: oRet.initFromDbWithId(self._oDb, idTestCase, self.tsNow, self.sPeriodBack); + except: self._handleDbException(); raise; + else: self._adCache[self.ksObjType_TestCase_idGenTestCase][oRet.idGenTestCase] = oRet; + self._adCache[self.ksObjType_TestCase_idTestCase][idTestCase] = oRet; + return oRet; + + def getTestCaseArgs(self, idTestCaseArgs): + """ Gets the corresponding TestCaseArgsData object. """ + oRet = self._adCache[self.ksObjType_TestCaseArgs_idTestCaseArgs].get(idTestCaseArgs); + if oRet is None: + # Load cache entry. + from testmanager.core.testcaseargs import TestCaseArgsData; + oRet = TestCaseArgsData(); + try: oRet.initFromDbWithId(self._oDb, idTestCaseArgs, self.tsNow, self.sPeriodBack); + except: self._handleDbException(); raise; + else: self._adCache[self.ksObjType_TestCaseArgs_idGenTestCaseArgs][oRet.idGenTestCaseArgs] = oRet; + self._adCache[self.ksObjType_TestCaseArgs_idTestCaseArgs][idTestCaseArgs] = oRet; + return oRet; + + def preloadVcsRevInfo(self, sRepository, aiRevisions): + """ + Preloads VCS revision information. + ASSUMES aiRevisions does not contain duplicate keys. + """ + from testmanager.core.vcsrevisions import VcsRevisionData; + dRepo = self._adCache[self.ksObjType_VcsRevision_sRepository_iRevision].get(sRepository); + if dRepo is None: + dRepo = {}; + self._adCache[self.ksObjType_VcsRevision_sRepository_iRevision][sRepository] = dRepo; + aiFiltered = aiRevisions; + else: + aiFiltered = []; + for iRevision in aiRevisions: + if iRevision not in dRepo: + aiFiltered.append(iRevision); + if aiFiltered: + self._oDb.execute('SELECT *\n' + 'FROM VcsRevisions\n' + 'WHERE sRepository = %s\n' + ' AND iRevision IN (' + ','.join([str(i) for i in aiFiltered]) + ')' + , ( sRepository, )); + for aoRow in self._oDb.fetchAll(): + oInfo = VcsRevisionData().initFromDbRow(aoRow); + dRepo[oInfo.iRevision] = oInfo; + return True; + + def getVcsRevInfo(self, sRepository, iRevision): + """ + Gets the corresponding VcsRevisionData object. + May return a default (all NULLs) VcsRevisionData object if the revision + information isn't available in the database yet. + """ + dRepo = self._adCache[self.ksObjType_VcsRevision_sRepository_iRevision].get(sRepository); + if dRepo is not None: + oRet = dRepo.get(iRevision); + else: + dRepo = {}; + self._adCache[self.ksObjType_VcsRevision_sRepository_iRevision][sRepository] = dRepo; + oRet = None; + if oRet is None: + from testmanager.core.vcsrevisions import VcsRevisionLogic; + oRet = VcsRevisionLogic(self._oDb).tryFetch(sRepository, iRevision); + if oRet is None: + from testmanager.core.vcsrevisions import VcsRevisionData; + oRet = VcsRevisionData(); + dRepo[iRevision] = oRet; + return oRet; + diff --git a/src/VBox/ValidationKit/testmanager/core/failurecategory.py b/src/VBox/ValidationKit/testmanager/core/failurecategory.py new file mode 100755 index 00000000..3da0f84b --- /dev/null +++ b/src/VBox/ValidationKit/testmanager/core/failurecategory.py @@ -0,0 +1,392 @@ +# -*- coding: utf-8 -*- +# $Id: failurecategory.py $ + +""" +Test Manager - Failure Categories. +""" + +__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 sys; + +# Validation Kit imports. +from testmanager.core.base import ModelDataBase, ModelLogicBase, TMRowInUse, TMInvalidData, TMRowNotFound, \ + ChangeLogEntry, AttributeChangeEntry; +from testmanager.core.useraccount import UserAccountLogic; + +# Python 3 hacks: +if sys.version_info[0] >= 3: + xrange = range; # pylint: disable=redefined-builtin,invalid-name + + +class FailureCategoryData(ModelDataBase): + """ + Failure Category Data. + """ + + ksIdAttr = 'idFailureCategory'; + + ksParam_idFailureCategory = 'FailureCategory_idFailureCategory' + ksParam_tsEffective = 'FailureCategory_tsEffective' + ksParam_tsExpire = 'FailureCategory_tsExpire' + ksParam_uidAuthor = 'FailureCategory_uidAuthor' + ksParam_sShort = 'FailureCategory_sShort' + ksParam_sFull = 'FailureCategory_sFull' + + kasAllowNullAttributes = [ 'idFailureCategory', 'tsEffective', 'tsExpire', 'uidAuthor' ] + + def __init__(self): + ModelDataBase.__init__(self); + + # + # Initialize with defaults. + # See the database for explanations of each of these fields. + # + + self.idFailureCategory = None + self.tsEffective = None + self.tsExpire = None + self.uidAuthor = None + self.sShort = None + self.sFull = None + + def initFromDbRow(self, aoRow): + """ + Re-initializes the data with a row from a SELECT * FROM FailureCategoryes. + + Returns self. Raises exception if the row is None or otherwise invalid. + """ + + if aoRow is None: + raise TMRowNotFound('Failure Category not found.'); + + self.idFailureCategory = aoRow[0] + self.tsEffective = aoRow[1] + self.tsExpire = aoRow[2] + self.uidAuthor = aoRow[3] + self.sShort = aoRow[4] + self.sFull = aoRow[5] + + return self + + def initFromDbWithId(self, oDb, idFailureCategory, tsNow = None, sPeriodBack = None): + """ + Initialize from the database, given the ID of a row. + """ + oDb.execute(self.formatSimpleNowAndPeriodQuery(oDb, + 'SELECT *\n' + 'FROM FailureCategories\n' + 'WHERE idFailureCategory = %s\n' + , ( idFailureCategory,), tsNow, sPeriodBack)); + aoRow = oDb.fetchOne() + if aoRow is None: + raise TMRowNotFound('idFailureCategory=%s not found (tsNow=%s sPeriodBack=%s)' + % (idFailureCategory, tsNow, sPeriodBack,)); + return self.initFromDbRow(aoRow); + + +class FailureCategoryLogic(ModelLogicBase): # pylint: disable=too-few-public-methods + """ + Failure Category logic. + """ + + def __init__(self, oDb): + ModelLogicBase.__init__(self, oDb) + self.dCache = None; + + def fetchForListing(self, iStart, cMaxRows, tsNow, aiSortColumns = None): + """ + Fetches Failure Category records. + + Returns an array (list) of FailureCategoryData items, empty list if none. + Raises exception on error. + """ + _ = aiSortColumns; + + if tsNow is None: + self._oDb.execute('SELECT *\n' + 'FROM FailureCategories\n' + 'WHERE tsExpire = \'infinity\'::TIMESTAMP\n' + 'ORDER BY idFailureCategory ASC\n' + 'LIMIT %s OFFSET %s\n' + , (cMaxRows, iStart,)); + else: + self._oDb.execute('SELECT *\n' + 'FROM FailureCategories\n' + 'WHERE tsExpire > %s\n' + ' AND tsEffective <= %s\n' + 'ORDER BY idFailureCategory ASC\n' + 'LIMIT %s OFFSET %s\n' + , (tsNow, tsNow, cMaxRows, iStart,)); + + aoRows = [] + for aoRow in self._oDb.fetchAll(): + aoRows.append(FailureCategoryData().initFromDbRow(aoRow)) + return aoRows + + + def fetchForChangeLog(self, idFailureCategory, iStart, cMaxRows, tsNow): # pylint: disable=too-many-locals + """ + Fetches change log entries for a failure reason. + + Returns an array of ChangeLogEntry instance and an indicator whether + there are more entries. + Raises exception on error. + """ + if tsNow is None: + tsNow = self._oDb.getCurrentTimestamp(); + + # 1. Get a list of the relevant changes. + self._oDb.execute('SELECT * FROM FailureCategories WHERE idFailureCategory = %s AND tsEffective <= %s\n' + 'ORDER BY tsEffective DESC\n' + 'LIMIT %s OFFSET %s\n' + , ( idFailureCategory, tsNow, cMaxRows + 1, iStart, )); + aoRows = []; + for aoChange in self._oDb.fetchAll(): + aoRows.append(FailureCategoryData().initFromDbRow(aoChange)); + + # 2. Calculate the changes. + aoEntries = []; + for i in xrange(0, len(aoRows) - 1): + oNew = aoRows[i]; + oOld = aoRows[i + 1]; + + aoChanges = []; + for sAttr in oNew.getDataAttributes(): + if sAttr not in [ 'tsEffective', 'tsExpire', 'uidAuthor', ]: + oOldAttr = getattr(oOld, sAttr); + oNewAttr = getattr(oNew, sAttr); + if oOldAttr != oNewAttr: + aoChanges.append(AttributeChangeEntry(sAttr, oNewAttr, oOldAttr, str(oNewAttr), str(oOldAttr))); + + aoEntries.append(ChangeLogEntry(oNew.uidAuthor, None, oNew.tsEffective, oNew.tsExpire, oNew, oOld, aoChanges)); + + # If we're at the end of the log, add the initial entry. + if len(aoRows) <= cMaxRows and aoRows: + oNew = aoRows[-1]; + aoEntries.append(ChangeLogEntry(oNew.uidAuthor, None, oNew.tsEffective, oNew.tsExpire, oNew, None, [])); + + return (UserAccountLogic(self._oDb).resolveChangeLogAuthors(aoEntries), len(aoRows) > cMaxRows); + + + def getFailureCategoriesForCombo(self, tsEffective = None): + """ + Gets the list of Failure Categories for a combo box. + Returns an array of (value [idFailureCategory], drop-down-name [sShort], + hover-text [sFull]) tuples. + """ + if tsEffective is None: + self._oDb.execute('SELECT idFailureCategory, sShort, sFull\n' + 'FROM FailureCategories\n' + 'WHERE tsExpire = \'infinity\'::TIMESTAMP\n' + 'ORDER BY sShort') + else: + self._oDb.execute('SELECT idFailureCategory, sShort, sFull\n' + 'FROM FailureCategories\n' + 'WHERE tsExpire > %s\n' + ' AND tsEffective <= %s\n' + 'ORDER BY sShort' + , (tsEffective, tsEffective)) + return self._oDb.fetchAll() + + + def getById(self, idFailureCategory): + """Get Failure Category data by idFailureCategory""" + + self._oDb.execute('SELECT *\n' + 'FROM FailureCategories\n' + 'WHERE tsExpire = \'infinity\'::timestamp\n' + ' AND idFailureCategory = %s;', (idFailureCategory,)) + aRows = self._oDb.fetchAll() + if len(aRows) not in (0, 1): + raise self._oDb.integrityException( + 'Found more than one failure categories with the same credentials. Database structure is corrupted.') + try: + return FailureCategoryData().initFromDbRow(aRows[0]) + except IndexError: + return None + + + def addEntry(self, oData, uidAuthor, fCommit = False): + """ + Add a failure reason category. + """ + # + # Validate inputs and read in the old(/current) data. + # + assert isinstance(oData, FailureCategoryData); + dErrors = oData.validateAndConvert(self._oDb, oData.ksValidateFor_Add); + if dErrors: + raise TMInvalidData('editEntry invalid input: %s' % (dErrors,)); + + # + # Add the record. + # + self._readdEntry(uidAuthor, oData); + self._oDb.maybeCommit(fCommit); + return True; + + + def editEntry(self, oData, uidAuthor, fCommit = False): + """ + Modifies a failure reason category. + """ + + # + # Validate inputs and read in the old(/current) data. + # + assert isinstance(oData, FailureCategoryData); + dErrors = oData.validateAndConvert(self._oDb, oData.ksValidateFor_Edit); + if dErrors: + raise TMInvalidData('editEntry invalid input: %s' % (dErrors,)); + + oOldData = FailureCategoryData().initFromDbWithId(self._oDb, oData.idFailureCategory); + + # + # Update the data that needs updating. + # + if not oData.isEqualEx(oOldData, [ 'tsEffective', 'tsExpire', 'uidAuthor', ]): + self._historizeEntry(oData.idFailureCategory); + self._readdEntry(uidAuthor, oData); + self._oDb.maybeCommit(fCommit); + return True; + + + def removeEntry(self, uidAuthor, idFailureCategory, fCascade = False, fCommit = False): + """ + Deletes a failure reason category. + """ + _ = fCascade; # too complicated for now. + + # + # Check whether it's being used by other tables and bitch if it is . + # We currently do not implement cascading. + # + self._oDb.execute('SELECT CONCAT(idFailureReason, \' - \', sShort)\n' + 'FROM FailureReasons\n' + 'WHERE idFailureCategory = %s\n' + ' AND tsExpire = \'infinity\'::TIMESTAMP\n' + , (idFailureCategory,)); + aaoRows = self._oDb.fetchAll(); + if aaoRows: + raise TMRowInUse('Cannot remove failure reason category %u because its being used by: %s' + % (idFailureCategory, ', '.join(aoRow[0] for aoRow in aaoRows),)); + + # + # Do the job. + # + oData = FailureCategoryData().initFromDbWithId(self._oDb, idFailureCategory); + (tsCur, tsCurMinusOne) = self._oDb.getCurrentTimestamps(); + if oData.tsEffective not in (tsCur, tsCurMinusOne): + self._historizeEntry(idFailureCategory, tsCurMinusOne); + self._readdEntry(uidAuthor, oData, tsCurMinusOne); + self._historizeEntry(idFailureCategory); + self._oDb.maybeCommit(fCommit); + return True; + + + def cachedLookup(self, idFailureCategory): + """ + Looks up the most recent FailureCategoryData object for idFailureCategory + via an object cache. + + Returns a shared FailureCategoryData object. None if not found. + Raises exception on DB error. + """ + if self.dCache is None: + self.dCache = self._oDb.getCache('FailureCategory'); + + oEntry = self.dCache.get(idFailureCategory, None); + if oEntry is None: + self._oDb.execute('SELECT *\n' + 'FROM FailureCategories\n' + 'WHERE idFailureCategory = %s\n' + ' AND tsExpire = \'infinity\'::TIMESTAMP\n' + , (idFailureCategory, )); + if self._oDb.getRowCount() == 0: + # Maybe it was deleted, try get the last entry. + self._oDb.execute('SELECT *\n' + 'FROM FailureCategories\n' + 'WHERE idFailureCategory = %s\n' + 'ORDER BY tsExpire DESC\n' + 'LIMIT 1\n' + , (idFailureCategory, )); + elif self._oDb.getRowCount() > 1: + raise self._oDb.integrityException('%s infinity rows for %s' % (self._oDb.getRowCount(), idFailureCategory)); + + if self._oDb.getRowCount() == 1: + oEntry = FailureCategoryData().initFromDbRow(self._oDb.fetchOne()); + self.dCache[idFailureCategory] = oEntry; + return oEntry; + + + # + # Helpers. + # + + def _readdEntry(self, uidAuthor, oData, tsEffective = None): + """ + Re-adds the FailureCategories entry. Used by addEntry, editEntry and removeEntry. + """ + if tsEffective is None: + tsEffective = self._oDb.getCurrentTimestamp(); + self._oDb.execute('INSERT INTO FailureCategories (\n' + ' uidAuthor,\n' + ' tsEffective,\n' + ' idFailureCategory,\n' + ' sShort,\n' + ' sFull)\n' + 'VALUES (%s, %s, ' + + ('DEFAULT' if oData.idFailureCategory is None else str(oData.idFailureCategory)) + + ', %s, %s)\n' + , ( uidAuthor, + tsEffective, + oData.sShort, + oData.sFull,) ); + return True; + + + def _historizeEntry(self, idFailureCategory, tsExpire = None): + """ Historizes the current entry. """ + if tsExpire is None: + tsExpire = self._oDb.getCurrentTimestamp(); + self._oDb.execute('UPDATE FailureCategories\n' + 'SET tsExpire = %s\n' + 'WHERE idFailureCategory = %s\n' + ' AND tsExpire = \'infinity\'::TIMESTAMP\n' + , (tsExpire, idFailureCategory,)); + return True; + diff --git a/src/VBox/ValidationKit/testmanager/core/failurereason.py b/src/VBox/ValidationKit/testmanager/core/failurereason.py new file mode 100755 index 00000000..0d9294b0 --- /dev/null +++ b/src/VBox/ValidationKit/testmanager/core/failurereason.py @@ -0,0 +1,580 @@ +# -*- coding: utf-8 -*- +# $Id: failurereason.py $ + +""" +Test Manager - Failure Reasons. +""" + +__copyright__ = \ +""" +Copyright (C) 2012-2023 Oracle and/or its affiliates. + +This file is part of VirtualBox base platform packages, as +available from https://www.virtualbox.org. + +This program is free software; you can redistribute it and/or +modify it under the terms of the GNU General Public License +as published by the Free Software Foundation, in version 3 of the +License. + +This program is distributed in the hope that it will be useful, but +WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program; if not, see <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 sys; + +# Validation Kit imports. +from testmanager.core.base import ModelDataBase, ModelLogicBase, TMRowNotFound, TMInvalidData, TMRowInUse, \ + AttributeChangeEntry, ChangeLogEntry; +from testmanager.core.useraccount import UserAccountLogic; + +# Python 3 hacks: +if sys.version_info[0] >= 3: + xrange = range; # pylint: disable=redefined-builtin,invalid-name + + +class FailureReasonData(ModelDataBase): + """ + Failure Reason Data. + """ + + ksIdAttr = 'idFailureReason'; + + ksParam_idFailureReason = 'FailureReasonData_idFailureReason' + ksParam_tsEffective = 'FailureReasonData_tsEffective' + ksParam_tsExpire = 'FailureReasonData_tsExpire' + ksParam_uidAuthor = 'FailureReasonData_uidAuthor' + ksParam_idFailureCategory = 'FailureReasonData_idFailureCategory' + ksParam_sShort = 'FailureReasonData_sShort' + ksParam_sFull = 'FailureReasonData_sFull' + ksParam_iTicket = 'FailureReasonData_iTicket' + ksParam_asUrls = 'FailureReasonData_asUrls' + + kasAllowNullAttributes = [ 'idFailureReason', 'tsEffective', 'tsExpire', + 'uidAuthor', 'iTicket', 'asUrls' ] + + def __init__(self): + ModelDataBase.__init__(self); + + # + # Initialize with defaults. + # See the database for explanations of each of these fields. + # + + self.idFailureReason = None + self.tsEffective = None + self.tsExpire = None + self.uidAuthor = None + self.idFailureCategory = None + self.sShort = None + self.sFull = None + self.iTicket = None + self.asUrls = None + + def initFromDbRow(self, aoRow): + """ + Re-initializes the data with a row from a SELECT * FROM FailureReasons. + + Returns self. Raises exception if the row is None or otherwise invalid. + """ + + if aoRow is None: + raise TMRowNotFound('Failure Reason not found.'); + + self.idFailureReason = aoRow[0] + self.tsEffective = aoRow[1] + self.tsExpire = aoRow[2] + self.uidAuthor = aoRow[3] + self.idFailureCategory = aoRow[4] + self.sShort = aoRow[5] + self.sFull = aoRow[6] + self.iTicket = aoRow[7] + self.asUrls = aoRow[8] + + return self; + + def initFromDbWithId(self, oDb, idFailureReason, tsNow = None, sPeriodBack = None): + """ + Initialize from the database, given the ID of a row. + """ + oDb.execute(self.formatSimpleNowAndPeriodQuery(oDb, + 'SELECT *\n' + 'FROM FailureReasons\n' + 'WHERE idFailureReason = %s\n' + , ( idFailureReason,), tsNow, sPeriodBack)); + aoRow = oDb.fetchOne() + if aoRow is None: + raise TMRowNotFound('idFailureReason=%s not found (tsNow=%s sPeriodBack=%s)' + % (idFailureReason, tsNow, sPeriodBack,)); + return self.initFromDbRow(aoRow); + + +class FailureReasonDataEx(FailureReasonData): + """ + Failure Reason Data, extended version that includes the category. + """ + + def __init__(self): + FailureReasonData.__init__(self); + self.oCategory = None; + self.oAuthor = None; + + def initFromDbRowEx(self, aoRow, oCategoryLogic, oUserAccountLogic): + """ + Re-initializes the data with a row from a SELECT * FROM FailureReasons. + + Returns self. Raises exception if the row is None or otherwise invalid. + """ + + self.initFromDbRow(aoRow); + self.oCategory = oCategoryLogic.cachedLookup(self.idFailureCategory); + self.oAuthor = oUserAccountLogic.cachedLookup(self.uidAuthor); + + return self; + + +class FailureReasonLogic(ModelLogicBase): # pylint: disable=too-few-public-methods + """ + Failure Reason logic. + """ + + def __init__(self, oDb): + ModelLogicBase.__init__(self, oDb) + self.dCache = None; + self.dCacheNameAndCat = None; + self.oCategoryLogic = None; + self.oUserAccountLogic = None; + + def fetchForListing(self, iStart, cMaxRows, tsNow, aiSortColumns = None): + """ + Fetches Failure Category records. + + Returns an array (list) of FailureReasonDataEx items, empty list if none. + Raises exception on error. + """ + _ = aiSortColumns; + self._ensureCachesPresent(); + + if tsNow is None: + self._oDb.execute('SELECT FailureReasons.*,\n' + ' FailureCategories.sShort AS sCategory\n' + 'FROM FailureReasons,\n' + ' FailureCategories\n' + 'WHERE FailureReasons.tsExpire = \'infinity\'::TIMESTAMP\n' + ' AND FailureCategories.idFailureCategory = FailureReasons.idFailureCategory\n' + ' AND FailureCategories.tsExpire = \'infinity\'::TIMESTAMP\n' + 'ORDER BY sCategory ASC, sShort ASC\n' + 'LIMIT %s OFFSET %s\n' + , (cMaxRows, iStart,)); + else: + self._oDb.execute('SELECT FailureReasons.*,\n' + ' FailureCategories.sShort AS sCategory\n' + 'FROM FailureReasons,\n' + ' FailureCategories\n' + 'WHERE FailureReasons.tsExpire > %s\n' + ' AND FailureReasons.tsEffective <= %s\n' + ' AND FailureCategories.idFailureCategory = FailureReasons.idFailureCategory\n' + ' AND FailureReasons.tsExpire > %s\n' + ' AND FailureReasons.tsEffective <= %s\n' + 'ORDER BY sCategory ASC, sShort ASC\n' + 'LIMIT %s OFFSET %s\n' + , (tsNow, tsNow, tsNow, tsNow, cMaxRows, iStart,)); + + aoRows = [] + for aoRow in self._oDb.fetchAll(): + aoRows.append(FailureReasonDataEx().initFromDbRowEx(aoRow, self.oCategoryLogic, self.oUserAccountLogic)); + return aoRows + + def fetchForListingInCategory(self, iStart, cMaxRows, tsNow, idFailureCategory, aiSortColumns = None): + """ + Fetches Failure Category records. + + Returns an array (list) of FailureReasonDataEx items, empty list if none. + Raises exception on error. + """ + _ = aiSortColumns; + self._ensureCachesPresent(); + + if tsNow is None: + self._oDb.execute('SELECT *\n' + 'FROM FailureReasons\n' + 'WHERE tsExpire = \'infinity\'::TIMESTAMP\n' + ' AND idFailureCategory = %s\n' + 'ORDER BY sShort ASC\n' + 'LIMIT %s OFFSET %s\n' + , ( idFailureCategory, cMaxRows, iStart,)); + else: + self._oDb.execute('SELECT *\n' + 'FROM FailureReasons\n' + 'WHERE idFailureCategory = %s\n' + ' AND tsExpire > %s\n' + ' AND tsEffective <= %s\n' + 'ORDER BY sShort ASC\n' + 'LIMIT %s OFFSET %s\n' + , ( idFailureCategory, tsNow, tsNow, cMaxRows, iStart,)); + + aoRows = [] + for aoRow in self._oDb.fetchAll(): + aoRows.append(FailureReasonDataEx().initFromDbRowEx(aoRow, self.oCategoryLogic, self.oUserAccountLogic)); + return aoRows + + + def fetchForSheriffByNamedCategory(self, sFailureCategory): + """ + Fetches the short names of the reasons in the named category. + + Returns array of strings. + Raises exception on error. + """ + self._oDb.execute('SELECT FailureReasons.sShort\n' + 'FROM FailureReasons,\n' + ' FailureCategories\n' + 'WHERE FailureReasons.tsExpire = \'infinity\'::TIMESTAMP\n' + ' AND FailureReasons.idFailureCategory = FailureCategories.idFailureCategory\n' + ' AND FailureCategories.sShort = %s\n' + 'ORDER BY FailureReasons.sShort ASC\n' + , ( sFailureCategory,)); + return [aoRow[0] for aoRow in self._oDb.fetchAll()]; + + + def fetchForCombo(self, sFirstEntry = 'Select a failure reason', tsEffective = None): + """ + Gets the list of Failure Reasons for a combo box. + Returns an array of (value [idFailureReason], drop-down-name [sShort], + hover-text [sFull]) tuples. + """ + if tsEffective is None: + self._oDb.execute('SELECT fr.idFailureReason, CONCAT(fc.sShort, \' / \', fr.sShort) as sComboText, fr.sFull\n' + 'FROM FailureReasons fr,\n' + ' FailureCategories fc\n' + 'WHERE fr.idFailureCategory = fc.idFailureCategory\n' + ' AND fr.tsExpire = \'infinity\'::TIMESTAMP\n' + ' AND fc.tsExpire = \'infinity\'::TIMESTAMP\n' + 'ORDER BY sComboText') + else: + self._oDb.execute('SELECT fr.idFailureReason, CONCAT(fc.sShort, \' / \', fr.sShort) as sComboText, fr.sFull\n' + 'FROM FailureReasons fr,\n' + ' FailureCategories fc\n' + 'WHERE fr.idFailureCategory = fc.idFailureCategory\n' + ' AND fr.tsExpire > %s\n' + ' AND fr.tsEffective <= %s\n' + ' AND fc.tsExpire > %s\n' + ' AND fc.tsEffective <= %s\n' + 'ORDER BY sComboText' + , (tsEffective, tsEffective, tsEffective, tsEffective)); + aoRows = self._oDb.fetchAll(); + return [(-1, sFirstEntry, '')] + aoRows; + + + def fetchForChangeLog(self, idFailureReason, iStart, cMaxRows, tsNow): # pylint: disable=too-many-locals + """ + Fetches change log entries for a failure reason. + + Returns an array of ChangeLogEntry instance and an indicator whether + there are more entries. + Raises exception on error. + """ + self._ensureCachesPresent(); + + if tsNow is None: + tsNow = self._oDb.getCurrentTimestamp(); + + # 1. Get a list of the relevant changes. + self._oDb.execute('SELECT * FROM FailureReasons WHERE idFailureReason = %s AND tsEffective <= %s\n' + 'ORDER BY tsEffective DESC\n' + 'LIMIT %s OFFSET %s\n' + , ( idFailureReason, tsNow, cMaxRows + 1, iStart, )); + aoRows = []; + for aoChange in self._oDb.fetchAll(): + aoRows.append(FailureReasonData().initFromDbRow(aoChange)); + + # 2. Calculate the changes. + aoEntries = []; + for i in xrange(0, len(aoRows) - 1): + oNew = aoRows[i]; + oOld = aoRows[i + 1]; + + aoChanges = []; + for sAttr in oNew.getDataAttributes(): + if sAttr not in [ 'tsEffective', 'tsExpire', 'uidAuthor', ]: + oOldAttr = getattr(oOld, sAttr); + oNewAttr = getattr(oNew, sAttr); + if oOldAttr != oNewAttr: + if sAttr == 'idFailureCategory': + oCat = self.oCategoryLogic.cachedLookup(oOldAttr); + if oCat is not None: + oOldAttr = '%s (%s)' % (oOldAttr, oCat.sShort, ); + oCat = self.oCategoryLogic.cachedLookup(oNewAttr); + if oCat is not None: + oNewAttr = '%s (%s)' % (oNewAttr, oCat.sShort, ); + aoChanges.append(AttributeChangeEntry(sAttr, oNewAttr, oOldAttr, str(oNewAttr), str(oOldAttr))); + + aoEntries.append(ChangeLogEntry(oNew.uidAuthor, None, oNew.tsEffective, oNew.tsExpire, oNew, oOld, aoChanges)); + + # If we're at the end of the log, add the initial entry. + if len(aoRows) <= cMaxRows and aoRows: + oNew = aoRows[-1]; + aoEntries.append(ChangeLogEntry(oNew.uidAuthor, None, oNew.tsEffective, oNew.tsExpire, oNew, None, [])); + + return (UserAccountLogic(self._oDb).resolveChangeLogAuthors(aoEntries), len(aoRows) > cMaxRows); + + + def getById(self, idFailureReason): + """Get Failure Reason data by idFailureReason""" + + self._oDb.execute('SELECT *\n' + 'FROM FailureReasons\n' + 'WHERE tsExpire = \'infinity\'::timestamp\n' + ' AND idFailureReason = %s;', (idFailureReason,)) + aRows = self._oDb.fetchAll() + if len(aRows) not in (0, 1): + raise self._oDb.integrityException( + 'Found more than one failure reasons with the same credentials. Database structure is corrupted.') + try: + return FailureReasonData().initFromDbRow(aRows[0]) + except IndexError: + return None + + + def addEntry(self, oData, uidAuthor, fCommit = False): + """ + Add a failure reason. + """ + # + # Validate. + # + dErrors = oData.validateAndConvert(self._oDb, oData.ksValidateFor_Add); + if dErrors: + raise TMInvalidData('addEntry invalid input: %s' % (dErrors,)); + + # + # Add the record. + # + self._readdEntry(uidAuthor, oData); + self._oDb.maybeCommit(fCommit); + return True; + + + def editEntry(self, oData, uidAuthor, fCommit = False): + """ + Modifies a failure reason. + """ + + # + # Validate inputs and read in the old(/current) data. + # + assert isinstance(oData, FailureReasonData); + dErrors = oData.validateAndConvert(self._oDb, oData.ksValidateFor_Edit); + if dErrors: + raise TMInvalidData('editEntry invalid input: %s' % (dErrors,)); + + oOldData = FailureReasonData().initFromDbWithId(self._oDb, oData.idFailureReason); + + # + # Update the data that needs updating. + # + if not oData.isEqualEx(oOldData, [ 'tsEffective', 'tsExpire', 'uidAuthor', ]): + self._historizeEntry(oData.idFailureReason); + self._readdEntry(uidAuthor, oData); + self._oDb.maybeCommit(fCommit); + return True; + + + def removeEntry(self, uidAuthor, idFailureReason, fCascade = False, fCommit = False): + """ + Deletes a failure reason. + """ + _ = fCascade; # too complicated for now. + + # + # Check whether it's being used by other tables and bitch if it is . + # We currently do not implement cascading. + # + self._oDb.execute('SELECT CONCAT(idBlacklisting, \' - blacklisting\')\n' + 'FROM BuildBlacklist\n' + 'WHERE idFailureReason = %s\n' + ' AND tsExpire = \'infinity\'::TIMESTAMP\n' + 'UNION\n' + 'SELECT CONCAT(idTestResult, \' - test result failure reason\')\n' + 'FROM TestResultFailures\n' + 'WHERE idFailureReason = %s\n' + ' AND tsExpire = \'infinity\'::TIMESTAMP\n' + , (idFailureReason, idFailureReason,)); + aaoRows = self._oDb.fetchAll(); + if aaoRows: + raise TMRowInUse('Cannot remove failure reason %u because its being used by: %s' + % (idFailureReason, ', '.join(aoRow[0] for aoRow in aaoRows),)); + + # + # Do the job. + # + oData = FailureReasonData().initFromDbWithId(self._oDb, idFailureReason); + assert oData.idFailureReason == idFailureReason; + (tsCur, tsCurMinusOne) = self._oDb.getCurrentTimestamps(); + if oData.tsEffective not in (tsCur, tsCurMinusOne): + self._historizeEntry(idFailureReason, tsCurMinusOne); + self._readdEntry(uidAuthor, oData, tsCurMinusOne); + self._historizeEntry(idFailureReason); + self._oDb.maybeCommit(fCommit); + return True; + + + def cachedLookup(self, idFailureReason): + """ + Looks up the most recent FailureReasonDataEx object for idFailureReason + via an object cache. + + Returns a shared FailureReasonData object. None if not found. + Raises exception on DB error. + """ + if self.dCache is None: + self.dCache = self._oDb.getCache('FailureReasonDataEx'); + oEntry = self.dCache.get(idFailureReason, None); + if oEntry is None: + self._oDb.execute('SELECT *\n' + 'FROM FailureReasons\n' + 'WHERE idFailureReason = %s\n' + ' AND tsExpire = \'infinity\'::TIMESTAMP\n' + , (idFailureReason, )); + if self._oDb.getRowCount() == 0: + # Maybe it was deleted, try get the last entry. + self._oDb.execute('SELECT *\n' + 'FROM FailureReasons\n' + 'WHERE idFailureReason = %s\n' + 'ORDER BY tsExpire DESC\n' + 'LIMIT 1\n' + , (idFailureReason, )); + elif self._oDb.getRowCount() > 1: + raise self._oDb.integrityException('%s infinity rows for %s' % (self._oDb.getRowCount(), idFailureReason)); + + if self._oDb.getRowCount() == 1: + self._ensureCachesPresent(); + oEntry = FailureReasonDataEx().initFromDbRowEx(self._oDb.fetchOne(), self.oCategoryLogic, + self.oUserAccountLogic); + self.dCache[idFailureReason] = oEntry; + return oEntry; + + + def cachedLookupByNameAndCategory(self, sName, sCategory): + """ + Looks up a failure reason by it's name and category. + + Should the request be ambigiuos, we'll return the oldest one. + + Returns a shared FailureReasonData object. None if not found. + Raises exception on DB error. + """ + if self.dCacheNameAndCat is None: + self.dCacheNameAndCat = self._oDb.getCache('FailureReasonDataEx-By-Name-And-Category'); + sKey = '%s:::%s' % (sName, sCategory,); + oEntry = self.dCacheNameAndCat.get(sKey, None); + if oEntry is None: + self._oDb.execute('SELECT *\n' + 'FROM FailureReasons,\n' + ' FailureCategories\n' + 'WHERE FailureReasons.sShort = %s\n' + ' AND FailureReasons.tsExpire = \'infinity\'::TIMESTAMP\n' + ' AND FailureReasons.idFailureCategory = FailureCategories.idFailureCategory ' + ' AND FailureCategories.sShort = %s\n' + ' AND FailureCategories.tsExpire = \'infinity\'::TIMESTAMP\n' + 'ORDER BY FailureReasons.tsEffective\n' + , ( sName, sCategory)); + if self._oDb.getRowCount() == 0: + sLikeSucks = self._oDb.formatBindArgs( + 'SELECT *\n' + 'FROM FailureReasons,\n' + ' FailureCategories\n' + 'WHERE ( FailureReasons.sShort ILIKE @@@@@@@! %s !@@@@@@@\n' + ' OR FailureReasons.sFull ILIKE @@@@@@@! %s !@@@@@@@)\n' + ' AND FailureCategories.tsExpire = \'infinity\'::TIMESTAMP\n' + ' AND FailureReasons.idFailureCategory = FailureCategories.idFailureCategory\n' + ' AND ( FailureCategories.sShort = %s\n' + ' OR FailureCategories.sFull = %s)\n' + ' AND FailureReasons.tsExpire = \'infinity\'::TIMESTAMP\n' + 'ORDER BY FailureReasons.tsEffective\n' + , ( sName, sName, sCategory, sCategory )); + sLikeSucks = sLikeSucks.replace('LIKE @@@@@@@! \'', 'LIKE \'%').replace('\' !@@@@@@@', '%\''); + self._oDb.execute(sLikeSucks); + if self._oDb.getRowCount() > 0: + self._ensureCachesPresent(); + oEntry = FailureReasonDataEx().initFromDbRowEx(self._oDb.fetchOne(), self.oCategoryLogic, + self.oUserAccountLogic); + self.dCacheNameAndCat[sKey] = oEntry; + if sName != oEntry.sShort or sCategory != oEntry.oCategory.sShort: + sKey2 = '%s:::%s' % (oEntry.sShort, oEntry.oCategory.sShort,); + self.dCacheNameAndCat[sKey2] = oEntry; + return oEntry; + + + # + # Helpers. + # + + def _readdEntry(self, uidAuthor, oData, tsEffective = None): + """ + Re-adds the FailureReasons entry. Used by addEntry, editEntry and removeEntry. + """ + if tsEffective is None: + tsEffective = self._oDb.getCurrentTimestamp(); + self._oDb.execute('INSERT INTO FailureReasons (\n' + ' uidAuthor,\n' + ' tsEffective,\n' + ' idFailureReason,\n' + ' idFailureCategory,\n' + ' sShort,\n' + ' sFull,\n' + ' iTicket,\n' + ' asUrls)\n' + 'VALUES (%s, %s, ' + + ( 'DEFAULT' if oData.idFailureReason is None else str(oData.idFailureReason) ) + + ', %s, %s, %s, %s, %s)\n' + , ( uidAuthor, + tsEffective, + oData.idFailureCategory, + oData.sShort, + oData.sFull, + oData.iTicket, + oData.asUrls,) ); + return True; + + + def _historizeEntry(self, idFailureReason, tsExpire = None): + """ Historizes the current entry. """ + if tsExpire is None: + tsExpire = self._oDb.getCurrentTimestamp(); + self._oDb.execute('UPDATE FailureReasons\n' + 'SET tsExpire = %s\n' + 'WHERE idFailureReason = %s\n' + ' AND tsExpire = \'infinity\'::TIMESTAMP\n' + , (tsExpire, idFailureReason,)); + return True; + + + def _ensureCachesPresent(self): + """ Ensures we've got the cache references resolved. """ + if self.oCategoryLogic is None: + from testmanager.core.failurecategory import FailureCategoryLogic; + self.oCategoryLogic = FailureCategoryLogic(self._oDb); + if self.oUserAccountLogic is None: + self.oUserAccountLogic = UserAccountLogic(self._oDb); + return True; + diff --git a/src/VBox/ValidationKit/testmanager/core/globalresource.pgsql b/src/VBox/ValidationKit/testmanager/core/globalresource.pgsql new file mode 100644 index 00000000..44b5c473 --- /dev/null +++ b/src/VBox/ValidationKit/testmanager/core/globalresource.pgsql @@ -0,0 +1,118 @@ +-- $Id: globalresource.pgsql $ +--- @file +-- VBox Test Manager Database Stored Procedures. +-- + +-- +-- Copyright (C) 2006-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 +-- + +\set ON_ERROR_STOP 1 +\connect testmanager; + +-- Args: uidAuthor, sName, sDescription, fEnabled +CREATE OR REPLACE function add_globalresource(integer, text, text, bool) RETURNS integer AS $$ + DECLARE + _idGlobalRsrc integer; + _uidAuthor ALIAS FOR $1; + _sName ALIAS FOR $2; + _sDescription ALIAS FOR $3; + _fEnabled ALIAS FOR $4; + BEGIN + -- Check if Global Resource name is unique + IF EXISTS(SELECT * FROM GlobalResources + WHERE sName=_sName AND + tsExpire='infinity'::timestamp) THEN + RAISE EXCEPTION 'Duplicate Global Resource name'; + END IF; + INSERT INTO GlobalResources (uidAuthor, sName, sDescription, fEnabled) + VALUES (_uidAuthor, _sName, _sDescription, _fEnabled) RETURNING idGlobalRsrc INTO _idGlobalRsrc; + RETURN _idGlobalRsrc; + END; +$$ LANGUAGE plpgsql; + +-- Args: uidAuthor, idGlobalRsrc +CREATE OR REPLACE function del_globalresource(integer, integer) RETURNS VOID AS $$ + DECLARE + _uidAuthor ALIAS FOR $1; + _idGlobalRsrc ALIAS FOR $2; + BEGIN + + -- Check if record exist + IF NOT EXISTS(SELECT * FROM GlobalResources WHERE idGlobalRsrc=_idGlobalRsrc AND tsExpire='infinity'::timestamp) THEN + RAISE EXCEPTION 'Global resource (%) does not exist', _idGlobalRsrc; + END IF; + + -- Historize record: GlobalResources + UPDATE GlobalResources + SET tsExpire=CURRENT_TIMESTAMP, + uidAuthor=_uidAuthor + WHERE idGlobalRsrc=_idGlobalRsrc AND + tsExpire='infinity'::timestamp; + + + -- Delete record: GlobalResourceStatuses + DELETE FROM GlobalResourceStatuses WHERE idGlobalRsrc=_idGlobalRsrc; + + -- Historize record: TestCaseGlobalRsrcDeps + UPDATE TestCaseGlobalRsrcDeps + SET tsExpire=CURRENT_TIMESTAMP, + uidAuthor=_uidAuthor + WHERE idGlobalRsrc=_idGlobalRsrc AND + tsExpire='infinity'::timestamp; + + END; +$$ LANGUAGE plpgsql; + +-- Args: uidAuthor, idGlobalRsrc, sName, sDescription, fEnabled +CREATE OR REPLACE function update_globalresource(integer, integer, text, text, bool) RETURNS VOID AS $$ + DECLARE + _uidAuthor ALIAS FOR $1; + _idGlobalRsrc ALIAS FOR $2; + _sName ALIAS FOR $3; + _sDescription ALIAS FOR $4; + _fEnabled ALIAS FOR $5; + BEGIN + -- Hostorize record + UPDATE GlobalResources + SET tsExpire=CURRENT_TIMESTAMP + WHERE idGlobalRsrc=_idGlobalRsrc AND + tsExpire='infinity'::timestamp; + -- Check if Global Resource name is unique + IF EXISTS(SELECT * FROM GlobalResources + WHERE sName=_sName AND + tsExpire='infinity'::timestamp) THEN + RAISE EXCEPTION 'Duplicate Global Resource name'; + END IF; + -- Add new record + INSERT INTO GlobalResources(uidAuthor, idGlobalRsrc, sName, sDescription, fEnabled) + VALUES (_uidAuthor, _idGlobalRsrc, _sName, _sDescription, _fEnabled); + END; +$$ LANGUAGE plpgsql; diff --git a/src/VBox/ValidationKit/testmanager/core/globalresource.py b/src/VBox/ValidationKit/testmanager/core/globalresource.py new file mode 100755 index 00000000..bd6f0e9e --- /dev/null +++ b/src/VBox/ValidationKit/testmanager/core/globalresource.py @@ -0,0 +1,328 @@ +# -*- coding: utf-8 -*- +# $Id: globalresource.py $ + +""" +Test Manager - Global Resources. +""" + +__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 unittest; + +# Validation Kit imports. +from testmanager.core.base import ModelDataBase, ModelDataBaseTestCase, ModelLogicBase, TMRowNotFound; + + +class GlobalResourceData(ModelDataBase): + """ + Global resource data + """ + + ksIdAttr = 'idGlobalRsrc'; + + ksParam_idGlobalRsrc = 'GlobalResource_idGlobalRsrc' + ksParam_tsEffective = 'GlobalResource_tsEffective' + ksParam_tsExpire = 'GlobalResource_tsExpire' + ksParam_uidAuthor = 'GlobalResource_uidAuthor' + ksParam_sName = 'GlobalResource_sName' + ksParam_sDescription = 'GlobalResource_sDescription' + ksParam_fEnabled = 'GlobalResource_fEnabled' + + kasAllowNullAttributes = ['idGlobalRsrc', 'tsEffective', 'tsExpire', 'uidAuthor', 'sDescription' ]; + kcchMin_sName = 2; + kcchMax_sName = 64; + + def __init__(self): + ModelDataBase.__init__(self); + + # + # Initialize with defaults. + # See the database for explanations of each of these fields. + # + self.idGlobalRsrc = None; + self.tsEffective = None; + self.tsExpire = None; + self.uidAuthor = None; + self.sName = None; + self.sDescription = None; + self.fEnabled = False + + def initFromDbRow(self, aoRow): + """ + Reinitialize from a SELECT * FROM GlobalResources row. + Returns self. Raises exception if no row. + """ + if aoRow is None: + raise TMRowNotFound('Global resource not found.') + + self.idGlobalRsrc = aoRow[0] + self.tsEffective = aoRow[1] + self.tsExpire = aoRow[2] + self.uidAuthor = aoRow[3] + self.sName = aoRow[4] + self.sDescription = aoRow[5] + self.fEnabled = aoRow[6] + return self + + def initFromDbWithId(self, oDb, idGlobalRsrc, tsNow = None, sPeriodBack = None): + """ + Initialize the object from the database. + """ + oDb.execute(self.formatSimpleNowAndPeriodQuery(oDb, + 'SELECT *\n' + 'FROM GlobalResources\n' + 'WHERE idGlobalRsrc = %s\n' + , ( idGlobalRsrc,), tsNow, sPeriodBack)); + aoRow = oDb.fetchOne() + if aoRow is None: + raise TMRowNotFound('idGlobalRsrc=%s not found (tsNow=%s sPeriodBack=%s)' % (idGlobalRsrc, tsNow, sPeriodBack,)); + return self.initFromDbRow(aoRow); + + def isEqual(self, oOther): + """ + Compares two instances. + """ + return self.idGlobalRsrc == oOther.idGlobalRsrc \ + and str(self.tsEffective) == str(oOther.tsEffective) \ + and str(self.tsExpire) == str(oOther.tsExpire) \ + and self.uidAuthor == oOther.uidAuthor \ + and self.sName == oOther.sName \ + and self.sDescription == oOther.sDescription \ + and self.fEnabled == oOther.fEnabled + + +class GlobalResourceLogic(ModelLogicBase): + """ + Global resource logic. + """ + + def __init__(self, oDb): + ModelLogicBase.__init__(self, oDb) + self.dCache = None; + + def fetchForListing(self, iStart, cMaxRows, tsNow, aiSortColumns = None): + """ + Returns an array (list) of FailureReasonData items, empty list if none. + Raises exception on error. + """ + _ = aiSortColumns; + + if tsNow is None: + self._oDb.execute('SELECT *\n' + 'FROM GlobalResources\n' + 'WHERE tsExpire = \'infinity\'::TIMESTAMP\n' + 'ORDER BY idGlobalRsrc DESC\n' + 'LIMIT %s OFFSET %s\n' + , (cMaxRows, iStart,)); + else: + self._oDb.execute('SELECT *\n' + 'FROM GlobalResources\n' + 'WHERE tsExpire > %s\n' + ' AND tsEffective <= %s\n' + 'ORDER BY idGlobalRsrc DESC\n' + 'LIMIT %s OFFSET %s\n' + , (tsNow, tsNow, cMaxRows, iStart,)) + + aoRows = [] + for aoRow in self._oDb.fetchAll(): + aoRows.append(GlobalResourceData().initFromDbRow(aoRow)) + return aoRows + + + def cachedLookup(self, idGlobalRsrc): + """ + Looks up the most recent GlobalResourceData object for idGlobalRsrc + via an object cache. + + Returns a shared GlobalResourceData object. None if not found. + Raises exception on DB error. + """ + if self.dCache is None: + self.dCache = self._oDb.getCache('GlobalResourceData'); + oEntry = self.dCache.get(idGlobalRsrc, None); + if oEntry is None: + self._oDb.execute('SELECT *\n' + 'FROM GlobalResources\n' + 'WHERE idGlobalRsrc = %s\n' + ' AND tsExpire = \'infinity\'::TIMESTAMP\n' + , (idGlobalRsrc, )); + if self._oDb.getRowCount() == 0: + # Maybe it was deleted, try get the last entry. + self._oDb.execute('SELECT *\n' + 'FROM GlobalResources\n' + 'WHERE idGlobalRsrc = %s\n' + 'ORDER BY tsExpire DESC\n' + 'LIMIT 1\n' + , (idGlobalRsrc, )); + elif self._oDb.getRowCount() > 1: + raise self._oDb.integrityException('%s infinity rows for %s' % (self._oDb.getRowCount(), idGlobalRsrc)); + + if self._oDb.getRowCount() == 1: + aaoRow = self._oDb.fetchOne(); + oEntry = GlobalResourceData(); + oEntry.initFromDbRow(aaoRow); + self.dCache[idGlobalRsrc] = oEntry; + return oEntry; + + + def getAll(self, tsEffective = None): + """ + Gets all global resources. + + Returns an array of GlobalResourceData instances on success (can be + empty). Raises exception on database error. + """ + if tsEffective is not None: + self._oDb.execute('SELECT *\n' + 'FROM GlobalResources\n' + 'WHERE tsExpire > %s\n' + ' AND tsEffective <= %s\n' + , (tsEffective, tsEffective)); + else: + self._oDb.execute('SELECT *\n' + 'FROM GlobalResources\n' + 'WHERE tsExpire = \'infinity\'::TIMESTAMP\n'); + aaoRows = self._oDb.fetchAll(); + aoRet = []; + for aoRow in aaoRows: + aoRet.append(GlobalResourceData().initFromDbRow(aoRow)); + + return aoRet; + + def addGlobalResource(self, uidAuthor, oData): + """Add Global Resource DB record""" + self._oDb.execute('SELECT * FROM add_globalresource(%s, %s, %s, %s);', + (uidAuthor, + oData.sName, + oData.sDescription, + oData.fEnabled)) + self._oDb.commit() + return True + + def editGlobalResource(self, uidAuthor, idGlobalRsrc, oData): + """Modify Global Resource DB record""" + # Check if anything has been changed + oGlobalResourcesDataOld = self.getById(idGlobalRsrc) + if oGlobalResourcesDataOld.isEqual(oData): + # Nothing has been changed, do nothing + return True + + self._oDb.execute('SELECT * FROM update_globalresource(%s, %s, %s, %s, %s);', + (uidAuthor, + idGlobalRsrc, + oData.sName, + oData.sDescription, + oData.fEnabled)) + self._oDb.commit() + return True + + def remove(self, uidAuthor, idGlobalRsrc): + """Delete Global Resource DB record""" + self._oDb.execute('SELECT * FROM del_globalresource(%s, %s);', + (uidAuthor, idGlobalRsrc)) + self._oDb.commit() + return True + + def getById(self, idGlobalRsrc): + """ + Get global resource record by its id + """ + self._oDb.execute('SELECT *\n' + 'FROM GlobalResources\n' + 'WHERE tsExpire = \'infinity\'::timestamp\n' + ' AND idGlobalRsrc=%s;', (idGlobalRsrc,)) + + aRows = self._oDb.fetchAll() + if len(aRows) not in (0, 1): + raise self._oDb.integrityException('Duplicate global resource entry with ID %u (current)' % (idGlobalRsrc,)); + try: + return GlobalResourceData().initFromDbRow(aRows[0]) + except IndexError: + raise TMRowNotFound('Global resource not found.') + + def allocateResources(self, idTestBox, aoGlobalRsrcs, fCommit = False): + """ + Allocates the given global resource. + + Returns True of successfully allocated the resources, False if not. + May raise exception on DB error. + """ + # Quit quickly if there is nothing to alloocate. + if not aoGlobalRsrcs: + return True; + + # + # Note! Someone else might have allocated the resources since the + # scheduler check that they were available. In such case we + # need too quietly rollback and return FALSE. + # + self._oDb.execute('SAVEPOINT allocateResources'); + + for oGlobalRsrc in aoGlobalRsrcs: + try: + self._oDb.execute('INSERT INTO GlobalResourceStatuses (idGlobalRsrc, idTestBox)\n' + 'VALUES (%s, %s)', (oGlobalRsrc.idGlobalRsrc, idTestBox, ) ); + except self._oDb.oXcptError: + self._oDb.execute('ROLLBACK TO SAVEPOINT allocateResources'); + return False; + + self._oDb.execute('RELEASE SAVEPOINT allocateResources'); + self._oDb.maybeCommit(fCommit); + return True; + + def freeGlobalResourcesByTestBox(self, idTestBox, fCommit = False): + """ + Frees all global resources own by the given testbox. + Returns True. May raise exception on DB error. + """ + self._oDb.execute('DELETE FROM GlobalResourceStatuses\n' + 'WHERE idTestBox = %s\n', (idTestBox, ) ); + self._oDb.maybeCommit(fCommit); + return True; + +# +# Unit testing. +# + +# pylint: disable=missing-docstring +class GlobalResourceDataTestCase(ModelDataBaseTestCase): + def setUp(self): + self.aoSamples = [GlobalResourceData(),]; + +if __name__ == '__main__': + unittest.main(); + # not reached. + diff --git a/src/VBox/ValidationKit/testmanager/core/report.py b/src/VBox/ValidationKit/testmanager/core/report.py new file mode 100755 index 00000000..f07e75e8 --- /dev/null +++ b/src/VBox/ValidationKit/testmanager/core/report.py @@ -0,0 +1,1307 @@ +# -*- coding: utf-8 -*- +# $Id: report.py $ + +""" +Test Manager - Report models. +""" + +__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 sys; + +# Validation Kit imports. +from testmanager.core.base import ModelLogicBase, TMExceptionBase; +from testmanager.core.build import BuildCategoryData; +from testmanager.core.dbobjcache import DatabaseObjCache; +from testmanager.core.failurereason import FailureReasonLogic; +from testmanager.core.testbox import TestBoxLogic, TestBoxData; +from testmanager.core.testcase import TestCaseLogic; +from testmanager.core.testcaseargs import TestCaseArgsLogic; +from testmanager.core.testresults import TestResultLogic, TestResultFilter; +from common import constants; + +# Python 3 hacks: +if sys.version_info[0] >= 3: + xrange = range; # pylint: disable=redefined-builtin,invalid-name + + + +class ReportFilter(TestResultFilter): + """ + Same as TestResultFilter for now. + """ + + def __init__(self): + TestResultFilter.__init__(self); + + + +class ReportModelBase(ModelLogicBase): # pylint: disable=too-few-public-methods + """ + Something all report logic(/miner) classes inherit from. + """ + + ## @name Report subjects + ## @{ + ksSubEverything = 'Everything'; + ksSubSchedGroup = 'SchedGroup'; + ksSubTestGroup = 'TestGroup'; + ksSubTestCase = 'TestCase'; + ksSubTestCaseArgs = 'TestCaseArgs'; + ksSubTestBox = 'TestBox'; + ksSubBuild = 'Build'; + ## @} + kasSubjects = [ ksSubEverything, ksSubSchedGroup, ksSubTestGroup, ksSubTestCase, ksSubTestBox, ksSubBuild, ]; + + + ## @name TestStatus_T + # @{ + ksTestStatus_Running = 'running'; + ksTestStatus_Success = 'success'; + ksTestStatus_Skipped = 'skipped'; + ksTestStatus_BadTestBox = 'bad-testbox'; + ksTestStatus_Aborted = 'aborted'; + ksTestStatus_Failure = 'failure'; + ksTestStatus_TimedOut = 'timed-out'; + ksTestStatus_Rebooted = 'rebooted'; + ## @} + + + def __init__(self, oDb, tsNow, cPeriods, cHoursPerPeriod, sSubject, aidSubjects, oFilter): + ModelLogicBase.__init__(self, oDb); + # Public so the report generator can easily access them. + self.tsNow = tsNow; # (Can be None.) + self.__tsNowDateTime = None; + self.cPeriods = cPeriods; + self.cHoursPerPeriod = cHoursPerPeriod; + self.sSubject = sSubject; + self.aidSubjects = aidSubjects; + self.oFilter = oFilter; + if self.oFilter is None: + class DummyFilter(object): + """ Dummy """ + def getTableJoins(self, sExtraIndent = '', iOmit = -1, dOmitTables = None): + """ Dummy """ + _ = sExtraIndent; _ = iOmit; _ = dOmitTables; # pylint: disable=redefined-variable-type + return ''; + def getWhereConditions(self, sExtraIndent = '', iOmit = -1): + """ Dummy """ + _ = sExtraIndent; _ = iOmit; # pylint: disable=redefined-variable-type + return ''; + def isJoiningWithTable(self, sTable): + """ Dummy """; + _ = sTable; + return False; + self.oFilter = DummyFilter(); + + def getExtraSubjectTables(self): + """ + Returns a list of additional tables needed by the subject. + """ + return []; + + def getExtraSubjectWhereExpr(self): + """ + Returns additional WHERE expression relating to the report subject. It starts + with an AND so that it can simply be appended to the WHERE clause. + """ + if self.sSubject == self.ksSubEverything: + return ''; + + if self.sSubject == self.ksSubSchedGroup: + sWhere = ' AND TestSets.idSchedGroup'; + elif self.sSubject == self.ksSubTestGroup: + sWhere = ' AND TestSets.idTestGroup'; + elif self.sSubject == self.ksSubTestCase: + sWhere = ' AND TestSets.idTestCase'; + elif self.sSubject == self.ksSubTestCaseArgs: + sWhere = ' AND TestSets.idTestCaseArgs'; + elif self.sSubject == self.ksSubTestBox: + sWhere = ' AND TestSets.idTestBox'; + elif self.sSubject == self.ksSubBuild: + sWhere = ' AND TestSets.idBuild'; + else: + raise TMExceptionBase(self.sSubject); + + if len(self.aidSubjects) == 1: + sWhere += self._oDb.formatBindArgs(' = %s\n', (self.aidSubjects[0],)); + else: + assert self.aidSubjects; + sWhere += self._oDb.formatBindArgs(' IN (%s', (self.aidSubjects[0],)); + for i in range(1, len(self.aidSubjects)): + sWhere += self._oDb.formatBindArgs(', %s', (self.aidSubjects[i],)); + sWhere += ')\n'; + + return sWhere; + + def getNowAsDateTime(self): + """ Returns a datetime instance corresponding to tsNow. """ + if self.__tsNowDateTime is None: + if self.tsNow is None: + self.__tsNowDateTime = self._oDb.getCurrentTimestamp(); + else: + self._oDb.execute('SELECT %s::TIMESTAMP WITH TIME ZONE', (self.tsNow,)); + self.__tsNowDateTime = self._oDb.fetchOne()[0]; + return self.__tsNowDateTime; + + def getPeriodStart(self, iPeriod): + """ Gets the python timestamp for the start of the given period. """ + from datetime import timedelta; + cHoursStart = (self.cPeriods - iPeriod ) * self.cHoursPerPeriod; + return self.getNowAsDateTime() - timedelta(hours = cHoursStart); + + def getPeriodEnd(self, iPeriod): + """ Gets the python timestamp for the end of the given period. """ + from datetime import timedelta; + cHoursEnd = (self.cPeriods - iPeriod - 1) * self.cHoursPerPeriod; + return self.getNowAsDateTime() - timedelta(hours = cHoursEnd); + + def getExtraWhereExprForPeriod(self, iPeriod): + """ + Returns additional WHERE expression for getting test sets for the + specified period. It starts with an AND so that it can simply be + appended to the WHERE clause. + """ + if self.tsNow is None: + sNow = 'CURRENT_TIMESTAMP'; + else: + sNow = self._oDb.formatBindArgs('%s::TIMESTAMP', (self.tsNow,)); + + cHoursStart = (self.cPeriods - iPeriod ) * self.cHoursPerPeriod; + cHoursEnd = (self.cPeriods - iPeriod - 1) * self.cHoursPerPeriod; + if cHoursEnd == 0: + return ' AND TestSets.tsDone >= (%s - interval \'%u hours\')\n' \ + ' AND TestSets.tsDone < %s\n' \ + % (sNow, cHoursStart, sNow); + return ' AND TestSets.tsDone >= (%s - interval \'%u hours\')\n' \ + ' AND TestSets.tsDone < (%s - interval \'%u hours\')\n' \ + % (sNow, cHoursStart, sNow, cHoursEnd); + + def getPeriodDesc(self, iPeriod): + """ + Returns the period description, usually for graph data. + """ + if iPeriod == 0: + return 'now' if self.tsNow is None else 'then'; + sTerm = 'ago' if self.tsNow is None else 'earlier'; + if self.cHoursPerPeriod == 24: + return '%dd %s' % (iPeriod, sTerm, ); + if (iPeriod * self.cHoursPerPeriod) % 24 == 0: + return '%dd %s' % (iPeriod * self.cHoursPerPeriod / 24, sTerm, ); + return '%dh %s' % (iPeriod * self.cHoursPerPeriod, sTerm); + + def getStraightPeriodDesc(self, iPeriod): + """ + Returns the period description, usually for graph data. + """ + iWickedPeriod = self.cPeriods - iPeriod - 1; + return self.getPeriodDesc(iWickedPeriod); + + +# +# Data structures produced and returned by the ReportLazyModel. +# + +class ReportTransientBase(object): + """ Details on the test where a problem was first/last seen. """ + def __init__(self, idBuild, iRevision, sRepository, idTestSet, idTestResult, tsDone, # pylint: disable=too-many-arguments + iPeriod, fEnter, idSubject, oSubject): + self.idBuild = idBuild; # Build ID. + self.iRevision = iRevision; # SVN revision for build. + self.sRepository = sRepository; # SVN repository for build. + self.idTestSet = idTestSet; # Test set. + self.idTestResult = idTestResult; # Test result. + self.tsDone = tsDone; # When the test set was done. + self.iPeriod = iPeriod; # Data set period. + self.fEnter = fEnter; # True if enter event, False if leave event. + self.idSubject = idSubject; + self.oSubject = oSubject; + +class ReportFailureReasonTransient(ReportTransientBase): + """ Details on the test where a failure reason was first/last seen. """ + def __init__(self, idBuild, iRevision, sRepository, idTestSet, idTestResult, tsDone, # pylint: disable=too-many-arguments + iPeriod, fEnter, oReason): + ReportTransientBase.__init__(self, idBuild, iRevision, sRepository, idTestSet, idTestResult, tsDone, iPeriod, fEnter, + oReason.idFailureReason, oReason); + self.oReason = oReason; # FailureReasonDataEx + + +class ReportHitRowBase(object): + """ A row in a period. """ + def __init__(self, idSubject, oSubject, cHits, tsMin = None, tsMax = None): + self.idSubject = idSubject; + self.oSubject = oSubject; + self.cHits = cHits; + self.tsMin = tsMin; + self.tsMax = tsMax; + +class ReportHitRowWithTotalBase(ReportHitRowBase): + """ A row in a period. """ + def __init__(self, idSubject, oSubject, cHits, cTotal, tsMin = None, tsMax = None): + ReportHitRowBase.__init__(self, idSubject, oSubject, cHits, tsMin, tsMax) + self.cTotal = cTotal; + self.uPct = cHits * 100 / cTotal; + +class ReportFailureReasonRow(ReportHitRowBase): + """ The account of one failure reason for a period. """ + def __init__(self, aoRow, oReason): + ReportHitRowBase.__init__(self, aoRow[0], oReason, aoRow[1], aoRow[2], aoRow[3]); + self.idFailureReason = aoRow[0]; + self.oReason = oReason; # FailureReasonDataEx + + +class ReportPeriodBase(object): + """ A period in ReportFailureReasonSet. """ + def __init__(self, oSet, iPeriod, sDesc, tsFrom, tsTo): + self.oSet = oSet # Reference to the parent ReportSetBase derived object. + self.iPeriod = iPeriod; # Period number in the set. + self.sDesc = sDesc; # Short period description. + self.tsStart = tsFrom; # Start of the period. + self.tsEnd = tsTo; # End of the period (exclusive). + self.tsMin = tsTo; # The earlierst hit of the period (only valid for cHits > 0). + self.tsMax = tsFrom; # The latest hit of the period (only valid for cHits > 0). + self.aoRows = []; # Rows in order the database returned them (ReportHitRowBase descendant). + self.dRowsById = {}; # Same as aoRows but indexed by object ID (see ReportSetBase::sIdAttr). + self.dFirst = {}; # The subjects seen for the first time - data object, keyed by ID. + self.dLast = {}; # The subjects seen for the last time - data object, keyed by ID. + self.cHits = 0; # Total number of hits in this period. + self.cMaxHits = 0; # Max hits in a row. + self.cMinHits = 99999999; # Min hits in a row (only valid for cHits > 0). + + def appendRow(self, oRow, idRow, oData): + """ Adds a row. """ + assert isinstance(oRow, ReportHitRowBase); + self.aoRows.append(oRow); + self.dRowsById[idRow] = oRow; + if idRow not in self.oSet.dSubjects: + self.oSet.dSubjects[idRow] = oData; + self._doStatsForRow(oRow, idRow, oData); + + def _doStatsForRow(self, oRow, idRow, oData): + """ Does the statistics for a row. Helper for appendRow as well as helpRecalcStats. """ + if oRow.tsMin is not None and oRow.tsMin < self.tsMin: + self.tsMin = oRow.tsMin; + if oRow.tsMax is not None and oRow.tsMax < self.tsMax: + self.tsMax = oRow.tsMax; + + self.cHits += oRow.cHits; + if oRow.cHits > self.cMaxHits: + self.cMaxHits = oRow.cHits; + if oRow.cHits < self.cMinHits: + self.cMinHits = oRow.cHits; + + if idRow in self.oSet.dcHitsPerId: + self.oSet.dcHitsPerId[idRow] += oRow.cHits; + else: + self.oSet.dcHitsPerId[idRow] = oRow.cHits; + + if oRow.cHits > 0: + if idRow not in self.oSet.diPeriodFirst: + self.dFirst[idRow] = oData; + self.oSet.diPeriodFirst[idRow] = self.iPeriod; + self.oSet.diPeriodLast[idRow] = self.iPeriod; + + def helperSetRecalcStats(self): + """ Recalc the statistics (do resetStats first on set). """ + for idRow, oRow in self.dRowsById.items(): + self._doStatsForRow(oRow, idRow, self.oSet.dSubjects[idRow]); + + def helperSetResetStats(self): + """ Resets the statistics. """ + self.tsMin = self.tsEnd; + self.tsMax = self.tsStart; + self.cHits = 0; + self.cMaxHits = 0; + self.cMinHits = 99999999; + self.dFirst = {}; + self.dLast = {}; + + def helperSetDeleteKeyFromSet(self, idKey): + """ Helper for ReportPeriodSetBase::deleteKey """ + if idKey in self.dRowsById: + oRow = self.dRowsById[idKey]; + self.aoRows.remove(oRow); + del self.dRowsById[idKey] + self.cHits -= oRow.cHits; + if idKey in self.dFirst: + del self.dFirst[idKey]; + if idKey in self.dLast: + del self.dLast[idKey]; + +class ReportPeriodWithTotalBase(ReportPeriodBase): + """ In addition to the cHits, we also have a total to relate it too. """ + def __init__(self, oSet, iPeriod, sDesc, tsFrom, tsTo): + ReportPeriodBase.__init__(self, oSet, iPeriod, sDesc, tsFrom, tsTo); + self.cTotal = 0; + self.cMaxTotal = 0; + self.cMinTotal = 99999999; + self.uMaxPct = 0; # Max percentage in a row (100 = 100%). + + def _doStatsForRow(self, oRow, idRow, oData): + assert isinstance(oRow, ReportHitRowWithTotalBase); + super(ReportPeriodWithTotalBase, self)._doStatsForRow(oRow, idRow, oData); + self.cTotal += oRow.cTotal; + if oRow.cTotal > self.cMaxTotal: + self.cMaxTotal = oRow.cTotal; + if oRow.cTotal < self.cMinTotal: + self.cMinTotal = oRow.cTotal; + + if oRow.uPct > self.uMaxPct: + self.uMaxPct = oRow.uPct; + + if idRow in self.oSet.dcTotalPerId: + self.oSet.dcTotalPerId[idRow] += oRow.cTotal; + else: + self.oSet.dcTotalPerId[idRow] = oRow.cTotal; + + def helperSetResetStats(self): + super(ReportPeriodWithTotalBase, self).helperSetResetStats(); + self.cTotal = 0; + self.cMaxTotal = 0; + self.cMinTotal = 99999999; + self.uMaxPct = 0; + +class ReportFailureReasonPeriod(ReportPeriodBase): + """ A period in ReportFailureReasonSet. """ + def __init__(self, oSet, iPeriod, sDesc, tsFrom, tsTo): + ReportPeriodBase.__init__(self, oSet, iPeriod, sDesc, tsFrom, tsTo); + self.cWithoutReason = 0; # Number of failed test sets without any assigned reason. + + + +class ReportPeriodSetBase(object): + """ Period data set base class. """ + def __init__(self, sIdAttr): + self.sIdAttr = sIdAttr; # The name of the key attribute. Mainly for documentation purposes. + self.aoPeriods = []; # Periods (ReportPeriodBase descendant) in ascending order (time wise). + self.dSubjects = {}; # The subject data objects, keyed by the subject ID. + self.dcHitsPerId = {}; # Sum hits per subject ID (key). + self.cHits = 0; # Sum number of hits in all periods and all reasons. + self.cMaxHits = 0; # Max hits in a row. + self.cMinHits = 99999999; # Min hits in a row. + self.cMaxRows = 0; # Max number of rows in a period. + self.cMinRows = 99999999; # Min number of rows in a period. + self.diPeriodFirst = {}; # The period number a reason was first seen (keyed by subject ID). + self.diPeriodLast = {}; # The period number a reason was last seen (keyed by subject ID). + self.aoEnterInfo = []; # Array of ReportTransientBase children order by iRevision. Excludes + # the first period of course. (Child class populates this.) + self.aoLeaveInfo = []; # Array of ReportTransientBase children order in descending order by + # iRevision. Excludes the last priod. (Child class populates this.) + + def appendPeriod(self, oPeriod): + """ Appends a period to the set. """ + assert isinstance(oPeriod, ReportPeriodBase); + self.aoPeriods.append(oPeriod); + self._doStatsForPeriod(oPeriod); + + def _doStatsForPeriod(self, oPeriod): + """ Worker for appendPeriod and recalcStats. """ + self.cHits += oPeriod.cHits; + if oPeriod.cMaxHits > self.cMaxHits: + self.cMaxHits = oPeriod.cMaxHits; + if oPeriod.cMinHits < self.cMinHits: + self.cMinHits = oPeriod.cMinHits; + + if len(oPeriod.aoRows) > self.cMaxRows: + self.cMaxRows = len(oPeriod.aoRows); + if len(oPeriod.aoRows) < self.cMinRows: + self.cMinRows = len(oPeriod.aoRows); + + def recalcStats(self): + """ Recalculates the statistics. ASSUMES finalizePass1 hasn't been done yet. """ + self.cHits = 0; + self.cMaxHits = 0; + self.cMinHits = 99999999; + self.cMaxRows = 0; + self.cMinRows = 99999999; + self.diPeriodFirst = {}; + self.diPeriodLast = {}; + self.dcHitsPerId = {}; + for oPeriod in self.aoPeriods: + oPeriod.helperSetResetStats(); + + for oPeriod in self.aoPeriods: + oPeriod.helperSetRecalcStats(); + self._doStatsForPeriod(oPeriod); + + def deleteKey(self, idKey): + """ Deletes a key from the set. May leave cMaxHits and cMinHits with outdated values. """ + self.cHits -= self.dcHitsPerId[idKey]; + del self.dcHitsPerId[idKey]; + if idKey in self.diPeriodFirst: + del self.diPeriodFirst[idKey]; + if idKey in self.diPeriodLast: + del self.diPeriodLast[idKey]; + if idKey in self.aoEnterInfo: + del self.aoEnterInfo[idKey]; + if idKey in self.aoLeaveInfo: + del self.aoLeaveInfo[idKey]; + del self.dSubjects[idKey]; + for oPeriod in self.aoPeriods: + oPeriod.helperSetDeleteKeyFromSet(idKey); + + def pruneRowsWithZeroSumHits(self): + """ Discards rows with zero sum hits across all periods. Works around lazy selects counting both totals and hits. """ + cDeleted = 0; + aidKeys = list(self.dcHitsPerId); + for idKey in aidKeys: + if self.dcHitsPerId[idKey] == 0: + self.deleteKey(idKey); + cDeleted += 1; + if cDeleted > 0: + self.recalcStats(); + return cDeleted; + + def finalizePass1(self): + """ Finished all but aoEnterInfo and aoLeaveInfo. """ + # All we need to do here is to populate the dLast members. + for idKey, iPeriod in self.diPeriodLast.items(): + self.aoPeriods[iPeriod].dLast[idKey] = self.dSubjects[idKey]; + return self; + + def finalizePass2(self): + """ Called after aoEnterInfo and aoLeaveInfo has been populated to sort them. """ + self.aoEnterInfo = sorted(self.aoEnterInfo, key = lambda oTrans: oTrans.iRevision); + self.aoLeaveInfo = sorted(self.aoLeaveInfo, key = lambda oTrans: oTrans.iRevision, reverse = True); + return self; + +class ReportPeriodSetWithTotalBase(ReportPeriodSetBase): + """ In addition to the cHits, we also have a total to relate it too. """ + def __init__(self, sIdAttr): + ReportPeriodSetBase.__init__(self, sIdAttr); + self.dcTotalPerId = {}; # Sum total per subject ID (key). + self.cTotal = 0; # Sum number of total in all periods and all reasons. + self.cMaxTotal = 0; # Max total in a row. + self.cMinTotal = 0; # Min total in a row. + self.uMaxPct = 0; # Max percentage in a row (100 = 100%). + + def _doStatsForPeriod(self, oPeriod): + assert isinstance(oPeriod, ReportPeriodWithTotalBase); + super(ReportPeriodSetWithTotalBase, self)._doStatsForPeriod(oPeriod); + self.cTotal += oPeriod.cTotal; + if oPeriod.cMaxTotal > self.cMaxTotal: + self.cMaxTotal = oPeriod.cMaxTotal; + if oPeriod.cMinTotal < self.cMinTotal: + self.cMinTotal = oPeriod.cMinTotal; + + if oPeriod.uMaxPct > self.uMaxPct: + self.uMaxPct = oPeriod.uMaxPct; + + def recalcStats(self): + self.dcTotalPerId = {}; + self.cTotal = 0; + self.cMaxTotal = 0; + self.cMinTotal = 0; + self.uMaxPct = 0; + super(ReportPeriodSetWithTotalBase, self).recalcStats(); + + def deleteKey(self, idKey): + self.cTotal -= self.dcTotalPerId[idKey]; + del self.dcTotalPerId[idKey]; + super(ReportPeriodSetWithTotalBase, self).deleteKey(idKey); + +class ReportFailureReasonSet(ReportPeriodSetBase): + """ What ReportLazyModel.getFailureReasons returns. """ + def __init__(self): + ReportPeriodSetBase.__init__(self, 'idFailureReason'); + + + +class ReportLazyModel(ReportModelBase): # pylint: disable=too-few-public-methods + """ + The 'lazy bird' report model class. + + We may want to have several classes, maybe one for each report even. But, + I'm thinking that's a bit overkill so we'll start with this and split it + if/when it becomes necessary. + """ + + kdsStatusSimplificationMap = { + ReportModelBase.ksTestStatus_Running: ReportModelBase.ksTestStatus_Running, + ReportModelBase.ksTestStatus_Success: ReportModelBase.ksTestStatus_Success, + ReportModelBase.ksTestStatus_Skipped: ReportModelBase.ksTestStatus_Skipped, + ReportModelBase.ksTestStatus_BadTestBox: ReportModelBase.ksTestStatus_Skipped, + ReportModelBase.ksTestStatus_Aborted: ReportModelBase.ksTestStatus_Skipped, + ReportModelBase.ksTestStatus_Failure: ReportModelBase.ksTestStatus_Failure, + ReportModelBase.ksTestStatus_TimedOut: ReportModelBase.ksTestStatus_Failure, + ReportModelBase.ksTestStatus_Rebooted: ReportModelBase.ksTestStatus_Failure, + }; + + def getSuccessRates(self): + """ + Gets the success rates of the subject in the specified period. + + Returns an array of data per period (0 is the oldes, self.cPeriods-1 is + the latest) where each entry is a status (TestStatus_T) dictionary with + the number of occurences of each final status (i.e. not running). + """ + + sBaseQuery = 'SELECT TestSets.enmStatus, COUNT(TestSets.idTestSet)\n' \ + 'FROM TestSets\n' \ + + self.oFilter.getTableJoins(); + for sTable in self.getExtraSubjectTables(): + sBaseQuery = sBaseQuery[:-1] + ',\n ' + sTable + '\n'; + sBaseQuery += 'WHERE enmStatus <> \'running\'\n' \ + + self.oFilter.getWhereConditions() \ + + self.getExtraSubjectWhereExpr(); + + adPeriods = []; + for iPeriod in xrange(self.cPeriods): + self._oDb.execute(sBaseQuery + self.getExtraWhereExprForPeriod(iPeriod) + 'GROUP BY enmStatus\n'); + + dRet = \ + { + self.ksTestStatus_Skipped: 0, + self.ksTestStatus_Failure: 0, + self.ksTestStatus_Success: 0, + }; + + for aoRow in self._oDb.fetchAll(): + sKey = self.kdsStatusSimplificationMap[aoRow[0]] + if sKey in dRet: + dRet[sKey] += aoRow[1]; + else: + dRet[sKey] = aoRow[1]; + + assert len(dRet) == 3; + + adPeriods.insert(0, dRet); + + return adPeriods; + + + def getFailureReasons(self): + """ + Gets the failure reasons of the subject in the specified period. + + Returns a ReportFailureReasonSet instance. + """ + + oFailureReasonLogic = FailureReasonLogic(self._oDb); + + # + # Create a temporary table + # + sTsNow = 'CURRENT_TIMESTAMP' if self.tsNow is None else self._oDb.formatBindArgs('%s::TIMESTAMP', (self.tsNow,)); + sTsFirst = '(%s - interval \'%s hours\')' \ + % (sTsNow, self.cHoursPerPeriod * self.cPeriods,); + sQuery = 'CREATE TEMPORARY TABLE TmpReasons ON COMMIT DROP AS\n' \ + 'SELECT TestResultFailures.idFailureReason AS idFailureReason,\n' \ + ' TestResultFailures.idTestResult AS idTestResult,\n' \ + ' TestSets.idTestSet AS idTestSet,\n' \ + ' TestSets.tsDone AS tsDone,\n' \ + ' TestSets.tsCreated AS tsCreated,\n' \ + ' TestSets.idBuild AS idBuild\n' \ + 'FROM TestResultFailures,\n' \ + ' TestResults,\n' \ + ' TestSets\n' \ + + self.oFilter.getTableJoins(dOmitTables = {'TestResults': True, 'TestResultFailures': True}); + for sTable in self.getExtraSubjectTables(): + if sTable not in [ 'TestResults', 'TestResultFailures' ] and not self.oFilter.isJoiningWithTable(sTable): + sQuery = sQuery[:-1] + ',\n ' + sTable + '\n'; + sQuery += 'WHERE TestResultFailures.idTestResult = TestResults.idTestResult\n' \ + ' AND TestResultFailures.tsExpire = \'infinity\'::TIMESTAMP\n' \ + ' AND TestResultFailures.tsEffective >= ' + sTsFirst + '\n' \ + ' AND TestResults.enmStatus <> \'running\'\n' \ + ' AND TestResults.enmStatus <> \'success\'\n' \ + ' AND TestResults.tsCreated >= ' + sTsFirst + '\n' \ + ' AND TestResults.tsCreated < ' + sTsNow + '\n' \ + ' AND TestResults.idTestSet = TestSets.idTestSet\n' \ + ' AND TestSets.tsDone >= ' + sTsFirst + '\n' \ + ' AND TestSets.tsDone < ' + sTsNow + '\n' \ + + self.oFilter.getWhereConditions() \ + + self.getExtraSubjectWhereExpr(); + self._oDb.execute(sQuery); + self._oDb.execute('SELECT idFailureReason FROM TmpReasons;'); + + # + # Retrieve the period results. + # + oSet = ReportFailureReasonSet(); + for iPeriod in xrange(self.cPeriods): + self._oDb.execute('SELECT idFailureReason,\n' + ' COUNT(idTestResult),\n' + ' MIN(tsDone),\n' + ' MAX(tsDone)\n' + 'FROM TmpReasons\n' + 'WHERE TRUE\n' + + self.getExtraWhereExprForPeriod(iPeriod).replace('TestSets.', '') + + 'GROUP BY idFailureReason\n'); + aaoRows = self._oDb.fetchAll() + + oPeriod = ReportFailureReasonPeriod(oSet, iPeriod, self.getStraightPeriodDesc(iPeriod), + self.getPeriodStart(iPeriod), self.getPeriodEnd(iPeriod)); + + for aoRow in aaoRows: + oReason = oFailureReasonLogic.cachedLookup(aoRow[0]); + oPeriodRow = ReportFailureReasonRow(aoRow, oReason); + oPeriod.appendRow(oPeriodRow, oReason.idFailureReason, oReason); + + # Count how many test sets we've got without any reason associated with them. + self._oDb.execute('SELECT COUNT(TestSets.idTestSet)\n' + 'FROM TestSets\n' + ' LEFT OUTER JOIN TestResultFailures\n' + ' ON TestSets.idTestSet = TestResultFailures.idTestSet\n' + ' AND TestResultFailures.tsEffective = \'infinity\'::TIMESTAMP\n' + 'WHERE TestSets.enmStatus <> \'running\'\n' + ' AND TestSets.enmStatus <> \'success\'\n' + + self.getExtraWhereExprForPeriod(iPeriod) + + ' AND TestResultFailures.idTestSet IS NULL\n'); + oPeriod.cWithoutReason = self._oDb.fetchOne()[0]; + + oSet.appendPeriod(oPeriod); + + + # + # For reasons entering after the first period, look up the build and + # test set it first occured with. + # + oSet.finalizePass1(); + + for iPeriod in xrange(1, self.cPeriods): + oPeriod = oSet.aoPeriods[iPeriod]; + for oReason in oPeriod.dFirst.values(): + oSet.aoEnterInfo.append(self._getEdgeFailureReasonOccurence(oReason, iPeriod, fEnter = True)); + + # Ditto for reasons leaving before the last. + for iPeriod in xrange(self.cPeriods - 1): + oPeriod = oSet.aoPeriods[iPeriod]; + for oReason in oPeriod.dLast.values(): + oSet.aoLeaveInfo.append(self._getEdgeFailureReasonOccurence(oReason, iPeriod, fEnter = False)); + + oSet.finalizePass2(); + + self._oDb.execute('DROP TABLE TmpReasons\n'); + return oSet; + + + def _getEdgeFailureReasonOccurence(self, oReason, iPeriod, fEnter = True): + """ + Helper for the failure reason report that finds the oldest or newest build + (SVN rev) and test set (start time) it occured with. + + If fEnter is set the oldest occurence is return, if fEnter clear the newest + is is returned. + + Returns ReportFailureReasonTransient instant. + + """ + + + sSorting = 'ASC' if fEnter else 'DESC'; + self._oDb.execute('SELECT TmpReasons.idTestResult,\n' + ' TmpReasons.idTestSet,\n' + ' TmpReasons.tsDone,\n' + ' TmpReasons.idBuild,\n' + ' Builds.iRevision,\n' + ' BuildCategories.sRepository\n' + 'FROM TmpReasons,\n' + ' Builds,\n' + ' BuildCategories\n' + 'WHERE TmpReasons.idFailureReason = %s\n' + ' AND TmpReasons.idBuild = Builds.idBuild\n' + ' AND Builds.tsExpire > TmpReasons.tsCreated\n' + ' AND Builds.tsEffective <= TmpReasons.tsCreated\n' + ' AND Builds.idBuildCategory = BuildCategories.idBuildCategory\n' + 'ORDER BY Builds.iRevision ' + sSorting + ',\n' + ' TmpReasons.tsCreated ' + sSorting + '\n' + 'LIMIT 1\n' + , ( oReason.idFailureReason, )); + aoRow = self._oDb.fetchOne(); + if aoRow is None: + return ReportFailureReasonTransient(-1, -1, 'internal-error', -1, -1, self._oDb.getCurrentTimestamp(), + iPeriod, fEnter, oReason); + return ReportFailureReasonTransient(idBuild = aoRow[3], iRevision = aoRow[4], sRepository = aoRow[5], + idTestSet = aoRow[1], idTestResult = aoRow[0], tsDone = aoRow[2], + iPeriod = iPeriod, fEnter = fEnter, oReason = oReason); + + + def getTestCaseFailures(self): + """ + Gets the test case failures of the subject in the specified period. + + Returns a ReportPeriodSetWithTotalBase instance. + + """ + return self._getSimpleFailures('idTestCase', TestCaseLogic); + + + def getTestCaseVariationFailures(self): + """ + Gets the test case failures of the subject in the specified period. + + Returns a ReportPeriodSetWithTotalBase instance. + + """ + return self._getSimpleFailures('idTestCaseArgs', TestCaseArgsLogic); + + + def getTestBoxFailures(self): + """ + Gets the test box failures of the subject in the specified period. + + Returns a ReportPeriodSetWithTotalBase instance. + + """ + return self._getSimpleFailures('idTestBox', TestBoxLogic); + + + def _getSimpleFailures(self, sIdColumn, oCacheLogicType, sIdAttr = None): + """ + Gets the test box failures of the subject in the specified period. + + Returns a ReportPeriodSetWithTotalBase instance. + + """ + + oLogic = oCacheLogicType(self._oDb); + oSet = ReportPeriodSetWithTotalBase(sIdColumn if sIdAttr is None else sIdAttr); + + # Construct base query. + sBaseQuery = 'SELECT TestSets.' + sIdColumn + ',\n' \ + ' COUNT(CASE WHEN TestSets.enmStatus >= \'failure\' THEN 1 END),\n' \ + ' MIN(TestSets.tsDone),\n' \ + ' MAX(TestSets.tsDone),\n' \ + ' COUNT(TestSets.idTestResult)\n' \ + 'FROM TestSets\n' \ + + self.oFilter.getTableJoins(); + for sTable in self.getExtraSubjectTables(): + sBaseQuery = sBaseQuery[:-1] + ',\n ' + sTable + '\n'; + sBaseQuery += 'WHERE TRUE\n' \ + + self.oFilter.getWhereConditions() \ + + self.getExtraSubjectWhereExpr() + '\n'; + + # Retrieve the period results. + for iPeriod in xrange(self.cPeriods): + self._oDb.execute(sBaseQuery + self.getExtraWhereExprForPeriod(iPeriod) + 'GROUP BY TestSets.' + sIdColumn + '\n'); + aaoRows = self._oDb.fetchAll() + + oPeriod = ReportPeriodWithTotalBase(oSet, iPeriod, self.getStraightPeriodDesc(iPeriod), + self.getPeriodStart(iPeriod), self.getPeriodEnd(iPeriod)); + + for aoRow in aaoRows: + oSubject = oLogic.cachedLookup(aoRow[0]); + oPeriodRow = ReportHitRowWithTotalBase(aoRow[0], oSubject, aoRow[1], aoRow[4], aoRow[2], aoRow[3]); + oPeriod.appendRow(oPeriodRow, aoRow[0], oSubject); + + oSet.appendPeriod(oPeriod); + oSet.pruneRowsWithZeroSumHits(); + + + + # + # For reasons entering after the first period, look up the build and + # test set it first occured with. + # + oSet.finalizePass1(); + + for iPeriod in xrange(1, self.cPeriods): + oPeriod = oSet.aoPeriods[iPeriod]; + for idSubject, oSubject in oPeriod.dFirst.items(): + oSet.aoEnterInfo.append(self._getEdgeSimpleFailureOccurence(idSubject, sIdColumn, oSubject, + iPeriod, fEnter = True)); + + # Ditto for reasons leaving before the last. + for iPeriod in xrange(self.cPeriods - 1): + oPeriod = oSet.aoPeriods[iPeriod]; + for idSubject, oSubject in oPeriod.dLast.items(): + oSet.aoLeaveInfo.append(self._getEdgeSimpleFailureOccurence(idSubject, sIdColumn, oSubject, + iPeriod, fEnter = False)); + + oSet.finalizePass2(); + + return oSet; + + def _getEdgeSimpleFailureOccurence(self, idSubject, sIdColumn, oSubject, iPeriod, fEnter = True): + """ + Helper for the failure reason report that finds the oldest or newest build + (SVN rev) and test set (start time) it occured with. + + If fEnter is set the oldest occurence is return, if fEnter clear the newest + is is returned. + + Returns ReportTransientBase instant. + + """ + sSorting = 'ASC' if fEnter else 'DESC'; + sQuery = 'SELECT TestSets.idTestResult,\n' \ + ' TestSets.idTestSet,\n' \ + ' TestSets.tsDone,\n' \ + ' TestSets.idBuild,\n' \ + ' Builds.iRevision,\n' \ + ' BuildCategories.sRepository\n' \ + 'FROM TestSets\n' \ + + self.oFilter.getTableJoins(dOmitTables = {'Builds': True, 'BuildCategories': True}); + sQuery = sQuery[:-1] + ',\n' \ + ' Builds,\n' \ + ' BuildCategories\n'; + for sTable in self.getExtraSubjectTables(): + if sTable not in [ 'Builds', 'BuildCategories' ] and not self.oFilter.isJoiningWithTable(sTable): + sQuery = sQuery[:-1] + ',\n ' + sTable + '\n'; + sQuery += 'WHERE TestSets.' + sIdColumn + ' = ' + str(idSubject) + '\n' \ + ' AND TestSets.idBuild = Builds.idBuild\n' \ + ' AND TestSets.enmStatus >= \'failure\'\n' \ + + self.getExtraWhereExprForPeriod(iPeriod) + \ + ' AND Builds.tsExpire > TestSets.tsCreated\n' \ + ' AND Builds.tsEffective <= TestSets.tsCreated\n' \ + ' AND Builds.idBuildCategory = BuildCategories.idBuildCategory\n' \ + + self.oFilter.getWhereConditions() \ + + self.getExtraSubjectWhereExpr() + '\n' \ + 'ORDER BY Builds.iRevision ' + sSorting + ',\n' \ + ' TestSets.tsCreated ' + sSorting + '\n' \ + 'LIMIT 1\n'; + self._oDb.execute(sQuery); + aoRow = self._oDb.fetchOne(); + if aoRow is None: + return ReportTransientBase(-1, -1, 'internal-error', -1, -1, self._oDb.getCurrentTimestamp(), + iPeriod, fEnter, idSubject, oSubject); + return ReportTransientBase(idBuild = aoRow[3], iRevision = aoRow[4], sRepository = aoRow[5], + idTestSet = aoRow[1], idTestResult = aoRow[0], tsDone = aoRow[2], + iPeriod = iPeriod, fEnter = fEnter, idSubject = idSubject, oSubject = oSubject); + + def fetchPossibleFilterOptions(self, oFilter, tsNow, sPeriod): + """ + Fetches possible filtering options. + """ + return TestResultLogic(self._oDb).fetchPossibleFilterOptions(oFilter, tsNow, sPeriod, oReportModel = self); + + + +class ReportGraphModel(ReportModelBase): # pylint: disable=too-few-public-methods + """ + Extended report model used when generating the more complicated graphs + detailing results, time elapsed and values over time. + """ + + ## @name Subject ID types. + ## These prefix the values in the aidSubjects array. The prefix is + ## followed by a colon and then a list of string IDs. Following the prefix + ## is one or more string table IDs separated by colons. These are used to + ## drill down the exact test result we're looking for, by matching against + ## TestResult::idStrName (in the db). + ## @{ + ksTypeResult = 'result'; + ksTypeElapsed = 'elapsed'; + ## The last string table ID gives the name of the value. + ksTypeValue = 'value'; + ## List of types. + kasTypes = (ksTypeResult, ksTypeElapsed, ksTypeValue); + ## @} + + class SampleSource(object): + """ A sample source. """ + def __init__(self, sType, aidStrTests, idStrValue): + self.sType = sType; + self.aidStrTests = aidStrTests; + self.idStrValue = idStrValue; + + def getTestResultTables(self): + """ Retrieves the list of TestResults tables to join with.""" + sRet = ''; + for i in range(len(self.aidStrTests)): + sRet += ' TestResults TR%u,\n' % (i,); + return sRet; + + def getTestResultConditions(self): + """ Retrieves the join conditions for the TestResults tables.""" + sRet = ''; + cItems = len(self.aidStrTests); + for i in range(cItems - 1): + sRet += ' AND TR%u.idStrName = %u\n' \ + ' AND TR%u.idTestResultParent = TR%u.idTestResult\n' \ + % ( i, self.aidStrTests[cItems - i - 1], i, i + 1 ); + sRet += ' AND TR%u.idStrName = %u\n' % (cItems - 1, self.aidStrTests[0]); + return sRet; + + class DataSeries(object): + """ A data series. """ + def __init__(self, oCache, idBuildCategory, idTestBox, idTestCase, idTestCaseArgs, iUnit): + _ = oCache; + self.idBuildCategory = idBuildCategory; + self.oBuildCategory = oCache.getBuildCategory(idBuildCategory); + self.idTestBox = idTestBox; + self.oTestBox = oCache.getTestBox(idTestBox); + self.idTestCase = idTestCase; + self.idTestCaseArgs = idTestCaseArgs; + if idTestCase is not None: + self.oTestCase = oCache.getTestCase(idTestCase); + self.oTestCaseArgs = None; + else: + self.oTestCaseArgs = oCache.getTestCaseArgs(idTestCaseArgs); + self.oTestCase = oCache.getTestCase(self.oTestCaseArgs.idTestCase); + self.iUnit = iUnit; + # Six parallel arrays. + self.aiRevisions = []; # The X values. + self.aiValues = []; # The Y values. + self.aiErrorBarBelow = []; # The Y value minimum errorbars, relative to the Y value (positive). + self.aiErrorBarAbove = []; # The Y value maximum errorbars, relative to the Y value (positive). + self.acSamples = []; # The number of samples at this X value. + self.aoRevInfo = []; # VcsRevisionData objects for each revision. Empty/SQL-NULL objects if no info. + + class DataSeriesCollection(object): + """ A collection of data series corresponding to one input sample source. """ + def __init__(self, oSampleSrc, asTests, sValue = None): + self.sType = oSampleSrc.sType; + self.aidStrTests = oSampleSrc.aidStrTests; + self.asTests = list(asTests); + self.idStrValue = oSampleSrc.idStrValue; + self.sValue = sValue; + self.aoSeries = []; + + def addDataSeries(self, oDataSeries): + """ Appends a data series to the collection. """ + self.aoSeries.append(oDataSeries); + return oDataSeries; + + + def __init__(self, oDb, tsNow, cPeriods, cHoursPerPeriod, sSubject, aidSubjects, # pylint: disable=too-many-arguments + aidTestBoxes, aidBuildCats, aidTestCases, fSepTestVars): + assert(sSubject == self.ksSubEverything); # dummy + ReportModelBase.__init__(self, oDb, tsNow, cPeriods, cHoursPerPeriod, sSubject, aidSubjects, oFilter = None); + self.aidTestBoxes = aidTestBoxes; + self.aidBuildCats = aidBuildCats; + self.aidTestCases = aidTestCases; + self.fOnTestCase = not fSepTestVars; # (Separates testcase variations into separate data series.) + self.oCache = DatabaseObjCache(self._oDb, self.tsNow, None, self.cPeriods * self.cHoursPerPeriod); + + + # Quickly validate and convert the subject "IDs". + self.aoLookups = []; + for sCur in self.aidSubjects: + asParts = sCur.split(':'); + if len(asParts) < 2: + raise TMExceptionBase('Invalid graph value "%s"' % (sCur,)); + + sType = asParts[0]; + if sType not in ReportGraphModel.kasTypes: + raise TMExceptionBase('Invalid graph value type "%s" (full: "%s")' % (sType, sCur,)); + + aidStrTests = []; + for sIdStr in asParts[1:]: + try: idStr = int(sIdStr); + except: raise TMExceptionBase('Invalid graph value id "%s" (full: "%s")' % (sIdStr, sCur,)); + if idStr < 0: + raise TMExceptionBase('Invalid graph value id "%u" (full: "%s")' % (idStr, sCur,)); + aidStrTests.append(idStr); + + idStrValue = None; + if sType == ReportGraphModel.ksTypeValue: + idStrValue = aidStrTests.pop(); + self.aoLookups.append(ReportGraphModel.SampleSource(sType, aidStrTests, idStrValue)); + + # done + + + def getExtraWhereExprForTotalPeriod(self, sTimestampField): + """ + Returns additional WHERE expression for getting test sets for the + specified period. It starts with an AND so that it can simply be + appended to the WHERE clause. + """ + return self.getExtraWhereExprForTotalPeriodEx(sTimestampField, sTimestampField, True); + + def getExtraWhereExprForTotalPeriodEx(self, sStartField = 'tsCreated', sEndField = 'tsDone', fLeadingAnd = True): + """ + Returns additional WHERE expression for getting test sets for the + specified period. + """ + if self.tsNow is None: + sNow = 'CURRENT_TIMESTAMP'; + else: + sNow = self._oDb.formatBindArgs('%s::TIMESTAMP', (self.tsNow,)); + + sRet = ' AND %s >= (%s - interval \'%u hours\')\n' \ + ' AND %s <= %s\n' \ + % ( sStartField, sNow, self.cPeriods * self.cHoursPerPeriod, + sEndField, sNow); + + if not fLeadingAnd: + assert sRet[8] == ' ' and sRet[7] == 'D'; + return sRet[9:]; + return sRet; + + def _getEligibleTestSetPeriod(self, sPrefix = 'TestSets.', fLeadingAnd = False): + """ + Returns additional WHERE expression for getting TestSets rows + potentially relevant for the selected period. + """ + if self.tsNow is None: + sNow = 'CURRENT_TIMESTAMP'; + else: + sNow = self._oDb.formatBindArgs('%s::TIMESTAMP', (self.tsNow,)); + + # The 2nd line is a performance hack on TestSets. It nudges postgresql + # into useing the TestSetsCreatedDoneIdx index instead of doing a table + # scan when we look for eligible bits there. + # ASSUMES no relevant test runs longer than 7 days! + sRet = ' AND %stsCreated <= %s\n' \ + ' AND %stsCreated >= (%s - interval \'%u hours\' - interval \'%u days\')\n' \ + ' AND %stsDone >= (%s - interval \'%u hours\')\n' \ + % ( sPrefix, sNow, + sPrefix, sNow, self.cPeriods * self.cHoursPerPeriod, 7, + sPrefix, sNow, self.cPeriods * self.cHoursPerPeriod, ); + + if not fLeadingAnd: + assert sRet[8] == ' ' and sRet[7] == 'D'; + return sRet[9:]; + return sRet; + + + def _getNameStrings(self, aidStrTests): + """ Returns an array of names corresponding to the array of string table entries. """ + return [self.oCache.getTestResultString(idStr) for idStr in aidStrTests]; + + def fetchGraphData(self): + """ returns data """ + sWantedTestCaseId = 'idTestCase' if self.fOnTestCase else 'idTestCaseArgs'; + + aoRet = []; + for oLookup in self.aoLookups: + # + # Set up the result collection. + # + if oLookup.sType == self.ksTypeValue: + oCollection = self.DataSeriesCollection(oLookup, self._getNameStrings(oLookup.aidStrTests), + self.oCache.getTestResultString(oLookup.idStrValue)); + else: + oCollection = self.DataSeriesCollection(oLookup, self._getNameStrings(oLookup.aidStrTests)); + + # + # Construct the query. + # + sQuery = 'SELECT Builds.iRevision,\n' \ + ' TestSets.idBuildCategory,\n' \ + ' TestSets.idTestBox,\n' \ + ' TestSets.' + sWantedTestCaseId + ',\n'; + if oLookup.sType == self.ksTypeValue: + sQuery += ' TestResultValues.iUnit as iUnit,\n' \ + ' MIN(TestResultValues.lValue),\n' \ + ' CAST(ROUND(AVG(TestResultValues.lValue)) AS BIGINT),\n' \ + ' MAX(TestResultValues.lValue),\n' \ + ' COUNT(TestResultValues.lValue)\n'; + elif oLookup.sType == self.ksTypeElapsed: + sQuery += ' %u as iUnit,\n' \ + ' CAST((EXTRACT(EPOCH FROM MIN(TR0.tsElapsed)) * 1000) AS INTEGER),\n' \ + ' CAST((EXTRACT(EPOCH FROM AVG(TR0.tsElapsed)) * 1000) AS INTEGER),\n' \ + ' CAST((EXTRACT(EPOCH FROM MAX(TR0.tsElapsed)) * 1000) AS INTEGER),\n' \ + ' COUNT(TR0.tsElapsed)\n' \ + % (constants.valueunit.MS,); + else: + sQuery += ' %u as iUnit,\n'\ + ' MIN(TR0.cErrors),\n' \ + ' CAST(ROUND(AVG(TR0.cErrors)) AS INTEGER),\n' \ + ' MAX(TR0.cErrors),\n' \ + ' COUNT(TR0.cErrors)\n' \ + % (constants.valueunit.OCCURRENCES,); + + if oLookup.sType == self.ksTypeValue: + sQuery += 'FROM TestResultValues,\n'; + sQuery += ' TestSets,\n' + sQuery += oLookup.getTestResultTables(); + else: + sQuery += 'FROM ' + oLookup.getTestResultTables().lstrip(); + sQuery += ' TestSets,\n'; + sQuery += ' Builds\n'; + + if oLookup.sType == self.ksTypeValue: + sQuery += 'WHERE TestResultValues.idStrName = %u\n' % ( oLookup.idStrValue, ); + sQuery += self.getExtraWhereExprForTotalPeriod('TestResultValues.tsCreated'); + sQuery += ' AND TestResultValues.idTestSet = TestSets.idTestSet\n'; + sQuery += self._getEligibleTestSetPeriod(fLeadingAnd = True); + else: + sQuery += 'WHERE ' + (self.getExtraWhereExprForTotalPeriod('TR0.tsCreated').lstrip()[4:]).lstrip(); + sQuery += ' AND TR0.idTestSet = TestSets.idTestSet\n'; + + if len(self.aidTestBoxes) == 1: + sQuery += ' AND TestSets.idTestBox = %u\n' % (self.aidTestBoxes[0],); + elif self.aidTestBoxes: + sQuery += ' AND TestSets.idTestBox IN (' + ','.join([str(i) for i in self.aidTestBoxes]) + ')\n'; + + if len(self.aidBuildCats) == 1: + sQuery += ' AND TestSets.idBuildCategory = %u\n' % (self.aidBuildCats[0],); + elif self.aidBuildCats: + sQuery += ' AND TestSets.idBuildCategory IN (' + ','.join([str(i) for i in self.aidBuildCats]) + ')\n'; + + if len(self.aidTestCases) == 1: + sQuery += ' AND TestSets.idTestCase = %u\n' % (self.aidTestCases[0],); + elif self.aidTestCases: + sQuery += ' AND TestSets.idTestCase IN (' + ','.join([str(i) for i in self.aidTestCases]) + ')\n'; + + if oLookup.sType == self.ksTypeElapsed: + sQuery += ' AND TestSets.enmStatus = \'%s\'::TestStatus_T\n' % (self.ksTestStatus_Success,); + + if oLookup.sType == self.ksTypeValue: + sQuery += ' AND TestResultValues.idTestResult = TR0.idTestResult\n' + sQuery += self.getExtraWhereExprForTotalPeriod('TR0.tsCreated'); # For better index matching in some cases. + + if oLookup.sType != self.ksTypeResult: + sQuery += ' AND TR0.enmStatus = \'%s\'::TestStatus_T\n' % (self.ksTestStatus_Success,); + + sQuery += oLookup.getTestResultConditions(); + sQuery += ' AND TestSets.idBuild = Builds.idBuild\n'; + + sQuery += 'GROUP BY TestSets.idBuildCategory,\n' \ + ' TestSets.idTestBox,\n' \ + ' TestSets.' + sWantedTestCaseId + ',\n' \ + ' iUnit,\n' \ + ' Builds.iRevision\n'; + sQuery += 'ORDER BY TestSets.idBuildCategory,\n' \ + ' TestSets.idTestBox,\n' \ + ' TestSets.' + sWantedTestCaseId + ',\n' \ + ' iUnit,\n' \ + ' Builds.iRevision\n'; + + # + # Execute it and collect the result. + # + sCurRepository = None; + dRevisions = {}; + oLastSeries = None; + idLastBuildCat = -1; + idLastTestBox = -1; + idLastTestCase = -1; + iLastUnit = -1; + self._oDb.execute(sQuery); + for aoRow in self._oDb.fetchAll(): # Fetching all here so we can make cache queries below. + if aoRow[1] != idLastBuildCat \ + or aoRow[2] != idLastTestBox \ + or aoRow[3] != idLastTestCase \ + or aoRow[4] != iLastUnit: + idLastBuildCat = aoRow[1]; + idLastTestBox = aoRow[2]; + idLastTestCase = aoRow[3]; + iLastUnit = aoRow[4]; + if self.fOnTestCase: + oLastSeries = self.DataSeries(self.oCache, idLastBuildCat, idLastTestBox, + idLastTestCase, None, iLastUnit); + else: + oLastSeries = self.DataSeries(self.oCache, idLastBuildCat, idLastTestBox, + None, idLastTestCase, iLastUnit); + oCollection.addDataSeries(oLastSeries); + if oLastSeries.oBuildCategory.sRepository != sCurRepository: + if sCurRepository is not None: + self.oCache.preloadVcsRevInfo(sCurRepository, dRevisions.keys()); + sCurRepository = oLastSeries.oBuildCategory.sRepository + dRevisions = {}; + oLastSeries.aiRevisions.append(aoRow[0]); + oLastSeries.aiValues.append(aoRow[6]); + oLastSeries.aiErrorBarBelow.append(aoRow[6] - aoRow[5]); + oLastSeries.aiErrorBarAbove.append(aoRow[7] - aoRow[6]); + oLastSeries.acSamples.append(aoRow[8]); + dRevisions[aoRow[0]] = 1; + + if sCurRepository is not None: + self.oCache.preloadVcsRevInfo(sCurRepository, dRevisions.keys()); + del dRevisions; + + # + # Look up the VCS revision details. + # + for oSeries in oCollection.aoSeries: + for iRevision in oSeries.aiRevisions: + oSeries.aoRevInfo.append(self.oCache.getVcsRevInfo(sCurRepository, iRevision)); + aoRet.append(oCollection); + + return aoRet; + + def getEligibleTestBoxes(self): + """ + Returns a list of TestBoxData objects with eligible testboxes for + the total period of time defined for this graph. + """ + + # Taking the simple way out now, getting all active testboxes at the + # time without filtering out on sample sources. + + # 1. Collect the relevant testbox generation IDs. + self._oDb.execute('SELECT DISTINCT idTestBox, idGenTestBox\n' + 'FROM TestSets\n' + 'WHERE ' + self._getEligibleTestSetPeriod(fLeadingAnd = False) + + 'ORDER BY idTestBox, idGenTestBox DESC'); + idPrevTestBox = -1; + asIdGenTestBoxes = []; + for _ in range(self._oDb.getRowCount()): + aoRow = self._oDb.fetchOne(); + if aoRow[0] != idPrevTestBox: + idPrevTestBox = aoRow[0]; + asIdGenTestBoxes.append(str(aoRow[1])); + + # 2. Query all the testbox data in one go. + aoRet = []; + if asIdGenTestBoxes: + self._oDb.execute('SELECT *\n' + 'FROM TestBoxesWithStrings\n' + 'WHERE idGenTestBox IN (' + ','.join(asIdGenTestBoxes) + ')\n' + 'ORDER BY sName'); + for _ in range(self._oDb.getRowCount()): + aoRet.append(TestBoxData().initFromDbRow(self._oDb.fetchOne())); + + return aoRet; + + def getEligibleBuildCategories(self): + """ + Returns a list of BuildCategoryData objects with eligible build + categories for the total period of time defined for this graph. In + addition it will add any currently selected categories that aren't + really relevant to the period, just to simplify the WUI code. + + """ + + # Taking the simple way out now, getting all used build cat without + # any testbox or testcase filtering. + + sSelectedBuildCats = ''; + if self.aidBuildCats: + sSelectedBuildCats = ' OR idBuildCategory IN (' + ','.join([str(i) for i in self.aidBuildCats]) + ')\n'; + + self._oDb.execute('SELECT DISTINCT *\n' + 'FROM BuildCategories\n' + 'WHERE idBuildCategory IN (\n' + ' SELECT DISTINCT idBuildCategory\n' + ' FROM TestSets\n' + ' WHERE ' + self._getEligibleTestSetPeriod(fLeadingAnd = False) + + ')\n' + + sSelectedBuildCats + + 'ORDER BY sProduct,\n' + ' sBranch,\n' + ' asOsArches,\n' + ' sType\n'); + aoRet = []; + for _ in range(self._oDb.getRowCount()): + aoRet.append(BuildCategoryData().initFromDbRow(self._oDb.fetchOne())); + + return aoRet; + diff --git a/src/VBox/ValidationKit/testmanager/core/restdispatcher.py b/src/VBox/ValidationKit/testmanager/core/restdispatcher.py new file mode 100755 index 00000000..75a1aa7c --- /dev/null +++ b/src/VBox/ValidationKit/testmanager/core/restdispatcher.py @@ -0,0 +1,455 @@ +# -*- coding: utf-8 -*- +# $Id: restdispatcher.py $ + +""" +Test Manager Core - REST cgi handler. +""" + +__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 sys; + +# Validation Kit imports. +#from common import constants; +from common import utils; +from testmanager import config; +#from testmanager.core import coreconsts; +from testmanager.core.db import TMDatabaseConnection; +from testmanager.core.base import TMExceptionBase, ModelDataBase; + +# Python 3 hacks: +if sys.version_info[0] >= 3: + long = int; # pylint: disable=redefined-builtin,invalid-name + + +# +# Exceptions +# + +class RestDispException(TMExceptionBase): + """ + Exception class for the REST dispatcher. + """ + def __init__(self, sMsg, iStatus): + TMExceptionBase.__init__(self, sMsg); + self.iStatus = iStatus; + +# 400 +class RestDispException400(RestDispException): + """ A 400 error """ + def __init__(self, sMsg): + RestDispException.__init__(self, sMsg, 400); + +class RestUnknownParameters(RestDispException400): + """ Unknown parameter(s). """ + pass; # pylint: disable=unnecessary-pass + +# 404 +class RestDispException404(RestDispException): + """ A 404 error """ + def __init__(self, sMsg): + RestDispException.__init__(self, sMsg, 404); + +class RestBadPathException(RestDispException404): + """ We've got a bad path. """ + pass; # pylint: disable=unnecessary-pass + +class RestBadParameter(RestDispException404): + """ Bad parameter. """ + pass; # pylint: disable=unnecessary-pass + +class RestMissingParameter(RestDispException404): + """ Missing parameter. """ + pass; # pylint: disable=unnecessary-pass + + + +class RestMain(object): # pylint: disable=too-few-public-methods + """ + REST main dispatcher class. + """ + + ksParam_sPath = 'sPath'; + + + def __init__(self, oSrvGlue): + self._oSrvGlue = oSrvGlue; + self._oDb = TMDatabaseConnection(oSrvGlue.dprint); + self._iFirstHandlerPath = 0; + self._iNextHandlerPath = 0; + self._sPath = None; # _getStandardParams / dispatchRequest sets this later on. + self._asPath = None; # _getStandardParams / dispatchRequest sets this later on. + self._sMethod = None; # _getStandardParams / dispatchRequest sets this later on. + self._dParams = None; # _getStandardParams / dispatchRequest sets this later on. + self._asCheckedParams = []; + self._dGetTree = { + 'vcs': { + 'changelog': self._handleVcsChangelog_Get, + 'bugreferences': self._handleVcsBugReferences_Get, + }, + }; + self._dMethodTrees = { + 'GET': self._dGetTree, + } + + # + # Helpers. + # + + def _getStringParam(self, sName, asValidValues = None, fStrip = False, sDefValue = None): + """ + Gets a string parameter (stripped). + + Raises exception if not found and no default is provided, or if the + value isn't found in asValidValues. + """ + if sName not in self._dParams: + if sDefValue is None: + raise RestMissingParameter('%s parameter %s is missing' % (self._sPath, sName)); + return sDefValue; + sValue = self._dParams[sName]; + if isinstance(sValue, list): + if len(sValue) == 1: + sValue = sValue[0]; + else: + raise RestBadParameter('%s parameter %s value is not a string but list: %s' + % (self._sPath, sName, sValue)); + if fStrip: + sValue = sValue.strip(); + + if sName not in self._asCheckedParams: + self._asCheckedParams.append(sName); + + if asValidValues is not None and sValue not in asValidValues: + raise RestBadParameter('%s parameter %s value "%s" not in %s ' + % (self._sPath, sName, sValue, asValidValues)); + return sValue; + + def _getBoolParam(self, sName, fDefValue = None): + """ + Gets a boolean parameter. + + Raises exception if not found and no default is provided, or if not a + valid boolean. + """ + sValue = self._getStringParam(sName, [ 'True', 'true', '1', 'False', 'false', '0'], sDefValue = str(fDefValue)); + return sValue in ('True', 'true', '1',); + + def _getIntParam(self, sName, iMin = None, iMax = None): + """ + Gets a string parameter. + Raises exception if not found, not a valid integer, or if the value + isn't in the range defined by iMin and iMax. + """ + sValue = self._getStringParam(sName); + try: + iValue = int(sValue, 0); + except: + raise RestBadParameter('%s parameter %s value "%s" cannot be convert to an integer' + % (self._sPath, sName, sValue)); + + if (iMin is not None and iValue < iMin) \ + or (iMax is not None and iValue > iMax): + raise RestBadParameter('%s parameter %s value %d is out of range [%s..%s]' + % (self._sPath, sName, iValue, iMin, iMax)); + return iValue; + + def _getLongParam(self, sName, lMin = None, lMax = None, lDefValue = None): + """ + Gets a string parameter. + Raises exception if not found, not a valid long integer, or if the value + isn't in the range defined by lMin and lMax. + """ + sValue = self._getStringParam(sName, sDefValue = (str(lDefValue) if lDefValue is not None else None)); + try: + lValue = long(sValue, 0); + except Exception as oXcpt: + raise RestBadParameter('%s parameter %s value "%s" cannot be convert to an integer (%s)' + % (self._sPath, sName, sValue, oXcpt)); + + if (lMin is not None and lValue < lMin) \ + or (lMax is not None and lValue > lMax): + raise RestBadParameter('%s parameter %s value %d is out of range [%s..%s]' + % (self._sPath, sName, lValue, lMin, lMax)); + return lValue; + + def _checkForUnknownParameters(self): + """ + Check if we've handled all parameters, raises exception if anything + unknown was found. + """ + + if len(self._asCheckedParams) != len(self._dParams): + sUnknownParams = ''; + for sKey in self._dParams: + if sKey not in self._asCheckedParams: + sUnknownParams += ' ' + sKey + '=' + self._dParams[sKey]; + raise RestUnknownParameters('Unknown parameters: ' + sUnknownParams); + + return True; + + def writeToMainLog(self, oTestSet, sText, fIgnoreSizeCheck = False): + """ Writes the text to the main log file. """ + + # Calc the file name and open the file. + sFile = os.path.join(config.g_ksFileAreaRootDir, oTestSet.sBaseFilename + '-main.log'); + if not os.path.exists(os.path.dirname(sFile)): + os.makedirs(os.path.dirname(sFile), 0o755); + + with open(sFile, 'ab') as oFile: + # Check the size. + fSizeOk = True; + if not fIgnoreSizeCheck: + oStat = os.fstat(oFile.fileno()); + fSizeOk = oStat.st_size / (1024 * 1024) < config.g_kcMbMaxMainLog; + + # Write the text. + if fSizeOk: + if sys.version_info[0] >= 3: + oFile.write(bytes(sText, 'utf-8')); + else: + oFile.write(sText); + + return fSizeOk; + + def _getNextPathElementString(self, sName, oDefault = None): + """ + Gets the next handler specific path element. + Returns unprocessed string. + Throws exception + """ + i = self._iNextHandlerPath; + if i < len(self._asPath): + self._iNextHandlerPath = i + 1; + return self._asPath[i]; + if oDefault is None: + raise RestBadPathException('Requires a "%s" element after "%s"' % (sName, self._sPath,)); + return oDefault; + + def _getNextPathElementInt(self, sName, iDefault = None, iMin = None, iMax = None): + """ + Gets the next handle specific path element as an integer. + Returns integer value. + Throws exception if not found or not a valid integer. + """ + sValue = self._getNextPathElementString(sName, oDefault = iDefault); + try: + iValue = int(sValue); + except: + raise RestBadPathException('Not an integer "%s" (%s)' % (sValue, sName,)); + if iMin is not None and iValue < iMin: + raise RestBadPathException('Integer "%s" value (%s) is too small, min %s' % (sValue, sName, iMin)); + if iMax is not None and iValue > iMax: + raise RestBadPathException('Integer "%s" value (%s) is too large, max %s' % (sValue, sName, iMax)); + return iValue; + + def _getNextPathElementLong(self, sName, iDefault = None, iMin = None, iMax = None): + """ + Gets the next handle specific path element as a long integer. + Returns integer value. + Throws exception if not found or not a valid integer. + """ + sValue = self._getNextPathElementString(sName, oDefault = iDefault); + try: + iValue = long(sValue); + except: + raise RestBadPathException('Not an integer "%s" (%s)' % (sValue, sName,)); + if iMin is not None and iValue < iMin: + raise RestBadPathException('Integer "%s" value (%s) is too small, min %s' % (sValue, sName, iMin)); + if iMax is not None and iValue > iMax: + raise RestBadPathException('Integer "%s" value (%s) is too large, max %s' % (sValue, sName, iMax)); + return iValue; + + def _checkNoMorePathElements(self): + """ + Checks that there are no more path elements. + Throws exception if there are. + """ + i = self._iNextHandlerPath; + if i < len(self._asPath): + raise RestBadPathException('Unknown subpath "%s" below "%s"' % + ('/'.join(self._asPath[i:]), '/'.join(self._asPath[:i]),)); + return True; + + def _doneParsingArguments(self): + """ + Checks that there are no more path elements or unhandled parameters. + Throws exception if there are. + """ + self._checkNoMorePathElements(); + self._checkForUnknownParameters(); + return True; + + def _dataArrayToJsonReply(self, aoData, sName = 'aoData', dExtraFields = None, iStatus = 200): + """ + Converts aoData into an array objects + return True. + """ + self._oSrvGlue.setContentType('application/json'); + self._oSrvGlue.setStatus(iStatus); + self._oSrvGlue.write(u'{\n'); + if dExtraFields: + for sKey in dExtraFields: + self._oSrvGlue.write(u' "%s": %s,\n' % (sKey, ModelDataBase.genericToJson(dExtraFields[sKey]),)); + self._oSrvGlue.write(u' "c%s": %u,\n' % (sName[2:],len(aoData),)); + self._oSrvGlue.write(u' "%s": [\n' % (sName,)); + for i, oData in enumerate(aoData): + if i > 0: + self._oSrvGlue.write(u',\n'); + self._oSrvGlue.write(ModelDataBase.genericToJson(oData)); + self._oSrvGlue.write(u' ]\n'); + ## @todo if config.g_kfWebUiSqlDebug: + self._oSrvGlue.write(u'}\n'); + self._oSrvGlue.flush(); + return True; + + + # + # Handlers. + # + + def _handleVcsChangelog_Get(self): + """ GET /vcs/changelog/{sRepository}/{iStartRev}[/{cEntriesBack}] """ + # Parse arguments + sRepository = self._getNextPathElementString('sRepository'); + iStartRev = self._getNextPathElementInt('iStartRev', iMin = 0); + cEntriesBack = self._getNextPathElementInt('cEntriesBack', iDefault = 32, iMin = 0, iMax = 8192); + self._checkNoMorePathElements(); + self._checkForUnknownParameters(); + + # Execute it. + from testmanager.core.vcsrevisions import VcsRevisionLogic; + oLogic = VcsRevisionLogic(self._oDb); + return self._dataArrayToJsonReply(oLogic.fetchTimeline(sRepository, iStartRev, cEntriesBack), 'aoCommits', + { 'sTracChangesetUrlFmt': + config.g_ksTracChangsetUrlFmt.replace('%(sRepository)s', sRepository), } ); + + def _handleVcsBugReferences_Get(self): + """ GET /vcs/bugreferences/{sTrackerId}/{lBugId} """ + # Parse arguments + sTrackerId = self._getNextPathElementString('sTrackerId'); + lBugId = self._getNextPathElementLong('lBugId', iMin = 0); + self._checkNoMorePathElements(); + self._checkForUnknownParameters(); + + # Execute it. + from testmanager.core.vcsbugreference import VcsBugReferenceLogic; + oLogic = VcsBugReferenceLogic(self._oDb); + oLogic.fetchForBug(sTrackerId, lBugId) + return self._dataArrayToJsonReply(oLogic.fetchForBug(sTrackerId, lBugId), 'aoCommits', + { 'sTracChangesetUrlFmt': config.g_ksTracChangsetUrlFmt, } ); + + + # + # Dispatching. + # + + def _dispatchRequestCommon(self): + """ + Dispatches the incoming request after have gotten the path and parameters. + + Will raise RestDispException on failure. + """ + + # + # Split up the path. + # + asPath = self._sPath.split('/'); + self._asPath = asPath; + + # + # Get the method and the corresponding handler tree. + # + try: + sMethod = self._oSrvGlue.getMethod(); + except Exception as oXcpt: + raise RestDispException('Error retriving request method: %s' % (oXcpt,), 400); + self._sMethod = sMethod; + + try: + dTree = self._dMethodTrees[sMethod]; + except KeyError: + raise RestDispException('Unsupported method %s' % (sMethod,), 405); + + # + # Walk the path till we find a handler for it. + # + iPath = 0; + while iPath < len(asPath): + try: + oTreeOrHandler = dTree[asPath[iPath]]; + except KeyError: + raise RestBadPathException('Path element #%u "%s" not found (path="%s")' % (iPath, asPath[iPath], self._sPath)); + iPath += 1; + if isinstance(oTreeOrHandler, dict): + dTree = oTreeOrHandler; + else: + # + # Call the handler. + # + self._iFirstHandlerPath = iPath; + self._iNextHandlerPath = iPath; + return oTreeOrHandler(); + + raise RestBadPathException('Empty path (%s)' % (self._sPath,)); + + def dispatchRequest(self): + """ + Dispatches the incoming request where the path is given as an argument. + + Will raise RestDispException on failure. + """ + + # + # Get the parameters. + # + try: + dParams = self._oSrvGlue.getParameters(); + except Exception as oXcpt: + raise RestDispException('Error retriving parameters: %s' % (oXcpt,), 500); + self._dParams = dParams; + + # + # Get the path parameter. + # + if self.ksParam_sPath not in dParams: + raise RestDispException('No "%s" parameter in request (params: %s)' % (self.ksParam_sPath, dParams,), 500); + self._sPath = self._getStringParam(self.ksParam_sPath); + assert utils.isString(self._sPath); + + return self._dispatchRequestCommon(); + diff --git a/src/VBox/ValidationKit/testmanager/core/schedgroup.py b/src/VBox/ValidationKit/testmanager/core/schedgroup.py new file mode 100755 index 00000000..a586a6bf --- /dev/null +++ b/src/VBox/ValidationKit/testmanager/core/schedgroup.py @@ -0,0 +1,1352 @@ +# -*- coding: utf-8 -*- +# $Id: schedgroup.py $ + +""" +Test Manager - Scheduling Group. +""" + +__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 unittest; + +# Validation Kit imports. +from testmanager.core.base import ModelDataBase, ModelDataBaseTestCase, ModelLogicBase, TMExceptionBase, \ + TMRowInUse, TMInvalidData, TMRowAlreadyExists, TMRowNotFound, \ + ChangeLogEntry, AttributeChangeEntry, AttributeChangeEntryPre; +from testmanager.core.buildsource import BuildSourceData; +from testmanager.core import db; +from testmanager.core.testcase import TestCaseData; +from testmanager.core.testcaseargs import TestCaseArgsData; +from testmanager.core.testbox import TestBoxLogic, TestBoxDataForSchedGroup; +from testmanager.core.testgroup import TestGroupData; +from testmanager.core.useraccount import UserAccountLogic; + + + +class SchedGroupMemberData(ModelDataBase): + """ + SchedGroupMember Data. + """ + + ksIdAttr = 'idSchedGroup'; + + ksParam_idSchedGroup = 'SchedGroupMember_idSchedGroup'; + ksParam_idTestGroup = 'SchedGroupMember_idTestGroup'; + ksParam_tsEffective = 'SchedGroupMember_tsEffective'; + ksParam_tsExpire = 'SchedGroupMember_tsExpire'; + ksParam_uidAuthor = 'SchedGroupMember_uidAuthor'; + ksParam_iSchedPriority = 'SchedGroupMember_iSchedPriority'; + ksParam_bmHourlySchedule = 'SchedGroupMember_bmHourlySchedule'; + ksParam_idTestGroupPreReq = 'SchedGroupMember_idTestGroupPreReq'; + + kasAllowNullAttributes = [ 'idSchedGroup', 'idTestGroup', 'tsEffective', 'tsExpire', + 'uidAuthor', 'bmHourlySchedule', 'idTestGroupPreReq' ]; + kiMin_iSchedPriority = 0; + kiMax_iSchedPriority = 32; + + kcDbColumns = 8 + + def __init__(self): + ModelDataBase.__init__(self); + + # + # Initialize with defaults. + # See the database for explanations of each of these fields. + # + self.idSchedGroup = None; + self.idTestGroup = None; + self.tsEffective = None; + self.tsExpire = None; + self.uidAuthor = None; + self.iSchedPriority = 16; + self.bmHourlySchedule = None; + self.idTestGroupPreReq = None; + + def initFromDbRow(self, aoRow): + """ + Re-initializes the data with a row from a SELECT * FROM SchedGroupMembers. + + Returns self. Raises exception if the row is None or otherwise invalid. + """ + + if aoRow is None: + raise TMRowNotFound('SchedGroupMember not found.'); + + self.idSchedGroup = aoRow[0]; + self.idTestGroup = aoRow[1]; + self.tsEffective = aoRow[2]; + self.tsExpire = aoRow[3]; + self.uidAuthor = aoRow[4]; + self.iSchedPriority = aoRow[5]; + self.bmHourlySchedule = aoRow[6]; ## @todo figure out how bitmaps are returned... + self.idTestGroupPreReq = aoRow[7]; + return self; + + +class SchedGroupMemberDataEx(SchedGroupMemberData): + """ + Extended SchedGroupMember data class. + This adds the testgroups. + """ + + def __init__(self): + SchedGroupMemberData.__init__(self); + self.oTestGroup = None; + + def initFromDbRow(self, aoRow): + """ + Re-initializes the data with a row from a query like this: + + SELECT SchedGroupMembers.*, TestGroups.* + FROM SchedGroupMembers + JOIN TestGroups + ON (SchedGroupMembers.idTestGroup = TestGroups.idTestGroup); + + Returns self. Raises exception if the row is None or otherwise invalid. + """ + SchedGroupMemberData.initFromDbRow(self, aoRow); + self.oTestGroup = TestGroupData().initFromDbRow(aoRow[SchedGroupMemberData.kcDbColumns:]); + return self; + + def getDataAttributes(self): + asAttributes = SchedGroupMemberData.getDataAttributes(self); + asAttributes.remove('oTestGroup'); + return asAttributes; + + def _validateAndConvertWorker(self, asAllowNullAttributes, oDb, enmValidateFor = ModelDataBase.ksValidateFor_Other): + dErrors = SchedGroupMemberData._validateAndConvertWorker(self, asAllowNullAttributes, oDb, enmValidateFor); + if self.ksParam_idTestGroup not in dErrors: + self.oTestGroup = TestGroupData(); + try: + self.oTestGroup.initFromDbWithId(oDb, self.idTestGroup); + except Exception as oXcpt: + self.oTestGroup = TestGroupData() + dErrors[self.ksParam_idTestGroup] = str(oXcpt); + return dErrors; + + + + +class SchedGroupData(ModelDataBase): + """ + SchedGroup Data. + """ + + ## @name TestBoxState_T + # @{ + ksScheduler_BestEffortContinuousIntegration = 'bestEffortContinousItegration'; # sic*2 + ksScheduler_Reserved = 'reserved'; + ## @} + + + ksIdAttr = 'idSchedGroup'; + + ksParam_idSchedGroup = 'SchedGroup_idSchedGroup'; + ksParam_tsEffective = 'SchedGroup_tsEffective'; + ksParam_tsExpire = 'SchedGroup_tsExpire'; + ksParam_uidAuthor = 'SchedGroup_uidAuthor'; + ksParam_sName = 'SchedGroup_sName'; + ksParam_sDescription = 'SchedGroup_sDescription'; + ksParam_fEnabled = 'SchedGroup_fEnabled'; + ksParam_enmScheduler = 'SchedGroup_enmScheduler'; + ksParam_idBuildSrc = 'SchedGroup_idBuildSrc'; + ksParam_idBuildSrcTestSuite = 'SchedGroup_idBuildSrcTestSuite'; + ksParam_sComment = 'SchedGroup_sComment'; + + kasAllowNullAttributes = ['idSchedGroup', 'tsEffective', 'tsExpire', 'uidAuthor', 'sDescription', + 'idBuildSrc', 'idBuildSrcTestSuite', 'sComment' ]; + kasValidValues_enmScheduler = [ ksScheduler_BestEffortContinuousIntegration, ]; + + kcDbColumns = 11; + + # Scheduler types + kasSchedulerDesc = \ + [ + ( ksScheduler_BestEffortContinuousIntegration, 'Best-Effort-Continuous-Integration (BECI) scheduler.', ''), + ] + + def __init__(self): + ModelDataBase.__init__(self); + + # + # Initialize with defaults. + # See the database for explanations of each of these fields. + # + self.idSchedGroup = None; + self.tsEffective = None; + self.tsExpire = None; + self.uidAuthor = None; + self.sName = None; + self.sDescription = None; + self.fEnabled = None; + self.enmScheduler = SchedGroupData.ksScheduler_BestEffortContinuousIntegration; + self.idBuildSrc = None; + self.idBuildSrcTestSuite = None; + self.sComment = None; + + def initFromDbRow(self, aoRow): + """ + Re-initializes the data with a row from a SELECT * FROM SchedGroups. + + Returns self. Raises exception if the row is None or otherwise invalid. + """ + + if aoRow is None: + raise TMRowNotFound('SchedGroup not found.'); + + self.idSchedGroup = aoRow[0]; + self.tsEffective = aoRow[1]; + self.tsExpire = aoRow[2]; + self.uidAuthor = aoRow[3]; + self.sName = aoRow[4]; + self.sDescription = aoRow[5]; + self.fEnabled = aoRow[6]; + self.enmScheduler = aoRow[7]; + self.idBuildSrc = aoRow[8]; + self.idBuildSrcTestSuite = aoRow[9]; + self.sComment = aoRow[10]; + return self; + + def initFromDbWithId(self, oDb, idSchedGroup, tsNow = None, sPeriodBack = None): + """ + Initialize the object from the database. + """ + oDb.execute(self.formatSimpleNowAndPeriodQuery(oDb, + 'SELECT *\n' + 'FROM SchedGroups\n' + 'WHERE idSchedGroup = %s\n' + , ( idSchedGroup,), tsNow, sPeriodBack)); + aoRow = oDb.fetchOne() + if aoRow is None: + raise TMRowNotFound('idSchedGroup=%s not found (tsNow=%s, sPeriodBack=%s)' % (idSchedGroup, tsNow, sPeriodBack)); + return self.initFromDbRow(aoRow); + + +class SchedGroupDataEx(SchedGroupData): + """ + Extended scheduling group data. + + Note! Similar to TestGroupDataEx. + """ + + ksParam_aoMembers = 'SchedGroup_aoMembers'; + ksParam_aoTestBoxes = 'SchedGroup_aoTestboxes'; + kasAltArrayNull = [ 'aoMembers', 'aoTestboxes' ]; + + ## Helper parameter containing the comma separated list with the IDs of + # potential members found in the parameters. + ksParam_aidTestGroups = 'TestGroupDataEx_aidTestGroups'; + ## Ditto for testbox meembers. + ksParam_aidTestBoxes = 'TestGroupDataEx_aidTestBoxes'; + + + def __init__(self): + SchedGroupData.__init__(self); + self.aoMembers = [] # type: list[SchedGroupMemberDataEx] + self.aoTestBoxes = [] # type: list[TestBoxDataForSchedGroup] + + # The two build sources for the sake of convenience. + self.oBuildSrc = None # type: BuildSourceData + self.oBuildSrcValidationKit = None # type: BuildSourceData + + def _initExtraMembersFromDb(self, oDb, tsNow = None, sPeriodBack = None): + """ + Worker shared by the initFromDb* methods. + Returns self. Raises exception if no row or database error. + """ + # + # Clear all members upfront so the object has some kind of consistency + # if anything below raises exceptions. + # + self.oBuildSrc = None; + self.oBuildSrcValidationKit = None; + self.aoTestBoxes = []; + self.aoMembers = []; + + # + # Build source. + # + if self.idBuildSrc: + self.oBuildSrc = BuildSourceData().initFromDbWithId(oDb, self.idBuildSrc, tsNow, sPeriodBack); + + if self.idBuildSrcTestSuite: + self.oBuildSrcValidationKit = BuildSourceData().initFromDbWithId(oDb, self.idBuildSrcTestSuite, + tsNow, sPeriodBack); + + # + # Test Boxes. + # + self.aoTestBoxes = TestBoxLogic(oDb).fetchForSchedGroup(self.idSchedGroup, tsNow); + + # + # Test groups. + # The fetchForChangeLog method makes ASSUMPTIONS about sorting! + # + oDb.execute('SELECT SchedGroupMembers.*, TestGroups.*\n' + 'FROM SchedGroupMembers\n' + 'LEFT OUTER JOIN TestGroups ON (SchedGroupMembers.idTestGroup = TestGroups.idTestGroup)\n' + 'WHERE SchedGroupMembers.idSchedGroup = %s\n' + + self.formatSimpleNowAndPeriod(oDb, tsNow, sPeriodBack, sTablePrefix = 'SchedGroupMembers.') + + self.formatSimpleNowAndPeriod(oDb, tsNow, sPeriodBack, sTablePrefix = 'TestGroups.') + + 'ORDER BY SchedGroupMembers.idTestGroupPreReq ASC NULLS FIRST,\n' + ' TestGroups.sName,\n' + ' SchedGroupMembers.idTestGroup\n' + , (self.idSchedGroup,)); + for aoRow in oDb.fetchAll(): + self.aoMembers.append(SchedGroupMemberDataEx().initFromDbRow(aoRow)); + return self; + + def initFromDbRowEx(self, aoRow, oDb, tsNow = None): + """ + Reinitialize from a SELECT * FROM SchedGroups row. Will query the + necessary additional data from oDb using tsNow. + Returns self. Raises exception if no row or database error. + """ + SchedGroupData.initFromDbRow(self, aoRow); + return self._initExtraMembersFromDb(oDb, tsNow); + + def initFromDbWithId(self, oDb, idSchedGroup, tsNow = None, sPeriodBack = None): + """ + Initialize the object from the database. + """ + SchedGroupData.initFromDbWithId(self, oDb, idSchedGroup, tsNow, sPeriodBack); + return self._initExtraMembersFromDb(oDb, tsNow, sPeriodBack); + + def getDataAttributes(self): + asAttributes = SchedGroupData.getDataAttributes(self); + asAttributes.remove('oBuildSrc'); + asAttributes.remove('oBuildSrcValidationKit'); + return asAttributes; + + def getAttributeParamNullValues(self, sAttr): + if sAttr not in [ 'aoMembers', 'aoTestBoxes' ]: + return SchedGroupData.getAttributeParamNullValues(self, sAttr); + return ['', [], None]; + + def convertParamToAttribute(self, sAttr, sParam, oValue, oDisp, fStrict): + aoNewValue = []; + if sAttr == 'aoMembers': + aidSelected = oDisp.getListOfIntParams(sParam, iMin = 1, iMax = 0x7ffffffe, aiDefaults = []) + sIds = oDisp.getStringParam(self.ksParam_aidTestGroups, sDefault = ''); + for idTestGroup in sIds.split(','): + try: idTestGroup = int(idTestGroup); + except: pass; + oDispWrapper = self.DispWrapper(oDisp, '%s[%s][%%s]' % (SchedGroupDataEx.ksParam_aoMembers, idTestGroup,)) + oMember = SchedGroupMemberDataEx().initFromParams(oDispWrapper, fStrict = False); + if idTestGroup in aidSelected: + oMember.idTestGroup = idTestGroup; + aoNewValue.append(oMember); + elif sAttr == 'aoTestBoxes': + aidSelected = oDisp.getListOfIntParams(sParam, iMin = 1, iMax = 0x7ffffffe, aiDefaults = []) + sIds = oDisp.getStringParam(self.ksParam_aidTestBoxes, sDefault = ''); + for idTestBox in sIds.split(','): + try: idTestBox = int(idTestBox); + except: pass; + oDispWrapper = self.DispWrapper(oDisp, '%s[%s][%%s]' % (SchedGroupDataEx.ksParam_aoTestBoxes, idTestBox,)) + oBoxInGrp = TestBoxDataForSchedGroup().initFromParams(oDispWrapper, fStrict = False); + if idTestBox in aidSelected: + oBoxInGrp.idTestBox = idTestBox; + aoNewValue.append(oBoxInGrp); + else: + return SchedGroupData.convertParamToAttribute(self, sAttr, sParam, oValue, oDisp, fStrict); + return aoNewValue; + + def _validateAndConvertAttribute(self, sAttr, sParam, oValue, aoNilValues, fAllowNull, oDb): + if sAttr not in [ 'aoMembers', 'aoTestBoxes' ]: + return SchedGroupData._validateAndConvertAttribute(self, sAttr, sParam, oValue, aoNilValues, fAllowNull, oDb); + + if oValue in aoNilValues: + return ([], None); + + asErrors = []; + aoNewMembers = []; + if sAttr == 'aoMembers': + asAllowNulls = ['bmHourlySchedule', 'idTestGroupPreReq', 'tsEffective', 'tsExpire', 'uidAuthor', ]; + if self.idSchedGroup in [None, '-1', -1]: + asAllowNulls.append('idSchedGroup'); # Probably new group, so allow null scheduling group. + + for oOldMember in oValue: + oNewMember = SchedGroupMemberDataEx().initFromOther(oOldMember); + aoNewMembers.append(oNewMember); + + dErrors = oNewMember.validateAndConvertEx(asAllowNulls, oDb, ModelDataBase.ksValidateFor_Other); + if dErrors: + asErrors.append(str(dErrors)); + + if not asErrors: + for i, _ in enumerate(aoNewMembers): + idTestGroup = aoNewMembers[i]; + for j in range(i + 1, len(aoNewMembers)): + if aoNewMembers[j].idTestGroup == idTestGroup: + asErrors.append('Duplicate test group #%d!' % (idTestGroup, )); + break; + else: + asAllowNulls = list(TestBoxDataForSchedGroup.kasAllowNullAttributes); + if self.idSchedGroup in [None, '-1', -1]: + asAllowNulls.append('idSchedGroup'); # Probably new group, so allow null scheduling group. + + for oOldMember in oValue: + oNewMember = TestBoxDataForSchedGroup().initFromOther(oOldMember); + aoNewMembers.append(oNewMember); + + dErrors = oNewMember.validateAndConvertEx(asAllowNulls, oDb, ModelDataBase.ksValidateFor_Other); + if dErrors: + asErrors.append(str(dErrors)); + + if not asErrors: + for i, _ in enumerate(aoNewMembers): + idTestBox = aoNewMembers[i]; + for j in range(i + 1, len(aoNewMembers)): + if aoNewMembers[j].idTestBox == idTestBox: + asErrors.append('Duplicate test box #%d!' % (idTestBox, )); + break; + + return (aoNewMembers, None if not asErrors else '<br>\n'.join(asErrors)); + + def _validateAndConvertWorker(self, asAllowNullAttributes, oDb, enmValidateFor = ModelDataBase.ksValidateFor_Other): + dErrors = SchedGroupData._validateAndConvertWorker(self, asAllowNullAttributes, oDb, enmValidateFor); + + # + # Fetch the extended build source bits. + # + if self.ksParam_idBuildSrc not in dErrors: + if self.idBuildSrc in self.getAttributeParamNullValues('idBuildSrc') \ + or self.idBuildSrc is None: + self.oBuildSrc = None; + else: + try: + self.oBuildSrc = BuildSourceData().initFromDbWithId(oDb, self.idBuildSrc); + except Exception as oXcpt: + self.oBuildSrc = BuildSourceData(); + dErrors[self.ksParam_idBuildSrc] = str(oXcpt); + + if self.ksParam_idBuildSrcTestSuite not in dErrors: + if self.idBuildSrcTestSuite in self.getAttributeParamNullValues('idBuildSrcTestSuite') \ + or self.idBuildSrcTestSuite is None: + self.oBuildSrcValidationKit = None; + else: + try: + self.oBuildSrcValidationKit = BuildSourceData().initFromDbWithId(oDb, self.idBuildSrcTestSuite); + except Exception as oXcpt: + self.oBuildSrcValidationKit = BuildSourceData(); + dErrors[self.ksParam_idBuildSrcTestSuite] = str(oXcpt); + + return dErrors; + + + +class SchedGroupLogic(ModelLogicBase): # pylint: disable=too-few-public-methods + """ + SchedGroup logic. + """ + + def __init__(self, oDb): + ModelLogicBase.__init__(self, oDb); + self.dCache = None; + + # + # Standard methods. + # + + def fetchForListing(self, iStart, cMaxRows, tsNow, aiSortColumns = None): + """ + Fetches build sources. + + Returns an array (list) of BuildSourceData items, empty list if none. + Raises exception on error. + """ + _ = aiSortColumns; + + if tsNow is None: + self._oDb.execute('SELECT *\n' + 'FROM SchedGroups\n' + 'WHERE tsExpire = \'infinity\'::TIMESTAMP\n' + 'ORDER BY fEnabled DESC, sName DESC\n' + 'LIMIT %s OFFSET %s\n' + , (cMaxRows, iStart,)); + else: + self._oDb.execute('SELECT *\n' + 'FROM SchedGroups\n' + 'WHERE tsExpire > %s\n' + ' AND tsEffective <= %s\n' + 'ORDER BY fEnabled DESC, sName DESC\n' + 'LIMIT %s OFFSET %s\n' + , (tsNow, tsNow, cMaxRows, iStart,)); + + aoRet = []; + for aoRow in self._oDb.fetchAll(): + aoRet.append(SchedGroupDataEx().initFromDbRowEx(aoRow, self._oDb, tsNow)); + return aoRet; + + def fetchForChangeLog(self, idSchedGroup, iStart, cMaxRows, tsNow): # pylint: disable=too-many-locals,too-many-statements + """ + Fetches change log entries for a scheduling group. + + Returns an array of ChangeLogEntry instance and an indicator whether + there are more entries. + Raises exception on error. + """ + + ## @todo calc changes to scheduler group! + + if tsNow is None: + tsNow = self._oDb.getCurrentTimestamp(); + + # + # First gather the change log timeline using the effective dates. + # (ASSUMES that we'll always have a separate delete entry, rather + # than just setting tsExpire.) + # + self._oDb.execute(''' +( +SELECT tsEffective, + uidAuthor +FROM SchedGroups +WHERE idSchedGroup = %s + AND tsEffective <= %s +ORDER BY tsEffective DESC +) UNION ( +SELECT CASE WHEN tsEffective + %s::INTERVAL = tsExpire THEN tsExpire ELSE tsEffective END, + uidAuthor +FROM SchedGroupMembers +WHERE idSchedGroup = %s + AND tsEffective <= %s +ORDER BY tsEffective DESC +) UNION ( +SELECT CASE WHEN tsEffective + %s::INTERVAL = tsExpire THEN tsExpire ELSE tsEffective END, + uidAuthor +FROM TestBoxesInSchedGroups +WHERE idSchedGroup = %s + AND tsEffective <= %s +ORDER BY tsEffective DESC +) +ORDER BY tsEffective DESC +LIMIT %s OFFSET %s +''', (idSchedGroup, tsNow, + db.dbOneTickIntervalString(), idSchedGroup, tsNow, + db.dbOneTickIntervalString(), idSchedGroup, tsNow, + cMaxRows + 1, iStart, )); + + aoEntries = [] # type: list[ChangeLogEntry] + tsPrevious = tsNow; + for aoDbRow in self._oDb.fetchAll(): + (tsEffective, uidAuthor) = aoDbRow; + aoEntries.append(ChangeLogEntry(uidAuthor, None, tsEffective, tsPrevious, None, None, [])); + tsPrevious = db.dbTimestampPlusOneTick(tsEffective); + + if True: # pylint: disable=using-constant-test + # + # Fetch data for each for each change log entry point. + # + # We add one tick to the timestamp here to skip past delete records + # that only there to record the user doing the deletion. + # + for iEntry, oEntry in enumerate(aoEntries): + oEntry.oNewRaw = SchedGroupDataEx().initFromDbWithId(self._oDb, idSchedGroup, oEntry.tsEffective); + if iEntry > 0: + aoEntries[iEntry - 1].oOldRaw = oEntry.oNewRaw; + + # Chop off the +1 entry, if any. + fMore = len(aoEntries) > cMaxRows; + if fMore: + aoEntries = aoEntries[:-1]; + + # Figure out the changes. + for oEntry in aoEntries: + oOld = oEntry.oOldRaw; + if not oOld: + break; + oNew = oEntry.oNewRaw; + aoChanges = oEntry.aoChanges; + for sAttr in oNew.getDataAttributes(): + if sAttr in [ 'tsEffective', 'tsExpire', 'uidAuthor', ]: + continue; + oOldAttr = getattr(oOld, sAttr); + oNewAttr = getattr(oNew, sAttr); + if oOldAttr == oNewAttr: + continue; + if sAttr in [ 'aoMembers', 'aoTestBoxes', ]: + iNew = 0; + iOld = 0; + asNewAttr = []; + asOldAttr = []; + if sAttr == 'aoMembers': + # ASSUMES aoMembers is sorted by idTestGroupPreReq (nulls first), oTestGroup.sName, idTestGroup! + while iNew < len(oNewAttr) and iOld < len(oOldAttr): + if oNewAttr[iNew].idTestGroup == oOldAttr[iOld].idTestGroup: + if oNewAttr[iNew].idTestGroupPreReq != oOldAttr[iOld].idTestGroupPreReq: + if oNewAttr[iNew].idTestGroupPreReq is None: + asOldAttr.append('Dropped test group #%s (%s) dependency on #%s' + % (oNewAttr[iNew].idTestGroup, oNewAttr[iNew].oTestGroup.sName, + oOldAttr[iOld].idTestGroupPreReq)); + elif oOldAttr[iOld].idTestGroupPreReq is None: + asNewAttr.append('Added test group #%s (%s) dependency on #%s' + % (oNewAttr[iNew].idTestGroup, oNewAttr[iNew].oTestGroup.sName, + oNewAttr[iOld].idTestGroupPreReq)); + else: + asNewAttr.append('Test group #%s (%s) dependency on #%s' + % (oNewAttr[iNew].idTestGroup, oNewAttr[iNew].oTestGroup.sName, + oNewAttr[iNew].idTestGroupPreReq)); + asOldAttr.append('Test group #%s (%s) dependency on #%s' + % (oNewAttr[iNew].idTestGroup, oNewAttr[iNew].oTestGroup.sName, + oOldAttr[iOld].idTestGroupPreReq)); + if oNewAttr[iNew].iSchedPriority != oOldAttr[iOld].iSchedPriority: + asNewAttr.append('Test group #%s (%s) priority %s' + % (oNewAttr[iNew].idTestGroup, oNewAttr[iNew].oTestGroup.sName, + oNewAttr[iNew].iSchedPriority)); + asOldAttr.append('Test group #%s (%s) priority %s' + % (oNewAttr[iNew].idTestGroup, oNewAttr[iNew].oTestGroup.sName, + oOldAttr[iOld].iSchedPriority)); + iNew += 1; + iOld += 1; + elif oNewAttr[iNew].oTestGroup.sName < oOldAttr[iOld].oTestGroup.sName \ + or ( oNewAttr[iNew].oTestGroup.sName == oOldAttr[iOld].oTestGroup.sName + and oNewAttr[iNew].idTestGroup < oOldAttr[iOld].idTestGroup): + asNewAttr.append('New test group #%s - %s' + % (oNewAttr[iNew].idTestGroup, oNewAttr[iNew].oTestGroup.sName)); + iNew += 1; + else: + asOldAttr.append('Removed test group #%s - %s' + % (oOldAttr[iOld].idTestGroup, oOldAttr[iOld].oTestGroup.sName)); + iOld += 1; + while iNew < len(oNewAttr): + asNewAttr.append('New test group #%s - %s' + % (oNewAttr[iNew].idTestGroup, oNewAttr[iNew].oTestGroup.sName)); + iNew += 1; + while iOld < len(oOldAttr): + asOldAttr.append('Removed test group #%s - %s' + % (oOldAttr[iOld].idTestGroup, oOldAttr[iOld].oTestGroup.sName)); + iOld += 1; + else: + dNewIds = { oBoxInGrp.idTestBox: oBoxInGrp for oBoxInGrp in oNewAttr }; + dOldIds = { oBoxInGrp.idTestBox: oBoxInGrp for oBoxInGrp in oOldAttr }; + hCommonIds = set(dNewIds.keys()) & set(dOldIds.keys()); + for idTestBox in hCommonIds: + oNewBoxInGrp = dNewIds[idTestBox]; + oOldBoxInGrp = dOldIds[idTestBox]; + if oNewBoxInGrp.iSchedPriority != oOldBoxInGrp.iSchedPriority: + asNewAttr.append('Test box \'%s\' (#%s) priority %s' + % (getattr(oNewBoxInGrp.oTestBox, 'sName', '[Partial DB]'), + oNewBoxInGrp.idTestBox, oNewBoxInGrp.iSchedPriority)); + asOldAttr.append('Test box \'%s\' (#%s) priority %s' + % (getattr(oOldBoxInGrp.oTestBox, 'sName', '[Partial DB]'), + oOldBoxInGrp.idTestBox, oOldBoxInGrp.iSchedPriority)); + asNewAttr = sorted(asNewAttr); + asOldAttr = sorted(asOldAttr); + for idTestBox in set(dNewIds.keys()) - hCommonIds: + oNewBoxInGrp = dNewIds[idTestBox]; + asNewAttr.append('New test box \'%s\' (#%s) priority %s' + % (getattr(oNewBoxInGrp.oTestBox, 'sName', '[Partial DB]'), + oNewBoxInGrp.idTestBox, oNewBoxInGrp.iSchedPriority)); + for idTestBox in set(dOldIds.keys()) - hCommonIds: + oOldBoxInGrp = dOldIds[idTestBox]; + asOldAttr.append('Removed test box \'%s\' (#%s) priority %s' + % (getattr(oOldBoxInGrp.oTestBox, 'sName', '[Partial DB]'), + oOldBoxInGrp.idTestBox, oOldBoxInGrp.iSchedPriority)); + + if asNewAttr or asOldAttr: + aoChanges.append(AttributeChangeEntryPre(sAttr, oNewAttr, oOldAttr, + '\n'.join(asNewAttr), '\n'.join(asOldAttr))); + else: + aoChanges.append(AttributeChangeEntry(sAttr, oNewAttr, oOldAttr, str(oNewAttr), str(oOldAttr))); + + else: + ## + ## @todo Incomplete: A more complicate apporach, probably faster though. + ## + def findEntry(tsEffective, iPrev = 0): + """ Find entry with matching effective + expiration time """ + self._oDb.dprint('findEntry: iPrev=%s len(aoEntries)=%s tsEffective=%s' % (iPrev, len(aoEntries), tsEffective)); + while iPrev < len(aoEntries): + self._oDb.dprint('%s iPrev=%u' % (aoEntries[iPrev].tsEffective, iPrev, )); + if aoEntries[iPrev].tsEffective > tsEffective: + iPrev += 1; + elif aoEntries[iPrev].tsEffective == tsEffective: + self._oDb.dprint('hit %u' % (iPrev,)); + return iPrev; + else: + break; + self._oDb.dprint('%s not found!' % (tsEffective,)); + return -1; + + fMore = True; + + # + # Track scheduling group changes. Not terribly efficient for large cMaxRows + # values, but not in the mood for figure out if there is any way to optimize that. + # + self._oDb.execute(''' +SELECT * +FROM SchedGroups +WHERE idSchedGroup = %s + AND tsEffective <= %s +ORDER BY tsEffective DESC +LIMIT %s''', (idSchedGroup, aoEntries[0].tsEffective, cMaxRows + 1,)); + + iEntry = 0; + aaoRows = self._oDb.fetchAll(); + for iRow, oRow in enumerate(aaoRows): + oNew = SchedGroupDataEx().initFromDbRow(oRow); + iEntry = findEntry(oNew.tsEffective, iEntry); + self._oDb.dprint('iRow=%s iEntry=%s' % (iRow, iEntry)); + if iEntry < 0: + break; + oEntry = aoEntries[iEntry]; + aoChanges = oEntry.aoChanges; + oEntry.oNewRaw = oNew; + if iRow + 1 < len(aaoRows): + oOld = SchedGroupDataEx().initFromDbRow(aaoRows[iRow + 1]); + self._oDb.dprint('oOld=%s' % (oOld,)); + for sAttr in oNew.getDataAttributes(): + if sAttr not in [ 'tsEffective', 'tsExpire', 'uidAuthor', ]: + oOldAttr = getattr(oOld, sAttr); + oNewAttr = getattr(oNew, sAttr); + if oOldAttr != oNewAttr: + aoChanges.append(AttributeChangeEntry(sAttr, oNewAttr, oOldAttr, str(oNewAttr), str(oOldAttr))); + else: + self._oDb.dprint('New'); + + # + # ... + # + + # FInally + UserAccountLogic(self._oDb).resolveChangeLogAuthors(aoEntries); + return (aoEntries, fMore); + + + def addEntry(self, oData, uidAuthor, fCommit = False): + """Add Scheduling Group record""" + + # + # Validate. + # + dDataErrors = oData.validateAndConvert(self._oDb, oData.ksValidateFor_Add); + if dDataErrors: + raise TMInvalidData('Invalid data passed to addEntry: %s' % (dDataErrors,)); + if self.exists(oData.sName): + raise TMRowAlreadyExists('Scheduling group "%s" already exists.' % (oData.sName,)); + + # + # Add it. + # + self._oDb.execute('INSERT INTO SchedGroups (\n' + ' uidAuthor,\n' + ' sName,\n' + ' sDescription,\n' + ' fEnabled,\n' + ' enmScheduler,\n' + ' idBuildSrc,\n' + ' idBuildSrcTestSuite,\n' + ' sComment)\n' + 'VALUES (%s, %s, %s, %s, %s, %s, %s, %s)\n' + 'RETURNING idSchedGroup\n' + , ( uidAuthor, + oData.sName, + oData.sDescription, + oData.fEnabled, + oData.enmScheduler, + oData.idBuildSrc, + oData.idBuildSrcTestSuite, + oData.sComment )); + idSchedGroup = self._oDb.fetchOne()[0]; + oData.idSchedGroup = idSchedGroup; + + for oBoxInGrp in oData.aoTestBoxes: + oBoxInGrp.idSchedGroup = idSchedGroup; + self._addSchedGroupTestBox(uidAuthor, oBoxInGrp); + + for oMember in oData.aoMembers: + oMember.idSchedGroup = idSchedGroup; + self._addSchedGroupMember(uidAuthor, oMember); + + self._oDb.maybeCommit(fCommit); + return True; + + def editEntry(self, oData, uidAuthor, fCommit = False): + """Edit Scheduling Group record""" + + # + # Validate input and retrieve the old data. + # + dErrors = oData.validateAndConvert(self._oDb, oData.ksValidateFor_Edit); + if dErrors: + raise TMInvalidData('editEntry got invalid data: %s' % (dErrors,)); + self._assertUnique(oData.sName, oData.idSchedGroup); + oOldData = SchedGroupDataEx().initFromDbWithId(self._oDb, oData.idSchedGroup); + + # + # Make the changes. + # + if not oData.isEqualEx(oOldData, [ 'tsEffective', 'tsExpire', 'uidAuthor', 'aoMembers', 'aoTestBoxes', + 'oBuildSrc', 'oBuildSrcValidationKit', ]): + self._historizeEntry(oData.idSchedGroup); + self._readdEntry(uidAuthor, oData); + + # Remove groups. + for oOld in oOldData.aoMembers: + fRemove = True; + for oNew in oData.aoMembers: + if oNew.idTestGroup == oOld.idTestGroup: + fRemove = False; + break; + if fRemove: + self._removeSchedGroupMember(uidAuthor, oOld); + + # Add / modify groups. + for oMember in oData.aoMembers: + oOldMember = None; + for oOld in oOldData.aoMembers: + if oOld.idTestGroup == oMember.idTestGroup: + oOldMember = oOld; + break; + + oMember.idSchedGroup = oData.idSchedGroup; + if oOldMember is None: + self._addSchedGroupMember(uidAuthor, oMember); + elif not oMember.isEqualEx(oOldMember, ['tsEffective', 'tsExpire', 'uidAuthor', 'oTestGroup']): + self._historizeSchedGroupMember(oMember); + self._addSchedGroupMember(uidAuthor, oMember); + + # Remove testboxes. + for oOld in oOldData.aoTestBoxes: + fRemove = True; + for oNew in oData.aoTestBoxes: + if oNew.idTestBox == oOld.idTestBox: + fRemove = False; + break; + if fRemove: + self._removeSchedGroupTestBox(uidAuthor, oOld); + + # Add / modify testboxes. + for oBoxInGrp in oData.aoTestBoxes: + oOldBoxInGrp = None; + for oOld in oOldData.aoTestBoxes: + if oOld.idTestBox == oBoxInGrp.idTestBox: + oOldBoxInGrp = oOld; + break; + + oBoxInGrp.idSchedGroup = oData.idSchedGroup; + if oOldBoxInGrp is None: + self._addSchedGroupTestBox(uidAuthor, oBoxInGrp); + elif not oBoxInGrp.isEqualEx(oOldBoxInGrp, ['tsEffective', 'tsExpire', 'uidAuthor', 'oTestBox']): + self._historizeSchedGroupTestBox(oBoxInGrp); + self._addSchedGroupTestBox(uidAuthor, oBoxInGrp); + + self._oDb.maybeCommit(fCommit); + return True; + + def removeEntry(self, uidAuthor, idSchedGroup, fCascade = False, fCommit = False): + """ + Deletes a scheduling group. + """ + _ = fCascade; + + # + # Input validation and retrival of current data. + # + if idSchedGroup == 1: + raise TMRowInUse('Cannot remove the default scheduling group (id 1).'); + oData = SchedGroupDataEx().initFromDbWithId(self._oDb, idSchedGroup); + + # + # Remove the test box member records. + # + for oBoxInGrp in oData.aoTestBoxes: + self._removeSchedGroupTestBox(uidAuthor, oBoxInGrp); + self._oDb.execute('UPDATE TestBoxesInSchedGroups\n' + 'SET tsExpire = CURRENT_TIMESTAMP\n' + 'WHERE idSchedGroup = %s\n' + ' AND tsExpire = \'infinity\'::TIMESTAMP\n' + , (idSchedGroup,)); + + # + # Remove the test group member records. + # + for oMember in oData.aoMembers: + self._removeSchedGroupMember(uidAuthor, oMember); + self._oDb.execute('UPDATE SchedGroupMembers\n' + 'SET tsExpire = CURRENT_TIMESTAMP\n' + 'WHERE idSchedGroup = %s\n' + ' AND tsExpire = \'infinity\'::TIMESTAMP\n' + , (idSchedGroup,)); + + # + # Now the SchedGroups entry. + # + (tsCur, tsCurMinusOne) = self._oDb.getCurrentTimestamps(); + if oData.tsEffective not in (tsCur, tsCurMinusOne): + self._historizeEntry(idSchedGroup, tsCurMinusOne); + self._readdEntry(uidAuthor, oData, tsCurMinusOne); + self._historizeEntry(idSchedGroup); + self._oDb.execute('UPDATE SchedGroups\n' + 'SET tsExpire = CURRENT_TIMESTAMP\n' + 'WHERE idSchedGroup = %s\n' + ' AND tsExpire = \'infinity\'::TIMESTAMP\n' + , (idSchedGroup,)) + + self._oDb.maybeCommit(fCommit) + return True; + + + def cachedLookup(self, idSchedGroup): + """ + Looks up the most recent SchedGroupData object for idSchedGroup + via an object cache. + + Returns a shared SchedGroupData object. None if not found. + Raises exception on DB error. + """ + if self.dCache is None: + self.dCache = self._oDb.getCache('SchedGroup'); + + oEntry = self.dCache.get(idSchedGroup, None); + if oEntry is None: + self._oDb.execute('SELECT *\n' + 'FROM SchedGroups\n' + 'WHERE idSchedGroup = %s\n' + ' AND tsExpire = \'infinity\'::TIMESTAMP\n' + , (idSchedGroup, )); + if self._oDb.getRowCount() == 0: + # Maybe it was deleted, try get the last entry. + self._oDb.execute('SELECT *\n' + 'FROM SchedGroups\n' + 'WHERE idSchedGroup = %s\n' + 'ORDER BY tsExpire DESC\n' + 'LIMIT 1\n' + , (idSchedGroup, )); + elif self._oDb.getRowCount() > 1: + raise self._oDb.integrityException('%s infinity rows for %s' % (self._oDb.getRowCount(), idSchedGroup)); + + if self._oDb.getRowCount() == 1: + oEntry = SchedGroupData().initFromDbRow(self._oDb.fetchOne()); + self.dCache[idSchedGroup] = oEntry; + return oEntry; + + + # + # Other methods. + # + + def fetchOrderedByName(self, tsNow = None): + """ + Return list of objects of type SchedGroups ordered by name. + May raise exception on database error. + """ + if tsNow is None: + self._oDb.execute('SELECT *\n' + 'FROM SchedGroups\n' + 'WHERE tsExpire = \'infinity\'::TIMESTAMP\n' + 'ORDER BY sName ASC\n'); + else: + self._oDb.execute('SELECT *\n' + 'FROM SchedGroups\n' + 'WHERE tsExpire > %s\n' + ' AND tsEffective <= %s\n' + 'ORDER BY sName ASC\n' + , (tsNow, tsNow,)); + aoRet = [] + for _ in range(self._oDb.getRowCount()): + aoRet.append(SchedGroupData().initFromDbRow(self._oDb.fetchOne())); + return aoRet; + + + def getAll(self, tsEffective = None): + """ + Gets the list of all scheduling groups. + Returns an array of SchedGroupData instances. + """ + if tsEffective is None: + self._oDb.execute('SELECT *\n' + 'FROM SchedGroups\n' + 'WHERE tsExpire = \'infinity\'::TIMESTAMP\n'); + else: + self._oDb.execute('SELECT *\n' + 'FROM SchedGroups\n' + 'WHERE tsExpire > %s\n' + ' AND tsEffective <= %s\n' + , (tsEffective, tsEffective)); + aoRet = []; + for aoRow in self._oDb.fetchAll(): + aoRet.append(SchedGroupData().initFromDbRow(aoRow)); + return aoRet; + + def getSchedGroupsForCombo(self, tsEffective = None): + """ + Gets the list of active scheduling groups for a combo box. + Returns an array of (value [idSchedGroup], drop-down-name [sName], + hover-text [sDescription]) tuples. + """ + if tsEffective is None: + self._oDb.execute('SELECT idSchedGroup, sName, sDescription\n' + 'FROM SchedGroups\n' + 'WHERE tsExpire = \'infinity\'::TIMESTAMP\n' + 'ORDER BY sName'); + else: + self._oDb.execute('SELECT idSchedGroup, sName, sDescription\n' + 'FROM SchedGroups\n' + 'WHERE tsExpire > %s\n' + ' AND tsEffective <= %s\n' + 'ORDER BY sName' + , (tsEffective, tsEffective)); + return self._oDb.fetchAll(); + + + def getMembers(self, idSchedGroup, tsEffective = None): + """ + Gets the scheduling groups members for the given scheduling group. + + Returns an array of SchedGroupMemberDataEx instances (sorted by + priority (descending) and idTestGroup). May raise exception DB error. + """ + + if tsEffective is None: + self._oDb.execute('SELECT *\n' + 'FROM SchedGroupMembers, TestGroups\n' + 'WHERE SchedGroupMembers.idSchedGroup = %s\n' + ' AND SchedGroupMembers.tsExpire = \'infinity\'::TIMESTAMP\n' + ' AND TestGroups.idTestGroup = SchedGroupMembers.idTestGroup\n' + ' AND TestGroups.tsExpire = \'infinity\'::TIMESTAMP\n' + 'ORDER BY SchedGroupMembers.iSchedPriority DESC, SchedGroupMembers.idTestGroup\n' + , (idSchedGroup,)); + else: + self._oDb.execute('SELECT *\n' + 'FROM SchedGroupMembers, TestGroups\n' + 'WHERE SchedGroupMembers.idSchedGroup = %s\n' + ' AND SchedGroupMembers.tsExpire < %s\n' + ' AND SchedGroupMembers.tsEffective >= %s\n' + ' AND TestGroups.idTestGroup = SchedGroupMembers.idTestGroup\n' + ' AND TestGroups.tsExpire < %s\n' + ' AND TestGroups.tsEffective >= %s\n' + 'ORDER BY SchedGroupMembers.iSchedPriority DESC, SchedGroupMembers.idTestGroup\n' + , (idSchedGroup, tsEffective, tsEffective, tsEffective, tsEffective, )); + aaoRows = self._oDb.fetchAll(); + aoRet = []; + for aoRow in aaoRows: + aoRet.append(SchedGroupMemberDataEx().initFromDbRow(aoRow)); + return aoRet; + + def getTestCasesForGroup(self, idSchedGroup, cMax = None): + """ + Gets the enabled testcases w/ testgroup+priority for the given scheduling group. + + Returns an array of TestCaseData instances (ordered by group id, descending + testcase priority, and testcase IDs) with an extra iSchedPriority member. + May raise exception on DB error or if the result exceeds cMax. + """ + + self._oDb.execute('SELECT TestGroupMembers.idTestGroup, TestGroupMembers.iSchedPriority, TestCases.*\n' + 'FROM SchedGroupMembers, TestGroups, TestGroupMembers, TestCases\n' + 'WHERE SchedGroupMembers.idSchedGroup = %s\n' + ' AND SchedGroupMembers.tsExpire = \'infinity\'::TIMESTAMP\n' + ' AND TestGroups.idTestGroup = SchedGroupMembers.idTestGroup\n' + ' AND TestGroups.tsExpire = \'infinity\'::TIMESTAMP\n' + ' AND TestGroupMembers.idTestGroup = TestGroups.idTestGroup\n' + ' AND TestGroupMembers.tsExpire = \'infinity\'::TIMESTAMP\n' + ' AND TestCases.idTestCase = TestGroupMembers.idTestCase\n' + ' AND TestCases.tsExpire = \'infinity\'::TIMESTAMP\n' + ' AND TestCases.fEnabled = TRUE\n' + 'ORDER BY TestGroupMembers.idTestGroup, TestGroupMembers.iSchedPriority DESC, TestCases.idTestCase\n' + , (idSchedGroup,)); + + if cMax is not None and self._oDb.getRowCount() > cMax: + raise TMExceptionBase('Too many testcases for scheduling group %s: %s, max %s' + % (idSchedGroup, cMax, self._oDb.getRowCount(),)); + + aoRet = []; + for aoRow in self._oDb.fetchAll(): + oTestCase = TestCaseData().initFromDbRow(aoRow[2:]); + oTestCase.idTestGroup = aoRow[0]; + oTestCase.iSchedPriority = aoRow[1]; + aoRet.append(oTestCase); + return aoRet; + + def getTestCaseArgsForGroup(self, idSchedGroup, cMax = None): + """ + Gets the testcase argument variation w/ testgroup+priority for the given scheduling group. + + Returns an array TestCaseArgsData instance (sorted by group and + variation id) with an extra iSchedPriority member. + May raise exception on DB error or if the result exceeds cMax. + """ + + self._oDb.execute('SELECT TestGroupMembers.idTestGroup, TestGroupMembers.iSchedPriority, TestCaseArgs.*\n' + 'FROM SchedGroupMembers, TestGroups, TestGroupMembers, TestCaseArgs, TestCases\n' + 'WHERE SchedGroupMembers.idSchedGroup = %s\n' + ' AND SchedGroupMembers.tsExpire = \'infinity\'::TIMESTAMP\n' + ' AND TestGroups.idTestGroup = SchedGroupMembers.idTestGroup\n' + ' AND TestGroups.tsExpire = \'infinity\'::TIMESTAMP\n' + ' AND TestGroupMembers.idTestGroup = TestGroups.idTestGroup\n' + ' AND TestGroupMembers.tsExpire = \'infinity\'::TIMESTAMP\n' + ' AND TestCaseArgs.idTestCase = TestGroupMembers.idTestCase\n' + ' AND TestCaseArgs.tsExpire = \'infinity\'::TIMESTAMP\n' + ' AND ( TestGroupMembers.aidTestCaseArgs is NULL\n' + ' OR TestCaseArgs.idTestCaseArgs = ANY(TestGroupMembers.aidTestCaseArgs) )\n' + ' AND TestCases.idTestCase = TestCaseArgs.idTestCase\n' + ' AND TestCases.tsExpire = \'infinity\'::TIMESTAMP\n' + ' AND TestCases.fEnabled = TRUE\n' + 'ORDER BY TestGroupMembers.idTestGroup, TestGroupMembers.idTestCase, TestCaseArgs.idTestCaseArgs\n' + , (idSchedGroup,)); + + if cMax is not None and self._oDb.getRowCount() > cMax: + raise TMExceptionBase('Too many argument variations for scheduling group %s: %s, max %s' + % (idSchedGroup, cMax, self._oDb.getRowCount(),)); + + aoRet = []; + for aoRow in self._oDb.fetchAll(): + oVariation = TestCaseArgsData().initFromDbRow(aoRow[2:]); + oVariation.idTestGroup = aoRow[0]; + oVariation.iSchedPriority = aoRow[1]; + aoRet.append(oVariation); + return aoRet; + + def exists(self, sName): + """Checks if a group with the given name exists.""" + self._oDb.execute('SELECT idSchedGroup\n' + 'FROM SchedGroups\n' + 'WHERE tsExpire = \'infinity\'::TIMESTAMP\n' + ' AND sName = %s\n' + 'LIMIT 1\n' + , (sName,)); + return self._oDb.getRowCount() > 0; + + def getById(self, idSchedGroup): + """Get Scheduling Group data by idSchedGroup""" + self._oDb.execute('SELECT *\n' + 'FROM SchedGroups\n' + 'WHERE tsExpire = \'infinity\'::timestamp\n' + ' AND idSchedGroup = %s;', (idSchedGroup,)) + aRows = self._oDb.fetchAll() + if len(aRows) not in (0, 1): + raise self._oDb.integrityException( + 'Found more than one scheduling groups with the same credentials. Database structure is corrupted.') + try: + return SchedGroupData().initFromDbRow(aRows[0]) + except IndexError: + return None + + + # + # Internal helpers. + # + + def _assertUnique(self, sName, idSchedGroupIgnore = None): + """ + Checks that the scheduling group name is unique. + Raises exception if the name is already in use. + """ + if idSchedGroupIgnore is None: + self._oDb.execute('SELECT idSchedGroup\n' + 'FROM SchedGroups\n' + 'WHERE tsExpire = \'infinity\'::TIMESTAMP\n' + ' AND sName = %s\n' + , ( sName, ) ); + else: + self._oDb.execute('SELECT idSchedGroup\n' + 'FROM SchedGroups\n' + 'WHERE tsExpire = \'infinity\'::TIMESTAMP\n' + ' AND sName = %s\n' + ' AND idSchedGroup <> %s\n' + , ( sName, idSchedGroupIgnore, ) ); + if self._oDb.getRowCount() > 0: + raise TMRowInUse('Scheduling group name (%s) is already in use.' % (sName,)); + return True; + + def _readdEntry(self, uidAuthor, oData, tsEffective = None): + """ + Re-adds the SchedGroups entry. Used by editEntry and removeEntry. + """ + if tsEffective is None: + tsEffective = self._oDb.getCurrentTimestamp(); + self._oDb.execute('INSERT INTO SchedGroups (\n' + ' uidAuthor,\n' + ' tsEffective,\n' + ' idSchedGroup,\n' + ' sName,\n' + ' sDescription,\n' + ' fEnabled,\n' + ' enmScheduler,\n' + ' idBuildSrc,\n' + ' idBuildSrcTestSuite,\n' + ' sComment )\n' + 'VALUES ( %s, %s, %s, %s, %s, %s, %s, %s, %s, %s )\n' + , ( uidAuthor, + tsEffective, + oData.idSchedGroup, + oData.sName, + oData.sDescription, + oData.fEnabled, + oData.enmScheduler, + oData.idBuildSrc, + oData.idBuildSrcTestSuite, + oData.sComment, )); + return True; + + def _historizeEntry(self, idSchedGroup, tsExpire = None): + """ + Historizes the current entry for the given scheduling group. + """ + if tsExpire is None: + tsExpire = self._oDb.getCurrentTimestamp(); + self._oDb.execute('UPDATE SchedGroups\n' + 'SET tsExpire = %s\n' + 'WHERE idSchedGroup = %s\n' + ' AND tsExpire = \'infinity\'::TIMESTAMP\n' + , ( tsExpire, idSchedGroup, )); + return True; + + def _addSchedGroupMember(self, uidAuthor, oMember, tsEffective = None): + """ + addEntry worker for adding a scheduling group member. + """ + if tsEffective is None: + tsEffective = self._oDb.getCurrentTimestamp(); + self._oDb.execute('INSERT INTO SchedGroupMembers(\n' + ' idSchedGroup,\n' + ' idTestGroup,\n' + ' tsEffective,\n' + ' uidAuthor,\n' + ' iSchedPriority,\n' + ' bmHourlySchedule,\n' + ' idTestGroupPreReq)\n' + 'VALUES (%s, %s, %s, %s, %s, %s, %s)\n' + , ( oMember.idSchedGroup, + oMember.idTestGroup, + tsEffective, + uidAuthor, + oMember.iSchedPriority, + oMember.bmHourlySchedule, + oMember.idTestGroupPreReq, )); + return True; + + def _removeSchedGroupMember(self, uidAuthor, oMember): + """ + Removes a scheduling group member. + """ + + # Try record who removed it by adding an dummy entry that expires immediately. + (tsCur, tsCurMinusOne) = self._oDb.getCurrentTimestamps(); + if oMember.tsEffective not in (tsCur, tsCurMinusOne): + self._historizeSchedGroupMember(oMember, tsCurMinusOne); + self._addSchedGroupMember(uidAuthor, oMember, tsCurMinusOne); # lazy bird. + self._historizeSchedGroupMember(oMember); + else: + self._historizeSchedGroupMember(oMember); + return True; + + def _historizeSchedGroupMember(self, oMember, tsExpire = None): + """ + Historizes the current entry for the given scheduling group. + """ + if tsExpire is None: + tsExpire = self._oDb.getCurrentTimestamp(); + self._oDb.execute('UPDATE SchedGroupMembers\n' + 'SET tsExpire = %s\n' + 'WHERE idSchedGroup = %s\n' + ' AND idTestGroup = %s\n' + ' AND tsExpire = \'infinity\'::TIMESTAMP\n' + , ( tsExpire, oMember.idSchedGroup, oMember.idTestGroup, )); + return True; + + # + def _addSchedGroupTestBox(self, uidAuthor, oBoxInGroup, tsEffective = None): + """ + addEntry worker for adding a test box to a scheduling group. + """ + if tsEffective is None: + tsEffective = self._oDb.getCurrentTimestamp(); + self._oDb.execute('INSERT INTO TestBoxesInSchedGroups(\n' + ' idSchedGroup,\n' + ' idTestBox,\n' + ' tsEffective,\n' + ' uidAuthor,\n' + ' iSchedPriority)\n' + 'VALUES (%s, %s, %s, %s, %s)\n' + , ( oBoxInGroup.idSchedGroup, + oBoxInGroup.idTestBox, + tsEffective, + uidAuthor, + oBoxInGroup.iSchedPriority, )); + return True; + + def _removeSchedGroupTestBox(self, uidAuthor, oBoxInGroup): + """ + Removes a testbox from a scheduling group. + """ + + # Try record who removed it by adding an dummy entry that expires immediately. + (tsCur, tsCurMinusOne) = self._oDb.getCurrentTimestamps(); + if oBoxInGroup.tsEffective not in (tsCur, tsCurMinusOne): + self._historizeSchedGroupTestBox(oBoxInGroup, tsCurMinusOne); + self._addSchedGroupTestBox(uidAuthor, oBoxInGroup, tsCurMinusOne); # lazy bird. + self._historizeSchedGroupTestBox(oBoxInGroup); + else: + self._historizeSchedGroupTestBox(oBoxInGroup); + return True; + + def _historizeSchedGroupTestBox(self, oBoxInGroup, tsExpire = None): + """ + Historizes the current entry for the given scheduling group. + """ + if tsExpire is None: + tsExpire = self._oDb.getCurrentTimestamp(); + self._oDb.execute('UPDATE TestBoxesInSchedGroups\n' + 'SET tsExpire = %s\n' + 'WHERE idSchedGroup = %s\n' + ' AND idTestBox = %s\n' + ' AND tsExpire = \'infinity\'::TIMESTAMP\n' + , ( tsExpire, oBoxInGroup.idSchedGroup, oBoxInGroup.idTestBox, )); + return True; + + + +# +# Unit testing. +# + +# pylint: disable=missing-docstring +class SchedGroupMemberDataTestCase(ModelDataBaseTestCase): + def setUp(self): + self.aoSamples = [SchedGroupMemberData(),]; + +class SchedGroupDataTestCase(ModelDataBaseTestCase): + def setUp(self): + self.aoSamples = [SchedGroupData(),]; + +if __name__ == '__main__': + unittest.main(); + # not reached. + diff --git a/src/VBox/ValidationKit/testmanager/core/schedqueue.py b/src/VBox/ValidationKit/testmanager/core/schedqueue.py new file mode 100755 index 00000000..f49c4ad3 --- /dev/null +++ b/src/VBox/ValidationKit/testmanager/core/schedqueue.py @@ -0,0 +1,153 @@ +# -*- coding: utf-8 -*- +# "$Id: schedqueue.py $" + +""" +Test Manager - Test Case Queue. +""" + +__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 unittest + +from testmanager.core.base import ModelDataBase, ModelLogicBase, TMExceptionBase #, ModelDataBaseTestCase + + +class SchedQueueEntry(ModelDataBase): + """ + SchedQueue listing entry + + Note! This could be turned into a SchedQueueDataEx class if we start + fetching all the fields from the scheduing queue. + """ + + def __init__(self): + ModelDataBase.__init__(self) + + self.idItem = None + self.tsLastScheduled = None + self.sSchedGroup = None + self.sTestGroup = None + self.sTestCase = None + self.fUpToDate = None + self.iPerSchedGroupRowNumber = None; + + def initFromDbRow(self, aoRow): + """ + Re-initializes the object from a SchedQueueLogic::fetchForListing select. + Returns self. Raises exception if aoRow is None. + """ + if aoRow is None: + raise TMExceptionBase('TestCaseQueue row not found.') + + self.idItem = aoRow[0] + self.tsLastScheduled = aoRow[1] + self.sSchedGroup = aoRow[2] + self.sTestGroup = aoRow[3] + self.sTestCase = aoRow[4] + self.fUpToDate = aoRow[5] + self.iPerSchedGroupRowNumber = aoRow[6]; + return self + + +class SchedQueueLogic(ModelLogicBase): + """ + SchedQueues logic. + """ + def __init__(self, oDb): + ModelLogicBase.__init__(self, oDb) + + def fetchForListing(self, iStart, cMaxRows, tsNow, aiSortColumns = None): + """ + Fetches SchedQueues entries. + + Returns an array (list) of SchedQueueEntry items, empty list if none. + Raises exception on error. + """ + _, _ = tsNow, aiSortColumns + self._oDb.execute(''' +SELECT SchedQueues.idItem, + SchedQueues.tsLastScheduled, + SchedGroups.sName, + TestGroups.sName, + TestCases.sName, + SchedGroups.tsExpire = 'infinity'::TIMESTAMP + AND TestGroups.tsExpire = 'infinity'::TIMESTAMP + AND TestGroups.tsExpire = 'infinity'::TIMESTAMP + AND TestCaseArgs.tsExpire = 'infinity'::TIMESTAMP + AND TestCases.tsExpire = 'infinity'::TIMESTAMP AS fUpToDate, + ROW_NUMBER() OVER (PARTITION BY SchedQueues.idSchedGroup + ORDER BY SchedQueues.tsLastScheduled, + SchedQueues.idItem) AS iPerSchedGroupRowNumber +FROM SchedQueues + INNER JOIN SchedGroups + ON SchedGroups.idSchedGroup = SchedQueues.idSchedGroup + AND SchedGroups.tsExpire > SchedQueues.tsConfig + AND SchedGroups.tsEffective <= SchedQueues.tsConfig + INNER JOIN TestGroups + ON TestGroups.idTestGroup = SchedQueues.idTestGroup + AND TestGroups.tsExpire > SchedQueues.tsConfig + AND TestGroups.tsEffective <= SchedQueues.tsConfig + INNER JOIN TestCaseArgs + ON TestCaseArgs.idGenTestCaseArgs = SchedQueues.idGenTestCaseArgs + INNER JOIN TestCases + ON TestCases.idTestCase = TestCaseArgs.idTestCase + AND TestCases.tsExpire > SchedQueues.tsConfig + AND TestCases.tsEffective <= SchedQueues.tsConfig +ORDER BY iPerSchedGroupRowNumber, + SchedGroups.sName DESC +LIMIT %s OFFSET %s''' % (cMaxRows, iStart,)) + aoRows = [] + for _ in range(self._oDb.getRowCount()): + aoRows.append(SchedQueueEntry().initFromDbRow(self._oDb.fetchOne())) + return aoRows + +# +# Unit testing. +# + +## @todo SchedQueueEntry isn't a typical ModelDataBase child (not fetching all +## fields; is an extended data class mixing data from multiple tables), so +## this won't work yet. +# +## pylint: disable=missing-docstring +#class TestCaseQueueDataTestCase(ModelDataBaseTestCase): +# def setUp(self): +# self.aoSamples = [SchedQueueEntry(),] +# +# +#if __name__ == '__main__': +# unittest.main() +# # not reached. +# diff --git a/src/VBox/ValidationKit/testmanager/core/schedulerbase.py b/src/VBox/ValidationKit/testmanager/core/schedulerbase.py new file mode 100755 index 00000000..c28b43cf --- /dev/null +++ b/src/VBox/ValidationKit/testmanager/core/schedulerbase.py @@ -0,0 +1,1570 @@ +# -*- coding: utf-8 -*- +# $Id: schedulerbase.py $ +# pylint: disable=too-many-lines + + +""" +Test Manager - Base class and utilities for the schedulers. +""" + +__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 sys; +import unittest; + +# Validation Kit imports. +from common import utils, constants; +from testmanager import config; +from testmanager.core.build import BuildDataEx, BuildLogic; +from testmanager.core.base import ModelDataBase, ModelDataBaseTestCase, TMExceptionBase; +from testmanager.core.buildsource import BuildSourceData, BuildSourceLogic; +from testmanager.core.globalresource import GlobalResourceLogic; +from testmanager.core.schedgroup import SchedGroupData, SchedGroupLogic; +from testmanager.core.systemlog import SystemLogData, SystemLogLogic; +from testmanager.core.testbox import TestBoxData, TestBoxDataEx; +from testmanager.core.testboxstatus import TestBoxStatusData, TestBoxStatusLogic; +from testmanager.core.testcase import TestCaseLogic; +from testmanager.core.testcaseargs import TestCaseArgsDataEx, TestCaseArgsLogic; +from testmanager.core.testset import TestSetData, TestSetLogic; + +# Python 3 hacks: +if sys.version_info[0] >= 3: + xrange = range; # pylint: disable=redefined-builtin,invalid-name + + + +class ReCreateQueueData(object): + """ + Data object for recreating a scheduling queue. + + It's mostly a storage object, but has a few data checking operation + associated with it. + """ + + def __init__(self, oDb, idSchedGroup): + # + # Load data from the database. + # + oSchedGroupLogic = SchedGroupLogic(oDb); + self.oSchedGroup = oSchedGroupLogic.cachedLookup(idSchedGroup); + + # Will extend the entries with aoTestCases and dTestCases members + # further down (SchedGroupMemberDataEx). checkForGroupDepCycles + # will add aidTestGroupPreReqs. + self.aoTestGroups = oSchedGroupLogic.getMembers(idSchedGroup); + + # aoTestCases entries are TestCaseData instance with iSchedPriority + # and idTestGroup added for our purposes. + # We will add oTestGroup and aoArgsVariations members to each further down. + self.aoTestCases = oSchedGroupLogic.getTestCasesForGroup(idSchedGroup, cMax = 4096); + + # Load dependencies. + oTestCaseLogic = TestCaseLogic(oDb) + for oTestCase in self.aoTestCases: + oTestCase.aidPreReqs = oTestCaseLogic.getTestCasePreReqIds(oTestCase.idTestCase, cMax = 4096); + + # aoTestCases entries are TestCaseArgsData instance with iSchedPriority + # and idTestGroup added for our purposes. + # We will add oTestGroup and oTestCase members to each further down. + self.aoArgsVariations = oSchedGroupLogic.getTestCaseArgsForGroup(idSchedGroup, cMax = 65536); + + # + # Generate global lookups. + # + + # Generate a testcase lookup dictionary for use when working on + # argument variations. + self.dTestCases = {}; + for oTestCase in self.aoTestCases: + self.dTestCases[oTestCase.idTestCase] = oTestCase; + assert len(self.dTestCases) <= len(self.aoTestCases); # Note! Can be shorter! + + # Generate a testgroup lookup dictionary. + self.dTestGroups = {}; + for oTestGroup in self.aoTestGroups: + self.dTestGroups[oTestGroup.idTestGroup] = oTestGroup; + assert len(self.dTestGroups) == len(self.aoTestGroups); + + # + # Associate extra members with the base data. + # + if self.aoTestGroups: + # Prep the test groups. + for oTestGroup in self.aoTestGroups: + oTestGroup.aoTestCases = []; + oTestGroup.dTestCases = {}; + + # Link testcases to their group, both directions. Prep testcases for + # argument varation association. + oTestGroup = self.aoTestGroups[0]; + for oTestCase in self.aoTestCases: + if oTestGroup.idTestGroup != oTestCase.idTestGroup: + oTestGroup = self.dTestGroups[oTestCase.idTestGroup]; + + assert oTestCase.idTestCase not in oTestGroup.dTestCases; + oTestGroup.dTestCases[oTestCase.idTestCase] = oTestCase; + oTestGroup.aoTestCases.append(oTestCase); + oTestCase.oTestGroup = oTestGroup; + oTestCase.aoArgsVariations = []; + + # Associate testcase argument variations with their testcases (group) + # in both directions. + oTestGroup = self.aoTestGroups[0]; + oTestCase = self.aoTestCases[0] if self.aoTestCases else None; + for oArgVariation in self.aoArgsVariations: + if oTestGroup.idTestGroup != oArgVariation.idTestGroup: + oTestGroup = self.dTestGroups[oArgVariation.idTestGroup]; + if oTestCase.idTestCase != oArgVariation.idTestCase or oTestCase.idTestGroup != oArgVariation.idTestGroup: + oTestCase = oTestGroup.dTestCases[oArgVariation.idTestCase]; + + oTestCase.aoArgsVariations.append(oArgVariation); + oArgVariation.oTestCase = oTestCase; + oArgVariation.oTestGroup = oTestGroup; + + else: + assert not self.aoTestCases; + assert not self.aoArgsVariations; + # done. + + @staticmethod + def _addPreReqError(aoErrors, aidChain, oObj, sMsg): + """ Returns a chain of IDs error entry. """ + + sMsg += ' Dependency chain: %s' % (aidChain[0],); + for i in range(1, len(aidChain)): + sMsg += ' -> %s' % (aidChain[i],); + + aoErrors.append([sMsg, oObj]); + return aoErrors; + + def checkForGroupDepCycles(self): + """ + Checks for testgroup depencency cycles and any missing testgroup + dependencies. + Returns array of errors (see SchedulderBase.recreateQueue()). + """ + aoErrors = []; + for oTestGroup in self.aoTestGroups: + idPreReq = oTestGroup.idTestGroupPreReq; + if idPreReq is None: + oTestGroup.aidTestGroupPreReqs = []; + continue; + + aidChain = [oTestGroup.idTestGroup,]; + while idPreReq is not None: + aidChain.append(idPreReq); + if len(aidChain) >= 10: + self._addPreReqError(aoErrors, aidChain, oTestGroup, + 'TestGroup #%s prerequisite chain is too long!' + % (oTestGroup.idTestGroup,)); + break; + + oDep = self.dTestGroups.get(idPreReq, None); + if oDep is None: + self._addPreReqError(aoErrors, aidChain, oTestGroup, + 'TestGroup #%s prerequisite #%s is not in the scheduling group!' + % (oTestGroup.idTestGroup, idPreReq,)); + break; + + idPreReq = oDep.idTestGroupPreReq; + oTestGroup.aidTestGroupPreReqs = aidChain[1:]; + + return aoErrors; + + + def checkForMissingTestCaseDeps(self): + """ + Checks that testcase dependencies stays within bounds. We do not allow + dependencies outside a testgroup, no dependency cycles or even remotely + long dependency chains. + + Returns array of errors (see SchedulderBase.recreateQueue()). + """ + aoErrors = []; + for oTestGroup in self.aoTestGroups: + for oTestCase in oTestGroup.aoTestCases: + if not oTestCase.aidPreReqs: + continue; + + # Stupid recursion code using special stack(s). + aiIndexes = [[oTestCase, 0], ]; + aidChain = [oTestCase.idTestGroup,]; + while aiIndexes: + (oCur, i) = aiIndexes[-1]; + if i >= len(oCur.aidPreReqs): + aiIndexes.pop(); + aidChain.pop(); + else: + aiIndexes[-1][1] = i + 1; # whatever happens, we'll advance on the current level. + + idPreReq = oTestCase.aidPreReqs[i]; + oDep = oTestGroup.dTestCases.get(idPreReq, None); + if oDep is None: + self._addPreReqError(aoErrors, aidChain, oTestCase, + 'TestCase #%s prerequisite #%s is not in the scheduling group!' + % (oTestCase.idTestCase, idPreReq)); + elif idPreReq in aidChain: + self._addPreReqError(aoErrors, aidChain, oTestCase, + 'TestCase #%s prerequisite #%s creates a cycle!' + % (oTestCase.idTestCase, idPreReq)); + elif not oDep.aiPreReqs: + pass; + elif len(aidChain) >= 10: + self._addPreReqError(aoErrors, aidChain, oTestCase, + 'TestCase #%s prerequisite chain is too long!' % (oTestCase.idTestCase,)); + else: + aiIndexes.append([oDep, 0]); + aidChain.append(idPreReq); + + return aoErrors; + + def deepTestGroupSort(self): + """ + Sorts the testgroups and their testcases by priority and dependencies. + Note! Don't call this before checking for dependency cycles! + """ + if not self.aoTestGroups: + return; + + # + # ASSUMES groups as well as testcases are sorted by priority by the + # database. So we only have to concern ourselves with the dependency + # sorting. + # + iGrpPrio = self.aoTestGroups[0].iSchedPriority; + for iTestGroup, oTestGroup in enumerate(self.aoTestGroups): + if oTestGroup.iSchedPriority > iGrpPrio: + raise TMExceptionBase('Incorrectly sorted testgroups returned by database: iTestGroup=%s prio=%s %s' + % ( iTestGroup, iGrpPrio, + ', '.join(['(%s: %s)' % (oCur.idTestGroup, oCur.iSchedPriority) + for oCur in self.aoTestGroups]), ) ); + iGrpPrio = oTestGroup.iSchedPriority; + + if oTestGroup.aoTestCases: + iTstPrio = oTestGroup.aoTestCases[0].iSchedPriority; + for iTestCase, oTestCase in enumerate(oTestGroup.aoTestCases): + if oTestCase.iSchedPriority > iTstPrio: + raise TMExceptionBase('Incorrectly sorted testcases returned by database: i=%s prio=%s idGrp=%s %s' + % ( iTestCase, iTstPrio, oTestGroup.idTestGroup, + ', '.join(['(%s: %s)' % (oCur.idTestCase, oCur.iSchedPriority) + for oCur in oTestGroup.aoTestCases]),)); + + # + # Sort the testgroups by dependencies. + # + i = 0; + while i < len(self.aoTestGroups): + oTestGroup = self.aoTestGroups[i]; + if oTestGroup.idTestGroupPreReq is not None: + iPreReq = self.aoTestGroups.index(self.dTestGroups[oTestGroup.idTestGroupPreReq]); + if iPreReq > i: + # The prerequisite is after the current entry. Move the + # current entry so that it's following it's prereq entry. + self.aoTestGroups.insert(iPreReq + 1, oTestGroup); + self.aoTestGroups.pop(i); + continue; + assert iPreReq < i; + i += 1; # Advance. + + # + # Sort the testcases by dependencies. + # Same algorithm as above, just more prerequisites. + # + for oTestGroup in self.aoTestGroups: + i = 0; + while i < len(oTestGroup.aoTestCases): + oTestCase = oTestGroup.aoTestCases[i]; + if oTestCase.aidPreReqs: + for idPreReq in oTestCase.aidPreReqs: + iPreReq = oTestGroup.aoTestCases.index(oTestGroup.dTestCases[idPreReq]); + if iPreReq > i: + # The prerequisite is after the current entry. Move the + # current entry so that it's following it's prereq entry. + oTestGroup.aoTestGroups.insert(iPreReq + 1, oTestCase); + oTestGroup.aoTestGroups.pop(i); + i -= 1; # Don't advance. + break; + assert iPreReq < i; + i += 1; # Advance. + + + +class SchedQueueData(ModelDataBase): + """ + Scheduling queue data item. + """ + + ksIdAttr = 'idSchedGroup'; + + ksParam_idSchedGroup = 'SchedQueueData_idSchedGroup'; + ksParam_idItem = 'SchedQueueData_idItem'; + ksParam_offQueue = 'SchedQueueData_offQueue'; + ksParam_idGenTestCaseArgs = 'SchedQueueData_idGenTestCaseArgs'; + ksParam_idTestGroup = 'SchedQueueData_idTestGroup'; + ksParam_aidTestGroupPreReqs = 'SchedQueueData_aidTestGroupPreReqs'; + ksParam_bmHourlySchedule = 'SchedQueueData_bmHourlySchedule'; + ksParam_tsConfig = 'SchedQueueData_tsConfig'; + ksParam_tsLastScheduled = 'SchedQueueData_tsLastScheduled'; + ksParam_idTestSetGangLeader = 'SchedQueueData_idTestSetGangLeader'; + ksParam_cMissingGangMembers = 'SchedQueueData_cMissingGangMembers'; + + kasAllowNullAttributes = [ 'idItem', 'offQueue', 'aidTestGroupPreReqs', 'bmHourlySchedule', 'idTestSetGangLeader', + 'tsConfig', 'tsLastScheduled' ]; + + + def __init__(self): + ModelDataBase.__init__(self); + + # + # Initialize with defaults. + # See the database for explanations of each of these fields. + # + self.idSchedGroup = None; + self.idItem = None; + self.offQueue = None; + self.idGenTestCaseArgs = None; + self.idTestGroup = None; + self.aidTestGroupPreReqs = None; + self.bmHourlySchedule = None; + self.tsConfig = None; + self.tsLastScheduled = None; + self.idTestSetGangLeader = None; + self.cMissingGangMembers = 1; + + def initFromValues(self, idSchedGroup, idGenTestCaseArgs, idTestGroup, aidTestGroupPreReqs, # pylint: disable=too-many-arguments + bmHourlySchedule, cMissingGangMembers, + idItem = None, offQueue = None, tsConfig = None, tsLastScheduled = None, idTestSetGangLeader = None): + """ + Reinitialize with all attributes potentially given as inputs. + Return self. + """ + self.idSchedGroup = idSchedGroup; + self.idItem = idItem; + self.offQueue = offQueue; + self.idGenTestCaseArgs = idGenTestCaseArgs; + self.idTestGroup = idTestGroup; + self.aidTestGroupPreReqs = aidTestGroupPreReqs; + self.bmHourlySchedule = bmHourlySchedule; + self.tsConfig = tsConfig; + self.tsLastScheduled = tsLastScheduled; + self.idTestSetGangLeader = idTestSetGangLeader; + self.cMissingGangMembers = cMissingGangMembers; + return self; + + def initFromDbRow(self, aoRow): + """ + Initialize from database row (SELECT * FROM SchedQueues). + Returns self. + Raises exception if no row is specfied. + """ + if aoRow is None: + raise TMExceptionBase('SchedQueueData not found.'); + + self.idSchedGroup = aoRow[0]; + self.idItem = aoRow[1]; + self.offQueue = aoRow[2]; + self.idGenTestCaseArgs = aoRow[3]; + self.idTestGroup = aoRow[4]; + self.aidTestGroupPreReqs = aoRow[5]; + self.bmHourlySchedule = aoRow[6]; + self.tsConfig = aoRow[7]; + self.tsLastScheduled = aoRow[8]; + self.idTestSetGangLeader = aoRow[9]; + self.cMissingGangMembers = aoRow[10]; + return self; + + + + + + +class SchedulerBase(object): + """ + The scheduler base class. + + The scheduler classes have two functions: + 1. Recreate the scheduling queue. + 2. Pick the next task from the queue. + + The first is scheduler specific, the latter isn't. + """ + + class BuildCache(object): + """ Build cache. """ + + class BuildCacheIterator(object): + """ Build class iterator. """ + def __init__(self, oCache): + self.oCache = oCache; + self.iCur = 0; + + def __iter__(self): + """Returns self, required by the language.""" + return self; + + def __next__(self): + """Returns the next build, raises StopIteration when the end has been reached.""" + while True: + if self.iCur >= len(self.oCache.aoEntries): + oEntry = self.oCache.fetchFromCursor(); + if oEntry is None: + raise StopIteration; + else: + oEntry = self.oCache.aoEntries[self.iCur]; + self.iCur += 1; + if not oEntry.fRemoved: + return oEntry; + return None; # not reached, but make pylint happy (for now). + + def next(self): + """ For python 2.x. """ + return self.__next__(); + + class BuildCacheEntry(object): + """ Build cache entry. """ + + def __init__(self, oBuild, fMaybeBlacklisted): + self.oBuild = oBuild; + self._fBlacklisted = None if fMaybeBlacklisted is True else False; + self.fRemoved = False; + self._dPreReqDecisions = {}; + + def remove(self): + """ + Marks the cache entry as removed. + This doesn't actually remove it from the cache array, only marks + it as removed. It has no effect on open iterators. + """ + self.fRemoved = True; + + def getPreReqDecision(self, sPreReqSet): + """ + Retrieves a cached prerequisite decision. + Returns boolean if found, None if not. + """ + return self._dPreReqDecisions.get(sPreReqSet); + + def setPreReqDecision(self, sPreReqSet, fDecision): + """ + Caches a prerequistie decision. + """ + self._dPreReqDecisions[sPreReqSet] = fDecision; + return fDecision; + + def isBlacklisted(self, oDb): + """ Checks if the build is blacklisted. """ + if self._fBlacklisted is None: + self._fBlacklisted = BuildLogic(oDb).isBuildBlacklisted(self.oBuild); + return self._fBlacklisted; + + + def __init__(self): + self.aoEntries = []; + self.oCursor = None; + + def setupSource(self, oDb, idBuildSrc, sOs, sCpuArch, tsNow): + """ Configures the build cursor for the cache. """ + if not self.aoEntries and self.oCursor is None: + oBuildSource = BuildSourceData().initFromDbWithId(oDb, idBuildSrc, tsNow); + self.oCursor = BuildSourceLogic(oDb).openBuildCursor(oBuildSource, sOs, sCpuArch, tsNow); + return True; + + def __iter__(self): + """Return an iterator.""" + return self.BuildCacheIterator(self); + + def fetchFromCursor(self): + """ Fetches a build from the cursor and adds it to the cache.""" + if self.oCursor is None: + return None; + + try: + aoRow = self.oCursor.fetchOne(); + except: + return None; + if aoRow is None: + return None; + + oBuild = BuildDataEx().initFromDbRow(aoRow); + oEntry = self.BuildCacheEntry(oBuild, aoRow[-1]); + self.aoEntries.append(oEntry); + return oEntry; + + def __init__(self, oDb, oSchedGrpData, iVerbosity = 0, tsSecStart = None): + self._oDb = oDb; + self._oSchedGrpData = oSchedGrpData; + self._iVerbosity = iVerbosity; + self._asMessages = []; + self._tsSecStart = tsSecStart if tsSecStart is not None else utils.timestampSecond(); + self.oBuildCache = self.BuildCache(); + self.dTestGroupMembers = {}; + + @staticmethod + def _instantiate(oDb, oSchedGrpData, iVerbosity = 0, tsSecStart = None): + """ + Instantiate the scheduler specified by the scheduling group. + Returns scheduler child class instance. May raise exception if + the input is invalid. + """ + if oSchedGrpData.enmScheduler == SchedGroupData.ksScheduler_BestEffortContinuousIntegration: + from testmanager.core.schedulerbeci import SchdulerBeci; + oScheduler = SchdulerBeci(oDb, oSchedGrpData, iVerbosity, tsSecStart); + else: + raise oDb.integrityException('Invalid scheduler "%s", idSchedGroup=%d' \ + % (oSchedGrpData.enmScheduler, oSchedGrpData.idSchedGroup)); + return oScheduler; + + + # + # Misc. + # + + def msgDebug(self, sText): + """Debug printing.""" + if self._iVerbosity > 1: + self._asMessages.append('debug:' + sText); + return None; + + def msgInfo(self, sText): + """Info printing.""" + if self._iVerbosity > 1: + self._asMessages.append('info: ' + sText); + return None; + + def dprint(self, sMsg): + """Prints a debug message to the srv glue log (see config.py). """ + if config.g_kfSrvGlueDebugScheduler: + self._oDb.dprint(sMsg); + return None; + + def getElapsedSecs(self): + """ Returns the number of seconds this scheduling task has been running. """ + tsSecNow = utils.timestampSecond(); + if tsSecNow < self._tsSecStart: # paranoia + self._tsSecStart = tsSecNow; + return tsSecNow - self._tsSecStart; + + + # + # Create schedule. + # + + def _recreateQueueCancelGatherings(self): + """ + Cancels all pending gang gatherings on the current queue. + """ + self._oDb.execute('SELECT idTestSetGangLeader\n' + 'FROM SchedQueues\n' + 'WHERE idSchedGroup = %s\n' + ' AND idTestSetGangLeader is not NULL\n' + , (self._oSchedGrpData.idSchedGroup,)); + if self._oDb.getRowCount() > 0: + oTBStatusLogic = TestBoxStatusLogic(self._oDb); + for aoRow in self._oDb.fetchAll(): + idTestSetGangLeader = aoRow[0]; + oTBStatusLogic.updateGangStatus(idTestSetGangLeader, + TestBoxStatusData.ksTestBoxState_GangGatheringTimedOut, + fCommit = False); + return True; + + def _recreateQueueItems(self, oData): + """ + Returns an array of queue items (SchedQueueData). + Child classes must override this. + """ + _ = oData; + return []; + + def recreateQueueWorker(self): + """ + Worker for recreateQueue. + """ + + # + # Collect the necessary data and validate it. + # + oData = ReCreateQueueData(self._oDb, self._oSchedGrpData.idSchedGroup); + aoErrors = oData.checkForGroupDepCycles(); + aoErrors.extend(oData.checkForMissingTestCaseDeps()); + if not aoErrors: + oData.deepTestGroupSort(); + + # + # The creation of the scheduling queue is done by the child class. + # + # We will try guess where in queue we're currently at and rotate + # the items such that we will resume execution in the approximately + # same position. The goal of the scheduler is to provide a 100% + # deterministic result so that if we regenerate the queue when there + # are no changes to the testcases, testgroups or scheduling groups + # involved, test execution will be unchanged (save for maybe just a + # little for gang gathering). + # + aoItems = []; + if not oData.oSchedGroup.fEnabled: + self.msgInfo('Disabled.'); + elif not oData.aoArgsVariations: + self.msgInfo('Found no test case argument variations.'); + else: + aoItems = self._recreateQueueItems(oData); + self.msgDebug('len(aoItems)=%s' % (len(aoItems),)); + #for i in range(len(aoItems)): + # self.msgDebug('aoItems[%2d]=%s' % (i, aoItems[i])); + if aoItems: + self._oDb.execute('SELECT offQueue FROM SchedQueues WHERE idSchedGroup = %s ORDER BY idItem LIMIT 1' + , (self._oSchedGrpData.idSchedGroup,)); + if self._oDb.getRowCount() > 0: + offQueue = self._oDb.fetchOne()[0]; + self._oDb.execute('SELECT COUNT(*) FROM SchedQueues WHERE idSchedGroup = %s' + , (self._oSchedGrpData.idSchedGroup,)); + cItems = self._oDb.fetchOne()[0]; + offQueueNew = (offQueue * cItems) // len(aoItems); + if offQueueNew != 0: + aoItems = aoItems[offQueueNew:] + aoItems[:offQueueNew]; + + # + # Replace the scheduling queue. + # Care need to be take to first timeout/abort any gangs in the + # gathering state since these use the queue to set up the date. + # + self._recreateQueueCancelGatherings(); + self._oDb.execute('DELETE FROM SchedQueues WHERE idSchedGroup = %s\n', (self._oSchedGrpData.idSchedGroup,)); + if aoItems: + self._oDb.insertList('INSERT INTO SchedQueues (\n' + ' idSchedGroup,\n' + ' offQueue,\n' + ' idGenTestCaseArgs,\n' + ' idTestGroup,\n' + ' aidTestGroupPreReqs,\n' + ' bmHourlySchedule,\n' + ' cMissingGangMembers )\n', + aoItems, self._formatItemForInsert); + return (aoErrors, self._asMessages); + + def _formatItemForInsert(self, oItem): + """ + Used by recreateQueueWorker together with TMDatabaseConnect::insertList + """ + return self._oDb.formatBindArgs('(%s,%s,%s,%s,%s,%s,%s)' + , ( oItem.idSchedGroup, + oItem.offQueue, + oItem.idGenTestCaseArgs, + oItem.idTestGroup, + oItem.aidTestGroupPreReqs if oItem.aidTestGroupPreReqs else None, + oItem.bmHourlySchedule, + oItem.cMissingGangMembers + )); + + @staticmethod + def recreateQueue(oDb, uidAuthor, idSchedGroup, iVerbosity = 1): + """ + (Re-)creates the scheduling queue for the given group. + + Returns (asMessages, asMessages). On success the array with the error + will be empty, on failure it will contain (sError, oRelatedObject) + entries. The messages is for debugging and are simple strings. + + Raises exception database error. + """ + + aoExtraMsgs = []; + if oDb.debugIsExplainEnabled(): + aoExtraMsgs += ['Warning! Disabling SQL explain to avoid deadlocking against locked tables.']; + oDb.debugDisableExplain(); + + aoErrors = []; + asMessages = []; + try: + # + # To avoid concurrency issues (SchedQueues) and inconsistent data (*), + # we lock quite a few tables while doing this work. We access more + # data than scheduleNewTask so we lock some additional tables. + # + oDb.rollback(); + oDb.begin(); + oDb.execute('LOCK TABLE SchedGroups, SchedGroupMembers, TestGroups, TestGroupMembers IN SHARE MODE'); + oDb.execute('LOCK TABLE TestBoxes, TestCaseArgs, TestCases IN SHARE MODE'); + oDb.execute('LOCK TABLE TestBoxStatuses, SchedQueues IN EXCLUSIVE MODE'); + + # + # Instantiate the scheduler and call the worker function. + # + oSchedGrpData = SchedGroupData().initFromDbWithId(oDb, idSchedGroup); + oScheduler = SchedulerBase._instantiate(oDb, oSchedGrpData, iVerbosity); + + (aoErrors, asMessages) = oScheduler.recreateQueueWorker(); + if not aoErrors: + SystemLogLogic(oDb).addEntry(SystemLogData.ksEvent_SchedQueueRecreate, + 'User #%d recreated sched queue #%d.' % (uidAuthor, idSchedGroup,)); + oDb.commit(); + else: + oDb.rollback(); + + except: + oDb.rollback(); + raise; + + return (aoErrors, aoExtraMsgs + asMessages); + + + @staticmethod + def cleanUpOrphanedQueues(oDb): + """ + Removes orphan scheduling queues from the SchedQueues table. + + Queues becomes orphaned when the scheduling group they belongs to has been deleted. + + Returns number of orphaned queues. + Raises exception database error. + """ + cRet = 0; + try: + oDb.rollback(); + oDb.begin(); + oDb.execute(''' +SELECT SchedQueues.idSchedGroup +FROM SchedQueues + LEFT OUTER JOIN SchedGroups + ON SchedGroups.idSchedGroup = SchedQueues.idSchedGroup + AND SchedGroups.tsExpire = 'infinity'::TIMESTAMP +WHERE SchedGroups.idSchedGroup is NULL +GROUP BY SchedQueues.idSchedGroup'''); + aaoOrphanRows = oDb.fetchAll(); + cRet = len(aaoOrphanRows); + if cRet > 0: + oDb.execute('DELETE FROM SchedQueues WHERE idSchedGroup IN (%s)' + % (','.join([str(aoRow[0]) for aoRow in aaoOrphanRows]),)); + oDb.commit(); + except: + oDb.rollback(); + raise; + return cRet; + + + # + # Schedule Task. + # + + def _composeGangArguments(self, idTestSet): + """ + Composes the gang specific testdriver arguments. + Returns command line string, including a leading space. + """ + + oTestSet = TestSetData().initFromDbWithId(self._oDb, idTestSet); + aoGangMembers = TestSetLogic(self._oDb).getGang(oTestSet.idTestSetGangLeader); + + sArgs = ' --gang-member-no %s --gang-members %s' % (oTestSet.iGangMemberNo, len(aoGangMembers)); + for i, _ in enumerate(aoGangMembers): + sArgs = ' --gang-ipv4-%s %s' % (i, aoGangMembers[i].ip); ## @todo IPv6 + + return sArgs; + + + def composeExecResponseWorker(self, idTestSet, oTestEx, oTestBox, oBuild, oValidationKitBuild, sBaseUrl): + """ + Given all the bits of data, compose an EXEC command response to the testbox. + """ + sScriptZips = oTestEx.oTestCase.sValidationKitZips; + if sScriptZips is None or sScriptZips.find('@VALIDATIONKIT_ZIP@') >= 0: + assert oValidationKitBuild; + if sScriptZips is None: + sScriptZips = oValidationKitBuild.sBinaries; + else: + sScriptZips = sScriptZips.replace('@VALIDATIONKIT_ZIP@', oValidationKitBuild.sBinaries); + sScriptZips = sScriptZips.replace('@DOWNLOAD_BASE_URL@', sBaseUrl + config.g_ksTmDownloadBaseUrlRel); + + sCmdLine = oTestEx.oTestCase.sBaseCmd + ' ' + oTestEx.sArgs; + sCmdLine = sCmdLine.replace('@BUILD_BINARIES@', oBuild.sBinaries); + sCmdLine = sCmdLine.strip(); + if oTestEx.cGangMembers > 1: + sCmdLine += ' ' + self._composeGangArguments(idTestSet); + + cSecTimeout = oTestEx.cSecTimeout if oTestEx.cSecTimeout is not None else oTestEx.oTestCase.cSecTimeout; + cSecTimeout = cSecTimeout * oTestBox.pctScaleTimeout // 100; + + dResponse = \ + { + constants.tbresp.ALL_PARAM_RESULT: constants.tbresp.CMD_EXEC, + constants.tbresp.EXEC_PARAM_RESULT_ID: idTestSet, + constants.tbresp.EXEC_PARAM_SCRIPT_ZIPS: sScriptZips, + constants.tbresp.EXEC_PARAM_SCRIPT_CMD_LINE: sCmdLine, + constants.tbresp.EXEC_PARAM_TIMEOUT: cSecTimeout, + }; + return dResponse; + + @staticmethod + def composeExecResponse(oDb, idTestSet, sBaseUrl, iVerbosity = 0): + """ + Composes an EXEC response for a gang member (other than the last). + Returns a EXEC response or raises an exception (DB/input error). + """ + # + # Gather the necessary data. + # + oTestSet = TestSetData().initFromDbWithId(oDb, idTestSet); + oTestBox = TestBoxData().initFromDbWithGenId(oDb, oTestSet.idGenTestBox); + oTestEx = TestCaseArgsDataEx().initFromDbWithGenIdEx(oDb, oTestSet.idGenTestCaseArgs, + tsConfigEff = oTestSet.tsConfig, + tsRsrcEff = oTestSet.tsConfig); + oBuild = BuildDataEx().initFromDbWithId(oDb, oTestSet.idBuild); + oValidationKitBuild = None; + if oTestSet.idBuildTestSuite is not None: + oValidationKitBuild = BuildDataEx().initFromDbWithId(oDb, oTestSet.idBuildTestSuite); + + # + # Instantiate the specified scheduler and let it do the rest. + # + oSchedGrpData = SchedGroupData().initFromDbWithId(oDb, oTestSet.idSchedGroup, oTestSet.tsCreated); + assert oSchedGrpData.fEnabled is True; + assert oSchedGrpData.idBuildSrc is not None; + oScheduler = SchedulerBase._instantiate(oDb, oSchedGrpData, iVerbosity); + + return oScheduler.composeExecResponseWorker(idTestSet, oTestEx, oTestBox, oBuild, oValidationKitBuild, sBaseUrl); + + + def _updateTask(self, oTask, tsNow): + """ + Updates a gang schedule task. + """ + assert oTask.cMissingGangMembers >= 1; + assert oTask.idTestSetGangLeader is not None; + assert oTask.idTestSetGangLeader >= 1; + if tsNow is not None: + self._oDb.execute('UPDATE SchedQueues\n' + ' SET idTestSetGangLeader = %s,\n' + ' cMissingGangMembers = %s,\n' + ' tsLastScheduled = %s\n' + 'WHERE idItem = %s\n' + , (oTask.idTestSetGangLeader, oTask.cMissingGangMembers, tsNow, oTask.idItem,) ); + else: + self._oDb.execute('UPDATE SchedQueues\n' + ' SET cMissingGangMembers = %s\n' + 'WHERE idItem = %s\n' + , (oTask.cMissingGangMembers, oTask.idItem,) ); + return True; + + def _moveTaskToEndOfQueue(self, oTask, cGangMembers, tsNow): + """ + The task has been scheduled successfully, reset it's data move it to + the end of the queue. + """ + if cGangMembers > 1: + self._oDb.execute('UPDATE SchedQueues\n' + ' SET idItem = NEXTVAL(\'SchedQueueItemIdSeq\'),\n' + ' idTestSetGangLeader = NULL,\n' + ' cMissingGangMembers = %s\n' + 'WHERE idItem = %s\n' + , (cGangMembers, oTask.idItem,) ); + else: + self._oDb.execute('UPDATE SchedQueues\n' + ' SET idItem = NEXTVAL(\'SchedQueueItemIdSeq\'),\n' + ' idTestSetGangLeader = NULL,\n' + ' cMissingGangMembers = 1,\n' + ' tsLastScheduled = %s\n' + 'WHERE idItem = %s\n' + , (tsNow, oTask.idItem,) ); + return True; + + + + + def _createTestSet(self, oTask, oTestEx, oTestBoxData, oBuild, oValidationKitBuild, tsNow): + # type: (SchedQueueData, TestCaseArgsDataEx, TestBoxData, BuildDataEx, BuildDataEx, datetime.datetime) -> int + """ + Creates a test set for using the given data. + Will not commit, someone up the callstack will that later on. + + Returns the test set ID, may raise an exception on database error. + """ + # Lazy bird doesn't want to write testset.py and does it all here. + + # + # We're getting the TestSet ID first in order to include it in the base + # file name (that way we can directly relate files on the disk to the + # test set when doing batch work), and also for idTesetSetGangLeader. + # + self._oDb.execute('SELECT NEXTVAL(\'TestSetIdSeq\')'); + idTestSet = self._oDb.fetchOne()[0]; + + sBaseFilename = '%04d/%02d/%02d/%02d/TestSet-%s' \ + % (tsNow.year, tsNow.month, tsNow.day, (tsNow.hour // 6) * 6, idTestSet); + + # + # Gang scheduling parameters. Changes the oTask data for updating by caller. + # + iGangMemberNo = 0; + + if oTestEx.cGangMembers <= 1: + assert oTask.idTestSetGangLeader is None; + assert oTask.cMissingGangMembers <= 1; + elif oTask.idTestSetGangLeader is None: + assert oTask.cMissingGangMembers == oTestEx.cGangMembers; + oTask.cMissingGangMembers = oTestEx.cGangMembers - 1; + oTask.idTestSetGangLeader = idTestSet; + else: + assert oTask.cMissingGangMembers > 0 and oTask.cMissingGangMembers < oTestEx.cGangMembers; + oTask.cMissingGangMembers -= 1; + + # + # Do the database stuff. + # + self._oDb.execute('INSERT INTO TestSets (\n' + ' idTestSet,\n' + ' tsConfig,\n' + ' tsCreated,\n' + ' idBuild,\n' + ' idBuildCategory,\n' + ' idBuildTestSuite,\n' + ' idGenTestBox,\n' + ' idTestBox,\n' + ' idSchedGroup,\n' + ' idTestGroup,\n' + ' idGenTestCase,\n' + ' idTestCase,\n' + ' idGenTestCaseArgs,\n' + ' idTestCaseArgs,\n' + ' sBaseFilename,\n' + ' iGangMemberNo,\n' + ' idTestSetGangLeader )\n' + 'VALUES ( %s,\n' # idTestSet + ' %s,\n' # tsConfig + ' %s,\n' # tsCreated + ' %s,\n' # idBuild + ' %s,\n' # idBuildCategory + ' %s,\n' # idBuildTestSuite + ' %s,\n' # idGenTestBox + ' %s,\n' # idTestBox + ' %s,\n' # idSchedGroup + ' %s,\n' # idTestGroup + ' %s,\n' # idGenTestCase + ' %s,\n' # idTestCase + ' %s,\n' # idGenTestCaseArgs + ' %s,\n' # idTestCaseArgs + ' %s,\n' # sBaseFilename + ' %s,\n' # iGangMemberNo + ' %s)\n' # idTestSetGangLeader + , ( idTestSet, + oTask.tsConfig, + tsNow, + oBuild.idBuild, + oBuild.idBuildCategory, + oValidationKitBuild.idBuild if oValidationKitBuild is not None else None, + oTestBoxData.idGenTestBox, + oTestBoxData.idTestBox, + oTask.idSchedGroup, + oTask.idTestGroup, + oTestEx.oTestCase.idGenTestCase, + oTestEx.oTestCase.idTestCase, + oTestEx.idGenTestCaseArgs, + oTestEx.idTestCaseArgs, + sBaseFilename, + iGangMemberNo, + oTask.idTestSetGangLeader, + )); + + self._oDb.execute('INSERT INTO TestResults (\n' + ' idTestResultParent,\n' + ' idTestSet,\n' + ' tsCreated,\n' + ' idStrName,\n' + ' cErrors,\n' + ' enmStatus,\n' + ' iNestingDepth)\n' + 'VALUES ( NULL,\n' # idTestResultParent + ' %s,\n' # idTestSet + ' %s,\n' # tsCreated + ' 0,\n' # idStrName + ' 0,\n' # cErrors + ' \'running\'::TestStatus_T,\n' + ' 0)\n' # iNestingDepth + 'RETURNING idTestResult' + , ( idTestSet, tsNow, )); + idTestResult = self._oDb.fetchOne()[0]; + + self._oDb.execute('UPDATE TestSets\n' + ' SET idTestResult = %s\n' + 'WHERE idTestSet = %s\n' + , (idTestResult, idTestSet, )); + + return idTestSet; + + def _tryFindValidationKitBit(self, oTestBoxData, tsNow): + """ + Tries to find the most recent validation kit build suitable for the given testbox. + Returns BuildDataEx or None. Raise exception on database error. + + Can be overridden by child classes to change the default build requirements. + """ + oBuildLogic = BuildLogic(self._oDb); + oBuildSource = BuildSourceData().initFromDbWithId(self._oDb, self._oSchedGrpData.idBuildSrcTestSuite, tsNow); + oCursor = BuildSourceLogic(self._oDb).openBuildCursor(oBuildSource, oTestBoxData.sOs, oTestBoxData.sCpuArch, tsNow); + for _ in range(oCursor.getRowCount()): + oBuild = BuildDataEx().initFromDbRow(oCursor.fetchOne()); + if not oBuildLogic.isBuildBlacklisted(oBuild): + return oBuild; + return None; + + def _tryFindBuild(self, oTask, oTestEx, oTestBoxData, tsNow): + """ + Tries to find a fitting build. + Returns BuildDataEx or None. Raise exception on database error. + + Can be overridden by child classes to change the default build requirements. + """ + + # + # Gather the set of prerequisites we have and turn them into a value + # set for use in the loop below. + # + # Note! We're scheduling on testcase level and ignoring argument variation + # selections in TestGroupMembers is intentional. + # + dPreReqs = {}; + + # Direct prerequisites. We assume they're all enabled as this can be + # checked at queue creation time. + for oPreReq in oTestEx.aoTestCasePreReqs: + dPreReqs[oPreReq.idTestCase] = 1; + + # Testgroup dependencies from the scheduling group config. + if oTask.aidTestGroupPreReqs is not None: + for iTestGroup in oTask.aidTestGroupPreReqs: + # Make sure the _active_ test group members are in the cache. + if iTestGroup not in self.dTestGroupMembers: + self._oDb.execute('SELECT DISTINCT TestGroupMembers.idTestCase\n' + 'FROM TestGroupMembers, TestCases\n' + 'WHERE TestGroupMembers.idTestGroup = %s\n' + ' AND TestGroupMembers.tsExpire > %s\n' + ' AND TestGroupMembers.tsEffective <= %s\n' + ' AND TestCases.idTestCase = TestGroupMembers.idTestCase\n' + ' AND TestCases.tsExpire > %s\n' + ' AND TestCases.tsEffective <= %s\n' + ' AND TestCases.fEnabled is TRUE\n' + , (iTestGroup, oTask.tsConfig, oTask.tsConfig, oTask.tsConfig, oTask.tsConfig,)); + aidTestCases = []; + for aoRow in self._oDb.fetchAll(): + aidTestCases.append(aoRow[0]); + self.dTestGroupMembers[iTestGroup] = aidTestCases; + + # Add the testgroup members to the prerequisites. + for idTestCase in self.dTestGroupMembers[iTestGroup]: + dPreReqs[idTestCase] = 1; + + # Create a SQL values table out of them. + sPreReqSet = '' + if dPreReqs: + for idPreReq in sorted(dPreReqs): + sPreReqSet += ', (' + str(idPreReq) + ')'; + sPreReqSet = sPreReqSet[2:]; # drop the leading ', '. + + # + # Try the builds. + # + self.oBuildCache.setupSource(self._oDb, self._oSchedGrpData.idBuildSrc, oTestBoxData.sOs, oTestBoxData.sCpuArch, tsNow); + for oEntry in self.oBuildCache: + # + # Check build requirements set by the test. + # + if not oTestEx.matchesBuildProps(oEntry.oBuild): + continue; + + if oEntry.isBlacklisted(self._oDb): + oEntry.remove(); + continue; + + # + # Check prerequisites. The default scheduler is satisfied if one + # argument variation has been executed successfully. It is not + # satisfied if there are any failure runs. + # + if sPreReqSet: + fDecision = oEntry.getPreReqDecision(sPreReqSet); + if fDecision is None: + # Check for missing prereqs. + self._oDb.execute('SELECT COUNT(*)\n' + 'FROM (VALUES ' + sPreReqSet + ') AS PreReqs(idTestCase)\n' + 'LEFT OUTER JOIN (SELECT idTestSet\n' + ' FROM TestSets\n' + ' WHERE enmStatus IN (%s, %s)\n' + ' AND idBuild = %s\n' + ' ) AS TestSets\n' + ' ON (PreReqs.idTestCase = TestSets.idTestCase)\n' + 'WHERE TestSets.idTestSet is NULL\n' + , ( TestSetData.ksTestStatus_Success, TestSetData.ksTestStatus_Skipped, + oEntry.oBuild.idBuild, )); + cMissingPreReqs = self._oDb.fetchOne()[0]; + if cMissingPreReqs > 0: + self.dprint('build %s is missing %u prerequisites (out of %s)' + % (oEntry.oBuild.idBuild, cMissingPreReqs, sPreReqSet,)); + oEntry.setPreReqDecision(sPreReqSet, False); + continue; + + # Check for failed prereq runs. + self._oDb.execute('SELECT COUNT(*)\n' + 'FROM (VALUES ' + sPreReqSet + ') AS PreReqs(idTestCase),\n' + ' TestSets\n' + 'WHERE PreReqs.idTestCase = TestSets.idTestCase\n' + ' AND TestSets.idBuild = %s\n' + ' AND TestSets.enmStatus IN (%s, %s, %s)\n' + , ( oEntry.oBuild.idBuild, + TestSetData.ksTestStatus_Failure, + TestSetData.ksTestStatus_TimedOut, + TestSetData.ksTestStatus_Rebooted, + ) + ); + cFailedPreReqs = self._oDb.fetchOne()[0]; + if cFailedPreReqs > 0: + self.dprint('build %s is has %u prerequisite failures (out of %s)' + % (oEntry.oBuild.idBuild, cFailedPreReqs, sPreReqSet,)); + oEntry.setPreReqDecision(sPreReqSet, False); + continue; + + oEntry.setPreReqDecision(sPreReqSet, True); + elif not fDecision: + continue; + + # + # If we can, check if the build files still exist. + # + if oEntry.oBuild.areFilesStillThere() is False: + self.dprint('build %s no longer exists' % (oEntry.oBuild.idBuild,)); + oEntry.remove(); + continue; + + self.dprint('found oBuild=%s' % (oEntry.oBuild,)); + return oEntry.oBuild; + return None; + + def _tryFindMatchingBuild(self, oLeaderBuild, oTestBoxData, idBuildSrc): + """ + Tries to find a matching build for gang scheduling. + Returns BuildDataEx or None. Raise exception on database error. + + Can be overridden by child classes to change the default build requirements. + """ + # + # Note! Should probably check build prerequisites if we get a different + # build back, so that we don't use a build which hasn't passed + # the smoke test. + # + _ = idBuildSrc; + return BuildLogic(self._oDb).tryFindSameBuildForOsArch(oLeaderBuild, oTestBoxData.sOs, oTestBoxData.sCpuArch); + + + def _tryAsLeader(self, oTask, oTestEx, oTestBoxData, tsNow, sBaseUrl): + """ + Try schedule the task as a gang leader (can be a gang of one). + Returns response or None. May raise exception on DB error. + """ + + # We don't wait for busy resources, we just try the next test. + oTestArgsLogic = TestCaseArgsLogic(self._oDb); + if not oTestArgsLogic.areResourcesFree(oTestEx): + self.dprint('Cannot get global test resources!'); + return None; + + # + # Find a matching build (this is the difficult bit). + # + oBuild = self._tryFindBuild(oTask, oTestEx, oTestBoxData, tsNow); + if oBuild is None: + self.dprint('No build!'); + return None; + if oTestEx.oTestCase.needValidationKitBit(): + oValidationKitBuild = self._tryFindValidationKitBit(oTestBoxData, tsNow); + if oValidationKitBuild is None: + self.dprint('No validation kit build!'); + return None; + else: + oValidationKitBuild = None; + + # + # Create a testset, allocate the resources and update the state. + # Note! Since resource allocation may still fail, we create a nested + # transaction so we can roll back. (Heed lock warning in docs!) + # + self._oDb.execute('SAVEPOINT tryAsLeader'); + idTestSet = self._createTestSet(oTask, oTestEx, oTestBoxData, oBuild, oValidationKitBuild, tsNow); + + if GlobalResourceLogic(self._oDb).allocateResources(oTestBoxData.idTestBox, oTestEx.aoGlobalRsrc, fCommit = False) \ + is not True: + self._oDb.execute('ROLLBACK TO SAVEPOINT tryAsLeader'); + self.dprint('Failed to allocate global resources!'); + return False; + + if oTestEx.cGangMembers <= 1: + # We're alone, put the task back at the end of the queue and issue EXEC cmd. + self._moveTaskToEndOfQueue(oTask, oTestEx.cGangMembers, tsNow); + dResponse = self.composeExecResponseWorker(idTestSet, oTestEx, oTestBoxData, oBuild, oValidationKitBuild, sBaseUrl); + sTBState = TestBoxStatusData.ksTestBoxState_Testing; + else: + # We're missing gang members, issue WAIT cmd. + self._updateTask(oTask, tsNow if idTestSet == oTask.idTestSetGangLeader else None); + dResponse = { constants.tbresp.ALL_PARAM_RESULT: constants.tbresp.CMD_WAIT, }; + sTBState = TestBoxStatusData.ksTestBoxState_GangGathering; + + TestBoxStatusLogic(self._oDb).updateState(oTestBoxData.idTestBox, sTBState, idTestSet, fCommit = False); + self._oDb.execute('RELEASE SAVEPOINT tryAsLeader'); + return dResponse; + + def _tryAsGangMember(self, oTask, oTestEx, oTestBoxData, tsNow, sBaseUrl): + """ + Try schedule the task as a gang member. + Returns response or None. May raise exception on DB error. + """ + + # + # The leader has choosen a build, we need to find a matching one for our platform. + # (It's up to the scheduler decide upon how strict dependencies are to be enforced + # upon subordinate group members.) + # + oLeaderTestSet = TestSetData().initFromDbWithId(self._oDb, oTestBoxData.idTestSetGangLeader); + + oLeaderBuild = BuildDataEx().initFromDbWithId(self._oDb, oLeaderTestSet.idBuild); + oBuild = self._tryFindMatchingBuild(oLeaderBuild, oTestBoxData, self._oSchedGrpData.idBuildSrc); + if oBuild is None: + return None; + + oValidationKitBuild = None; + if oLeaderTestSet.idBuildTestSuite is not None: + oLeaderValidationKitBit = BuildDataEx().initFromDbWithId(self._oDb, oLeaderTestSet.idBuildTestSuite); + oValidationKitBuild = self._tryFindMatchingBuild(oLeaderValidationKitBit, oTestBoxData, + self._oSchedGrpData.idBuildSrcTestSuite); + + # + # Create a testset and update the state(s). + # + idTestSet = self._createTestSet(oTask, oTestEx, oTestBoxData, oBuild, oValidationKitBuild, tsNow); + + oTBStatusLogic = TestBoxStatusLogic(self._oDb); + if oTask.cMissingGangMembers < 1: + # The whole gang is there, move the task to the end of the queue + # and update the status on the other gang members. + self._moveTaskToEndOfQueue(oTask, oTestEx.cGangMembers, tsNow); + dResponse = self.composeExecResponseWorker(idTestSet, oTestEx, oTestBoxData, oBuild, oValidationKitBuild, sBaseUrl); + sTBState = TestBoxStatusData.ksTestBoxState_GangTesting; + oTBStatusLogic.updateGangStatus(oTask.idTestSetGangLeader, sTBState, fCommit = False); + else: + # We're still missing some gang members, issue WAIT cmd. + self._updateTask(oTask, tsNow if idTestSet == oTask.idTestSetGangLeader else None); + dResponse = { constants.tbresp.ALL_PARAM_RESULT: constants.tbresp.CMD_WAIT, }; + sTBState = TestBoxStatusData.ksTestBoxState_GangGathering; + + oTBStatusLogic.updateState(oTestBoxData.idTestBox, sTBState, idTestSet, fCommit = False); + return dResponse; + + + def scheduleNewTaskWorker(self, oTestBoxData, tsNow, sBaseUrl): + """ + Worker for schduling a new task. + """ + + # + # Iterate the scheduler queue (fetch all to avoid having to concurrent + # queries), trying out each task to see if the testbox can execute it. + # + dRejected = {}; # variations we've already checked out and rejected. + self._oDb.execute('SELECT *\n' + 'FROM SchedQueues\n' + 'WHERE idSchedGroup = %s\n' + ' AND ( bmHourlySchedule IS NULL\n' + ' OR get_bit(bmHourlySchedule, %s) = 1 )\n' + 'ORDER BY idItem ASC\n' + , (self._oSchedGrpData.idSchedGroup, utils.getLocalHourOfWeek()) ); + aaoRows = self._oDb.fetchAll(); + for aoRow in aaoRows: + # Don't loop forever. + if self.getElapsedSecs() >= config.g_kcSecMaxNewTask: + break; + + # Unpack the data and check if we've rejected the testcasevar/group variation already (they repeat). + oTask = SchedQueueData().initFromDbRow(aoRow); + if config.g_kfSrvGlueDebugScheduler: + self.dprint('** Considering: idItem=%s idGenTestCaseArgs=%s idTestGroup=%s Deps=%s last=%s cfg=%s\n' + % ( oTask.idItem, oTask.idGenTestCaseArgs, oTask.idTestGroup, oTask.aidTestGroupPreReqs, + oTask.tsLastScheduled, oTask.tsConfig,)); + + sRejectNm = '%s:%s' % (oTask.idGenTestCaseArgs, oTask.idTestGroup,); + if sRejectNm in dRejected: + self.dprint('Duplicate, already rejected! (%s)' % (sRejectNm,)); + continue; + dRejected[sRejectNm] = 1; + + # Fetch all the test case info (too much, but who cares right now). + oTestEx = TestCaseArgsDataEx().initFromDbWithGenIdEx(self._oDb, oTask.idGenTestCaseArgs, + tsConfigEff = oTask.tsConfig, + tsRsrcEff = oTask.tsConfig); + if config.g_kfSrvGlueDebugScheduler: + self.dprint('TestCase "%s": %s %s' % (oTestEx.oTestCase.sName, oTestEx.oTestCase.sBaseCmd, oTestEx.sArgs,)); + + # This shouldn't happen, but just in case it does... + if oTestEx.oTestCase.fEnabled is not True: + self.dprint('Testcase is not enabled!!'); + continue; + + # Check if the testbox properties matches the test. + if not oTestEx.matchesTestBoxProps(oTestBoxData): + self.dprint('Testbox mismatch!'); + continue; + + # Try schedule it. + if oTask.idTestSetGangLeader is None or oTestEx.cGangMembers <= 1: + dResponse = self._tryAsLeader(oTask, oTestEx, oTestBoxData, tsNow, sBaseUrl); + elif oTask.cMissingGangMembers > 1: + dResponse = self._tryAsGangMember(oTask, oTestEx, oTestBoxData, tsNow, sBaseUrl); + else: + dResponse = None; # Shouldn't happen! + if dResponse is not None: + self.dprint('Found a task! dResponse=%s' % (dResponse,)); + return dResponse; + + # Found no suitable task. + return None; + + @staticmethod + def _pickSchedGroup(oTestBoxDataEx, iWorkItem, dIgnoreSchedGroupIds): + """ + Picks the next scheduling group for the given testbox. + """ + if len(oTestBoxDataEx.aoInSchedGroups) == 1: + oSchedGroup = oTestBoxDataEx.aoInSchedGroups[0].oSchedGroup; + if oSchedGroup.fEnabled \ + and oSchedGroup.idBuildSrc is not None \ + and oSchedGroup.idSchedGroup not in dIgnoreSchedGroupIds: + return (oSchedGroup, 0); + iWorkItem = 0; + + elif oTestBoxDataEx.aoInSchedGroups: + # Construct priority table of currently enabled scheduling groups. + aaoList1 = []; + for oInGroup in oTestBoxDataEx.aoInSchedGroups: + oSchedGroup = oInGroup.oSchedGroup; + if oSchedGroup.fEnabled and oSchedGroup.idBuildSrc is not None: + iSchedPriority = oInGroup.iSchedPriority; + if iSchedPriority > 31: # paranoia + iSchedPriority = 31; + elif iSchedPriority < 0: # paranoia + iSchedPriority = 0; + + for iSchedPriority in xrange(min(iSchedPriority, len(aaoList1))): + aaoList1[iSchedPriority].append(oSchedGroup); + while len(aaoList1) <= iSchedPriority: + aaoList1.append([oSchedGroup,]); + + # Flatten it into a single list, mixing the priorities a little so it doesn't + # take forever before low priority stuff is executed. + aoFlat = []; + iLo = 0; + iHi = len(aaoList1) - 1; + while iHi >= iLo: + aoFlat += aaoList1[iHi]; + if iLo < iHi: + aoFlat += aaoList1[iLo]; + iLo += 1; + iHi -= 1; + + # Pick the next one. + cLeft = len(aoFlat); + while cLeft > 0: + cLeft -= 1; + iWorkItem += 1; + if iWorkItem >= len(aoFlat) or iWorkItem < 0: + iWorkItem = 0; + if aoFlat[iWorkItem].idSchedGroup not in dIgnoreSchedGroupIds: + return (aoFlat[iWorkItem], iWorkItem); + else: + iWorkItem = 0; + + # No active group. + return (None, iWorkItem); + + @staticmethod + def scheduleNewTask(oDb, oTestBoxData, iWorkItem, sBaseUrl, iVerbosity = 0): + # type: (TMDatabaseConnection, TestBoxData, int, str, int) -> None + """ + Schedules a new task for a testbox. + """ + oTBStatusLogic = TestBoxStatusLogic(oDb); + + try: + # + # To avoid concurrency issues in SchedQueues we lock all the rows + # related to our scheduling queue. Also, since this is a very + # expensive operation we lock the testbox status row to fend of + # repeated retires by faulty testbox scripts. + # + tsSecStart = utils.timestampSecond(); + oDb.rollback(); + oDb.begin(); + oDb.execute('SELECT idTestBox FROM TestBoxStatuses WHERE idTestBox = %s FOR UPDATE NOWAIT' + % (oTestBoxData.idTestBox,)); + oDb.execute('SELECT SchedQueues.idSchedGroup\n' + ' FROM SchedQueues, TestBoxesInSchedGroups\n' + 'WHERE TestBoxesInSchedGroups.idTestBox = %s\n' + ' AND TestBoxesInSchedGroups.tsExpire = \'infinity\'::TIMESTAMP\n' + ' AND TestBoxesInSchedGroups.idSchedGroup = SchedQueues.idSchedGroup\n' + ' FOR UPDATE' + % (oTestBoxData.idTestBox,)); + + # We need the current timestamp. + tsNow = oDb.getCurrentTimestamp(); + + # Re-read the testbox data with scheduling group relations. + oTestBoxDataEx = TestBoxDataEx().initFromDbWithId(oDb, oTestBoxData.idTestBox, tsNow); + if oTestBoxDataEx.fEnabled \ + and oTestBoxDataEx.idGenTestBox == oTestBoxData.idGenTestBox: + + # We may have to skip scheduling groups that are out of work (e.g. 'No build'). + iInitialWorkItem = iWorkItem; + dIgnoreSchedGroupIds = {}; + while True: + # Now, pick the scheduling group. + (oSchedGroup, iWorkItem) = SchedulerBase._pickSchedGroup(oTestBoxDataEx, iWorkItem, dIgnoreSchedGroupIds); + if oSchedGroup is None: + break; + assert oSchedGroup.fEnabled and oSchedGroup.idBuildSrc is not None; + + # Instantiate the specified scheduler and let it do the rest. + oScheduler = SchedulerBase._instantiate(oDb, oSchedGroup, iVerbosity, tsSecStart); + dResponse = oScheduler.scheduleNewTaskWorker(oTestBoxDataEx, tsNow, sBaseUrl); + if dResponse is not None: + oTBStatusLogic.updateWorkItem(oTestBoxDataEx.idTestBox, iWorkItem); + oDb.commit(); + return dResponse; + + # Check out the next work item? + if oScheduler.getElapsedSecs() > config.g_kcSecMaxNewTask: + break; + dIgnoreSchedGroupIds[oSchedGroup.idSchedGroup] = oSchedGroup; + + # No luck, but best if we update the work item if we've made progress. + # Note! In case of a config.g_kcSecMaxNewTask timeout, this may accidentally skip + # a work item with actually work to do. But that's a small price to pay. + if iWorkItem != iInitialWorkItem: + oTBStatusLogic.updateWorkItem(oTestBoxDataEx.idTestBox, iWorkItem); + oDb.commit(); + return None; + except: + oDb.rollback(); + raise; + + # Not enabled, rollback and return no task. + oDb.rollback(); + return None; + + @staticmethod + def tryCancelGangGathering(oDb, oStatusData): + """ + Try canceling a gang gathering. + + Returns True if successfully cancelled. + Returns False if not (someone raced us to the SchedQueue table). + + Note! oStatusData is re-initialized. + """ + assert oStatusData.enmState == TestBoxStatusData.ksTestBoxState_GangGathering; + try: + # + # Lock the tables we're updating so we don't run into concurrency + # issues (we're racing both scheduleNewTask and other callers of + # this method). + # + oDb.rollback(); + oDb.begin(); + oDb.execute('LOCK TABLE TestBoxStatuses, SchedQueues IN EXCLUSIVE MODE'); + + # + # Re-read the testbox data and check that we're still in the same state. + # + oStatusData.initFromDbWithId(oDb, oStatusData.idTestBox); + if oStatusData.enmState == TestBoxStatusData.ksTestBoxState_GangGathering: + # + # Get the leader thru the test set and change the state of the whole gang. + # + oTestSetData = TestSetData().initFromDbWithId(oDb, oStatusData.idTestSet); + + oTBStatusLogic = TestBoxStatusLogic(oDb); + oTBStatusLogic.updateGangStatus(oTestSetData.idTestSetGangLeader, + TestBoxStatusData.ksTestBoxState_GangGatheringTimedOut, + fCommit = False); + + # + # Move the scheduling queue item to the end. + # + oDb.execute('SELECT *\n' + 'FROM SchedQueues\n' + 'WHERE idTestSetGangLeader = %s\n' + , (oTestSetData.idTestSetGangLeader,) ); + oTask = SchedQueueData().initFromDbRow(oDb.fetchOne()); + oTestEx = TestCaseArgsDataEx().initFromDbWithGenIdEx(oDb, oTask.idGenTestCaseArgs, + tsConfigEff = oTask.tsConfig, + tsRsrcEff = oTask.tsConfig); + oDb.execute('UPDATE SchedQueues\n' + ' SET idItem = NEXTVAL(\'SchedQueueItemIdSeq\'),\n' + ' idTestSetGangLeader = NULL,\n' + ' cMissingGangMembers = %s\n' + 'WHERE idItem = %s\n' + , (oTestEx.cGangMembers, oTask.idItem,) ); + + oDb.commit(); + return True; + + if oStatusData.enmState == TestBoxStatusData.ksTestBoxState_GangGatheringTimedOut: + oDb.rollback(); + return True; + except: + oDb.rollback(); + raise; + + # Not enabled, rollback and return no task. + oDb.rollback(); + return False; + + +# +# Unit testing. +# + +# pylint: disable=missing-docstring +class SchedQueueDataTestCase(ModelDataBaseTestCase): + def setUp(self): + self.aoSamples = [SchedQueueData(),]; + +if __name__ == '__main__': + unittest.main(); + # not reached. + diff --git a/src/VBox/ValidationKit/testmanager/core/schedulerbeci.py b/src/VBox/ValidationKit/testmanager/core/schedulerbeci.py new file mode 100755 index 00000000..5cd800d5 --- /dev/null +++ b/src/VBox/ValidationKit/testmanager/core/schedulerbeci.py @@ -0,0 +1,128 @@ +# -*- coding: utf-8 -*- +# $Id: schedulerbeci.py $ + +""" +Test Manager - Best-Effort-Continuous-Integration (BECI) scheduler. +""" + +__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 $" + + +# Validation Kit imports. +from testmanager.core.schedulerbase import SchedulerBase, SchedQueueData; + + +class SchdulerBeci(SchedulerBase): # pylint: disable=too-few-public-methods + """ + The best-effort-continuous-integration scheduler, BECI for short. + """ + + def __init__(self, oDb, oSchedGrpData, iVerbosity, tsSecStart): + SchedulerBase.__init__(self, oDb, oSchedGrpData, iVerbosity, tsSecStart); + + def _recreateQueueItems(self, oData): + # + # Prepare the input data for the loop below. We compress the priority + # to reduce the number of loops we need to executes below. + # + # Note! For BECI test group priority only applies to the ordering of + # test groups, which has been resolved by the done sorting in the + # base class. + # + iMinPriority = 0x7fff; + iMaxPriority = 0; + for oTestGroup in oData.aoTestGroups: + for oTestCase in oTestGroup.aoTestCases: + iPrio = oTestCase.iSchedPriority; + assert iPrio in range(32); + iPrio = iPrio // 4; + assert iPrio in range(8); + if iPrio > iMaxPriority: + iMaxPriority = iPrio; + if iPrio < iMinPriority: + iMinPriority = iPrio; + + oTestCase.iBeciPrio = iPrio; + oTestCase.iNextVariation = -1; + + assert iMinPriority in range(8); + assert iMaxPriority in range(8); + assert iMinPriority <= iMaxPriority; + + # + # Generate the + # + cMaxItems = len(oData.aoArgsVariations) * 64; + cMaxItems = min(cMaxItems, 1048576); + + aoItems = []; + cNotAtEnd = len(oData.aoTestCases); + while len(aoItems) < cMaxItems: + self.msgDebug('outer loop: %s items' % (len(aoItems),)); + for iPrio in range(iMaxPriority, iMinPriority - 1, -1): + #self.msgDebug('prio loop: %s' % (iPrio,)); + for oTestGroup in oData.aoTestGroups: + #self.msgDebug('testgroup loop: %s' % (oTestGroup,)); + for oTestCase in oTestGroup.aoTestCases: + #self.msgDebug('testcase loop: idTestCase=%s' % (oTestCase.idTestCase,)); + if iPrio <= oTestCase.iBeciPrio and oTestCase.aoArgsVariations: + # Get variation. + iNext = oTestCase.iNextVariation; + if iNext != 0: + if iNext == -1: iNext = 0; + cNotAtEnd -= 1; + oArgsVariation = oTestCase.aoArgsVariations[iNext]; + + # Update next variation. + iNext = (iNext + 1) % len(oTestCase.aoArgsVariations); + cNotAtEnd += iNext != 0; + oTestCase.iNextVariation = iNext; + + # Create queue item and append it. + oItem = SchedQueueData(); + oItem.initFromValues(idSchedGroup = self._oSchedGrpData.idSchedGroup, + idGenTestCaseArgs = oArgsVariation.idGenTestCaseArgs, + idTestGroup = oTestGroup.idTestGroup, + aidTestGroupPreReqs = oTestGroup.aidTestGroupPreReqs, + bmHourlySchedule = oTestGroup.bmHourlySchedule, + cMissingGangMembers = oArgsVariation.cGangMembers, + offQueue = len(aoItems)); + aoItems.append(oItem); + + # Done? + if cNotAtEnd == 0: + self.msgDebug('returns %s items' % (len(aoItems),)); + return aoItems; + return aoItems; + diff --git a/src/VBox/ValidationKit/testmanager/core/systemchangelog.py b/src/VBox/ValidationKit/testmanager/core/systemchangelog.py new file mode 100755 index 00000000..7c85c76a --- /dev/null +++ b/src/VBox/ValidationKit/testmanager/core/systemchangelog.py @@ -0,0 +1,202 @@ +# -*- coding: utf-8 -*- +# $Id: systemchangelog.py $ + +""" +Test Manager - System changelog compilation. +""" + +__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 $" + + +# Validation Kit imports. +from testmanager.core.base import ModelLogicBase; +from testmanager.core.useraccount import UserAccountLogic; +from testmanager.core.systemlog import SystemLogData; + + +class SystemChangelogEntry(object): # pylint: disable=too-many-instance-attributes + """ + System changelog entry. + """ + + def __init__(self, tsEffective, oAuthor, sEvent, idWhat, sDesc): + self.tsEffective = tsEffective; + self.oAuthor = oAuthor; + self.sEvent = sEvent; + self.idWhat = idWhat; + self.sDesc = sDesc; + + +class SystemChangelogLogic(ModelLogicBase): + """ + System changelog compilation logic. + """ + + ## @name What kind of change. + ## @{ + ksWhat_TestBox = 'chlog::TestBox'; + ksWhat_TestCase = 'chlog::TestCase'; + ksWhat_Blacklisting = 'chlog::Blacklisting'; + ksWhat_Build = 'chlog::Build'; + ksWhat_BuildSource = 'chlog::BuildSource'; + ksWhat_FailureCategory = 'chlog::FailureCategory'; + ksWhat_FailureReason = 'chlog::FailureReason'; + ksWhat_GlobalRsrc = 'chlog::GlobalRsrc'; + ksWhat_SchedGroup = 'chlog::SchedGroup'; + ksWhat_TestGroup = 'chlog::TestGroup'; + ksWhat_User = 'chlog::User'; + ksWhat_TestResult = 'chlog::TestResult'; + ## @} + + ## Mapping a changelog entry kind to a table, key and clue. + kdWhatToTable = dict({ # pylint: disable=star-args + ksWhat_TestBox: ( 'TestBoxes', 'idTestBox', None, ), + ksWhat_TestCase: ( 'TestCasees', 'idTestCase', None, ), + ksWhat_Blacklisting: ( 'Blacklist', 'idBlacklisting', None, ), + ksWhat_Build: ( 'Builds', 'idBuild', None, ), + ksWhat_BuildSource: ( 'BuildSources', 'idBuildSrc', None, ), + ksWhat_FailureCategory: ( 'FailureCategories', 'idFailureCategory', None, ), + ksWhat_FailureReason: ( 'FailureReasons', 'idFailureReason', None, ), + ksWhat_GlobalRsrc: ( 'GlobalResources', 'idGlobalRsrc', None, ), + ksWhat_SchedGroup: ( 'SchedGroups', 'idSchedGroup', None, ), + ksWhat_TestGroup: ( 'TestGroups', 'idTestGroup', None, ), + ksWhat_User: ( 'Users', 'idUser', None, ), + ksWhat_TestResult: ( 'TestResults', 'idTestResult', None, ), + }, **{sEvent: ( 'SystemLog', 'tsCreated', 'TimestampId', ) for sEvent in SystemLogData.kasEvents}); + + ## The table key is the effective timestamp. (Can't be used above for some weird scoping reason.) + ksClue_TimestampId = 'TimestampId'; + + ## @todo move to config.py? + ksVSheriffLoginName = 'vsheriff'; + + + ## @name for kaasChangelogTables + ## @internal + ## @{ + ksTweak_None = ''; + ksTweak_NotNullAuthor = 'uidAuthorNotNull'; + ksTweak_NotNullAuthorOrVSheriff = 'uidAuthorNotNullOrVSheriff'; + ## @} + + ## @internal + kaasChangelogTables = ( + # [0]: change name, [1]: Table name, [2]: key column, [3]:later, [4]: tweak + ( ksWhat_TestBox, 'TestBoxes', 'idTestBox', None, ksTweak_NotNullAuthor, ), + ( ksWhat_TestBox, 'TestBoxesInSchedGroups', 'idTestBox', None, ksTweak_None, ), + ( ksWhat_TestCase, 'TestCases', 'idTestCase', None, ksTweak_None, ), + ( ksWhat_TestCase, 'TestCaseArgs', 'idTestCase', None, ksTweak_None, ), + ( ksWhat_TestCase, 'TestCaseDeps', 'idTestCase', None, ksTweak_None, ), + ( ksWhat_TestCase, 'TestCaseGlobalRsrcDeps', 'idTestCase', None, ksTweak_None, ), + ( ksWhat_Blacklisting, 'BuildBlacklist', 'idBlacklisting', None, ksTweak_None, ), + ( ksWhat_Build, 'Builds', 'idBuild', None, ksTweak_NotNullAuthor, ), + ( ksWhat_BuildSource, 'BuildSources', 'idBuildSrc', None, ksTweak_None, ), + ( ksWhat_FailureCategory, 'FailureCategories', 'idFailureCategory', None, ksTweak_None, ), + ( ksWhat_FailureReason, 'FailureReasons', 'idFailureReason', None, ksTweak_None, ), + ( ksWhat_GlobalRsrc, 'GlobalResources', 'idGlobalRsrc', None, ksTweak_None, ), + ( ksWhat_SchedGroup, 'SchedGroups', 'idSchedGroup', None, ksTweak_None, ), + ( ksWhat_SchedGroup, 'SchedGroupMembers', 'idSchedGroup', None, ksTweak_None, ), + ( ksWhat_TestGroup, 'TestGroups', 'idTestGroup', None, ksTweak_None, ), + ( ksWhat_TestGroup, 'TestGroupMembers', 'idTestGroup', None, ksTweak_None, ), + ( ksWhat_User, 'Users', 'uid', None, ksTweak_None, ), + ( ksWhat_TestResult, 'TestResultFailures', 'idTestResult', None, ksTweak_NotNullAuthorOrVSheriff, ), + ); + + def __init__(self, oDb): + ModelLogicBase.__init__(self, oDb); + + + def fetchForListingEx(self, iStart, cMaxRows, tsNow, cDaysBack, aiSortColumns = None): + """ + Fetches SystemLog entries. + + Returns an array (list) of SystemLogData items, empty list if none. + Raises exception on error. + """ + _ = aiSortColumns; + + # + # Construct the query. + # + oUserAccountLogic = UserAccountLogic(self._oDb); + oVSheriff = oUserAccountLogic.tryFetchAccountByLoginName(self.ksVSheriffLoginName); + uidVSheriff = oVSheriff.uid if oVSheriff is not None else -1; + + if tsNow is None: + sWhereTime = self._oDb.formatBindArgs(' WHERE tsEffective >= CURRENT_TIMESTAMP - \'%s days\'::interval\n', + (cDaysBack,)); + else: + sWhereTime = self._oDb.formatBindArgs(' WHERE tsEffective >= (%s::timestamptz - \'%s days\'::interval)\n' + ' AND tsEffective <= %s\n', + (tsNow, cDaysBack, tsNow)); + + # Special entry for the system log. + sQuery = '(\n' + sQuery += ' SELECT NULL AS uidAuthor,\n'; + sQuery += ' tsCreated AS tsEffective,\n'; + sQuery += ' sEvent AS sEvent,\n'; + sQuery += ' NULL AS idWhat,\n'; + sQuery += ' sLogText AS sDesc\n'; + sQuery += ' FROM SystemLog\n'; + sQuery += sWhereTime.replace('tsEffective', 'tsCreated'); + sQuery += ' ORDER BY tsCreated DESC\n' + sQuery += ')' + + for asEntry in self.kaasChangelogTables: + sQuery += ' UNION (\n' + sQuery += ' SELECT uidAuthor, tsEffective, \'' + asEntry[0] + '\', ' + asEntry[2] + ', \'\'\n'; + sQuery += ' FROM ' + asEntry[1] + '\n' + sQuery += sWhereTime; + if asEntry[4] == self.ksTweak_NotNullAuthor or asEntry[4] == self.ksTweak_NotNullAuthorOrVSheriff: + sQuery += ' AND uidAuthor IS NOT NULL\n'; + if asEntry[4] == self.ksTweak_NotNullAuthorOrVSheriff: + sQuery += ' AND uidAuthor <> %u\n' % (uidVSheriff,); + sQuery += ' ORDER BY tsEffective DESC\n' + sQuery += ')'; + sQuery += ' ORDER BY 2 DESC\n'; + sQuery += ' LIMIT %u OFFSET %u\n' % (cMaxRows, iStart, ); + + + # + # Execute the query and construct the return data. + # + self._oDb.execute(sQuery); + aoRows = []; + for aoRow in self._oDb.fetchAll(): + aoRows.append(SystemChangelogEntry(aoRow[1], oUserAccountLogic.cachedLookup(aoRow[0]), + aoRow[2], aoRow[3], aoRow[4])); + + + return aoRows; + diff --git a/src/VBox/ValidationKit/testmanager/core/systemlog.py b/src/VBox/ValidationKit/testmanager/core/systemlog.py new file mode 100755 index 00000000..85929c51 --- /dev/null +++ b/src/VBox/ValidationKit/testmanager/core/systemlog.py @@ -0,0 +1,186 @@ +# -*- coding: utf-8 -*- +# $Id: systemlog.py $ + +""" +Test Manager - SystemLog. +""" + +__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 unittest; + +# Validation Kit imports. +from testmanager.core.base import ModelDataBase, ModelDataBaseTestCase, ModelLogicBase, TMExceptionBase; + + +class SystemLogData(ModelDataBase): # pylint: disable=too-many-instance-attributes + """ + SystemLog Data. + """ + + ## @name Event Constants + # @{ + ksEvent_CmdNacked = 'CmdNack '; + ksEvent_TestBoxUnknown = 'TBoxUnkn'; + ksEvent_TestSetAbandoned = 'TSetAbdd'; + ksEvent_UserAccountUnknown = 'TAccUnkn'; + ksEvent_XmlResultMalformed = 'XmlRMalf'; + ksEvent_SchedQueueRecreate = 'SchQRecr'; + ## @} + + ## Valid event types. + kasEvents = \ + [ \ + ksEvent_CmdNacked, + ksEvent_TestBoxUnknown, + ksEvent_TestSetAbandoned, + ksEvent_UserAccountUnknown, + ksEvent_XmlResultMalformed, + ksEvent_SchedQueueRecreate, + ]; + + ksParam_tsCreated = 'tsCreated'; + ksParam_sEvent = 'sEvent'; + ksParam_sLogText = 'sLogText'; + + kasValidValues_sEvent = kasEvents; + + def __init__(self): + ModelDataBase.__init__(self); + + # + # Initialize with defaults. + # See the database for explanations of each of these fields. + # + self.tsCreated = None; + self.sEvent = None; + self.sLogText = None; + + def initFromDbRow(self, aoRow): + """ + Internal worker for initFromDbWithId and initFromDbWithGenId as well as + SystemLogLogic. + """ + + if aoRow is None: + raise TMExceptionBase('SystemLog row not found.'); + + self.tsCreated = aoRow[0]; + self.sEvent = aoRow[1]; + self.sLogText = aoRow[2]; + return self; + + +class SystemLogLogic(ModelLogicBase): + """ + SystemLog logic. + """ + + def __init__(self, oDb): + ModelLogicBase.__init__(self, oDb); + + def fetchForListing(self, iStart, cMaxRows, tsNow, aiSortColumns = None): + """ + Fetches SystemLog entries. + + Returns an array (list) of SystemLogData items, empty list if none. + Raises exception on error. + """ + _ = aiSortColumns; + if tsNow is None: + self._oDb.execute('SELECT *\n' + 'FROM SystemLog\n' + 'ORDER BY tsCreated DESC\n' + 'LIMIT %s OFFSET %s\n', + (cMaxRows, iStart)); + else: + self._oDb.execute('SELECT *\n' + 'FROM SystemLog\n' + 'WHERE tsCreated <= %s\n' + 'ORDER BY tsCreated DESC\n' + 'LIMIT %s OFFSET %s\n', + (tsNow, cMaxRows, iStart)); + aoRows = []; + for _ in range(self._oDb.getRowCount()): + oData = SystemLogData(); + oData.initFromDbRow(self._oDb.fetchOne()); + aoRows.append(oData); + return aoRows; + + def addEntry(self, sEvent, sLogText, cHoursRepeat = 0, fCommit = False): + """ + Adds an entry to the SystemLog table. + Raises exception on problem. + """ + if sEvent not in SystemLogData.kasEvents: + raise TMExceptionBase('Unknown event type "%s"' % (sEvent,)); + + # Check the repeat restriction first. + if cHoursRepeat > 0: + self._oDb.execute('SELECT COUNT(*) as Stuff\n' + 'FROM SystemLog\n' + 'WHERE tsCreated >= (current_timestamp - interval \'%s hours\')\n' + ' AND sEvent = %s\n' + ' AND sLogText = %s\n', + (cHoursRepeat, + sEvent, + sLogText)); + aRow = self._oDb.fetchOne(); + if aRow[0] > 0: + return None; + + # Insert it. + self._oDb.execute('INSERT INTO SystemLog (sEvent, sLogText)\n' + 'VALUES (%s, %s)\n', + (sEvent, sLogText)); + + if fCommit: + self._oDb.commit(); + return True; + +# +# Unit testing. +# + +# pylint: disable=missing-docstring +class SystemLogDataTestCase(ModelDataBaseTestCase): + def setUp(self): + self.aoSamples = [SystemLogData(),]; + +if __name__ == '__main__': + unittest.main(); + # not reached. + diff --git a/src/VBox/ValidationKit/testmanager/core/testbox.pgsql b/src/VBox/ValidationKit/testmanager/core/testbox.pgsql new file mode 100644 index 00000000..a6a085b8 --- /dev/null +++ b/src/VBox/ValidationKit/testmanager/core/testbox.pgsql @@ -0,0 +1,635 @@ +-- $Id: testbox.pgsql $ +--- @file +-- VBox Test Manager Database Stored Procedures - TestBoxes. +-- + +-- +-- 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 +-- + + +-- +-- Old type signatures. +-- +DROP FUNCTION IF EXISTS TestBoxLogic_addEntry(a_uidAuthor INTEGER, + a_ip inet, + a_uuidSystem uuid, + a_sName TEXT, + a_sDescription TEXT, + a_idSchedGroup INTEGER, + a_fEnabled BOOLEAN, + a_enmLomKind LomKind_T, + a_ipLom inet, + a_pctScaleTimeout INTEGER, -- Actually smallint, but default typing fun. + a_sComment TEXT, + a_enmPendingCmd TestBoxCmd_T, + OUT r_idTestBox INTEGER, + OUT r_idGenTestBox INTEGER, + OUT r_tsEffective TIMESTAMP WITH TIME ZONE); +DROP FUNCTION IF EXISTS TestBoxLogic_editEntry(a_uidAuthor INTEGER, + a_idTestBox INTEGER, + a_ip inet, + a_uuidSystem uuid, + a_sName TEXT, + a_sDescription TEXT, + a_idSchedGroup INTEGER, + a_fEnabled BOOLEAN, + a_enmLomKind LomKind_T, + a_ipLom inet, + a_pctScaleTimeout INTEGER, -- Actually smallint, but default typing fun. + a_sComment TEXT, + a_enmPendingCmd TestBoxCmd_T, + OUT r_idGenTestBox INTEGER, + OUT r_tsEffective TIMESTAMP WITH TIME ZONE); +DROP FUNCTION IF EXISTS TestBoxLogic_removeEntry(INTEGER, INTEGER, BOOLEAN); +DROP FUNCTION IF EXISTS TestBoxLogic_addGroupEntry(a_uidAuthor INTEGER, + a_idTestBox INTEGER, + a_idSchedGroup INTEGER, + a_iSchedPriority INTEGER, + OUT r_tsEffective TIMESTAMP WITH TIME ZONE); +DROP FUNCTION IF EXISTS TestBoxLogic_editGroupEntry(a_uidAuthor INTEGER, + a_idTestBox INTEGER, + a_idSchedGroup INTEGER, + a_iSchedPriority INTEGER, + OUT r_tsEffective INTEGER); + + +--- +-- Checks if the test box name is unique, ignoring a_idTestCaseIgnore. +-- Raises exception if duplicates are found. +-- +-- @internal +-- +CREATE OR REPLACE FUNCTION TestBoxLogic_checkUniqueName(a_sName TEXT, a_idTestBoxIgnore INTEGER) + RETURNS VOID AS $$ + DECLARE + v_cRows INTEGER; + BEGIN + SELECT COUNT(*) INTO v_cRows + FROM TestBoxes + WHERE sName = a_sName + AND tsExpire = 'infinity'::TIMESTAMP + AND idTestBox <> a_idTestBoxIgnore; + IF v_cRows <> 0 THEN + RAISE EXCEPTION 'Duplicate test box name "%" (% times)', a_sName, v_cRows; + END IF; + END; +$$ LANGUAGE plpgsql; + + +--- +-- Checks that the given scheduling group exists. +-- Raises exception if it doesn't. +-- +-- @internal +-- +CREATE OR REPLACE FUNCTION TestBoxLogic_checkSchedGroupExists(a_idSchedGroup INTEGER) + RETURNS VOID AS $$ + DECLARE + v_cRows INTEGER; + BEGIN + SELECT COUNT(*) INTO v_cRows + FROM SchedGroups + WHERE idSchedGroup = a_idSchedGroup + AND tsExpire = 'infinity'::TIMESTAMP; + IF v_cRows <> 1 THEN + IF v_cRows = 0 THEN + RAISE EXCEPTION 'Scheduling group with ID % was not found', a_idSchedGroup; + END IF; + RAISE EXCEPTION 'Integrity error in SchedGroups: % current rows with idSchedGroup=%', v_cRows, a_idSchedGroup; + END IF; + END; +$$ LANGUAGE plpgsql; + + +--- +-- Checks that the given testbxo + scheduling group pair does not currently exists. +-- Raises exception if it does. +-- +-- @internal +-- +CREATE OR REPLACE FUNCTION TestBoxLogic_checkTestBoxNotInSchedGroup(a_idTestBox INTEGER, a_idSchedGroup INTEGER) + RETURNS VOID AS $$ + DECLARE + v_cRows INTEGER; + BEGIN + SELECT COUNT(*) INTO v_cRows + FROM TestBoxesInSchedGroups + WHERE idTestBox = a_idTestBox + AND idSchedGroup = a_idSchedGroup + AND tsExpire = 'infinity'::TIMESTAMP; + IF v_cRows <> 0 THEN + RAISE EXCEPTION 'TestBox % is already a member of scheduling group %', a_idTestBox, a_idSchedGroup; + END IF; + END; +$$ LANGUAGE plpgsql; + + +--- +-- Historize a row. +-- @internal +-- +CREATE OR REPLACE FUNCTION TestBoxLogic_historizeEntry(a_idGenTestBox INTEGER, a_tsExpire TIMESTAMP WITH TIME ZONE) + RETURNS VOID AS $$ + DECLARE + v_cUpdatedRows INTEGER; + BEGIN + UPDATE TestBoxes + SET tsExpire = a_tsExpire + WHERE idGenTestBox = a_idGenTestBox + AND tsExpire = 'infinity'::TIMESTAMP; + GET DIAGNOSTICS v_cUpdatedRows = ROW_COUNT; + IF v_cUpdatedRows <> 1 THEN + IF v_cUpdatedRows = 0 THEN + RAISE EXCEPTION 'Test box generation ID % is no longer valid', a_idGenTestBox; + END IF; + RAISE EXCEPTION 'Integrity error in TestBoxes: % current rows with idGenTestBox=%', v_cUpdatedRows, a_idGenTestBox; + END IF; + END; +$$ LANGUAGE plpgsql; + + +--- +-- Historize a in-scheduling-group row. +-- @internal +-- +CREATE OR REPLACE FUNCTION TestBoxLogic_historizeGroupEntry(a_idTestBox INTEGER, + a_idSchedGroup INTEGER, + a_tsExpire TIMESTAMP WITH TIME ZONE) + RETURNS VOID AS $$ + DECLARE + v_cUpdatedRows INTEGER; + BEGIN + UPDATE TestBoxesInSchedGroups + SET tsExpire = a_tsExpire + WHERE idTestBox = a_idTestBox + AND idSchedGroup = a_idSchedGroup + AND tsExpire = 'infinity'::TIMESTAMP; + GET DIAGNOSTICS v_cUpdatedRows = ROW_COUNT; + IF v_cUpdatedRows <> 1 THEN + IF v_cUpdatedRows = 0 THEN + RAISE EXCEPTION 'TestBox ID % / SchedGroup ID % is no longer a valid combination', a_idTestBox, a_idSchedGroup; + END IF; + RAISE EXCEPTION 'Integrity error in TestBoxesInSchedGroups: % current rows for % / %', + v_cUpdatedRows, a_idTestBox, a_idSchedGroup; + END IF; + END; +$$ LANGUAGE plpgsql; + + +--- +-- Translate string via the string table. +-- +-- @returns NULL if a_sValue is NULL, otherwise a string ID. +-- +CREATE OR REPLACE FUNCTION TestBoxLogic_lookupOrFindString(a_sValue TEXT) + RETURNS INTEGER AS $$ + DECLARE + v_idStr INTEGER; + v_cRows INTEGER; + BEGIN + IF a_sValue IS NULL THEN + RETURN NULL; + END IF; + + SELECT idStr + INTO v_idStr + FROM TestBoxStrTab + WHERE sValue = a_sValue; + GET DIAGNOSTICS v_cRows = ROW_COUNT; + IF v_cRows = 0 THEN + INSERT INTO TestBoxStrTab (sValue) + VALUES (a_sValue) + RETURNING idStr INTO v_idStr; + END IF; + RETURN v_idStr; + END; +$$ LANGUAGE plpgsql; + + +--- +-- Only adds the user settable parts of the row, i.e. not what TestBoxLogic_updateOnSignOn touches. +-- +CREATE OR REPLACE function TestBoxLogic_addEntry(a_uidAuthor INTEGER, + a_ip inet, + a_uuidSystem uuid, + a_sName TEXT, + a_sDescription TEXT, + a_fEnabled BOOLEAN, + a_enmLomKind LomKind_T, + a_ipLom inet, + a_pctScaleTimeout INTEGER, -- Actually smallint, but default typing fun. + a_sComment TEXT, + a_enmPendingCmd TestBoxCmd_T, + OUT r_idTestBox INTEGER, + OUT r_idGenTestBox INTEGER, + OUT r_tsEffective TIMESTAMP WITH TIME ZONE + ) AS $$ + DECLARE + v_idStrDescription INTEGER; + v_idStrComment INTEGER; + BEGIN + PERFORM TestBoxLogic_checkUniqueName(a_sName, -1); + + SELECT TestBoxLogic_lookupOrFindString(a_sDescription) INTO v_idStrDescription; + SELECT TestBoxLogic_lookupOrFindString(a_sComment) INTO v_idStrComment; + + INSERT INTO TestBoxes ( + tsEffective, -- 1 + uidAuthor, -- 2 + ip, -- 3 + uuidSystem, -- 4 + sName, -- 5 + idStrDescription, -- 6 + fEnabled, -- 7 + enmLomKind, -- 8 + ipLom, -- 9 + pctScaleTimeout, -- 10 + idStrComment, -- 11 + enmPendingCmd ) -- 12 + VALUES (CURRENT_TIMESTAMP, -- 1 + a_uidAuthor, -- 2 + a_ip, -- 3 + a_uuidSystem, -- 4 + a_sName, -- 5 + v_idStrDescription, -- 6 + a_fEnabled, -- 7 + a_enmLomKind, -- 8 + a_ipLom, -- 9 + a_pctScaleTimeout, -- 10 + v_idStrComment, -- 11 + a_enmPendingCmd ) -- 12 + RETURNING idTestBox, idGenTestBox, tsEffective INTO r_idTestBox, r_idGenTestBox, r_tsEffective; + END; +$$ LANGUAGE plpgsql; + + +CREATE OR REPLACE function TestBoxLogic_addGroupEntry(a_uidAuthor INTEGER, + a_idTestBox INTEGER, + a_idSchedGroup INTEGER, + a_iSchedPriority INTEGER, + OUT r_tsEffective TIMESTAMP WITH TIME ZONE + ) AS $$ + BEGIN + PERFORM TestBoxLogic_checkSchedGroupExists(a_idSchedGroup); + PERFORM TestBoxLogic_checkTestBoxNotInSchedGroup(a_idTestBox, a_idSchedGroup); + + INSERT INTO TestBoxesInSchedGroups ( + idTestBox, + idSchedGroup, + tsEffective, + tsExpire, + uidAuthor, + iSchedPriority) + VALUES (a_idTestBox, + a_idSchedGroup, + CURRENT_TIMESTAMP, + 'infinity'::TIMESTAMP, + a_uidAuthor, + a_iSchedPriority) + RETURNING tsEffective INTO r_tsEffective; + END; +$$ LANGUAGE plpgsql; + + +--- +-- Only adds the user settable parts of the row, i.e. not what TestBoxLogic_updateOnSignOn touches. +-- +CREATE OR REPLACE function TestBoxLogic_editEntry(a_uidAuthor INTEGER, + a_idTestBox INTEGER, + a_ip inet, + a_uuidSystem uuid, + a_sName TEXT, + a_sDescription TEXT, + a_fEnabled BOOLEAN, + a_enmLomKind LomKind_T, + a_ipLom inet, + a_pctScaleTimeout INTEGER, -- Actually smallint, but default typing fun. + a_sComment TEXT, + a_enmPendingCmd TestBoxCmd_T, + OUT r_idGenTestBox INTEGER, + OUT r_tsEffective TIMESTAMP WITH TIME ZONE + ) AS $$ + DECLARE + v_Row TestBoxes%ROWTYPE; + v_idStrDescription INTEGER; + v_idStrComment INTEGER; + BEGIN + PERFORM TestBoxLogic_checkUniqueName(a_sName, a_idTestBox); + + SELECT TestBoxLogic_lookupOrFindString(a_sDescription) INTO v_idStrDescription; + SELECT TestBoxLogic_lookupOrFindString(a_sComment) INTO v_idStrComment; + + -- Fetch and historize the current row - there must be one. + UPDATE TestBoxes + SET tsExpire = CURRENT_TIMESTAMP + WHERE idTestBox = a_idTestBox + AND tsExpire = 'infinity'::TIMESTAMP + RETURNING * INTO STRICT v_Row; + + -- Modify the row with the new data. + v_Row.uidAuthor := a_uidAuthor; + v_Row.ip := a_ip; + v_Row.uuidSystem := a_uuidSystem; + v_Row.sName := a_sName; + v_Row.idStrDescription := v_idStrDescription; + v_Row.fEnabled := a_fEnabled; + v_Row.enmLomKind := a_enmLomKind; + v_Row.ipLom := a_ipLom; + v_Row.pctScaleTimeout := a_pctScaleTimeout; + v_Row.idStrComment := v_idStrComment; + v_Row.enmPendingCmd := a_enmPendingCmd; + v_Row.tsEffective := v_Row.tsExpire; + r_tsEffective := v_Row.tsExpire; + v_Row.tsExpire := 'infinity'::TIMESTAMP; + + -- Get a new generation ID. + SELECT NEXTVAL('TestBoxGenIdSeq') INTO v_Row.idGenTestBox; + r_idGenTestBox := v_Row.idGenTestBox; + + -- Insert the modified row. + INSERT INTO TestBoxes VALUES (v_Row.*); + END; +$$ LANGUAGE plpgsql; + + +CREATE OR REPLACE function TestBoxLogic_editGroupEntry(a_uidAuthor INTEGER, + a_idTestBox INTEGER, + a_idSchedGroup INTEGER, + a_iSchedPriority INTEGER, + OUT r_tsEffective TIMESTAMP WITH TIME ZONE + ) AS $$ + DECLARE + v_Row TestBoxesInSchedGroups%ROWTYPE; + v_idStrDescription INTEGER; + v_idStrComment INTEGER; + BEGIN + PERFORM TestBoxLogic_checkSchedGroupExists(a_idSchedGroup); + + -- Fetch and historize the current row - there must be one. + UPDATE TestBoxesInSchedGroups + SET tsExpire = CURRENT_TIMESTAMP + WHERE idTestBox = a_idTestBox + AND idSchedGroup = a_idSchedGroup + AND tsExpire = 'infinity'::TIMESTAMP + RETURNING * INTO STRICT v_Row; + + -- Modify the row with the new data. + v_Row.uidAuthor := a_uidAuthor; + v_Row.iSchedPriority := a_iSchedPriority; + v_Row.tsEffective := v_Row.tsExpire; + r_tsEffective := v_Row.tsExpire; + v_Row.tsExpire := 'infinity'::TIMESTAMP; + + -- Insert the modified row. + INSERT INTO TestBoxesInSchedGroups VALUES (v_Row.*); + END; +$$ LANGUAGE plpgsql; + + +CREATE OR REPLACE FUNCTION TestBoxLogic_removeEntry(a_uidAuthor INTEGER, a_idTestBox INTEGER, a_fCascade BOOLEAN) + RETURNS VOID AS $$ + DECLARE + v_Row TestBoxes%ROWTYPE; + v_tsEffective TIMESTAMP WITH TIME ZONE; + v_Rec RECORD; + v_sErrors TEXT; + BEGIN + -- + -- Check preconditions. + -- + IF a_fCascade <> TRUE THEN + -- @todo implement checks which throws useful exceptions. + ELSE + RAISE EXCEPTION 'CASCADE test box deletion is not implemented'; + END IF; + + -- + -- Delete all current groups, skipping history since we're also deleting the testbox. + -- + UPDATE TestBoxesInSchedGroups + SET tsExpire = CURRENT_TIMESTAMP + WHERE idTestBox = a_idTestBox + AND tsExpire = 'infinity'::TIMESTAMP; + + -- + -- To preserve the information about who deleted the record, we try to + -- add a dummy record which expires immediately. I say try because of + -- the primary key, we must let the new record be valid for 1 us. :-( + -- + SELECT * INTO STRICT v_Row + FROM TestBoxes + WHERE idTestBox = a_idTestBox + AND tsExpire = 'infinity'::TIMESTAMP; + + v_tsEffective := CURRENT_TIMESTAMP - INTERVAL '1 microsecond'; + IF v_Row.tsEffective < v_tsEffective THEN + PERFORM TestBoxLogic_historizeEntry(v_Row.idGenTestBox, v_tsEffective); + + v_Row.tsEffective := v_tsEffective; + v_Row.tsExpire := CURRENT_TIMESTAMP; + v_Row.uidAuthor := a_uidAuthor; + SELECT NEXTVAL('TestBoxGenIdSeq') INTO v_Row.idGenTestBox; + INSERT INTO TestBoxes VALUES (v_Row.*); + ELSE + PERFORM TestBoxLogic_historizeEntry(v_Row.idGenTestBox, CURRENT_TIMESTAMP); + END IF; + + EXCEPTION + WHEN NO_DATA_FOUND THEN + RAISE EXCEPTION 'Test box with ID % does not currently exist', a_idTestBox; + WHEN TOO_MANY_ROWS THEN + RAISE EXCEPTION 'Integrity error in TestBoxes: Too many current rows for %', a_idTestBox; + END; +$$ LANGUAGE plpgsql; + + +CREATE OR REPLACE FUNCTION TestBoxLogic_removeGroupEntry(a_uidAuthor INTEGER, a_idTestBox INTEGER, a_idSchedGroup INTEGER) + RETURNS VOID AS $$ + DECLARE + v_Row TestBoxesInSchedGroups%ROWTYPE; + v_tsEffective TIMESTAMP WITH TIME ZONE; + BEGIN + -- + -- To preserve the information about who deleted the record, we try to + -- add a dummy record which expires immediately. I say try because of + -- the primary key, we must let the new record be valid for 1 us. :-( + -- + SELECT * INTO STRICT v_Row + FROM TestBoxesInSchedGroups + WHERE idTestBox = a_idTestBox + AND idSchedGroup = a_idSchedGroup + AND tsExpire = 'infinity'::TIMESTAMP; + + v_tsEffective := CURRENT_TIMESTAMP - INTERVAL '1 microsecond'; + IF v_Row.tsEffective < v_tsEffective THEN + PERFORM TestBoxLogic_historizeGroupEntry(a_idTestBox, a_idSchedGroup, v_tsEffective); + + v_Row.tsEffective := v_tsEffective; + v_Row.tsExpire := CURRENT_TIMESTAMP; + v_Row.uidAuthor := a_uidAuthor; + INSERT INTO TestBoxesInSchedGroups VALUES (v_Row.*); + ELSE + PERFORM TestBoxLogic_historizeGroupEntry(a_idTestBox, a_idSchedGroup, CURRENT_TIMESTAMP); + END IF; + + EXCEPTION + WHEN NO_DATA_FOUND THEN + RAISE EXCEPTION 'TestBox #% does is not currently a member of scheduling group #%', a_idTestBox, a_idSchedGroup; + WHEN TOO_MANY_ROWS THEN + RAISE EXCEPTION 'Integrity error in TestBoxesInSchedGroups: Too many current rows for % / %', + a_idTestBox, a_idSchedGroup; + END; +$$ LANGUAGE plpgsql; + + +--- +-- Sign on update +-- +CREATE OR REPLACE function TestBoxLogic_updateOnSignOn(a_idTestBox INTEGER, + a_ip inet, + a_sOs TEXT, + a_sOsVersion TEXT, + a_sCpuVendor TEXT, + a_sCpuArch TEXT, + a_sCpuName TEXT, + a_lCpuRevision bigint, + a_cCpus INTEGER, -- Actually smallint, but default typing fun. + a_fCpuHwVirt boolean, + a_fCpuNestedPaging boolean, + a_fCpu64BitGuest boolean, + a_fChipsetIoMmu boolean, + a_fRawMode boolean, + a_cMbMemory bigint, + a_cMbScratch bigint, + a_sReport TEXT, + a_iTestBoxScriptRev INTEGER, + a_iPythonHexVersion INTEGER, + OUT r_idGenTestBox INTEGER + ) AS $$ + DECLARE + v_Row TestBoxes%ROWTYPE; + v_idStrOs INTEGER; + v_idStrOsVersion INTEGER; + v_idStrCpuVendor INTEGER; + v_idStrCpuArch INTEGER; + v_idStrCpuName INTEGER; + v_idStrReport INTEGER; + BEGIN + SELECT TestBoxLogic_lookupOrFindString(a_sOs) INTO v_idStrOs; + SELECT TestBoxLogic_lookupOrFindString(a_sOsVersion) INTO v_idStrOsVersion; + SELECT TestBoxLogic_lookupOrFindString(a_sCpuVendor) INTO v_idStrCpuVendor; + SELECT TestBoxLogic_lookupOrFindString(a_sCpuArch) INTO v_idStrCpuArch; + SELECT TestBoxLogic_lookupOrFindString(a_sCpuName) INTO v_idStrCpuName; + SELECT TestBoxLogic_lookupOrFindString(a_sReport) INTO v_idStrReport; + + -- Fetch and historize the current row - there must be one. + UPDATE TestBoxes + SET tsExpire = CURRENT_TIMESTAMP + WHERE idTestBox = a_idTestBox + AND tsExpire = 'infinity'::TIMESTAMP + RETURNING * INTO STRICT v_Row; + + -- Modify the row with the new data. + v_Row.uidAuthor := NULL; + v_Row.ip := a_ip; + v_Row.idStrOs := v_idStrOs; + v_Row.idStrOsVersion := v_idStrOsVersion; + v_Row.idStrCpuVendor := v_idStrCpuVendor; + v_Row.idStrCpuArch := v_idStrCpuArch; + v_Row.idStrCpuName := v_idStrCpuName; + v_Row.lCpuRevision := a_lCpuRevision; + v_Row.cCpus := a_cCpus; + v_Row.fCpuHwVirt := a_fCpuHwVirt; + v_Row.fCpuNestedPaging := a_fCpuNestedPaging; + v_Row.fCpu64BitGuest := a_fCpu64BitGuest; + v_Row.fChipsetIoMmu := a_fChipsetIoMmu; + v_Row.fRawMode := a_fRawMode; + v_Row.cMbMemory := a_cMbMemory; + v_Row.cMbScratch := a_cMbScratch; + v_Row.idStrReport := v_idStrReport; + v_Row.iTestBoxScriptRev := a_iTestBoxScriptRev; + v_Row.iPythonHexVersion := a_iPythonHexVersion; + v_Row.tsEffective := v_Row.tsExpire; + v_Row.tsExpire := 'infinity'::TIMESTAMP; + + -- Get a new generation ID. + SELECT NEXTVAL('TestBoxGenIdSeq') INTO v_Row.idGenTestBox; + r_idGenTestBox := v_Row.idGenTestBox; + + -- Insert the modified row. + INSERT INTO TestBoxes VALUES (v_Row.*); + END; +$$ LANGUAGE plpgsql; + + +--- +-- Set new command. +-- +CREATE OR REPLACE function TestBoxLogic_setCommand(a_uidAuthor INTEGER, + a_idTestBox INTEGER, + a_enmOldCmd TestBoxCmd_T, + a_enmNewCmd TestBoxCmd_T, + a_sComment TEXT, + OUT r_idGenTestBox INTEGER, + OUT r_tsEffective TIMESTAMP WITH TIME ZONE + ) AS $$ + DECLARE + v_Row TestBoxes%ROWTYPE; + v_idStrComment INTEGER; + BEGIN + SELECT TestBoxLogic_lookupOrFindString(a_sComment) INTO v_idStrComment; + + -- Fetch and historize the current row - there must be one. + UPDATE TestBoxes + SET tsExpire = CURRENT_TIMESTAMP + WHERE idTestBox = a_idTestBox + AND tsExpire = 'infinity'::TIMESTAMP + AND enmPendingCmd = a_enmOldCmd + RETURNING * INTO STRICT v_Row; + + -- Modify the row with the new data. + v_Row.enmPendingCmd := a_enmNewCmd; + IF v_idStrComment IS NOT NULL THEN + v_Row.idStrComment := v_idStrComment; + END IF; + v_Row.tsEffective := v_Row.tsExpire; + r_tsEffective := v_Row.tsExpire; + v_Row.tsExpire := 'infinity'::TIMESTAMP; + + -- Get a new generation ID. + SELECT NEXTVAL('TestBoxGenIdSeq') INTO v_Row.idGenTestBox; + r_idGenTestBox := v_Row.idGenTestBox; + + -- Insert the modified row. + INSERT INTO TestBoxes VALUES (v_Row.*); + END; +$$ LANGUAGE plpgsql; + diff --git a/src/VBox/ValidationKit/testmanager/core/testbox.py b/src/VBox/ValidationKit/testmanager/core/testbox.py new file mode 100755 index 00000000..6686ca3b --- /dev/null +++ b/src/VBox/ValidationKit/testmanager/core/testbox.py @@ -0,0 +1,1286 @@ +# -*- coding: utf-8 -*- +# $Id: testbox.py $ + +""" +Test Manager - TestBox. +""" + +__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 copy; +import sys; +import unittest; + +# Validation Kit imports. +from testmanager.core import db; +from testmanager.core.base import ModelDataBase, ModelDataBaseTestCase, ModelLogicBase, TMInFligthCollision, \ + TMInvalidData, TMTooManyRows, TMRowNotFound, \ + ChangeLogEntry, AttributeChangeEntry, AttributeChangeEntryPre; +from testmanager.core.useraccount import UserAccountLogic; + +# Python 3 hacks: +if sys.version_info[0] >= 3: + xrange = range; # pylint: disable=redefined-builtin,invalid-name + + +class TestBoxInSchedGroupData(ModelDataBase): + """ + TestBox in SchedGroup data. + """ + + ksParam_idTestBox = 'TestBoxInSchedGroup_idTestBox'; + ksParam_idSchedGroup = 'TestBoxInSchedGroup_idSchedGroup'; + ksParam_tsEffective = 'TestBoxInSchedGroup_tsEffective'; + ksParam_tsExpire = 'TestBoxInSchedGroup_tsExpire'; + ksParam_uidAuthor = 'TestBoxInSchedGroup_uidAuthor'; + ksParam_iSchedPriority = 'TestBoxInSchedGroup_iSchedPriority'; + + kasAllowNullAttributes = [ 'tsEffective', 'tsExpire', 'uidAuthor', ] + + kiMin_iSchedPriority = 0; + kiMax_iSchedPriority = 32; + + kcDbColumns = 6; + + def __init__(self): + ModelDataBase.__init__(self); + self.idTestBox = None; + self.idSchedGroup = None; + self.tsEffective = None; + self.tsExpire = None; + self.uidAuthor = None; + self.iSchedPriority = 16; + + def initFromDbRow(self, aoRow): + """ + Expecting the result from a query like this: + SELECT * FROM TestBoxesInSchedGroups + """ + if aoRow is None: + raise TMRowNotFound('TestBox/SchedGroup not found.'); + + self.idTestBox = aoRow[0]; + self.idSchedGroup = aoRow[1]; + self.tsEffective = aoRow[2]; + self.tsExpire = aoRow[3]; + self.uidAuthor = aoRow[4]; + self.iSchedPriority = aoRow[5]; + + return self; + +class TestBoxInSchedGroupDataEx(TestBoxInSchedGroupData): + """ + Extended version of TestBoxInSchedGroupData that contains the scheduling group. + """ + + def __init__(self): + TestBoxInSchedGroupData.__init__(self); + self.oSchedGroup = None # type: SchedGroupData + + def initFromDbRowEx(self, aoRow, oDb, tsNow = None, sPeriodBack = None): + """ + Extended version of initFromDbRow that fills in the rest from the database. + """ + from testmanager.core.schedgroup import SchedGroupData; + self.initFromDbRow(aoRow); + self.oSchedGroup = SchedGroupData().initFromDbWithId(oDb, self.idSchedGroup, tsNow, sPeriodBack); + return self; + +class TestBoxDataForSchedGroup(TestBoxInSchedGroupData): + """ + Extended version of TestBoxInSchedGroupData that adds the testbox data (if available). + Used by TestBoxLogic.fetchForSchedGroup + """ + + def __init__(self): + TestBoxInSchedGroupData.__init__(self); + self.oTestBox = None # type: TestBoxData + + def initFromDbRow(self, aoRow): + """ + The row is: TestBoxesInSchedGroups.*, TestBoxesWithStrings.* + """ + TestBoxInSchedGroupData.initFromDbRow(self, aoRow); + if aoRow[self.kcDbColumns]: + self.oTestBox = TestBoxData().initFromDbRow(aoRow[self.kcDbColumns:]); + else: + self.oTestBox = None; + return self; + + def getDataAttributes(self): + asAttributes = TestBoxInSchedGroupData.getDataAttributes(self); + asAttributes.remove('oTestBox'); + return asAttributes; + + def _validateAndConvertWorker(self, asAllowNullAttributes, oDb, enmValidateFor = ModelDataBase.ksValidateFor_Other): + dErrors = TestBoxInSchedGroupData._validateAndConvertWorker(self, asAllowNullAttributes, oDb, enmValidateFor); + if self.ksParam_idTestBox not in dErrors: + self.oTestBox = TestBoxData(); + try: + self.oTestBox.initFromDbWithId(oDb, self.idTestBox); + except Exception as oXcpt: + self.oTestBox = TestBoxData() + dErrors[self.ksParam_idTestBox] = str(oXcpt); + return dErrors; + + +# pylint: disable=invalid-name +class TestBoxData(ModelDataBase): # pylint: disable=too-many-instance-attributes + """ + TestBox Data. + """ + + ## LomKind_T + ksLomKind_None = 'none'; + ksLomKind_ILOM = 'ilom'; + ksLomKind_ELOM = 'elom'; + ksLomKind_AppleXserveLom = 'apple-xserver-lom'; + kasLomKindValues = [ ksLomKind_None, ksLomKind_ILOM, ksLomKind_ELOM, ksLomKind_AppleXserveLom]; + kaoLomKindDescs = \ + [ + ( ksLomKind_None, 'None', ''), + ( ksLomKind_ILOM, 'ILOM', ''), + ( ksLomKind_ELOM, 'ELOM', ''), + ( ksLomKind_AppleXserveLom, 'Apple Xserve LOM', ''), + ]; + + + ## TestBoxCmd_T + ksTestBoxCmd_None = 'none'; + ksTestBoxCmd_Abort = 'abort'; + ksTestBoxCmd_Reboot = 'reboot'; + ksTestBoxCmd_Upgrade = 'upgrade'; + ksTestBoxCmd_UpgradeAndReboot = 'upgrade-and-reboot'; + ksTestBoxCmd_Special = 'special'; + kasTestBoxCmdValues = [ ksTestBoxCmd_None, ksTestBoxCmd_Abort, ksTestBoxCmd_Reboot, ksTestBoxCmd_Upgrade, + ksTestBoxCmd_UpgradeAndReboot, ksTestBoxCmd_Special]; + kaoTestBoxCmdDescs = \ + [ + ( ksTestBoxCmd_None, 'None', ''), + ( ksTestBoxCmd_Abort, 'Abort current test', ''), + ( ksTestBoxCmd_Reboot, 'Reboot TestBox', ''), + ( ksTestBoxCmd_Upgrade, 'Upgrade TestBox Script', ''), + ( ksTestBoxCmd_UpgradeAndReboot, 'Upgrade TestBox Script and reboot', ''), + ( ksTestBoxCmd_Special, 'Special (reserved)', ''), + ]; + + + ksIdAttr = 'idTestBox'; + ksIdGenAttr = 'idGenTestBox'; + + ksParam_idTestBox = 'TestBox_idTestBox'; + ksParam_tsEffective = 'TestBox_tsEffective'; + ksParam_tsExpire = 'TestBox_tsExpire'; + ksParam_uidAuthor = 'TestBox_uidAuthor'; + ksParam_idGenTestBox = 'TestBox_idGenTestBox'; + ksParam_ip = 'TestBox_ip'; + ksParam_uuidSystem = 'TestBox_uuidSystem'; + ksParam_sName = 'TestBox_sName'; + ksParam_sDescription = 'TestBox_sDescription'; + ksParam_fEnabled = 'TestBox_fEnabled'; + ksParam_enmLomKind = 'TestBox_enmLomKind'; + ksParam_ipLom = 'TestBox_ipLom'; + ksParam_pctScaleTimeout = 'TestBox_pctScaleTimeout'; + ksParam_sComment = 'TestBox_sComment'; + ksParam_sOs = 'TestBox_sOs'; + ksParam_sOsVersion = 'TestBox_sOsVersion'; + ksParam_sCpuVendor = 'TestBox_sCpuVendor'; + ksParam_sCpuArch = 'TestBox_sCpuArch'; + ksParam_sCpuName = 'TestBox_sCpuName'; + ksParam_lCpuRevision = 'TestBox_lCpuRevision'; + ksParam_cCpus = 'TestBox_cCpus'; + ksParam_fCpuHwVirt = 'TestBox_fCpuHwVirt'; + ksParam_fCpuNestedPaging = 'TestBox_fCpuNestedPaging'; + ksParam_fCpu64BitGuest = 'TestBox_fCpu64BitGuest'; + ksParam_fChipsetIoMmu = 'TestBox_fChipsetIoMmu'; + ksParam_fRawMode = 'TestBox_fRawMode'; + ksParam_cMbMemory = 'TestBox_cMbMemory'; + ksParam_cMbScratch = 'TestBox_cMbScratch'; + ksParam_sReport = 'TestBox_sReport'; + ksParam_iTestBoxScriptRev = 'TestBox_iTestBoxScriptRev'; + ksParam_iPythonHexVersion = 'TestBox_iPythonHexVersion'; + ksParam_enmPendingCmd = 'TestBox_enmPendingCmd'; + + kasInternalAttributes = [ 'idStrDescription', 'idStrComment', 'idStrOs', 'idStrOsVersion', 'idStrCpuVendor', + 'idStrCpuArch', 'idStrCpuName', 'idStrReport', ]; + kasMachineSettableOnly = [ 'sOs', 'sOsVersion', 'sCpuVendor', 'sCpuArch', 'sCpuName', 'lCpuRevision', 'cCpus', + 'fCpuHwVirt', 'fCpuNestedPaging', 'fCpu64BitGuest', 'fChipsetIoMmu', 'fRawMode', + 'cMbMemory', 'cMbScratch', 'sReport', 'iTestBoxScriptRev', 'iPythonHexVersion', ]; + kasAllowNullAttributes = ['idTestBox', 'tsEffective', 'tsExpire', 'uidAuthor', 'idGenTestBox', 'sDescription', + 'ipLom', 'sComment', ] + kasMachineSettableOnly + kasInternalAttributes; + + kasValidValues_enmLomKind = kasLomKindValues; + kasValidValues_enmPendingCmd = kasTestBoxCmdValues; + kiMin_pctScaleTimeout = 11; + kiMax_pctScaleTimeout = 19999; + kcchMax_sReport = 65535; + + kcDbColumns = 40; # including the 7 string joins columns + + + def __init__(self): + ModelDataBase.__init__(self); + + # + # Initialize with defaults. + # See the database for explanations of each of these fields. + # + self.idTestBox = None; + self.tsEffective = None; + self.tsExpire = None; + self.uidAuthor = None; + self.idGenTestBox = None; + self.ip = None; + self.uuidSystem = None; + self.sName = None; + self.idStrDescription = None; + self.fEnabled = False; + self.enmLomKind = self.ksLomKind_None; + self.ipLom = None; + self.pctScaleTimeout = 100; + self.idStrComment = None; + self.idStrOs = None; + self.idStrOsVersion = None; + self.idStrCpuVendor = None; + self.idStrCpuArch = None; + self.idStrCpuName = None; + self.lCpuRevision = None; + self.cCpus = 1; + self.fCpuHwVirt = False; + self.fCpuNestedPaging = False; + self.fCpu64BitGuest = False; + self.fChipsetIoMmu = False; + self.fRawMode = None; + self.cMbMemory = 1; + self.cMbScratch = 0; + self.idStrReport = None; + self.iTestBoxScriptRev = 0; + self.iPythonHexVersion = 0; + self.enmPendingCmd = self.ksTestBoxCmd_None; + # String table values. + self.sDescription = None; + self.sComment = None; + self.sOs = None; + self.sOsVersion = None; + self.sCpuVendor = None; + self.sCpuArch = None; + self.sCpuName = None; + self.sReport = None; + + def initFromDbRow(self, aoRow): + """ + Internal worker for initFromDbWithId and initFromDbWithGenId as well as + from TestBoxLogic. Expecting the result from a query like this: + SELECT TestBoxesWithStrings.* FROM TestBoxesWithStrings + """ + if aoRow is None: + raise TMRowNotFound('TestBox not found.'); + + self.idTestBox = aoRow[0]; + self.tsEffective = aoRow[1]; + self.tsExpire = aoRow[2]; + self.uidAuthor = aoRow[3]; + self.idGenTestBox = aoRow[4]; + self.ip = aoRow[5]; + self.uuidSystem = aoRow[6]; + self.sName = aoRow[7]; + self.idStrDescription = aoRow[8]; + self.fEnabled = aoRow[9]; + self.enmLomKind = aoRow[10]; + self.ipLom = aoRow[11]; + self.pctScaleTimeout = aoRow[12]; + self.idStrComment = aoRow[13]; + self.idStrOs = aoRow[14]; + self.idStrOsVersion = aoRow[15]; + self.idStrCpuVendor = aoRow[16]; + self.idStrCpuArch = aoRow[17]; + self.idStrCpuName = aoRow[18]; + self.lCpuRevision = aoRow[19]; + self.cCpus = aoRow[20]; + self.fCpuHwVirt = aoRow[21]; + self.fCpuNestedPaging = aoRow[22]; + self.fCpu64BitGuest = aoRow[23]; + self.fChipsetIoMmu = aoRow[24]; + self.fRawMode = aoRow[25]; + self.cMbMemory = aoRow[26]; + self.cMbScratch = aoRow[27]; + self.idStrReport = aoRow[28]; + self.iTestBoxScriptRev = aoRow[29]; + self.iPythonHexVersion = aoRow[30]; + self.enmPendingCmd = aoRow[31]; + + # String table values. + if len(aoRow) > 32: + self.sDescription = aoRow[32]; + self.sComment = aoRow[33]; + self.sOs = aoRow[34]; + self.sOsVersion = aoRow[35]; + self.sCpuVendor = aoRow[36]; + self.sCpuArch = aoRow[37]; + self.sCpuName = aoRow[38]; + self.sReport = aoRow[39]; + + return self; + + def initFromDbWithId(self, oDb, idTestBox, tsNow = None, sPeriodBack = None): + """ + Initialize the object from the database. + """ + oDb.execute(self.formatSimpleNowAndPeriodQuery(oDb, + 'SELECT TestBoxesWithStrings.*\n' + 'FROM TestBoxesWithStrings\n' + 'WHERE idTestBox = %s\n' + , ( idTestBox, ), tsNow, sPeriodBack)); + aoRow = oDb.fetchOne() + if aoRow is None: + raise TMRowNotFound('idTestBox=%s not found (tsNow=%s sPeriodBack=%s)' % (idTestBox, tsNow, sPeriodBack,)); + return self.initFromDbRow(aoRow); + + def initFromDbWithGenId(self, oDb, idGenTestBox, tsNow = None): + """ + Initialize the object from the database. + """ + _ = tsNow; # Only useful for extended data classes. + oDb.execute('SELECT TestBoxesWithStrings.*\n' + 'FROM TestBoxesWithStrings\n' + 'WHERE idGenTestBox = %s\n' + , (idGenTestBox, ) ); + return self.initFromDbRow(oDb.fetchOne()); + + def _validateAndConvertWorker(self, asAllowNullAttributes, oDb, enmValidateFor = ModelDataBase.ksValidateFor_Other): + # Override to do extra ipLom checks. + dErrors = ModelDataBase._validateAndConvertWorker(self, asAllowNullAttributes, oDb, enmValidateFor); + if self.ksParam_ipLom not in dErrors \ + and self.ksParam_enmLomKind not in dErrors \ + and self.enmLomKind != self.ksLomKind_None \ + and self.ipLom is None: + dErrors[self.ksParam_ipLom] = 'Light-out-management IP is mandatory and a LOM is selected.' + return dErrors; + + @staticmethod + def formatPythonVersionEx(iPythonHexVersion): + """ Unbuttons the version number and formats it as a version string. """ + if iPythonHexVersion is None: + return 'N/A'; + return 'v%d.%d.%d.%d' \ + % ( iPythonHexVersion >> 24, + (iPythonHexVersion >> 16) & 0xff, + (iPythonHexVersion >> 8) & 0xff, + iPythonHexVersion & 0xff); + + def formatPythonVersion(self): + """ Unbuttons the version number and formats it as a version string. """ + return self.formatPythonVersionEx(self.iPythonHexVersion); + + + @staticmethod + def getCpuFamilyEx(lCpuRevision): + """ Returns the CPU family for a x86 or amd64 testboxes.""" + if lCpuRevision is None: + return 0; + return (lCpuRevision >> 24 & 0xff); + + def getCpuFamily(self): + """ Returns the CPU family for a x86 or amd64 testboxes.""" + return self.getCpuFamilyEx(self.lCpuRevision); + + @staticmethod + def getCpuModelEx(lCpuRevision): + """ Returns the CPU model for a x86 or amd64 testboxes.""" + if lCpuRevision is None: + return 0; + return (lCpuRevision >> 8 & 0xffff); + + def getCpuModel(self): + """ Returns the CPU model for a x86 or amd64 testboxes.""" + return self.getCpuModelEx(self.lCpuRevision); + + @staticmethod + def getCpuSteppingEx(lCpuRevision): + """ Returns the CPU stepping for a x86 or amd64 testboxes.""" + if lCpuRevision is None: + return 0; + return (lCpuRevision & 0xff); + + def getCpuStepping(self): + """ Returns the CPU stepping for a x86 or amd64 testboxes.""" + return self.getCpuSteppingEx(self.lCpuRevision); + + + # The following is a translation of the g_aenmIntelFamily06 array in CPUMR3CpuId.cpp: + kdIntelFamily06 = { + 0x00: 'P6', + 0x01: 'P6', + 0x03: 'P6_II', + 0x05: 'P6_II', + 0x06: 'P6_II', + 0x07: 'P6_III', + 0x08: 'P6_III', + 0x09: 'P6_M_Banias', + 0x0a: 'P6_III', + 0x0b: 'P6_III', + 0x0d: 'P6_M_Dothan', + 0x0e: 'Core_Yonah', + 0x0f: 'Core2_Merom', + 0x15: 'P6_M_Dothan', + 0x16: 'Core2_Merom', + 0x17: 'Core2_Penryn', + 0x1a: 'Core7_Nehalem', + 0x1c: 'Atom_Bonnell', + 0x1d: 'Core2_Penryn', + 0x1e: 'Core7_Nehalem', + 0x1f: 'Core7_Nehalem', + 0x25: 'Core7_Westmere', + 0x26: 'Atom_Lincroft', + 0x27: 'Atom_Saltwell', + 0x2a: 'Core7_SandyBridge', + 0x2c: 'Core7_Westmere', + 0x2d: 'Core7_SandyBridge', + 0x2e: 'Core7_Nehalem', + 0x2f: 'Core7_Westmere', + 0x35: 'Atom_Saltwell', + 0x36: 'Atom_Saltwell', + 0x37: 'Atom_Silvermont', + 0x3a: 'Core7_IvyBridge', + 0x3c: 'Core7_Haswell', + 0x3d: 'Core7_Broadwell', + 0x3e: 'Core7_IvyBridge', + 0x3f: 'Core7_Haswell', + 0x45: 'Core7_Haswell', + 0x46: 'Core7_Haswell', + 0x47: 'Core7_Broadwell', + 0x4a: 'Atom_Silvermont', + 0x4c: 'Atom_Airmount', + 0x4d: 'Atom_Silvermont', + 0x4e: 'Core7_Skylake', + 0x4f: 'Core7_Broadwell', + 0x55: 'Core7_Skylake', + 0x56: 'Core7_Broadwell', + 0x5a: 'Atom_Silvermont', + 0x5c: 'Atom_Goldmont', + 0x5d: 'Atom_Silvermont', + 0x5e: 'Core7_Skylake', + 0x66: 'Core7_Cannonlake', + }; + # Also from CPUMR3CpuId.cpp, but the switch. + kdIntelFamily15 = { + 0x00: 'NB_Willamette', + 0x01: 'NB_Willamette', + 0x02: 'NB_Northwood', + 0x03: 'NB_Prescott', + 0x04: 'NB_Prescott2M', + 0x05: 'NB_Unknown', + 0x06: 'NB_CedarMill', + 0x07: 'NB_Gallatin', + }; + + @staticmethod + def queryCpuMicroarchEx(lCpuRevision, sCpuVendor): + """ Try guess the microarch name for the cpu. Returns None if we cannot. """ + if lCpuRevision is None or sCpuVendor is None: + return None; + uFam = TestBoxData.getCpuFamilyEx(lCpuRevision); + uMod = TestBoxData.getCpuModelEx(lCpuRevision); + if sCpuVendor == 'GenuineIntel': + if uFam == 6: + return TestBoxData.kdIntelFamily06.get(uMod, None); + if uFam == 15: + return TestBoxData.kdIntelFamily15.get(uMod, None); + elif sCpuVendor == 'AuthenticAMD': + if uFam == 0xf: + if uMod < 0x10: return 'K8_130nm'; + if 0x60 <= uMod < 0x80: return 'K8_65nm'; + if uMod >= 0x40: return 'K8_90nm_AMDV'; + if uMod in [0x21, 0x23, 0x2b, 0x37, 0x3f]: return 'K8_90nm_DualCore'; + return 'AMD_K8_90nm'; + if uFam == 0x10: return 'K10'; + if uFam == 0x11: return 'K10_Lion'; + if uFam == 0x12: return 'K10_Llano'; + if uFam == 0x14: return 'Bobcat'; + if uFam == 0x15: + if uMod <= 0x01: return 'Bulldozer'; + if uMod in [0x02, 0x10, 0x13]: return 'Piledriver'; + return None; + if uFam == 0x16: + return 'Jaguar'; + elif sCpuVendor == 'CentaurHauls': + if uFam == 0x05: + if uMod == 0x01: return 'Centaur_C6'; + if uMod == 0x04: return 'Centaur_C6'; + if uMod == 0x08: return 'Centaur_C2'; + if uMod == 0x09: return 'Centaur_C3'; + if uFam == 0x06: + if uMod == 0x05: return 'VIA_C3_M2'; + if uMod == 0x06: return 'VIA_C3_C5A'; + if uMod == 0x07: return 'VIA_C3_C5B' if TestBoxData.getCpuSteppingEx(lCpuRevision) < 8 else 'VIA_C3_C5C'; + if uMod == 0x08: return 'VIA_C3_C5N'; + if uMod == 0x09: return 'VIA_C3_C5XL' if TestBoxData.getCpuSteppingEx(lCpuRevision) < 8 else 'VIA_C3_C5P'; + if uMod == 0x0a: return 'VIA_C7_C5J'; + if uMod == 0x0f: return 'VIA_Isaiah'; + elif sCpuVendor == ' Shanghai ': + if uFam == 0x07: + if uMod == 0x0b: return 'Shanghai_KX-5000'; + return None; + + def queryCpuMicroarch(self): + """ Try guess the microarch name for the cpu. Returns None if we cannot. """ + return self.queryCpuMicroarchEx(self.lCpuRevision, self.sCpuVendor); + + @staticmethod + def getPrettyCpuVersionEx(lCpuRevision, sCpuVendor): + """ Pretty formatting of the family/model/stepping with microarch optimizations. """ + if lCpuRevision is None or sCpuVendor is None: + return u'<none>'; + sMarch = TestBoxData.queryCpuMicroarchEx(lCpuRevision, sCpuVendor); + if sMarch is not None: + return '%s %02x:%x' \ + % (sMarch, TestBoxData.getCpuModelEx(lCpuRevision), TestBoxData.getCpuSteppingEx(lCpuRevision)); + return 'fam%02X m%02X s%02X' \ + % ( TestBoxData.getCpuFamilyEx(lCpuRevision), TestBoxData.getCpuModelEx(lCpuRevision), + TestBoxData.getCpuSteppingEx(lCpuRevision)); + + def getPrettyCpuVersion(self): + """ Pretty formatting of the family/model/stepping with microarch optimizations. """ + return self.getPrettyCpuVersionEx(self.lCpuRevision, self.sCpuVendor); + + def getArchBitString(self): + """ Returns 32-bit, 64-bit, <none>, or sCpuArch. """ + if self.sCpuArch is None: + return '<none>'; + if self.sCpuArch in [ 'x86',]: + return '32-bit'; + if self.sCpuArch in [ 'amd64',]: + return '64-bit'; + return self.sCpuArch; + + def getPrettyCpuVendor(self): + """ Pretty vendor name.""" + if self.sCpuVendor is None: + return '<none>'; + if self.sCpuVendor == 'GenuineIntel': return 'Intel'; + if self.sCpuVendor == 'AuthenticAMD': return 'AMD'; + if self.sCpuVendor == 'CentaurHauls': return 'VIA'; + if self.sCpuVendor == ' Shanghai ': return 'Shanghai'; + return self.sCpuVendor; + + +class TestBoxDataEx(TestBoxData): + """ + TestBox data. + """ + + ksParam_aoInSchedGroups = 'TestBox_aoInSchedGroups'; + + # Use [] instead of None. + kasAltArrayNull = [ 'aoInSchedGroups', ]; + + ## Helper parameter containing the comma separated list with the IDs of + # potential members found in the parameters. + ksParam_aidSchedGroups = 'TestBoxDataEx_aidSchedGroups'; + + def __init__(self): + TestBoxData.__init__(self); + self.aoInSchedGroups = [] # type: list[TestBoxInSchedGroupData] + + def _initExtraMembersFromDb(self, oDb, tsNow = None, sPeriodBack = None): + """ + Worker shared by the initFromDb* methods. + Returns self. Raises exception if no row or database error. + """ + oDb.execute(self.formatSimpleNowAndPeriodQuery(oDb, + 'SELECT *\n' + 'FROM TestBoxesInSchedGroups\n' + 'WHERE idTestBox = %s\n' + , (self.idTestBox,), tsNow, sPeriodBack) + + 'ORDER BY idSchedGroup\n' ); + self.aoInSchedGroups = []; + for aoRow in oDb.fetchAll(): + self.aoInSchedGroups.append(TestBoxInSchedGroupDataEx().initFromDbRowEx(aoRow, oDb, tsNow, sPeriodBack)); + return self; + + def initFromDbRowEx(self, aoRow, oDb, tsNow = None): + """ + Reinitialize from a SELECT * FROM TestBoxesWithStrings row. Will query the + necessary additional data from oDb using tsNow. + Returns self. Raises exception if no row or database error. + """ + TestBoxData.initFromDbRow(self, aoRow); + return self._initExtraMembersFromDb(oDb, tsNow); + + def initFromDbWithId(self, oDb, idTestBox, tsNow = None, sPeriodBack = None): + """ + Initialize the object from the database. + """ + TestBoxData.initFromDbWithId(self, oDb, idTestBox, tsNow, sPeriodBack); + return self._initExtraMembersFromDb(oDb, tsNow, sPeriodBack); + + def initFromDbWithGenId(self, oDb, idGenTestBox, tsNow = None): + """ + Initialize the object from the database. + """ + TestBoxData.initFromDbWithGenId(self, oDb, idGenTestBox); + if tsNow is None and not oDb.isTsInfinity(self.tsExpire): + tsNow = self.tsEffective; + return self._initExtraMembersFromDb(oDb, tsNow); + + def getAttributeParamNullValues(self, sAttr): # Necessary? + if sAttr in ['aoInSchedGroups', ]: + return [[], '']; + return TestBoxData.getAttributeParamNullValues(self, sAttr); + + def convertParamToAttribute(self, sAttr, sParam, oValue, oDisp, fStrict): + """ + For dealing with the in-scheduling-group list. + """ + if sAttr != 'aoInSchedGroups': + return TestBoxData.convertParamToAttribute(self, sAttr, sParam, oValue, oDisp, fStrict); + + aoNewValues = []; + aidSelected = oDisp.getListOfIntParams(sParam, iMin = 1, iMax = 0x7ffffffe, aiDefaults = []); + asIds = oDisp.getStringParam(self.ksParam_aidSchedGroups, sDefault = '').split(','); + for idSchedGroup in asIds: + try: idSchedGroup = int(idSchedGroup); + except: pass; + oDispWrapper = self.DispWrapper(oDisp, '%s[%s][%%s]' % (TestBoxDataEx.ksParam_aoInSchedGroups, idSchedGroup,)) + oMember = TestBoxInSchedGroupData().initFromParams(oDispWrapper, fStrict = False); + if idSchedGroup in aidSelected: + aoNewValues.append(oMember); + return aoNewValues; + + def _validateAndConvertAttribute(self, sAttr, sParam, oValue, aoNilValues, fAllowNull, oDb): # pylint: disable=too-many-locals + """ + Validate special arrays and requirement expressions. + + Some special needs for the in-scheduling-group list. + """ + if sAttr != 'aoInSchedGroups': + return TestBoxData._validateAndConvertAttribute(self, sAttr, sParam, oValue, aoNilValues, fAllowNull, oDb); + + asErrors = []; + aoNewValues = []; + + # Note! We'll be returning an error dictionary instead of an string here. + dErrors = {}; + + # HACK ALERT! idTestBox might not have been validated and converted yet, but we need detect + # adding so we can ignore idTestBox being NIL when validating group memberships. + ## @todo make base.py pass us the ksValidateFor_Xxxx value. + fIsAdding = bool(self.idTestBox in [ None, -1, '-1', 'None', '' ]) + + for iInGrp, oInSchedGroup in enumerate(self.aoInSchedGroups): + oInSchedGroup = copy.copy(oInSchedGroup); + oInSchedGroup.idTestBox = self.idTestBox; + if fIsAdding: + dCurErrors = oInSchedGroup.validateAndConvertEx(['idTestBox',] + oInSchedGroup.kasAllowNullAttributes, + oDb, ModelDataBase.ksValidateFor_Add); + else: + dCurErrors = oInSchedGroup.validateAndConvert(oDb, ModelDataBase.ksValidateFor_Other); + if not dCurErrors: + pass; ## @todo figure out the ID? + else: + asErrors = []; + for sKey in dCurErrors: + asErrors.append('%s: %s' % (sKey[len('TestBoxInSchedGroup_'):], + dCurErrors[sKey] + ('{%s}' % self.idTestBox))) + dErrors[iInGrp] = '<br>\n'.join(asErrors) + aoNewValues.append(oInSchedGroup); + + for iInGrp, oInSchedGroup in enumerate(self.aoInSchedGroups): + for iInGrp2 in xrange(iInGrp + 1, len(self.aoInSchedGroups)): + if self.aoInSchedGroups[iInGrp2].idSchedGroup == oInSchedGroup.idSchedGroup: + sMsg = 'Duplicate scheduling group #%s".' % (oInSchedGroup.idSchedGroup,); + if iInGrp in dErrors: dErrors[iInGrp] += '<br>\n' + sMsg; + else: dErrors[iInGrp] = sMsg; + if iInGrp2 in dErrors: dErrors[iInGrp2] += '<br>\n' + sMsg; + else: dErrors[iInGrp2] = sMsg; + break; + + return (aoNewValues, dErrors if dErrors else None); + + +class TestBoxLogic(ModelLogicBase): + """ + TestBox logic. + """ + + kiSortColumn_sName = 1; + kiSortColumn_sOs = 2; + kiSortColumn_sOsVersion = 3; + kiSortColumn_sCpuVendor = 4; + kiSortColumn_sCpuArch = 5; + kiSortColumn_lCpuRevision = 6; + kiSortColumn_cCpus = 7; + kiSortColumn_cMbMemory = 8; + kiSortColumn_cMbScratch = 9; + kiSortColumn_fCpuNestedPaging = 10; + kiSortColumn_iTestBoxScriptRev = 11; + kiSortColumn_iPythonHexVersion = 12; + kiSortColumn_enmPendingCmd = 13; + kiSortColumn_fEnabled = 14; + kiSortColumn_enmState = 15; + kiSortColumn_tsUpdated = 16; + kcMaxSortColumns = 17; + kdSortColumnMap = { + 0: 'TestBoxesWithStrings.sName', + kiSortColumn_sName: "regexp_replace(TestBoxesWithStrings.sName,'[0-9]*', '', 'g'), " \ + "RIGHT(CONCAT(regexp_replace(TestBoxesWithStrings.sName,'[^0-9]*','', 'g'),'0'),8)::int", + -kiSortColumn_sName: "regexp_replace(TestBoxesWithStrings.sName,'[0-9]*', '', 'g') DESC, " \ + "RIGHT(CONCAT(regexp_replace(TestBoxesWithStrings.sName,'[^0-9]*','', 'g'),'0'),8)::int DESC", + kiSortColumn_sOs: 'TestBoxesWithStrings.sOs', + -kiSortColumn_sOs: 'TestBoxesWithStrings.sOs DESC', + kiSortColumn_sOsVersion: 'TestBoxesWithStrings.sOsVersion', + -kiSortColumn_sOsVersion: 'TestBoxesWithStrings.sOsVersion DESC', + kiSortColumn_sCpuVendor: 'TestBoxesWithStrings.sCpuVendor', + -kiSortColumn_sCpuVendor: 'TestBoxesWithStrings.sCpuVendor DESC', + kiSortColumn_sCpuArch: 'TestBoxesWithStrings.sCpuArch', + -kiSortColumn_sCpuArch: 'TestBoxesWithStrings.sCpuArch DESC', + kiSortColumn_lCpuRevision: 'TestBoxesWithStrings.lCpuRevision', + -kiSortColumn_lCpuRevision: 'TestBoxesWithStrings.lCpuRevision DESC', + kiSortColumn_cCpus: 'TestBoxesWithStrings.cCpus', + -kiSortColumn_cCpus: 'TestBoxesWithStrings.cCpus DESC', + kiSortColumn_cMbMemory: 'TestBoxesWithStrings.cMbMemory', + -kiSortColumn_cMbMemory: 'TestBoxesWithStrings.cMbMemory DESC', + kiSortColumn_cMbScratch: 'TestBoxesWithStrings.cMbScratch', + -kiSortColumn_cMbScratch: 'TestBoxesWithStrings.cMbScratch DESC', + kiSortColumn_fCpuNestedPaging: 'TestBoxesWithStrings.fCpuNestedPaging', + -kiSortColumn_fCpuNestedPaging: 'TestBoxesWithStrings.fCpuNestedPaging DESC', + kiSortColumn_iTestBoxScriptRev: 'TestBoxesWithStrings.iTestBoxScriptRev', + -kiSortColumn_iTestBoxScriptRev: 'TestBoxesWithStrings.iTestBoxScriptRev DESC', + kiSortColumn_iPythonHexVersion: 'TestBoxesWithStrings.iPythonHexVersion', + -kiSortColumn_iPythonHexVersion: 'TestBoxesWithStrings.iPythonHexVersion DESC', + kiSortColumn_enmPendingCmd: 'TestBoxesWithStrings.enmPendingCmd', + -kiSortColumn_enmPendingCmd: 'TestBoxesWithStrings.enmPendingCmd DESC', + kiSortColumn_fEnabled: 'TestBoxesWithStrings.fEnabled', + -kiSortColumn_fEnabled: 'TestBoxesWithStrings.fEnabled DESC', + kiSortColumn_enmState: 'TestBoxStatuses.enmState', + -kiSortColumn_enmState: 'TestBoxStatuses.enmState DESC', + kiSortColumn_tsUpdated: 'TestBoxStatuses.tsUpdated', + -kiSortColumn_tsUpdated: 'TestBoxStatuses.tsUpdated DESC', + }; + + def __init__(self, oDb): + ModelLogicBase.__init__(self, oDb); + self.dCache = None; + + def tryFetchTestBoxByUuid(self, sTestBoxUuid): + """ + Tries to fetch a testbox by its UUID alone. + """ + self._oDb.execute('SELECT TestBoxesWithStrings.*\n' + 'FROM TestBoxesWithStrings\n' + 'WHERE uuidSystem = %s\n' + ' AND tsExpire = \'infinity\'::timestamp\n' + 'ORDER BY tsEffective DESC\n', + (sTestBoxUuid,)); + if self._oDb.getRowCount() == 0: + return None; + if self._oDb.getRowCount() != 1: + raise TMTooManyRows('Database integrity error: %u hits' % (self._oDb.getRowCount(),)); + oData = TestBoxData(); + oData.initFromDbRow(self._oDb.fetchOne()); + return oData; + + def fetchForListing(self, iStart, cMaxRows, tsNow, aiSortColumns = None): + """ + Fetches testboxes for listing. + + Returns an array (list) of TestBoxDataForListing items, empty list if none. + The TestBoxDataForListing instances are just TestBoxData with two extra + members, an extra oStatus member that is either None or a TestBoxStatusData + instance, and a member tsCurrent holding CURRENT_TIMESTAMP. + + Raises exception on error. + """ + class TestBoxDataForListing(TestBoxDataEx): + """ We add two members for the listing. """ + def __init__(self): + TestBoxDataEx.__init__(self); + self.tsCurrent = None; # CURRENT_TIMESTAMP + self.oStatus = None # type: TestBoxStatusData + + from testmanager.core.testboxstatus import TestBoxStatusData; + + if not aiSortColumns: + aiSortColumns = [self.kiSortColumn_sName,]; + + if tsNow is None: + self._oDb.execute('SELECT TestBoxesWithStrings.*,\n' + ' TestBoxStatuses.*\n' + 'FROM TestBoxesWithStrings\n' + ' LEFT OUTER JOIN TestBoxStatuses\n' + ' ON TestBoxStatuses.idTestBox = TestBoxesWithStrings.idTestBox\n' + 'WHERE TestBoxesWithStrings.tsExpire = \'infinity\'::TIMESTAMP\n' + 'ORDER BY ' + (', '.join([self.kdSortColumnMap[i] for i in aiSortColumns])) + '\n' + 'LIMIT %s OFFSET %s\n' + , (cMaxRows, iStart,)); + else: + self._oDb.execute('SELECT TestBoxesWithStrings.*,\n' + ' TestBoxStatuses.*\n' + 'FROM TestBoxesWithStrings\n' + ' LEFT OUTER JOIN TestBoxStatuses\n' + ' ON TestBoxStatuses.idTestBox = TestBoxesWithStrings.idTestBox\n' + 'WHERE tsExpire > %s\n' + ' AND tsEffective <= %s\n' + 'ORDER BY ' + (', '.join([self.kdSortColumnMap[i] for i in aiSortColumns])) + '\n' + 'LIMIT %s OFFSET %s\n' + , ( tsNow, tsNow, cMaxRows, iStart,)); + + aoRows = []; + for aoOne in self._oDb.fetchAll(): + oTestBox = TestBoxDataForListing().initFromDbRowEx(aoOne, self._oDb, tsNow); + oTestBox.tsCurrent = self._oDb.getCurrentTimestamp(); + if aoOne[TestBoxData.kcDbColumns] is not None: + oTestBox.oStatus = TestBoxStatusData().initFromDbRow(aoOne[TestBoxData.kcDbColumns:]); + aoRows.append(oTestBox); + return aoRows; + + def fetchForSchedGroup(self, idSchedGroup, tsNow, aiSortColumns = None): + """ + Fetches testboxes for listing. + + Returns an array (list) of TestBoxDataForSchedGroup items, empty list if none. + + Raises exception on error. + """ + if not aiSortColumns: + aiSortColumns = [self.kiSortColumn_sName,]; + asSortColumns = [self.kdSortColumnMap[i] for i in aiSortColumns]; + asSortColumns.append('TestBoxesInSchedGroups.idTestBox'); + + if tsNow is None: + self._oDb.execute(''' +SELECT TestBoxesInSchedGroups.*, + TestBoxesWithStrings.* +FROM TestBoxesInSchedGroups + LEFT OUTER JOIN TestBoxesWithStrings + ON TestBoxesWithStrings.idTestBox = TestBoxesInSchedGroups.idTestBox + AND TestBoxesWithStrings.tsExpire = 'infinity'::TIMESTAMP +WHERE TestBoxesInSchedGroups.idSchedGroup = %s + AND TestBoxesInSchedGroups.tsExpire = 'infinity'::TIMESTAMP +ORDER BY ''' + ', '.join(asSortColumns), (idSchedGroup, )); + else: + self._oDb.execute(''' +SELECT TestBoxesInSchedGroups.*, + TestBoxesWithStrings.* +FROM TestBoxesInSchedGroups + LEFT OUTER JOIN TestBoxesWithStrings + ON TestBoxesWithStrings.idTestBox = TestBoxesInSchedGroups.idTestBox + AND TestBoxesWithStrings.tsExpire > %s + AND TestBoxesWithStrings.tsEffective <= %s +WHERE TestBoxesInSchedGroups.idSchedGroup = %s + AND TestBoxesInSchedGroups.tsExpire > %s + AND TestBoxesInSchedGroups.tsEffective <= %s +ORDER BY ''' + ', '.join(asSortColumns), (tsNow, tsNow, idSchedGroup, tsNow, tsNow, )); + + aoRows = []; + for aoOne in self._oDb.fetchAll(): + aoRows.append(TestBoxDataForSchedGroup().initFromDbRow(aoOne)); + return aoRows; + + def fetchForChangeLog(self, idTestBox, iStart, cMaxRows, tsNow): # pylint: disable=too-many-locals + """ + 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. + """ + + ## @todo calc changes to scheduler group! + + if tsNow is None: + tsNow = self._oDb.getCurrentTimestamp(); + + self._oDb.execute('SELECT TestBoxesWithStrings.*\n' + 'FROM TestBoxesWithStrings\n' + 'WHERE TestBoxesWithStrings.tsEffective <= %s\n' + ' AND TestBoxesWithStrings.idTestBox = %s\n' + 'ORDER BY TestBoxesWithStrings.tsExpire DESC\n' + 'LIMIT %s OFFSET %s\n' + , (tsNow, idTestBox, cMaxRows + 1, iStart,)); + + aoRows = []; + for aoDbRow in self._oDb.fetchAll(): + aoRows.append(TestBoxData().initFromDbRow(aoDbRow)); + + # Calculate the changes. + aoEntries = []; + for i in xrange(0, len(aoRows) - 1): + oNew = aoRows[i]; + oOld = aoRows[i + 1]; + aoChanges = []; + for sAttr in oNew.getDataAttributes(): + if sAttr not in [ 'tsEffective', 'tsExpire', 'uidAuthor', ]: + oOldAttr = getattr(oOld, sAttr); + oNewAttr = getattr(oNew, sAttr); + if oOldAttr != oNewAttr: + if sAttr == 'sReport': + aoChanges.append(AttributeChangeEntryPre(sAttr, oNewAttr, oOldAttr, str(oNewAttr), str(oOldAttr))); + else: + aoChanges.append(AttributeChangeEntry(sAttr, oNewAttr, oOldAttr, str(oNewAttr), str(oOldAttr))); + aoEntries.append(ChangeLogEntry(oNew.uidAuthor, None, oNew.tsEffective, oNew.tsExpire, oNew, oOld, aoChanges)); + + # If we're at the end of the log, add the initial entry. + if len(aoRows) <= cMaxRows and aoRows: + oNew = aoRows[-1]; + aoEntries.append(ChangeLogEntry(oNew.uidAuthor, None, oNew.tsEffective, oNew.tsExpire, oNew, None, [])); + + UserAccountLogic(self._oDb).resolveChangeLogAuthors(aoEntries); + return (aoEntries, len(aoRows) > cMaxRows); + + def _validateAndConvertData(self, oData, enmValidateFor): + # type: (TestBoxDataEx, str) -> None + """ + Helper for addEntry and editEntry that validates the scheduling group IDs in + addtion to what's covered by the default validateAndConvert of the data object. + + Raises exception on invalid input. + """ + dDataErrors = oData.validateAndConvert(self._oDb, enmValidateFor); + if dDataErrors: + raise TMInvalidData('TestBoxLogic.addEntry: %s' % (dDataErrors,)); + if isinstance(oData, TestBoxDataEx): + if oData.aoInSchedGroups: + sSchedGrps = ', '.join('(%s)' % oCur.idSchedGroup for oCur in oData.aoInSchedGroups); + self._oDb.execute('SELECT SchedGroupIDs.idSchedGroup\n' + 'FROM (VALUES ' + sSchedGrps + ' ) AS SchedGroupIDs(idSchedGroup)\n' + ' LEFT OUTER JOIN SchedGroups\n' + ' ON SchedGroupIDs.idSchedGroup = SchedGroups.idSchedGroup\n' + ' AND SchedGroups.tsExpire = \'infinity\'::TIMESTAMP\n' + 'WHERE SchedGroups.idSchedGroup IS NULL\n'); + aaoRows = self._oDb.fetchAll(); + if aaoRows: + raise TMInvalidData('TestBoxLogic.addEntry missing scheduling groups: %s' + % (', '.join(str(aoRow[0]) for aoRow in aaoRows),)); + return None; + + def addEntry(self, oData, uidAuthor, fCommit = False): + # type: (TestBoxDataEx, int, bool) -> (int, int, datetime.datetime) + """ + Creates a testbox in the database. + Returns the testbox ID, testbox generation ID and effective timestamp + of the created testbox on success. Throws error on failure. + """ + + # + # Validate. Extra work because of missing foreign key (due to history). + # + self._validateAndConvertData(oData, oData.ksValidateFor_Add); + + # + # Do it. + # + self._oDb.callProc('TestBoxLogic_addEntry' + , ( uidAuthor, + oData.ip, # Should we allow setting the IP? + oData.uuidSystem, + oData.sName, + oData.sDescription, + oData.fEnabled, + oData.enmLomKind, + oData.ipLom, + oData.pctScaleTimeout, + oData.sComment, + oData.enmPendingCmd, ) ); + (idTestBox, idGenTestBox, tsEffective) = self._oDb.fetchOne(); + + for oInSchedGrp in oData.aoInSchedGroups: + self._oDb.callProc('TestBoxLogic_addGroupEntry', + ( uidAuthor, idTestBox, oInSchedGrp.idSchedGroup, oInSchedGrp.iSchedPriority,) ); + + self._oDb.maybeCommit(fCommit); + return (idTestBox, idGenTestBox, tsEffective); + + + def editEntry(self, oData, uidAuthor, fCommit = False): + """ + Data edit update, web UI is the primary user. + + oData is either TestBoxDataEx or TestBoxData. The latter is for enabling + Returns the new generation ID and effective date. + """ + + # + # Validate. + # + self._validateAndConvertData(oData, oData.ksValidateFor_Edit); + + # + # Get current data. + # + oOldData = TestBoxDataEx().initFromDbWithId(self._oDb, oData.idTestBox); + + # + # Do it. + # + if not oData.isEqualEx(oOldData, [ 'tsEffective', 'tsExpire', 'uidAuthor', 'aoInSchedGroups', ] + + TestBoxData.kasMachineSettableOnly ): + self._oDb.callProc('TestBoxLogic_editEntry' + , ( uidAuthor, + oData.idTestBox, + oData.ip, # Should we allow setting the IP? + oData.uuidSystem, + oData.sName, + oData.sDescription, + oData.fEnabled, + oData.enmLomKind, + oData.ipLom, + oData.pctScaleTimeout, + oData.sComment, + oData.enmPendingCmd, )); + (idGenTestBox, tsEffective) = self._oDb.fetchOne(); + else: + idGenTestBox = oOldData.idGenTestBox; + tsEffective = oOldData.tsEffective; + + if isinstance(oData, TestBoxDataEx): + # Calc in-group changes. + aoRemoved = list(oOldData.aoInSchedGroups); + aoNew = []; + aoUpdated = []; + for oNewInGroup in oData.aoInSchedGroups: + oOldInGroup = None; + for iCur, oCur in enumerate(aoRemoved): + if oCur.idSchedGroup == oNewInGroup.idSchedGroup: + oOldInGroup = aoRemoved.pop(iCur); + break; + if oOldInGroup is None: + aoNew.append(oNewInGroup); + elif oNewInGroup.iSchedPriority != oOldInGroup.iSchedPriority: + aoUpdated.append(oNewInGroup); + + # Remove in-groups. + for oInGroup in aoRemoved: + self._oDb.callProc('TestBoxLogic_removeGroupEntry', (uidAuthor, oData.idTestBox, oInGroup.idSchedGroup, )); + + # Add new ones. + for oInGroup in aoNew: + self._oDb.callProc('TestBoxLogic_addGroupEntry', + ( uidAuthor, oData.idTestBox, oInGroup.idSchedGroup, oInGroup.iSchedPriority, ) ); + + # Edit existing ones. + for oInGroup in aoUpdated: + self._oDb.callProc('TestBoxLogic_editGroupEntry', + ( uidAuthor, oData.idTestBox, oInGroup.idSchedGroup, oInGroup.iSchedPriority, ) ); + else: + assert isinstance(oData, TestBoxData); + + self._oDb.maybeCommit(fCommit); + return (idGenTestBox, tsEffective); + + + def removeEntry(self, uidAuthor, idTestBox, fCascade = False, fCommit = False): + """ + Delete test box and scheduling group associations. + """ + self._oDb.callProc('TestBoxLogic_removeEntry' + , ( uidAuthor, idTestBox, fCascade,)); + self._oDb.maybeCommit(fCommit); + return True; + + + def updateOnSignOn(self, idTestBox, idGenTestBox, sTestBoxAddr, sOs, sOsVersion, # pylint: disable=too-many-arguments,too-many-locals + sCpuVendor, sCpuArch, sCpuName, lCpuRevision, cCpus, fCpuHwVirt, fCpuNestedPaging, fCpu64BitGuest, + fChipsetIoMmu, fRawMode, cMbMemory, cMbScratch, sReport, iTestBoxScriptRev, iPythonHexVersion): + """ + Update the testbox attributes automatically on behalf of the testbox script. + Returns the new generation id on success, raises an exception on failure. + """ + _ = idGenTestBox; + self._oDb.callProc('TestBoxLogic_updateOnSignOn' + , ( idTestBox, + sTestBoxAddr, + sOs, + sOsVersion, + sCpuVendor, + sCpuArch, + sCpuName, + lCpuRevision, + cCpus, + fCpuHwVirt, + fCpuNestedPaging, + fCpu64BitGuest, + fChipsetIoMmu, + fRawMode, + cMbMemory, + cMbScratch, + sReport, + iTestBoxScriptRev, + iPythonHexVersion,)); + return self._oDb.fetchOne()[0]; + + + def setCommand(self, idTestBox, sOldCommand, sNewCommand, uidAuthor = None, fCommit = False, sComment = None): + """ + Sets or resets the pending command on a testbox. + Returns (idGenTestBox, tsEffective) of the new row. + """ + ## @todo throw TMInFligthCollision again... + self._oDb.callProc('TestBoxLogic_setCommand' + , ( uidAuthor, idTestBox, sOldCommand, sNewCommand, sComment,)); + aoRow = self._oDb.fetchOne(); + self._oDb.maybeCommit(fCommit); + return (aoRow[0], aoRow[1]); + + + def getAll(self): + """ + Retrieve list of all registered Test Box records from DB. + """ + self._oDb.execute('SELECT *\n' + 'FROM TestBoxesWithStrings\n' + 'WHERE tsExpire=\'infinity\'::timestamp\n' + 'ORDER BY sName') + + aaoRows = self._oDb.fetchAll() + aoRet = [] + for aoRow in aaoRows: + aoRet.append(TestBoxData().initFromDbRow(aoRow)) + return aoRet + + + def cachedLookup(self, idTestBox): + # type: (int) -> TestBoxDataEx + """ + Looks up the most recent TestBoxData object for idTestBox via + an object cache. + + Returns a shared TestBoxDataEx object. None if not found. + Raises exception on DB error. + """ + if self.dCache is None: + self.dCache = self._oDb.getCache('TestBoxData'); + oEntry = self.dCache.get(idTestBox, None); + if oEntry is None: + fNeedNow = False; + self._oDb.execute('SELECT TestBoxesWithStrings.*\n' + 'FROM TestBoxesWithStrings\n' + 'WHERE idTestBox = %s\n' + ' AND tsExpire = \'infinity\'::TIMESTAMP\n' + , (idTestBox, )); + if self._oDb.getRowCount() == 0: + # Maybe it was deleted, try get the last entry. + self._oDb.execute('SELECT TestBoxesWithStrings.*\n' + 'FROM TestBoxesWithStrings\n' + 'WHERE idTestBox = %s\n' + 'ORDER BY tsExpire DESC\n' + 'LIMIT 1\n' + , (idTestBox, )); + fNeedNow = True; + elif self._oDb.getRowCount() > 1: + raise self._oDb.integrityException('%s infinity rows for %s' % (self._oDb.getRowCount(), idTestBox)); + + if self._oDb.getRowCount() == 1: + aaoRow = self._oDb.fetchOne(); + if not fNeedNow: + oEntry = TestBoxDataEx().initFromDbRowEx(aaoRow, self._oDb); + else: + oEntry = TestBoxDataEx().initFromDbRow(aaoRow); + oEntry.initFromDbRowEx(aaoRow, self._oDb, tsNow = db.dbTimestampMinusOneTick(oEntry.tsExpire)); + self.dCache[idTestBox] = oEntry; + return oEntry; + + + + # + # The virtual test sheriff interface. + # + + def hasTestBoxRecentlyBeenRebooted(self, idTestBox, cHoursBack = 2, tsNow = None): + """ + Checks if the testbox has been rebooted in the specified time period. + + This does not include already pending reboots, though under some + circumstances it may. These being the test box entry being edited for + other reasons. + + Returns True / False. + """ + if tsNow is None: + tsNow = self._oDb.getCurrentTimestamp(); + self._oDb.execute('SELECT COUNT(idTestBox)\n' + 'FROM TestBoxes\n' + 'WHERE idTestBox = %s\n' + ' AND tsExpire < %s\n' + ' AND tsExpire >= %s - interval \'%s hours\'\n' + ' AND enmPendingCmd IN (%s, %s)\n' + , ( idTestBox, tsNow, tsNow, cHoursBack, + TestBoxData.ksTestBoxCmd_Reboot, TestBoxData.ksTestBoxCmd_UpgradeAndReboot, )); + return self._oDb.fetchOne()[0] > 0; + + + def rebootTestBox(self, idTestBox, uidAuthor, sComment, sOldCommand = TestBoxData.ksTestBoxCmd_None, fCommit = False): + """ + Issues a reboot command for the given test box. + Return True on succes, False on in-flight collision. + May raise DB exception on other trouble. + """ + try: + self.setCommand(idTestBox, sOldCommand, TestBoxData.ksTestBoxCmd_Reboot, + uidAuthor = uidAuthor, fCommit = fCommit, sComment = sComment); + except TMInFligthCollision: + return False; + return True; + + + def disableTestBox(self, idTestBox, uidAuthor, sComment, fCommit = False): + """ + Disables the given test box. + + Raises exception on trouble, without rollback. + """ + oTestBox = TestBoxData().initFromDbWithId(self._oDb, idTestBox); + if oTestBox.fEnabled: + oTestBox.fEnabled = False; + if sComment is not None: + oTestBox.sComment = sComment; + self.editEntry(oTestBox, uidAuthor = uidAuthor, fCommit = fCommit); + return None; + + +# +# Unit testing. +# + +# pylint: disable=missing-docstring +class TestBoxDataTestCase(ModelDataBaseTestCase): + def setUp(self): + self.aoSamples = [TestBoxData(),]; + +if __name__ == '__main__': + unittest.main(); + # not reached. + diff --git a/src/VBox/ValidationKit/testmanager/core/testboxcontroller.py b/src/VBox/ValidationKit/testmanager/core/testboxcontroller.py new file mode 100755 index 00000000..b131aa88 --- /dev/null +++ b/src/VBox/ValidationKit/testmanager/core/testboxcontroller.py @@ -0,0 +1,954 @@ +# -*- coding: utf-8 -*- +# $Id: testboxcontroller.py $ + +""" +Test Manager Core - Web Server Abstraction Base Class. +""" + +__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 re; +import os; +import string; # pylint: disable=deprecated-module +import sys; +import uuid; + +# Validation Kit imports. +from common import constants; +from testmanager import config; +from testmanager.core import coreconsts; +from testmanager.core.db import TMDatabaseConnection; +from testmanager.core.base import TMExceptionBase; +from testmanager.core.globalresource import GlobalResourceLogic; +from testmanager.core.testboxstatus import TestBoxStatusData, TestBoxStatusLogic; +from testmanager.core.testbox import TestBoxData, TestBoxLogic; +from testmanager.core.testresults import TestResultLogic, TestResultFileData; +from testmanager.core.testset import TestSetData, TestSetLogic; +from testmanager.core.systemlog import SystemLogData, SystemLogLogic; +from testmanager.core.schedulerbase import SchedulerBase; + +# Python 3 hacks: +if sys.version_info[0] >= 3: + long = int; # pylint: disable=redefined-builtin,invalid-name + + +class TestBoxControllerException(TMExceptionBase): + """ + Exception class for TestBoxController. + """ + pass; # pylint: disable=unnecessary-pass + + +class TestBoxController(object): # pylint: disable=too-few-public-methods + """ + TestBox Controller class. + """ + + ## Applicable testbox commands to an idle TestBox. + kasIdleCmds = [TestBoxData.ksTestBoxCmd_Reboot, + TestBoxData.ksTestBoxCmd_Upgrade, + TestBoxData.ksTestBoxCmd_UpgradeAndReboot, + TestBoxData.ksTestBoxCmd_Special]; + ## Applicable testbox commands to a busy TestBox. + kasBusyCmds = [TestBoxData.ksTestBoxCmd_Abort, TestBoxData.ksTestBoxCmd_Reboot]; + ## Commands that can be ACK'ed. + kasAckableCmds = [constants.tbresp.CMD_EXEC, constants.tbresp.CMD_ABORT, constants.tbresp.CMD_REBOOT, + constants.tbresp.CMD_UPGRADE, constants.tbresp.CMD_UPGRADE_AND_REBOOT, constants.tbresp.CMD_SPECIAL]; + ## Commands that can be NACK'ed or NOTSUP'ed. + kasNackableCmds = kasAckableCmds + [kasAckableCmds, constants.tbresp.CMD_IDLE, constants.tbresp.CMD_WAIT]; + + ## Mapping from TestBoxCmd_T to TestBoxState_T + kdCmdToState = \ + { \ + TestBoxData.ksTestBoxCmd_Abort: None, + TestBoxData.ksTestBoxCmd_Reboot: TestBoxStatusData.ksTestBoxState_Rebooting, + TestBoxData.ksTestBoxCmd_Upgrade: TestBoxStatusData.ksTestBoxState_Upgrading, + TestBoxData.ksTestBoxCmd_UpgradeAndReboot: TestBoxStatusData.ksTestBoxState_UpgradingAndRebooting, + TestBoxData.ksTestBoxCmd_Special: TestBoxStatusData.ksTestBoxState_DoingSpecialCmd, + }; + + ## Mapping from TestBoxCmd_T to TestBox responses commands. + kdCmdToTbRespCmd = \ + { + TestBoxData.ksTestBoxCmd_Abort: constants.tbresp.CMD_ABORT, + TestBoxData.ksTestBoxCmd_Reboot: constants.tbresp.CMD_REBOOT, + TestBoxData.ksTestBoxCmd_Upgrade: constants.tbresp.CMD_UPGRADE, + TestBoxData.ksTestBoxCmd_UpgradeAndReboot: constants.tbresp.CMD_UPGRADE_AND_REBOOT, + TestBoxData.ksTestBoxCmd_Special: constants.tbresp.CMD_SPECIAL, + }; + + ## Mapping from TestBox responses to TestBoxCmd_T commands. + kdTbRespCmdToCmd = \ + { + constants.tbresp.CMD_IDLE: None, + constants.tbresp.CMD_WAIT: None, + constants.tbresp.CMD_EXEC: None, + constants.tbresp.CMD_ABORT: TestBoxData.ksTestBoxCmd_Abort, + constants.tbresp.CMD_REBOOT: TestBoxData.ksTestBoxCmd_Reboot, + constants.tbresp.CMD_UPGRADE: TestBoxData.ksTestBoxCmd_Upgrade, + constants.tbresp.CMD_UPGRADE_AND_REBOOT: TestBoxData.ksTestBoxCmd_UpgradeAndReboot, + constants.tbresp.CMD_SPECIAL: TestBoxData.ksTestBoxCmd_Special, + }; + + + ## The path to the upgrade zip, relative WebServerGlueBase.getBaseUrl(). + ksUpgradeZip = 'htdocs/upgrade/VBoxTestBoxScript.zip'; + + ## Valid TestBox result values. + kasValidResults = list(constants.result.g_kasValidResults); + ## Mapping TestBox result values to TestStatus_T values. + kadTbResultToStatus = \ + { + constants.result.PASSED: TestSetData.ksTestStatus_Success, + constants.result.SKIPPED: TestSetData.ksTestStatus_Skipped, + constants.result.ABORTED: TestSetData.ksTestStatus_Aborted, + constants.result.BAD_TESTBOX: TestSetData.ksTestStatus_BadTestBox, + constants.result.FAILED: TestSetData.ksTestStatus_Failure, + constants.result.TIMED_OUT: TestSetData.ksTestStatus_TimedOut, + constants.result.REBOOTED: TestSetData.ksTestStatus_Rebooted, + }; + + + def __init__(self, oSrvGlue): + """ + Won't raise exceptions. + """ + self._oSrvGlue = oSrvGlue; + self._sAction = None; # _getStandardParams / dispatchRequest sets this later on. + self._idTestBox = None; # _getStandardParams / dispatchRequest sets this later on. + self._sTestBoxUuid = None; # _getStandardParams / dispatchRequest sets this later on. + self._sTestBoxAddr = None; # _getStandardParams / dispatchRequest sets this later on. + self._idTestSet = None; # _getStandardParams / dispatchRequest sets this later on. + self._dParams = None; # _getStandardParams / dispatchRequest sets this later on. + self._asCheckedParams = []; + self._dActions = \ + { \ + constants.tbreq.SIGNON : self._actionSignOn, + constants.tbreq.REQUEST_COMMAND_BUSY: self._actionRequestCommandBusy, + constants.tbreq.REQUEST_COMMAND_IDLE: self._actionRequestCommandIdle, + constants.tbreq.COMMAND_ACK : self._actionCommandAck, + constants.tbreq.COMMAND_NACK : self._actionCommandNack, + constants.tbreq.COMMAND_NOTSUP : self._actionCommandNotSup, + constants.tbreq.LOG_MAIN : self._actionLogMain, + constants.tbreq.UPLOAD : self._actionUpload, + constants.tbreq.XML_RESULTS : self._actionXmlResults, + constants.tbreq.EXEC_COMPLETED : self._actionExecCompleted, + }; + + def _getStringParam(self, sName, asValidValues = None, fStrip = False, sDefValue = None): + """ + Gets a string parameter (stripped). + + Raises exception if not found and no default is provided, or if the + value isn't found in asValidValues. + """ + if sName not in self._dParams: + if sDefValue is None: + raise TestBoxControllerException('%s parameter %s is missing' % (self._sAction, sName)); + return sDefValue; + sValue = self._dParams[sName]; + if fStrip: + sValue = sValue.strip(); + + if sName not in self._asCheckedParams: + self._asCheckedParams.append(sName); + + if asValidValues is not None and sValue not in asValidValues: + raise TestBoxControllerException('%s parameter %s value "%s" not in %s ' \ + % (self._sAction, sName, sValue, asValidValues)); + return sValue; + + def _getBoolParam(self, sName, fDefValue = None): + """ + Gets a boolean parameter. + + Raises exception if not found and no default is provided, or if not a + valid boolean. + """ + sValue = self._getStringParam(sName, [ 'True', 'true', '1', 'False', 'false', '0'], sDefValue = str(fDefValue)); + return sValue in ('True', 'true', '1',); + + def _getIntParam(self, sName, iMin = None, iMax = None): + """ + Gets a string parameter. + Raises exception if not found, not a valid integer, or if the value + isn't in the range defined by iMin and iMax. + """ + sValue = self._getStringParam(sName); + try: + iValue = int(sValue, 0); + except: + raise TestBoxControllerException('%s parameter %s value "%s" cannot be convert to an integer' \ + % (self._sAction, sName, sValue)); + + if (iMin is not None and iValue < iMin) \ + or (iMax is not None and iValue > iMax): + raise TestBoxControllerException('%s parameter %s value %d is out of range [%s..%s]' \ + % (self._sAction, sName, iValue, iMin, iMax)); + return iValue; + + def _getLongParam(self, sName, lMin = None, lMax = None, lDefValue = None): + """ + Gets a string parameter. + Raises exception if not found, not a valid long integer, or if the value + isn't in the range defined by lMin and lMax. + """ + sValue = self._getStringParam(sName, sDefValue = (str(lDefValue) if lDefValue is not None else None)); + try: + lValue = long(sValue, 0); + except Exception as oXcpt: + raise TestBoxControllerException('%s parameter %s value "%s" cannot be convert to an integer (%s)' \ + % (self._sAction, sName, sValue, oXcpt)); + + if (lMin is not None and lValue < lMin) \ + or (lMax is not None and lValue > lMax): + raise TestBoxControllerException('%s parameter %s value %d is out of range [%s..%s]' \ + % (self._sAction, sName, lValue, lMin, lMax)); + return lValue; + + def _checkForUnknownParameters(self): + """ + Check if we've handled all parameters, raises exception if anything + unknown was found. + """ + + if len(self._asCheckedParams) != len(self._dParams): + sUnknownParams = ''; + for sKey in self._dParams: + if sKey not in self._asCheckedParams: + sUnknownParams += ' ' + sKey + '=' + self._dParams[sKey]; + raise TestBoxControllerException('Unknown parameters: ' + sUnknownParams); + + return True; + + def _writeResponse(self, dParams): + """ + Makes a reply to the testbox script. + Will raise exception on failure. + """ + self._oSrvGlue.writeParams(dParams); + self._oSrvGlue.flush(); + return True; + + def _resultResponse(self, sResultValue): + """ + Makes a simple reply to the testbox script. + Will raise exception on failure. + """ + return self._writeResponse({constants.tbresp.ALL_PARAM_RESULT: sResultValue}); + + + def _idleResponse(self): + """ + Makes an IDLE reply to the testbox script. + Will raise exception on failure. + """ + return self._writeResponse({ constants.tbresp.ALL_PARAM_RESULT: constants.tbresp.CMD_IDLE }); + + def _cleanupOldTest(self, oDb, oStatusData): + """ + Cleans up any old test set that may be left behind and changes the + state to 'idle'. See scenario #9: + file://../../docs/AutomaticTestingRevamp.html#cleaning-up-abandoned-testcase + + Note. oStatusData.enmState is set to idle, but tsUpdated is not changed. + """ + + # Cleanup any abandoned test. + if oStatusData.idTestSet is not None: + SystemLogLogic(oDb).addEntry(SystemLogData.ksEvent_TestSetAbandoned, + "idTestSet=%u idTestBox=%u enmState=%s %s" + % (oStatusData.idTestSet, oStatusData.idTestBox, + oStatusData.enmState, self._sAction), + fCommit = False); + TestSetLogic(oDb).completeAsAbandoned(oStatusData.idTestSet, fCommit = False); + GlobalResourceLogic(oDb).freeGlobalResourcesByTestBox(self._idTestBox, fCommit = False); + + # Change to idle status + if oStatusData.enmState != TestBoxStatusData.ksTestBoxState_Idle: + TestBoxStatusLogic(oDb).updateState(self._idTestBox, TestBoxStatusData.ksTestBoxState_Idle, fCommit = False); + oStatusData.tsUpdated = oDb.getCurrentTimestamp(); + oStatusData.enmState = TestBoxStatusData.ksTestBoxState_Idle; + + # Commit. + oDb.commit(); + + return True; + + def _connectToDbAndValidateTb(self, asValidStates = None): + """ + Connects to the database and validates the testbox. + + Returns (TMDatabaseConnection, TestBoxStatusData, TestBoxData) on success. + Returns (None, None, None) on failure after sending the box an appropriate response. + May raise exception on DB error. + """ + oDb = TMDatabaseConnection(self._oSrvGlue.dprint); + oLogic = TestBoxStatusLogic(oDb); + (oStatusData, oTestBoxData) = oLogic.tryFetchStatusAndConfig(self._idTestBox, self._sTestBoxUuid, self._sTestBoxAddr); + if oStatusData is None: + self._resultResponse(constants.tbresp.STATUS_DEAD); + elif asValidStates is not None and oStatusData.enmState not in asValidStates: + self._resultResponse(constants.tbresp.STATUS_NACK); + elif self._idTestSet is not None and self._idTestSet != oStatusData.idTestSet: + self._resultResponse(constants.tbresp.STATUS_NACK); + else: + return (oDb, oStatusData, oTestBoxData); + return (None, None, None); + + def writeToMainLog(self, oTestSet, sText, fIgnoreSizeCheck = False): + """ Writes the text to the main log file. """ + + # Calc the file name and open the file. + sFile = os.path.join(config.g_ksFileAreaRootDir, oTestSet.sBaseFilename + '-main.log'); + if not os.path.exists(os.path.dirname(sFile)): + os.makedirs(os.path.dirname(sFile), 0o755); + + with open(sFile, 'ab') as oFile: + # Check the size. + fSizeOk = True; + if not fIgnoreSizeCheck: + oStat = os.fstat(oFile.fileno()); + fSizeOk = oStat.st_size / (1024 * 1024) < config.g_kcMbMaxMainLog; + + # Write the text. + if fSizeOk: + if sys.version_info[0] >= 3: + oFile.write(bytes(sText, 'utf-8')); + else: + oFile.write(sText); + + return fSizeOk; + + def _actionSignOn(self): # pylint: disable=too-many-locals + """ Implement sign-on """ + + # + # Validate parameters (raises exception on failure). + # + sOs = self._getStringParam(constants.tbreq.SIGNON_PARAM_OS, coreconsts.g_kasOses); + sOsVersion = self._getStringParam(constants.tbreq.SIGNON_PARAM_OS_VERSION); + sCpuVendor = self._getStringParam(constants.tbreq.SIGNON_PARAM_CPU_VENDOR); + sCpuArch = self._getStringParam(constants.tbreq.SIGNON_PARAM_CPU_ARCH, coreconsts.g_kasCpuArches); + sCpuName = self._getStringParam(constants.tbreq.SIGNON_PARAM_CPU_NAME, fStrip = True, sDefValue = ''); # new + lCpuRevision = self._getLongParam( constants.tbreq.SIGNON_PARAM_CPU_REVISION, lMin = 0, lDefValue = 0); # new + cCpus = self._getIntParam( constants.tbreq.SIGNON_PARAM_CPU_COUNT, 1, 16384); + fCpuHwVirt = self._getBoolParam( constants.tbreq.SIGNON_PARAM_HAS_HW_VIRT); + fCpuNestedPaging = self._getBoolParam( constants.tbreq.SIGNON_PARAM_HAS_NESTED_PAGING); + fCpu64BitGuest = self._getBoolParam( constants.tbreq.SIGNON_PARAM_HAS_64_BIT_GUEST, fDefValue = True); + fChipsetIoMmu = self._getBoolParam( constants.tbreq.SIGNON_PARAM_HAS_IOMMU); + fRawMode = self._getBoolParam( constants.tbreq.SIGNON_PARAM_WITH_RAW_MODE, fDefValue = None); + cMbMemory = self._getLongParam( constants.tbreq.SIGNON_PARAM_MEM_SIZE, 8, 1073741823); # 8MB..1PB + cMbScratch = self._getLongParam( constants.tbreq.SIGNON_PARAM_SCRATCH_SIZE, 0, 1073741823); # 0..1PB + sReport = self._getStringParam(constants.tbreq.SIGNON_PARAM_REPORT, fStrip = True, sDefValue = ''); # new + iTestBoxScriptRev = self._getIntParam( constants.tbreq.SIGNON_PARAM_SCRIPT_REV, 1, 100000000); + iPythonHexVersion = self._getIntParam( constants.tbreq.SIGNON_PARAM_PYTHON_VERSION, 0x020300f0, 0x030f00f0); + self._checkForUnknownParameters(); + + # Null conversions for new parameters. + if not sReport: + sReport = None; + if not sCpuName: + sCpuName = None; + if lCpuRevision <= 0: + lCpuRevision = None; + + # + # Connect to the database and validate the testbox. + # + oDb = TMDatabaseConnection(self._oSrvGlue.dprint); + oTestBoxLogic = TestBoxLogic(oDb); + oTestBox = oTestBoxLogic.tryFetchTestBoxByUuid(self._sTestBoxUuid); + if oTestBox is None: + oSystemLogLogic = SystemLogLogic(oDb); + oSystemLogLogic.addEntry(SystemLogData.ksEvent_TestBoxUnknown, + 'addr=%s uuid=%s os=%s %d cpus' \ + % (self._sTestBoxAddr, self._sTestBoxUuid, sOs, cCpus), + 24, fCommit = True); + return self._resultResponse(constants.tbresp.STATUS_NACK); + + # + # Update the row in TestBoxes if something changed. + # + if oTestBox.cMbScratch is not None and oTestBox.cMbScratch != 0: + cPctScratchDiff = (cMbScratch - oTestBox.cMbScratch) * 100 / oTestBox.cMbScratch; + else: + cPctScratchDiff = 100; + + # pylint: disable=too-many-boolean-expressions + if self._sTestBoxAddr != oTestBox.ip \ + or sOs != oTestBox.sOs \ + or sOsVersion != oTestBox.sOsVersion \ + or sCpuVendor != oTestBox.sCpuVendor \ + or sCpuArch != oTestBox.sCpuArch \ + or sCpuName != oTestBox.sCpuName \ + or lCpuRevision != oTestBox.lCpuRevision \ + or cCpus != oTestBox.cCpus \ + or fCpuHwVirt != oTestBox.fCpuHwVirt \ + or fCpuNestedPaging != oTestBox.fCpuNestedPaging \ + or fCpu64BitGuest != oTestBox.fCpu64BitGuest \ + or fChipsetIoMmu != oTestBox.fChipsetIoMmu \ + or fRawMode != oTestBox.fRawMode \ + or cMbMemory != oTestBox.cMbMemory \ + or abs(cPctScratchDiff) >= min(4 + cMbScratch / 10240, 12) \ + or sReport != oTestBox.sReport \ + or iTestBoxScriptRev != oTestBox.iTestBoxScriptRev \ + or iPythonHexVersion != oTestBox.iPythonHexVersion: + oTestBoxLogic.updateOnSignOn(oTestBox.idTestBox, + oTestBox.idGenTestBox, + sTestBoxAddr = self._sTestBoxAddr, + sOs = sOs, + sOsVersion = sOsVersion, + sCpuVendor = sCpuVendor, + sCpuArch = sCpuArch, + sCpuName = sCpuName, + lCpuRevision = lCpuRevision, + cCpus = cCpus, + fCpuHwVirt = fCpuHwVirt, + fCpuNestedPaging = fCpuNestedPaging, + fCpu64BitGuest = fCpu64BitGuest, + fChipsetIoMmu = fChipsetIoMmu, + fRawMode = fRawMode, + cMbMemory = cMbMemory, + cMbScratch = cMbScratch, + sReport = sReport, + iTestBoxScriptRev = iTestBoxScriptRev, + iPythonHexVersion = iPythonHexVersion); + + # + # Update the testbox status, making sure there is a status. + # + oStatusLogic = TestBoxStatusLogic(oDb); + oStatusData = oStatusLogic.tryFetchStatus(oTestBox.idTestBox); + if oStatusData is not None: + self._cleanupOldTest(oDb, oStatusData); + else: + oStatusLogic.insertIdleStatus(oTestBox.idTestBox, oTestBox.idGenTestBox, fCommit = True); + + # + # ACK the request. + # + dResponse = \ + { + constants.tbresp.ALL_PARAM_RESULT: constants.tbresp.STATUS_ACK, + constants.tbresp.SIGNON_PARAM_ID: oTestBox.idTestBox, + constants.tbresp.SIGNON_PARAM_NAME: oTestBox.sName, + } + return self._writeResponse(dResponse); + + def _doGangCleanup(self, oDb, oStatusData): + """ + _doRequestCommand worker for handling a box in gang-cleanup. + This will check if all testboxes has completed their run, pretending to + be busy until that happens. Once all are completed, resources will be + freed and the testbox returns to idle state (we update oStatusData). + """ + oStatusLogic = TestBoxStatusLogic(oDb) + oTestSet = TestSetData().initFromDbWithId(oDb, oStatusData.idTestSet); + if oStatusLogic.isWholeGangDoneTesting(oTestSet.idTestSetGangLeader): + oDb.begin(); + + GlobalResourceLogic(oDb).freeGlobalResourcesByTestBox(self._idTestBox, fCommit = False); + TestBoxStatusLogic(oDb).updateState(self._idTestBox, TestBoxStatusData.ksTestBoxState_Idle, fCommit = False); + + oStatusData.tsUpdated = oDb.getCurrentTimestamp(); + oStatusData.enmState = TestBoxStatusData.ksTestBoxState_Idle; + + oDb.commit(); + return None; + + def _doGangGatheringTimedOut(self, oDb, oStatusData): + """ + _doRequestCommand worker for handling a box in gang-gathering-timed-out state. + This will do clean-ups similar to _cleanupOldTest and update the state likewise. + """ + oDb.begin(); + + TestSetLogic(oDb).completeAsGangGatheringTimeout(oStatusData.idTestSet, fCommit = False); + GlobalResourceLogic(oDb).freeGlobalResourcesByTestBox(self._idTestBox, fCommit = False); + TestBoxStatusLogic(oDb).updateState(self._idTestBox, TestBoxStatusData.ksTestBoxState_Idle, fCommit = False); + + oStatusData.tsUpdated = oDb.getCurrentTimestamp(); + oStatusData.enmState = TestBoxStatusData.ksTestBoxState_Idle; + + oDb.commit(); + return None; + + def _doGangGathering(self, oDb, oStatusData): + """ + _doRequestCommand worker for handling a box in gang-gathering state. + This only checks for timeout. It will update the oStatusData if a + timeout is detected, so that the box will be idle upon return. + """ + oStatusLogic = TestBoxStatusLogic(oDb); + if oStatusLogic.timeSinceLastChangeInSecs(oStatusData) > config.g_kcSecGangGathering \ + and SchedulerBase.tryCancelGangGathering(oDb, oStatusData): # <-- Updates oStatusData. + self._doGangGatheringTimedOut(oDb, oStatusData); + return None; + + def _doRequestCommand(self, fIdle): + """ + Common code for handling command request. + """ + + (oDb, oStatusData, oTestBoxData) = self._connectToDbAndValidateTb(); + if oDb is None: + return False; + + # + # Status clean up. + # + # Only when BUSY will the TestBox Script request and execute commands + # concurrently. So, it must be idle when sending REQUEST_COMMAND_IDLE. + # + if fIdle: + if oStatusData.enmState == TestBoxStatusData.ksTestBoxState_GangGathering: + self._doGangGathering(oDb, oStatusData); + elif oStatusData.enmState == TestBoxStatusData.ksTestBoxState_GangGatheringTimedOut: + self._doGangGatheringTimedOut(oDb, oStatusData); + elif oStatusData.enmState == TestBoxStatusData.ksTestBoxState_GangTesting: + dResponse = SchedulerBase.composeExecResponse(oDb, oTestBoxData.idTestBox, self._oSrvGlue.getBaseUrl()); + if dResponse is not None: + return dResponse; + elif oStatusData.enmState == TestBoxStatusData.ksTestBoxState_GangCleanup: + self._doGangCleanup(oDb, oStatusData); + elif oStatusData.enmState != TestBoxStatusData.ksTestBoxState_Idle: # (includes ksTestBoxState_GangGatheringTimedOut) + self._cleanupOldTest(oDb, oStatusData); + + # + # Check for pending command. + # + if oTestBoxData.enmPendingCmd != TestBoxData.ksTestBoxCmd_None: + asValidCmds = TestBoxController.kasIdleCmds if fIdle else TestBoxController.kasBusyCmds; + if oTestBoxData.enmPendingCmd in asValidCmds: + dResponse = { constants.tbresp.ALL_PARAM_RESULT: TestBoxController.kdCmdToTbRespCmd[oTestBoxData.enmPendingCmd] }; + if oTestBoxData.enmPendingCmd in [TestBoxData.ksTestBoxCmd_Upgrade, TestBoxData.ksTestBoxCmd_UpgradeAndReboot]: + dResponse[constants.tbresp.UPGRADE_PARAM_URL] = self._oSrvGlue.getBaseUrl() + TestBoxController.ksUpgradeZip; + return self._writeResponse(dResponse); + + if oTestBoxData.enmPendingCmd == TestBoxData.ksTestBoxCmd_Abort and fIdle: + TestBoxLogic(oDb).setCommand(self._idTestBox, sOldCommand = oTestBoxData.enmPendingCmd, + sNewCommand = TestBoxData.ksTestBoxCmd_None, fCommit = True); + + # + # If doing gang stuff, return 'CMD_WAIT'. + # + ## @todo r=bird: Why is GangTesting included here? Figure out when testing gang testing. + if oStatusData.enmState in [TestBoxStatusData.ksTestBoxState_GangGathering, + TestBoxStatusData.ksTestBoxState_GangTesting, + TestBoxStatusData.ksTestBoxState_GangCleanup]: + return self._resultResponse(constants.tbresp.CMD_WAIT); + + # + # If idling and enabled try schedule a new task. + # + if fIdle \ + and oTestBoxData.fEnabled \ + and not TestSetLogic(oDb).isTestBoxExecutingTooRapidly(oTestBoxData.idTestBox) \ + and oStatusData.enmState == TestBoxStatusData.ksTestBoxState_Idle: # (paranoia) + dResponse = SchedulerBase.scheduleNewTask(oDb, oTestBoxData, oStatusData.iWorkItem, self._oSrvGlue.getBaseUrl()); + if dResponse is not None: + return self._writeResponse(dResponse); + + # + # Touch the status row every couple of mins so we can tell that the box is alive. + # + oStatusLogic = TestBoxStatusLogic(oDb); + if oStatusData.enmState != TestBoxStatusData.ksTestBoxState_GangGathering \ + and oStatusLogic.timeSinceLastChangeInSecs(oStatusData) >= TestBoxStatusLogic.kcSecIdleTouchStatus: + oStatusLogic.touchStatus(oTestBoxData.idTestBox, fCommit = True); + + return self._idleResponse(); + + def _actionRequestCommandBusy(self): + """ Implement request for command. """ + self._checkForUnknownParameters(); + return self._doRequestCommand(False); + + def _actionRequestCommandIdle(self): + """ Implement request for command. """ + self._checkForUnknownParameters(); + return self._doRequestCommand(True); + + def _doCommandAckNck(self, sCmd): + """ Implements ACK, NACK and NACK(ENOTSUP). """ + + (oDb, _, _) = self._connectToDbAndValidateTb(); + if oDb is None: + return False; + + # + # If the command maps to a TestBoxCmd_T value, it means we have to + # check and update TestBoxes. If it's an ACK, the testbox status will + # need updating as well. + # + sPendingCmd = TestBoxController.kdTbRespCmdToCmd[sCmd]; + if sPendingCmd is not None: + oTestBoxLogic = TestBoxLogic(oDb) + oTestBoxLogic.setCommand(self._idTestBox, sOldCommand = sPendingCmd, + sNewCommand = TestBoxData.ksTestBoxCmd_None, fCommit = False); + + if self._sAction == constants.tbreq.COMMAND_ACK \ + and TestBoxController.kdCmdToState[sPendingCmd] is not None: + oStatusLogic = TestBoxStatusLogic(oDb); + oStatusLogic.updateState(self._idTestBox, TestBoxController.kdCmdToState[sPendingCmd], fCommit = False); + + # Commit the two updates. + oDb.commit(); + + # + # Log NACKs. + # + if self._sAction != constants.tbreq.COMMAND_ACK: + oSysLogLogic = SystemLogLogic(oDb); + oSysLogLogic.addEntry(SystemLogData.ksEvent_CmdNacked, + 'idTestBox=%s sCmd=%s' % (self._idTestBox, sPendingCmd), + 24, fCommit = True); + + return self._resultResponse(constants.tbresp.STATUS_ACK); + + def _actionCommandAck(self): + """ Implement command ACK'ing """ + sCmd = self._getStringParam(constants.tbreq.COMMAND_ACK_PARAM_CMD_NAME, TestBoxController.kasAckableCmds); + self._checkForUnknownParameters(); + return self._doCommandAckNck(sCmd); + + def _actionCommandNack(self): + """ Implement command NACK'ing """ + sCmd = self._getStringParam(constants.tbreq.COMMAND_ACK_PARAM_CMD_NAME, TestBoxController.kasNackableCmds); + self._checkForUnknownParameters(); + return self._doCommandAckNck(sCmd); + + def _actionCommandNotSup(self): + """ Implement command NACK(ENOTSUP)'ing """ + sCmd = self._getStringParam(constants.tbreq.COMMAND_ACK_PARAM_CMD_NAME, TestBoxController.kasNackableCmds); + self._checkForUnknownParameters(); + return self._doCommandAckNck(sCmd); + + def _actionLogMain(self): + """ Implement submitting log entries to the main log file. """ + # + # Parameter validation. + # + sBody = self._getStringParam(constants.tbreq.LOG_PARAM_BODY, fStrip = False); + if not sBody: + return self._resultResponse(constants.tbresp.STATUS_NACK); + self._checkForUnknownParameters(); + + (oDb, oStatusData, _) = self._connectToDbAndValidateTb([TestBoxStatusData.ksTestBoxState_Testing, + TestBoxStatusData.ksTestBoxState_GangTesting]); + if oStatusData is None: + return False; + + # + # Write the text to the log file. + # + oTestSet = TestSetData().initFromDbWithId(oDb, oStatusData.idTestSet); + self.writeToMainLog(oTestSet, sBody); + ## @todo Overflow is a hanging offence, need to note it and fail whatever is going on... + + # Done. + return self._resultResponse(constants.tbresp.STATUS_ACK); + + def _actionUpload(self): + """ Implement uploading of files. """ + # + # Parameter validation. + # + sName = self._getStringParam(constants.tbreq.UPLOAD_PARAM_NAME); + sMime = self._getStringParam(constants.tbreq.UPLOAD_PARAM_MIME); + sKind = self._getStringParam(constants.tbreq.UPLOAD_PARAM_KIND); + sDesc = self._getStringParam(constants.tbreq.UPLOAD_PARAM_DESC); + self._checkForUnknownParameters(); + + (oDb, oStatusData, _) = self._connectToDbAndValidateTb([TestBoxStatusData.ksTestBoxState_Testing, + TestBoxStatusData.ksTestBoxState_GangTesting]); + if oStatusData is None: + return False; + + if len(sName) > 128 or len(sName) < 3: + raise TestBoxControllerException('Invalid file name "%s"' % (sName,)); + if re.match(r'^[a-zA-Z0-9_\-(){}#@+,.=]*$', sName) is None: + raise TestBoxControllerException('Invalid file name "%s"' % (sName,)); + + if sMime not in [ 'text/plain', #'text/html', 'text/xml', + 'application/octet-stream', + 'image/png', #'image/gif', 'image/jpeg', + 'video/webm', #'video/mpeg', 'video/mpeg4-generic', + ]: + raise TestBoxControllerException('Invalid MIME type "%s"' % (sMime,)); + + if sKind not in TestResultFileData.kasKinds: + raise TestBoxControllerException('Invalid kind "%s"' % (sKind,)); + + if len(sDesc) > 256: + raise TestBoxControllerException('Invalid description "%s"' % (sDesc,)); + if not set(sDesc).issubset(set(string.printable)): + raise TestBoxControllerException('Invalid description "%s"' % (sDesc,)); + + if ('application/octet-stream', {}) != self._oSrvGlue.getContentType(): + raise TestBoxControllerException('Unexpected content type: %s; %s' % self._oSrvGlue.getContentType()); + + cbFile = self._oSrvGlue.getContentLength(); + if cbFile <= 0: + raise TestBoxControllerException('File "%s" is empty or negative in size (%s)' % (sName, cbFile)); + if (cbFile + 1048575) / 1048576 > config.g_kcMbMaxUploadSingle: + raise TestBoxControllerException('File "%s" is too big %u bytes (max %u MiB)' + % (sName, cbFile, config.g_kcMbMaxUploadSingle,)); + + # + # Write the text to the log file. + # + oTestSet = TestSetData().initFromDbWithId(oDb, oStatusData.idTestSet); + oDstFile = TestSetLogic(oDb).createFile(oTestSet, sName = sName, sMime = sMime, sKind = sKind, sDesc = sDesc, + cbFile = cbFile, fCommit = True); + + offFile = 0; + oSrcFile = self._oSrvGlue.getBodyIoStreamBinary(); + while offFile < cbFile: + cbToRead = cbFile - offFile; + if cbToRead > 256*1024: + cbToRead = 256*1024; + offFile += cbToRead; + + abBuf = oSrcFile.read(cbToRead); + oDstFile.write(abBuf); # pylint: disable=maybe-no-member + del abBuf; + + oDstFile.close(); # pylint: disable=maybe-no-member + + # Done. + return self._resultResponse(constants.tbresp.STATUS_ACK); + + def _actionXmlResults(self): + """ Implement submitting "XML" like test result stream. """ + # + # Parameter validation. + # + sXml = self._getStringParam(constants.tbreq.XML_RESULT_PARAM_BODY, fStrip = False); + self._checkForUnknownParameters(); + if not sXml: # Used for link check by vboxinstaller.py on Windows. + return self._resultResponse(constants.tbresp.STATUS_ACK); + + (oDb, oStatusData, _) = self._connectToDbAndValidateTb([TestBoxStatusData.ksTestBoxState_Testing, + TestBoxStatusData.ksTestBoxState_GangTesting]); + if oStatusData is None: + return False; + + # + # Process the XML. + # + (sError, fUnforgivable) = TestResultLogic(oDb).processXmlStream(sXml, self._idTestSet); + if sError is not None: + oTestSet = TestSetData().initFromDbWithId(oDb, oStatusData.idTestSet); + self.writeToMainLog(oTestSet, '\n!!XML error: %s\n%s\n\n' % (sError, sXml,)); + if fUnforgivable: + return self._resultResponse(constants.tbresp.STATUS_NACK); + return self._resultResponse(constants.tbresp.STATUS_ACK); + + + def _actionExecCompleted(self): + """ + Implement EXEC completion. + + Because the action is request by the worker thread of the testbox + script we cannot pass pending commands back to it like originally + planned. So, we just complete the test set and update the status. + """ + # + # Parameter validation. + # + sStatus = self._getStringParam(constants.tbreq.EXEC_COMPLETED_PARAM_RESULT, TestBoxController.kasValidResults); + self._checkForUnknownParameters(); + + (oDb, oStatusData, _) = self._connectToDbAndValidateTb([TestBoxStatusData.ksTestBoxState_Testing, + TestBoxStatusData.ksTestBoxState_GangTesting]); + if oStatusData is None: + return False; + + # + # Complete the status. + # + oDb.rollback(); + oDb.begin(); + oTestSetLogic = TestSetLogic(oDb); + idTestSetGangLeader = oTestSetLogic.complete(oStatusData.idTestSet, self.kadTbResultToStatus[sStatus], fCommit = False); + + oStatusLogic = TestBoxStatusLogic(oDb); + if oStatusData.enmState == TestBoxStatusData.ksTestBoxState_Testing: + assert idTestSetGangLeader is None; + GlobalResourceLogic(oDb).freeGlobalResourcesByTestBox(self._idTestBox); + oStatusLogic.updateState(self._idTestBox, TestBoxStatusData.ksTestBoxState_Idle, fCommit = False); + else: + assert idTestSetGangLeader is not None; + oStatusLogic.updateState(self._idTestBox, TestBoxStatusData.ksTestBoxState_GangCleanup, oStatusData.idTestSet, + fCommit = False); + if oStatusLogic.isWholeGangDoneTesting(idTestSetGangLeader): + GlobalResourceLogic(oDb).freeGlobalResourcesByTestBox(self._idTestBox); + oStatusLogic.updateState(self._idTestBox, TestBoxStatusData.ksTestBoxState_Idle, fCommit = False); + + oDb.commit(); + return self._resultResponse(constants.tbresp.STATUS_ACK); + + + + def _getStandardParams(self, dParams): + """ + Gets the standard parameters and validates them. + + The parameters are returned as a tuple: sAction, idTestBox, sTestBoxUuid. + Note! the sTextBoxId can be None if it's a SIGNON request. + + Raises TestBoxControllerException on invalid input. + """ + # + # Get the action parameter and validate it. + # + if constants.tbreq.ALL_PARAM_ACTION not in dParams: + raise TestBoxControllerException('No "%s" parameter in request (params: %s)' \ + % (constants.tbreq.ALL_PARAM_ACTION, dParams,)); + sAction = dParams[constants.tbreq.ALL_PARAM_ACTION]; + + if sAction not in self._dActions: + raise TestBoxControllerException('Unknown action "%s" in request (params: %s; action: %s)' \ + % (sAction, dParams, self._dActions)); + + # + # TestBox UUID. + # + if constants.tbreq.ALL_PARAM_TESTBOX_UUID not in dParams: + raise TestBoxControllerException('No "%s" parameter in request (params: %s)' \ + % (constants.tbreq.ALL_PARAM_TESTBOX_UUID, dParams,)); + sTestBoxUuid = dParams[constants.tbreq.ALL_PARAM_TESTBOX_UUID]; + try: + sTestBoxUuid = str(uuid.UUID(sTestBoxUuid)); + except Exception as oXcpt: + raise TestBoxControllerException('Invalid %s parameter value "%s": %s ' \ + % (constants.tbreq.ALL_PARAM_TESTBOX_UUID, sTestBoxUuid, oXcpt)); + if sTestBoxUuid == '00000000-0000-0000-0000-000000000000': + raise TestBoxControllerException('Invalid %s parameter value "%s": NULL UUID not allowed.' \ + % (constants.tbreq.ALL_PARAM_TESTBOX_UUID, sTestBoxUuid)); + + # + # TestBox ID. + # + if constants.tbreq.ALL_PARAM_TESTBOX_ID in dParams: + sTestBoxId = dParams[constants.tbreq.ALL_PARAM_TESTBOX_ID]; + try: + idTestBox = int(sTestBoxId); + if idTestBox <= 0 or idTestBox >= 0x7fffffff: + raise Exception; + except: + raise TestBoxControllerException('Bad value for "%s": "%s"' \ + % (constants.tbreq.ALL_PARAM_TESTBOX_ID, sTestBoxId)); + elif sAction == constants.tbreq.SIGNON: + idTestBox = None; + else: + raise TestBoxControllerException('No "%s" parameter in request (params: %s)' \ + % (constants.tbreq.ALL_PARAM_TESTBOX_ID, dParams,)); + + # + # Test Set ID. + # + if constants.tbreq.RESULT_PARAM_TEST_SET_ID in dParams: + sTestSetId = dParams[constants.tbreq.RESULT_PARAM_TEST_SET_ID]; + try: + idTestSet = int(sTestSetId); + if idTestSet <= 0 or idTestSet >= 0x7fffffff: + raise Exception; + except: + raise TestBoxControllerException('Bad value for "%s": "%s"' \ + % (constants.tbreq.RESULT_PARAM_TEST_SET_ID, sTestSetId)); + elif sAction not in [ constants.tbreq.XML_RESULTS, ]: ## More later. + idTestSet = None; + else: + raise TestBoxControllerException('No "%s" parameter in request (params: %s)' \ + % (constants.tbreq.RESULT_PARAM_TEST_SET_ID, dParams,)); + + # + # The testbox address. + # + sTestBoxAddr = self._oSrvGlue.getClientAddr(); + if sTestBoxAddr is None or sTestBoxAddr.strip() == '': + raise TestBoxControllerException('Invalid client address "%s"' % (sTestBoxAddr,)); + + # + # Update the list of checked parameters. + # + self._asCheckedParams.extend([constants.tbreq.ALL_PARAM_TESTBOX_UUID, constants.tbreq.ALL_PARAM_ACTION]); + if idTestBox is not None: + self._asCheckedParams.append(constants.tbreq.ALL_PARAM_TESTBOX_ID); + if idTestSet is not None: + self._asCheckedParams.append(constants.tbreq.RESULT_PARAM_TEST_SET_ID); + + return (sAction, idTestBox, sTestBoxUuid, sTestBoxAddr, idTestSet); + + def dispatchRequest(self): + """ + Dispatches the incoming request. + + Will raise TestBoxControllerException on failure. + """ + + # + # Must be a POST request. + # + try: + sMethod = self._oSrvGlue.getMethod(); + except Exception as oXcpt: + raise TestBoxControllerException('Error retriving request method: %s' % (oXcpt,)); + if sMethod != 'POST': + raise TestBoxControllerException('Error expected POST request not "%s"' % (sMethod,)); + + # + # Get the parameters and checks for duplicates. + # + try: + dParams = self._oSrvGlue.getParameters(); + except Exception as oXcpt: + raise TestBoxControllerException('Error retriving parameters: %s' % (oXcpt,)); + for sKey in dParams.keys(): + if len(dParams[sKey]) > 1: + raise TestBoxControllerException('Parameter "%s" is given multiple times: %s' % (sKey, dParams[sKey])); + dParams[sKey] = dParams[sKey][0]; + self._dParams = dParams; + + # + # Get+validate the standard action parameters and dispatch the request. + # + (self._sAction, self._idTestBox, self._sTestBoxUuid, self._sTestBoxAddr, self._idTestSet) = \ + self._getStandardParams(dParams); + return self._dActions[self._sAction](); diff --git a/src/VBox/ValidationKit/testmanager/core/testboxstatus.py b/src/VBox/ValidationKit/testmanager/core/testboxstatus.py new file mode 100755 index 00000000..86c6041f --- /dev/null +++ b/src/VBox/ValidationKit/testmanager/core/testboxstatus.py @@ -0,0 +1,317 @@ +# -*- coding: utf-8 -*- +# $Id: testboxstatus.py $ + +""" +Test Manager - TestBoxStatus. +""" + +__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 unittest; + +# Validation Kit imports. +from testmanager.core.base import ModelDataBase, ModelDataBaseTestCase, ModelLogicBase, TMTooManyRows, TMRowNotFound; +from testmanager.core.testbox import TestBoxData; + + +class TestBoxStatusData(ModelDataBase): + """ + TestBoxStatus Data. + """ + + ## @name TestBoxState_T + # @{ + ksTestBoxState_Idle = 'idle'; + ksTestBoxState_Testing = 'testing'; + ksTestBoxState_GangGathering = 'gang-gathering'; + ksTestBoxState_GangGatheringTimedOut = 'gang-gathering-timedout'; + ksTestBoxState_GangTesting = 'gang-testing'; + ksTestBoxState_GangCleanup = 'gang-cleanup'; + ksTestBoxState_Rebooting = 'rebooting'; + ksTestBoxState_Upgrading = 'upgrading'; + ksTestBoxState_UpgradingAndRebooting = 'upgrading-and-rebooting'; + ksTestBoxState_DoingSpecialCmd = 'doing-special-cmd'; + ## @} + + ksParam_idTestBox = 'TestBoxStatus_idTestBox'; + ksParam_idGenTestBox = 'TestBoxStatus_idGenTestBox' + ksParam_tsUpdated = 'TestBoxStatus_tsUpdated'; + ksParam_enmState = 'TestBoxStatus_enmState'; + ksParam_idTestSet = 'TestBoxStatus_idTestSet'; + ksParam_iWorkItem = 'TestBoxStatus_iWorkItem'; + + kasAllowNullAttributes = ['idTestSet', ]; + kasValidValues_enmState = \ + [ + ksTestBoxState_Idle, ksTestBoxState_Testing, ksTestBoxState_GangGathering, + ksTestBoxState_GangGatheringTimedOut, ksTestBoxState_GangTesting, ksTestBoxState_GangCleanup, + ksTestBoxState_Rebooting, ksTestBoxState_Upgrading, ksTestBoxState_UpgradingAndRebooting, + ksTestBoxState_DoingSpecialCmd, + ]; + + kcDbColumns = 6; + + def __init__(self): + ModelDataBase.__init__(self); + + # + # Initialize with defaults. + # See the database for explanations of each of these fields. + # + self.idTestBox = None; + self.idGenTestBox = None; + self.tsUpdated = None; + self.enmState = self.ksTestBoxState_Idle; + self.idTestSet = None; + self.iWorkItem = None; + + def initFromDbRow(self, aoRow): + """ + Internal worker for initFromDbWithId and initFromDbWithGenId as well as + TestBoxStatusLogic. + """ + + if aoRow is None: + raise TMRowNotFound('TestBoxStatus not found.'); + + self.idTestBox = aoRow[0]; + self.idGenTestBox = aoRow[1]; + self.tsUpdated = aoRow[2]; + self.enmState = aoRow[3]; + self.idTestSet = aoRow[4]; + self.iWorkItem = aoRow[5]; + return self; + + def initFromDbWithId(self, oDb, idTestBox): + """ + Initialize the object from the database. + """ + oDb.execute('SELECT *\n' + 'FROM TestBoxStatuses\n' + 'WHERE idTestBox = %s\n' + , (idTestBox, ) ); + return self.initFromDbRow(oDb.fetchOne()); + + def initFromDbWithGenId(self, oDb, idGenTestBox): + """ + Initialize the object from the database. + """ + oDb.execute('SELECT *\n' + 'FROM TestBoxStatuses\n' + 'WHERE idGenTestBox = %s\n' + , (idGenTestBox, ) ); + return self.initFromDbRow(oDb.fetchOne()); + + +class TestBoxStatusLogic(ModelLogicBase): + """ + TestBoxStatus logic. + """ + + ## The number of seconds between each time to call touchStatus() when + # returning CMD_IDLE. + kcSecIdleTouchStatus = 120; + + + def __init__(self, oDb): + ModelLogicBase.__init__(self, oDb); + + + def tryFetchStatus(self, idTestBox): + """ + Attempts to fetch the status of the given testbox. + + Returns a TestBoxStatusData object on success. + Returns None if no status was found. + Raises exception on other errors. + """ + self._oDb.execute('SELECT *\n' + 'FROM TestBoxStatuses\n' + 'WHERE idTestBox = %s\n', + (idTestBox,)); + if self._oDb.getRowCount() == 0: + return None; + oStatus = TestBoxStatusData(); + return oStatus.initFromDbRow(self._oDb.fetchOne()); + + def tryFetchStatusAndConfig(self, idTestBox, sTestBoxUuid, sTestBoxAddr): + """ + Tries to fetch the testbox status and current testbox config. + + Returns (TestBoxStatusData, TestBoxData) on success, (None, None) if + not found. May throw an exception on database error. + """ + self._oDb.execute('SELECT TestBoxStatuses.*,\n' + ' TestBoxesWithStrings.*\n' + 'FROM TestBoxStatuses,\n' + ' TestBoxesWithStrings\n' + 'WHERE TestBoxStatuses.idTestBox = %s\n' + ' AND TestBoxesWithStrings.idTestBox = %s\n' + ' AND TestBoxesWithStrings.tsExpire = \'infinity\'::TIMESTAMP\n' + ' AND TestBoxesWithStrings.uuidSystem = %s\n' + ' AND TestBoxesWithStrings.ip = %s\n' + , ( idTestBox, + idTestBox, + sTestBoxUuid, + sTestBoxAddr,) ); + cRows = self._oDb.getRowCount(); + if cRows != 1: + if cRows != 0: + raise TMTooManyRows('tryFetchStatusForCommandReq got %s rows for idTestBox=%s' % (cRows, idTestBox)); + return (None, None); + aoRow = self._oDb.fetchOne(); + return (TestBoxStatusData().initFromDbRow(aoRow[:TestBoxStatusData.kcDbColumns]), + TestBoxData().initFromDbRow(aoRow[TestBoxStatusData.kcDbColumns:])); + + + def insertIdleStatus(self, idTestBox, idGenTestBox, fCommit = False): + """ + Inserts an idle status for the specified testbox. + """ + self._oDb.execute('INSERT INTO TestBoxStatuses (\n' + ' idTestBox,\n' + ' idGenTestBox,\n' + ' enmState,\n' + ' idTestSet,\n' + ' iWorkItem)\n' + 'VALUES ( %s,\n' + ' %s,\n' + ' \'idle\'::TestBoxState_T,\n' + ' NULL,\n' + ' 0)\n' + , (idTestBox, idGenTestBox) ); + self._oDb.maybeCommit(fCommit); + return True; + + def touchStatus(self, idTestBox, fCommit = False): + """ + Touches the testbox status row, i.e. sets tsUpdated to the current time. + """ + self._oDb.execute('UPDATE TestBoxStatuses\n' + 'SET tsUpdated = CURRENT_TIMESTAMP\n' + 'WHERE idTestBox = %s\n' + , (idTestBox,)); + self._oDb.maybeCommit(fCommit); + return True; + + def updateState(self, idTestBox, sNewState, idTestSet = None, fCommit = False): + """ + Updates the testbox state. + """ + self._oDb.execute('UPDATE TestBoxStatuses\n' + 'SET enmState = %s,\n' + ' idTestSet = %s,\n' + ' tsUpdated = CURRENT_TIMESTAMP\n' + 'WHERE idTestBox = %s\n', + (sNewState, idTestSet, idTestBox)); + self._oDb.maybeCommit(fCommit); + return True; + + def updateGangStatus(self, idTestSetGangLeader, sNewState, fCommit = False): + """ + Update the state of all members of a gang. + """ + self._oDb.execute('UPDATE TestBoxStatuses\n' + 'SET enmState = %s,\n' + ' tsUpdated = CURRENT_TIMESTAMP\n' + 'WHERE idTestBox IN (SELECT idTestBox\n' + ' FROM TestSets\n' + ' WHERE idTestSetGangLeader = %s)\n' + , (sNewState, idTestSetGangLeader,) ); + self._oDb.maybeCommit(fCommit); + return True; + + def updateWorkItem(self, idTestBox, iWorkItem, fCommit = False): + """ + Updates the testbox state. + """ + self._oDb.execute('UPDATE TestBoxStatuses\n' + 'SET iWorkItem = %s\n' + 'WHERE idTestBox = %s\n' + , ( iWorkItem, idTestBox,)); + self._oDb.maybeCommit(fCommit); + return True; + + def isWholeGangDoneTesting(self, idTestSetGangLeader): + """ + Checks if the whole gang is done testing. + """ + self._oDb.execute('SELECT COUNT(*)\n' + 'FROM TestBoxStatuses, TestSets\n' + 'WHERE TestBoxStatuses.idTestSet = TestSets.idTestSet\n' + ' AND TestSets.idTestSetGangLeader = %s\n' + ' AND TestBoxStatuses.enmState IN (%s, %s)\n' + , ( idTestSetGangLeader, + TestBoxStatusData.ksTestBoxState_GangGathering, + TestBoxStatusData.ksTestBoxState_GangTesting)); + return self._oDb.fetchOne()[0] == 0; + + def isTheWholeGangThere(self, idTestSetGangLeader): + """ + Checks if the whole gang is done testing. + """ + self._oDb.execute('SELECT COUNT(*)\n' + 'FROM TestBoxStatuses, TestSets\n' + 'WHERE TestBoxStatuses.idTestSet = TestSets.idTestSet\n' + ' AND TestSets.idTestSetGangLeader = %s\n' + ' AND TestBoxStatuses.enmState IN (%s, %s)\n' + , ( idTestSetGangLeader, + TestBoxStatusData.ksTestBoxState_GangGathering, + TestBoxStatusData.ksTestBoxState_GangTesting)); + return self._oDb.fetchOne()[0] == 0; + + def timeSinceLastChangeInSecs(self, oStatusData): + """ + Figures the time since the last status change. + """ + tsNow = self._oDb.getCurrentTimestamp(); + oDelta = tsNow - oStatusData.tsUpdated; + return oDelta.seconds + oDelta.days * 24 * 3600; + + +# +# Unit testing. +# + +# pylint: disable=missing-docstring +class TestBoxStatusDataTestCase(ModelDataBaseTestCase): + def setUp(self): + self.aoSamples = [TestBoxStatusData(),]; + +if __name__ == '__main__': + unittest.main(); + # not reached. + diff --git a/src/VBox/ValidationKit/testmanager/core/testcase.pgsql b/src/VBox/ValidationKit/testmanager/core/testcase.pgsql new file mode 100644 index 00000000..8d32d1c9 --- /dev/null +++ b/src/VBox/ValidationKit/testmanager/core/testcase.pgsql @@ -0,0 +1,275 @@ +-- $Id: testcase.pgsql $ +--- @file +-- VBox Test Manager Database Stored Procedures - TestCases. +-- + +-- +-- 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 +-- + +\set ON_ERROR_STOP 1 +\connect testmanager; + +DROP FUNCTION IF EXISTS add_testcase(INTEGER, TEXT, TEXT, BOOLEAN, INTEGER, TEXT, TEXT); +DROP FUNCTION IF EXISTS edit_testcase(INTEGER, INTEGER, TEXT, TEXT, BOOLEAN, INTEGER, TEXT, TEXT); +DROP FUNCTION IF EXISTS del_testcase(INTEGER); +DROP FUNCTION IF EXISTS TestCaseLogic_delEntry(INTEGER, INTEGER); +DROP FUNCTION IF EXISTS TestCaseLogic_addEntry(a_uidAuthor INTEGER, a_sName TEXT, a_sDescription TEXT, + a_fEnabled BOOL, a_cSecTimeout INTEGER, a_sTestBoxReqExpr TEXT, + a_sBuildReqExpr TEXT, a_sBaseCmd TEXT, a_sTestSuiteZips TEXT); +DROP FUNCTION IF EXISTS TestCaseLogic_editEntry(a_uidAuthor INTEGER, a_idTestCase INTEGER, a_sName TEXT, a_sDescription TEXT, + a_fEnabled BOOL, a_cSecTimeout INTEGER, a_sTestBoxReqExpr TEXT, + a_sBuildReqExpr TEXT, a_sBaseCmd TEXT, a_sTestSuiteZips TEXT); + +--- +-- Checks if the test case name is unique, ignoring a_idTestCaseIgnore. +-- Raises exception if duplicates are found. +-- +-- @internal +-- +CREATE OR REPLACE FUNCTION TestCaseLogic_checkUniqueName(a_sName TEXT, a_idTestCaseIgnore INTEGER) + RETURNS VOID AS $$ + DECLARE + v_cRows INTEGER; + BEGIN + SELECT COUNT(*) INTO v_cRows + FROM TestCases + WHERE sName = a_sName + AND tsExpire = 'infinity'::TIMESTAMP + AND idTestCase <> a_idTestCaseIgnore; + IF v_cRows <> 0 THEN + RAISE EXCEPTION 'Duplicate test case name "%" (% times)', a_sName, v_cRows; + END IF; + END; +$$ LANGUAGE plpgsql; + +--- +-- Check that the test case exists. +-- Raises exception if it doesn't. +-- +-- @internal +-- +CREATE OR REPLACE FUNCTION TestCaseLogic_checkExists(a_idTestCase INTEGER) RETURNS VOID AS $$ + BEGIN + IF NOT EXISTS( SELECT * + FROM TestCases + WHERE idTestCase = a_idTestCase + AND tsExpire = 'infinity'::TIMESTAMP ) THEN + RAISE EXCEPTION 'Test case with ID % does not currently exist', a_idTestCase; + END IF; + END; +$$ LANGUAGE plpgsql; + + +--- +-- Historize a row. +-- @internal +-- +CREATE OR REPLACE FUNCTION TestCaseLogic_historizeEntry(a_idTestCase INTEGER, a_tsExpire TIMESTAMP WITH TIME ZONE) + RETURNS VOID AS $$ + DECLARE + v_cUpdatedRows INTEGER; + BEGIN + UPDATE TestCases + SET tsExpire = a_tsExpire + WHERE idTestcase = a_idTestCase + AND tsExpire = 'infinity'::TIMESTAMP; + GET DIAGNOSTICS v_cUpdatedRows = ROW_COUNT; + IF v_cUpdatedRows <> 1 THEN + IF v_cUpdatedRows = 0 THEN + RAISE EXCEPTION 'Test case ID % does not currently exist', a_idTestCase; + END IF; + RAISE EXCEPTION 'Integrity error in TestCases: % current rows with idTestCase=%d', v_cUpdatedRows, a_idTestCase; + END IF; + END; +$$ LANGUAGE plpgsql; + + +CREATE OR REPLACE function TestCaseLogic_addEntry(a_uidAuthor INTEGER, a_sName TEXT, a_sDescription TEXT, + a_fEnabled BOOL, a_cSecTimeout INTEGER, a_sTestBoxReqExpr TEXT, + a_sBuildReqExpr TEXT, a_sBaseCmd TEXT, a_sTestSuiteZips TEXT, + a_sComment TEXT) + RETURNS INTEGER AS $$ + DECLARE + v_idTestCase INTEGER; + BEGIN + PERFORM TestCaseLogic_checkUniqueName(a_sName, -1); + + INSERT INTO TestCases (uidAuthor, sName, sDescription, fEnabled, cSecTimeout, + sTestBoxReqExpr, sBuildReqExpr, sBaseCmd, sTestSuiteZips, sComment) + VALUES (a_uidAuthor, a_sName, a_sDescription, a_fEnabled, a_cSecTimeout, + a_sTestBoxReqExpr, a_sBuildReqExpr, a_sBaseCmd, a_sTestSuiteZips, a_sComment) + RETURNING idTestcase INTO v_idTestCase; + RETURN v_idTestCase; + END; +$$ LANGUAGE plpgsql; + + +CREATE OR REPLACE function TestCaseLogic_editEntry(a_uidAuthor INTEGER, a_idTestCase INTEGER, a_sName TEXT, a_sDescription TEXT, + a_fEnabled BOOL, a_cSecTimeout INTEGER, a_sTestBoxReqExpr TEXT, + a_sBuildReqExpr TEXT, a_sBaseCmd TEXT, a_sTestSuiteZips TEXT, + a_sComment TEXT) + RETURNS INTEGER AS $$ + DECLARE + v_idGenTestCase INTEGER; + BEGIN + PERFORM TestCaseLogic_checkExists(a_idTestCase); + PERFORM TestCaseLogic_checkUniqueName(a_sName, a_idTestCase); + + PERFORM TestCaseLogic_historizeEntry(a_idTestCase, CURRENT_TIMESTAMP); + INSERT INTO TestCases (idTestCase, uidAuthor, sName, sDescription, fEnabled, cSecTimeout, + sTestBoxReqExpr, sBuildReqExpr, sBaseCmd, sTestSuiteZips, sComment) + VALUES (a_idTestCase, a_uidAuthor, a_sName, a_sDescription, a_fEnabled, a_cSecTimeout, + a_sTestBoxReqExpr, a_sBuildReqExpr, a_sBaseCmd, a_sTestSuiteZips, a_sComment) + RETURNING idGenTestCase INTO v_idGenTestCase; + RETURN v_idGenTestCase; + END; +$$ LANGUAGE plpgsql; + + +CREATE OR REPLACE FUNCTION TestCaseLogic_delEntry(a_uidAuthor INTEGER, a_idTestCase INTEGER, a_fCascade BOOLEAN) + RETURNS VOID AS $$ + DECLARE + v_Row TestCases%ROWTYPE; + v_tsEffective TIMESTAMP WITH TIME ZONE; + v_Rec RECORD; + v_sErrors TEXT; + BEGIN + -- + -- Check preconditions. + -- + IF a_fCascade <> TRUE THEN + IF EXISTS( SELECT * + FROM TestCaseDeps + WHERE idTestCasePreReq = a_idTestCase + AND tsExpire = 'infinity'::TIMESTAMP ) THEN + v_sErrors := ''; + FOR v_Rec IN + SELECT TestCases.idTestCase AS idTestCase, + TestCases.sName AS sName + FROM TestCaseDeps, TestCases + WHERE TestCaseDeps.idTestCasePreReq = a_idTestCase + AND TestCaseDeps.tsExpire = 'infinity'::TIMESTAMP + AND TestCases.idTestCase = TestCaseDeps.idTestCase + AND TestCases.tsExpire = 'infinity'::TIMESTAMP + LOOP + IF v_sErrors <> '' THEN + v_sErrors := v_sErrors || ', '; + END IF; + v_sErrors := v_sErrors || v_Rec.sName || ' (idTestCase=' || v_Rec.idTestCase || ')'; + END LOOP; + RAISE EXCEPTION 'Other test cases depends on test case with ID %: % ', a_idTestCase, v_sErrors; + END IF; + + IF EXISTS( SELECT * + FROM TestGroupMembers + WHERE idTestCase = a_idTestCase + AND tsExpire = 'infinity'::TIMESTAMP ) THEN + v_sErrors := ''; + FOR v_Rec IN + SELECT TestGroups.idTestGroup AS idTestGroup, + TestGroups.sName AS sName + FROM TestGroupMembers, TestGroups + WHERE TestGroupMembers.idTestCase = a_idTestCase + AND TestGroupMembers.tsExpire = 'infinity'::TIMESTAMP + AND TestGroupMembers.idTestGroup = TestGroups.idTestGroup + AND TestGroups.tsExpire = 'infinity'::TIMESTAMP + LOOP + IF v_sErrors <> '' THEN + v_sErrors := v_sErrors || ', '; + END IF; + v_sErrors := v_sErrors || v_Rec.sName || ' (idTestGroup=' || v_Rec.idTestGroup || ')'; + END LOOP; + RAISE EXCEPTION 'Test case with ID % is member of the following test group(s): % ', a_idTestCase, v_sErrors; + END IF; + END IF; + + -- + -- To preserve the information about who deleted the record, we try to + -- add a dummy record which expires immediately. I say try because of + -- the primary key, we must let the new record be valid for 1 us. :-( + -- + SELECT * INTO STRICT v_Row + FROM TestCases + WHERE idTestCase = a_idTestCase + AND tsExpire = 'infinity'::TIMESTAMP; + + v_tsEffective := CURRENT_TIMESTAMP - INTERVAL '1 microsecond'; + IF v_Row.tsEffective < v_tsEffective THEN + PERFORM TestCaseLogic_historizeEntry(a_idTestCase, v_tsEffective); + v_Row.tsEffective := v_tsEffective; + v_Row.tsExpire := CURRENT_TIMESTAMP; + v_Row.uidAuthor := a_uidAuthor; + SELECT NEXTVAL('TestCaseGenIdSeq') INTO v_Row.idGenTestCase; + INSERT INTO TestCases VALUES (v_Row.*); + ELSE + PERFORM TestCaseLogic_historizeEntry(a_idTestCase, CURRENT_TIMESTAMP); + END IF; + + -- + -- Delete arguments, test case dependencies and resource dependencies. + -- (We don't bother recording who deleted the records here since it's + -- a lot of work and sufficiently covered in the TestCases table.) + -- + UPDATE TestCaseArgs + SET tsExpire = CURRENT_TIMESTAMP + WHERE idTestCase = a_idTestCase + AND tsExpire = 'infinity'::TIMESTAMP; + + UPDATE TestCaseDeps + SET tsExpire = CURRENT_TIMESTAMP + WHERE idTestCase = a_idTestCase + AND tsExpire = 'infinity'::TIMESTAMP; + + UPDATE TestCaseGlobalRsrcDeps + SET tsExpire = CURRENT_TIMESTAMP + WHERE idTestCase = a_idTestCase + AND tsExpire = 'infinity'::TIMESTAMP; + + IF a_fCascade = TRUE THEN + UPDATE TestCaseDeps + SET tsExpire = CURRENT_TIMESTAMP + WHERE idTestCasePreReq = a_idTestCase + AND tsExpire = 'infinity'::TIMESTAMP; + + UPDATE TestGroupMembers + SET tsExpire = CURRENT_TIMESTAMP + WHERE idTestCase = a_idTestCase + AND tsExpire = 'infinity'::TIMESTAMP; + END IF; + + EXCEPTION + WHEN NO_DATA_FOUND THEN + RAISE EXCEPTION 'Test case with ID % does not currently exist', a_idTestCase; + WHEN TOO_MANY_ROWS THEN + RAISE EXCEPTION 'Integrity error in TestCases: Too many current rows for %', a_idTestCase; + END; +$$ LANGUAGE plpgsql; + diff --git a/src/VBox/ValidationKit/testmanager/core/testcase.py b/src/VBox/ValidationKit/testmanager/core/testcase.py new file mode 100755 index 00000000..b2820ff2 --- /dev/null +++ b/src/VBox/ValidationKit/testmanager/core/testcase.py @@ -0,0 +1,1467 @@ +# -*- coding: utf-8 -*- +# $Id: testcase.py $ +# pylint: disable=too-many-lines + +""" +Test Manager - Test Case. +""" + +__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 copy; +import sys; +import unittest; + +# Validation Kit imports. +from common import utils; +from testmanager.core.base import ModelDataBase, ModelDataBaseTestCase, ModelLogicBase, TMExceptionBase, \ + TMInvalidData, TMRowNotFound, ChangeLogEntry, AttributeChangeEntry; +from testmanager.core.globalresource import GlobalResourceData; +from testmanager.core.useraccount import UserAccountLogic; + +# Python 3 hacks: +if sys.version_info[0] >= 3: + long = int; # pylint: disable=redefined-builtin,invalid-name + + + +class TestCaseGlobalRsrcDepData(ModelDataBase): + """ + Test case dependency on a global resource - data. + """ + + ksParam_idTestCase = 'TestCaseDependency_idTestCase'; + ksParam_idGlobalRsrc = 'TestCaseDependency_idGlobalRsrc'; + ksParam_tsEffective = 'TestCaseDependency_tsEffective'; + ksParam_tsExpire = 'TestCaseDependency_tsExpire'; + ksParam_uidAuthor = 'TestCaseDependency_uidAuthor'; + + kasAllowNullAttributes = ['idTestSet', ]; + + def __init__(self): + ModelDataBase.__init__(self); + + # + # Initialize with defaults. + # See the database for explanations of each of these fields. + # + self.idTestCase = None; + self.idGlobalRsrc = None; + self.tsEffective = None; + self.tsExpire = None; + self.uidAuthor = None; + + def initFromDbRow(self, aoRow): + """ + Reinitialize from a SELECT * FROM TestCaseDeps row. + """ + if aoRow is None: + raise TMRowNotFound('Test case not found.'); + + self.idTestCase = aoRow[0]; + self.idGlobalRsrc = aoRow[1]; + self.tsEffective = aoRow[2]; + self.tsExpire = aoRow[3]; + self.uidAuthor = aoRow[4]; + return self; + + +class TestCaseGlobalRsrcDepLogic(ModelLogicBase): + """ + Test case dependency on a global resources - logic. + """ + + def getTestCaseDeps(self, idTestCase, tsNow = None): + """ + 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: + self._oDb.execute('SELECT *\n' + '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' + , (idTestCase, tsNow, tsNow, tsNow, tsNow) ); + else: + self._oDb.execute('SELECT *\n' + '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,)) + aaoRows = self._oDb.fetchAll(); + aoRet = [] + for aoRow in aaoRows: + oItem = [TestCaseDependencyData().initFromDbRow(aoRow), + GlobalResourceData().initFromDbRow(aoRow[5:])]; + aoRet.append(oItem); + + return aoRet + + def getTestCaseDepsIds(self, idTestCase, tsNow = None): + """ + 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: + self._oDb.execute('SELECT idGlobalRsrc\n' + 'FROM TestCaseGlobalRsrcDeps\n' + 'WHERE TestCaseGlobalRsrcDeps.idTestCase = %s\n' + ' AND TestCaseGlobalRsrcDeps.tsExpire > %s\n' + ' AND TestCaseGlobalRsrcDeps.tsEffective <= %s\n' + , (idTestCase, tsNow, tsNow, ) ); + else: + self._oDb.execute('SELECT idGlobalRsrc\n' + 'FROM TestCaseGlobalRsrcDeps\n' + 'WHERE TestCaseGlobalRsrcDeps.idTestCase = %s\n' + ' AND TestCaseGlobalRsrcDeps.tsExpire = \'infinity\'::TIMESTAMP\n' + , (idTestCase,)) + aidGlobalRsrcs = [] + for aoRow in self._oDb.fetchAll(): + aidGlobalRsrcs.append(aoRow[0]); + return aidGlobalRsrcs; + + + def getDepGlobalResourceData(self, idTestCase, tsNow = None): + """ + Returns an array of objects of type GlobalResourceData on which the + specified test case depends on. + """ + if tsNow is None : + self._oDb.execute('SELECT GlobalResources.*\n' + '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: + self._oDb.execute('SELECT GlobalResources.*\n' + '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' + , (idTestCase, tsNow, tsNow, tsNow, tsNow)); + + aaoRows = self._oDb.fetchAll() + aoRet = [] + for aoRow in aaoRows: + aoRet.append(GlobalResourceData().initFromDbRow(aoRow)); + + return aoRet + + +class TestCaseDependencyData(ModelDataBase): + """ + Test case dependency data + """ + + ksParam_idTestCase = 'TestCaseDependency_idTestCase'; + ksParam_idTestCasePreReq = 'TestCaseDependency_idTestCasePreReq'; + ksParam_tsEffective = 'TestCaseDependency_tsEffective'; + ksParam_tsExpire = 'TestCaseDependency_tsExpire'; + ksParam_uidAuthor = 'TestCaseDependency_uidAuthor'; + + + def __init__(self): + ModelDataBase.__init__(self); + + # + # Initialize with defaults. + # See the database for explanations of each of these fields. + # + self.idTestCase = None; + self.idTestCasePreReq = None; + self.tsEffective = None; + self.tsExpire = None; + self.uidAuthor = None; + + def initFromDbRow(self, aoRow): + """ + Reinitialize from a SELECT * FROM TestCaseDeps row. + """ + if aoRow is None: + raise TMRowNotFound('Test case not found.'); + + self.idTestCase = aoRow[0]; + self.idTestCasePreReq = aoRow[1]; + self.tsEffective = aoRow[2]; + self.tsExpire = aoRow[3]; + self.uidAuthor = aoRow[4]; + return self; + + def initFromParams(self, oDisp, fStrict=True): + """ + 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.convertToParamNull(); + fn = oDisp.getStringParam; # Shorter... + + self.idTestCase = fn(self.ksParam_idTestCase, None, None if fStrict else self.idTestCase); + self.idTestCasePreReq = fn(self.ksParam_idTestCasePreReq, None, None if fStrict else self.idTestCasePreReq); + self.tsEffective = fn(self.ksParam_tsEffective, None, None if fStrict else self.tsEffective); + self.tsExpire = fn(self.ksParam_tsExpire, None, None if fStrict else self.tsExpire); + self.uidAuthor = fn(self.ksParam_uidAuthor, None, None if fStrict else self.uidAuthor); + + return True + + def validateAndConvert(self, oDb = None, enmValidateFor = ModelDataBase.ksValidateFor_Other): + """ + 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_*. + """ + dErrors = {} + + self.idTestCase = self._validateInt( dErrors, self.ksParam_idTestCase, self.idTestCase); + self.idTestCasePreReq = self._validateInt( dErrors, self.ksParam_idTestCasePreReq, self.idTestCasePreReq); + self.tsEffective = self._validateTs( dErrors, self.ksParam_tsEffective, self.tsEffective); + self.tsExpire = self._validateTs( dErrors, self.ksParam_tsExpire, self.tsExpire); + self.uidAuthor = self._validateInt( dErrors, self.ksParam_uidAuthor, self.uidAuthor); + + _ = oDb; + _ = enmValidateFor; + return dErrors + + def convertFromParamNull(self): + """ + Converts from parameter NULL values to database NULL values (None). + """ + if self.idTestCase in [-1, '']: self.idTestCase = None; + if self.idTestCasePreReq in [-1, '']: self.idTestCasePreReq = None; + if self.tsEffective == '': self.tsEffective = None; + if self.tsExpire == '': self.tsExpire = None; + if self.uidAuthor in [-1, '']: self.uidAuthor = None; + return True; + + def convertToParamNull(self): + """ + Converts from database NULL values (None) to special values we can + pass thru parameters list. + """ + if self.idTestCase is None: self.idTestCase = -1; + if self.idTestCasePreReq is None: self.idTestCasePreReq = -1; + if self.tsEffective is None: self.tsEffective = ''; + if self.tsExpire is None: self.tsExpire = ''; + if self.uidAuthor is None: self.uidAuthor = -1; + return True; + + def isEqual(self, oOther): + """ Compares two instances. """ + return self.idTestCase == oOther.idTestCase \ + and self.idTestCasePreReq == oOther.idTestCasePreReq \ + and self.tsEffective == oOther.tsEffective \ + and self.tsExpire == oOther.tsExpire \ + and self.uidAuthor == oOther.uidAuthor; + + def getTestCasePreReqIds(self, aTestCaseDependencyData): + """ + Get list of Test Case IDs which current + Test Case depends on + """ + if not aTestCaseDependencyData: + return [] + + aoRet = [] + for oTestCaseDependencyData in aTestCaseDependencyData: + aoRet.append(oTestCaseDependencyData.idTestCasePreReq) + + return aoRet + +class TestCaseDependencyLogic(ModelLogicBase): + """Test case dependency management logic""" + + def getTestCaseDeps(self, idTestCase, tsEffective = None): + """ + 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: + self._oDb.execute('SELECT *\n' + 'FROM TestCaseDeps\n' + 'WHERE idTestCase = %s\n' + ' AND tsExpire > %s\n' + ' AND tsEffective <= %s\n' + , (idTestCase, tsEffective, tsEffective, ) ); + else: + self._oDb.execute('SELECT *\n' + 'FROM TestCaseDeps\n' + 'WHERE idTestCase = %s\n' + ' AND tsExpire = \'infinity\'::TIMESTAMP\n' + , (idTestCase, ) ); + aaoRows = self._oDb.fetchAll(); + aoRet = []; + for aoRow in aaoRows: + aoRet.append(TestCaseDependencyData().initFromDbRow(aoRow)); + + return aoRet + + def getTestCaseDepsIds(self, idTestCase, tsNow = None): + """ + 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: + self._oDb.execute('SELECT idTestCase\n' + 'FROM TestCaseDeps\n' + 'WHERE idTestCase = %s\n' + ' AND tsExpire > %s\n' + ' AND tsEffective <= %s\n' + , (idTestCase, tsNow, tsNow, ) ); + else: + self._oDb.execute('SELECT idTestCase\n' + 'FROM TestCaseDeps\n' + 'WHERE idTestCase = %s\n' + ' AND tsExpire = \'infinity\'::TIMESTAMP\n' + , (idTestCase, ) ); + aidPreReqs = []; + for aoRow in self._oDb.fetchAll(): + aidPreReqs.append(aoRow[0]); + return aidPreReqs; + + + def getDepTestCaseData(self, idTestCase, tsNow = None): + """ + Returns an array of objects of type TestCaseData2 on which + specified test case depends on + """ + if tsNow is None: + self._oDb.execute('SELECT TestCases.*\n' + '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: + self._oDb.execute('SELECT TestCases.*\n' + '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' + , (idTestCase, tsNow, tsNow, tsNow, tsNow, ) ); + + aaoRows = self._oDb.fetchAll() + aoRet = [] + for aoRow in aaoRows: + aoRet.append(TestCaseData().initFromDbRow(aoRow)); + + return aoRet + + def getApplicableDepTestCaseData(self, idTestCase): + """ + 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) + """ + self._oDb.execute('SELECT *\n' + '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) ) + + aaoRows = self._oDb.fetchAll() + aoRet = [] + for aoRow in aaoRows: + aoRet.append(TestCaseData().initFromDbRow(aoRow)); + + return aoRet + +class TestCaseData(ModelDataBase): + """ + Test case data + """ + + ksIdAttr = 'idTestCase'; + ksIdGenAttr = 'idGenTestCase'; + + ksParam_idTestCase = 'TestCase_idTestCase' + ksParam_tsEffective = 'TestCase_tsEffective' + ksParam_tsExpire = 'TestCase_tsExpire' + ksParam_uidAuthor = 'TestCase_uidAuthor' + ksParam_idGenTestCase = 'TestCase_idGenTestCase' + ksParam_sName = 'TestCase_sName' + ksParam_sDescription = 'TestCase_sDescription' + ksParam_fEnabled = 'TestCase_fEnabled' + ksParam_cSecTimeout = 'TestCase_cSecTimeout' + ksParam_sTestBoxReqExpr = 'TestCase_sTestBoxReqExpr'; + ksParam_sBuildReqExpr = 'TestCase_sBuildReqExpr'; + ksParam_sBaseCmd = 'TestCase_sBaseCmd' + ksParam_sValidationKitZips = 'TestCase_sValidationKitZips' + ksParam_sComment = 'TestCase_sComment' + + kasAllowNullAttributes = [ 'idTestCase', 'tsEffective', 'tsExpire', 'uidAuthor', 'idGenTestCase', 'sDescription', + 'sTestBoxReqExpr', 'sBuildReqExpr', 'sValidationKitZips', 'sComment' ]; + + kcDbColumns = 14; + + def __init__(self): + ModelDataBase.__init__(self); + + # + # Initialize with defaults. + # See the database for explanations of each of these fields. + # + self.idTestCase = None; + self.tsEffective = None; + self.tsExpire = None; + self.uidAuthor = None; + self.idGenTestCase = None; + self.sName = None; + self.sDescription = None; + self.fEnabled = False; + self.cSecTimeout = 10; # Init with minimum timeout value + self.sTestBoxReqExpr = None; + self.sBuildReqExpr = None; + self.sBaseCmd = None; + self.sValidationKitZips = None; + self.sComment = None; + + def initFromDbRow(self, aoRow): + """ + Reinitialize from a SELECT * FROM TestCases row. + Returns self. Raises exception if no row. + """ + if aoRow is None: + raise TMRowNotFound('Test case not found.'); + + self.idTestCase = aoRow[0]; + self.tsEffective = aoRow[1]; + self.tsExpire = aoRow[2]; + self.uidAuthor = aoRow[3]; + self.idGenTestCase = aoRow[4]; + self.sName = aoRow[5]; + self.sDescription = aoRow[6]; + self.fEnabled = aoRow[7]; + self.cSecTimeout = aoRow[8]; + self.sTestBoxReqExpr = aoRow[9]; + self.sBuildReqExpr = aoRow[10]; + self.sBaseCmd = aoRow[11]; + self.sValidationKitZips = aoRow[12]; + self.sComment = aoRow[13]; + return self; + + def initFromDbWithId(self, oDb, idTestCase, tsNow = None, sPeriodBack = None): + """ + Initialize the object from the database. + """ + oDb.execute(self.formatSimpleNowAndPeriodQuery(oDb, + 'SELECT *\n' + 'FROM TestCases\n' + 'WHERE idTestCase = %s\n' + , ( idTestCase,), tsNow, sPeriodBack)); + aoRow = oDb.fetchOne() + if aoRow is None: + raise TMRowNotFound('idTestCase=%s not found (tsNow=%s sPeriodBack=%s)' % (idTestCase, tsNow, sPeriodBack,)); + return self.initFromDbRow(aoRow); + + def initFromDbWithGenId(self, oDb, idGenTestCase, tsNow = None): + """ + Initialize the object from the database. + """ + _ = tsNow; # For relevant for the TestCaseDataEx version only. + oDb.execute('SELECT *\n' + 'FROM TestCases\n' + 'WHERE idGenTestCase = %s\n' + , (idGenTestCase, ) ); + return self.initFromDbRow(oDb.fetchOne()); + + def _validateAndConvertAttribute(self, sAttr, sParam, oValue, aoNilValues, fAllowNull, oDb): + if sAttr == 'cSecTimeout' and oValue not in aoNilValues: # Allow human readable interval formats. + return utils.parseIntervalSeconds(oValue); + + (oValue, sError) = ModelDataBase._validateAndConvertAttribute(self, sAttr, sParam, oValue, aoNilValues, fAllowNull, oDb); + if sError is None: + if sAttr == 'sTestBoxReqExpr': + sError = TestCaseData.validateTestBoxReqExpr(oValue); + elif sAttr == 'sBuildReqExpr': + sError = TestCaseData.validateBuildReqExpr(oValue); + elif sAttr == 'sBaseCmd': + _, sError = TestCaseData.validateStr(oValue, fAllowUnicodeSymbols=False); + return (oValue, sError); + + + # + # Misc. + # + + def needValidationKitBit(self): + """ + Predicate method for checking whether a validation kit build is required. + """ + return self.sValidationKitZips is None \ + or self.sValidationKitZips.find('@VALIDATIONKIT_ZIP@') >= 0; + + def matchesTestBoxProps(self, oTestBoxData): + """ + 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. + """ + return TestCaseData.matchesTestBoxPropsEx(oTestBoxData, self.sTestBoxReqExpr); + + def matchesBuildProps(self, oBuildDataEx): + """ + 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. + """ + return TestCaseData.matchesBuildPropsEx(oBuildDataEx, self.sBuildReqExpr); + + + # + # Expression validation code shared with TestCaseArgsDataEx. + # + @staticmethod + def _safelyEvalExpr(sExpr, dLocals, fMayRaiseXcpt = False): + """ + 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. + """ + if sExpr is None or sExpr == '': + 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, + 'versionCompare': utils.versionCompare, + }; + + try: + fRc = eval(sExpr, dGlobals, dLocals); + except: + if fMayRaiseXcpt: + raise; + return None; + + if not isinstance(fRc, bool): + if fMayRaiseXcpt: + raise Exception('not a boolean result: "%s" - %s' % (fRc, type(fRc)) ); + return None; + + return fRc; + + @staticmethod + def _safelyValidateReqExpr(sExpr, adLocals): + """ + Validates a requirement expression using the given sets of locals, + returning None on success and an error string on failure. + """ + for dLocals in adLocals: + try: + TestCaseData._safelyEvalExpr(sExpr, dLocals, True); + except Exception as oXcpt: + return str(oXcpt); + return None; + + @staticmethod + def validateTestBoxReqExpr(sExpr): + """ + 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, + 'fRawMode': 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, + 'fRawMode': True, + 'cMbMemory': 9999999999, + 'cMbScratch': 9999999999999, + 'iTestBoxScriptRev': 9999999, + 'sName': 'emanon', + 'uuidSystem': '00000000-0000-0000-0000-000000000000', + }, + ]; + return TestCaseData._safelyValidateReqExpr(sExpr, adTestBoxes); + + @staticmethod + def matchesTestBoxPropsEx(oTestBoxData, sExpr): + """ Worker for TestCaseData.matchesTestBoxProps and TestCaseArgsDataEx.matchesTestBoxProps. """ + if sExpr is None: + return True; + dLocals = \ + { + 'sOs': oTestBoxData.sOs, + 'sOsVersion': oTestBoxData.sOsVersion, + 'sCpuVendor': oTestBoxData.sCpuVendor, + 'sCpuArch': oTestBoxData.sCpuArch, + 'iCpuFamily': oTestBoxData.getCpuFamily(), + 'iCpuModel': oTestBoxData.getCpuModel(), + 'cCpus': oTestBoxData.cCpus, + 'fCpuHwVirt': oTestBoxData.fCpuHwVirt, + 'fCpuNestedPaging': oTestBoxData.fCpuNestedPaging, + 'fCpu64BitGuest': oTestBoxData.fCpu64BitGuest, + 'fChipsetIoMmu': oTestBoxData.fChipsetIoMmu, + 'fRawMode': oTestBoxData.fRawMode, + 'cMbMemory': oTestBoxData.cMbMemory, + 'cMbScratch': oTestBoxData.cMbScratch, + 'iTestBoxScriptRev': oTestBoxData.iTestBoxScriptRev, + 'iPythonHexVersion': oTestBoxData.iPythonHexVersion, + 'sName': oTestBoxData.sName, + 'uuidSystem': oTestBoxData.uuidSystem, + }; + return TestCaseData._safelyEvalExpr(sExpr, dLocals); + + @staticmethod + def validateBuildReqExpr(sExpr): + """ + 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, + }, + ]; + return TestCaseData._safelyValidateReqExpr(sExpr, adBuilds); + + @staticmethod + def matchesBuildPropsEx(oBuildDataEx, sExpr): + """ + Checks if the all of the build related test requirements matches the + given build. + """ + if sExpr is None: + return True; + dLocals = \ + { + 'sProduct': oBuildDataEx.oCat.sProduct, + 'sBranch': oBuildDataEx.oCat.sBranch, + 'sType': oBuildDataEx.oCat.sType, + 'asOsArches': oBuildDataEx.oCat.asOsArches, + 'sVersion': oBuildDataEx.sVersion, + 'iRevision': oBuildDataEx.iRevision, + 'uidAuthor': oBuildDataEx.uidAuthor, + 'idBuild': oBuildDataEx.idBuild, + }; + return TestCaseData._safelyEvalExpr(sExpr, dLocals); + + + + +class TestCaseDataEx(TestCaseData): + """ + Test case data. + """ + + ksParam_aoTestCaseArgs = 'TestCase_aoTestCaseArgs'; + ksParam_aoDepTestCases = 'TestCase_aoDepTestCases'; + ksParam_aoDepGlobalResources = 'TestCase_aoDepGlobalResources'; + + # Use [] instead of None. + kasAltArrayNull = [ 'aoTestCaseArgs', 'aoDepTestCases', 'aoDepGlobalResources' ]; + + + def __init__(self): + TestCaseData.__init__(self); + + # 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 = []; + + def _initExtraMembersFromDb(self, oDb, tsNow = None, sPeriodBack = None): + """ + Worker shared by the initFromDb* methods. + Returns self. Raises exception if no row or database error. + """ + _ = sPeriodBack; ## @todo sPeriodBack + from testmanager.core.testcaseargs import TestCaseArgsLogic; + self.aoDepTestCases = TestCaseDependencyLogic(oDb).getDepTestCaseData(self.idTestCase, tsNow); + self.aoDepGlobalResources = TestCaseGlobalRsrcDepLogic(oDb).getDepGlobalResourceData(self.idTestCase, tsNow); + self.aoTestCaseArgs = TestCaseArgsLogic(oDb).getTestCaseArgs(self.idTestCase, tsNow); + # Note! The above arrays are sorted by their relvant IDs for fetchForChangeLog's sake. + return self; + + def initFromDbRowEx(self, aoRow, oDb, tsNow = None): + """ + 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. + """ + TestCaseData.initFromDbRow(self, aoRow); + return self._initExtraMembersFromDb(oDb, tsNow); + + def initFromDbWithId(self, oDb, idTestCase, tsNow = None, sPeriodBack = None): + """ + Initialize the object from the database. + """ + TestCaseData.initFromDbWithId(self, oDb, idTestCase, tsNow, sPeriodBack); + return self._initExtraMembersFromDb(oDb, tsNow, sPeriodBack); + + def initFromDbWithGenId(self, oDb, idGenTestCase, tsNow = None): + """ + Initialize the object from the database. + """ + TestCaseData.initFromDbWithGenId(self, oDb, idGenTestCase); + if tsNow is None and not oDb.isTsInfinity(self.tsExpire): + tsNow = self.tsEffective; + return self._initExtraMembersFromDb(oDb, tsNow); + + def getAttributeParamNullValues(self, sAttr): + if sAttr in ['aoDepTestCases', 'aoDepGlobalResources', 'aoTestCaseArgs']: + return [[], '']; + return TestCaseData.getAttributeParamNullValues(self, sAttr); + + def convertParamToAttribute(self, sAttr, sParam, oValue, oDisp, fStrict): + """For dealing with the arrays.""" + if sAttr not in ['aoDepTestCases', 'aoDepGlobalResources', 'aoTestCaseArgs']: + return TestCaseData.convertParamToAttribute(self, sAttr, sParam, oValue, oDisp, fStrict); + + aoNewValues = []; + if sAttr == 'aoDepTestCases': + for idTestCase in oDisp.getListOfIntParams(sParam, 1, 0x7ffffffe, []): + oDep = TestCaseData(); + oDep.idTestCase = str(idTestCase); + aoNewValues.append(oDep); + + elif sAttr == 'aoDepGlobalResources': + for idGlobalRsrc in oDisp.getListOfIntParams(sParam, 1, 0x7ffffffe, []): + oGlobalRsrc = GlobalResourceData(); + oGlobalRsrc.idGlobalRsrc = str(idGlobalRsrc); + aoNewValues.append(oGlobalRsrc); + + elif sAttr == 'aoTestCaseArgs': + from testmanager.core.testcaseargs import TestCaseArgsData; + for sArgKey in oDisp.getStringParam(TestCaseDataEx.ksParam_aoTestCaseArgs, sDefault = '').split(','): + oDispWrapper = self.DispWrapper(oDisp, '%s[%s][%%s]' % (TestCaseDataEx.ksParam_aoTestCaseArgs, sArgKey,)) + aoNewValues.append(TestCaseArgsData().initFromParams(oDispWrapper, fStrict = False)); + return aoNewValues; + + def _validateAndConvertAttribute(self, sAttr, sParam, oValue, aoNilValues, fAllowNull, oDb): # pylint: disable=too-many-locals + """ + 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. + """ + if sAttr not in ['aoDepTestCases', 'aoDepGlobalResources', 'aoTestCaseArgs']: + return TestCaseData._validateAndConvertAttribute(self, sAttr, sParam, oValue, aoNilValues, fAllowNull, oDb); + + asErrors = []; + aoNewValues = []; + if sAttr == 'aoDepTestCases': + for oTestCase in self.aoDepTestCases: + if utils.isString(oTestCase.idTestCase): # Stored as string convertParamToAttribute. + oTestCase = copy.copy(oTestCase); + try: + oTestCase.idTestCase = int(oTestCase.idTestCase); + oTestCase.initFromDbWithId(oDb, oTestCase.idTestCase); + except Exception as oXcpt: + asErrors.append('Test case dependency #%s: %s' % (oTestCase.idTestCase, oXcpt)); + aoNewValues.append(oTestCase); + + elif sAttr == 'aoDepGlobalResources': + for oGlobalRsrc in self.aoDepGlobalResources: + if utils.isString(oGlobalRsrc.idGlobalRsrc): # Stored as string convertParamToAttribute. + oGlobalRsrc = copy.copy(oGlobalRsrc); + try: + oGlobalRsrc.idTestCase = int(oGlobalRsrc.idGlobalRsrc); + oGlobalRsrc.initFromDbWithId(oDb, oGlobalRsrc.idGlobalRsrc); + except Exception as oXcpt: + asErrors.append('Resource dependency #%s: %s' % (oGlobalRsrc.idGlobalRsrc, oXcpt)); + aoNewValues.append(oGlobalRsrc); + + else: + assert sAttr == 'aoTestCaseArgs'; + if not self.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 = {}; + + for iVar, oVar in enumerate(self.aoTestCaseArgs): + oVar = copy.copy(oVar); + oVar.idTestCase = self.idTestCase; + dCurErrors = oVar.validateAndConvert(oDb, ModelDataBase.ksValidateFor_Other); + if not dCurErrors: + pass; ## @todo figure out the ID? + else: + asErrors = []; + for sKey in dCurErrors: + asErrors.append('%s: %s' % (sKey[len('TestCaseArgs_'):], dCurErrors[sKey])); + dErrors[iVar] = '<br>\n'.join(asErrors) + aoNewValues.append(oVar); + + for iVar, oVar in enumerate(self.aoTestCaseArgs): + sArgs = oVar.sArgs; + for iVar2 in range(iVar + 1, len(self.aoTestCaseArgs)): + if self.aoTestCaseArgs[iVar2].sArgs == sArgs: + sMsg = 'Duplicate argument variation "%s".' % (sArgs); + if iVar in dErrors: dErrors[iVar] += '<br>\n' + sMsg; + else: dErrors[iVar] = sMsg; + if iVar2 in dErrors: dErrors[iVar2] += '<br>\n' + sMsg; + else: dErrors[iVar2] = sMsg; + break; + + return (aoNewValues, dErrors if dErrors else None); + + return (aoNewValues, None if not asErrors else ' <br>'.join(asErrors)); + + def _validateAndConvertWorker(self, asAllowNullAttributes, oDb, enmValidateFor = ModelDataBase.ksValidateFor_Other): + dErrors = TestCaseData._validateAndConvertWorker(self, asAllowNullAttributes, oDb, enmValidateFor); + + # Validate dependencies a wee bit for paranoid reasons. The scheduler + # queue generation code does the real validation here! + if not dErrors and self.idTestCase is not None: + for oDep in self.aoDepTestCases: + if oDep.idTestCase == self.idTestCase: + if self.ksParam_aoDepTestCases in dErrors: + dErrors[self.ksParam_aoDepTestCases] += ' Depending on itself!'; + else: + dErrors[self.ksParam_aoDepTestCases] = 'Depending on itself!'; + return dErrors; + + + + + +class TestCaseLogic(ModelLogicBase): + """ + Test case management logic. + """ + + def __init__(self, oDb): + ModelLogicBase.__init__(self, oDb) + self.dCache = None; + + def getAll(self): + """ + Fetches all test case records from DB (TestCaseData). + """ + self._oDb.execute('SELECT *\n' + 'FROM TestCases\n' + 'WHERE tsExpire = \'infinity\'::TIMESTAMP\n' + 'ORDER BY idTestCase ASC;') + + aaoRows = self._oDb.fetchAll() + aoRet = []; + for aoRow in aaoRows: + aoRet.append(TestCaseData().initFromDbRow(aoRow)) + return aoRet + + def fetchForListing(self, iStart, cMaxRows, tsNow, aiSortColumns = None): + """ + Fetches test cases. + + Returns an array (list) of TestCaseDataEx items, empty list if none. + Raises exception on error. + """ + _ = aiSortColumns; + if tsNow is None: + self._oDb.execute('SELECT *\n' + 'FROM TestCases\n' + 'WHERE tsExpire = \'infinity\'::TIMESTAMP\n' + 'ORDER BY sName ASC\n' + 'LIMIT %s OFFSET %s\n' + , (cMaxRows, iStart, )); + else: + self._oDb.execute('SELECT *\n' + 'FROM TestCases\n' + 'WHERE tsExpire > %s\n' + ' AND tsEffective <= %s\n' + 'ORDER BY sName ASC\n' + 'LIMIT %s OFFSET %s\n' + , (tsNow, tsNow, cMaxRows, iStart, )); + + aoRows = []; + for aoRow in self._oDb.fetchAll(): + aoRows.append(TestCaseDataEx().initFromDbRowEx(aoRow, self._oDb, tsNow)); + return aoRows; + + def fetchForChangeLog(self, idTestCase, iStart, cMaxRows, tsNow): # pylint: disable=too-many-locals + """ + 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: + tsNow = self._oDb.getCurrentTimestamp(); + + # 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, + idTestCase, tsNow, + idTestCase, tsNow, + idTestCase, tsNow, + cMaxRows + 1, iStart, )); + aaoChanges = self._oDb.fetchAll(); + + # 2. Collect data sets for each of those points. + # (Doing it the lazy + inefficient way for now.) + aoRows = []; + for aoChange in aaoChanges: + aoRows.append(TestCaseDataEx().initFromDbWithId(self._oDb, idTestCase, aoChange[0])); + + # 3. Calculate the changes. + aoEntries = []; + for i in range(0, len(aoRows) - 1): + oNew = aoRows[i]; + oOld = aoRows[i + 1]; + (tsEffective, uidAuthor) = aaoChanges[i]; + (tsExpire, _) = aaoChanges[i - 1] if i > 0 else (oNew.tsExpire, None) + assert self._oDb.isTsInfinity(tsEffective) != self._oDb.isTsInfinity(tsExpire) or tsEffective < tsExpire, \ + '%s vs %s' % (tsEffective, tsExpire); + + aoChanges = []; + + # The testcase object. + if oNew.tsEffective != oOld.tsEffective: + for sAttr in oNew.getDataAttributes(): + if sAttr not in [ 'tsEffective', 'tsExpire', 'uidAuthor', \ + 'aoTestCaseArgs', 'aoDepTestCases', 'aoDepGlobalResources']: + oOldAttr = getattr(oOld, sAttr); + oNewAttr = getattr(oNew, sAttr); + if oOldAttr != oNewAttr: + aoChanges.append(AttributeChangeEntry(sAttr, oNewAttr, oOldAttr, str(oNewAttr), str(oOldAttr))); + + # The argument variations. + iChildOld = 0; + for oChildNew in oNew.aoTestCaseArgs: + # Locate the old entry, emitting removed markers for old items we have to skip. + while iChildOld < len(oOld.aoTestCaseArgs) \ + and oOld.aoTestCaseArgs[iChildOld].idTestCaseArgs < oChildNew.idTestCaseArgs: + oChildOld = oOld.aoTestCaseArgs[iChildOld]; + aoChanges.append(AttributeChangeEntry('Variation #%s' % (oChildOld.idTestCaseArgs,), + None, oChildOld, 'Removed', str(oChildOld))); + iChildOld += 1; + + if iChildOld < len(oOld.aoTestCaseArgs) \ + and oOld.aoTestCaseArgs[iChildOld].idTestCaseArgs == oChildNew.idTestCaseArgs: + oChildOld = oOld.aoTestCaseArgs[iChildOld]; + if oChildNew.tsEffective != oChildOld.tsEffective: + for sAttr in oChildNew.getDataAttributes(): + if sAttr not in [ 'tsEffective', 'tsExpire', 'uidAuthor', 'idGenTestCase', ]: + oOldAttr = getattr(oChildOld, sAttr); + oNewAttr = getattr(oChildNew, sAttr); + if oOldAttr != oNewAttr: + aoChanges.append(AttributeChangeEntry('Variation[#%s].%s' + % (oChildOld.idTestCaseArgs, sAttr,), + oNewAttr, oOldAttr, + str(oNewAttr), str(oOldAttr))); + iChildOld += 1; + else: + aoChanges.append(AttributeChangeEntry('Variation #%s' % (oChildNew.idTestCaseArgs,), + oChildNew, None, + str(oChildNew), 'Did not exist')); + + # The testcase dependencies. + iChildOld = 0; + for oChildNew in oNew.aoDepTestCases: + # Locate the old entry, emitting removed markers for old items we have to skip. + while iChildOld < len(oOld.aoDepTestCases) \ + and oOld.aoDepTestCases[iChildOld].idTestCase < oChildNew.idTestCase: + oChildOld = oOld.aoDepTestCases[iChildOld]; + aoChanges.append(AttributeChangeEntry('Dependency #%s' % (oChildOld.idTestCase,), + None, oChildOld, 'Removed', + '%s (#%u)' % (oChildOld.sName, oChildOld.idTestCase,))); + iChildOld += 1; + if iChildOld < len(oOld.aoDepTestCases) \ + and oOld.aoDepTestCases[iChildOld].idTestCase == oChildNew.idTestCase: + iChildOld += 1; + else: + aoChanges.append(AttributeChangeEntry('Dependency #%s' % (oChildNew.idTestCase,), + oChildNew, None, + '%s (#%u)' % (oChildNew.sName, oChildNew.idTestCase,), + 'Did not exist')); + + # The global resource dependencies. + iChildOld = 0; + for oChildNew in oNew.aoDepGlobalResources: + # Locate the old entry, emitting removed markers for old items we have to skip. + while iChildOld < len(oOld.aoDepGlobalResources) \ + and oOld.aoDepGlobalResources[iChildOld].idGlobalRsrc < oChildNew.idGlobalRsrc: + oChildOld = oOld.aoDepGlobalResources[iChildOld]; + aoChanges.append(AttributeChangeEntry('Global Resource #%s' % (oChildOld.idGlobalRsrc,), + None, oChildOld, 'Removed', + '%s (#%u)' % (oChildOld.sName, oChildOld.idGlobalRsrc,))); + iChildOld += 1; + if iChildOld < len(oOld.aoDepGlobalResources) \ + and oOld.aoDepGlobalResources[iChildOld].idGlobalRsrc == oChildNew.idGlobalRsrc: + iChildOld += 1; + else: + aoChanges.append(AttributeChangeEntry('Global Resource #%s' % (oChildNew.idGlobalRsrc,), + oChildNew, None, + '%s (#%u)' % (oChildNew.sName, oChildNew.idGlobalRsrc,), + 'Did not exist')); + + # Done. + aoEntries.append(ChangeLogEntry(uidAuthor, None, tsEffective, tsExpire, oNew, oOld, aoChanges)); + + # If we're at the end of the log, add the initial entry. + if len(aoRows) <= cMaxRows and aoRows: + oNew = aoRows[-1]; + aoEntries.append(ChangeLogEntry(oNew.uidAuthor, None, + aaoChanges[-1][0], aaoChanges[-2][0] if len(aaoChanges) > 1 else oNew.tsExpire, + oNew, None, [])); + + return (UserAccountLogic(self._oDb).resolveChangeLogAuthors(aoEntries), len(aoRows) > cMaxRows); + + + def addEntry(self, oData, uidAuthor, fCommit = False): + """ + Add a new testcase to the DB. + """ + + # + # Validate the input first. + # + assert isinstance(oData, TestCaseDataEx); + dErrors = oData.validateAndConvert(self._oDb, oData.ksValidateFor_Add); + if dErrors: + raise TMInvalidData('Invalid input data: %s' % (dErrors,)); + + # + # Add the testcase. + # + self._oDb.callProc('TestCaseLogic_addEntry', + ( uidAuthor, oData.sName, oData.sDescription, oData.fEnabled, oData.cSecTimeout, + oData.sTestBoxReqExpr, oData.sBuildReqExpr, oData.sBaseCmd, oData.sValidationKitZips, + oData.sComment )); + oData.idTestCase = self._oDb.fetchOne()[0]; + + # Add testcase dependencies. + for oDep in oData.aoDepTestCases: + self._oDb.execute('INSERT INTO TestCaseDeps (idTestCase, idTestCasePreReq, uidAuthor) VALUES (%s, %s, %s)' + , (oData.idTestCase, oDep.idTestCase, uidAuthor)) + + # Add global resource dependencies. + for oDep in oData.aoDepGlobalResources: + self._oDb.execute('INSERT INTO TestCaseGlobalRsrcDeps (idTestCase, idGlobalRsrc, uidAuthor) VALUES (%s, %s, %s)' + , (oData.idTestCase, oDep.idGlobalRsrc, uidAuthor)) + + # Set Test Case Arguments variations + for oVar in oData.aoTestCaseArgs: + self._oDb.execute('INSERT INTO TestCaseArgs (\n' + ' idTestCase, uidAuthor, sArgs, cSecTimeout,\n' + ' sTestBoxReqExpr, sBuildReqExpr, cGangMembers, sSubName)\n' + 'VALUES (%s, %s, %s, %s, %s, %s, %s, %s)' + , ( oData.idTestCase, uidAuthor, oVar.sArgs, oVar.cSecTimeout, + oVar.sTestBoxReqExpr, oVar.sBuildReqExpr, oVar.cGangMembers, oVar.sSubName, )); + + self._oDb.maybeCommit(fCommit); + return True; + + def editEntry(self, oData, uidAuthor, fCommit = False): # pylint: disable=too-many-locals + """ + Edit a testcase entry (extended). + Caller is expected to rollback the database transactions on exception. + """ + + # + # Validate the input. + # + assert isinstance(oData, TestCaseDataEx); + dErrors = oData.validateAndConvert(self._oDb, oData.ksValidateFor_Edit); + if dErrors: + raise TMInvalidData('Invalid input data: %s' % (dErrors,)); + + # + # Did anything change? If not return straight away. + # + oOldDataEx = TestCaseDataEx().initFromDbWithId(self._oDb, oData.idTestCase); + if oOldDataEx.isEqual(oData): + self._oDb.maybeCommit(fCommit); + return True; + + # + # Make the necessary changes. + # + + # The test case itself. + if not TestCaseData().initFromOther(oOldDataEx).isEqual(oData): + self._oDb.callProc('TestCaseLogic_editEntry', ( uidAuthor, oData.idTestCase, oData.sName, oData.sDescription, + oData.fEnabled, oData.cSecTimeout, oData.sTestBoxReqExpr, + oData.sBuildReqExpr, oData.sBaseCmd, oData.sValidationKitZips, + oData.sComment )); + oData.idGenTestCase = self._oDb.fetchOne()[0]; + + # + # Its dependencies on other testcases. + # + aidNewDeps = [oDep.idTestCase for oDep in oData.aoDepTestCases]; + aidOldDeps = [oDep.idTestCase for oDep in oOldDataEx.aoDepTestCases]; + + sQuery = self._oDb.formatBindArgs('UPDATE TestCaseDeps\n' + '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: + asKeepers.append(str(idDep)); + if asKeepers: + sQuery += ' AND idTestCasePreReq NOT IN (' + ', '.join(asKeepers) + ')\n'; + self._oDb.execute(sQuery); + + for idDep in aidNewDeps: + if idDep not in aidOldDeps: + self._oDb.execute('INSERT INTO TestCaseDeps (idTestCase, idTestCasePreReq, uidAuthor)\n' + 'VALUES (%s, %s, %s)\n' + , (oData.idTestCase, idDep, uidAuthor) ); + + # + # Its dependencies on global resources. + # + aidNewDeps = [oDep.idGlobalRsrc for oDep in oData.aoDepGlobalResources]; + aidOldDeps = [oDep.idGlobalRsrc for oDep in oOldDataEx.aoDepGlobalResources]; + + sQuery = self._oDb.formatBindArgs('UPDATE TestCaseGlobalRsrcDeps\n' + '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: + asKeepers.append(str(idDep)); + if asKeepers: + sQuery = ' AND idGlobalRsrc NOT IN (' + ', '.join(asKeepers) + ')\n'; + self._oDb.execute(sQuery); + + for idDep in aidNewDeps: + if idDep not in aidOldDeps: + self._oDb.execute('INSERT INTO TestCaseGlobalRsrcDeps (idTestCase, idGlobalRsrc, uidAuthor)\n' + 'VALUES (%s, %s, %s)\n' + , (oData.idTestCase, idDep, uidAuthor) ); + + # + # Update Test Case Args + # Note! Primary key is idTestCase, tsExpire, sArgs. + # + + # Historize rows that have been removed. + sQuery = self._oDb.formatBindArgs('UPDATE TestCaseArgs\n' + 'SET tsExpire = CURRENT_TIMESTAMP\n' + 'WHERE idTestCase = %s\n' + ' AND tsExpire = \'infinity\'::TIMESTAMP' + , (oData.idTestCase, )); + for oNewVar in oData.aoTestCaseArgs: + asKeepers.append(self._oDb.formatBindArgs('%s', (oNewVar.sArgs,))); + if asKeepers: + sQuery += ' AND sArgs NOT IN (' + ', '.join(asKeepers) + ')\n'; + self._oDb.execute(sQuery); + + # Add new TestCaseArgs records if necessary, reusing old IDs when possible. + from testmanager.core.testcaseargs import TestCaseArgsData; + for oNewVar in oData.aoTestCaseArgs: + self._oDb.execute('SELECT *\n' + 'FROM TestCaseArgs\n' + 'WHERE idTestCase = %s\n' + ' AND sArgs = %s\n' + 'ORDER BY tsExpire DESC\n' + 'LIMIT 1\n' + , (oData.idTestCase, oNewVar.sArgs,)); + aoRow = self._oDb.fetchOne(); + if aoRow is None: + # New + self._oDb.execute('INSERT INTO TestCaseArgs (\n' + ' idTestCase, uidAuthor, sArgs, cSecTimeout,\n' + ' sTestBoxReqExpr, sBuildReqExpr, cGangMembers, sSubName)\n' + 'VALUES (%s, %s, %s, %s, %s, %s, %s, %s)' + , ( oData.idTestCase, uidAuthor, oNewVar.sArgs, oNewVar.cSecTimeout, + oNewVar.sTestBoxReqExpr, oNewVar.sBuildReqExpr, oNewVar.cGangMembers, oNewVar.sSubName)); + else: + oCurVar = TestCaseArgsData().initFromDbRow(aoRow); + if self._oDb.isTsInfinity(oCurVar.tsExpire): + # Existing current entry, updated if changed. + if oNewVar.cSecTimeout == oCurVar.cSecTimeout \ + and oNewVar.sTestBoxReqExpr == oCurVar.sTestBoxReqExpr \ + and oNewVar.sBuildReqExpr == oCurVar.sBuildReqExpr \ + and oNewVar.cGangMembers == oCurVar.cGangMembers \ + and oNewVar.sSubName == oCurVar.sSubName: + oNewVar.idTestCaseArgs = oCurVar.idTestCaseArgs; + oNewVar.idGenTestCaseArgs = oCurVar.idGenTestCaseArgs; + 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; + self._oDb.execute('INSERT INTO TestCaseArgs (\n' + ' idTestCaseArgs, idTestCase, uidAuthor, sArgs, cSecTimeout,\n' + ' sTestBoxReqExpr, sBuildReqExpr, cGangMembers, sSubName)\n' + 'VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s)\n' + 'RETURNING idGenTestCaseArgs\n' + , ( oCurVar.idTestCaseArgs, oData.idTestCase, uidAuthor, oNewVar.sArgs, oNewVar.cSecTimeout, + oNewVar.sTestBoxReqExpr, oNewVar.sBuildReqExpr, oNewVar.cGangMembers, oNewVar.sSubName)); + oNewVar.idGenTestCaseArgs = self._oDb.fetchOne()[0]; + + self._oDb.maybeCommit(fCommit); + return True; + + def removeEntry(self, uidAuthor, idTestCase, fCascade = False, fCommit = False): + """ Deletes the test case if possible. """ + self._oDb.callProc('TestCaseLogic_delEntry', (uidAuthor, idTestCase, fCascade)); + self._oDb.maybeCommit(fCommit); + return True + + + def getTestCasePreReqIds(self, idTestCase, tsEffective = None, cMax = None): + """ + 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: + self._oDb.execute('SELECT idTestCasePreReq\n' + 'FROM TestCaseDeps\n' + 'WHERE idTestCase = %s\n' + ' AND tsExpire = \'infinity\'::TIMESTAMP\n' + 'ORDER BY idTestCasePreReq\n' + , (idTestCase,) ); + else: + self._oDb.execute('SELECT idTestCasePreReq\n' + 'FROM TestCaseDeps\n' + 'WHERE idTestCase = %s\n' + ' AND tsExpire > %s\n' + ' AND tsEffective <= %s\n' + 'ORDER BY idTestCasePreReq\n' + , (idTestCase, tsEffective, tsEffective) ); + + + if cMax is not None and self._oDb.getRowCount() > cMax: + raise TMExceptionBase('Too many prerequisites for testcase %s: %s, max %s' + % (idTestCase, cMax, self._oDb.getRowCount(),)); + + aidPreReqs = []; + for aoRow in self._oDb.fetchAll(): + aidPreReqs.append(aoRow[0]); + return aidPreReqs; + + + def cachedLookup(self, idTestCase): + """ + Looks up the most recent TestCaseDataEx object for idTestCase + via an object cache. + + Returns a shared TestCaseDataEx object. None if not found. + Raises exception on DB error. + """ + if self.dCache is None: + self.dCache = self._oDb.getCache('TestCaseDataEx'); + oEntry = self.dCache.get(idTestCase, None); + if oEntry is None: + fNeedTsNow = False; + self._oDb.execute('SELECT *\n' + 'FROM TestCases\n' + 'WHERE idTestCase = %s\n' + ' AND tsExpire = \'infinity\'::TIMESTAMP\n' + , (idTestCase, )); + if self._oDb.getRowCount() == 0: + # Maybe it was deleted, try get the last entry. + self._oDb.execute('SELECT *\n' + 'FROM TestCases\n' + 'WHERE idTestCase = %s\n' + 'ORDER BY tsExpire DESC\n' + 'LIMIT 1\n' + , (idTestCase, )); + fNeedTsNow = True; + elif self._oDb.getRowCount() > 1: + raise self._oDb.integrityException('%s infinity rows for %s' % (self._oDb.getRowCount(), idTestCase)); + + if self._oDb.getRowCount() == 1: + aaoRow = self._oDb.fetchOne(); + oEntry = TestCaseDataEx(); + tsNow = oEntry.initFromDbRow(aaoRow).tsEffective if fNeedTsNow else None; + oEntry.initFromDbRowEx(aaoRow, self._oDb, tsNow); + self.dCache[idTestCase] = oEntry; + return oEntry; + + + +# +# Unit testing. +# + +# pylint: disable=missing-docstring +class TestCaseGlobalRsrcDepDataTestCase(ModelDataBaseTestCase): + def setUp(self): + self.aoSamples = [TestCaseGlobalRsrcDepData(),]; + +class TestCaseDataTestCase(ModelDataBaseTestCase): + def setUp(self): + self.aoSamples = [TestCaseData(),]; + + def testEmptyExpr(self): + self.assertEqual(TestCaseData.validateTestBoxReqExpr(None), None); + self.assertEqual(TestCaseData.validateTestBoxReqExpr(''), None); + + def testSimpleExpr(self): + self.assertEqual(TestCaseData.validateTestBoxReqExpr('cMbMemory > 10'), None); + self.assertEqual(TestCaseData.validateTestBoxReqExpr('cMbScratch < 10'), None); + self.assertEqual(TestCaseData.validateTestBoxReqExpr('fChipsetIoMmu'), None); + self.assertEqual(TestCaseData.validateTestBoxReqExpr('fChipsetIoMmu is True'), None); + self.assertEqual(TestCaseData.validateTestBoxReqExpr('fChipsetIoMmu is False'), None); + self.assertEqual(TestCaseData.validateTestBoxReqExpr('fChipsetIoMmu is None'), None); + self.assertEqual(TestCaseData.validateTestBoxReqExpr('isinstance(fChipsetIoMmu, bool)'), None); + self.assertEqual(TestCaseData.validateTestBoxReqExpr('isinstance(iTestBoxScriptRev, int)'), None); + self.assertEqual(TestCaseData.validateTestBoxReqExpr('isinstance(cMbScratch, long)'), None); + + def testBadExpr(self): + self.assertNotEqual(TestCaseData.validateTestBoxReqExpr('this is an bad expression, surely it must be'), None); + self.assertNotEqual(TestCaseData.validateTestBoxReqExpr('x = 1 + 1'), None); + self.assertNotEqual(TestCaseData.validateTestBoxReqExpr('__import__(\'os\').unlink(\'/tmp/no/such/file\')'), None); + self.assertNotEqual(TestCaseData.validateTestBoxReqExpr('print "foobar"'), None); + +class TestCaseDataExTestCase(ModelDataBaseTestCase): + def setUp(self): + self.aoSamples = [TestCaseDataEx(),]; + +if __name__ == '__main__': + unittest.main(); + # not reached. + diff --git a/src/VBox/ValidationKit/testmanager/core/testcaseargs.py b/src/VBox/ValidationKit/testmanager/core/testcaseargs.py new file mode 100755 index 00000000..e63df090 --- /dev/null +++ b/src/VBox/ValidationKit/testmanager/core/testcaseargs.py @@ -0,0 +1,416 @@ +# -*- coding: utf-8 -*- +# $Id: testcaseargs.py $ + +""" +Test Manager - Test Case Arguments Variations. +""" + +__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 unittest; +import sys; + +# Validation Kit imports. +from common import utils; +from testmanager.core.base import ModelDataBase, ModelDataBaseTestCase, ModelLogicBase, TMExceptionBase, \ + TMRowNotFound; +from testmanager.core.testcase import TestCaseData, TestCaseDependencyLogic, TestCaseGlobalRsrcDepLogic; + +# Python 3 hacks: +if sys.version_info[0] >= 3: + long = int; # pylint: disable=redefined-builtin,invalid-name + + +class TestCaseArgsData(ModelDataBase): + """ + Test case argument variation. + """ + + ksIdAttr = 'idTestCaseArgs'; + ksIdGenAttr = 'idGenTestCaseArgs'; + + ksParam_idTestCase = 'TestCaseArgs_idTestCase'; + ksParam_idTestCaseArgs = 'TestCaseArgs_idTestCaseArgs'; + ksParam_tsEffective = 'TestCaseArgs_tsEffective'; + ksParam_tsExpire = 'TestCaseArgs_tsExpire'; + ksParam_uidAuthor = 'TestCaseArgs_uidAuthor'; + ksParam_idGenTestCaseArgs = 'TestCaseArgs_idGenTestCaseArgs'; + ksParam_sArgs = 'TestCaseArgs_sArgs'; + ksParam_cSecTimeout = 'TestCaseArgs_cSecTimeout'; + ksParam_sTestBoxReqExpr = 'TestCaseArgs_sTestBoxReqExpr'; + ksParam_sBuildReqExpr = 'TestCaseArgs_sBuildReqExpr'; + ksParam_cGangMembers = 'TestCaseArgs_cGangMembers'; + ksParam_sSubName = 'TestCaseArgs_sSubName'; + + kcDbColumns = 12; + + kasAllowNullAttributes = [ 'idTestCase', 'idTestCaseArgs', 'tsEffective', 'tsExpire', 'uidAuthor', 'idGenTestCaseArgs', + 'cSecTimeout', 'sTestBoxReqExpr', 'sBuildReqExpr', 'sSubName', ]; + + def __init__(self): + ModelDataBase.__init__(self); + + # + # Initialize with defaults. + # See the database for explanations of each of these fields. + # + self.idTestCase = None; + self.idTestCaseArgs = None; + self.tsEffective = None; + self.tsExpire = None; + self.uidAuthor = None; + self.idGenTestCaseArgs = None; + self.sArgs = ''; + self.cSecTimeout = None; + self.sTestBoxReqExpr = None; + self.sBuildReqExpr = None; + self.cGangMembers = 1; + self.sSubName = None; + + def initFromDbRow(self, aoRow): + """ + Re-initializes the object from a SELECT * FROM TestCaseArgs row. + Returns self. Raises exception if aoRow is None. + """ + if aoRow is None: + raise TMRowNotFound('TestBoxStatus not found.'); + + self.idTestCase = aoRow[0]; + self.idTestCaseArgs = aoRow[1]; + self.tsEffective = aoRow[2]; + self.tsExpire = aoRow[3]; + self.uidAuthor = aoRow[4]; + self.idGenTestCaseArgs = aoRow[5]; + self.sArgs = aoRow[6]; + self.cSecTimeout = aoRow[7]; + self.sTestBoxReqExpr = aoRow[8]; + self.sBuildReqExpr = aoRow[9]; + self.cGangMembers = aoRow[10]; + self.sSubName = aoRow[11]; + return self; + + def initFromDbWithId(self, oDb, idTestCaseArgs, tsNow = None, sPeriodBack = None): + """ + Initialize from the database. + """ + oDb.execute(self.formatSimpleNowAndPeriodQuery(oDb, + 'SELECT *\n' + 'FROM TestCaseArgs\n' + 'WHERE idTestCaseArgs = %s\n' + , ( idTestCaseArgs,), tsNow, sPeriodBack)); + aoRow = oDb.fetchOne() + if aoRow is None: + raise TMRowNotFound('idTestCaseArgs=%s not found (tsNow=%s sPeriodBack=%s)' + % (idTestCaseArgs, tsNow, sPeriodBack,)); + return self.initFromDbRow(aoRow); + + def initFromDbWithGenId(self, oDb, idGenTestCaseArgs): + """ + Initialize from the database, given the generation ID of a row. + """ + oDb.execute('SELECT * FROM TestCaseArgs WHERE idGenTestCaseArgs = %s', (idGenTestCaseArgs,)); + return self.initFromDbRow(oDb.fetchOne()); + + def initFromValues(self, sArgs, cSecTimeout = None, sTestBoxReqExpr = None, sBuildReqExpr = None, # pylint: disable=too-many-arguments + cGangMembers = 1, idTestCase = None, idTestCaseArgs = None, tsEffective = None, tsExpire = None, + uidAuthor = None, idGenTestCaseArgs = None, sSubName = None): + """ + Reinitialize from values. + Returns self. + """ + self.idTestCase = idTestCase; + self.idTestCaseArgs = idTestCaseArgs; + self.tsEffective = tsEffective; + self.tsExpire = tsExpire; + self.uidAuthor = uidAuthor; + self.idGenTestCaseArgs = idGenTestCaseArgs; + self.sArgs = sArgs; + self.cSecTimeout = utils.parseIntervalSeconds(cSecTimeout); + self.sTestBoxReqExpr = sTestBoxReqExpr; + self.sBuildReqExpr = sBuildReqExpr; + self.cGangMembers = cGangMembers; + self.sSubName = sSubName; + return self; + + def getAttributeParamNullValues(self, sAttr): + aoNilValues = ModelDataBase.getAttributeParamNullValues(self, sAttr); + if sAttr == 'cSecTimeout': + aoNilValues.insert(0, ''); # Prettier NULL value for cSecTimeout. + elif sAttr == 'sArgs': + aoNilValues = []; # No NULL value here, thank you. + return aoNilValues; + + def _validateAndConvertAttribute(self, sAttr, sParam, oValue, aoNilValues, fAllowNull, oDb): + if sAttr == 'cSecTimeout' and oValue not in aoNilValues: # Allow human readable interval formats. + return utils.parseIntervalSeconds(oValue); + + (oValue, sError) = ModelDataBase._validateAndConvertAttribute(self, sAttr, sParam, oValue, aoNilValues, fAllowNull, oDb); + if sError is None: + if sAttr == 'sTestBoxReqExpr': + sError = TestCaseData.validateTestBoxReqExpr(oValue); + elif sAttr == 'sBuildReqExpr': + sError = TestCaseData.validateBuildReqExpr(oValue); + return (oValue, sError); + + + + +class TestCaseArgsDataEx(TestCaseArgsData): + """ + Complete data set. + """ + + def __init__(self): + TestCaseArgsData.__init__(self); + self.oTestCase = None; + self.aoTestCasePreReqs = []; + self.aoGlobalRsrc = []; + + def initFromDbRow(self, aoRow): + raise TMExceptionBase('Do not call me: %s' % (aoRow,)) + + def initFromDbRowEx(self, aoRow, oDb, tsConfigEff = None, tsRsrcEff = None): + """ + Extended version of initFromDbRow that fills in the rest from the database. + """ + TestCaseArgsData.initFromDbRow(self, aoRow); + + if tsConfigEff is None: tsConfigEff = oDb.getCurrentTimestamp(); + if tsRsrcEff is None: tsRsrcEff = oDb.getCurrentTimestamp(); + + self.oTestCase = TestCaseData().initFromDbWithId(oDb, self.idTestCase, tsConfigEff); + self.aoTestCasePreReqs = TestCaseDependencyLogic(oDb).getTestCaseDeps(self.idTestCase, tsConfigEff); + self.aoGlobalRsrc = TestCaseGlobalRsrcDepLogic(oDb).getTestCaseDeps(self.idTestCase, tsRsrcEff); + + return self; + + def initFromDbWithId(self, oDb, idTestCaseArgs, tsNow = None, sPeriodBack = None): + _ = oDb; _ = idTestCaseArgs; _ = tsNow; _ = sPeriodBack; + raise TMExceptionBase('Not supported.'); + + def initFromDbWithGenId(self, oDb, idGenTestCaseArgs): + _ = oDb; _ = idGenTestCaseArgs; + raise TMExceptionBase('Use initFromDbWithGenIdEx...'); + + def initFromDbWithGenIdEx(self, oDb, idGenTestCaseArgs, tsConfigEff = None, tsRsrcEff = None): + """ + Initialize from the database, given the ID of a row. + """ + oDb.execute('SELECT *, CURRENT_TIMESTAMP FROM TestCaseArgs WHERE idGenTestCaseArgs = %s', (idGenTestCaseArgs,)); + aoRow = oDb.fetchOne(); + return self.initFromDbRowEx(aoRow, oDb, tsConfigEff, tsRsrcEff); + + def convertFromParamNull(self): + raise TMExceptionBase('Not implemented'); + + def convertToParamNull(self): + raise TMExceptionBase('Not implemented'); + + def isEqual(self, oOther): + raise TMExceptionBase('Not implemented'); + + def matchesTestBoxProps(self, oTestBoxData): + """ + 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. + """ + return TestCaseData.matchesTestBoxPropsEx(oTestBoxData, self.oTestCase.sTestBoxReqExpr) \ + and TestCaseData.matchesTestBoxPropsEx(oTestBoxData, self.sTestBoxReqExpr); + + def matchesBuildProps(self, oBuildDataEx): + """ + 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. + """ + return TestCaseData.matchesBuildPropsEx(oBuildDataEx, self.oTestCase.sBuildReqExpr) \ + and TestCaseData.matchesBuildPropsEx(oBuildDataEx, self.sBuildReqExpr); + + +class TestCaseArgsLogic(ModelLogicBase): + """ + TestCaseArgs database logic. + """ + + def __init__(self, oDb): + ModelLogicBase.__init__(self, oDb); + self.dCache = None; + + + def areResourcesFree(self, oDataEx): + """ + Checks if all global resources are currently still in existance and free. + Returns True/False. May raise exception on database error. + """ + + # Create a set of global resource IDs. + if not oDataEx.aoGlobalRsrc: + return True; + asIdRsrcs = [str(oDep.idGlobalRsrc) for oDep, _ in oDataEx.aoGlobalRsrc]; + + # A record in the resource status table means it's allocated. + self._oDb.execute('SELECT COUNT(*)\n' + 'FROM GlobalResourceStatuses\n' + 'WHERE GlobalResourceStatuses.idGlobalRsrc IN (' + ', '.join(asIdRsrcs) + ')\n'); + if self._oDb.fetchOne()[0] == 0: + # Check for disabled or deleted resources (we cannot allocate them). + self._oDb.execute('SELECT COUNT(*)\n' + 'FROM GlobalResources\n' + 'WHERE GlobalResources.idGlobalRsrc IN (' + ', '.join(asIdRsrcs) + ')\n' + ' AND GlobalResources.tsExpire = \'infinity\'::TIMESTAMP\n' + ' AND GlobalResources.fEnabled = TRUE\n'); + if self._oDb.fetchOne()[0] == len(oDataEx.aoGlobalRsrc): + return True; + return False; + + def getAll(self): + """Get list of objects of type TestCaseArgsData""" + self._oDb.execute('SELECT *\n' + 'FROM TestCaseArgs\n' + 'WHERE tsExpire = \'infinity\'::TIMESTAMP') + aaoRows = self._oDb.fetchAll() + aoRet = [] + for aoRow in aaoRows: + aoRet.append(TestCaseArgsData().initFromDbRow(aoRow)) + + return aoRet + + def getTestCaseArgs(self, idTestCase, tsNow = None, aiWhiteList = None): + """Get list of testcase's arguments variations""" + if aiWhiteList is None: + if tsNow is None: + self._oDb.execute('SELECT *\n' + 'FROM TestCaseArgs\n' + 'WHERE idTestCase = %s\n' + ' AND tsExpire = \'infinity\'::TIMESTAMP\n' + 'ORDER BY TestCaseArgs.idTestCaseArgs\n' + , (idTestCase,)); + else: + self._oDb.execute('SELECT *\n' + 'FROM TestCaseArgs\n' + 'WHERE idTestCase = %s\n' + ' AND tsExpire > %s\n' + ' AND tsEffective <= %s\n' + 'ORDER BY TestCaseArgs.idTestCaseArgs\n' + , (idTestCase, tsNow, tsNow)); + else: + sWhiteList = ','.join((str(x) for x in aiWhiteList)); + if tsNow is None: + self._oDb.execute('SELECT *\n' + 'FROM TestCaseArgs\n' + 'WHERE idTestCase = %s\n' + ' AND tsExpire = \'infinity\'::TIMESTAMP\n' + ' AND idTestCaseArgs IN (' + sWhiteList + ')\n' + 'ORDER BY TestCaseArgs.idTestCaseArgs\n' + , (idTestCase,)); + else: + self._oDb.execute('SELECT *\n' + 'FROM TestCaseArgs\n' + 'WHERE idTestCase = %s\n' + ' AND tsExpire > %s\n' + ' AND tsEffective <= %s\n' + ' AND idTestCaseArgs IN (' + sWhiteList + ')\n' + 'ORDER BY TestCaseArgs.idTestCaseArgs\n' + , (idTestCase, tsNow, tsNow)); + + aaoRows = self._oDb.fetchAll() + aoRet = [] + for aoRow in aaoRows: + aoRet.append(TestCaseArgsData().initFromDbRow(aoRow)) + + return aoRet + + def addTestCaseArgs(self, oTestCaseArgsData): + """Add Test Case Args record into DB""" + pass; # pylint: disable=unnecessary-pass + + def cachedLookup(self, idTestCaseArgs): + """ + Looks up the most recent TestCaseArgsDataEx object for idTestCaseArg + via in an object cache. + + Returns a shared TestCaseArgDataEx object. None if not found. + Raises exception on DB error. + """ + if self.dCache is None: + self.dCache = self._oDb.getCache('TestCaseArgsDataEx'); + oEntry = self.dCache.get(idTestCaseArgs, None); + if oEntry is None: + fNeedTsNow = False; + self._oDb.execute('SELECT *\n' + 'FROM TestCaseArgs\n' + 'WHERE idTestCaseArgs = %s\n' + ' AND tsExpire = \'infinity\'::TIMESTAMP\n' + , (idTestCaseArgs, )); + if self._oDb.getRowCount() == 0: + # Maybe it was deleted, try get the last entry. + self._oDb.execute('SELECT *\n' + 'FROM TestCaseArgs\n' + 'WHERE idTestCaseArgs = %s\n' + 'ORDER BY tsExpire DESC\n' + 'LIMIT 1\n' + , (idTestCaseArgs, )); + fNeedTsNow = True; + elif self._oDb.getRowCount() > 1: + raise self._oDb.integrityException('%s infinity rows for %s' % (self._oDb.getRowCount(), idTestCaseArgs)); + + if self._oDb.getRowCount() == 1: + aaoRow = self._oDb.fetchOne(); + oEntry = TestCaseArgsDataEx(); + tsNow = TestCaseArgsData().initFromDbRow(aaoRow).tsEffective if fNeedTsNow else None; + oEntry.initFromDbRowEx(aaoRow, self._oDb, tsNow, tsNow); + self.dCache[idTestCaseArgs] = oEntry; + return oEntry; + + +# +# Unit testing. +# + +# pylint: disable=missing-docstring +class TestCaseArgsDataTestCase(ModelDataBaseTestCase): + def setUp(self): + self.aoSamples = [TestCaseArgsData(),]; + +if __name__ == '__main__': + unittest.main(); + # not reached. + diff --git a/src/VBox/ValidationKit/testmanager/core/testgroup.py b/src/VBox/ValidationKit/testmanager/core/testgroup.py new file mode 100755 index 00000000..9a648b0d --- /dev/null +++ b/src/VBox/ValidationKit/testmanager/core/testgroup.py @@ -0,0 +1,771 @@ +# -*- coding: utf-8 -*- +# $Id: testgroup.py $ + +""" +Test Manager - Test groups management. +""" + +__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 unittest; + +# Validation Kit imports. +from testmanager.core.base import ModelDataBase, ModelDataBaseTestCase, ModelLogicBase, TMRowInUse, \ + TMTooManyRows, TMInvalidData, TMRowNotFound, TMRowAlreadyExists; +from testmanager.core.testcase import TestCaseData, TestCaseDataEx; + + +class TestGroupMemberData(ModelDataBase): + """Representation of a test group member database row.""" + + ksParam_idTestGroup = 'TestGroupMember_idTestGroup'; + ksParam_idTestCase = 'TestGroupMember_idTestCase'; + ksParam_tsEffective = 'TestGroupMember_tsEffective'; + ksParam_tsExpire = 'TestGroupMember_tsExpire'; + ksParam_uidAuthor = 'TestGroupMember_uidAuthor'; + ksParam_iSchedPriority = 'TestGroupMember_iSchedPriority'; + ksParam_aidTestCaseArgs = 'TestGroupMember_aidTestCaseArgs'; + + kasAllowNullAttributes = ['idTestGroup', 'idTestCase', 'tsEffective', 'tsExpire', 'uidAuthor', 'aidTestCaseArgs' ]; + kiMin_iSchedPriority = 0; + kiMax_iSchedPriority = 31; + + kcDbColumns = 7; + + def __init__(self): + ModelDataBase.__init__(self) + + # + # Initialize with defaults. + # See the database for explanations of each of these fields. + # + self.idTestGroup = None; + self.idTestCase = None; + self.tsEffective = None; + self.tsExpire = None; + self.uidAuthor = None; + self.iSchedPriority = 16; + self.aidTestCaseArgs = None; + + def initFromDbRow(self, aoRow): + """ + Reinitialize from a SELECT * FROM TestCaseGroupMembers. + Return self. Raises exception if no row. + """ + if aoRow is None: + raise TMRowNotFound('Test group member not found.') + + self.idTestGroup = aoRow[0]; + self.idTestCase = aoRow[1]; + self.tsEffective = aoRow[2]; + self.tsExpire = aoRow[3]; + self.uidAuthor = aoRow[4]; + self.iSchedPriority = aoRow[5]; + self.aidTestCaseArgs = aoRow[6]; + return self + + + def getAttributeParamNullValues(self, sAttr): + # Arrays default to [] as NULL currently. That doesn't work for us. + if sAttr == 'aidTestCaseArgs': + aoNilValues = [None, '-1']; + else: + aoNilValues = ModelDataBase.getAttributeParamNullValues(self, sAttr); + return aoNilValues; + + def _validateAndConvertAttribute(self, sAttr, sParam, oValue, aoNilValues, fAllowNull, oDb): + if sAttr != 'aidTestCaseArgs': + return ModelDataBase._validateAndConvertAttribute(self, sAttr, sParam, oValue, aoNilValues, fAllowNull, oDb); + + # -1 is a special value, which when present make the whole thing NULL (None). + (aidVariations, sError) = self.validateListOfInts(oValue, aoNilValues = aoNilValues, fAllowNull = fAllowNull, + iMin = -1, iMax = 0x7ffffffe); + if sError is None: + if aidVariations is None: + pass; + elif -1 in aidVariations: + aidVariations = None; + elif 0 in aidVariations: + sError = 'Invalid test case varation ID #0.'; + else: + aidVariations = sorted(aidVariations); + return (aidVariations, sError); + + + +class TestGroupMemberDataEx(TestGroupMemberData): + """Extended representation of a test group member.""" + + def __init__(self): + """Extend parent class""" + TestGroupMemberData.__init__(self) + self.oTestCase = None; # TestCaseDataEx. + + def initFromDbRowEx(self, aoRow, oDb, tsNow = None): + """ + Reinitialize from a SELECT * FROM TestGroupMembers, TestCases row. + Will query the necessary additional data from oDb using tsNow. + + Returns self. Raises exception if no row or database error. + """ + TestGroupMemberData.initFromDbRow(self, aoRow); + self.oTestCase = TestCaseDataEx(); + self.oTestCase.initFromDbRowEx(aoRow[TestGroupMemberData.kcDbColumns:], oDb, tsNow); + return self; + + def initFromParams(self, oDisp, fStrict = True): + self.oTestCase = None; + return TestGroupMemberData.initFromParams(self, oDisp, fStrict); + + def getDataAttributes(self): + asAttributes = TestGroupMemberData.getDataAttributes(self); + asAttributes.remove('oTestCase'); + return asAttributes; + + def _validateAndConvertWorker(self, asAllowNullAttributes, oDb, enmValidateFor = ModelDataBase.ksValidateFor_Other): + dErrors = TestGroupMemberData._validateAndConvertWorker(self, asAllowNullAttributes, oDb, enmValidateFor); + if self.ksParam_idTestCase not in dErrors: + self.oTestCase = TestCaseDataEx() + try: + self.oTestCase.initFromDbWithId(oDb, self.idTestCase); + except Exception as oXcpt: + self.oTestCase = TestCaseDataEx() + dErrors[self.ksParam_idTestCase] = str(oXcpt); + return dErrors; + + +class TestGroupMemberData2(TestCaseData): + """Special representation of a Test Group Member item""" + + def __init__(self): + """Extend parent class""" + TestCaseData.__init__(self) + self.idTestGroup = None + self.aidTestCaseArgs = [] + + def initFromDbRowEx(self, aoRow): + """ + Reinitialize from this query: + + SELECT TestCases.*, + TestGroupMembers.idTestGroup, + TestGroupMembers.aidTestCaseArgs + FROM TestCases, TestGroupMembers + WHERE TestCases.idTestCase = TestGroupMembers.idTestCase + + Represents complete test group member (test case) info. + Returns object of type TestGroupMemberData2. Raises exception if no row. + """ + TestCaseData.initFromDbRow(self, aoRow); + self.idTestGroup = aoRow[-2] + self.aidTestCaseArgs = aoRow[-1] + return self; + + +class TestGroupData(ModelDataBase): + """ + Test group data. + """ + + ksIdAttr = 'idTestGroup'; + + ksParam_idTestGroup = 'TestGroup_idTestGroup' + ksParam_tsEffective = 'TestGroup_tsEffective' + ksParam_tsExpire = 'TestGroup_tsExpire' + ksParam_uidAuthor = 'TestGroup_uidAuthor' + ksParam_sName = 'TestGroup_sName' + ksParam_sDescription = 'TestGroup_sDescription' + ksParam_sComment = 'TestGroup_sComment' + + kasAllowNullAttributes = ['idTestGroup', 'tsEffective', 'tsExpire', 'uidAuthor', 'sDescription', 'sComment' ]; + + kcDbColumns = 7; + + def __init__(self): + ModelDataBase.__init__(self); + + # + # Initialize with defaults. + # See the database for explanations of each of these fields. + # + self.idTestGroup = None + self.tsEffective = None + self.tsExpire = None + self.uidAuthor = None + self.sName = None + self.sDescription = None + self.sComment = None + + def initFromDbRow(self, aoRow): + """ + Reinitialize from a SELECT * FROM TestGroups row. + Returns object of type TestGroupData. Raises exception if no row. + """ + if aoRow is None: + raise TMRowNotFound('Test group not found.') + + self.idTestGroup = aoRow[0] + self.tsEffective = aoRow[1] + self.tsExpire = aoRow[2] + self.uidAuthor = aoRow[3] + self.sName = aoRow[4] + self.sDescription = aoRow[5] + self.sComment = aoRow[6] + return self + + def initFromDbWithId(self, oDb, idTestGroup, tsNow = None, sPeriodBack = None): + """ + Initialize the object from the database. + """ + oDb.execute(self.formatSimpleNowAndPeriodQuery(oDb, + 'SELECT *\n' + 'FROM TestGroups\n' + 'WHERE idTestGroup = %s\n' + , ( idTestGroup,), tsNow, sPeriodBack)); + aoRow = oDb.fetchOne() + if aoRow is None: + raise TMRowNotFound('idTestGroup=%s not found (tsNow=%s sPeriodBack=%s)' % (idTestGroup, tsNow, sPeriodBack,)); + return self.initFromDbRow(aoRow); + + +class TestGroupDataEx(TestGroupData): + """ + Extended test group data. + """ + + ksParam_aoMembers = 'TestGroupDataEx_aoMembers'; + kasAltArrayNull = [ 'aoMembers', ]; + + ## Helper parameter containing the comma separated list with the IDs of + # potential members found in the parameters. + ksParam_aidTestCases = 'TestGroupDataEx_aidTestCases'; + + + def __init__(self): + TestGroupData.__init__(self); + self.aoMembers = []; # TestGroupMemberDataEx. + + def _initExtraMembersFromDb(self, oDb, tsNow = None, sPeriodBack = None): + """ + Worker shared by the initFromDb* methods. + Returns self. Raises exception if no row or database error. + """ + self.aoMembers = []; + _ = sPeriodBack; ## @todo sPeriodBack + + if tsNow is None: + oDb.execute('SELECT TestGroupMembers.*, TestCases.*\n' + 'FROM TestGroupMembers\n' + 'LEFT OUTER JOIN TestCases ON (\n' + ' TestGroupMembers.idTestCase = TestCases.idTestCase\n' + ' AND TestCases.tsExpire = \'infinity\'::TIMESTAMP)\n' + 'WHERE TestGroupMembers.idTestGroup = %s\n' + ' AND TestGroupMembers.tsExpire = \'infinity\'::TIMESTAMP\n' + 'ORDER BY TestCases.sName, TestCases.idTestCase\n' + , (self.idTestGroup,)); + else: + oDb.execute('SELECT TestGroupMembers.*, TestCases.*\n' + 'FROM TestGroupMembers\n' + 'LEFT OUTER JOIN TestCases ON (\n' + ' TestGroupMembers.idTestCase = TestCases.idTestCase\n' + ' AND TestCases.tsExpire > %s\n' + ' AND TestCases.tsEffective <= %s)\n' + 'WHERE TestGroupMembers.idTestGroup = %s\n' + ' AND TestGroupMembers.tsExpire > %s\n' + ' AND TestGroupMembers.tsEffective <= %s\n' + 'ORDER BY TestCases.sName, TestCases.idTestCase\n' + , (tsNow, tsNow, self.idTestGroup, tsNow, tsNow)); + + for aoRow in oDb.fetchAll(): + self.aoMembers.append(TestGroupMemberDataEx().initFromDbRowEx(aoRow, oDb, tsNow)); + return self; + + def initFromDbRowEx(self, aoRow, oDb, tsNow = None, sPeriodBack = None): + """ + Reinitialize from a SELECT * FROM TestGroups row. Will query the + necessary additional data from oDb using tsNow. + Returns self. Raises exception if no row or database error. + """ + TestGroupData.initFromDbRow(self, aoRow); + return self._initExtraMembersFromDb(oDb, tsNow, sPeriodBack); + + def initFromDbWithId(self, oDb, idTestGroup, tsNow = None, sPeriodBack = None): + """ + Initialize the object from the database. + """ + TestGroupData.initFromDbWithId(self, oDb, idTestGroup, tsNow, sPeriodBack); + return self._initExtraMembersFromDb(oDb, tsNow, sPeriodBack); + + + def getAttributeParamNullValues(self, sAttr): + if sAttr != 'aoMembers': + return TestGroupData.getAttributeParamNullValues(self, sAttr); + return ['', [], None]; + + def convertParamToAttribute(self, sAttr, sParam, oValue, oDisp, fStrict): + if sAttr != 'aoMembers': + return TestGroupData.convertParamToAttribute(self, sAttr, sParam, oValue, oDisp, fStrict); + + aoNewValue = []; + aidSelected = oDisp.getListOfIntParams(sParam, iMin = 1, iMax = 0x7ffffffe, aiDefaults = []) + sIds = oDisp.getStringParam(self.ksParam_aidTestCases, sDefault = ''); + for idTestCase in sIds.split(','): + try: idTestCase = int(idTestCase); + except: pass; + oDispWrapper = self.DispWrapper(oDisp, '%s[%s][%%s]' % (TestGroupDataEx.ksParam_aoMembers, idTestCase,)) + oMember = TestGroupMemberDataEx().initFromParams(oDispWrapper, fStrict = False); + if idTestCase in aidSelected: + aoNewValue.append(oMember); + return aoNewValue; + + def _validateAndConvertAttribute(self, sAttr, sParam, oValue, aoNilValues, fAllowNull, oDb): + if sAttr != 'aoMembers': + return TestGroupData._validateAndConvertAttribute(self, sAttr, sParam, oValue, aoNilValues, fAllowNull, oDb); + + asErrors = []; + aoNewMembers = []; + for oOldMember in oValue: + oNewMember = TestGroupMemberDataEx().initFromOther(oOldMember); + aoNewMembers.append(oNewMember); + + dErrors = oNewMember.validateAndConvert(oDb, ModelDataBase.ksValidateFor_Other); + if dErrors: + asErrors.append(str(dErrors)); + + if not asErrors: + for i, _ in enumerate(aoNewMembers): + idTestCase = aoNewMembers[i]; + for j in range(i + 1, len(aoNewMembers)): + if aoNewMembers[j].idTestCase == idTestCase: + asErrors.append('Duplicate testcase #%d!' % (idTestCase, )); + break; + + return (aoNewMembers, None if not asErrors else '<br>\n'.join(asErrors)); + + +class TestGroupLogic(ModelLogicBase): + """ + Test case management logic. + """ + + def __init__(self, oDb): + ModelLogicBase.__init__(self, oDb) + self.dCache = None; + + # + # Standard methods. + # + + def fetchForListing(self, iStart, cMaxRows, tsNow, aiSortColumns = None): + """ + Fetches test groups. + + Returns an array (list) of TestGroupDataEx items, empty list if none. + Raises exception on error. + """ + _ = aiSortColumns; + if tsNow is None: + self._oDb.execute('SELECT *\n' + 'FROM TestGroups\n' + 'WHERE tsExpire = \'infinity\'::TIMESTAMP\n' + 'ORDER BY sName ASC\n' + 'LIMIT %s OFFSET %s\n' + , (cMaxRows, iStart,)); + else: + self._oDb.execute('SELECT *\n' + 'FROM TestGroups\n' + 'WHERE tsExpire > %s\n' + ' AND tsEffective <= %s\n' + 'ORDER BY sName ASC\n' + 'LIMIT %s OFFSET %s\n' + , (tsNow, tsNow, cMaxRows, iStart,)); + + aoRet = []; + for aoRow in self._oDb.fetchAll(): + aoRet.append(TestGroupDataEx().initFromDbRowEx(aoRow, self._oDb, tsNow)); + return aoRet; + + def addEntry(self, oData, uidAuthor, fCommit = False): + """ + Adds a testgroup to the database. + """ + + # + # Validate inputs. + # + assert isinstance(oData, TestGroupDataEx); + dErrors = oData.validateAndConvert(self._oDb, oData.ksValidateFor_Add); + if dErrors: + raise TMInvalidData('addEntry invalid input: %s' % (dErrors,)); + self._assertUniq(oData, None); + + # + # Do the job. + # + self._oDb.execute('INSERT INTO TestGroups (uidAuthor, sName, sDescription, sComment)\n' + 'VALUES (%s, %s, %s, %s)\n' + 'RETURNING idTestGroup\n' + , ( uidAuthor, + oData.sName, + oData.sDescription, + oData.sComment )); + idTestGroup = self._oDb.fetchOne()[0]; + oData.idTestGroup = idTestGroup; + + for oMember in oData.aoMembers: + oMember.idTestGroup = idTestGroup; + self._insertTestGroupMember(uidAuthor, oMember) + + self._oDb.maybeCommit(fCommit); + return True; + + def editEntry(self, oData, uidAuthor, fCommit = False): + """ + Modifies a test group. + """ + + # + # Validate inputs and read in the old(/current) data. + # + assert isinstance(oData, TestGroupDataEx); + dErrors = oData.validateAndConvert(self._oDb, oData.ksValidateFor_Edit); + if dErrors: + raise TMInvalidData('editEntry invalid input: %s' % (dErrors,)); + self._assertUniq(oData, oData.idTestGroup); + + oOldData = TestGroupDataEx().initFromDbWithId(self._oDb, oData.idTestGroup); + + # + # Update the data that needs updating. + # + + if not oData.isEqualEx(oOldData, [ 'aoMembers', 'tsEffective', 'tsExpire', 'uidAuthor', ]): + self._historizeTestGroup(oData.idTestGroup); + self._oDb.execute('INSERT INTO TestGroups\n' + ' (uidAuthor, idTestGroup, sName, sDescription, sComment)\n' + 'VALUES (%s, %s, %s, %s, %s)\n' + , ( uidAuthor, + oData.idTestGroup, + oData.sName, + oData.sDescription, + oData.sComment )); + + # Create a lookup dictionary for old entries. + dOld = {}; + for oOld in oOldData.aoMembers: + dOld[oOld.idTestCase] = oOld; + assert len(dOld) == len(oOldData.aoMembers); + + # Add new members, updated existing ones. + dNew = {}; + for oNewMember in oData.aoMembers: + oNewMember.idTestGroup = oData.idTestGroup; + if oNewMember.idTestCase in dNew: + raise TMRowAlreadyExists('Duplicate test group member: idTestCase=%d (%s / %s)' + % (oNewMember.idTestCase, oNewMember, dNew[oNewMember.idTestCase],)); + dNew[oNewMember.idTestCase] = oNewMember; + + oOldMember = dOld.get(oNewMember.idTestCase, None); + if oOldMember is not None: + if oNewMember.isEqualEx(oOldMember, [ 'uidAuthor', 'tsEffective', 'tsExpire' ]): + continue; # Skip, nothing changed. + self._historizeTestGroupMember(oData.idTestGroup, oNewMember.idTestCase); + self._insertTestGroupMember(uidAuthor, oNewMember); + + # Expire members that have been removed. + sQuery = self._oDb.formatBindArgs('UPDATE TestGroupMembers\n' + 'SET tsExpire = CURRENT_TIMESTAMP\n' + 'WHERE idTestGroup = %s\n' + ' AND tsExpire = \'infinity\'::TIMESTAMP\n' + , ( oData.idTestGroup, )); + if dNew: + sQuery += ' AND idTestCase NOT IN (%s)' % (', '.join([str(iKey) for iKey in dNew]),); + self._oDb.execute(sQuery); + + self._oDb.maybeCommit(fCommit); + return True; + + def removeEntry(self, uidAuthor, idTestGroup, fCascade = False, fCommit = False): + """ + Deletes a test group. + """ + _ = uidAuthor; ## @todo record uidAuthor. + + # + # Cascade. + # + if fCascade is not True: + self._oDb.execute('SELECT SchedGroups.idSchedGroup, SchedGroups.sName\n' + 'FROM SchedGroupMembers, SchedGroups\n' + 'WHERE SchedGroupMembers.idTestGroup = %s\n' + ' AND SchedGroupMembers.tsExpire = \'infinity\'::TIMESTAMP\n' + ' AND SchedGroups.idSchedGroup = SchedGroupMembers.idSchedGroup\n' + ' AND SchedGroups.tsExpire = \'infinity\'::TIMESTAMP\n' + , ( idTestGroup, )); + aoGroups = self._oDb.fetchAll(); + if aoGroups: + asGroups = ['%s (#%d)' % (sName, idSchedGroup) for idSchedGroup, sName in aoGroups]; + raise TMRowInUse('Test group #%d is member of one or more scheduling groups: %s' + % (idTestGroup, ', '.join(asGroups),)); + else: + self._oDb.execute('UPDATE SchedGroupMembers\n' + 'SET tsExpire = CURRENT_TIMESTAMP\n' + 'WHERE idTestGroup = %s\n' + ' AND tsExpire = \'infinity\'::TIMESTAMP\n' + , ( idTestGroup, )); + + # + # Remove the group. + # + self._oDb.execute('UPDATE TestGroupMembers\n' + 'SET tsExpire = CURRENT_TIMESTAMP\n' + 'WHERE idTestGroup = %s\n' + ' AND tsExpire = \'infinity\'::TIMESTAMP\n' + , (idTestGroup,)) + self._oDb.execute('UPDATE TestGroups\n' + 'SET tsExpire = CURRENT_TIMESTAMP\n' + 'WHERE idTestGroup = %s\n' + ' AND tsExpire = \'infinity\'::TIMESTAMP\n' + , (idTestGroup,)) + + self._oDb.maybeCommit(fCommit) + return True; + + def cachedLookup(self, idTestGroup): + """ + Looks up the most recent TestGroupDataEx object for idTestGroup + via an object cache. + + Returns a shared TestGroupDataEx object. None if not found. + Raises exception on DB error. + """ + if self.dCache is None: + self.dCache = self._oDb.getCache('TestGroupDataEx'); + oEntry = self.dCache.get(idTestGroup, None); + if oEntry is None: + fNeedTsNow = False; + self._oDb.execute('SELECT *\n' + 'FROM TestGroups\n' + 'WHERE idTestGroup = %s\n' + ' AND tsExpire = \'infinity\'::TIMESTAMP\n' + , (idTestGroup, )); + if self._oDb.getRowCount() == 0: + # Maybe it was deleted, try get the last entry. + self._oDb.execute('SELECT *\n' + 'FROM TestGroups\n' + 'WHERE idTestGroup = %s\n' + 'ORDER BY tsExpire DESC\n' + 'LIMIT 1\n' + , (idTestGroup, )); + fNeedTsNow = True; + elif self._oDb.getRowCount() > 1: + raise self._oDb.integrityException('%s infinity rows for %s' % (self._oDb.getRowCount(), idTestGroup)); + + if self._oDb.getRowCount() == 1: + aaoRow = self._oDb.fetchOne(); + oEntry = TestGroupDataEx(); + tsNow = oEntry.initFromDbRow(aaoRow).tsEffective if fNeedTsNow else None; + oEntry.initFromDbRowEx(aaoRow, self._oDb, tsNow); + self.dCache[idTestGroup] = oEntry; + return oEntry; + + + # + # Other methods. + # + + def fetchOrderedByName(self, tsNow = None): + """ + Return list of objects of type TestGroupData ordered by name. + May raise exception on database error. + """ + if tsNow is None: + self._oDb.execute('SELECT *\n' + 'FROM TestGroups\n' + 'WHERE tsExpire = \'infinity\'::TIMESTAMP\n' + 'ORDER BY sName ASC\n'); + else: + self._oDb.execute('SELECT *\n' + 'FROM TestGroups\n' + 'WHERE tsExpire > %s\n' + ' AND tsEffective <= %s\n' + 'ORDER BY sName ASC\n' + , (tsNow, tsNow,)); + aoRet = [] + for _ in range(self._oDb.getRowCount()): + aoRet.append(TestGroupData().initFromDbRow(self._oDb.fetchOne())); + return aoRet; + + def getMembers(self, idTestGroup): + """ + Fetches all test case records from DB which are + belong to current Test Group. + Returns list of objects of type TestGroupMemberData2 (!). + """ + self._oDb.execute('SELECT TestCases.*,\n' + ' TestGroupMembers.idTestGroup,\n' + ' TestGroupMembers.aidTestCaseArgs\n' + 'FROM TestCases, TestGroupMembers\n' + 'WHERE TestCases.tsExpire = \'infinity\'::TIMESTAMP\n' + ' AND TestGroupMembers.tsExpire = \'infinity\'::TIMESTAMP\n' + ' AND TestGroupMembers.idTestCase = TestCases.idTestCase\n' + ' AND TestGroupMembers.idTestGroup = %s\n' + 'ORDER BY TestCases.idTestCase ASC;', + (idTestGroup,)) + + aaoRows = self._oDb.fetchAll() + aoRet = [] + for aoRow in aaoRows: + aoRet.append(TestGroupMemberData2().initFromDbRowEx(aoRow)) + + return aoRet + + def getAll(self, tsNow=None): + """Return list of objects of type TestGroupData""" + + if tsNow is None: + self._oDb.execute('SELECT *\n' + 'FROM TestGroups\n' + 'WHERE tsExpire = \'infinity\'::TIMESTAMP\n' + 'ORDER BY idTestGroup ASC;') + else: + self._oDb.execute('SELECT *\n' + 'FROM TestGroups\n' + 'WHERE tsExpire > %s\n' + ' AND tsEffective <= %s\n' + 'ORDER BY idTestGroup ASC;', + (tsNow, tsNow)) + + aaoRows = self._oDb.fetchAll() + aoRet = [] + for aoRow in aaoRows: + aoRet.append(TestGroupData().initFromDbRow(aoRow)) + + return aoRet + + def getById(self, idTestGroup, tsNow=None): + """Get Test Group data by its ID""" + + if tsNow is None: + self._oDb.execute('SELECT *\n' + 'FROM TestGroups\n' + 'WHERE tsExpire = \'infinity\'::timestamp\n' + ' AND idTestGroup = %s\n' + 'ORDER BY idTestGroup ASC;' + , (idTestGroup,)) + else: + self._oDb.execute('SELECT *\n' + 'FROM TestGroups\n' + 'WHERE tsExpire > %s\n' + ' AND tsEffective <= %s\n' + ' AND idTestGroup = %s\n' + 'ORDER BY idTestGroup ASC;' + , (tsNow, tsNow, idTestGroup)) + + aRows = self._oDb.fetchAll() + if len(aRows) not in (0, 1): + raise TMTooManyRows('Found more than one test groups with the same credentials. Database structure is corrupted.') + try: + return TestGroupData().initFromDbRow(aRows[0]) + except IndexError: + return None + + # + # Helpers. + # + + def _assertUniq(self, oData, idTestGroupIgnore): + """ Checks that the test group name is unique, raises exception if it isn't. """ + self._oDb.execute('SELECT idTestGroup\n' + 'FROM TestGroups\n' + 'WHERE sName = %s\n' + ' AND tsExpire = \'infinity\'::TIMESTAMP\n' + + ('' if idTestGroupIgnore is None else ' AND idTestGroup <> %d\n' % (idTestGroupIgnore,)) + , ( oData.sName, )) + if self._oDb.getRowCount() > 0: + raise TMRowAlreadyExists('A Test group with name "%s" already exist.' % (oData.sName,)); + return True; + + def _historizeTestGroup(self, idTestGroup): + """ Historize Test Group record. """ + self._oDb.execute('UPDATE TestGroups\n' + 'SET tsExpire = CURRENT_TIMESTAMP\n' + 'WHERE idTestGroup = %s\n' + ' AND tsExpire = \'infinity\'::TIMESTAMP\n' + , ( idTestGroup, )); + return True; + + def _historizeTestGroupMember(self, idTestGroup, idTestCase): + """ Historize Test Group Member record. """ + self._oDb.execute('UPDATE TestGroupMembers\n' + 'SET tsExpire = CURRENT_TIMESTAMP\n' + 'WHERE idTestGroup = %s\n' + ' AND idTestCase = %s\n' + ' AND tsExpire = \'infinity\'::timestamp\n' + , (idTestGroup, idTestCase,)); + return True; + + def _insertTestGroupMember(self, uidAuthor, oMember): + """ Inserts a test group member. """ + self._oDb.execute('INSERT INTO TestGroupMembers\n' + ' (uidAuthor, idTestGroup, idTestCase, iSchedPriority, aidTestCaseArgs)\n' + 'VALUES (%s, %s, %s, %s, %s)\n' + , ( uidAuthor, + oMember.idTestGroup, + oMember.idTestCase, + oMember.iSchedPriority, + oMember.aidTestCaseArgs, )); + return True; + + + +# +# Unit testing. +# + +# pylint: disable=missing-docstring +class TestGroupMemberDataTestCase(ModelDataBaseTestCase): + def setUp(self): + self.aoSamples = [TestGroupMemberData(),]; + +class TestGroupDataTestCase(ModelDataBaseTestCase): + def setUp(self): + self.aoSamples = [TestGroupData(),]; + +if __name__ == '__main__': + unittest.main(); + # not reached. + diff --git a/src/VBox/ValidationKit/testmanager/core/testresultfailures.py b/src/VBox/ValidationKit/testmanager/core/testresultfailures.py new file mode 100755 index 00000000..b94ff7d5 --- /dev/null +++ b/src/VBox/ValidationKit/testmanager/core/testresultfailures.py @@ -0,0 +1,529 @@ +# -*- coding: utf-8 -*- +# $Id: testresultfailures.py $ +# pylint: disable=too-many-lines + +## @todo Rename this file to testresult.py! + +""" +Test Manager - Test result failures. +""" + +__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 sys; +import unittest; + +# Validation Kit imports. +from testmanager.core.base import ModelDataBase, ModelLogicBase, ModelDataBaseTestCase, TMInvalidData, TMRowNotFound, \ + TMRowAlreadyExists, ChangeLogEntry, AttributeChangeEntry; +from testmanager.core.failurereason import FailureReasonData; +from testmanager.core.useraccount import UserAccountLogic; + +# Python 3 hacks: +if sys.version_info[0] >= 3: + xrange = range; # pylint: disable=redefined-builtin,invalid-name + + +class TestResultFailureData(ModelDataBase): + """ + Test result failure reason data. + """ + + ksIdAttr = 'idTestResult'; + kfIdAttrIsForForeign = True; # Modifies the 'add' validation. + + ksParam_idTestResult = 'TestResultFailure_idTestResult'; + ksParam_tsEffective = 'TestResultFailure_tsEffective'; + ksParam_tsExpire = 'TestResultFailure_tsExpire'; + ksParam_uidAuthor = 'TestResultFailure_uidAuthor'; + ksParam_idTestSet = 'TestResultFailure_idTestSet'; + ksParam_idFailureReason = 'TestResultFailure_idFailureReason'; + ksParam_sComment = 'TestResultFailure_sComment'; + + kasAllowNullAttributes = ['tsEffective', 'tsExpire', 'uidAuthor', 'sComment', 'idTestSet' ]; + + kcDbColumns = 7; + + def __init__(self): + ModelDataBase.__init__(self) + self.idTestResult = None; + self.tsEffective = None; + self.tsExpire = None; + self.uidAuthor = None; + self.idTestSet = None; + self.idFailureReason = None; + self.sComment = None; + + def initFromDbRow(self, aoRow): + """ + Reinitialize from a SELECT * FROM TestResultFailures. + Return self. Raises exception if no row. + """ + if aoRow is None: + raise TMRowNotFound('Test result file record not found.') + + self.idTestResult = aoRow[0]; + self.tsEffective = aoRow[1]; + self.tsExpire = aoRow[2]; + self.uidAuthor = aoRow[3]; + self.idTestSet = aoRow[4]; + self.idFailureReason = aoRow[5]; + self.sComment = aoRow[6]; + return self; + + def initFromDbWithId(self, oDb, idTestResult, tsNow = None, sPeriodBack = None): + """ + Initialize the object from the database. + """ + oDb.execute(self.formatSimpleNowAndPeriodQuery(oDb, + 'SELECT *\n' + 'FROM TestResultFailures\n' + 'WHERE idTestResult = %s\n' + , ( idTestResult,), tsNow, sPeriodBack)); + aoRow = oDb.fetchOne() + if aoRow is None: + raise TMRowNotFound('idTestResult=%s not found (tsNow=%s, sPeriodBack=%s)' % (idTestResult, tsNow, sPeriodBack)); + assert len(aoRow) == self.kcDbColumns; + return self.initFromDbRow(aoRow); + + def initFromValues(self, idTestResult, idFailureReason, uidAuthor, + tsExpire = None, tsEffective = None, idTestSet = None, sComment = None): + """ + Initialize from values. + """ + self.idTestResult = idTestResult; + self.tsEffective = tsEffective; + self.tsExpire = tsExpire; + self.uidAuthor = uidAuthor; + self.idTestSet = idTestSet; + self.idFailureReason = idFailureReason; + self.sComment = sComment; + return self; + + + +class TestResultFailureDataEx(TestResultFailureData): + """ + Extends TestResultFailureData by resolving reasons and user. + """ + + def __init__(self): + TestResultFailureData.__init__(self); + self.oFailureReason = None; + self.oAuthor = None; + + def initFromDbRowEx(self, aoRow, oFailureReasonLogic, oUserAccountLogic): + """ + Reinitialize from a SELECT * FROM TestResultFailures. + Return self. Raises exception if no row. + """ + self.initFromDbRow(aoRow); + self.oFailureReason = oFailureReasonLogic.cachedLookup(self.idFailureReason); + self.oAuthor = oUserAccountLogic.cachedLookup(self.uidAuthor); + return self; + + +class TestResultListingData(ModelDataBase): # pylint: disable=too-many-instance-attributes + """ + Test case result data representation for table listing + """ + + def __init__(self): + """Initialize""" + ModelDataBase.__init__(self) + + self.idTestSet = None + + self.idBuildCategory = None; + self.sProduct = None + self.sRepository = None; + self.sBranch = None + self.sType = None + self.idBuild = None; + self.sVersion = None; + self.iRevision = None + + self.sOs = None; + self.sOsVersion = None; + self.sArch = None; + self.sCpuVendor = None; + self.sCpuName = None; + self.cCpus = None; + self.fCpuHwVirt = None; + self.fCpuNestedPaging = None; + self.fCpu64BitGuest = None; + self.idTestBox = None + self.sTestBoxName = None + + self.tsCreated = None + self.tsElapsed = None + self.enmStatus = None + self.cErrors = None; + + self.idTestCase = None + self.sTestCaseName = None + self.sBaseCmd = None + self.sArgs = None + self.sSubName = None; + + self.idBuildTestSuite = None; + self.iRevisionTestSuite = None; + + self.oFailureReason = None; + self.oFailureReasonAssigner = None; + self.tsFailureReasonAssigned = None; + self.sFailureReasonComment = None; + + def initFromDbRowEx(self, aoRow, oFailureReasonLogic, oUserAccountLogic): + """ + Reinitialize from a database query. + Return self. Raises exception if no row. + """ + if aoRow is None: + raise TMRowNotFound('Test result record not found.') + + self.idTestSet = aoRow[0]; + + self.idBuildCategory = aoRow[1]; + self.sProduct = aoRow[2]; + self.sRepository = aoRow[3]; + self.sBranch = aoRow[4]; + self.sType = aoRow[5]; + self.idBuild = aoRow[6]; + self.sVersion = aoRow[7]; + self.iRevision = aoRow[8]; + + self.sOs = aoRow[9]; + self.sOsVersion = aoRow[10]; + self.sArch = aoRow[11]; + self.sCpuVendor = aoRow[12]; + self.sCpuName = aoRow[13]; + self.cCpus = aoRow[14]; + self.fCpuHwVirt = aoRow[15]; + self.fCpuNestedPaging = aoRow[16]; + self.fCpu64BitGuest = aoRow[17]; + self.idTestBox = aoRow[18]; + self.sTestBoxName = aoRow[19]; + + self.tsCreated = aoRow[20]; + self.tsElapsed = aoRow[21]; + self.enmStatus = aoRow[22]; + self.cErrors = aoRow[23]; + + self.idTestCase = aoRow[24]; + self.sTestCaseName = aoRow[25]; + self.sBaseCmd = aoRow[26]; + self.sArgs = aoRow[27]; + self.sSubName = aoRow[28]; + + self.idBuildTestSuite = aoRow[29]; + self.iRevisionTestSuite = aoRow[30]; + + self.oFailureReason = None; + if aoRow[31] is not None: + self.oFailureReason = oFailureReasonLogic.cachedLookup(aoRow[31]); + self.oFailureReasonAssigner = None; + if aoRow[32] is not None: + self.oFailureReasonAssigner = oUserAccountLogic.cachedLookup(aoRow[32]); + self.tsFailureReasonAssigned = aoRow[33]; + self.sFailureReasonComment = aoRow[34]; + + return self + + + +class TestResultFailureLogic(ModelLogicBase): # pylint: disable=too-few-public-methods + """ + Test result failure reason logic. + """ + + def __init__(self, oDb): + ModelLogicBase.__init__(self, oDb) + + def fetchForChangeLog(self, idTestResult, iStart, cMaxRows, tsNow): # pylint: disable=too-many-locals + """ + Fetches change log entries for a failure reason. + + Returns an array of ChangeLogEntry instance and an indicator whether + there are more entries. + Raises exception on error. + """ + + if tsNow is None: + tsNow = self._oDb.getCurrentTimestamp(); + + # 1. Get a list of the changes from both TestResultFailures and assoicated + # FailureReasons. The latter is useful since the failure reason + # description may evolve along side the invidiual failure analysis. + self._oDb.execute('( SELECT trf.tsEffective AS tsEffectiveChangeLog,\n' + ' trf.uidAuthor AS uidAuthorChangeLog,\n' + ' trf.*,\n' + ' fr.*\n' + ' FROM TestResultFailures trf,\n' + ' FailureReasons fr\n' + ' WHERE trf.idTestResult = %s\n' + ' AND trf.tsEffective <= %s\n' + ' AND trf.idFailureReason = fr.idFailureReason\n' + ' AND fr.tsEffective <= trf.tsEffective\n' + ' AND fr.tsExpire > trf.tsEffective\n' + ')\n' + 'UNION\n' + '( SELECT fr.tsEffective AS tsEffectiveChangeLog,\n' + ' fr.uidAuthor AS uidAuthorChangeLog,\n' + ' trf.*,\n' + ' fr.*\n' + ' FROM TestResultFailures trf,\n' + ' FailureReasons fr\n' + ' WHERE trf.idTestResult = %s\n' + ' AND trf.tsEffective <= %s\n' + ' AND trf.idFailureReason = fr.idFailureReason\n' + ' AND fr.tsEffective > trf.tsEffective\n' + ' AND fr.tsEffective < trf.tsExpire\n' + ')\n' + 'ORDER BY tsEffectiveChangeLog DESC\n' + 'LIMIT %s OFFSET %s\n' + , ( idTestResult, tsNow, idTestResult, tsNow, cMaxRows + 1, iStart, )); + + aaoRows = []; + for aoChange in self._oDb.fetchAll(): + oTrf = TestResultFailureDataEx().initFromDbRow(aoChange[2:]); + oFr = FailureReasonData().initFromDbRow(aoChange[(2+TestResultFailureData.kcDbColumns):]); + oTrf.oFailureReason = oFr; + aaoRows.append([aoChange[0], aoChange[1], oTrf, oFr]); + + # 2. Calculate the changes. + oFailureCategoryLogic = None; + aoEntries = []; + for i in xrange(0, len(aaoRows) - 1): + aoNew = aaoRows[i]; + aoOld = aaoRows[i + 1]; + + aoChanges = []; + oNew = aoNew[2]; + oOld = aoOld[2]; + for sAttr in oNew.getDataAttributes(): + if sAttr not in [ 'tsEffective', 'tsExpire', 'uidAuthor', 'oFailureReason', 'oAuthor' ]: + oOldAttr = getattr(oOld, sAttr); + oNewAttr = getattr(oNew, sAttr); + if oOldAttr != oNewAttr: + if sAttr == 'idFailureReason': + oNewAttr = '%s (%s)' % (oNewAttr, oNew.oFailureReason.sShort, ); + oOldAttr = '%s (%s)' % (oOldAttr, oOld.oFailureReason.sShort, ); + aoChanges.append(AttributeChangeEntry(sAttr, oNewAttr, oOldAttr, str(oNewAttr), str(oOldAttr))); + if oOld.idFailureReason == oNew.idFailureReason: + oNew = aoNew[3]; + oOld = aoOld[3]; + for sAttr in oNew.getDataAttributes(): + if sAttr not in [ 'tsEffective', 'tsExpire', 'uidAuthor', ]: + oOldAttr = getattr(oOld, sAttr); + oNewAttr = getattr(oNew, sAttr); + if oOldAttr != oNewAttr: + if sAttr == 'idFailureCategory': + if oFailureCategoryLogic is None: + from testmanager.core.failurecategory import FailureCategoryLogic; + oFailureCategoryLogic = FailureCategoryLogic(self._oDb); + oCat = oFailureCategoryLogic.cachedLookup(oNewAttr); + if oCat is not None: + oNewAttr = '%s (%s)' % (oNewAttr, oCat.sShort, ); + oCat = oFailureCategoryLogic.cachedLookup(oOldAttr); + if oCat is not None: + oOldAttr = '%s (%s)' % (oOldAttr, oCat.sShort, ); + aoChanges.append(AttributeChangeEntry(sAttr, oNewAttr, oOldAttr, str(oNewAttr), str(oOldAttr))); + + + tsExpire = aaoRows[i - 1][0] if i > 0 else aoNew[2].tsExpire; + aoEntries.append(ChangeLogEntry(aoNew[1], None, aoNew[0], tsExpire, aoNew[2], aoOld[2], aoChanges)); + + # If we're at the end of the log, add the initial entry. + if len(aaoRows) <= cMaxRows and aaoRows: + aoNew = aaoRows[-1]; + tsExpire = aaoRows[-1 - 1][0] if len(aaoRows) > 1 else aoNew[2].tsExpire; + aoEntries.append(ChangeLogEntry(aoNew[1], None, aoNew[0], tsExpire, aoNew[2], None, [])); + + return (UserAccountLogic(self._oDb).resolveChangeLogAuthors(aoEntries), len(aaoRows) > cMaxRows); + + + def getById(self, idTestResult): + """Get Test result failure reason data by idTestResult""" + + self._oDb.execute('SELECT *\n' + 'FROM TestResultFailures\n' + 'WHERE tsExpire = \'infinity\'::timestamp\n' + ' AND idTestResult = %s;', (idTestResult,)) + aRows = self._oDb.fetchAll() + if len(aRows) not in (0, 1): + raise self._oDb.integrityException( + 'Found more than one failure reasons with the same credentials. Database structure is corrupted.') + try: + return TestResultFailureData().initFromDbRow(aRows[0]) + except IndexError: + return None + + def addEntry(self, oData, uidAuthor, fCommit = False): + """ + Add a test result failure reason record. + """ + + # + # Validate inputs and read in the old(/current) data. + # + assert isinstance(oData, TestResultFailureData); + dErrors = oData.validateAndConvert(self._oDb, oData.ksValidateFor_AddForeignId); + if dErrors: + raise TMInvalidData('editEntry invalid input: %s' % (dErrors,)); + + # Check if it exist first (we're adding, not editing, collisions not allowed). + oOldData = self.getById(oData.idTestResult); + if oOldData is not None: + raise TMRowAlreadyExists('TestResult %d already have a failure reason associated with it:' + '%s\n' + 'Perhaps someone else beat you to it? Or did you try resubmit?' + % (oData.idTestResult, oOldData)); + oData = self._resolveSetTestIdIfMissing(oData); + + # + # Add record. + # + self._readdEntry(uidAuthor, oData); + self._oDb.maybeCommit(fCommit); + return True; + + def editEntry(self, oData, uidAuthor, fCommit = False): + """ + Modifies a test result failure reason. + """ + + # + # Validate inputs and read in the old(/current) data. + # + assert isinstance(oData, TestResultFailureData); + dErrors = oData.validateAndConvert(self._oDb, oData.ksValidateFor_Edit); + if dErrors: + raise TMInvalidData('editEntry invalid input: %s' % (dErrors,)); + + oOldData = self.getById(oData.idTestResult) + oData.idTestSet = oOldData.idTestSet; + + # + # Update the data that needs updating. + # + if not oData.isEqualEx(oOldData, [ 'tsEffective', 'tsExpire', 'uidAuthor', ]): + self._historizeEntry(oData.idTestResult); + self._readdEntry(uidAuthor, oData); + self._oDb.maybeCommit(fCommit); + return True; + + + def removeEntry(self, uidAuthor, idTestResult, fCascade = False, fCommit = False): + """ + Deletes a test result failure reason. + """ + _ = fCascade; # Not applicable. + + oData = self.getById(idTestResult) + (tsCur, tsCurMinusOne) = self._oDb.getCurrentTimestamps(); + if oData.tsEffective not in (tsCur, tsCurMinusOne): + self._historizeEntry(idTestResult, tsCurMinusOne); + self._readdEntry(uidAuthor, oData, tsCurMinusOne); + self._historizeEntry(idTestResult); + self._oDb.execute('UPDATE TestResultFailures\n' + 'SET tsExpire = CURRENT_TIMESTAMP\n' + 'WHERE idTestResult = %s\n' + ' AND tsExpire = \'infinity\'::TIMESTAMP\n' + , (idTestResult,)); + self._oDb.maybeCommit(fCommit); + return True; + + # + # Helpers. + # + + def _readdEntry(self, uidAuthor, oData, tsEffective = None): + """ + Re-adds the TestResultFailure entry. Used by addEntry, editEntry and removeEntry. + """ + if tsEffective is None: + tsEffective = self._oDb.getCurrentTimestamp(); + self._oDb.execute('INSERT INTO TestResultFailures (\n' + ' uidAuthor,\n' + ' tsEffective,\n' + ' idTestResult,\n' + ' idTestSet,\n' + ' idFailureReason,\n' + ' sComment)\n' + 'VALUES (%s, %s, %s, %s, %s, %s)\n' + , ( uidAuthor, + tsEffective, + oData.idTestResult, + oData.idTestSet, + oData.idFailureReason, + oData.sComment,) ); + return True; + + + def _historizeEntry(self, idTestResult, tsExpire = None): + """ Historizes the current entry. """ + if tsExpire is None: + tsExpire = self._oDb.getCurrentTimestamp(); + self._oDb.execute('UPDATE TestResultFailures\n' + 'SET tsExpire = %s\n' + 'WHERE idTestResult = %s\n' + ' AND tsExpire = \'infinity\'::TIMESTAMP\n' + , (tsExpire, idTestResult,)); + return True; + + + def _resolveSetTestIdIfMissing(self, oData): + """ Resolve any missing idTestSet reference (it's a duplicate for speed efficiency). """ + if oData.idTestSet is None and oData.idTestResult is not None: + self._oDb.execute('SELECT idTestSet FROM TestResults WHERE idTestResult = %s', (oData.idTestResult,)); + oData.idTestSet = self._oDb.fetchOne()[0]; + return oData; + + + +# +# Unit testing. +# + +# pylint: disable=missing-docstring +class TestResultFailureDataTestCase(ModelDataBaseTestCase): + def setUp(self): + self.aoSamples = [TestResultFailureData(),]; + +if __name__ == '__main__': + unittest.main(); + # not reached. + diff --git a/src/VBox/ValidationKit/testmanager/core/testresults.py b/src/VBox/ValidationKit/testmanager/core/testresults.py new file mode 100755 index 00000000..58f056d2 --- /dev/null +++ b/src/VBox/ValidationKit/testmanager/core/testresults.py @@ -0,0 +1,2926 @@ +# -*- coding: utf-8 -*- +# $Id: testresults.py $ +# pylint: disable=too-many-lines + +## @todo Rename this file to testresult.py! + +""" +Test Manager - Fetch test results. +""" + +__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 sys; +import unittest; + +# Validation Kit imports. +from common import constants; +from testmanager import config; +from testmanager.core.base import ModelDataBase, ModelLogicBase, ModelDataBaseTestCase, ModelFilterBase, \ + FilterCriterion, FilterCriterionValueAndDescription, \ + TMExceptionBase, TMTooManyRows, TMRowNotFound; +from testmanager.core.testgroup import TestGroupData; +from testmanager.core.build import BuildDataEx, BuildCategoryData; +from testmanager.core.failurereason import FailureReasonLogic; +from testmanager.core.testbox import TestBoxData, TestBoxLogic; +from testmanager.core.testcase import TestCaseData; +from testmanager.core.schedgroup import SchedGroupData, SchedGroupLogic; +from testmanager.core.systemlog import SystemLogData, SystemLogLogic; +from testmanager.core.testresultfailures import TestResultFailureDataEx; +from testmanager.core.useraccount import UserAccountLogic; + +# Python 3 hacks: +if sys.version_info[0] >= 3: + long = int; # pylint: disable=redefined-builtin,invalid-name + + +class TestResultData(ModelDataBase): + """ + Test case execution result data + """ + + ## @name TestStatus_T + # @{ + ksTestStatus_Running = 'running'; + ksTestStatus_Success = 'success'; + ksTestStatus_Skipped = 'skipped'; + ksTestStatus_BadTestBox = 'bad-testbox'; + ksTestStatus_Aborted = 'aborted'; + ksTestStatus_Failure = 'failure'; + ksTestStatus_TimedOut = 'timed-out'; + ksTestStatus_Rebooted = 'rebooted'; + ## @} + + ## List of relatively harmless (to testgroup/case) statuses. + kasHarmlessTestStatuses = [ ksTestStatus_Skipped, ksTestStatus_BadTestBox, ksTestStatus_Aborted, ]; + ## List of bad statuses. + kasBadTestStatuses = [ ksTestStatus_Failure, ksTestStatus_TimedOut, ksTestStatus_Rebooted, ]; + + + ksIdAttr = 'idTestResult'; + + ksParam_idTestResult = 'TestResultData_idTestResult'; + ksParam_idTestResultParent = 'TestResultData_idTestResultParent'; + ksParam_idTestSet = 'TestResultData_idTestSet'; + ksParam_tsCreated = 'TestResultData_tsCreated'; + ksParam_tsElapsed = 'TestResultData_tsElapsed'; + ksParam_idStrName = 'TestResultData_idStrName'; + ksParam_cErrors = 'TestResultData_cErrors'; + ksParam_enmStatus = 'TestResultData_enmStatus'; + ksParam_iNestingDepth = 'TestResultData_iNestingDepth'; + kasValidValues_enmStatus = [ + ksTestStatus_Running, + ksTestStatus_Success, + ksTestStatus_Skipped, + ksTestStatus_BadTestBox, + ksTestStatus_Aborted, + ksTestStatus_Failure, + ksTestStatus_TimedOut, + ksTestStatus_Rebooted + ]; + + + def __init__(self): + ModelDataBase.__init__(self) + self.idTestResult = None + self.idTestResultParent = None + self.idTestSet = None + self.tsCreated = None + self.tsElapsed = None + self.idStrName = None + self.cErrors = 0; + self.enmStatus = None + self.iNestingDepth = None + + def initFromDbRow(self, aoRow): + """ + Reinitialize from a SELECT * FROM TestResults. + Return self. Raises exception if no row. + """ + if aoRow is None: + raise TMRowNotFound('Test result record not found.') + + self.idTestResult = aoRow[0] + self.idTestResultParent = aoRow[1] + self.idTestSet = aoRow[2] + self.tsCreated = aoRow[3] + self.tsElapsed = aoRow[4] + self.idStrName = aoRow[5] + self.cErrors = aoRow[6] + self.enmStatus = aoRow[7] + self.iNestingDepth = aoRow[8] + return self; + + def initFromDbWithId(self, oDb, idTestResult, tsNow = None, sPeriodBack = None): + """ + Initialize from the database, given the ID of a row. + """ + _ = tsNow; + _ = sPeriodBack; + oDb.execute('SELECT *\n' + 'FROM TestResults\n' + 'WHERE idTestResult = %s\n' + , ( idTestResult,)); + aoRow = oDb.fetchOne() + if aoRow is None: + raise TMRowNotFound('idTestResult=%s not found' % (idTestResult,)); + return self.initFromDbRow(aoRow); + + def isFailure(self): + """ Check if it's a real failure. """ + return self.enmStatus in self.kasBadTestStatuses; + + +class TestResultDataEx(TestResultData): + """ + Extended test result data class. + + This is intended for use as a node in a result tree. This is not intended + for serialization to parameters or vice versa. Use TestResultLogic to + construct the tree. + """ + + def __init__(self): + TestResultData.__init__(self) + self.sName = None; # idStrName resolved. + self.oParent = None; # idTestResultParent within the tree. + + self.aoChildren = []; # TestResultDataEx; + self.aoValues = []; # TestResultValueDataEx; + self.aoMsgs = []; # TestResultMsgDataEx; + self.aoFiles = []; # TestResultFileDataEx; + self.oReason = None; # TestResultReasonDataEx; + + def initFromDbRow(self, aoRow): + """ + Initialize from a query like this: + SELECT TestResults.*, TestResultStrTab.sValue + FROM TestResults, TestResultStrTab + WHERE TestResultStrTab.idStr = TestResults.idStrName + + Note! The caller is expected to fetch children, values, failure + details, and files. + """ + self.sName = None; + self.oParent = None; + self.aoChildren = []; + self.aoValues = []; + self.aoMsgs = []; + self.aoFiles = []; + self.oReason = None; + + TestResultData.initFromDbRow(self, aoRow); + + self.sName = aoRow[9]; + return self; + + def deepCountErrorContributers(self): + """ + Counts how many test result instances actually contributed to cErrors. + """ + + # Check each child (if any). + cChanges = 0; + cChildErrors = 0; + for oChild in self.aoChildren: + if oChild.cErrors > 0: + cChildErrors += oChild.cErrors; + cChanges += oChild.deepCountErrorContributers(); + + # Did we contribute as well? + if self.cErrors > cChildErrors: + cChanges += 1; + return cChanges; + + def getListOfFailures(self): + """ + Get a list of test results instances actually contributing to cErrors. + + Returns a list of TestResultDataEx instances from this tree. (shared!) + """ + # Check each child (if any). + aoRet = []; + cChildErrors = 0; + for oChild in self.aoChildren: + if oChild.cErrors > 0: + cChildErrors += oChild.cErrors; + aoRet.extend(oChild.getListOfFailures()); + + # Did we contribute as well? + if self.cErrors > cChildErrors: + aoRet.append(self); + + return aoRet; + + def getListOfLogFilesByKind(self, asKinds): + """ + Get a list of test results instances actually contributing to cErrors. + + Returns a list of TestResultFileDataEx instances from this tree. (shared!) + """ + aoRet = []; + + # Check the children first. + for oChild in self.aoChildren: + aoRet.extend(oChild.getListOfLogFilesByKind(asKinds)); + + # Check our own files next. + for oFile in self.aoFiles: + if oFile.sKind in asKinds: + aoRet.append(oFile); + + return aoRet; + + def getFullName(self): + """ Constructs the full name of this test result. """ + if self.oParent is None: + return self.sName; + return self.oParent.getFullName() + ' / ' + self.sName; + + + +class TestResultValueData(ModelDataBase): + """ + Test result value data. + """ + + ksIdAttr = 'idTestResultValue'; + + ksParam_idTestResultValue = 'TestResultValue_idTestResultValue'; + ksParam_idTestResult = 'TestResultValue_idTestResult'; + ksParam_idTestSet = 'TestResultValue_idTestSet'; + ksParam_tsCreated = 'TestResultValue_tsCreated'; + ksParam_idStrName = 'TestResultValue_idStrName'; + ksParam_lValue = 'TestResultValue_lValue'; + ksParam_iUnit = 'TestResultValue_iUnit'; + + kasAllowNullAttributes = [ 'idTestSet', ]; + + def __init__(self): + ModelDataBase.__init__(self) + self.idTestResultValue = None; + self.idTestResult = None; + self.idTestSet = None; + self.tsCreated = None; + self.idStrName = None; + self.lValue = None; + self.iUnit = 0; + + def initFromDbRow(self, aoRow): + """ + Reinitialize from a SELECT * FROM TestResultValues. + Return self. Raises exception if no row. + """ + if aoRow is None: + raise TMRowNotFound('Test result value record not found.') + + self.idTestResultValue = aoRow[0]; + self.idTestResult = aoRow[1]; + self.idTestSet = aoRow[2]; + self.tsCreated = aoRow[3]; + self.idStrName = aoRow[4]; + self.lValue = aoRow[5]; + self.iUnit = aoRow[6]; + return self; + + +class TestResultValueDataEx(TestResultValueData): + """ + Extends TestResultValue by resolving the value name and unit string. + """ + + def __init__(self): + TestResultValueData.__init__(self) + self.sName = None; + self.sUnit = ''; + + def initFromDbRow(self, aoRow): + """ + Reinitialize from a query like this: + SELECT TestResultValues.*, TestResultStrTab.sValue + FROM TestResultValues, TestResultStrTab + WHERE TestResultStrTab.idStr = TestResultValues.idStrName + + Return self. Raises exception if no row. + """ + TestResultValueData.initFromDbRow(self, aoRow); + self.sName = aoRow[7]; + if self.iUnit < len(constants.valueunit.g_asNames): + self.sUnit = constants.valueunit.g_asNames[self.iUnit]; + else: + self.sUnit = '<%d>' % (self.iUnit,); + return self; + +class TestResultMsgData(ModelDataBase): + """ + Test result message data. + """ + + ksIdAttr = 'idTestResultMsg'; + + ksParam_idTestResultMsg = 'TestResultValue_idTestResultMsg'; + ksParam_idTestResult = 'TestResultValue_idTestResult'; + ksParam_idTestSet = 'TestResultValue_idTestSet'; + ksParam_tsCreated = 'TestResultValue_tsCreated'; + ksParam_idStrMsg = 'TestResultValue_idStrMsg'; + ksParam_enmLevel = 'TestResultValue_enmLevel'; + + kasAllowNullAttributes = [ 'idTestSet', ]; + + kcDbColumns = 6 + + def __init__(self): + ModelDataBase.__init__(self) + self.idTestResultMsg = None; + self.idTestResult = None; + self.idTestSet = None; + self.tsCreated = None; + self.idStrMsg = None; + self.enmLevel = None; + + def initFromDbRow(self, aoRow): + """ + Reinitialize from a SELECT * FROM TestResultMsgs. + Return self. Raises exception if no row. + """ + if aoRow is None: + raise TMRowNotFound('Test result value record not found.') + + self.idTestResultMsg = aoRow[0]; + self.idTestResult = aoRow[1]; + self.idTestSet = aoRow[2]; + self.tsCreated = aoRow[3]; + self.idStrMsg = aoRow[4]; + self.enmLevel = aoRow[5]; + return self; + +class TestResultMsgDataEx(TestResultMsgData): + """ + Extends TestResultMsg by resolving the message string. + """ + + def __init__(self): + TestResultMsgData.__init__(self) + self.sMsg = None; + + def initFromDbRow(self, aoRow): + """ + Reinitialize from a query like this: + SELECT TestResultMsg.*, TestResultStrTab.sValue + FROM TestResultMsg, TestResultStrTab + WHERE TestResultStrTab.idStr = TestResultMsgs.idStrName + + Return self. Raises exception if no row. + """ + TestResultMsgData.initFromDbRow(self, aoRow); + self.sMsg = aoRow[self.kcDbColumns]; + return self; + + +class TestResultFileData(ModelDataBase): + """ + Test result message data. + """ + + ksIdAttr = 'idTestResultFile'; + + ksParam_idTestResultFile = 'TestResultFile_idTestResultFile'; + ksParam_idTestResult = 'TestResultFile_idTestResult'; + ksParam_tsCreated = 'TestResultFile_tsCreated'; + ksParam_idStrFile = 'TestResultFile_idStrFile'; + ksParam_idStrDescription = 'TestResultFile_idStrDescription'; + ksParam_idStrKind = 'TestResultFile_idStrKind'; + ksParam_idStrMime = 'TestResultFile_idStrMime'; + + ## @name Kind of files. + ## @{ + ksKind_LogReleaseVm = 'log/release/vm'; + ksKind_LogDebugVm = 'log/debug/vm'; + ksKind_LogReleaseSvc = 'log/release/svc'; + ksKind_LogDebugSvc = 'log/debug/svc'; + ksKind_LogReleaseClient = 'log/release/client'; + ksKind_LogDebugClient = 'log/debug/client'; + ksKind_LogInstaller = 'log/installer'; + ksKind_LogUninstaller = 'log/uninstaller'; + ksKind_LogGuestKernel = 'log/guest/kernel'; + ksKind_ProcessReportVm = 'process/report/vm'; + ksKind_CrashReportVm = 'crash/report/vm'; + ksKind_CrashDumpVm = 'crash/dump/vm'; + ksKind_CrashReportSvc = 'crash/report/svc'; + ksKind_CrashDumpSvc = 'crash/dump/svc'; + ksKind_CrashReportClient = 'crash/report/client'; + ksKind_CrashDumpClient = 'crash/dump/client'; + ksKind_InfoCollection = 'info/collection'; + ksKind_InfoVgaText = 'info/vgatext'; + ksKind_MiscOther = 'misc/other'; + ksKind_ScreenshotFailure = 'screenshot/failure'; + ksKind_ScreenshotSuccesss = 'screenshot/success'; + ksKind_ScreenRecordingFailure = 'screenrecording/failure'; + ksKind_ScreenRecordingSuccess = 'screenrecording/success'; + ## @} + + kasKinds = [ + ksKind_LogReleaseVm, + ksKind_LogDebugVm, + ksKind_LogReleaseSvc, + ksKind_LogDebugSvc, + ksKind_LogReleaseClient, + ksKind_LogDebugClient, + ksKind_LogInstaller, + ksKind_LogUninstaller, + ksKind_LogGuestKernel, + ksKind_ProcessReportVm, + ksKind_CrashReportVm, + ksKind_CrashDumpVm, + ksKind_CrashReportSvc, + ksKind_CrashDumpSvc, + ksKind_CrashReportClient, + ksKind_CrashDumpClient, + ksKind_InfoCollection, + ksKind_InfoVgaText, + ksKind_MiscOther, + ksKind_ScreenshotFailure, + ksKind_ScreenshotSuccesss, + ksKind_ScreenRecordingFailure, + ksKind_ScreenRecordingSuccess, + ]; + + kasAllowNullAttributes = [ 'idTestSet', ]; + + kcDbColumns = 8 + + def __init__(self): + ModelDataBase.__init__(self) + self.idTestResultFile = None; + self.idTestResult = None; + self.idTestSet = None; + self.tsCreated = None; + self.idStrFile = None; + self.idStrDescription = None; + self.idStrKind = None; + self.idStrMime = None; + + def initFromDbRow(self, aoRow): + """ + Reinitialize from a SELECT * FROM TestResultFiles. + Return self. Raises exception if no row. + """ + if aoRow is None: + raise TMRowNotFound('Test result file record not found.') + + self.idTestResultFile = aoRow[0]; + self.idTestResult = aoRow[1]; + self.idTestSet = aoRow[2]; + self.tsCreated = aoRow[3]; + self.idStrFile = aoRow[4]; + self.idStrDescription = aoRow[5]; + self.idStrKind = aoRow[6]; + self.idStrMime = aoRow[7]; + return self; + +class TestResultFileDataEx(TestResultFileData): + """ + Extends TestResultFile by resolving the strings. + """ + + def __init__(self): + TestResultFileData.__init__(self) + self.sFile = None; + self.sDescription = None; + self.sKind = None; + self.sMime = None; + + def initFromDbRow(self, aoRow): + """ + Reinitialize from a query like this: + SELECT TestResultFiles.*, + StrTabFile.sValue AS sFile, + StrTabDesc.sValue AS sDescription + StrTabKind.sValue AS sKind, + StrTabMime.sValue AS sMime, + FROM ... + + Return self. Raises exception if no row. + """ + TestResultFileData.initFromDbRow(self, aoRow); + self.sFile = aoRow[self.kcDbColumns]; + self.sDescription = aoRow[self.kcDbColumns + 1]; + self.sKind = aoRow[self.kcDbColumns + 2]; + self.sMime = aoRow[self.kcDbColumns + 3]; + return self; + + def initFakeMainLog(self, oTestSet): + """ + Reinitializes to represent the main.log object (not in DB). + + Returns self. + """ + self.idTestResultFile = 0; + self.idTestResult = oTestSet.idTestResult; + self.tsCreated = oTestSet.tsCreated; + self.idStrFile = None; + self.idStrDescription = None; + self.idStrKind = None; + self.idStrMime = None; + + self.sFile = 'main.log'; + self.sDescription = ''; + self.sKind = 'log/main'; + self.sMime = 'text/plain'; + return self; + + def isProbablyUtf8Encoded(self): + """ + Checks if the file is likely to be UTF-8 encoded. + """ + if self.sMime in [ 'text/plain', 'text/html' ]: + return True; + return False; + + def getMimeWithEncoding(self): + """ + Gets the MIME type with encoding if likely to be UTF-8. + """ + if self.isProbablyUtf8Encoded(): + return '%s; charset=utf-8' % (self.sMime,); + return self.sMime; + + + +class TestResultListingData(ModelDataBase): # pylint: disable=too-many-instance-attributes + """ + Test case result data representation for table listing + """ + + class FailureReasonListingData(object): + """ Failure reason listing data """ + def __init__(self): + self.oFailureReason = None; + self.oFailureReasonAssigner = None; + self.tsFailureReasonAssigned = None; + self.sFailureReasonComment = None; + + def __init__(self): + """Initialize""" + ModelDataBase.__init__(self) + + self.idTestSet = None + + self.idBuildCategory = None; + self.sProduct = None + self.sRepository = None; + self.sBranch = None + self.sType = None + self.idBuild = None; + self.sVersion = None; + self.iRevision = None + + self.sOs = None; + self.sOsVersion = None; + self.sArch = None; + self.sCpuVendor = None; + self.sCpuName = None; + self.cCpus = None; + self.fCpuHwVirt = None; + self.fCpuNestedPaging = None; + self.fCpu64BitGuest = None; + self.idTestBox = None + self.sTestBoxName = None + + self.tsCreated = None + self.tsElapsed = None + self.enmStatus = None + self.cErrors = None; + + self.idTestCase = None + self.sTestCaseName = None + self.sBaseCmd = None + self.sArgs = None + self.sSubName = None; + + self.idBuildTestSuite = None; + self.iRevisionTestSuite = None; + + self.aoFailureReasons = []; + + def initFromDbRowEx(self, aoRow, oFailureReasonLogic, oUserAccountLogic): + """ + Reinitialize from a database query. + Return self. Raises exception if no row. + """ + if aoRow is None: + raise TMRowNotFound('Test result record not found.') + + self.idTestSet = aoRow[0]; + + self.idBuildCategory = aoRow[1]; + self.sProduct = aoRow[2]; + self.sRepository = aoRow[3]; + self.sBranch = aoRow[4]; + self.sType = aoRow[5]; + self.idBuild = aoRow[6]; + self.sVersion = aoRow[7]; + self.iRevision = aoRow[8]; + + self.sOs = aoRow[9]; + self.sOsVersion = aoRow[10]; + self.sArch = aoRow[11]; + self.sCpuVendor = aoRow[12]; + self.sCpuName = aoRow[13]; + self.cCpus = aoRow[14]; + self.fCpuHwVirt = aoRow[15]; + self.fCpuNestedPaging = aoRow[16]; + self.fCpu64BitGuest = aoRow[17]; + self.idTestBox = aoRow[18]; + self.sTestBoxName = aoRow[19]; + + self.tsCreated = aoRow[20]; + self.tsElapsed = aoRow[21]; + self.enmStatus = aoRow[22]; + self.cErrors = aoRow[23]; + + self.idTestCase = aoRow[24]; + self.sTestCaseName = aoRow[25]; + self.sBaseCmd = aoRow[26]; + self.sArgs = aoRow[27]; + self.sSubName = aoRow[28]; + + self.idBuildTestSuite = aoRow[29]; + self.iRevisionTestSuite = aoRow[30]; + + self.aoFailureReasons = []; + for i, _ in enumerate(aoRow[31]): + if aoRow[31][i] is not None \ + or aoRow[32][i] is not None \ + or aoRow[33][i] is not None \ + or aoRow[34][i] is not None: + oReason = self.FailureReasonListingData(); + if aoRow[31][i] is not None: + oReason.oFailureReason = oFailureReasonLogic.cachedLookup(aoRow[31][i]); + if aoRow[32][i] is not None: + oReason.oFailureReasonAssigner = oUserAccountLogic.cachedLookup(aoRow[32][i]); + oReason.tsFailureReasonAssigned = aoRow[33][i]; + oReason.sFailureReasonComment = aoRow[34][i]; + self.aoFailureReasons.append(oReason); + + return self + + +class TestResultHangingOffence(TMExceptionBase): + """Hanging offence committed by test case.""" + pass; # pylint: disable=unnecessary-pass + + +class TestResultFilter(ModelFilterBase): + """ + Test result filter. + """ + + kiTestStatus = 0; + kiErrorCounts = 1; + kiBranches = 2; + kiBuildTypes = 3; + kiRevisions = 4; + kiRevisionRange = 5; + kiFailReasons = 6; + kiTestCases = 7; + kiTestCaseMisc = 8; + kiTestBoxes = 9; + kiOses = 10; + kiCpuArches = 11; + kiCpuVendors = 12; + kiCpuCounts = 13; + kiMemory = 14; + kiTestboxMisc = 15; + kiPythonVersions = 16; + kiSchedGroups = 17; + + ## Misc test case / variation name filters. + ## Presented in table order. The first sub element is the presistent ID. + kaTcMisc = ( + ( 1, 'x86', ), + ( 2, 'amd64', ), + ( 3, 'uni', ), + ( 4, 'smp', ), + ( 5, 'raw', ), + ( 6, 'hw', ), + ( 7, 'np', ), + ( 8, 'Install', ), + ( 20, 'UInstall', ), # NB. out of order. + ( 9, 'Benchmark', ), + ( 18, 'smoke', ), # NB. out of order. + ( 19, 'unit', ), # NB. out of order. + ( 10, 'USB', ), + ( 11, 'Debian', ), + ( 12, 'Fedora', ), + ( 13, 'Oracle', ), + ( 14, 'RHEL', ), + ( 15, 'SUSE', ), + ( 16, 'Ubuntu', ), + ( 17, 'Win', ), + ); + + kiTbMisc_NestedPaging = 0; + kiTbMisc_NoNestedPaging = 1; + kiTbMisc_RawMode = 2; + kiTbMisc_NoRawMode = 3; + kiTbMisc_64BitGuest = 4; + kiTbMisc_No64BitGuest = 5; + kiTbMisc_HwVirt = 6; + kiTbMisc_NoHwVirt = 7; + kiTbMisc_IoMmu = 8; + kiTbMisc_NoIoMmu = 9; + + def __init__(self): + ModelFilterBase.__init__(self); + + # Test statuses + oCrit = FilterCriterion('Test statuses', sVarNm = 'ts', sType = FilterCriterion.ksType_String, + sTable = 'TestSets', sColumn = 'enmStatus'); + self.aCriteria.append(oCrit); + assert self.aCriteria[self.kiTestStatus] is oCrit; + + # Error counts + oCrit = FilterCriterion('Error counts', sVarNm = 'ec', sTable = 'TestResults', sColumn = 'cErrors'); + self.aCriteria.append(oCrit); + assert self.aCriteria[self.kiErrorCounts] is oCrit; + + # Branches + oCrit = FilterCriterion('Branches', sVarNm = 'br', sType = FilterCriterion.ksType_String, + sTable = 'BuildCategories', sColumn = 'sBranch'); + self.aCriteria.append(oCrit); + assert self.aCriteria[self.kiBranches] is oCrit; + + # Build types + oCrit = FilterCriterion('Build types', sVarNm = 'bt', sType = FilterCriterion.ksType_String, + sTable = 'BuildCategories', sColumn = 'sType'); + self.aCriteria.append(oCrit); + assert self.aCriteria[self.kiBuildTypes] is oCrit; + + # Revisions + oCrit = FilterCriterion('Revisions', sVarNm = 'rv', sTable = 'Builds', sColumn = 'iRevision'); + self.aCriteria.append(oCrit); + assert self.aCriteria[self.kiRevisions] is oCrit; + + # Revision Range + oCrit = FilterCriterion('Revision Range', sVarNm = 'rr', sType = FilterCriterion.ksType_Ranges, + sKind = FilterCriterion.ksKind_ElementOfOrNot, sTable = 'Builds', sColumn = 'iRevision'); + self.aCriteria.append(oCrit); + assert self.aCriteria[self.kiRevisionRange] is oCrit; + + # Failure reasons + oCrit = FilterCriterion('Failure reasons', sVarNm = 'fr', sType = FilterCriterion.ksType_UIntNil, + sTable = 'TestResultFailures', sColumn = 'idFailureReason'); + self.aCriteria.append(oCrit); + assert self.aCriteria[self.kiFailReasons] is oCrit; + + # Test cases and variations. + oCrit = FilterCriterion('Test case / var', sVarNm = 'tc', sTable = 'TestSets', sColumn = 'idTestCase', + oSub = FilterCriterion('Test variations', sVarNm = 'tv', + sTable = 'TestSets', sColumn = 'idTestCaseArgs')); + self.aCriteria.append(oCrit); + assert self.aCriteria[self.kiTestCases] is oCrit; + + # Special test case and varation name sub string matching. + oCrit = FilterCriterion('Test case name', sVarNm = 'cm', sKind = FilterCriterion.ksKind_Special, + asTables = ('TestCases', 'TestCaseArgs')); + oCrit.aoPossible = [ + FilterCriterionValueAndDescription(aoCur[0], 'Include %s' % (aoCur[1],)) for aoCur in self.kaTcMisc + ]; + oCrit.aoPossible.extend([ + FilterCriterionValueAndDescription(aoCur[0] + 32, 'Exclude %s' % (aoCur[1],)) for aoCur in self.kaTcMisc + ]); + self.aCriteria.append(oCrit); + assert self.aCriteria[self.kiTestCaseMisc] is oCrit; + + # Testboxes + oCrit = FilterCriterion('Testboxes', sVarNm = 'tb', sTable = 'TestSets', sColumn = 'idTestBox'); + self.aCriteria.append(oCrit); + assert self.aCriteria[self.kiTestBoxes] is oCrit; + + # Testbox OS and OS version. + oCrit = FilterCriterion('OS / version', sVarNm = 'os', sTable = 'TestBoxesWithStrings', sColumn = 'idStrOs', + oSub = FilterCriterion('OS Versions', sVarNm = 'ov', + sTable = 'TestBoxesWithStrings', sColumn = 'idStrOsVersion')); + self.aCriteria.append(oCrit); + assert self.aCriteria[self.kiOses] is oCrit; + + # Testbox CPU architectures. + oCrit = FilterCriterion('CPU arches', sVarNm = 'ca', sTable = 'TestBoxesWithStrings', sColumn = 'idStrCpuArch'); + self.aCriteria.append(oCrit); + assert self.aCriteria[self.kiCpuArches] is oCrit; + + # Testbox CPU vendors and revisions. + oCrit = FilterCriterion('CPU vendor / rev', sVarNm = 'cv', sTable = 'TestBoxesWithStrings', sColumn = 'idStrCpuVendor', + oSub = FilterCriterion('CPU revisions', sVarNm = 'cr', + sTable = 'TestBoxesWithStrings', sColumn = 'lCpuRevision')); + self.aCriteria.append(oCrit); + assert self.aCriteria[self.kiCpuVendors] is oCrit; + + # Testbox CPU (thread) count + oCrit = FilterCriterion('CPU counts', sVarNm = 'cc', sTable = 'TestBoxesWithStrings', sColumn = 'cCpus'); + self.aCriteria.append(oCrit); + assert self.aCriteria[self.kiCpuCounts] is oCrit; + + # Testbox memory sizes. + oCrit = FilterCriterion('Memory', sVarNm = 'mb', sTable = 'TestBoxesWithStrings', sColumn = 'cMbMemory'); + self.aCriteria.append(oCrit); + assert self.aCriteria[self.kiMemory] is oCrit; + + # Testbox features. + oCrit = FilterCriterion('Testbox features', sVarNm = 'tm', sKind = FilterCriterion.ksKind_Special, + sTable = 'TestBoxesWithStrings'); + oCrit.aoPossible = [ + FilterCriterionValueAndDescription(self.kiTbMisc_NestedPaging, "req nested paging"), + FilterCriterionValueAndDescription(self.kiTbMisc_NoNestedPaging, "w/o nested paging"), + #FilterCriterionValueAndDescription(self.kiTbMisc_RawMode, "req raw-mode"), - not implemented yet. + #FilterCriterionValueAndDescription(self.kiTbMisc_NoRawMode, "w/o raw-mode"), - not implemented yet. + FilterCriterionValueAndDescription(self.kiTbMisc_64BitGuest, "req 64-bit guests"), + FilterCriterionValueAndDescription(self.kiTbMisc_No64BitGuest, "w/o 64-bit guests"), + FilterCriterionValueAndDescription(self.kiTbMisc_HwVirt, "req VT-x / AMD-V"), + FilterCriterionValueAndDescription(self.kiTbMisc_NoHwVirt, "w/o VT-x / AMD-V"), + #FilterCriterionValueAndDescription(self.kiTbMisc_IoMmu, "req I/O MMU"), - not implemented yet. + #FilterCriterionValueAndDescription(self.kiTbMisc_NoIoMmu, "w/o I/O MMU"), - not implemented yet. + ]; + self.aCriteria.append(oCrit); + assert self.aCriteria[self.kiTestboxMisc] is oCrit; + + # Testbox python versions. + oCrit = FilterCriterion('Python', sVarNm = 'py', sTable = 'TestBoxesWithStrings', sColumn = 'iPythonHexVersion'); + self.aCriteria.append(oCrit); + assert self.aCriteria[self.kiPythonVersions] is oCrit; + + # Scheduling groups. + oCrit = FilterCriterion('Sched groups', sVarNm = 'sg', sTable = 'TestSets', sColumn = 'idSchedGroup'); + self.aCriteria.append(oCrit); + assert self.aCriteria[self.kiSchedGroups] is oCrit; + + + kdTbMiscConditions = { + kiTbMisc_NestedPaging: 'TestBoxesWithStrings.fCpuNestedPaging IS TRUE', + kiTbMisc_NoNestedPaging: 'TestBoxesWithStrings.fCpuNestedPaging IS FALSE', + kiTbMisc_RawMode: 'TestBoxesWithStrings.fRawMode IS TRUE', + kiTbMisc_NoRawMode: 'TestBoxesWithStrings.fRawMode IS NOT TRUE', + kiTbMisc_64BitGuest: 'TestBoxesWithStrings.fCpu64BitGuest IS TRUE', + kiTbMisc_No64BitGuest: 'TestBoxesWithStrings.fCpu64BitGuest IS FALSE', + kiTbMisc_HwVirt: 'TestBoxesWithStrings.fCpuHwVirt IS TRUE', + kiTbMisc_NoHwVirt: 'TestBoxesWithStrings.fCpuHwVirt IS FALSE', + kiTbMisc_IoMmu: 'TestBoxesWithStrings.fChipsetIoMmu IS TRUE', + kiTbMisc_NoIoMmu: 'TestBoxesWithStrings.fChipsetIoMmu IS FALSE', + }; + + def _getWhereWorker(self, iCrit, oCrit, sExtraIndent, iOmit): + """ Formats one - main or sub. """ + sQuery = ''; + if oCrit.sState == FilterCriterion.ksState_Selected and iCrit != iOmit: + if iCrit == self.kiTestCaseMisc: + for iValue, sLike in self.kaTcMisc: + if iValue in oCrit.aoSelected: sNot = ''; + elif iValue + 32 in oCrit.aoSelected: sNot = 'NOT '; + else: continue; + sQuery += '%s AND %s (' % (sExtraIndent, sNot,); + if len(sLike) <= 3: # do word matching for small substrings (hw, np, smp, uni, ++). + sQuery += 'TestCases.sName ~ \'.*\\y%s\\y.*\' ' \ + 'OR COALESCE(TestCaseArgs.sSubName, \'\') ~ \'.*\\y%s\\y.*\')\n' \ + % ( sLike, sLike,); + else: + sQuery += 'TestCases.sName LIKE \'%%%s%%\' ' \ + 'OR COALESCE(TestCaseArgs.sSubName, \'\') LIKE \'%%%s%%\')\n' \ + % ( sLike, sLike,); + elif iCrit == self.kiTestboxMisc: + dConditions = self.kdTbMiscConditions; + for iValue in oCrit.aoSelected: + if iValue in dConditions: + sQuery += '%s AND %s\n' % (sExtraIndent, dConditions[iValue],); + elif oCrit.sType == FilterCriterion.ksType_Ranges: + assert not oCrit.aoPossible; + if oCrit.aoSelected: + asConditions = []; + for tRange in oCrit.aoSelected: + if tRange[0] == tRange[1]: + asConditions.append('%s.%s = %s' % (oCrit.asTables[0], oCrit.sColumn, tRange[0])); + elif tRange[1] is None: # 9999- + asConditions.append('%s.%s >= %s' % (oCrit.asTables[0], oCrit.sColumn, tRange[0])); + elif tRange[0] is None: # -9999 + asConditions.append('%s.%s <= %s' % (oCrit.asTables[0], oCrit.sColumn, tRange[1])); + else: + asConditions.append('%s.%s BETWEEN %s AND %s' % (oCrit.asTables[0], oCrit.sColumn, + tRange[0], tRange[1])); + if not oCrit.fInverted: + sQuery += '%s AND (%s)\n' % (sExtraIndent, ' OR '.join(asConditions)); + else: + sQuery += '%s AND NOT (%s)\n' % (sExtraIndent, ' OR '.join(asConditions)); + else: + assert len(oCrit.asTables) == 1; + sQuery += '%s AND (' % (sExtraIndent,); + + if oCrit.sType != FilterCriterion.ksType_UIntNil or max(oCrit.aoSelected) != -1: + if iCrit == self.kiMemory: + sQuery += '(%s.%s / 1024)' % (oCrit.asTables[0], oCrit.sColumn,); + else: + sQuery += '%s.%s' % (oCrit.asTables[0], oCrit.sColumn,); + if not oCrit.fInverted: + sQuery += ' IN ('; + else: + sQuery += ' NOT IN ('; + if oCrit.sType == FilterCriterion.ksType_String: + sQuery += ', '.join('\'%s\'' % (sValue,) for sValue in oCrit.aoSelected) + ')'; + else: + sQuery += ', '.join(str(iValue) for iValue in oCrit.aoSelected if iValue != -1) + ')'; + + if oCrit.sType == FilterCriterion.ksType_UIntNil \ + and -1 in oCrit.aoSelected: + if sQuery[-1] != '(': sQuery += ' OR '; + sQuery += '%s.%s IS NULL' % (oCrit.asTables[0], oCrit.sColumn,); + + if iCrit == self.kiFailReasons: + if oCrit.fInverted: + sQuery += '%s OR TestResultFailures.idFailureReason IS NULL\n' % (sExtraIndent,); + else: + sQuery += '%s AND TestSets.enmStatus >= \'failure\'::TestStatus_T\n' % (sExtraIndent,); + sQuery += ')\n'; + if oCrit.oSub is not None: + sQuery += self._getWhereWorker(iCrit | (((iCrit >> 8) + 1) << 8), oCrit.oSub, sExtraIndent, iOmit); + return sQuery; + + def getWhereConditions(self, sExtraIndent = '', iOmit = -1): + """ + Construct the WHERE conditions for the filter, optionally omitting one + criterion. + """ + sQuery = ''; + for iCrit, oCrit in enumerate(self.aCriteria): + sQuery += self._getWhereWorker(iCrit, oCrit, sExtraIndent, iOmit); + return sQuery; + + def getTableJoins(self, sExtraIndent = '', iOmit = -1, dOmitTables = None): + """ + Construct the WHERE conditions for the filter, optionally omitting one + criterion. + """ + afDone = { 'TestSets': True, }; + if dOmitTables is not None: + afDone.update(dOmitTables); + + sQuery = ''; + for iCrit, oCrit in enumerate(self.aCriteria): + if oCrit.sState == FilterCriterion.ksState_Selected \ + and iCrit != iOmit: + for sTable in oCrit.asTables: + if sTable not in afDone: + afDone[sTable] = True; + if sTable == 'Builds': + sQuery += '%sINNER JOIN Builds\n' \ + '%s ON Builds.idBuild = TestSets.idBuild\n' \ + '%s AND Builds.tsExpire > TestSets.tsCreated\n' \ + '%s AND Builds.tsEffective <= TestSets.tsCreated\n' \ + % ( sExtraIndent, sExtraIndent, sExtraIndent, sExtraIndent, ); + elif sTable == 'BuildCategories': + sQuery += '%sINNER JOIN BuildCategories\n' \ + '%s ON BuildCategories.idBuildCategory = TestSets.idBuildCategory\n' \ + % ( sExtraIndent, sExtraIndent, ); + elif sTable == 'TestBoxesWithStrings': + sQuery += '%sLEFT OUTER JOIN TestBoxesWithStrings\n' \ + '%s ON TestBoxesWithStrings.idGenTestBox = TestSets.idGenTestBox\n' \ + % ( sExtraIndent, sExtraIndent, ); + elif sTable == 'TestCases': + sQuery += '%sINNER JOIN TestCases\n' \ + '%s ON TestCases.idGenTestCase = TestSets.idGenTestCase\n' \ + % ( sExtraIndent, sExtraIndent, ); + elif sTable == 'TestCaseArgs': + sQuery += '%sINNER JOIN TestCaseArgs\n' \ + '%s ON TestCaseArgs.idGenTestCaseArgs = TestSets.idGenTestCaseArgs\n' \ + % ( sExtraIndent, sExtraIndent, ); + elif sTable == 'TestResults': + sQuery += '%sINNER JOIN TestResults\n' \ + '%s ON TestResults.idTestResult = TestSets.idTestResult\n' \ + % ( sExtraIndent, sExtraIndent, ); + elif sTable == 'TestResultFailures': + sQuery += '%sLEFT OUTER JOIN TestResultFailures\n' \ + '%s ON TestResultFailures.idTestSet = TestSets.idTestSet\n' \ + '%s AND TestResultFailures.tsExpire = \'infinity\'::TIMESTAMP\n' \ + % ( sExtraIndent, sExtraIndent, sExtraIndent, ); + else: + assert False, sTable; + return sQuery; + + def isJoiningWithTable(self, sTable): + """ Checks whether getTableJoins already joins with TestResultFailures. """ + for oCrit in self.aCriteria: + if oCrit.sState == FilterCriterion.ksState_Selected and sTable in oCrit.asTables: + return True; + return False + + + +class TestResultLogic(ModelLogicBase): # pylint: disable=too-few-public-methods + """ + Results grouped by scheduling group. + """ + + # + # Result grinding for displaying in the WUI. + # + + ksResultsGroupingTypeNone = 'ResultsGroupingTypeNone'; + ksResultsGroupingTypeTestGroup = 'ResultsGroupingTypeTestGroup'; + ksResultsGroupingTypeBuildCat = 'ResultsGroupingTypeBuildCat'; + ksResultsGroupingTypeBuildRev = 'ResultsGroupingTypeBuildRev'; + ksResultsGroupingTypeTestBox = 'ResultsGroupingTypeTestBox'; + ksResultsGroupingTypeTestCase = 'ResultsGroupingTypeTestCase'; + ksResultsGroupingTypeOS = 'ResultsGroupingTypeOS'; + ksResultsGroupingTypeArch = 'ResultsGroupingTypeArch'; + ksResultsGroupingTypeSchedGroup = 'ResultsGroupingTypeSchedGroup'; + + ## @name Result sorting options. + ## @{ + ksResultsSortByRunningAndStart = 'ResultsSortByRunningAndStart'; ##< Default + ksResultsSortByBuildRevision = 'ResultsSortByBuildRevision'; + ksResultsSortByTestBoxName = 'ResultsSortByTestBoxName'; + ksResultsSortByTestBoxOs = 'ResultsSortByTestBoxOs'; + ksResultsSortByTestBoxOsVersion = 'ResultsSortByTestBoxOsVersion'; + ksResultsSortByTestBoxOsArch = 'ResultsSortByTestBoxOsArch'; + ksResultsSortByTestBoxArch = 'ResultsSortByTestBoxArch'; + ksResultsSortByTestBoxCpuVendor = 'ResultsSortByTestBoxCpuVendor'; + ksResultsSortByTestBoxCpuName = 'ResultsSortByTestBoxCpuName'; + ksResultsSortByTestBoxCpuRev = 'ResultsSortByTestBoxCpuRev'; + ksResultsSortByTestBoxCpuFeatures = 'ResultsSortByTestBoxCpuFeatures'; + ksResultsSortByTestCaseName = 'ResultsSortByTestCaseName'; + ksResultsSortByFailureReason = 'ResultsSortByFailureReason'; + kasResultsSortBy = { + ksResultsSortByRunningAndStart, + ksResultsSortByBuildRevision, + ksResultsSortByTestBoxName, + ksResultsSortByTestBoxOs, + ksResultsSortByTestBoxOsVersion, + ksResultsSortByTestBoxOsArch, + ksResultsSortByTestBoxArch, + ksResultsSortByTestBoxCpuVendor, + ksResultsSortByTestBoxCpuName, + ksResultsSortByTestBoxCpuRev, + ksResultsSortByTestBoxCpuFeatures, + ksResultsSortByTestCaseName, + ksResultsSortByFailureReason, + }; + ## Used by the WUI for generating the drop down. + kaasResultsSortByTitles = ( + ( ksResultsSortByRunningAndStart, 'Running & Start TS' ), + ( ksResultsSortByBuildRevision, 'Build Revision' ), + ( ksResultsSortByTestBoxName, 'TestBox Name' ), + ( ksResultsSortByTestBoxOs, 'O/S' ), + ( ksResultsSortByTestBoxOsVersion, 'O/S Version' ), + ( ksResultsSortByTestBoxOsArch, 'O/S & Architecture' ), + ( ksResultsSortByTestBoxArch, 'Architecture' ), + ( ksResultsSortByTestBoxCpuVendor, 'CPU Vendor' ), + ( ksResultsSortByTestBoxCpuName, 'CPU Vendor & Name' ), + ( ksResultsSortByTestBoxCpuRev, 'CPU Vendor & Revision' ), + ( ksResultsSortByTestBoxCpuFeatures, 'CPU Features' ), + ( ksResultsSortByTestCaseName, 'Test Case Name' ), + ( ksResultsSortByFailureReason, 'Failure Reason' ), + ); + ## @} + + ## Default sort by map. + kdResultSortByMap = { + ksResultsSortByRunningAndStart: ( (), None, None, '', '' ), + ksResultsSortByBuildRevision: ( + # Sorting tables. + ('Builds',), + # Sorting table join(s). + ' AND TestSets.idBuild = Builds.idBuild' + ' AND Builds.tsExpire >= TestSets.tsCreated' + ' AND Builds.tsEffective <= TestSets.tsCreated', + # Start of ORDER BY statement. + ' Builds.iRevision DESC', + # Extra columns to fetch for the above ORDER BY to work in a SELECT DISTINCT statement. + '', + # Columns for the GROUP BY + ''), + ksResultsSortByTestBoxName: ( + ('TestBoxes',), + ' AND TestSets.idGenTestBox = TestBoxes.idGenTestBox', + ' TestBoxes.sName DESC', + '', '' ), + ksResultsSortByTestBoxOsArch: ( + ('TestBoxesWithStrings',), + ' AND TestSets.idGenTestBox = TestBoxesWithStrings.idGenTestBox', + ' TestBoxesWithStrings.sOs, TestBoxesWithStrings.sCpuArch', + '', '' ), + ksResultsSortByTestBoxOs: ( + ('TestBoxesWithStrings',), + ' AND TestSets.idGenTestBox = TestBoxesWithStrings.idGenTestBox', + ' TestBoxesWithStrings.sOs', + '', '' ), + ksResultsSortByTestBoxOsVersion: ( + ('TestBoxesWithStrings',), + ' AND TestSets.idGenTestBox = TestBoxesWithStrings.idGenTestBox', + ' TestBoxesWithStrings.sOs, TestBoxesWithStrings.sOsVersion DESC', + '', '' ), + ksResultsSortByTestBoxArch: ( + ('TestBoxesWithStrings',), + ' AND TestSets.idGenTestBox = TestBoxesWithStrings.idGenTestBox', + ' TestBoxesWithStrings.sCpuArch', + '', '' ), + ksResultsSortByTestBoxCpuVendor: ( + ('TestBoxesWithStrings',), + ' AND TestSets.idGenTestBox = TestBoxesWithStrings.idGenTestBox', + ' TestBoxesWithStrings.sCpuVendor', + '', '' ), + ksResultsSortByTestBoxCpuName: ( + ('TestBoxesWithStrings',), + ' AND TestSets.idGenTestBox = TestBoxesWithStrings.idGenTestBox', + ' TestBoxesWithStrings.sCpuVendor, TestBoxesWithStrings.sCpuName', + '', '' ), + ksResultsSortByTestBoxCpuRev: ( + ('TestBoxesWithStrings',), + ' AND TestSets.idGenTestBox = TestBoxesWithStrings.idGenTestBox', + ' TestBoxesWithStrings.sCpuVendor, TestBoxesWithStrings.lCpuRevision DESC', + ', TestBoxesWithStrings.lCpuRevision', + ', TestBoxesWithStrings.lCpuRevision' ), + ksResultsSortByTestBoxCpuFeatures: ( + ('TestBoxes',), + ' AND TestSets.idGenTestBox = TestBoxes.idGenTestBox', + ' TestBoxes.fCpuHwVirt DESC, TestBoxes.fCpuNestedPaging DESC, TestBoxes.fCpu64BitGuest DESC, TestBoxes.cCpus DESC', + '', + '' ), + ksResultsSortByTestCaseName: ( + ('TestCases',), + ' AND TestSets.idGenTestCase = TestCases.idGenTestCase', + ' TestCases.sName', + '', '' ), + ksResultsSortByFailureReason: ( + (), '', + 'asSortByFailureReason ASC', + ', array_agg(FailureReasons.sShort ORDER BY TestResultFailures.idTestResult) AS asSortByFailureReason', + '' ), + }; + + kdResultGroupingMap = { + ksResultsGroupingTypeNone: ( + # Grouping tables; + (), + # Grouping field; + None, + # Grouping where addition. + None, + # Sort by overrides. + {}, + ), + ksResultsGroupingTypeTestGroup: ('', 'TestSets.idTestGroup', None, {},), + ksResultsGroupingTypeTestBox: ('', 'TestSets.idTestBox', None, {},), + ksResultsGroupingTypeTestCase: ('', 'TestSets.idTestCase', None, {},), + ksResultsGroupingTypeOS: ( + ('TestBoxes',), + 'TestBoxes.idStrOs', + ' AND TestBoxes.idGenTestBox = TestSets.idGenTestBox', + {}, + ), + ksResultsGroupingTypeArch: ( + ('TestBoxes',), + 'TestBoxes.idStrCpuArch', + ' AND TestBoxes.idGenTestBox = TestSets.idGenTestBox', + {}, + ), + ksResultsGroupingTypeBuildCat: ('', 'TestSets.idBuildCategory', None, {},), + ksResultsGroupingTypeBuildRev: ( + ('Builds',), + 'Builds.iRevision', + ' AND Builds.idBuild = TestSets.idBuild' + ' AND Builds.tsExpire > TestSets.tsCreated' + ' AND Builds.tsEffective <= TestSets.tsCreated', + { ksResultsSortByBuildRevision: ( (), None, ' Builds.iRevision DESC' ), } + ), + ksResultsGroupingTypeSchedGroup: ( '', 'TestSets.idSchedGroup', None, {},), + }; + + + def __init__(self, oDb): + ModelLogicBase.__init__(self, oDb) + self.oFailureReasonLogic = None; + self.oUserAccountLogic = None; + + def _getTimePeriodQueryPart(self, tsNow, sInterval, sExtraIndent = ''): + """ + Get part of SQL query responsible for SELECT data within + specified period of time. + """ + assert sInterval is not None; # too many rows. + + cMonthsMourningPeriod = 2; # Stop reminding everyone about testboxes after 2 months. (May also speed up the query.) + if tsNow is None: + sRet = '(TestSets.tsDone IS NULL OR TestSets.tsDone >= (CURRENT_TIMESTAMP - \'%s\'::interval))\n' \ + '%s AND TestSets.tsCreated >= (CURRENT_TIMESTAMP - \'%s\'::interval - \'%u months\'::interval)\n' \ + % ( sInterval, + sExtraIndent, sInterval, cMonthsMourningPeriod); + else: + sTsNow = '\'%s\'::TIMESTAMP' % (tsNow,); # It's actually a string already. duh. + sRet = 'TestSets.tsCreated <= %s\n' \ + '%s AND TestSets.tsCreated >= (%s - \'%s\'::interval - \'%u months\'::interval)\n' \ + '%s AND (TestSets.tsDone IS NULL OR TestSets.tsDone >= (%s - \'%s\'::interval))\n' \ + % ( sTsNow, + sExtraIndent, sTsNow, sInterval, cMonthsMourningPeriod, + sExtraIndent, sTsNow, sInterval ); + return sRet + + def fetchResultsForListing(self, iStart, cMaxRows, tsNow, sInterval, oFilter, enmResultSortBy, # pylint: disable=too-many-arguments + enmResultsGroupingType, iResultsGroupingValue, fOnlyFailures, fOnlyNeedingReason): + """ + Fetches TestResults table content. + + If @param enmResultsGroupingType and @param iResultsGroupingValue + are not None, then resulting (returned) list contains only records + that match specified @param enmResultsGroupingType. + + If @param enmResultsGroupingType is None, then + @param iResultsGroupingValue is ignored. + + Returns an array (list) of TestResultData items, empty list if none. + Raises exception on error. + """ + + _ = oFilter; + + # + # Get SQL query parameters + # + if enmResultsGroupingType is None or enmResultsGroupingType not in self.kdResultGroupingMap: + raise TMExceptionBase('Unknown grouping type'); + if enmResultSortBy is None or enmResultSortBy not in self.kasResultsSortBy: + raise TMExceptionBase('Unknown sorting'); + asGroupingTables, sGroupingField, sGroupingCondition, dSortOverrides = self.kdResultGroupingMap[enmResultsGroupingType]; + if enmResultSortBy in dSortOverrides: + asSortTables, sSortWhere, sSortOrderBy, sSortColumns, sSortGroupBy = dSortOverrides[enmResultSortBy]; + else: + asSortTables, sSortWhere, sSortOrderBy, sSortColumns, sSortGroupBy = self.kdResultSortByMap[enmResultSortBy]; + + # + # Construct the query. + # + sQuery = 'SELECT DISTINCT TestSets.idTestSet,\n' \ + ' BuildCategories.idBuildCategory,\n' \ + ' BuildCategories.sProduct,\n' \ + ' BuildCategories.sRepository,\n' \ + ' BuildCategories.sBranch,\n' \ + ' BuildCategories.sType,\n' \ + ' Builds.idBuild,\n' \ + ' Builds.sVersion,\n' \ + ' Builds.iRevision,\n' \ + ' TestBoxesWithStrings.sOs,\n' \ + ' TestBoxesWithStrings.sOsVersion,\n' \ + ' TestBoxesWithStrings.sCpuArch,\n' \ + ' TestBoxesWithStrings.sCpuVendor,\n' \ + ' TestBoxesWithStrings.sCpuName,\n' \ + ' TestBoxesWithStrings.cCpus,\n' \ + ' TestBoxesWithStrings.fCpuHwVirt,\n' \ + ' TestBoxesWithStrings.fCpuNestedPaging,\n' \ + ' TestBoxesWithStrings.fCpu64BitGuest,\n' \ + ' TestBoxesWithStrings.idTestBox,\n' \ + ' TestBoxesWithStrings.sName,\n' \ + ' TestResults.tsCreated,\n' \ + ' COALESCE(TestResults.tsElapsed, CURRENT_TIMESTAMP - TestResults.tsCreated) AS tsElapsedTestResult,\n' \ + ' TestSets.enmStatus,\n' \ + ' TestResults.cErrors,\n' \ + ' TestCases.idTestCase,\n' \ + ' TestCases.sName,\n' \ + ' TestCases.sBaseCmd,\n' \ + ' TestCaseArgs.sArgs,\n' \ + ' TestCaseArgs.sSubName,\n' \ + ' TestSuiteBits.idBuild AS idBuildTestSuite,\n' \ + ' TestSuiteBits.iRevision AS iRevisionTestSuite,\n' \ + ' array_agg(TestResultFailures.idFailureReason ORDER BY TestResultFailures.idTestResult),\n' \ + ' array_agg(TestResultFailures.uidAuthor ORDER BY TestResultFailures.idTestResult),\n' \ + ' array_agg(TestResultFailures.tsEffective ORDER BY TestResultFailures.idTestResult),\n' \ + ' array_agg(TestResultFailures.sComment ORDER BY TestResultFailures.idTestResult),\n' \ + ' (TestSets.tsDone IS NULL) SortRunningFirst' + sSortColumns + '\n' \ + 'FROM ( SELECT TestSets.idTestSet AS idTestSet,\n' \ + ' TestSets.tsDone AS tsDone,\n' \ + ' TestSets.tsCreated AS tsCreated,\n' \ + ' TestSets.enmStatus AS enmStatus,\n' \ + ' TestSets.idBuild AS idBuild,\n' \ + ' TestSets.idBuildTestSuite AS idBuildTestSuite,\n' \ + ' TestSets.idGenTestBox AS idGenTestBox,\n' \ + ' TestSets.idGenTestCase AS idGenTestCase,\n' \ + ' TestSets.idGenTestCaseArgs AS idGenTestCaseArgs\n' \ + ' FROM TestSets\n'; + sQuery += oFilter.getTableJoins(' '); + if fOnlyNeedingReason and not oFilter.isJoiningWithTable('TestResultFailures'): + sQuery += '\n' \ + ' LEFT OUTER JOIN TestResultFailures\n' \ + ' ON TestSets.idTestSet = TestResultFailures.idTestSet\n' \ + ' AND TestResultFailures.tsExpire = \'infinity\'::TIMESTAMP'; + for asTables in [asGroupingTables, asSortTables]: + for sTable in asTables: + if not oFilter.isJoiningWithTable(sTable): + sQuery = sQuery[:-1] + ',\n ' + sTable + '\n'; + + sQuery += ' WHERE ' + self._getTimePeriodQueryPart(tsNow, sInterval, ' ') + \ + oFilter.getWhereConditions(' '); + if fOnlyFailures or fOnlyNeedingReason: + sQuery += ' AND TestSets.enmStatus != \'success\'::TestStatus_T\n' \ + ' AND TestSets.enmStatus != \'running\'::TestStatus_T\n'; + if fOnlyNeedingReason: + sQuery += ' AND TestResultFailures.idTestSet IS NULL\n'; + if sGroupingField is not None: + sQuery += ' AND %s = %d\n' % (sGroupingField, iResultsGroupingValue,); + if sGroupingCondition is not None: + sQuery += sGroupingCondition.replace(' AND ', ' AND '); + if sSortWhere is not None: + sQuery += sSortWhere.replace(' AND ', ' AND '); + sQuery += ' ORDER BY '; + if sSortOrderBy is not None and sSortOrderBy.find('FailureReason') < 0: + sQuery += sSortOrderBy + ',\n '; + sQuery += '(TestSets.tsDone IS NULL) DESC, TestSets.idTestSet DESC\n' \ + ' LIMIT %s OFFSET %s\n' % (cMaxRows, iStart,); + + # Note! INNER JOIN TestBoxesWithStrings performs miserable compared to LEFT OUTER JOIN. Doesn't matter for the result + # because TestSets.idGenTestBox is a foreign key and unique in TestBoxes. So, let's do what ever is faster. + sQuery += ' ) AS TestSets\n' \ + ' LEFT OUTER JOIN TestBoxesWithStrings\n' \ + ' ON TestSets.idGenTestBox = TestBoxesWithStrings.idGenTestBox' \ + ' LEFT OUTER JOIN Builds AS TestSuiteBits\n' \ + ' ON TestSuiteBits.idBuild = TestSets.idBuildTestSuite\n' \ + ' AND TestSuiteBits.tsExpire > TestSets.tsCreated\n' \ + ' AND TestSuiteBits.tsEffective <= TestSets.tsCreated\n' \ + ' LEFT OUTER JOIN TestResultFailures\n' \ + ' ON TestSets.idTestSet = TestResultFailures.idTestSet\n' \ + ' AND TestResultFailures.tsExpire = \'infinity\'::TIMESTAMP'; + if sSortOrderBy is not None and sSortOrderBy.find('FailureReason') >= 0: + sQuery += '\n' \ + ' LEFT OUTER JOIN FailureReasons\n' \ + ' ON TestResultFailures.idFailureReason = FailureReasons.idFailureReason\n' \ + ' AND FailureReasons.tsExpire = \'infinity\'::TIMESTAMP'; + sQuery += ',\n' \ + ' BuildCategories,\n' \ + ' Builds,\n' \ + ' TestResults,\n' \ + ' TestCases,\n' \ + ' TestCaseArgs\n'; + sQuery += 'WHERE TestSets.idTestSet = TestResults.idTestSet\n' \ + ' AND TestResults.idTestResultParent is NULL\n' \ + ' AND TestSets.idBuild = Builds.idBuild\n' \ + ' AND Builds.tsExpire > TestSets.tsCreated\n' \ + ' AND Builds.tsEffective <= TestSets.tsCreated\n' \ + ' AND Builds.idBuildCategory = BuildCategories.idBuildCategory\n' \ + ' AND TestSets.idGenTestCase = TestCases.idGenTestCase\n' \ + ' AND TestSets.idGenTestCaseArgs = TestCaseArgs.idGenTestCaseArgs\n'; + sQuery += 'GROUP BY TestSets.idTestSet,\n' \ + ' BuildCategories.idBuildCategory,\n' \ + ' BuildCategories.sProduct,\n' \ + ' BuildCategories.sRepository,\n' \ + ' BuildCategories.sBranch,\n' \ + ' BuildCategories.sType,\n' \ + ' Builds.idBuild,\n' \ + ' Builds.sVersion,\n' \ + ' Builds.iRevision,\n' \ + ' TestBoxesWithStrings.sOs,\n' \ + ' TestBoxesWithStrings.sOsVersion,\n' \ + ' TestBoxesWithStrings.sCpuArch,\n' \ + ' TestBoxesWithStrings.sCpuVendor,\n' \ + ' TestBoxesWithStrings.sCpuName,\n' \ + ' TestBoxesWithStrings.cCpus,\n' \ + ' TestBoxesWithStrings.fCpuHwVirt,\n' \ + ' TestBoxesWithStrings.fCpuNestedPaging,\n' \ + ' TestBoxesWithStrings.fCpu64BitGuest,\n' \ + ' TestBoxesWithStrings.idTestBox,\n' \ + ' TestBoxesWithStrings.sName,\n' \ + ' TestResults.tsCreated,\n' \ + ' tsElapsedTestResult,\n' \ + ' TestSets.enmStatus,\n' \ + ' TestResults.cErrors,\n' \ + ' TestCases.idTestCase,\n' \ + ' TestCases.sName,\n' \ + ' TestCases.sBaseCmd,\n' \ + ' TestCaseArgs.sArgs,\n' \ + ' TestCaseArgs.sSubName,\n' \ + ' TestSuiteBits.idBuild,\n' \ + ' TestSuiteBits.iRevision,\n' \ + ' SortRunningFirst' + sSortGroupBy + '\n'; + sQuery += 'ORDER BY '; + if sSortOrderBy is not None: + sQuery += sSortOrderBy.replace('TestBoxes.', 'TestBoxesWithStrings.') + ',\n '; + sQuery += '(TestSets.tsDone IS NULL) DESC, TestSets.idTestSet DESC\n'; + + # + # Execute the query and return the wrapped results. + # + self._oDb.execute(sQuery); + + if self.oFailureReasonLogic is None: + self.oFailureReasonLogic = FailureReasonLogic(self._oDb); + if self.oUserAccountLogic is None: + self.oUserAccountLogic = UserAccountLogic(self._oDb); + + aoRows = []; + for aoRow in self._oDb.fetchAll(): + aoRows.append(TestResultListingData().initFromDbRowEx(aoRow, self.oFailureReasonLogic, self.oUserAccountLogic)); + + return aoRows + + + def fetchTimestampsForLogViewer(self, idTestSet): + """ + Returns an ordered list with all the test result timestamps, both start + and end. + + The log viewer create anchors in the log text so we can jump directly to + the log lines relevant for a test event. + """ + self._oDb.execute('(\n' + 'SELECT tsCreated\n' + 'FROM TestResults\n' + 'WHERE idTestSet = %s\n' + ') UNION (\n' + 'SELECT tsCreated + tsElapsed\n' + 'FROM TestResults\n' + 'WHERE idTestSet = %s\n' + ' AND tsElapsed IS NOT NULL\n' + ') UNION (\n' + 'SELECT TestResultFiles.tsCreated\n' + 'FROM TestResultFiles\n' + 'WHERE idTestSet = %s\n' + ') UNION (\n' + 'SELECT tsCreated\n' + 'FROM TestResultValues\n' + 'WHERE idTestSet = %s\n' + ') UNION (\n' + 'SELECT TestResultMsgs.tsCreated\n' + 'FROM TestResultMsgs\n' + 'WHERE idTestSet = %s\n' + ') ORDER by 1' + , ( idTestSet, idTestSet, idTestSet, idTestSet, idTestSet, )); + return [aoRow[0] for aoRow in self._oDb.fetchAll()]; + + + def getEntriesCount(self, tsNow, sInterval, oFilter, enmResultsGroupingType, iResultsGroupingValue, + fOnlyFailures, fOnlyNeedingReason): + """ + Get number of table records. + + If @param enmResultsGroupingType and @param iResultsGroupingValue + are not None, then we count only only those records + that match specified @param enmResultsGroupingType. + + If @param enmResultsGroupingType is None, then + @param iResultsGroupingValue is ignored. + """ + _ = oFilter; + + # + # Get SQL query parameters + # + if enmResultsGroupingType is None: + raise TMExceptionBase('Unknown grouping type') + + if enmResultsGroupingType not in self.kdResultGroupingMap: + raise TMExceptionBase('Unknown grouping type') + asGroupingTables, sGroupingField, sGroupingCondition, _ = self.kdResultGroupingMap[enmResultsGroupingType]; + + # + # Construct the query. + # + sQuery = 'SELECT COUNT(TestSets.idTestSet)\n' \ + 'FROM TestSets\n'; + sQuery += oFilter.getTableJoins(); + if fOnlyNeedingReason and not oFilter.isJoiningWithTable('TestResultFailures'): + sQuery += ' LEFT OUTER JOIN TestResultFailures\n' \ + ' ON TestSets.idTestSet = TestResultFailures.idTestSet\n' \ + ' AND TestResultFailures.tsExpire = \'infinity\'::TIMESTAMP\n'; + for sTable in asGroupingTables: + if not oFilter.isJoiningWithTable(sTable): + sQuery = sQuery[:-1] + ',\n ' + sTable + '\n'; + sQuery += 'WHERE ' + self._getTimePeriodQueryPart(tsNow, sInterval) + \ + oFilter.getWhereConditions(); + if fOnlyFailures or fOnlyNeedingReason: + sQuery += ' AND TestSets.enmStatus != \'success\'::TestStatus_T\n' \ + ' AND TestSets.enmStatus != \'running\'::TestStatus_T\n'; + if fOnlyNeedingReason: + sQuery += ' AND TestResultFailures.idTestSet IS NULL\n'; + if sGroupingField is not None: + sQuery += ' AND %s = %d\n' % (sGroupingField, iResultsGroupingValue,); + if sGroupingCondition is not None: + sQuery += sGroupingCondition.replace(' AND ', ' AND '); + + # + # Execute the query and return the result. + # + self._oDb.execute(sQuery) + return self._oDb.fetchOne()[0] + + def getTestGroups(self, tsNow, sPeriod): + """ + Get list of uniq TestGroupData objects which + found in all test results. + """ + + self._oDb.execute('SELECT DISTINCT TestGroups.*\n' + 'FROM TestGroups, TestSets\n' + 'WHERE TestSets.idTestGroup = TestGroups.idTestGroup\n' + ' AND TestGroups.tsExpire > TestSets.tsCreated\n' + ' AND TestGroups.tsEffective <= TestSets.tsCreated' + ' AND ' + self._getTimePeriodQueryPart(tsNow, sPeriod)) + aaoRows = self._oDb.fetchAll() + aoRet = [] + for aoRow in aaoRows: + aoRet.append(TestGroupData().initFromDbRow(aoRow)) + return aoRet + + def getBuilds(self, tsNow, sPeriod): + """ + Get list of uniq BuildDataEx objects which + found in all test results. + """ + + self._oDb.execute('SELECT DISTINCT Builds.*, BuildCategories.*\n' + 'FROM Builds, BuildCategories, TestSets\n' + 'WHERE TestSets.idBuild = Builds.idBuild\n' + ' AND Builds.idBuildCategory = BuildCategories.idBuildCategory\n' + ' AND Builds.tsExpire > TestSets.tsCreated\n' + ' AND Builds.tsEffective <= TestSets.tsCreated' + ' AND ' + self._getTimePeriodQueryPart(tsNow, sPeriod)) + aaoRows = self._oDb.fetchAll() + aoRet = [] + for aoRow in aaoRows: + aoRet.append(BuildDataEx().initFromDbRow(aoRow)) + return aoRet + + def getTestBoxes(self, tsNow, sPeriod): + """ + Get list of uniq TestBoxData objects which + found in all test results. + """ + # Note! INNER JOIN TestBoxesWithStrings performs miserable compared to LEFT OUTER JOIN. Doesn't matter for the result + # because TestSets.idGenTestBox is a foreign key and unique in TestBoxes. So, let's do what ever is faster. + self._oDb.execute('SELECT TestBoxesWithStrings.*\n' + 'FROM ( SELECT idTestBox AS idTestBox,\n' + ' MAX(idGenTestBox) AS idGenTestBox\n' + ' FROM TestSets\n' + ' WHERE ' + self._getTimePeriodQueryPart(tsNow, sPeriod, ' ') + + ' GROUP BY idTestBox\n' + ' ) AS TestBoxIDs\n' + ' LEFT OUTER JOIN TestBoxesWithStrings\n' + ' ON TestBoxesWithStrings.idGenTestBox = TestBoxIDs.idGenTestBox\n' + 'ORDER BY TestBoxesWithStrings.sName\n' ); + aoRet = [] + for aoRow in self._oDb.fetchAll(): + aoRet.append(TestBoxData().initFromDbRow(aoRow)); + return aoRet + + def getTestCases(self, tsNow, sPeriod): + """ + Get a list of unique TestCaseData objects which is appears in the test + specified result period. + """ + + # Using LEFT OUTER JOIN instead of INNER JOIN in case it performs better, doesn't matter for the result. + self._oDb.execute('SELECT TestCases.*\n' + 'FROM ( SELECT idTestCase AS idTestCase,\n' + ' MAX(idGenTestCase) AS idGenTestCase\n' + ' FROM TestSets\n' + ' WHERE ' + self._getTimePeriodQueryPart(tsNow, sPeriod, ' ') + + ' GROUP BY idTestCase\n' + ' ) AS TestCasesIDs\n' + ' LEFT OUTER JOIN TestCases ON TestCases.idGenTestCase = TestCasesIDs.idGenTestCase\n' + 'ORDER BY TestCases.sName\n' ); + + aoRet = []; + for aoRow in self._oDb.fetchAll(): + aoRet.append(TestCaseData().initFromDbRow(aoRow)); + return aoRet + + def getOSes(self, tsNow, sPeriod): + """ + Get a list of [idStrOs, sOs] tuples of the OSes that appears in the specified result period. + """ + + # Note! INNER JOIN TestBoxesWithStrings performs miserable compared to LEFT OUTER JOIN. Doesn't matter for the result + # because TestSets.idGenTestBox is a foreign key and unique in TestBoxes. So, let's do what ever is faster. + self._oDb.execute('SELECT DISTINCT TestBoxesWithStrings.idStrOs, TestBoxesWithStrings.sOs\n' + 'FROM ( SELECT idTestBox AS idTestBox,\n' + ' MAX(idGenTestBox) AS idGenTestBox\n' + ' FROM TestSets\n' + ' WHERE ' + self._getTimePeriodQueryPart(tsNow, sPeriod, ' ') + + ' GROUP BY idTestBox\n' + ' ) AS TestBoxIDs\n' + ' LEFT OUTER JOIN TestBoxesWithStrings\n' + ' ON TestBoxesWithStrings.idGenTestBox = TestBoxIDs.idGenTestBox\n' + 'ORDER BY TestBoxesWithStrings.sOs\n' ); + return self._oDb.fetchAll(); + + def getArchitectures(self, tsNow, sPeriod): + """ + Get a list of [idStrCpuArch, sCpuArch] tuples of the architecutres + that appears in the specified result period. + """ + + # Note! INNER JOIN TestBoxesWithStrings performs miserable compared to LEFT OUTER JOIN. Doesn't matter for the result + # because TestSets.idGenTestBox is a foreign key and unique in TestBoxes. So, let's do what ever is faster. + self._oDb.execute('SELECT DISTINCT TestBoxesWithStrings.idStrCpuArch, TestBoxesWithStrings.sCpuArch\n' + 'FROM ( SELECT idTestBox AS idTestBox,\n' + ' MAX(idGenTestBox) AS idGenTestBox\n' + ' FROM TestSets\n' + ' WHERE ' + self._getTimePeriodQueryPart(tsNow, sPeriod, ' ') + + ' GROUP BY idTestBox\n' + ' ) AS TestBoxIDs\n' + ' LEFT OUTER JOIN TestBoxesWithStrings\n' + ' ON TestBoxesWithStrings.idGenTestBox = TestBoxIDs.idGenTestBox\n' + 'ORDER BY TestBoxesWithStrings.sCpuArch\n' ); + return self._oDb.fetchAll(); + + def getBuildCategories(self, tsNow, sPeriod): + """ + Get a list of BuildCategoryData that appears in the specified result period. + """ + + self._oDb.execute('SELECT DISTINCT BuildCategories.*\n' + 'FROM ( SELECT DISTINCT idBuildCategory AS idBuildCategory\n' + ' FROM TestSets\n' + ' WHERE ' + self._getTimePeriodQueryPart(tsNow, sPeriod, ' ') + + ' ) AS BuildCategoryIDs\n' + ' LEFT OUTER JOIN BuildCategories\n' + ' ON BuildCategories.idBuildCategory = BuildCategoryIDs.idBuildCategory\n' + 'ORDER BY BuildCategories.sProduct, BuildCategories.sBranch, BuildCategories.sType\n'); + aoRet = []; + for aoRow in self._oDb.fetchAll(): + aoRet.append(BuildCategoryData().initFromDbRow(aoRow)); + return aoRet; + + def getSchedGroups(self, tsNow, sPeriod): + """ + Get list of uniq SchedGroupData objects which + found in all test results. + """ + + self._oDb.execute('SELECT SchedGroups.*\n' + 'FROM ( SELECT idSchedGroup,\n' + ' MAX(TestSets.tsCreated) AS tsNow\n' + ' FROM TestSets\n' + ' WHERE ' + self._getTimePeriodQueryPart(tsNow, sPeriod, ' ') + + ' GROUP BY idSchedGroup\n' + ' ) AS SchedGroupIDs\n' + ' INNER JOIN SchedGroups\n' + ' ON SchedGroups.idSchedGroup = SchedGroupIDs.idSchedGroup\n' + ' AND SchedGroups.tsExpire > SchedGroupIDs.tsNow\n' + ' AND SchedGroups.tsEffective <= SchedGroupIDs.tsNow\n' + 'ORDER BY SchedGroups.sName\n' ); + aoRet = [] + for aoRow in self._oDb.fetchAll(): + aoRet.append(SchedGroupData().initFromDbRow(aoRow)); + return aoRet + + def getById(self, idTestResult): + """ + Get build record by its id + """ + self._oDb.execute('SELECT *\n' + 'FROM TestResults\n' + 'WHERE idTestResult = %s\n', + (idTestResult,)) + + aRows = self._oDb.fetchAll() + if len(aRows) not in (0, 1): + raise TMTooManyRows('Found more than one test result with the same credentials. Database structure is corrupted.') + try: + return TestResultData().initFromDbRow(aRows[0]) + except IndexError: + return None + + def fetchPossibleFilterOptions(self, oFilter, tsNow, sPeriod, oReportModel = None): + """ + Fetches the available filter criteria, given the current filtering. + + Returns oFilter. + """ + assert isinstance(oFilter, TestResultFilter); + + # Hack to avoid lot's of conditionals or duplicate this code. + if oReportModel is None: + class DummyReportModel(object): + """ Dummy """ + def getExtraSubjectTables(self): + """ Dummy """ + return []; + def getExtraSubjectWhereExpr(self): + """ Dummy """ + return ''; + oReportModel = DummyReportModel(); + + def workerDoFetch(oMissingLogicType, sNameAttr = 'sName', fIdIsName = False, idxHover = -1, + idNull = -1, sNullDesc = '<NULL>'): + """ Does the tedious result fetching and handling of missing bits. """ + dLeft = { oValue: 1 for oValue in oCrit.aoSelected }; + oCrit.aoPossible = []; + for aoRow in self._oDb.fetchAll(): + oCrit.aoPossible.append(FilterCriterionValueAndDescription(aoRow[0] if aoRow[0] is not None else idNull, + aoRow[1] if aoRow[1] is not None else sNullDesc, + aoRow[2], + aoRow[idxHover] if idxHover >= 0 else None)); + if aoRow[0] in dLeft: + del dLeft[aoRow[0]]; + if dLeft: + if fIdIsName: + for idMissing in dLeft: + oCrit.aoPossible.append(FilterCriterionValueAndDescription(idMissing, str(idMissing), + fIrrelevant = True)); + else: + oMissingLogic = oMissingLogicType(self._oDb); + for idMissing in dLeft: + oMissing = oMissingLogic.cachedLookup(idMissing); + if oMissing is not None: + oCrit.aoPossible.append(FilterCriterionValueAndDescription(idMissing, + getattr(oMissing, sNameAttr), + fIrrelevant = True)); + + def workerDoFetchNested(): + """ Does the tedious result fetching and handling of missing bits. """ + oCrit.aoPossible = []; + oCrit.oSub.aoPossible = []; + dLeft = { oValue: 1 for oValue in oCrit.aoSelected }; + dSubLeft = { oValue: 1 for oValue in oCrit.oSub.aoSelected }; + oMain = None; + for aoRow in self._oDb.fetchAll(): + if oMain is None or oMain.oValue != aoRow[0]: + oMain = FilterCriterionValueAndDescription(aoRow[0], aoRow[1], 0); + oCrit.aoPossible.append(oMain); + if aoRow[0] in dLeft: + del dLeft[aoRow[0]]; + oCurSub = FilterCriterionValueAndDescription(aoRow[2], aoRow[3], aoRow[4]); + oCrit.oSub.aoPossible.append(oCurSub); + if aoRow[2] in dSubLeft: + del dSubLeft[aoRow[2]]; + + oMain.aoSubs.append(oCurSub); + oMain.cTimes += aoRow[4]; + + if dLeft: + pass; ## @todo + + # Statuses. + oCrit = oFilter.aCriteria[TestResultFilter.kiTestStatus]; + self._oDb.execute('SELECT TestSets.enmStatus, TestSets.enmStatus, COUNT(TestSets.idTestSet)\n' + 'FROM TestSets\n' + oFilter.getTableJoins(iOmit = TestResultFilter.kiTestStatus) + + ''.join(' , %s\n' % (sTable,) for sTable in oReportModel.getExtraSubjectTables()) + + 'WHERE ' + self._getTimePeriodQueryPart(tsNow, sPeriod) + + oFilter.getWhereConditions(iOmit = TestResultFilter.kiTestStatus) + + oReportModel.getExtraSubjectWhereExpr() + + 'GROUP BY TestSets.enmStatus\n' + 'ORDER BY TestSets.enmStatus\n'); + workerDoFetch(None, fIdIsName = True); + + # Scheduling groups (see getSchedGroups). + oCrit = oFilter.aCriteria[TestResultFilter.kiSchedGroups]; + self._oDb.execute('SELECT SchedGroups.idSchedGroup, SchedGroups.sName, SchedGroupIDs.cTimes\n' + 'FROM ( SELECT TestSets.idSchedGroup,\n' + ' MAX(TestSets.tsCreated) AS tsNow,\n' + ' COUNT(TestSets.idTestSet) AS cTimes\n' + ' FROM TestSets\n' + oFilter.getTableJoins(iOmit = TestResultFilter.kiSchedGroups) + + ''.join(' , %s\n' % (sTable,) for sTable in oReportModel.getExtraSubjectTables()) + + ' WHERE ' + self._getTimePeriodQueryPart(tsNow, sPeriod, ' ') + + oFilter.getWhereConditions(iOmit = TestResultFilter.kiSchedGroups) + + oReportModel.getExtraSubjectWhereExpr() + + ' GROUP BY TestSets.idSchedGroup\n' + ' ) AS SchedGroupIDs\n' + ' INNER JOIN SchedGroups\n' + ' ON SchedGroups.idSchedGroup = SchedGroupIDs.idSchedGroup\n' + ' AND SchedGroups.tsExpire > SchedGroupIDs.tsNow\n' + ' AND SchedGroups.tsEffective <= SchedGroupIDs.tsNow\n' + 'ORDER BY SchedGroups.sName\n' ); + workerDoFetch(SchedGroupLogic); + + # Testboxes (see getTestBoxes). + oCrit = oFilter.aCriteria[TestResultFilter.kiTestBoxes]; + self._oDb.execute('SELECT TestBoxesWithStrings.idTestBox,\n' + ' TestBoxesWithStrings.sName,\n' + ' TestBoxIDs.cTimes\n' + 'FROM ( SELECT TestSets.idTestBox AS idTestBox,\n' + ' MAX(TestSets.idGenTestBox) AS idGenTestBox,\n' + ' COUNT(TestSets.idTestSet) AS cTimes\n' + ' FROM TestSets\n' + oFilter.getTableJoins(iOmit = TestResultFilter.kiTestBoxes) + + ''.join(' , %s\n' % (sTable,) for sTable in oReportModel.getExtraSubjectTables()) + + ' WHERE ' + self._getTimePeriodQueryPart(tsNow, sPeriod, ' ') + + oFilter.getWhereConditions(iOmit = TestResultFilter.kiTestBoxes) + + oReportModel.getExtraSubjectWhereExpr() + + ' GROUP BY TestSets.idTestBox\n' + ' ) AS TestBoxIDs\n' + ' LEFT OUTER JOIN TestBoxesWithStrings\n' + ' ON TestBoxesWithStrings.idGenTestBox = TestBoxIDs.idGenTestBox\n' + 'ORDER BY TestBoxesWithStrings.sName\n' ); + workerDoFetch(TestBoxLogic); + + # Testbox OSes and versions. + oCrit = oFilter.aCriteria[TestResultFilter.kiOses]; + self._oDb.execute('SELECT TestBoxesWithStrings.idStrOs,\n' + ' TestBoxesWithStrings.sOs,\n' + ' TestBoxesWithStrings.idStrOsVersion,\n' + ' TestBoxesWithStrings.sOsVersion,\n' + ' SUM(TestBoxGenIDs.cTimes)\n' + 'FROM ( SELECT TestSets.idGenTestBox,\n' + ' COUNT(TestSets.idTestSet) AS cTimes\n' + ' FROM TestSets\n' + oFilter.getTableJoins(iOmit = TestResultFilter.kiOses) + + ''.join(' , %s\n' % (sTable,) for sTable in oReportModel.getExtraSubjectTables()) + + ' WHERE ' + self._getTimePeriodQueryPart(tsNow, sPeriod, ' ') + + oFilter.getWhereConditions(iOmit = TestResultFilter.kiOses) + + oReportModel.getExtraSubjectWhereExpr() + + ' GROUP BY TestSets.idGenTestBox\n' + ' ) AS TestBoxGenIDs\n' + ' LEFT OUTER JOIN TestBoxesWithStrings\n' + ' ON TestBoxesWithStrings.idGenTestBox = TestBoxGenIDs.idGenTestBox\n' + 'GROUP BY TestBoxesWithStrings.idStrOs,\n' + ' TestBoxesWithStrings.sOs,\n' + ' TestBoxesWithStrings.idStrOsVersion,\n' + ' TestBoxesWithStrings.sOsVersion\n' + 'ORDER BY TestBoxesWithStrings.sOs,\n' + ' TestBoxesWithStrings.sOs = \'win\' AND TestBoxesWithStrings.sOsVersion = \'10\' DESC,\n' + ' TestBoxesWithStrings.sOsVersion DESC\n' + ); + workerDoFetchNested(); + + # Testbox CPU(/OS) architectures. + oCrit = oFilter.aCriteria[TestResultFilter.kiCpuArches]; + self._oDb.execute('SELECT TestBoxesWithStrings.idStrCpuArch,\n' + ' TestBoxesWithStrings.sCpuArch,\n' + ' SUM(TestBoxGenIDs.cTimes)\n' + 'FROM ( SELECT TestSets.idGenTestBox,\n' + ' COUNT(TestSets.idTestSet) AS cTimes\n' + ' FROM TestSets\n' + oFilter.getTableJoins(iOmit = TestResultFilter.kiCpuArches) + + ''.join(' , %s\n' % (sTable,) for sTable in oReportModel.getExtraSubjectTables()) + + ' WHERE ' + self._getTimePeriodQueryPart(tsNow, sPeriod, ' ') + + oFilter.getWhereConditions(iOmit = TestResultFilter.kiCpuArches) + + oReportModel.getExtraSubjectWhereExpr() + + ' GROUP BY TestSets.idGenTestBox\n' + ' ) AS TestBoxGenIDs\n' + ' LEFT OUTER JOIN TestBoxesWithStrings\n' + ' ON TestBoxesWithStrings.idGenTestBox = TestBoxGenIDs.idGenTestBox\n' + 'GROUP BY TestBoxesWithStrings.idStrCpuArch, TestBoxesWithStrings.sCpuArch\n' + 'ORDER BY TestBoxesWithStrings.sCpuArch\n' ); + workerDoFetch(None, fIdIsName = True); + + # Testbox CPU revisions. + oCrit = oFilter.aCriteria[TestResultFilter.kiCpuVendors]; + self._oDb.execute('SELECT TestBoxesWithStrings.idStrCpuVendor,\n' + ' TestBoxesWithStrings.sCpuVendor,\n' + ' TestBoxesWithStrings.lCpuRevision,\n' + ' TestBoxesWithStrings.sCpuVendor,\n' + ' SUM(TestBoxGenIDs.cTimes)\n' + 'FROM ( SELECT TestSets.idGenTestBox,\n' + ' COUNT(TestSets.idTestSet) AS cTimes\n' + ' FROM TestSets\n' + oFilter.getTableJoins(iOmit = TestResultFilter.kiCpuVendors) + + ''.join(' , %s\n' % (sTable,) for sTable in oReportModel.getExtraSubjectTables()) + + ' WHERE ' + self._getTimePeriodQueryPart(tsNow, sPeriod, ' ') + + oFilter.getWhereConditions(iOmit = TestResultFilter.kiCpuVendors) + + oReportModel.getExtraSubjectWhereExpr() + + ' GROUP BY TestSets.idGenTestBox' + ' ) AS TestBoxGenIDs\n' + ' LEFT OUTER JOIN TestBoxesWithStrings\n' + ' ON TestBoxesWithStrings.idGenTestBox = TestBoxGenIDs.idGenTestBox\n' + 'GROUP BY TestBoxesWithStrings.idStrCpuVendor,\n' + ' TestBoxesWithStrings.sCpuVendor,\n' + ' TestBoxesWithStrings.lCpuRevision,\n' + ' TestBoxesWithStrings.sCpuVendor\n' + 'ORDER BY TestBoxesWithStrings.sCpuVendor DESC,\n' + ' TestBoxesWithStrings.sCpuVendor = \'GenuineIntel\'\n' + ' AND (TestBoxesWithStrings.lCpuRevision >> 24) = 15,\n' # P4 at the bottom is a start... + ' TestBoxesWithStrings.lCpuRevision DESC\n' + ); + workerDoFetchNested(); + for oCur in oCrit.oSub.aoPossible: + oCur.sDesc = TestBoxData.getPrettyCpuVersionEx(oCur.oValue, oCur.sDesc).replace('_', ' '); + + # Testbox CPU core/thread counts. + oCrit = oFilter.aCriteria[TestResultFilter.kiCpuCounts]; + self._oDb.execute('SELECT TestBoxesWithStrings.cCpus,\n' + ' CAST(TestBoxesWithStrings.cCpus AS TEXT),\n' + ' SUM(TestBoxGenIDs.cTimes)\n' + 'FROM ( SELECT TestSets.idGenTestBox,\n' + ' COUNT(TestSets.idTestSet) AS cTimes\n' + ' FROM TestSets\n' + oFilter.getTableJoins(iOmit = TestResultFilter.kiCpuCounts) + + ''.join(' , %s\n' % (sTable,) for sTable in oReportModel.getExtraSubjectTables()) + + ' WHERE ' + self._getTimePeriodQueryPart(tsNow, sPeriod, ' ') + + oFilter.getWhereConditions(iOmit = TestResultFilter.kiCpuCounts) + + oReportModel.getExtraSubjectWhereExpr() + + ' GROUP BY TestSets.idGenTestBox' + ' ) AS TestBoxGenIDs\n' + ' LEFT OUTER JOIN TestBoxesWithStrings\n' + ' ON TestBoxesWithStrings.idGenTestBox = TestBoxGenIDs.idGenTestBox\n' + 'GROUP BY TestBoxesWithStrings.cCpus\n' + 'ORDER BY TestBoxesWithStrings.cCpus\n' ); + workerDoFetch(None, fIdIsName = True); + + # Testbox memory. + oCrit = oFilter.aCriteria[TestResultFilter.kiMemory]; + self._oDb.execute('SELECT TestBoxesWithStrings.cMbMemory / 1024,\n' + ' NULL,\n' + ' SUM(TestBoxGenIDs.cTimes)\n' + 'FROM ( SELECT TestSets.idGenTestBox,\n' + ' COUNT(TestSets.idTestSet) AS cTimes\n' + ' FROM TestSets\n' + oFilter.getTableJoins(iOmit = TestResultFilter.kiMemory) + + ''.join(' , %s\n' % (sTable,) for sTable in oReportModel.getExtraSubjectTables()) + + ' WHERE ' + self._getTimePeriodQueryPart(tsNow, sPeriod, ' ') + + oFilter.getWhereConditions(iOmit = TestResultFilter.kiMemory) + + oReportModel.getExtraSubjectWhereExpr() + + ' GROUP BY TestSets.idGenTestBox' + ' ) AS TestBoxGenIDs\n' + ' LEFT OUTER JOIN TestBoxesWithStrings\n' + ' ON TestBoxesWithStrings.idGenTestBox = TestBoxGenIDs.idGenTestBox\n' + 'GROUP BY TestBoxesWithStrings.cMbMemory / 1024\n' + 'ORDER BY 1\n' ); + workerDoFetch(None, fIdIsName = True); + for oCur in oCrit.aoPossible: + oCur.sDesc = '%u GB' % (oCur.oValue,); + + # Testbox python versions . + oCrit = oFilter.aCriteria[TestResultFilter.kiPythonVersions]; + self._oDb.execute('SELECT TestBoxesWithStrings.iPythonHexVersion,\n' + ' NULL,\n' + ' SUM(TestBoxGenIDs.cTimes)\n' + 'FROM ( SELECT TestSets.idGenTestBox AS idGenTestBox,\n' + ' COUNT(TestSets.idTestSet) AS cTimes\n' + ' FROM TestSets\n' + oFilter.getTableJoins(iOmit = TestResultFilter.kiPythonVersions) + + ''.join(' , %s\n' % (sTable,) for sTable in oReportModel.getExtraSubjectTables()) + + ' WHERE ' + self._getTimePeriodQueryPart(tsNow, sPeriod, ' ') + + oFilter.getWhereConditions(iOmit = TestResultFilter.kiPythonVersions) + + oReportModel.getExtraSubjectWhereExpr() + + ' GROUP BY TestSets.idGenTestBox\n' + ' ) AS TestBoxGenIDs\n' + ' LEFT OUTER JOIN TestBoxesWithStrings\n' + ' ON TestBoxesWithStrings.idGenTestBox = TestBoxGenIDs.idGenTestBox\n' + 'GROUP BY TestBoxesWithStrings.iPythonHexVersion\n' + 'ORDER BY TestBoxesWithStrings.iPythonHexVersion\n' ); + workerDoFetch(None, fIdIsName = True); + for oCur in oCrit.aoPossible: + oCur.sDesc = TestBoxData.formatPythonVersionEx(oCur.oValue); # pylint: disable=redefined-variable-type + + # Testcase with variation. + oCrit = oFilter.aCriteria[TestResultFilter.kiTestCases]; + self._oDb.execute('SELECT TestCaseArgsIDs.idTestCase,\n' + ' TestCases.sName,\n' + ' TestCaseArgsIDs.idTestCaseArgs,\n' + ' CASE WHEN TestCaseArgs.sSubName IS NULL OR TestCaseArgs.sSubName = \'\' THEN\n' + ' CONCAT(\'/ #\', TestCaseArgs.idTestCaseArgs)\n' + ' ELSE\n' + ' TestCaseArgs.sSubName\n' + ' END,' + ' TestCaseArgsIDs.cTimes\n' + 'FROM ( SELECT TestSets.idTestCase AS idTestCase,\n' + ' TestSets.idTestCaseArgs AS idTestCaseArgs,\n' + ' MAX(TestSets.idGenTestCase) AS idGenTestCase,\n' + ' MAX(TestSets.idGenTestCaseArgs) AS idGenTestCaseArgs,\n' + ' COUNT(TestSets.idTestSet) AS cTimes\n' + ' FROM TestSets\n' + oFilter.getTableJoins(iOmit = TestResultFilter.kiTestCases) + + ''.join(' , %s\n' % (sTable,) for sTable in oReportModel.getExtraSubjectTables()) + + ' WHERE ' + self._getTimePeriodQueryPart(tsNow, sPeriod, ' ') + + oFilter.getWhereConditions(iOmit = TestResultFilter.kiTestCases) + + oReportModel.getExtraSubjectWhereExpr() + + ' GROUP BY TestSets.idTestCase, TestSets.idTestCaseArgs\n' + ' ) AS TestCaseArgsIDs\n' + ' LEFT OUTER JOIN TestCases ON TestCases.idGenTestCase = TestCaseArgsIDs.idGenTestCase\n' + ' LEFT OUTER JOIN TestCaseArgs\n' + ' ON TestCaseArgs.idGenTestCaseArgs = TestCaseArgsIDs.idGenTestCaseArgs\n' + 'ORDER BY TestCases.sName, 4\n' ); + workerDoFetchNested(); + + # Build revisions. + oCrit = oFilter.aCriteria[TestResultFilter.kiRevisions]; + self._oDb.execute('SELECT Builds.iRevision, CONCAT(\'r\', Builds.iRevision), SUM(BuildIDs.cTimes)\n' + 'FROM ( SELECT TestSets.idBuild AS idBuild,\n' + ' MAX(TestSets.tsCreated) AS tsNow,\n' + ' COUNT(TestSets.idBuild) AS cTimes\n' + ' FROM TestSets\n' + oFilter.getTableJoins(iOmit = TestResultFilter.kiRevisions) + + ''.join(' , %s\n' % (sTable,) for sTable in oReportModel.getExtraSubjectTables()) + + ' WHERE ' + self._getTimePeriodQueryPart(tsNow, sPeriod, ' ') + + oFilter.getWhereConditions(iOmit = TestResultFilter.kiRevisions) + + oReportModel.getExtraSubjectWhereExpr() + + ' GROUP BY TestSets.idBuild\n' + ' ) AS BuildIDs\n' + ' INNER JOIN Builds\n' + ' ON Builds.idBuild = BuildIDs.idBuild\n' + ' AND Builds.tsExpire > BuildIDs.tsNow\n' + ' AND Builds.tsEffective <= BuildIDs.tsNow\n' + 'GROUP BY Builds.iRevision\n' + 'ORDER BY Builds.iRevision DESC\n' ); + workerDoFetch(None, fIdIsName = True); + + # Build branches. + oCrit = oFilter.aCriteria[TestResultFilter.kiBranches]; + self._oDb.execute('SELECT BuildCategories.sBranch, BuildCategories.sBranch, SUM(BuildCategoryIDs.cTimes)\n' + 'FROM ( SELECT TestSets.idBuildCategory,\n' + ' COUNT(TestSets.idTestSet) AS cTimes\n' + ' FROM TestSets\n' + oFilter.getTableJoins(iOmit = TestResultFilter.kiBranches) + + ''.join(' , %s\n' % (sTable,) for sTable in oReportModel.getExtraSubjectTables()) + + ' WHERE ' + self._getTimePeriodQueryPart(tsNow, sPeriod, ' ') + + oFilter.getWhereConditions(iOmit = TestResultFilter.kiBranches) + + oReportModel.getExtraSubjectWhereExpr() + + ' GROUP BY TestSets.idBuildCategory\n' + ' ) AS BuildCategoryIDs\n' + ' INNER JOIN BuildCategories\n' + ' ON BuildCategories.idBuildCategory = BuildCategoryIDs.idBuildCategory\n' + 'GROUP BY BuildCategories.sBranch\n' + 'ORDER BY BuildCategories.sBranch DESC\n' ); + workerDoFetch(None, fIdIsName = True); + + # Build types. + oCrit = oFilter.aCriteria[TestResultFilter.kiBuildTypes]; + self._oDb.execute('SELECT BuildCategories.sType, BuildCategories.sType, SUM(BuildCategoryIDs.cTimes)\n' + 'FROM ( SELECT TestSets.idBuildCategory,\n' + ' COUNT(TestSets.idTestSet) AS cTimes\n' + ' FROM TestSets\n' + oFilter.getTableJoins(iOmit = TestResultFilter.kiBuildTypes) + + ''.join(' , %s\n' % (sTable,) for sTable in oReportModel.getExtraSubjectTables()) + + ' WHERE ' + self._getTimePeriodQueryPart(tsNow, sPeriod, ' ') + + oFilter.getWhereConditions(iOmit = TestResultFilter.kiBuildTypes) + + oReportModel.getExtraSubjectWhereExpr() + + ' GROUP BY TestSets.idBuildCategory\n' + ' ) AS BuildCategoryIDs\n' + ' INNER JOIN BuildCategories\n' + ' ON BuildCategories.idBuildCategory = BuildCategoryIDs.idBuildCategory\n' + 'GROUP BY BuildCategories.sType\n' + 'ORDER BY BuildCategories.sType DESC\n' ); + workerDoFetch(None, fIdIsName = True); + + # Failure reasons. + oCrit = oFilter.aCriteria[TestResultFilter.kiFailReasons]; + self._oDb.execute('SELECT FailureReasons.idFailureReason, FailureReasons.sShort, FailureReasonIDs.cTimes\n' + 'FROM ( SELECT TestResultFailures.idFailureReason,\n' + ' COUNT(TestSets.idTestSet) as cTimes\n' + ' FROM TestSets\n' + ' LEFT OUTER JOIN TestResultFailures\n' + ' ON TestResultFailures.idTestSet = TestSets.idTestSet\n' + ' AND TestResultFailures.tsExpire = \'infinity\'::TIMESTAMP\n' + + oFilter.getTableJoins(iOmit = TestResultFilter.kiFailReasons) + + ''.join(' , %s\n' % (sTable,) for sTable in oReportModel.getExtraSubjectTables()) + + ' WHERE ' + self._getTimePeriodQueryPart(tsNow, sPeriod, ' ') + + ' AND TestSets.enmStatus >= \'failure\'::TestStatus_T\n' + + oFilter.getWhereConditions(iOmit = TestResultFilter.kiFailReasons) + + oReportModel.getExtraSubjectWhereExpr() + + ' GROUP BY TestResultFailures.idFailureReason\n' + ' ) AS FailureReasonIDs\n' + ' LEFT OUTER JOIN FailureReasons\n' + ' ON FailureReasons.idFailureReason = FailureReasonIDs.idFailureReason\n' + ' AND FailureReasons.tsExpire = \'infinity\'::TIMESTAMP\n' + 'ORDER BY FailureReasons.idFailureReason IS NULL DESC,\n' + ' FailureReasons.sShort\n' ); + workerDoFetch(FailureReasonLogic, 'sShort', sNullDesc = 'Not given'); + + # Error counts. + oCrit = oFilter.aCriteria[TestResultFilter.kiErrorCounts]; + self._oDb.execute('SELECT TestResults.cErrors, CAST(TestResults.cErrors AS TEXT), COUNT(TestResults.idTestResult)\n' + 'FROM ( SELECT TestSets.idTestResult AS idTestResult\n' + ' FROM TestSets\n' + + oFilter.getTableJoins(iOmit = TestResultFilter.kiFailReasons) + + ''.join(' , %s\n' % (sTable,) for sTable in oReportModel.getExtraSubjectTables()) + + ' WHERE ' + self._getTimePeriodQueryPart(tsNow, sPeriod, ' ') + + oFilter.getWhereConditions(iOmit = TestResultFilter.kiFailReasons) + + oReportModel.getExtraSubjectWhereExpr() + + ' ) AS TestSetIDs\n' + ' INNER JOIN TestResults\n' + ' ON TestResults.idTestResult = TestSetIDs.idTestResult\n' + 'GROUP BY TestResults.cErrors\n' + 'ORDER BY TestResults.cErrors\n'); + + workerDoFetch(None, fIdIsName = True); + + return oFilter; + + + # + # Details view and interface. + # + + def fetchResultTree(self, idTestSet, cMaxDepth = None): + """ + Fetches the result tree for the given test set. + + Returns a tree of TestResultDataEx nodes. + Raises exception on invalid input and database issues. + """ + # Depth first, i.e. just like the XML added them. + ## @todo this still isn't performing extremely well, consider optimizations. + sQuery = self._oDb.formatBindArgs( + 'SELECT TestResults.*,\n' + ' TestResultStrTab.sValue,\n' + ' EXISTS ( SELECT idTestResultValue\n' + ' FROM TestResultValues\n' + ' WHERE TestResultValues.idTestResult = TestResults.idTestResult ) AS fHasValues,\n' + ' EXISTS ( SELECT idTestResultMsg\n' + ' FROM TestResultMsgs\n' + ' WHERE TestResultMsgs.idTestResult = TestResults.idTestResult ) AS fHasMsgs,\n' + ' EXISTS ( SELECT idTestResultFile\n' + ' FROM TestResultFiles\n' + ' WHERE TestResultFiles.idTestResult = TestResults.idTestResult ) AS fHasFiles,\n' + ' EXISTS ( SELECT idTestResult\n' + ' FROM TestResultFailures\n' + ' WHERE TestResultFailures.idTestResult = TestResults.idTestResult ) AS fHasReasons\n' + 'FROM TestResults, TestResultStrTab\n' + 'WHERE TestResults.idTestSet = %s\n' + ' AND TestResults.idStrName = TestResultStrTab.idStr\n' + , ( idTestSet, )); + if cMaxDepth is not None: + sQuery += self._oDb.formatBindArgs(' AND TestResults.iNestingDepth <= %s\n', (cMaxDepth,)); + sQuery += 'ORDER BY idTestResult ASC\n' + + self._oDb.execute(sQuery); + cRows = self._oDb.getRowCount(); + if cRows > 65536: + raise TMTooManyRows('Too many rows returned for idTestSet=%d: %d' % (idTestSet, cRows,)); + + aaoRows = self._oDb.fetchAll(); + if not aaoRows: + raise TMRowNotFound('No test results for idTestSet=%d.' % (idTestSet,)); + + # Set up the root node first. + aoRow = aaoRows[0]; + oRoot = TestResultDataEx().initFromDbRow(aoRow); + if oRoot.idTestResultParent is not None: + raise self._oDb.integrityException('The root TestResult (#%s) has a parent (#%s)!' + % (oRoot.idTestResult, oRoot.idTestResultParent)); + self._fetchResultTreeNodeExtras(oRoot, aoRow[-4], aoRow[-3], aoRow[-2], aoRow[-1]); + + # The children (if any). + dLookup = { oRoot.idTestResult: oRoot }; + oParent = oRoot; + for iRow in range(1, len(aaoRows)): + aoRow = aaoRows[iRow]; + oCur = TestResultDataEx().initFromDbRow(aoRow); + self._fetchResultTreeNodeExtras(oCur, aoRow[-4], aoRow[-3], aoRow[-2], aoRow[-1]); + + # Figure out and vet the parent. + if oParent.idTestResult != oCur.idTestResultParent: + oParent = dLookup.get(oCur.idTestResultParent, None); + if oParent is None: + raise self._oDb.integrityException('TestResult #%d is orphaned from its parent #%s.' + % (oCur.idTestResult, oCur.idTestResultParent,)); + if oParent.iNestingDepth + 1 != oCur.iNestingDepth: + raise self._oDb.integrityException('TestResult #%d has incorrect nesting depth (%d instead of %d)' + % (oCur.idTestResult, oCur.iNestingDepth, oParent.iNestingDepth + 1,)); + + # Link it up. + oCur.oParent = oParent; + oParent.aoChildren.append(oCur); + dLookup[oCur.idTestResult] = oCur; + + return (oRoot, dLookup); + + def _fetchResultTreeNodeExtras(self, oCurNode, fHasValues, fHasMsgs, fHasFiles, fHasReasons): + """ + fetchResultTree worker that fetches values, message and files for the + specified node. + """ + assert(oCurNode.aoValues == []); + assert(oCurNode.aoMsgs == []); + assert(oCurNode.aoFiles == []); + assert(oCurNode.oReason is None); + + if fHasValues: + self._oDb.execute('SELECT TestResultValues.*,\n' + ' TestResultStrTab.sValue\n' + 'FROM TestResultValues, TestResultStrTab\n' + 'WHERE TestResultValues.idTestResult = %s\n' + ' AND TestResultValues.idStrName = TestResultStrTab.idStr\n' + 'ORDER BY idTestResultValue ASC\n' + , ( oCurNode.idTestResult, )); + for aoRow in self._oDb.fetchAll(): + oCurNode.aoValues.append(TestResultValueDataEx().initFromDbRow(aoRow)); + + if fHasMsgs: + self._oDb.execute('SELECT TestResultMsgs.*,\n' + ' TestResultStrTab.sValue\n' + 'FROM TestResultMsgs, TestResultStrTab\n' + 'WHERE TestResultMsgs.idTestResult = %s\n' + ' AND TestResultMsgs.idStrMsg = TestResultStrTab.idStr\n' + 'ORDER BY idTestResultMsg ASC\n' + , ( oCurNode.idTestResult, )); + for aoRow in self._oDb.fetchAll(): + oCurNode.aoMsgs.append(TestResultMsgDataEx().initFromDbRow(aoRow)); + + if fHasFiles: + self._oDb.execute('SELECT TestResultFiles.*,\n' + ' StrTabFile.sValue AS sFile,\n' + ' StrTabDesc.sValue AS sDescription,\n' + ' StrTabKind.sValue AS sKind,\n' + ' StrTabMime.sValue AS sMime\n' + 'FROM TestResultFiles,\n' + ' TestResultStrTab AS StrTabFile,\n' + ' TestResultStrTab AS StrTabDesc,\n' + ' TestResultStrTab AS StrTabKind,\n' + ' TestResultStrTab AS StrTabMime\n' + 'WHERE TestResultFiles.idTestResult = %s\n' + ' AND TestResultFiles.idStrFile = StrTabFile.idStr\n' + ' AND TestResultFiles.idStrDescription = StrTabDesc.idStr\n' + ' AND TestResultFiles.idStrKind = StrTabKind.idStr\n' + ' AND TestResultFiles.idStrMime = StrTabMime.idStr\n' + 'ORDER BY idTestResultFile ASC\n' + , ( oCurNode.idTestResult, )); + for aoRow in self._oDb.fetchAll(): + oCurNode.aoFiles.append(TestResultFileDataEx().initFromDbRow(aoRow)); + + if fHasReasons: + if self.oFailureReasonLogic is None: + self.oFailureReasonLogic = FailureReasonLogic(self._oDb); + if self.oUserAccountLogic is None: + self.oUserAccountLogic = UserAccountLogic(self._oDb); + self._oDb.execute('SELECT *\n' + 'FROM TestResultFailures\n' + 'WHERE idTestResult = %s\n' + ' AND tsExpire = \'infinity\'::TIMESTAMP\n' + , ( oCurNode.idTestResult, )); + if self._oDb.getRowCount() > 0: + oCurNode.oReason = TestResultFailureDataEx().initFromDbRowEx(self._oDb.fetchOne(), self.oFailureReasonLogic, + self.oUserAccountLogic); + + return True; + + + + # + # TestBoxController interface(s). + # + + def _inhumeTestResults(self, aoStack, idTestSet, sError): + """ + The test produces too much output, kill and bury it. + + Note! We leave the test set open, only the test result records are + completed. Thus, _getResultStack will return an empty stack and + cause XML processing to fail immediately, while we can still + record when it actually completed in the test set the normal way. + """ + self._oDb.dprint('** _inhumeTestResults: idTestSet=%d\n%s' % (idTestSet, self._stringifyStack(aoStack),)); + + # + # First add a message. + # + self._newFailureDetails(aoStack[0].idTestResult, idTestSet, sError, None); + + # + # The complete all open test results. + # + for oTestResult in aoStack: + oTestResult.cErrors += 1; + self._completeTestResults(oTestResult, None, TestResultData.ksTestStatus_Failure, oTestResult.cErrors); + + # A bit of paranoia. + self._oDb.execute('UPDATE TestResults\n' + 'SET cErrors = cErrors + 1,\n' + ' enmStatus = \'failure\'::TestStatus_T,\n' + ' tsElapsed = CURRENT_TIMESTAMP - tsCreated\n' + 'WHERE idTestSet = %s\n' + ' AND enmStatus = \'running\'::TestStatus_T\n' + , ( idTestSet, )); + self._oDb.commit(); + + return None; + + def strTabString(self, sString, fCommit = False): + """ + Gets the string table id for the given string, adding it if new. + + Note! A copy of this code is also in TestSetLogic. + """ + ## @todo move this and make a stored procedure for it. + self._oDb.execute('SELECT idStr\n' + 'FROM TestResultStrTab\n' + 'WHERE sValue = %s' + , (sString,)); + if self._oDb.getRowCount() == 0: + self._oDb.execute('INSERT INTO TestResultStrTab (sValue)\n' + 'VALUES (%s)\n' + 'RETURNING idStr\n' + , (sString,)); + if fCommit: + self._oDb.commit(); + return self._oDb.fetchOne()[0]; + + @staticmethod + def _stringifyStack(aoStack): + """Returns a string rep of the stack.""" + sRet = ''; + for i, _ in enumerate(aoStack): + sRet += 'aoStack[%d]=%s\n' % (i, aoStack[i]); + return sRet; + + def _getResultStack(self, idTestSet): + """ + Gets the current stack of result sets. + """ + self._oDb.execute('SELECT *\n' + 'FROM TestResults\n' + 'WHERE idTestSet = %s\n' + ' AND enmStatus = \'running\'::TestStatus_T\n' + 'ORDER BY idTestResult DESC' + , ( idTestSet, )); + aoStack = []; + for aoRow in self._oDb.fetchAll(): + aoStack.append(TestResultData().initFromDbRow(aoRow)); + + for i, _ in enumerate(aoStack): + assert aoStack[i].iNestingDepth == len(aoStack) - i - 1, self._stringifyStack(aoStack); + + return aoStack; + + def _newTestResult(self, idTestResultParent, idTestSet, iNestingDepth, tsCreated, sName, dCounts, fCommit = False): + """ + Creates a new test result. + Returns the TestResultData object for the new record. + May raise exception on database error. + """ + assert idTestResultParent is not None; + assert idTestResultParent > 1; + + # + # This isn't necessarily very efficient, but it's necessary to prevent + # a wild test or testbox from filling up the database. + # + sCountName = 'cTestResults'; + if sCountName not in dCounts: + self._oDb.execute('SELECT COUNT(idTestResult)\n' + 'FROM TestResults\n' + 'WHERE idTestSet = %s\n' + , ( idTestSet,)); + dCounts[sCountName] = self._oDb.fetchOne()[0]; + dCounts[sCountName] += 1; + if dCounts[sCountName] > config.g_kcMaxTestResultsPerTS: + raise TestResultHangingOffence('Too many sub-tests in total!'); + + sCountName = 'cTestResultsIn%d' % (idTestResultParent,); + if sCountName not in dCounts: + self._oDb.execute('SELECT COUNT(idTestResult)\n' + 'FROM TestResults\n' + 'WHERE idTestResultParent = %s\n' + , ( idTestResultParent,)); + dCounts[sCountName] = self._oDb.fetchOne()[0]; + dCounts[sCountName] += 1; + if dCounts[sCountName] > config.g_kcMaxTestResultsPerTR: + raise TestResultHangingOffence('Too many immediate sub-tests!'); + + # This is also a hanging offence. + if iNestingDepth > config.g_kcMaxTestResultDepth: + raise TestResultHangingOffence('To deep sub-test nesting!'); + + # Ditto. + if len(sName) > config.g_kcchMaxTestResultName: + raise TestResultHangingOffence('Test name is too long: %d chars - "%s"' % (len(sName), sName)); + + # + # Within bounds, do the job. + # + idStrName = self.strTabString(sName, fCommit); + self._oDb.execute('INSERT INTO TestResults (\n' + ' idTestResultParent,\n' + ' idTestSet,\n' + ' tsCreated,\n' + ' idStrName,\n' + ' iNestingDepth )\n' + 'VALUES (%s, %s, TIMESTAMP WITH TIME ZONE %s, %s, %s)\n' + 'RETURNING *\n' + , ( idTestResultParent, idTestSet, tsCreated, idStrName, iNestingDepth) ) + oData = TestResultData().initFromDbRow(self._oDb.fetchOne()); + + self._oDb.maybeCommit(fCommit); + return oData; + + def _newTestValue(self, idTestResult, idTestSet, sName, lValue, sUnit, dCounts, tsCreated = None, fCommit = False): + """ + Creates a test value. + May raise exception on database error. + """ + + # + # Bounds checking. + # + sCountName = 'cTestValues'; + if sCountName not in dCounts: + self._oDb.execute('SELECT COUNT(idTestResultValue)\n' + 'FROM TestResultValues, TestResults\n' + 'WHERE TestResultValues.idTestResult = TestResults.idTestResult\n' + ' AND TestResults.idTestSet = %s\n' + , ( idTestSet,)); + dCounts[sCountName] = self._oDb.fetchOne()[0]; + dCounts[sCountName] += 1; + if dCounts[sCountName] > config.g_kcMaxTestValuesPerTS: + raise TestResultHangingOffence('Too many values in total!'); + + sCountName = 'cTestValuesIn%d' % (idTestResult,); + if sCountName not in dCounts: + self._oDb.execute('SELECT COUNT(idTestResultValue)\n' + 'FROM TestResultValues\n' + 'WHERE idTestResult = %s\n' + , ( idTestResult,)); + dCounts[sCountName] = self._oDb.fetchOne()[0]; + dCounts[sCountName] += 1; + if dCounts[sCountName] > config.g_kcMaxTestValuesPerTR: + raise TestResultHangingOffence('Too many immediate values for one test result!'); + + if len(sName) > config.g_kcchMaxTestValueName: + raise TestResultHangingOffence('Value name is too long: %d chars - "%s"' % (len(sName), sName)); + + # + # Do the job. + # + iUnit = constants.valueunit.g_kdNameToConst.get(sUnit, constants.valueunit.NONE); + + idStrName = self.strTabString(sName, fCommit); + if tsCreated is None: + self._oDb.execute('INSERT INTO TestResultValues (\n' + ' idTestResult,\n' + ' idTestSet,\n' + ' idStrName,\n' + ' lValue,\n' + ' iUnit)\n' + 'VALUES ( %s, %s, %s, %s, %s )\n' + , ( idTestResult, idTestSet, idStrName, lValue, iUnit,) ); + else: + self._oDb.execute('INSERT INTO TestResultValues (\n' + ' idTestResult,\n' + ' idTestSet,\n' + ' tsCreated,\n' + ' idStrName,\n' + ' lValue,\n' + ' iUnit)\n' + 'VALUES ( %s, %s, TIMESTAMP WITH TIME ZONE %s, %s, %s, %s )\n' + , ( idTestResult, idTestSet, tsCreated, idStrName, lValue, iUnit,) ); + self._oDb.maybeCommit(fCommit); + return True; + + def _newFailureDetails(self, idTestResult, idTestSet, sText, dCounts, tsCreated = None, fCommit = False): + """ + Creates a record detailing cause of failure. + May raise exception on database error. + """ + + # + # Overflow protection. + # + if dCounts is not None: + sCountName = 'cTestMsgsIn%d' % (idTestResult,); + if sCountName not in dCounts: + self._oDb.execute('SELECT COUNT(idTestResultMsg)\n' + 'FROM TestResultMsgs\n' + 'WHERE idTestResult = %s\n' + , ( idTestResult,)); + dCounts[sCountName] = self._oDb.fetchOne()[0]; + dCounts[sCountName] += 1; + if dCounts[sCountName] > config.g_kcMaxTestMsgsPerTR: + raise TestResultHangingOffence('Too many messages under for one test result!'); + + if len(sText) > config.g_kcchMaxTestMsg: + raise TestResultHangingOffence('Failure details message is too long: %d chars - "%s"' % (len(sText), sText)); + + # + # Do the job. + # + idStrMsg = self.strTabString(sText, fCommit); + if tsCreated is None: + self._oDb.execute('INSERT INTO TestResultMsgs (\n' + ' idTestResult,\n' + ' idTestSet,\n' + ' idStrMsg,\n' + ' enmLevel)\n' + 'VALUES ( %s, %s, %s, %s)\n' + , ( idTestResult, idTestSet, idStrMsg, 'failure',) ); + else: + self._oDb.execute('INSERT INTO TestResultMsgs (\n' + ' idTestResult,\n' + ' idTestSet,\n' + ' tsCreated,\n' + ' idStrMsg,\n' + ' enmLevel)\n' + 'VALUES ( %s, %s, TIMESTAMP WITH TIME ZONE %s, %s, %s)\n' + , ( idTestResult, idTestSet, tsCreated, idStrMsg, 'failure',) ); + + self._oDb.maybeCommit(fCommit); + return True; + + + def _completeTestResults(self, oTestResult, tsDone, enmStatus, cErrors = 0, fCommit = False): + """ + Completes a test result. Updates the oTestResult object. + May raise exception on database error. + """ + self._oDb.dprint('** _completeTestResults: cErrors=%s tsDone=%s enmStatus=%s oTestResults=\n%s' + % (cErrors, tsDone, enmStatus, oTestResult,)); + + # + # Sanity check: No open sub tests (aoStack should make sure about this!). + # + self._oDb.execute('SELECT COUNT(idTestResult)\n' + 'FROM TestResults\n' + 'WHERE idTestResultParent = %s\n' + ' AND enmStatus = %s\n' + , ( oTestResult.idTestResult, TestResultData.ksTestStatus_Running,)); + cOpenSubTest = self._oDb.fetchOne()[0]; + assert cOpenSubTest == 0, 'cOpenSubTest=%d - %s' % (cOpenSubTest, oTestResult,); + assert oTestResult.enmStatus == TestResultData.ksTestStatus_Running; + + # + # Make sure the reporter isn't lying about successes or error counts. + # + self._oDb.execute('SELECT COALESCE(SUM(cErrors), 0)\n' + 'FROM TestResults\n' + 'WHERE idTestResultParent = %s\n' + , ( oTestResult.idTestResult, )); + cMinErrors = self._oDb.fetchOne()[0] + oTestResult.cErrors; + cErrors = max(cErrors, cMinErrors); + if cErrors > 0 and enmStatus == TestResultData.ksTestStatus_Success: + enmStatus = TestResultData.ksTestStatus_Failure + + # + # Do the update. + # + if tsDone is None: + self._oDb.execute('UPDATE TestResults\n' + 'SET cErrors = %s,\n' + ' enmStatus = %s,\n' + ' tsElapsed = CURRENT_TIMESTAMP - tsCreated\n' + 'WHERE idTestResult = %s\n' + 'RETURNING tsElapsed' + , ( cErrors, enmStatus, oTestResult.idTestResult,) ); + else: + self._oDb.execute('UPDATE TestResults\n' + 'SET cErrors = %s,\n' + ' enmStatus = %s,\n' + ' tsElapsed = TIMESTAMP WITH TIME ZONE %s - tsCreated\n' + 'WHERE idTestResult = %s\n' + 'RETURNING tsElapsed' + , ( cErrors, enmStatus, tsDone, oTestResult.idTestResult,) ); + + oTestResult.tsElapsed = self._oDb.fetchOne()[0]; + oTestResult.enmStatus = enmStatus; + oTestResult.cErrors = cErrors; + + self._oDb.maybeCommit(fCommit); + return None; + + def _doPopHint(self, aoStack, cStackEntries, dCounts, idTestSet): + """ Executes a PopHint. """ + assert cStackEntries >= 0; + while len(aoStack) > cStackEntries: + if aoStack[0].enmStatus == TestResultData.ksTestStatus_Running: + self._newFailureDetails(aoStack[0].idTestResult, idTestSet, 'XML error: Missing </Test>', dCounts); + self._completeTestResults(aoStack[0], tsDone = None, cErrors = 1, + enmStatus = TestResultData.ksTestStatus_Failure, fCommit = True); + aoStack.pop(0); + return True; + + + @staticmethod + def _validateElement(sName, dAttribs, fClosed): + """ + Validates an element and its attributes. + """ + + # + # Validate attributes by name. + # + + # Validate integer attributes. + for sAttr in [ 'errors', 'testdepth' ]: + if sAttr in dAttribs: + try: + _ = int(dAttribs[sAttr]); + except: + return 'Element %s has an invalid %s attribute value: %s.' % (sName, sAttr, dAttribs[sAttr],); + + # Validate long attributes. + for sAttr in [ 'value', ]: + if sAttr in dAttribs: + try: + _ = long(dAttribs[sAttr]); # pylint: disable=redefined-variable-type + except: + return 'Element %s has an invalid %s attribute value: %s.' % (sName, sAttr, dAttribs[sAttr],); + + # Validate string attributes. + for sAttr in [ 'name', 'text' ]: # 'unit' can be zero length. + if sAttr in dAttribs and not dAttribs[sAttr]: + return 'Element %s has an empty %s attribute value.' % (sName, sAttr,); + + # Validate the timestamp attribute. + if 'timestamp' in dAttribs: + (dAttribs['timestamp'], sError) = ModelDataBase.validateTs(dAttribs['timestamp'], fAllowNull = False); + if sError is not None: + return 'Element %s has an invalid timestamp ("%s"): %s' % (sName, dAttribs['timestamp'], sError,); + + + # + # Check that attributes that are required are present. + # We ignore extra attributes. + # + dElementAttribs = \ + { + 'Test': [ 'timestamp', 'name', ], + 'Value': [ 'timestamp', 'name', 'unit', 'value', ], + 'FailureDetails': [ 'timestamp', 'text', ], + 'Passed': [ 'timestamp', ], + 'Skipped': [ 'timestamp', ], + 'Failed': [ 'timestamp', 'errors', ], + 'TimedOut': [ 'timestamp', 'errors', ], + 'End': [ 'timestamp', ], + 'PushHint': [ 'testdepth', ], + 'PopHint': [ 'testdepth', ], + }; + if sName not in dElementAttribs: + return 'Unknown element "%s".' % (sName,); + for sAttr in dElementAttribs[sName]: + if sAttr not in dAttribs: + return 'Element %s requires attribute "%s".' % (sName, sAttr); + + # + # Only the Test element can (and must) remain open. + # + if sName == 'Test' and fClosed: + return '<Test/> is not allowed.'; + if sName != 'Test' and not fClosed: + return 'All elements except <Test> must be closed.'; + + return None; + + @staticmethod + def _parseElement(sElement): + """ + Parses an element. + + """ + # + # Element level bits. + # + sName = sElement.split()[0]; + sElement = sElement[len(sName):]; + + fClosed = sElement[-1] == '/'; + if fClosed: + sElement = sElement[:-1]; + + # + # Attributes. + # + sError = None; + dAttribs = {}; + sElement = sElement.strip(); + while sElement: + # Extract attribute name. + off = sElement.find('='); + if off < 0 or not sElement[:off].isalnum(): + sError = 'Attributes shall have alpha numberical names and have values.'; + break; + sAttr = sElement[:off]; + + # Extract attribute value. + if off + 2 >= len(sElement) or sElement[off + 1] != '"': + sError = 'Attribute (%s) value is missing or not in double quotes.' % (sAttr,); + break; + off += 2; + offEndQuote = sElement.find('"', off); + if offEndQuote < 0: + sError = 'Attribute (%s) value is missing end quotation mark.' % (sAttr,); + break; + sValue = sElement[off:offEndQuote]; + + # Check for duplicates. + if sAttr in dAttribs: + sError = 'Attribute "%s" appears more than once.' % (sAttr,); + break; + + # Unescape the value. + sValue = sValue.replace('<', '<'); + sValue = sValue.replace('>', '>'); + sValue = sValue.replace(''', '\''); + sValue = sValue.replace('"', '"'); + sValue = sValue.replace('
', '\n'); + sValue = sValue.replace('
', '\r'); + sValue = sValue.replace('&', '&'); # last + + # Done. + dAttribs[sAttr] = sValue; + + # advance + sElement = sElement[offEndQuote + 1:]; + sElement = sElement.lstrip(); + + # + # Validate the element before we return. + # + if sError is None: + sError = TestResultLogic._validateElement(sName, dAttribs, fClosed); + + return (sName, dAttribs, sError) + + def _handleElement(self, sName, dAttribs, idTestSet, aoStack, aaiHints, dCounts): + """ + Worker for processXmlStream that handles one element. + + Returns None on success, error string on bad XML or similar. + Raises exception on hanging offence and on database error. + """ + if sName == 'Test': + iNestingDepth = aoStack[0].iNestingDepth + 1 if aoStack else 0; + aoStack.insert(0, self._newTestResult(idTestResultParent = aoStack[0].idTestResult, idTestSet = idTestSet, + tsCreated = dAttribs['timestamp'], sName = dAttribs['name'], + iNestingDepth = iNestingDepth, dCounts = dCounts, fCommit = True) ); + + elif sName == 'Value': + self._newTestValue(idTestResult = aoStack[0].idTestResult, idTestSet = idTestSet, tsCreated = dAttribs['timestamp'], + sName = dAttribs['name'], sUnit = dAttribs['unit'], lValue = long(dAttribs['value']), + dCounts = dCounts, fCommit = True); + + elif sName == 'FailureDetails': + self._newFailureDetails(idTestResult = aoStack[0].idTestResult, idTestSet = idTestSet, + tsCreated = dAttribs['timestamp'], sText = dAttribs['text'], dCounts = dCounts, + fCommit = True); + + elif sName == 'Passed': + self._completeTestResults(aoStack[0], tsDone = dAttribs['timestamp'], + enmStatus = TestResultData.ksTestStatus_Success, fCommit = True); + + elif sName == 'Skipped': + self._completeTestResults(aoStack[0], tsDone = dAttribs['timestamp'], + enmStatus = TestResultData.ksTestStatus_Skipped, fCommit = True); + + elif sName == 'Failed': + self._completeTestResults(aoStack[0], tsDone = dAttribs['timestamp'], cErrors = int(dAttribs['errors']), + enmStatus = TestResultData.ksTestStatus_Failure, fCommit = True); + + elif sName == 'TimedOut': + self._completeTestResults(aoStack[0], tsDone = dAttribs['timestamp'], cErrors = int(dAttribs['errors']), + enmStatus = TestResultData.ksTestStatus_TimedOut, fCommit = True); + + elif sName == 'End': + self._completeTestResults(aoStack[0], tsDone = dAttribs['timestamp'], + cErrors = int(dAttribs.get('errors', '1')), + enmStatus = TestResultData.ksTestStatus_Success, fCommit = True); + + elif sName == 'PushHint': + if len(aaiHints) > 1: + return 'PushHint cannot be nested.' + + aaiHints.insert(0, [len(aoStack), int(dAttribs['testdepth'])]); + + elif sName == 'PopHint': + if not aaiHints: + return 'No hint to pop.' + + iDesiredTestDepth = int(dAttribs['testdepth']); + cStackEntries, iTestDepth = aaiHints.pop(0); + self._doPopHint(aoStack, cStackEntries, dCounts, idTestSet); # Fake the necessary '<End/></Test>' tags. + if iDesiredTestDepth != iTestDepth: + return 'PopHint tag has different testdepth: %d, on stack %d.' % (iDesiredTestDepth, iTestDepth); + else: + return 'Unexpected element "%s".' % (sName,); + return None; + + + def processXmlStream(self, sXml, idTestSet): + """ + Processes the "XML" stream section given in sXml. + + The sXml isn't a complete XML document, even should we save up all sXml + for a given set, they may not form a complete and well formed XML + document since the test may be aborted, abend or simply be buggy. We + therefore do our own parsing and treat the XML tags as commands more + than anything else. + + Returns (sError, fUnforgivable), where sError is None on success. + May raise database exception. + """ + aoStack = self._getResultStack(idTestSet); # [0] == top; [-1] == bottom. + if not aoStack: + return ('No open results', True); + self._oDb.dprint('** processXmlStream len(aoStack)=%s' % (len(aoStack),)); + #self._oDb.dprint('processXmlStream: %s' % (self._stringifyStack(aoStack),)); + #self._oDb.dprint('processXmlStream: sXml=%s' % (sXml,)); + + dCounts = {}; + aaiHints = []; + sError = None; + + fExpectCloseTest = False; + sXml = sXml.strip(); + while sXml: + if sXml.startswith('</Test>'): # Only closing tag. + offNext = len('</Test>'); + if len(aoStack) <= 1: + sError = 'Trying to close the top test results.' + break; + # ASSUMES that we've just seen an <End/>, <Passed/>, <Failed/>, + # <TimedOut/> or <Skipped/> tag earlier in this call! + if aoStack[0].enmStatus == TestResultData.ksTestStatus_Running or not fExpectCloseTest: + sError = 'Missing <End/>, <Passed/>, <Failed/>, <TimedOut/> or <Skipped/> tag.'; + break; + aoStack.pop(0); + fExpectCloseTest = False; + + elif fExpectCloseTest: + sError = 'Expected </Test>.' + break; + + elif sXml.startswith('<?xml '): # Ignore (included files). + offNext = sXml.find('?>'); + if offNext < 0: + sError = 'Unterminated <?xml ?> element.'; + break; + offNext += 2; + + elif sXml[0] == '<': + # Parse and check the tag. + if not sXml[1].isalpha(): + sError = 'Malformed element.'; + break; + offNext = sXml.find('>') + if offNext < 0: + sError = 'Unterminated element.'; + break; + (sName, dAttribs, sError) = self._parseElement(sXml[1:offNext]); + offNext += 1; + if sError is not None: + break; + + # Handle it. + try: + sError = self._handleElement(sName, dAttribs, idTestSet, aoStack, aaiHints, dCounts); + except TestResultHangingOffence as oXcpt: + self._inhumeTestResults(aoStack, idTestSet, str(oXcpt)); + return (str(oXcpt), True); + + + fExpectCloseTest = sName in [ 'End', 'Passed', 'Failed', 'TimedOut', 'Skipped', ]; + else: + sError = 'Unexpected content.'; + break; + + # Advance. + sXml = sXml[offNext:]; + sXml = sXml.lstrip(); + + # + # Post processing checks. + # + if sError is None and fExpectCloseTest: + sError = 'Expected </Test> before the end of the XML section.' + elif sError is None and aaiHints: + sError = 'Expected </PopHint> before the end of the XML section.' + if aaiHints: + self._doPopHint(aoStack, aaiHints[-1][0], dCounts, idTestSet); + + # + # Log the error. + # + if sError is not None: + SystemLogLogic(self._oDb).addEntry(SystemLogData.ksEvent_XmlResultMalformed, + 'idTestSet=%s idTestResult=%s XML="%s" %s' + % ( idTestSet, + aoStack[0].idTestResult if aoStack else -1, + sXml[:min(len(sXml), 30)], + sError, ), + cHoursRepeat = 6, fCommit = True); + return (sError, False); + + + + + +# +# Unit testing. +# + +# pylint: disable=missing-docstring +class TestResultDataTestCase(ModelDataBaseTestCase): + def setUp(self): + self.aoSamples = [TestResultData(),]; + +class TestResultValueDataTestCase(ModelDataBaseTestCase): + def setUp(self): + self.aoSamples = [TestResultValueData(),]; + +if __name__ == '__main__': + unittest.main(); + # not reached. + diff --git a/src/VBox/ValidationKit/testmanager/core/testset.py b/src/VBox/ValidationKit/testmanager/core/testset.py new file mode 100755 index 00000000..e9e408fe --- /dev/null +++ b/src/VBox/ValidationKit/testmanager/core/testset.py @@ -0,0 +1,869 @@ +# -*- coding: utf-8 -*- +# $Id: testset.py $ + +""" +Test Manager - TestSet. +""" + +__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 zipfile; +import unittest; + +# Validation Kit imports. +from common import utils; +from testmanager import config; +from testmanager.core import db; +from testmanager.core.base import ModelDataBase, ModelDataBaseTestCase, ModelLogicBase, \ + TMExceptionBase, TMTooManyRows, TMRowNotFound; +from testmanager.core.testbox import TestBoxData; +from testmanager.core.testresults import TestResultFileDataEx; + + +class TestSetData(ModelDataBase): + """ + TestSet Data. + """ + + ## @name TestStatus_T + # @{ + ksTestStatus_Running = 'running'; + ksTestStatus_Success = 'success'; + ksTestStatus_Skipped = 'skipped'; + ksTestStatus_BadTestBox = 'bad-testbox'; + ksTestStatus_Aborted = 'aborted'; + ksTestStatus_Failure = 'failure'; + ksTestStatus_TimedOut = 'timed-out'; + ksTestStatus_Rebooted = 'rebooted'; + ## @} + + ## List of relatively harmless (to testgroup/case) statuses. + kasHarmlessTestStatuses = [ ksTestStatus_Skipped, ksTestStatus_BadTestBox, ksTestStatus_Aborted, ]; + ## List of bad statuses. + kasBadTestStatuses = [ ksTestStatus_Failure, ksTestStatus_TimedOut, ksTestStatus_Rebooted, ]; + + ksIdAttr = 'idTestSet'; + + ksParam_idTestSet = 'TestSet_idTestSet'; + ksParam_tsConfig = 'TestSet_tsConfig'; + ksParam_tsCreated = 'TestSet_tsCreated'; + ksParam_tsDone = 'TestSet_tsDone'; + ksParam_enmStatus = 'TestSet_enmStatus'; + ksParam_idBuild = 'TestSet_idBuild'; + ksParam_idBuildCategory = 'TestSet_idBuildCategory'; + ksParam_idBuildTestSuite = 'TestSet_idBuildTestSuite'; + ksParam_idGenTestBox = 'TestSet_idGenTestBox'; + ksParam_idTestBox = 'TestSet_idTestBox'; + ksParam_idSchedGroup = 'TestSet_idSchedGroup'; + ksParam_idTestGroup = 'TestSet_idTestGroup'; + ksParam_idGenTestCase = 'TestSet_idGenTestCase'; + ksParam_idTestCase = 'TestSet_idTestCase'; + ksParam_idGenTestCaseArgs = 'TestSet_idGenTestCaseArgs'; + ksParam_idTestCaseArgs = 'TestSet_idTestCaseArgs'; + ksParam_idTestResult = 'TestSet_idTestResult'; + ksParam_sBaseFilename = 'TestSet_sBaseFilename'; + ksParam_iGangMemberNo = 'TestSet_iGangMemberNo'; + ksParam_idTestSetGangLeader = 'TestSet_idTestSetGangLeader'; + + kasAllowNullAttributes = [ 'tsDone', 'idBuildTestSuite', 'idTestSetGangLeader' ]; + kasValidValues_enmStatus = [ + ksTestStatus_Running, + ksTestStatus_Success, + ksTestStatus_Skipped, + ksTestStatus_BadTestBox, + ksTestStatus_Aborted, + ksTestStatus_Failure, + ksTestStatus_TimedOut, + ksTestStatus_Rebooted, + ]; + kiMin_iGangMemberNo = 0; + kiMax_iGangMemberNo = 1023; + + + kcDbColumns = 20; + + def __init__(self): + ModelDataBase.__init__(self); + + # + # Initialize with defaults. + # See the database for explanations of each of these fields. + # + self.idTestSet = None; + self.tsConfig = None; + self.tsCreated = None; + self.tsDone = None; + self.enmStatus = 'running'; + self.idBuild = None; + self.idBuildCategory = None; + self.idBuildTestSuite = None; + self.idGenTestBox = None; + self.idTestBox = None; + self.idSchedGroup = None; + self.idTestGroup = None; + self.idGenTestCase = None; + self.idTestCase = None; + self.idGenTestCaseArgs = None; + self.idTestCaseArgs = None; + self.idTestResult = None; + self.sBaseFilename = None; + self.iGangMemberNo = 0; + self.idTestSetGangLeader = None; + + def initFromDbRow(self, aoRow): + """ + Internal worker for initFromDbWithId and initFromDbWithGenId as well as + TestBoxSetLogic. + """ + + if aoRow is None: + raise TMRowNotFound('TestSet not found.'); + + self.idTestSet = aoRow[0]; + self.tsConfig = aoRow[1]; + self.tsCreated = aoRow[2]; + self.tsDone = aoRow[3]; + self.enmStatus = aoRow[4]; + self.idBuild = aoRow[5]; + self.idBuildCategory = aoRow[6]; + self.idBuildTestSuite = aoRow[7]; + self.idGenTestBox = aoRow[8]; + self.idTestBox = aoRow[9]; + self.idSchedGroup = aoRow[10]; + self.idTestGroup = aoRow[11]; + self.idGenTestCase = aoRow[12]; + self.idTestCase = aoRow[13]; + self.idGenTestCaseArgs = aoRow[14]; + self.idTestCaseArgs = aoRow[15]; + self.idTestResult = aoRow[16]; + self.sBaseFilename = aoRow[17]; + self.iGangMemberNo = aoRow[18]; + self.idTestSetGangLeader = aoRow[19]; + return self; + + + def initFromDbWithId(self, oDb, idTestSet): + """ + Initialize the object from the database. + """ + oDb.execute('SELECT *\n' + 'FROM TestSets\n' + 'WHERE idTestSet = %s\n' + , (idTestSet, ) ); + aoRow = oDb.fetchOne() + if aoRow is None: + raise TMRowNotFound('idTestSet=%s not found' % (idTestSet,)); + return self.initFromDbRow(aoRow); + + + def openFile(self, sFilename, sMode = 'rb'): + """ + Opens a file. + + Returns (oFile, cbFile, fIsStream) on success. + Returns (None, sErrorMsg, None) on failure. + Will not raise exceptions, unless the class instance is invalid. + """ + assert sMode in [ 'rb', 'r', 'rU' ]; + + # Try raw file first. + sFile1 = os.path.join(config.g_ksFileAreaRootDir, self.sBaseFilename + '-' + sFilename); + try: + oFile = open(sFile1, sMode); # pylint: disable=consider-using-with,unspecified-encoding + return (oFile, os.fstat(oFile.fileno()).st_size, False); + except Exception as oXcpt1: + # Try the zip archive next. + sFile2 = os.path.join(config.g_ksZipFileAreaRootDir, self.sBaseFilename + '.zip'); + try: + oZipFile = zipfile.ZipFile(sFile2, 'r'); # pylint: disable=consider-using-with + oFile = oZipFile.open(sFilename, sMode if sMode != 'rb' else 'r'); # pylint: disable=consider-using-with + cbFile = oZipFile.getinfo(sFilename).file_size; + return (oFile, cbFile, True); + except Exception as oXcpt2: + # Construct a meaningful error message. + try: + if os.path.exists(sFile1): + return (None, 'Error opening "%s": %s' % (sFile1, oXcpt1), None); + if not os.path.exists(sFile2): + return (None, 'File "%s" not found. [%s, %s]' % (sFilename, sFile1, sFile2,), None); + return (None, 'Error opening "%s" inside "%s": %s' % (sFilename, sFile2, oXcpt2), None); + except Exception as oXcpt3: + return (None, 'OMG! %s; %s; %s' % (oXcpt1, oXcpt2, oXcpt3,), None); + return (None, 'Code not reachable!', None); + + def createFile(self, sFilename, sMode = 'wb'): + """ + Creates a new file. + + Returns oFile on success. + Returns sErrorMsg on failure. + """ + assert sMode in [ 'wb', 'w', 'wU' ]; + + # Try raw file first. + sFile1 = os.path.join(config.g_ksFileAreaRootDir, self.sBaseFilename + '-' + sFilename); + try: + if not os.path.exists(os.path.dirname(sFile1)): + os.makedirs(os.path.dirname(sFile1), 0o755); + oFile = open(sFile1, sMode); # pylint: disable=consider-using-with,unspecified-encoding + except Exception as oXcpt1: + return str(oXcpt1); + return oFile; + + @staticmethod + def findLogOffsetForTimestamp(sLogContent, tsTimestamp, offStart = 0, fAfter = False): + """ + Log parsing utility function for finding the offset for the given timestamp. + + We ASSUME the log lines are prefixed with UTC timestamps on the format + '09:43:55.789353'. + + Return index into the sLogContent string, 0 if not found. + """ + # Turn tsTimestamp into a string compatible with what we expect to find in the log. + oTsZulu = db.dbTimestampToZuluDatetime(tsTimestamp); + sWantedTs = oTsZulu.strftime('%H:%M:%S.%f'); + assert len(sWantedTs) == 15; + + # Now loop thru the string, line by line. + offRet = offStart; + off = offStart; + while True: + sThisTs = sLogContent[off : off + 15]; + if len(sThisTs) >= 15 \ + and sThisTs[2] == ':' \ + and sThisTs[5] == ':' \ + and sThisTs[8] == '.' \ + and sThisTs[14] in '0123456789': + if sThisTs < sWantedTs: + offRet = off; + elif sThisTs == sWantedTs: + if not fAfter: + return off; + offRet = off; + else: + if fAfter: + offRet = off; + break; + + # next line. + off = sLogContent.find('\n', off); + if off < 0: + if fAfter: + offRet = len(sLogContent); + break; + off += 1; + + return offRet; + + @staticmethod + def extractLogSection(sLogContent, tsStart, tsLast): + """ + Returns log section from tsStart to tsLast (or all if we cannot make sense of it). + """ + offStart = TestSetData.findLogOffsetForTimestamp(sLogContent, tsStart); + offEnd = TestSetData.findLogOffsetForTimestamp(sLogContent, tsLast, offStart, fAfter = True); + return sLogContent[offStart : offEnd]; + + @staticmethod + def extractLogSectionElapsed(sLogContent, tsStart, tsElapsed): + """ + Returns log section from tsStart and tsElapsed forward (or all if we cannot make sense of it). + """ + tsStart = db.dbTimestampToZuluDatetime(tsStart); + tsLast = tsStart + tsElapsed; + return TestSetData.extractLogSection(sLogContent, tsStart, tsLast); + + + +class TestSetLogic(ModelLogicBase): + """ + TestSet logic. + """ + + + def __init__(self, oDb): + ModelLogicBase.__init__(self, oDb); + + + def tryFetch(self, idTestSet): + """ + Attempts to fetch a test set. + + Returns a TestSetData object on success. + Returns None if no status was found. + Raises exception on other errors. + """ + self._oDb.execute('SELECT *\n' + 'FROM TestSets\n' + 'WHERE idTestSet = %s\n', + (idTestSet,)); + if self._oDb.getRowCount() == 0: + return None; + oData = TestSetData(); + return oData.initFromDbRow(self._oDb.fetchOne()); + + def strTabString(self, sString, fCommit = False): + """ + Gets the string table id for the given string, adding it if new. + """ + ## @todo move this and make a stored procedure for it. + self._oDb.execute('SELECT idStr\n' + 'FROM TestResultStrTab\n' + 'WHERE sValue = %s' + , (sString,)); + if self._oDb.getRowCount() == 0: + self._oDb.execute('INSERT INTO TestResultStrTab (sValue)\n' + 'VALUES (%s)\n' + 'RETURNING idStr\n' + , (sString,)); + if fCommit: + self._oDb.commit(); + return self._oDb.fetchOne()[0]; + + def complete(self, idTestSet, sStatus, fCommit = False): + """ + Completes the testset. + Returns the test set ID of the gang leader, None if no gang involvement. + Raises exceptions on database errors and invalid input. + """ + + assert sStatus != TestSetData.ksTestStatus_Running; + + # + # Get the basic test set data and check if there is anything to do here. + # + oData = TestSetData().initFromDbWithId(self._oDb, idTestSet); + if oData.enmStatus != TestSetData.ksTestStatus_Running: + raise TMExceptionBase('TestSet %s is already completed as %s.' % (idTestSet, oData.enmStatus)); + if oData.idTestResult is None: + raise self._oDb.integrityException('idTestResult is NULL for TestSet %u' % (idTestSet,)); + + # + # Close open sub test results, count these as errors. + # Note! No need to propagate error counts here. Only one tree line will + # have open sets, and it will go all the way to the root. + # + self._oDb.execute('SELECT idTestResult\n' + 'FROM TestResults\n' + 'WHERE idTestSet = %s\n' + ' AND enmStatus = %s\n' + ' AND idTestResult <> %s\n' + 'ORDER BY idTestResult DESC\n' + , (idTestSet, TestSetData.ksTestStatus_Running, oData.idTestResult)); + aaoRows = self._oDb.fetchAll(); + if aaoRows: + idStr = self.strTabString('Unclosed test result', fCommit = fCommit); + for aoRow in aaoRows: + self._oDb.execute('UPDATE TestResults\n' + 'SET enmStatus = \'failure\',\n' + ' tsElapsed = CURRENT_TIMESTAMP - tsCreated,\n' + ' cErrors = cErrors + 1\n' + 'WHERE idTestResult = %s\n' + , (aoRow[0],)); + self._oDb.execute('INSERT INTO TestResultMsgs (idTestResult, idTestSet, idStrMsg, enmLevel)\n' + 'VALUES ( %s, %s, %s, \'failure\'::TestResultMsgLevel_T)\n' + , (aoRow[0], idTestSet, idStr,)); + + # + # If it's a success result, check it against error counters. + # + if sStatus not in TestSetData.kasBadTestStatuses: + self._oDb.execute('SELECT COUNT(*)\n' + 'FROM TestResults\n' + 'WHERE idTestSet = %s\n' + ' AND cErrors > 0\n' + , (idTestSet,)); + cErrors = self._oDb.fetchOne()[0]; + if cErrors > 0: + sStatus = TestSetData.ksTestStatus_Failure; + + # + # If it's an pure 'failure', check for timeouts and propagate it. + # + if sStatus == TestSetData.ksTestStatus_Failure: + self._oDb.execute('SELECT COUNT(*)\n' + 'FROM TestResults\n' + 'WHERE idTestSet = %s\n' + ' AND enmStatus = %s\n' + , ( idTestSet, TestSetData.ksTestStatus_TimedOut, )); + if self._oDb.fetchOne()[0] > 0: + sStatus = TestSetData.ksTestStatus_TimedOut; + + # + # Complete the top level test result and then the test set. + # + self._oDb.execute('UPDATE TestResults\n' + 'SET cErrors = (SELECT COALESCE(SUM(cErrors), 0)\n' + ' FROM TestResults\n' + ' WHERE idTestResultParent = %s)\n' + 'WHERE idTestResult = %s\n' + 'RETURNING cErrors\n' + , (oData.idTestResult, oData.idTestResult)); + cErrors = self._oDb.fetchOne()[0]; + if cErrors == 0 and sStatus in TestSetData.kasBadTestStatuses: + self._oDb.execute('UPDATE TestResults\n' + 'SET cErrors = 1\n' + 'WHERE idTestResult = %s\n' + , (oData.idTestResult,)); + elif cErrors > 0 and sStatus not in TestSetData.kasBadTestStatuses: + sStatus = TestSetData.ksTestStatus_Failure; # Impossible. + self._oDb.execute('UPDATE TestResults\n' + 'SET enmStatus = %s,\n' + ' tsElapsed = CURRENT_TIMESTAMP - tsCreated\n' + 'WHERE idTestResult = %s\n' + , (sStatus, oData.idTestResult,)); + + self._oDb.execute('UPDATE TestSets\n' + 'SET enmStatus = %s,\n' + ' tsDone = CURRENT_TIMESTAMP\n' + 'WHERE idTestSet = %s\n' + , (sStatus, idTestSet,)); + + self._oDb.maybeCommit(fCommit); + return oData.idTestSetGangLeader; + + def completeAsAbandoned(self, idTestSet, fCommit = False): + """ + Completes the testset as abandoned if necessary. + + See scenario #9: + file://../../docs/AutomaticTestingRevamp.html#cleaning-up-abandond-testcase + + Returns True if successfully completed as abandond, False if it's already + completed, and raises exceptions under exceptional circumstances. + """ + + # + # Get the basic test set data and check if there is anything to do here. + # + oData = self.tryFetch(idTestSet); + if oData is None: + return False; + if oData.enmStatus != TestSetData.ksTestStatus_Running: + return False; + + if oData.idTestResult is not None: + # + # Clean up test results, adding a message why they failed. + # + self._oDb.execute('UPDATE TestResults\n' + 'SET enmStatus = \'failure\',\n' + ' tsElapsed = CURRENT_TIMESTAMP - tsCreated,\n' + ' cErrors = cErrors + 1\n' + 'WHERE idTestSet = %s\n' + ' AND enmStatus = \'running\'::TestStatus_T\n' + , (idTestSet,)); + + idStr = self.strTabString('The test was abandond by the testbox', fCommit = fCommit); + self._oDb.execute('INSERT INTO TestResultMsgs (idTestResult, idTestSet, idStrMsg, enmLevel)\n' + 'VALUES ( %s, %s, %s, \'failure\'::TestResultMsgLevel_T)\n' + , (oData.idTestResult, idTestSet, idStr,)); + + # + # Complete the testset. + # + self._oDb.execute('UPDATE TestSets\n' + 'SET enmStatus = \'failure\',\n' + ' tsDone = CURRENT_TIMESTAMP\n' + 'WHERE idTestSet = %s\n' + ' AND enmStatus = \'running\'::TestStatus_T\n' + , (idTestSet,)); + + self._oDb.maybeCommit(fCommit); + return True; + + def completeAsGangGatheringTimeout(self, idTestSet, fCommit = False): + """ + Completes the testset with a gang-gathering timeout. + Raises exceptions on database errors and invalid input. + """ + # + # Get the basic test set data and check if there is anything to do here. + # + oData = TestSetData().initFromDbWithId(self._oDb, idTestSet); + if oData.enmStatus != TestSetData.ksTestStatus_Running: + raise TMExceptionBase('TestSet %s is already completed as %s.' % (idTestSet, oData.enmStatus)); + if oData.idTestResult is None: + raise self._oDb.integrityException('idTestResult is NULL for TestSet %u' % (idTestSet,)); + + # + # Complete the top level test result and then the test set. + # + self._oDb.execute('UPDATE TestResults\n' + 'SET enmStatus = \'failure\',\n' + ' tsElapsed = CURRENT_TIMESTAMP - tsCreated,\n' + ' cErrors = cErrors + 1\n' + 'WHERE idTestSet = %s\n' + ' AND enmStatus = \'running\'::TestStatus_T\n' + , (idTestSet,)); + + idStr = self.strTabString('Gang gathering timed out', fCommit = fCommit); + self._oDb.execute('INSERT INTO TestResultMsgs (idTestResult, idTestSet, idStrMsg, enmLevel)\n' + 'VALUES ( %s, %s, %s, \'failure\'::TestResultMsgLevel_T)\n' + , (oData.idTestResult, idTestSet, idStr,)); + + self._oDb.execute('UPDATE TestSets\n' + 'SET enmStatus = \'failure\',\n' + ' tsDone = CURRENT_TIMESTAMP\n' + 'WHERE idTestSet = %s\n' + , (idTestSet,)); + + self._oDb.maybeCommit(fCommit); + return True; + + def createFile(self, oTestSet, sName, sMime, sKind, sDesc, cbFile, fCommit = False): # pylint: disable=too-many-locals + """ + Creates a file and associating with the current test result record in + the test set. + + Returns file object that the file content can be written to. + Raises exception on database error, I/O errors, if there are too many + files in the test set or if they take up too much disk space. + + The caller (testboxdisp.py) is expected to do basic input validation, + so we skip that and get on with the bits only we can do. + """ + + # + # Furhter input and limit checks. + # + if oTestSet.enmStatus != TestSetData.ksTestStatus_Running: + raise TMExceptionBase('Cannot create files on a test set with status "%s".' % (oTestSet.enmStatus,)); + + self._oDb.execute('SELECT TestResultStrTab.sValue\n' + 'FROM TestResultFiles,\n' + ' TestResults,\n' + ' TestResultStrTab\n' + 'WHERE TestResults.idTestSet = %s\n' + ' AND TestResultFiles.idTestResult = TestResults.idTestResult\n' + ' AND TestResultStrTab.idStr = TestResultFiles.idStrFile\n' + , ( oTestSet.idTestSet,)); + if self._oDb.getRowCount() + 1 > config.g_kcMaxUploads: + raise TMExceptionBase('Uploaded too many files already (%d).' % (self._oDb.getRowCount(),)); + + dFiles = {} + cbTotalFiles = 0; + for aoRow in self._oDb.fetchAll(): + dFiles[aoRow[0].lower()] = 1; # For determining a unique filename further down. + sFile = os.path.join(config.g_ksFileAreaRootDir, oTestSet.sBaseFilename + '-' + aoRow[0]); + try: + cbTotalFiles += os.path.getsize(sFile); + except: + cbTotalFiles += config.g_kcMbMaxUploadSingle * 1048576; + if (cbTotalFiles + cbFile + 1048575) / 1048576 > config.g_kcMbMaxUploadTotal: + raise TMExceptionBase('Will exceed total upload limit: %u bytes + %u bytes > %s MiB.' \ + % (cbTotalFiles, cbFile, config.g_kcMbMaxUploadTotal)); + + # + # Create a new file. + # + self._oDb.execute('SELECT idTestResult\n' + 'FROM TestResults\n' + 'WHERE idTestSet = %s\n' + ' AND enmStatus = \'running\'::TestStatus_T\n' + 'ORDER BY idTestResult DESC\n' + 'LIMIT 1\n' + % ( oTestSet.idTestSet, )); + if self._oDb.getRowCount() < 1: + raise TMExceptionBase('No open test results - someone committed a capital offence or we ran into a race.'); + idTestResult = self._oDb.fetchOne()[0]; + + if sName.lower() in dFiles: + # Note! There is in theory a race here, but that's something the + # test driver doing parallel upload with non-unique names + # should worry about. The TD should always avoid this path. + sOrgName = sName; + for i in range(2, config.g_kcMaxUploads + 6): + sName = '%s-%s' % (i, sName,); + if sName not in dFiles: + break; + sName = None; + if sName is None: + raise TMExceptionBase('Failed to find unique name for %s.' % (sOrgName,)); + + self._oDb.execute('INSERT INTO TestResultFiles(idTestResult, idTestSet, idStrFile, idStrDescription,\n' + ' idStrKind, idStrMime)\n' + 'VALUES (%s, %s, %s, %s, %s, %s)\n' + , ( idTestResult, + oTestSet.idTestSet, + self.strTabString(sName), + self.strTabString(sDesc), + self.strTabString(sKind), + self.strTabString(sMime), + )); + + oFile = oTestSet.createFile(sName, 'wb'); + if utils.isString(oFile): + raise TMExceptionBase('Error creating "%s": %s' % (sName, oFile)); + self._oDb.maybeCommit(fCommit); + return oFile; + + def getGang(self, idTestSetGangLeader): + """ + Returns an array of TestBoxData object representing the gang for the given testset. + """ + self._oDb.execute('SELECT TestBoxesWithStrings.*\n' + 'FROM TestBoxesWithStrings,\n' + ' TestSets' + 'WHERE TestSets.idTestSetGangLeader = %s\n' + ' AND TestSets.idGenTestBox = TestBoxesWithStrings.idGenTestBox\n' + 'ORDER BY iGangMemberNo ASC\n' + , ( idTestSetGangLeader,)); + aaoRows = self._oDb.fetchAll(); + aoTestBoxes = []; + for aoRow in aaoRows: + aoTestBoxes.append(TestBoxData().initFromDbRow(aoRow)); + return aoTestBoxes; + + def getFile(self, idTestSet, idTestResultFile): + """ + Gets the TestResultFileEx corresponding to idTestResultFile. + + Raises an exception if the file wasn't found, doesn't belong to + idTestSet, and on DB error. + """ + self._oDb.execute('SELECT TestResultFiles.*,\n' + ' StrTabFile.sValue AS sFile,\n' + ' StrTabDesc.sValue AS sDescription,\n' + ' StrTabKind.sValue AS sKind,\n' + ' StrTabMime.sValue AS sMime\n' + 'FROM TestResultFiles,\n' + ' TestResultStrTab AS StrTabFile,\n' + ' TestResultStrTab AS StrTabDesc,\n' + ' TestResultStrTab AS StrTabKind,\n' + ' TestResultStrTab AS StrTabMime,\n' + ' TestResults\n' + 'WHERE TestResultFiles.idTestResultFile = %s\n' + ' AND TestResultFiles.idStrFile = StrTabFile.idStr\n' + ' AND TestResultFiles.idStrDescription = StrTabDesc.idStr\n' + ' AND TestResultFiles.idStrKind = StrTabKind.idStr\n' + ' AND TestResultFiles.idStrMime = StrTabMime.idStr\n' + ' AND TestResults.idTestResult = TestResultFiles.idTestResult\n' + ' AND TestResults.idTestSet = %s\n' + , ( idTestResultFile, idTestSet, )); + return TestResultFileDataEx().initFromDbRow(self._oDb.fetchOne()); + + + def getById(self, idTestSet): + """ + Get TestSet table record by its id + """ + self._oDb.execute('SELECT *\n' + 'FROM TestSets\n' + 'WHERE idTestSet=%s\n', + (idTestSet,)) + + aRows = self._oDb.fetchAll() + if len(aRows) not in (0, 1): + raise TMTooManyRows('Found more than one test sets with the same credentials. Database structure is corrupted.') + try: + return TestSetData().initFromDbRow(aRows[0]) + except IndexError: + return None + + + def fetchOrphaned(self): + """ + Returns a list of TestSetData objects of orphaned test sets. + + A test set is orphaned if tsDone is NULL and the testbox has created + one or more newer testsets. + """ + + self._oDb.execute('SELECT TestSets.*\n' + 'FROM TestSets,\n' + ' (SELECT idTestSet, idTestBox FROM TestSets WHERE tsDone is NULL) AS t\n' + 'WHERE TestSets.idTestSet = t.idTestSet\n' + ' AND EXISTS(SELECT 1 FROM TestSets st\n' + ' WHERE st.idTestBox = t.idTestBox AND st.idTestSet > t.idTestSet)\n' + ' AND NOT EXISTS(SELECT 1 FROM TestBoxStatuses tbs\n' + ' WHERE tbs.idTestBox = t.idTestBox AND tbs.idTestSet = t.idTestSet)\n' + 'ORDER by TestSets.idTestBox, TestSets.idTestSet' + ); + aoRet = []; + for aoRow in self._oDb.fetchAll(): + aoRet.append(TestSetData().initFromDbRow(aoRow)); + return aoRet; + + def fetchByAge(self, tsNow = None, cHoursBack = 24): + """ + Returns a list of TestSetData objects of a given time period (default is 24 hours). + + Returns None if no testsets stored, + Returns an empty list if no testsets found with given criteria. + """ + if tsNow is None: + tsNow = self._oDb.getCurrentTimestamp(); + + if self._oDb.getRowCount() == 0: + return None; + + self._oDb.execute('(SELECT *\n' + ' FROM TestSets\n' + ' WHERE tsDone <= %s\n' + ' AND tsDone > (%s - interval \'%s hours\')\n' + ')\n' + , ( tsNow, tsNow, cHoursBack, )); + + aoRet = []; + for aoRow in self._oDb.fetchAll(): + aoRet.append(TestSetData().initFromDbRow(aoRow)); + return aoRet; + + def isTestBoxExecutingTooRapidly(self, idTestBox): ## s/To/Too/ + """ + Checks whether the specified test box is executing tests too rapidly. + + The parameters defining too rapid execution are defined in config.py. + + Returns True if it does, False if it doesn't. + May raise database problems. + """ + + self._oDb.execute('(\n' + 'SELECT tsCreated\n' + 'FROM TestSets\n' + 'WHERE idTestBox = %s\n' + ' AND tsCreated >= (CURRENT_TIMESTAMP - interval \'%s seconds\')\n' + ') UNION (\n' + 'SELECT tsCreated\n' + 'FROM TestSets\n' + 'WHERE idTestBox = %s\n' + ' AND tsCreated >= (CURRENT_TIMESTAMP - interval \'%s seconds\')\n' + ' AND enmStatus >= \'failure\'\n' + ')' + , ( idTestBox, config.g_kcSecMinSinceLastTask, + idTestBox, config.g_kcSecMinSinceLastFailedTask, )); + return self._oDb.getRowCount() > 0; + + + # + # The virtual test sheriff interface. + # + + def fetchBadTestBoxIds(self, cHoursBack = 2, tsNow = None, aidFailureReasons = None): + """ + Fetches a list of test box IDs which returned bad-testbox statuses in the + given period (tsDone). + """ + if tsNow is None: + tsNow = self._oDb.getCurrentTimestamp(); + if aidFailureReasons is None: + aidFailureReasons = [ -1, ]; + self._oDb.execute('(SELECT idTestBox\n' + ' FROM TestSets\n' + ' WHERE TestSets.enmStatus = \'bad-testbox\'\n' + ' AND tsDone <= %s\n' + ' AND tsDone > (%s - interval \'%s hours\')\n' + ') UNION (\n' + ' SELECT TestSets.idTestBox\n' + ' FROM TestSets,\n' + ' TestResultFailures\n' + ' WHERE TestSets.tsDone <= %s\n' + ' AND TestSets.tsDone > (%s - interval \'%s hours\')\n' + ' AND TestSets.enmStatus >= \'failure\'::TestStatus_T\n' + ' AND TestSets.idTestSet = TestResultFailures.idTestSet\n' + ' AND TestResultFailures.tsExpire = \'infinity\'::TIMESTAMP\n' + ' AND TestResultFailures.idFailureReason IN (' + + ', '.join([str(i) for i in aidFailureReasons]) + ')\n' + ')\n' + , ( tsNow, tsNow, cHoursBack, + tsNow, tsNow, cHoursBack, )); + return [aoRow[0] for aoRow in self._oDb.fetchAll()]; + + def fetchSetsForTestBox(self, idTestBox, cHoursBack = 2, tsNow = None): + """ + Fetches the TestSet rows for idTestBox for the given period (tsDone), w/o running ones. + + Returns list of TestSetData sorted by tsDone in descending order. + """ + if tsNow is None: + tsNow = self._oDb.getCurrentTimestamp(); + self._oDb.execute('SELECT *\n' + 'FROM TestSets\n' + 'WHERE TestSets.idTestBox = %s\n' + ' AND tsDone IS NOT NULL\n' + ' AND tsDone <= %s\n' + ' AND tsDone > (%s - interval \'%s hours\')\n' + 'ORDER by tsDone DESC\n' + , ( idTestBox, tsNow, tsNow, cHoursBack,)); + return self._dbRowsToModelDataList(TestSetData); + + def fetchFailedSetsWithoutReason(self, cHoursBack = 2, tsNow = None): + """ + Fetches the TestSet failure rows without any currently (CURRENT_TIMESTAMP + not tsNow) assigned failure reason. + + Returns list of TestSetData sorted by tsDone in descending order. + + Note! Includes bad-testbox sets too as it can be useful to analyze these + too even if we normally count them in the 'skipped' category. + """ + if tsNow is None: + tsNow = self._oDb.getCurrentTimestamp(); + self._oDb.execute('SELECT TestSets.*\n' + 'FROM TestSets\n' + ' LEFT OUTER JOIN TestResultFailures\n' + ' ON TestResultFailures.idTestSet = TestSets.idTestSet\n' + ' AND TestResultFailures.tsExpire = \'infinity\'::TIMESTAMP\n' + 'WHERE TestSets.tsDone IS NOT NULL\n' + ' AND TestSets.enmStatus IN ( %s, %s, %s, %s )\n' + ' AND TestSets.tsDone <= %s\n' + ' AND TestSets.tsDone > (%s - interval \'%s hours\')\n' + ' AND TestResultFailures.idTestSet IS NULL\n' + 'ORDER by tsDone DESC\n' + , ( TestSetData.ksTestStatus_Failure, TestSetData.ksTestStatus_TimedOut, + TestSetData.ksTestStatus_Rebooted, TestSetData.ksTestStatus_BadTestBox, + tsNow, + tsNow, cHoursBack,)); + return self._dbRowsToModelDataList(TestSetData); + + + +# +# Unit testing. +# + +# pylint: disable=missing-docstring +class TestSetDataTestCase(ModelDataBaseTestCase): + def setUp(self): + self.aoSamples = [TestSetData(),]; + +if __name__ == '__main__': + unittest.main(); + # not reached. diff --git a/src/VBox/ValidationKit/testmanager/core/useraccount.pgsql b/src/VBox/ValidationKit/testmanager/core/useraccount.pgsql new file mode 100644 index 00000000..1abd8b71 --- /dev/null +++ b/src/VBox/ValidationKit/testmanager/core/useraccount.pgsql @@ -0,0 +1,178 @@ +-- $Id: useraccount.pgsql $ +--- @file +-- VBox Test Manager Database Stored Procedures - UserAccounts. +-- + +-- +-- 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 +-- + +\set ON_ERROR_STOP 1 +\connect testmanager; + +--- +-- Checks if the user name and login name are unique, ignoring a_uidIgnore. +-- Raises exception if duplicates are found. +-- +-- @internal +-- +CREATE OR REPLACE FUNCTION UserAccountLogic_checkUniqueUser(a_sUsername TEXT, a_sLoginName TEXT, a_uidIgnore INTEGER) + RETURNS VOID AS $$ + DECLARE + v_cRows INTEGER; + BEGIN + -- sUserName + SELECT COUNT(*) INTO v_cRows + FROM Users + WHERE sUsername = a_sUsername + AND tsExpire = 'infinity'::TIMESTAMP + AND uid <> a_uidIgnore; + IF v_cRows <> 0 THEN + RAISE EXCEPTION 'Duplicate user name "%" (% times)', a_sUsername, v_cRows; + END IF; + + -- sLoginName + SELECT COUNT(*) INTO v_cRows + FROM Users + WHERE sLoginName = a_sLoginName + AND tsExpire = 'infinity'::TIMESTAMP + AND uid <> a_uidIgnore; + IF v_cRows <> 0 THEN + RAISE EXCEPTION 'Duplicate login name "%" (% times)', a_sUsername, v_cRows; + END IF; + END; +$$ LANGUAGE plpgsql; + +--- +-- Check that the user account exists. +-- Raises exception if it doesn't. +-- +-- @internal +-- +CREATE OR REPLACE FUNCTION UserAccountLogic_checkExists(a_uid INTEGER) RETURNS VOID AS $$ + DECLARE + v_cUpdatedRows INTEGER; + BEGIN + IF NOT EXISTS( SELECT * + FROM Users + WHERE uid = a_uid + AND tsExpire = 'infinity'::TIMESTAMP ) THEN + RAISE EXCEPTION 'User with ID % does not currently exist', a_uid; + END IF; + END; +$$ LANGUAGE plpgsql; + +--- +-- Historize a row. +-- @internal +-- +CREATE OR REPLACE FUNCTION UserAccountLogic_historizeEntry(a_uid INTEGER, a_tsExpire TIMESTAMP WITH TIME ZONE) + RETURNS VOID AS $$ + DECLARE + v_cUpdatedRows INTEGER; + BEGIN + UPDATE Users + SET tsExpire = a_tsExpire + WHERE uid = a_uid + AND tsExpire = 'infinity'::TIMESTAMP; + GET DIAGNOSTICS v_cUpdatedRows = ROW_COUNT; + IF v_cUpdatedRows <> 1 THEN + IF v_cUpdatedRows = 0 THEN + RAISE EXCEPTION 'User with ID % does not currently exist', a_uid; + END IF; + RAISE EXCEPTION 'Integrity error in UserAccounts: % current rows with uid=%d', v_cUpdatedRows, a_uid; + END IF; + END; +$$ LANGUAGE plpgsql; + + +--- +-- Adds a new user. +-- +CREATE OR REPLACE FUNCTION UserAccountLogic_addEntry(a_uidAuthor INTEGER, a_sUsername TEXT, a_sEmail TEXT, a_sFullName TEXT, + a_sLoginName TEXT, a_fReadOnly BOOLEAN) + RETURNS VOID AS $$ + DECLARE + v_cRows INTEGER; + BEGIN + PERFORM UserAccountLogic_checkUniqueUser(a_sUsername, a_sLoginName, -1); + INSERT INTO Users(uidAuthor, sUsername, sEmail, sFullName, sLoginName) + VALUES (a_uidAuthor, a_sUsername, a_sEmail, a_sFullName, a_sLoginName); + END; +$$ LANGUAGE plpgsql; + +CREATE OR REPLACE FUNCTION UserAccountLogic_editEntry(a_uidAuthor INTEGER, a_uid INTEGER, a_sUsername TEXT, a_sEmail TEXT, + a_sFullName TEXT, a_sLoginName TEXT, a_fReadOnly BOOLEAN) + RETURNS VOID AS $$ + BEGIN + PERFORM UserAccountLogic_checkExists(a_uid); + PERFORM UserAccountLogic_checkUniqueUser(a_sUsername, a_sLoginName, a_uid); + + PERFORM UserAccountLogic_historizeEntry(a_uid, CURRENT_TIMESTAMP); + INSERT INTO Users (uid, uidAuthor, sUsername, sEmail, sFullName, sLoginName, fReadOnly) + VALUES (a_uid, a_uidAuthor, a_sUsername, a_sEmail, a_sFullName, a_sLoginName, a_fReadOnly); + END; +$$ LANGUAGE plpgsql; + + +CREATE OR REPLACE FUNCTION UserAccountLogic_delEntry(a_uidAuthor INTEGER, a_uid INTEGER) RETURNS VOID AS $$ + DECLARE + v_Row Users%ROWTYPE; + v_tsEffective TIMESTAMP WITH TIME ZONE; + BEGIN + -- + -- To preserve the information about who deleted the record, we try to + -- add a dummy record which expires immediately. I say try because of + -- the primary key, we must let the new record be valid for 1 us. :-( + -- + + SELECT * INTO STRICT v_Row + FROM Users + WHERE uid = a_uid + AND tsExpire = 'infinity'::TIMESTAMP; + + v_tsEffective := CURRENT_TIMESTAMP - INTERVAL '1 microsecond'; + IF v_Row.tsEffective < v_tsEffective THEN + PERFORM UserAccountLogic_historizeEntry(a_uid, v_tsEffective); + v_Row.tsEffective = v_tsEffective; + v_Row.tsExpire = CURRENT_TIMESTAMP; + INSERT INTO Users VALUES (v_Row.*); + ELSE + PERFORM UserAccountLogic_historizeEntry(a_uid, CURRENT_TIMESTAMP); + END IF; + + EXCEPTION + WHEN NO_DATA_FOUND THEN + RAISE EXCEPTION 'User with ID % does not currently exist', a_uid; + WHEN TOO_MANY_ROWS THEN + RAISE EXCEPTION 'Integrity error in UserAccounts: Too many current rows for %', a_uid; + END; +$$ LANGUAGE plpgsql; + diff --git a/src/VBox/ValidationKit/testmanager/core/useraccount.py b/src/VBox/ValidationKit/testmanager/core/useraccount.py new file mode 100755 index 00000000..6c3f3f51 --- /dev/null +++ b/src/VBox/ValidationKit/testmanager/core/useraccount.py @@ -0,0 +1,302 @@ +# -*- coding: utf-8 -*- +# $Id: useraccount.py $ + +""" +Test Manager - User DB records management. +""" + +__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 unittest; + +# Validation Kit imports. +from testmanager import config; +from testmanager.core.base import ModelDataBase, ModelLogicBase, ModelDataBaseTestCase, TMTooManyRows, TMRowNotFound; + + +class UserAccountData(ModelDataBase): + """ + User account data + """ + + ksIdAttr = 'uid'; + + ksParam_uid = 'UserAccount_uid' + ksParam_tsExpire = 'UserAccount_tsExpire' + ksParam_tsEffective = 'UserAccount_tsEffective' + ksParam_uidAuthor = 'UserAccount_uidAuthor' + ksParam_sLoginName = 'UserAccount_sLoginName' + ksParam_sUsername = 'UserAccount_sUsername' + ksParam_sEmail = 'UserAccount_sEmail' + ksParam_sFullName = 'UserAccount_sFullName' + ksParam_fReadOnly = 'UserAccount_fReadOnly' + + kasAllowNullAttributes = ['uid', 'tsEffective', 'tsExpire', 'uidAuthor']; + + + def __init__(self): + """Init parameters""" + ModelDataBase.__init__(self); + self.uid = None; + self.tsEffective = None; + self.tsExpire = None; + self.uidAuthor = None; + self.sUsername = None; + self.sEmail = None; + self.sFullName = None; + self.sLoginName = None; + self.fReadOnly = None; + + def initFromDbRow(self, aoRow): + """ + Init from database table row + Returns self. Raises exception of the row is None. + """ + if aoRow is None: + raise TMRowNotFound('User not found.'); + + self.uid = aoRow[0]; + self.tsEffective = aoRow[1]; + self.tsExpire = aoRow[2]; + self.uidAuthor = aoRow[3]; + self.sUsername = aoRow[4]; + self.sEmail = aoRow[5]; + self.sFullName = aoRow[6]; + self.sLoginName = aoRow[7]; + self.fReadOnly = aoRow[8]; + return self; + + def initFromDbWithId(self, oDb, uid, tsNow = None, sPeriodBack = None): + """ + Initialize the object from the database. + """ + oDb.execute(self.formatSimpleNowAndPeriodQuery(oDb, + 'SELECT *\n' + 'FROM Users\n' + 'WHERE uid = %s\n' + , ( uid, ), tsNow, sPeriodBack)); + aoRow = oDb.fetchOne() + if aoRow is None: + raise TMRowNotFound('uid=%s not found (tsNow=%s sPeriodBack=%s)' % (uid, tsNow, sPeriodBack,)); + return self.initFromDbRow(aoRow); + + def _validateAndConvertAttribute(self, sAttr, sParam, oValue, aoNilValues, fAllowNull, oDb): + # Custom handling of the email field. + if sAttr == 'sEmail': + return ModelDataBase.validateEmail(oValue, aoNilValues = aoNilValues, fAllowNull = fAllowNull); + + # Automatically lowercase the login name if we're supposed to do case + # insensitive matching. (The feature assumes lower case in DB.) + if sAttr == 'sLoginName' and oValue is not None and config.g_kfLoginNameCaseInsensitive: + oValue = oValue.lower(); + + return ModelDataBase._validateAndConvertAttribute(self, sAttr, sParam, oValue, aoNilValues, fAllowNull, oDb); + + +class UserAccountLogic(ModelLogicBase): + """ + User account logic (for the Users table). + """ + + def __init__(self, oDb): + ModelLogicBase.__init__(self, oDb) + self.dCache = None; + + def fetchForListing(self, iStart, cMaxRows, tsNow, aiSortColumns = None): + """ + Fetches user accounts. + + Returns an array (list) of UserAccountData items, empty list if none. + Raises exception on error. + """ + _ = aiSortColumns; + if tsNow is None: + self._oDb.execute('SELECT *\n' + 'FROM Users\n' + 'WHERE tsExpire = \'infinity\'::TIMESTAMP\n' + 'ORDER BY sUsername DESC\n' + 'LIMIT %s OFFSET %s\n' + , (cMaxRows, iStart,)); + else: + self._oDb.execute('SELECT *\n' + 'FROM Users\n' + 'WHERE tsExpire > %s\n' + ' AND tsEffective <= %s\n' + 'ORDER BY sUsername DESC\n' + 'LIMIT %s OFFSET %s\n' + , (tsNow, tsNow, cMaxRows, iStart,)); + + aoRows = []; + for _ in range(self._oDb.getRowCount()): + aoRows.append(UserAccountData().initFromDbRow(self._oDb.fetchOne())); + return aoRows; + + def addEntry(self, oData, uidAuthor, fCommit = False): + """ + Add user account entry to the DB. + """ + self._oDb.callProc('UserAccountLogic_addEntry', + (uidAuthor, oData.sUsername, oData.sEmail, oData.sFullName, oData.sLoginName, oData.fReadOnly)); + self._oDb.maybeCommit(fCommit); + return True; + + def editEntry(self, oData, uidAuthor, fCommit = False): + """ + Modify user account. + """ + self._oDb.callProc('UserAccountLogic_editEntry', + ( uidAuthor, oData.uid, oData.sUsername, oData.sEmail, + oData.sFullName, oData.sLoginName, oData.fReadOnly)); + self._oDb.maybeCommit(fCommit); + return True; + + def removeEntry(self, uidAuthor, uid, fCascade = False, fCommit = False): + """ + Delete user account + """ + self._oDb.callProc('UserAccountLogic_delEntry', (uidAuthor, uid)); + self._oDb.maybeCommit(fCommit); + _ = fCascade; + return True; + + def _getByField(self, sField, sValue): + """ + Get user account record by its field value + """ + self._oDb.execute('SELECT *\n' + 'FROM Users\n' + 'WHERE tsExpire = \'infinity\'::TIMESTAMP\n' + ' AND ' + sField + ' = %s' + , (sValue,)) + + aRows = self._oDb.fetchAll() + if len(aRows) not in (0, 1): + raise TMTooManyRows('Found more than one user account with the same credentials. Database structure is corrupted.') + + try: + return aRows[0] + except IndexError: + return [] + + def getById(self, idUserId): + """ + Get user account information by ID. + """ + return self._getByField('uid', idUserId) + + def tryFetchAccountByLoginName(self, sLoginName): + """ + Try get user account information by login name. + + Returns UserAccountData if found, None if not. + Raises exception on DB error. + """ + if config.g_kfLoginNameCaseInsensitive: + sLoginName = sLoginName.lower(); + + self._oDb.execute('SELECT *\n' + 'FROM Users\n' + 'WHERE sLoginName = %s\n' + ' AND tsExpire = \'infinity\'::TIMESTAMP\n' + , (sLoginName, )); + if self._oDb.getRowCount() != 1: + if self._oDb.getRowCount() != 0: + raise self._oDb.integrityException('%u rows in Users with sLoginName="%s"' + % (self._oDb.getRowCount(), sLoginName)); + return None; + return UserAccountData().initFromDbRow(self._oDb.fetchOne()); + + def cachedLookup(self, uid): + """ + Looks up the current UserAccountData object for uid via an object cache. + + Returns a shared UserAccountData object. None if not found. + Raises exception on DB error. + """ + if self.dCache is None: + self.dCache = self._oDb.getCache('UserAccount'); + + oUser = self.dCache.get(uid, None); + if oUser is None: + self._oDb.execute('SELECT *\n' + 'FROM Users\n' + 'WHERE uid = %s\n' + ' AND tsExpire = \'infinity\'::TIMESTAMP\n' + , (uid, )); + if self._oDb.getRowCount() == 0: + # Maybe it was deleted, try get the last entry. + self._oDb.execute('SELECT *\n' + 'FROM Users\n' + 'WHERE uid = %s\n' + 'ORDER BY tsExpire DESC\n' + 'LIMIT 1\n' + , (uid, )); + elif self._oDb.getRowCount() > 1: + raise self._oDb.integrityException('%s infinity rows for %s' % (self._oDb.getRowCount(), uid)); + + if self._oDb.getRowCount() == 1: + oUser = UserAccountData().initFromDbRow(self._oDb.fetchOne()); + self.dCache[uid] = oUser; + return oUser; + + def resolveChangeLogAuthors(self, aoEntries): + """ + Given an array of ChangeLogEntry instances, set sAuthor to whatever + uidAuthor resolves to. + + Returns aoEntries. + Raises exception on DB error. + """ + for oEntry in aoEntries: + oUser = self.cachedLookup(oEntry.uidAuthor) + if oUser is not None: + oEntry.sAuthor = oUser.sUsername; + return aoEntries; + + +# +# Unit testing. +# + +# pylint: disable=missing-docstring +class UserAccountDataTestCase(ModelDataBaseTestCase): + def setUp(self): + self.aoSamples = [UserAccountData(),]; + +if __name__ == '__main__': + unittest.main(); + # not reached. + diff --git a/src/VBox/ValidationKit/testmanager/core/vcsbugreference.py b/src/VBox/ValidationKit/testmanager/core/vcsbugreference.py new file mode 100755 index 00000000..9ae9c941 --- /dev/null +++ b/src/VBox/ValidationKit/testmanager/core/vcsbugreference.py @@ -0,0 +1,251 @@ +# -*- coding: utf-8 -*- +# $Id: vcsbugreference.py $ + +""" +Test Manager - VcsBugReferences +""" + +__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 unittest; + +# Validation Kit imports. +from testmanager.core.base import ModelDataBase, ModelDataBaseTestCase, ModelLogicBase, TMExceptionBase; + + +class VcsBugReferenceData(ModelDataBase): + """ + A version control system (VCS) bug tracker reference (commit message tag). + """ + + #kasIdAttr = ['sRepository','iRevision', 'sBugTracker', 'iBugNo']; + + ksParam_sRepository = 'VcsBugReference_sRepository'; + ksParam_iRevision = 'VcsBugReference_iRevision'; + ksParam_sBugTracker = 'VcsBugReference_sBugTracker'; + ksParam_lBugNo = 'VcsBugReference_lBugNo'; + + kasAllowNullAttributes = [ ]; + + def __init__(self): + ModelDataBase.__init__(self); + + # + # Initialize with defaults. + # See the database for explanations of each of these fields. + # + self.sRepository = None; + self.iRevision = None; + self.sBugTracker = None; + self.lBugNo = None; + + def initFromDbRow(self, aoRow): + """ + Re-initializes the object from a SELECT * FROM VcsBugReferences row. + Returns self. Raises exception if aoRow is None. + """ + if aoRow is None: + raise TMExceptionBase('VcsBugReference not found.'); + + self.sRepository = aoRow[0]; + self.iRevision = aoRow[1]; + self.sBugTracker = aoRow[2]; + self.lBugNo = aoRow[3]; + return self; + + def initFromValues(self, sRepository, iRevision, sBugTracker, lBugNo): + """ + Reinitializes form a set of values. + return self. + """ + self.sRepository = sRepository; + self.iRevision = iRevision; + self.sBugTracker = sBugTracker; + self.lBugNo = lBugNo; + return self; + + +class VcsBugReferenceDataEx(VcsBugReferenceData): + """ + Extended version of VcsBugReferenceData that includes the commit details. + """ + def __init__(self): + VcsBugReferenceData.__init__(self); + self.tsCreated = None; + self.sAuthor = None; + self.sMessage = None; + + def initFromDbRow(self, aoRow): + VcsBugReferenceData.initFromDbRow(self, aoRow); + self.tsCreated = aoRow[4]; + self.sAuthor = aoRow[5]; + self.sMessage = aoRow[6]; + return self; + + +class VcsBugReferenceLogic(ModelLogicBase): # pylint: disable=too-few-public-methods + """ + VCS revision <-> bug tracker references database logic. + """ + + # + # Standard methods. + # + + def fetchForListing(self, iStart, cMaxRows, tsNow, aiSortColumns = None): + """ + Fetches VCS revisions for listing. + + Returns an array (list) of VcsBugReferenceData items, empty list if none. + Raises exception on error. + """ + _ = tsNow; _ = aiSortColumns; + self._oDb.execute(''' +SELECT * +FROM VcsBugReferences +ORDER BY sRepository, iRevision, sBugTracker, lBugNo +LIMIT %s OFFSET %s +''', (cMaxRows, iStart,)); + + aoRows = []; + for _ in range(self._oDb.getRowCount()): + aoRows.append(VcsBugReferenceData().initFromDbRow(self._oDb.fetchOne())); + return aoRows; + + def exists(self, oData): + """ + Checks if the data is already present in the DB. + Returns True / False. + Raises exception on input and database errors. + """ + self._oDb.execute(''' +SELECT COUNT(*) +FROM VcsBugReferences +WHERE sRepository = %s + AND iRevision = %s + AND sBugTracker = %s + AND lBugNo = %s +''', ( oData.sRepository, oData.iRevision, oData.sBugTracker, oData.lBugNo)); + cRows = self._oDb.fetchOne()[0]; + if cRows < 0 or cRows > 1: + raise TMExceptionBase('VcsBugReferences has a primary key problem: %u duplicates' % (cRows,)); + return cRows != 0; + + + # + # Other methods. + # + + def addVcsBugReference(self, oData, fCommit = False): + """ + Adds (or updates) a tree revision record. + Raises exception on input and database errors. + """ + + # Check VcsBugReferenceData before do anything + dDataErrors = oData.validateAndConvert(self._oDb, oData.ksValidateFor_Add); + if dDataErrors: + raise TMExceptionBase('Invalid data passed to addVcsBugReference(): %s' % (dDataErrors,)); + + # Does it already exist? + if not self.exists(oData): + # New row. + self._oDb.execute('INSERT INTO VcsBugReferences (sRepository, iRevision, sBugTracker, lBugNo)\n' + 'VALUES (%s, %s, %s, %s)\n' + , ( oData.sRepository, + oData.iRevision, + oData.sBugTracker, + oData.lBugNo, + )); + + self._oDb.maybeCommit(fCommit); + return oData; + + def getLastRevision(self, sRepository): + """ + Get the last known revision number for the given repository, returns 0 + if the repository is not known to us: + """ + self._oDb.execute(''' +SELECT iRevision +FROM VcsBugReferences +WHERE sRepository = %s +ORDER BY iRevision DESC +LIMIT 1 +''', ( sRepository, )); + if self._oDb.getRowCount() == 0: + return 0; + return self._oDb.fetchOne()[0]; + + def fetchForBug(self, sBugTracker, lBugNo): + """ + Fetches VCS revisions for a bug. + + Returns an array (list) of VcsBugReferenceDataEx items, empty list if none. + Raises exception on error. + """ + self._oDb.execute(''' +SELECT VcsBugReferences.*, + VcsRevisions.tsCreated, + VcsRevisions.sAuthor, + VcsRevisions.sMessage +FROM VcsBugReferences +LEFT OUTER JOIN VcsRevisions ON ( VcsRevisions.sRepository = VcsBugReferences.sRepository + AND VcsRevisions.iRevision = VcsBugReferences.iRevision ) +WHERE sBugTracker = %s + AND lBugNo = %s +ORDER BY VcsRevisions.tsCreated, VcsBugReferences.sRepository, VcsBugReferences.iRevision +''', (sBugTracker, lBugNo,)); + + aoRows = []; + for _ in range(self._oDb.getRowCount()): + aoRows.append(VcsBugReferenceDataEx().initFromDbRow(self._oDb.fetchOne())); + return aoRows; + + +# +# Unit testing. +# + +# pylint: disable=missing-docstring +class VcsBugReferenceDataTestCase(ModelDataBaseTestCase): + def setUp(self): + self.aoSamples = [VcsBugReferenceData(),]; + +if __name__ == '__main__': + unittest.main(); + # not reached. + diff --git a/src/VBox/ValidationKit/testmanager/core/vcsrevisions.py b/src/VBox/ValidationKit/testmanager/core/vcsrevisions.py new file mode 100755 index 00000000..ea841d11 --- /dev/null +++ b/src/VBox/ValidationKit/testmanager/core/vcsrevisions.py @@ -0,0 +1,254 @@ +# -*- coding: utf-8 -*- +# $Id: vcsrevisions.py $ + +""" +Test Manager - VcsRevisions +""" + +__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 unittest; + +# Validation Kit imports. +from testmanager.core.base import ModelDataBase, ModelDataBaseTestCase, ModelLogicBase, TMExceptionBase; + + +class VcsRevisionData(ModelDataBase): + """ + A version control system (VCS) revision. + """ + + #kasIdAttr = ['sRepository',iRevision]; + + ksParam_sRepository = 'VcsRevision_sRepository'; + ksParam_iRevision = 'VcsRevision_iRevision'; + ksParam_tsCreated = 'VcsRevision_tsCreated'; + ksParam_sAuthor = 'VcsRevision_sAuthor'; + ksParam_sMessage = 'VcsRevision_sMessage'; + + kasAllowNullAttributes = [ ]; + kfAllowUnicode_sMessage = True; + kcchMax_sMessage = 8192; + + def __init__(self): + ModelDataBase.__init__(self); + + # + # Initialize with defaults. + # See the database for explanations of each of these fields. + # + self.sRepository = None; + self.iRevision = None; + self.tsCreated = None; + self.sAuthor = None; + self.sMessage = None; + + def initFromDbRow(self, aoRow): + """ + Re-initializes the object from a SELECT * FROM VcsRevisions row. + Returns self. Raises exception if aoRow is None. + """ + if aoRow is None: + raise TMExceptionBase('VcsRevision not found.'); + + self.sRepository = aoRow[0]; + self.iRevision = aoRow[1]; + self.tsCreated = aoRow[2]; + self.sAuthor = aoRow[3]; + self.sMessage = aoRow[4]; + return self; + + def initFromDbWithRepoAndRev(self, oDb, sRepository, iRevision): + """ + Initialize from the database, given the tree and revision of a row. + """ + oDb.execute('SELECT * FROM VcsRevisions WHERE sRepository = %s AND iRevision = %u', (sRepository, iRevision,)); + aoRow = oDb.fetchOne() + if aoRow is None: + raise TMExceptionBase('sRepository = %s iRevision = %u not found' % (sRepository, iRevision, )); + return self.initFromDbRow(aoRow); + + def initFromValues(self, sRepository, iRevision, tsCreated, sAuthor, sMessage): + """ + Reinitializes form a set of values. + return self. + """ + self.sRepository = sRepository; + self.iRevision = iRevision; + self.tsCreated = tsCreated; + self.sAuthor = sAuthor; + self.sMessage = sMessage; + return self; + + +class VcsRevisionLogic(ModelLogicBase): # pylint: disable=too-few-public-methods + """ + VCS revisions database logic. + """ + + # + # Standard methods. + # + + def fetchForListing(self, iStart, cMaxRows, tsNow, aiSortColumns = None): + """ + Fetches VCS revisions for listing. + + Returns an array (list) of VcsRevisionData items, empty list if none. + Raises exception on error. + """ + _ = tsNow; _ = aiSortColumns; + self._oDb.execute('SELECT *\n' + 'FROM VcsRevisions\n' + 'ORDER BY tsCreated, sRepository, iRevision\n' + 'LIMIT %s OFFSET %s\n' + , (cMaxRows, iStart,)); + + aoRows = []; + for _ in range(self._oDb.getRowCount()): + aoRows.append(VcsRevisionData().initFromDbRow(self._oDb.fetchOne())); + return aoRows; + + def tryFetch(self, sRepository, iRevision): + """ + Tries to fetch the specified tree revision record. + Returns VcsRevisionData instance if found, None if not found. + Raises exception on input and database errors. + """ + self._oDb.execute('SELECT * FROM VcsRevisions WHERE sRepository = %s AND iRevision = %s', + ( sRepository, iRevision, )); + aaoRows = self._oDb.fetchAll(); + if len(aaoRows) == 1: + return VcsRevisionData().initFromDbRow(aaoRows[0]); + if aaoRows: + raise TMExceptionBase('VcsRevisions has a primary key problem: %u duplicates' % (len(aaoRows),)); + return None + + + # + # Other methods. + # + + def addVcsRevision(self, oData, fCommit = False): + """ + Adds (or updates) a tree revision record. + Raises exception on input and database errors. + """ + + # Check VcsRevisionData before do anything + dDataErrors = oData.validateAndConvert(self._oDb, oData.ksValidateFor_Add); + if dDataErrors: + raise TMExceptionBase('Invalid data passed to addVcsRevision(): %s' % (dDataErrors,)); + + # Does it already exist? + oOldData = self.tryFetch(oData.sRepository, oData.iRevision); + if oOldData is None: + # New row. + self._oDb.execute('INSERT INTO VcsRevisions (sRepository, iRevision, tsCreated, sAuthor, sMessage)\n' + 'VALUES (%s, %s, %s, %s, %s)\n' + , ( oData.sRepository, + oData.iRevision, + oData.tsCreated, + oData.sAuthor, + oData.sMessage, + )); + elif not oOldData.isEqual(oData): + # Update old row. + self._oDb.execute('UPDATE VcsRevisions\n' + ' SET tsCreated = %s,\n' + ' sAuthor = %s,\n' + ' sMessage = %s\n' + 'WHERE sRepository = %s\n' + ' AND iRevision = %s' + , ( oData.tsCreated, + oData.sAuthor, + oData.sMessage, + oData.sRepository, + oData.iRevision, + )); + + self._oDb.maybeCommit(fCommit); + return oData; + + def getLastRevision(self, sRepository): + """ + Get the last known revision number for the given repository, returns 0 + if the repository is not known to us: + """ + self._oDb.execute('SELECT iRevision\n' + 'FROM VcsRevisions\n' + 'WHERE sRepository = %s\n' + 'ORDER BY iRevision DESC\n' + 'LIMIT 1\n' + , ( sRepository, )); + if self._oDb.getRowCount() == 0: + return 0; + return self._oDb.fetchOne()[0]; + + def fetchTimeline(self, sRepository, iRevision, cEntriesBack): + """ + Fetches a VCS timeline portion for a repository. + + Returns an array (list) of VcsRevisionData items, empty list if none. + Raises exception on error. + """ + self._oDb.execute('SELECT *\n' + 'FROM VcsRevisions\n' + 'WHERE sRepository = %s\n' + ' AND iRevision > %s\n' + ' AND iRevision <= %s\n' + 'ORDER BY iRevision DESC\n' + 'LIMIT %s\n' + , ( sRepository, iRevision - cEntriesBack*2 + 1, iRevision, cEntriesBack)); + aoRows = []; + for _ in range(self._oDb.getRowCount()): + aoRows.append(VcsRevisionData().initFromDbRow(self._oDb.fetchOne())); + return aoRows; + + +# +# Unit testing. +# + +# pylint: disable=missing-docstring +class VcsRevisionDataTestCase(ModelDataBaseTestCase): + def setUp(self): + self.aoSamples = [VcsRevisionData(),]; + +if __name__ == '__main__': + unittest.main(); + # not reached. + diff --git a/src/VBox/ValidationKit/testmanager/core/webservergluebase.py b/src/VBox/ValidationKit/testmanager/core/webservergluebase.py new file mode 100755 index 00000000..a36e1af7 --- /dev/null +++ b/src/VBox/ValidationKit/testmanager/core/webservergluebase.py @@ -0,0 +1,717 @@ +# -*- coding: utf-8 -*- +# $Id: webservergluebase.py $ + +""" +Test Manager Core - Web Server Abstraction Base Class. +""" + +__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 cgitb +import codecs; +import os +import sys + +# Validation Kit imports. +from common import webutils, utils; +from testmanager import config; + + +class WebServerGlueException(Exception): + """ + For exceptions raised by glue code. + """ + pass; # pylint: disable=unnecessary-pass + + +class WebServerGlueBase(object): + """ + Web server interface abstraction and some HTML utils. + """ + + ## Enables more debug output. + kfDebugInfoEnabled = True; + + ## The maximum number of characters to cache. + kcchMaxCached = 65536; + + ## Special getUserName return value. + ksUnknownUser = 'Unknown User'; + + ## HTTP status codes and their messages. + kdStatusMsgs = { + 100: 'Continue', + 101: 'Switching Protocols', + 102: 'Processing', + 103: 'Early Hints', + 200: 'OK', + 201: 'Created', + 202: 'Accepted', + 203: 'Non-Authoritative Information', + 204: 'No Content', + 205: 'Reset Content', + 206: 'Partial Content', + 207: 'Multi-Status', + 208: 'Already Reported', + 226: 'IM Used', + 300: 'Multiple Choices', + 301: 'Moved Permantently', + 302: 'Found', + 303: 'See Other', + 304: 'Not Modified', + 305: 'Use Proxy', + 306: 'Switch Proxy', + 307: 'Temporary Redirect', + 308: 'Permanent Redirect', + 400: 'Bad Request', + 401: 'Unauthorized', + 402: 'Payment Required', + 403: 'Forbidden', + 404: 'Not Found', + 405: 'Method Not Allowed', + 406: 'Not Acceptable', + 407: 'Proxy Authentication Required', + 408: 'Request Timeout', + 409: 'Conflict', + 410: 'Gone', + 411: 'Length Required', + 412: 'Precondition Failed', + 413: 'Payload Too Large', + 414: 'URI Too Long', + 415: 'Unsupported Media Type', + 416: 'Range Not Satisfiable', + 417: 'Expectation Failed', + 418: 'I\'m a teapot', + 421: 'Misdirection Request', + 422: 'Unprocessable Entity', + 423: 'Locked', + 424: 'Failed Dependency', + 425: 'Too Early', + 426: 'Upgrade Required', + 428: 'Precondition Required', + 429: 'Too Many Requests', + 431: 'Request Header Fields Too Large', + 451: 'Unavailable For Legal Reasons', + 500: 'Internal Server Error', + 501: 'Not Implemented', + 502: 'Bad Gateway', + 503: 'Service Unavailable', + 504: 'Gateway Timeout', + 505: 'HTTP Version Not Supported', + 506: 'Variant Also Negotiates', + 507: 'Insufficient Storage', + 508: 'Loop Detected', + 510: 'Not Extended', + 511: 'Network Authentication Required', + }; + + + def __init__(self, sValidationKitDir, fHtmlDebugOutput = True): + self._sValidationKitDir = sValidationKitDir; + + # Debug + self.tsStart = utils.timestampNano(); + self._fHtmlDebugOutput = fHtmlDebugOutput; # For trace + self._oDbgFile = sys.stderr; + if config.g_ksSrvGlueDebugLogDst is not None and config.g_kfSrvGlueDebug is True: + self._oDbgFile = open(config.g_ksSrvGlueDebugLogDst, 'a'); # pylint: disable=consider-using-with,unspecified-encoding + if config.g_kfSrvGlueCgiDumpArgs: + self._oDbgFile.write('Arguments: %s\nEnvironment:\n' % (sys.argv,)); + if config.g_kfSrvGlueCgiDumpEnv: + for sVar in sorted(os.environ): + self._oDbgFile.write(' %s=\'%s\' \\\n' % (sVar, os.environ[sVar],)); + + self._afnDebugInfo = []; + + # HTTP header. + self._fHeaderWrittenOut = False; + self._dHeaderFields = \ + { \ + 'Content-Type': 'text/html; charset=utf-8', + }; + + # Body. + self._sBodyType = None; + self._dParams = {}; + self._sHtmlBody = ''; + self._cchCached = 0; + self._cchBodyWrittenOut = 0; + + # Output. + if sys.version_info[0] >= 3: + self.oOutputRaw = sys.stdout.detach(); # pylint: disable=no-member + sys.stdout = None; # Prevents flush_std_files() from complaining on stderr during sys.exit(). + else: + self.oOutputRaw = sys.stdout; + self.oOutputText = codecs.getwriter('utf-8')(self.oOutputRaw); + + + # + # Get stuff. + # + + def getParameters(self): + """ + Returns a dictionary with the query parameters. + + The parameter name is the key, the values are given as lists. If a + parameter is given more than once, the value is appended to the + existing dictionary entry. + """ + return {}; + + def getClientAddr(self): + """ + Returns the client address, as a string. + """ + raise WebServerGlueException('getClientAddr is not implemented'); + + def getMethod(self): + """ + Gets the HTTP request method. + """ + return 'POST'; + + def getLoginName(self): + """ + Gets login name provided by Apache. + Returns kUnknownUser if not logged on. + """ + return WebServerGlueBase.ksUnknownUser; + + def getUrlScheme(self): + """ + Gets scheme name (aka. access protocol) from request URL, i.e. 'http' or 'https'. + See also urlparse.scheme. + """ + return 'http'; + + def getUrlNetLoc(self): + """ + Gets the network location (server host name / ip) from the request URL. + See also urlparse.netloc. + """ + raise WebServerGlueException('getUrlNetLoc is not implemented'); + + def getUrlPath(self): + """ + Gets the hirarchical path (relative to server) from the request URL. + See also urlparse.path. + Note! This includes the leading slash. + """ + raise WebServerGlueException('getUrlPath is not implemented'); + + def getUrlBasePath(self): + """ + Gets the hirarchical base path (relative to server) from the request URL. + Note! This includes both a leading an trailing slash. + """ + sPath = self.getUrlPath(); # virtual method # pylint: disable=assignment-from-no-return + iLastSlash = sPath.rfind('/'); + if iLastSlash >= 0: + sPath = sPath[:iLastSlash]; + sPath = sPath.rstrip('/'); + return sPath + '/'; + + def getUrl(self): + """ + Gets the URL being accessed, sans parameters. + For instance this will return, "http://localhost/testmanager/admin.cgi" + when "http://localhost/testmanager/admin.cgi?blah=blah" is being access. + """ + return '%s://%s%s' % (self.getUrlScheme(), self.getUrlNetLoc(), self.getUrlPath()); + + def getBaseUrl(self): + """ + Gets the base URL (with trailing slash). + For instance this will return, "http://localhost/testmanager/" when + "http://localhost/testmanager/admin.cgi?blah=blah" is being access. + """ + return '%s://%s%s' % (self.getUrlScheme(), self.getUrlNetLoc(), self.getUrlBasePath()); + + def getUserAgent(self): + """ + Gets the User-Agent field of the HTTP header, returning empty string + if not present. + """ + return ''; + + def getContentType(self): + """ + Gets the Content-Type field of the HTTP header, parsed into a type + string and a dictionary. + """ + return ('text/html', {}); + + def getContentLength(self): + """ + Gets the content length. + Returns int. + """ + return 0; + + def getBodyIoStream(self): + """ + Returns file object for reading the HTML body. + """ + raise WebServerGlueException('getUrlPath is not implemented'); + + def getBodyIoStreamBinary(self): + """ + Returns file object for reading the binary HTML body. + """ + raise WebServerGlueException('getBodyIoStreamBinary is not implemented'); + + # + # Output stuff. + # + + def _writeHeader(self, sHeaderLine): + """ + Worker function which child classes can override. + """ + sys.stderr.write('_writeHeader: cch=%s "%s..."\n' % (len(sHeaderLine), sHeaderLine[0:10],)) + self.oOutputText.write(sHeaderLine); + return True; + + def flushHeader(self): + """ + Flushes the HTTP header. + """ + if self._fHeaderWrittenOut is False: + for sKey, sValue in self._dHeaderFields.items(): + self._writeHeader('%s: %s\n' % (sKey, sValue,)); + self._fHeaderWrittenOut = True; + self._writeHeader('\n'); # End of header indicator. + return None; + + def setHeaderField(self, sField, sValue): + """ + Sets a header field. + """ + assert self._fHeaderWrittenOut is False; + self._dHeaderFields[sField] = sValue; + return True; + + def setRedirect(self, sLocation, iCode = 302): + """ + Sets up redirection of the page. + Raises an exception if called too late. + """ + if self._fHeaderWrittenOut is True: + raise WebServerGlueException('setRedirect called after the header was written'); + if iCode != 302: + raise WebServerGlueException('Redirection code %d is not supported' % (iCode,)); + + self.setHeaderField('Location', sLocation); + self.setHeaderField('Status', '302 Found'); + return True; + + def setStatus(self, iStatus, sMsg = None): + """ Sets the status code. """ + if not sMsg: + sMsg = self.kdStatusMsgs[iStatus]; + return self.setHeaderField('Status', '%u %s' % (iStatus, sMsg)); + + def setContentType(self, sType): + """ Sets the content type header field. """ + return self.setHeaderField('Content-Type', sType); + + def _writeWorker(self, sChunkOfHtml): + """ + Worker function which child classes can override. + """ + sys.stderr.write('_writeWorker: cch=%s "%s..."\n' % (len(sChunkOfHtml), sChunkOfHtml[0:10],)) + self.oOutputText.write(sChunkOfHtml); + return True; + + def write(self, sChunkOfHtml): + """ + Writes chunk of HTML, making sure the HTTP header is flushed first. + """ + if self._sBodyType is None: + self._sBodyType = 'html'; + elif self._sBodyType != 'html': + raise WebServerGlueException('Cannot use writeParameter when body type is "%s"' % (self._sBodyType, )); + + self._sHtmlBody += sChunkOfHtml; + self._cchCached += len(sChunkOfHtml); + + if self._cchCached > self.kcchMaxCached: + self.flush(); + return True; + + def writeRaw(self, abChunk): + """ + Writes a raw chunk the document. Can be binary or any encoding. + No caching. + """ + if self._sBodyType is None: + self._sBodyType = 'raw'; + elif self._sBodyType != 'raw': + raise WebServerGlueException('Cannot use writeRaw when body type is "%s"' % (self._sBodyType, )); + + self.flushHeader(); + if self._cchCached > 0: + self.flush(); + + sys.stderr.write('writeRaw: cb=%s\n' % (len(abChunk),)) + self.oOutputRaw.write(abChunk); + return True; + + def writeParams(self, dParams): + """ + Writes one or more reply parameters in a form style response. The names + and values in dParams are unencoded, this method takes care of that. + + Note! This automatically changes the content type to + 'application/x-www-form-urlencoded', if the header hasn't been flushed + already. + """ + if self._sBodyType is None: + if not self._fHeaderWrittenOut: + self.setHeaderField('Content-Type', 'application/x-www-form-urlencoded; charset=utf-8'); + elif self._dHeaderFields['Content-Type'] != 'application/x-www-form-urlencoded; charset=utf-8': + raise WebServerGlueException('Cannot use writeParams when content-type is "%s"' % \ + (self._dHeaderFields['Content-Type'],)); + self._sBodyType = 'form'; + + elif self._sBodyType != 'form': + raise WebServerGlueException('Cannot use writeParams when body type is "%s"' % (self._sBodyType, )); + + for sKey in dParams: + sValue = str(dParams[sKey]); + self._dParams[sKey] = sValue; + self._cchCached += len(sKey) + len(sValue); + + if self._cchCached > self.kcchMaxCached: + self.flush(); + + return True; + + def flush(self): + """ + Flush the output. + """ + self.flushHeader(); + + if self._sBodyType == 'form': + sBody = webutils.encodeUrlParams(self._dParams); + self._writeWorker(sBody); + + self._dParams = {}; + self._cchBodyWrittenOut += self._cchCached; + + elif self._sBodyType == 'html': + self._writeWorker(self._sHtmlBody); + + self._sHtmlBody = ''; + self._cchBodyWrittenOut += self._cchCached; + + self._cchCached = 0; + return None; + + # + # Paths. + # + + def pathTmWebUI(self): + """ + Gets the path to the TM 'webui' directory. + """ + return os.path.join(self._sValidationKitDir, 'testmanager', 'webui'); + + # + # Error stuff & Debugging. + # + + def errorLog(self, sError, aXcptInfo, sLogFile): + """ + Writes the error to a log file. + """ + # Easy solution for log file size: Only one report. + try: os.unlink(sLogFile); + except: pass; + + # Try write the log file. + fRc = True; + fSaved = self._fHtmlDebugOutput; + + try: + with open(sLogFile, 'w') as oFile: # pylint: disable=unspecified-encoding + oFile.write(sError + '\n\n'); + if aXcptInfo[0] is not None: + oFile.write(' B a c k t r a c e\n'); + oFile.write('===================\n'); + oFile.write(cgitb.text(aXcptInfo, 5)); + oFile.write('\n\n'); + + oFile.write(' D e b u g I n f o\n'); + oFile.write('=====================\n\n'); + self._fHtmlDebugOutput = False; + self.debugDumpStuff(oFile.write); + except: + fRc = False; + + self._fHtmlDebugOutput = fSaved; + return fRc; + + def errorPage(self, sError, aXcptInfo, sLogFile = None): + """ + Displays a page with an error message. + """ + if sLogFile is not None: + self.errorLog(sError, aXcptInfo, sLogFile); + + # Reset buffering, hoping that nothing was flushed yet. + self._sBodyType = None; + self._sHtmlBody = ''; + self._cchCached = 0; + if not self._fHeaderWrittenOut: + if self._fHtmlDebugOutput: + self.setHeaderField('Content-Type', 'text/html; charset=utf-8'); + else: + self.setHeaderField('Content-Type', 'text/plain; charset=utf-8'); + + # Write the error page. + if self._fHtmlDebugOutput: + self.write('<html><head><title>Test Manage Error</title></head>\n' + + '<body><h1>Test Manager Error:</h1>\n' + + '<p>' + sError + '</p>\n'); + else: + self.write(' Test Manage Error\n' + '===================\n' + '\n' + '' + sError + '\n\n'); + + if aXcptInfo[0] is not None: + if self._fHtmlDebugOutput: + self.write('<h1>Backtrace:</h1>\n'); + self.write(cgitb.html(aXcptInfo, 5)); + else: + self.write('Backtrace\n' + '---------\n' + '\n'); + self.write(cgitb.text(aXcptInfo, 5)); + self.write('\n\n'); + + if self.kfDebugInfoEnabled: + if self._fHtmlDebugOutput: + self.write('<h1>Debug Info:</h1>\n'); + else: + self.write('Debug Info\n' + '----------\n' + '\n'); + self.debugDumpStuff(); + + for fn in self._afnDebugInfo: + try: + fn(self, self._fHtmlDebugOutput); + except Exception as oXcpt: + self.write('\nDebug info callback %s raised exception: %s\n' % (fn, oXcpt)); + + if self._fHtmlDebugOutput: + self.write('</body></html>'); + + self.flush(); + + def debugInfoPage(self, fnWrite = None): + """ + Dumps useful debug info. + """ + if fnWrite is None: + fnWrite = self.write; + + fnWrite('<html><head><title>Test Manage Debug Info</title></head>\n<body>\n'); + self.debugDumpStuff(fnWrite = fnWrite); + fnWrite('</body></html>'); + self.flush(); + + def debugDumpDict(self, sName, dDict, fSorted = True, fnWrite = None): + """ + Dumps dictionary. + """ + if fnWrite is None: + fnWrite = self.write; + + asKeys = list(dDict.keys()); + if fSorted: + asKeys.sort(); + + if self._fHtmlDebugOutput: + fnWrite('<h2>%s</h2>\n' + '<table border="1"><tr><th>name</th><th>value</th></tr>\n' % (sName,)); + for sKey in asKeys: + fnWrite(' <tr><td>' + webutils.escapeElem(sKey) + '</td><td>' \ + + webutils.escapeElem(str(dDict.get(sKey))) \ + + '</td></tr>\n'); + fnWrite('</table>\n'); + else: + for i in range(len(sName) - 1): + fnWrite('%s ' % (sName[i],)); + fnWrite('%s\n\n' % (sName[-1],)); + + fnWrite('%28s Value\n' % ('Name',)); + fnWrite('------------------------------------------------------------------------\n'); + for sKey in asKeys: + fnWrite('%28s: %s\n' % (sKey, dDict.get(sKey),)); + fnWrite('\n'); + + return True; + + def debugDumpList(self, sName, aoStuff, fnWrite = None): + """ + Dumps array. + """ + if fnWrite is None: + fnWrite = self.write; + + if self._fHtmlDebugOutput: + fnWrite('<h2>%s</h2>\n' + '<table border="1"><tr><th>index</th><th>value</th></tr>\n' % (sName,)); + for i, _ in enumerate(aoStuff): + fnWrite(' <tr><td>' + str(i) + '</td><td>' + webutils.escapeElem(str(aoStuff[i])) + '</td></tr>\n'); + fnWrite('</table>\n'); + else: + for ch in sName[:-1]: + fnWrite('%s ' % (ch,)); + fnWrite('%s\n\n' % (sName[-1],)); + + fnWrite('Index Value\n'); + fnWrite('------------------------------------------------------------------------\n'); + for i, oStuff in enumerate(aoStuff): + fnWrite('%5u %s\n' % (i, str(oStuff))); + fnWrite('\n'); + + return True; + + def debugDumpParameters(self, fnWrite): + """ Dumps request parameters. """ + if fnWrite is None: + fnWrite = self.write; + + try: + dParams = self.getParameters(); + return self.debugDumpDict('Parameters', dParams); + except Exception as oXcpt: + if self._fHtmlDebugOutput: + fnWrite('<p>Exception %s while retriving parameters.</p>\n' % (oXcpt,)) + else: + fnWrite('Exception %s while retriving parameters.\n' % (oXcpt,)) + return False; + + def debugDumpEnv(self, fnWrite = None): + """ Dumps os.environ. """ + return self.debugDumpDict('Environment (os.environ)', os.environ, fnWrite = fnWrite); + + def debugDumpArgv(self, fnWrite = None): + """ Dumps sys.argv. """ + return self.debugDumpList('Arguments (sys.argv)', sys.argv, fnWrite = fnWrite); + + def debugDumpPython(self, fnWrite = None): + """ + Dump python info. + """ + dInfo = {}; + dInfo['sys.version'] = sys.version; + dInfo['sys.hexversion'] = sys.hexversion; + dInfo['sys.api_version'] = sys.api_version; + if hasattr(sys, 'subversion'): + dInfo['sys.subversion'] = sys.subversion; # pylint: disable=no-member + dInfo['sys.platform'] = sys.platform; + dInfo['sys.executable'] = sys.executable; + dInfo['sys.copyright'] = sys.copyright; + dInfo['sys.byteorder'] = sys.byteorder; + dInfo['sys.exec_prefix'] = sys.exec_prefix; + dInfo['sys.prefix'] = sys.prefix; + dInfo['sys.path'] = sys.path; + dInfo['sys.builtin_module_names'] = sys.builtin_module_names; + dInfo['sys.flags'] = sys.flags; + + return self.debugDumpDict('Python Info', dInfo, fnWrite = fnWrite); + + + def debugDumpStuff(self, fnWrite = None): + """ + Dumps stuff to the error page and debug info page. + Should be extended by child classes when possible. + """ + self.debugDumpParameters(fnWrite); + self.debugDumpEnv(fnWrite); + self.debugDumpArgv(fnWrite); + self.debugDumpPython(fnWrite); + return True; + + def dprint(self, sMessage): + """ + Prints to debug log (usually apache error log). + """ + if config.g_kfSrvGlueDebug is True: + if config.g_kfSrvGlueDebugTS is False: + self._oDbgFile.write(sMessage); + if not sMessage.endswith('\n'): + self._oDbgFile.write('\n'); + else: + tsNow = utils.timestampMilli(); + tsReq = tsNow - (self.tsStart / 1000000); + iPid = os.getpid(); + for sLine in sMessage.split('\n'): + self._oDbgFile.write('%s/%03u,pid=%04x: %s\n' % (tsNow, tsReq, iPid, sLine,)); + + return True; + + def registerDebugInfoCallback(self, fnDebugInfo): + """ + Registers a debug info method for calling when the error page is shown. + + The fnDebugInfo function takes two parameters. The first is this + object, the second is a boolean indicating html (True) or text (False) + output. The return value is ignored. + """ + if self.kfDebugInfoEnabled: + self._afnDebugInfo.append(fnDebugInfo); + return True; + + def unregisterDebugInfoCallback(self, fnDebugInfo): + """ + Unregisters a debug info method previously registered by + registerDebugInfoCallback. + """ + if self.kfDebugInfoEnabled: + try: self._afnDebugInfo.remove(fnDebugInfo); + except: pass; + return True; + diff --git a/src/VBox/ValidationKit/testmanager/core/webservergluecgi.py b/src/VBox/ValidationKit/testmanager/core/webservergluecgi.py new file mode 100755 index 00000000..e530c7ec --- /dev/null +++ b/src/VBox/ValidationKit/testmanager/core/webservergluecgi.py @@ -0,0 +1,100 @@ +# -*- coding: utf-8 -*- +# $Id: webservergluecgi.py $ + +""" +Test Manager Core - Web Server Abstraction Base Class. +""" + +__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 cgitb; +import os; +import sys; +import cgi; + +# Validation Kit imports. +from testmanager.core.webservergluebase import WebServerGlueBase; +from testmanager import config; + + +class WebServerGlueCgi(WebServerGlueBase): + """ + CGI glue. + """ + def __init__(self, sValidationKitDir, fHtmlOutput=True): + WebServerGlueBase.__init__(self, sValidationKitDir, fHtmlOutput); + + if config.g_kfSrvGlueCgiTb is True: + cgitb.enable(format=('html' if fHtmlOutput else 'text')); + + def getParameters(self): + return cgi.parse(keep_blank_values=True); + + def getClientAddr(self): + return os.environ.get('REMOTE_ADDR'); + + def getMethod(self): + return os.environ.get('REQUEST_METHOD', 'POST'); + + def getLoginName(self): + return os.environ.get('REMOTE_USER', WebServerGlueBase.ksUnknownUser); + + def getUrlScheme(self): + return 'https' if 'HTTPS' in os.environ else 'http'; + + def getUrlNetLoc(self): + return os.environ['HTTP_HOST']; + + def getUrlPath(self): + return os.environ['REQUEST_URI']; + + def getUserAgent(self): + return os.getenv('HTTP_USER_AGENT', ''); + + def getContentType(self): + return cgi.parse_header(os.environ.get('CONTENT_TYPE', 'text/html')); + + def getContentLength(self): + return int(os.environ.get('CONTENT_LENGTH', 0)); + + def getBodyIoStream(self): + return sys.stdin; + + def getBodyIoStreamBinary(self): + # Python 3: sys.stdin.read() returns a string. To get untranslated + # binary data we use the sys.stdin.buffer object instead. + return getattr(sys.stdin, 'buffer', sys.stdin); + diff --git a/src/VBox/ValidationKit/testmanager/db/Makefile.kmk b/src/VBox/ValidationKit/testmanager/db/Makefile.kmk new file mode 100644 index 00000000..194c01a2 --- /dev/null +++ b/src/VBox/ValidationKit/testmanager/db/Makefile.kmk @@ -0,0 +1,98 @@ +# $Id: Makefile.kmk $ +## @file +# VirtualBox Validation Kit - Makefile for generating .html from .txt. +# + +# +# Copyright (C) 2006-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 +# + + +# Need proper shell on windows. +DEPTH = ../../../../.. +ifneq ($(wildcard $(DEPTH)/Config.kmk),) + include $(KBUILD_PATH)/header.kmk +else + VBOX_BLD_PYTHON ?= python +endif + + +GENERATED_FILES = TestManagerDatabaseComments.pgsql +PSQL := $(firstword $(which $(foreach pgver, 16 15 14 13 12 10 11 95 94 93 92,psql$(pgver)) ) psql) +ifeq ($(PSQL_DB_HOST),) + PSQL_DB_HOST := localhost # Use localhost if nothing else is set. +endif +ifeq ($(PSQL_DB_PORT),) + PSQL_DB_PORT := 5432 # Same for the port; use the default. +endif +ifeq ($(PSQL_DB_USER),) + PSQL_DB_USER := postgres +endif +PSQL_OPTS = --user=$(PSQL_DB_USER) --set=ON_ERROR_STOP=1 --host=$(PSQL_DB_HOST) --port=$(PSQL_DB_PORT) + +all: $(GENERATED_FILES) + +clean: + kmk_builtin_rm -f -- $(GENERATED_FILES) + + +TestManagerDatabaseComments.pgsql: TestManagerDatabaseInit.pgsql gen-sql-comments.py + LC_ALL=C $(VBOX_BLD_PYTHON) gen-sql-comments.py $< > $@ + + +load-testmanager-db: \ + TestManagerDatabaseInit.pgsql \ + TestManagerDatabaseComments.pgsql \ + ../core/useraccount.pgsql \ + ../core/testcase.pgsql \ + ../core/testbox.pgsql \ + ../core/globalresource.pgsql + @kmk_builtin_echo "Creating testmanager database: For script verification only!" + $(PSQL) $(PSQL_OPTS) -f TestManagerDatabaseInit.pgsql + $(PSQL) $(PSQL_OPTS) -d testmanager -f TestManagerDatabaseComments.pgsql + $(PSQL) $(PSQL_OPTS) -d testmanager -f ../core/useraccount.pgsql + $(PSQL) $(PSQL_OPTS) -d testmanager -f ../core/testcase.pgsql + $(PSQL) $(PSQL_OPTS) -d testmanager -f ../core/testbox.pgsql + $(PSQL) $(PSQL_OPTS) -d testmanager -f ../core/globalresource.pgsql + $(PSQL) $(PSQL_OPTS) -d testmanager -f TestManagerDatabaseDefaultUserAccounts.pgsql + +reload-testmanager-db-functions: \ + ../core/useraccount.pgsql \ + ../core/testcase.pgsql \ + ../core/testbox.pgsql \ + ../core/globalresource.pgsql + @kmk_builtin_echo "Reloading testmanager database functions" + $(PSQL) $(PSQL_OPTS) -d testmanager -f ../core/useraccount.pgsql + $(PSQL) $(PSQL_OPTS) -d testmanager -f ../core/testcase.pgsql + $(PSQL) $(PSQL_OPTS) -d testmanager -f ../core/testbox.pgsql + $(PSQL) $(PSQL_OPTS) -d testmanager -f ../core/globalresource.pgsql + +# Only for prettier graphs: +# $(PSQL) $(PSQL_OPTS) -d testmanager -f TestManagerDatabaseForeignKeyErHacks.pgsql diff --git a/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase.dmd b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase.dmd new file mode 100644 index 00000000..cf35f3fb --- /dev/null +++ b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase.dmd @@ -0,0 +1,8 @@ +<?xml version = '1.0' encoding = 'UTF-8'?> +<OSDM_Design class="oracle.dbtools.crest.model.design.Design" name="TestManagerDatabase" id="99299876-6D97-026B-55F9-DF582D334681" version="3.5"> +<createdBy>bird</createdBy> +<createdTime>2012-08-20 21:58:45 UTC</createdTime> +<ownerDesignName>TestManagerDatabase</ownerDesignName> +<capitalNames>false</capitalNames> +<designId>99299876-6D97-026B-55F9-DF582D334681</designId> +</OSDM_Design>
\ No newline at end of file diff --git a/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/datatypes/DataTypes.xml b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/datatypes/DataTypes.xml new file mode 100644 index 00000000..9b86b6dd --- /dev/null +++ b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/datatypes/DataTypes.xml @@ -0,0 +1,15 @@ +<?xml version = '1.0' encoding = 'UTF-8'?> +<DataTypesDesign class="oracle.dbtools.crest.model.design.datatypes.DataTypesDesign" name="DataTypes" id="E0EE53BE-07B1-7CE9-B0DA-5D939EA4A3C9" mainViewID="E9476B45-3C62-EE27-4705-6F1EFAD11B74"> +<createdBy>bird</createdBy> +<createdTime>2012-08-20 21:58:45 UTC</createdTime> +<ownerDesignName>TestManagerDatabase</ownerDesignName> +<shouldBeOpen>false</shouldBeOpen> +<collectionOfRefsPrefix>array_ref_</collectionOfRefsPrefix> +<collectionPrefix>array_</collectionPrefix> +<defaultArrayLimit>10</defaultArrayLimit> +<defaultCollectionType_Kind>ARRAY</defaultCollectionType_Kind> +<defaultCollectionType_Suffix>_Array</defaultCollectionType_Suffix> +<embeddedStructuredTypePrefix>inst_</embeddedStructuredTypePrefix> +<referencePrefix>ref_</referencePrefix> +<useRoleInAssociationEndAsName>true</useRoleInAssociationEndAsName> +</DataTypesDesign>
\ No newline at end of file diff --git a/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/datatypes/structuredtype/seg_0/47E390DE-0671-C4B1-8428-0F45CBEE18F8.xml b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/datatypes/structuredtype/seg_0/47E390DE-0671-C4B1-8428-0F45CBEE18F8.xml new file mode 100644 index 00000000..e274e153 --- /dev/null +++ b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/datatypes/structuredtype/seg_0/47E390DE-0671-C4B1-8428-0F45CBEE18F8.xml @@ -0,0 +1,37 @@ +<?xml version = '1.0' encoding = 'UTF-8'?> +<StructuredType class="oracle.dbtools.crest.model.design.datatypes.StructuredType" name="SDO_GEOMETRY" id="47E390DE-0671-C4B1-8428-0F45CBEE18F8" directorySegmentName="seg_0"> +<createdBy>bird</createdBy> +<createdTime>2012-08-20 21:58:45 UTC</createdTime> +<ownerDesignName>TestManagerDatabase</ownerDesignName> +<visible>false</visible> +<predefined>true</predefined> +<final>false</final> +<instantiable>true</instantiable> +<fonts> +<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr"> +<foType>Title</foType> +<fontStyle>1</fontStyle> +<colorRGB>-16776961</colorRGB> +</FontObject> +<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr"> +<foType>Attribute</foType> +<colorRGB>-16777216</colorRGB> +</FontObject> +<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr"> +<foType>Datatype</foType> +<colorRGB>-16777056</colorRGB> +</FontObject> +<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr"> +<foType>Method</foType> +<colorRGB>-16777216</colorRGB> +</FontObject> +<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr"> +<foType>Not Instantiable</foType> +<colorRGB>-65536</colorRGB> +</FontObject> +<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr"> +<foType>Mandatory</foType> +<colorRGB>-65536</colorRGB> +</FontObject> +</fonts> +</StructuredType>
\ No newline at end of file diff --git a/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/datatypes/structuredtype/seg_0/F72C39E0-D1CA-8821-2AD7-A1E95A37D3D1.xml b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/datatypes/structuredtype/seg_0/F72C39E0-D1CA-8821-2AD7-A1E95A37D3D1.xml new file mode 100644 index 00000000..5ff7c301 --- /dev/null +++ b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/datatypes/structuredtype/seg_0/F72C39E0-D1CA-8821-2AD7-A1E95A37D3D1.xml @@ -0,0 +1,37 @@ +<?xml version = '1.0' encoding = 'UTF-8'?> +<StructuredType class="oracle.dbtools.crest.model.design.datatypes.StructuredType" name="XMLTYPE" id="F72C39E0-D1CA-8821-2AD7-A1E95A37D3D1" directorySegmentName="seg_0"> +<createdBy>bird</createdBy> +<createdTime>2012-08-20 21:58:45 UTC</createdTime> +<ownerDesignName>TestManagerDatabase</ownerDesignName> +<visible>false</visible> +<predefined>true</predefined> +<final>false</final> +<instantiable>true</instantiable> +<fonts> +<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr"> +<foType>Title</foType> +<fontStyle>1</fontStyle> +<colorRGB>-16776961</colorRGB> +</FontObject> +<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr"> +<foType>Attribute</foType> +<colorRGB>-16777216</colorRGB> +</FontObject> +<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr"> +<foType>Datatype</foType> +<colorRGB>-16777056</colorRGB> +</FontObject> +<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr"> +<foType>Method</foType> +<colorRGB>-16777216</colorRGB> +</FontObject> +<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr"> +<foType>Not Instantiable</foType> +<colorRGB>-65536</colorRGB> +</FontObject> +<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr"> +<foType>Mandatory</foType> +<colorRGB>-65536</colorRGB> +</FontObject> +</fonts> +</StructuredType>
\ No newline at end of file diff --git a/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/datatypes/subviews/E9476B45-3C62-EE27-4705-6F1EFAD11B74.xml b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/datatypes/subviews/E9476B45-3C62-EE27-4705-6F1EFAD11B74.xml new file mode 100644 index 00000000..d09e8a61 --- /dev/null +++ b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/datatypes/subviews/E9476B45-3C62-EE27-4705-6F1EFAD11B74.xml @@ -0,0 +1,21 @@ +<?xml version = '1.0' encoding = 'UTF-8'?> +<Diagram class="oracle.dbtools.crest.swingui.datatypes.DPVDataTypes" id="E9476B45-3C62-EE27-4705-6F1EFAD11B74"> +<createdBy>bird</createdBy> +<createdTime>2012-08-20 21:58:45 UTC</createdTime> +<autoRoute>false</autoRoute> +<boxInbox>true</boxInbox> +<showLegend>false</showLegend> +<showLabels>false</showLabels> +<showGrid>false</showGrid> +<diagramColor>-1</diagramColor> +<display>false</display> +<notation>0</notation> +<objectViews> +<OView class="oracle.dbtools.crest.swingui.datatypes.TVStructuredType" oid="47E390DE-0671-C4B1-8428-0F45CBEE18F8" otype="StructuredType" vid="48CB0B19-5276-2CC9-FC10-6C17D5E5FAC6"> +<bounds x="20" y="20" width="100" height="100"/> +</OView> +<OView class="oracle.dbtools.crest.swingui.datatypes.TVStructuredType" oid="F72C39E0-D1CA-8821-2AD7-A1E95A37D3D1" otype="StructuredType" vid="5CEA75E2-B53E-AD72-1C75-F8820307529C"> +<bounds x="20" y="20" width="100" height="100"/> +</OView> +</objectViews> +</Diagram>
\ No newline at end of file diff --git a/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/defaultRDBMSSites.xml b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/defaultRDBMSSites.xml new file mode 100644 index 00000000..07122f85 --- /dev/null +++ b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/defaultRDBMSSites.xml @@ -0,0 +1,12 @@ +<?xml version="1.0" encoding="ISO-8859-1" ?> +<sites version="1.0"> + <site name="Oracle Database 11g" type="9" oid="32076570-2523-435C-2E92-BF29817DFF70" pathid="1" /> + <site name="Oracle Database 10g" type="8" oid="D9582E4E-79E2-319F-387A-2ED963CB9D32" pathid="2" /> + <site name="Oracle9i" type="7" oid="9807C1FA-0550-772D-1F14-16B19CA63681" pathid="3" /> + <site name="SQL Server 2005" type="5" oid="B0943E51-0387-1F2A-CED9-5FB738BA5A0C" pathid="4" /> + <site name="SQL Server 2000" type="4" oid="3424E3DB-6FE1-14EB-9311-F76EF3096E76" pathid="5" /> + <site name="DB2/390 8" type="1" oid="CC7FDCE5-F5A5-F2C0-C9A7-0C07C92C898D" pathid="6" /> + <site name="DB2/390 7" type="0" oid="26535E02-9B31-3EDE-24D5-4E3188C99288" pathid="7" /> + <site name="DB2/UDB 8.1" type="3" oid="2BAE410E-5CEB-5134-8F33-CCB20E003569" pathid="8" /> + <site name="DB2/UDB 7.1" type="2" oid="BA6252DC-29CE-184D-7701-48F55E3954D4" pathid="9" /> +</sites>
\ No newline at end of file diff --git a/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/defaultdomains.xml b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/defaultdomains.xml new file mode 100644 index 00000000..d858b5c4 --- /dev/null +++ b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/defaultdomains.xml @@ -0,0 +1,13 @@ +<?xml version = '1.0' encoding = 'UTF-8'?> +<DomainFile class="oracle.dbtools.crest.model.design.DomainFileWrapper" fileName="defaultdomains"> + <domains> + <Domain class="oracle.dbtools.crest.model.design.Domain" name="Unknown" id="DOM3000004"> + <createdBy>bird</createdBy> + <createdTime>2012-08-20 21:58:45 UTC</createdTime> + <ownerDesignName>System</ownerDesignName> + <avTSortOrder>0</avTSortOrder> + <fileName>defaultdomains</fileName> + <logicalDatatype>LOGDT017</logicalDatatype> + </Domain> + </domains> +</DomainFile>
\ No newline at end of file diff --git a/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/dl_settings.xml b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/dl_settings.xml new file mode 100644 index 00000000..6c426008 --- /dev/null +++ b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/dl_settings.xml @@ -0,0 +1,288 @@ +<?xml version="1.0" encoding="UTF-8" ?> +<settings> + <logical_type_for_domain_presentation value="false" /> + <automatic_pk_generation value="false" /> + <automatic_uk_generation value="false" /> + <automatic_fk_generation value="false" /> + <substitution_patterns> + </substitution_patterns> + <classification_types> + <type name="Fact" color="-7482" prefix="" id="1" /> + <type name="Dimension" color="-1781507" prefix="" id="2" /> + <type name="Logging" color="-1776412" prefix="" id="3" /> + <type name="Summary" color="-3148598" prefix="" id="4" /> + <type name="Temporary" color="-1" prefix="" id="5" /> + </classification_types> + <default_fonts_and_colors> + <fc_object classname="Entity" background="-5971457" foreground="-16776961"> + <fonts> + <font_object fo_type="Title" font_color="-16776961" font_name="Dialog" font_size="10" font_style="1"/> + <font_object fo_type="Attribute" font_color="-16777216" font_name="Dialog" font_size="10" font_style="0"/> + <font_object fo_type="Datatype" font_color="-16744448" font_name="Dialog" font_size="10" font_style="0"/> + <font_object fo_type="PK Element" font_color="-16776961" font_name="Dialog" font_size="10" font_style="0"/> + <font_object fo_type="FK Element" font_color="-16777216" font_name="Dialog" font_size="10" font_style="0"/> + <font_object fo_type="UK Element" font_color="-16776961" font_name="Dialog" font_size="10" font_style="0"/> + <font_object fo_type="Not Null" font_color="-65536" font_name="Dialog" font_size="10" font_style="0"/> + <font_object fo_type="Key" font_color="-16777216" font_name="Dialog" font_size="10" font_style="0"/> + </fonts> + </fc_object> + <fc_object classname="Logical View" background="-25750" foreground="-16776961"> + <fonts> + <font_object fo_type="Title" font_color="-16776961" font_name="Dialog" font_size="10" font_style="1"/> + <font_object fo_type="Attribute" font_color="-16777216" font_name="Dialog" font_size="10" font_style="0"/> + <font_object fo_type="Datatype" font_color="-16744448" font_name="Dialog" font_size="10" font_style="0"/> + </fonts> + </fc_object> + <fc_object classname="Table" background="-76" foreground="-16776961"> + <fonts> + <font_object fo_type="Title" font_color="-16776961" font_name="Dialog" font_size="10" font_style="1"/> + <font_object fo_type="Column" font_color="-16777216" font_name="Dialog" font_size="10" font_style="0"/> + <font_object fo_type="Datatype" font_color="-16744448" font_name="Dialog" font_size="10" font_style="0"/> + <font_object fo_type="PK Element" font_color="-16776961" font_name="Dialog" font_size="10" font_style="0"/> + <font_object fo_type="FK Element" font_color="-16777216" font_name="Dialog" font_size="10" font_style="0"/> + <font_object fo_type="UK Element" font_color="-16776961" font_name="Dialog" font_size="10" font_style="0"/> + <font_object fo_type="Not Null" font_color="-65536" font_name="Dialog" font_size="10" font_style="0"/> + <font_object fo_type="Key" font_color="-16777216" font_name="Dialog" font_size="10" font_style="0"/> + </fonts> + </fc_object> + <fc_object classname="Relational View" background="-6881386" foreground="-16776961"> + <fonts> + <font_object fo_type="Title" font_color="-16776961" font_name="Dialog" font_size="10" font_style="1"/> + <font_object fo_type="Column" font_color="-16777216" font_name="Dialog" font_size="10" font_style="0"/> + <font_object fo_type="Datatype" font_color="-16744448" font_name="Dialog" font_size="10" font_style="0"/> + </fonts> + </fc_object> + <fc_object classname="Structured Type" background="-7537956" foreground="-16777216"> + <fonts> + <font_object fo_type="Title" font_color="-16776961" font_name="Dialog" font_size="10" font_style="1"/> + <font_object fo_type="Attribute" font_color="-16777216" font_name="Dialog" font_size="10" font_style="0"/> + <font_object fo_type="Datatype" font_color="-16777056" font_name="Dialog" font_size="10" font_style="0"/> + <font_object fo_type="Method" font_color="-16777216" font_name="Dialog" font_size="10" font_style="0"/> + <font_object fo_type="Not Instantiable" font_color="-65536" font_name="Dialog" font_size="10" font_style="0"/> + <font_object fo_type="Mandatory" font_color="-65536" font_name="Dialog" font_size="10" font_style="0"/> + </fonts> + </fc_object> + <fc_object classname="Cube" background="-7482" foreground="-16777216"> + <fonts> + <font_object fo_type="Title" font_color="-16776961" font_name="Dialog" font_size="10" font_style="1"/> + <font_object fo_type="Fact Entities" font_color="-16776961" font_name="Dialog" font_size="10" font_style="0"/> + <font_object fo_type="Measure Type" font_color="-16776961" font_name="Dialog" font_size="10" font_style="0"/> + <font_object fo_type="Measure" font_color="-16777216" font_name="Dialog" font_size="10" font_style="0"/> + <font_object fo_type="Function" font_color="-16777056" font_name="Dialog" font_size="10" font_style="0"/> + <font_object fo_type="Formula" font_color="-65536" font_name="Dialog" font_size="10" font_style="0"/> + <font_object fo_type="Child to Parent Attributes" font_color="-16777216" font_name="Dialog" font_size="10" font_style="0"/> + </fonts> + </fc_object> + <fc_object classname="Dimension" background="-16713196" foreground="-16777216"> + <fonts> + <font_object fo_type="Title" font_color="-16776961" font_name="Dialog" font_size="10" font_style="1"/> + </fonts> + </fc_object> + <fc_object classname="Level" background="-1781507" foreground="-16777216"> + <fonts> + <font_object fo_type="Title" font_color="-16776961" font_name="Dialog" font_size="10" font_style="1"/> + <font_object fo_type="Level Entity" font_color="-16776961" font_name="Dialog" font_size="10" font_style="0"/> + <font_object fo_type="Type" font_color="-16776961" font_name="Dialog" font_size="10" font_style="0"/> + <font_object fo_type="Attribute" font_color="-16777216" font_name="Dialog" font_size="10" font_style="0"/> + <font_object fo_type="Function" font_color="-16777216" font_name="Dialog" font_size="10" font_style="0"/> + </fonts> + </fc_object> + <fc_object classname="Process" background="-106" foreground="-16777216"> + <fonts> + <font_object fo_type="Title" font_color="-16776961" font_name="Dialog" font_size="10" font_style="1"/> + <font_object fo_type="Process Number" font_color="-65536" font_name="Dialog" font_size="10" font_style="0"/> + <font_object fo_type="Transformation Task" font_color="-16777216" font_name="Dialog" font_size="10" font_style="0"/> + </fonts> + </fc_object> + <fc_object classname="External Agent" background="-5570646" foreground="-16777216"> + <fonts> + <font_object fo_type="Title" font_color="-16776961" font_name="Dialog" font_size="10" font_style="1"/> + </fonts> + </fc_object> + <fc_object classname="Information Store" background="-10170881" foreground="-16777216"> + <fonts> + <font_object fo_type="Title" font_color="-16776961" font_name="Dialog" font_size="10" font_style="1"/> + <font_object fo_type="Number" font_color="-1" font_name="Dialog" font_size="10" font_style="1"/> + </fonts> + </fc_object> + <fc_object classname="In-Out Parameters" background="-328966" foreground="-16777216"> + <fonts> + <font_object fo_type="Title" font_color="-16777216" font_name="Dialog" font_size="10" font_style="1"/> + <font_object fo_type="Parameters" font_color="-16777216" font_name="Dialog" font_size="10" font_style="0"/> + <font_object fo_type="Datatype" font_color="-65536" font_name="Dialog" font_size="10" font_style="0"/> + </fonts> + </fc_object> + <fc_object classname="Transformation" background="-43" foreground="-16777216"> + <fonts> + <font_object fo_type="Title" font_color="-16777216" font_name="Dialog" font_size="10" font_style="1"/> + <font_object fo_type="Process Number" font_color="-65536" font_name="Dialog" font_size="10" font_style="0"/> + </fonts> + </fc_object> + <fc_object classname="Note" background="-4144960" foreground="-16777216"> + <fonts> + <font_object fo_type="Title" font_color="-16777216" font_name="Dialog" font_size="10" font_style="0"/> + </fonts> + </fc_object> + <fc_object classname="Label" background="-1" foreground="-16777216"> + <fonts> + <font_object fo_type="Text" font_color="-16777216" font_name="Dialog" font_size="10" font_style="0"/> + </fonts> + </fc_object> + <fc_object classname="Legend" background="-1" foreground="-16777216"> + <fonts> + <font_object fo_type="Text" font_color="-16777216" font_name="Dialog" font_size="10" font_style="0"/> + </fonts> + </fc_object> + </default_fonts_and_colors> + <default_line_widths_and_colors> + <lwc_object classname="Logical Relation" color="-16777216" width="1"> + </lwc_object> + <lwc_object classname="Logical Inheritance" color="-65536" width="1"> + </lwc_object> + <lwc_object classname="Relational Foreign Key" color="-16777216" width="1"> + </lwc_object> + <lwc_object classname="Type Substitution" color="-16725996" width="1"> + </lwc_object> + <lwc_object classname="Datatype Reference" color="-16776961" width="1"> + </lwc_object> + <lwc_object classname="Datatype Inheritance" color="-65536" width="1"> + </lwc_object> + <lwc_object classname="Multidimentional Link" color="-16776961" width="1"> + </lwc_object> + <lwc_object classname="Multidimensional Hierarchy" color="-16725996" width="1"> + </lwc_object> + <lwc_object classname="Process Flow" color="-65536" width="1"> + </lwc_object> + </default_line_widths_and_colors> + <naming_standard_rules> + <logical> + <separator value= "Title Case" char=" "/> + <entity> + </entity> + <attribute> + </attribute> + </logical> + <relational> + <separator value= "_" abbreviated_only="false"/> + <table> + </table> + <column> + </column> + </relational> + <domains> + <separator value= " "/> + <domain> + </domain> + </domains> + <constraints> + <pk value="{table}_PK"/> + <fk value="{child}_{parent}_FK"/> + <ck value="{table}_CK"/> + <un value="{table}_{column}_UN"/> + <idx value="{table}_{column}_IDX"/> + <colck value="CK_{table}_{column}"/> + <column_foreign_key value="{ref table}_{ref column}"/> + <ui value="{entity} PK"/> + <relation_attribute value="{ref entity}_{ref attribute}"/> + </constraints> + <glossaries> + </glossaries> + </naming_standard_rules> +<comparemapping> +</comparemapping> + <engineering_params> + <delete_without_origin value="false"/> + <engineer_coordinates value="true"/> + <engineer_generated value="false"/> + <show_engineering_intree value="false"/> + <apply_naming_std value="false"/> + <use_pref_abbreviation value="true"/> + <upload_directory value=""/> + <date_format value="YYYY/MM/DD HH24:MI:SS"/> + <timestamp_format value="YYYY/MM/DD HH24:MI:SS.FF"/> + <timestamp_tz_format value="YYYY/MM/DD HH24:MI:SS.FFTZH:TZM"/> + </engineering_params> + <eng_compare show_sel_prop_only="true" not_apply_for_new_objects="true" exclude_from_tree="false"> + <entity_table> + <property name="Name" selected="true"/> + <property name="Short Name / Abbreviation" selected="true"/> + <property name="Comment" selected="true"/> + <property name="Comment in RDBMS" selected="true"/> + <property name="Notes" selected="true"/> + <property name="Temporary Table Scope" selected="true"/> + <property name="Table Type" selected="true"/> + <property name="Structured Type" selected="true"/> + <property name="Type Substitution (Super-Type Object)" selected="true"/> + <property name="Min Volumes" selected="true"/> + <property name="Expected Volumes" selected="true"/> + <property name="Max Volumes" selected="true"/> + <property name="Growth Percent" selected="true"/> + <property name="Growth Type" selected="true"/> + <property name="Normal Form" selected="true"/> + <property name="Adequately Normalized" selected="true"/> + </entity_table> + <attribute_column> + <property name="Name" selected="true"/> + <property name="Data Type" selected="true"/> + <property name="Data Type Kind" selected="true"/> + <property name="Mandatory" selected="true"/> + <property name="Default Value" selected="true"/> + <property name="Check Constraint Name" selected="true"/> + <property name="Use Domain Constraint" selected="true"/> + <property name="Check Constraint" selected="true"/> + <property name="Range Constraint" selected="true"/> + <property name="LOV Constraint" selected="true"/> + <property name="Comment" selected="true"/> + <property name="Comment in RDBMS" selected="true"/> + <property name="Notes" selected="true"/> + <property name="Source Type" selected="true"/> + <property name="Formula Description" selected="true"/> + <property name="Type Substitution" selected="true"/> + <property name="Scope" selected="true"/> + </attribute_column> + <key_index> + <property name="Name" selected="true"/> + <property name="Comment" selected="true"/> + <property name="Comment in RDBMS" selected="true"/> + <property name="Notes" selected="true"/> + <property name="Primary Key" selected="true"/> + <property name="Attributes/Columns" selected="true"/> + </key_index> + <relation_fk> + <property name="Name" selected="true"/> + <property name="Delete Rule" selected="true"/> + <property name="Comment" selected="true"/> + <property name="Comment in RDBMS" selected="true"/> + <property name="Notes" selected="true"/> + </relation_fk> + <entityview_view> + <property name="Name" selected="true"/> + <property name="Comment" selected="true"/> + <property name="Comment in RDBMS" selected="true"/> + <property name="Notes" selected="true"/> + <property name="Structured Type" selected="true"/> + <property name="Where" selected="true"/> + <property name="Having" selected="true"/> + <property name="User Defined SQL" selected="true"/> + </entityview_view> + </eng_compare> + <naming_options> + <model_options objectid="B082B14A-BEA8-D8A7-D661-197F34766ED3"> + <naming_option class_name="oracle.dbtools.crest.model.design.relational.Table" max_name_length="30" case_type="2" valid_characters="" all_valid="true" /> + <naming_option class_name="oracle.dbtools.crest.model.design.relational.Column" max_name_length="30" case_type="2" valid_characters="" all_valid="true" /> + <naming_option class_name="oracle.dbtools.crest.model.design.relational.TableView" max_name_length="30" case_type="2" valid_characters="" all_valid="true" /> + <naming_option class_name="oracle.dbtools.crest.model.design.constraint.TableLevelConstraint" max_name_length="30" case_type="2" valid_characters="" all_valid="true" /> + <naming_option class_name="oracle.dbtools.crest.model.design.relational.FKIndexAssociation" max_name_length="30" case_type="2" valid_characters="" all_valid="true" /> + <naming_option class_name="oracle.dbtools.crest.model.design.relational.Index" max_name_length="30" case_type="2" valid_characters="" all_valid="true" /> + </model_options> + <model_options objectid="E3665D68-35D3-8757-63ED-30AEFB972A2C"> + <naming_option class_name="oracle.dbtools.crest.model.design.logical.Entity" max_name_length="254" case_type="2" valid_characters="[a-z][A-Z][0-9]" all_valid="false" /> + <naming_option class_name="oracle.dbtools.crest.model.design.logical.Attribute" max_name_length="254" case_type="2" valid_characters="[a-z][A-Z][0-9] " all_valid="false" /> + <naming_option class_name="oracle.dbtools.crest.model.design.logical.EntityView" max_name_length="254" case_type="2" valid_characters="[a-z][A-Z][0-9] " all_valid="false" /> + </model_options> + </naming_options> + <merge_conflicts> + </merge_conflicts> + <deleted_files> + </deleted_files> +</settings>
\ No newline at end of file diff --git a/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/dr_custom_scripts.xml b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/dr_custom_scripts.xml new file mode 100644 index 00000000..3580f0f0 --- /dev/null +++ b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/dr_custom_scripts.xml @@ -0,0 +1,360 @@ +<?xml version="1.0" encoding="UTF-8" ?> +<dr_custom_scripts> + <scr id="D36CE536-D575-BE5C-625F-23DE23913C6B" name="Complex rule - check comments demo" object="Table" engine="Mozilla Rhino" type="Warning" var="table" library="my first library" method="checkcomments" purpose="validation" > + <script> + <![CDATA[var ruleMessage; +var errType; +var table; +function checkcomments(object){ + result = true; + ruleMessage=""; + if(table.getCommentInRDBMS().equals("")){ + ruleMessage="no comments in RDBMS defined"; + errType="Problem:"; + result = false; + } + if(table.getComment().equals("")){ + if(ruleMessage.equals("")){ + ruleMessage="no comments defined"; + }else{ + ruleMessage= ruleMessage +" , no comments defined"; + } + errType="Error"; + return false; + } + return result; +}]]> + </script> + </scr> + <scr id="0BAA564F-AB5F-D776-2E4F-31FDB3047F69" name="Tables to lower case - Rhino" object="relational" engine="Mozilla Rhino" type="" var="model" library="" method="" purpose="transformation" > + <script> + <![CDATA[tables = model.getTableSet().toArray(); +for (var t = 0; t<tables.length;t++){ + table = tables[t]; + name = table.getName().toLowerCase(); + table.setName(name); + columns = table.getElements(); + size = table.getElementsCollection().size(); + for (var i = 0; i < size; i++) { + column = columns[i]; + cname = column.getName().toLowerCase(); + column.setName(cname); + } + table.setDirty(true); + keys = table.getKeys(); + for (var i = 0; i < keys.length; i++) { + key = keys[i]; + if(!key.isFK()){ + kname = key.getName().toLowerCase(); + key.setName(kname); + }else{ + kname = key.getFKAssociation().getName().toLowerCase(); + key.getFKAssociation().setName(kname); + key.getFKAssociation().setDirty(true); + } + } +}]]> + </script> + </scr> + <scr id="B673F271-4836-DD48-15AC-487DDECCAF49" name="Tables to upper case - JRuby" object="relational" engine="JSR 223 JRuby Engine" type="" var="model" library="" method="" purpose="transformation" > + <script> + <![CDATA[tables =$model.getTableSet().toArray() +for t in 0..tables.length-1 + table = tables[t] + name = table.getName().upcase + table.setName(name) + columns = table.getElements() + size = table.getElementsCollection().size()-1 + for i in 0..size + column = columns[i] + cname = column.getName().upcase + column.setName(cname) + end + keys = table.getKeys() + for i in 0..keys.length-1 + key = keys[i] + kname = key.getName().upcase + key.setName(kname) + end +end]]> + </script> + </scr> + <scr id="3E7C4F9E-9FCB-56C7-086F-F976F9A66384" name="Tables to upper case - JRuby - library usage" object="relational" engine="JSR 223 JRuby Engine" type="" var="model" library="Jruby lib" method="tables_up" purpose="transformation" > + <script> + <![CDATA[def tables_up(model) +tables = model.getTableSet().toArray() +for t in 0..tables.length-1 + table = tables[t] + name = table.getName().upcase + table.setName(name) + columns = table.getElements() + size = table.getElementsCollection().size()-1 + for i in 0..size + column = columns[i] + cname = column.getName().upcase + column.setName(cname) + end + keys = table.getKeys() + for i in 0..keys.length-1 + key = keys[i] + kname = key.getName().upcase + key.setName(kname) + end +end +return true +end]]> + </script> + </scr> + <scr id="E60A5A28-BB9B-3787-10E7-259DF900B9E6" name="Table abbreviation to column" object="relational" engine="Mozilla Rhino" type="" var="model" library="" method="" purpose="transformation" > + <script> + <![CDATA[tables = model.getTableSet().toArray(); +for (var t = 0; t<tables.length;t++){ + table = tables[t]; + abbr = table.getAbbreviation()+"_"; + if(!"_".equals(abbr)){ + columns = table.getElements(); + for (var i = 0; i < columns.length; i++) { + column = columns[i]; + cname = column.getName(); + if(!cname.startsWith(abbr)){ + column.setName(abbr+cname); + } + } + } +}]]> + </script> + </scr> + <scr id="9BE4E26C-36D8-A92C-ADEA-F183327DC239" name="Remove Table abbr from column" object="relational" engine="Mozilla Rhino" type="" var="model" library="" method="" purpose="transformation" > + <script> + <![CDATA[tables = model.getTableSet().toArray(); +for (var t = 0; t<tables.length;t++){ + table = tables[t]; + abbr = table.getAbbreviation()+"_"; + count = table.getAbbreviation().length()+1; + if(!"_".equals(abbr)){ + columns = table.getElements(); + for (var i = 0; i < columns.length; i++) { + column = columns[i]; + cname = column.getName(); + if(cname.startsWith(abbr)){ + column.setName(cname.substring(count)); + table.setDirty(true); + } + } + } +}]]> + </script> + </scr> + <scr id="23BE8827-D732-72B0-C6E6-266EFE116EDD" name="Table template" object="relational" engine="Mozilla Rhino" type="" var="model" library="" method="" purpose="transformation" > + <script> + <![CDATA[var t_name = "table_template"; +var p_name = "ctemplateID"; +template = model.getTableSet().getByName(t_name); +if(template!=null){ + tcolumns = template.getElements(); + tables = model.getTableSet().toArray(); + for (var t = 0; t<tables.length;t++){ + table = tables[t]; + // compare name ignoring the case + if(!table.getName().equalsIgnoreCase(t_name)){ + for (var i = 0; i < tcolumns.length; i++) { + column = tcolumns[i]; + col = table.getColumnByProperty(p_name,column.getObjectID()); + if(col==null){ + col = table.createColumn(); + } + column.copy(col); + //set property after copy otherwise it'll be cleared + col.setProperty(p_name,column.getObjectID()); + table.setDirty(true); + } + } + } +}]]> + </script> + </scr> + <scr id="5A8A151A-13FD-4B0A-E233-E3C5126BA02C" name="Tables to upper case - Rhino" object="relational" engine="Mozilla Rhino" type="" var="model" library="" method="" purpose="transformation" > + <script> + <![CDATA[tables = model.getTableSet().toArray(); +for (var t = 0; t<tables.length;t++){ + table = tables[t]; + name = table.getName().toUpperCase(); + table.setName(name); + columns = table.getElements(); + size = table.getElementsCollection().size(); + for (var i = 0; i < size; i++) { + column = columns[i]; + cname = column.getName().toUpperCase(); + column.setName(cname); + } + table.setDirty(true); + keys = table.getKeys(); + for (var i = 0; i < keys.length; i++) { + key = keys[i]; + if(!key.isFK()){ + kname = key.getName().toUpperCase(); + key.setName(kname); + }else{ + kname = key.getFKAssociation().getName().toUpperCase(); + key.getFKAssociation().setName(kname); + key.getFKAssociation().setDirty(true); + } + } +}]]> + </script> + </scr> + <scr id="0528C35C-F29B-E7BB-57AC-37BA2780A98D" name="Table template - uses column name" object="relational" engine="Mozilla Rhino" type="" var="model" library="" method="" purpose="transformation" > + <script> + <![CDATA[// version without usage of dynamic properties, columns are found by column name +// this allow reuse of already existing columns +var t_name = "table_template"; +template = model.getTableSet().getByName(t_name); +if(template!=null){ + tcolumns = template.getElements(); + tables = model.getTableSet().toArray(); + for (var t = 0; t<tables.length;t++){ + table = tables[t]; + // compare name ignoring the case + if(!table.getName().equalsIgnoreCase(t_name)){ + for (var i = 0; i < tcolumns.length; i++) { + column = tcolumns[i]; + col = table.getElementByName(column.getName()); + if(col==null){ + col = table.createColumn(); + } + column.copy(col); + table.setDirty(true); + } + } + } +}]]> + </script> + </scr> + <scr id="6279C414-90DD-A52B-4CEB-8D49AB31DC10" name="Copy Comments to Comments in RDBMS" object="relational" engine="Mozilla Rhino" type="" var="model" library="" method="" purpose="transformation" > + <script> + <![CDATA[max_length = 4000; +function copyComments(object){ + if(object.getCommentInRDBMS().equals("")){ + if(!object.getComment().equals("")){ + if(object.getComment().length()>max_length){ + object.setCommentInRDBMS(object.getComment().substring(0, max_length)); + }else{ + object.setCommentInRDBMS(object.getComment()); + } + object.setDirty(true); + } + } +} + +tables = model.getTableSet().toArray(); +for (var t = 0; t<tables.length;t++){ + table = tables[t] + copyComments(table); + columns = table.getElements(); + size = table.getElementsCollection().size(); + for (var i = 0; i < columns.length; i++) { + column = columns[i]; + copyComments(column); + } + keys = table.getKeys(); + for (var i = 0; i < keys.length; i++) { + key = keys[i]; + if(!key.isFK()){ + copyComments(key); + }else{ + copyComments(key.getFKAssociation()); + } + } +}]]> + </script> + </scr> + <scr id="7C4EDFC0-26EA-859C-DBD9-AC9345DEAF98" name="Create index on FK" object="relational" engine="Mozilla Rhino" type="" var="model" library="" method="" purpose="transformation" > + <script> + <![CDATA[function getIndex(tab,cols){ + keys = tab.getKeys(); + for (var i = 0; i < keys.length; i++) { + index = keys[i]; + if(!(index.isPK() || index.isUnique()) && !index.isFK() && index.isIndexForColumns(cols)){ + return index + } + } + return null; +} + +tables = model.getTableSet().toArray(); +for (var t = 0; t<tables.length;t++){ + table = tables[t]; + indexes = table.getKeys(); + for (var i = 0; i < indexes.length; i++) { + index = indexes[i]; + if(index.isFK()){ + columns = index.getColumns(); + if(columns.length>0){ + newIndex = getIndex(table,columns); + if(newIndex==null){ + newIndex = table.createIndex() + table.setDirty(true); + for (var k = 0; k < columns.length; k++){ + newIndex.add(columns[k]); + } + } + } + } + } +}]]> + </script> + </scr> + + <lib id="B310E434-78AE-6AED-EA94-6808B0262483" name="my first library" engine="Mozilla Rhino" methods="checkcomments" > + <script> + <![CDATA[var ruleMessage; +var errType; +var table; +function checkcomments(object){ + result = true; + ruleMessage=""; + if(table.getCommentInRDBMS().equals("")){ + ruleMessage="no comments in RDBMS defined"; + errType="Problem:"; + result = false; + } + if(table.getComment().equals("")){ + if(ruleMessage.equals("")){ + ruleMessage="no comments defined"; + }else{ + ruleMessage= ruleMessage +" , no comments defined"; + } + errType="Error"; + return false; + } + return result; +}]]> + </script> + </lib> + <lib id="2518F33A-DE50-9E1D-7216-DD2A0FD6B84C" name="Jruby lib" engine="JRuby Engine" methods="tables_up" > + <script> + <![CDATA[def tables_up(model) +tables = model.getTableSet().toArray() +for t in 0..tables.length-1 + table = tables[t] + name = table.getName().upcase + table.setName(name) + columns = table.getElements() + size = table.getElementsCollection().size()-1 + for i in 0..size + column = columns[i] + cname = column.getName().upcase + column.setName(cname) + end + keys = table.getKeys() + for i in 0..keys.length-1 + key = keys[i] + kname = key.getName().upcase + key.setName(kname) + end +end +return true +end]]> + </script> + </lib> +</dr_custom_scripts>
\ No newline at end of file diff --git a/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/Logical.xml b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/Logical.xml new file mode 100644 index 00000000..0403a605 --- /dev/null +++ b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/Logical.xml @@ -0,0 +1,7 @@ +<?xml version = '1.0' encoding = 'UTF-8'?> +<LogicalDesign class="oracle.dbtools.crest.model.design.logical.LogicalDesign" name="Logical" id="E3665D68-35D3-8757-63ED-30AEFB972A2C" mainViewID="AFCEF013-4CF2-4A5A-79A3-31521C1CA20A"> +<createdBy>bird</createdBy> +<createdTime>2012-08-20 21:58:45 UTC</createdTime> +<ownerDesignName>TestManagerDatabase</ownerDesignName> +<shouldBeOpen>false</shouldBeOpen> +</LogicalDesign>
\ No newline at end of file diff --git a/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/entity/seg_0/16464F5A-64BE-D2ED-91E0-BCBD0AA34680.xml b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/entity/seg_0/16464F5A-64BE-D2ED-91E0-BCBD0AA34680.xml new file mode 100644 index 00000000..6904bb54 --- /dev/null +++ b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/entity/seg_0/16464F5A-64BE-D2ED-91E0-BCBD0AA34680.xml @@ -0,0 +1,52 @@ +<?xml version = '1.0' encoding = 'UTF-8'?> +<Entity class="oracle.dbtools.crest.model.design.logical.Entity" id="16464F5A-64BE-D2ED-91E0-BCBD0AA34680" directorySegmentName="seg_0" name="TestResults"> +<createdBy>bird</createdBy> +<createdTime>2012-08-21 09:11:26 UTC</createdTime> +<ownerDesignName>TestManagerDatabase</ownerDesignName> +<engineerTo> +<item key="B082B14A-BEA8-D8A7-D661-197F34766ED3" value="true"/> +</engineerTo> +<adequatelyNormalized>NO</adequatelyNormalized> +<expectedVolumes>0</expectedVolumes> +<fwdEngineeringStrategyName>Single Table</fwdEngineeringStrategyName> +<growthPercent>0</growthPercent> +<growthType>Year</growthType> +<maxVolumes>9999999</maxVolumes> +<minVolumes>0</minVolumes> +<normalForm>Third</normalForm> +<fonts> +<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr"> +<foType>Title</foType> +<fontStyle>1</fontStyle> +<colorRGB>-16776961</colorRGB> +</FontObject> +<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr"> +<foType>Attribute</foType> +<colorRGB>-16777216</colorRGB> +</FontObject> +<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr"> +<foType>Datatype</foType> +<colorRGB>-16744448</colorRGB> +</FontObject> +<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr"> +<foType>PK Element</foType> +<colorRGB>-16776961</colorRGB> +</FontObject> +<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr"> +<foType>FK Element</foType> +<colorRGB>-16777216</colorRGB> +</FontObject> +<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr"> +<foType>UK Element</foType> +<colorRGB>-16776961</colorRGB> +</FontObject> +<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr"> +<foType>Not Null</foType> +<colorRGB>-65536</colorRGB> +</FontObject> +<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr"> +<foType>Key</foType> +<colorRGB>-16777216</colorRGB> +</FontObject> +</fonts> +</Entity>
\ No newline at end of file diff --git a/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/entity/seg_0/1BEAB532-23CA-8628-0C97-7CAD39119A4E.xml b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/entity/seg_0/1BEAB532-23CA-8628-0C97-7CAD39119A4E.xml new file mode 100644 index 00000000..9f54c6cd --- /dev/null +++ b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/entity/seg_0/1BEAB532-23CA-8628-0C97-7CAD39119A4E.xml @@ -0,0 +1,52 @@ +<?xml version = '1.0' encoding = 'UTF-8'?> +<Entity class="oracle.dbtools.crest.model.design.logical.Entity" id="1BEAB532-23CA-8628-0C97-7CAD39119A4E" directorySegmentName="seg_0" name="TestCaseArgs"> +<createdBy>bird</createdBy> +<createdTime>2012-08-20 22:38:18 UTC</createdTime> +<ownerDesignName>TestManagerDatabase</ownerDesignName> +<engineerTo> +<item key="B082B14A-BEA8-D8A7-D661-197F34766ED3" value="true"/> +</engineerTo> +<adequatelyNormalized>NO</adequatelyNormalized> +<expectedVolumes>0</expectedVolumes> +<fwdEngineeringStrategyName>Single Table</fwdEngineeringStrategyName> +<growthPercent>0</growthPercent> +<growthType>Year</growthType> +<maxVolumes>9999999</maxVolumes> +<minVolumes>0</minVolumes> +<normalForm>Third</normalForm> +<fonts> +<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr"> +<foType>Title</foType> +<fontStyle>1</fontStyle> +<colorRGB>-16776961</colorRGB> +</FontObject> +<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr"> +<foType>Attribute</foType> +<colorRGB>-16777216</colorRGB> +</FontObject> +<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr"> +<foType>Datatype</foType> +<colorRGB>-16744448</colorRGB> +</FontObject> +<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr"> +<foType>PK Element</foType> +<colorRGB>-16776961</colorRGB> +</FontObject> +<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr"> +<foType>FK Element</foType> +<colorRGB>-16777216</colorRGB> +</FontObject> +<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr"> +<foType>UK Element</foType> +<colorRGB>-16776961</colorRGB> +</FontObject> +<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr"> +<foType>Not Null</foType> +<colorRGB>-65536</colorRGB> +</FontObject> +<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr"> +<foType>Key</foType> +<colorRGB>-16777216</colorRGB> +</FontObject> +</fonts> +</Entity>
\ No newline at end of file diff --git a/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/entity/seg_0/24150FB1-B00F-4F69-6F77-49ECB58F0F66.xml b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/entity/seg_0/24150FB1-B00F-4F69-6F77-49ECB58F0F66.xml new file mode 100644 index 00000000..3a02553a --- /dev/null +++ b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/entity/seg_0/24150FB1-B00F-4F69-6F77-49ECB58F0F66.xml @@ -0,0 +1,52 @@ +<?xml version = '1.0' encoding = 'UTF-8'?> +<Entity class="oracle.dbtools.crest.model.design.logical.Entity" id="24150FB1-B00F-4F69-6F77-49ECB58F0F66" directorySegmentName="seg_0" name="BuildSources"> +<createdBy>bird</createdBy> +<createdTime>2012-08-21 08:54:55 UTC</createdTime> +<ownerDesignName>TestManagerDatabase</ownerDesignName> +<engineerTo> +<item key="B082B14A-BEA8-D8A7-D661-197F34766ED3" value="true"/> +</engineerTo> +<adequatelyNormalized>NO</adequatelyNormalized> +<expectedVolumes>0</expectedVolumes> +<fwdEngineeringStrategyName>Single Table</fwdEngineeringStrategyName> +<growthPercent>0</growthPercent> +<growthType>Year</growthType> +<maxVolumes>9999999</maxVolumes> +<minVolumes>0</minVolumes> +<normalForm>Third</normalForm> +<fonts> +<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr"> +<foType>Title</foType> +<fontStyle>1</fontStyle> +<colorRGB>-16776961</colorRGB> +</FontObject> +<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr"> +<foType>Attribute</foType> +<colorRGB>-16777216</colorRGB> +</FontObject> +<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr"> +<foType>Datatype</foType> +<colorRGB>-16744448</colorRGB> +</FontObject> +<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr"> +<foType>PK Element</foType> +<colorRGB>-16776961</colorRGB> +</FontObject> +<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr"> +<foType>FK Element</foType> +<colorRGB>-16777216</colorRGB> +</FontObject> +<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr"> +<foType>UK Element</foType> +<colorRGB>-16776961</colorRGB> +</FontObject> +<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr"> +<foType>Not Null</foType> +<colorRGB>-65536</colorRGB> +</FontObject> +<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr"> +<foType>Key</foType> +<colorRGB>-16777216</colorRGB> +</FontObject> +</fonts> +</Entity>
\ No newline at end of file diff --git a/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/entity/seg_0/28DD93CF-D058-7343-CD47-E9B435E1AC16.xml b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/entity/seg_0/28DD93CF-D058-7343-CD47-E9B435E1AC16.xml new file mode 100644 index 00000000..3a9992c9 --- /dev/null +++ b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/entity/seg_0/28DD93CF-D058-7343-CD47-E9B435E1AC16.xml @@ -0,0 +1,52 @@ +<?xml version = '1.0' encoding = 'UTF-8'?> +<Entity class="oracle.dbtools.crest.model.design.logical.Entity" id="28DD93CF-D058-7343-CD47-E9B435E1AC16" directorySegmentName="seg_0" name="TestResultFiles"> +<createdBy>bird</createdBy> +<createdTime>2012-08-21 09:12:51 UTC</createdTime> +<ownerDesignName>TestManagerDatabase</ownerDesignName> +<engineerTo> +<item key="B082B14A-BEA8-D8A7-D661-197F34766ED3" value="true"/> +</engineerTo> +<adequatelyNormalized>NO</adequatelyNormalized> +<expectedVolumes>0</expectedVolumes> +<fwdEngineeringStrategyName>Single Table</fwdEngineeringStrategyName> +<growthPercent>0</growthPercent> +<growthType>Year</growthType> +<maxVolumes>9999999</maxVolumes> +<minVolumes>0</minVolumes> +<normalForm>Third</normalForm> +<fonts> +<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr"> +<foType>Title</foType> +<fontStyle>1</fontStyle> +<colorRGB>-16776961</colorRGB> +</FontObject> +<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr"> +<foType>Attribute</foType> +<colorRGB>-16777216</colorRGB> +</FontObject> +<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr"> +<foType>Datatype</foType> +<colorRGB>-16744448</colorRGB> +</FontObject> +<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr"> +<foType>PK Element</foType> +<colorRGB>-16776961</colorRGB> +</FontObject> +<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr"> +<foType>FK Element</foType> +<colorRGB>-16777216</colorRGB> +</FontObject> +<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr"> +<foType>UK Element</foType> +<colorRGB>-16776961</colorRGB> +</FontObject> +<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr"> +<foType>Not Null</foType> +<colorRGB>-65536</colorRGB> +</FontObject> +<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr"> +<foType>Key</foType> +<colorRGB>-16777216</colorRGB> +</FontObject> +</fonts> +</Entity>
\ No newline at end of file diff --git a/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/entity/seg_0/2F6ACC6D-3D17-537D-8ADF-F8424395B345.xml b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/entity/seg_0/2F6ACC6D-3D17-537D-8ADF-F8424395B345.xml new file mode 100644 index 00000000..4ea40fc7 --- /dev/null +++ b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/entity/seg_0/2F6ACC6D-3D17-537D-8ADF-F8424395B345.xml @@ -0,0 +1,52 @@ +<?xml version = '1.0' encoding = 'UTF-8'?> +<Entity class="oracle.dbtools.crest.model.design.logical.Entity" id="2F6ACC6D-3D17-537D-8ADF-F8424395B345" directorySegmentName="seg_0" name="GlobalRsrcStatuses"> +<createdBy>bird</createdBy> +<createdTime>2012-08-21 09:17:42 UTC</createdTime> +<ownerDesignName>TestManagerDatabase</ownerDesignName> +<engineerTo> +<item key="B082B14A-BEA8-D8A7-D661-197F34766ED3" value="true"/> +</engineerTo> +<adequatelyNormalized>NO</adequatelyNormalized> +<expectedVolumes>0</expectedVolumes> +<fwdEngineeringStrategyName>Single Table</fwdEngineeringStrategyName> +<growthPercent>0</growthPercent> +<growthType>Year</growthType> +<maxVolumes>9999999</maxVolumes> +<minVolumes>0</minVolumes> +<normalForm>Third</normalForm> +<fonts> +<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr"> +<foType>Title</foType> +<fontStyle>1</fontStyle> +<colorRGB>-16776961</colorRGB> +</FontObject> +<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr"> +<foType>Attribute</foType> +<colorRGB>-16777216</colorRGB> +</FontObject> +<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr"> +<foType>Datatype</foType> +<colorRGB>-16744448</colorRGB> +</FontObject> +<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr"> +<foType>PK Element</foType> +<colorRGB>-16776961</colorRGB> +</FontObject> +<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr"> +<foType>FK Element</foType> +<colorRGB>-16777216</colorRGB> +</FontObject> +<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr"> +<foType>UK Element</foType> +<colorRGB>-16776961</colorRGB> +</FontObject> +<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr"> +<foType>Not Null</foType> +<colorRGB>-65536</colorRGB> +</FontObject> +<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr"> +<foType>Key</foType> +<colorRGB>-16777216</colorRGB> +</FontObject> +</fonts> +</Entity>
\ No newline at end of file diff --git a/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/entity/seg_0/44FFF5E9-0C2F-7BAC-B5B7-73CA3A230B39.xml b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/entity/seg_0/44FFF5E9-0C2F-7BAC-B5B7-73CA3A230B39.xml new file mode 100644 index 00000000..e3300354 --- /dev/null +++ b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/entity/seg_0/44FFF5E9-0C2F-7BAC-B5B7-73CA3A230B39.xml @@ -0,0 +1,52 @@ +<?xml version = '1.0' encoding = 'UTF-8'?> +<Entity class="oracle.dbtools.crest.model.design.logical.Entity" id="44FFF5E9-0C2F-7BAC-B5B7-73CA3A230B39" directorySegmentName="seg_0" name="FailureReasons"> +<createdBy>bird</createdBy> +<createdTime>2012-08-22 11:47:11 UTC</createdTime> +<ownerDesignName>TestManagerDatabase</ownerDesignName> +<engineerTo> +<item key="B082B14A-BEA8-D8A7-D661-197F34766ED3" value="true"/> +</engineerTo> +<adequatelyNormalized>NO</adequatelyNormalized> +<expectedVolumes>0</expectedVolumes> +<fwdEngineeringStrategyName>Single Table</fwdEngineeringStrategyName> +<growthPercent>0</growthPercent> +<growthType>Year</growthType> +<maxVolumes>9999999</maxVolumes> +<minVolumes>0</minVolumes> +<normalForm>Third</normalForm> +<fonts> +<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr"> +<foType>Title</foType> +<fontStyle>1</fontStyle> +<colorRGB>-16776961</colorRGB> +</FontObject> +<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr"> +<foType>Attribute</foType> +<colorRGB>-16777216</colorRGB> +</FontObject> +<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr"> +<foType>Datatype</foType> +<colorRGB>-16744448</colorRGB> +</FontObject> +<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr"> +<foType>PK Element</foType> +<colorRGB>-16776961</colorRGB> +</FontObject> +<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr"> +<foType>FK Element</foType> +<colorRGB>-16777216</colorRGB> +</FontObject> +<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr"> +<foType>UK Element</foType> +<colorRGB>-16776961</colorRGB> +</FontObject> +<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr"> +<foType>Not Null</foType> +<colorRGB>-65536</colorRGB> +</FontObject> +<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr"> +<foType>Key</foType> +<colorRGB>-16777216</colorRGB> +</FontObject> +</fonts> +</Entity>
\ No newline at end of file diff --git a/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/entity/seg_0/4579B792-2F35-D72A-1A3B-C7E53C41A766.xml b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/entity/seg_0/4579B792-2F35-D72A-1A3B-C7E53C41A766.xml new file mode 100644 index 00000000..e35d8bc0 --- /dev/null +++ b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/entity/seg_0/4579B792-2F35-D72A-1A3B-C7E53C41A766.xml @@ -0,0 +1,52 @@ +<?xml version = '1.0' encoding = 'UTF-8'?> +<Entity class="oracle.dbtools.crest.model.design.logical.Entity" id="4579B792-2F35-D72A-1A3B-C7E53C41A766" directorySegmentName="seg_0" name="TestResultMsgs"> +<createdBy>bird</createdBy> +<createdTime>2012-08-21 09:13:03 UTC</createdTime> +<ownerDesignName>TestManagerDatabase</ownerDesignName> +<engineerTo> +<item key="B082B14A-BEA8-D8A7-D661-197F34766ED3" value="true"/> +</engineerTo> +<adequatelyNormalized>NO</adequatelyNormalized> +<expectedVolumes>0</expectedVolumes> +<fwdEngineeringStrategyName>Single Table</fwdEngineeringStrategyName> +<growthPercent>0</growthPercent> +<growthType>Year</growthType> +<maxVolumes>9999999</maxVolumes> +<minVolumes>0</minVolumes> +<normalForm>Third</normalForm> +<fonts> +<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr"> +<foType>Title</foType> +<fontStyle>1</fontStyle> +<colorRGB>-16776961</colorRGB> +</FontObject> +<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr"> +<foType>Attribute</foType> +<colorRGB>-16777216</colorRGB> +</FontObject> +<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr"> +<foType>Datatype</foType> +<colorRGB>-16744448</colorRGB> +</FontObject> +<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr"> +<foType>PK Element</foType> +<colorRGB>-16776961</colorRGB> +</FontObject> +<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr"> +<foType>FK Element</foType> +<colorRGB>-16777216</colorRGB> +</FontObject> +<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr"> +<foType>UK Element</foType> +<colorRGB>-16776961</colorRGB> +</FontObject> +<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr"> +<foType>Not Null</foType> +<colorRGB>-65536</colorRGB> +</FontObject> +<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr"> +<foType>Key</foType> +<colorRGB>-16777216</colorRGB> +</FontObject> +</fonts> +</Entity>
\ No newline at end of file diff --git a/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/entity/seg_0/4D937E7C-3A28-E52D-89C0-EC8804C62367.xml b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/entity/seg_0/4D937E7C-3A28-E52D-89C0-EC8804C62367.xml new file mode 100644 index 00000000..7b2d1d01 --- /dev/null +++ b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/entity/seg_0/4D937E7C-3A28-E52D-89C0-EC8804C62367.xml @@ -0,0 +1,52 @@ +<?xml version = '1.0' encoding = 'UTF-8'?> +<Entity class="oracle.dbtools.crest.model.design.logical.Entity" id="4D937E7C-3A28-E52D-89C0-EC8804C62367" directorySegmentName="seg_0" name="FailureCategories"> +<createdBy>bird</createdBy> +<createdTime>2012-08-22 11:47:19 UTC</createdTime> +<ownerDesignName>TestManagerDatabase</ownerDesignName> +<engineerTo> +<item key="B082B14A-BEA8-D8A7-D661-197F34766ED3" value="true"/> +</engineerTo> +<adequatelyNormalized>NO</adequatelyNormalized> +<expectedVolumes>0</expectedVolumes> +<fwdEngineeringStrategyName>Single Table</fwdEngineeringStrategyName> +<growthPercent>0</growthPercent> +<growthType>Year</growthType> +<maxVolumes>9999999</maxVolumes> +<minVolumes>0</minVolumes> +<normalForm>Third</normalForm> +<fonts> +<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr"> +<foType>Title</foType> +<fontStyle>1</fontStyle> +<colorRGB>-16776961</colorRGB> +</FontObject> +<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr"> +<foType>Attribute</foType> +<colorRGB>-16777216</colorRGB> +</FontObject> +<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr"> +<foType>Datatype</foType> +<colorRGB>-16744448</colorRGB> +</FontObject> +<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr"> +<foType>PK Element</foType> +<colorRGB>-16776961</colorRGB> +</FontObject> +<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr"> +<foType>FK Element</foType> +<colorRGB>-16777216</colorRGB> +</FontObject> +<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr"> +<foType>UK Element</foType> +<colorRGB>-16776961</colorRGB> +</FontObject> +<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr"> +<foType>Not Null</foType> +<colorRGB>-65536</colorRGB> +</FontObject> +<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr"> +<foType>Key</foType> +<colorRGB>-16777216</colorRGB> +</FontObject> +</fonts> +</Entity>
\ No newline at end of file diff --git a/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/entity/seg_0/504221DA-1B57-4EAD-39DB-40FD553E9FA2.xml b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/entity/seg_0/504221DA-1B57-4EAD-39DB-40FD553E9FA2.xml new file mode 100644 index 00000000..e536867c --- /dev/null +++ b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/entity/seg_0/504221DA-1B57-4EAD-39DB-40FD553E9FA2.xml @@ -0,0 +1,52 @@ +<?xml version = '1.0' encoding = 'UTF-8'?> +<Entity class="oracle.dbtools.crest.model.design.logical.Entity" id="504221DA-1B57-4EAD-39DB-40FD553E9FA2" directorySegmentName="seg_0" name="Builds"> +<createdBy>bird</createdBy> +<createdTime>2012-08-21 08:52:15 UTC</createdTime> +<ownerDesignName>TestManagerDatabase</ownerDesignName> +<engineerTo> +<item key="B082B14A-BEA8-D8A7-D661-197F34766ED3" value="true"/> +</engineerTo> +<adequatelyNormalized>NO</adequatelyNormalized> +<expectedVolumes>0</expectedVolumes> +<fwdEngineeringStrategyName>Single Table</fwdEngineeringStrategyName> +<growthPercent>0</growthPercent> +<growthType>Year</growthType> +<maxVolumes>9999999</maxVolumes> +<minVolumes>0</minVolumes> +<normalForm>Third</normalForm> +<fonts> +<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr"> +<foType>Title</foType> +<fontStyle>1</fontStyle> +<colorRGB>-16776961</colorRGB> +</FontObject> +<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr"> +<foType>Attribute</foType> +<colorRGB>-16777216</colorRGB> +</FontObject> +<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr"> +<foType>Datatype</foType> +<colorRGB>-16744448</colorRGB> +</FontObject> +<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr"> +<foType>PK Element</foType> +<colorRGB>-16776961</colorRGB> +</FontObject> +<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr"> +<foType>FK Element</foType> +<colorRGB>-16777216</colorRGB> +</FontObject> +<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr"> +<foType>UK Element</foType> +<colorRGB>-16776961</colorRGB> +</FontObject> +<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr"> +<foType>Not Null</foType> +<colorRGB>-65536</colorRGB> +</FontObject> +<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr"> +<foType>Key</foType> +<colorRGB>-16777216</colorRGB> +</FontObject> +</fonts> +</Entity>
\ No newline at end of file diff --git a/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/entity/seg_0/6A886CEE-579B-48FF-63F6-0FB03393FBF6.xml b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/entity/seg_0/6A886CEE-579B-48FF-63F6-0FB03393FBF6.xml new file mode 100644 index 00000000..20424c7c --- /dev/null +++ b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/entity/seg_0/6A886CEE-579B-48FF-63F6-0FB03393FBF6.xml @@ -0,0 +1,52 @@ +<?xml version = '1.0' encoding = 'UTF-8'?> +<Entity class="oracle.dbtools.crest.model.design.logical.Entity" id="6A886CEE-579B-48FF-63F6-0FB03393FBF6" directorySegmentName="seg_0" name="SchedGroups"> +<createdBy>bird</createdBy> +<createdTime>2012-08-20 22:16:15 UTC</createdTime> +<ownerDesignName>TestManagerDatabase</ownerDesignName> +<engineerTo> +<item key="B082B14A-BEA8-D8A7-D661-197F34766ED3" value="true"/> +</engineerTo> +<adequatelyNormalized>NO</adequatelyNormalized> +<expectedVolumes>0</expectedVolumes> +<fwdEngineeringStrategyName>Single Table</fwdEngineeringStrategyName> +<growthPercent>0</growthPercent> +<growthType>Year</growthType> +<maxVolumes>9999999</maxVolumes> +<minVolumes>0</minVolumes> +<normalForm>Third</normalForm> +<fonts> +<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr"> +<foType>Title</foType> +<fontStyle>1</fontStyle> +<colorRGB>-16776961</colorRGB> +</FontObject> +<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr"> +<foType>Attribute</foType> +<colorRGB>-16777216</colorRGB> +</FontObject> +<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr"> +<foType>Datatype</foType> +<colorRGB>-16744448</colorRGB> +</FontObject> +<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr"> +<foType>PK Element</foType> +<colorRGB>-16776961</colorRGB> +</FontObject> +<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr"> +<foType>FK Element</foType> +<colorRGB>-16777216</colorRGB> +</FontObject> +<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr"> +<foType>UK Element</foType> +<colorRGB>-16776961</colorRGB> +</FontObject> +<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr"> +<foType>Not Null</foType> +<colorRGB>-65536</colorRGB> +</FontObject> +<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr"> +<foType>Key</foType> +<colorRGB>-16777216</colorRGB> +</FontObject> +</fonts> +</Entity>
\ No newline at end of file diff --git a/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/entity/seg_0/7AE36CC1-A030-63E5-6EF3-72FCD04815EE.xml b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/entity/seg_0/7AE36CC1-A030-63E5-6EF3-72FCD04815EE.xml new file mode 100644 index 00000000..9475385d --- /dev/null +++ b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/entity/seg_0/7AE36CC1-A030-63E5-6EF3-72FCD04815EE.xml @@ -0,0 +1,52 @@ +<?xml version = '1.0' encoding = 'UTF-8'?> +<Entity class="oracle.dbtools.crest.model.design.logical.Entity" id="7AE36CC1-A030-63E5-6EF3-72FCD04815EE" directorySegmentName="seg_0" name="TestBoxes"> +<createdBy>bird</createdBy> +<createdTime>2012-08-20 22:34:30 UTC</createdTime> +<ownerDesignName>TestManagerDatabase</ownerDesignName> +<engineerTo> +<item key="B082B14A-BEA8-D8A7-D661-197F34766ED3" value="true"/> +</engineerTo> +<adequatelyNormalized>NO</adequatelyNormalized> +<expectedVolumes>0</expectedVolumes> +<fwdEngineeringStrategyName>Single Table</fwdEngineeringStrategyName> +<growthPercent>0</growthPercent> +<growthType>Year</growthType> +<maxVolumes>9999999</maxVolumes> +<minVolumes>0</minVolumes> +<normalForm>Third</normalForm> +<fonts> +<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr"> +<foType>Title</foType> +<fontStyle>1</fontStyle> +<colorRGB>-16776961</colorRGB> +</FontObject> +<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr"> +<foType>Attribute</foType> +<colorRGB>-16777216</colorRGB> +</FontObject> +<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr"> +<foType>Datatype</foType> +<colorRGB>-16744448</colorRGB> +</FontObject> +<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr"> +<foType>PK Element</foType> +<colorRGB>-16776961</colorRGB> +</FontObject> +<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr"> +<foType>FK Element</foType> +<colorRGB>-16777216</colorRGB> +</FontObject> +<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr"> +<foType>UK Element</foType> +<colorRGB>-16776961</colorRGB> +</FontObject> +<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr"> +<foType>Not Null</foType> +<colorRGB>-65536</colorRGB> +</FontObject> +<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr"> +<foType>Key</foType> +<colorRGB>-16777216</colorRGB> +</FontObject> +</fonts> +</Entity>
\ No newline at end of file diff --git a/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/entity/seg_0/90367AFB-BA2D-A918-46B9-1E5DE53ACC48.xml b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/entity/seg_0/90367AFB-BA2D-A918-46B9-1E5DE53ACC48.xml new file mode 100644 index 00000000..96b815e1 --- /dev/null +++ b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/entity/seg_0/90367AFB-BA2D-A918-46B9-1E5DE53ACC48.xml @@ -0,0 +1,52 @@ +<?xml version = '1.0' encoding = 'UTF-8'?> +<Entity class="oracle.dbtools.crest.model.design.logical.Entity" id="90367AFB-BA2D-A918-46B9-1E5DE53ACC48" directorySegmentName="seg_0" name="BuildBlacklist"> +<createdBy>bird</createdBy> +<createdTime>2012-08-21 08:59:31 UTC</createdTime> +<ownerDesignName>TestManagerDatabase</ownerDesignName> +<engineerTo> +<item key="B082B14A-BEA8-D8A7-D661-197F34766ED3" value="true"/> +</engineerTo> +<adequatelyNormalized>NO</adequatelyNormalized> +<expectedVolumes>0</expectedVolumes> +<fwdEngineeringStrategyName>Single Table</fwdEngineeringStrategyName> +<growthPercent>0</growthPercent> +<growthType>Year</growthType> +<maxVolumes>9999999</maxVolumes> +<minVolumes>0</minVolumes> +<normalForm>Third</normalForm> +<fonts> +<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr"> +<foType>Title</foType> +<fontStyle>1</fontStyle> +<colorRGB>-16776961</colorRGB> +</FontObject> +<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr"> +<foType>Attribute</foType> +<colorRGB>-16777216</colorRGB> +</FontObject> +<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr"> +<foType>Datatype</foType> +<colorRGB>-16744448</colorRGB> +</FontObject> +<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr"> +<foType>PK Element</foType> +<colorRGB>-16776961</colorRGB> +</FontObject> +<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr"> +<foType>FK Element</foType> +<colorRGB>-16777216</colorRGB> +</FontObject> +<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr"> +<foType>UK Element</foType> +<colorRGB>-16776961</colorRGB> +</FontObject> +<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr"> +<foType>Not Null</foType> +<colorRGB>-65536</colorRGB> +</FontObject> +<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr"> +<foType>Key</foType> +<colorRGB>-16777216</colorRGB> +</FontObject> +</fonts> +</Entity>
\ No newline at end of file diff --git a/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/entity/seg_0/90F477EE-35D6-21A7-B693-E5724FB07476.xml b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/entity/seg_0/90F477EE-35D6-21A7-B693-E5724FB07476.xml new file mode 100644 index 00000000..6bcf734c --- /dev/null +++ b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/entity/seg_0/90F477EE-35D6-21A7-B693-E5724FB07476.xml @@ -0,0 +1,52 @@ +<?xml version = '1.0' encoding = 'UTF-8'?> +<Entity class="oracle.dbtools.crest.model.design.logical.Entity" id="90F477EE-35D6-21A7-B693-E5724FB07476" directorySegmentName="seg_0" name="TestSets"> +<createdBy>bird</createdBy> +<createdTime>2012-08-21 09:11:20 UTC</createdTime> +<ownerDesignName>TestManagerDatabase</ownerDesignName> +<engineerTo> +<item key="B082B14A-BEA8-D8A7-D661-197F34766ED3" value="true"/> +</engineerTo> +<adequatelyNormalized>NO</adequatelyNormalized> +<expectedVolumes>0</expectedVolumes> +<fwdEngineeringStrategyName>Single Table</fwdEngineeringStrategyName> +<growthPercent>0</growthPercent> +<growthType>Year</growthType> +<maxVolumes>9999999</maxVolumes> +<minVolumes>0</minVolumes> +<normalForm>Third</normalForm> +<fonts> +<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr"> +<foType>Title</foType> +<fontStyle>1</fontStyle> +<colorRGB>-16776961</colorRGB> +</FontObject> +<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr"> +<foType>Attribute</foType> +<colorRGB>-16777216</colorRGB> +</FontObject> +<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr"> +<foType>Datatype</foType> +<colorRGB>-16744448</colorRGB> +</FontObject> +<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr"> +<foType>PK Element</foType> +<colorRGB>-16776961</colorRGB> +</FontObject> +<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr"> +<foType>FK Element</foType> +<colorRGB>-16777216</colorRGB> +</FontObject> +<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr"> +<foType>UK Element</foType> +<colorRGB>-16776961</colorRGB> +</FontObject> +<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr"> +<foType>Not Null</foType> +<colorRGB>-65536</colorRGB> +</FontObject> +<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr"> +<foType>Key</foType> +<colorRGB>-16777216</colorRGB> +</FontObject> +</fonts> +</Entity>
\ No newline at end of file diff --git a/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/entity/seg_0/9F78B73C-056D-DDEF-8C50-A9DA76B9E724.xml b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/entity/seg_0/9F78B73C-056D-DDEF-8C50-A9DA76B9E724.xml new file mode 100644 index 00000000..d672b27e --- /dev/null +++ b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/entity/seg_0/9F78B73C-056D-DDEF-8C50-A9DA76B9E724.xml @@ -0,0 +1,52 @@ +<?xml version = '1.0' encoding = 'UTF-8'?> +<Entity class="oracle.dbtools.crest.model.design.logical.Entity" id="9F78B73C-056D-DDEF-8C50-A9DA76B9E724" directorySegmentName="seg_0" name="BuildTypes"> +<createdBy>bird</createdBy> +<createdTime>2012-08-21 08:52:32 UTC</createdTime> +<ownerDesignName>TestManagerDatabase</ownerDesignName> +<engineerTo> +<item key="B082B14A-BEA8-D8A7-D661-197F34766ED3" value="true"/> +</engineerTo> +<adequatelyNormalized>NO</adequatelyNormalized> +<expectedVolumes>0</expectedVolumes> +<fwdEngineeringStrategyName>Single Table</fwdEngineeringStrategyName> +<growthPercent>0</growthPercent> +<growthType>Year</growthType> +<maxVolumes>9999999</maxVolumes> +<minVolumes>0</minVolumes> +<normalForm>Third</normalForm> +<fonts> +<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr"> +<foType>Title</foType> +<fontStyle>1</fontStyle> +<colorRGB>-16776961</colorRGB> +</FontObject> +<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr"> +<foType>Attribute</foType> +<colorRGB>-16777216</colorRGB> +</FontObject> +<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr"> +<foType>Datatype</foType> +<colorRGB>-16744448</colorRGB> +</FontObject> +<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr"> +<foType>PK Element</foType> +<colorRGB>-16776961</colorRGB> +</FontObject> +<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr"> +<foType>FK Element</foType> +<colorRGB>-16777216</colorRGB> +</FontObject> +<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr"> +<foType>UK Element</foType> +<colorRGB>-16776961</colorRGB> +</FontObject> +<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr"> +<foType>Not Null</foType> +<colorRGB>-65536</colorRGB> +</FontObject> +<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr"> +<foType>Key</foType> +<colorRGB>-16777216</colorRGB> +</FontObject> +</fonts> +</Entity>
\ No newline at end of file diff --git a/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/entity/seg_0/A352A20F-310D-E285-FBC9-90DD0DA7BB9B.xml b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/entity/seg_0/A352A20F-310D-E285-FBC9-90DD0DA7BB9B.xml new file mode 100644 index 00000000..301a3f28 --- /dev/null +++ b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/entity/seg_0/A352A20F-310D-E285-FBC9-90DD0DA7BB9B.xml @@ -0,0 +1,52 @@ +<?xml version = '1.0' encoding = 'UTF-8'?> +<Entity class="oracle.dbtools.crest.model.design.logical.Entity" id="A352A20F-310D-E285-FBC9-90DD0DA7BB9B" directorySegmentName="seg_0" name="TestBoxStatuses"> +<createdBy>bird</createdBy> +<createdTime>2012-08-21 09:09:55 UTC</createdTime> +<ownerDesignName>TestManagerDatabase</ownerDesignName> +<engineerTo> +<item key="B082B14A-BEA8-D8A7-D661-197F34766ED3" value="true"/> +</engineerTo> +<adequatelyNormalized>NO</adequatelyNormalized> +<expectedVolumes>0</expectedVolumes> +<fwdEngineeringStrategyName>Single Table</fwdEngineeringStrategyName> +<growthPercent>0</growthPercent> +<growthType>Year</growthType> +<maxVolumes>9999999</maxVolumes> +<minVolumes>0</minVolumes> +<normalForm>Third</normalForm> +<fonts> +<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr"> +<foType>Title</foType> +<fontStyle>1</fontStyle> +<colorRGB>-16776961</colorRGB> +</FontObject> +<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr"> +<foType>Attribute</foType> +<colorRGB>-16777216</colorRGB> +</FontObject> +<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr"> +<foType>Datatype</foType> +<colorRGB>-16744448</colorRGB> +</FontObject> +<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr"> +<foType>PK Element</foType> +<colorRGB>-16776961</colorRGB> +</FontObject> +<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr"> +<foType>FK Element</foType> +<colorRGB>-16777216</colorRGB> +</FontObject> +<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr"> +<foType>UK Element</foType> +<colorRGB>-16776961</colorRGB> +</FontObject> +<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr"> +<foType>Not Null</foType> +<colorRGB>-65536</colorRGB> +</FontObject> +<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr"> +<foType>Key</foType> +<colorRGB>-16777216</colorRGB> +</FontObject> +</fonts> +</Entity>
\ No newline at end of file diff --git a/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/entity/seg_0/A6A5F317-479C-A0DD-CAAE-9DCB56B29D40.xml b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/entity/seg_0/A6A5F317-479C-A0DD-CAAE-9DCB56B29D40.xml new file mode 100644 index 00000000..a6f31387 --- /dev/null +++ b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/entity/seg_0/A6A5F317-479C-A0DD-CAAE-9DCB56B29D40.xml @@ -0,0 +1,52 @@ +<?xml version = '1.0' encoding = 'UTF-8'?> +<Entity class="oracle.dbtools.crest.model.design.logical.Entity" id="A6A5F317-479C-A0DD-CAAE-9DCB56B29D40" directorySegmentName="seg_0" name="RequirementSets"> +<createdBy>bird</createdBy> +<createdTime>2012-08-20 22:14:04 UTC</createdTime> +<ownerDesignName>TestManagerDatabase</ownerDesignName> +<engineerTo> +<item key="B082B14A-BEA8-D8A7-D661-197F34766ED3" value="true"/> +</engineerTo> +<adequatelyNormalized>NO</adequatelyNormalized> +<expectedVolumes>0</expectedVolumes> +<fwdEngineeringStrategyName>Single Table</fwdEngineeringStrategyName> +<growthPercent>0</growthPercent> +<growthType>Year</growthType> +<maxVolumes>9999999</maxVolumes> +<minVolumes>0</minVolumes> +<normalForm>Third</normalForm> +<fonts> +<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr"> +<foType>Title</foType> +<fontStyle>1</fontStyle> +<colorRGB>-16776961</colorRGB> +</FontObject> +<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr"> +<foType>Attribute</foType> +<colorRGB>-16777216</colorRGB> +</FontObject> +<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr"> +<foType>Datatype</foType> +<colorRGB>-16744448</colorRGB> +</FontObject> +<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr"> +<foType>PK Element</foType> +<colorRGB>-16776961</colorRGB> +</FontObject> +<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr"> +<foType>FK Element</foType> +<colorRGB>-16777216</colorRGB> +</FontObject> +<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr"> +<foType>UK Element</foType> +<colorRGB>-16776961</colorRGB> +</FontObject> +<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr"> +<foType>Not Null</foType> +<colorRGB>-65536</colorRGB> +</FontObject> +<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr"> +<foType>Key</foType> +<colorRGB>-16777216</colorRGB> +</FontObject> +</fonts> +</Entity>
\ No newline at end of file diff --git a/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/entity/seg_0/B36A186B-CDB3-7851-8C38-12EA8D50EAEB.xml b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/entity/seg_0/B36A186B-CDB3-7851-8C38-12EA8D50EAEB.xml new file mode 100644 index 00000000..7e22bcc2 --- /dev/null +++ b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/entity/seg_0/B36A186B-CDB3-7851-8C38-12EA8D50EAEB.xml @@ -0,0 +1,52 @@ +<?xml version = '1.0' encoding = 'UTF-8'?> +<Entity class="oracle.dbtools.crest.model.design.logical.Entity" id="B36A186B-CDB3-7851-8C38-12EA8D50EAEB" directorySegmentName="seg_0" name="RequirementsNum"> +<createdBy>bird</createdBy> +<createdTime>2012-08-20 22:14:37 UTC</createdTime> +<ownerDesignName>TestManagerDatabase</ownerDesignName> +<engineerTo> +<item key="B082B14A-BEA8-D8A7-D661-197F34766ED3" value="true"/> +</engineerTo> +<adequatelyNormalized>NO</adequatelyNormalized> +<expectedVolumes>0</expectedVolumes> +<fwdEngineeringStrategyName>Single Table</fwdEngineeringStrategyName> +<growthPercent>0</growthPercent> +<growthType>Year</growthType> +<maxVolumes>9999999</maxVolumes> +<minVolumes>0</minVolumes> +<normalForm>Third</normalForm> +<fonts> +<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr"> +<foType>Title</foType> +<fontStyle>1</fontStyle> +<colorRGB>-16776961</colorRGB> +</FontObject> +<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr"> +<foType>Attribute</foType> +<colorRGB>-16777216</colorRGB> +</FontObject> +<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr"> +<foType>Datatype</foType> +<colorRGB>-16744448</colorRGB> +</FontObject> +<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr"> +<foType>PK Element</foType> +<colorRGB>-16776961</colorRGB> +</FontObject> +<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr"> +<foType>FK Element</foType> +<colorRGB>-16777216</colorRGB> +</FontObject> +<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr"> +<foType>UK Element</foType> +<colorRGB>-16776961</colorRGB> +</FontObject> +<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr"> +<foType>Not Null</foType> +<colorRGB>-65536</colorRGB> +</FontObject> +<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr"> +<foType>Key</foType> +<colorRGB>-16777216</colorRGB> +</FontObject> +</fonts> +</Entity>
\ No newline at end of file diff --git a/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/entity/seg_0/B82DAF9A-6F99-5CF6-4D99-A391BAD66192.xml b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/entity/seg_0/B82DAF9A-6F99-5CF6-4D99-A391BAD66192.xml new file mode 100644 index 00000000..aa84dcf3 --- /dev/null +++ b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/entity/seg_0/B82DAF9A-6F99-5CF6-4D99-A391BAD66192.xml @@ -0,0 +1,52 @@ +<?xml version = '1.0' encoding = 'UTF-8'?> +<Entity class="oracle.dbtools.crest.model.design.logical.Entity" id="B82DAF9A-6F99-5CF6-4D99-A391BAD66192" directorySegmentName="seg_0" name="TestCases"> +<createdBy>bird</createdBy> +<createdTime>2012-08-20 22:34:30 UTC</createdTime> +<ownerDesignName>TestManagerDatabase</ownerDesignName> +<engineerTo> +<item key="B082B14A-BEA8-D8A7-D661-197F34766ED3" value="true"/> +</engineerTo> +<adequatelyNormalized>NO</adequatelyNormalized> +<expectedVolumes>0</expectedVolumes> +<fwdEngineeringStrategyName>Single Table</fwdEngineeringStrategyName> +<growthPercent>0</growthPercent> +<growthType>Year</growthType> +<maxVolumes>9999999</maxVolumes> +<minVolumes>0</minVolumes> +<normalForm>Third</normalForm> +<fonts> +<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr"> +<foType>Title</foType> +<fontStyle>1</fontStyle> +<colorRGB>-16776961</colorRGB> +</FontObject> +<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr"> +<foType>Attribute</foType> +<colorRGB>-16777216</colorRGB> +</FontObject> +<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr"> +<foType>Datatype</foType> +<colorRGB>-16744448</colorRGB> +</FontObject> +<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr"> +<foType>PK Element</foType> +<colorRGB>-16776961</colorRGB> +</FontObject> +<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr"> +<foType>FK Element</foType> +<colorRGB>-16777216</colorRGB> +</FontObject> +<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr"> +<foType>UK Element</foType> +<colorRGB>-16776961</colorRGB> +</FontObject> +<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr"> +<foType>Not Null</foType> +<colorRGB>-65536</colorRGB> +</FontObject> +<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr"> +<foType>Key</foType> +<colorRGB>-16777216</colorRGB> +</FontObject> +</fonts> +</Entity>
\ No newline at end of file diff --git a/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/entity/seg_0/C332E3D7-638B-6CA8-24BF-383CA8659A3A.xml b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/entity/seg_0/C332E3D7-638B-6CA8-24BF-383CA8659A3A.xml new file mode 100644 index 00000000..f093d805 --- /dev/null +++ b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/entity/seg_0/C332E3D7-638B-6CA8-24BF-383CA8659A3A.xml @@ -0,0 +1,52 @@ +<?xml version = '1.0' encoding = 'UTF-8'?> +<Entity class="oracle.dbtools.crest.model.design.logical.Entity" id="C332E3D7-638B-6CA8-24BF-383CA8659A3A" directorySegmentName="seg_0" name="SchedQueues"> +<createdBy>bird</createdBy> +<createdTime>2012-08-21 09:09:44 UTC</createdTime> +<ownerDesignName>TestManagerDatabase</ownerDesignName> +<engineerTo> +<item key="B082B14A-BEA8-D8A7-D661-197F34766ED3" value="true"/> +</engineerTo> +<adequatelyNormalized>NO</adequatelyNormalized> +<expectedVolumes>0</expectedVolumes> +<fwdEngineeringStrategyName>Single Table</fwdEngineeringStrategyName> +<growthPercent>0</growthPercent> +<growthType>Year</growthType> +<maxVolumes>9999999</maxVolumes> +<minVolumes>0</minVolumes> +<normalForm>Third</normalForm> +<fonts> +<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr"> +<foType>Title</foType> +<fontStyle>1</fontStyle> +<colorRGB>-16776961</colorRGB> +</FontObject> +<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr"> +<foType>Attribute</foType> +<colorRGB>-16777216</colorRGB> +</FontObject> +<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr"> +<foType>Datatype</foType> +<colorRGB>-16744448</colorRGB> +</FontObject> +<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr"> +<foType>PK Element</foType> +<colorRGB>-16776961</colorRGB> +</FontObject> +<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr"> +<foType>FK Element</foType> +<colorRGB>-16777216</colorRGB> +</FontObject> +<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr"> +<foType>UK Element</foType> +<colorRGB>-16776961</colorRGB> +</FontObject> +<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr"> +<foType>Not Null</foType> +<colorRGB>-65536</colorRGB> +</FontObject> +<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr"> +<foType>Key</foType> +<colorRGB>-16777216</colorRGB> +</FontObject> +</fonts> +</Entity>
\ No newline at end of file diff --git a/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/entity/seg_0/C79482B8-771B-FAD8-0337-163E3A45003A.xml b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/entity/seg_0/C79482B8-771B-FAD8-0337-163E3A45003A.xml new file mode 100644 index 00000000..3550b18c --- /dev/null +++ b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/entity/seg_0/C79482B8-771B-FAD8-0337-163E3A45003A.xml @@ -0,0 +1,52 @@ +<?xml version = '1.0' encoding = 'UTF-8'?> +<Entity class="oracle.dbtools.crest.model.design.logical.Entity" id="C79482B8-771B-FAD8-0337-163E3A45003A" directorySegmentName="seg_0" name="GlobalResources"> +<createdBy>bird</createdBy> +<createdTime>2012-08-20 22:13:16 UTC</createdTime> +<ownerDesignName>TestManagerDatabase</ownerDesignName> +<engineerTo> +<item key="B082B14A-BEA8-D8A7-D661-197F34766ED3" value="true"/> +</engineerTo> +<adequatelyNormalized>NO</adequatelyNormalized> +<expectedVolumes>0</expectedVolumes> +<fwdEngineeringStrategyName>Single Table</fwdEngineeringStrategyName> +<growthPercent>0</growthPercent> +<growthType>Year</growthType> +<maxVolumes>9999999</maxVolumes> +<minVolumes>0</minVolumes> +<normalForm>Third</normalForm> +<fonts> +<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr"> +<foType>Title</foType> +<fontStyle>1</fontStyle> +<colorRGB>-16776961</colorRGB> +</FontObject> +<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr"> +<foType>Attribute</foType> +<colorRGB>-16777216</colorRGB> +</FontObject> +<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr"> +<foType>Datatype</foType> +<colorRGB>-16744448</colorRGB> +</FontObject> +<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr"> +<foType>PK Element</foType> +<colorRGB>-16776961</colorRGB> +</FontObject> +<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr"> +<foType>FK Element</foType> +<colorRGB>-16777216</colorRGB> +</FontObject> +<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr"> +<foType>UK Element</foType> +<colorRGB>-16776961</colorRGB> +</FontObject> +<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr"> +<foType>Not Null</foType> +<colorRGB>-65536</colorRGB> +</FontObject> +<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr"> +<foType>Key</foType> +<colorRGB>-16777216</colorRGB> +</FontObject> +</fonts> +</Entity>
\ No newline at end of file diff --git a/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/entity/seg_0/D09E0DE5-99D6-2991-032A-A8A124F6ACBA.xml b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/entity/seg_0/D09E0DE5-99D6-2991-032A-A8A124F6ACBA.xml new file mode 100644 index 00000000..1e10ffb7 --- /dev/null +++ b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/entity/seg_0/D09E0DE5-99D6-2991-032A-A8A124F6ACBA.xml @@ -0,0 +1,52 @@ +<?xml version = '1.0' encoding = 'UTF-8'?> +<Entity class="oracle.dbtools.crest.model.design.logical.Entity" id="D09E0DE5-99D6-2991-032A-A8A124F6ACBA" directorySegmentName="seg_0" name="TestResultValues"> +<createdBy>bird</createdBy> +<createdTime>2012-08-21 09:11:32 UTC</createdTime> +<ownerDesignName>TestManagerDatabase</ownerDesignName> +<engineerTo> +<item key="B082B14A-BEA8-D8A7-D661-197F34766ED3" value="true"/> +</engineerTo> +<adequatelyNormalized>NO</adequatelyNormalized> +<expectedVolumes>0</expectedVolumes> +<fwdEngineeringStrategyName>Single Table</fwdEngineeringStrategyName> +<growthPercent>0</growthPercent> +<growthType>Year</growthType> +<maxVolumes>9999999</maxVolumes> +<minVolumes>0</minVolumes> +<normalForm>Third</normalForm> +<fonts> +<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr"> +<foType>Title</foType> +<fontStyle>1</fontStyle> +<colorRGB>-16776961</colorRGB> +</FontObject> +<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr"> +<foType>Attribute</foType> +<colorRGB>-16777216</colorRGB> +</FontObject> +<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr"> +<foType>Datatype</foType> +<colorRGB>-16744448</colorRGB> +</FontObject> +<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr"> +<foType>PK Element</foType> +<colorRGB>-16776961</colorRGB> +</FontObject> +<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr"> +<foType>FK Element</foType> +<colorRGB>-16777216</colorRGB> +</FontObject> +<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr"> +<foType>UK Element</foType> +<colorRGB>-16776961</colorRGB> +</FontObject> +<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr"> +<foType>Not Null</foType> +<colorRGB>-65536</colorRGB> +</FontObject> +<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr"> +<foType>Key</foType> +<colorRGB>-16777216</colorRGB> +</FontObject> +</fonts> +</Entity>
\ No newline at end of file diff --git a/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/entity/seg_0/DCC79294-5434-1DED-298C-6473DEE59FBA.xml b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/entity/seg_0/DCC79294-5434-1DED-298C-6473DEE59FBA.xml new file mode 100644 index 00000000..7891dab7 --- /dev/null +++ b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/entity/seg_0/DCC79294-5434-1DED-298C-6473DEE59FBA.xml @@ -0,0 +1,52 @@ +<?xml version = '1.0' encoding = 'UTF-8'?> +<Entity class="oracle.dbtools.crest.model.design.logical.Entity" id="DCC79294-5434-1DED-298C-6473DEE59FBA" directorySegmentName="seg_0" name="TestResultFailures"> +<createdBy>bird</createdBy> +<createdTime>2012-08-22 11:46:51 UTC</createdTime> +<ownerDesignName>TestManagerDatabase</ownerDesignName> +<engineerTo> +<item key="B082B14A-BEA8-D8A7-D661-197F34766ED3" value="true"/> +</engineerTo> +<adequatelyNormalized>NO</adequatelyNormalized> +<expectedVolumes>0</expectedVolumes> +<fwdEngineeringStrategyName>Single Table</fwdEngineeringStrategyName> +<growthPercent>0</growthPercent> +<growthType>Year</growthType> +<maxVolumes>9999999</maxVolumes> +<minVolumes>0</minVolumes> +<normalForm>Third</normalForm> +<fonts> +<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr"> +<foType>Title</foType> +<fontStyle>1</fontStyle> +<colorRGB>-16776961</colorRGB> +</FontObject> +<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr"> +<foType>Attribute</foType> +<colorRGB>-16777216</colorRGB> +</FontObject> +<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr"> +<foType>Datatype</foType> +<colorRGB>-16744448</colorRGB> +</FontObject> +<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr"> +<foType>PK Element</foType> +<colorRGB>-16776961</colorRGB> +</FontObject> +<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr"> +<foType>FK Element</foType> +<colorRGB>-16777216</colorRGB> +</FontObject> +<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr"> +<foType>UK Element</foType> +<colorRGB>-16776961</colorRGB> +</FontObject> +<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr"> +<foType>Not Null</foType> +<colorRGB>-65536</colorRGB> +</FontObject> +<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr"> +<foType>Key</foType> +<colorRGB>-16777216</colorRGB> +</FontObject> +</fonts> +</Entity>
\ No newline at end of file diff --git a/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/entity/seg_0/DE366053-6F7A-7F42-ABA3-00E583098C37.xml b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/entity/seg_0/DE366053-6F7A-7F42-ABA3-00E583098C37.xml new file mode 100644 index 00000000..145b2c76 --- /dev/null +++ b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/entity/seg_0/DE366053-6F7A-7F42-ABA3-00E583098C37.xml @@ -0,0 +1,52 @@ +<?xml version = '1.0' encoding = 'UTF-8'?> +<Entity class="oracle.dbtools.crest.model.design.logical.Entity" id="DE366053-6F7A-7F42-ABA3-00E583098C37" directorySegmentName="seg_0" name="TestGroups"> +<createdBy>bird</createdBy> +<createdTime>2012-08-20 22:34:30 UTC</createdTime> +<ownerDesignName>TestManagerDatabase</ownerDesignName> +<engineerTo> +<item key="B082B14A-BEA8-D8A7-D661-197F34766ED3" value="true"/> +</engineerTo> +<adequatelyNormalized>NO</adequatelyNormalized> +<expectedVolumes>0</expectedVolumes> +<fwdEngineeringStrategyName>Single Table</fwdEngineeringStrategyName> +<growthPercent>0</growthPercent> +<growthType>Year</growthType> +<maxVolumes>9999999</maxVolumes> +<minVolumes>0</minVolumes> +<normalForm>Third</normalForm> +<fonts> +<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr"> +<foType>Title</foType> +<fontStyle>1</fontStyle> +<colorRGB>-16776961</colorRGB> +</FontObject> +<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr"> +<foType>Attribute</foType> +<colorRGB>-16777216</colorRGB> +</FontObject> +<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr"> +<foType>Datatype</foType> +<colorRGB>-16744448</colorRGB> +</FontObject> +<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr"> +<foType>PK Element</foType> +<colorRGB>-16776961</colorRGB> +</FontObject> +<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr"> +<foType>FK Element</foType> +<colorRGB>-16777216</colorRGB> +</FontObject> +<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr"> +<foType>UK Element</foType> +<colorRGB>-16776961</colorRGB> +</FontObject> +<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr"> +<foType>Not Null</foType> +<colorRGB>-65536</colorRGB> +</FontObject> +<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr"> +<foType>Key</foType> +<colorRGB>-16777216</colorRGB> +</FontObject> +</fonts> +</Entity>
\ No newline at end of file diff --git a/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/entity/seg_0/E93BBF08-067B-A665-39F3-CF488A6547B2.xml b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/entity/seg_0/E93BBF08-067B-A665-39F3-CF488A6547B2.xml new file mode 100644 index 00000000..c8632bf7 --- /dev/null +++ b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/entity/seg_0/E93BBF08-067B-A665-39F3-CF488A6547B2.xml @@ -0,0 +1,52 @@ +<?xml version = '1.0' encoding = 'UTF-8'?> +<Entity class="oracle.dbtools.crest.model.design.logical.Entity" id="E93BBF08-067B-A665-39F3-CF488A6547B2" directorySegmentName="seg_0" name="RequirementsText"> +<createdBy>bird</createdBy> +<createdTime>2012-08-20 22:14:21 UTC</createdTime> +<ownerDesignName>TestManagerDatabase</ownerDesignName> +<engineerTo> +<item key="B082B14A-BEA8-D8A7-D661-197F34766ED3" value="true"/> +</engineerTo> +<adequatelyNormalized>NO</adequatelyNormalized> +<expectedVolumes>0</expectedVolumes> +<fwdEngineeringStrategyName>Single Table</fwdEngineeringStrategyName> +<growthPercent>0</growthPercent> +<growthType>Year</growthType> +<maxVolumes>9999999</maxVolumes> +<minVolumes>0</minVolumes> +<normalForm>Third</normalForm> +<fonts> +<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr"> +<foType>Title</foType> +<fontStyle>1</fontStyle> +<colorRGB>-16776961</colorRGB> +</FontObject> +<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr"> +<foType>Attribute</foType> +<colorRGB>-16777216</colorRGB> +</FontObject> +<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr"> +<foType>Datatype</foType> +<colorRGB>-16744448</colorRGB> +</FontObject> +<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr"> +<foType>PK Element</foType> +<colorRGB>-16776961</colorRGB> +</FontObject> +<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr"> +<foType>FK Element</foType> +<colorRGB>-16777216</colorRGB> +</FontObject> +<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr"> +<foType>UK Element</foType> +<colorRGB>-16776961</colorRGB> +</FontObject> +<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr"> +<foType>Not Null</foType> +<colorRGB>-65536</colorRGB> +</FontObject> +<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr"> +<foType>Key</foType> +<colorRGB>-16777216</colorRGB> +</FontObject> +</fonts> +</Entity>
\ No newline at end of file diff --git a/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/note/seg_0/876CB767-80BA-6C8E-AACA-F1CCC95C445E.xml b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/note/seg_0/876CB767-80BA-6C8E-AACA-F1CCC95C445E.xml new file mode 100644 index 00000000..31ddc417 --- /dev/null +++ b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/note/seg_0/876CB767-80BA-6C8E-AACA-F1CCC95C445E.xml @@ -0,0 +1,16 @@ +<?xml version = '1.0' encoding = 'UTF-8'?> +<Note class="oracle.dbtools.crest.model.design.Note" name="Note_1" id="876CB767-80BA-6C8E-AACA-F1CCC95C445E" directorySegmentName="seg_0"> +<createdBy>bird</createdBy> +<createdTime>2012-08-21 08:43:49 UTC</createdTime> +<comment>Priority, scheduling time, and testgroup dependencies are associated with SchedGroup membership.</comment> +<ownerDesignName>TestManagerDatabase</ownerDesignName> +<engineerTo> +<item key="B082B14A-BEA8-D8A7-D661-197F34766ED3" value="true"/> +</engineerTo> +<fonts> +<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr"> +<foType>Title</foType> +<colorRGB>-16777216</colorRGB> +</FontObject> +</fonts> +</Note>
\ No newline at end of file diff --git a/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/note/seg_0/D487AFDC-4027-F824-EA29-5C6D0ABB9E1E.xml b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/note/seg_0/D487AFDC-4027-F824-EA29-5C6D0ABB9E1E.xml new file mode 100644 index 00000000..9152a7c6 --- /dev/null +++ b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/note/seg_0/D487AFDC-4027-F824-EA29-5C6D0ABB9E1E.xml @@ -0,0 +1,16 @@ +<?xml version = '1.0' encoding = 'UTF-8'?> +<Note class="oracle.dbtools.crest.model.design.Note" name="Note_3" id="D487AFDC-4027-F824-EA29-5C6D0ABB9E1E" directorySegmentName="seg_0"> +<createdBy>bird</createdBy> +<createdTime>2012-08-21 08:57:21 UTC</createdTime> +<comment>Testsuite and build sources.</comment> +<ownerDesignName>TestManagerDatabase</ownerDesignName> +<engineerTo> +<item key="B082B14A-BEA8-D8A7-D661-197F34766ED3" value="true"/> +</engineerTo> +<fonts> +<FontObject class="oracle.dbtools.crest.model.design.FontObjectWr"> +<foType>Title</foType> +<colorRGB>-16777216</colorRGB> +</FontObject> +</fonts> +</Note>
\ No newline at end of file diff --git a/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/relation/seg_0/01537211-CCFB-0A1E-B43B-E8C641B69471.xml b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/relation/seg_0/01537211-CCFB-0A1E-B43B-E8C641B69471.xml new file mode 100644 index 00000000..e8b317cd --- /dev/null +++ b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/relation/seg_0/01537211-CCFB-0A1E-B43B-E8C641B69471.xml @@ -0,0 +1,17 @@ +<?xml version = '1.0' encoding = 'UTF-8'?> +<Relation class="oracle.dbtools.crest.model.design.logical.Relation" name="WhichTestcaseArgs" id="01537211-CCFB-0A1E-B43B-E8C641B69471" directorySegmentName="seg_0"> +<createdBy>bird</createdBy> +<createdTime>2012-08-21 09:57:18 UTC</createdTime> +<ownerDesignName>TestManagerDatabase</ownerDesignName> +<engineerTo> +<item key="B082B14A-BEA8-D8A7-D661-197F34766ED3" value="true"/> +</engineerTo> +<identifying>false</identifying> +<optionalSource>false</optionalSource> +<optionalTarget>true</optionalTarget> +<sourceCardinality>*</sourceCardinality> +<sourceEntity>90F477EE-35D6-21A7-B693-E5724FB07476</sourceEntity> +<targetCardinalityString>1</targetCardinalityString> +<targetEntity>1BEAB532-23CA-8628-0C97-7CAD39119A4E</targetEntity> +<transferable>true</transferable> +</Relation>
\ No newline at end of file diff --git a/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/relation/seg_0/02096BBB-0795-1759-1E26-2877BE36BB59.xml b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/relation/seg_0/02096BBB-0795-1759-1E26-2877BE36BB59.xml new file mode 100644 index 00000000..48df0a07 --- /dev/null +++ b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/relation/seg_0/02096BBB-0795-1759-1E26-2877BE36BB59.xml @@ -0,0 +1,17 @@ +<?xml version = '1.0' encoding = 'UTF-8'?> +<Relation class="oracle.dbtools.crest.model.design.logical.Relation" name="NestedTestResults" id="02096BBB-0795-1759-1E26-2877BE36BB59" directorySegmentName="seg_0"> +<createdBy>bird</createdBy> +<createdTime>2012-08-21 09:16:26 UTC</createdTime> +<ownerDesignName>TestManagerDatabase</ownerDesignName> +<engineerTo> +<item key="B082B14A-BEA8-D8A7-D661-197F34766ED3" value="true"/> +</engineerTo> +<identifying>false</identifying> +<optionalSource>true</optionalSource> +<optionalTarget>true</optionalTarget> +<sourceCardinality>1</sourceCardinality> +<sourceEntity>16464F5A-64BE-D2ED-91E0-BCBD0AA34680</sourceEntity> +<targetCardinalityString>*</targetCardinalityString> +<targetEntity>16464F5A-64BE-D2ED-91E0-BCBD0AA34680</targetEntity> +<transferable>true</transferable> +</Relation>
\ No newline at end of file diff --git a/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/relation/seg_0/0CCF1DE3-7916-9054-BEA6-C601FF564DB2.xml b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/relation/seg_0/0CCF1DE3-7916-9054-BEA6-C601FF564DB2.xml new file mode 100644 index 00000000..e5304e1d --- /dev/null +++ b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/relation/seg_0/0CCF1DE3-7916-9054-BEA6-C601FF564DB2.xml @@ -0,0 +1,17 @@ +<?xml version = '1.0' encoding = 'UTF-8'?> +<Relation class="oracle.dbtools.crest.model.design.logical.Relation" name="TestBoxGrouping" id="0CCF1DE3-7916-9054-BEA6-C601FF564DB2" directorySegmentName="seg_0"> +<createdBy>bird</createdBy> +<createdTime>2012-08-20 22:35:28 UTC</createdTime> +<ownerDesignName>TestManagerDatabase</ownerDesignName> +<engineerTo> +<item key="B082B14A-BEA8-D8A7-D661-197F34766ED3" value="true"/> +</engineerTo> +<identifying>true</identifying> +<optionalSource>false</optionalSource> +<optionalTarget>false</optionalTarget> +<sourceCardinality>1</sourceCardinality> +<sourceEntity>7AE36CC1-A030-63E5-6EF3-72FCD04815EE</sourceEntity> +<targetCardinalityString>*</targetCardinalityString> +<targetEntity>6A886CEE-579B-48FF-63F6-0FB03393FBF6</targetEntity> +<transferable>true</transferable> +</Relation>
\ No newline at end of file diff --git a/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/relation/seg_0/10867E70-94CE-FDAF-6B6E-2742D3A49E57.xml b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/relation/seg_0/10867E70-94CE-FDAF-6B6E-2742D3A49E57.xml new file mode 100644 index 00000000..ed642271 --- /dev/null +++ b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/relation/seg_0/10867E70-94CE-FDAF-6B6E-2742D3A49E57.xml @@ -0,0 +1,17 @@ +<?xml version = '1.0' encoding = 'UTF-8'?> +<Relation class="oracle.dbtools.crest.model.design.logical.Relation" name="ReasonForBlacklisting" id="10867E70-94CE-FDAF-6B6E-2742D3A49E57" directorySegmentName="seg_0"> +<createdBy>bird</createdBy> +<createdTime>2012-08-22 11:56:22 UTC</createdTime> +<ownerDesignName>TestManagerDatabase</ownerDesignName> +<engineerTo> +<item key="B082B14A-BEA8-D8A7-D661-197F34766ED3" value="true"/> +</engineerTo> +<identifying>false</identifying> +<optionalSource>false</optionalSource> +<optionalTarget>true</optionalTarget> +<sourceCardinality>1</sourceCardinality> +<sourceEntity>90367AFB-BA2D-A918-46B9-1E5DE53ACC48</sourceEntity> +<targetCardinalityString>1</targetCardinalityString> +<targetEntity>44FFF5E9-0C2F-7BAC-B5B7-73CA3A230B39</targetEntity> +<transferable>true</transferable> +</Relation>
\ No newline at end of file diff --git a/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/relation/seg_0/11710A55-6423-1904-841A-C7D2AB8CEEBF.xml b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/relation/seg_0/11710A55-6423-1904-841A-C7D2AB8CEEBF.xml new file mode 100644 index 00000000..4c37ff79 --- /dev/null +++ b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/relation/seg_0/11710A55-6423-1904-841A-C7D2AB8CEEBF.xml @@ -0,0 +1,17 @@ +<?xml version = '1.0' encoding = 'UTF-8'?> +<Relation class="oracle.dbtools.crest.model.design.logical.Relation" name="TestResultValues\" id="11710A55-6423-1904-841A-C7D2AB8CEEBF" directorySegmentName="seg_0"> +<createdBy>bird</createdBy> +<createdTime>2012-08-21 09:17:15 UTC</createdTime> +<ownerDesignName>TestManagerDatabase</ownerDesignName> +<engineerTo> +<item key="B082B14A-BEA8-D8A7-D661-197F34766ED3" value="true"/> +</engineerTo> +<identifying>false</identifying> +<optionalSource>true</optionalSource> +<optionalTarget>true</optionalTarget> +<sourceCardinality>1</sourceCardinality> +<sourceEntity>16464F5A-64BE-D2ED-91E0-BCBD0AA34680</sourceEntity> +<targetCardinalityString>*</targetCardinalityString> +<targetEntity>D09E0DE5-99D6-2991-032A-A8A124F6ACBA</targetEntity> +<transferable>true</transferable> +</Relation>
\ No newline at end of file diff --git a/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/relation/seg_0/1C189437-742B-B999-C955-7754C8ADB089.xml b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/relation/seg_0/1C189437-742B-B999-C955-7754C8ADB089.xml new file mode 100644 index 00000000..ee340833 --- /dev/null +++ b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/relation/seg_0/1C189437-742B-B999-C955-7754C8ADB089.xml @@ -0,0 +1,17 @@ +<?xml version = '1.0' encoding = 'UTF-8'?> +<Relation class="oracle.dbtools.crest.model.design.logical.Relation" name="SchedTestGroupMembership" id="1C189437-742B-B999-C955-7754C8ADB089" directorySegmentName="seg_0"> +<createdBy>bird</createdBy> +<createdTime>2012-08-20 22:46:08 UTC</createdTime> +<ownerDesignName>TestManagerDatabase</ownerDesignName> +<engineerTo> +<item key="B082B14A-BEA8-D8A7-D661-197F34766ED3" value="true"/> +</engineerTo> +<identifying>false</identifying> +<optionalSource>true</optionalSource> +<optionalTarget>true</optionalTarget> +<sourceCardinality>*</sourceCardinality> +<sourceEntity>6A886CEE-579B-48FF-63F6-0FB03393FBF6</sourceEntity> +<targetCardinalityString>*</targetCardinalityString> +<targetEntity>DE366053-6F7A-7F42-ABA3-00E583098C37</targetEntity> +<transferable>true</transferable> +</Relation>
\ No newline at end of file diff --git a/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/relation/seg_0/34733942-1305-4CA1-47EB-ACE724B04E69.xml b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/relation/seg_0/34733942-1305-4CA1-47EB-ACE724B04E69.xml new file mode 100644 index 00000000..bde14e2c --- /dev/null +++ b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/relation/seg_0/34733942-1305-4CA1-47EB-ACE724B04E69.xml @@ -0,0 +1,17 @@ +<?xml version = '1.0' encoding = 'UTF-8'?> +<Relation class="oracle.dbtools.crest.model.design.logical.Relation" name="TestResultFiles" id="34733942-1305-4CA1-47EB-ACE724B04E69" directorySegmentName="seg_0"> +<createdBy>bird</createdBy> +<createdTime>2012-08-21 09:16:58 UTC</createdTime> +<ownerDesignName>TestManagerDatabase</ownerDesignName> +<engineerTo> +<item key="B082B14A-BEA8-D8A7-D661-197F34766ED3" value="true"/> +</engineerTo> +<identifying>false</identifying> +<optionalSource>true</optionalSource> +<optionalTarget>true</optionalTarget> +<sourceCardinality>1</sourceCardinality> +<sourceEntity>16464F5A-64BE-D2ED-91E0-BCBD0AA34680</sourceEntity> +<targetCardinalityString>*</targetCardinalityString> +<targetEntity>28DD93CF-D058-7343-CD47-E9B435E1AC16</targetEntity> +<transferable>true</transferable> +</Relation>
\ No newline at end of file diff --git a/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/relation/seg_0/3563C940-E524-7F96-7AE0-DAC3C1C17AFC.xml b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/relation/seg_0/3563C940-E524-7F96-7AE0-DAC3C1C17AFC.xml new file mode 100644 index 00000000..0d924eae --- /dev/null +++ b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/relation/seg_0/3563C940-E524-7F96-7AE0-DAC3C1C17AFC.xml @@ -0,0 +1,17 @@ +<?xml version = '1.0' encoding = 'UTF-8'?> +<Relation class="oracle.dbtools.crest.model.design.logical.Relation" name="TestedBuild" id="3563C940-E524-7F96-7AE0-DAC3C1C17AFC" directorySegmentName="seg_0"> +<createdBy>bird</createdBy> +<createdTime>2012-08-21 10:14:03 UTC</createdTime> +<ownerDesignName>TestManagerDatabase</ownerDesignName> +<engineerTo> +<item key="B082B14A-BEA8-D8A7-D661-197F34766ED3" value="true"/> +</engineerTo> +<identifying>true</identifying> +<optionalSource>true</optionalSource> +<optionalTarget>false</optionalTarget> +<sourceCardinality>1</sourceCardinality> +<sourceEntity>504221DA-1B57-4EAD-39DB-40FD553E9FA2</sourceEntity> +<targetCardinalityString>*</targetCardinalityString> +<targetEntity>90F477EE-35D6-21A7-B693-E5724FB07476</targetEntity> +<transferable>true</transferable> +</Relation>
\ No newline at end of file diff --git a/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/relation/seg_0/3983F50A-EBB9-E4DE-1958-60EA4EDD6D6C.xml b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/relation/seg_0/3983F50A-EBB9-E4DE-1958-60EA4EDD6D6C.xml new file mode 100644 index 00000000..f0a22501 --- /dev/null +++ b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/relation/seg_0/3983F50A-EBB9-E4DE-1958-60EA4EDD6D6C.xml @@ -0,0 +1,17 @@ +<?xml version = '1.0' encoding = 'UTF-8'?> +<Relation class="oracle.dbtools.crest.model.design.logical.Relation" name="BuildSource" id="3983F50A-EBB9-E4DE-1958-60EA4EDD6D6C" directorySegmentName="seg_0"> +<createdBy>bird</createdBy> +<createdTime>2012-08-21 08:55:43 UTC</createdTime> +<ownerDesignName>TestManagerDatabase</ownerDesignName> +<engineerTo> +<item key="B082B14A-BEA8-D8A7-D661-197F34766ED3" value="true"/> +</engineerTo> +<identifying>false</identifying> +<optionalSource>true</optionalSource> +<optionalTarget>false</optionalTarget> +<sourceCardinality>1</sourceCardinality> +<sourceEntity>24150FB1-B00F-4F69-6F77-49ECB58F0F66</sourceEntity> +<targetCardinalityString>*</targetCardinalityString> +<targetEntity>6A886CEE-579B-48FF-63F6-0FB03393FBF6</targetEntity> +<transferable>true</transferable> +</Relation>
\ No newline at end of file diff --git a/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/relation/seg_0/3B7C8913-EB6A-47B1-27D0-E2C85EE9048B.xml b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/relation/seg_0/3B7C8913-EB6A-47B1-27D0-E2C85EE9048B.xml new file mode 100644 index 00000000..9a95a66a --- /dev/null +++ b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/relation/seg_0/3B7C8913-EB6A-47B1-27D0-E2C85EE9048B.xml @@ -0,0 +1,17 @@ +<?xml version = '1.0' encoding = 'UTF-8'?> +<Relation class="oracle.dbtools.crest.model.design.logical.Relation" name="NumericalRequirement" id="3B7C8913-EB6A-47B1-27D0-E2C85EE9048B" directorySegmentName="seg_0"> +<createdBy>bird</createdBy> +<createdTime>2012-08-20 22:41:40 UTC</createdTime> +<ownerDesignName>TestManagerDatabase</ownerDesignName> +<engineerTo> +<item key="B082B14A-BEA8-D8A7-D661-197F34766ED3" value="true"/> +</engineerTo> +<identifying>true</identifying> +<optionalSource>true</optionalSource> +<optionalTarget>false</optionalTarget> +<sourceCardinality>1</sourceCardinality> +<sourceEntity>A6A5F317-479C-A0DD-CAAE-9DCB56B29D40</sourceEntity> +<targetCardinalityString>*</targetCardinalityString> +<targetEntity>B36A186B-CDB3-7851-8C38-12EA8D50EAEB</targetEntity> +<transferable>true</transferable> +</Relation>
\ No newline at end of file diff --git a/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/relation/seg_0/518CE489-97B4-C05C-07A2-E3DBF14EE267.xml b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/relation/seg_0/518CE489-97B4-C05C-07A2-E3DBF14EE267.xml new file mode 100644 index 00000000..7987194b --- /dev/null +++ b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/relation/seg_0/518CE489-97B4-C05C-07A2-E3DBF14EE267.xml @@ -0,0 +1,17 @@ +<?xml version = '1.0' encoding = 'UTF-8'?> +<Relation class="oracle.dbtools.crest.model.design.logical.Relation" name="TestResultFailureReason" id="518CE489-97B4-C05C-07A2-E3DBF14EE267" directorySegmentName="seg_0"> +<createdBy>bird</createdBy> +<createdTime>2012-08-22 11:58:35 UTC</createdTime> +<ownerDesignName>TestManagerDatabase</ownerDesignName> +<engineerTo> +<item key="B082B14A-BEA8-D8A7-D661-197F34766ED3" value="true"/> +</engineerTo> +<identifying>false</identifying> +<optionalSource>false</optionalSource> +<optionalTarget>true</optionalTarget> +<sourceCardinality>1</sourceCardinality> +<sourceEntity>DCC79294-5434-1DED-298C-6473DEE59FBA</sourceEntity> +<targetCardinalityString>1</targetCardinalityString> +<targetEntity>44FFF5E9-0C2F-7BAC-B5B7-73CA3A230B39</targetEntity> +<transferable>true</transferable> +</Relation>
\ No newline at end of file diff --git a/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/relation/seg_0/68A0C3E1-0FA1-8414-A361-33B08A8EDB39.xml b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/relation/seg_0/68A0C3E1-0FA1-8414-A361-33B08A8EDB39.xml new file mode 100644 index 00000000..bf2200dc --- /dev/null +++ b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/relation/seg_0/68A0C3E1-0FA1-8414-A361-33B08A8EDB39.xml @@ -0,0 +1,17 @@ +<?xml version = '1.0' encoding = 'UTF-8'?> +<Relation class="oracle.dbtools.crest.model.design.logical.Relation" name="FailureRegardingTestResult" id="68A0C3E1-0FA1-8414-A361-33B08A8EDB39" directorySegmentName="seg_0"> +<createdBy>bird</createdBy> +<createdTime>2012-08-22 11:48:45 UTC</createdTime> +<ownerDesignName>TestManagerDatabase</ownerDesignName> +<engineerTo> +<item key="B082B14A-BEA8-D8A7-D661-197F34766ED3" value="true"/> +</engineerTo> +<identifying>false</identifying> +<optionalSource>false</optionalSource> +<optionalTarget>true</optionalTarget> +<sourceCardinality>1</sourceCardinality> +<sourceEntity>DCC79294-5434-1DED-298C-6473DEE59FBA</sourceEntity> +<targetCardinalityString>1</targetCardinalityString> +<targetEntity>16464F5A-64BE-D2ED-91E0-BCBD0AA34680</targetEntity> +<transferable>true</transferable> +</Relation>
\ No newline at end of file diff --git a/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/relation/seg_0/7497D76B-781B-3BDD-D797-FFBDB974F772.xml b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/relation/seg_0/7497D76B-781B-3BDD-D797-FFBDB974F772.xml new file mode 100644 index 00000000..43673229 --- /dev/null +++ b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/relation/seg_0/7497D76B-781B-3BDD-D797-FFBDB974F772.xml @@ -0,0 +1,17 @@ +<?xml version = '1.0' encoding = 'UTF-8'?> +<Relation class="oracle.dbtools.crest.model.design.logical.Relation" name="GlobalResourceDependencies" id="7497D76B-781B-3BDD-D797-FFBDB974F772" directorySegmentName="seg_0"> +<createdBy>bird</createdBy> +<createdTime>2012-08-20 22:42:25 UTC</createdTime> +<ownerDesignName>TestManagerDatabase</ownerDesignName> +<engineerTo> +<item key="B082B14A-BEA8-D8A7-D661-197F34766ED3" value="true"/> +</engineerTo> +<identifying>false</identifying> +<optionalSource>true</optionalSource> +<optionalTarget>true</optionalTarget> +<sourceCardinality>*</sourceCardinality> +<sourceEntity>B82DAF9A-6F99-5CF6-4D99-A391BAD66192</sourceEntity> +<targetCardinalityString>*</targetCardinalityString> +<targetEntity>C79482B8-771B-FAD8-0337-163E3A45003A</targetEntity> +<transferable>true</transferable> +</Relation>
\ No newline at end of file diff --git a/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/relation/seg_0/7DA9DD83-A52E-CA1E-FCBF-FC9CE71AF635.xml b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/relation/seg_0/7DA9DD83-A52E-CA1E-FCBF-FC9CE71AF635.xml new file mode 100644 index 00000000..dd75d4cb --- /dev/null +++ b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/relation/seg_0/7DA9DD83-A52E-CA1E-FCBF-FC9CE71AF635.xml @@ -0,0 +1,17 @@ +<?xml version = '1.0' encoding = 'UTF-8'?> +<Relation class="oracle.dbtools.crest.model.design.logical.Relation" name="TestResultMessages" id="7DA9DD83-A52E-CA1E-FCBF-FC9CE71AF635" directorySegmentName="seg_0"> +<createdBy>bird</createdBy> +<createdTime>2012-08-21 09:17:23 UTC</createdTime> +<ownerDesignName>TestManagerDatabase</ownerDesignName> +<engineerTo> +<item key="B082B14A-BEA8-D8A7-D661-197F34766ED3" value="true"/> +</engineerTo> +<identifying>false</identifying> +<optionalSource>true</optionalSource> +<optionalTarget>true</optionalTarget> +<sourceCardinality>1</sourceCardinality> +<sourceEntity>16464F5A-64BE-D2ED-91E0-BCBD0AA34680</sourceEntity> +<targetCardinalityString>*</targetCardinalityString> +<targetEntity>4579B792-2F35-D72A-1A3B-C7E53C41A766</targetEntity> +<transferable>true</transferable> +</Relation>
\ No newline at end of file diff --git a/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/relation/seg_0/89A83E25-364B-6B73-0613-FEAD875EF9FB.xml b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/relation/seg_0/89A83E25-364B-6B73-0613-FEAD875EF9FB.xml new file mode 100644 index 00000000..e8a4730c --- /dev/null +++ b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/relation/seg_0/89A83E25-364B-6B73-0613-FEAD875EF9FB.xml @@ -0,0 +1,17 @@ +<?xml version = '1.0' encoding = 'UTF-8'?> +<Relation class="oracle.dbtools.crest.model.design.logical.Relation" name="TestcaseArguments" id="89A83E25-364B-6B73-0613-FEAD875EF9FB" directorySegmentName="seg_0"> +<createdBy>bird</createdBy> +<createdTime>2012-08-20 22:40:39 UTC</createdTime> +<ownerDesignName>TestManagerDatabase</ownerDesignName> +<engineerTo> +<item key="B082B14A-BEA8-D8A7-D661-197F34766ED3" value="true"/> +</engineerTo> +<identifying>false</identifying> +<optionalSource>false</optionalSource> +<optionalTarget>false</optionalTarget> +<sourceCardinality>1</sourceCardinality> +<sourceEntity>B82DAF9A-6F99-5CF6-4D99-A391BAD66192</sourceEntity> +<targetCardinalityString>*</targetCardinalityString> +<targetEntity>1BEAB532-23CA-8628-0C97-7CAD39119A4E</targetEntity> +<transferable>true</transferable> +</Relation>
\ No newline at end of file diff --git a/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/relation/seg_0/8E5018CC-34E3-9AFC-D6D1-31E2BC4E9FE2.xml b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/relation/seg_0/8E5018CC-34E3-9AFC-D6D1-31E2BC4E9FE2.xml new file mode 100644 index 00000000..9d086559 --- /dev/null +++ b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/relation/seg_0/8E5018CC-34E3-9AFC-D6D1-31E2BC4E9FE2.xml @@ -0,0 +1,17 @@ +<?xml version = '1.0' encoding = 'UTF-8'?> +<Relation class="oracle.dbtools.crest.model.design.logical.Relation" name="WhatToRun" id="8E5018CC-34E3-9AFC-D6D1-31E2BC4E9FE2" directorySegmentName="seg_0"> +<createdBy>bird</createdBy> +<createdTime>2012-08-21 09:41:56 UTC</createdTime> +<ownerDesignName>TestManagerDatabase</ownerDesignName> +<engineerTo> +<item key="B082B14A-BEA8-D8A7-D661-197F34766ED3" value="true"/> +</engineerTo> +<identifying>false</identifying> +<optionalSource>false</optionalSource> +<optionalTarget>true</optionalTarget> +<sourceCardinality>*</sourceCardinality> +<sourceEntity>C332E3D7-638B-6CA8-24BF-383CA8659A3A</sourceEntity> +<targetCardinalityString>*</targetCardinalityString> +<targetEntity>1BEAB532-23CA-8628-0C97-7CAD39119A4E</targetEntity> +<transferable>true</transferable> +</Relation>
\ No newline at end of file diff --git a/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/relation/seg_0/9B1FE0CF-B2AD-EED0-22FC-461A7D46DE51.xml b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/relation/seg_0/9B1FE0CF-B2AD-EED0-22FC-461A7D46DE51.xml new file mode 100644 index 00000000..b50ed32a --- /dev/null +++ b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/relation/seg_0/9B1FE0CF-B2AD-EED0-22FC-461A7D46DE51.xml @@ -0,0 +1,17 @@ +<?xml version = '1.0' encoding = 'UTF-8'?> +<Relation class="oracle.dbtools.crest.model.design.logical.Relation" name="WhichResource" id="9B1FE0CF-B2AD-EED0-22FC-461A7D46DE51" directorySegmentName="seg_0"> +<createdBy>bird</createdBy> +<createdTime>2012-08-21 09:52:20 UTC</createdTime> +<ownerDesignName>TestManagerDatabase</ownerDesignName> +<engineerTo> +<item key="B082B14A-BEA8-D8A7-D661-197F34766ED3" value="true"/> +</engineerTo> +<identifying>false</identifying> +<optionalSource>false</optionalSource> +<optionalTarget>true</optionalTarget> +<sourceCardinality>1</sourceCardinality> +<sourceEntity>2F6ACC6D-3D17-537D-8ADF-F8424395B345</sourceEntity> +<targetCardinalityString>1</targetCardinalityString> +<targetEntity>C79482B8-771B-FAD8-0337-163E3A45003A</targetEntity> +<transferable>true</transferable> +</Relation>
\ No newline at end of file diff --git a/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/relation/seg_0/A182A65A-47AE-5D00-9A30-BC20AB050BF2.xml b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/relation/seg_0/A182A65A-47AE-5D00-9A30-BC20AB050BF2.xml new file mode 100644 index 00000000..b29652bd --- /dev/null +++ b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/relation/seg_0/A182A65A-47AE-5D00-9A30-BC20AB050BF2.xml @@ -0,0 +1,17 @@ +<?xml version = '1.0' encoding = 'UTF-8'?> +<Relation class="oracle.dbtools.crest.model.design.logical.Relation" name="TestSetResult" id="A182A65A-47AE-5D00-9A30-BC20AB050BF2" directorySegmentName="seg_0"> +<createdBy>bird</createdBy> +<createdTime>2012-08-21 09:15:48 UTC</createdTime> +<ownerDesignName>TestManagerDatabase</ownerDesignName> +<engineerTo> +<item key="B082B14A-BEA8-D8A7-D661-197F34766ED3" value="true"/> +</engineerTo> +<identifying>false</identifying> +<optionalSource>true</optionalSource> +<optionalTarget>true</optionalTarget> +<sourceCardinality>1</sourceCardinality> +<sourceEntity>90F477EE-35D6-21A7-B693-E5724FB07476</sourceEntity> +<targetCardinalityString>1</targetCardinalityString> +<targetEntity>16464F5A-64BE-D2ED-91E0-BCBD0AA34680</targetEntity> +<transferable>true</transferable> +</Relation>
\ No newline at end of file diff --git a/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/relation/seg_0/B346381F-48FE-E495-01A7-E22EC26AEE8A.xml b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/relation/seg_0/B346381F-48FE-E495-01A7-E22EC26AEE8A.xml new file mode 100644 index 00000000..ba60f398 --- /dev/null +++ b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/relation/seg_0/B346381F-48FE-E495-01A7-E22EC26AEE8A.xml @@ -0,0 +1,17 @@ +<?xml version = '1.0' encoding = 'UTF-8'?> +<Relation class="oracle.dbtools.crest.model.design.logical.Relation" name="TestGroupMember" id="B346381F-48FE-E495-01A7-E22EC26AEE8A" directorySegmentName="seg_0"> +<createdBy>bird</createdBy> +<createdTime>2012-08-20 22:37:24 UTC</createdTime> +<ownerDesignName>TestManagerDatabase</ownerDesignName> +<engineerTo> +<item key="B082B14A-BEA8-D8A7-D661-197F34766ED3" value="true"/> +</engineerTo> +<identifying>false</identifying> +<optionalSource>true</optionalSource> +<optionalTarget>true</optionalTarget> +<sourceCardinality>*</sourceCardinality> +<sourceEntity>B82DAF9A-6F99-5CF6-4D99-A391BAD66192</sourceEntity> +<targetCardinalityString>*</targetCardinalityString> +<targetEntity>DE366053-6F7A-7F42-ABA3-00E583098C37</targetEntity> +<transferable>true</transferable> +</Relation>
\ No newline at end of file diff --git a/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/relation/seg_0/B3596116-540F-6397-ECE4-58A386644E15.xml b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/relation/seg_0/B3596116-540F-6397-ECE4-58A386644E15.xml new file mode 100644 index 00000000..d4f9edd8 --- /dev/null +++ b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/relation/seg_0/B3596116-540F-6397-ECE4-58A386644E15.xml @@ -0,0 +1,17 @@ +<?xml version = '1.0' encoding = 'UTF-8'?> +<Relation class="oracle.dbtools.crest.model.design.logical.Relation" name="TestcaseDependencies" id="B3596116-540F-6397-ECE4-58A386644E15" directorySegmentName="seg_0"> +<createdBy>bird</createdBy> +<createdTime>2012-08-20 22:39:51 UTC</createdTime> +<ownerDesignName>TestManagerDatabase</ownerDesignName> +<engineerTo> +<item key="B082B14A-BEA8-D8A7-D661-197F34766ED3" value="true"/> +</engineerTo> +<identifying>false</identifying> +<optionalSource>true</optionalSource> +<optionalTarget>true</optionalTarget> +<sourceCardinality>*</sourceCardinality> +<sourceEntity>B82DAF9A-6F99-5CF6-4D99-A391BAD66192</sourceEntity> +<targetCardinalityString>1</targetCardinalityString> +<targetEntity>B82DAF9A-6F99-5CF6-4D99-A391BAD66192</targetEntity> +<transferable>true</transferable> +</Relation>
\ No newline at end of file diff --git a/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/relation/seg_0/BAD8EC05-6F14-4E38-366C-B4B660C6F38A.xml b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/relation/seg_0/BAD8EC05-6F14-4E38-366C-B4B660C6F38A.xml new file mode 100644 index 00000000..da1e2a8f --- /dev/null +++ b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/relation/seg_0/BAD8EC05-6F14-4E38-366C-B4B660C6F38A.xml @@ -0,0 +1,17 @@ +<?xml version = '1.0' encoding = 'UTF-8'?> +<Relation class="oracle.dbtools.crest.model.design.logical.Relation" name="InFailureCategory" id="BAD8EC05-6F14-4E38-366C-B4B660C6F38A" directorySegmentName="seg_0"> +<createdBy>bird</createdBy> +<createdTime>2012-08-22 11:57:18 UTC</createdTime> +<ownerDesignName>TestManagerDatabase</ownerDesignName> +<engineerTo> +<item key="B082B14A-BEA8-D8A7-D661-197F34766ED3" value="true"/> +</engineerTo> +<identifying>true</identifying> +<optionalSource>false</optionalSource> +<optionalTarget>false</optionalTarget> +<sourceCardinality>1</sourceCardinality> +<sourceEntity>44FFF5E9-0C2F-7BAC-B5B7-73CA3A230B39</sourceEntity> +<targetCardinalityString>*</targetCardinalityString> +<targetEntity>4D937E7C-3A28-E52D-89C0-EC8804C62367</targetEntity> +<transferable>true</transferable> +</Relation>
\ No newline at end of file diff --git a/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/relation/seg_0/C5B67DD4-FA4F-EF9F-1FF5-0445D51B32EE.xml b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/relation/seg_0/C5B67DD4-FA4F-EF9F-1FF5-0445D51B32EE.xml new file mode 100644 index 00000000..d75c9a0a --- /dev/null +++ b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/relation/seg_0/C5B67DD4-FA4F-EF9F-1FF5-0445D51B32EE.xml @@ -0,0 +1,17 @@ +<?xml version = '1.0' encoding = 'UTF-8'?> +<Relation class="oracle.dbtools.crest.model.design.logical.Relation" name="WhichTestBox" id="C5B67DD4-FA4F-EF9F-1FF5-0445D51B32EE" directorySegmentName="seg_0"> +<createdBy>bird</createdBy> +<createdTime>2012-08-21 09:59:42 UTC</createdTime> +<ownerDesignName>TestManagerDatabase</ownerDesignName> +<engineerTo> +<item key="B082B14A-BEA8-D8A7-D661-197F34766ED3" value="true"/> +</engineerTo> +<identifying>false</identifying> +<optionalSource>false</optionalSource> +<optionalTarget>true</optionalTarget> +<sourceCardinality>*</sourceCardinality> +<sourceEntity>90F477EE-35D6-21A7-B693-E5724FB07476</sourceEntity> +<targetCardinalityString>1</targetCardinalityString> +<targetEntity>7AE36CC1-A030-63E5-6EF3-72FCD04815EE</targetEntity> +<transferable>true</transferable> +</Relation>
\ No newline at end of file diff --git a/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/relation/seg_0/CCD38E11-8557-EB34-2651-07EB29E83FA6.xml b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/relation/seg_0/CCD38E11-8557-EB34-2651-07EB29E83FA6.xml new file mode 100644 index 00000000..bf216b5d --- /dev/null +++ b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/relation/seg_0/CCD38E11-8557-EB34-2651-07EB29E83FA6.xml @@ -0,0 +1,17 @@ +<?xml version = '1.0' encoding = 'UTF-8'?> +<Relation class="oracle.dbtools.crest.model.design.logical.Relation" name="TestSuiteSource" id="CCD38E11-8557-EB34-2651-07EB29E83FA6" directorySegmentName="seg_0"> +<createdBy>bird</createdBy> +<createdTime>2012-08-21 08:56:11 UTC</createdTime> +<ownerDesignName>TestManagerDatabase</ownerDesignName> +<engineerTo> +<item key="B082B14A-BEA8-D8A7-D661-197F34766ED3" value="true"/> +</engineerTo> +<identifying>false</identifying> +<optionalSource>true</optionalSource> +<optionalTarget>true</optionalTarget> +<sourceCardinality>1</sourceCardinality> +<sourceEntity>24150FB1-B00F-4F69-6F77-49ECB58F0F66</sourceEntity> +<targetCardinalityString>*</targetCardinalityString> +<targetEntity>6A886CEE-579B-48FF-63F6-0FB03393FBF6</targetEntity> +<transferable>true</transferable> +</Relation>
\ No newline at end of file diff --git a/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/relation/seg_0/E2A47942-ED55-E81D-4C71-9A134C49C147.xml b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/relation/seg_0/E2A47942-ED55-E81D-4C71-9A134C49C147.xml new file mode 100644 index 00000000..5164076c --- /dev/null +++ b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/relation/seg_0/E2A47942-ED55-E81D-4C71-9A134C49C147.xml @@ -0,0 +1,17 @@ +<?xml version = '1.0' encoding = 'UTF-8'?> +<Relation class="oracle.dbtools.crest.model.design.logical.Relation" name="TestBox" id="E2A47942-ED55-E81D-4C71-9A134C49C147" directorySegmentName="seg_0"> +<createdBy>bird</createdBy> +<createdTime>2012-08-21 09:43:14 UTC</createdTime> +<ownerDesignName>TestManagerDatabase</ownerDesignName> +<engineerTo> +<item key="B082B14A-BEA8-D8A7-D661-197F34766ED3" value="true"/> +</engineerTo> +<identifying>false</identifying> +<optionalSource>false</optionalSource> +<optionalTarget>true</optionalTarget> +<sourceCardinality>1</sourceCardinality> +<sourceEntity>7AE36CC1-A030-63E5-6EF3-72FCD04815EE</sourceEntity> +<targetCardinalityString>*</targetCardinalityString> +<targetEntity>A352A20F-310D-E285-FBC9-90DD0DA7BB9B</targetEntity> +<transferable>true</transferable> +</Relation>
\ No newline at end of file diff --git a/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/relation/seg_0/E4FE88E9-EE21-B43B-B0FE-A153E38246F9.xml b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/relation/seg_0/E4FE88E9-EE21-B43B-B0FE-A153E38246F9.xml new file mode 100644 index 00000000..fc0ec020 --- /dev/null +++ b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/relation/seg_0/E4FE88E9-EE21-B43B-B0FE-A153E38246F9.xml @@ -0,0 +1,17 @@ +<?xml version = '1.0' encoding = 'UTF-8'?> +<Relation class="oracle.dbtools.crest.model.design.logical.Relation" name="TestcaseRequirements" id="E4FE88E9-EE21-B43B-B0FE-A153E38246F9" directorySegmentName="seg_0"> +<createdBy>bird</createdBy> +<createdTime>2012-08-20 22:38:38 UTC</createdTime> +<ownerDesignName>TestManagerDatabase</ownerDesignName> +<engineerTo> +<item key="B082B14A-BEA8-D8A7-D661-197F34766ED3" value="true"/> +</engineerTo> +<identifying>false</identifying> +<optionalSource>true</optionalSource> +<optionalTarget>true</optionalTarget> +<sourceCardinality>*</sourceCardinality> +<sourceEntity>B82DAF9A-6F99-5CF6-4D99-A391BAD66192</sourceEntity> +<targetCardinalityString>1</targetCardinalityString> +<targetEntity>A6A5F317-479C-A0DD-CAAE-9DCB56B29D40</targetEntity> +<transferable>true</transferable> +</Relation>
\ No newline at end of file diff --git a/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/relation/seg_0/E62AE7DF-49EE-9280-B328-A867CBD273AE.xml b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/relation/seg_0/E62AE7DF-49EE-9280-B328-A867CBD273AE.xml new file mode 100644 index 00000000..3121966f --- /dev/null +++ b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/relation/seg_0/E62AE7DF-49EE-9280-B328-A867CBD273AE.xml @@ -0,0 +1,17 @@ +<?xml version = '1.0' encoding = 'UTF-8'?> +<Relation class="oracle.dbtools.crest.model.design.logical.Relation" name="CurrentTestSet" id="E62AE7DF-49EE-9280-B328-A867CBD273AE" directorySegmentName="seg_0"> +<createdBy>bird</createdBy> +<createdTime>2012-08-21 09:48:53 UTC</createdTime> +<ownerDesignName>TestManagerDatabase</ownerDesignName> +<engineerTo> +<item key="B082B14A-BEA8-D8A7-D661-197F34766ED3" value="true"/> +</engineerTo> +<identifying>false</identifying> +<optionalSource>true</optionalSource> +<optionalTarget>true</optionalTarget> +<sourceCardinality>1</sourceCardinality> +<sourceEntity>A352A20F-310D-E285-FBC9-90DD0DA7BB9B</sourceEntity> +<targetCardinalityString>1</targetCardinalityString> +<targetEntity>90F477EE-35D6-21A7-B693-E5724FB07476</targetEntity> +<transferable>true</transferable> +</Relation>
\ No newline at end of file diff --git a/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/relation/seg_0/E74406B5-20F1-4323-DC99-6E45982CB606.xml b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/relation/seg_0/E74406B5-20F1-4323-DC99-6E45982CB606.xml new file mode 100644 index 00000000..498ce1fb --- /dev/null +++ b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/relation/seg_0/E74406B5-20F1-4323-DC99-6E45982CB606.xml @@ -0,0 +1,17 @@ +<?xml version = '1.0' encoding = 'UTF-8'?> +<Relation class="oracle.dbtools.crest.model.design.logical.Relation" name="TextRequirements" id="E74406B5-20F1-4323-DC99-6E45982CB606" directorySegmentName="seg_0"> +<createdBy>bird</createdBy> +<createdTime>2012-08-20 22:41:57 UTC</createdTime> +<ownerDesignName>TestManagerDatabase</ownerDesignName> +<engineerTo> +<item key="B082B14A-BEA8-D8A7-D661-197F34766ED3" value="true"/> +</engineerTo> +<identifying>true</identifying> +<optionalSource>true</optionalSource> +<optionalTarget>false</optionalTarget> +<sourceCardinality>1</sourceCardinality> +<sourceEntity>A6A5F317-479C-A0DD-CAAE-9DCB56B29D40</sourceEntity> +<targetCardinalityString>*</targetCardinalityString> +<targetEntity>E93BBF08-067B-A665-39F3-CF488A6547B2</targetEntity> +<transferable>true</transferable> +</Relation>
\ No newline at end of file diff --git a/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/relation/seg_0/EC4EB506-3DBE-7F36-6451-F31920EDAB52.xml b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/relation/seg_0/EC4EB506-3DBE-7F36-6451-F31920EDAB52.xml new file mode 100644 index 00000000..18840e25 --- /dev/null +++ b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/relation/seg_0/EC4EB506-3DBE-7F36-6451-F31920EDAB52.xml @@ -0,0 +1,17 @@ +<?xml version = '1.0' encoding = 'UTF-8'?> +<Relation class="oracle.dbtools.crest.model.design.logical.Relation" name="AllocatedBy" id="EC4EB506-3DBE-7F36-6451-F31920EDAB52" directorySegmentName="seg_0"> +<createdBy>bird</createdBy> +<createdTime>2012-08-21 09:44:47 UTC</createdTime> +<ownerDesignName>TestManagerDatabase</ownerDesignName> +<engineerTo> +<item key="B082B14A-BEA8-D8A7-D661-197F34766ED3" value="true"/> +</engineerTo> +<identifying>false</identifying> +<optionalSource>true</optionalSource> +<optionalTarget>true</optionalTarget> +<sourceCardinality>1</sourceCardinality> +<sourceEntity>7AE36CC1-A030-63E5-6EF3-72FCD04815EE</sourceEntity> +<targetCardinalityString>*</targetCardinalityString> +<targetEntity>2F6ACC6D-3D17-537D-8ADF-F8424395B345</targetEntity> +<transferable>true</transferable> +</Relation>
\ No newline at end of file diff --git a/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/relation/seg_0/EE1D98EF-6AEA-2790-D9B9-DBC2ED21D880.xml b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/relation/seg_0/EE1D98EF-6AEA-2790-D9B9-DBC2ED21D880.xml new file mode 100644 index 00000000..6fcc7e2b --- /dev/null +++ b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/relation/seg_0/EE1D98EF-6AEA-2790-D9B9-DBC2ED21D880.xml @@ -0,0 +1,17 @@ +<?xml version = '1.0' encoding = 'UTF-8'?> +<Relation class="oracle.dbtools.crest.model.design.logical.Relation" name="BuildToType" id="EE1D98EF-6AEA-2790-D9B9-DBC2ED21D880" directorySegmentName="seg_0"> +<createdBy>bird</createdBy> +<createdTime>2012-08-21 08:53:25 UTC</createdTime> +<ownerDesignName>TestManagerDatabase</ownerDesignName> +<engineerTo> +<item key="B082B14A-BEA8-D8A7-D661-197F34766ED3" value="true"/> +</engineerTo> +<identifying>false</identifying> +<optionalSource>true</optionalSource> +<optionalTarget>false</optionalTarget> +<sourceCardinality>1</sourceCardinality> +<sourceEntity>9F78B73C-056D-DDEF-8C50-A9DA76B9E724</sourceEntity> +<targetCardinalityString>*</targetCardinalityString> +<targetEntity>504221DA-1B57-4EAD-39DB-40FD553E9FA2</targetEntity> +<transferable>true</transferable> +</Relation>
\ No newline at end of file diff --git a/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/subviews/016BA1CF-6EA4-9CA4-CDF7-3AAA507EF6EF.xml b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/subviews/016BA1CF-6EA4-9CA4-CDF7-3AAA507EF6EF.xml new file mode 100644 index 00000000..e947c03a --- /dev/null +++ b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/subviews/016BA1CF-6EA4-9CA4-CDF7-3AAA507EF6EF.xml @@ -0,0 +1,40 @@ +<?xml version = '1.0' encoding = 'UTF-8'?> +<Diagram class="oracle.dbtools.crest.swingui.logical.DPVLogicalSubView" name="Failure Tracking" id="016BA1CF-6EA4-9CA4-CDF7-3AAA507EF6EF"> +<createdBy>bird</createdBy> +<createdTime>2012-08-22 12:01:22 UTC</createdTime> +<autoRoute>false</autoRoute> +<boxInbox>true</boxInbox> +<showLegend>false</showLegend> +<showLabels>false</showLabels> +<showGrid>false</showGrid> +<diagramColor>-1</diagramColor> +<display>false</display> +<notation>0</notation> +<objectViews> +<OView class="oracle.dbtools.crest.swingui.logical.TVEntity" oid="44FFF5E9-0C2F-7BAC-B5B7-73CA3A230B39" otype="Entity" vid="D1B4D1DF-E3AB-F84A-F479-87FB68F0A2D2"> +<bounds x="1270" y="448" width="151" height="41"/> +</OView> +<OView class="oracle.dbtools.crest.swingui.logical.TVEntity" oid="4D937E7C-3A28-E52D-89C0-EC8804C62367" otype="Entity" vid="37DED3CC-443D-FC8B-A30D-07BF0D742C62"> +<bounds x="1270" y="522" width="152" height="43"/> +</OView> +<OView class="oracle.dbtools.crest.swingui.logical.TVEntity" oid="DCC79294-5434-1DED-298C-6473DEE59FBA" otype="Entity" vid="95A5D57E-9986-0942-BCE8-4B9F5F46AE30"> +<bounds x="1087" y="460" width="157" height="51"/> +</OView> +</objectViews> +<connectors> +<Connector class="oracle.dbtools.crest.swingui.logical.TVRelation" oid="518CE489-97B4-C05C-07A2-E3DBF14EE267" otype="Relation" vid_source="95A5D57E-9986-0942-BCE8-4B9F5F46AE30" vid_target="D1B4D1DF-E3AB-F84A-F479-87FB68F0A2D2"> +<lineWidth>1</lineWidth> +<points> +<point x="1244" y="474"/> +<point x="1270" y="474"/> +</points> +</Connector> +<Connector class="oracle.dbtools.crest.swingui.logical.TVRelation" oid="BAD8EC05-6F14-4E38-366C-B4B660C6F38A" otype="Relation" vid_source="D1B4D1DF-E3AB-F84A-F479-87FB68F0A2D2" vid_target="37DED3CC-443D-FC8B-A30D-07BF0D742C62"> +<lineWidth>1</lineWidth> +<points> +<point x="1345" y="489"/> +<point x="1345" y="522"/> +</points> +</Connector> +</connectors> +</Diagram>
\ No newline at end of file diff --git a/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/subviews/32D718B4-250F-95DC-37F0-C0A817F69020.xml b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/subviews/32D718B4-250F-95DC-37F0-C0A817F69020.xml new file mode 100644 index 00000000..6493425b --- /dev/null +++ b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/subviews/32D718B4-250F-95DC-37F0-C0A817F69020.xml @@ -0,0 +1,70 @@ +<?xml version = '1.0' encoding = 'UTF-8'?> +<Diagram class="oracle.dbtools.crest.swingui.logical.DPVLogicalSubView" name="Outputs" id="32D718B4-250F-95DC-37F0-C0A817F69020"> +<createdBy>bird</createdBy> +<createdTime>2012-08-21 09:19:53 UTC</createdTime> +<autoRoute>false</autoRoute> +<boxInbox>true</boxInbox> +<showLegend>false</showLegend> +<showLabels>false</showLabels> +<showGrid>false</showGrid> +<diagramColor>-1</diagramColor> +<display>false</display> +<notation>0</notation> +<objectViews> +<OView class="oracle.dbtools.crest.swingui.logical.TVEntity" oid="16464F5A-64BE-D2ED-91E0-BCBD0AA34680" otype="Entity" vid="636E76B2-6F21-38E5-BF29-D4C078AC8F61"> +<bounds x="1014" y="625" width="121" height="102"/> +</OView> +<OView class="oracle.dbtools.crest.swingui.logical.TVEntity" oid="28DD93CF-D058-7343-CD47-E9B435E1AC16" otype="Entity" vid="89BDF7A8-D79D-A869-BE57-BD2E1C2B290C"> +<bounds x="1190" y="610" width="131" height="41"/> +</OView> +<OView class="oracle.dbtools.crest.swingui.logical.TVEntity" oid="4579B792-2F35-D72A-1A3B-C7E53C41A766" otype="Entity" vid="D72D72DA-F9C0-CE9C-E6A6-7A44DA7656DC"> +<bounds x="1190" y="710" width="131" height="41"/> +</OView> +<OView class="oracle.dbtools.crest.swingui.logical.TVEntity" oid="90F477EE-35D6-21A7-B693-E5724FB07476" otype="Entity" vid="0A09F0EB-AF09-D080-F1B5-EC4E3693C1C5"> +<bounds x="824" y="652" width="141" height="51"/> +</OView> +<OView class="oracle.dbtools.crest.swingui.logical.TVEntity" oid="D09E0DE5-99D6-2991-032A-A8A124F6ACBA" otype="Entity" vid="239CADB1-5F1D-1286-1C79-0DCD91157E84"> +<bounds x="1190" y="662" width="131" height="39"/> +</OView> +</objectViews> +<connectors> +<Connector class="oracle.dbtools.crest.swingui.logical.TVRelation" oid="02096BBB-0795-1759-1E26-2877BE36BB59" otype="Relation" vid_source="636E76B2-6F21-38E5-BF29-D4C078AC8F61" vid_target="636E76B2-6F21-38E5-BF29-D4C078AC8F61"> +<lineWidth>1</lineWidth> +<points> +<point x="1135" y="676"/> +<point x="1150" y="676"/> +<point x="1150" y="742"/> +<point x="1074" y="742"/> +<point x="1074" y="727"/> +</points> +</Connector> +<Connector class="oracle.dbtools.crest.swingui.logical.TVRelation" oid="11710A55-6423-1904-841A-C7D2AB8CEEBF" otype="Relation" vid_source="636E76B2-6F21-38E5-BF29-D4C078AC8F61" vid_target="239CADB1-5F1D-1286-1C79-0DCD91157E84"> +<lineWidth>1</lineWidth> +<points> +<point x="1135" y="691"/> +<point x="1190" y="691"/> +</points> +</Connector> +<Connector class="oracle.dbtools.crest.swingui.logical.TVRelation" oid="34733942-1305-4CA1-47EB-ACE724B04E69" otype="Relation" vid_source="636E76B2-6F21-38E5-BF29-D4C078AC8F61" vid_target="89BDF7A8-D79D-A869-BE57-BD2E1C2B290C"> +<lineWidth>1</lineWidth> +<points> +<point x="1135" y="638"/> +<point x="1190" y="638"/> +</points> +</Connector> +<Connector class="oracle.dbtools.crest.swingui.logical.TVRelation" oid="7DA9DD83-A52E-CA1E-FCBF-FC9CE71AF635" otype="Relation" vid_source="636E76B2-6F21-38E5-BF29-D4C078AC8F61" vid_target="D72D72DA-F9C0-CE9C-E6A6-7A44DA7656DC"> +<lineWidth>1</lineWidth> +<points> +<point x="1135" y="718"/> +<point x="1190" y="718"/> +</points> +</Connector> +<Connector class="oracle.dbtools.crest.swingui.logical.TVRelation" oid="A182A65A-47AE-5D00-9A30-BC20AB050BF2" otype="Relation" vid_source="0A09F0EB-AF09-D080-F1B5-EC4E3693C1C5" vid_target="636E76B2-6F21-38E5-BF29-D4C078AC8F61"> +<lineWidth>1</lineWidth> +<points> +<point x="965" y="677"/> +<point x="1014" y="677"/> +</points> +</Connector> +</connectors> +</Diagram>
\ No newline at end of file diff --git a/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/subviews/571DBBAF-CDDA-1C46-4220-D1319C0EEC00.xml b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/subviews/571DBBAF-CDDA-1C46-4220-D1319C0EEC00.xml new file mode 100644 index 00000000..25df5afc --- /dev/null +++ b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/subviews/571DBBAF-CDDA-1C46-4220-D1319C0EEC00.xml @@ -0,0 +1,24 @@ +<?xml version = '1.0' encoding = 'UTF-8'?> +<Diagram class="oracle.dbtools.crest.swingui.logical.DPVLogicalSubView" name="Persistent Test Manager Data" id="571DBBAF-CDDA-1C46-4220-D1319C0EEC00"> +<createdBy>bird</createdBy> +<createdTime>2012-08-21 09:19:18 UTC</createdTime> +<autoRoute>false</autoRoute> +<boxInbox>true</boxInbox> +<showLegend>false</showLegend> +<showLabels>false</showLabels> +<showGrid>false</showGrid> +<diagramColor>-1</diagramColor> +<display>false</display> +<notation>0</notation> +<objectViews> +<OView class="oracle.dbtools.crest.swingui.logical.TVEntity" oid="2F6ACC6D-3D17-537D-8ADF-F8424395B345" otype="Entity" vid="B4E5F358-5BC8-9B06-4A13-EDF705ED9089"> +<bounds x="110" y="570" width="151" height="61"/> +</OView> +<OView class="oracle.dbtools.crest.swingui.logical.TVEntity" oid="A352A20F-310D-E285-FBC9-90DD0DA7BB9B" otype="Entity" vid="8747577F-8999-3CBF-1376-1DD291702774"> +<bounds x="300" y="570" width="151" height="61"/> +</OView> +<OView class="oracle.dbtools.crest.swingui.logical.TVEntity" oid="C332E3D7-638B-6CA8-24BF-383CA8659A3A" otype="Entity" vid="F053C992-CB30-88B3-66FF-F4E522C60155"> +<bounds x="499" y="570" width="136" height="61"/> +</OView> +</objectViews> +</Diagram>
\ No newline at end of file diff --git a/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/subviews/65FA5BA0-CC9C-C108-BB1B-AC9E13F5BC83.xml b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/subviews/65FA5BA0-CC9C-C108-BB1B-AC9E13F5BC83.xml new file mode 100644 index 00000000..c248a58e --- /dev/null +++ b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/subviews/65FA5BA0-CC9C-C108-BB1B-AC9E13F5BC83.xml @@ -0,0 +1,127 @@ +<?xml version = '1.0' encoding = 'UTF-8'?> +<Diagram class="oracle.dbtools.crest.swingui.logical.DPVLogicalSubView" name="Configuration" id="65FA5BA0-CC9C-C108-BB1B-AC9E13F5BC83"> +<createdBy>bird</createdBy> +<createdTime>2012-08-21 08:58:45 UTC</createdTime> +<autoRoute>false</autoRoute> +<boxInbox>true</boxInbox> +<showLegend>false</showLegend> +<showLabels>false</showLabels> +<showGrid>false</showGrid> +<diagramColor>-1</diagramColor> +<display>false</display> +<notation>0</notation> +<objectViews> +<OView class="oracle.dbtools.crest.swingui.logical.TVEntity" oid="1BEAB532-23CA-8628-0C97-7CAD39119A4E" otype="Entity" vid="459DD9CF-0825-0BAE-7BBA-FADAA3B895BB"> +<bounds x="680" y="419" width="161" height="52"/> +</OView> +<OView class="oracle.dbtools.crest.swingui.logical.TVEntity" oid="24150FB1-B00F-4F69-6F77-49ECB58F0F66" otype="Entity" vid="398E8687-F10E-D31E-DD4E-EA0A6A7868A3"> +<bounds x="273" y="96" width="138" height="61"/> +</OView> +<OView class="oracle.dbtools.crest.swingui.logical.TVEntity" oid="6A886CEE-579B-48FF-63F6-0FB03393FBF6" otype="Entity" vid="E301FF23-DE18-19FB-9A6A-9F170D26B939"> +<bounds x="180" y="250" width="131" height="71"/> +</OView> +<OView class="oracle.dbtools.crest.swingui.logical.TVEntity" oid="7AE36CC1-A030-63E5-6EF3-72FCD04815EE" otype="Entity" vid="B06DA0BE-1DA3-3AB7-06CD-E7EA9FDC0B3E"> +<bounds x="101" y="95" width="131" height="61"/> +</OView> +<OView class="oracle.dbtools.crest.swingui.logical.TVEntity" oid="A6A5F317-479C-A0DD-CAAE-9DCB56B29D40" otype="Entity" vid="49F6288A-70A0-788D-3FEE-BE0053D8D44C"> +<bounds x="680" y="130" width="161" height="41"/> +</OView> +<OView class="oracle.dbtools.crest.swingui.logical.TVEntity" oid="B36A186B-CDB3-7851-8C38-12EA8D50EAEB" otype="Entity" vid="9E4B525D-2B00-0B76-39EE-0C0F74693333"> +<bounds x="600" y="30" width="141" height="31"/> +</OView> +<OView class="oracle.dbtools.crest.swingui.logical.TVEntity" oid="B82DAF9A-6F99-5CF6-4D99-A391BAD66192" otype="Entity" vid="2C49F347-32B8-CA7C-2646-4F16FDDA087E"> +<bounds x="680" y="250" width="161" height="71"/> +</OView> +<OView class="oracle.dbtools.crest.swingui.logical.TVEntity" oid="C79482B8-771B-FAD8-0337-163E3A45003A" otype="Entity" vid="8FAC087B-6133-162A-207B-3FAFB7B41E98"> +<bounds x="908" y="250" width="153" height="31"/> +</OView> +<OView class="oracle.dbtools.crest.swingui.logical.TVEntity" oid="DE366053-6F7A-7F42-ABA3-00E583098C37" otype="Entity" vid="61150DED-91F4-1AE3-BD02-4EDC4CC0D98F"> +<bounds x="430" y="250" width="131" height="71"/> +</OView> +<OView class="oracle.dbtools.crest.swingui.logical.TVEntity" oid="E93BBF08-067B-A665-39F3-CF488A6547B2" otype="Entity" vid="C41DA40C-A50A-BDCC-4DA0-2DCA7874C1A2"> +<bounds x="789" y="30" width="132" height="31"/> +</OView> +</objectViews> +<connectors> +<Connector class="oracle.dbtools.crest.swingui.logical.TVRelation" oid="0CCF1DE3-7916-9054-BEA6-C601FF564DB2" otype="Relation" vid_source="B06DA0BE-1DA3-3AB7-06CD-E7EA9FDC0B3E" vid_target="E301FF23-DE18-19FB-9A6A-9F170D26B939"> +<lineWidth>1</lineWidth> +<points> +<point x="206" y="156"/> +<point x="206" y="250"/> +</points> +</Connector> +<Connector class="oracle.dbtools.crest.swingui.logical.TVRelation" oid="1C189437-742B-B999-C955-7754C8ADB089" otype="Relation" vid_source="E301FF23-DE18-19FB-9A6A-9F170D26B939" vid_target="61150DED-91F4-1AE3-BD02-4EDC4CC0D98F"> +<lineWidth>1</lineWidth> +<points> +<point x="311" y="285"/> +<point x="430" y="285"/> +</points> +</Connector> +<Connector class="oracle.dbtools.crest.swingui.logical.TVRelation" oid="3983F50A-EBB9-E4DE-1958-60EA4EDD6D6C" otype="Relation" vid_source="398E8687-F10E-D31E-DD4E-EA0A6A7868A3" vid_target="E301FF23-DE18-19FB-9A6A-9F170D26B939"> +<lineWidth>1</lineWidth> +<points> +<point x="292" y="157"/> +<point x="292" y="250"/> +</points> +</Connector> +<Connector class="oracle.dbtools.crest.swingui.logical.TVRelation" oid="3B7C8913-EB6A-47B1-27D0-E2C85EE9048B" otype="Relation" vid_source="49F6288A-70A0-788D-3FEE-BE0053D8D44C" vid_target="9E4B525D-2B00-0B76-39EE-0C0F74693333"> +<lineWidth>1</lineWidth> +<points> +<point x="710" y="130"/> +<point x="710" y="61"/> +</points> +</Connector> +<Connector class="oracle.dbtools.crest.swingui.logical.TVRelation" oid="7497D76B-781B-3BDD-D797-FFBDB974F772" otype="Relation" vid_source="2C49F347-32B8-CA7C-2646-4F16FDDA087E" vid_target="8FAC087B-6133-162A-207B-3FAFB7B41E98"> +<lineWidth>1</lineWidth> +<points> +<point x="841" y="265"/> +<point x="908" y="265"/> +</points> +</Connector> +<Connector class="oracle.dbtools.crest.swingui.logical.TVRelation" oid="89A83E25-364B-6B73-0613-FEAD875EF9FB" otype="Relation" vid_source="2C49F347-32B8-CA7C-2646-4F16FDDA087E" vid_target="459DD9CF-0825-0BAE-7BBA-FADAA3B895BB"> +<lineWidth>1</lineWidth> +<points> +<point x="760" y="321"/> +<point x="760" y="419"/> +</points> +</Connector> +<Connector class="oracle.dbtools.crest.swingui.logical.TVRelation" oid="B346381F-48FE-E495-01A7-E22EC26AEE8A" otype="Relation" vid_source="2C49F347-32B8-CA7C-2646-4F16FDDA087E" vid_target="61150DED-91F4-1AE3-BD02-4EDC4CC0D98F"> +<lineWidth>1</lineWidth> +<points> +<point x="680" y="285"/> +<point x="561" y="285"/> +</points> +</Connector> +<Connector class="oracle.dbtools.crest.swingui.logical.TVRelation" oid="B3596116-540F-6397-ECE4-58A386644E15" otype="Relation" vid_source="2C49F347-32B8-CA7C-2646-4F16FDDA087E" vid_target="2C49F347-32B8-CA7C-2646-4F16FDDA087E"> +<lineWidth>1</lineWidth> +<points> +<point x="841" y="285"/> +<point x="856" y="285"/> +<point x="856" y="336"/> +<point x="760" y="336"/> +<point x="760" y="321"/> +</points> +</Connector> +<Connector class="oracle.dbtools.crest.swingui.logical.TVRelation" oid="CCD38E11-8557-EB34-2651-07EB29E83FA6" otype="Relation" vid_source="398E8687-F10E-D31E-DD4E-EA0A6A7868A3" vid_target="E301FF23-DE18-19FB-9A6A-9F170D26B939"> +<lineWidth>1</lineWidth> +<points> +<point x="302" y="157"/> +<point x="302" y="250"/> +</points> +</Connector> +<Connector class="oracle.dbtools.crest.swingui.logical.TVRelation" oid="E4FE88E9-EE21-B43B-B0FE-A153E38246F9" otype="Relation" vid_source="2C49F347-32B8-CA7C-2646-4F16FDDA087E" vid_target="49F6288A-70A0-788D-3FEE-BE0053D8D44C"> +<lineWidth>1</lineWidth> +<points> +<point x="760" y="250"/> +<point x="760" y="171"/> +</points> +</Connector> +<Connector class="oracle.dbtools.crest.swingui.logical.TVRelation" oid="E74406B5-20F1-4323-DC99-6E45982CB606" otype="Relation" vid_source="49F6288A-70A0-788D-3FEE-BE0053D8D44C" vid_target="C41DA40C-A50A-BDCC-4DA0-2DCA7874C1A2"> +<lineWidth>1</lineWidth> +<points> +<point x="815" y="130"/> +<point x="815" y="61"/> +</points> +</Connector> +</connectors> +</Diagram>
\ No newline at end of file diff --git a/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/subviews/AFCEF013-4CF2-4A5A-79A3-31521C1CA20A.xml b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/subviews/AFCEF013-4CF2-4A5A-79A3-31521C1CA20A.xml new file mode 100644 index 00000000..14a7566f --- /dev/null +++ b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/subviews/AFCEF013-4CF2-4A5A-79A3-31521C1CA20A.xml @@ -0,0 +1,306 @@ +<?xml version = '1.0' encoding = 'UTF-8'?> +<Diagram class="oracle.dbtools.crest.swingui.logical.DPVLogical" name="Logical" id="AFCEF013-4CF2-4A5A-79A3-31521C1CA20A"> +<createdBy>bird</createdBy> +<createdTime>2012-08-20 22:02:17 UTC</createdTime> +<autoRoute>false</autoRoute> +<boxInbox>true</boxInbox> +<showLegend>false</showLegend> +<showLabels>true</showLabels> +<showGrid>true</showGrid> +<diagramColor>-1</diagramColor> +<legendPosX>265</legendPosX> +<legendPosY>490</legendPosY> +<display>false</display> +<notation>0</notation> +<objectViews> +<OView class="oracle.dbtools.crest.swingui.logical.TVEntity" oid="16464F5A-64BE-D2ED-91E0-BCBD0AA34680" otype="Entity" vid="5B100733-B921-D478-15B5-3BE9A7747A87"> +<bounds x="1014" y="625" width="121" height="102"/> +</OView> +<OView class="oracle.dbtools.crest.swingui.logical.TVEntity" oid="1BEAB532-23CA-8628-0C97-7CAD39119A4E" otype="Entity" vid="62F579AD-F97F-1F92-7C5F-525AE1A2F26C"> +<bounds x="680" y="419" width="161" height="52"/> +</OView> +<OView class="oracle.dbtools.crest.swingui.logical.TVEntity" oid="24150FB1-B00F-4F69-6F77-49ECB58F0F66" otype="Entity" vid="B3D29C8C-8482-D7AF-BE58-122AB07FB853"> +<bounds x="273" y="96" width="138" height="61"/> +</OView> +<OView class="oracle.dbtools.crest.swingui.logical.TVEntity" oid="28DD93CF-D058-7343-CD47-E9B435E1AC16" otype="Entity" vid="ABB72A58-23E7-DF85-4B01-74F467F60284"> +<bounds x="1190" y="610" width="131" height="41"/> +</OView> +<OView class="oracle.dbtools.crest.swingui.logical.TVEntity" oid="2F6ACC6D-3D17-537D-8ADF-F8424395B345" otype="Entity" vid="40AB3AA2-7D9F-7BA7-AB96-050F27CF81AB"> +<bounds x="110" y="570" width="151" height="51"/> +</OView> +<OView class="oracle.dbtools.crest.swingui.logical.TVEntity" oid="44FFF5E9-0C2F-7BAC-B5B7-73CA3A230B39" otype="Entity" vid="BE78445F-B005-8F1A-E390-120DCC587063"> +<bounds x="1270" y="448" width="151" height="41"/> +</OView> +<OView class="oracle.dbtools.crest.swingui.logical.TVEntity" oid="4579B792-2F35-D72A-1A3B-C7E53C41A766" otype="Entity" vid="BA629852-B837-F348-59DD-12899B260C79"> +<bounds x="1190" y="710" width="131" height="41"/> +</OView> +<OView class="oracle.dbtools.crest.swingui.logical.TVEntity" oid="4D937E7C-3A28-E52D-89C0-EC8804C62367" otype="Entity" vid="109E2A3F-B942-1D32-CB1C-4F60260ACF5C"> +<bounds x="1270" y="522" width="152" height="43"/> +</OView> +<OView class="oracle.dbtools.crest.swingui.logical.TVEntity" oid="504221DA-1B57-4EAD-39DB-40FD553E9FA2" otype="Entity" vid="F4CED71A-65B7-151C-3ADC-26F25043F168"> +<bounds x="1092" y="301" width="151" height="70"/> +</OView> +<OView class="oracle.dbtools.crest.swingui.logical.TVEntity" oid="6A886CEE-579B-48FF-63F6-0FB03393FBF6" otype="Entity" vid="81A8E233-0690-CBFE-6102-F71A991903FC"> +<bounds x="180" y="250" width="131" height="71"/> +</OView> +<OView class="oracle.dbtools.crest.swingui.logical.TVEntity" oid="7AE36CC1-A030-63E5-6EF3-72FCD04815EE" otype="Entity" vid="C8DAF849-7026-3615-7FC8-4397BFC6CA14"> +<bounds x="101" y="95" width="131" height="61"/> +</OView> +<OView class="oracle.dbtools.crest.swingui.TVNote" oid="876CB767-80BA-6C8E-AACA-F1CCC95C445E" otype="Note" vid="593FF096-DB74-2562-91B0-A4F1423FEBA7"> +<bounds x="292" y="336" width="149" height="61"/> +</OView> +<OView class="oracle.dbtools.crest.swingui.logical.TVEntity" oid="90367AFB-BA2D-A918-46B9-1E5DE53ACC48" otype="Entity" vid="5A1E3970-E7C2-5B4A-B4FC-A4224370E349"> +<bounds x="1270" y="300" width="145" height="72"/> +</OView> +<OView class="oracle.dbtools.crest.swingui.logical.TVEntity" oid="90F477EE-35D6-21A7-B693-E5724FB07476" otype="Entity" vid="B6946DC3-6424-2A37-D668-5BD36839859C"> +<bounds x="824" y="652" width="141" height="51"/> +</OView> +<OView class="oracle.dbtools.crest.swingui.logical.TVEntity" oid="9F78B73C-056D-DDEF-8C50-A9DA76B9E724" otype="Entity" vid="EEE8DCBD-05DB-E390-AE27-14DFF3B0DD56"> +<bounds x="1091" y="205" width="151" height="63"/> +</OView> +<OView class="oracle.dbtools.crest.swingui.logical.TVEntity" oid="A352A20F-310D-E285-FBC9-90DD0DA7BB9B" otype="Entity" vid="27BF1041-8402-6396-1A77-2223122117A1"> +<bounds x="292" y="570" width="148" height="51"/> +</OView> +<OView class="oracle.dbtools.crest.swingui.logical.TVEntity" oid="A6A5F317-479C-A0DD-CAAE-9DCB56B29D40" otype="Entity" vid="AB9AED98-F420-DDD6-02BA-ABA20D05AFB3"> +<bounds x="680" y="130" width="161" height="41"/> +</OView> +<OView class="oracle.dbtools.crest.swingui.logical.TVEntity" oid="B36A186B-CDB3-7851-8C38-12EA8D50EAEB" otype="Entity" vid="8B654282-58D6-084A-69E2-3C8D7E390802"> +<bounds x="600" y="30" width="141" height="31"/> +</OView> +<OView class="oracle.dbtools.crest.swingui.logical.TVEntity" oid="B82DAF9A-6F99-5CF6-4D99-A391BAD66192" otype="Entity" vid="2F2EDF15-4992-FE58-E928-D09AF0373D9E"> +<bounds x="680" y="250" width="161" height="71"/> +</OView> +<OView class="oracle.dbtools.crest.swingui.logical.TVEntity" oid="C332E3D7-638B-6CA8-24BF-383CA8659A3A" otype="Entity" vid="03B42717-C78B-007E-11B3-EEA11AABA415"> +<bounds x="472" y="570" width="136" height="51"/> +</OView> +<OView class="oracle.dbtools.crest.swingui.logical.TVEntity" oid="C79482B8-771B-FAD8-0337-163E3A45003A" otype="Entity" vid="8D1A1E0A-0651-0364-F81D-EC5D599DF29A"> +<bounds x="909" y="251" width="132" height="51"/> +</OView> +<OView class="oracle.dbtools.crest.swingui.logical.TVEntity" oid="D09E0DE5-99D6-2991-032A-A8A124F6ACBA" otype="Entity" vid="2446BDB4-EEEF-A6B8-6F46-4C1208EDECC2"> +<bounds x="1190" y="662" width="131" height="39"/> +</OView> +<OView class="oracle.dbtools.crest.swingui.TVNote" oid="D487AFDC-4027-F824-EA29-5C6D0ABB9E1E" otype="Note" vid="583B257A-5AD8-026F-84FF-AB3956387595"> +<bounds x="322" y="179" width="89" height="40"/> +</OView> +<OView class="oracle.dbtools.crest.swingui.logical.TVEntity" oid="DCC79294-5434-1DED-298C-6473DEE59FBA" otype="Entity" vid="8689850E-1426-9DCF-EF62-4753AFEE7BE6"> +<bounds x="1087" y="460" width="157" height="51"/> +</OView> +<OView class="oracle.dbtools.crest.swingui.logical.TVEntity" oid="DE366053-6F7A-7F42-ABA3-00E583098C37" otype="Entity" vid="CAF127DE-45F6-6BCE-8FAB-7BAE679347E1"> +<bounds x="430" y="250" width="131" height="71"/> +</OView> +<OView class="oracle.dbtools.crest.swingui.logical.TVEntity" oid="E93BBF08-067B-A665-39F3-CF488A6547B2" otype="Entity" vid="2862D2B6-5340-9024-1DF2-E4408EA96B6E"> +<bounds x="789" y="30" width="132" height="31"/> +</OView> +</objectViews> +<connectors> +<Connector class="oracle.dbtools.crest.swingui.logical.TVRelation" oid="01537211-CCFB-0A1E-B43B-E8C641B69471" otype="Relation" vid_source="B6946DC3-6424-2A37-D668-5BD36839859C" vid_target="62F579AD-F97F-1F92-7C5F-525AE1A2F26C"> +<lineWidth>1</lineWidth> +<points> +<point x="832" y="652"/> +<point x="832" y="471"/> +</points> +</Connector> +<Connector class="oracle.dbtools.crest.swingui.logical.TVRelation" oid="02096BBB-0795-1759-1E26-2877BE36BB59" otype="Relation" vid_source="5B100733-B921-D478-15B5-3BE9A7747A87" vid_target="5B100733-B921-D478-15B5-3BE9A7747A87"> +<lineWidth>1</lineWidth> +<points> +<point x="1135" y="676"/> +<point x="1150" y="676"/> +<point x="1150" y="742"/> +<point x="1074" y="742"/> +<point x="1074" y="727"/> +</points> +</Connector> +<Connector class="oracle.dbtools.crest.swingui.logical.TVRelation" oid="0CCF1DE3-7916-9054-BEA6-C601FF564DB2" otype="Relation" vid_source="C8DAF849-7026-3615-7FC8-4397BFC6CA14" vid_target="81A8E233-0690-CBFE-6102-F71A991903FC"> +<lineWidth>1</lineWidth> +<points> +<point x="206" y="156"/> +<point x="206" y="250"/> +</points> +</Connector> +<Connector class="oracle.dbtools.crest.swingui.logical.TVRelation" oid="10867E70-94CE-FDAF-6B6E-2742D3A49E57" otype="Relation" vid_source="5A1E3970-E7C2-5B4A-B4FC-A4224370E349" vid_target="BE78445F-B005-8F1A-E390-120DCC587063"> +<lineWidth>1</lineWidth> +<points> +<point x="1342" y="372"/> +<point x="1342" y="448"/> +</points> +</Connector> +<Connector class="oracle.dbtools.crest.swingui.logical.TVRelation" oid="11710A55-6423-1904-841A-C7D2AB8CEEBF" otype="Relation" vid_source="5B100733-B921-D478-15B5-3BE9A7747A87" vid_target="2446BDB4-EEEF-A6B8-6F46-4C1208EDECC2"> +<lineWidth>1</lineWidth> +<points> +<point x="1135" y="690"/> +<point x="1190" y="690"/> +</points> +</Connector> +<Connector class="oracle.dbtools.crest.swingui.logical.TVRelation" oid="1C189437-742B-B999-C955-7754C8ADB089" otype="Relation" vid_source="81A8E233-0690-CBFE-6102-F71A991903FC" vid_target="CAF127DE-45F6-6BCE-8FAB-7BAE679347E1"> +<lineWidth>1</lineWidth> +<points> +<point x="311" y="285"/> +<point x="430" y="285"/> +</points> +</Connector> +<Connector class="oracle.dbtools.crest.swingui.logical.TVRelation" oid="34733942-1305-4CA1-47EB-ACE724B04E69" otype="Relation" vid_source="5B100733-B921-D478-15B5-3BE9A7747A87" vid_target="ABB72A58-23E7-DF85-4B01-74F467F60284"> +<lineWidth>1</lineWidth> +<points> +<point x="1135" y="638"/> +<point x="1190" y="638"/> +</points> +</Connector> +<Connector class="oracle.dbtools.crest.swingui.logical.TVRelation" oid="3563C940-E524-7F96-7AE0-DAC3C1C17AFC" otype="Relation" vid_source="F4CED71A-65B7-151C-3ADC-26F25043F168" vid_target="B6946DC3-6424-2A37-D668-5BD36839859C"> +<lineWidth>1</lineWidth> +<points> +<point x="1167" y="371"/> +<point x="894" y="652"/> +</points> +</Connector> +<Connector class="oracle.dbtools.crest.swingui.logical.TVRelation" oid="3983F50A-EBB9-E4DE-1958-60EA4EDD6D6C" otype="Relation" vid_source="B3D29C8C-8482-D7AF-BE58-122AB07FB853" vid_target="81A8E233-0690-CBFE-6102-F71A991903FC"> +<lineWidth>1</lineWidth> +<points> +<point x="300" y="157"/> +<point x="300" y="250"/> +</points> +</Connector> +<Connector class="oracle.dbtools.crest.swingui.logical.TVRelation" oid="3B7C8913-EB6A-47B1-27D0-E2C85EE9048B" otype="Relation" vid_source="AB9AED98-F420-DDD6-02BA-ABA20D05AFB3" vid_target="8B654282-58D6-084A-69E2-3C8D7E390802"> +<lineWidth>1</lineWidth> +<points> +<point x="710" y="130"/> +<point x="710" y="61"/> +</points> +</Connector> +<Connector class="oracle.dbtools.crest.swingui.logical.TVRelation" oid="518CE489-97B4-C05C-07A2-E3DBF14EE267" otype="Relation" vid_source="8689850E-1426-9DCF-EF62-4753AFEE7BE6" vid_target="BE78445F-B005-8F1A-E390-120DCC587063"> +<lineWidth>1</lineWidth> +<points> +<point x="1244" y="474"/> +<point x="1270" y="474"/> +</points> +</Connector> +<Connector class="oracle.dbtools.crest.swingui.logical.TVRelation" oid="68A0C3E1-0FA1-8414-A361-33B08A8EDB39" otype="Relation" vid_source="8689850E-1426-9DCF-EF62-4753AFEE7BE6" vid_target="5B100733-B921-D478-15B5-3BE9A7747A87"> +<lineWidth>1</lineWidth> +<points> +<point x="1111" y="511"/> +<point x="1111" y="625"/> +</points> +</Connector> +<Connector class="oracle.dbtools.crest.swingui.logical.TVRelation" oid="7497D76B-781B-3BDD-D797-FFBDB974F772" otype="Relation" vid_source="2F2EDF15-4992-FE58-E928-D09AF0373D9E" vid_target="8D1A1E0A-0651-0364-F81D-EC5D599DF29A"> +<lineWidth>1</lineWidth> +<points> +<point x="841" y="266"/> +<point x="909" y="266"/> +</points> +</Connector> +<Connector class="oracle.dbtools.crest.swingui.logical.TVRelation" oid="7DA9DD83-A52E-CA1E-FCBF-FC9CE71AF635" otype="Relation" vid_source="5B100733-B921-D478-15B5-3BE9A7747A87" vid_target="BA629852-B837-F348-59DD-12899B260C79"> +<lineWidth>1</lineWidth> +<points> +<point x="1135" y="718"/> +<point x="1190" y="718"/> +</points> +</Connector> +<Connector class="oracle.dbtools.crest.swingui.logical.TVRelation" oid="89A83E25-364B-6B73-0613-FEAD875EF9FB" otype="Relation" vid_source="2F2EDF15-4992-FE58-E928-D09AF0373D9E" vid_target="62F579AD-F97F-1F92-7C5F-525AE1A2F26C"> +<lineWidth>1</lineWidth> +<points> +<point x="750" y="321"/> +<point x="750" y="419"/> +</points> +</Connector> +<Connector class="oracle.dbtools.crest.swingui.logical.TVRelation" oid="8E5018CC-34E3-9AFC-D6D1-31E2BC4E9FE2" otype="Relation" vid_source="03B42717-C78B-007E-11B3-EEA11AABA415" vid_target="62F579AD-F97F-1F92-7C5F-525AE1A2F26C"> +<lineWidth>1</lineWidth> +<points> +<point x="540" y="570"/> +<point x="760" y="471"/> +</points> +</Connector> +<Connector class="oracle.dbtools.crest.swingui.logical.TVRelation" oid="9B1FE0CF-B2AD-EED0-22FC-461A7D46DE51" otype="Relation" vid_source="40AB3AA2-7D9F-7BA7-AB96-050F27CF81AB" vid_target="8D1A1E0A-0651-0364-F81D-EC5D599DF29A"> +<lineWidth>1</lineWidth> +<points> +<point x="185" y="570"/> +<point x="985" y="302"/> +</points> +</Connector> +<Connector class="oracle.dbtools.crest.swingui.logical.TVRelation" oid="A182A65A-47AE-5D00-9A30-BC20AB050BF2" otype="Relation" vid_source="B6946DC3-6424-2A37-D668-5BD36839859C" vid_target="5B100733-B921-D478-15B5-3BE9A7747A87"> +<lineWidth>1</lineWidth> +<points> +<point x="965" y="677"/> +<point x="1014" y="677"/> +</points> +</Connector> +<Connector class="oracle.dbtools.crest.swingui.logical.TVRelation" oid="B346381F-48FE-E495-01A7-E22EC26AEE8A" otype="Relation" vid_source="2F2EDF15-4992-FE58-E928-D09AF0373D9E" vid_target="CAF127DE-45F6-6BCE-8FAB-7BAE679347E1"> +<lineWidth>1</lineWidth> +<points> +<point x="680" y="285"/> +<point x="561" y="285"/> +</points> +</Connector> +<Connector class="oracle.dbtools.crest.swingui.logical.TVRelation" oid="B3596116-540F-6397-ECE4-58A386644E15" otype="Relation" vid_source="2F2EDF15-4992-FE58-E928-D09AF0373D9E" vid_target="2F2EDF15-4992-FE58-E928-D09AF0373D9E"> +<lineWidth>1</lineWidth> +<points> +<point x="841" y="285"/> +<point x="856" y="285"/> +<point x="856" y="336"/> +<point x="760" y="336"/> +<point x="760" y="321"/> +</points> +</Connector> +<Connector class="oracle.dbtools.crest.swingui.logical.TVRelation" oid="BAD8EC05-6F14-4E38-366C-B4B660C6F38A" otype="Relation" vid_source="BE78445F-B005-8F1A-E390-120DCC587063" vid_target="109E2A3F-B942-1D32-CB1C-4F60260ACF5C"> +<lineWidth>1</lineWidth> +<points> +<point x="1345" y="489"/> +<point x="1345" y="522"/> +</points> +</Connector> +<Connector class="oracle.dbtools.crest.swingui.logical.TVRelation" oid="C5B67DD4-FA4F-EF9F-1FF5-0445D51B32EE" otype="Relation" vid_source="B6946DC3-6424-2A37-D668-5BD36839859C" vid_target="C8DAF849-7026-3615-7FC8-4397BFC6CA14"> +<lineWidth>1</lineWidth> +<points> +<point x="894" y="652"/> +<point x="166" y="156"/> +</points> +</Connector> +<Connector class="oracle.dbtools.crest.swingui.logical.TVRelation" oid="CCD38E11-8557-EB34-2651-07EB29E83FA6" otype="Relation" vid_source="B3D29C8C-8482-D7AF-BE58-122AB07FB853" vid_target="81A8E233-0690-CBFE-6102-F71A991903FC"> +<lineWidth>1</lineWidth> +<points> +<point x="280" y="157"/> +<point x="280" y="250"/> +</points> +</Connector> +<Connector class="oracle.dbtools.crest.swingui.logical.TVRelation" oid="E2A47942-ED55-E81D-4C71-9A134C49C147" otype="Relation" vid_source="C8DAF849-7026-3615-7FC8-4397BFC6CA14" vid_target="27BF1041-8402-6396-1A77-2223122117A1"> +<lineWidth>1</lineWidth> +<points> +<point x="166" y="156"/> +<point x="330" y="570"/> +</points> +</Connector> +<Connector class="oracle.dbtools.crest.swingui.logical.TVRelation" oid="E4FE88E9-EE21-B43B-B0FE-A153E38246F9" otype="Relation" vid_source="2F2EDF15-4992-FE58-E928-D09AF0373D9E" vid_target="AB9AED98-F420-DDD6-02BA-ABA20D05AFB3"> +<lineWidth>1</lineWidth> +<points> +<point x="760" y="250"/> +<point x="760" y="171"/> +</points> +</Connector> +<Connector class="oracle.dbtools.crest.swingui.logical.TVRelation" oid="E62AE7DF-49EE-9280-B328-A867CBD273AE" otype="Relation" vid_source="27BF1041-8402-6396-1A77-2223122117A1" vid_target="B6946DC3-6424-2A37-D668-5BD36839859C"> +<lineWidth>1</lineWidth> +<points> +<point x="360" y="621"/> +<point x="824" y="677"/> +</points> +</Connector> +<Connector class="oracle.dbtools.crest.swingui.logical.TVRelation" oid="E74406B5-20F1-4323-DC99-6E45982CB606" otype="Relation" vid_source="AB9AED98-F420-DDD6-02BA-ABA20D05AFB3" vid_target="2862D2B6-5340-9024-1DF2-E4408EA96B6E"> +<lineWidth>1</lineWidth> +<points> +<point x="815" y="130"/> +<point x="815" y="61"/> +</points> +</Connector> +<Connector class="oracle.dbtools.crest.swingui.logical.TVRelation" oid="EC4EB506-3DBE-7F36-6451-F31920EDAB52" otype="Relation" vid_source="C8DAF849-7026-3615-7FC8-4397BFC6CA14" vid_target="40AB3AA2-7D9F-7BA7-AB96-050F27CF81AB"> +<lineWidth>1</lineWidth> +<points> +<point x="130" y="156"/> +<point x="130" y="570"/> +</points> +</Connector> +<Connector class="oracle.dbtools.crest.swingui.logical.TVRelation" oid="EE1D98EF-6AEA-2790-D9B9-DBC2ED21D880" otype="Relation" vid_source="EEE8DCBD-05DB-E390-AE27-14DFF3B0DD56" vid_target="F4CED71A-65B7-151C-3ADC-26F25043F168"> +<lineWidth>1</lineWidth> +<points> +<point x="1166" y="243"/> +<point x="1167" y="301"/> +</points> +</Connector> +</connectors> +</Diagram>
\ No newline at end of file diff --git a/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/subviews/F936BE6D-7A74-1B57-7564-41C1E13B973B.xml b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/subviews/F936BE6D-7A74-1B57-7564-41C1E13B973B.xml new file mode 100644 index 00000000..bcc0009f --- /dev/null +++ b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/logical/subviews/F936BE6D-7A74-1B57-7564-41C1E13B973B.xml @@ -0,0 +1,33 @@ +<?xml version = '1.0' encoding = 'UTF-8'?> +<Diagram class="oracle.dbtools.crest.swingui.logical.DPVLogicalSubView" name="Inputs" id="F936BE6D-7A74-1B57-7564-41C1E13B973B"> +<createdBy>bird</createdBy> +<createdTime>2012-08-21 09:08:50 UTC</createdTime> +<autoRoute>false</autoRoute> +<boxInbox>true</boxInbox> +<showLegend>false</showLegend> +<showLabels>false</showLabels> +<showGrid>false</showGrid> +<diagramColor>-1</diagramColor> +<display>false</display> +<notation>0</notation> +<objectViews> +<OView class="oracle.dbtools.crest.swingui.logical.TVEntity" oid="504221DA-1B57-4EAD-39DB-40FD553E9FA2" otype="Entity" vid="EA3885E3-FEE4-031B-1751-1C6351610836"> +<bounds x="1091" y="476" width="151" height="70"/> +</OView> +<OView class="oracle.dbtools.crest.swingui.logical.TVEntity" oid="90367AFB-BA2D-A918-46B9-1E5DE53ACC48" otype="Entity" vid="86784B28-925D-6EAF-24D8-27DE22A0A93B"> +<bounds x="1090" y="376" width="151" height="68"/> +</OView> +<OView class="oracle.dbtools.crest.swingui.logical.TVEntity" oid="9F78B73C-056D-DDEF-8C50-A9DA76B9E724" otype="Entity" vid="1B62E962-0DFC-D5AE-0AC4-33E14F65E825"> +<bounds x="1297" y="477" width="151" height="71"/> +</OView> +</objectViews> +<connectors> +<Connector class="oracle.dbtools.crest.swingui.logical.TVRelation" oid="EE1D98EF-6AEA-2790-D9B9-DBC2ED21D880" otype="Relation" vid_source="1B62E962-0DFC-D5AE-0AC4-33E14F65E825" vid_target="EA3885E3-FEE4-031B-1751-1C6351610836"> +<lineWidth>1</lineWidth> +<points> +<point x="1297" y="511"/> +<point x="1242" y="511"/> +</points> +</Connector> +</connectors> +</Diagram>
\ No newline at end of file diff --git a/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/mapping/ExtendedMap.xml b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/mapping/ExtendedMap.xml new file mode 100644 index 00000000..6811f63f --- /dev/null +++ b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/mapping/ExtendedMap.xml @@ -0,0 +1,3 @@ +<?xml version = '1.0' encoding = 'UTF-8'?> +<ExtendedMap class="oracle.dbtools.crest.model.xtdmapping.ExtendedMap"> +</ExtendedMap>
\ No newline at end of file diff --git a/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/mapping/ExtendedMap_RMB082B14A-BEA8-D8A7-D661-197F34766ED3.xml b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/mapping/ExtendedMap_RMB082B14A-BEA8-D8A7-D661-197F34766ED3.xml new file mode 100644 index 00000000..7ea5df08 --- /dev/null +++ b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/mapping/ExtendedMap_RMB082B14A-BEA8-D8A7-D661-197F34766ED3.xml @@ -0,0 +1,3 @@ +<?xml version = '1.0' encoding = 'UTF-8'?> +<RMExtendedMap class="oracle.dbtools.crest.model.xtdmapping.RMExtendedMap"> +</RMExtendedMap>
\ No newline at end of file diff --git a/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/rdbms/TestManagerDatabase_RDBMSSites.xml b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/rdbms/TestManagerDatabase_RDBMSSites.xml new file mode 100644 index 00000000..e0c5dad0 --- /dev/null +++ b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/rdbms/TestManagerDatabase_RDBMSSites.xml @@ -0,0 +1,2 @@ +<?xml version = '1.0' encoding = 'UTF-8'?> +<metadatadoc version="2.0"/>
\ No newline at end of file diff --git a/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/rel/B082B14A-197F34766ED3.xml b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/rel/B082B14A-197F34766ED3.xml new file mode 100644 index 00000000..76bdad85 --- /dev/null +++ b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/rel/B082B14A-197F34766ED3.xml @@ -0,0 +1,8 @@ +<?xml version = '1.0' encoding = 'UTF-8'?> +<relationalModel class="oracle.dbtools.crest.model.design.relational.RelationalDesign" name="Relational_1" id="B082B14A-BEA8-D8A7-D661-197F34766ED3" mainViewID="6CEC5843-B4DD-D9B0-54D4-2845569D5E9F"> +<createdBy>bird</createdBy> +<createdTime>2012-08-20 21:58:45 UTC</createdTime> +<ownerDesignName>TestManagerDatabase</ownerDesignName> +<shouldBeOpen>false</shouldBeOpen> +<selectedRDBMSSite>32076570-2523-435C-2E92-BF29817DFF70</selectedRDBMSSite> +</relationalModel>
\ No newline at end of file diff --git a/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/rel/B082B14A-197F34766ED3/subviews/6CEC5843-B4DD-D9B0-54D4-2845569D5E9F.xml b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/rel/B082B14A-197F34766ED3/subviews/6CEC5843-B4DD-D9B0-54D4-2845569D5E9F.xml new file mode 100644 index 00000000..44b040be --- /dev/null +++ b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/rel/B082B14A-197F34766ED3/subviews/6CEC5843-B4DD-D9B0-54D4-2845569D5E9F.xml @@ -0,0 +1,13 @@ +<?xml version = '1.0' encoding = 'UTF-8'?> +<Diagram class="oracle.dbtools.crest.swingui.relational.DPVRelational" name="Relational_1" id="6CEC5843-B4DD-D9B0-54D4-2845569D5E9F"> +<createdBy>bird</createdBy> +<createdTime>2012-08-20 22:02:17 UTC</createdTime> +<autoRoute>false</autoRoute> +<boxInbox>true</boxInbox> +<showLegend>false</showLegend> +<showLabels>false</showLabels> +<showGrid>false</showGrid> +<diagramColor>-1</diagramColor> +<display>false</display> +<notation>0</notation> +</Diagram>
\ No newline at end of file diff --git a/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/types.xml b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/types.xml new file mode 100644 index 00000000..64fa7ab8 --- /dev/null +++ b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabase/types.xml @@ -0,0 +1,933 @@ +<?xml version="1.0" encoding="UTF-8" ?> +<logtypes> + <logicaltype name="Audio" objectid="LOGDT005"> + <mapping rdbms="Oracle Database 11g">BLOB</mapping> + <mapping rdbms="Oracle Database 10g">BLOB</mapping> + <mapping rdbms="Oracle9i">BLOB</mapping> + <mapping rdbms="SQL Server 2005">BINARY, size</mapping> + <mapping rdbms="SQL Server 2000">BINARY, size</mapping> + <mapping rdbms="DB2/390 8">BLOB, size</mapping> + <mapping rdbms="DB2/390 7">BLOB, size</mapping> + <mapping rdbms="DB2/UDB 8.1">BLOB, size</mapping> + <mapping rdbms="DB2/UDB 7.1">BLOB, size</mapping> + </logicaltype> + <logicaltype name="BFile" objectid="LogDes-7DD553FD-11E8-61FA-399D-2E531FB621D0@LOGDT10034"> + <mapping rdbms="Oracle Database 11g">BFILE</mapping> + <mapping rdbms="Oracle Database 10g">BFILE</mapping> + <mapping rdbms="Oracle9i">BFILE</mapping> + <mapping rdbms="SQL Server 2005">VARCHAR, size</mapping> + <mapping rdbms="SQL Server 2000">VARCHAR, size</mapping> + <mapping rdbms="DB2/390 8">VARCHAR, size</mapping> + <mapping rdbms="DB2/390 7">VARCHAR, size</mapping> + <mapping rdbms="DB2/UDB 8.1">DATALINK</mapping> + <mapping rdbms="DB2/UDB 7.1">DATALINK</mapping> + </logicaltype> + <logicaltype name="BIGINT" objectid="LOGDT027"> + <mapping rdbms="Oracle Database 11g">INTEGER</mapping> + <mapping rdbms="Oracle Database 10g">INTEGER</mapping> + <mapping rdbms="Oracle9i">INTEGER</mapping> + <mapping rdbms="SQL Server 2005">BIGINT</mapping> + <mapping rdbms="SQL Server 2000">BIGINT</mapping> + <mapping rdbms="DB2/390 8">INTEGER</mapping> + <mapping rdbms="DB2/390 7">INTEGER</mapping> + <mapping rdbms="DB2/UDB 8.1">INTEGER</mapping> + <mapping rdbms="DB2/UDB 7.1">INTEGER</mapping> + </logicaltype> + <logicaltype name="BINARY" objectid="LOGDT033"> + <mapping rdbms="Oracle Database 11g">BLOB</mapping> + <mapping rdbms="Oracle Database 10g">BLOB</mapping> + <mapping rdbms="Oracle9i">BLOB</mapping> + <mapping rdbms="SQL Server 2005">BINARY, size</mapping> + <mapping rdbms="SQL Server 2000">BINARY, size</mapping> + <mapping rdbms="DB2/390 8">BLOB, size</mapping> + <mapping rdbms="DB2/390 7">BLOB, size</mapping> + <mapping rdbms="DB2/UDB 8.1">BLOB, size</mapping> + <mapping rdbms="DB2/UDB 7.1">BLOB, size</mapping> + </logicaltype> + <logicaltype name="BINARY DOUBLE" objectid="LOGDT056"> + <mapping rdbms="Oracle Database 11g">BINARY_DOUBLE</mapping> + <mapping rdbms="Oracle Database 10g">BINARY_DOUBLE</mapping> + <mapping rdbms="Oracle9i">NUMBER</mapping> + <mapping rdbms="SQL Server 2005">FLOAT</mapping> + <mapping rdbms="SQL Server 2000">FLOAT</mapping> + <mapping rdbms="DB2/390 8">DOUBLE</mapping> + <mapping rdbms="DB2/390 7">DOUBLE</mapping> + <mapping rdbms="DB2/UDB 8.1">DOUBLE</mapping> + <mapping rdbms="DB2/UDB 7.1">DOUBLE</mapping> + </logicaltype> + <logicaltype name="BINARY FLOAT" objectid="LOGDT055"> + <mapping rdbms="Oracle Database 11g">BINARY_FLOAT</mapping> + <mapping rdbms="Oracle Database 10g">BINARY_FLOAT</mapping> + <mapping rdbms="Oracle9i">NUMBER</mapping> + <mapping rdbms="SQL Server 2005">REAL</mapping> + <mapping rdbms="SQL Server 2000">REAL</mapping> + <mapping rdbms="DB2/390 8">REAL</mapping> + <mapping rdbms="DB2/390 7">REAL</mapping> + <mapping rdbms="DB2/UDB 8.1">REAL</mapping> + <mapping rdbms="DB2/UDB 7.1">REAL</mapping> + </logicaltype> + <logicaltype name="BIT" objectid="LOGDT034"> + <mapping rdbms="Oracle Database 11g">CHAR</mapping> + <mapping rdbms="Oracle Database 10g">CHAR</mapping> + <mapping rdbms="Oracle9i">CHAR</mapping> + <mapping rdbms="SQL Server 2005">BIT</mapping> + <mapping rdbms="SQL Server 2000">BIT</mapping> + <mapping rdbms="DB2/390 8">CHAR, size</mapping> + <mapping rdbms="DB2/390 7">CHAR, size</mapping> + <mapping rdbms="DB2/UDB 8.1">CHAR, size</mapping> + <mapping rdbms="DB2/UDB 7.1">CHAR, size</mapping> + </logicaltype> + <logicaltype name="BLOB" objectid="LOGDT029"> + <mapping rdbms="Oracle Database 11g">BLOB</mapping> + <mapping rdbms="Oracle Database 10g">BLOB</mapping> + <mapping rdbms="Oracle9i">BLOB</mapping> + <mapping rdbms="SQL Server 2005">IMAGE</mapping> + <mapping rdbms="SQL Server 2000">IMAGE</mapping> + <mapping rdbms="DB2/390 8">BLOB, size</mapping> + <mapping rdbms="DB2/390 7">BLOB, size</mapping> + <mapping rdbms="DB2/UDB 8.1">BLOB, size</mapping> + <mapping rdbms="DB2/UDB 7.1">BLOB, size</mapping> + </logicaltype> + <logicaltype name="Boolean" objectid="LOGDT006"> + <mapping rdbms="Oracle Database 11g">CHAR</mapping> + <mapping rdbms="Oracle Database 10g">CHAR</mapping> + <mapping rdbms="Oracle9i">CHAR</mapping> + <mapping rdbms="SQL Server 2005">BIT</mapping> + <mapping rdbms="SQL Server 2000">BIT</mapping> + <mapping rdbms="DB2/390 8">CHAR</mapping> + <mapping rdbms="DB2/390 7">CHAR</mapping> + <mapping rdbms="DB2/UDB 8.1">CHAR</mapping> + <mapping rdbms="DB2/UDB 7.1">CHAR</mapping> + </logicaltype> + <logicaltype name="CHAR" objectid="LOGDT025"> + <mapping rdbms="Oracle Database 11g">CHAR, size</mapping> + <mapping rdbms="Oracle Database 10g">CHAR, size</mapping> + <mapping rdbms="Oracle9i">CHAR, size</mapping> + <mapping rdbms="SQL Server 2005">CHAR, size</mapping> + <mapping rdbms="SQL Server 2000">CHAR, size</mapping> + <mapping rdbms="DB2/390 8">CHAR, size</mapping> + <mapping rdbms="DB2/390 7">CHAR, size</mapping> + <mapping rdbms="DB2/UDB 8.1">CHAR, size</mapping> + <mapping rdbms="DB2/UDB 7.1">CHAR, size</mapping> + </logicaltype> + <logicaltype name="CLOB" objectid="LOGDT028"> + <mapping rdbms="Oracle Database 11g">CLOB</mapping> + <mapping rdbms="Oracle Database 10g">CLOB</mapping> + <mapping rdbms="Oracle9i">CLOB</mapping> + <mapping rdbms="SQL Server 2005" size_default_value="max">VARCHAR, size</mapping> + <mapping rdbms="SQL Server 2000">TEXT</mapping> + <mapping rdbms="DB2/390 8">CLOB, size</mapping> + <mapping rdbms="DB2/390 7">CLOB, size</mapping> + <mapping rdbms="DB2/UDB 8.1">CLOB, size</mapping> + <mapping rdbms="DB2/UDB 7.1">CLOB, size</mapping> + </logicaltype> + <logicaltype name="DATALINK" objectid="LOGDT030"> + <mapping rdbms="Oracle Database 11g">BLOB</mapping> + <mapping rdbms="Oracle Database 10g">BLOB</mapping> + <mapping rdbms="Oracle9i">BLOB</mapping> + <mapping rdbms="SQL Server 2005">BINARY</mapping> + <mapping rdbms="SQL Server 2000">BINARY</mapping> + <mapping rdbms="DB2/390 8">VARCHAR, size</mapping> + <mapping rdbms="DB2/390 7">VARCHAR, size</mapping> + <mapping rdbms="DB2/UDB 8.1">DATALINK</mapping> + <mapping rdbms="DB2/UDB 7.1">DATALINK</mapping> + </logicaltype> + <logicaltype name="DBURIType" objectid="LOGDT054"> + <mapping rdbms="Oracle Database 11g">DBURITYPE</mapping> + <mapping rdbms="Oracle Database 10g">DBURITYPE</mapping> + <mapping rdbms="Oracle9i">DBURITYPE</mapping> + <mapping rdbms="SQL Server 2005">CHAR, size</mapping> + <mapping rdbms="SQL Server 2000">CHAR, size</mapping> + <mapping rdbms="DB2/390 8">CHAR, size</mapping> + <mapping rdbms="DB2/390 7">CHAR, size</mapping> + <mapping rdbms="DB2/UDB 8.1">DATALINK</mapping> + <mapping rdbms="DB2/UDB 7.1">DATALINK</mapping> + </logicaltype> + <logicaltype name="DECIMAL" objectid="LOGDT026"> + <mapping rdbms="Oracle Database 11g">NUMBER, precision, scale</mapping> + <mapping rdbms="Oracle Database 10g">NUMBER, precision, scale</mapping> + <mapping rdbms="Oracle9i">NUMBER, precision, scale</mapping> + <mapping rdbms="SQL Server 2005">DECIMAL, precision, scale</mapping> + <mapping rdbms="SQL Server 2000">DECIMAL, precision, scale</mapping> + <mapping rdbms="DB2/390 8">DECIMAL, precision, scale</mapping> + <mapping rdbms="DB2/390 7">DECIMAL, precision, scale</mapping> + <mapping rdbms="DB2/UDB 8.1">DECIMAL, precision, scale</mapping> + <mapping rdbms="DB2/UDB 7.1">DECIMAL, precision, scale</mapping> + </logicaltype> + <logicaltype name="DOUBLE" objectid="LOGDT020"> + <mapping rdbms="Oracle Database 11g">NUMBER</mapping> + <mapping rdbms="Oracle Database 10g">NUMBER</mapping> + <mapping rdbms="Oracle9i">NUMBER</mapping> + <mapping rdbms="SQL Server 2005">BIGINT</mapping> + <mapping rdbms="SQL Server 2000">BIGINT</mapping> + <mapping rdbms="DB2/390 8">DOUBLE</mapping> + <mapping rdbms="DB2/390 7">DOUBLE</mapping> + <mapping rdbms="DB2/UDB 8.1">DOUBLE</mapping> + <mapping rdbms="DB2/UDB 7.1">DOUBLE</mapping> + </logicaltype> + <logicaltype name="Date" objectid="LOGDT007"> + <mapping rdbms="Oracle Database 11g">DATE</mapping> + <mapping rdbms="Oracle Database 10g">DATE</mapping> + <mapping rdbms="Oracle9i">DATE</mapping> + <mapping rdbms="SQL Server 2005">DATETIME</mapping> + <mapping rdbms="SQL Server 2000">DATETIME</mapping> + <mapping rdbms="DB2/390 8">DATE</mapping> + <mapping rdbms="DB2/390 7">DATE</mapping> + <mapping rdbms="DB2/UDB 8.1">DATE</mapping> + <mapping rdbms="DB2/UDB 7.1">DATE</mapping> + </logicaltype> + <logicaltype name="Datetime" objectid="LOGDT008"> + <mapping rdbms="Oracle Database 11g">DATE</mapping> + <mapping rdbms="Oracle Database 10g">DATE</mapping> + <mapping rdbms="Oracle9i">DATE</mapping> + <mapping rdbms="SQL Server 2005">DATETIME</mapping> + <mapping rdbms="SQL Server 2000">DATETIME</mapping> + <mapping rdbms="DB2/390 8">TIMESTAMP</mapping> + <mapping rdbms="DB2/390 7">TIMESTAMP</mapping> + <mapping rdbms="DB2/UDB 8.1">TIMESTAMP</mapping> + <mapping rdbms="DB2/UDB 7.1">TIMESTAMP</mapping> + </logicaltype> + <logicaltype name="FLOAT" objectid="LOGDT021"> + <mapping rdbms="Oracle Database 11g">FLOAT, precision</mapping> + <mapping rdbms="Oracle Database 10g">FLOAT, precision</mapping> + <mapping rdbms="Oracle9i">FLOAT, precision</mapping> + <mapping rdbms="SQL Server 2005">FLOAT, precision</mapping> + <mapping rdbms="SQL Server 2000">FLOAT, precision</mapping> + <mapping rdbms="DB2/390 8">FLOAT, precision</mapping> + <mapping rdbms="DB2/390 7">FLOAT, precision</mapping> + <mapping rdbms="DB2/UDB 8.1">FLOAT, precision</mapping> + <mapping rdbms="DB2/UDB 7.1">FLOAT, precision</mapping> + </logicaltype> + <logicaltype name="GRAPHIC" objectid="LOGDT031"> + <mapping rdbms="Oracle Database 11g">BLOB</mapping> + <mapping rdbms="Oracle Database 10g">BLOB</mapping> + <mapping rdbms="Oracle9i">BLOB</mapping> + <mapping rdbms="SQL Server 2005">BINARY</mapping> + <mapping rdbms="SQL Server 2000">BINARY</mapping> + <mapping rdbms="DB2/390 8">GRAPHIC, size</mapping> + <mapping rdbms="DB2/390 7">GRAPHIC, size</mapping> + <mapping rdbms="DB2/UDB 8.1">GRAPHIC, size</mapping> + <mapping rdbms="DB2/UDB 7.1">GRAPHIC, size</mapping> + </logicaltype> + <logicaltype name="HTTPURIType" objectid="LOGDT052"> + <mapping rdbms="Oracle Database 11g">HTTPURITYPE</mapping> + <mapping rdbms="Oracle Database 10g">HTTPURITYPE</mapping> + <mapping rdbms="Oracle9i">HTTPURITYPE</mapping> + <mapping rdbms="SQL Server 2005">CHAR, size</mapping> + <mapping rdbms="SQL Server 2000">CHAR, size</mapping> + <mapping rdbms="DB2/390 8">CHAR, size</mapping> + <mapping rdbms="DB2/390 7">CHAR, size</mapping> + <mapping rdbms="DB2/UDB 8.1">CHAR, size</mapping> + <mapping rdbms="DB2/UDB 7.1">CHAR, size</mapping> + </logicaltype> + <logicaltype name="INTERVAL DAY TO SECOND" objectid="LOGDT049"> + <mapping rdbms="Oracle Database 11g">INTERVAL DAY TO SECOND, precision, scale</mapping> + <mapping rdbms="Oracle Database 10g">INTERVAL DAY TO SECOND, precision, scale</mapping> + <mapping rdbms="Oracle9i">INTERVAL DAY TO SECOND, precision, scale</mapping> + <mapping rdbms="SQL Server 2005">CHAR, size</mapping> + <mapping rdbms="SQL Server 2000">CHAR, size</mapping> + <mapping rdbms="DB2/390 8">CHAR, size</mapping> + <mapping rdbms="DB2/390 7">CHAR, size</mapping> + <mapping rdbms="DB2/UDB 8.1">CHAR, size</mapping> + <mapping rdbms="DB2/UDB 7.1">CHAR, size</mapping> + </logicaltype> + <logicaltype name="INTERVAL YEAR TO MONTH" objectid="LOGDT048"> + <mapping rdbms="Oracle Database 11g">INTERVAL YEAR TO MONTH, precision</mapping> + <mapping rdbms="Oracle Database 10g">INTERVAL YEAR TO MONTH, precision</mapping> + <mapping rdbms="Oracle9i">INTERVAL YEAR TO MONTH, precision</mapping> + <mapping rdbms="SQL Server 2005">CHAR, size</mapping> + <mapping rdbms="SQL Server 2000">CHAR, size</mapping> + <mapping rdbms="DB2/390 8">CHAR, size</mapping> + <mapping rdbms="DB2/390 7">CHAR, size</mapping> + <mapping rdbms="DB2/UDB 8.1">CHAR, size</mapping> + <mapping rdbms="DB2/UDB 7.1">CHAR, size</mapping> + </logicaltype> + <logicaltype name="Image" objectid="LOGDT010"> + <mapping rdbms="Oracle Database 11g">BLOB</mapping> + <mapping rdbms="Oracle Database 10g">BLOB</mapping> + <mapping rdbms="Oracle9i">BLOB</mapping> + <mapping rdbms="SQL Server 2005">IMAGE</mapping> + <mapping rdbms="SQL Server 2000">IMAGE</mapping> + <mapping rdbms="DB2/390 8">BLOB, size</mapping> + <mapping rdbms="DB2/390 7">BLOB, size</mapping> + <mapping rdbms="DB2/UDB 8.1">BLOB, size</mapping> + <mapping rdbms="DB2/UDB 7.1">BLOB, size</mapping> + </logicaltype> + <logicaltype name="Integer" objectid="LOGDT011"> + <mapping rdbms="Oracle Database 11g">INTEGER</mapping> + <mapping rdbms="Oracle Database 10g">INTEGER</mapping> + <mapping rdbms="Oracle9i">INTEGER</mapping> + <mapping rdbms="SQL Server 2005">INTEGER</mapping> + <mapping rdbms="SQL Server 2000">INTEGER</mapping> + <mapping rdbms="DB2/390 8">INTEGER</mapping> + <mapping rdbms="DB2/390 7">INTEGER</mapping> + <mapping rdbms="DB2/UDB 8.1">INTEGER</mapping> + <mapping rdbms="DB2/UDB 7.1">INTEGER</mapping> + </logicaltype> + <logicaltype name="Long Char" objectid="LogDes-1768A872-F385-FDBA-D95E-0CB63F5908E2@LOGDT10045"> + <mapping rdbms="Oracle Database 11g">LONG</mapping> + <mapping rdbms="Oracle Database 10g">LONG</mapping> + <mapping rdbms="Oracle9i">LONG</mapping> + <mapping rdbms="SQL Server 2005">VARCHAR, size</mapping> + <mapping rdbms="SQL Server 2000">VARCHAR, size</mapping> + <mapping rdbms="DB2/390 8">CLOB, size</mapping> + <mapping rdbms="DB2/390 7">CLOB, size</mapping> + <mapping rdbms="DB2/UDB 8.1">CLOB, size</mapping> + <mapping rdbms="DB2/UDB 7.1">CLOB, size</mapping> + </logicaltype> + <logicaltype name="Long_Raw" objectid="LogDes-7DD553FD-11E8-61FA-399D-2E531FB621D0@LOGDT10036"> + <mapping rdbms="Oracle Database 11g">LONG RAW</mapping> + <mapping rdbms="Oracle Database 10g">LONG RAW</mapping> + <mapping rdbms="Oracle9i">LONG RAW</mapping> + <mapping rdbms="SQL Server 2005">VARBINARY, size</mapping> + <mapping rdbms="SQL Server 2000">VARBINARY, size</mapping> + <mapping rdbms="DB2/390 8">BLOB, size</mapping> + <mapping rdbms="DB2/390 7">BLOB, size</mapping> + <mapping rdbms="DB2/UDB 8.1">BLOB, size</mapping> + <mapping rdbms="DB2/UDB 7.1">BLOB, size</mapping> + </logicaltype> + <logicaltype name="MONEY" objectid="LOGDT043"> + <mapping rdbms="Oracle Database 11g">NUMBER, precision, scale</mapping> + <mapping rdbms="Oracle Database 10g">NUMBER, precision, scale</mapping> + <mapping rdbms="Oracle9i">NUMBER, precision, scale</mapping> + <mapping rdbms="SQL Server 2005">MONEY</mapping> + <mapping rdbms="SQL Server 2000">MONEY</mapping> + <mapping rdbms="DB2/390 8">DOUBLE</mapping> + <mapping rdbms="DB2/390 7">DOUBLE</mapping> + <mapping rdbms="DB2/UDB 8.1">DOUBLE</mapping> + <mapping rdbms="DB2/UDB 7.1">DOUBLE</mapping> + </logicaltype> + <logicaltype name="NCHAR" objectid="LOGDT035"> + <mapping rdbms="Oracle Database 11g">NCHAR, size</mapping> + <mapping rdbms="Oracle Database 10g">NCHAR, size</mapping> + <mapping rdbms="Oracle9i">NCHAR, size</mapping> + <mapping rdbms="SQL Server 2005">NCHAR, size</mapping> + <mapping rdbms="SQL Server 2000">NCHAR, size</mapping> + <mapping rdbms="DB2/390 8">CHAR, size</mapping> + <mapping rdbms="DB2/390 7">CHAR, size</mapping> + <mapping rdbms="DB2/UDB 8.1">CHAR, size</mapping> + <mapping rdbms="DB2/UDB 7.1">CHAR, size</mapping> + </logicaltype> + <logicaltype name="NClob" objectid="LogDes-7DD553FD-11E8-61FA-399D-2E531FB621D0@LOGDT10035"> + <mapping rdbms="Oracle Database 11g">NCLOB</mapping> + <mapping rdbms="Oracle Database 10g">NCLOB</mapping> + <mapping rdbms="Oracle9i">NCLOB</mapping> + <mapping rdbms="SQL Server 2005">NTEXT</mapping> + <mapping rdbms="SQL Server 2000">NTEXT</mapping> + <mapping rdbms="DB2/390 8">CLOB, size</mapping> + <mapping rdbms="DB2/390 7">CLOB, size</mapping> + <mapping rdbms="DB2/UDB 8.1">CLOB, size</mapping> + <mapping rdbms="DB2/UDB 7.1">CLOB, size</mapping> + </logicaltype> + <logicaltype name="NTEXT" objectid="LOGDT036"> + <mapping rdbms="Oracle Database 11g">NCLOB</mapping> + <mapping rdbms="Oracle Database 10g">NCLOB</mapping> + <mapping rdbms="Oracle9i">NCLOB</mapping> + <mapping rdbms="SQL Server 2005">NTEXT</mapping> + <mapping rdbms="SQL Server 2000">NTEXT</mapping> + <mapping rdbms="DB2/390 8">CLOB, size</mapping> + <mapping rdbms="DB2/390 7">CLOB, size</mapping> + <mapping rdbms="DB2/UDB 8.1">CLOB, size</mapping> + <mapping rdbms="DB2/UDB 7.1">CLOB, size</mapping> + </logicaltype> + <logicaltype name="NUMERIC" objectid="LOGDT019"> + <mapping rdbms="Oracle Database 11g">NUMBER, precision, scale</mapping> + <mapping rdbms="Oracle Database 10g">NUMBER, precision, scale</mapping> + <mapping rdbms="Oracle9i">NUMBER, precision, scale</mapping> + <mapping rdbms="SQL Server 2005">NUMERIC, precision, scale</mapping> + <mapping rdbms="SQL Server 2000">NUMERIC, precision, scale</mapping> + <mapping rdbms="DB2/390 8">NUMERIC, precision, scale</mapping> + <mapping rdbms="DB2/390 7">NUMERIC, precision, scale</mapping> + <mapping rdbms="DB2/UDB 8.1">NUMERIC, precision, scale</mapping> + <mapping rdbms="DB2/UDB 7.1">NUMERIC, precision, scale</mapping> + </logicaltype> + <logicaltype name="NVARCHAR" objectid="LOGDT037"> + <mapping rdbms="Oracle Database 11g">NVARCHAR2, size</mapping> + <mapping rdbms="Oracle Database 10g">NVARCHAR2, size</mapping> + <mapping rdbms="Oracle9i">NVARCHAR2, size</mapping> + <mapping rdbms="SQL Server 2005">NVARCHAR, size</mapping> + <mapping rdbms="SQL Server 2000">NVARCHAR, size</mapping> + <mapping rdbms="DB2/390 8">VARCHAR, size</mapping> + <mapping rdbms="DB2/390 7">VARCHAR, size</mapping> + <mapping rdbms="DB2/UDB 8.1">VARCHAR, size</mapping> + <mapping rdbms="DB2/UDB 7.1">VARCHAR, size</mapping> + </logicaltype> + <logicaltype name="ORDAUDIO" objectid="LogDes-4972B6D2-6F93-8AE5-6E24-3599E65A7CFE@LOGDT10005"> + <mapping rdbms="Oracle Database 11g">ORDSYS.ORDAudio</mapping> + <mapping rdbms="Oracle Database 10g">ORDSYS.ORDAudio</mapping> + <mapping rdbms="Oracle9i">ORDSYS.ORDAudio</mapping> + <mapping rdbms="SQL Server 2005">UNKNOWN</mapping> + <mapping rdbms="SQL Server 2000">UNKNOWN</mapping> + <mapping rdbms="DB2/390 8">UNKNOWN</mapping> + <mapping rdbms="DB2/390 7">UNKNOWN</mapping> + <mapping rdbms="DB2/UDB 8.1">UNKNOWN</mapping> + <mapping rdbms="DB2/UDB 7.1">UNKNOWN</mapping> + </logicaltype> + <logicaltype name="ORDDOC" objectid="LogDes-4972B6D2-6F93-8AE5-6E24-3599E65A7CFE@LOGDT10009"> + <mapping rdbms="Oracle Database 11g">ORDSYS.ORDDoc</mapping> + <mapping rdbms="Oracle Database 10g">ORDSYS.ORDDoc</mapping> + <mapping rdbms="Oracle9i">ORDSYS.ORDDoc</mapping> + <mapping rdbms="SQL Server 2005">UNKNOWN</mapping> + <mapping rdbms="SQL Server 2000">UNKNOWN</mapping> + <mapping rdbms="DB2/390 8">UNKNOWN</mapping> + <mapping rdbms="DB2/390 7">UNKNOWN</mapping> + <mapping rdbms="DB2/UDB 8.1">UNKNOWN</mapping> + <mapping rdbms="DB2/UDB 7.1">UNKNOWN</mapping> + </logicaltype> + <logicaltype name="ORDIMAGE" objectid="LogDes-4972B6D2-6F93-8AE5-6E24-3599E65A7CFE@LOGDT10006"> + <mapping rdbms="Oracle Database 11g">ORDSYS.ORDImage</mapping> + <mapping rdbms="Oracle Database 10g">ORDSYS.ORDImage</mapping> + <mapping rdbms="Oracle9i">ORDSYS.ORDImage</mapping> + <mapping rdbms="SQL Server 2005">UNKNOWN</mapping> + <mapping rdbms="SQL Server 2000">UNKNOWN</mapping> + <mapping rdbms="DB2/390 8">UNKNOWN</mapping> + <mapping rdbms="DB2/390 7">UNKNOWN</mapping> + <mapping rdbms="DB2/UDB 8.1">UNKNOWN</mapping> + <mapping rdbms="DB2/UDB 7.1">UNKNOWN</mapping> + </logicaltype> + <logicaltype name="ORDIMAGE_SIGNATURE" objectid="LogDes-4972B6D2-6F93-8AE5-6E24-3599E65A7CFE@LOGDT10007"> + <mapping rdbms="Oracle Database 11g">ORDSYS.ORDImageSignature</mapping> + <mapping rdbms="Oracle Database 10g">ORDSYS.ORDImageSignature</mapping> + <mapping rdbms="Oracle9i">ORDSYS.ORDImageSignature</mapping> + <mapping rdbms="SQL Server 2005">UNKNOWN</mapping> + <mapping rdbms="SQL Server 2000">UNKNOWN</mapping> + <mapping rdbms="DB2/390 8">UNKNOWN</mapping> + <mapping rdbms="DB2/390 7">UNKNOWN</mapping> + <mapping rdbms="DB2/UDB 8.1">UNKNOWN</mapping> + <mapping rdbms="DB2/UDB 7.1">UNKNOWN</mapping> + </logicaltype> + <logicaltype name="ORDVIDEO" objectid="LogDes-4972B6D2-6F93-8AE5-6E24-3599E65A7CFE@LOGDT10008"> + <mapping rdbms="Oracle Database 11g">ORDSYS.ORDVideo</mapping> + <mapping rdbms="Oracle Database 10g">ORDSYS.ORDVideo</mapping> + <mapping rdbms="Oracle9i">ORDSYS.ORDVideo</mapping> + <mapping rdbms="SQL Server 2005">UNKNOWN</mapping> + <mapping rdbms="SQL Server 2000">UNKNOWN</mapping> + <mapping rdbms="DB2/390 8">UNKNOWN</mapping> + <mapping rdbms="DB2/390 7">UNKNOWN</mapping> + <mapping rdbms="DB2/UDB 8.1">UNKNOWN</mapping> + <mapping rdbms="DB2/UDB 7.1">UNKNOWN</mapping> + </logicaltype> + <logicaltype name="REAL" objectid="LOGDT022"> + <mapping rdbms="Oracle Database 11g">REAL</mapping> + <mapping rdbms="Oracle Database 10g">REAL</mapping> + <mapping rdbms="Oracle9i">REAL</mapping> + <mapping rdbms="SQL Server 2005">REAL, precision</mapping> + <mapping rdbms="SQL Server 2000">REAL, precision</mapping> + <mapping rdbms="DB2/390 8">REAL</mapping> + <mapping rdbms="DB2/390 7">REAL</mapping> + <mapping rdbms="DB2/UDB 8.1">REAL</mapping> + <mapping rdbms="DB2/UDB 7.1">REAL</mapping> + </logicaltype> + <logicaltype name="ROWID" objectid="LOGDT032"> + <mapping rdbms="Oracle Database 11g">ROWID</mapping> + <mapping rdbms="Oracle Database 10g">ROWID</mapping> + <mapping rdbms="Oracle9i">ROWID</mapping> + <mapping rdbms="SQL Server 2005">CHAR, size</mapping> + <mapping rdbms="SQL Server 2000">CHAR, size</mapping> + <mapping rdbms="DB2/390 8">ROWID</mapping> + <mapping rdbms="DB2/390 7">ROWID</mapping> + <mapping rdbms="DB2/UDB 8.1">CHAR, size</mapping> + <mapping rdbms="DB2/UDB 7.1">CHAR, size</mapping> + </logicaltype> + <logicaltype name="Raw" objectid="LogDes-4BABEC65-108B-2A3C-F7C4-84AC47D292B0@LOGDT10040"> + <mapping rdbms="Oracle Database 11g">RAW, size</mapping> + <mapping rdbms="Oracle Database 10g">RAW, size</mapping> + <mapping rdbms="Oracle9i">RAW, size</mapping> + <mapping rdbms="SQL Server 2005">VARBINARY, size</mapping> + <mapping rdbms="SQL Server 2000">VARBINARY, size</mapping> + <mapping rdbms="DB2/390 8">VARGRAPHIC, size</mapping> + <mapping rdbms="DB2/390 7">VARGRAPHIC, size</mapping> + <mapping rdbms="DB2/UDB 8.1">VARGRAPHIC, size</mapping> + <mapping rdbms="DB2/UDB 7.1">VARGRAPHIC, size</mapping> + </logicaltype> + <logicaltype name="SMALLDATETIME" objectid="LOGDT038"> + <mapping rdbms="Oracle Database 11g">DATE</mapping> + <mapping rdbms="Oracle Database 10g">DATE</mapping> + <mapping rdbms="Oracle9i">DATE</mapping> + <mapping rdbms="SQL Server 2005">SMALLDATETIME</mapping> + <mapping rdbms="SQL Server 2000">SMALLDATETIME</mapping> + <mapping rdbms="DB2/390 8">TIMESTAMP</mapping> + <mapping rdbms="DB2/390 7">TIMESTAMP</mapping> + <mapping rdbms="DB2/UDB 8.1">TIMESTAMP</mapping> + <mapping rdbms="DB2/UDB 7.1">TIMESTAMP</mapping> + </logicaltype> + <logicaltype name="SMALLINT" objectid="LOGDT018"> + <mapping rdbms="Oracle Database 11g">SMALLINT</mapping> + <mapping rdbms="Oracle Database 10g">SMALLINT</mapping> + <mapping rdbms="Oracle9i">SMALLINT</mapping> + <mapping rdbms="SQL Server 2005">SMALLINT</mapping> + <mapping rdbms="SQL Server 2000">SMALLINT</mapping> + <mapping rdbms="DB2/390 8">SMALLINT</mapping> + <mapping rdbms="DB2/390 7">SMALLINT</mapping> + <mapping rdbms="DB2/UDB 8.1">SMALLINT</mapping> + <mapping rdbms="DB2/UDB 7.1">SMALLINT</mapping> + </logicaltype> + <logicaltype name="SMALLMONEY" objectid="LOGDT044"> + <mapping rdbms="Oracle Database 11g">NUMBER, precision, scale</mapping> + <mapping rdbms="Oracle Database 10g">NUMBER, precision, scale</mapping> + <mapping rdbms="Oracle9i">NUMBER, precision, scale</mapping> + <mapping rdbms="SQL Server 2005">SMALLMONEY</mapping> + <mapping rdbms="SQL Server 2000">SMALLMONEY</mapping> + <mapping rdbms="DB2/390 8">REAL</mapping> + <mapping rdbms="DB2/390 7">REAL</mapping> + <mapping rdbms="DB2/UDB 8.1">REAL</mapping> + <mapping rdbms="DB2/UDB 7.1">REAL</mapping> + </logicaltype> + <logicaltype name="SQL_VARIANT" objectid="LOGDT045"> + <mapping rdbms="Oracle Database 11g">SYS.ANYDATA</mapping> + <mapping rdbms="Oracle Database 10g">SYS.ANYDATA</mapping> + <mapping rdbms="Oracle9i">SYS.ANYDATA</mapping> + <mapping rdbms="SQL Server 2005">SQL_VARIANT</mapping> + <mapping rdbms="SQL Server 2000">SQL_VARIANT</mapping> + <mapping rdbms="DB2/390 8">UNKNOWN</mapping> + <mapping rdbms="DB2/390 7">UNKNOWN</mapping> + <mapping rdbms="DB2/UDB 8.1">UNKNOWN</mapping> + <mapping rdbms="DB2/UDB 7.1">UNKNOWN</mapping> + </logicaltype> + <logicaltype name="SYSNAME" objectid="LOGDT039"> + <mapping rdbms="Oracle Database 11g">VARCHAR2, size</mapping> + <mapping rdbms="Oracle Database 10g">VARCHAR2, size</mapping> + <mapping rdbms="Oracle9i">VARCHAR2, size</mapping> + <mapping rdbms="SQL Server 2005">SYSNAME</mapping> + <mapping rdbms="SQL Server 2000">SYSNAME</mapping> + <mapping rdbms="DB2/390 8">VARCHAR, size</mapping> + <mapping rdbms="DB2/390 7">VARCHAR, size</mapping> + <mapping rdbms="DB2/UDB 8.1">VARCHAR, size</mapping> + <mapping rdbms="DB2/UDB 7.1">VARCHAR, size</mapping> + </logicaltype> + <logicaltype name="SYS_ANYDATA" objectid="LogDes-F046B719-7D91-3873-3302-38C441683842@LOGDT10010"> + <mapping rdbms="Oracle Database 11g">SYS.ANYDATA</mapping> + <mapping rdbms="Oracle Database 10g">SYS.ANYDATA</mapping> + <mapping rdbms="Oracle9i">SYS.ANYDATA</mapping> + <mapping rdbms="SQL Server 2005">SQL_VARIANT</mapping> + <mapping rdbms="SQL Server 2000">SQL_VARIANT</mapping> + <mapping rdbms="DB2/390 8">UNKNOWN</mapping> + <mapping rdbms="DB2/390 7">UNKNOWN</mapping> + <mapping rdbms="DB2/UDB 8.1">UNKNOWN</mapping> + <mapping rdbms="DB2/UDB 7.1">UNKNOWN</mapping> + </logicaltype> + <logicaltype name="SYS_ANYDATASET" objectid="LogDes-22E251EB-9F6C-8137-56B2-DD4B87DC1E33@LOGDT10030"> + <mapping rdbms="Oracle Database 11g">SYS.ANYDATASET</mapping> + <mapping rdbms="Oracle Database 10g">SYS.ANYDATASET</mapping> + <mapping rdbms="Oracle9i">SYS.ANYDATASET</mapping> + <mapping rdbms="SQL Server 2005">UNKNOWN</mapping> + <mapping rdbms="SQL Server 2000">UNKNOWN</mapping> + <mapping rdbms="DB2/390 8">UNKNOWN</mapping> + <mapping rdbms="DB2/390 7">UNKNOWN</mapping> + <mapping rdbms="DB2/UDB 8.1">UNKNOWN</mapping> + <mapping rdbms="DB2/UDB 7.1">UNKNOWN</mapping> + </logicaltype> + <logicaltype name="SYS_ANYTYPE" objectid="LogDes-F046B719-7D91-3873-3302-38C441683842@LOGDT10011"> + <mapping rdbms="Oracle Database 11g">SYS.ANYTYPE</mapping> + <mapping rdbms="Oracle Database 10g">SYS.ANYTYPE</mapping> + <mapping rdbms="Oracle9i">SYS.ANYTYPE</mapping> + <mapping rdbms="SQL Server 2005">UNKNOWN</mapping> + <mapping rdbms="SQL Server 2000">UNKNOWN</mapping> + <mapping rdbms="DB2/390 8">UNKNOWN</mapping> + <mapping rdbms="DB2/390 7">UNKNOWN</mapping> + <mapping rdbms="DB2/UDB 8.1">UNKNOWN</mapping> + <mapping rdbms="DB2/UDB 7.1">UNKNOWN</mapping> + </logicaltype> + <logicaltype name="TEXT" objectid="LOGDT040"> + <mapping rdbms="Oracle Database 11g">CLOB</mapping> + <mapping rdbms="Oracle Database 10g">CLOB</mapping> + <mapping rdbms="Oracle9i">CLOB</mapping> + <mapping rdbms="SQL Server 2005">TEXT</mapping> + <mapping rdbms="SQL Server 2000">TEXT</mapping> + <mapping rdbms="DB2/390 8">VARCHAR, size</mapping> + <mapping rdbms="DB2/390 7">VARCHAR, size</mapping> + <mapping rdbms="DB2/UDB 8.1">VARCHAR, size</mapping> + <mapping rdbms="DB2/UDB 7.1">VARCHAR, size</mapping> + </logicaltype> + <logicaltype name="TIMESTAMP WITH LOCAL TIME ZONE" objectid="LOGDT047"> + <mapping rdbms="Oracle Database 11g">TIMESTAMP WITH LOCAL TIME ZONE, precision</mapping> + <mapping rdbms="Oracle Database 10g">TIMESTAMP WITH LOCAL TIME ZONE, precision</mapping> + <mapping rdbms="Oracle9i">TIMESTAMP WITH LOCAL TIME ZONE, precision</mapping> + <mapping rdbms="SQL Server 2005">DATETIME</mapping> + <mapping rdbms="SQL Server 2000">DATETIME</mapping> + <mapping rdbms="DB2/390 8">TIMESTAMP</mapping> + <mapping rdbms="DB2/390 7">TIMESTAMP</mapping> + <mapping rdbms="DB2/UDB 8.1">TIMESTAMP</mapping> + <mapping rdbms="DB2/UDB 7.1">TIMESTAMP</mapping> + </logicaltype> + <logicaltype name="TIMESTAMP WITH TIME ZONE" objectid="LOGDT046"> + <mapping rdbms="Oracle Database 11g">TIMESTAMP WITH TIME ZONE, precision</mapping> + <mapping rdbms="Oracle Database 10g">TIMESTAMP WITH TIME ZONE, precision</mapping> + <mapping rdbms="Oracle9i">TIMESTAMP WITH TIME ZONE, precision</mapping> + <mapping rdbms="SQL Server 2005">DATETIME</mapping> + <mapping rdbms="SQL Server 2000">DATETIME</mapping> + <mapping rdbms="DB2/390 8">TIMESTAMP</mapping> + <mapping rdbms="DB2/390 7">TIMESTAMP</mapping> + <mapping rdbms="DB2/UDB 8.1">TIMESTAMP</mapping> + <mapping rdbms="DB2/UDB 7.1">TIMESTAMP</mapping> + </logicaltype> + <logicaltype name="TINYINT" objectid="LOGDT042"> + <mapping rdbms="Oracle Database 11g">SMALLINT</mapping> + <mapping rdbms="Oracle Database 10g">SMALLINT</mapping> + <mapping rdbms="Oracle9i">SMALLINT</mapping> + <mapping rdbms="SQL Server 2005">TINYINT</mapping> + <mapping rdbms="SQL Server 2000">TINYINT</mapping> + <mapping rdbms="DB2/390 8">SMALLINT</mapping> + <mapping rdbms="DB2/390 7">SMALLINT</mapping> + <mapping rdbms="DB2/UDB 8.1">SMALLINT</mapping> + <mapping rdbms="DB2/UDB 7.1">SMALLINT</mapping> + </logicaltype> + <logicaltype name="Time" objectid="LOGDT014"> + <mapping rdbms="Oracle Database 11g">DATE</mapping> + <mapping rdbms="Oracle Database 10g">DATE</mapping> + <mapping rdbms="Oracle9i">DATE</mapping> + <mapping rdbms="SQL Server 2005">DATETIME</mapping> + <mapping rdbms="SQL Server 2000">DATETIME</mapping> + <mapping rdbms="DB2/390 8">TIME</mapping> + <mapping rdbms="DB2/390 7">TIME</mapping> + <mapping rdbms="DB2/UDB 8.1">TIME</mapping> + <mapping rdbms="DB2/UDB 7.1">TIME</mapping> + </logicaltype> + <logicaltype name="Timestamp" objectid="LOGDT015"> + <mapping rdbms="Oracle Database 11g">TIMESTAMP, precision</mapping> + <mapping rdbms="Oracle Database 10g">TIMESTAMP, precision</mapping> + <mapping rdbms="Oracle9i">TIMESTAMP, precision</mapping> + <mapping rdbms="SQL Server 2005">DATETIME</mapping> + <mapping rdbms="SQL Server 2000">DATETIME</mapping> + <mapping rdbms="DB2/390 8">TIMESTAMP</mapping> + <mapping rdbms="DB2/390 7">TIMESTAMP</mapping> + <mapping rdbms="DB2/UDB 8.1">TIMESTAMP</mapping> + <mapping rdbms="DB2/UDB 7.1">TIMESTAMP</mapping> + </logicaltype> + <logicaltype name="UNIQUEIDENTIFIER" objectid="LOGDT057"> + <mapping rdbms="Oracle Database 11g">CHAR, size</mapping> + <mapping rdbms="Oracle Database 10g">CHAR, size</mapping> + <mapping rdbms="Oracle9i">CHAR, size</mapping> + <mapping rdbms="SQL Server 2005">UNIQUEIDENTIFIER</mapping> + <mapping rdbms="SQL Server 2000">UNIQUEIDENTIFIER</mapping> + <mapping rdbms="DB2/390 8">CHAR, size</mapping> + <mapping rdbms="DB2/390 7">CHAR, size</mapping> + <mapping rdbms="DB2/UDB 8.1">CHAR, size</mapping> + <mapping rdbms="DB2/UDB 7.1">CHAR, size</mapping> + </logicaltype> + <logicaltype name="URIType" objectid="LOGDT051"> + <mapping rdbms="Oracle Database 11g">URITYPE</mapping> + <mapping rdbms="Oracle Database 10g">URITYPE</mapping> + <mapping rdbms="Oracle9i">URITYPE</mapping> + <mapping rdbms="SQL Server 2005">CHAR, size</mapping> + <mapping rdbms="SQL Server 2000">CHAR, size</mapping> + <mapping rdbms="DB2/390 8">CHAR, size</mapping> + <mapping rdbms="DB2/390 7">CHAR, size</mapping> + <mapping rdbms="DB2/UDB 8.1">CHAR, size</mapping> + <mapping rdbms="DB2/UDB 7.1">CHAR, size</mapping> + </logicaltype> + <logicaltype name="URowID" objectid="LogDes-4BABEC65-108B-2A3C-F7C4-84AC47D292B0@LOGDT10041"> + <mapping rdbms="Oracle Database 11g">UROWID, size</mapping> + <mapping rdbms="Oracle Database 10g">UROWID, size</mapping> + <mapping rdbms="Oracle9i">UROWID, size</mapping> + <mapping rdbms="SQL Server 2005">VARCHAR, size</mapping> + <mapping rdbms="SQL Server 2000">VARCHAR, size</mapping> + <mapping rdbms="DB2/390 8">VARCHAR, size</mapping> + <mapping rdbms="DB2/390 7">VARCHAR, size</mapping> + <mapping rdbms="DB2/UDB 8.1">VARCHAR, size</mapping> + <mapping rdbms="DB2/UDB 7.1">VARCHAR, size</mapping> + </logicaltype> + <logicaltype name="VARBINARY" objectid="LOGDT041"> + <mapping rdbms="Oracle Database 11g">BLOB</mapping> + <mapping rdbms="Oracle Database 10g">BLOB</mapping> + <mapping rdbms="Oracle9i">BLOB</mapping> + <mapping rdbms="SQL Server 2005">VARBINARY, size</mapping> + <mapping rdbms="SQL Server 2000">VARBINARY, size</mapping> + <mapping rdbms="DB2/390 8">BLOB, size</mapping> + <mapping rdbms="DB2/390 7">BLOB, size</mapping> + <mapping rdbms="DB2/UDB 8.1">BLOB, size</mapping> + <mapping rdbms="DB2/UDB 7.1">BLOB, size</mapping> + </logicaltype> + <logicaltype name="VARCHAR" objectid="LOGDT024"> + <mapping rdbms="Oracle Database 11g">VARCHAR2, size</mapping> + <mapping rdbms="Oracle Database 10g">VARCHAR2, size</mapping> + <mapping rdbms="Oracle9i">VARCHAR2, size</mapping> + <mapping rdbms="SQL Server 2005">VARCHAR, size</mapping> + <mapping rdbms="SQL Server 2000">VARCHAR, size</mapping> + <mapping rdbms="DB2/390 8">VARCHAR, size</mapping> + <mapping rdbms="DB2/390 7">VARCHAR, size</mapping> + <mapping rdbms="DB2/UDB 8.1">VARCHAR, size</mapping> + <mapping rdbms="DB2/UDB 7.1">VARCHAR, size</mapping> + </logicaltype> + <logicaltype name="VARGRAPHIC" objectid="LOGDT023"> + <mapping rdbms="Oracle Database 11g">BLOB</mapping> + <mapping rdbms="Oracle Database 10g">BLOB</mapping> + <mapping rdbms="Oracle9i">BLOB</mapping> + <mapping rdbms="SQL Server 2005">VARBINARY, size</mapping> + <mapping rdbms="SQL Server 2000">VARBINARY, size</mapping> + <mapping rdbms="DB2/390 8">VARGRAPHIC, size</mapping> + <mapping rdbms="DB2/390 7">VARGRAPHIC, size</mapping> + <mapping rdbms="DB2/UDB 8.1">VARGRAPHIC, size</mapping> + <mapping rdbms="DB2/UDB 7.1">VARGRAPHIC, size</mapping> + </logicaltype> + <logicaltype name="Video" objectid="LOGDT016"> + <mapping rdbms="Oracle Database 11g">BLOB</mapping> + <mapping rdbms="Oracle Database 10g">BLOB</mapping> + <mapping rdbms="Oracle9i">BLOB</mapping> + <mapping rdbms="SQL Server 2005">IMAGE</mapping> + <mapping rdbms="SQL Server 2000">IMAGE</mapping> + <mapping rdbms="DB2/390 8">VARGRAPHIC, size</mapping> + <mapping rdbms="DB2/390 7">VARGRAPHIC, size</mapping> + <mapping rdbms="DB2/UDB 8.1">BLOB</mapping> + <mapping rdbms="DB2/UDB 7.1">BLOB</mapping> + </logicaltype> + <logicaltype name="XDBURIType" objectid="LOGDT053"> + <mapping rdbms="Oracle Database 11g">XDBURITYPE</mapping> + <mapping rdbms="Oracle Database 10g">XDBURITYPE</mapping> + <mapping rdbms="Oracle9i">XDBURITYPE</mapping> + <mapping rdbms="SQL Server 2005">CHAR, size</mapping> + <mapping rdbms="SQL Server 2000">CHAR, size</mapping> + <mapping rdbms="DB2/390 8">CHAR, size</mapping> + <mapping rdbms="DB2/390 7">CHAR, size</mapping> + <mapping rdbms="DB2/UDB 8.1">CHAR, size</mapping> + <mapping rdbms="DB2/UDB 7.1">CHAR, size</mapping> + </logicaltype> + <logicaltype name="XMLType" objectid="LOGDT050"> + <mapping rdbms="Oracle Database 11g">XMLTYPE</mapping> + <mapping rdbms="Oracle Database 10g">XMLTYPE</mapping> + <mapping rdbms="Oracle9i">XMLTYPE</mapping> + <mapping rdbms="SQL Server 2005">XML</mapping> + <mapping rdbms="SQL Server 2000">TEXT</mapping> + <mapping rdbms="DB2/390 8">CLOB, size</mapping> + <mapping rdbms="DB2/390 7">CLOB, size</mapping> + <mapping rdbms="DB2/UDB 8.1">XML</mapping> + <mapping rdbms="DB2/UDB 7.1">CLOB, size</mapping> + </logicaltype> + <logicaltype name="unknown" objectid="LOGDT017" default="true"> + <mapping rdbms="Oracle Database 11g">UNKNOWN</mapping> + <mapping rdbms="Oracle Database 10g">UNKNOWN</mapping> + <mapping rdbms="Oracle9i">UNKNOWN</mapping> + <mapping rdbms="SQL Server 2005">UNKNOWN</mapping> + <mapping rdbms="SQL Server 2000">UNKNOWN</mapping> + <mapping rdbms="DB2/390 8">UNKNOWN</mapping> + <mapping rdbms="DB2/390 7">UNKNOWN</mapping> + <mapping rdbms="DB2/UDB 8.1">UNKNOWN</mapping> + <mapping rdbms="DB2/UDB 7.1">UNKNOWN</mapping> + </logicaltype> + <native_to_logical_mappings> + <mappings_for_RDBMS_type rdbms_type="Oracle Database 11g"> + <mapping native_type="BFILE" logicaltype="BFile" log_type_id="LogDes-7DD553FD-11E8-61FA-399D-2E531FB621D0@LOGDT10034" /> + <mapping native_type="BINARY_DOUBLE" logicaltype="BINARY DOUBLE" log_type_id="LOGDT056" /> + <mapping native_type="BINARY_FLOAT" logicaltype="BINARY FLOAT" log_type_id="LOGDT055" /> + <mapping native_type="BLOB" logicaltype="BLOB" log_type_id="LOGDT029" /> + <mapping native_type="CHAR" logicaltype="CHAR" log_type_id="LOGDT025" /> + <mapping native_type="CHAR VARYING" logicaltype="VARCHAR" log_type_id="LOGDT024" /> + <mapping native_type="CHARACTER" logicaltype="CHAR" log_type_id="LOGDT025" /> + <mapping native_type="CHARACTER VARYING" logicaltype="VARCHAR" log_type_id="LOGDT024" /> + <mapping native_type="CLOB" logicaltype="CLOB" log_type_id="LOGDT028" /> + <mapping native_type="DATE" logicaltype="Date" log_type_id="LOGDT007" /> + <mapping native_type="DECIMAL" logicaltype="DECIMAL" log_type_id="LOGDT026" /> + <mapping native_type="DOUBLE" logicaltype="DOUBLE" log_type_id="LOGDT020" /> + <mapping native_type="FLOAT" logicaltype="FLOAT" log_type_id="LOGDT021" /> + <mapping native_type="INTEGER" logicaltype="Integer" log_type_id="LOGDT011" /> + <mapping native_type="LONG" logicaltype="Long Char" log_type_id="LogDes-1768A872-F385-FDBA-D95E-0CB63F5908E2@LOGDT10045" /> + <mapping native_type="LONG RAW" logicaltype="Long_Raw" log_type_id="LogDes-7DD553FD-11E8-61FA-399D-2E531FB621D0@LOGDT10036" /> + <mapping native_type="LONG ROW" logicaltype="Long_Raw" log_type_id="LogDes-7DD553FD-11E8-61FA-399D-2E531FB621D0@LOGDT10036" /> + <mapping native_type="LONGROW" logicaltype="Long_Raw" log_type_id="LogDes-7DD553FD-11E8-61FA-399D-2E531FB621D0@LOGDT10036" /> + <mapping native_type="NATIONAL CHAR" logicaltype="NCHAR" log_type_id="LOGDT035" /> + <mapping native_type="NATIONAL CHAR VARYING" logicaltype="NVARCHAR" log_type_id="LOGDT037" /> + <mapping native_type="NATIONAL CHARACTER" logicaltype="NCHAR" log_type_id="LOGDT035" /> + <mapping native_type="NATIONAL CHARACTER VARYING" logicaltype="NVARCHAR" log_type_id="LOGDT037" /> + <mapping native_type="NCHAR" logicaltype="NCHAR" log_type_id="LOGDT035" /> + <mapping native_type="NCHAR VARYING" logicaltype="NVARCHAR" log_type_id="LOGDT037" /> + <mapping native_type="NCLOB" logicaltype="NClob" log_type_id="LogDes-7DD553FD-11E8-61FA-399D-2E531FB621D0@LOGDT10035" /> + <mapping native_type="NUMBER" logicaltype="NUMERIC" log_type_id="LOGDT019" /> + <mapping native_type="NUMERIC" logicaltype="NUMERIC" log_type_id="LOGDT019" /> + <mapping native_type="RAW" logicaltype="Raw" log_type_id="LogDes-4BABEC65-108B-2A3C-F7C4-84AC47D292B0@LOGDT10040" /> + <mapping native_type="REAL" logicaltype="REAL" log_type_id="LOGDT022" /> + <mapping native_type="ROWID" logicaltype="ROWID" log_type_id="LOGDT032" /> + <mapping native_type="SMALLINT" logicaltype="SMALLINT" log_type_id="LOGDT018" /> + <mapping native_type="UROWID" logicaltype="URowID" log_type_id="LogDes-4BABEC65-108B-2A3C-F7C4-84AC47D292B0@LOGDT10041" /> + <mapping native_type="VARCHAR" logicaltype="VARCHAR" log_type_id="LOGDT024" /> + <mapping native_type="VARCHAR2" logicaltype="VARCHAR" log_type_id="LOGDT024" /> + </mappings_for_RDBMS_type> + <mappings_for_RDBMS_type rdbms_type="Oracle Database 10g"> + <mapping native_type="BFILE" logicaltype="BFile" log_type_id="LogDes-7DD553FD-11E8-61FA-399D-2E531FB621D0@LOGDT10034" /> + <mapping native_type="BINARY_DOUBLE" logicaltype="BINARY DOUBLE" log_type_id="LOGDT056" /> + <mapping native_type="BINARY_FLOAT" logicaltype="BINARY FLOAT" log_type_id="LOGDT055" /> + <mapping native_type="BLOB" logicaltype="BLOB" log_type_id="LOGDT029" /> + <mapping native_type="CHAR" logicaltype="CHAR" log_type_id="LOGDT025" /> + <mapping native_type="CHAR VARYING" logicaltype="VARCHAR" log_type_id="LOGDT024" /> + <mapping native_type="CHARACTER" logicaltype="CHAR" log_type_id="LOGDT025" /> + <mapping native_type="CHARACTER VARYING" logicaltype="VARCHAR" log_type_id="LOGDT024" /> + <mapping native_type="CLOB" logicaltype="CLOB" log_type_id="LOGDT028" /> + <mapping native_type="DATE" logicaltype="Date" log_type_id="LOGDT007" /> + <mapping native_type="DECIMAL" logicaltype="DECIMAL" log_type_id="LOGDT026" /> + <mapping native_type="DOUBLE" logicaltype="DOUBLE" log_type_id="LOGDT020" /> + <mapping native_type="FLOAT" logicaltype="FLOAT" log_type_id="LOGDT021" /> + <mapping native_type="INTEGER" logicaltype="Integer" log_type_id="LOGDT011" /> + <mapping native_type="LONG" logicaltype="Long Char" log_type_id="LogDes-1768A872-F385-FDBA-D95E-0CB63F5908E2@LOGDT10045" /> + <mapping native_type="LONG RAW" logicaltype="Long_Raw" log_type_id="LogDes-7DD553FD-11E8-61FA-399D-2E531FB621D0@LOGDT10036" /> + <mapping native_type="LONG ROW" logicaltype="Long_Raw" log_type_id="LogDes-7DD553FD-11E8-61FA-399D-2E531FB621D0@LOGDT10036" /> + <mapping native_type="LONGROW" logicaltype="Long_Raw" log_type_id="LogDes-7DD553FD-11E8-61FA-399D-2E531FB621D0@LOGDT10036" /> + <mapping native_type="NATIONAL CHAR" logicaltype="NCHAR" log_type_id="LOGDT035" /> + <mapping native_type="NATIONAL CHAR VARYING" logicaltype="NVARCHAR" log_type_id="LOGDT037" /> + <mapping native_type="NATIONAL CHARACTER" logicaltype="NCHAR" log_type_id="LOGDT035" /> + <mapping native_type="NATIONAL CHARACTER VARYING" logicaltype="NVARCHAR" log_type_id="LOGDT037" /> + <mapping native_type="NCHAR" logicaltype="NCHAR" log_type_id="LOGDT035" /> + <mapping native_type="NCHAR VARYING" logicaltype="NVARCHAR" log_type_id="LOGDT037" /> + <mapping native_type="NCLOB" logicaltype="NClob" log_type_id="LogDes-7DD553FD-11E8-61FA-399D-2E531FB621D0@LOGDT10035" /> + <mapping native_type="NUMBER" logicaltype="NUMERIC" log_type_id="LOGDT019" /> + <mapping native_type="NUMERIC" logicaltype="NUMERIC" log_type_id="LOGDT019" /> + <mapping native_type="RAW" logicaltype="Raw" log_type_id="LogDes-4BABEC65-108B-2A3C-F7C4-84AC47D292B0@LOGDT10040" /> + <mapping native_type="REAL" logicaltype="REAL" log_type_id="LOGDT022" /> + <mapping native_type="ROWID" logicaltype="ROWID" log_type_id="LOGDT032" /> + <mapping native_type="SMALLINT" logicaltype="SMALLINT" log_type_id="LOGDT018" /> + <mapping native_type="UROWID" logicaltype="URowID" log_type_id="LogDes-4BABEC65-108B-2A3C-F7C4-84AC47D292B0@LOGDT10041" /> + <mapping native_type="VARCHAR" logicaltype="VARCHAR" log_type_id="LOGDT024" /> + <mapping native_type="VARCHAR2" logicaltype="VARCHAR" log_type_id="LOGDT024" /> + </mappings_for_RDBMS_type> + <mappings_for_RDBMS_type rdbms_type="Oracle9i"> + <mapping native_type="BFILE" logicaltype="BFile" log_type_id="LogDes-7DD553FD-11E8-61FA-399D-2E531FB621D0@LOGDT10034" /> + <mapping native_type="BLOB" logicaltype="BLOB" log_type_id="LOGDT029" /> + <mapping native_type="CHAR" logicaltype="CHAR" log_type_id="LOGDT025" /> + <mapping native_type="CHAR VARYING" logicaltype="VARCHAR" log_type_id="LOGDT024" /> + <mapping native_type="CHARACTER" logicaltype="CHAR" log_type_id="LOGDT025" /> + <mapping native_type="CHARACTER VARYING" logicaltype="VARCHAR" log_type_id="LOGDT024" /> + <mapping native_type="CLOB" logicaltype="CLOB" log_type_id="LOGDT028" /> + <mapping native_type="DATE" logicaltype="Date" log_type_id="LOGDT007" /> + <mapping native_type="DBURITYPE" logicaltype="DBURIType" log_type_id="LOGDT054" /> + <mapping native_type="DECIMAL" logicaltype="DECIMAL" log_type_id="LOGDT026" /> + <mapping native_type="DOUBLE" logicaltype="DOUBLE" log_type_id="LOGDT020" /> + <mapping native_type="FLOAT" logicaltype="FLOAT" log_type_id="LOGDT021" /> + <mapping native_type="HTTPURITYPE" logicaltype="HTTPURIType" log_type_id="LOGDT052" /> + <mapping native_type="INTEGER" logicaltype="Integer" log_type_id="LOGDT011" /> + <mapping native_type="INTERVAL DAY TO SECOND" logicaltype="INTERVAL DAY TO SECOND" log_type_id="LOGDT049" /> + <mapping native_type="INTERVAL YEAR TO MONTH" logicaltype="INTERVAL YEAR TO MONTH" log_type_id="LOGDT048" /> + <mapping native_type="LONG" logicaltype="Long Char" log_type_id="LogDes-1768A872-F385-FDBA-D95E-0CB63F5908E2@LOGDT10045" /> + <mapping native_type="LONG RAW" logicaltype="Long_Raw" log_type_id="LogDes-7DD553FD-11E8-61FA-399D-2E531FB621D0@LOGDT10036" /> + <mapping native_type="LONG ROW" logicaltype="Long_Raw" log_type_id="LogDes-7DD553FD-11E8-61FA-399D-2E531FB621D0@LOGDT10036" /> + <mapping native_type="LONGROW" logicaltype="Long_Raw" log_type_id="LogDes-7DD553FD-11E8-61FA-399D-2E531FB621D0@LOGDT10036" /> + <mapping native_type="NATIONAL CHAR" logicaltype="NCHAR" log_type_id="LOGDT035" /> + <mapping native_type="NATIONAL CHAR VARYING" logicaltype="NVARCHAR" log_type_id="LOGDT037" /> + <mapping native_type="NATIONAL CHARACTER" logicaltype="NCHAR" log_type_id="LOGDT035" /> + <mapping native_type="NATIONAL CHARACTER VARYING" logicaltype="NVARCHAR" log_type_id="LOGDT037" /> + <mapping native_type="NCHAR" logicaltype="NCHAR" log_type_id="LOGDT035" /> + <mapping native_type="NCHAR VARYING" logicaltype="NVARCHAR" log_type_id="LOGDT037" /> + <mapping native_type="NCLOB" logicaltype="NClob" log_type_id="LogDes-7DD553FD-11E8-61FA-399D-2E531FB621D0@LOGDT10035" /> + <mapping native_type="NUMBER" logicaltype="NUMERIC" log_type_id="LOGDT019" /> + <mapping native_type="NUMERIC" logicaltype="NUMERIC" log_type_id="LOGDT019" /> + <mapping native_type="NVARCHAR2" logicaltype="NVARCHAR" log_type_id="LOGDT037" /> + <mapping native_type="ORDSYS.ORDAudio" logicaltype="ORDAUDIO" log_type_id="LogDes-4972B6D2-6F93-8AE5-6E24-3599E65A7CFE@LOGDT10005" /> + <mapping native_type="ORDSYS.ORDDoc" logicaltype="ORDDOC" log_type_id="LogDes-4972B6D2-6F93-8AE5-6E24-3599E65A7CFE@LOGDT10009" /> + <mapping native_type="ORDSYS.ORDImage" logicaltype="ORDIMAGE" log_type_id="LogDes-4972B6D2-6F93-8AE5-6E24-3599E65A7CFE@LOGDT10006" /> + <mapping native_type="ORDSYS.ORDImageSignature" logicaltype="ORDIMAGE_SIGNATURE" log_type_id="LogDes-4972B6D2-6F93-8AE5-6E24-3599E65A7CFE@LOGDT10007" /> + <mapping native_type="ORDSYS.ORDVideo" logicaltype="ORDVIDEO" log_type_id="LogDes-4972B6D2-6F93-8AE5-6E24-3599E65A7CFE@LOGDT10008" /> + <mapping native_type="RAW" logicaltype="Raw" log_type_id="LogDes-4BABEC65-108B-2A3C-F7C4-84AC47D292B0@LOGDT10040" /> + <mapping native_type="REAL" logicaltype="REAL" log_type_id="LOGDT022" /> + <mapping native_type="ROWID" logicaltype="ROWID" log_type_id="LOGDT032" /> + <mapping native_type="SMALLINT" logicaltype="SMALLINT" log_type_id="LOGDT018" /> + <mapping native_type="SYS.ANYDATA" logicaltype="SYS_ANYDATA" log_type_id="LogDes-F046B719-7D91-3873-3302-38C441683842@LOGDT10010" /> + <mapping native_type="SYS.ANYDATASET" logicaltype="SYS_ANYDATASET" log_type_id="LogDes-22E251EB-9F6C-8137-56B2-DD4B87DC1E33@LOGDT10030" /> + <mapping native_type="SYS.ANYTYPE" logicaltype="SYS_ANYTYPE" log_type_id="LogDes-F046B719-7D91-3873-3302-38C441683842@LOGDT10011" /> + <mapping native_type="TIMESTAMP" logicaltype="Timestamp" log_type_id="LOGDT015" /> + <mapping native_type="TIMESTAMP WITH LOCAL TIME ZONE" logicaltype="TIMESTAMP WITH LOCAL TIME ZONE" log_type_id="LOGDT047" /> + <mapping native_type="TIMESTAMP WITH TIME ZONE" logicaltype="TIMESTAMP WITH TIME ZONE" log_type_id="LOGDT046" /> + <mapping native_type="URITYPE" logicaltype="URIType" log_type_id="LOGDT051" /> + <mapping native_type="UROWID" logicaltype="URowID" log_type_id="LogDes-4BABEC65-108B-2A3C-F7C4-84AC47D292B0@LOGDT10041" /> + <mapping native_type="VARCHAR" logicaltype="VARCHAR" log_type_id="LOGDT024" /> + <mapping native_type="VARCHAR2" logicaltype="VARCHAR" log_type_id="LOGDT024" /> + <mapping native_type="XDBURITYPE" logicaltype="XDBURIType" log_type_id="LOGDT053" /> + <mapping native_type="XMLTYPE" logicaltype="XMLType" log_type_id="LOGDT050" /> + </mappings_for_RDBMS_type> + <mappings_for_RDBMS_type rdbms_type="SQL Server 2005"> + <mapping native_type="DATE" logicaltype="Date" log_type_id="LOGDT007" /> + <mapping native_type="DOUBLE" logicaltype="DOUBLE" log_type_id="LOGDT020" /> + <mapping native_type="XML" logicaltype="XMLType" log_type_id="LOGDT050" /> + </mappings_for_RDBMS_type> + <mappings_for_RDBMS_type rdbms_type="SQL Server 2000"> + <mapping native_type="BIGINT" logicaltype="BIGINT" log_type_id="LOGDT027" /> + <mapping native_type="BINARY" logicaltype="BINARY" log_type_id="LOGDT033" /> + <mapping native_type="BIT" logicaltype="BIT" log_type_id="LOGDT034" /> + <mapping native_type="CHAR" logicaltype="CHAR" log_type_id="LOGDT025" /> + <mapping native_type="DATE" logicaltype="Date" log_type_id="LOGDT007" /> + <mapping native_type="DATETIME" logicaltype="Datetime" log_type_id="LOGDT008" /> + <mapping native_type="DECIMAL" logicaltype="DECIMAL" log_type_id="LOGDT026" /> + <mapping native_type="DOUBLE" logicaltype="DOUBLE" log_type_id="LOGDT020" /> + <mapping native_type="FLOAT" logicaltype="FLOAT" log_type_id="LOGDT021" /> + <mapping native_type="IMAGE" logicaltype="Image" log_type_id="LOGDT010" /> + <mapping native_type="INT" logicaltype="Integer" log_type_id="LOGDT011" /> + <mapping native_type="INTEGER" logicaltype="Integer" log_type_id="LOGDT011" /> + <mapping native_type="MONEY" logicaltype="MONEY" log_type_id="LOGDT043" /> + <mapping native_type="NCHAR" logicaltype="NCHAR" log_type_id="LOGDT035" /> + <mapping native_type="NTEXT" logicaltype="NTEXT" log_type_id="LOGDT036" /> + <mapping native_type="NUMERIC" logicaltype="NUMERIC" log_type_id="LOGDT019" /> + <mapping native_type="NVARCHAR" logicaltype="NVARCHAR" log_type_id="LOGDT037" /> + <mapping native_type="REAL" logicaltype="REAL" log_type_id="LOGDT022" /> + <mapping native_type="ROWID" logicaltype="ROWID" log_type_id="LOGDT032" /> + <mapping native_type="SMALLDATETIME" logicaltype="SMALLDATETIME" log_type_id="LOGDT038" /> + <mapping native_type="SMALLINT" logicaltype="SMALLINT" log_type_id="LOGDT018" /> + <mapping native_type="SMALLMONEY" logicaltype="SMALLMONEY" log_type_id="LOGDT044" /> + <mapping native_type="SQL_VARIANT" logicaltype="SQL_VARIANT" log_type_id="LOGDT045" /> + <mapping native_type="SYSNAME" logicaltype="SYSNAME" log_type_id="LOGDT039" /> + <mapping native_type="TEXT" logicaltype="TEXT" log_type_id="LOGDT040" /> + <mapping native_type="TIMESTAMP" logicaltype="Timestamp" log_type_id="LOGDT015" /> + <mapping native_type="TINYINT" logicaltype="TINYINT" log_type_id="LOGDT042" /> + <mapping native_type="UNIQUEIDENTIFIER" logicaltype="UNIQUEIDENTIFIER" log_type_id="LOGDT057" /> + <mapping native_type="VARBINARY" logicaltype="VARBINARY" log_type_id="LOGDT041" /> + <mapping native_type="VARCHAR" logicaltype="VARCHAR" log_type_id="LOGDT024" /> + </mappings_for_RDBMS_type> + <mappings_for_RDBMS_type rdbms_type="DB2/390 8"> + <mapping native_type="GRAPHIC" logicaltype="GRAPHIC" log_type_id="LOGDT031" /> + </mappings_for_RDBMS_type> + <mappings_for_RDBMS_type rdbms_type="DB2/390 7"> + <mapping native_type="BINARY LARGE OBJECT" logicaltype="BLOB" log_type_id="LOGDT029" /> + <mapping native_type="BLOB" logicaltype="BLOB" log_type_id="LOGDT029" /> + <mapping native_type="CHAR" logicaltype="CHAR" log_type_id="LOGDT025" /> + <mapping native_type="CHAR LARGE OBJECT" logicaltype="CLOB" log_type_id="LOGDT028" /> + <mapping native_type="CHAR VARYING" logicaltype="VARCHAR" log_type_id="LOGDT024" /> + <mapping native_type="CHARACTER" logicaltype="CHAR" log_type_id="LOGDT025" /> + <mapping native_type="CHARACTER LARGE OBJECT" logicaltype="CLOB" log_type_id="LOGDT028" /> + <mapping native_type="CHARACTER VARYING" logicaltype="VARCHAR" log_type_id="LOGDT024" /> + <mapping native_type="CLOB" logicaltype="CLOB" log_type_id="LOGDT028" /> + <mapping native_type="DATE" logicaltype="Date" log_type_id="LOGDT007" /> + <mapping native_type="DBCLOB" logicaltype="CLOB" log_type_id="LOGDT028" /> + <mapping native_type="DECIMAL" logicaltype="DECIMAL" log_type_id="LOGDT026" /> + <mapping native_type="DOUBLE" logicaltype="DOUBLE" log_type_id="LOGDT020" /> + <mapping native_type="FLOAT" logicaltype="FLOAT" log_type_id="LOGDT021" /> + <mapping native_type="GRAPHIC" logicaltype="GRAPHIC" log_type_id="LOGDT031" /> + <mapping native_type="INTEGER" logicaltype="Integer" log_type_id="LOGDT011" /> + <mapping native_type="LONG VARCHAR" logicaltype="VARCHAR" log_type_id="LOGDT024" /> + <mapping native_type="LONG VARGRAPHIC" logicaltype="VARGRAPHIC" log_type_id="LOGDT023" /> + <mapping native_type="NUMERIC" logicaltype="NUMERIC" log_type_id="LOGDT019" /> + <mapping native_type="REAL" logicaltype="REAL" log_type_id="LOGDT022" /> + <mapping native_type="ROWID" logicaltype="ROWID" log_type_id="LOGDT032" /> + <mapping native_type="SMALLINT" logicaltype="SMALLINT" log_type_id="LOGDT018" /> + <mapping native_type="TIME" logicaltype="Time" log_type_id="LOGDT014" /> + <mapping native_type="TIMESTAMP" logicaltype="Timestamp" log_type_id="LOGDT015" /> + <mapping native_type="VARCHAR" logicaltype="VARCHAR" log_type_id="LOGDT024" /> + <mapping native_type="VARGRAPHIC" logicaltype="VARGRAPHIC" log_type_id="LOGDT023" /> + </mappings_for_RDBMS_type> + <mappings_for_RDBMS_type rdbms_type="DB2/UDB 8.1"> + <mapping native_type="GRAPHIC" logicaltype="GRAPHIC" log_type_id="LOGDT031" /> + <mapping native_type="XML" logicaltype="XMLType" log_type_id="LOGDT050" /> + </mappings_for_RDBMS_type> + <mappings_for_RDBMS_type rdbms_type="DB2/UDB 7.1"> + <mapping native_type="BIGINT" logicaltype="BIGINT" log_type_id="LOGDT027" /> + <mapping native_type="BLOB" logicaltype="BLOB" log_type_id="LOGDT029" /> + <mapping native_type="CHAR" logicaltype="CHAR" log_type_id="LOGDT025" /> + <mapping native_type="CHAR VARYING" logicaltype="VARCHAR" log_type_id="LOGDT024" /> + <mapping native_type="CHARACTER" logicaltype="CHAR" log_type_id="LOGDT025" /> + <mapping native_type="CHARACTER VARYING" logicaltype="VARCHAR" log_type_id="LOGDT024" /> + <mapping native_type="CLOB" logicaltype="CLOB" log_type_id="LOGDT028" /> + <mapping native_type="DATALINK" logicaltype="DATALINK" log_type_id="LOGDT030" /> + <mapping native_type="DATE" logicaltype="Date" log_type_id="LOGDT007" /> + <mapping native_type="DBCLOB" logicaltype="CLOB" log_type_id="LOGDT028" /> + <mapping native_type="DECIMAL" logicaltype="DECIMAL" log_type_id="LOGDT026" /> + <mapping native_type="DOUBLE" logicaltype="DOUBLE" log_type_id="LOGDT020" /> + <mapping native_type="FLOAT" logicaltype="FLOAT" log_type_id="LOGDT021" /> + <mapping native_type="GRAPHIC" logicaltype="GRAPHIC" log_type_id="LOGDT031" /> + <mapping native_type="INTEGER" logicaltype="Integer" log_type_id="LOGDT011" /> + <mapping native_type="LONG VARCHAR" logicaltype="VARCHAR" log_type_id="LOGDT024" /> + <mapping native_type="LONG VARGRAPHIC" logicaltype="VARGRAPHIC" log_type_id="LOGDT023" /> + <mapping native_type="NUMERIC" logicaltype="NUMERIC" log_type_id="LOGDT019" /> + <mapping native_type="REAL" logicaltype="REAL" log_type_id="LOGDT022" /> + <mapping native_type="SMALLINT" logicaltype="SMALLINT" log_type_id="LOGDT018" /> + <mapping native_type="TIME" logicaltype="Time" log_type_id="LOGDT014" /> + <mapping native_type="TIMESTAMP" logicaltype="Timestamp" log_type_id="LOGDT015" /> + <mapping native_type="VARCHAR" logicaltype="VARCHAR" log_type_id="LOGDT024" /> + <mapping native_type="VARGRAPHIC" logicaltype="VARGRAPHIC" log_type_id="LOGDT023" /> + </mappings_for_RDBMS_type> + </native_to_logical_mappings> + <ud_native_db_types /> +</logtypes>
\ No newline at end of file diff --git a/src/VBox/ValidationKit/testmanager/db/TestManagerDatabaseComments.pgsql b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabaseComments.pgsql new file mode 100644 index 00000000..75d3e053 --- /dev/null +++ b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabaseComments.pgsql @@ -0,0 +1,1193 @@ +-- $Id: TestManagerDatabaseComments.pgsql $ +--- @file +-- Autogenerated from TestManagerDatabaseInit.pgsql. Do not edit! +-- + +-- +-- 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 +-- + + +COMMENT ON COLUMN SystemLog.tsCreated IS + 'When this was logged.'; + +COMMENT ON COLUMN SystemLog.sEvent IS + 'The event type. +This is a 8 character string identifier so that we don''t need to change +some enum type everytime we introduce a new event type.'; + +COMMENT ON COLUMN SystemLog.sLogText IS + 'The log text.'; + +COMMENT ON TABLE Users IS + 'Test manager users. + +This is mainly for doing simple access checks before permitting access to +the test manager. This needs to be coordinated with +apache/ldap/Oracle-Single-Sign-On. + +The main purpose, though, is for tracing who changed the test config and +analysis data. + +@remarks This table stores history. Never update or delete anything. The + equivalent of deleting is done by setting the ''tsExpire'' field to + current_timestamp.'; + +COMMENT ON COLUMN Users.tsEffective IS + 'When this row starts taking effect (inclusive).'; + +COMMENT ON COLUMN Users.tsExpire IS + 'When this row stops being tsEffective (exclusive).'; + +COMMENT ON COLUMN Users.uidAuthor IS + 'The user id of the one who created/modified this entry. +Non-unique foreign key: Users(uid)'; + +COMMENT ON COLUMN Users.sUsername IS + 'User name.'; + +COMMENT ON COLUMN Users.sEmail IS + 'The email address of the user.'; + +COMMENT ON COLUMN Users.sFullName IS + 'The full name.'; + +COMMENT ON COLUMN Users.sLoginName IS + 'The login name used by apache.'; + +COMMENT ON COLUMN Users.fReadOnly IS + 'Read access only.'; + +COMMENT ON TABLE GlobalResources IS + 'Global resource configuration. + +For example an iSCSI target. + +@remarks This table stores history. Never update or delete anything. The + equivalent of deleting is done by setting the ''tsExpire'' field to + current_timestamp.'; + +COMMENT ON COLUMN GlobalResources.tsEffective IS + 'When this row starts taking effect (inclusive).'; + +COMMENT ON COLUMN GlobalResources.tsExpire IS + 'When this row stops being tsEffective (exclusive).'; + +COMMENT ON COLUMN GlobalResources.uidAuthor IS + 'The user id of the one who created/modified this entry. +Non-unique foreign key: Users(uid)'; + +COMMENT ON COLUMN GlobalResources.sName IS + 'The name of the resource.'; + +COMMENT ON COLUMN GlobalResources.sDescription IS + 'Optional resource description.'; + +COMMENT ON COLUMN GlobalResources.fEnabled IS + 'Indicates whether this resource is currently enabled (online).'; + +COMMENT ON TABLE BuildSources IS + 'Build sources. + +This is used by a scheduling group to select builds and the default +Validation Kit from the Builds table. + +@remarks This table stores history. Never update or delete anything. The + equivalent of deleting is done by setting the ''tsExpire'' field to + current_timestamp. + +@todo Any better way of representing this so we could more easily + join/whatever when searching for builds?'; + +COMMENT ON COLUMN BuildSources.tsEffective IS + 'When this row starts taking effect (inclusive).'; + +COMMENT ON COLUMN BuildSources.tsExpire IS + 'When this row stops being tsEffective (exclusive).'; + +COMMENT ON COLUMN BuildSources.uidAuthor IS + 'The user id of the one who created/modified this entry. +Non-unique foreign key: Users(uid)'; + +COMMENT ON COLUMN BuildSources.sName IS + 'The name of the build source.'; + +COMMENT ON COLUMN BuildSources.sDescription IS + 'Description.'; + +COMMENT ON COLUMN BuildSources.sProduct IS + 'Which product. +ASSUME that it is okay to limit a build source to a single product.'; + +COMMENT ON COLUMN BuildSources.sBranch IS + 'Which branch. +ASSUME that it is okay to limit a build source to a branch.'; + +COMMENT ON COLUMN BuildSources.asTypes IS + 'Build types to include, all matches if NULL. +@todo Weighting the types would be nice in a later version.'; + +COMMENT ON COLUMN BuildSources.asOsArches IS + 'Array of the ''sOs.sCpuArch'' to match, all matches if NULL. +See KBUILD_OSES in kBuild for a list of standard target OSes, and +KBUILD_ARCHES for a list of standard architectures. + +@remarks See marks on ''os-agnostic'' and ''noarch'' in BuildCategories.'; + +COMMENT ON COLUMN BuildSources.iFirstRevision IS + 'The first subversion tree revision to match, no lower limit if NULL.'; + +COMMENT ON COLUMN BuildSources.iLastRevision IS + 'The last subversion tree revision to match, no upper limit if NULL.'; + +COMMENT ON COLUMN BuildSources.cSecMaxAge IS + 'The maximum age of the builds in seconds, unlimited if NULL.'; + +COMMENT ON TABLE TestCases IS + 'Test case configuration. + +@remarks This table stores history. Never update or delete anything. The + equivalent of deleting is done by setting the ''tsExpire'' field to + current_timestamp.'; + +COMMENT ON COLUMN TestCases.tsEffective IS + 'When this row starts taking effect (inclusive).'; + +COMMENT ON COLUMN TestCases.tsExpire IS + 'When this row stops being tsEffective (exclusive).'; + +COMMENT ON COLUMN TestCases.uidAuthor IS + 'The user id of the one who created/modified this entry. +Non-unique foreign key: Users(uid)'; + +COMMENT ON COLUMN TestCases.sName IS + 'The name of the test case.'; + +COMMENT ON COLUMN TestCases.sDescription IS + 'Optional test case description.'; + +COMMENT ON COLUMN TestCases.fEnabled IS + 'Indicates whether this test case is currently enabled.'; + +COMMENT ON COLUMN TestCases.cSecTimeout IS + 'Default test case timeout given in seconds.'; + +COMMENT ON COLUMN TestCases.sTestBoxReqExpr IS + 'Default TestBox requirement expression (python boolean expression). +All the scheduler properties are available for use with the same names +as in that table. +If NULL everything matches.'; + +COMMENT ON COLUMN TestCases.sBuildReqExpr IS + 'Default build requirement expression (python boolean expression). +The following build properties are available: sProduct, sBranch, +sType, asOsArches, sVersion, iRevision, uidAuthor and idBuild. +If NULL everything matches.'; + +COMMENT ON COLUMN TestCases.sBaseCmd IS + 'The base command. +String suitable for executing in bourne shell with space as separator +(IFS). References to @BUILD_BINARIES@ will be replaced WITH the content +of the Builds(sBinaries) field.'; + +COMMENT ON COLUMN TestCases.sTestSuiteZips IS + 'Comma separated list of test suite zips (or tars) that the testbox will +need to download and expand prior to testing. +If NULL the current test suite of the scheduling group will be used (the +scheduling group will have an optional test suite build queue associated +with it). The current test suite can also be referenced by +@VALIDATIONKIT_ZIP@ in case more downloads are required. Files may also be +uploaded to the test manager download area, in which case the +@DOWNLOAD_BASE_URL@ prefix can be used to refer to this area.'; + +COMMENT ON TABLE TestCaseArgs IS + 'Test case argument list variations. + +For example, we have a test case that does a set of tests on a virtual +machine. To get better code/feature coverage of this testcase we wish to +run it with different guest hardware configuration. The test case may do +the same stuff, but the guest OS as well as the VMM may react differently to +the hardware configurations and uncover issues in the VMM, device emulation +or other places. + +Typical hardware variations are: + - guest memory size (RAM), + - guest video memory size (VRAM), + - virtual CPUs / cores / threads, + - virtual chipset + - virtual network interface card (NIC) + - USB 1.1, USB 2.0, no USB + +The TM web UI will help the user create a reasonable set of permutations +of these parameters, the user specifies a maximum and the TM uses certain +rules together with random selection to generate the desired number. The +UI will also help suggest fitting testbox requirements according to the +RAM/VRAM sizes and the virtual CPU counts. The user may then make +adjustments to the suggestions before commit them. + +Alternatively, the user may also enter all the permutations without any +help from the UI. + +Note! All test cases has at least one entry in this table, even if it is +empty, because testbox requirements are specified thru this. + +Querying the valid parameter lists for a testase this way: + SELECT * ... WHERE idTestCase = TestCases.idTestCase + AND tsExpire > <when> + AND tsEffective <= <when>; + +Querying the valid parameter list for the latest generation can be +simplified by just checking tsExpire date: + SELECT * ... WHERE idTestCase = TestCases.idTestCase + AND tsExpire == TIMESTAMP WITH TIME ZONE ''infinity''; + +@remarks This table stores history. Never update or delete anything. The + equivalent of deleting is done by setting the ''tsExpire'' field to + current_timestamp.'; + +COMMENT ON COLUMN TestCaseArgs.tsEffective IS + 'When this row starts taking effect (inclusive).'; + +COMMENT ON COLUMN TestCaseArgs.tsExpire IS + 'When this row stops being tsEffective (exclusive).'; + +COMMENT ON COLUMN TestCaseArgs.uidAuthor IS + 'The user id of the one who created/modified this entry. +Non-unique foreign key: Users(uid)'; + +COMMENT ON COLUMN TestCaseArgs.sArgs IS + 'The additional arguments. +String suitable for bourne shell style argument parsing with space as +separator (IFS). References to @BUILD_BINARIES@ will be replaced with +the content of the Builds(sBinaries) field.'; + +COMMENT ON COLUMN TestCaseArgs.cSecTimeout IS + 'Optional test case timeout given in seconds. +If NULL, the TestCases.cSecTimeout field is used instead.'; + +COMMENT ON COLUMN TestCaseArgs.sTestBoxReqExpr IS + 'Additional TestBox requirement expression (python boolean expression). +All the scheduler properties are available for use with the same names +as in that table. This is checked after first checking the requirements +in the TestCases.sTestBoxReqExpr field.'; + +COMMENT ON COLUMN TestCaseArgs.sBuildReqExpr IS + 'Additional build requirement expression (python boolean expression). +The following build properties are available: sProduct, sBranch, +sType, asOsArches, sVersion, iRevision, uidAuthor and idBuild. This is +checked after first checking the requirements in the +TestCases.sBuildReqExpr field.'; + +COMMENT ON COLUMN TestCaseArgs.cGangMembers IS + 'Number of testboxes required (gang scheduling).'; + +COMMENT ON COLUMN TestCaseArgs.sSubName IS + 'Optional variation sub-name.'; + +COMMENT ON INDEX TestCaseArgsLookupIdx IS + 'The arguments are part of the primary key for several reasons. +No duplicate argument lists (makes no sense - if you want to prioritize +argument lists, we add that explicitly). This may hopefully enable us +to more easily check coverage later on, even when the test case is +reconfigured with more/less permutations.'; + +COMMENT ON TABLE TestCaseDeps IS + 'Test case dependencies (N:M) + +This effect build selection. The build must have passed all runs of the +given prerequisite testcase (idTestCasePreReq) and executed at a minimum one +argument list variation. + +This should also affect scheduling order, if possible at least one +prerequisite testcase variation should be place before the specific testcase +in the scheduling queue. + +@remarks This table stores history. Never update or delete anything. The + equivalent of deleting is done by setting the ''tsExpire'' field to + current_timestamp. To select the currently valid entries use + tsExpire = TIMESTAMP WITH TIME ZONE ''infinity''.'; + +COMMENT ON COLUMN TestCaseDeps.tsEffective IS + 'When this row starts taking effect (inclusive).'; + +COMMENT ON COLUMN TestCaseDeps.tsExpire IS + 'When this row stops being tsEffective (exclusive).'; + +COMMENT ON COLUMN TestCaseDeps.uidAuthor IS + 'The user id of the one who created/modified this entry. +Non-unique foreign key: Users(uid)'; + +COMMENT ON TABLE TestCaseGlobalRsrcDeps IS + 'Test case dependencies on global resources (N:M) + +@remarks This table stores history. Never update or delete anything. The + equivalent of deleting is done by setting the ''tsExpire'' field to + current_timestamp. To select the currently valid entries use + tsExpire = TIMESTAMP WITH TIME ZONE ''infinity''.'; + +COMMENT ON COLUMN TestCaseGlobalRsrcDeps.tsEffective IS + 'When this row starts taking effect (inclusive).'; + +COMMENT ON COLUMN TestCaseGlobalRsrcDeps.tsExpire IS + 'When this row stops being tsEffective (exclusive).'; + +COMMENT ON COLUMN TestCaseGlobalRsrcDeps.uidAuthor IS + 'The user id of the one who created/modified this entry. +Non-unique foreign key: Users(uid)'; + +COMMENT ON TABLE TestGroups IS + 'Test Group - A collection of test cases. + +This is for simplifying test configuration by working with a few groups +instead of a herd of individual testcases. It may also be used for creating +test suites for certain areas (like guest additions) or tasks (like +performance measurements). + +A test case can be member of any number of test groups. + +@remarks This table stores history. Never update or delete anything. The + equivalent of deleting is done by setting the ''tsExpire'' field to + current_timestamp. To select the currently valid entries use + tsExpire = TIMESTAMP WITH TIME ZONE ''infinity''.'; + +COMMENT ON COLUMN TestGroups.tsEffective IS + 'When this row starts taking effect (inclusive).'; + +COMMENT ON COLUMN TestGroups.tsExpire IS + 'When this row stops being tsEffective (exclusive).'; + +COMMENT ON COLUMN TestGroups.uidAuthor IS + 'The user id of the one who created/modified this entry. +Non-unique foreign key: Users(uid)'; + +COMMENT ON COLUMN TestGroups.sName IS + 'The name of the scheduling group.'; + +COMMENT ON COLUMN TestGroups.sDescription IS + 'Optional group description.'; + +COMMENT ON TABLE TestGroupMembers IS + 'The N:M relationship between test case configurations and test groups. + +@remarks This table stores history. Never update or delete anything. The + equivalent of deleting is done by setting the ''tsExpire'' field to + current_timestamp. To select the currently valid entries use + tsExpire = TIMESTAMP WITH TIME ZONE ''infinity''.'; + +COMMENT ON COLUMN TestGroupMembers.tsEffective IS + 'When this row starts taking effect (inclusive).'; + +COMMENT ON COLUMN TestGroupMembers.tsExpire IS + 'When this row stops being tsEffective (exclusive).'; + +COMMENT ON COLUMN TestGroupMembers.uidAuthor IS + 'The user id of the one who created/modified this entry. +Non-unique foreign key: Users(uid)'; + +COMMENT ON COLUMN TestGroupMembers.iSchedPriority IS + 'Test case scheduling priority. +Higher number causes the test case to be run more frequently. +@sa SchedGroupMembers.iSchedPriority, TestBoxesInSchedGroups.iSchedPriority +@todo Not sure we want to keep this...'; + +COMMENT ON TABLE SchedGroups IS + 'Scheduling group (aka. testbox partitioning) configuration. + +A testbox is associated with exactly one scheduling group. This association +can be changed, of course. If we (want to) retire a group which still has +testboxes associated with it, these will be moved to the ''default'' group. + +The TM web UI will make sure that a testbox is always in a group and that +the default group cannot be deleted. + +A scheduling group combines several things: + - A selection of builds to test (via idBuildSrc). + - A collection of test groups to test with (via SchedGroupMembers). + - A set of testboxes to test on (via TestBoxes.idSchedGroup). + +In additions there is an optional source of fresh test suite builds (think +VBoxTestSuite) as well as scheduling options. + +@remarks This table stores history. Never update or delete anything. The + equivalent of deleting is done by setting the ''tsExpire'' field to + current_timestamp. To select the currently valid entries use + tsExpire = TIMESTAMP WITH TIME ZONE ''infinity''.'; + +COMMENT ON COLUMN SchedGroups.tsEffective IS + 'When this row starts taking effect (inclusive).'; + +COMMENT ON COLUMN SchedGroups.tsExpire IS + 'When this row stops being tsEffective (exclusive).'; + +COMMENT ON COLUMN SchedGroups.uidAuthor IS + 'The user id of the one who created/modified this entry. +Non-unique foreign key: Users(uid) +@note This is NULL for the default group.'; + +COMMENT ON COLUMN SchedGroups.sName IS + 'The name of the scheduling group.'; + +COMMENT ON COLUMN SchedGroups.sDescription IS + 'Optional group description.'; + +COMMENT ON COLUMN SchedGroups.fEnabled IS + 'Indicates whether this group is currently enabled.'; + +COMMENT ON COLUMN SchedGroups.enmScheduler IS + 'The scheduler to use. +This is for when we later desire different scheduling that the best +effort stuff provided by the initial implementation.'; + +COMMENT ON COLUMN SchedGroups.sComment IS + 'The Validation Kit build source (@VALIDATIONKIT_ZIP@). +Non-unique foreign key: BuildSources(idBuildSrc)'; + +COMMENT ON TABLE SchedGroupMembers IS + 'N:M relationship between scheduling groups and test groups. + +Several scheduling parameters are associated with this relationship. + +The test group dependency (idTestGroupPreReq) can be used in the same way as +TestCaseDeps.idTestCasePreReq, only here on test group level. This means it +affects the build selection. The builds needs to have passed all test runs +the prerequisite test group and done at least one argument variation of each +test case in it. + +@remarks This table stores history. Never update or delete anything. The + equivalent of deleting is done by setting the ''tsExpire'' field to + current_timestamp. To select the currently valid entries use + tsExpire = TIMESTAMP WITH TIME ZONE ''infinity''.'; + +COMMENT ON COLUMN SchedGroupMembers.tsEffective IS + 'When this row starts taking effect (inclusive).'; + +COMMENT ON COLUMN SchedGroupMembers.tsExpire IS + 'When this row stops being tsEffective (exclusive).'; + +COMMENT ON COLUMN SchedGroupMembers.uidAuthor IS + 'The user id of the one who created/modified this entry. +Non-unique foreign key: Users(uid)'; + +COMMENT ON COLUMN SchedGroupMembers.iSchedPriority IS + 'The scheduling priority of the test group. +Higher number causes the test case to be run more frequently. +@sa TestGroupMembers.iSchedPriority, TestBoxesInSchedGroups.iSchedPriority'; + +COMMENT ON COLUMN SchedGroupMembers.bmHourlySchedule IS + 'When during the week this group is allowed to start running, NULL means +there are no constraints. +Each bit in the bitstring represents one hour, with bit 0 indicating the +midnight hour on a monday.'; + +COMMENT ON TABLE TestBoxStrTab IS + 'String table for the test boxes. + +This is a string cache for all string members in TestBoxes except the name. +The rational is to avoid duplicating large strings like sReport when the +testbox reports a new cMbScratch value or the box when the test sheriff +sends a reboot command or similar. + +At the time this table was introduced, we had 400558 TestBoxes rows, where +the SUM(LENGTH(sReport)) was 993MB. There were really just 1066 distinct +sReport values, with a total length of 0x3 MB. + +Nothing is ever deleted from this table. + +@note Should use a stored procedure to query/insert a string. + + +TestBox stats prior to conversion: + SELECT COUNT(*) FROM TestBoxes: 400558 rows + SELECT pg_total_relation_size(''TestBoxes''): 740794368 bytes (706 MB) + Average row cost: 740794368 / 400558 = 1849 bytes/row + +After conversion: + SELECT COUNT(*) FROM TestBoxes: 400558 rows + SELECT pg_total_relation_size(''TestBoxes''): 144375808 bytes (138 MB) + SELECT COUNT(idStr) FROM TestBoxStrTab: 1292 rows + SELECT pg_total_relation_size(''TestBoxStrTab''): 5709824 bytes (5.5 MB) + (144375808 + 5709824) / 740794368 = 20 % + Average row cost boxes: 144375808 / 400558 = 360 bytes/row + Average row cost strings: 5709824 / 1292 = 4420 bytes/row'; + +COMMENT ON COLUMN TestBoxStrTab.sValue IS + 'The string value.'; + +COMMENT ON COLUMN TestBoxStrTab.tsCreated IS + 'Creation time stamp.'; + +COMMENT ON TYPE TestBoxCmd_T IS + 'Testbox commands.'; + +COMMENT ON TYPE LomKind_T IS + 'The kind of lights out management on a testbox.'; + +COMMENT ON TABLE TestBoxes IS + 'Testbox configurations. + +The testboxes are identified by IP and the system UUID if available. Should +the IP change, the testbox will be refused at sign on and the testbox +sheriff will have to update it''s IP. + +@todo Implement the UUID stuff. Get it from DMI, UEFI or whereever. + Mismatching needs to be logged somewhere... + +To query the currently valid configuration: + SELECT ... WHERE id = idTestBox AND tsExpire = TIMESTAMP WITH TIME ZONE ''infinity''; + +@remarks This table stores history. Never update or delete anything. The + equivalent of deleting is done by setting the ''tsExpire'' field to + current_timestamp. To select the currently valid entries use + tsExpire = TIMESTAMP WITH TIME ZONE ''infinity''.'; + +COMMENT ON COLUMN TestBoxes.tsEffective IS + 'When this row starts taking effect (inclusive).'; + +COMMENT ON COLUMN TestBoxes.tsExpire IS + 'When this row stops being tsEffective (exclusive).'; + +COMMENT ON COLUMN TestBoxes.uidAuthor IS + 'The user id of the one who created/modified this entry. +When modified automatically by the testbox, NULL is used. +Non-unique foreign key: Users(uid)'; + +COMMENT ON COLUMN TestBoxes.uuidSystem IS + 'The system or firmware UUID. +This uniquely identifies the testbox when talking to the server. After +SIGNON though, the testbox will also provide idTestBox and ip to +establish its identity beyond doubt.'; + +COMMENT ON COLUMN TestBoxes.sName IS + 'The testbox name. +Usually similar to the DNS name.'; + +COMMENT ON COLUMN TestBoxes.fEnabled IS + 'Indicates whether this testbox is enabled. +A testbox gets disabled when we''re doing maintenance, debugging a issue +that happens only on that testbox, or some similar stuff. This is an +alternative to deleting the testbox.'; + +COMMENT ON COLUMN TestBoxes.enmLomKind IS + 'The kind of lights-out-management.'; + +COMMENT ON COLUMN TestBoxes.lCpuRevision IS + 'Number identifying the CPU family/model/stepping/whatever. +For x86 and AMD64 type CPUs, this will on the following format: + (EffFamily << 24) | (EffModel << 8) | Stepping.'; + +COMMENT ON COLUMN TestBoxes.cCpus IS + 'Number of CPUs, CPU cores and CPU threads.'; + +COMMENT ON COLUMN TestBoxes.fCpuHwVirt IS + 'Set if capable of hardware virtualization.'; + +COMMENT ON COLUMN TestBoxes.fCpuNestedPaging IS + 'Set if capable of nested paging.'; + +COMMENT ON COLUMN TestBoxes.fCpu64BitGuest IS + 'Set if CPU capable of 64-bit (VBox) guests.'; + +COMMENT ON COLUMN TestBoxes.fChipsetIoMmu IS + 'Set if chipset with usable IOMMU (VT-d / AMD-Vi).'; + +COMMENT ON COLUMN TestBoxes.fRawMode IS + 'Set if the test box does raw-mode tests.'; + +COMMENT ON COLUMN TestBoxes.cMbMemory IS + 'The (approximate) memory size in megabytes (rounded down to nearest 4 MB).'; + +COMMENT ON COLUMN TestBoxes.cMbScratch IS + 'The amount of scratch space in megabytes (rounded down to nearest 64 MB).'; + +COMMENT ON COLUMN TestBoxes.iTestBoxScriptRev IS + 'The testbox script revision number, serves the purpose of a version number. +Probably good to have when scheduling upgrades as well for status purposes.'; + +COMMENT ON COLUMN TestBoxes.iPythonHexVersion IS + 'The python sys.hexversion (layed out as of 2.7). +Good to know which python versions we need to support.'; + +COMMENT ON COLUMN TestBoxes.enmPendingCmd IS + 'Pending command. +@note We put it here instead of in TestBoxStatuses to get history.'; + +COMMENT ON INDEX TestBoxesUuidIdx IS + 'Nested paging requires hardware virtualization.'; + +COMMENT ON TABLE TestBoxesInSchedGroups IS + 'N:M relationship between test boxes and scheduling groups. + +We associate a priority with this relationship. + +@remarks This table stores history. Never update or delete anything. The + equivalent of deleting is done by setting the ''tsExpire'' field to + current_timestamp. To select the currently valid entries use + tsExpire = TIMESTAMP WITH TIME ZONE ''infinity''.'; + +COMMENT ON COLUMN TestBoxesInSchedGroups.tsEffective IS + 'When this row starts taking effect (inclusive).'; + +COMMENT ON COLUMN TestBoxesInSchedGroups.tsExpire IS + 'When this row stops being tsEffective (exclusive).'; + +COMMENT ON COLUMN TestBoxesInSchedGroups.uidAuthor IS + 'The user id of the one who created/modified this entry. +Non-unique foreign key: Users(uid)'; + +COMMENT ON COLUMN TestBoxesInSchedGroups.iSchedPriority IS + 'The scheduling priority of the scheduling group for the test box. +Higher number causes the scheduling group to be serviced more frequently. +@sa TestGroupMembers.iSchedPriority, SchedGroups.iSchedPriority'; + +COMMENT ON TABLE FailureCategories IS + 'Failure categories. + +This is for organizing the failure reasons. + +@remarks This table stores history. Never update or delete anything. The + equivalent of deleting is done by setting the ''tsExpire'' field to + current_timestamp. To select the currently valid entries use + tsExpire = TIMESTAMP WITH TIME ZONE ''infinity''.'; + +COMMENT ON COLUMN FailureCategories.tsEffective IS + 'When this row starts taking effect (inclusive).'; + +COMMENT ON COLUMN FailureCategories.tsExpire IS + 'When this row stops being tsEffective (exclusive).'; + +COMMENT ON COLUMN FailureCategories.uidAuthor IS + 'The user id of the one who created/modified this entry. +Non-unique foreign key: Users(uid)'; + +COMMENT ON COLUMN FailureCategories.sShort IS + 'The short category description. +For combo boxes and other selection lists.'; + +COMMENT ON COLUMN FailureCategories.sFull IS + 'Full description +For cursor-over-poppups for instance.'; + +COMMENT ON TABLE FailureReasons IS + 'Failure reasons. + +When analysing a test failure, the testbox sheriff will try assign a fitting +reason for the failure. This table is here to help the sheriff in his/hers +job as well as developers looking checking if their changes affected the +test results in any way. + +@remarks This table stores history. Never update or delete anything. The + equivalent of deleting is done by setting the ''tsExpire'' field to + current_timestamp. To select the currently valid entries use + tsExpire = TIMESTAMP WITH TIME ZONE ''infinity''.'; + +COMMENT ON COLUMN FailureReasons.tsEffective IS + 'When this row starts taking effect (inclusive).'; + +COMMENT ON COLUMN FailureReasons.tsExpire IS + 'When this row stops being tsEffective (exclusive).'; + +COMMENT ON COLUMN FailureReasons.uidAuthor IS + 'The user id of the one who created/modified this entry. +Non-unique foreign key: Users(uid)'; + +COMMENT ON COLUMN FailureReasons.sShort IS + 'The short failure description. +For combo boxes and other selection lists.'; + +COMMENT ON COLUMN FailureReasons.sFull IS + 'Full failure description.'; + +COMMENT ON COLUMN FailureReasons.iTicket IS + 'Ticket number in the primary bugtracker.'; + +COMMENT ON COLUMN FailureReasons.asUrls IS + 'Other URLs to reports or discussions of the observed symptoms.'; + +COMMENT ON TABLE TestResultFailures IS + 'This is for tracking/discussing test result failures. + +The rational for putting this is a separate table is that we need history on +this while TestResults does not. + +@remarks This table stores history. Never update or delete anything. The + equivalent of deleting is done by setting the ''tsExpire'' field to + current_timestamp. To select the currently valid entries use + tsExpire = TIMESTAMP WITH TIME ZONE ''infinity''.'; + +COMMENT ON COLUMN TestResultFailures.tsEffective IS + 'When this row starts taking effect (inclusive).'; + +COMMENT ON COLUMN TestResultFailures.tsExpire IS + 'When this row stops being tsEffective (exclusive).'; + +COMMENT ON COLUMN TestResultFailures.uidAuthor IS + 'The user id of the one who created/modified this entry. +Non-unique foreign key: Users(uid)'; + +COMMENT ON COLUMN TestResultFailures.sComment IS + 'Optional comment.'; + +COMMENT ON TABLE BuildBlacklist IS + 'Table used to blacklist sets of builds. + +The best usage example is a VMM developer realizing that a change causes the +host to panic, hang, or otherwise misbehave. To prevent the testbox sheriff +from repeatedly having to reboot testboxes, the builds gets blacklisted +until there is a working build again. This may mean adding an open ended +blacklist spec and then updating it with the final revision number once the +fix has been committed. + +@remarks This table stores history. Never update or delete anything. The + equivalent of deleting is done by setting the ''tsExpire'' field to + current_timestamp. To select the currently valid entries use + tsExpire = TIMESTAMP WITH TIME ZONE ''infinity''. + +@todo Would be nice if we could replace the text strings below with a set of + BuildCategories, or sore it in any other way which would enable us to + do a negative join with build category... The way it is specified + now, it looks like we have to open a cursor of prospecitve builds and + filter then thru this table one by one. + + Any better representation is welcome, but this is low prioirty for + now, as it''s relatively easy to change this later one.'; + +COMMENT ON COLUMN BuildBlacklist.tsEffective IS + 'When this row starts taking effect (inclusive).'; + +COMMENT ON COLUMN BuildBlacklist.tsExpire IS + 'When this row stops being tsEffective (exclusive).'; + +COMMENT ON COLUMN BuildBlacklist.uidAuthor IS + 'The user id of the one who created/modified this entry. +Non-unique foreign key: Users(uid)'; + +COMMENT ON COLUMN BuildBlacklist.sProduct IS + 'Which product. +ASSUME that it is okay to limit a blacklisting to a single product.'; + +COMMENT ON COLUMN BuildBlacklist.sBranch IS + 'Which branch. +ASSUME that it is okay to limit a blacklisting to a branch.'; + +COMMENT ON COLUMN BuildBlacklist.asTypes IS + 'Build types to include, all matches if NULL.'; + +COMMENT ON COLUMN BuildBlacklist.asOsArches IS + 'Array of the ''sOs.sCpuArch'' to match, all matches if NULL. +See KBUILD_OSES in kBuild for a list of standard target OSes, and +KBUILD_ARCHES for a list of standard architectures. + +@remarks See marks on ''os-agnostic'' and ''noarch'' in BuildCategories.'; + +COMMENT ON COLUMN BuildBlacklist.iFirstRevision IS + 'The first subversion tree revision to blacklist.'; + +COMMENT ON COLUMN BuildBlacklist.iLastRevision IS + 'The last subversion tree revision to blacklist, no upper limit if NULL.'; + +COMMENT ON TABLE BuildCategories IS + 'Build categories. + +The purpose of this table is saving space in the Builds table and hopefully +speed things up when selecting builds as well (compared to selecting on 4 +text fields in the much larger Builds table). + +Insert only table, no update, no delete. History is not needed.'; + +COMMENT ON COLUMN BuildCategories.sProduct IS + 'Product. +The product name. For instance ''VBox'' or ''VBoxTestSuite''.'; + +COMMENT ON COLUMN BuildCategories.sRepository IS + 'The version control repository name.'; + +COMMENT ON COLUMN BuildCategories.sBranch IS + 'The branch name (in the version control system).'; + +COMMENT ON COLUMN BuildCategories.sType IS + 'The build type. +See KBUILD_BLD_TYPES in kBuild for a list of standard build types.'; + +COMMENT ON COLUMN BuildCategories.asOsArches IS + 'Array of the ''sOs.sCpuArch'' supported by the build. +See KBUILD_OSES in kBuild for a list of standard target OSes, and +KBUILD_ARCHES for a list of standard architectures. + +@remarks ''os-agnostic'' is used if the build doesn''t really target any + specific OS or if it targets all applicable OSes. + ''noarch'' is used if the build is architecture independent or if + all applicable architectures are handled. + Thus, ''os-agnostic.noarch'' will run on all build boxes. + +@note The array shall be sorted ascendingly to prevent unnecessary duplicates!'; + +COMMENT ON TABLE Builds IS + 'The builds table contains builds from the tinderboxes and oaccasionally from +developers. + +The tinderbox side could be fed by a batch job enumerating the build output +directories every so often, looking for new builds. Or we could query them +from the tinderbox database. Yet another alternative is making the +tinderbox server or client side software inform us about all new builds. + +The developer builds are entered manually thru the TM web UI. They are used +for subjecting new code to some larger scale testing before commiting, +enabling, or merging a private branch. + +The builds are being selected from this table by the via the build source +specification that SchedGroups.idBuildSrc and +SchedGroups.idBuildSrcTestSuite links to. + +@remarks This table stores history. Never update or delete anything. The + equivalent of deleting is done by setting the ''tsExpire'' field to + current_timestamp. To select the currently valid entries use + tsExpire = TIMESTAMP WITH TIME ZONE ''infinity''.'; + +COMMENT ON COLUMN Builds.tsCreated IS + 'When this build was created or entered into the database. +This remains unchanged'; + +COMMENT ON COLUMN Builds.tsEffective IS + 'When this row starts taking effect (inclusive).'; + +COMMENT ON COLUMN Builds.tsExpire IS + 'When this row stops being tsEffective (exclusive).'; + +COMMENT ON COLUMN Builds.uidAuthor IS + 'The user id of the one who created/modified this entry. +Non-unique foreign key: Users(uid) +@note This is NULL if added by a batch job / tinderbox.'; + +COMMENT ON COLUMN Builds.iRevision IS + 'The subversion tree revision of the build.'; + +COMMENT ON COLUMN Builds.sVersion IS + 'The product version number (suitable for RTStrVersionCompare).'; + +COMMENT ON COLUMN Builds.sLogUrl IS + 'The link to the tinderbox log of this build.'; + +COMMENT ON COLUMN Builds.sBinaries IS + 'Comma separated list of binaries. +The binaries have paths relative to the TESTBOX_PATH_BUILDS or full URLs.'; + +COMMENT ON COLUMN Builds.fBinariesDeleted IS + 'Set when the binaries gets deleted by the build quota script.'; + +COMMENT ON TABLE VcsRevisions IS + 'This table is for translating build revisions into commit details. + +For graphs and test results, it would be useful to translate revisions into +dates and maybe provide commit message and the committer. + +Data is entered exclusively thru one or more batch jobs, so no internal +authorship needed. Also, since we''re mirroring data from external sources +here, the batch job is allowed to update/replace existing records. + +@todo We we could collect more info from the version control systems, if we + believe it''s useful and can be presented in a reasonable manner. + Getting a list of affected files would be simple (requires + a separate table with a M:1 relationship to this table), or try + associate a commit to a branch.'; + +COMMENT ON COLUMN VcsRevisions.sRepository IS + 'The version control tree name.'; + +COMMENT ON COLUMN VcsRevisions.iRevision IS + 'The version control tree revision number.'; + +COMMENT ON COLUMN VcsRevisions.tsCreated IS + 'When the revision was created (committed).'; + +COMMENT ON COLUMN VcsRevisions.sAuthor IS + 'The name of the committer. +@note Not to be confused with uidAuthor and test manager users.'; + +COMMENT ON COLUMN VcsRevisions.sMessage IS + 'The commit message.'; + +COMMENT ON TABLE TestResultStrTab IS + 'String table for the test results. + +This is a string cache for value names, test names and possible more, that +is frequently repated in the test results record for each test run. The +purpose is not only to save space, but to make datamining queries faster by +giving them integer fields to work on instead of text fields. There may +possibly be some benefits on INSERT as well as there are only integer +indexes. + +Nothing is ever deleted from this table. + +@note Should use a stored procedure to query/insert a string.'; + +COMMENT ON COLUMN TestResultStrTab.sValue IS + 'The string value.'; + +COMMENT ON COLUMN TestResultStrTab.tsCreated IS + 'Creation time stamp.'; + +COMMENT ON TYPE TestStatus_T IS + 'The status of a test (set / result).'; + +COMMENT ON TABLE TestResults IS + 'Test results - a recursive bundle of joy! + +A test case will be created when the testdriver calls reporter.testStart and +concluded with reporter.testDone. The testdriver (or it subordinates) can +use these methods to create nested test results. For IPRT based test cases, +RTTestCreate, RTTestInitAndCreate and RTTestSub will both create new test +result records, where as RTTestSubDone, RTTestSummaryAndDestroy and +RTTestDestroy will conclude records. + +By concluding is meant updating the status. When the test driver reports +success, we check it against reported results. (paranoia strikes again!) + +Nothing is ever deleted from this table. + +@note As seen below, several other tables associate data with a + test result, and the top most test result is referenced by the + test set.'; + +COMMENT ON COLUMN TestResults.tsCreated IS + 'Creation time stamp. This may also be the timestamp of when the test started.'; + +COMMENT ON COLUMN TestResults.tsElapsed IS + 'The elapsed time for this test. +This is either reported by the directly (with some sanity checking) or +calculated (current_timestamp - created_ts). +@todo maybe use a nanosecond field here, check with what'; + +COMMENT ON COLUMN TestResults.cErrors IS + 'The error count.'; + +COMMENT ON COLUMN TestResults.enmStatus IS + 'The test status.'; + +COMMENT ON COLUMN TestResults.iNestingDepth IS + 'Nesting depth.'; + +COMMENT ON TABLE TestResultValues IS + 'Test result values. + +A testdriver or subordinate may report a test value via +reporter.testValue(), while IPRT based test will use RTTestValue and +associates. + +This is an insert only table, no deletes, no updates.'; + +COMMENT ON COLUMN TestResultValues.tsCreated IS + 'Creation time stamp.'; + +COMMENT ON COLUMN TestResultValues.lValue IS + 'The value.'; + +COMMENT ON COLUMN TestResultValues.iUnit IS + 'The unit. +@todo This is currently not defined properly. Will fix/correlate this + with the other places we use unit (IPRT/testdriver/VMMDev).'; + +COMMENT ON TABLE TestResultFiles IS + 'Test result files. + +A testdriver or subordinate may report a file by using +reporter.addFile() or reporter.addLogFile(). + +The files stored here as well as the primary log file will be processed by a +batch job and compressed if considered compressable. Thus, TM will look for +files with a .gz/.bz2 suffix first and then without a suffix. + +This is an insert only table, no deletes, no updates.'; + +COMMENT ON COLUMN TestResultFiles.tsCreated IS + 'Creation time stamp.'; + +COMMENT ON INDEX TestResultFilesIdx IS + 'The mime type for the file. +For instance: ''text/plain'', + ''image/png'', + ''video/webm'', + ''text/xml'''; + +COMMENT ON TABLE TestResultMsgs IS + 'Test result message. + +A testdriver or subordinate may report a message via the sDetails parameter +of the reporter.testFailure() method, while IPRT test cases will use +RTTestFailed, RTTestPrintf and their friends. For RTTestPrintf, we will +ignore the more verbose message levels since these can also be found in one +of the logs. + +This is an insert only table, no deletes, no updates.'; + +COMMENT ON COLUMN TestResultMsgs.tsCreated IS + 'Creation time stamp.'; + +COMMENT ON COLUMN TestResultMsgs.enmLevel IS + 'The message level.'; + +COMMENT ON TABLE TestSets IS + 'Test sets / Test case runs. + +This is where we collect data about test runs. + +@todo Not entirely sure where the ''test set'' term came from. Consider + finding something more appropriate.'; + +COMMENT ON COLUMN TestSets.tsConfig IS + 'The test config timestamp, used when reading test config.'; + +COMMENT ON COLUMN TestSets.tsCreated IS + 'When this test set was scheduled. +idGenTestBox is valid at this point.'; + +COMMENT ON COLUMN TestSets.tsDone IS + 'When this test completed, i.e. testing stopped. This should only be set once.'; + +COMMENT ON COLUMN TestSets.enmStatus IS + 'The current status.'; + +COMMENT ON COLUMN TestSets.sBaseFilename IS + 'The base filename used for storing files related to this test set. +This is a path relative to wherever TM is dumping log files. In order +to not become a file system test case, we will try not to put too many +hundred thousand files in a directory. A simple first approach would +be to just use the current date (tsCreated) like this: + TM_FILE_DIR/year/month/day/TestSets.idTestSet + +The primary log file for the test is this name suffixed by ''.log''. + +The files in the testresultfile table gets their full names like this: + TM_FILE_DIR/sBaseFilename-testresultfile.id-TestResultStrTab(testresultfile.idStrFilename) + +@remarks We store this explicitly in case we change the directly layout + at some later point.'; + +COMMENT ON COLUMN TestSets.iGangMemberNo IS + 'The gang member number number, 0 is the leader.'; + +COMMENT ON INDEX TestSetsGangIdx IS + 'The test set of the gang leader, NULL if no gang involved. +@note This is set by the gang leader as well, so that we can find all + gang members by WHERE idTestSetGangLeader = :id.'; + +COMMENT ON INDEX TestSetsDoneCreatedBuildCatIdx IS + 'The TestSetsDoneCreatedBuildCatIdx is for testbox results, graph options and such.'; + +COMMENT ON INDEX TestSetsGraphBoxIdx IS + 'For graphs.'; + +COMMENT ON TYPE TestBoxState_T IS + 'TestBox state. + +@todo Consider drawing a state diagram for this.'; + +COMMENT ON TABLE TestBoxStatuses IS + 'Testbox status table. + +History is not planned on this table.'; + +COMMENT ON COLUMN TestBoxStatuses.tsUpdated IS + 'When this status was last updated. +This is updated everytime the testbox talks to the test manager, thus it +can easily be used to find testboxes which has stopped responding. + +This is used for timeout calculation during gang-gathering, so in that +scenario it won''t be updated until the gang is gathered or we time out.'; + +COMMENT ON COLUMN TestBoxStatuses.enmState IS + 'The current state.'; + +COMMENT ON COLUMN TestBoxStatuses.iWorkItem IS + 'Interal work item number. +This is used to pick and prioritize between multiple scheduling groups.'; + +COMMENT ON TABLE GlobalResourceStatuses IS + 'Global resource status, tracks which test set resources are allocated by. + +History is not planned on this table.'; + +COMMENT ON COLUMN GlobalResourceStatuses.tsAllocated IS + 'When the allocation took place.'; + +COMMENT ON TABLE SchedQueues IS + 'Scheduler queue. + +The queues are currently associated with a scheduling group, it could +alternative be changed to hook on to a testbox instead. It depends on what +kind of scheduling method we prefer. The former method aims at test case +thruput, making sacrifices in the hardware distribution area. The latter is +more like the old buildbox style testing, making sure that each test case is +executed on each testbox. + +When there are configuration changes, TM will regenerate the scheduling +queue for the affected scheduling groups. We do not concern ourselves with +trying to continue at the approximately same queue position, we simply take +it from the top. + +When a testbox ask for work, we will open a cursor on the queue and take the +first test in the queue that can be executed on that testbox. The test will +be moved to the end of the queue (getting a new item_id). + +If a test is manually changed to the head of the queue, the item will get a +item_id which is 1 lower than the head of the queue. Unless someone does +this a couple of billion times, we shouldn''t have any trouble running out of +number space. :-) + +Manually moving a test to the end of the queue is easy, just get a new +''item_id''. + +History is not planned on this table.'; + +COMMENT ON COLUMN SchedQueues.bmHourlySchedule IS + 'The scheduling time constraints (see SchedGroupMembers.bmHourlySchedule).'; + +COMMENT ON COLUMN SchedQueues.tsConfig IS + 'When the queue entry was created and for which config is valid. +This is the timestamp that should be used when reading config info.'; + +COMMENT ON COLUMN SchedQueues.tsLastScheduled IS + 'When this status was last scheduled. +This is set to current_timestamp when moving the entry to the end of the +queue. It''s initial value is unix-epoch. Not entirely sure if it''s +useful beyond introspection and non-unique foreign key hacking.'; + +COMMENT ON COLUMN SchedQueues.cMissingGangMembers IS + 'The number of gang members still missing. + +This saves calculating the number of missing members via selects like: + SELECT COUNT(*) FROM TestSets WHERE idTestSetGangLeader = :idGang; +and + SELECT cGangMembers FROM TestCaseArgs WHERE idGenTestCaseArgs = :idTest; +to figure out whether to remain in ''gather-gang''::TestBoxState_T.'; + +COMMENT ON INDEX SchedQueuesItemIdx IS + 'The number of times this has been considered for scheduling. +cConsidered SMALLINT DEFAULT 0 NOT NULL,'; + diff --git a/src/VBox/ValidationKit/testmanager/db/TestManagerDatabaseDefaultUserAccounts.pgsql b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabaseDefaultUserAccounts.pgsql new file mode 100644 index 00000000..41198b4f --- /dev/null +++ b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabaseDefaultUserAccounts.pgsql @@ -0,0 +1,43 @@ +-- $Id: TestManagerDatabaseDefaultUserAccounts.pgsql $ +--- @file +-- VBox Test Manager default user account records creation script. +-- + +-- +-- 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 +-- + +\set ON_ERROR_STOP 1 +\connect testmanager; + +-- Add record for user 'admin' +INSERT INTO Users (sUsername, sEmail, sFullName, sLoginName) + VALUES ('root', 'admin@example.org', 'Administrator', 'admin'); + diff --git a/src/VBox/ValidationKit/testmanager/db/TestManagerDatabaseForeignKeyErHacks.pgsql b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabaseForeignKeyErHacks.pgsql new file mode 100644 index 00000000..1430169c --- /dev/null +++ b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabaseForeignKeyErHacks.pgsql @@ -0,0 +1,90 @@ +-- $Id: TestManagerDatabaseForeignKeyErHacks.pgsql $ +--- @file +-- VBox Test Manager Database Addendum that adds non-unique foreign keys. +-- +-- This is for getting better visualization in reverse engeering ER tools, +-- it is not for production databases. +-- + +-- +-- 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 +-- + +\set ON_ERROR_STOP 1 +\connect testmanager + +ALTER TABLE TestCaseArgs + ADD CONSTRAINT non_unique_fk1 FOREIGN KEY (idTestCase, tsExpire) REFERENCES TestCases(idTestCase, tsExpire) MATCH FULL; + +ALTER TABLE TestcaseDeps + ADD CONSTRAINT non_unique_fk1 FOREIGN KEY (idTestCase, tsExpire) REFERENCES TestCases(idTestCase, tsExpire) MATCH FULL; +ALTER TABLE TestcaseDeps + ADD CONSTRAINT non_unique_fk2 FOREIGN KEY (idTestCasePreReq,tsExpire) REFERENCES TestCases(idTestCase, tsExpire) MATCH FULL; + +ALTER TABLE TestCaseGlobalRsrcDeps + ADD CONSTRAINT non_unique_fk1 FOREIGN KEY (idTestCase, tsExpire) REFERENCES TestCases(idTestCase, tsExpire) MATCH FULL; +ALTER TABLE TestCaseGlobalRsrcDeps + ADD CONSTRAINT non_unique_fk2 FOREIGN KEY (idGlobalRsrc, tsExpire) REFERENCES GlobalResources(idGlobalRsrc, tsExpire) MATCH FULL; + +ALTER TABLE TestGroupMembers + ADD CONSTRAINT non_unique_fk1 FOREIGN KEY (idTestGroup, tsExpire) REFERENCES TestGroups(idTestGroup, tsExpire) MATCH FULL; +ALTER TABLE TestGroupMembers + ADD CONSTRAINT non_unique_fk2 FOREIGN KEY (idTestCase, tsExpire) REFERENCES TestCases(idTestCase, tsExpire) MATCH FULL; + +ALTER TABLE SchedGroups + ADD CONSTRAINT non_unique_fk1 FOREIGN KEY (idBuildSrc, tsExpire) REFERENCES BuildSources(idBuildSrc, tsExpire) MATCH SIMPLE; +ALTER TABLE SchedGroups + ADD CONSTRAINT non_unique_fk2 FOREIGN KEY (idBuildSrcTestSuite, tsExpire) REFERENCES BuildSources(idBuildSrc, tsExpire) MATCH SIMPLE; + +ALTER TABLE SchedGroupMembers + ADD CONSTRAINT non_unique_fk1 FOREIGN KEY (idSchedGroup, tsExpire) REFERENCES SchedGroups(idSchedGroup, tsExpire) MATCH FULL; +ALTER TABLE SchedGroupMembers + ADD CONSTRAINT non_unique_fk2 FOREIGN KEY (idTestGroup, tsExpire) REFERENCES TestGroups(idTestGroup, tsExpire) MATCH FULL; +ALTER TABLE SchedGroupMembers + ADD CONSTRAINT non_unique_fk3 FOREIGN KEY (idTestGroupPreReq, tsExpire) REFERENCES TestGroups(idTestGroup, tsExpire) MATCH FULL; + +ALTER TABLE TestBoxes + ADD CONSTRAINT non_unique_fk1 FOREIGN KEY (idSchedGroup, tsExpire) REFERENCES SchedGroups(idSchedGroup, tsExpire) MATCH FULL; + +ALTER TABLE FailureReasons + ADD CONSTRAINT non_unique_fk1 FOREIGN KEY (idFailureCategory, tsExpire) REFERENCES FailureCategories(idFailureCategory, tsExpire) MATCH FULL; + +ALTER TABLE TestResultFailures + ADD CONSTRAINT non_unique_fk1 FOREIGN KEY (idFailureReason, tsExpire) REFERENCES FailureReasons(idFailureReason, tsExpire) MATCH FULL; + +ALTER TABLE BuildBlacklist + ADD CONSTRAINT non_unique_fk1 FOREIGN KEY (idFailureReason, tsExpire) REFERENCES FailureReasons(idFailureReason, tsExpire) MATCH FULL; + +ALTER TABLE GlobalResourceStatuses + ADD CONSTRAINT non_unique_fk1 FOREIGN KEY (idGlobalRsrc, tsAllocated) REFERENCES GlobalResources(idGlobalRsrc, tsExpire) MATCH FULL; + +ALTER TABLE SchedQueues + ADD CONSTRAINT non_unique_fk1 FOREIGN KEY (idSchedGroup, tsLastScheduled) REFERENCES SchedGroups(idSchedGroup, tsExpire) MATCH FULL; + diff --git a/src/VBox/ValidationKit/testmanager/db/TestManagerDatabaseForeignKeyErHacks2.pgsql b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabaseForeignKeyErHacks2.pgsql new file mode 100644 index 00000000..12686d81 --- /dev/null +++ b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabaseForeignKeyErHacks2.pgsql @@ -0,0 +1,77 @@ +-- $Id: TestManagerDatabaseForeignKeyErHacks2.pgsql $ +--- @file +-- VBox Test Manager Database Addendum that adds non-unique foreign keys to Users. +-- +-- This is for getting better visualization in reverse engeering ER tools, +-- it is not for production databases. +-- + +-- +-- 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 +-- + +\set ON_ERROR_STOP 1 +\connect testmanager + +ALTER TABLE GlobalResources + ADD CONSTRAINT non_unique_fk9 FOREIGN KEY (uidAuthor, tsExpire) REFERENCES Users(uid, tsExpire) MATCH FULL; +ALTER TABLE BuildSources + ADD CONSTRAINT non_unique_fk9 FOREIGN KEY (uidAuthor, tsExpire) REFERENCES Users(uid, tsExpire) MATCH FULL; +ALTER TABLE RequirementSets + ADD CONSTRAINT non_unique_fk9 FOREIGN KEY (uidAuthor, tsCreated) REFERENCES Users(uid, tsExpire) MATCH FULL; +ALTER TABLE TestCases + ADD CONSTRAINT non_unique_fk9 FOREIGN KEY (uidAuthor, tsExpire) REFERENCES Users(uid, tsExpire) MATCH FULL; +ALTER TABLE TestCaseArgs + ADD CONSTRAINT non_unique_fk9 FOREIGN KEY (uidAuthor, tsExpire) REFERENCES Users(uid, tsExpire) MATCH FULL; +ALTER TABLE TestcaseDeps + ADD CONSTRAINT non_unique_fk9 FOREIGN KEY (uidAuthor, tsExpire) REFERENCES Users(uid, tsExpire) MATCH FULL; +ALTER TABLE TestCaseGlobalRsrcDeps + ADD CONSTRAINT non_unique_fk9 FOREIGN KEY (uidAuthor, tsExpire) REFERENCES Users(uid, tsExpire) MATCH FULL; +ALTER TABLE TestGroups + ADD CONSTRAINT non_unique_fk9 FOREIGN KEY (uidAuthor, tsExpire) REFERENCES Users(uid, tsExpire) MATCH FULL; +ALTER TABLE TestGroupMembers + ADD CONSTRAINT non_unique_fk9 FOREIGN KEY (uidAuthor, tsExpire) REFERENCES Users(uid, tsExpire) MATCH FULL; +ALTER TABLE SchedGroups + ADD CONSTRAINT non_unique_fk9 FOREIGN KEY (uidAuthor, tsExpire) REFERENCES Users(uid, tsExpire) MATCH SIMPLE; +ALTER TABLE SchedGroupMembers + ADD CONSTRAINT non_unique_fk9 FOREIGN KEY (uidAuthor, tsExpire) REFERENCES Users(uid, tsExpire) MATCH FULL; +ALTER TABLE TestBoxes + ADD CONSTRAINT non_unique_fk9 FOREIGN KEY (uidAuthor, tsExpire) REFERENCES Users(uid, tsExpire) MATCH FULL; +ALTER TABLE FailureCategories + ADD CONSTRAINT non_unique_fk9 FOREIGN KEY (uidAuthor, tsExpire) REFERENCES Users(uid, tsExpire) MATCH FULL; +ALTER TABLE FailureReasons + ADD CONSTRAINT non_unique_fk9 FOREIGN KEY (uidAuthor, tsExpire) REFERENCES Users(uid, tsExpire) MATCH FULL; +ALTER TABLE TestResultFailures + ADD CONSTRAINT non_unique_fk9 FOREIGN KEY (uidAuthor, tsExpire) REFERENCES Users(uid, tsExpire) MATCH FULL; +ALTER TABLE BuildBlacklist + ADD CONSTRAINT non_unique_fk9 FOREIGN KEY (uidAuthor, tsExpire) REFERENCES Users(uid, tsExpire) MATCH FULL; +ALTER TABLE Builds + ADD CONSTRAINT non_unique_fk9 FOREIGN KEY (uidAuthor, tsCreated) REFERENCES Users(uid, tsExpire) MATCH FULL; + diff --git a/src/VBox/ValidationKit/testmanager/db/TestManagerDatabaseInit.pgsql b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabaseInit.pgsql new file mode 100644 index 00000000..10e93ff2 --- /dev/null +++ b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabaseInit.pgsql @@ -0,0 +1,1950 @@ +-- $Id: TestManagerDatabaseInit.pgsql $ +--- @file +-- VBox Test Manager Database Creation script. +-- + +-- +-- 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 +-- + +-- +-- Declaimer: +-- +-- The guys working on this design are not database experts, web +-- programming experts or similar, rather we are low level guys +-- who's main job is x86 & AMD64 virtualization. So, please don't +-- be too hard on us. :-) +-- +-- + + +-- D R O P D A T A B A S E t e s t m a n a g e r - - you do this now. +\set ON_ERROR_STOP 1 +CREATE DATABASE testmanager; +\connect testmanager; + + +-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- +-- +-- S y s t e m +-- +-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- + +--- +-- Log table for a few important events. +-- +-- Currently, two events are planned to be logged: +-- - Sign on of an unknown testbox, including the IP and System UUID. +-- This will be restricted to one entry per 24h or something like that: +-- SELECT COUNT(*) +-- FROM SystemLog +-- WHERE tsCreated >= (current_timestamp - interval '24 hours') +-- AND sEvent = 'TBoxUnkn' +-- AND sLogText = :sNewLogText; +-- - When cleaning up an abandoned testcase (scenario #9), log which +-- testbox abandoned which testset. +-- +-- The Web UI will have some way of displaying the log. +-- +-- A batch job should regularly clean out old log messages, like for instance +-- > 64 days. +-- +CREATE TABLE SystemLog ( + --- When this was logged. + tsCreated TIMESTAMP WITH TIME ZONE DEFAULT current_timestamp NOT NULL, + --- The event type. + -- This is a 8 character string identifier so that we don't need to change + -- some enum type everytime we introduce a new event type. + sEvent CHAR(8) NOT NULL, + --- The log text. + sLogText text NOT NULL, + + PRIMARY KEY (tsCreated, sEvent) +); + + +-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- +-- +-- C o n f i g u r a t i o n +-- +-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- + +--- @table Users +-- Test manager users. +-- +-- This is mainly for doing simple access checks before permitting access to +-- the test manager. This needs to be coordinated with +-- apache/ldap/Oracle-Single-Sign-On. +-- +-- The main purpose, though, is for tracing who changed the test config and +-- analysis data. +-- +-- @remarks This table stores history. Never update or delete anything. The +-- equivalent of deleting is done by setting the 'tsExpire' field to +-- current_timestamp. +-- +CREATE SEQUENCE UserIdSeq + START 1 + INCREMENT BY 1 + NO MAXVALUE + NO MINVALUE + CACHE 1; +CREATE TABLE Users ( + --- The user id. + uid INTEGER DEFAULT NEXTVAL('UserIdSeq') NOT NULL, + --- When this row starts taking effect (inclusive). + tsEffective TIMESTAMP WITH TIME ZONE DEFAULT current_timestamp NOT NULL, + --- When this row stops being tsEffective (exclusive). + tsExpire TIMESTAMP WITH TIME ZONE DEFAULT TIMESTAMP WITH TIME ZONE 'infinity' NOT NULL, + --- The user id of the one who created/modified this entry. + -- Non-unique foreign key: Users(uid) + uidAuthor INTEGER DEFAULT NULL, + --- User name. + sUsername text NOT NULL, + --- The email address of the user. + sEmail text NOT NULL, + --- The full name. + sFullName text NOT NULL, + --- The login name used by apache. + sLoginName text NOT NULL, + --- Read access only. + fReadOnly BOOLEAN NOT NULL DEFAULT FALSE, + + PRIMARY KEY (uid, tsExpire) +); +CREATE INDEX UsersLoginNameIdx ON Users (sLoginName, tsExpire DESC); + + +--- @table GlobalResources +-- Global resource configuration. +-- +-- For example an iSCSI target. +-- +-- @remarks This table stores history. Never update or delete anything. The +-- equivalent of deleting is done by setting the 'tsExpire' field to +-- current_timestamp. +-- +CREATE SEQUENCE GlobalResourceIdSeq + START 1 + INCREMENT BY 1 + NO MAXVALUE + NO MINVALUE + CACHE 1; +CREATE TABLE GlobalResources ( + --- The global resource ID. + -- This stays the same thru updates. + idGlobalRsrc INTEGER DEFAULT NEXTVAL('GlobalResourceIdSeq') NOT NULL, + --- When this row starts taking effect (inclusive). + tsEffective TIMESTAMP WITH TIME ZONE DEFAULT current_timestamp NOT NULL, + --- When this row stops being tsEffective (exclusive). + tsExpire TIMESTAMP WITH TIME ZONE DEFAULT TIMESTAMP WITH TIME ZONE 'infinity' NOT NULL, + --- The user id of the one who created/modified this entry. + -- Non-unique foreign key: Users(uid) + uidAuthor INTEGER NOT NULL, + --- The name of the resource. + sName text NOT NULL, + --- Optional resource description. + sDescription text, + --- Indicates whether this resource is currently enabled (online). + fEnabled boolean DEFAULT FALSE NOT NULL, + + PRIMARY KEY (idGlobalRsrc, tsExpire) +); + + +--- @table BuildSources +-- Build sources. +-- +-- This is used by a scheduling group to select builds and the default +-- Validation Kit from the Builds table. +-- +-- @remarks This table stores history. Never update or delete anything. The +-- equivalent of deleting is done by setting the 'tsExpire' field to +-- current_timestamp. +-- +-- @todo Any better way of representing this so we could more easily +-- join/whatever when searching for builds? +-- +CREATE SEQUENCE BuildSourceIdSeq + START 1 + INCREMENT BY 1 + NO MAXVALUE + NO MINVALUE + CACHE 1; +CREATE TABLE BuildSources ( + --- The build source identifier. + -- This stays constant over time. + idBuildSrc INTEGER DEFAULT NEXTVAL('BuildSourceIdSeq') NOT NULL, + --- When this row starts taking effect (inclusive). + tsEffective TIMESTAMP WITH TIME ZONE DEFAULT current_timestamp NOT NULL, + --- When this row stops being tsEffective (exclusive). + tsExpire TIMESTAMP WITH TIME ZONE DEFAULT TIMESTAMP WITH TIME ZONE 'infinity' NOT NULL, + --- The user id of the one who created/modified this entry. + -- Non-unique foreign key: Users(uid) + uidAuthor INTEGER NOT NULL, + + --- The name of the build source. + sName TEXT NOT NULL, + --- Description. + sDescription TEXT DEFAULT NULL, + + --- Which product. + -- ASSUME that it is okay to limit a build source to a single product. + sProduct text NOT NULL, + --- Which branch. + -- ASSUME that it is okay to limit a build source to a branch. + sBranch text NOT NULL, + + --- Build types to include, all matches if NULL. + -- @todo Weighting the types would be nice in a later version. + asTypes text ARRAY DEFAULT NULL, + --- Array of the 'sOs.sCpuArch' to match, all matches if NULL. + -- See KBUILD_OSES in kBuild for a list of standard target OSes, and + -- KBUILD_ARCHES for a list of standard architectures. + -- + -- @remarks See marks on 'os-agnostic' and 'noarch' in BuildCategories. + asOsArches text ARRAY DEFAULT NULL, + + --- The first subversion tree revision to match, no lower limit if NULL. + iFirstRevision INTEGER DEFAULT NULL, + --- The last subversion tree revision to match, no upper limit if NULL. + iLastRevision INTEGER DEFAULT NULL, + + --- The maximum age of the builds in seconds, unlimited if NULL. + cSecMaxAge INTEGER DEFAULT NULL, + + PRIMARY KEY (idBuildSrc, tsExpire) +); + + +--- @table TestCases +-- Test case configuration. +-- +-- @remarks This table stores history. Never update or delete anything. The +-- equivalent of deleting is done by setting the 'tsExpire' field to +-- current_timestamp. +-- +CREATE SEQUENCE TestCaseIdSeq + START 1 + INCREMENT BY 1 + NO MAXVALUE + NO MINVALUE + CACHE 1; +CREATE SEQUENCE TestCaseGenIdSeq + START 1 + INCREMENT BY 1 + NO MAXVALUE + NO MINVALUE + CACHE 1; +CREATE TABLE TestCases ( + --- The fixed test case ID. + -- This is assigned when the test case is created and will never change. + idTestCase INTEGER DEFAULT NEXTVAL('TestCaseIdSeq') NOT NULL, + --- When this row starts taking effect (inclusive). + tsEffective TIMESTAMP WITH TIME ZONE DEFAULT current_timestamp NOT NULL, + --- When this row stops being tsEffective (exclusive). + tsExpire TIMESTAMP WITH TIME ZONE DEFAULT TIMESTAMP WITH TIME ZONE 'infinity' NOT NULL, + --- The user id of the one who created/modified this entry. + -- Non-unique foreign key: Users(uid) + uidAuthor INTEGER NOT NULL, + --- Generation ID for this row, a truly unique identifier. + -- This is primarily for referencing by TestSets. + idGenTestCase INTEGER UNIQUE DEFAULT NEXTVAL('TestCaseGenIdSeq') NOT NULL, + + --- The name of the test case. + sName TEXT NOT NULL, + --- Optional test case description. + sDescription TEXT DEFAULT NULL, + --- Indicates whether this test case is currently enabled. + fEnabled BOOLEAN DEFAULT FALSE NOT NULL, + --- Default test case timeout given in seconds. + cSecTimeout INTEGER NOT NULL CHECK (cSecTimeout > 0), + --- Default TestBox requirement expression (python boolean expression). + -- All the scheduler properties are available for use with the same names + -- as in that table. + -- If NULL everything matches. + sTestBoxReqExpr TEXT DEFAULT NULL, + --- Default build requirement expression (python boolean expression). + -- The following build properties are available: sProduct, sBranch, + -- sType, asOsArches, sVersion, iRevision, uidAuthor and idBuild. + -- If NULL everything matches. + sBuildReqExpr TEXT DEFAULT NULL, + + --- The base command. + -- String suitable for executing in bourne shell with space as separator + -- (IFS). References to @BUILD_BINARIES@ will be replaced WITH the content + -- of the Builds(sBinaries) field. + sBaseCmd TEXT NOT NULL, + + --- Comma separated list of test suite zips (or tars) that the testbox will + -- need to download and expand prior to testing. + -- If NULL the current test suite of the scheduling group will be used (the + -- scheduling group will have an optional test suite build queue associated + -- with it). The current test suite can also be referenced by + -- @VALIDATIONKIT_ZIP@ in case more downloads are required. Files may also be + -- uploaded to the test manager download area, in which case the + -- @DOWNLOAD_BASE_URL@ prefix can be used to refer to this area. + sTestSuiteZips TEXT DEFAULT NULL, + + -- Comment regarding a change or something. + sComment TEXT DEFAULT NULL, + + PRIMARY KEY (idTestCase, tsExpire) +); + + +--- @table TestCaseArgs +-- Test case argument list variations. +-- +-- For example, we have a test case that does a set of tests on a virtual +-- machine. To get better code/feature coverage of this testcase we wish to +-- run it with different guest hardware configuration. The test case may do +-- the same stuff, but the guest OS as well as the VMM may react differently to +-- the hardware configurations and uncover issues in the VMM, device emulation +-- or other places. +-- +-- Typical hardware variations are: +-- - guest memory size (RAM), +-- - guest video memory size (VRAM), +-- - virtual CPUs / cores / threads, +-- - virtual chipset +-- - virtual network interface card (NIC) +-- - USB 1.1, USB 2.0, no USB +-- +-- The TM web UI will help the user create a reasonable set of permutations +-- of these parameters, the user specifies a maximum and the TM uses certain +-- rules together with random selection to generate the desired number. The +-- UI will also help suggest fitting testbox requirements according to the +-- RAM/VRAM sizes and the virtual CPU counts. The user may then make +-- adjustments to the suggestions before commit them. +-- +-- Alternatively, the user may also enter all the permutations without any +-- help from the UI. +-- +-- Note! All test cases has at least one entry in this table, even if it is +-- empty, because testbox requirements are specified thru this. +-- +-- Querying the valid parameter lists for a testase this way: +-- SELECT * ... WHERE idTestCase = TestCases.idTestCase +-- AND tsExpire > <when> +-- AND tsEffective <= <when>; +-- +-- Querying the valid parameter list for the latest generation can be +-- simplified by just checking tsExpire date: +-- SELECT * ... WHERE idTestCase = TestCases.idTestCase +-- AND tsExpire == TIMESTAMP WITH TIME ZONE 'infinity'; +-- +-- @remarks This table stores history. Never update or delete anything. The +-- equivalent of deleting is done by setting the 'tsExpire' field to +-- current_timestamp. +-- +CREATE SEQUENCE TestCaseArgsIdSeq + START 1 + INCREMENT BY 1 + NO MAXVALUE + NO MINVALUE + CACHE 1; +CREATE SEQUENCE TestCaseArgsGenIdSeq + START 1 + INCREMENT BY 1 + NO MAXVALUE + NO MINVALUE + CACHE 1; +CREATE TABLE TestCaseArgs ( + --- The test case ID. + -- Non-unique foreign key: TestCases(idTestCase). + idTestCase INTEGER NOT NULL, + --- The testcase argument variation ID (fixed). + -- This is primarily for TestGroupMembers.aidTestCaseArgs. + idTestCaseArgs INTEGER DEFAULT NEXTVAL('TestCaseArgsIdSeq') NOT NULL, + --- When this row starts taking effect (inclusive). + tsEffective TIMESTAMP WITH TIME ZONE DEFAULT current_timestamp NOT NULL, + --- When this row stops being tsEffective (exclusive). + tsExpire TIMESTAMP WITH TIME ZONE DEFAULT TIMESTAMP WITH TIME ZONE 'infinity' NOT NULL, + --- The user id of the one who created/modified this entry. + -- Non-unique foreign key: Users(uid) + uidAuthor INTEGER NOT NULL, + --- Generation ID for this row. + -- This is primarily for efficient referencing by TestSets and SchedQueues. + idGenTestCaseArgs INTEGER UNIQUE DEFAULT NEXTVAL('TestCaseArgsGenIdSeq') NOT NULL, + + --- The additional arguments. + -- String suitable for bourne shell style argument parsing with space as + -- separator (IFS). References to @BUILD_BINARIES@ will be replaced with + -- the content of the Builds(sBinaries) field. + sArgs TEXT NOT NULL, + --- Optional test case timeout given in seconds. + -- If NULL, the TestCases.cSecTimeout field is used instead. + cSecTimeout INTEGER DEFAULT NULL CHECK (cSecTimeout IS NULL OR cSecTimeout > 0), + --- Additional TestBox requirement expression (python boolean expression). + -- All the scheduler properties are available for use with the same names + -- as in that table. This is checked after first checking the requirements + -- in the TestCases.sTestBoxReqExpr field. + sTestBoxReqExpr TEXT DEFAULT NULL, + --- Additional build requirement expression (python boolean expression). + -- The following build properties are available: sProduct, sBranch, + -- sType, asOsArches, sVersion, iRevision, uidAuthor and idBuild. This is + -- checked after first checking the requirements in the + -- TestCases.sBuildReqExpr field. + sBuildReqExpr TEXT DEFAULT NULL, + --- Number of testboxes required (gang scheduling). + cGangMembers SMALLINT DEFAULT 1 NOT NULL CHECK (cGangMembers > 0 AND cGangMembers < 1024), + --- Optional variation sub-name. + sSubName TEXT DEFAULT NULL, + + --- The arguments are part of the primary key for several reasons. + -- No duplicate argument lists (makes no sense - if you want to prioritize + -- argument lists, we add that explicitly). This may hopefully enable us + -- to more easily check coverage later on, even when the test case is + -- reconfigured with more/less permutations. + PRIMARY KEY (idTestCase, tsExpire, sArgs) +); +CREATE INDEX TestCaseArgsLookupIdx ON TestCaseArgs (idTestCase, tsExpire DESC, tsEffective ASC); + + +--- @table TestCaseDeps +-- Test case dependencies (N:M) +-- +-- This effect build selection. The build must have passed all runs of the +-- given prerequisite testcase (idTestCasePreReq) and executed at a minimum one +-- argument list variation. +-- +-- This should also affect scheduling order, if possible at least one +-- prerequisite testcase variation should be place before the specific testcase +-- in the scheduling queue. +-- +-- @remarks This table stores history. Never update or delete anything. The +-- equivalent of deleting is done by setting the 'tsExpire' field to +-- current_timestamp. To select the currently valid entries use +-- tsExpire = TIMESTAMP WITH TIME ZONE 'infinity'. +-- +CREATE TABLE TestCaseDeps ( + --- The test case that depends on someone. + -- Non-unique foreign key: TestCases(idTestCase). + idTestCase INTEGER NOT NULL, + --- The prerequisite test case ID. + -- Non-unique foreign key: TestCases(idTestCase). + idTestCasePreReq INTEGER NOT NULL, + --- When this row starts taking effect (inclusive). + tsEffective TIMESTAMP WITH TIME ZONE DEFAULT current_timestamp NOT NULL, + --- When this row stops being tsEffective (exclusive). + tsExpire TIMESTAMP WITH TIME ZONE DEFAULT TIMESTAMP WITH TIME ZONE 'infinity' NOT NULL, + --- The user id of the one who created/modified this entry. + -- Non-unique foreign key: Users(uid) + uidAuthor INTEGER NOT NULL, + + PRIMARY KEY (idTestCase, idTestCasePreReq, tsExpire) +); + + +--- @table TestCaseGlobalRsrcDeps +-- Test case dependencies on global resources (N:M) +-- +-- @remarks This table stores history. Never update or delete anything. The +-- equivalent of deleting is done by setting the 'tsExpire' field to +-- current_timestamp. To select the currently valid entries use +-- tsExpire = TIMESTAMP WITH TIME ZONE 'infinity'. +-- +CREATE TABLE TestCaseGlobalRsrcDeps ( + --- The test case that depends on someone. + -- Non-unique foreign key: TestCases(idTestCase). + idTestCase INTEGER NOT NULL, + --- The prerequisite resource ID. + -- Non-unique foreign key: GlobalResources(idGlobalRsrc). + idGlobalRsrc INTEGER NOT NULL, + --- When this row starts taking effect (inclusive). + tsEffective TIMESTAMP WITH TIME ZONE DEFAULT current_timestamp NOT NULL, + --- When this row stops being tsEffective (exclusive). + tsExpire TIMESTAMP WITH TIME ZONE DEFAULT TIMESTAMP WITH TIME ZONE 'infinity' NOT NULL, + --- The user id of the one who created/modified this entry. + -- Non-unique foreign key: Users(uid) + uidAuthor INTEGER NOT NULL, + + PRIMARY KEY (idTestCase, idGlobalRsrc, tsExpire) +); + + +--- @table TestGroups +-- Test Group - A collection of test cases. +-- +-- This is for simplifying test configuration by working with a few groups +-- instead of a herd of individual testcases. It may also be used for creating +-- test suites for certain areas (like guest additions) or tasks (like +-- performance measurements). +-- +-- A test case can be member of any number of test groups. +-- +-- @remarks This table stores history. Never update or delete anything. The +-- equivalent of deleting is done by setting the 'tsExpire' field to +-- current_timestamp. To select the currently valid entries use +-- tsExpire = TIMESTAMP WITH TIME ZONE 'infinity'. +-- +CREATE SEQUENCE TestGroupIdSeq + START 1 + INCREMENT BY 1 + NO MAXVALUE + NO MINVALUE + CACHE 1; +CREATE TABLE TestGroups ( + --- The fixed scheduling group ID. + -- This is assigned when the group is created and will never change. + idTestGroup INTEGER DEFAULT NEXTVAL('TestGroupIdSeq') NOT NULL, + --- When this row starts taking effect (inclusive). + tsEffective TIMESTAMP WITH TIME ZONE DEFAULT current_timestamp NOT NULL, + --- When this row stops being tsEffective (exclusive). + tsExpire TIMESTAMP WITH TIME ZONE DEFAULT TIMESTAMP WITH TIME ZONE 'infinity' NOT NULL, + --- The user id of the one who created/modified this entry. + -- Non-unique foreign key: Users(uid) + uidAuthor INTEGER NOT NULL, + + --- The name of the scheduling group. + sName TEXT NOT NULL, + --- Optional group description. + sDescription TEXT, + -- Comment regarding a change or something. + sComment TEXT DEFAULT NULL, + + PRIMARY KEY (idTestGroup, tsExpire) +); +CREATE INDEX TestGroups_id_index ON TestGroups (idTestGroup, tsExpire DESC, tsEffective ASC); + + +--- @table TestGroupMembers +-- The N:M relationship between test case configurations and test groups. +-- +-- @remarks This table stores history. Never update or delete anything. The +-- equivalent of deleting is done by setting the 'tsExpire' field to +-- current_timestamp. To select the currently valid entries use +-- tsExpire = TIMESTAMP WITH TIME ZONE 'infinity'. +-- +CREATE TABLE TestGroupMembers ( + --- The group ID. + -- Non-unique foreign key: TestGroups(idTestGroup). + idTestGroup INTEGER NOT NULL, + --- The test case ID. + -- Non-unique foreign key: TestCases(idTestCase). + idTestCase INTEGER NOT NULL, + --- When this row starts taking effect (inclusive). + tsEffective TIMESTAMP WITH TIME ZONE DEFAULT current_timestamp NOT NULL, + --- When this row stops being tsEffective (exclusive). + tsExpire TIMESTAMP WITH TIME ZONE DEFAULT TIMESTAMP WITH TIME ZONE 'infinity' NOT NULL, + --- The user id of the one who created/modified this entry. + -- Non-unique foreign key: Users(uid) + uidAuthor INTEGER NOT NULL, + + --- Test case scheduling priority. + -- Higher number causes the test case to be run more frequently. + -- @sa SchedGroupMembers.iSchedPriority, TestBoxesInSchedGroups.iSchedPriority + -- @todo Not sure we want to keep this... + iSchedPriority INTEGER DEFAULT 16 CHECK (iSchedPriority >= 0 AND iSchedPriority < 32) NOT NULL, + + --- Limit the memberships to the given argument variations. + -- Non-unique foreign key: TestCaseArgs(idTestCase, idTestCaseArgs). + aidTestCaseArgs INTEGER ARRAY DEFAULT NULL, + + PRIMARY KEY (idTestGroup, idTestCase, tsExpire) +); + + +--- @table SchedGroups +-- Scheduling group (aka. testbox partitioning) configuration. +-- +-- A testbox is associated with exactly one scheduling group. This association +-- can be changed, of course. If we (want to) retire a group which still has +-- testboxes associated with it, these will be moved to the 'default' group. +-- +-- The TM web UI will make sure that a testbox is always in a group and that +-- the default group cannot be deleted. +-- +-- A scheduling group combines several things: +-- - A selection of builds to test (via idBuildSrc). +-- - A collection of test groups to test with (via SchedGroupMembers). +-- - A set of testboxes to test on (via TestBoxes.idSchedGroup). +-- +-- In additions there is an optional source of fresh test suite builds (think +-- VBoxTestSuite) as well as scheduling options. +-- +-- @remarks This table stores history. Never update or delete anything. The +-- equivalent of deleting is done by setting the 'tsExpire' field to +-- current_timestamp. To select the currently valid entries use +-- tsExpire = TIMESTAMP WITH TIME ZONE 'infinity'. +-- +CREATE TYPE Scheduler_T AS ENUM ( + 'bestEffortContinousItegration', + 'reserved' +); +CREATE SEQUENCE SchedGroupIdSeq + START 2 + INCREMENT BY 1 + NO MAXVALUE + NO MINVALUE + CACHE 1; +CREATE TABLE SchedGroups ( + --- The fixed scheduling group ID. + -- This is assigned when the group is created and will never change. + idSchedGroup INTEGER DEFAULT NEXTVAL('SchedGroupIdSeq') NOT NULL, + --- When this row starts taking effect (inclusive). + tsEffective TIMESTAMP WITH TIME ZONE DEFAULT current_timestamp NOT NULL, + --- When this row stops being tsEffective (exclusive). + tsExpire TIMESTAMP WITH TIME ZONE DEFAULT TIMESTAMP WITH TIME ZONE 'infinity' NOT NULL, + --- The user id of the one who created/modified this entry. + -- Non-unique foreign key: Users(uid) + -- @note This is NULL for the default group. + uidAuthor INTEGER DEFAULT NULL, + + --- The name of the scheduling group. + sName TEXT NOT NULL, + --- Optional group description. + sDescription TEXT, + --- Indicates whether this group is currently enabled. + fEnabled boolean NOT NULL, + --- The scheduler to use. + -- This is for when we later desire different scheduling that the best + -- effort stuff provided by the initial implementation. + enmScheduler Scheduler_T DEFAULT 'bestEffortContinousItegration'::Scheduler_T NOT NULL, + --- The build source. + -- Non-unique foreign key: BuildSources(idBuildSrc) + idBuildSrc INTEGER DEFAULT NULL, + --- The Validation Kit build source (@VALIDATIONKIT_ZIP@). + -- Non-unique foreign key: BuildSources(idBuildSrc) + idBuildSrcTestSuite INTEGER DEFAULT NULL, + -- Comment regarding a change or something. + sComment TEXT DEFAULT NULL, + + PRIMARY KEY (idSchedGroup, tsExpire) +); + +-- Special default group. +INSERT INTO SchedGroups (idSchedGroup, tsEffective, tsExpire, sName, sDescription, fEnabled) + VALUES (1, TIMESTAMP WITH TIME ZONE 'epoch', TIMESTAMP WITH TIME ZONE 'infinity', 'default', 'default group', FALSE); + + +--- @table SchedGroupMembers +-- N:M relationship between scheduling groups and test groups. +-- +-- Several scheduling parameters are associated with this relationship. +-- +-- The test group dependency (idTestGroupPreReq) can be used in the same way as +-- TestCaseDeps.idTestCasePreReq, only here on test group level. This means it +-- affects the build selection. The builds needs to have passed all test runs +-- the prerequisite test group and done at least one argument variation of each +-- test case in it. +-- +-- @remarks This table stores history. Never update or delete anything. The +-- equivalent of deleting is done by setting the 'tsExpire' field to +-- current_timestamp. To select the currently valid entries use +-- tsExpire = TIMESTAMP WITH TIME ZONE 'infinity'. +-- +CREATE TABLE SchedGroupMembers ( + --- Scheduling ID. + -- Non-unique foreign key: SchedGroups(idSchedGroup). + idSchedGroup INTEGER NOT NULL, + --- Testgroup ID. + -- Non-unique foreign key: TestGroups(idTestGroup). + idTestGroup INTEGER NOT NULL, + --- When this row starts taking effect (inclusive). + tsEffective TIMESTAMP WITH TIME ZONE DEFAULT current_timestamp NOT NULL, + --- When this row stops being tsEffective (exclusive). + tsExpire TIMESTAMP WITH TIME ZONE DEFAULT TIMESTAMP WITH TIME ZONE 'infinity' NOT NULL, + --- The user id of the one who created/modified this entry. + -- Non-unique foreign key: Users(uid) + uidAuthor INTEGER NOT NULL, + + --- The scheduling priority of the test group. + -- Higher number causes the test case to be run more frequently. + -- @sa TestGroupMembers.iSchedPriority, TestBoxesInSchedGroups.iSchedPriority + iSchedPriority INTEGER DEFAULT 16 CHECK (iSchedPriority >= 0 AND iSchedPriority < 32) NOT NULL, + --- When during the week this group is allowed to start running, NULL means + -- there are no constraints. + -- Each bit in the bitstring represents one hour, with bit 0 indicating the + -- midnight hour on a monday. + bmHourlySchedule bit(168) DEFAULT NULL, + --- Optional test group dependency. + -- Non-unique foreign key: TestGroups(idTestGroup). + -- This is for requiring that a build has been subject to smoke tests + -- before bothering to subject it to longer tests. + -- @todo Not entirely sure this should be here, but I'm not so keen on yet + -- another table as the only use case is smoketests. + idTestGroupPreReq INTEGER DEFAULT NULL, + + PRIMARY KEY (idSchedGroup, idTestGroup, tsExpire) +); + + +--- @table TestBoxStrTab +-- String table for the test boxes. +-- +-- This is a string cache for all string members in TestBoxes except the name. +-- The rational is to avoid duplicating large strings like sReport when the +-- testbox reports a new cMbScratch value or the box when the test sheriff +-- sends a reboot command or similar. +-- +-- At the time this table was introduced, we had 400558 TestBoxes rows, where +-- the SUM(LENGTH(sReport)) was 993MB. There were really just 1066 distinct +-- sReport values, with a total length of 0x3 MB. +-- +-- Nothing is ever deleted from this table. +-- +-- @note Should use a stored procedure to query/insert a string. +-- +-- +-- TestBox stats prior to conversion: +-- SELECT COUNT(*) FROM TestBoxes: 400558 rows +-- SELECT pg_total_relation_size('TestBoxes'): 740794368 bytes (706 MB) +-- Average row cost: 740794368 / 400558 = 1849 bytes/row +-- +-- After conversion: +-- SELECT COUNT(*) FROM TestBoxes: 400558 rows +-- SELECT pg_total_relation_size('TestBoxes'): 144375808 bytes (138 MB) +-- SELECT COUNT(idStr) FROM TestBoxStrTab: 1292 rows +-- SELECT pg_total_relation_size('TestBoxStrTab'): 5709824 bytes (5.5 MB) +-- (144375808 + 5709824) / 740794368 = 20 % +-- Average row cost boxes: 144375808 / 400558 = 360 bytes/row +-- Average row cost strings: 5709824 / 1292 = 4420 bytes/row +-- +CREATE SEQUENCE TestBoxStrTabIdSeq + START 1 + INCREMENT BY 1 + NO MAXVALUE + NO MINVALUE + CACHE 1; +CREATE TABLE TestBoxStrTab ( + --- The ID of this string. + idStr INTEGER PRIMARY KEY DEFAULT NEXTVAL('TestBoxStrTabIdSeq'), + --- The string value. + sValue text NOT NULL, + --- Creation time stamp. + tsCreated TIMESTAMP WITH TIME ZONE DEFAULT current_timestamp NOT NULL +); +-- Note! Must use hash index as the sReport strings are too long for regular indexing. +CREATE INDEX TestBoxStrTabNameIdx ON TestBoxStrTab USING hash (sValue); + +--- Empty string with ID 0. +INSERT INTO TestBoxStrTab (idStr, sValue) VALUES (0, ''); + + +--- @type TestBoxCmd_T +-- Testbox commands. +CREATE TYPE TestBoxCmd_T AS ENUM ( + 'none', + 'abort', + 'reboot', --< This implies abort. Status changes when reaching 'idle'. + 'upgrade', --< This is only handled when asking for work. + 'upgrade-and-reboot', --< Ditto. + 'special' --< Similar to upgrade, reserved for the future. +); + + +--- @type LomKind_T +-- The kind of lights out management on a testbox. +CREATE TYPE LomKind_T AS ENUM ( + 'none', + 'ilom', + 'elom', + 'apple-xserve-lom' +); + + +--- @table TestBoxes +-- Testbox configurations. +-- +-- The testboxes are identified by IP and the system UUID if available. Should +-- the IP change, the testbox will be refused at sign on and the testbox +-- sheriff will have to update it's IP. +-- +-- @todo Implement the UUID stuff. Get it from DMI, UEFI or whereever. +-- Mismatching needs to be logged somewhere... +-- +-- To query the currently valid configuration: +-- SELECT ... WHERE id = idTestBox AND tsExpire = TIMESTAMP WITH TIME ZONE 'infinity'; +-- +-- @remarks This table stores history. Never update or delete anything. The +-- equivalent of deleting is done by setting the 'tsExpire' field to +-- current_timestamp. To select the currently valid entries use +-- tsExpire = TIMESTAMP WITH TIME ZONE 'infinity'. +-- +CREATE SEQUENCE TestBoxIdSeq + START 1 + INCREMENT BY 1 + NO MAXVALUE + NO MINVALUE + CACHE 1; +CREATE SEQUENCE TestBoxGenIdSeq + START 1 + INCREMENT BY 1 + NO MAXVALUE + NO MINVALUE + CACHE 1; +CREATE TABLE TestBoxes ( + --- The fixed testbox ID. + -- This is assigned when the testbox is created and will never change. + idTestBox INTEGER DEFAULT NEXTVAL('TestBoxIdSeq') NOT NULL, + --- When this row starts taking effect (inclusive). + tsEffective TIMESTAMP WITH TIME ZONE DEFAULT current_timestamp NOT NULL, + --- When this row stops being tsEffective (exclusive). + tsExpire TIMESTAMP WITH TIME ZONE DEFAULT TIMESTAMP WITH TIME ZONE 'infinity' NOT NULL, + --- The user id of the one who created/modified this entry. + -- When modified automatically by the testbox, NULL is used. + -- Non-unique foreign key: Users(uid) + uidAuthor INTEGER DEFAULT NULL, + --- Generation ID for this row. + -- This is primarily for referencing by TestSets. + idGenTestBox INTEGER UNIQUE DEFAULT NEXTVAL('TestBoxGenIdSeq') NOT NULL, + + --- The testbox IP. + -- This is from the webserver point of view and automatically updated on + -- SIGNON. The test setup doesn't permit for IP addresses to change while + -- the testbox is operational, because this will break gang tests. + ip inet NOT NULL, + --- The system or firmware UUID. + -- This uniquely identifies the testbox when talking to the server. After + -- SIGNON though, the testbox will also provide idTestBox and ip to + -- establish its identity beyond doubt. + uuidSystem uuid NOT NULL, + --- The testbox name. + -- Usually similar to the DNS name. + sName text NOT NULL, + --- Optional testbox description. + -- Intended for describing the box as well as making other relevant notes. + idStrDescription INTEGER REFERENCES TestBoxStrTab(idStr) DEFAULT NULL, + + --- Indicates whether this testbox is enabled. + -- A testbox gets disabled when we're doing maintenance, debugging a issue + -- that happens only on that testbox, or some similar stuff. This is an + -- alternative to deleting the testbox. + fEnabled BOOLEAN DEFAULT NULL, + + --- The kind of lights-out-management. + enmLomKind LomKind_T DEFAULT 'none'::LomKind_T NOT NULL, + --- The IP adress of the lights-out-management. + -- This can be NULL if enmLomKind is 'none', otherwise it must contain a valid address. + ipLom inet DEFAULT NULL, + + --- Timeout scale factor, given as a percent. + -- This is a crude adjustment of the test case timeout for slower hardware. + pctScaleTimeout smallint DEFAULT 100 NOT NULL CHECK (pctScaleTimeout > 10 AND pctScaleTimeout < 20000), + + --- Change comment or similar. + idStrComment INTEGER REFERENCES TestBoxStrTab(idStr) DEFAULT NULL, + + --- @name Scheduling properties (reported by testbox script). + -- @{ + --- Same abbrieviations as kBuild, see KBUILD_OSES. + idStrOs INTEGER REFERENCES TestBoxStrTab(idStr) DEFAULT NULL, + --- Informational, no fixed format. + idStrOsVersion INTEGER REFERENCES TestBoxStrTab(idStr) DEFAULT NULL, + --- Same as CPUID reports (GenuineIntel, AuthenticAMD, CentaurHauls, ...). + idStrCpuVendor INTEGER REFERENCES TestBoxStrTab(idStr) DEFAULT NULL, + --- Same as kBuild - x86, amd64, ... See KBUILD_ARCHES. + idStrCpuArch INTEGER REFERENCES TestBoxStrTab(idStr) DEFAULT NULL, + --- The CPU name if available. + idStrCpuName INTEGER REFERENCES TestBoxStrTab(idStr) DEFAULT NULL, + --- Number identifying the CPU family/model/stepping/whatever. + -- For x86 and AMD64 type CPUs, this will on the following format: + -- (EffFamily << 24) | (EffModel << 8) | Stepping. + lCpuRevision bigint DEFAULT NULL, + --- Number of CPUs, CPU cores and CPU threads. + cCpus smallint DEFAULT NULL CHECK (cCpus IS NULL OR cCpus > 0), + --- Set if capable of hardware virtualization. + fCpuHwVirt boolean DEFAULT NULL, + --- Set if capable of nested paging. + fCpuNestedPaging boolean DEFAULT NULL, + --- Set if CPU capable of 64-bit (VBox) guests. + fCpu64BitGuest boolean DEFAULT NULL, + --- Set if chipset with usable IOMMU (VT-d / AMD-Vi). + fChipsetIoMmu boolean DEFAULT NULL, + --- Set if the test box does raw-mode tests. + fRawMode boolean DEFAULT NULL, + --- The (approximate) memory size in megabytes (rounded down to nearest 4 MB). + cMbMemory bigint DEFAULT NULL CHECK (cMbMemory IS NULL OR cMbMemory > 0), + --- The amount of scratch space in megabytes (rounded down to nearest 64 MB). + cMbScratch bigint DEFAULT NULL CHECK (cMbScratch IS NULL OR cMbScratch >= 0), + --- Free form hardware and software report field. + idStrReport INTEGER REFERENCES TestBoxStrTab(idStr) DEFAULT NULL, + --- @} + + --- The testbox script revision number, serves the purpose of a version number. + -- Probably good to have when scheduling upgrades as well for status purposes. + iTestBoxScriptRev INTEGER DEFAULT 0 NOT NULL, + --- The python sys.hexversion (layed out as of 2.7). + -- Good to know which python versions we need to support. + iPythonHexVersion INTEGER DEFAULT NULL, + + --- Pending command. + -- @note We put it here instead of in TestBoxStatuses to get history. + enmPendingCmd TestBoxCmd_T DEFAULT 'none'::TestBoxCmd_T NOT NULL, + + PRIMARY KEY (idTestBox, tsExpire), + + --- Nested paging requires hardware virtualization. + CHECK (fCpuNestedPaging IS NULL OR (fCpuNestedPaging <> TRUE OR fCpuHwVirt = TRUE)) +); +CREATE UNIQUE INDEX TestBoxesUuidIdx ON TestBoxes (uuidSystem, tsExpire DESC); +CREATE INDEX TestBoxesExpireEffectiveIdx ON TestBoxes (tsExpire DESC, tsEffective ASC); + + +-- +-- Create a view for TestBoxes where the strings are resolved. +-- +CREATE VIEW TestBoxesWithStrings AS + SELECT TestBoxes.*, + Str1.sValue AS sDescription, + Str2.sValue AS sComment, + Str3.sValue AS sOs, + Str4.sValue AS sOsVersion, + Str5.sValue AS sCpuVendor, + Str6.sValue AS sCpuArch, + Str7.sValue AS sCpuName, + Str8.sValue AS sReport + FROM TestBoxes + LEFT OUTER JOIN TestBoxStrTab Str1 ON idStrDescription = Str1.idStr + LEFT OUTER JOIN TestBoxStrTab Str2 ON idStrComment = Str2.idStr + LEFT OUTER JOIN TestBoxStrTab Str3 ON idStrOs = Str3.idStr + LEFT OUTER JOIN TestBoxStrTab Str4 ON idStrOsVersion = Str4.idStr + LEFT OUTER JOIN TestBoxStrTab Str5 ON idStrCpuVendor = Str5.idStr + LEFT OUTER JOIN TestBoxStrTab Str6 ON idStrCpuArch = Str6.idStr + LEFT OUTER JOIN TestBoxStrTab Str7 ON idStrCpuName = Str7.idStr + LEFT OUTER JOIN TestBoxStrTab Str8 ON idStrReport = Str8.idStr; + + +--- @table TestBoxesInSchedGroups +-- N:M relationship between test boxes and scheduling groups. +-- +-- We associate a priority with this relationship. +-- +-- @remarks This table stores history. Never update or delete anything. The +-- equivalent of deleting is done by setting the 'tsExpire' field to +-- current_timestamp. To select the currently valid entries use +-- tsExpire = TIMESTAMP WITH TIME ZONE 'infinity'. +-- +CREATE TABLE TestBoxesInSchedGroups ( + --- TestBox ID. + -- Non-unique foreign key: TestBoxes(idTestBox). + idTestBox INTEGER NOT NULL, + --- Scheduling ID. + -- Non-unique foreign key: SchedGroups(idSchedGroup). + idSchedGroup INTEGER NOT NULL, + --- When this row starts taking effect (inclusive). + tsEffective TIMESTAMP WITH TIME ZONE DEFAULT current_timestamp NOT NULL, + --- When this row stops being tsEffective (exclusive). + tsExpire TIMESTAMP WITH TIME ZONE DEFAULT TIMESTAMP WITH TIME ZONE 'infinity' NOT NULL, + --- The user id of the one who created/modified this entry. + -- Non-unique foreign key: Users(uid) + uidAuthor INTEGER NOT NULL, + + --- The scheduling priority of the scheduling group for the test box. + -- Higher number causes the scheduling group to be serviced more frequently. + -- @sa TestGroupMembers.iSchedPriority, SchedGroups.iSchedPriority + iSchedPriority INTEGER DEFAULT 16 CHECK (iSchedPriority >= 0 AND iSchedPriority < 32) NOT NULL, + + PRIMARY KEY (idTestBox, idSchedGroup, tsExpire) +); + + +-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- +-- +-- F a i l u r e T r a c k i n g +-- +-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- + + +--- @table FailureCategories +-- Failure categories. +-- +-- This is for organizing the failure reasons. +-- +-- @remarks This table stores history. Never update or delete anything. The +-- equivalent of deleting is done by setting the 'tsExpire' field to +-- current_timestamp. To select the currently valid entries use +-- tsExpire = TIMESTAMP WITH TIME ZONE 'infinity'. +-- +CREATE SEQUENCE FailureCategoryIdSeq + START 1 + INCREMENT BY 1 + NO MAXVALUE + NO MINVALUE + CACHE 1; +CREATE TABLE FailureCategories ( + --- The identifier of this failure category (once assigned, it will never change). + idFailureCategory INTEGER DEFAULT NEXTVAL('FailureCategoryIdSeq') NOT NULL, + --- When this row starts taking effect (inclusive). + tsEffective TIMESTAMP WITH TIME ZONE DEFAULT current_timestamp NOT NULL, + --- When this row stops being tsEffective (exclusive). + tsExpire TIMESTAMP WITH TIME ZONE DEFAULT TIMESTAMP WITH TIME ZONE 'infinity' NOT NULL, + --- The user id of the one who created/modified this entry. + -- Non-unique foreign key: Users(uid) + uidAuthor INTEGER NOT NULL, + --- The short category description. + -- For combo boxes and other selection lists. + sShort text NOT NULL, + --- Full description + -- For cursor-over-poppups for instance. + sFull text NOT NULL, + + PRIMARY KEY (idFailureCategory, tsExpire) +); + + +--- @table FailureReasons +-- Failure reasons. +-- +-- When analysing a test failure, the testbox sheriff will try assign a fitting +-- reason for the failure. This table is here to help the sheriff in his/hers +-- job as well as developers looking checking if their changes affected the +-- test results in any way. +-- +-- @remarks This table stores history. Never update or delete anything. The +-- equivalent of deleting is done by setting the 'tsExpire' field to +-- current_timestamp. To select the currently valid entries use +-- tsExpire = TIMESTAMP WITH TIME ZONE 'infinity'. +-- +CREATE SEQUENCE FailureReasonIdSeq + START 1 + INCREMENT BY 1 + NO MAXVALUE + NO MINVALUE + CACHE 1; +CREATE TABLE FailureReasons ( + --- The identifier of this failure reason (once assigned, it will never change). + idFailureReason INTEGER DEFAULT NEXTVAL('FailureReasonIdSeq') NOT NULL, + --- When this row starts taking effect (inclusive). + tsEffective TIMESTAMP WITH TIME ZONE DEFAULT current_timestamp NOT NULL, + --- When this row stops being tsEffective (exclusive). + tsExpire TIMESTAMP WITH TIME ZONE DEFAULT TIMESTAMP WITH TIME ZONE 'infinity' NOT NULL, + --- The user id of the one who created/modified this entry. + -- Non-unique foreign key: Users(uid) + uidAuthor INTEGER NOT NULL, + + --- The failure category this reason belongs to. + -- Non-unique foreign key: FailureCategories(idFailureCategory) + idFailureCategory INTEGER NOT NULL, + --- The short failure description. + -- For combo boxes and other selection lists. + sShort text NOT NULL, + --- Full failure description. + sFull text NOT NULL, + --- Ticket number in the primary bugtracker. + iTicket INTEGER DEFAULT NULL, + --- Other URLs to reports or discussions of the observed symptoms. + asUrls text ARRAY DEFAULT NULL, + + PRIMARY KEY (idFailureReason, tsExpire) +); +CREATE INDEX FailureReasonsCategoryIdx ON FailureReasons (idFailureCategory, idFailureReason); + + + +--- @table TestResultFailures +-- This is for tracking/discussing test result failures. +-- +-- The rational for putting this is a separate table is that we need history on +-- this while TestResults does not. +-- +-- @remarks This table stores history. Never update or delete anything. The +-- equivalent of deleting is done by setting the 'tsExpire' field to +-- current_timestamp. To select the currently valid entries use +-- tsExpire = TIMESTAMP WITH TIME ZONE 'infinity'. +-- +CREATE TABLE TestResultFailures ( + --- The test result we're disucssing. + -- @note The foreign key is declared after TestResults (further down). + idTestResult INTEGER NOT NULL, + --- When this row starts taking effect (inclusive). + tsEffective TIMESTAMP WITH TIME ZONE DEFAULT current_timestamp NOT NULL, + --- When this row stops being tsEffective (exclusive). + tsExpire TIMESTAMP WITH TIME ZONE DEFAULT TIMESTAMP WITH TIME ZONE 'infinity' NOT NULL, + --- The user id of the one who created/modified this entry. + -- Non-unique foreign key: Users(uid) + uidAuthor INTEGER NOT NULL, + --- The testsest this result is a part of. + -- This is mainly an aid for bypassing the enormous TestResults table. + -- Note! This is a foreign key, but we have to add it after TestSets has + -- been created, see further down. + idTestSet INTEGER NOT NULL, + + --- The suggested failure reason. + -- Non-unique foreign key: FailureReasons(idFailureReason) + idFailureReason INTEGER NOT NULL, + --- Optional comment. + sComment text DEFAULT NULL, + + PRIMARY KEY (idTestResult, tsExpire) +); +CREATE INDEX TestResultFailureIdx ON TestResultFailures (idTestSet, tsExpire DESC, tsEffective ASC); +CREATE INDEX TestResultFailureIdx2 ON TestResultFailures (idTestResult, tsExpire DESC, tsEffective ASC); +CREATE INDEX TestResultFailureIdx3 ON TestResultFailures (idFailureReason, idTestResult, tsExpire DESC, tsEffective ASC); + + + + +-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- +-- +-- T e s t I n p u t +-- +-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- + + +--- @table BuildBlacklist +-- Table used to blacklist sets of builds. +-- +-- The best usage example is a VMM developer realizing that a change causes the +-- host to panic, hang, or otherwise misbehave. To prevent the testbox sheriff +-- from repeatedly having to reboot testboxes, the builds gets blacklisted +-- until there is a working build again. This may mean adding an open ended +-- blacklist spec and then updating it with the final revision number once the +-- fix has been committed. +-- +-- @remarks This table stores history. Never update or delete anything. The +-- equivalent of deleting is done by setting the 'tsExpire' field to +-- current_timestamp. To select the currently valid entries use +-- tsExpire = TIMESTAMP WITH TIME ZONE 'infinity'. +-- +-- @todo Would be nice if we could replace the text strings below with a set of +-- BuildCategories, or sore it in any other way which would enable us to +-- do a negative join with build category... The way it is specified +-- now, it looks like we have to open a cursor of prospecitve builds and +-- filter then thru this table one by one. +-- +-- Any better representation is welcome, but this is low prioirty for +-- now, as it's relatively easy to change this later one. +-- +CREATE SEQUENCE BuildBlacklistIdSeq + START 1 + INCREMENT BY 1 + NO MAXVALUE + NO MINVALUE + CACHE 1; +CREATE TABLE BuildBlacklist ( + --- The blacklist entry id. + -- This stays constant over time. + idBlacklisting INTEGER DEFAULT NEXTVAL('BuildBlacklistIdSeq') NOT NULL, + --- When this row starts taking effect (inclusive). + tsEffective TIMESTAMP WITH TIME ZONE DEFAULT current_timestamp NOT NULL, + --- When this row stops being tsEffective (exclusive). + tsExpire TIMESTAMP WITH TIME ZONE DEFAULT TIMESTAMP WITH TIME ZONE 'infinity' NOT NULL, + --- The user id of the one who created/modified this entry. + -- Non-unique foreign key: Users(uid) + uidAuthor INTEGER NOT NULL, + + --- The reason for the blacklisting. + -- Non-unique foreign key: FailureReasons(idFailureReason) + idFailureReason INTEGER NOT NULL, + + --- Which product. + -- ASSUME that it is okay to limit a blacklisting to a single product. + sProduct text NOT NULL, + --- Which branch. + -- ASSUME that it is okay to limit a blacklisting to a branch. + sBranch text NOT NULL, + + --- Build types to include, all matches if NULL. + asTypes text ARRAY DEFAULT NULL, + --- Array of the 'sOs.sCpuArch' to match, all matches if NULL. + -- See KBUILD_OSES in kBuild for a list of standard target OSes, and + -- KBUILD_ARCHES for a list of standard architectures. + -- + -- @remarks See marks on 'os-agnostic' and 'noarch' in BuildCategories. + asOsArches text ARRAY DEFAULT NULL, + + --- The first subversion tree revision to blacklist. + iFirstRevision INTEGER NOT NULL, + --- The last subversion tree revision to blacklist, no upper limit if NULL. + iLastRevision INTEGER NOT NULL, + + PRIMARY KEY (idBlacklisting, tsExpire) +); +CREATE INDEX BuildBlacklistIdx ON BuildBlacklist (iLastRevision DESC, iFirstRevision ASC, sProduct, sBranch, + tsExpire DESC, tsEffective ASC); + +--- @table BuildCategories +-- Build categories. +-- +-- The purpose of this table is saving space in the Builds table and hopefully +-- speed things up when selecting builds as well (compared to selecting on 4 +-- text fields in the much larger Builds table). +-- +-- Insert only table, no update, no delete. History is not needed. +-- +CREATE SEQUENCE BuildCategoryIdSeq + START 1 + INCREMENT BY 1 + NO MAXVALUE + NO MINVALUE + CACHE 1; +CREATE TABLE BuildCategories ( + --- The build type identifier. + idBuildCategory INTEGER PRIMARY KEY DEFAULT NEXTVAL('BuildCategoryIdSeq') NOT NULL, + --- Product. + -- The product name. For instance 'VBox' or 'VBoxTestSuite'. + sProduct TEXT NOT NULL, + --- The version control repository name. + sRepository TEXT NOT NULL, + --- The branch name (in the version control system). + sBranch TEXT NOT NULL, + --- The build type. + -- See KBUILD_BLD_TYPES in kBuild for a list of standard build types. + sType TEXT NOT NULL, + --- Array of the 'sOs.sCpuArch' supported by the build. + -- See KBUILD_OSES in kBuild for a list of standard target OSes, and + -- KBUILD_ARCHES for a list of standard architectures. + -- + -- @remarks 'os-agnostic' is used if the build doesn't really target any + -- specific OS or if it targets all applicable OSes. + -- 'noarch' is used if the build is architecture independent or if + -- all applicable architectures are handled. + -- Thus, 'os-agnostic.noarch' will run on all build boxes. + -- + -- @note The array shall be sorted ascendingly to prevent unnecessary duplicates! + -- + asOsArches TEXT ARRAY NOT NULL, + + UNIQUE (sProduct, sRepository, sBranch, sType, asOsArches) +); + + +--- @table Builds +-- The builds table contains builds from the tinderboxes and oaccasionally from +-- developers. +-- +-- The tinderbox side could be fed by a batch job enumerating the build output +-- directories every so often, looking for new builds. Or we could query them +-- from the tinderbox database. Yet another alternative is making the +-- tinderbox server or client side software inform us about all new builds. +-- +-- The developer builds are entered manually thru the TM web UI. They are used +-- for subjecting new code to some larger scale testing before commiting, +-- enabling, or merging a private branch. +-- +-- The builds are being selected from this table by the via the build source +-- specification that SchedGroups.idBuildSrc and +-- SchedGroups.idBuildSrcTestSuite links to. +-- +-- @remarks This table stores history. Never update or delete anything. The +-- equivalent of deleting is done by setting the 'tsExpire' field to +-- current_timestamp. To select the currently valid entries use +-- tsExpire = TIMESTAMP WITH TIME ZONE 'infinity'. +-- +CREATE SEQUENCE BuildIdSeq + START 1 + INCREMENT BY 1 + NO MAXVALUE + NO MINVALUE + CACHE 1; +CREATE TABLE Builds ( + --- The build identifier. + -- This remains unchanged + idBuild INTEGER DEFAULT NEXTVAL('BuildIdSeq') NOT NULL, + --- When this build was created or entered into the database. + -- This remains unchanged + tsCreated TIMESTAMP WITH TIME ZONE DEFAULT current_timestamp NOT NULL, + --- When this row starts taking effect (inclusive). + tsEffective TIMESTAMP WITH TIME ZONE DEFAULT current_timestamp NOT NULL, + --- When this row stops being tsEffective (exclusive). + tsExpire TIMESTAMP WITH TIME ZONE DEFAULT TIMESTAMP WITH TIME ZONE 'infinity' NOT NULL, + --- The user id of the one who created/modified this entry. + -- Non-unique foreign key: Users(uid) + -- @note This is NULL if added by a batch job / tinderbox. + uidAuthor INTEGER DEFAULT NULL, + --- The build category. + idBuildCategory INTEGER REFERENCES BuildCategories(idBuildCategory) NOT NULL, + --- The subversion tree revision of the build. + iRevision INTEGER NOT NULL, + --- The product version number (suitable for RTStrVersionCompare). + sVersion TEXT NOT NULL, + --- The link to the tinderbox log of this build. + sLogUrl TEXT, + --- Comma separated list of binaries. + -- The binaries have paths relative to the TESTBOX_PATH_BUILDS or full URLs. + sBinaries TEXT NOT NULL, + --- Set when the binaries gets deleted by the build quota script. + fBinariesDeleted BOOLEAN DEFAULT FALSE NOT NULL, + + UNIQUE (idBuild, tsExpire) +); +CREATE INDEX BuildsLookupIdx ON Builds (idBuildCategory, iRevision); + + +--- @table VcsRevisions +-- This table is for translating build revisions into commit details. +-- +-- For graphs and test results, it would be useful to translate revisions into +-- dates and maybe provide commit message and the committer. +-- +-- Data is entered exclusively thru one or more batch jobs, so no internal +-- authorship needed. Also, since we're mirroring data from external sources +-- here, the batch job is allowed to update/replace existing records. +-- +-- @todo We we could collect more info from the version control systems, if we +-- believe it's useful and can be presented in a reasonable manner. +-- Getting a list of affected files would be simple (requires +-- a separate table with a M:1 relationship to this table), or try +-- associate a commit to a branch. +-- +CREATE TABLE VcsRevisions ( + --- The version control tree name. + sRepository TEXT NOT NULL, + --- The version control tree revision number. + iRevision INTEGER NOT NULL, + --- When the revision was created (committed). + tsCreated TIMESTAMP WITH TIME ZONE NOT NULL, + --- The name of the committer. + -- @note Not to be confused with uidAuthor and test manager users. + sAuthor TEXT, + --- The commit message. + sMessage TEXT, + + UNIQUE (sRepository, iRevision) +); +CREATE INDEX VcsRevisionsByDate ON VcsRevisions (tsCreated DESC); + + +--- @table VcsBugReferences +-- This is for relating commits to a bug and vice versa. +-- +-- This feature isn't so much for the test manager as a cheap way of extending +-- bug trackers without VCS integration. We just need to parse the commit +-- messages when inserting them into the VcsRevisions table. +-- +-- Same input, updating and history considerations as VcsRevisions. +-- +CREATE TABLE VcsBugReferences ( + --- The version control tree name. + sRepository TEXT NOT NULL, + --- The version control tree revision number. + iRevision INTEGER NOT NULL, + --- The bug tracker identifier - see g_kdBugTrackers in config.py. + sBugTracker CHAR(4) NOT NULL, + --- The bug number in the bug tracker. + lBugNo BIGINT NOT NULL, + + UNIQUE (sRepository, iRevision, sBugTracker, lBugNo) +); +CREATE INDEX VcsBugReferencesLookupIdx ON VcsBugReferences (sBugTracker, lBugNo); + + + + +-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- +-- +-- T e s t R e s u l t s +-- +-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- + + +--- @table TestResultStrTab +-- String table for the test results. +-- +-- This is a string cache for value names, test names and possible more, that +-- is frequently repated in the test results record for each test run. The +-- purpose is not only to save space, but to make datamining queries faster by +-- giving them integer fields to work on instead of text fields. There may +-- possibly be some benefits on INSERT as well as there are only integer +-- indexes. +-- +-- Nothing is ever deleted from this table. +-- +-- @note Should use a stored procedure to query/insert a string. +-- +CREATE SEQUENCE TestResultStrTabIdSeq + START 1 + INCREMENT BY 1 + NO MAXVALUE + NO MINVALUE + CACHE 1; +CREATE TABLE TestResultStrTab ( + --- The ID of this string. + idStr INTEGER PRIMARY KEY DEFAULT NEXTVAL('TestResultStrTabIdSeq'), + --- The string value. + sValue text NOT NULL, + --- Creation time stamp. + tsCreated TIMESTAMP WITH TIME ZONE DEFAULT current_timestamp NOT NULL +); +CREATE UNIQUE INDEX TestResultStrTabNameIdx ON TestResultStrTab (sValue); + +--- Empty string with ID 0. +INSERT INTO TestResultStrTab (idStr, sValue) VALUES (0, ''); + + +--- @type TestStatus_T +-- The status of a test (set / result). +-- +CREATE TYPE TestStatus_T AS ENUM ( + -- Initial status: + 'running', + -- Final statuses: + 'success', + -- Final status: Test didn't fail as such, it was something else. + 'skipped', + 'bad-testbox', + 'aborted', + -- Final status: Test failed. + 'failure', + 'timed-out', + 'rebooted' +); + + +--- @table TestResults +-- Test results - a recursive bundle of joy! +-- +-- A test case will be created when the testdriver calls reporter.testStart and +-- concluded with reporter.testDone. The testdriver (or it subordinates) can +-- use these methods to create nested test results. For IPRT based test cases, +-- RTTestCreate, RTTestInitAndCreate and RTTestSub will both create new test +-- result records, where as RTTestSubDone, RTTestSummaryAndDestroy and +-- RTTestDestroy will conclude records. +-- +-- By concluding is meant updating the status. When the test driver reports +-- success, we check it against reported results. (paranoia strikes again!) +-- +-- Nothing is ever deleted from this table. +-- +-- @note As seen below, several other tables associate data with a +-- test result, and the top most test result is referenced by the +-- test set. +-- +CREATE SEQUENCE TestResultIdSeq + START 1 + INCREMENT BY 1 + NO MAXVALUE + NO MINVALUE + CACHE 1; +CREATE TABLE TestResults ( + --- The ID of this test result. + idTestResult INTEGER PRIMARY KEY DEFAULT NEXTVAL('TestResultIdSeq'), + --- The parent test result. + -- This is NULL for the top test result. + idTestResultParent INTEGER REFERENCES TestResults(idTestResult), + --- The test set this result is a part of. + -- Note! This is a foreign key, but we have to add it after TestSets has + -- been created, see further down. + idTestSet INTEGER NOT NULL, + --- Creation time stamp. This may also be the timestamp of when the test started. + tsCreated TIMESTAMP WITH TIME ZONE DEFAULT current_timestamp NOT NULL, + --- The elapsed time for this test. + -- This is either reported by the directly (with some sanity checking) or + -- calculated (current_timestamp - created_ts). + -- @todo maybe use a nanosecond field here, check with what + tsElapsed interval DEFAULT NULL, + --- The test name. + idStrName INTEGER REFERENCES TestResultStrTab(idStr) NOT NULL, + --- The error count. + cErrors INTEGER DEFAULT 0 NOT NULL, + --- The test status. + enmStatus TestStatus_T DEFAULT 'running'::TestStatus_T NOT NULL, + --- Nesting depth. + iNestingDepth smallint NOT NULL CHECK (iNestingDepth >= 0 AND iNestingDepth < 16), + -- Make sure errors and status match up. + CONSTRAINT CheckStatusMatchesErrors + CHECK ( (cErrors > 0 AND enmStatus IN ('running'::TestStatus_T, + 'failure'::TestStatus_T, 'timed-out'::TestStatus_T, 'rebooted'::TestStatus_T )) + OR (cErrors = 0 AND enmStatus IN ('running'::TestStatus_T, 'success'::TestStatus_T, + 'skipped'::TestStatus_T, 'aborted'::TestStatus_T, 'bad-testbox'::TestStatus_T)) + ), + -- The following is for the TestResultFailures foreign key. + -- Note! This was added with the name TestResults_idTestResult_idTestSet_key in the tmdb-r16 update script. + UNIQUE (idTestResult, idTestSet) +); + +CREATE INDEX TestResultsSetIdx ON TestResults (idTestSet, idStrName, idTestResult); +CREATE INDEX TestResultsParentIdx ON TestResults (idTestResultParent); +-- The TestResultsNameIdx and TestResultsNameIdx2 are for speeding up the result graph & reporting code. +CREATE INDEX TestResultsNameIdx ON TestResults (idStrName, tsCreated DESC); +CREATE INDEX TestResultsNameIdx2 ON TestResults (idTestResult, idStrName); + +ALTER TABLE TestResultFailures ADD CONSTRAINT TestResultFailures_idTestResult_idTestSet_fkey + FOREIGN KEY (idTestResult, idTestSet) REFERENCES TestResults(idTestResult, idTestSet) MATCH FULL; + + +--- @table TestResultValues +-- Test result values. +-- +-- A testdriver or subordinate may report a test value via +-- reporter.testValue(), while IPRT based test will use RTTestValue and +-- associates. +-- +-- This is an insert only table, no deletes, no updates. +-- +CREATE SEQUENCE TestResultValueIdSeq + START 1 + INCREMENT BY 1 + NO MAXVALUE + NO MINVALUE + CACHE 1; +CREATE TABLE TestResultValues ( + --- The ID of this value. + idTestResultValue INTEGER PRIMARY KEY DEFAULT NEXTVAL('TestResultValueIdSeq'), + --- The test result it was reported within. + idTestResult INTEGER REFERENCES TestResults(idTestResult) NOT NULL, + --- The test set this value is a part of (for avoiding joining thru TestResults). + -- Note! This is a foreign key, but we have to add it after TestSets has + -- been created, see further down. + idTestSet INTEGER NOT NULL, + --- Creation time stamp. + tsCreated TIMESTAMP WITH TIME ZONE DEFAULT current_timestamp NOT NULL, + --- The name. + idStrName INTEGER REFERENCES TestResultStrTab(idStr) NOT NULL, + --- The value. + lValue bigint NOT NULL, + --- The unit. + -- @todo This is currently not defined properly. Will fix/correlate this + -- with the other places we use unit (IPRT/testdriver/VMMDev). + iUnit smallint NOT NULL CHECK (iUnit >= 0 AND iUnit < 1024) +); + +CREATE INDEX TestResultValuesIdx ON TestResultValues(idTestResult); +-- The TestResultValuesGraphIdx is for speeding up the result graph & reporting code. +CREATE INDEX TestResultValuesGraphIdx ON TestResultValues(idStrName, tsCreated); +-- The TestResultValuesLogIdx is for speeding up the log viewer. +CREATE INDEX TestResultValuesLogIdx ON TestResultValues(idTestSet, tsCreated); + + +--- @table TestResultFiles +-- Test result files. +-- +-- A testdriver or subordinate may report a file by using +-- reporter.addFile() or reporter.addLogFile(). +-- +-- The files stored here as well as the primary log file will be processed by a +-- batch job and compressed if considered compressable. Thus, TM will look for +-- files with a .gz/.bz2 suffix first and then without a suffix. +-- +-- This is an insert only table, no deletes, no updates. +-- +CREATE SEQUENCE TestResultFileId + START 1 + INCREMENT BY 1 + NO MAXVALUE + NO MINVALUE + CACHE 1; +CREATE TABLE TestResultFiles ( + --- The ID of this file. + idTestResultFile INTEGER PRIMARY KEY DEFAULT NEXTVAL('TestResultFileId'), + --- The test result it was reported within. + idTestResult INTEGER REFERENCES TestResults(idTestResult) NOT NULL, + --- The test set this file is a part of (for avoiding joining thru TestResults). + -- Note! This is a foreign key, but we have to add it after TestSets has + -- been created, see further down. + idTestSet INTEGER NOT NULL, + --- Creation time stamp. + tsCreated TIMESTAMP WITH TIME ZONE DEFAULT current_timestamp NOT NULL, + --- The filename relative to TestSets(sBaseFilename) + '-'. + -- The set of valid filename characters should be very limited so that no + -- file system issues can occure either on the TM side or the user when + -- loading the files. Tests trying to use other characters will fail. + -- Valid character regular expession: '^[a-zA-Z0-9_-(){}#@+,.=]*$' + idStrFile INTEGER REFERENCES TestResultStrTab(idStr) NOT NULL, + --- The description. + idStrDescription INTEGER REFERENCES TestResultStrTab(idStr) NOT NULL, + --- The kind of file. + -- For instance: 'log/release/vm', + -- 'screenshot/failure', + -- 'screencapture/failure', + -- 'xmllog/somestuff' + idStrKind INTEGER REFERENCES TestResultStrTab(idStr) NOT NULL, + --- The mime type for the file. + -- For instance: 'text/plain', + -- 'image/png', + -- 'video/webm', + -- 'text/xml' + idStrMime INTEGER REFERENCES TestResultStrTab(idStr) NOT NULL +); + +CREATE INDEX TestResultFilesIdx ON TestResultFiles(idTestResult); +CREATE INDEX TestResultFilesIdx2 ON TestResultFiles(idTestSet, tsCreated DESC); + + +--- @table TestResultMsgs +-- Test result message. +-- +-- A testdriver or subordinate may report a message via the sDetails parameter +-- of the reporter.testFailure() method, while IPRT test cases will use +-- RTTestFailed, RTTestPrintf and their friends. For RTTestPrintf, we will +-- ignore the more verbose message levels since these can also be found in one +-- of the logs. +-- +-- This is an insert only table, no deletes, no updates. +-- +CREATE TYPE TestResultMsgLevel_T AS ENUM ( + 'failure', + 'info' +); +CREATE SEQUENCE TestResultMsgIdSeq + START 1 + INCREMENT BY 1 + NO MAXVALUE + NO MINVALUE + CACHE 1; +CREATE TABLE TestResultMsgs ( + --- The ID of this file. + idTestResultMsg INTEGER PRIMARY KEY DEFAULT NEXTVAL('TestResultMsgIdSeq'), + --- The test result it was reported within. + idTestResult INTEGER REFERENCES TestResults(idTestResult) NOT NULL, + --- The test set this file is a part of (for avoiding joining thru TestResults). + -- Note! This is a foreign key, but we have to add it after TestSets has + -- been created, see further down. + idTestSet INTEGER NOT NULL, + --- Creation time stamp. + tsCreated TIMESTAMP WITH TIME ZONE DEFAULT current_timestamp NOT NULL, + --- The message string. + idStrMsg INTEGER REFERENCES TestResultStrTab(idStr) NOT NULL, + --- The message level. + enmLevel TestResultMsgLevel_T NOT NULL +); + +CREATE INDEX TestResultMsgsIdx ON TestResultMsgs(idTestResult); +CREATE INDEX TestResultMsgsIdx2 ON TestResultMsgs(idTestSet, tsCreated DESC); + + +--- @table TestSets +-- Test sets / Test case runs. +-- +-- This is where we collect data about test runs. +-- +-- @todo Not entirely sure where the 'test set' term came from. Consider +-- finding something more appropriate. +-- +CREATE SEQUENCE TestSetIdSeq + START 1 + INCREMENT BY 1 + NO MAXVALUE + NO MINVALUE + CACHE 1; +CREATE TABLE TestSets ( + --- The ID of this test set. + idTestSet INTEGER PRIMARY KEY DEFAULT NEXTVAL('TestSetIdSeq') NOT NULL, + + --- The test config timestamp, used when reading test config. + tsConfig TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP NOT NULL, + --- When this test set was scheduled. + -- idGenTestBox is valid at this point. + tsCreated TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP NOT NULL, + --- When this test completed, i.e. testing stopped. This should only be set once. + tsDone TIMESTAMP WITH TIME ZONE DEFAULT NULL, + --- The current status. + enmStatus TestStatus_T DEFAULT 'running'::TestStatus_T NOT NULL, + + --- The build we're testing. + -- Non-unique foreign key: Builds(idBuild) + idBuild INTEGER NOT NULL, + --- The build category of idBuild when the test started. + -- This is for speeding up graph data collection, i.e. avoid idBuild + -- the WHERE part of the selection. + idBuildCategory INTEGER REFERENCES BuildCategories(idBuildCategory) NOT NULL, + --- The test suite build we're using to do the testing. + -- This is NULL if the test suite zip wasn't referred or if a test suite + -- build source wasn't configured. + -- Non-unique foreign key: Builds(idBuild) + idBuildTestSuite INTEGER DEFAULT NULL, + + --- The exact testbox configuration. + idGenTestBox INTEGER REFERENCES TestBoxes(idGenTestBox) NOT NULL, + --- The testbox ID for joining with (valid: tsStarted). + -- Non-unique foreign key: TestBoxes(idTestBox) + idTestBox INTEGER NOT NULL, + --- The scheduling group ID the test was scheduled thru (valid: tsStarted). + -- Non-unique foreign key: SchedGroups(idSchedGroup) + idSchedGroup INTEGER NOT NULL, + + --- The testgroup (valid: tsConfig). + -- Non-unique foreign key: TestBoxes(idTestGroup) + -- Note! This also gives the member ship entry, since a testcase can only + -- have one membership per test group. + idTestGroup INTEGER NOT NULL, + + --- The exact test case config we executed in this test run. + idGenTestCase INTEGER REFERENCES TestCases(idGenTestCase) NOT NULL, + --- The test case ID for joining with (valid: tsConfig). + -- Non-unique foreign key: TestBoxes(idTestCase) + idTestCase INTEGER NOT NULL, + + --- The arguments (and requirements++) we executed this test case with. + idGenTestCaseArgs INTEGER REFERENCES TestCaseArgs(idGenTestCaseArgs) NOT NULL, + --- The argument variation ID (valid: tsConfig). + -- Non-unique foreign key: TestCaseArgs(idTestCaseArgs) + idTestCaseArgs INTEGER NOT NULL, + + --- The root of the test result tree. + -- @note This will only be NULL early in the transaction setting up the testset. + -- @note If the test reports more than one top level test result, we'll + -- fail the whole test run and let the test developer fix it. + idTestResult INTEGER REFERENCES TestResults(idTestResult) DEFAULT NULL, + + --- The base filename used for storing files related to this test set. + -- This is a path relative to wherever TM is dumping log files. In order + -- to not become a file system test case, we will try not to put too many + -- hundred thousand files in a directory. A simple first approach would + -- be to just use the current date (tsCreated) like this: + -- TM_FILE_DIR/year/month/day/TestSets.idTestSet + -- + -- The primary log file for the test is this name suffixed by '.log'. + -- + -- The files in the testresultfile table gets their full names like this: + -- TM_FILE_DIR/sBaseFilename-testresultfile.id-TestResultStrTab(testresultfile.idStrFilename) + -- + -- @remarks We store this explicitly in case we change the directly layout + -- at some later point. + sBaseFilename text UNIQUE NOT NULL, + + --- The gang member number number, 0 is the leader. + iGangMemberNo SMALLINT DEFAULT 0 NOT NULL CHECK (iGangMemberNo >= 0 AND iGangMemberNo < 1024), + --- The test set of the gang leader, NULL if no gang involved. + -- @note This is set by the gang leader as well, so that we can find all + -- gang members by WHERE idTestSetGangLeader = :id. + idTestSetGangLeader INTEGER REFERENCES TestSets(idTestSet) DEFAULT NULL + +); +CREATE INDEX TestSetsGangIdx ON TestSets (idTestSetGangLeader); +CREATE INDEX TestSetsBoxIdx ON TestSets (idTestBox, idTestResult); +CREATE INDEX TestSetsBuildIdx ON TestSets (idBuild, idTestResult); +CREATE INDEX TestSetsTestCaseIdx ON TestSets (idTestCase, idTestResult); +CREATE INDEX TestSetsTestVarIdx ON TestSets (idTestCaseArgs, idTestResult); +--- The TestSetsDoneCreatedBuildCatIdx is for testbox results, graph options and such. +CREATE INDEX TestSetsDoneCreatedBuildCatIdx ON TestSets (tsDone DESC NULLS FIRST, tsCreated ASC, idBuildCategory); +--- For graphs. +CREATE INDEX TestSetsGraphBoxIdx ON TestSets (idTestBox, tsCreated DESC, tsDone ASC NULLS LAST, idBuildCategory, idTestCase); + +ALTER TABLE TestResults ADD FOREIGN KEY (idTestSet) REFERENCES TestSets(idTestSet) MATCH FULL; +ALTER TABLE TestResultValues ADD FOREIGN KEY (idTestSet) REFERENCES TestSets(idTestSet) MATCH FULL; +ALTER TABLE TestResultFiles ADD FOREIGN KEY (idTestSet) REFERENCES TestSets(idTestSet) MATCH FULL; +ALTER TABLE TestResultMsgs ADD FOREIGN KEY (idTestSet) REFERENCES TestSets(idTestSet) MATCH FULL; +ALTER TABLE TestResultFailures ADD FOREIGN KEY (idTestSet) REFERENCES TestSets(idTestSet) MATCH FULL; + + + + +-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- +-- +-- T e s t M a n g e r P e r s i s t e n t S t o r a g e +-- +-- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- -- + +--- @type TestBoxState_T +-- TestBox state. +-- +-- @todo Consider drawing a state diagram for this. +-- +CREATE TYPE TestBoxState_T AS ENUM ( + --- Nothing to do. + -- Prev: testing, gang-cleanup, rebooting, upgrading, + -- upgrading-and-rebooting, doing-special-cmd. + -- Next: testing, gang-gathering, rebooting, upgrading, + -- upgrading-and-rebooting, doing-special-cmd. + 'idle', + --- Executing a test. + -- Prev: idle + -- Next: idle + 'testing', + + -- Gang scheduling statuses: + --- The gathering of a gang. + -- Prev: idle + -- Next: gang-gathering-timedout, gang-testing + 'gang-gathering', + --- The gathering timed out, the testbox needs to cleanup and move on. + -- Prev: gang-gathering + -- Next: idle + -- This is set on all gathered members by the testbox who triggers the + -- timeout. + 'gang-gathering-timedout', + --- The gang scheduling equivalent of 'testing'. + -- Prev: gang-gathering + -- Next: gang-cleanup + 'gang-testing', + --- Waiting for the other gang members to stop testing so that cleanups + -- can be performed and members safely rescheduled. + -- Prev: gang-testing + -- Next: idle + -- + -- There are two resource clean up issues being targeted here: + -- 1. Global resources will be allocated by the leader when he enters the + -- 'gang-gathering' state. If the leader quits and frees the resource + -- while someone is still using it, bad things will happen. Imagine a + -- global resource without any access checks and relies exclusivly on + -- the TM doing its job. + -- 2. TestBox resource accessed by other gang members may also be used in + -- other tests. Should a gang member leave early and embark on a + -- testcase using the same resources, bad things will happen. Example: + -- Live migration. One partner leaves early because it detected some + -- fatal failure, the other one is still trying to connect to him. + -- The testbox is scheduled again on the same live migration testcase, + -- only with different arguments (VM config), it will try migrate using + -- the same TCP ports. Confusion ensues. + -- + -- To figure out whether to remain in this status because someone is + -- still testing: + -- SELECT COUNT(*) FROM TestBoxStatuses, TestSets + -- WHERE TestSets.idTestSetGangLeader = :idGangLeader + -- AND TestSets.idTestBox = TestBoxStatuses.idTestBox + -- AND TestSets.idTestSet = TestBoxStatuses.idTestSet + -- AND TestBoxStatuses.enmState = 'gang-testing'::TestBoxState_T; + 'gang-cleanup', + + -- Command related statuses (all command status changes comes from 'idle' + -- and goes back to 'idle'): + 'rebooting', + 'upgrading', + 'upgrading-and-rebooting', + 'doing-special-cmd' +); + +--- @table TestBoxStatuses +-- Testbox status table. +-- +-- History is not planned on this table. +-- +CREATE TABLE TestBoxStatuses ( + --- The testbox. + idTestBox INTEGER PRIMARY KEY NOT NULL, + --- The testbox generation ID. + idGenTestBox INTEGER REFERENCES TestBoxes(idGenTestBox) NOT NULL, + --- When this status was last updated. + -- This is updated everytime the testbox talks to the test manager, thus it + -- can easily be used to find testboxes which has stopped responding. + -- + -- This is used for timeout calculation during gang-gathering, so in that + -- scenario it won't be updated until the gang is gathered or we time out. + tsUpdated TIMESTAMP WITH TIME ZONE DEFAULT current_timestamp NOT NULL, + --- The current state. + enmState TestBoxState_T DEFAULT 'idle'::TestBoxState_T NOT NULL, + --- Reference to the test set + idTestSet INTEGER REFERENCES TestSets(idTestSet), + --- Interal work item number. + -- This is used to pick and prioritize between multiple scheduling groups. + iWorkItem INTEGER DEFAULT 0 NOT NULL +); + + +--- @table GlobalResourceStatuses +-- Global resource status, tracks which test set resources are allocated by. +-- +-- History is not planned on this table. +-- +CREATE TABLE GlobalResourceStatuses ( + --- The resource ID. + -- Non-unique foreign key: GlobalResources(idGlobalRsrc). + idGlobalRsrc INTEGER PRIMARY KEY NOT NULL, + --- The resource owner. + -- @note This is going thru testboxstatus to be able to use the testbox ID + -- as a foreign key. + idTestBox INTEGER REFERENCES TestBoxStatuses(idTestBox) NOT NULL, + --- When the allocation took place. + tsAllocated TIMESTAMP WITH TIME ZONE DEFAULT current_timestamp NOT NULL +); + + +--- @table SchedQueues +-- Scheduler queue. +-- +-- The queues are currently associated with a scheduling group, it could +-- alternative be changed to hook on to a testbox instead. It depends on what +-- kind of scheduling method we prefer. The former method aims at test case +-- thruput, making sacrifices in the hardware distribution area. The latter is +-- more like the old buildbox style testing, making sure that each test case is +-- executed on each testbox. +-- +-- When there are configuration changes, TM will regenerate the scheduling +-- queue for the affected scheduling groups. We do not concern ourselves with +-- trying to continue at the approximately same queue position, we simply take +-- it from the top. +-- +-- When a testbox ask for work, we will open a cursor on the queue and take the +-- first test in the queue that can be executed on that testbox. The test will +-- be moved to the end of the queue (getting a new item_id). +-- +-- If a test is manually changed to the head of the queue, the item will get a +-- item_id which is 1 lower than the head of the queue. Unless someone does +-- this a couple of billion times, we shouldn't have any trouble running out of +-- number space. :-) +-- +-- Manually moving a test to the end of the queue is easy, just get a new +-- 'item_id'. +-- +-- History is not planned on this table. +-- +CREATE SEQUENCE SchedQueueItemIdSeq + START 1 + INCREMENT BY 1 + NO MAXVALUE + NO MINVALUE + CACHE 1; +CREATE TABLE SchedQueues ( + --- The scheduling queue (one queue per scheduling group). + -- Non-unique foreign key: SchedGroups(idSchedGroup) + idSchedGroup INTEGER NOT NULL, + --- The scheduler queue entry ID. + -- Lower numbers means early queue position. + idItem INTEGER DEFAULT NEXTVAL('SchedQueueItemIdSeq') NOT NULL, + --- The queue offset. + -- This is used for repositining the queue when recreating it. It can also + -- be used to figure out how jumbled the queue gets after real life has had + -- it's effect on it. + offQueue INTEGER NOT NULL, + --- The test case argument variation to execute. + idGenTestCaseArgs INTEGER REFERENCES TestCaseArgs(idGenTestCaseArgs) NOT NULL, + --- The relevant testgroup. + -- Non-unique foreign key: TestGroups(idTestGroup). + idTestGroup INTEGER NOT NULL, + --- Aggregated test group dependencies (NULL if none). + -- Non-unique foreign key: TestGroups(idTestGroup). + -- See also comments on SchedGroupMembers.idTestGroupPreReq. + aidTestGroupPreReqs INTEGER ARRAY DEFAULT NULL, + --- The scheduling time constraints (see SchedGroupMembers.bmHourlySchedule). + bmHourlySchedule bit(168) DEFAULT NULL, + --- When the queue entry was created and for which config is valid. + -- This is the timestamp that should be used when reading config info. + tsConfig TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP NOT NULL, + --- When this status was last scheduled. + -- This is set to current_timestamp when moving the entry to the end of the + -- queue. It's initial value is unix-epoch. Not entirely sure if it's + -- useful beyond introspection and non-unique foreign key hacking. + tsLastScheduled TIMESTAMP WITH TIME ZONE DEFAULT TIMESTAMP WITH TIME ZONE 'epoch' NOT NULL, + + --- This is used in gang scheduling. + idTestSetGangLeader INTEGER REFERENCES TestSets(idTestSet) DEFAULT NULL UNIQUE, + --- The number of gang members still missing. + -- + -- This saves calculating the number of missing members via selects like: + -- SELECT COUNT(*) FROM TestSets WHERE idTestSetGangLeader = :idGang; + -- and + -- SELECT cGangMembers FROM TestCaseArgs WHERE idGenTestCaseArgs = :idTest; + -- to figure out whether to remain in 'gather-gang'::TestBoxState_T. + -- + cMissingGangMembers smallint DEFAULT 1 NOT NULL, + + --- @todo + --- The number of times this has been considered for scheduling. + -- cConsidered SMALLINT DEFAULT 0 NOT NULL, + + PRIMARY KEY (idSchedGroup, idItem) +); +CREATE INDEX SchedQueuesItemIdx ON SchedQueues(idItem); +CREATE INDEX SchedQueuesSchedGroupIdx ON SchedQueues(idSchedGroup); + diff --git a/src/VBox/ValidationKit/testmanager/db/TestManagerDatabaseMap.png b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabaseMap.png Binary files differnew file mode 100644 index 00000000..861a407d --- /dev/null +++ b/src/VBox/ValidationKit/testmanager/db/TestManagerDatabaseMap.png diff --git a/src/VBox/ValidationKit/testmanager/db/TestManagerVBoxPilot-1.pgsql b/src/VBox/ValidationKit/testmanager/db/TestManagerVBoxPilot-1.pgsql new file mode 100644 index 00000000..bdff3bc4 --- /dev/null +++ b/src/VBox/ValidationKit/testmanager/db/TestManagerVBoxPilot-1.pgsql @@ -0,0 +1,101 @@ +-- $Id: TestManagerVBoxPilot-1.pgsql $ +--- @file +-- VBox Test Manager - Setup for the 1st VBox Pilot. +-- + +-- +-- 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 +-- + + + +\set ON_ERROR_STOP 1 +\connect testmanager; + +BEGIN WORK; + +-- +-- The user we assign all the changes too. +-- +INSERT INTO Users (sUsername, sEmail, sFullName, sLoginName) + VALUES ('vbox-pilot-config', 'pilot1@example.org', 'VBox Pilot Configurator', 'vbox-pilot-config'); +\set idUserQuery '(SELECT uid FROM Users WHERE sUsername = \'vbox-pilot-config\')' + +-- +-- Configure a scheduling group with build sources. +-- +INSERT INTO BuildSources (uidAuthor, sName, sProduct, sBranch, asTypes, asOsArches) + VALUES (:idUserQuery, 'VBox trunk builds', 'VirtualBox', 'trunk', ARRAY['release', 'strict'], NULL); + +INSERT INTO BuildSources (uidAuthor, sName, sProduct, sBranch, asTypes, asOsArches) + VALUES (:idUserQuery, 'VBox TestSuite trunk builds', 'VBox TestSuite', 'trunk', ARRAY['release'], NULL); + +INSERT INTO SchedGroups (sName, sDescription, fEnabled, idBuildSrc, idBuildSrcTestSuite) + VALUES ('VirtualBox Trunk', NULL, TRUE, + (SELECT idBuildSrc FROM BuildSources WHERE sName = 'VBox trunk builds'), + (SELECT idBuildSrc FROM BuildSources WHERE sName = 'VBox TestSuite trunk builds') ); +\set idSchedGroupQuery '(SELECT idSchedGroup FROM SchedGroups WHERE sName = \'VirtualBox Trunk\')' + +-- +-- Configure three test groups. +-- +INSERT INTO TestGroups (uidAuthor, sName) + VALUES (:idUserQuery, 'VBox smoketests'); +\set idGrpSmokeQuery '(SELECT idTestGroup FROM TestGroups WHERE sName = \'VBox smoketests\')' +INSERT INTO SchedGroupMembers (idSchedGroup, idTestGroup, uidAuthor, idTestGroupPreReq) + VALUES (:idSchedGroupQuery, :idGrpSmokeQuery, :idUserQuery, NULL); + +INSERT INTO TestGroups (uidAuthor, sName) + VALUES (:idUserQuery, 'VBox general'); +\set idGrpGeneralQuery '(SELECT idTestGroup FROM TestGroups WHERE sName = \'VBox general\')' +INSERT INTO SchedGroupMembers (idSchedGroup, idTestGroup, uidAuthor, idTestGroupPreReq) + VALUES (:idSchedGroupQuery, :idGrpGeneralQuery, :idUserQuery, :idGrpSmokeQuery); + +INSERT INTO TestGroups (uidAuthor, sName) + VALUES (:idUserQuery, 'VBox benchmarks'); +\set idGrpBenchmarksQuery '(SELECT idTestGroup FROM TestGroups WHERE sName = \'VBox benchmarks\')' +INSERT INTO SchedGroupMembers (idSchedGroup, idTestGroup, uidAuthor, idTestGroupPreReq) + VALUES (:idSchedGroupQuery, :idGrpBenchmarksQuery, :idUserQuery, :idGrpGeneralQuery); + + +-- +-- Testcases +-- +INSERT INTO TestCases (uidAuthor, sName, fEnabled, cSecTimeout, sBaseCmd, sTestSuiteZips) + VALUES (:idUserQuery, 'VBox install', TRUE, 600, + 'validationkit/testdriver/vboxinstaller.py --vbox-build @BUILD_BINARIES@ @ACTION@ -- testdriver/base.py @ACTION@', + '@VALIDATIONKIT_ZIP@'); +INSERT INTO TestCaseArgs (idTestCase, uidAuthor, sArgs) + VALUES ((SELECT idTestCase FROM TestCases WHERE sName = 'VBox install'), :idUserQuery, ''); +INSERT INTO TestGroupMembers (idTestGroup, idTestCase, uidAuthor) + VALUES (:idGrpSmokeQuery, (SELECT idTestCase FROM TestCases WHERE sName = 'VBox install'), :idUserQuery); + +COMMIT WORK; + diff --git a/src/VBox/ValidationKit/testmanager/db/gen-sql-comments.py b/src/VBox/ValidationKit/testmanager/db/gen-sql-comments.py new file mode 100755 index 00000000..2bdc239c --- /dev/null +++ b/src/VBox/ValidationKit/testmanager/db/gen-sql-comments.py @@ -0,0 +1,236 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# $Id: gen-sql-comments.py $ + +""" +Converts doxygen style comments in SQL script to COMMENT ON statements. +""" + +__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 +""" + +import sys; +import re; + + +def errorMsg(sMsg): + sys.stderr.write('error: %s\n' % (sMsg,)); + return 1; + +class SqlDox(object): + """ + Class for parsing relevant comments out of a pgsql file + and emit COMMENT ON statements from it. + """ + + def __init__(self, oFile, sFilename): + self.oFile = oFile; + self.sFilename = sFilename; + self.iLine = 0; # The current input line number. + self.sComment = None; # The current comment. + self.fCommentComplete = False; # Indicates that the comment has ended. + self.sCommentSqlObj = None; # SQL object indicated by the comment (@table). + self.sOuterSqlObj = None; # Like 'table yyyy' or 'type zzzz'. + self.sPrevSqlObj = None; # Like 'table xxxx'. + + + def error(self, sMsg): + return errorMsg('%s(%d): %s' % (self.sFilename, self.iLine, sMsg,)); + + def dprint(self, sMsg): + sys.stderr.write('debug: %s\n' % (sMsg,)); + return True; + + def resetComment(self): + self.sComment = None; + self.fCommentComplete = False; + self.sCommentSqlObj = None; + + def quoteSqlString(self, s): + return s.replace("'", "''"); + + def commitComment2(self, sSqlObj): + if self.sComment is not None and sSqlObj is not None: + print("COMMENT ON %s IS\n '%s';\n" % (sSqlObj, self.quoteSqlString(self.sComment.strip()))); + self.resetComment(); + return True; + + def commitComment(self): + return self.commitComment2(self.sCommentSqlObj); + + def process(self): + for sLine in self.oFile: + self.iLine += 1; + + sLine = sLine.strip(); + self.dprint('line %d: %s\n' % (self.iLine, sLine)); + if sLine.startswith('--'): + if sLine.startswith('--- '): + # + # New comment. + # The first list may have a @table, @type or similar that we're interested in. + # + self.commitComment(); + + sLine = sLine.lstrip('- '); + if sLine.startswith('@table '): + self.sCommentSqlObj = 'TABLE ' + (sLine[7:]).rstrip(); + self.sComment = ''; + elif sLine.startswith('@type '): + self.sCommentSqlObj = 'TYPE ' + (sLine[6:]).rstrip(); + self.sComment = ''; + elif sLine.startswith('@todo') \ + or sLine.startswith('@file') \ + or sLine.startswith('@page') \ + or sLine.startswith('@name') \ + or sLine.startswith('@{') \ + or sLine.startswith('@}'): + # Ignore. + pass; + elif sLine.startswith('@'): + return self.error('Unknown tag: %s' % (sLine,)); + else: + self.sComment = sLine; + + elif (sLine.startswith('-- ') or sLine == '--') \ + and self.sComment is not None and self.fCommentComplete is False: + # + # Append line to comment. + # + if sLine == '--': + sLine = ''; + else: + sLine = (sLine[3:]); + if self.sComment == '': + self.sComment = sLine; + else: + self.sComment += "\n" + sLine; + + elif sLine.startswith('--< '): + # + # Comment that starts on the same line as the object it describes. + # + sLine = (sLine[4:]).rstrip(); + # => Later/never. + else: + # + # Not a comment that interests us. So, complete any open + # comment and commit it if we know which SQL object it + # applies to. + # + self.fCommentComplete = True; + if self.sCommentSqlObj is not None: + self.commitComment(); + else: + # + # Not a comment. As above, we complete and optionally commit + # any open comment. + # + self.fCommentComplete = True; + if self.sCommentSqlObj is not None: + self.commitComment(); + + # + # Check for SQL (very fuzzy and bad). + # + asWords = sLine.split(' '); + if len(asWords) >= 3 \ + and asWords[0] == 'CREATE': + # CREATE statement. + sType = asWords[1]; + sName = asWords[2]; + if sType == 'UNIQUE' and sName == 'INDEX' and len(asWords) >= 4: + sType = asWords[2]; + sName = asWords[3]; + if sType in ('TABLE', 'TYPE', 'INDEX', 'VIEW'): + self.sOuterSqlObj = sType + ' ' + sName; + self.sPrevSqlObj = self.sOuterSqlObj; + self.dprint('%s' % (self.sOuterSqlObj,)); + self.commitComment2(self.sOuterSqlObj); + elif len(asWords) >= 1 \ + and self.sOuterSqlObj is not None \ + and self.sOuterSqlObj.startswith('TABLE ') \ + and re.search("^(as|al|bm|c|enm|f|i|l|s|ts|uid|uuid)[A-Z][a-zA-Z0-9]*$", asWords[0]) is not None: + # Possibly a column name. + self.sPrevSqlObj = 'COLUMN ' + self.sOuterSqlObj[6:] + '.' + asWords[0]; + self.dprint('column? %s' % (self.sPrevSqlObj)); + self.commitComment2(self.sPrevSqlObj); + + # + # Check for semicolon. + # + if sLine.find(");") >= 0: + self.sOuterSqlObj = None; + + return 0; + + +def usage(): + sys.stderr.write('usage: gen-sql-comments.py <filename.pgsql>\n' + '\n' + 'The output goes to stdout.\n'); + return 0; + + +def main(asArgs): + # Parse the argument. :-) + sInput = None; + if (len(asArgs) != 2): + sys.stderr.write('syntax error: expected exactly 1 argument, a psql file\n'); + usage(); + return 2; + sInput = asArgs[1]; + + # Do the job, outputting to standard output. + try: + oFile = open(sInput, 'r'); + except: + return errorMsg("failed to open '%s' for reading" % (sInput,)); + + # header. + print("-- $" "Id" "$"); + print("--- @file"); + print("-- Autogenerated from %s. Do not edit!" % (sInput,)); + print("--"); + print(""); + for sLine in __copyright__.split('\n'): + if len(sLine) > 0: + print("-- %s" % (sLine,)); + else: + print("--"); + print(""); + print(""); + me = SqlDox(oFile, sInput); + return me.process(); + +sys.exit(main(sys.argv)); + diff --git a/src/VBox/ValidationKit/testmanager/db/partial-db-dump.py b/src/VBox/ValidationKit/testmanager/db/partial-db-dump.py new file mode 100755 index 00000000..6676de47 --- /dev/null +++ b/src/VBox/ValidationKit/testmanager/db/partial-db-dump.py @@ -0,0 +1,392 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# $Id: partial-db-dump.py $ +# pylint: disable=line-too-long + +""" +Utility for dumping the last X days of data. +""" + +__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 sys; +import os; +import zipfile; +from optparse import OptionParser; +import xml.etree.ElementTree as ET; + +# Add Test Manager's modules path +g_ksTestManagerDir = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))); +sys.path.append(g_ksTestManagerDir); + +# Test Manager imports +from testmanager.core.db import TMDatabaseConnection; +from common import utils; + + +class PartialDbDump(object): # pylint: disable=too-few-public-methods + """ + Dumps or loads the last X days of database data. + + This is a useful tool when hacking on the test manager locally. You can get + a small sample from the last few days from the production test manager server + without spending hours dumping, downloading, and loading the whole database + (because it is gigantic). + + """ + + def __init__(self): + """ + Parse command line. + """ + + oParser = OptionParser() + oParser.add_option('-q', '--quiet', dest = 'fQuiet', action = 'store_true', + help = 'Quiet execution'); + oParser.add_option('-f', '--filename', dest = 'sFilename', metavar = '<filename>', + default = 'partial-db-dump.zip', help = 'The name of the partial database zip file to write/load.'); + + oParser.add_option('-t', '--tmp-file', dest = 'sTempFile', metavar = '<temp-file>', + default = '/tmp/tm-partial-db-dump.pgtxt', + help = 'Name of temporary file for duping tables. Must be absolute'); + oParser.add_option('--days-to-dump', dest = 'cDays', metavar = '<days>', type = 'int', default = 14, + help = 'How many days to dump (counting backward from current date).'); + oParser.add_option('--load-dump-into-database', dest = 'fLoadDumpIntoDatabase', action = 'store_true', + default = False, help = 'For loading instead of dumping.'); + oParser.add_option('--store', dest = 'fStore', action = 'store_true', + default = False, help = 'Do not compress the zip file.'); + + (self.oConfig, _) = oParser.parse_args(); + + + ## + # Tables dumped in full because they're either needed in full or they normally + # aren't large enough to bother reducing. + kasTablesToDumpInFull = [ + 'Users', + 'BuildBlacklist', + 'BuildCategories', + 'BuildSources', + 'FailureCategories', + 'FailureReasons', + 'GlobalResources', + 'Testcases', + 'TestcaseArgs', + 'TestcaseDeps', + 'TestcaseGlobalRsrcDeps', + 'TestGroups', + 'TestGroupMembers', + 'SchedGroups', + 'SchedGroupMembers', # ? + 'TestBoxesInSchedGroups', # ? + 'SchedQueues', + 'TestResultStrTab', # 36K rows, never mind complicated then. + ]; + + ## + # Tables where we only dump partial info (the TestResult* tables are rather + # gigantic). + kasTablesToPartiallyDump = [ + 'TestBoxes', # 2016-05-25: ca. 641 MB + 'TestSets', # 2016-05-25: ca. 525 MB + 'TestResults', # 2016-05-25: ca. 13 GB + 'TestResultFiles', # 2016-05-25: ca. 87 MB + 'TestResultMsgs', # 2016-05-25: ca. 29 MB + 'TestResultValues', # 2016-05-25: ca. 3728 MB + 'TestResultFailures', + 'Builds', + 'TestBoxStrTab', + 'SystemLog', + 'VcsRevisions', + ]; + + def _doCopyTo(self, sTable, oZipFile, oDb, sSql, aoArgs = None): + """ Does one COPY TO job. """ + print('Dumping %s...' % (sTable,)); + + if aoArgs is not None: + sSql = oDb.formatBindArgs(sSql, aoArgs); + + oFile = open(self.oConfig.sTempFile, 'w'); + oDb.copyExpert(sSql, oFile); + cRows = oDb.getRowCount(); + oFile.close(); + print('... %s rows.' % (cRows,)); + + oZipFile.write(self.oConfig.sTempFile, sTable); + return True; + + def _doDump(self, oDb): + """ Does the dumping of the database. """ + + enmCompression = zipfile.ZIP_DEFLATED; + if self.oConfig.fStore: + enmCompression = zipfile.ZIP_STORED; + oZipFile = zipfile.ZipFile(self.oConfig.sFilename, 'w', enmCompression); + + oDb.begin(); + + # Dumping full tables is simple. + for sTable in self.kasTablesToDumpInFull: + self._doCopyTo(sTable, oZipFile, oDb, 'COPY ' + sTable + ' TO STDOUT WITH (FORMAT TEXT)'); + + # Figure out how far back we need to go. + oDb.execute('SELECT CURRENT_TIMESTAMP - INTERVAL \'%s days\'' % (self.oConfig.cDays,)); + tsEffective = oDb.fetchOne()[0]; + oDb.execute('SELECT CURRENT_TIMESTAMP - INTERVAL \'%s days\'' % (self.oConfig.cDays + 2,)); + tsEffectiveSafe = oDb.fetchOne()[0]; + print('Going back to: %s (safe: %s)' % (tsEffective, tsEffectiveSafe)); + + # We dump test boxes back to the safe timestamp because the test sets may + # use slightly dated test box references and we don't wish to have dangling + # references when loading. + for sTable in [ 'TestBoxes', ]: + self._doCopyTo(sTable, oZipFile, oDb, + 'COPY (SELECT * FROM ' + sTable + ' WHERE tsExpire >= %s) TO STDOUT WITH (FORMAT TEXT)', + (tsEffectiveSafe,)); + + # The test results needs to start with test sets and then dump everything + # releated to them. So, figure the lowest (oldest) test set ID we'll be + # dumping first. + oDb.execute('SELECT idTestSet FROM TestSets WHERE tsCreated >= %s', (tsEffective, )); + idFirstTestSet = 0; + if oDb.getRowCount() > 0: + idFirstTestSet = oDb.fetchOne()[0]; + print('First test set ID: %s' % (idFirstTestSet,)); + + oDb.execute('SELECT MAX(idTestSet) FROM TestSets WHERE tsCreated >= %s', (tsEffective, )); + idLastTestSet = 0; + if oDb.getRowCount() > 0: + idLastTestSet = oDb.fetchOne()[0]; + print('Last test set ID: %s' % (idLastTestSet,)); + + oDb.execute('SELECT MAX(idTestResult) FROM TestResults WHERE tsCreated >= %s', (tsEffective, )); + idLastTestResult = 0; + if oDb.getRowCount() > 0: + idLastTestResult = oDb.fetchOne()[0]; + print('Last test result ID: %s' % (idLastTestResult,)); + + # Tables with idTestSet member. + for sTable in [ 'TestSets', 'TestResults', 'TestResultValues' ]: + self._doCopyTo(sTable, oZipFile, oDb, + 'COPY (SELECT *\n' + ' FROM ' + sTable + '\n' + ' WHERE idTestSet >= %s\n' + ' AND idTestSet <= %s\n' + ' AND idTestResult <= %s\n' + ') TO STDOUT WITH (FORMAT TEXT)' + , ( idFirstTestSet, idLastTestSet, idLastTestResult,)); + + # Tables where we have to go via TestResult. + for sTable in [ 'TestResultFiles', 'TestResultMsgs', 'TestResultFailures' ]: + self._doCopyTo(sTable, oZipFile, oDb, + 'COPY (SELECT it.*\n' + ' FROM ' + sTable + ' it, TestResults tr\n' + ' WHERE tr.idTestSet >= %s\n' + ' AND tr.idTestSet <= %s\n' + ' AND tr.idTestResult <= %s\n' + ' AND tr.tsCreated >= %s\n' # performance hack. + ' AND it.idTestResult = tr.idTestResult\n' + ') TO STDOUT WITH (FORMAT TEXT)' + , ( idFirstTestSet, idLastTestSet, idLastTestResult, tsEffective,)); + + # Tables which goes exclusively by tsCreated using tsEffectiveSafe. + for sTable in [ 'SystemLog', 'VcsRevisions' ]: + self._doCopyTo(sTable, oZipFile, oDb, + 'COPY (SELECT * FROM ' + sTable + ' WHERE tsCreated >= %s) TO STDOUT WITH (FORMAT TEXT)', + (tsEffectiveSafe,)); + + # The builds table. + oDb.execute('SELECT MIN(idBuild), MIN(idBuildTestSuite) FROM TestSets WHERE idTestSet >= %s', (idFirstTestSet,)); + idFirstBuild = 0; + if oDb.getRowCount() > 0: + idFirstBuild = min(oDb.fetchOne()); + print('First build ID: %s' % (idFirstBuild,)); + for sTable in [ 'Builds', ]: + self._doCopyTo(sTable, oZipFile, oDb, + 'COPY (SELECT * FROM ' + sTable + ' WHERE idBuild >= %s) TO STDOUT WITH (FORMAT TEXT)', + (idFirstBuild,)); + + # The test box string table. + self._doCopyTo('TestBoxStrTab', oZipFile, oDb, ''' +COPY (SELECT * FROM TestBoxStrTab WHERE idStr IN ( + ( SELECT 0 + ) UNION ( SELECT idStrComment FROM TestBoxes WHERE tsExpire >= %s + ) UNION ( SELECT idStrCpuArch FROM TestBoxes WHERE tsExpire >= %s + ) UNION ( SELECT idStrCpuName FROM TestBoxes WHERE tsExpire >= %s + ) UNION ( SELECT idStrCpuVendor FROM TestBoxes WHERE tsExpire >= %s + ) UNION ( SELECT idStrDescription FROM TestBoxes WHERE tsExpire >= %s + ) UNION ( SELECT idStrOS FROM TestBoxes WHERE tsExpire >= %s + ) UNION ( SELECT idStrOsVersion FROM TestBoxes WHERE tsExpire >= %s + ) UNION ( SELECT idStrReport FROM TestBoxes WHERE tsExpire >= %s + ) ) ) TO STDOUT WITH (FORMAT TEXT) +''', (tsEffectiveSafe, tsEffectiveSafe, tsEffectiveSafe, tsEffectiveSafe, + tsEffectiveSafe, tsEffectiveSafe, tsEffectiveSafe, tsEffectiveSafe,)); + + oZipFile.close(); + print('Done!'); + return 0; + + def _doLoad(self, oDb): + """ Does the loading of the dumped data into the database. """ + + try: + oZipFile = zipfile.ZipFile(self.oConfig.sFilename, 'r'); + except: + print('error: Dump file "%s" cannot be opened! Use "-f <file>" to specify a file.' % (self.oConfig.sFilename,)); + return 1; + + asTablesInLoadOrder = [ + 'Users', + 'BuildBlacklist', + 'BuildCategories', + 'BuildSources', + 'FailureCategories', + 'FailureReasons', + 'GlobalResources', + 'Testcases', + 'TestcaseArgs', + 'TestcaseDeps', + 'TestcaseGlobalRsrcDeps', + 'TestGroups', + 'TestGroupMembers', + 'SchedGroups', + 'TestBoxStrTab', + 'TestBoxes', + 'SchedGroupMembers', + 'TestBoxesInSchedGroups', + 'SchedQueues', + 'Builds', + 'SystemLog', + 'VcsRevisions', + 'TestResultStrTab', + 'TestSets', + 'TestResults', + 'TestResultFiles', + 'TestResultMsgs', + 'TestResultValues', + 'TestResultFailures', + ]; + assert len(asTablesInLoadOrder) == len(self.kasTablesToDumpInFull) + len(self.kasTablesToPartiallyDump); + + oDb.begin(); + oDb.execute('SET CONSTRAINTS ALL DEFERRED;'); + + print('Checking if the database looks empty...\n'); + for sTable in asTablesInLoadOrder + [ 'TestBoxStatuses', 'GlobalResourceStatuses' ]: + oDb.execute('SELECT COUNT(*) FROM ' + sTable); + cRows = oDb.fetchOne()[0]; + cMaxRows = 0; + if sTable in [ 'SchedGroups', 'TestBoxStrTab', 'TestResultStrTab', 'Users' ]: cMaxRows = 1; + if cRows > cMaxRows: + print('error: Table %s has %u rows which is more than %u - refusing to delete and load.' + % (sTable, cRows, cMaxRows,)); + print('info: Please drop and recreate the database before loading!'); + return 1; + + print('Dropping default table content...\n'); + for sTable in [ 'SchedGroups', 'TestBoxStrTab', 'TestResultStrTab', 'Users']: + oDb.execute('DELETE FROM ' + sTable); + + oDb.execute('ALTER TABLE TestSets DROP CONSTRAINT IF EXISTS TestSets_idTestResult_fkey'); + + for sTable in asTablesInLoadOrder: + print('Loading %s...' % (sTable,)); + oFile = oZipFile.open(sTable); + oDb.copyExpert('COPY ' + sTable + ' FROM STDIN WITH (FORMAT TEXT)', oFile); + cRows = oDb.getRowCount(); + print('... %s rows.' % (cRows,)); + + oDb.execute('ALTER TABLE TestSets ADD FOREIGN KEY (idTestResult) REFERENCES TestResults(idTestResult)'); + oDb.commit(); + + # Correct sequences. + atSequences = [ + ( 'UserIdSeq', 'Users', 'uid' ), + ( 'GlobalResourceIdSeq', 'GlobalResources', 'idGlobalRsrc' ), + ( 'BuildSourceIdSeq', 'BuildSources', 'idBuildSrc' ), + ( 'TestCaseIdSeq', 'TestCases', 'idTestCase' ), + ( 'TestCaseGenIdSeq', 'TestCases', 'idGenTestCase' ), + ( 'TestCaseArgsIdSeq', 'TestCaseArgs', 'idTestCaseArgs' ), + ( 'TestCaseArgsGenIdSeq', 'TestCaseArgs', 'idGenTestCaseArgs' ), + ( 'TestGroupIdSeq', 'TestGroups', 'idTestGroup' ), + ( 'SchedGroupIdSeq', 'SchedGroups', 'idSchedGroup' ), + ( 'TestBoxStrTabIdSeq', 'TestBoxStrTab', 'idStr' ), + ( 'TestBoxIdSeq', 'TestBoxes', 'idTestBox' ), + ( 'TestBoxGenIdSeq', 'TestBoxes', 'idGenTestBox' ), + ( 'FailureCategoryIdSeq', 'FailureCategories', 'idFailureCategory' ), + ( 'FailureReasonIdSeq', 'FailureReasons', 'idFailureReason' ), + ( 'BuildBlacklistIdSeq', 'BuildBlacklist', 'idBlacklisting' ), + ( 'BuildCategoryIdSeq', 'BuildCategories', 'idBuildCategory' ), + ( 'BuildIdSeq', 'Builds', 'idBuild' ), + ( 'TestResultStrTabIdSeq', 'TestResultStrTab', 'idStr' ), + ( 'TestResultIdSeq', 'TestResults', 'idTestResult' ), + ( 'TestResultValueIdSeq', 'TestResultValues', 'idTestResultValue' ), + ( 'TestResultFileId', 'TestResultFiles', 'idTestResultFile' ), + ( 'TestResultMsgIdSeq', 'TestResultMsgs', 'idTestResultMsg' ), + ( 'TestSetIdSeq', 'TestSets', 'idTestSet' ), + ( 'SchedQueueItemIdSeq', 'SchedQueues', 'idItem' ), + ]; + for (sSeq, sTab, sCol) in atSequences: + oDb.execute('SELECT MAX(%s) FROM %s' % (sCol, sTab,)); + idMax = oDb.fetchOne()[0]; + print('%s: idMax=%s' % (sSeq, idMax)); + if idMax is not None: + oDb.execute('SELECT setval(\'%s\', %s)' % (sSeq, idMax)); + + # Last step. + print('Analyzing...'); + oDb.execute('ANALYZE'); + oDb.commit(); + + print('Done!'); + return 0; + + def main(self): + """ + Main function. + """ + oDb = TMDatabaseConnection(); + + if self.oConfig.fLoadDumpIntoDatabase is not True: + rc = self._doDump(oDb); + else: + rc = self._doLoad(oDb); + + oDb.close(); + return 0; + +if __name__ == '__main__': + sys.exit(PartialDbDump().main()); diff --git a/src/VBox/ValidationKit/testmanager/db/tmdb-r01-builds-1.pgsql b/src/VBox/ValidationKit/testmanager/db/tmdb-r01-builds-1.pgsql new file mode 100644 index 00000000..f8ab331d --- /dev/null +++ b/src/VBox/ValidationKit/testmanager/db/tmdb-r01-builds-1.pgsql @@ -0,0 +1,91 @@ +-- $Id: tmdb-r01-builds-1.pgsql $ +--- @file +-- VBox Test Manager Database - Changed Builds to be historized. +-- + +-- +-- 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 +-- + + +DROP TABLE OldBuilds; +DROP TABLE NewBuilds; +DROP INDEX BuildsLookupIdx; + +\set ON_ERROR_STOP 1 + +-- +-- idBuild won't be unique, so it cannot be used directly as a foreign key +-- by TestSets. +-- +ALTER TABLE TestSets + DROP CONSTRAINT TestSets_idBuild_fkey; +ALTER TABLE TestSets + DROP CONSTRAINT TestSets_idBuildTestSuite_fkey; + + +-- +-- Create the table, filling it with the current Builds content. +-- +CREATE TABLE NewBuilds ( + idBuild INTEGER DEFAULT NEXTVAL('BuildIdSeq') NOT NULL, + tsCreated TIMESTAMP WITH TIME ZONE DEFAULT current_timestamp NOT NULL, + tsEffective TIMESTAMP WITH TIME ZONE DEFAULT current_timestamp NOT NULL, + tsExpire TIMESTAMP WITH TIME ZONE DEFAULT TIMESTAMP WITH TIME ZONE 'infinity' NOT NULL, + uidAuthor INTEGER DEFAULT NULL, + idBuildCategory INTEGER REFERENCES BuildCategories(idBuildCategory) NOT NULL, + iRevision INTEGER NOT NULL, + sVersion TEXT NOT NULL, + sLogUrl TEXT, + sBinaries TEXT NOT NULL, + fBinariesDeleted BOOLEAN DEFAULT FALSE NOT NULL, + UNIQUE (idBuild, tsExpire) +); + +INSERT INTO NewBuilds (idBuild, tsCreated, tsEffective, uidAuthor, idBuildCategory, iRevision, sVersion, sLogUrl, sBinaries) + SELECT idBuild, tsCreated, tsCreated, uidAuthor, idBuildCategory, iRevision, sVersion, sLogUrl, sBinaries + FROM Builds; +COMMIT; + +-- Switch the tables. +ALTER TABLE Builds RENAME TO OldBuilds; +ALTER TABLE NewBuilds RENAME TO Builds; +COMMIT; + +-- Finally index the table. +CREATE INDEX BuildsLookupIdx ON Builds (idBuildCategory, iRevision); +COMMIT; + +DROP TABLE OldBuilds; +COMMIT; + +-- Fix implicit index name. +ALTER INDEX newbuilds_idbuild_tsexpire_key RENAME TO builds_idbuild_tsexpire_key; + diff --git a/src/VBox/ValidationKit/testmanager/db/tmdb-r02-testboxes-1.pgsql b/src/VBox/ValidationKit/testmanager/db/tmdb-r02-testboxes-1.pgsql new file mode 100644 index 00000000..2cd75da0 --- /dev/null +++ b/src/VBox/ValidationKit/testmanager/db/tmdb-r02-testboxes-1.pgsql @@ -0,0 +1,194 @@ +-- $Id: tmdb-r02-testboxes-1.pgsql $ +--- @file +-- VBox Test Manager Database - Adds fCpu64BitGuest to TestBoxes +-- + +-- +-- Copyright (C) 2013-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 +-- + + +DROP TABLE OldTestBoxes; +DROP TABLE NewTestBoxes; + +\d TestBoxes; + +\set ON_ERROR_STOP 1 +\set AUTOCOMMIT 0 + +LOCK TABLE TestBoxStatuses IN ACCESS EXCLUSIVE MODE; +LOCK TABLE TestSets IN ACCESS EXCLUSIVE MODE; +LOCK TABLE TestBoxes IN ACCESS EXCLUSIVE MODE; + +DROP INDEX TestBoxesUuidIdx; + +-- +-- Rename the original table, drop constrains and foreign key references so we +-- get the right name automatic when creating the new one. +-- +ALTER TABLE TestBoxes RENAME TO OldTestBoxes; + +ALTER TABLE OldTestBoxes DROP CONSTRAINT testboxes_ccpus_check; +ALTER TABLE OldTestBoxes DROP CONSTRAINT testboxes_check; +ALTER TABLE OldTestBoxes DROP CONSTRAINT testboxes_cmbmemory_check; +ALTER TABLE OldTestBoxes DROP CONSTRAINT testboxes_cmbscratch_check; +ALTER TABLE OldTestBoxes DROP CONSTRAINT testboxes_pctscaletimeout_check; + +ALTER TABLE TestBoxStatuses DROP CONSTRAINT TestBoxStatuses_idGenTestBox_fkey; +ALTER TABLE TestSets DROP CONSTRAINT TestSets_idGenTestBox_fkey; + +ALTER TABLE OldTestBoxes DROP CONSTRAINT testboxes_pkey; +ALTER TABLE OldTestBoxes DROP CONSTRAINT testboxes_idgentestbox_key; + +-- +-- Create the new table, filling it with the current TestBoxes content. +-- +CREATE TABLE TestBoxes ( + --- The fixed testbox ID. + -- This is assigned when the testbox is created and will never change. + idTestBox INTEGER DEFAULT NEXTVAL('TestBoxIdSeq') NOT NULL, + --- When this row starts taking effect (inclusive). + tsEffective TIMESTAMP WITH TIME ZONE DEFAULT current_timestamp NOT NULL, + --- When this row stops being tsEffective (exclusive). + tsExpire TIMESTAMP WITH TIME ZONE DEFAULT TIMESTAMP WITH TIME ZONE 'infinity' NOT NULL, + --- The user id of the one who created/modified this entry. + -- When modified automatically by the testbox, NULL is used. + -- Non-unique foreign key: Users(uid) + uidAuthor INTEGER DEFAULT NULL, + --- Generation ID for this row. + -- This is primarily for referencing by TestSets. + idGenTestBox INTEGER UNIQUE DEFAULT NEXTVAL('TestBoxGenIdSeq') NOT NULL, + + --- The testbox IP. + -- This is from the webserver point of view and automatically updated on + -- SIGNON. The test setup doesn't permit for IP addresses to change while + -- the testbox is operational, because this will break gang tests. + ip inet NOT NULL, + --- The system or firmware UUID. + -- This uniquely identifies the testbox when talking to the server. After + -- SIGNON though, the testbox will also provide idTestBox and ip to + -- establish its identity beyond doubt. + uuidSystem uuid NOT NULL, + --- The testbox name. + -- Usually similar to the DNS name. + sName text NOT NULL, + --- Optional testbox description. + -- Intended for describing the box as well as making other relevant notes. + sDescription text DEFAULT NULL, + + --- Reference to the scheduling group that this testbox is a member of. + -- Non-unique foreign key: SchedGroups(idSchedGroup) + -- A testbox is always part of a group, the default one nothing else. + idSchedGroup INTEGER DEFAULT 1 NOT NULL, + + --- Indicates whether this testbox is enabled. + -- A testbox gets disabled when we're doing maintenance, debugging a issue + -- that happens only on that testbox, or some similar stuff. This is an + -- alternative to deleting the testbox. + fEnabled BOOLEAN DEFAULT NULL, + + --- The kind of lights-out-management. + enmLomKind LomKind_T DEFAULT 'none'::LomKind_T NOT NULL, + --- The IP adress of the lights-out-management. + -- This can be NULL if enmLomKind is 'none', otherwise it must contain a valid address. + ipLom inet DEFAULT NULL, + + --- Timeout scale factor, given as a percent. + -- This is a crude adjustment of the test case timeout for slower hardware. + pctScaleTimeout smallint DEFAULT 100 NOT NULL CHECK (pctScaleTimeout > 10 AND pctScaleTimeout < 20000), + + --- @name Scheduling properties (reported by testbox script). + -- @{ + --- Same abbrieviations as kBuild, see KBUILD_OSES. + sOs text DEFAULT NULL, + --- Informational, no fixed format. + sOsVersion text DEFAULT NULL, + --- Same as CPUID reports (GenuineIntel, AuthenticAMD, CentaurHauls, ...). + sCpuVendor text DEFAULT NULL, + --- Same as kBuild - x86, amd64, ... See KBUILD_ARCHES. + sCpuArch text DEFAULT NULL, + --- Number of CPUs, CPU cores and CPU threads. + cCpus smallint DEFAULT NULL CHECK (cCpus IS NULL OR cCpus > 0), + --- Set if capable of hardware virtualization. + fCpuHwVirt boolean DEFAULT NULL, + --- Set if capable of nested paging. + fCpuNestedPaging boolean DEFAULT NULL, + --- Set if CPU capable of 64-bit (VBox) guests. + fCpu64BitGuest boolean DEFAULT NULL, + --- Set if chipset with usable IOMMU (VT-d / AMD-Vi). + fChipsetIoMmu boolean DEFAULT NULL, + --- The (approximate) memory size in megabytes (rounded down to nearest 4 MB). + cMbMemory bigint DEFAULT NULL CHECK (cMbMemory IS NULL OR cMbMemory > 0), + --- The amount of scratch space in megabytes (rounded down to nearest 64 MB). + cMbScratch bigint DEFAULT NULL CHECK (cMbScratch IS NULL OR cMbScratch >= 0), + --- @} + + --- The testbox script revision number, serves the purpose of a version number. + -- Probably good to have when scheduling upgrades as well for status purposes. + iTestBoxScriptRev INTEGER DEFAULT 0 NOT NULL, + --- The python sys.hexversion (layed out as of 2.7). + -- Good to know which python versions we need to support. + iPythonHexVersion INTEGER DEFAULT NULL, + + --- Pending command. + -- @note We put it here instead of in TestBoxStatuses to get history. + enmPendingCmd TestBoxCmd_T DEFAULT 'none'::TestBoxCmd_T NOT NULL, + + PRIMARY KEY (idTestBox, tsExpire), + + --- Nested paging requires hardware virtualization. + CHECK (fCpuNestedPaging IS NULL OR (fCpuNestedPaging <> TRUE OR fCpuHwVirt = TRUE)) +); + + +INSERT INTO TestBoxes ( idTestBox, tsEffective, tsExpire, uidAuthor, idGenTestBox, ip, uuidSystem, sName, sDescription, + idSchedGroup, fEnabled, enmLomKind, ipLom, pctScaleTimeout, sOs, sOsVersion, sCpuVendor, sCpuArch, cCpus, fCpuHwVirt, + fCpuNestedPaging, fCpu64BitGuest, fChipsetIoMmu, cMbMemory, cMbScratch, iTestBoxScriptRev, iPythonHexVersion, + enmPendingCmd ) + SELECT idTestBox, tsEffective, tsExpire, uidAuthor, idGenTestBox, ip, uuidSystem, sName, sDescription, + idSchedGroup, fEnabled, enmLomKind, ipLom, pctScaleTimeout, sOs, sOsVersion, sCpuVendor, sCpuArch, cCpus, fCpuHwVirt, + fCpuNestedPaging, TRUE, fChipsetIoMmu, cMbMemory, cMbScratch, iTestBoxScriptRev, iPythonHexVersion, + enmPendingCmd + FROM OldTestBoxes; + +-- Add index. +CREATE UNIQUE INDEX TestBoxesUuidIdx ON TestBoxes (uuidSystem, tsExpire); + +-- Restore foreign key references to the table. +ALTER TABLE TestBoxStatuses ADD CONSTRAINT TestBoxStatuses_idGenTestBox_fkey FOREIGN KEY (idGenTestBox) REFERENCES TestBoxes(idGenTestBox); +ALTER TABLE TestSets ADD CONSTRAINT TestSets_idGenTestBox_fkey FOREIGN KEY (idGenTestBox) REFERENCES TestBoxes(idGenTestBox); + +-- Drop the old table. +DROP TABLE OldTestBoxes; + +COMMIT; + +\d TestBoxes; + diff --git a/src/VBox/ValidationKit/testmanager/db/tmdb-r03-teststatus-1.pgsql b/src/VBox/ValidationKit/testmanager/db/tmdb-r03-teststatus-1.pgsql new file mode 100644 index 00000000..2eed9f3c --- /dev/null +++ b/src/VBox/ValidationKit/testmanager/db/tmdb-r03-teststatus-1.pgsql @@ -0,0 +1,48 @@ +-- $Id: tmdb-r03-teststatus-1.pgsql $ +--- @file +-- VBox Test Manager Database - Adds 'bad-testbox', 'aborted', and 'timeout' to TestStatus_T. +-- + +-- +-- Copyright (C) 2013-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 +-- + + +\set ON_ERROR_STOP 1 +\set AUTOCOMMIT 1 + +\dT+ TestStatus_T + +ALTER TYPE TestStatus_T ADD VALUE 'bad-testbox' BEFORE 'failure'; +ALTER TYPE TestStatus_T ADD VALUE 'aborted' BEFORE 'failure'; +ALTER TYPE TestStatus_T ADD VALUE 'timed-out' AFTER 'failure'; + +\dT+ TestStatus_T + diff --git a/src/VBox/ValidationKit/testmanager/db/tmdb-r04-teststatus-2.pgsql b/src/VBox/ValidationKit/testmanager/db/tmdb-r04-teststatus-2.pgsql new file mode 100644 index 00000000..fa82d36b --- /dev/null +++ b/src/VBox/ValidationKit/testmanager/db/tmdb-r04-teststatus-2.pgsql @@ -0,0 +1,46 @@ +-- $Id: tmdb-r04-teststatus-2.pgsql $ +--- @file +-- VBox Test Manager Database - Adds 'rebooted' to TestStatus_T. +-- + +-- +-- Copyright (C) 2013-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 +-- + + +\set ON_ERROR_STOP 1 +\set AUTOCOMMIT 1 + +\dT+ TestStatus_T + +ALTER TYPE TestStatus_T ADD VALUE 'rebooted' AFTER 'timed-out'; + +\dT+ TestStatus_T + diff --git a/src/VBox/ValidationKit/testmanager/db/tmdb-r05-teststatus-3.pgsql b/src/VBox/ValidationKit/testmanager/db/tmdb-r05-teststatus-3.pgsql new file mode 100644 index 00000000..87656ca9 --- /dev/null +++ b/src/VBox/ValidationKit/testmanager/db/tmdb-r05-teststatus-3.pgsql @@ -0,0 +1,54 @@ +-- $Id: tmdb-r05-teststatus-3.pgsql $ +--- @file +-- VBox Test Manager Database - Adds 'rebooted' to TestStatus_T. +-- + +-- +-- Copyright (C) 2013-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 +-- + + +\set ON_ERROR_STOP 1 +\set AUTOCOMMIT 0 + +\d+ TestResults + +ALTER TABLE TestResults + DROP CONSTRAINT CheckStatusMatchesErrors; +ALTER TABLE TestResults + ADD CONSTRAINT CheckStatusMatchesErrors + CHECK ( (cErrors > 0 AND enmStatus IN ('running'::TestStatus_T, + 'failure'::TestStatus_T, 'timed-out'::TestStatus_T, 'rebooted'::TestStatus_T )) + OR (cErrors = 0 AND enmStatus IN ('running'::TestStatus_T, 'success'::TestStatus_T, + 'skipped'::TestStatus_T, 'aborted'::TestStatus_T, 'bad-testbox'::TestStatus_T)) + ); +COMMIT; +\d+ TestResults + diff --git a/src/VBox/ValidationKit/testmanager/db/tmdb-r06-buildsources-1.pgsql b/src/VBox/ValidationKit/testmanager/db/tmdb-r06-buildsources-1.pgsql new file mode 100644 index 00000000..8b4213c0 --- /dev/null +++ b/src/VBox/ValidationKit/testmanager/db/tmdb-r06-buildsources-1.pgsql @@ -0,0 +1,46 @@ +-- $Id: tmdb-r06-buildsources-1.pgsql $ +--- @file +-- VBox Test Manager Database - Adds cMaxSecondsOld to BuildSources. +-- + +-- +-- Copyright (C) 2013-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 +-- + + +\set ON_ERROR_STOP 1 +\set AUTOCOMMIT 1 + +\d+ buildsources + +ALTER TABLE BuildSources ADD COLUMN cSecMaxAge INTEGER DEFAULT NULL; + +\d+ buildsources + diff --git a/src/VBox/ValidationKit/testmanager/db/tmdb-r07-testresults-1.pgsql b/src/VBox/ValidationKit/testmanager/db/tmdb-r07-testresults-1.pgsql new file mode 100644 index 00000000..5ab20bc2 --- /dev/null +++ b/src/VBox/ValidationKit/testmanager/db/tmdb-r07-testresults-1.pgsql @@ -0,0 +1,47 @@ +-- $Id: tmdb-r07-testresults-1.pgsql $ +--- @file +-- VBox Test Manager Database - Adds an index to TestResults. +-- + +-- +-- Copyright (C) 2013-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 +-- + + +\set ON_ERROR_STOP 1 +\set AUTOCOMMIT 0 + +\d+ TestResults + +CREATE INDEX TestResultsNameIdx ON TestResults (idStrName, idTestResult, tsCreated); +COMMIT; + +\d+ TestResults + diff --git a/src/VBox/ValidationKit/testmanager/db/tmdb-r08-testresultvalues-1.pgsql b/src/VBox/ValidationKit/testmanager/db/tmdb-r08-testresultvalues-1.pgsql new file mode 100644 index 00000000..5d963664 --- /dev/null +++ b/src/VBox/ValidationKit/testmanager/db/tmdb-r08-testresultvalues-1.pgsql @@ -0,0 +1,47 @@ +-- $Id: tmdb-r08-testresultvalues-1.pgsql $ +--- @file +-- VBox Test Manager Database - Adds an index to TestResultValues. +-- + +-- +-- Copyright (C) 2013-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 +-- + + +\set ON_ERROR_STOP 1 +\set AUTOCOMMIT 0 + +\d+ TestResultValues + +CREATE INDEX TestResultValuesNameIdx ON TestResultValues (idStrName, tsCreated); +COMMIT; + +\d+ TestResultValues + diff --git a/src/VBox/ValidationKit/testmanager/db/tmdb-r09-testsets-1.pgsql b/src/VBox/ValidationKit/testmanager/db/tmdb-r09-testsets-1.pgsql new file mode 100644 index 00000000..5f002dc4 --- /dev/null +++ b/src/VBox/ValidationKit/testmanager/db/tmdb-r09-testsets-1.pgsql @@ -0,0 +1,48 @@ +-- $Id: tmdb-r09-testsets-1.pgsql $ +--- @file +-- VBox Test Manager Database - Adds two indexes to TestSets. +-- + +-- +-- Copyright (C) 2013-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 +-- + + +\set ON_ERROR_STOP 1 +\set AUTOCOMMIT 0 + +\d+ TestSets + +CREATE INDEX TestSetsCreated ON TestSets (tsCreated); +CREATE INDEX TestSetsDone ON TestSets (tsDone); +COMMIT; + +\d+ TestSets + diff --git a/src/VBox/ValidationKit/testmanager/db/tmdb-r10-testresultvalues-2.pgsql b/src/VBox/ValidationKit/testmanager/db/tmdb-r10-testresultvalues-2.pgsql new file mode 100644 index 00000000..ceb4a429 --- /dev/null +++ b/src/VBox/ValidationKit/testmanager/db/tmdb-r10-testresultvalues-2.pgsql @@ -0,0 +1,111 @@ +-- $Id: tmdb-r10-testresultvalues-2.pgsql $ +--- @file +-- VBox Test Manager Database - Adds an idTestSet to TestResultValues. +-- + +-- +-- Copyright (C) 2013-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 +-- + +-- +-- Cleanup after failed runs. +-- +DROP TABLE NewTestResultValues; + +-- +-- Drop all indexes (might already be dropped). +-- +DROP INDEX TestResultValuesIdx; +DROP INDEX TestResultValuesNameIdx; + +-- Die on error from now on. +\set ON_ERROR_STOP 1 +\set AUTOCOMMIT 0 + +\d+ TestResultValues; + +-- +-- Create the new version of the table and filling with the content of the old. +-- +CREATE TABLE NewTestResultValues ( + --- The ID of this value. + idTestResultValue INTEGER DEFAULT NEXTVAL('TestResultValueIdSeq'), -- PRIMARY KEY + --- The test result it was reported within. + idTestResult INTEGER NOT NULL, -- REFERENCES TestResults(idTestResult) NOT NULL, + --- The test result it was reported within. + idTestSet INTEGER NOT NULL, -- REFERENCES TestSets(idTestSet) NOT NULL, + --- Creation time stamp. + tsCreated TIMESTAMP WITH TIME ZONE DEFAULT current_timestamp NOT NULL, + --- The name. + idStrName INTEGER NOT NULL, -- REFERENCES TestResultStrTab(idStr) NOT NULL, + --- The value. + lValue bigint NOT NULL, + --- The unit. + -- @todo This is currently not defined properly. Will fix/correlate this + -- with the other places we use unit (IPRT/testdriver/VMMDev). + iUnit smallint NOT NULL --CHECK (iUnit >= 0 AND iUnit < 1024) +); +COMMIT; +\d+ NewTestResultValues + +-- Note! Using left out join here to speed up things (no hashing). +SELECT COUNT(*) FROM TestResultValues a LEFT OUTER JOIN TestResults b ON (a.idTestResult = b.idTestResult); +SELECT COUNT(*) FROM TestResultValues; + +INSERT INTO NewTestResultValues (idTestResultValue, idTestResult, idTestSet, tsCreated, idStrName, lValue, iUnit) + SELECT a.idTestResultValue, a.idTestResult, b.idTestSet, a.tsCreated, a.idStrName, a.lValue, a.iUnit + FROM TestResultValues a LEFT OUTER JOIN TestResults b ON (a.idTestResult = b.idTestResult); +COMMIT; +SELECT COUNT(*) FROM NewTestResultValues; + +-- Switch the tables. +ALTER TABLE TestResultValues RENAME TO OldTestResultValues; +ALTER TABLE NewTestResultValues RENAME TO TestResultValues; +COMMIT; + +-- Index the table. +CREATE INDEX TestResultValuesIdx ON TestResultValues(idTestResult); +CREATE INDEX TestResultValuesNameIdx ON TestResultValues(idStrName, tsCreated); +COMMIT; + +-- Drop the old table. +DROP TABLE OldTestResultValues; +COMMIT; + +-- Add the constraints constraint. +ALTER TABLE TestResultValues ADD CONSTRAINT TestResultValues_iUnit_Check CHECK (iUnit >= 0 AND iUnit < 1024); +ALTER TABLE TestResultValues ADD PRIMARY KEY (idTestResultValue); +ALTER TABLE TestResultValues ADD FOREIGN KEY (idStrName) REFERENCES TestResultstrtab(idStr); +ALTER TABLE TestResultValues ADD FOREIGN KEY (idTestResult) REFERENCES TestResults(idTestResult); +ALTER TABLE TestResultValues ADD FOREIGN KEY (idTestSet) REFERENCES TestSets(idTestSet); +COMMIT; + +\d+ TestResultValues; + diff --git a/src/VBox/ValidationKit/testmanager/db/tmdb-r11-testsets-2.pgsql b/src/VBox/ValidationKit/testmanager/db/tmdb-r11-testsets-2.pgsql new file mode 100644 index 00000000..1df2807f --- /dev/null +++ b/src/VBox/ValidationKit/testmanager/db/tmdb-r11-testsets-2.pgsql @@ -0,0 +1,214 @@ +-- $Id: tmdb-r11-testsets-2.pgsql $ +--- @file +-- VBox Test Manager Database - Adds an idBuildCategories to TestSets. +-- + +-- +-- Copyright (C) 2013-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 +-- + +-- +-- Drop all indexes (might already be dropped). +-- +DROP INDEX TestSetsGangIdx; +DROP INDEX TestSetsBoxIdx; +DROP INDEX TestSetsBuildIdx; +DROP INDEX TestSetsTestCaseIdx; +DROP INDEX TestSetsTestVarIdx; +DROP INDEX TestSetsCreated; +DROP INDEX TestSetsDone; + +-- +-- Drop foreign keys on this table. +-- +ALTER TABLE SchedQueues DROP CONSTRAINT SchedQueues_idTestSetGangLeader_fkey; +ALTER TABLE TestBoxStatuses DROP CONSTRAINT TestBoxStatuses_idTestSet_fkey; +ALTER TABLE TestResults DROP CONSTRAINT idTestSetFk; -- old name +ALTER TABLE TestResults DROP CONSTRAINT TestResults_idTestSet_fkey; +ALTER TABLE TestResultValues DROP CONSTRAINT TestResultValues_idTestSet_fkey; + +-- +-- Cleanup after failed runs. +-- +DROP TABLE NewTestSets; +DROP TABLE OldTestSets; + +-- Die on error from now on. +\set ON_ERROR_STOP 1 +\set AUTOCOMMIT 0 + +\d+ TestSets; + +-- +-- Create the new version of the table and filling with the content of the old. +-- +CREATE TABLE NewTestSets ( + --- The ID of this test set. + idTestSet INTEGER DEFAULT NEXTVAL('TestSetIdSeq') NOT NULL, -- PRIMARY KEY + + --- The test config timestamp, used when reading test config. + tsConfig TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP NOT NULL, + --- When this test set was scheduled. + -- idGenTestBox is valid at this point. + tsCreated TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP NOT NULL, + --- When this test completed, i.e. testing stopped. This should only be set once. + tsDone TIMESTAMP WITH TIME ZONE DEFAULT NULL, + --- The current status. + enmStatus TestStatus_T DEFAULT 'running'::TestStatus_T NOT NULL, + + --- The build we're testing. + -- Non-unique foreign key: Builds(idBuild) + idBuild INTEGER NOT NULL, + --- The build category of idBuild when the test started. + -- This is for speeding up graph data collection, i.e. avoid idBuild + -- the WHERE part of the selection. + idBuildCategory INTEGER , -- NOT NULL REFERENCES BuildCategories(idBuildCategory) + --- The test suite build we're using to do the testing. + -- This is NULL if the test suite zip wasn't referred or if a test suite + -- build source wasn't configured. + -- Non-unique foreign key: Builds(idBuild) + idBuildTestSuite INTEGER DEFAULT NULL, + + --- The exact testbox configuration. + idGenTestBox INTEGER NOT NULL, -- REFERENCES TestBoxes(idGenTestBox) + --- The testbox ID for joining with (valid: tsStarted). + -- Non-unique foreign key: TestBoxes(idTestBox) + idTestBox INTEGER NOT NULL, + + --- The testgroup (valid: tsConfig). + -- Non-unique foreign key: TestBoxes(idTestGroup) + -- Note! This also gives the member ship entry, since a testcase can only + -- have one membership per test group. + idTestGroup INTEGER NOT NULL, + + --- The exact test case config we executed in this test run. + idGenTestCase INTEGER NOT NULL, -- REFERENCES TestCases(idGenTestCase) + --- The test case ID for joining with (valid: tsConfig). + -- Non-unique foreign key: TestBoxes(idTestCase) + idTestCase INTEGER NOT NULL, + + --- The arguments (and requirements++) we executed this test case with. + idGenTestCaseArgs INTEGER NOT NULL, -- REFERENCES TestCaseArgs(idGenTestCaseArgs) + --- The argument variation ID (valid: tsConfig). + -- Non-unique foreign key: TestCaseArgs(idTestCaseArgs) + idTestCaseArgs INTEGER NOT NULL, + + --- The root of the test result tree. + -- @note This will only be NULL early in the transaction setting up the testset. + -- @note If the test reports more than one top level test result, we'll + -- fail the whole test run and let the test developer fix it. + idTestResult INTEGER DEFAULT NULL, -- REFERENCES TestResults(idTestResult) + + --- The base filename used for storing files related to this test set. + -- This is a path relative to wherever TM is dumping log files. In order + -- to not become a file system test case, we will try not to put too many + -- hundred thousand files in a directory. A simple first approach would + -- be to just use the current date (tsCreated) like this: + -- TM_FILE_DIR/year/month/day/TestSets.idTestSet + -- + -- The primary log file for the test is this name suffixed by '.log'. + -- + -- The files in the testresultfile table gets their full names like this: + -- TM_FILE_DIR/sBaseFilename-testresultfile.id-TestResultStrTab(testresultfile.idStrFilename) + -- + -- @remarks We store this explicitly in case we change the directly layout + -- at some later point. + sBaseFilename text UNIQUE NOT NULL, + + --- The gang member number number, 0 is the leader. + iGangMemberNo SMALLINT DEFAULT 0 NOT NULL, --CHECK (iGangMemberNo >= 0 AND iGangMemberNo < 1024), + --- The test set of the gang leader, NULL if no gang involved. + -- @note This is set by the gang leader as well, so that we can find all + -- gang members by WHERE idTestSetGangLeader = :id. + idTestSetGangLeader INTEGER DEFAULT NULL -- REFERENCES TestSets(idTestSet) + +); +COMMIT; +\d+ NewTestSets + +-- Note! Using left out join here to speed up things (no hashing). +SELECT COUNT(*) FROM TestSets a LEFT OUTER JOIN Builds b ON (a.idBuild = b.idBuild AND b.tsExpire = 'infinity'::TIMESTAMP); +SELECT COUNT(*) FROM TestSets; + +INSERT INTO NewTestSets (idTestSet, tsConfig, tsCreated, tsDone, enmStatus, idBuild, idBuildCategory, idBuildTestSuite, + idGenTestBox, idTestBox, idTestGroup, idGenTestCase, idTestCase, idGenTestCaseArgs, idTestCaseArgs, + idTestResult, sBaseFilename, iGangMemberNo, idTestSetGangLeader ) + SELECT a.idTestSet, a.tsConfig, a.tsCreated, tsDone, a.enmStatus, a.idBuild, b.idBuildCategory, a.idBuildTestSuite, + a.idGenTestBox, a.idTestBox, a.idTestGroup, a.idGenTestCase, a.idTestCase, a.idGenTestCaseArgs, a.idTestCaseArgs, + a.idTestResult, a.sBaseFilename, a.iGangMemberNo, a.idTestSetGangLeader + FROM TestSets a LEFT OUTER JOIN Builds b ON (a.idBuild = b.idBuild AND b.tsExpire = 'infinity'::TIMESTAMP); +COMMIT; +SELECT COUNT(*) FROM NewTestSets; + +-- Note! 2-3 builds are missing from the Builds table, so fudge it. +UPDATE NewTestSets + SET idBuildCategory = 1 + WHERE idBuildCategory IS NULL; + +-- Switch the tables. +ALTER TABLE TestSets RENAME TO OldTestSets; +ALTER TABLE NewTestSets RENAME TO TestSets; +COMMIT; + +-- Index the table. +CREATE INDEX TestSetsGangIdx ON TestSets (idTestSetGangLeader); +CREATE INDEX TestSetsBoxIdx ON TestSets (idTestBox, idTestResult); +CREATE INDEX TestSetsBuildIdx ON TestSets (idBuild, idTestResult); +CREATE INDEX TestSetsTestCaseIdx ON TestSets (idTestCase, idTestResult); +CREATE INDEX TestSetsTestVarIdx ON TestSets (idTestCaseArgs, idTestResult); +CREATE INDEX TestSetsCreated ON TestSets (tsCreated); +CREATE INDEX TestSetsDone ON TestSets (tsDone); +COMMIT; + +-- Drop the old table. +DROP TABLE OldTestSets; +COMMIT; + +-- Add the constraints constraint. +ALTER TABLE TestSets ADD CONSTRAINT TestSets_iGangMemberNo_Check CHECK (iGangMemberNo >= 0 AND iGangMemberNo < 1024); +ALTER TABLE TestSets ADD PRIMARY KEY (idTestSet); +ALTER TABLE TestSets ADD FOREIGN KEY (idBuildCategory) REFERENCES BuildCategories(idBuildCategory); +ALTER TABLE TestSets ADD FOREIGN KEY (idGenTestBox) REFERENCES TestBoxes(idGenTestBox); +ALTER TABLE TestSets ADD FOREIGN KEY (idGenTestCase) REFERENCES TestCases(idGenTestCase); +ALTER TABLE TestSets ADD FOREIGN KEY (idGenTestCaseArgs) REFERENCES TestCaseArgs(idGenTestCaseArgs); +ALTER TABLE TestSets ADD FOREIGN KEY (idTestResult) REFERENCES TestResults(idTestResult); +ALTER TABLE TestSets ADD FOREIGN KEY (idTestSetGangLeader) REFERENCES TestSets(idTestSet); +COMMIT; + +-- Restore foreign keys. +LOCK TABLE SchedQueues, TestBoxStatuses, TestResults, TestResultValues IN EXCLUSIVE MODE; +ALTER TABLE SchedQueues ADD FOREIGN KEY (idTestSetGangLeader) REFERENCES TestSets(idTestSet) MATCH FULL; +ALTER TABLE TestBoxStatuses ADD FOREIGN KEY (idTestSet) REFERENCES TestSets(idTestSet) MATCH FULL; +ALTER TABLE TestResults ADD FOREIGN KEY (idTestSet) REFERENCES TestSets(idTestSet) MATCH FULL; +ALTER TABLE TestResultValues ADD FOREIGN KEY (idTestSet) REFERENCES TestSets(idTestSet) MATCH FULL; +COMMIT; + +\d+ TestSets; + diff --git a/src/VBox/ValidationKit/testmanager/db/tmdb-r12-testresultvalues-3-testsets-3.pgsql b/src/VBox/ValidationKit/testmanager/db/tmdb-r12-testresultvalues-3-testsets-3.pgsql new file mode 100644 index 00000000..57f48dea --- /dev/null +++ b/src/VBox/ValidationKit/testmanager/db/tmdb-r12-testresultvalues-3-testsets-3.pgsql @@ -0,0 +1,58 @@ +-- $Id: tmdb-r12-testresultvalues-3-testsets-3.pgsql $ +--- @file +-- VBox Test Manager Database - Graph related optimizations for TestResultValues and TestSets. +-- + +-- +-- Copyright (C) 2013-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 +-- + + +\set ON_ERROR_STOP 1 +\set AUTOCOMMIT 0 + +\d+ TestResultValues + +-- Rename index to better show it's purpose. +ALTER INDEX TestResultValuesNameIdx RENAME TO TestResultValuesGraphIdx; +COMMIT; + +-- Combine the tsCreated and tsDone indexes. +DROP INDEX TestSetsCreated; +DROP INDEX TestSetsDone; +CREATE INDEX TestSetsCreatedDoneIdx ON TestSets (tsCreated, tsDone); +COMMIT; + +-- Create index for graph. +CREATE INDEX TestSetsGraphBoxIdx ON TestSets (idTestBox, tsCreated, tsDone, idBuildCategory, idTestCase); +COMMIT; + +\d+ TestResultValues + diff --git a/src/VBox/ValidationKit/testmanager/db/tmdb-r13-buildcategories-1-vcsrevisions-1.pgsql b/src/VBox/ValidationKit/testmanager/db/tmdb-r13-buildcategories-1-vcsrevisions-1.pgsql new file mode 100644 index 00000000..0dfdd8c7 --- /dev/null +++ b/src/VBox/ValidationKit/testmanager/db/tmdb-r13-buildcategories-1-vcsrevisions-1.pgsql @@ -0,0 +1,134 @@ +-- $Id: tmdb-r13-buildcategories-1-vcsrevisions-1.pgsql $ +--- @file +-- VBox Test Manager Database - Adds an sRepository to Builds and creates a new VcsRepositories table. +-- + +-- +-- Copyright (C) 2013-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 +-- + +-- +-- Cleanup after failed runs. +-- +DROP TABLE NewBuildCategories; +DROP TABLE OldBuildCategories; + +-- +-- Drop foreign keys on this table. +-- +ALTER TABLE Builds DROP CONSTRAINT NewBuilds_idBuildCategory_fkey; +ALTER TABLE Builds DROP CONSTRAINT Builds_idBuildCategory_fkey; +ALTER TABLE TestSets DROP CONSTRAINT TestSets_idBuildCategory_fkey; + +-- Die on error from now on. +\set ON_ERROR_STOP 1 +\set AUTOCOMMIT 0 + +\d+ BuildCategories; + +-- +-- Create the new version of the table and filling with the content of the old. +-- +CREATE TABLE NewBuildCategories ( + --- The build type identifier. + idBuildCategory INTEGER PRIMARY KEY DEFAULT NEXTVAL('BuildCategoryIdSeq') NOT NULL, + --- Product. + -- The product name. For instance 'VBox' or 'VBoxTestSuite'. + sProduct TEXT NOT NULL, + --- The version control repository name. + sRepository TEXT NOT NULL, + --- The branch name (in the version control system). + sBranch TEXT NOT NULL, + --- The build type. + -- See KBUILD_BLD_TYPES in kBuild for a list of standard build types. + sType TEXT NOT NULL, + --- Array of the 'sOs.sCpuArch' supported by the build. + -- See KBUILD_OSES in kBuild for a list of standard target OSes, and + -- KBUILD_ARCHES for a list of standard architectures. + -- + -- @remarks 'os-agnostic' is used if the build doesn't really target any + -- specific OS or if it targets all applicable OSes. + -- 'noarch' is used if the build is architecture independent or if + -- all applicable architectures are handled. + -- Thus, 'os-agnostic.noarch' will run on all build boxes. + -- + -- @note The array shall be sorted ascendingly to prevent unnecessary duplicates! + -- + asOsArches TEXT ARRAY NOT NULL, + + UNIQUE (sProduct, sRepository, sBranch, sType, asOsArches) +); +COMMIT; +\d+ NewBuildCategories + +INSERT INTO NewBuildCategories (idBuildCategory, sProduct, sRepository, sBranch, sType, asOsArches) + SELECT idBuildCategory, sProduct, 'vbox', sBranch, sType, asOsArches + FROM BuildCategories +COMMIT; + +-- Switch the tables. +ALTER TABLE BuildCategories RENAME TO OldBuildCategories; +ALTER TABLE NewBuildCategories RENAME TO BuildCategories; +COMMIT; + +-- Drop the old table. +DROP TABLE OldBuildCategories; +COMMIT; + +-- Restore foreign keys. +LOCK TABLE Builds, TestSets; +ALTER TABLE Builds ADD FOREIGN KEY (idBuildCategory) REFERENCES BuildCategories(idBuildCategory); +ALTER TABLE TestSets ADD FOREIGN KEY (idBuildCategory) REFERENCES BuildCategories(idBuildCategory); +COMMIT; + +\d+ BuildCategories; + + +-- +-- Create the new VcsRevisions table. +-- +CREATE TABLE VcsRevisions ( + --- The version control tree name. + sRepository TEXT NOT NULL, + --- The version control tree revision number. + iRevision INTEGER NOT NULL, + --- When the revision was created (committed). + tsCreated TIMESTAMP WITH TIME ZONE NOT NULL, + --- The name of the committer. + -- @note Not to be confused with uidAuthor and test manager users. + sAuthor TEXT, + --- The commit message. + sMessage TEXT, + + UNIQUE (sRepository, iRevision) +); +COMMIT; +\d+ VcsRevisions; + diff --git a/src/VBox/ValidationKit/testmanager/db/tmdb-r14-testboxes-2.pgsql b/src/VBox/ValidationKit/testmanager/db/tmdb-r14-testboxes-2.pgsql new file mode 100644 index 00000000..784398b5 --- /dev/null +++ b/src/VBox/ValidationKit/testmanager/db/tmdb-r14-testboxes-2.pgsql @@ -0,0 +1,201 @@ +-- $Id: tmdb-r14-testboxes-2.pgsql $ +--- @file +-- VBox Test Manager Database - Adds sCpuName, lCpuRevision and sReport to TestBoxes. +-- + +-- +-- Copyright (C) 2013-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 +-- + + +DROP TABLE OldTestBoxes; +DROP TABLE NewTestBoxes; + +\d TestBoxes; + +\set ON_ERROR_STOP 1 +\set AUTOCOMMIT 0 + +LOCK TABLE TestBoxStatuses IN ACCESS EXCLUSIVE MODE; +LOCK TABLE TestSets IN ACCESS EXCLUSIVE MODE; +LOCK TABLE TestBoxes IN ACCESS EXCLUSIVE MODE; + +DROP INDEX TestBoxesUuidIdx; + +-- +-- Rename the original table, drop constrains and foreign key references so we +-- get the right name automatic when creating the new one. +-- +ALTER TABLE TestBoxes RENAME TO OldTestBoxes; + +ALTER TABLE OldTestBoxes DROP CONSTRAINT testboxes_ccpus_check; +ALTER TABLE OldTestBoxes DROP CONSTRAINT testboxes_check; +ALTER TABLE OldTestBoxes DROP CONSTRAINT testboxes_cmbmemory_check; +ALTER TABLE OldTestBoxes DROP CONSTRAINT testboxes_cmbscratch_check; +ALTER TABLE OldTestBoxes DROP CONSTRAINT testboxes_pctscaletimeout_check; + +ALTER TABLE TestBoxStatuses DROP CONSTRAINT TestBoxStatuses_idGenTestBox_fkey; +ALTER TABLE TestSets DROP CONSTRAINT TestSets_idGenTestBox_fkey; + +ALTER TABLE OldTestBoxes DROP CONSTRAINT testboxes_pkey; +ALTER TABLE OldTestBoxes DROP CONSTRAINT testboxes_idgentestbox_key; + +-- +-- Create the new table, filling it with the current TestBoxes content. +-- +CREATE TABLE TestBoxes ( + --- The fixed testbox ID. + -- This is assigned when the testbox is created and will never change. + idTestBox INTEGER DEFAULT NEXTVAL('TestBoxIdSeq') NOT NULL, + --- When this row starts taking effect (inclusive). + tsEffective TIMESTAMP WITH TIME ZONE DEFAULT current_timestamp NOT NULL, + --- When this row stops being tsEffective (exclusive). + tsExpire TIMESTAMP WITH TIME ZONE DEFAULT TIMESTAMP WITH TIME ZONE 'infinity' NOT NULL, + --- The user id of the one who created/modified this entry. + -- When modified automatically by the testbox, NULL is used. + -- Non-unique foreign key: Users(uid) + uidAuthor INTEGER DEFAULT NULL, + --- Generation ID for this row. + -- This is primarily for referencing by TestSets. + idGenTestBox INTEGER UNIQUE DEFAULT NEXTVAL('TestBoxGenIdSeq') NOT NULL, + + --- The testbox IP. + -- This is from the webserver point of view and automatically updated on + -- SIGNON. The test setup doesn't permit for IP addresses to change while + -- the testbox is operational, because this will break gang tests. + ip inet NOT NULL, + --- The system or firmware UUID. + -- This uniquely identifies the testbox when talking to the server. After + -- SIGNON though, the testbox will also provide idTestBox and ip to + -- establish its identity beyond doubt. + uuidSystem uuid NOT NULL, + --- The testbox name. + -- Usually similar to the DNS name. + sName text NOT NULL, + --- Optional testbox description. + -- Intended for describing the box as well as making other relevant notes. + sDescription text DEFAULT NULL, + + --- Reference to the scheduling group that this testbox is a member of. + -- Non-unique foreign key: SchedGroups(idSchedGroup) + -- A testbox is always part of a group, the default one nothing else. + idSchedGroup INTEGER DEFAULT 1 NOT NULL, + + --- Indicates whether this testbox is enabled. + -- A testbox gets disabled when we're doing maintenance, debugging a issue + -- that happens only on that testbox, or some similar stuff. This is an + -- alternative to deleting the testbox. + fEnabled BOOLEAN DEFAULT NULL, + + --- The kind of lights-out-management. + enmLomKind LomKind_T DEFAULT 'none'::LomKind_T NOT NULL, + --- The IP adress of the lights-out-management. + -- This can be NULL if enmLomKind is 'none', otherwise it must contain a valid address. + ipLom inet DEFAULT NULL, + + --- Timeout scale factor, given as a percent. + -- This is a crude adjustment of the test case timeout for slower hardware. + pctScaleTimeout smallint DEFAULT 100 NOT NULL CHECK (pctScaleTimeout > 10 AND pctScaleTimeout < 20000), + + --- @name Scheduling properties (reported by testbox script). + -- @{ + --- Same abbrieviations as kBuild, see KBUILD_OSES. + sOs text DEFAULT NULL, + --- Informational, no fixed format. + sOsVersion text DEFAULT NULL, + --- Same as CPUID reports (GenuineIntel, AuthenticAMD, CentaurHauls, ...). + sCpuVendor text DEFAULT NULL, + --- Same as kBuild - x86, amd64, ... See KBUILD_ARCHES. + sCpuArch text DEFAULT NULL, + --- The CPU name if available. + sCpuName text DEFAULT NULL, + --- Number identifying the CPU family/model/stepping/whatever. + -- For x86 and AMD64 type CPUs, this will on the following format: + -- (EffFamily << 24) | (EffModel << 8) | Stepping. + lCpuRevision bigint DEFAULT NULL, + --- Number of CPUs, CPU cores and CPU threads. + cCpus smallint DEFAULT NULL CHECK (cCpus IS NULL OR cCpus > 0), + --- Set if capable of hardware virtualization. + fCpuHwVirt boolean DEFAULT NULL, + --- Set if capable of nested paging. + fCpuNestedPaging boolean DEFAULT NULL, + --- Set if CPU capable of 64-bit (VBox) guests. + fCpu64BitGuest boolean DEFAULT NULL, + --- Set if chipset with usable IOMMU (VT-d / AMD-Vi). + fChipsetIoMmu boolean DEFAULT NULL, + --- The (approximate) memory size in megabytes (rounded down to nearest 4 MB). + cMbMemory bigint DEFAULT NULL CHECK (cMbMemory IS NULL OR cMbMemory > 0), + --- The amount of scratch space in megabytes (rounded down to nearest 64 MB). + cMbScratch bigint DEFAULT NULL CHECK (cMbScratch IS NULL OR cMbScratch >= 0), + --- Free form hardware and software report field. + sReport text DEFAULT NULL, + --- @} + + --- The testbox script revision number, serves the purpose of a version number. + -- Probably good to have when scheduling upgrades as well for status purposes. + iTestBoxScriptRev INTEGER DEFAULT 0 NOT NULL, + --- The python sys.hexversion (layed out as of 2.7). + -- Good to know which python versions we need to support. + iPythonHexVersion INTEGER DEFAULT NULL, + + --- Pending command. + -- @note We put it here instead of in TestBoxStatuses to get history. + enmPendingCmd TestBoxCmd_T DEFAULT 'none'::TestBoxCmd_T NOT NULL, + + PRIMARY KEY (idTestBox, tsExpire), + + --- Nested paging requires hardware virtualization. + CHECK (fCpuNestedPaging IS NULL OR (fCpuNestedPaging <> TRUE OR fCpuHwVirt = TRUE)) +); + +INSERT INTO TestBoxes ( idTestBox, tsEffective, tsExpire, uidAuthor, idGenTestBox, ip, uuidSystem, sName, sDescription, + idSchedGroup, fEnabled, enmLomKind, ipLom, pctScaleTimeout, sOs, sOsVersion, sCpuVendor, sCpuArch, sCpuName, + lCpuRevision, cCpus, fCpuHwVirt, fCpuNestedPaging, fCpu64BitGuest, fChipsetIoMmu, cMbMemory, cMbScratch, sReport, + iTestBoxScriptRev, iPythonHexVersion, enmPendingCmd ) + SELECT idTestBox, tsEffective, tsExpire, uidAuthor, idGenTestBox, ip, uuidSystem, sName, sDescription, + idSchedGroup, fEnabled, enmLomKind, ipLom, pctScaleTimeout, sOs, sOsVersion, sCpuVendor, sCpuArch, NULL, + NULL, cCpus, fCpuHwVirt, fCpuNestedPaging, fCpu64BitGuest, fChipsetIoMmu, cMbMemory, cMbScratch, NULL, + iTestBoxScriptRev, iPythonHexVersion, enmPendingCmd + FROM OldTestBoxes; + +-- Add index. +CREATE UNIQUE INDEX TestBoxesUuidIdx ON TestBoxes (uuidSystem, tsExpire); + +-- Restore foreign key references to the table. +ALTER TABLE TestBoxStatuses ADD CONSTRAINT TestBoxStatuses_idGenTestBox_fkey FOREIGN KEY (idGenTestBox) REFERENCES TestBoxes(idGenTestBox); +ALTER TABLE TestSets ADD CONSTRAINT TestSets_idGenTestBox_fkey FOREIGN KEY (idGenTestBox) REFERENCES TestBoxes(idGenTestBox); + +-- Drop the old table. +DROP TABLE OldTestBoxes; + +COMMIT; + +\d TestBoxes; + diff --git a/src/VBox/ValidationKit/testmanager/db/tmdb-r15-index-sorting.pgsql b/src/VBox/ValidationKit/testmanager/db/tmdb-r15-index-sorting.pgsql new file mode 100644 index 00000000..b95dff80 --- /dev/null +++ b/src/VBox/ValidationKit/testmanager/db/tmdb-r15-index-sorting.pgsql @@ -0,0 +1,108 @@ +-- $Id: tmdb-r15-index-sorting.pgsql $ +--- @file +-- VBox Test Manager Database - Index tuning effort. +-- + +-- +-- Copyright (C) 2015-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 +-- + + +-- +-- Reordered, modified and new indexes. +-- +\d UsersLoginNameIdx; +DROP INDEX UsersLoginNameIdx; +CREATE INDEX UsersLoginNameIdx ON Users (sLoginName, tsExpire DESC); +\d UsersLoginNameIdx; +ANALYZE VERBOSE Users; + + +\d TestCaseArgsLookupIdx; +DROP INDEX TestCaseArgsLookupIdx; +CREATE INDEX TestCaseArgsLookupIdx ON TestCaseArgs (idTestCase, tsExpire DESC, tsEffective ASC); +\d TestCaseArgsLookupIdx; +ANALYZE VERBOSE TestCaseArgs; + + +\d TestGroups_id_index; +DROP INDEX TestGroups_id_index; +CREATE INDEX TestGroups_id_index ON TestGroups (idTestGroup, tsExpire DESC, tsEffective ASC); +\d TestGroups_id_index; +ANALYZE VERBOSE TestGroups; + + +\d TestBoxesUuidIdx; +DROP INDEX TestBoxesUuidIdx; +CREATE UNIQUE INDEX TestBoxesUuidIdx ON TestBoxes (uuidSystem, tsExpire DESC); +\d TestBoxesUuidIdx; +DROP INDEX IF EXISTS TestBoxesExpireEffectiveIdx; +CREATE INDEX TestBoxesExpireEffectiveIdx ON TestBoxes (tsExpire DESC, tsEffective ASC); +\d TestBoxesExpireEffectiveIdx; +ANALYZE VERBOSE TestBoxes; + + +DROP INDEX IF EXISTS BuildBlacklistIdx; +CREATE INDEX BuildBlacklistIdx ON BuildBlacklist (iLastRevision DESC, iFirstRevision ASC, sProduct, sBranch, + tsExpire DESC, tsEffective ASC); +\d BuildBlacklist; +ANALYZE VERBOSE BuildBlacklist; + + +\d TestResultsNameIdx; +DROP INDEX TestResultsNameIdx; +CREATE INDEX TestResultsNameIdx ON TestResults (idStrName, tsCreated DESC); +\d TestResultsNameIdx; +DROP INDEX IF EXISTS TestResultsNameIdx2; +CREATE INDEX TestResultsNameIdx2 ON TestResults (idTestResult, idStrName); +\d TestResultsNameIdx2; +ANALYZE VERBOSE TestResults; + + +\d TestSetsCreatedDoneIdx; +DROP INDEX TestSetsCreatedDoneIdx; +DROP INDEX IF EXISTS TestSetsDoneCreatedBuildCatIdx; +CREATE INDEX TestSetsDoneCreatedBuildCatIdx ON TestSets (tsDone DESC NULLS FIRST, tsCreated ASC, idBuildCategory); +\d TestSetsDoneCreatedBuildCatIdx; +\d TestSetsGraphBoxIdx; +DROP INDEX TestSetsGraphBoxIdx; +CREATE INDEX TestSetsGraphBoxIdx ON TestSets (idTestBox, tsCreated DESC, tsDone ASC NULLS LAST, idBuildCategory, idTestCase); +\d TestSetsGraphBoxIdx; +ANALYZE VERBOSE TestSets; + + +DROP INDEX IF EXISTS SchedQueuesItemIdx; +CREATE INDEX SchedQueuesItemIdx ON SchedQueues(idItem); +\d SchedQueuesItemIdx; +DROP INDEX IF EXISTS SchedQueuesSchedGroupIdx; +CREATE INDEX SchedQueuesSchedGroupIdx ON SchedQueues(idSchedGroup); +\d SchedQueuesSchedGroupIdx; +ANALYZE VERBOSE SchedQueues; + diff --git a/src/VBox/ValidationKit/testmanager/db/tmdb-r16-testcaseargs-1-testresultfailures-1.pgsql b/src/VBox/ValidationKit/testmanager/db/tmdb-r16-testcaseargs-1-testresultfailures-1.pgsql new file mode 100644 index 00000000..108da811 --- /dev/null +++ b/src/VBox/ValidationKit/testmanager/db/tmdb-r16-testcaseargs-1-testresultfailures-1.pgsql @@ -0,0 +1,122 @@ +-- $Id: tmdb-r16-testcaseargs-1-testresultfailures-1.pgsql $ +--- @file +-- VBox Test Manager Database - Adds sName to TestCaseArgs, idTestSet +-- to TestResultFailures and add some indexes to the latter as well. +-- + +-- +-- Copyright (C) 2013-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 +-- + + +DROP TABLE OldTestCaseArgs; +DROP TABLE NewTestCaseArgs; + + +\set ON_ERROR_STOP 1 +\set AUTOCOMMIT 0 + +LOCK TABLE TestBoxStatuses IN ACCESS EXCLUSIVE MODE; +LOCK TABLE TestSets IN ACCESS EXCLUSIVE MODE; +LOCK TABLE TestCaseArgs IN ACCESS EXCLUSIVE MODE; +LOCK TABLE TestResultFailures IN ACCESS EXCLUSIVE MODE; + +-- +-- TestCaseArgs is simple and we can use ALTER TABLE for a change. +-- +\d TestCaseArgs; +ALTER TABLE TestCaseArgs ADD COLUMN sSubName text DEFAULT NULL; +\d TestCaseArgs; + + +-- +-- Rename the original table, drop constrains and foreign key references so we +-- get the right name automatic when creating the new one. +-- +\d TestResultFailures; +ALTER TABLE TestResultFailures DROP CONSTRAINT idTestResultFk; +ALTER TABLE TestResultFailures RENAME TO OldTestResultFailures; + +DROP INDEX IF EXISTS TestResultFailureIdx; +DROP INDEX IF EXISTS TestResultFailureIdx2; +DROP INDEX IF EXISTS TestResultFailureIdx3; + + +CREATE TABLE TestResultFailures ( + --- The test result we're disucssing. + -- @note The foreign key is declared after TestResults (further down). + idTestResult INTEGER NOT NULL, + --- When this row starts taking effect (inclusive). + tsEffective TIMESTAMP WITH TIME ZONE DEFAULT current_timestamp NOT NULL, + --- When this row stops being tsEffective (exclusive). + tsExpire TIMESTAMP WITH TIME ZONE DEFAULT TIMESTAMP WITH TIME ZONE 'infinity' NOT NULL, + --- The user id of the one who created/modified this entry. + -- Non-unique foreign key: Users(uid) + uidAuthor INTEGER NOT NULL, + --- The testsest this result is a part of. + -- This is mainly an aid for bypassing the enormous TestResults table. + -- Note! This is a foreign key, but we have to add it after TestSets has + -- been created, see further down. + idTestSet INTEGER NOT NULL, + + --- The suggested failure reason. + -- Non-unique foreign key: FailureReasons(idFailureReason) + idFailureReason INTEGER NOT NULL, + --- Optional comment. + sComment text DEFAULT NULL, + + PRIMARY KEY (idTestResult, tsExpire) +); + +INSERT INTO TestResultFailures ( idTestResult, tsEffective, tsExpire, uidAuthor, idTestSet, idFailureReason, sComment ) + SELECT o.idTestResult, o.tsEffective, o.tsExpire, o.uidAuthor, tr.idTestSet, o.idFailureReason, sComment + FROM OldTestResultFailures o, + TestResults tr + WHERE o.idTestResult = tr.idTestResult; + +-- Add unique constraint to TestResult for our new foreign key. +ALTER TABLE TestResults ADD CONSTRAINT TestResults_idTestResult_idTestSet_key UNIQUE (idTestResult, idTestSet); + +-- Restore foreign key. +ALTER TABLE TestResultFailures ADD CONSTRAINT TestResultFailures_idTestResult_idTestSet_fkey + FOREIGN KEY (idTestResult, idTestSet) REFERENCES TestResults(idTestResult, idTestSet) MATCH FULL; + +-- Add new indexes. +CREATE INDEX TestResultFailureIdx ON TestResultFailures (idTestSet, tsExpire DESC, tsEffective ASC); +CREATE INDEX TestResultFailureIdx2 ON TestResultFailures (idTestResult, tsExpire DESC, tsEffective ASC); +CREATE INDEX TestResultFailureIdx3 ON TestResultFailures (idFailureReason, idTestResult, tsExpire DESC, tsEffective ASC); + +-- Drop the old table. +DROP TABLE OldTestResultFailures; + +COMMIT; + +\d TestResultFailures; + diff --git a/src/VBox/ValidationKit/testmanager/db/tmdb-r17-testresultvalues-4.pgsql b/src/VBox/ValidationKit/testmanager/db/tmdb-r17-testresultvalues-4.pgsql new file mode 100644 index 00000000..d97954b6 --- /dev/null +++ b/src/VBox/ValidationKit/testmanager/db/tmdb-r17-testresultvalues-4.pgsql @@ -0,0 +1,48 @@ +-- $Id: tmdb-r17-testresultvalues-4.pgsql $ +--- @file +-- VBox Test Manager Database - Log viewer related optimizations for TestResultValues. +-- + +-- +-- Copyright (C) 2013-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 +-- + + +\set ON_ERROR_STOP 1 +\set AUTOCOMMIT 0 + +\d+ TestResultValues + +-- Create index for the log viewer +CREATE INDEX TestResultValuesLogIdx ON TestResultValues(idTestSet, tsCreated); +COMMIT; + +\d+ TestResultValues + diff --git a/src/VBox/ValidationKit/testmanager/db/tmdb-r18-testresultfiles-1-testresultmsgs-1.pgsql b/src/VBox/ValidationKit/testmanager/db/tmdb-r18-testresultfiles-1-testresultmsgs-1.pgsql new file mode 100644 index 00000000..88788273 --- /dev/null +++ b/src/VBox/ValidationKit/testmanager/db/tmdb-r18-testresultfiles-1-testresultmsgs-1.pgsql @@ -0,0 +1,171 @@ +-- $Id: tmdb-r18-testresultfiles-1-testresultmsgs-1.pgsql $ +--- @file +-- VBox Test Manager Database - Adds an idTestSet to TestResultFiles and TestResultMsgs. +-- + +-- +-- Copyright (C) 2013-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 +-- + +-- +-- Cleanup after failed runs. +-- +DROP TABLE IF EXISTS NewTestResultFiles; +DROP TABLE IF EXISTS OldTestResultFiles; +DROP TABLE IF EXISTS NewTestResultMsgs; +DROP TABLE IF EXISTS OldTestResultMsgs; + +-- Die on error from now on. +\set ON_ERROR_STOP 1 +\set AUTOCOMMIT 0 + + +-- +-- Rename the original table, drop constrains and foreign key references so we +-- get the right name automatic when creating the new one. +-- +\d+ TestResultFiles; +ALTER TABLE TestResultFiles RENAME TO OldTestResultFiles; + +DROP INDEX IF EXISTS TestResultFilesIdx; +DROP INDEX IF EXISTS TestResultFilesIdx2; + +-- +-- Create the new version of the table and filling with the content of the old. +-- +CREATE TABLE TestResultFiles ( + --- The ID of this file. + idTestResultFile INTEGER PRIMARY KEY DEFAULT NEXTVAL('TestResultFileId'), + --- The test result it was reported within. + idTestResult INTEGER NOT NULL, + --- The test set this file is a part of (for avoiding joining thru TestResults). + idTestSet INTEGER NOT NULL, + --- Creation time stamp. + tsCreated TIMESTAMP WITH TIME ZONE DEFAULT current_timestamp NOT NULL, + --- The filename relative to TestSets(sBaseFilename) + '-'. + -- The set of valid filename characters should be very limited so that no + -- file system issues can occure either on the TM side or the user when + -- loading the files. Tests trying to use other characters will fail. + -- Valid character regular expession: '^[a-zA-Z0-9_-(){}#@+,.=]*$' + idStrFile INTEGER NOT NULL, + --- The description. + idStrDescription INTEGER NOT NULL, + --- The kind of file. + -- For instance: 'log/release/vm', + -- 'screenshot/failure', + -- 'screencapture/failure', + -- 'xmllog/somestuff' + idStrKind INTEGER NOT NULL, + --- The mime type for the file. + -- For instance: 'text/plain', + -- 'image/png', + -- 'video/webm', + -- 'text/xml' + idStrMime INTEGER NOT NULL +); + +INSERT INTO TestResultFiles ( idTestResultFile, idTestResult, idTestSet, tsCreated, idStrFile, idStrDescription, + idStrKind, idStrMime) + SELECT o.idTestResultFile, o.idTestResult, tr.idTestSet, o.tsCreated, o.idStrFile, o.idStrDescription, + o.idStrKind, o.idStrMime + FROM OldTestResultFiles o, + TestResults tr + WHERE o.idTestResult = tr.idTestResult; + +-- Add new indexes. +CREATE INDEX TestResultFilesIdx ON TestResultFiles(idTestResult); +CREATE INDEX TestResultFilesIdx2 ON TestResultFiles(idTestSet, tsCreated DESC); + +-- Restore foreign keys. +ALTER TABLE TestResultFiles ADD CONSTRAINT TestResultFiles_idTestResult_fkey FOREIGN KEY(idTestResult) REFERENCES TestResults(idTestResult); +ALTER TABLE TestResultFiles ADD CONSTRAINT TestResultFiles_idTestSet_fkey FOREIGN KEY(idTestSet) REFERENCES TestSets(idTestSet); +ALTER TABLE TestResultFiles ADD CONSTRAINT TestResultFiles_idStrFile_fkey FOREIGN KEY(idStrFile) REFERENCES TestResultStrTab(idStr); +ALTER TABLE TestResultFiles ADD CONSTRAINT TestResultFiles_idStrDescription_fkey FOREIGN KEY(idStrDescription) REFERENCES TestResultStrTab(idStr); +ALTER TABLE TestResultFiles ADD CONSTRAINT TestResultFiles_idStrKind_fkey FOREIGN KEY(idStrKind) REFERENCES TestResultStrTab(idStr); +ALTER TABLE TestResultFiles ADD CONSTRAINT TestResultFiles_idStrMime_fkey FOREIGN KEY(idStrMime) REFERENCES TestResultStrTab(idStr); + +\d TestResultFiles; + + +-- +-- Rename the original table, drop constrains and foreign key references so we +-- get the right name automatic when creating the new one. +-- +\d+ TestResultMsgs; +ALTER TABLE TestResultMsgs RENAME TO OldTestResultMsgs; + +DROP INDEX IF EXISTS TestResultMsgsIdx; +DROP INDEX IF EXISTS TestResultMsgsIdx2; + +-- +-- Create the new version of the table and filling with the content of the old. +-- +CREATE TABLE TestResultMsgs ( + --- The ID of this file. + idTestResultMsg INTEGER PRIMARY KEY DEFAULT NEXTVAL('TestResultMsgIdSeq'), + --- The test result it was reported within. + idTestResult INTEGER NOT NULL, + --- The test set this file is a part of (for avoiding joining thru TestResults). + idTestSet INTEGER NOT NULL, + --- Creation time stamp. + tsCreated TIMESTAMP WITH TIME ZONE DEFAULT current_timestamp NOT NULL, + --- The message string. + idStrMsg INTEGER NOT NULL, + --- The message level. + enmLevel TestResultMsgLevel_T NOT NULL +); + +INSERT INTO TestResultMsgs ( idTestResultMsg, idTestResult, idTestSet, tsCreated, idStrMsg, enmLevel) + SELECT o.idTestResultMsg, o.idTestResult, tr.idTestSet, o.tsCreated, o.idStrMsg, o.enmLevel + FROM OldTestResultMsgs o, + TestResults tr + WHERE o.idTestResult = tr.idTestResult; + +-- Add new indexes. +CREATE INDEX TestResultMsgsIdx ON TestResultMsgs(idTestResult); +CREATE INDEX TestResultMsgsIdx2 ON TestResultMsgs(idTestSet, tsCreated DESC); + +-- Restore foreign keys. +ALTER TABLE TestResultMsgs ADD CONSTRAINT TestResultMsgs_idTestResult_fkey FOREIGN KEY(idTestResult) REFERENCES TestResults(idTestResult); +ALTER TABLE TestResultMsgs ADD CONSTRAINT TestResultMsgs_idTestSet_fkey FOREIGN KEY(idTestSet) REFERENCES TestSets(idTestSet); +ALTER TABLE TestResultMsgs ADD CONSTRAINT TestResultMsgs_idStrMsg_fkey FOREIGN KEY(idStrMsg) REFERENCES TestResultStrTab(idStr); + + +\d TestResultMsgs; + + +-- +-- Drop the old tables and commit. +-- +DROP TABLE OldTestResultFiles; +DROP TABLE OldTestResultMsgs; + +COMMIT; + diff --git a/src/VBox/ValidationKit/testmanager/db/tmdb-r19-testboxes-3.pgsql b/src/VBox/ValidationKit/testmanager/db/tmdb-r19-testboxes-3.pgsql new file mode 100644 index 00000000..26228a9d --- /dev/null +++ b/src/VBox/ValidationKit/testmanager/db/tmdb-r19-testboxes-3.pgsql @@ -0,0 +1,356 @@ +-- $Id: tmdb-r19-testboxes-3.pgsql $ +--- @file +-- VBox Test Manager Database - Adds sComment and fRawMode to TestBoxes and +-- moves the strings to separate table. +-- + +-- +-- Copyright (C) 2013-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 +-- + +-- +-- Cleanup after failed runs. +-- +DROP TABLE IF EXISTS OldTestBoxes; + +-- Die on error from now on. +\set ON_ERROR_STOP 1 +\set AUTOCOMMIT 0 + +-- Sanity check that we haven't already run this script. +SELECT 'done conversion already?', COUNT(sReport) FROM TestBoxes WHERE tsExpire = 'infinity'::TIMESTAMP; + +-- Total grid lock. +LOCK TABLE TestBoxStatuses IN ACCESS EXCLUSIVE MODE; +LOCK TABLE TestSets IN ACCESS EXCLUSIVE MODE; +LOCK TABLE TestBoxes IN ACCESS EXCLUSIVE MODE; +LOCK TABLE SchedGroupMembers IN ACCESS EXCLUSIVE MODE; + +\d+ TestBoxes; + +-- +-- Rename the table, drop foreign keys refering to it, and drop constrains +-- within the table itself. The latter is mostly for naming and we do it +-- up front in case the database we're running against has different names +-- due to previous conversions. +-- +ALTER TABLE TestBoxes RENAME TO OldTestBoxes; + +ALTER TABLE OldTestBoxes DROP CONSTRAINT testboxes_ccpus_check; +ALTER TABLE OldTestBoxes DROP CONSTRAINT testboxes_check; +ALTER TABLE OldTestBoxes DROP CONSTRAINT testboxes_cmbmemory_check; +ALTER TABLE OldTestBoxes DROP CONSTRAINT testboxes_cmbscratch_check; +ALTER TABLE OldTestBoxes DROP CONSTRAINT testboxes_pctscaletimeout_check; + +ALTER TABLE TestBoxStatuses DROP CONSTRAINT TestBoxStatuses_idGenTestBox_fkey; +ALTER TABLE TestSets DROP CONSTRAINT TestSets_idGenTestBox_fkey; + +ALTER TABLE OldTestBoxes DROP CONSTRAINT testboxes_pkey; +ALTER TABLE OldTestBoxes DROP CONSTRAINT testboxes_idgentestbox_key; + +DROP INDEX IF EXISTS TestBoxesUuidIdx; +DROP INDEX IF EXISTS TestBoxesExpireEffectiveIdx; + +-- This output should be free of index, constraints and references from other tables. +\d+ OldTestBoxes; + +-- +-- Create the two new tables before starting data migration (don't want to spend time +-- on converting strings just to find a typo in the TestBoxes create table syntax). +-- +CREATE SEQUENCE TestBoxStrTabIdSeq + START 1 + INCREMENT BY 1 + NO MAXVALUE + NO MINVALUE + CACHE 1; +CREATE TABLE TestBoxStrTab ( + --- The ID of this string. + idStr INTEGER PRIMARY KEY DEFAULT NEXTVAL('TestBoxStrTabIdSeq'), + --- The string value. + sValue text NOT NULL, + --- Creation time stamp. + tsCreated TIMESTAMP WITH TIME ZONE DEFAULT current_timestamp NOT NULL +); + +CREATE TABLE TestBoxes ( + --- The fixed testbox ID. + -- This is assigned when the testbox is created and will never change. + idTestBox INTEGER DEFAULT NEXTVAL('TestBoxIdSeq') NOT NULL, + --- When this row starts taking effect (inclusive). + tsEffective TIMESTAMP WITH TIME ZONE DEFAULT current_timestamp NOT NULL, + --- When this row stops being tsEffective (exclusive). + tsExpire TIMESTAMP WITH TIME ZONE DEFAULT TIMESTAMP WITH TIME ZONE 'infinity' NOT NULL, + --- The user id of the one who created/modified this entry. + -- When modified automatically by the testbox, NULL is used. + -- Non-unique foreign key: Users(uid) + uidAuthor INTEGER DEFAULT NULL, + --- Generation ID for this row. + -- This is primarily for referencing by TestSets. + idGenTestBox INTEGER UNIQUE DEFAULT NEXTVAL('TestBoxGenIdSeq') NOT NULL, + + --- The testbox IP. + -- This is from the webserver point of view and automatically updated on + -- SIGNON. The test setup doesn't permit for IP addresses to change while + -- the testbox is operational, because this will break gang tests. + ip inet NOT NULL, + --- The system or firmware UUID. + -- This uniquely identifies the testbox when talking to the server. After + -- SIGNON though, the testbox will also provide idTestBox and ip to + -- establish its identity beyond doubt. + uuidSystem uuid NOT NULL, + --- The testbox name. + -- Usually similar to the DNS name. + sName text NOT NULL, + --- Optional testbox description. + -- Intended for describing the box as well as making other relevant notes. + idStrDescription INTEGER REFERENCES TestBoxStrTab(idStr) DEFAULT NULL, + + --- Reference to the scheduling group that this testbox is a member of. + -- Non-unique foreign key: SchedGroups(idSchedGroup) + -- A testbox is always part of a group, the default one nothing else. + idSchedGroup INTEGER DEFAULT 1 NOT NULL, + + --- Indicates whether this testbox is enabled. + -- A testbox gets disabled when we're doing maintenance, debugging a issue + -- that happens only on that testbox, or some similar stuff. This is an + -- alternative to deleting the testbox. + fEnabled BOOLEAN DEFAULT NULL, + + --- The kind of lights-out-management. + enmLomKind LomKind_T DEFAULT 'none'::LomKind_T NOT NULL, + --- The IP adress of the lights-out-management. + -- This can be NULL if enmLomKind is 'none', otherwise it must contain a valid address. + ipLom inet DEFAULT NULL, + + --- Timeout scale factor, given as a percent. + -- This is a crude adjustment of the test case timeout for slower hardware. + pctScaleTimeout smallint DEFAULT 100 NOT NULL CHECK (pctScaleTimeout > 10 AND pctScaleTimeout < 20000), + + --- Change comment or similar. + idStrComment INTEGER REFERENCES TestBoxStrTab(idStr) DEFAULT NULL, + + --- @name Scheduling properties (reported by testbox script). + -- @{ + --- Same abbrieviations as kBuild, see KBUILD_OSES. + idStrOs INTEGER REFERENCES TestBoxStrTab(idStr) DEFAULT NULL, + --- Informational, no fixed format. + idStrOsVersion INTEGER REFERENCES TestBoxStrTab(idStr) DEFAULT NULL, + --- Same as CPUID reports (GenuineIntel, AuthenticAMD, CentaurHauls, ...). + idStrCpuVendor INTEGER REFERENCES TestBoxStrTab(idStr) DEFAULT NULL, + --- Same as kBuild - x86, amd64, ... See KBUILD_ARCHES. + idStrCpuArch INTEGER REFERENCES TestBoxStrTab(idStr) DEFAULT NULL, + --- The CPU name if available. + idStrCpuName INTEGER REFERENCES TestBoxStrTab(idStr) DEFAULT NULL, + --- Number identifying the CPU family/model/stepping/whatever. + -- For x86 and AMD64 type CPUs, this will on the following format: + -- (EffFamily << 24) | (EffModel << 8) | Stepping. + lCpuRevision bigint DEFAULT NULL, + --- Number of CPUs, CPU cores and CPU threads. + cCpus smallint DEFAULT NULL CHECK (cCpus IS NULL OR cCpus > 0), + --- Set if capable of hardware virtualization. + fCpuHwVirt boolean DEFAULT NULL, + --- Set if capable of nested paging. + fCpuNestedPaging boolean DEFAULT NULL, + --- Set if CPU capable of 64-bit (VBox) guests. + fCpu64BitGuest boolean DEFAULT NULL, + --- Set if chipset with usable IOMMU (VT-d / AMD-Vi). + fChipsetIoMmu boolean DEFAULT NULL, + --- Set if the test box does raw-mode tests. + fRawMode boolean DEFAULT NULL, + --- The (approximate) memory size in megabytes (rounded down to nearest 4 MB). + cMbMemory bigint DEFAULT NULL CHECK (cMbMemory IS NULL OR cMbMemory > 0), + --- The amount of scratch space in megabytes (rounded down to nearest 64 MB). + cMbScratch bigint DEFAULT NULL CHECK (cMbScratch IS NULL OR cMbScratch >= 0), + --- Free form hardware and software report field. + idStrReport INTEGER REFERENCES TestBoxStrTab(idStr) DEFAULT NULL, + --- @} + + --- The testbox script revision number, serves the purpose of a version number. + -- Probably good to have when scheduling upgrades as well for status purposes. + iTestBoxScriptRev INTEGER DEFAULT 0 NOT NULL, + --- The python sys.hexversion (layed out as of 2.7). + -- Good to know which python versions we need to support. + iPythonHexVersion INTEGER DEFAULT NULL, + + --- Pending command. + -- @note We put it here instead of in TestBoxStatuses to get history. + enmPendingCmd TestBoxCmd_T DEFAULT 'none'::TestBoxCmd_T NOT NULL, + + PRIMARY KEY (idTestBox, tsExpire), + + --- Nested paging requires hardware virtualization. + CHECK (fCpuNestedPaging IS NULL OR (fCpuNestedPaging <> TRUE OR fCpuHwVirt = TRUE)) +); + +-- Convenience view that simplifies querying a lot. +CREATE VIEW TestBoxesWithStrings AS + SELECT TestBoxes.*, + Str1.sValue AS sDescription, + Str2.sValue AS sComment, + Str3.sValue AS sOs, + Str4.sValue AS sOsVersion, + Str5.sValue AS sCpuVendor, + Str6.sValue AS sCpuArch, + Str7.sValue AS sCpuName, + Str8.sValue AS sReport + FROM TestBoxes + LEFT OUTER JOIN TestBoxStrTab Str1 ON idStrDescription = Str1.idStr + LEFT OUTER JOIN TestBoxStrTab Str2 ON idStrComment = Str2.idStr + LEFT OUTER JOIN TestBoxStrTab Str3 ON idStrOs = Str3.idStr + LEFT OUTER JOIN TestBoxStrTab Str4 ON idStrOsVersion = Str4.idStr + LEFT OUTER JOIN TestBoxStrTab Str5 ON idStrCpuVendor = Str5.idStr + LEFT OUTER JOIN TestBoxStrTab Str6 ON idStrCpuArch = Str6.idStr + LEFT OUTER JOIN TestBoxStrTab Str7 ON idStrCpuName = Str7.idStr + LEFT OUTER JOIN TestBoxStrTab Str8 ON idStrReport = Str8.idStr; + + +-- +-- Populate the string table. +-- + +--- Empty string with ID 0. +INSERT INTO TestBoxStrTab (idStr, sValue) VALUES (0, ''); + +INSERT INTO TestBoxStrTab (sValue) +( SELECT DISTINCT sDescription FROM OldTestBoxes WHERE sDescription IS NOT NULL +) UNION ( SELECT DISTINCT sOs FROM OldTestBoxes WHERE sOs IS NOT NULL +) UNION ( SELECT DISTINCT sOsVersion FROM OldTestBoxes WHERE sOsVersion IS NOT NULL +) UNION ( SELECT DISTINCT sCpuVendor FROM OldTestBoxes WHERE sCpuVendor IS NOT NULL +) UNION ( SELECT DISTINCT sCpuArch FROM OldTestBoxes WHERE sCpuArch IS NOT NULL +) UNION ( SELECT DISTINCT sCpuName FROM OldTestBoxes WHERE sCpuName IS NOT NULL +) UNION ( SELECT DISTINCT sReport FROM OldTestBoxes WHERE sReport IS NOT NULL ); + +-- Index and analyze the string table as we'll be using it a lot below already. +CREATE INDEX TestBoxStrTabNameIdx ON TestBoxStrTab USING hash (sValue); +ANALYZE VERBOSE TestBoxStrTab; + +SELECT MAX(idStr) FROM TestBoxStrTab; +SELECT pg_total_relation_size('TestBoxStrTab'); + + +-- +-- Populate the test box table. +-- + +INSERT INTO TestBoxes ( + idTestBox, -- 0 + tsEffective, -- 1 + tsExpire, -- 2 + uidAuthor, -- 3 + idGenTestBox, -- 4 + ip, -- 5 + uuidSystem, -- 6 + sName, -- 7 + idStrDescription, -- 8 + idSchedGroup, -- 9 + fEnabled, -- 10 + enmLomKind, -- 11 + ipLom, -- 12 + pctScaleTimeout, -- 13 + idStrComment, -- 14 + idStrOs, -- 15 + idStrOsVersion, -- 16 + idStrCpuVendor, -- 17 + idStrCpuArch, -- 18 + idStrCpuName, -- 19 + lCpuRevision, -- 20 + cCpus, -- 21 + fCpuHwVirt, -- 22 + fCpuNestedPaging, -- 23 + fCpu64BitGuest, -- 24 + fChipsetIoMmu, -- 25 + fRawMode, -- 26 + cMbMemory, -- 27 + cMbScratch, -- 28 + idStrReport, -- 29 + iTestBoxScriptRev, -- 30 + iPythonHexVersion, -- 31 + enmPendingCmd -- 32 + ) +SELECT idTestBox, + tsEffective, + tsExpire, + uidAuthor, + idGenTestBox, + ip, + uuidSystem, + sName, + st1.idStr, + idSchedGroup, + fEnabled, + enmLomKind, + ipLom, + pctScaleTimeout, + NULL, + st2.idStr, + st3.idStr, + st4.idStr, + st5.idStr, + st6.idStr, + lCpuRevision, + cCpus, + fCpuHwVirt, + fCpuNestedPaging, + fCpu64BitGuest, + fChipsetIoMmu, + NULL, + cMbMemory, + cMbScratch, + st7.idStr, + iTestBoxScriptRev, + iPythonHexVersion, + enmPendingCmd +FROM OldTestBoxes + LEFT OUTER JOIN TestBoxStrTab st1 ON sDescription = st1.sValue + LEFT OUTER JOIN TestBoxStrTab st2 ON sOs = st2.sValue + LEFT OUTER JOIN TestBoxStrTab st3 ON sOsVersion = st3.sValue + LEFT OUTER JOIN TestBoxStrTab st4 ON sCpuVendor = st4.sValue + LEFT OUTER JOIN TestBoxStrTab st5 ON sCpuArch = st5.sValue + LEFT OUTER JOIN TestBoxStrTab st6 ON sCpuName = st6.sValue + LEFT OUTER JOIN TestBoxStrTab st7 ON sReport = st7.sValue; + +-- Restore indexes. +CREATE UNIQUE INDEX TestBoxesUuidIdx ON TestBoxes (uuidSystem, tsExpire DESC); +CREATE INDEX TestBoxesExpireEffectiveIdx ON TestBoxes (tsExpire DESC, tsEffective ASC); + +-- Restore foreign key references to the table. +ALTER TABLE TestBoxStatuses ADD CONSTRAINT TestBoxStatuses_idGenTestBox_fkey + FOREIGN KEY (idGenTestBox) REFERENCES TestBoxes(idGenTestBox); +ALTER TABLE TestSets ADD CONSTRAINT TestSets_idGenTestBox_fkey + FOREIGN KEY (idGenTestBox) REFERENCES TestBoxes(idGenTestBox); + +-- Drop the old table. +DROP TABLE OldTestBoxes; + +COMMIT; + +\d TestBoxes; + diff --git a/src/VBox/ValidationKit/testmanager/db/tmdb-r20-testcases-1-testgroups-1-schedgroups-1.pgsql b/src/VBox/ValidationKit/testmanager/db/tmdb-r20-testcases-1-testgroups-1-schedgroups-1.pgsql new file mode 100644 index 00000000..ac40ebb8 --- /dev/null +++ b/src/VBox/ValidationKit/testmanager/db/tmdb-r20-testcases-1-testgroups-1-schedgroups-1.pgsql @@ -0,0 +1,67 @@ +-- $Id: tmdb-r20-testcases-1-testgroups-1-schedgroups-1.pgsql $ +--- @file +-- VBox Test Manager Database - Adds sComment to TestCases, TestGroups +-- and SchedGroups. +-- + +-- +-- Copyright (C) 2013-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 +-- + + + +\set ON_ERROR_STOP 1 +\set AUTOCOMMIT 0 + +LOCK TABLE TestBoxes IN ACCESS EXCLUSIVE MODE; +LOCK TABLE TestBoxStatuses IN ACCESS EXCLUSIVE MODE; +LOCK TABLE TestCases IN ACCESS EXCLUSIVE MODE; +LOCK TABLE TestGroups IN ACCESS EXCLUSIVE MODE; +LOCK TABLE SchedGroups IN ACCESS EXCLUSIVE MODE; + +-- +-- All the changes are rather simple and we'll just add the sComment column last. +-- +\d TestCases; +\d TestGroups; +\d SchedGroups; + +ALTER TABLE TestCases ADD COLUMN sComment TEXT DEFAULT NULL; +ALTER TABLE TestGroups ADD COLUMN sComment TEXT DEFAULT NULL; +ALTER TABLE SchedGroups ADD COLUMN sComment TEXT DEFAULT NULL; + +\d TestCases; +\d TestGroups; +\d SchedGroups; + +\prompt "Update python files while everything is locked. Hurry!" dummy + +COMMIT; + diff --git a/src/VBox/ValidationKit/testmanager/db/tmdb-r21-testsets-4.pgsql b/src/VBox/ValidationKit/testmanager/db/tmdb-r21-testsets-4.pgsql new file mode 100644 index 00000000..13a57854 --- /dev/null +++ b/src/VBox/ValidationKit/testmanager/db/tmdb-r21-testsets-4.pgsql @@ -0,0 +1,290 @@ +-- $Id: tmdb-r21-testsets-4.pgsql $ +--- @file +-- VBox Test Manager Database - Adds an idSchedGroup to TestSets in +-- preparation for testboxes belonging to multiple scheduling queues. +-- + +-- +-- Copyright (C) 2013-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 +-- + +-- +-- Cleanup after failed runs. +-- +DROP TABLE IF EXISTS OldTestSets; + +-- +-- Die on error from now on. +-- +\set ON_ERROR_STOP 1 +\set AUTOCOMMIT 0 + + +-- Total grid lock (don't want to deadlock below). +LOCK TABLE TestBoxStatuses IN ACCESS EXCLUSIVE MODE; +LOCK TABLE TestSets IN ACCESS EXCLUSIVE MODE; +LOCK TABLE TestBoxes IN ACCESS EXCLUSIVE MODE; +LOCK TABLE TestResults IN ACCESS EXCLUSIVE MODE; +LOCK TABLE TestResultFailures IN ACCESS EXCLUSIVE MODE; +LOCK TABLE TestResultFiles IN ACCESS EXCLUSIVE MODE; +LOCK TABLE TestResultMsgs IN ACCESS EXCLUSIVE MODE; +LOCK TABLE TestResultValues IN ACCESS EXCLUSIVE MODE; +LOCK TABLE SchedGroups IN ACCESS EXCLUSIVE MODE; +LOCK TABLE SchedQueues IN ACCESS EXCLUSIVE MODE; +LOCK TABLE SchedGroupMembers IN ACCESS EXCLUSIVE MODE; + +\d+ TestSets; + +-- +-- Rename the table, drop foreign keys refering to it, and drop constrains +-- within the table itself. The latter is mostly for naming and we do it +-- up front in case the database we're running against has different names +-- due to previous conversions. +-- +ALTER TABLE TestSets RENAME TO OldTestSets; + +ALTER TABLE TestResultFailures DROP CONSTRAINT IF EXISTS idtestsetfk; +ALTER TABLE TestResultFailures DROP CONSTRAINT IF EXISTS TestResultFailures_idTestSet_fkey; +ALTER TABLE SchedQueues DROP CONSTRAINT IF EXISTS SchedQueues_idTestSetGangLeader_fkey; +ALTER TABLE TestBoxStatuses DROP CONSTRAINT IF EXISTS TestBoxStatuses_idTestSet_fkey; +ALTER TABLE TestResultFiles DROP CONSTRAINT IF EXISTS TestResultFiles_idTestSet_fkey; +ALTER TABLE TestResultMsgs DROP CONSTRAINT IF EXISTS TestResultMsgs_idTestSet_fkey; +ALTER TABLE TestResults DROP CONSTRAINT IF EXISTS TestResults_idTestSet_fkey; +ALTER TABLE TestResultValues DROP CONSTRAINT IF EXISTS TestResultValues_idTestSet_fkey; +ALTER TABLE TestResultValues DROP CONSTRAINT IF EXISTS TestResultValues_idTestSet_fkey1; + +ALTER TABLE OldTestSets DROP CONSTRAINT testsets_igangmemberno_check; + +ALTER TABLE OldTestSets DROP CONSTRAINT TestSets_idBuildCategory_fkey; +ALTER TABLE OldTestSets DROP CONSTRAINT TestSets_idGenTestBox_fkey; +ALTER TABLE OldTestSets DROP CONSTRAINT TestSets_idGenTestCase_fkey; +ALTER TABLE OldTestSets DROP CONSTRAINT TestSets_idGenTestCaseArgs_fkey; +ALTER TABLE OldTestSets DROP CONSTRAINT TestSets_idTestResult_fkey; +ALTER TABLE OldTestSets DROP CONSTRAINT TestSets_idTestSetGangLeader_fkey; + +ALTER TABLE OldTestSets DROP CONSTRAINT IF EXISTS TestSets_sBaseFilename_key; +ALTER TABLE OldTestSets DROP CONSTRAINT IF EXISTS NewTestSets_sBaseFilename_key; +ALTER TABLE OldTestSets DROP CONSTRAINT TestSets_pkey; + +DROP INDEX IF EXISTS TestSetsGangIdx; +DROP INDEX IF EXISTS TestSetsBoxIdx; +DROP INDEX IF EXISTS TestSetsBuildIdx; +DROP INDEX IF EXISTS TestSetsTestCaseIdx; +DROP INDEX IF EXISTS TestSetsTestVarIdx; +DROP INDEX IF EXISTS TestSetsDoneCreatedBuildCatIdx; +DROP INDEX IF EXISTS TestSetsGraphBoxIdx; + + +-- This output should be free of indexes, constraints and references from other tables. +\d+ OldTestSets; + +\prompt "Is the above table completely free of indexes, constraints and references? Ctrl-C if not." dummy + +-- +-- Create the new table (no foreign keys). +-- +CREATE TABLE TestSets ( + --- The ID of this test set. + idTestSet INTEGER DEFAULT NEXTVAL('TestSetIdSeq') NOT NULL, + + --- The test config timestamp, used when reading test config. + tsConfig TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP NOT NULL, + --- When this test set was scheduled. + -- idGenTestBox is valid at this point. + tsCreated TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP NOT NULL, + --- When this test completed, i.e. testing stopped. This should only be set once. + tsDone TIMESTAMP WITH TIME ZONE DEFAULT NULL, + --- The current status. + enmStatus TestStatus_T DEFAULT 'running'::TestStatus_T NOT NULL, + + --- The build we're testing. + -- Non-unique foreign key: Builds(idBuild) + idBuild INTEGER NOT NULL, + --- The build category of idBuild when the test started. + -- This is for speeding up graph data collection, i.e. avoid idBuild + -- the WHERE part of the selection. + idBuildCategory INTEGER NOT NULL, + --- The test suite build we're using to do the testing. + -- This is NULL if the test suite zip wasn't referred or if a test suite + -- build source wasn't configured. + -- Non-unique foreign key: Builds(idBuild) + idBuildTestSuite INTEGER DEFAULT NULL, + + --- The exact testbox configuration. + idGenTestBox INTEGER NOT NULL, + --- The testbox ID for joining with (valid: tsStarted). + -- Non-unique foreign key: TestBoxes(idTestBox) + idTestBox INTEGER NOT NULL, + --- The scheduling group ID the test was scheduled thru (valid: tsStarted). + -- Non-unique foreign key: SchedGroups(idSchedGroup) + idSchedGroup INTEGER NOT NULL, + + --- The testgroup (valid: tsConfig). + -- Non-unique foreign key: TestBoxes(idTestGroup) + -- Note! This also gives the member ship entry, since a testcase can only + -- have one membership per test group. + idTestGroup INTEGER NOT NULL, + + --- The exact test case config we executed in this test run. + idGenTestCase INTEGER NOT NULL, + --- The test case ID for joining with (valid: tsConfig). + -- Non-unique foreign key: TestBoxes(idTestCase) + idTestCase INTEGER NOT NULL, + + --- The arguments (and requirements++) we executed this test case with. + idGenTestCaseArgs INTEGER NOT NULL, + --- The argument variation ID (valid: tsConfig). + -- Non-unique foreign key: TestCaseArgs(idTestCaseArgs) + idTestCaseArgs INTEGER NOT NULL, + + --- The root of the test result tree. + -- @note This will only be NULL early in the transaction setting up the testset. + -- @note If the test reports more than one top level test result, we'll + -- fail the whole test run and let the test developer fix it. + idTestResult INTEGER DEFAULT NULL, + + --- The base filename used for storing files related to this test set. + -- This is a path relative to wherever TM is dumping log files. In order + -- to not become a file system test case, we will try not to put too many + -- hundred thousand files in a directory. A simple first approach would + -- be to just use the current date (tsCreated) like this: + -- TM_FILE_DIR/year/month/day/TestSets.idTestSet + -- + -- The primary log file for the test is this name suffixed by '.log'. + -- + -- The files in the testresultfile table gets their full names like this: + -- TM_FILE_DIR/sBaseFilename-testresultfile.id-TestResultStrTab(testresultfile.idStrFilename) + -- + -- @remarks We store this explicitly in case we change the directly layout + -- at some later point. + sBaseFilename text NOT NULL, + + --- The gang member number number, 0 is the leader. + iGangMemberNo SMALLINT DEFAULT 0 NOT NULL, -- CHECK (iGangMemberNo >= 0 AND iGangMemberNo < 1024), + --- The test set of the gang leader, NULL if no gang involved. + -- @note This is set by the gang leader as well, so that we can find all + -- gang members by WHERE idTestSetGangLeader = :id. + idTestSetGangLeader INTEGER DEFAULT NULL + +); + +-- Convert the data. +INSERT INTO TestSets ( + idTestSet, + tsConfig, + tsCreated, + tsDone, + enmStatus, + idBuild, + idBuildCategory, + idBuildTestSuite, + idGenTestBox, + idTestBox, + idSchedGroup, + idTestGroup, + idGenTestCase, + idTestCase, + idGenTestCaseArgs, + idTestCaseArgs, + idTestResult, + sBaseFilename, + iGangMemberNo, + idTestSetGangLeader + ) +SELECT OldTestSets.idTestSet, + OldTestSets.tsConfig, + OldTestSets.tsCreated, + OldTestSets.tsDone, + OldTestSets.enmStatus, + OldTestSets.idBuild, + OldTestSets.idBuildCategory, + OldTestSets.idBuildTestSuite, + OldTestSets.idGenTestBox, + OldTestSets.idTestBox, + TestBoxes.idSchedGroup, + OldTestSets.idTestGroup, + OldTestSets.idGenTestCase, + OldTestSets.idTestCase, + OldTestSets.idGenTestCaseArgs, + OldTestSets.idTestCaseArgs, + OldTestSets.idTestResult, + OldTestSets.sBaseFilename, + OldTestSets.iGangMemberNo, + OldTestSets.idTestSetGangLeader +FROM OldTestSets + INNER JOIN TestBoxes + ON OldTestSets.idGenTestBox = TestBoxes.idGenTestBox; + +-- Restore the primary key and unique constraints. +ALTER TABLE TestSets ADD PRIMARY KEY (idTestSet); +ALTER TABLE TestSets ADD UNIQUE (sBaseFilename); + +-- Restore check constraints. +ALTER TABLE TestSets ADD CONSTRAINT TestSets_iGangMemberNo_Check CHECK (iGangMemberNo >= 0 AND iGangMemberNo < 1024); + +-- Restore foreign keys in the table. +ALTER TABLE TestSets ADD FOREIGN KEY (idBuildCategory) REFERENCES BuildCategories(idBuildCategory); +ALTER TABLE TestSets ADD FOREIGN KEY (idGenTestBox) REFERENCES TestBoxes(idGenTestBox); +ALTER TABLE TestSets ADD FOREIGN KEY (idGenTestCase) REFERENCES TestCases(idGenTestCase); +ALTER TABLE TestSets ADD FOREIGN KEY (idGenTestCaseArgs) REFERENCES TestCaseArgs(idGenTestCaseArgs); +ALTER TABLE TestSets ADD FOREIGN KEY (idTestResult) REFERENCES TestResults(idTestResult); +ALTER TABLE TestSets ADD FOREIGN KEY (idTestSetGangLeader) REFERENCES TestSets(idTestSet); + +-- Restore indexes. +CREATE INDEX TestSetsGangIdx ON TestSets (idTestSetGangLeader); +CREATE INDEX TestSetsBoxIdx ON TestSets (idTestBox, idTestResult); +CREATE INDEX TestSetsBuildIdx ON TestSets (idBuild, idTestResult); +CREATE INDEX TestSetsTestCaseIdx ON TestSets (idTestCase, idTestResult); +CREATE INDEX TestSetsTestVarIdx ON TestSets (idTestCaseArgs, idTestResult); +CREATE INDEX TestSetsDoneCreatedBuildCatIdx ON TestSets (tsDone DESC NULLS FIRST, tsCreated ASC, idBuildCategory); +CREATE INDEX TestSetsGraphBoxIdx ON TestSets (idTestBox, tsCreated DESC, tsDone ASC NULLS LAST, idBuildCategory, idTestCase); + +-- Restore foreign key references to the table. +ALTER TABLE TestResults ADD FOREIGN KEY (idTestSet) REFERENCES TestSets(idTestSet) MATCH FULL; +ALTER TABLE TestResultValues ADD FOREIGN KEY (idTestSet) REFERENCES TestSets(idTestSet) MATCH FULL; +ALTER TABLE TestResultFiles ADD FOREIGN KEY (idTestSet) REFERENCES TestSets(idTestSet) MATCH FULL; +ALTER TABLE TestResultMsgs ADD FOREIGN KEY (idTestSet) REFERENCES TestSets(idTestSet) MATCH FULL; +ALTER TABLE TestResultFailures ADD FOREIGN KEY (idTestSet) REFERENCES TestSets(idTestSet) MATCH FULL; + +ALTER TABLE TestBoxStatuses ADD FOREIGN KEY (idTestSet) REFERENCES TestSets(idTestSet) MATCH FULL; +ALTER TABLE SchedQueues ADD FOREIGN KEY (idTestSetGangLeader) REFERENCES TestSets(idTestSet) MATCH FULL; + +-- Drop the old table. +DROP TABLE OldTestSets; + +\prompt "Update python files while everything is locked. Hurry!" dummy + +-- Grant access to the new table. +GRANT ALL PRIVILEGES ON TABLE TestSets TO testmanager; + +COMMIT; + +\d TestSets; + diff --git a/src/VBox/ValidationKit/testmanager/db/tmdb-r22-testboxes-3-teststatus-4-testboxinschedgroups-1.pgsql b/src/VBox/ValidationKit/testmanager/db/tmdb-r22-testboxes-3-teststatus-4-testboxinschedgroups-1.pgsql new file mode 100644 index 00000000..8d7d7df0 --- /dev/null +++ b/src/VBox/ValidationKit/testmanager/db/tmdb-r22-testboxes-3-teststatus-4-testboxinschedgroups-1.pgsql @@ -0,0 +1,181 @@ +-- $Id: tmdb-r22-testboxes-3-teststatus-4-testboxinschedgroups-1.pgsql $ +--- @file +-- VBox Test Manager Database - Turns idSchedGroup column in TestBoxes +-- into an N:M relationship with a priority via the new table +-- TestBoxesInSchedGroups. Adds an internal scheduling table index to +-- TestBoxStatuses to implement testboxes switching between groups. +-- + +-- +-- Copyright (C) 2013-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 +-- + +-- +-- Cleanup after failed runs. +-- +DROP TABLE IF EXISTS OldTestBoxes; + +-- +-- Die on error from now on. +-- +\set ON_ERROR_STOP 1 +\set AUTOCOMMIT 0 + + +-- Total grid lock. +LOCK TABLE TestBoxStatuses IN ACCESS EXCLUSIVE MODE; +LOCK TABLE TestSets IN ACCESS EXCLUSIVE MODE; +LOCK TABLE TestBoxes IN ACCESS EXCLUSIVE MODE; +LOCK TABLE SchedGroups IN ACCESS EXCLUSIVE MODE; +LOCK TABLE SchedGroupMembers IN ACCESS EXCLUSIVE MODE; + +\d+ TestBoxes; + +-- +-- We'll only be doing simple alterations so, no need to drop constraints +-- and stuff like we usually do first. +-- + +-- +-- Create the new table and populate it. +-- + +CREATE TABLE TestBoxesInSchedGroups ( + --- TestBox ID. + -- Non-unique foreign key: TestBoxes(idTestBox). + idTestBox INTEGER NOT NULL, + --- Scheduling ID. + -- Non-unique foreign key: SchedGroups(idSchedGroup). + idSchedGroup INTEGER NOT NULL, + --- When this row starts taking effect (inclusive). + tsEffective TIMESTAMP WITH TIME ZONE DEFAULT current_timestamp NOT NULL, + --- When this row stops being tsEffective (exclusive). + tsExpire TIMESTAMP WITH TIME ZONE DEFAULT TIMESTAMP WITH TIME ZONE 'infinity' NOT NULL, + --- The user id of the one who created/modified this entry. + -- Non-unique foreign key: Users(uid) + uidAuthor INTEGER NOT NULL, + + --- The scheduling priority of the scheduling group for the test box. + -- Higher number causes the scheduling group to be serviced more frequently. + -- @sa TestGroupMembers.iSchedPriority, SchedGroups.iSchedPriority + iSchedPriority INTEGER DEFAULT 16 CHECK (iSchedPriority >= 0 AND iSchedPriority < 32) NOT NULL, + + PRIMARY KEY (idTestBox, idSchedGroup, tsExpire) +); + +GRANT ALL PRIVILEGES ON TABLE TestBoxesInSchedGroups TO testmanager; + +CREATE OR REPLACE FUNCTION TestBoxesInSchedGroups_ConvertedOneBox(a_idTestBox INTEGER) + RETURNS VOID AS $$ + DECLARE + v_Row RECORD; + v_idSchedGroup INTEGER; + v_uidAuthor INTEGER; + v_tsEffective TIMESTAMP WITH TIME ZONE; + v_tsExpire TIMESTAMP WITH TIME ZONE; + BEGIN + FOR v_Row IN + SELECT idTestBox, + idSchedGroup, + tsEffective, + tsExpire, + uidAuthor + FROM TestBoxes + WHERE idTestBox = a_idTestBox + ORDER BY tsEffective, tsExpire + LOOP + IF v_idSchedGroup IS NOT NULL THEN + IF (v_idSchedGroup != v_Row.idSchedGroup) OR (v_Row.tsEffective <> v_tsExpire) THEN + INSERT INTO TestBoxesInSchedGroups (idTestBox, idSchedGroup, tsEffective, tsExpire, uidAuthor) + VALUES (a_idTestBox, v_idSchedGroup, v_tsEffective, v_tsExpire, v_uidAuthor); + v_idSchedGroup := NULL; + END IF; + END IF; + + IF v_idSchedGroup IS NULL THEN + v_idSchedGroup := v_Row.idSchedGroup; + v_tsEffective := v_Row.tsEffective; + END IF; + IF v_Row.uidAuthor IS NOT NULL THEN + v_uidAuthor := v_Row.uidAuthor; + END IF; + v_tsExpire := v_Row.tsExpire; + END LOOP; + + IF v_idSchedGroup != -1 THEN + INSERT INTO TestBoxesInSchedGroups (idTestBox, idSchedGroup, tsEffective, tsExpire, uidAuthor) + VALUES (a_idTestBox, v_idSchedGroup, v_tsEffective, v_tsExpire, v_uidAuthor); + END IF; + END; +$$ LANGUAGE plpgsql; + +SELECT TestBoxesInSchedGroups_ConvertedOneBox(TestBoxIDs.idTestBox) +FROM ( SELECT DISTINCT idTestBox FROM TestBoxes ) AS TestBoxIDs; + +DROP FUNCTION TestBoxesInSchedGroups_ConvertedOneBox(INTEGER); + +-- +-- Do the other two modifications. +-- +ALTER TABLE TestBoxStatuses ADD COLUMN iWorkItem INTEGER DEFAULT 0 NOT NULL; + +DROP VIEW TestBoxesWithStrings; +ALTER TABLE TestBoxes DROP COLUMN idSchedGroup; +CREATE VIEW TestBoxesWithStrings AS + SELECT TestBoxes.*, + Str1.sValue AS sDescription, + Str2.sValue AS sComment, + Str3.sValue AS sOs, + Str4.sValue AS sOsVersion, + Str5.sValue AS sCpuVendor, + Str6.sValue AS sCpuArch, + Str7.sValue AS sCpuName, + Str8.sValue AS sReport + FROM TestBoxes + LEFT OUTER JOIN TestBoxStrTab Str1 ON idStrDescription = Str1.idStr + LEFT OUTER JOIN TestBoxStrTab Str2 ON idStrComment = Str2.idStr + LEFT OUTER JOIN TestBoxStrTab Str3 ON idStrOs = Str3.idStr + LEFT OUTER JOIN TestBoxStrTab Str4 ON idStrOsVersion = Str4.idStr + LEFT OUTER JOIN TestBoxStrTab Str5 ON idStrCpuVendor = Str5.idStr + LEFT OUTER JOIN TestBoxStrTab Str6 ON idStrCpuArch = Str6.idStr + LEFT OUTER JOIN TestBoxStrTab Str7 ON idStrCpuName = Str7.idStr + LEFT OUTER JOIN TestBoxStrTab Str8 ON idStrReport = Str8.idStr; + +GRANT ALL PRIVILEGES ON TABLE TestBoxesWithStrings TO testmanager; + +\prompt "Update python files while everything is locked. Hurry!" dummy + +COMMIT; + +\d TestBoxesInSchedGroups; +\d TestBoxStatuses; +\d TestBoxes; +ANALYZE VERBOSE TestBoxesInSchedGroups; + diff --git a/src/VBox/ValidationKit/testmanager/db/tmdb-r23-users-2.pgsql b/src/VBox/ValidationKit/testmanager/db/tmdb-r23-users-2.pgsql new file mode 100644 index 00000000..7a919da3 --- /dev/null +++ b/src/VBox/ValidationKit/testmanager/db/tmdb-r23-users-2.pgsql @@ -0,0 +1,60 @@ +-- $Id: tmdb-r23-users-2.pgsql $ +--- @file +-- VBox Test Manager Database - Adds fReadOnly column to Users. +-- + +-- +-- Copyright (C) 2013-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 +-- + +-- +-- Cleanup after failed runs. +-- +DROP TABLE IF EXISTS OldTestBoxes; + +-- +-- Die on error from now on. +-- +\set ON_ERROR_STOP 1 +\set AUTOCOMMIT 0 + + + +-- This change can be implemented using ALTER TABLE. Yeah! +\d+ Users; + +ALTER TABLE Users + ADD COLUMN fReadOnly BOOLEAN NOT NULL DEFAULT FALSE; + +COMMIT; + +\d Users; +ANALYZE VERBOSE Users; + diff --git a/src/VBox/ValidationKit/testmanager/db/tmdb-r24-vcsbugreferences-1.pgsql b/src/VBox/ValidationKit/testmanager/db/tmdb-r24-vcsbugreferences-1.pgsql new file mode 100644 index 00000000..0bb92ed4 --- /dev/null +++ b/src/VBox/ValidationKit/testmanager/db/tmdb-r24-vcsbugreferences-1.pgsql @@ -0,0 +1,59 @@ +-- $Id: tmdb-r24-vcsbugreferences-1.pgsql $ +--- @file +-- VBox Test Manager Database - Creates a new VcsBugReferences table. +-- + +-- +-- Copyright (C) 2020-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 +-- + +-- Die on error from now on. +\set ON_ERROR_STOP 1 +\set AUTOCOMMIT 0 + +-- +-- Create the new VcsBugReferences table. +-- +CREATE TABLE VcsBugReferences ( + --- The version control tree name. + sRepository TEXT NOT NULL, + --- The version control tree revision number. + iRevision INTEGER NOT NULL, + --- The bug tracker identifier - see g_kdBugTrackers in config.py. + sBugTracker CHAR(4) NOT NULL, + --- The bug number in the bug tracker. + lBugNo BIGINT NOT NULL, + + UNIQUE (sRepository, iRevision, sBugTracker, lBugNo) +); +CREATE INDEX VcsBugReferencesLookupIdx ON VcsBugReferences (sBugTracker, lBugNo); +COMMIT; +\d+ VcsBugReferences; + diff --git a/src/VBox/ValidationKit/testmanager/db/tmdb-r25-vcsrevisions-2.pgsql b/src/VBox/ValidationKit/testmanager/db/tmdb-r25-vcsrevisions-2.pgsql new file mode 100644 index 00000000..f0c4e2bd --- /dev/null +++ b/src/VBox/ValidationKit/testmanager/db/tmdb-r25-vcsrevisions-2.pgsql @@ -0,0 +1,45 @@ +-- $Id: tmdb-r25-vcsrevisions-2.pgsql $ +--- @file +-- VBox Test Manager Database - Creates a new index on VcsRevisions +-- + +-- +-- Copyright (C) 2013-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 +-- + +-- +-- Die on error from now on. +-- +\set ON_ERROR_STOP 1 +\set AUTOCOMMIT 1 + + +CREATE INDEX VcsRevisionsByDate ON VcsRevisions (tsCreated DESC); + diff --git a/src/VBox/ValidationKit/testmanager/debug/Makefile.kmk b/src/VBox/ValidationKit/testmanager/debug/Makefile.kmk new file mode 100644 index 00000000..74d882cc --- /dev/null +++ b/src/VBox/ValidationKit/testmanager/debug/Makefile.kmk @@ -0,0 +1,46 @@ +# $Id: Makefile.kmk $ +## @file +# VirtualBox Validation Kit. +# + +# +# Copyright (C) 2006-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 +# + +SUB_DEPTH = ../../../../.. +include $(KBUILD_PATH)/subheader.kmk + + +VBOX_VALIDATIONKIT_PYTHON_SOURCES += $(wildcard $(PATH_SUB_CURRENT)/*.py) + +$(evalcall def_vbox_validationkit_process_python_sources) +$(evalcall def_vbox_validationkit_process_js_sources) +include $(FILE_KBUILD_SUB_FOOTER) + diff --git a/src/VBox/ValidationKit/testmanager/debug/__init__.py b/src/VBox/ValidationKit/testmanager/debug/__init__.py new file mode 100644 index 00000000..1a97eacf --- /dev/null +++ b/src/VBox/ValidationKit/testmanager/debug/__init__.py @@ -0,0 +1,40 @@ +# -*- coding: utf-8 -*- +# $Id: __init__.py $ + +""" +Test Manager - Debug Utilities. +""" + +__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 $" + diff --git a/src/VBox/ValidationKit/testmanager/debug/add_testbox.pgsql b/src/VBox/ValidationKit/testmanager/debug/add_testbox.pgsql new file mode 100644 index 00000000..faf303c2 --- /dev/null +++ b/src/VBox/ValidationKit/testmanager/debug/add_testbox.pgsql @@ -0,0 +1,76 @@ +-- $Id: add_testbox.pgsql $ +--- @file +-- Test data. +-- + +-- +-- 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 +-- + + +\connect testmanager; + +INSERT INTO users (sUsername, sNickname, sFullName, sLoginName) + VALUES ('testmanager', 'testmanager', 'testmanager', 'testmanager'); + +INSERT INTO testboxes (uidAuthor, + ip, + uuidSystem, + sName, + fEnabled, + sOs, + sOsVersion, + sCpuVendor, + sCpuArch, + cCpus, + fCpuHwVirt, + fCpuNestedPaging, + fCpu64BitGuest, + fChipsetIoMmu, + cMbMemory, + cMbScratch) + + VALUES (1, + '127.0.0.1', + '9394c36a-cb2f-3c7f-b987-9f54a4519bbc', + 'localhost', + TRUE, + 'LINUX', + '1.0', + 'Intel', + 'AMD64', + 2, + TRUE, + TRUE, + TRUE, + TRUE, + 1024, + 1024); + diff --git a/src/VBox/ValidationKit/testmanager/debug/cgiprofiling.py b/src/VBox/ValidationKit/testmanager/debug/cgiprofiling.py new file mode 100755 index 00000000..be049e41 --- /dev/null +++ b/src/VBox/ValidationKit/testmanager/debug/cgiprofiling.py @@ -0,0 +1,83 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +# $Id: cgiprofiling.py $ + +""" +Debug - CGI Profiling. +""" + +__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 $" + + +def profileIt(fnMain, sAppendToElement = 'main', sSort = 'time'): + """ + Profiles a main() type function call (no parameters, returns int) and + outputs a hacky HTML section. + """ + + # + # Execute it. + # + import cProfile; + oProfiler = cProfile.Profile(); + rc = oProfiler.runcall(fnMain); + + # + # Output HTML to stdout (CGI assumption). + # + print('<div id="debug2"><br>\n' # Lazy BR-layouting!! + ' <h2>Profiler Output</h2>\n' + ' <pre>'); + try: + oProfiler.print_stats(sort = sSort); + except Exception as oXcpt: + print('<p><pre>%s</pre></p>\n' % (oXcpt,)); + else: + print('</pre>\n'); + oProfiler = None; + print('</div>\n'); + + # + # Trick to move the section in under the SQL trace. + # + print('<script lang="script/javascript">\n' + 'var oMain = document.getElementById(\'%s\');\n' + 'if (oMain) {\n' + ' oMain.appendChild(document.getElementById(\'debug2\'));\n' + '}\n' + '</script>\n' + % (sAppendToElement, ) ); + + return rc; + diff --git a/src/VBox/ValidationKit/testmanager/debug/functions.pgsql b/src/VBox/ValidationKit/testmanager/debug/functions.pgsql new file mode 100644 index 00000000..00854e91 --- /dev/null +++ b/src/VBox/ValidationKit/testmanager/debug/functions.pgsql @@ -0,0 +1,82 @@ +-- $Id: functions.pgsql $ +--- @file +-- ????????????????????????? +-- + +-- +-- 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 +-- + +\connect testmanager; + +DROP FUNCTION authenticate_testbox(inet, uuid); +DROP FUNCTION testbox_status_set(integer, TestBoxState_T); + +-- Authenticate Test Box record by IP and UUID and set its state to IDLE +-- Args: IP, UUID +CREATE OR REPLACE FUNCTION authenticate_testbox(inet, uuid) RETURNS testboxes AS $$ + DECLARE + _ip ALIAS FOR $1; + _uuidSystem ALIAS FOR $2; + _box TestBoxes; + BEGIN + -- Find Test Box record + SELECT * + FROM testboxes + WHERE ip=_ip AND uuidSystem=_uuidSystem INTO _box; + IF FOUND THEN + -- Update Test Box status if exists + UPDATE TestBoxStatuses SET enmState='idle' WHERE idTestBox=_box.idTestBox; + IF NOT FOUND THEN + -- Otherwise, add new record to TestBoxStatuses table + INSERT + INTO TestBoxStatuses(idTestBox, idGenTestBox, enmState) + VALUES (_box.idTestBox, _box.idGenTestBox, 'idle'); + END IF; + END IF; + return _box; + END; +$$ LANGUAGE plpgsql; + +-- Set Test Box status and make sure if it has been set +-- Args: Test Box ID, new status +CREATE OR REPLACE FUNCTION testbox_status_set(integer, TestBoxState_T) RETURNS VOID AS $$ + DECLARE + _box ALIAS FOR $1; + _status ALIAS FOR $2; + BEGIN + -- Update Test Box status if exists + UPDATE TestBoxStatuses SET enmState=_status WHERE idTestBox=_box; + IF NOT FOUND THEN + RAISE EXCEPTION 'Test Box (#%) was not found in database', _box; + END IF; + END; +$$ LANGUAGE plpgsql; + diff --git a/src/VBox/ValidationKit/testmanager/htdocs/Makefile.kup b/src/VBox/ValidationKit/testmanager/htdocs/Makefile.kup new file mode 100644 index 00000000..e69de29b --- /dev/null +++ b/src/VBox/ValidationKit/testmanager/htdocs/Makefile.kup diff --git a/src/VBox/ValidationKit/testmanager/htdocs/css/common.css b/src/VBox/ValidationKit/testmanager/htdocs/css/common.css new file mode 100644 index 00000000..9ccf7a54 --- /dev/null +++ b/src/VBox/ValidationKit/testmanager/htdocs/css/common.css @@ -0,0 +1,1183 @@ +/* $Id: common.css $ */ +/** @file + * Test Manager - Common CSS. + */ + +/* + * 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 + */ + +@charset "UTF-8"; + +/* + * Basic HTML elements. + */ +* { + margin: 0; + padding: 0; +} + +html, body { + height: 100%; +} + +body { + background: #f9f9f9 repeat-y center; + font-family: Georgia, "Times New Roman", Times, serif; + font-family: Arial, Helvetica, sans-serif; + font-size: 0.8em; + color: #2f2f2f; +} + +p, ul, ol { + margin-top: 0; +} + +div { + margin: 0; + padding: 0; +} + +h1, h2, h3 { + margin: 0px 0 10px 0; + padding: 0; + font-weight: normal; + color: #2f2f2f; + line-height: 180%; +} +h1 { + font-size: 2.4em; +} +h2 { + font-size: 2.0em; +} +h3 { + font-size: 1.5em; +} + +dl { + margin-bottom: 10px; +} + + +/* + * Misc class stuff. + */ +.clear { + clear: both; +} + +.left { + float: left; +} + +.right { + float: right; +} + + + +/* + * The general layout. + * + * Note! Not quite sure if something like this will work well everywhere... + * Will get back to that when the logic and content is all there, not + * worth wasting more time on CSS now. + */ + +html, body { + height: 100%; +} + +#wrap { + position: relative; + width: 100%; + height: 100%; +} + +#head-wrap { + position: fixed; + top: 0; + left: 0; + height: 74px; /**< header + top-menu. */ + width: 100%; + background: #f9f9f9; +} + +#logo { + width: 42px; + height: 46px; + top: 0; + left: 0; + right: 0; + bottom: auto; + /* Center the image in both directions. */ + display: flex; + align-items: center; + justify-content: center; + justify-content: flex-end; +} + +#logo img { + height: 36px; + width: 36px; +} + +#header { + position: fixed; + width: 100%; /** @todo this is too wide, darn! */ + height: 46px; + left: 42px; + top: 0; + right: 0; + bottom: auto; + margin-top: 0px; + margin-left: 0px; + text-align: left; + /* Center the h1 child vertically: */ + display: flex; + align-items: center; +} + +#login { + position: absolute; + top: 0; + left: auto; + right: 2px; + bottom: auto; + height: auto; +} + +#top-menu { + position: fixed; + padding: 0px; + width: 99%; + height: auto; + max-height: 22px; + top: 46px; + left: 0px; + right: 0px; + bottom: auto; +} + +body.tm-wide-side-menu #side-menu-wrap { + width: 300px; +} +#side-menu-wrap { + position: fixed; + top: 0px; + left: 0; + right: auto; + bottom: auto; + + width: 164px; + height: 100vh; + min-height: 100vh; + max-height: 100vh; + + display: flex; +} + +#side-menu { + margin-top: 46px; + margin-top: 70px; + padding-top: 6px + height: auto; + max-height: 100%; + width: 95%; + width: calc(100% - 8px); /* CSS3 */ + + display: flex; + flex-direction: column; + justify-content: space-between; +} + +#side-menu-body { + display: block; + max-height: 100%; + overflow: auto; +} + +body.tm-wide-side-menu #main { + margin-left: 300px; +} +#main { + height: 100%; + margin-top: 74px; /**< header + top-menu + padding. */ + margin-left: 164px; + padding-left: 2px; + padding-right: 2px; + padding-top: 2px; + padding-bottom: 2px; +} + + +/* + * Header and logo specifics. + */ +#header h1 { + margin-left: 8px; + margin-top: 0px; + margin-right: 0px; + margin-bottom: 0px; + font-weight: bold; + font-size: 2.2em; + font-family: Times New, Times, serif; +} + +#login p { + line-height: 100%; +} + + +/* + * Navigation menus (common). + */ +#top-menu, #side-menu { + font-weight: bold; + font-size: 1em; + font-family: Arial, Helvetica, sans-serif; + background-color: #c0d0e0; + padding: 2px 2px 2px 2px; +} + +#top-menu.tm-top-menu-wo-side { + border-radius: 12px; +} +#top-menu { + border-radius: 12px 12px 12px 0px; +} + +#side-menu { + border-radius: 0px 0px 12px 12px; +} + +#head-wrap { + line-height: 180%; +} + +#top-menu ul li a, #side-menu ul li a { + text-decoration: none; + color: #000000; + font-weight: bold; + font-size: 1em; + font-family: Arial, Helvetica, sans-serif; +} + +#top-menu a:hover, #top-menu .current_page_item a, #side-menu a:hover, #side-menu .current_page_item a { + text-decoration: none; + color: #b23c1c; +} + + +/* + * Navigation in on the left side. + */ + + +/* Side menu: */ +#side-menu { + /* margin-top and padding-top are set up in layout !*/ + margin-right: 3px; + margin-left: 3px; + margin-bottom: 3px; +} + +#side-menu p { + margin-right: 3px; + margin-left: 3px; +} + +#side-menu ul { + list-style: none; + margin-left: 3px; + margin-right: 3px; +} + +#side-menu li { + padding-top: 0.3em; + padding-bottom: 0.3em; + line-height: 1.0em; + text-align: left; +} + +#side-menu .subheader_item { + font-style: italic; + font-size: 1.1em; + text-decoration: underline; +} + +.subheader_item:not(:first-child) { + margin-top: 0.5em; +} + +/* The following is for the element of / not element of checkbox, supplying text and hiding the actual box. */ +input.tm-side-filter-union-input { + display: none; +} +input.tm-side-filter-union-input + label { + vertical-align: middle; +} +input.tm-side-filter-union-input[type=checkbox]:checked + label::after { + content: '∉'; /* U+2209: not an element of. */ +} +input.tm-side-filter-union-input[type=checkbox] + label::after { + content: '∈'; /* U+2208: element of. */ +} + +/* Webkit: Pretty scroll bars on the menu body as well as inside filter criteria. */ +#side-menu ::-webkit-scrollbar { + width: 8px; +} +#side-menu ::-webkit-scrollbar-track { + -webkit-box-shadow: inset 0 0 3px rgba(0, 0, 0, 0.3); + -webkit-border-radius: 4px; + border-radius: 4px; +} +#side-menu ::-webkit-scrollbar-thumb { + -webkit-box-shadow: inset 0 0 3px rgba(0, 0, 0, 0.5); + -webkit-border-radius: 4px; + border-radius: 4px; + background: rgba(112, 128, 144, 0.9); +} +#side-menu ::-webkit-scrollbar-thumb:window-inactive { + background: rgba(112, 128, 144, 0.7); +} + +/* Filters: */ +.tm-side-filter-title-buttons { + float: right; +} +body.tm-wide-side-menu .tm-side-filter-title-buttons input { + display: none; +} +.tm-side-filter-title-buttons input { + display: inline; +} +.tm-side-filter-title-buttons input { + font-size: 0.6em; +} +.tm-side-filter-dt-buttons input { + font-size: 0.6em; +} +body.tm-wide-side-menu .tm-side-filter-dt-buttons input[type=submit] { + display: inline; +} +.tm-side-filter-dt-buttons input[type=submit] { + display: none; +} +.tm-side-filter-dt-buttons { + float: right; +} + +#side-filters p:first-child { + margin-top: 0.5em; + font-style: italic; + font-size: 1.1em; + text-decoration: underline; +} + +#side-filters dd.sf-collapsible { + display: block; +} + +#side-filters dd.sf-expandable { + display: none; +} + +#side-filters a { + text-decoration: none; + color: #000000; +} + +#side-filters dt { + margin-top: 0.4em; +} + +#side-filters dd { + font-size: 0.82em; + font-family: "Arial Narrow", Arial, sans-serif; + font-weight: normal; + clear: both; /* cancel .tm-side-filter-dt-buttons */ +} + +#side-filters li, #side-filters input[type=checkbox], #side-filters p { + line-height: 0.9em; + vertical-align: text-bottom; +} + +#side-filters input[type=checkbox] { + margin-right: 0.20em; + width: 1.0em; + height: 1.0em; +} +@supports(-moz-appearance:meterbar) { + #side-filters input[type=checkbox] { + /* not currently used */ + } +} +@media all and (-ms-high-contrast: none), (-ms-high-contrast: active) { /* IE 10+ specific tweaks */ + #side-filters input[type=checkbox] { + width: 1.1em; + height: 1.1em; + } +} + +#side-filters dd > ul { + max-height: 22em; + overflow: auto; +} + +#side-filters ul ul { + margin-left: 1.4em; +} + +#side-filters li { + padding-top: 1px; + padding-bottom: 1px; + overflow-wrap: break-word; +} + +ul.sf-checkbox-collapsible { + display: block; +} + +ul.sf-checkbox-expandable { + display: none; +} + +.side-filter-irrelevant { + font-style: italic; + font-weight: normal; +} +.side-filter-count { + font-size: smaller; + vertical-align: text-top; +} + +/* Footer: */ +#side-footer { + width: 100%; + margin-left: 2px; + margin-right: 2px; + margin-top: 1em; + padding-top: 1em; + padding-bottom: 0.8em; + border-top: thin white ridge; +} + +#side-footer p { + margin-left: 3px; + margin-right: 3px; + margin-bottom: 0.5em; + font-family: Times New, Times, serif; + font-size: 0.86em; + font-style: normal; + font-weight: normal; + line-height: 1.2em; + text-align: center; +} + + +/* + * Navigation in the header. + */ +#top-menu { + margin-right: 3px; /* same as #side-menu! */ + margin-left: 3px; +} + +#top-menu ul li a { + padding: .1em 1em; +} + +#top-menu ul li { + display: inline; +} + +#top-menu ul { + margin: 0; + padding: 0; + list-style: none; + list-style-type: none; + text-align: center; +} + +#top-menu a { + border: none; +} + +#top-menu .current_page_item a { +} + +/* + * Time navigation forms on a line with some padding between them. + */ +.tmtimenav form { + display: inline-block; +} + +.tmtimenav form + form { + padding-left: 0.6em; +} + +/* + * Items per page and next. + */ +.tmnextanditemsperpage form { + display: inline-block; + padding-left: 1em; +} + +/* + * Error message (typically a paragraph in the body). + */ +.tmerrormsg { + color: #ff0000; + white-space: pre; + font-family: Monospace, "Lucida Console", "Courier New", "Courier"; + display: block; + border: 1px solid; + margin: 1em; + padding: 0.6em; +} + + +/* + * Generic odd/even row and sub-row attribs. + */ +.tmeven { + background-color: #ececec; +} + +.tmodd { + background-color: #fcfcfc; +} + +/** @todo adjust the sub row colors (see change logs for examples). */ +.tmeveneven { + background-color: #d8e0f8; +} + +.tmevenodd { + background-color: #e8f0ff; +} + +.tmoddeven { + background-color: #d8e0f8; +} + +.tmoddodd { + background-color: #e8f0ff; +} + +/* + * Multi color row/item coloring, 0..7. + */ +.tmshade0 { background-color: #ececec; } +.tmshade1 { background-color: #fbfbfb; } +.tmshade2 { background-color: #e4e4e4; } +.tmshade3 { background-color: #f4f4f4; } +.tmshade4 { background-color: #e0e0e0; } +.tmshade5 { background-color: #f0f0f0; } +.tmshade6 { background-color: #dcdcdc; } +.tmshade7 { background-color: #fdfdfd; } + + +/* + * Generic thead class (first-child doesn't work for multiple header rows). + */ +.tmheader { + background-color: #d0d0d0; + color: black; +} + +/* + * Generic class for div elements wrapping pre inside a table. This prevents + * the <pre> from taking up way more screen space that available. + */ +.tdpre { + display: table; + table-layout: fixed; + width: 100%; +} +.tdpre pre { + overflow: auto; +} + + +/* + * A typical table. + */ +/* table.tmtable th { + background-color: #d0d0d0; + color: black; +} */ + +table.tmtable caption { + text-align: left; +} + +table.tmtable { + width: 100%; + border-spacing: 0px; +} + +table.tmtable th { + font-size: 1.3em; + text-align: center; +} + +table.tmtable, table.tmtable tr, table.tmtable td, table.tmtable th { + vertical-align: top; +} + +table.tmtable { + border-left: 1px solid black; + border-top: 1px solid black; + border-right: none; + border-bottom: none; +} + +table.tmtable td, table.tmtable th { + border-left: none; + border-top: none; + border-right: 1px solid black; + border-bottom: 1px solid black; +} + +table.tmtable td { + padding-left: 3px; + padding-right: 3px; + padding-top: 3px; + padding-bottom: 3px; +} + +table.tmtable th { + padding-left: 3px; + padding-right: 3px; + padding-top: 6px; + padding-bottom: 6px; +} + +.tmtable td { +} + +tr.tmseparator td { + border-bottom: 2px solid black; + font-size: 0; + padding-top: 0; + padding-bottom: 0; +} + + + +/* + * Table placed inside of a big table used to display *all* stuff of a category. + */ + +table.tminnertbl tr:nth-child(odd) { + background-color: #e8e8e8; +} +table.tminnertbl tr:nth-child(even) { + background-color: #f8f8f8; +} +table.tminnertbl tr:first-child { + background-color: #d0d0d0; + color: black; +} + +table.tminnertbl { + border-style: dashed; + border-spacing: 1px; + border-width: 1px; + border-color: gray; + border-collapse: separate; +} + +table.tminnertbl th, table.tminnertbl td { + font-size: 1em; + text-align: center; + border-style: none; + padding: 1px; + border-width: 1px; + border-color: #FFFFF0; +} + +/* + * Table placed inside a form. + */ +table.tmformtbl { + border-style: none; + border-spacing: 1px; + border-width: 1px; + border-collapse: separate; +} + +table.tmformtbl th, table.tmformtbl td { + font-size: 1em; + padding-left: 0.5em; + padding-right: 0.5em; + padding-bottom: 1px; + padding-top: 1px; + border-width: 1px; +} + +table.tmformtbl th, table.tmformtbl thead { + background-color: #d0d0d0; + font-size: 1em; + font-weight: bold; +} + +table.tmformtbl tr.tmodd { + background: #e2e2e2; +} + +table.tmformtblschedgroupmembers tr td:nth-child(3), +table.tmformtblschedgroupmembers tr td:nth-child(4) { + text-align: center; +} + + +/* + * Change log table (used with tmtable). + */ +table.tmchangelog > tbody { + font-size: 1em; +} + +table.tmchangelog tr.tmodd td:nth-child(1), +table.tmchangelog tr.tmeven td:nth-child(1), +table.tmchangelog tr.tmodd td:nth-child(2), +table.tmchangelog tr.tmeven td:nth-child(2) { + min-width: 5em; + max-width: 10em; /* futile */ +} + +table.tmchangelog tr.tmeven { + background-color: #e8f0ff; +} + +table.tmchangelog tr.tmodd { + background-color: #d8e0f8; +} + +table.tmchangelog tr.tmoddeven, table.tmchangelog tr.tmeveneven { + background-color: #fcfcfc; +} + +table.tmchangelog tr.tmoddodd, table.tmchangelog tr.tmevenodd { + background-color: #ececec; +} + +table.tmchangelog tr.tmoddeven, table.tmchangelog tr.tmeveneven, table.tmchangelog tr.tmoddodd, table.tmchangelog tr.tmevenodd { + font-size: 0.86em; +} + +.tmsyschlogattr { + font-size: 0.80em; +} + +.tmsyschlogspacer { + width: 0.8em; +} + +td.tmsyschlogspacer:not(:last-child) { + width: 1.8em; + border-bottom: 0px solid green !important; +} + +.tmsyschlogevent { + border-bottom: 0px solid green !important; +} + +.tmsyschlogspacerrowabove { + height: 0.22em; +} + +.tmsyschlogspacerrowbelow { + height: 0.80em; +} + + +/* + * Elements to be shows on *Show All* pages. + */ + +ul.tmshowall { + margin-left: 15px; + margin-right: 15px; +} + +li.tmshowall { + margin-left: 5px; + margin-right: 5px; +} + + +/* + * List navigation table + */ +table.tmlistnavtab { + width: 100%; +} + +table.tmlistnavtab tr td:nth-child(1) { + text-align: left; +} + +table.tmlistnavtab tr td:nth-child(2) { + text-align: right; +} + + +/* + * A typical form. + * + * Note! This _has_ to be redone. It sucks for the wide fields and such. + */ +.tmform ul { + list-style: none; + list-style-type: none; +} + +.tmform li { + line-height: 160%; +} + + +.tmform-field { + display: block; + clear: both; +} + +.tmform-field label { + float: left; + text-align: right; + width: 20%; + min-width: 10em; + max-width: 16em; + padding-right: 0.9em; +} + +.tmform-error-desc { + display: block; + color: #ff0000; + font-style: italic; +} + +.tmform-button { + float: left; + padding-top: 0.8em; +} + +.tmform-field input { +} + +.tmform-field-tiny-int input { + width: 2em; +} + +.tmform-field-int input { + width: 6em; +} + +.tmform-field-long input { + width: 9em; +} + +.tmform-field-submit input { +} + +.tmform-field-string input { + width: 24em; +} + +.tmform-field-subname input { + width: 10em; +} + +.tmform-field-timestamp input { + width: 20em; +} + +.tmform-field-uuid input { + width: 24em; +} + +.tmform-field-wide input { + width: 78%; + overflow: hidden; +} + +.tmform-field-wide100 input { + width: 100%; + overflow: hidden; +} + +.tmform-field-list { + padding-top: 2px; + padding-bottom: 2px; +} + +.tmform-checkboxes-container { + padding: 3px; + overflow: auto; + border: 1px dotted #cccccc; +} + +.tmform-checkbox-holder { + float: left; + min-width: 20em; +} + +#tmform-checkbox-list-os-arches .tmform-checkbox-holder { + min-width: 11em; +} + +#tmform-checkbox-list-build-types .tmform-checkbox-holder { + min-width: 6em; +} + +.tmform-input-readonly { + background: #ADD8EF; + color: #ffffff; +} + +/* (Test case argument variation.) */ + +table.tmform-innertbl { + border-style: none; + border-spacing: 1px; + border-width: 1px; + border-collapse: separate; + width: 78%; +} + +table.tmform-innertbl caption { + text-align: left; +} + +table.tmform-innertbl th, table.tmform-innertbl td { + font-size: 1em; + text-align: center; + border-style: none; + /* padding-top: 1px;*/ + /*padding-bottom: 1px;*/ + padding-left: 2px; + padding-right: 2px; + border-width: 1px; + border-color: #FFFFF0; + background-color: #f9f9f9; +} + +.tmform-inntertbl-td-wide input { + width: 100%; + overflow: hidden; +} + +.tmform-inntertbl-td-wide { + width: 100%; +} + + +/* + * The test case argument variation table. + */ +table.tmform-testcasevars { + border-style: none; + border-spacing: 0px; + border-width: 0px; + border-collapse: collapse; + width: 78%; +} + +table.tmform-testcasevars tbody { + border-style: solid; + border-spacing: 1px; + border-width: 1px; + margin: 2px; +} + +table.tmform-testcasevars td { + padding-right: 3px; + padding-left: 3px; +} + +table.tmform-testcasevars td:first-child, table.tmform-testcasevars td:nth-child(3) { + width: 8em; + text-align: right; +} +table.tmform-testcasevars td:nth-child(5) { + width: 4em; + text-align: left; +} + + +.tmform-testcasevars caption { + text-align: left; +} + +tr.tmform-testcasevars-first-row td { + padding-top: 0px; + padding-bottom: 0px; + background-color: #e3e3ec; +} + +.tmform-testcasevars-inner-row td { + padding-top: 0px; + padding-bottom: 0px; +} + +tr.tmform-testcasevars-final-row td { + padding-top: 0px; + padding-bottom: 1px; +} + +td.tmform-testcasevars-stupid-border-column { + /* Stupid hack. */ + min-width: 2px; + width: 0.1%; +} + + + +/* + * Log viewer. + */ +.tmlog a[href] { + background-color: #e0e0e0; + padding-left: 0.8em; + padding-right: 0.8em; +} + +.tmlog pre { + background-color: #000000; + color: #00ff00; + font-family: "Monospace", "Lucida Console", "Courier New", "Courier"; +} + + +/* + * Debug SQL traceback. + */ +#debug, #debug h1, #debug h2, #debug h3, +#debug2, #debug2 h1, #debug2 h2, #debug2 h3 { + color: #00009f; +} + +table.tmsqltable { + border-collapse: collapse; +} + +table.tmsqltable, table.tmsqltable tr, table.tmsqltable td, table.tmsqltable th { + border: 1px solid; + vertical-align: middle; + padding: 0.1ex 0.5ex; +} + +table.tmsqltable pre { + text-align: left; +} + +table.tmsqltable tr td { + text-align: left; +} + +table.tmsqltable tr td:nth-child(1), +table.tmsqltable tr td:nth-child(2), +table.tmsqltable tr td:nth-child(3), +table.tmsqltable tr td:nth-child(4) { + text-align: right; +} + + + +/* + * Various more or less common span classes. + */ +.tmspan-offline { + color: #f08020; + font-size: 0.75em; +} + +.tmspan-online { + font-size: 0.75em; +} + +.tmspan-name, .tmspan-osarch { + font-weight: bold; +} + +.tmspan-osver1 { + font-style: italic; +} + +.tmspan-osver2 { + font-style: normal; +} + + +/* + * Subversion tooltip. + */ +.tmvcstooltip { + padding: 0px; + min-width: 50em; + overflow: hidden; + border: 0px none; +} + +.tmvcstooltip iframe { + padding: 0px; + margin: 0px; + border: 0px none; + width: 100%; + //overflow: auto; + overflow: hidden; +} + +.tmvcstooltipnew { + padding: 0px; + min-width: 50em; + overflow: hidden; + border: 0px none; + background-color: #f9f9f9; +} + + +/* + * Workaround for flickering tooltips in the column bar graphs (see + * https://github.com/google/google-visualization-issues/issues/2162). + */ +.google-visualization-tooltip { + pointer-events: none; +} + diff --git a/src/VBox/ValidationKit/testmanager/htdocs/css/details.css b/src/VBox/ValidationKit/testmanager/htdocs/css/details.css new file mode 100644 index 00000000..1ae05671 --- /dev/null +++ b/src/VBox/ValidationKit/testmanager/htdocs/css/details.css @@ -0,0 +1,216 @@ +/* $Id: details.css $ */ +/** @file + * Test Manager - Test Details CSS. + */ + +/* + * 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 + */ + + + +/* + * The test details page has no side menu, so adjust the top-menu and main + * sections so they start at the left border. + */ + +#top-menu, #main { + left: 0; +} +#main { + margin-left: 0px; +} + +.tmtbl-events { + +} + +.tmstatusrow-failure, .tmstatusrow-timed-out, .tmstatusrow-rebooted { + color: #e80000; +} + +.tmstatusrow-skipped, .tmstatusrow-aborted, .tmstatusrow-bad-testbox { + color: #0000f0; +} + + +/* + * Test results. + */ + +/* + * Details table on the individual test result page. + */ +table.tmtbl-testresult-details { + border-style: dashed; + border-spacing: 1px; + border-width: 1px; + border-color: gray; + border-collapse: separate; +} + +table.tmtbl-testresult-details caption { + text-align: left; + font-weight: bold; + font-size: 1.2em; +} + +table.tmtbl-testresult-details td, table.tmtbl-testresult-details th { + font-size: 1em; + border-style: none; + padding-bottom: 3px; + padding-top: 3px; + padding-left: 2px; + padding-right: 2px; + border-width: 1px; +} + +table.tmtbl-testresult-details th { + text-align: left; +} + +.tmtbl-result-details-caption { + font-size: 1.2em; + font-weight: bold; + text-align: center; + background-color: #c0d0e0; +} + +.tmtbl-result-details-subcaption { + text-align: center; +} + + +/* + * Event log on the individual test result page. + */ +.tmtbl-events td { + padding-bottom: 1px; + padding-top: 1px; + padding-left: 1px; + padding-right: 1px; + vertical-align: top; +} + +.tmtbl-events th { + font-size: 1.3em; + text-align: center; +} + +table.tmtbl-events, table.tmtbl-events tr, table.tmtbl-events td, table.tmtbl-events th { + border-collapse: collapse; +} + +tr.tmtbl-events-leaf { +} + +tr.tmtbl-events-first { + border-top: 1px dotted; +} + +tr.tmtbl-events-value { +} + +tr.tmtbl-events-final { + border-bottom: 1px dotted; +} + + +tr.tmtbl-events-lvl0 td { + padding-top: 8px; + padding-bottom: 8px; +} + +tr.tmtbl-events-lvl1 td { + padding-top: 6px; + padding-bottom: 6px; +} + +tr.tmtbl-events-lvl2 td { + padding-top: 4px; + padding-bottom: 4px; +} + +tr.tmtbl-events-lvl3 td { + padding-top: 2px; + padding-bottom: 2px; +} + +tr.tmtbl-events-lvl4 td { + padding-top: 1px; + padding-bottom: 1px; +} + +tr.tmtbl-events-lvl5 td, +tr.tmtbl-events-lvl6 td, +tr.tmtbl-events-lvl7 td, +tr.tmtbl-events-lvl8 td, +tr.tmtbl-events-lvl9 td, +tr.tmtbl-events-lvl10 td { + padding-top: 0px; + padding-bottom: 0px; +} + +td.tmtbl-events-number { + text-align: right; +} + +td.tmtbl-events-number, td.tmtbl-events-unit { +} + +tr.tmtbl-events-value td:nth-child(3), +tr.tmtbl-events-file td:nth-child(3), +tr.tmtbl-events-message td:nth-child(3) { + padding-left: 2em; +} + +tr.tmtbl-events-value td:nth-child(3), +tr.tmtbl-events-message td:nth-child(3) { + font-style: italic; +} + + +/* + * Status coloring. (move to common.css?) + */ +.tmspan-status-success { + color: green; +} +.tmspan-status-skipped { + color: blue; +} +.tmspan-status-failure { + color: red; +} +.tmspan-status-success, .tmspan-status-skipped, .tmspan-status-failure { + font-weight: bold; + text-transform: uppercase; +} + diff --git a/src/VBox/ValidationKit/testmanager/htdocs/css/graphwiz.css b/src/VBox/ValidationKit/testmanager/htdocs/css/graphwiz.css new file mode 100644 index 00000000..2354bfc1 --- /dev/null +++ b/src/VBox/ValidationKit/testmanager/htdocs/css/graphwiz.css @@ -0,0 +1,237 @@ +/* $Id: graphwiz.css $ */ +/** @file + * Test Manager - Graph Wizard CSS. + */ + +/* + * 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 + */ + + + +/* + * The graph wizard page currently has no side menu, so adjust the top-menu + * and main sections so they start at the left border. + */ + +#main { + margin-left: 0; +} + +.tmtbl-events { + +} + +/* + * Let the top navigation and end selection inputs look alike. + */ +#graphwiz-nav, #graphwiz-end-selection { + background-color: #c0cbd6; + padding-left: 3px; + padding-right: 3px; + padding-top: 3px; + padding-bottom: 3px; + margin-left: 1px; + margin-right: 1px; + margin-top: 3px; + margin-bottom: 3px; + width: 100%; +} + + +/* + * Navigation and it's inputs. + */ + +#graphwiz-nav { + min-height: 4.2em; +} + +#graphwiz-top-1, #graphwiz-top-2 { + clear: both; +} + +#graphwiz-time, #graphwiz-top-options-1, #graphwiz-top-submit, #graphwiz-top-options-2 { + display: block; +} + +#graphwiz-time, #graphwiz-top-submit { + margin-left: 1em; + margin-right: 2em; + float: left; +} + +#graphwiz-top-options-1, #graphwiz-top-options-2 { + margin-left: 2em; + margin-right: 1em; + float: right; +} + +.graphwiz-pixel-input, .graphwiz-dpi-input, .graphwiz-time-input, .graphwiz-period-input { + margin-top: 0.2em; + margin-bottom: 0.2em; +} + +.graphwiz-pixel-input { + width: 3em; + text-align: right +} + +.graphwiz-dpi-input { + width: 2em; + text-align: right +} + +.graphwiz-time-input { + width: 18em; + text-align: left +} + +.graphwiz-period-input { + width: 4em; + text-align: right +} + +.graphwiz-maxerrorbar-input { + width: 2em; + text-align: right; +} + +.graphwiz-fontsize-input { + width: 2em; + text-align: right; +} + +.graphwiz-maxpergraph-input { + width: 2em; + text-align: right; +} + +/* + * The graphs. + */ +#graphwiz-graphs { + margin-top: 0.5em; +} + +.graphwiz-collection { + margin-top: 1em; + background-color: #f0f0f0; + padding-bottom: 1em; +} + +.graphwiz-src-select { + margin-left: 0.2em; + margin-right: 0.2em; + margin-top: 0.2em; + margin-bottom: 0.2em; + padding-left: 0.3em; + padding-top: 0.3em; + padding-bottom: 0.3em; + padding-right: 0.3em; + font-size: 1.4em; +} + +.graphwiz-graph { + margin-left: 1em; + margin-right: 1em; +} + +.graphwiz-graph svg { + width: 100%; +} + +/* + * Table data. + */ +table.graphwiz-tab { + width: auto; +} + +.graphwiz-tab td { + text-align: right; +} + +/* + * The end selection. + */ +#graphwiz-end-selection { + margin-top: 1em; +} + +.graphwiz-end-selection-group { + clear: both; + display: block; +} + +.graphwiz-end-selection-group li { + display: block; + width: 25%; + float: left; +} + +#graphwiz-buildcategories li, #graphwiz-testcase-variations li { + width: 50%; +} + +.graphwiz-end-selection-group label { + margin-left: 0.3em; + vertical-align: middle; +} + +.graphwiz-end-selection-group input { + vertical-align: middle; +} + +.graphwiz-end-selection-group h3 { + font-size: 1.2em; + font-style: italic; + font-weight: bold; + margin-bottom: 0.26em; +} + +#graphwiz-buildcategories h3, #graphwiz-testcase-variations h3, #graphwiz-end-submit { + padding-top: 1em; +} + +#graphwiz-end-submit { + clear: both; + display: block; +} + + + +/* + * Tool tip tables. + */ +table.graphwiz-tt td:nth-child(1) { + font-weight: bold; +} + diff --git a/src/VBox/ValidationKit/testmanager/htdocs/css/tooltip.css b/src/VBox/ValidationKit/testmanager/htdocs/css/tooltip.css new file mode 100644 index 00000000..cb90ae0f --- /dev/null +++ b/src/VBox/ValidationKit/testmanager/htdocs/css/tooltip.css @@ -0,0 +1,132 @@ +/* $Id: tooltip.css $ */ +/** @file + * Test Manager - Tooltip content (via iframe). + */ + +/* + * 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 + */ + +/* + * Form the main divs in template-tooltip.html. + */ +.tooltip-main { + width: 100%; +} + +.tooltip-inner { + clear: both; + border: 2px solid black; + padding-left: 2px; + padding-right: 2px; + padding-top: 2px; + padding-bottom: 2px +} + +/* + * Timeline tooltip. + */ +.tmtimelinetooltip { + font-size: 1em; +} + +/* + * Relative stuff that could also be used for a non-tooltip VCS timeline. + */ +.tmvcstimeline-highlighted { + background: #f0f0f0; +} + +.tmvcstimeline h2 { + clear: both; + font-size: 120%; + background: #e8e8e8; + border-bottom: 1px solid #c8c8c8; + border-radius: 3px; + margin-left: 0.2em; + margin-right: 0.2em; + margin-top: 0.2em; + margin-bottom: 0.4em; + padding-left: 0.2em; + padding-right: 0.2em; + padding-top: 0.2em; + padding-bottom: 0.2em; +} + +.tmvcstimeline dl { + margin-left: 0.8em; + margin-right: 0.2em; + margin-top: 0.2em; + margin-bottom: 0.8em; +} + +.tmvcstimeline dt { + font-size: 118%; + padding-left: 0.2em; + margin-top: 0.1em; + margin-bottom: 0.0em; +} + +.tmvcstimeline dt, .tmvcstimeline :link, .tmvcstimeline :link:visited, .tmvcstimeline :link:hover { + color: black; + text-decoration: none; +} + +.tmvcstimeline :link:hover { + border: 1px dotted black; +} + +.tmvcstimeline-time { + font-size: 88%; + margin-right: 0.2em; +} + +.tmvcstimeline-time, .tmvcstimeline-author { + color: #5858a0; +} + +.tmvcstimeline-rev { + color: #0000ee; +} + +.tmvcstimeline dd { + padding-left: 2em; + margin-top: 0.0em; + margin-bottom: 0.4em; + color: #424250; +} + +/* This helps highlighting the revision we're showing the tooltip for. */ +.tmvcstimeline-highlight, .tmvcstimeline :target, .tmvcstimeline :target + dd { + background-color: #d8e8ff; + padding-top: 0.2em; + padding-bottom: 0.2em; +} + diff --git a/src/VBox/ValidationKit/testmanager/htdocs/images/VirtualBox.svg b/src/VBox/ValidationKit/testmanager/htdocs/images/VirtualBox.svg new file mode 100644 index 00000000..2369828b --- /dev/null +++ b/src/VBox/ValidationKit/testmanager/htdocs/images/VirtualBox.svg @@ -0,0 +1,806 @@ +<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 13.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 14948) -->
+<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
+<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+ width="256px" height="256px" viewBox="0 0 256 256" enable-background="new 0 0 256 256" xml:space="preserve">
+<g>
+ <rect fill="none" width="256" height="256"/>
+ <g>
+
+ <linearGradient id="SVGID_1_" gradientUnits="userSpaceOnUse" x1="-180.1821" y1="784.8389" x2="-56.9487" y2="784.8389" gradientTransform="matrix(1 0 0 1 246.6899 -587.7051)">
+ <stop offset="0" style="stop-color:#0A2B4D"/>
+ <stop offset="0.0423" style="stop-color:#7DA5DC"/>
+ <stop offset="0.0441" style="stop-color:#83A9DE"/>
+ <stop offset="0.0553" style="stop-color:#A1BFE8"/>
+ <stop offset="0.067" style="stop-color:#B9D0F0"/>
+ <stop offset="0.0794" style="stop-color:#CADCF6"/>
+ <stop offset="0.093" style="stop-color:#D4E4F9"/>
+ <stop offset="0.1099" style="stop-color:#D7E6FA"/>
+ <stop offset="0.254" style="stop-color:#D0DCFA"/>
+ <stop offset="0.42" style="stop-color:#85A0C8"/>
+ <stop offset="0.57" style="stop-color:#2E4573"/>
+ <stop offset="0.6978" style="stop-color:#14335E"/>
+ <stop offset="0.7692" style="stop-color:#1C3866"/>
+ <stop offset="0.8462" style="stop-color:#4F73AA"/>
+ <stop offset="0.9153" style="stop-color:#6487B9"/>
+ <stop offset="1" style="stop-color:#1A3B61"/>
+ </linearGradient>
+ <path fill="url(#SVGID_1_)" d="M129.117,141.783c33.426,0,60.732,23.482,60.732,52.32c0,1.838,0,4.744,0,6.578
+ c0,28.838-27.308,51.803-60.732,51.803c-33.503,0-60.811-22.965-60.811-51.803c0-1.834,0-4.74,0-6.578
+ C68.307,165.266,95.614,141.783,129.117,141.783z"/>
+
+ <linearGradient id="SVGID_2_" gradientUnits="userSpaceOnUse" x1="-150.8486" y1="822.7695" x2="-84.4476" y2="740.7712" gradientTransform="matrix(1 0 0 1 246.6899 -587.7051)">
+ <stop offset="0" style="stop-color:#E1E6FA"/>
+ <stop offset="0.07" style="stop-color:#B4C3E1"/>
+ <stop offset="0.11" style="stop-color:#8293B8"/>
+ <stop offset="0.1551" style="stop-color:#2D4173"/>
+ <stop offset="0.2888" style="stop-color:#1B2F61"/>
+ <stop offset="0.5" style="stop-color:#14285A"/>
+ <stop offset="0.7" style="stop-color:#14285A"/>
+ <stop offset="0.8663" style="stop-color:#213970"/>
+ <stop offset="1" style="stop-color:#4164A5"/>
+ </linearGradient>
+ <ellipse fill="url(#SVGID_2_)" cx="129.041" cy="194.065" rx="57.889" ry="49.1"/>
+ <defs>
+ <filter id="Adobe_OpacityMaskFilter" filterUnits="userSpaceOnUse" x="70.895" y="144.746" width="116.292" height="98.638">
+ <feColorMatrix type="matrix" values="1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 1 0"/>
+ </filter>
+ </defs>
+ <mask maskUnits="userSpaceOnUse" x="70.895" y="144.746" width="116.292" height="98.638" id="SVGID_3_">
+ <g filter="url(#Adobe_OpacityMaskFilter)">
+
+ <linearGradient id="SVGID_4_" gradientUnits="userSpaceOnUse" x1="-85.9404" y1="823.8486" x2="-149.3573" y2="739.6917" gradientTransform="matrix(1 0 0 1 246.6899 -587.7051)">
+ <stop offset="0" style="stop-color:#CCCCCC"/>
+ <stop offset="0.15" style="stop-color:#000000"/>
+ </linearGradient>
+ <ellipse fill="url(#SVGID_4_)" cx="129.041" cy="194.065" rx="58.146" ry="49.319"/>
+ </g>
+ </mask>
+
+ <radialGradient id="SVGID_5_" cx="-148.8311" cy="824.0654" r="100.2425" gradientTransform="matrix(1 0 0 1 246.6899 -587.7051)" gradientUnits="userSpaceOnUse">
+ <stop offset="0" style="stop-color:#DCE6FA"/>
+ <stop offset="0.4" style="stop-color:#AAC3EB"/>
+ <stop offset="1" style="stop-color:#AAC3EB"/>
+ </radialGradient>
+ <ellipse mask="url(#SVGID_3_)" fill="url(#SVGID_5_)" cx="129.041" cy="194.065" rx="58.146" ry="49.319"/>
+ <defs>
+ <filter id="Adobe_OpacityMaskFilter_1_" filterUnits="userSpaceOnUse" x="70.895" y="144.746" width="116.292" height="98.638">
+ <feColorMatrix type="matrix" values="1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 1 0"/>
+ </filter>
+ </defs>
+ <mask maskUnits="userSpaceOnUse" x="70.895" y="144.746" width="116.292" height="98.638" id="SVGID_6_">
+ <g filter="url(#Adobe_OpacityMaskFilter_1_)">
+
+ <linearGradient id="SVGID_7_" gradientUnits="userSpaceOnUse" x1="-117.6489" y1="831.0889" x2="-117.6489" y2="732.4512" gradientTransform="matrix(1 0 0 1 246.6899 -587.7051)">
+ <stop offset="0" style="stop-color:#999999"/>
+ <stop offset="0.05" style="stop-color:#000000"/>
+ </linearGradient>
+ <ellipse fill="url(#SVGID_7_)" cx="129.041" cy="194.065" rx="58.146" ry="49.319"/>
+ </g>
+ </mask>
+
+ <radialGradient id="SVGID_8_" cx="-148.8311" cy="824.0654" r="100.2425" gradientTransform="matrix(1 0 0 1 246.6899 -587.7051)" gradientUnits="userSpaceOnUse">
+ <stop offset="0" style="stop-color:#DCE6FA"/>
+ <stop offset="0.4" style="stop-color:#AAC3EB"/>
+ <stop offset="1" style="stop-color:#AAC3EB"/>
+ </radialGradient>
+ <ellipse mask="url(#SVGID_6_)" fill="url(#SVGID_8_)" cx="129.041" cy="194.065" rx="58.146" ry="49.319"/>
+ <defs>
+ <filter id="Adobe_OpacityMaskFilter_2_" filterUnits="userSpaceOnUse" x="70.895" y="144.746" width="116.292" height="98.638">
+ <feColorMatrix type="matrix" values="1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 1 0"/>
+ </filter>
+ </defs>
+ <mask maskUnits="userSpaceOnUse" x="70.895" y="144.746" width="116.292" height="98.638" id="SVGID_9_">
+ <g filter="url(#Adobe_OpacityMaskFilter_2_)">
+
+ <linearGradient id="SVGID_10_" gradientUnits="userSpaceOnUse" x1="-100.396" y1="829.1719" x2="-134.902" y2="734.3676" gradientTransform="matrix(1 0 0 1 246.6899 -587.7051)">
+ <stop offset="0" style="stop-color:#595959"/>
+ <stop offset="0.0535" style="stop-color:#282828"/>
+ <stop offset="0.1" style="stop-color:#000000"/>
+ </linearGradient>
+ <ellipse fill="url(#SVGID_10_)" cx="129.041" cy="194.065" rx="58.146" ry="49.319"/>
+ </g>
+ </mask>
+
+ <radialGradient id="SVGID_11_" cx="-148.8311" cy="824.0654" r="100.2425" gradientTransform="matrix(1 0 0 1 246.6899 -587.7051)" gradientUnits="userSpaceOnUse">
+ <stop offset="0" style="stop-color:#DCE6FA"/>
+ <stop offset="0.4" style="stop-color:#AAC3EB"/>
+ <stop offset="1" style="stop-color:#AAC3EB"/>
+ </radialGradient>
+ <ellipse mask="url(#SVGID_9_)" fill="url(#SVGID_11_)" cx="129.041" cy="194.065" rx="58.146" ry="49.319"/>
+ <defs>
+ <filter id="Adobe_OpacityMaskFilter_3_" filterUnits="userSpaceOnUse" x="70.895" y="144.746" width="116.292" height="98.638">
+ <feColorMatrix type="matrix" values="1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 1 0"/>
+ </filter>
+ </defs>
+ <mask maskUnits="userSpaceOnUse" x="70.895" y="144.746" width="116.292" height="98.638" id="SVGID_12_">
+ <g filter="url(#Adobe_OpacityMaskFilter_3_)">
+
+ <linearGradient id="SVGID_13_" gradientUnits="userSpaceOnUse" x1="-134.9023" y1="829.1719" x2="-100.3965" y2="734.368" gradientTransform="matrix(1 0 0 1 246.6899 -587.7051)">
+ <stop offset="0" style="stop-color:#FFFFFF"/>
+ <stop offset="0.05" style="stop-color:#1A1A1A"/>
+ <stop offset="0.09" style="stop-color:#000000"/>
+ </linearGradient>
+ <ellipse fill="url(#SVGID_13_)" cx="129.041" cy="194.065" rx="58.146" ry="49.319"/>
+ </g>
+ </mask>
+
+ <radialGradient id="SVGID_14_" cx="-148.8311" cy="824.0654" r="100.2425" gradientTransform="matrix(1 0 0 1 246.6899 -587.7051)" gradientUnits="userSpaceOnUse">
+ <stop offset="0" style="stop-color:#DCE6FA"/>
+ <stop offset="0.4" style="stop-color:#AAC3EB"/>
+ <stop offset="1" style="stop-color:#AAC3EB"/>
+ </radialGradient>
+ <ellipse mask="url(#SVGID_12_)" fill="url(#SVGID_14_)" cx="129.041" cy="194.065" rx="58.146" ry="49.319"/>
+
+ <linearGradient id="SVGID_15_" gradientUnits="userSpaceOnUse" x1="-178.3818" y1="813.666" x2="-56.8384" y2="813.666" gradientTransform="matrix(1 0 0 1 246.6899 -587.7051)">
+ <stop offset="0.0055" style="stop-color:#3C5A78"/>
+ <stop offset="0.033" style="stop-color:#6E8CBE"/>
+ <stop offset="0.123" style="stop-color:#A5BEE1"/>
+ <stop offset="0.25" style="stop-color:#96AFD2"/>
+ <stop offset="0.52" style="stop-color:#3C5A87"/>
+ <stop offset="0.72" style="stop-color:#2B486D"/>
+ <stop offset="0.9" style="stop-color:#466491"/>
+ <stop offset="1" style="stop-color:#3C5082"/>
+ </linearGradient>
+ <path fill="url(#SVGID_15_)" d="M189.852,198.923c0,0.61,0,1.222,0,1.759c0,28.838-27.309,52.318-60.733,52.318
+ c-33.501,0-60.81-23.48-60.81-52.318c0-0.537,0-1.146,0-1.759c0,28.761,27.308,52.243,60.81,52.243
+ C162.543,251.166,189.852,227.684,189.852,198.923z"/>
+
+ <linearGradient id="SVGID_16_" gradientUnits="userSpaceOnUse" x1="-178.1328" y1="776.4775" x2="-57.1652" y2="787.0609" gradientTransform="matrix(1 0 0 1 246.6899 -587.7051)">
+ <stop offset="0.0053" style="stop-color:#9BBEE1"/>
+ <stop offset="0.0802" style="stop-color:#D2E6FA"/>
+ <stop offset="0.1337" style="stop-color:#F0F5FF"/>
+ <stop offset="0.1979" style="stop-color:#F0F5FF"/>
+ <stop offset="0.35" style="stop-color:#DEE6F0"/>
+ <stop offset="0.55" style="stop-color:#AFBEDC"/>
+ <stop offset="0.7" style="stop-color:#96AFD7"/>
+ <stop offset="0.9091" style="stop-color:#A0B4DC"/>
+ <stop offset="1" style="stop-color:#6487AF"/>
+ </linearGradient>
+ <path fill="url(#SVGID_16_)" d="M129.041,141.693c-33.563,0-60.771,23.449-60.771,52.371s27.208,52.371,60.771,52.371
+ c33.564,0,60.771-23.449,60.771-52.371C189.813,165.145,162.604,141.693,129.041,141.693z M129.041,243.165
+ c-31.971,0-57.887-21.983-57.887-49.101c0-27.115,25.916-49.104,57.887-49.104c31.973,0,57.889,21.986,57.889,49.104
+ S161.014,243.165,129.041,243.165z"/>
+ <defs>
+ <filter id="Adobe_OpacityMaskFilter_4_" filterUnits="userSpaceOnUse" x="68.27" y="141.693" width="121.542" height="104.742">
+ <feColorMatrix type="matrix" values="1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 1 0"/>
+ </filter>
+ </defs>
+ <mask maskUnits="userSpaceOnUse" x="68.27" y="141.693" width="121.542" height="104.742" id="SVGID_17_">
+ <g filter="url(#Adobe_OpacityMaskFilter_4_)">
+
+ <linearGradient id="SVGID_18_" gradientUnits="userSpaceOnUse" x1="-117.6489" y1="834.1406" x2="-117.6489" y2="729.4004" gradientTransform="matrix(1 0 0 1 246.6899 -587.7051)">
+ <stop offset="0.35" style="stop-color:#000000"/>
+ <stop offset="0.6" style="stop-color:#FFFFFF"/>
+ </linearGradient>
+ <ellipse fill="url(#SVGID_18_)" cx="129.041" cy="194.065" rx="60.771" ry="52.37"/>
+ </g>
+ </mask>
+ <path mask="url(#SVGID_17_)" fill="#142355" d="M129.041,141.693c-33.563,0-60.771,23.449-60.771,52.371
+ s27.208,52.371,60.771,52.371c33.564,0,60.771-23.449,60.771-52.371C189.813,165.145,162.604,141.693,129.041,141.693z
+ M129.041,243.165c-31.971,0-57.887-21.983-57.887-49.101c0-27.115,25.916-49.104,57.887-49.104
+ c31.973,0,57.889,21.986,57.889,49.104S161.014,243.165,129.041,243.165z"/>
+ <defs>
+ <filter id="Adobe_OpacityMaskFilter_5_" filterUnits="userSpaceOnUse" x="68.27" y="141.693" width="121.542" height="104.742">
+ <feColorMatrix type="matrix" values="1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 1 0"/>
+ </filter>
+ </defs>
+ <mask maskUnits="userSpaceOnUse" x="68.27" y="141.693" width="121.542" height="104.742" id="SVGID_19_">
+ <g filter="url(#Adobe_OpacityMaskFilter_5_)">
+
+ <linearGradient id="SVGID_20_" gradientUnits="userSpaceOnUse" x1="-135.9253" y1="831.9824" x2="-99.3735" y2="731.5574" gradientTransform="matrix(1 0 0 1 246.6899 -587.7051)">
+ <stop offset="0.3" style="stop-color:#000000"/>
+ <stop offset="1" style="stop-color:#CCCCCC"/>
+ </linearGradient>
+ <ellipse fill="url(#SVGID_20_)" cx="129.041" cy="194.065" rx="60.771" ry="52.37"/>
+ </g>
+ </mask>
+ <path mask="url(#SVGID_19_)" fill="#142355" d="M129.041,141.693c-33.563,0-60.771,23.449-60.771,52.371
+ s27.208,52.371,60.771,52.371c33.564,0,60.771-23.449,60.771-52.371C189.813,165.145,162.604,141.693,129.041,141.693z
+ M129.041,243.165c-31.971,0-57.887-21.983-57.887-49.101c0-27.115,25.916-49.104,57.887-49.104
+ c31.973,0,57.889,21.986,57.889,49.104S161.014,243.165,129.041,243.165z"/>
+
+ <radialGradient id="SVGID_21_" cx="-242.0352" cy="-534.5098" r="111.9119" gradientTransform="matrix(1 0 0 -1 370.0347 -414.4775)" gradientUnits="userSpaceOnUse">
+ <stop offset="0" style="stop-color:#FFFFFF"/>
+ <stop offset="1" style="stop-color:#C3D2F0"/>
+ </radialGradient>
+ <path fill="url(#SVGID_21_)" d="M234.529,52.965l-8.549,108.286c-0.113,1.454-0.854,2.812-2.012,3.699l-92.826,71.046
+ c-0.933,0.714-2.037,1.068-3.144,1.068c-1.105,0-2.21-0.354-3.14-1.068l-92.832-71.045c-1.157-0.889-1.897-2.246-2.011-3.7
+ L21.47,52.965c-0.173-2.195,1.062-4.258,3.078-5.141L125.929,3.433c1.318-0.577,2.827-0.577,4.147,0l101.375,44.393
+ C233.467,48.708,234.703,50.771,234.529,52.965z"/>
+
+ <linearGradient id="SVGID_22_" gradientUnits="userSpaceOnUse" x1="-121.1333" y1="710.2393" x2="-217.5764" y2="629.314" gradientTransform="matrix(1 0 0 1 246.6899 -587.7051)">
+ <stop offset="0" style="stop-color:#FFFFFF"/>
+ <stop offset="0.9" style="stop-color:#F0F5FF"/>
+ <stop offset="0.96" style="stop-color:#DCE6F8"/>
+ <stop offset="0.99" style="stop-color:#C3D2F0"/>
+ </linearGradient>
+ <path fill="url(#SVGID_22_)" d="M22.205,49.877c-1.1,1.812,2.384,3.694,2.678,3.872l99.186,60.164
+ c1.081,0.655,1.748,1.842,1.752,3.109l4.361-0.001c0.004-1.266,0.672-2.454,1.752-3.109l-1.959-3.612
+ c-0.578,0.348-1.229,0.521-1.882,0.521c-0.652,0-1.305-0.173-1.883-0.521L26.503,50.528c-0.292-0.175-0.46-0.499-0.439-0.837
+ C26.063,49.691,23.283,48.1,22.205,49.877z"/>
+
+ <linearGradient id="SVGID_23_" gradientUnits="userSpaceOnUse" x1="-106.9102" y1="716.4375" x2="-29.1729" y2="623.7938" gradientTransform="matrix(1 0 0 1 246.6899 -587.7051)">
+ <stop offset="0" style="stop-color:#FFFFFF"/>
+ <stop offset="0.6" style="stop-color:#F5FAFF"/>
+ <stop offset="0.92" style="stop-color:#E1E6FA"/>
+ <stop offset="0.97" style="stop-color:#CFDAF4"/>
+ <stop offset="1" style="stop-color:#C0CFEE"/>
+ </linearGradient>
+ <path fill="url(#SVGID_23_)" d="M233.797,49.876c1.1,1.812-2.385,3.694-2.678,3.872l-99.188,60.163
+ c-1.08,0.655-1.748,1.842-1.752,3.109l-4.36-0.001c-0.005-1.266-0.672-2.454-1.752-3.109l1.958-3.612
+ c0.579,0.348,1.231,0.521,1.882,0.521c0.652,0,1.304-0.172,1.882-0.521l99.707-59.771c0.291-0.175,0.461-0.499,0.438-0.837
+ C229.938,49.69,232.719,48.099,233.797,49.876z"/>
+
+ <linearGradient id="SVGID_24_" gradientUnits="userSpaceOnUse" x1="-118.689" y1="698.002" x2="-118.689" y2="824.7695" gradientTransform="matrix(1 0 0 1 246.6899 -587.7051)">
+ <stop offset="0" style="stop-color:#FFFFFF"/>
+ <stop offset="0.9" style="stop-color:#DCE6FF"/>
+ <stop offset="1" style="stop-color:#C2D4F0"/>
+ </linearGradient>
+ <path fill="url(#SVGID_24_)" d="M128,237.064c-2.653,0-2.922-3.642-2.922-3.642c0.138,0,0.278-0.03,0.405-0.096
+ c0.31-0.155,0.507-0.474,0.507-0.819l-0.17-115.49c-0.005-1.267-0.672-2.454-1.752-3.109l2.142-3.612
+ c0.579,0.348,1.231,0.52,1.883,0.52c0.65,0,1.304-0.172,1.882-0.52l1.959,3.611c-1.082,0.655-1.748,1.843-1.752,3.109
+ l-0.17,115.49c0,0.349,0.194,0.665,0.506,0.819c0.127,0.063,0.268,0.095,0.403,0.095C130.922,233.423,130.654,237.064,128,237.064
+ z"/>
+ <defs>
+ <filter id="Adobe_OpacityMaskFilter_6_" filterUnits="userSpaceOnUse" x="21.454" y="3" width="213.092" height="234.064">
+ <feColorMatrix type="matrix" values="1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 1 0"/>
+ </filter>
+ </defs>
+ <mask maskUnits="userSpaceOnUse" x="21.454" y="3" width="213.092" height="234.064" id="SVGID_25_">
+ <g filter="url(#Adobe_OpacityMaskFilter_6_)">
+
+ <linearGradient id="SVGID_26_" gradientUnits="userSpaceOnUse" x1="-225.665" y1="639.6367" x2="-23.757" y2="755.2705" gradientTransform="matrix(1 0 0 1 246.6899 -587.7051)">
+ <stop offset="0.78" style="stop-color:#000000"/>
+ <stop offset="0.8" style="stop-color:#FFFFFF"/>
+ </linearGradient>
+ <path fill="url(#SVGID_26_)" d="M234.529,52.965l-8.549,108.286c-0.113,1.454-0.854,2.812-2.012,3.699l-92.826,71.046
+ c-0.933,0.714-2.037,1.068-3.144,1.068c-1.105,0-2.21-0.354-3.14-1.068l-92.832-71.045c-1.157-0.889-1.897-2.246-2.011-3.7
+ L21.47,52.965c-0.173-2.195,1.062-4.258,3.078-5.141L125.929,3.433c1.318-0.577,2.827-0.577,4.147,0l101.375,44.393
+ C233.467,48.708,234.703,50.771,234.529,52.965z"/>
+ </g>
+ </mask>
+
+ <linearGradient id="SVGID_27_" gradientUnits="userSpaceOnUse" x1="-213.625" y1="755.2715" x2="-11.7157" y2="639.637" gradientTransform="matrix(1 0 0 1 246.6899 -587.7051)">
+ <stop offset="0.2" style="stop-color:#C3D2F0"/>
+ <stop offset="0.22" style="stop-color:#7DA0D2"/>
+ <stop offset="0.2674" style="stop-color:#6E91CD"/>
+ <stop offset="0.7" style="stop-color:#7396D2"/>
+ <stop offset="1" style="stop-color:#5A78B4"/>
+ </linearGradient>
+ <path mask="url(#SVGID_25_)" fill="url(#SVGID_27_)" d="M234.529,52.965l-8.549,108.286c-0.113,1.454-0.854,2.812-2.012,3.699
+ l-92.826,71.046c-0.933,0.714-2.037,1.068-3.144,1.068c-1.105,0-2.21-0.354-3.14-1.068l-92.832-71.045
+ c-1.157-0.889-1.897-2.246-2.011-3.7L21.47,52.965c-0.173-2.195,1.062-4.258,3.078-5.141L125.929,3.433
+ c1.318-0.577,2.827-0.577,4.147,0l101.375,44.393C233.467,48.708,234.703,50.771,234.529,52.965z"/>
+ <defs>
+ <filter id="Adobe_OpacityMaskFilter_7_" filterUnits="userSpaceOnUse" x="21.454" y="3" width="213.092" height="234.064">
+ <feColorMatrix type="matrix" values="1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 1 0"/>
+ </filter>
+ </defs>
+ <mask maskUnits="userSpaceOnUse" x="21.454" y="3" width="213.092" height="234.064" id="SVGID_28_">
+ <g filter="url(#Adobe_OpacityMaskFilter_7_)">
+
+ <linearGradient id="SVGID_29_" gradientUnits="userSpaceOnUse" x1="-213.522" y1="755.417" x2="-12.0263" y2="639.0833" gradientTransform="matrix(1 0 0 1 246.6899 -587.7051)">
+ <stop offset="0.2" style="stop-color:#FFFFFF"/>
+ <stop offset="0.22" style="stop-color:#000000"/>
+ </linearGradient>
+ <path fill="url(#SVGID_29_)" d="M234.529,52.965l-8.549,108.286c-0.113,1.454-0.854,2.812-2.012,3.699l-92.826,71.046
+ c-0.933,0.714-2.037,1.068-3.144,1.068c-1.105,0-2.21-0.354-3.14-1.068l-92.832-71.045c-1.157-0.889-1.897-2.246-2.011-3.7
+ L21.47,52.965c-0.173-2.195,1.062-4.258,3.078-5.141L125.929,3.433c1.318-0.577,2.827-0.577,4.147,0l101.375,44.393
+ C233.467,48.708,234.703,50.771,234.529,52.965z"/>
+ </g>
+ </mask>
+
+ <linearGradient id="SVGID_30_" gradientUnits="userSpaceOnUse" x1="-225.354" y1="639.084" x2="-23.8602" y2="755.4165" gradientTransform="matrix(1 0 0 1 246.6899 -587.7051)">
+ <stop offset="0" style="stop-color:#B9C8DC"/>
+ <stop offset="0.3" style="stop-color:#D7D5EB"/>
+ <stop offset="0.5" style="stop-color:#D7DCEB"/>
+ <stop offset="0.78" style="stop-color:#DCDCEB"/>
+ <stop offset="0.8" style="stop-color:#C3D2F0"/>
+ </linearGradient>
+ <path mask="url(#SVGID_28_)" fill="url(#SVGID_30_)" d="M234.529,52.965l-8.549,108.286c-0.113,1.454-0.854,2.812-2.012,3.699
+ l-92.826,71.046c-0.933,0.714-2.037,1.068-3.144,1.068c-1.105,0-2.21-0.354-3.14-1.068l-92.832-71.045
+ c-1.157-0.889-1.897-2.246-2.011-3.7L21.47,52.965c-0.173-2.195,1.062-4.258,3.078-5.141L125.929,3.433
+ c1.318-0.577,2.827-0.577,4.147,0l101.375,44.393C233.467,48.708,234.703,50.771,234.529,52.965z"/>
+ <defs>
+ <filter id="Adobe_OpacityMaskFilter_8_" filterUnits="userSpaceOnUse" x="21.454" y="3" width="213.092" height="234.064">
+ <feColorMatrix type="matrix" values="1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 1 0"/>
+ </filter>
+ </defs>
+ <mask maskUnits="userSpaceOnUse" x="21.454" y="3" width="213.092" height="234.064" id="SVGID_31_">
+ <g filter="url(#Adobe_OpacityMaskFilter_8_)">
+
+ <radialGradient id="SVGID_32_" cx="-214.9419" cy="621.7891" r="31.5227" gradientTransform="matrix(1 0 0 1 246.6899 -587.7051)" gradientUnits="userSpaceOnUse">
+ <stop offset="0" style="stop-color:#595959"/>
+ <stop offset="1" style="stop-color:#000000"/>
+ </radialGradient>
+ <path fill="url(#SVGID_32_)" d="M234.529,52.965l-8.549,108.286c-0.113,1.454-0.854,2.812-2.012,3.699l-92.826,71.046
+ c-0.933,0.714-2.037,1.068-3.144,1.068c-1.105,0-2.21-0.354-3.14-1.068l-92.832-71.045c-1.157-0.889-1.897-2.246-2.011-3.7
+ L21.47,52.965c-0.173-2.195,1.062-4.258,3.078-5.141L125.929,3.433c1.318-0.577,2.827-0.577,4.147,0l101.375,44.393
+ C233.467,48.708,234.703,50.771,234.529,52.965z"/>
+ </g>
+ </mask>
+ <path mask="url(#SVGID_31_)" fill="#E1F5FF" d="M234.529,52.965l-8.549,108.286c-0.113,1.454-0.854,2.812-2.012,3.699
+ l-92.826,71.046c-0.933,0.714-2.037,1.068-3.144,1.068c-1.105,0-2.21-0.354-3.14-1.068l-92.832-71.045
+ c-1.157-0.889-1.897-2.246-2.011-3.7L21.47,52.965c-0.173-2.195,1.062-4.258,3.078-5.141L125.929,3.433
+ c1.318-0.577,2.827-0.577,4.147,0l101.375,44.393C233.467,48.708,234.703,50.771,234.529,52.965z"/>
+ <g>
+
+ <radialGradient id="SVGID_33_" cx="-118.6724" cy="642.2432" r="64.9423" gradientTransform="matrix(1 0 0 1 246.6899 -587.7051)" gradientUnits="userSpaceOnUse">
+ <stop offset="0" style="stop-color:#19416E"/>
+ <stop offset="1" style="stop-color:#0A2D64"/>
+ </radialGradient>
+ <path fill="url(#SVGID_33_)" d="M209.039,50.425l-79.438,46.227c-0.477,0.275-1.002,0.412-1.528,0.412
+ c-0.527,0-1.053-0.136-1.528-0.412L46.999,50.426c-0.25-0.145-0.398-0.417-0.384-0.705c0.014-0.288,0.188-0.545,0.449-0.666
+ l79.834-36.784c0.746-0.346,1.604-0.346,2.354,0l79.721,36.783c0.262,0.122,0.435,0.377,0.447,0.667
+ C209.436,50.008,209.287,50.28,209.039,50.425z"/>
+
+ <radialGradient id="SVGID_34_" cx="-172.187" cy="727.8721" r="56.6285" gradientTransform="matrix(1 0 0 1 246.6899 -587.7051)" gradientUnits="userSpaceOnUse">
+ <stop offset="0" style="stop-color:#19416E"/>
+ <stop offset="1" style="stop-color:#0A2D64"/>
+ </radialGradient>
+ <path fill="url(#SVGID_34_)" d="M114.98,123.905l0.871,83.976c0.002,0.327-0.178,0.628-0.471,0.775
+ c-0.123,0.063-0.257,0.096-0.391,0.096c-0.18,0-0.361-0.057-0.512-0.169l-74.515-54.917c-0.674-0.497-1.095-1.257-1.153-2.092
+ l-5.652-79.07c-0.024-0.324,0.138-0.634,0.417-0.8c0.277-0.166,0.626-0.162,0.902,0.008l79.063,49.616
+ C114.428,121.886,114.969,122.855,114.98,123.905z"/>
+
+ <radialGradient id="SVGID_35_" cx="-1001.7246" cy="782.7354" r="61.9365" gradientTransform="matrix(-0.9143 0 0 0.9143 -734.343 -575.489)" gradientUnits="userSpaceOnUse">
+ <stop offset="0" style="stop-color:#19416E"/>
+ <stop offset="1" style="stop-color:#0A2D64"/>
+ </radialGradient>
+ <path fill="url(#SVGID_35_)" d="M141.057,123.904l-0.871,83.977c-0.002,0.326,0.179,0.627,0.472,0.775
+ c0.122,0.063,0.258,0.095,0.391,0.095c0.181,0,0.361-0.058,0.513-0.168l74.516-54.917c0.674-0.498,1.096-1.257,1.152-2.093
+ l5.651-79.07c0.022-0.324-0.139-0.634-0.416-0.8c-0.276-0.166-0.627-0.163-0.901,0.008L142.5,121.327
+ C141.609,121.885,141.068,122.854,141.057,123.904z"/>
+ </g>
+
+ <linearGradient id="SVGID_36_" gradientUnits="userSpaceOnUse" x1="-199.5181" y1="619.627" x2="-37.8291" y2="619.627" gradientTransform="matrix(1 0 0 1 246.6899 -587.7051)">
+ <stop offset="0" style="stop-color:#234B82"/>
+ <stop offset="0.489" style="stop-color:#3C5A8C"/>
+ <stop offset="0.5112" style="stop-color:#E6F0FF"/>
+ <stop offset="0.7697" style="stop-color:#FFFFFF"/>
+ <stop offset="1" style="stop-color:#F0F5FF"/>
+ </linearGradient>
+ <path fill="url(#SVGID_36_)" d="M208.004,51.354c0.525-0.314,0.885-0.917,0.855-1.54c-0.027-0.646-0.422-1.267-1.006-1.537
+ l-78.097-36.02c-0.534-0.248-1.11-0.371-1.687-0.371s-1.153,0.123-1.688,0.373l-78.2,36.018c-0.586,0.27-1.01,0.89-1.01,1.525
+ c0,0.621,0.328,1.245,0.863,1.557l1.186,0.6c-0.243-0.141-0.387-0.405-0.373-0.686c0.014-0.279,0.182-0.53,0.437-0.647
+ l77.641-35.774c0.726-0.335,1.561-0.335,2.289,0l77.533,35.772c0.254,0.119,0.422,0.367,0.436,0.649
+ c0.015,0.279-0.131,0.544-0.371,0.685L208.004,51.354z"/>
+
+ <linearGradient id="SVGID_37_" gradientUnits="userSpaceOnUse" x1="-114.0732" y1="786.165" x2="-6.8749" y2="678.9667" gradientTransform="matrix(1 0 0 1 246.6899 -587.7051)">
+ <stop offset="0" style="stop-color:#AFC3D7"/>
+ <stop offset="0.2033" style="stop-color:#BED2E6"/>
+ <stop offset="0.5879" style="stop-color:#BED7EB"/>
+ <stop offset="0.6154" style="stop-color:#3C5A8C"/>
+ <stop offset="1" style="stop-color:#234B82"/>
+ </linearGradient>
+ <path fill="url(#SVGID_37_)" d="M140.545,205.538l-0.002,0.062c-0.002,0.713,0.405,1.371,1.039,1.696
+ c0.271,0.142,0.572,0.212,0.871,0.212c0.393,0,0.795-0.121,1.137-0.375l71.861-52.972c0.945-0.697,1.549-1.777,1.627-2.957
+ l5.451-76.295c0.003-0.051,0.006-0.087,0.006-0.139c-0.003-0.675-0.354-1.291-0.93-1.635c-0.299-0.178-0.64-0.271-0.979-0.271
+ c-0.346,0-0.701,0.094-1.021,0.296l-0.818,0.53c0.266-0.164,0.6-0.167,0.865-0.008c0.266,0.159,0.424,0.457,0.398,0.767
+ l-5.425,75.887c-0.058,0.803-0.461,1.53-1.106,2.008l-71.517,52.707c-0.146,0.108-0.317,0.161-0.489,0.161
+ c-0.13,0-0.259-0.03-0.375-0.091c-0.281-0.143-0.455-0.43-0.453-0.744L140.545,205.538z"/>
+
+ <linearGradient id="SVGID_38_" gradientUnits="userSpaceOnUse" x1="-230.4624" y1="678.9688" x2="-123.2632" y2="786.1679" gradientTransform="matrix(1 0 0 1 246.6899 -587.7051)">
+ <stop offset="0" style="stop-color:#234B82"/>
+ <stop offset="0.3846" style="stop-color:#3C5A8C"/>
+ <stop offset="0.4045" style="stop-color:#F0F5FF"/>
+ <stop offset="0.691" style="stop-color:#FFFFFF"/>
+ <stop offset="1" style="stop-color:#FFFFFF"/>
+ </linearGradient>
+ <path fill="url(#SVGID_38_)" d="M115.354,204.379c0.003,0.314-0.171,0.604-0.451,0.744c-0.119,0.061-0.247,0.091-0.376,0.091
+ c-0.173,0-0.346-0.053-0.49-0.161L42.52,152.346c-0.646-0.478-1.05-1.205-1.106-2.008l-5.425-75.887
+ c-0.023-0.311,0.133-0.608,0.4-0.767c0.266-0.159,0.601-0.156,0.865,0.008l-0.818-0.53c-0.318-0.203-0.675-0.296-1.02-0.296
+ c-0.341,0-0.681,0.092-0.98,0.271c-0.576,0.344-0.926,0.96-0.928,1.635c0,0.052,0.002,0.087,0.004,0.139l5.452,76.295
+ c0.079,1.18,0.682,2.26,1.628,2.957l71.861,52.972c0.341,0.254,0.743,0.375,1.137,0.375c0.298,0,0.599-0.07,0.871-0.212
+ c0.633-0.325,1.04-0.983,1.038-1.696l-0.002-0.062L115.354,204.379z"/>
+
+ <linearGradient id="SVGID_39_" gradientUnits="userSpaceOnUse" x1="-242.0366" y1="-525.2964" x2="-242.0366" y2="-418.9517" gradientTransform="matrix(1 0 0 -1 370.0347 -414.4775)">
+ <stop offset="0" style="stop-color:#FFFFFF"/>
+ <stop offset="1" style="stop-color:#BEDCFA"/>
+ </linearGradient>
+ <path fill="url(#SVGID_39_)" d="M229.395,48.914l-99.83-44.127c-0.938-0.417-2.003-0.417-2.947,0L26.605,48.914
+ c-0.312,0.138-0.521,0.437-0.542,0.777c-0.021,0.339,0.147,0.662,0.439,0.837l99.707,59.771c0.578,0.348,1.231,0.52,1.883,0.52
+ c0.652,0,1.303-0.172,1.883-0.52l99.52-59.771c0.293-0.175,0.463-0.499,0.439-0.837C229.912,49.351,229.705,49.051,229.395,48.914
+ z M206.813,50.538l-77.256,44.957c-0.461,0.267-0.976,0.4-1.486,0.4c-0.513,0-1.024-0.132-1.486-0.4L49.224,50.539
+ c-0.243-0.141-0.387-0.405-0.373-0.686c0.013-0.279,0.182-0.53,0.437-0.647l77.641-35.774c0.726-0.335,1.561-0.335,2.29,0
+ l77.531,35.772c0.256,0.119,0.422,0.367,0.438,0.649C207.199,50.133,207.057,50.397,206.813,50.538z"/>
+ <defs>
+ <filter id="Adobe_OpacityMaskFilter_9_" filterUnits="userSpaceOnUse" x="26.061" y="4.475" width="203.875" height="106.344">
+ <feColorMatrix type="matrix" values="1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 1 0"/>
+ </filter>
+ </defs>
+ <mask maskUnits="userSpaceOnUse" x="26.061" y="4.475" width="203.875" height="106.344" id="SVGID_40_">
+ <g filter="url(#Adobe_OpacityMaskFilter_9_)">
+
+ <linearGradient id="SVGID_41_" gradientUnits="userSpaceOnUse" x1="-318.0947" y1="-509.2944" x2="-164.9766" y2="-420.8915" gradientTransform="matrix(1 0 0 -1 370.0347 -414.4775)">
+ <stop offset="0.6" style="stop-color:#000000"/>
+ <stop offset="1" style="stop-color:#FFFFFF"/>
+ </linearGradient>
+ <path fill="url(#SVGID_41_)" d="M229.395,48.914l-99.83-44.127c-0.938-0.417-2.003-0.417-2.947,0L26.605,48.914
+ c-0.312,0.138-0.521,0.437-0.542,0.777c-0.021,0.339,0.147,0.662,0.439,0.837l99.707,59.771c0.578,0.348,1.231,0.52,1.883,0.52
+ c0.652,0,1.303-0.172,1.883-0.52l99.52-59.771c0.293-0.175,0.463-0.499,0.439-0.837
+ C229.912,49.351,229.705,49.051,229.395,48.914z M206.813,50.538l-77.256,44.957c-0.461,0.267-0.976,0.4-1.486,0.4
+ c-0.513,0-1.024-0.132-1.486-0.4L49.224,50.539c-0.243-0.141-0.387-0.405-0.373-0.686c0.013-0.279,0.182-0.53,0.437-0.647
+ l77.641-35.774c0.726-0.335,1.561-0.335,2.29,0l77.531,35.772c0.256,0.119,0.422,0.367,0.438,0.649
+ C207.199,50.133,207.057,50.397,206.813,50.538z"/>
+ </g>
+ </mask>
+ <path mask="url(#SVGID_40_)" fill="#C3DCFA" d="M229.395,48.914l-99.83-44.127c-0.938-0.417-2.003-0.417-2.947,0L26.605,48.914
+ c-0.312,0.138-0.521,0.437-0.542,0.777c-0.021,0.339,0.147,0.662,0.439,0.837l99.707,59.771c0.578,0.348,1.231,0.52,1.883,0.52
+ c0.652,0,1.303-0.172,1.883-0.52l99.52-59.771c0.293-0.175,0.463-0.499,0.439-0.837C229.912,49.351,229.705,49.051,229.395,48.914
+ z M206.813,50.538l-77.256,44.957c-0.461,0.267-0.976,0.4-1.486,0.4c-0.513,0-1.024-0.132-1.486-0.4L49.224,50.539
+ c-0.243-0.141-0.387-0.405-0.373-0.686c0.013-0.279,0.182-0.53,0.437-0.647l77.641-35.774c0.726-0.335,1.561-0.335,2.29,0
+ l77.531,35.772c0.256,0.119,0.422,0.367,0.438,0.649C207.199,50.133,207.057,50.397,206.813,50.538z"/>
+ <defs>
+ <filter id="Adobe_OpacityMaskFilter_10_" filterUnits="userSpaceOnUse" x="26.061" y="4.475" width="203.875" height="106.344">
+ <feColorMatrix type="matrix" values="1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 1 0"/>
+ </filter>
+ </defs>
+ <mask maskUnits="userSpaceOnUse" x="26.061" y="4.475" width="203.875" height="106.344" id="SVGID_42_">
+ <g filter="url(#Adobe_OpacityMaskFilter_10_)">
+
+ <linearGradient id="SVGID_43_" gradientUnits="userSpaceOnUse" x1="-284.3638" y1="-514.6699" x2="-199.7078" y2="-413.781" gradientTransform="matrix(1 0 0 -1 370.0347 -414.4775)">
+ <stop offset="0" style="stop-color:#7A7A7A"/>
+ <stop offset="0.3048" style="stop-color:#000000"/>
+ </linearGradient>
+ <path fill="url(#SVGID_43_)" d="M229.395,48.914l-99.83-44.127c-0.938-0.417-2.003-0.417-2.947,0L26.605,48.914
+ c-0.312,0.138-0.521,0.437-0.542,0.777c-0.021,0.339,0.147,0.662,0.439,0.837l99.707,59.771c0.578,0.348,1.231,0.52,1.883,0.52
+ c0.652,0,1.303-0.172,1.883-0.52l99.52-59.771c0.293-0.175,0.463-0.499,0.439-0.837
+ C229.912,49.351,229.705,49.051,229.395,48.914z M206.813,50.538l-77.256,44.957c-0.461,0.267-0.976,0.4-1.486,0.4
+ c-0.513,0-1.024-0.132-1.486-0.4L49.224,50.539c-0.243-0.141-0.387-0.405-0.373-0.686c0.013-0.279,0.182-0.53,0.437-0.647
+ l77.641-35.774c0.726-0.335,1.561-0.335,2.29,0l77.531,35.772c0.256,0.119,0.422,0.367,0.438,0.649
+ C207.199,50.133,207.057,50.397,206.813,50.538z"/>
+ </g>
+ </mask>
+
+ <linearGradient id="SVGID_44_" gradientUnits="userSpaceOnUse" x1="-268.7026" y1="-510.4141" x2="-216.3228" y2="-419.6894" gradientTransform="matrix(1 0 0 -1 370.0347 -414.4775)">
+ <stop offset="0.0053" style="stop-color:#FFF5F0"/>
+ <stop offset="0.25" style="stop-color:#FFFFFF"/>
+ </linearGradient>
+ <path mask="url(#SVGID_42_)" fill="url(#SVGID_44_)" d="M229.395,48.914l-99.83-44.127c-0.938-0.417-2.003-0.417-2.947,0
+ L26.605,48.914c-0.312,0.138-0.521,0.437-0.542,0.777c-0.021,0.339,0.147,0.662,0.439,0.837l99.707,59.771
+ c0.578,0.348,1.231,0.52,1.883,0.52c0.652,0,1.303-0.172,1.883-0.52l99.52-59.771c0.293-0.175,0.463-0.499,0.439-0.837
+ C229.912,49.351,229.705,49.051,229.395,48.914z M206.813,50.538l-77.256,44.957c-0.461,0.267-0.976,0.4-1.486,0.4
+ c-0.513,0-1.024-0.132-1.486-0.4L49.224,50.539c-0.243-0.141-0.387-0.405-0.373-0.686c0.013-0.279,0.182-0.53,0.437-0.647
+ l77.641-35.774c0.726-0.335,1.561-0.335,2.29,0l77.531,35.772c0.256,0.119,0.422,0.367,0.438,0.649
+ C207.199,50.133,207.057,50.397,206.813,50.538z"/>
+ <defs>
+ <filter id="Adobe_OpacityMaskFilter_11_" filterUnits="userSpaceOnUse" x="26.061" y="4.475" width="203.875" height="106.344">
+ <feColorMatrix type="matrix" values="1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 1 0"/>
+ </filter>
+ </defs>
+ <mask maskUnits="userSpaceOnUse" x="26.061" y="4.475" width="203.875" height="106.344" id="SVGID_45_">
+ <g filter="url(#Adobe_OpacityMaskFilter_11_)">
+
+ <linearGradient id="SVGID_46_" gradientUnits="userSpaceOnUse" x1="-302.0186" y1="-413.8936" x2="-182.0531" y2="-514.5565" gradientTransform="matrix(1 0 0 -1 370.0347 -414.4775)">
+ <stop offset="0" style="stop-color:#141414"/>
+ <stop offset="0.25" style="stop-color:#000000"/>
+ </linearGradient>
+ <path fill="url(#SVGID_46_)" d="M229.395,48.914l-99.83-44.127c-0.938-0.417-2.003-0.417-2.947,0L26.605,48.914
+ c-0.312,0.138-0.521,0.437-0.542,0.777c-0.021,0.339,0.147,0.662,0.439,0.837l99.707,59.771c0.578,0.348,1.231,0.52,1.883,0.52
+ c0.652,0,1.303-0.172,1.883-0.52l99.52-59.771c0.293-0.175,0.463-0.499,0.439-0.837
+ C229.912,49.351,229.705,49.051,229.395,48.914z M206.813,50.538l-77.256,44.957c-0.461,0.267-0.976,0.4-1.486,0.4
+ c-0.513,0-1.024-0.132-1.486-0.4L49.224,50.539c-0.243-0.141-0.387-0.405-0.373-0.686c0.013-0.279,0.182-0.53,0.437-0.647
+ l77.641-35.774c0.726-0.335,1.561-0.335,2.29,0l77.531,35.772c0.256,0.119,0.422,0.367,0.438,0.649
+ C207.199,50.133,207.057,50.397,206.813,50.538z"/>
+ </g>
+ </mask>
+ <path mask="url(#SVGID_45_)" fill="#2B388F" d="M229.395,48.914l-99.83-44.127c-0.938-0.417-2.003-0.417-2.947,0L26.605,48.914
+ c-0.312,0.138-0.521,0.437-0.542,0.777c-0.021,0.339,0.147,0.662,0.439,0.837l99.707,59.771c0.578,0.348,1.231,0.52,1.883,0.52
+ c0.652,0,1.303-0.172,1.883-0.52l99.52-59.771c0.293-0.175,0.463-0.499,0.439-0.837C229.912,49.351,229.705,49.051,229.395,48.914
+ z M206.813,50.538l-77.256,44.957c-0.461,0.267-0.976,0.4-1.486,0.4c-0.513,0-1.024-0.132-1.486-0.4L49.224,50.539
+ c-0.243-0.141-0.387-0.405-0.373-0.686c0.013-0.279,0.182-0.53,0.437-0.647l77.641-35.774c0.726-0.335,1.561-0.335,2.29,0
+ l77.531,35.772c0.256,0.119,0.422,0.367,0.438,0.649C207.199,50.133,207.057,50.397,206.813,50.538z"/>
+
+ <linearGradient id="SVGID_47_" gradientUnits="userSpaceOnUse" x1="-321.6987" y1="-603.7334" x2="-268.8858" y2="-512.2586" gradientTransform="matrix(1 0 0 -1 370.0347 -414.4775)">
+ <stop offset="0.0053" style="stop-color:#E6E1E1"/>
+ <stop offset="0.5" style="stop-color:#E3E0DC"/>
+ <stop offset="1" style="stop-color:#EEE9E1"/>
+ </linearGradient>
+ <path fill="url(#SVGID_47_)" d="M125.82,117.02c-0.005-1.267-0.672-2.453-1.752-3.109L24.881,53.748
+ c-0.294-0.178-0.658-0.178-0.951,0.001c-0.292,0.179-0.459,0.505-0.432,0.847l8.144,106.359c0.079,1.038,0.599,1.994,1.429,2.624
+ l91.455,69.657c0.161,0.123,0.357,0.188,0.552,0.188c0.138,0,0.278-0.031,0.405-0.095c0.311-0.155,0.508-0.474,0.507-0.819
+ L125.82,117.02z M113.735,205.9c-0.118,0.061-0.247,0.091-0.375,0.091c-0.174,0-0.347-0.054-0.492-0.161l-71.516-52.707
+ c-0.646-0.479-1.051-1.206-1.107-2.008L34.82,75.227c-0.023-0.311,0.133-0.608,0.4-0.768c0.266-0.159,0.601-0.156,0.865,0.008
+ l75.882,47.62c0.853,0.535,1.372,1.466,1.384,2.473l0.835,80.597C114.19,205.471,114.018,205.758,113.735,205.9z"/>
+ <defs>
+ <filter id="Adobe_OpacityMaskFilter_12_" filterUnits="userSpaceOnUse" x="23.495" y="53.614" width="102.495" height="179.81">
+ <feColorMatrix type="matrix" values="1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 1 0"/>
+ </filter>
+ </defs>
+ <mask maskUnits="userSpaceOnUse" x="23.495" y="53.614" width="102.495" height="179.81" id="SVGID_48_">
+ <g filter="url(#Adobe_OpacityMaskFilter_12_)">
+
+ <linearGradient id="SVGID_49_" gradientUnits="userSpaceOnUse" x1="-218.2163" y1="-602.4976" x2="-372.3692" y2="-513.4973" gradientTransform="matrix(1 0 0 -1 370.0347 -414.4775)">
+ <stop offset="0.2" style="stop-color:#000000"/>
+ <stop offset="0.9" style="stop-color:#FFFFFF"/>
+ </linearGradient>
+ <path fill="url(#SVGID_49_)" d="M125.82,117.02c-0.005-1.267-0.672-2.453-1.752-3.109L24.881,53.748
+ c-0.294-0.178-0.658-0.178-0.951,0.001c-0.292,0.179-0.459,0.505-0.432,0.847l8.144,106.359
+ c0.079,1.038,0.599,1.994,1.429,2.624l91.455,69.657c0.161,0.123,0.357,0.188,0.552,0.188c0.138,0,0.278-0.031,0.405-0.095
+ c0.311-0.155,0.508-0.474,0.507-0.819L125.82,117.02z M113.735,205.9c-0.118,0.061-0.247,0.091-0.375,0.091
+ c-0.174,0-0.347-0.054-0.492-0.161l-71.516-52.707c-0.646-0.479-1.051-1.206-1.107-2.008L34.82,75.227
+ c-0.023-0.311,0.133-0.608,0.4-0.768c0.266-0.159,0.601-0.156,0.865,0.008l75.882,47.62c0.853,0.535,1.372,1.466,1.384,2.473
+ l0.835,80.597C114.19,205.471,114.018,205.758,113.735,205.9z"/>
+ </g>
+ </mask>
+
+ <linearGradient id="SVGID_50_" gradientUnits="userSpaceOnUse" x1="-225.4541" y1="-631.8145" x2="-355.862" y2="-476.4005" gradientTransform="matrix(1 0 0 -1 370.0347 -414.4775)">
+ <stop offset="0" style="stop-color:#E6D2F0"/>
+ <stop offset="1" style="stop-color:#C3C8DC"/>
+ </linearGradient>
+ <path mask="url(#SVGID_48_)" fill="url(#SVGID_50_)" d="M125.82,117.02c-0.005-1.267-0.672-2.453-1.752-3.109L24.881,53.748
+ c-0.294-0.178-0.658-0.178-0.951,0.001c-0.292,0.179-0.459,0.505-0.432,0.847l8.144,106.359c0.079,1.038,0.599,1.994,1.429,2.624
+ l91.455,69.657c0.161,0.123,0.357,0.188,0.552,0.188c0.138,0,0.278-0.031,0.405-0.095c0.311-0.155,0.508-0.474,0.507-0.819
+ L125.82,117.02z M113.735,205.9c-0.118,0.061-0.247,0.091-0.375,0.091c-0.174,0-0.347-0.054-0.492-0.161l-71.516-52.707
+ c-0.646-0.479-1.051-1.206-1.107-2.008L34.82,75.227c-0.023-0.311,0.133-0.608,0.4-0.768c0.266-0.159,0.601-0.156,0.865,0.008
+ l75.882,47.62c0.853,0.535,1.372,1.466,1.384,2.473l0.835,80.597C114.19,205.471,114.018,205.758,113.735,205.9z"/>
+
+ <linearGradient id="SVGID_51_" gradientUnits="userSpaceOnUse" x1="-162.3706" y1="-603.7349" x2="-215.1844" y2="-512.2589" gradientTransform="matrix(1 0 0 -1 370.0347 -414.4775)">
+ <stop offset="0" style="stop-color:#7896BE"/>
+ <stop offset="1" style="stop-color:#4164A5"/>
+ </linearGradient>
+ <path fill="url(#SVGID_51_)" d="M232.068,53.75c-0.293-0.18-0.654-0.18-0.951-0.001l-99.186,60.162
+ c-1.08,0.655-1.748,1.843-1.752,3.109l-0.17,115.49c0,0.348,0.196,0.665,0.506,0.819c0.129,0.063,0.268,0.095,0.405,0.095
+ c0.195,0,0.392-0.063,0.554-0.188l91.457-69.657c0.828-0.629,1.348-1.585,1.428-2.623l8.145-106.359
+ C232.527,54.254,232.361,53.93,232.068,53.75z M215.791,151.115c-0.057,0.802-0.459,1.529-1.104,2.008l-71.52,52.707
+ c-0.145,0.107-0.318,0.161-0.491,0.161c-0.128,0-0.257-0.03-0.375-0.091c-0.28-0.145-0.454-0.433-0.45-0.744l0.834-80.597
+ c0.014-1.007,0.531-1.938,1.383-2.473l75.886-47.62c0.264-0.165,0.599-0.167,0.862-0.008c0.269,0.159,0.425,0.456,0.4,0.768
+ L215.791,151.115z"/>
+ <defs>
+ <filter id="Adobe_OpacityMaskFilter_13_" filterUnits="userSpaceOnUse" x="130.01" y="53.615" width="102.496" height="179.809">
+ <feColorMatrix type="matrix" values="1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 1 0"/>
+ </filter>
+ </defs>
+ <mask maskUnits="userSpaceOnUse" x="130.01" y="53.615" width="102.496" height="179.809" id="SVGID_52_">
+ <g filter="url(#Adobe_OpacityMaskFilter_13_)">
+
+ <linearGradient id="SVGID_53_" gradientUnits="userSpaceOnUse" x1="-148.5908" y1="-581.1997" x2="-238.9791" y2="-529.0141" gradientTransform="matrix(1 0 0 -1 370.0347 -414.4775)">
+ <stop offset="0.0053" style="stop-color:#808080"/>
+ <stop offset="0.6" style="stop-color:#000000"/>
+ </linearGradient>
+ <path fill="url(#SVGID_53_)" d="M232.068,53.75c-0.293-0.18-0.654-0.18-0.951-0.001l-99.186,60.162
+ c-1.08,0.655-1.748,1.843-1.752,3.109l-0.17,115.49c0,0.348,0.196,0.665,0.506,0.819c0.129,0.063,0.268,0.095,0.405,0.095
+ c0.195,0,0.392-0.063,0.554-0.188l91.457-69.657c0.828-0.629,1.348-1.585,1.428-2.623l8.145-106.359
+ C232.527,54.254,232.361,53.93,232.068,53.75z M215.791,151.115c-0.057,0.802-0.459,1.529-1.104,2.008l-71.52,52.707
+ c-0.145,0.107-0.318,0.161-0.491,0.161c-0.128,0-0.257-0.03-0.375-0.091c-0.28-0.145-0.454-0.433-0.45-0.744l0.834-80.597
+ c0.014-1.007,0.531-1.938,1.383-2.473l75.886-47.62c0.264-0.165,0.599-0.167,0.862-0.008c0.269,0.159,0.425,0.456,0.4,0.768
+ L215.791,151.115z"/>
+ </g>
+ </mask>
+ <path mask="url(#SVGID_52_)" fill="#78A0E6" d="M232.068,53.75c-0.293-0.18-0.654-0.18-0.951-0.001l-99.186,60.162
+ c-1.08,0.655-1.748,1.843-1.752,3.109l-0.17,115.49c0,0.348,0.196,0.665,0.506,0.819c0.129,0.063,0.268,0.095,0.405,0.095
+ c0.195,0,0.392-0.063,0.554-0.188l91.457-69.657c0.828-0.629,1.348-1.585,1.428-2.623l8.145-106.359
+ C232.527,54.254,232.361,53.93,232.068,53.75z M215.791,151.115c-0.057,0.802-0.459,1.529-1.104,2.008l-71.52,52.707
+ c-0.145,0.107-0.318,0.161-0.491,0.161c-0.128,0-0.257-0.03-0.375-0.091c-0.28-0.145-0.454-0.433-0.45-0.744l0.834-80.597
+ c0.014-1.007,0.531-1.938,1.383-2.473l75.886-47.62c0.264-0.165,0.599-0.167,0.862-0.008c0.269,0.159,0.425,0.456,0.4,0.768
+ L215.791,151.115z"/>
+ <defs>
+ <filter id="Adobe_OpacityMaskFilter_14_" filterUnits="userSpaceOnUse" x="130.01" y="53.615" width="102.496" height="179.809">
+ <feColorMatrix type="matrix" values="1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 1 0"/>
+ </filter>
+ </defs>
+ <mask maskUnits="userSpaceOnUse" x="130.01" y="53.615" width="102.496" height="179.809" id="SVGID_54_">
+ <g filter="url(#Adobe_OpacityMaskFilter_14_)">
+
+ <linearGradient id="SVGID_55_" gradientUnits="userSpaceOnUse" x1="-210.332" y1="-652.9912" x2="-176.5425" y2="-461.3605" gradientTransform="matrix(1 0 0 -1 370.0347 -414.4775)">
+ <stop offset="0.0053" style="stop-color:#808080"/>
+ <stop offset="0.4" style="stop-color:#000000"/>
+ </linearGradient>
+ <path fill="url(#SVGID_55_)" d="M232.068,53.75c-0.293-0.18-0.654-0.18-0.951-0.001l-99.186,60.162
+ c-1.08,0.655-1.748,1.843-1.752,3.109l-0.17,115.49c0,0.348,0.196,0.665,0.506,0.819c0.129,0.063,0.268,0.095,0.405,0.095
+ c0.195,0,0.392-0.063,0.554-0.188l91.457-69.657c0.828-0.629,1.348-1.585,1.428-2.623l8.145-106.359
+ C232.527,54.254,232.361,53.93,232.068,53.75z M215.791,151.115c-0.057,0.802-0.459,1.529-1.104,2.008l-71.52,52.707
+ c-0.145,0.107-0.318,0.161-0.491,0.161c-0.128,0-0.257-0.03-0.375-0.091c-0.28-0.145-0.454-0.433-0.45-0.744l0.834-80.597
+ c0.014-1.007,0.531-1.938,1.383-2.473l75.886-47.62c0.264-0.165,0.599-0.167,0.862-0.008c0.269,0.159,0.425,0.456,0.4,0.768
+ L215.791,151.115z"/>
+ </g>
+ </mask>
+ <path mask="url(#SVGID_54_)" fill="#7896BE" d="M232.068,53.75c-0.293-0.18-0.654-0.18-0.951-0.001l-99.186,60.162
+ c-1.08,0.655-1.748,1.843-1.752,3.109l-0.17,115.49c0,0.348,0.196,0.665,0.506,0.819c0.129,0.063,0.268,0.095,0.405,0.095
+ c0.195,0,0.392-0.063,0.554-0.188l91.457-69.657c0.828-0.629,1.348-1.585,1.428-2.623l8.145-106.359
+ C232.527,54.254,232.361,53.93,232.068,53.75z M215.791,151.115c-0.057,0.802-0.459,1.529-1.104,2.008l-71.52,52.707
+ c-0.145,0.107-0.318,0.161-0.491,0.161c-0.128,0-0.257-0.03-0.375-0.091c-0.28-0.145-0.454-0.433-0.45-0.744l0.834-80.597
+ c0.014-1.007,0.531-1.938,1.383-2.473l75.886-47.62c0.264-0.165,0.599-0.167,0.862-0.008c0.269,0.159,0.425,0.456,0.4,0.768
+ L215.791,151.115z"/>
+ <g id="text_3_">
+ <g>
+ <path fill="#FFFFFF" d="M49.103,122.684c-0.36-0.256-0.717-0.627-1.054-1.077c-0.337-0.45-0.654-0.981-0.936-1.557
+ c-0.281-0.577-0.526-1.199-0.719-1.834c-0.192-0.634-0.332-1.281-0.403-1.907c-0.07-0.625-0.064-1.169,0.006-1.617
+ c0.07-0.449,0.206-0.8,0.394-1.043c0.189-0.242,0.432-0.373,0.718-0.38c0.285-0.008,0.613,0.109,0.973,0.367l4.473,3.189
+ c0.36,0.256,0.718,0.628,1.056,1.08c0.337,0.452,0.657,0.984,0.939,1.562c0.282,0.579,0.528,1.203,0.721,1.84
+ c0.193,0.636,0.334,1.285,0.404,1.911c0.07,0.626,0.064,1.169-0.007,1.617c-0.071,0.447-0.208,0.797-0.398,1.037
+ c-0.19,0.24-0.435,0.369-0.721,0.375c-0.287,0.005-0.616-0.113-0.976-0.371L49.103,122.684 M52.382,114.425l-4.682-3.337
+ c-0.555-0.397-1.063-0.577-1.504-0.567c-0.441,0.012-0.817,0.216-1.109,0.591c-0.293,0.374-0.502,0.919-0.611,1.611
+ c-0.109,0.693-0.117,1.533-0.008,2.5c0.108,0.963,0.325,1.963,0.622,2.943c0.298,0.98,0.677,1.943,1.112,2.835
+ c0.435,0.891,0.926,1.712,1.447,2.409c0.521,0.697,1.073,1.271,1.628,1.667l4.68,3.342c0.557,0.397,1.065,0.581,1.506,0.57
+ c0.442-0.009,0.819-0.212,1.112-0.583c0.293-0.372,0.503-0.915,0.613-1.604c0.11-0.69,0.119-1.529,0.01-2.493
+ c-0.108-0.968-0.324-1.97-0.623-2.954c-0.298-0.983-0.678-1.949-1.115-2.844c-0.435-0.895-0.927-1.717-1.449-2.416
+ C53.492,115.396,52.938,114.821,52.382,114.425 M64.026,122.726l-6.789-4.84l1.575,13.998l1.548,1.105l-1.295-11.521
+ l5.131,3.659c0.18,0.129,0.36,0.314,0.528,0.54c0.169,0.225,0.328,0.491,0.469,0.778c0.141,0.289,0.262,0.6,0.359,0.917
+ c0.095,0.317,0.165,0.642,0.2,0.955c0.034,0.313,0.032,0.585-0.002,0.811c-0.036,0.226-0.103,0.402-0.197,0.522
+ c-0.095,0.123-0.216,0.188-0.359,0.192c-0.143,0.003-0.308-0.055-0.489-0.184l-4.374-3.13l5.418,10.305l2.253,1.607
+ l-3.628-6.758l0.713,0.51c0.376,0.269,0.72,0.393,1.02,0.386c0.299-0.006,0.555-0.142,0.753-0.394
+ c0.199-0.252,0.342-0.619,0.417-1.086c0.075-0.469,0.082-1.035,0.008-1.689c-0.073-0.653-0.219-1.329-0.42-1.994
+ c-0.203-0.666-0.459-1.32-0.755-1.925c-0.295-0.606-0.628-1.164-0.981-1.637C64.775,123.383,64.401,122.994,64.026,122.726
+ M87.361,139.361l-5.595-3.986c-0.558-0.398-1.068-0.581-1.511-0.57c-0.444,0.01-0.823,0.213-1.118,0.588
+ c-0.295,0.373-0.507,0.918-0.619,1.611c-0.111,0.691-0.122,1.536-0.013,2.503c0.109,0.966,0.325,1.968,0.624,2.95
+ c0.298,0.982,0.679,1.949,1.116,2.844c0.438,0.896,0.931,1.719,1.454,2.417c0.523,0.699,1.077,1.274,1.635,1.673l4.802,3.43
+ l0.645-1.813l-5.616-4.009c-0.362-0.258-0.721-0.63-1.059-1.083c-0.339-0.452-0.659-0.984-0.942-1.563
+ c-0.283-0.58-0.529-1.205-0.723-1.84c-0.193-0.639-0.333-1.285-0.403-1.912c-0.07-0.627-0.063-1.173,0.009-1.621
+ c0.072-0.447,0.209-0.802,0.4-1.041c0.192-0.242,0.438-0.373,0.725-0.379c0.288-0.008,0.618,0.112,0.98,0.371l4.571,3.26
+ L87.361,139.361 M89.758,141.07l-1.557-1.109l1.415,12.684c0.01,0.088,0.024,0.177,0.043,0.268
+ c0.018,0.092,0.041,0.184,0.068,0.273c0.028,0.092,0.06,0.186,0.097,0.278c0.037,0.093,0.078,0.187,0.124,0.279
+ c0.044,0.089,0.089,0.174,0.138,0.256c0.048,0.079,0.099,0.153,0.15,0.224c0.052,0.067,0.105,0.131,0.16,0.187
+ c0.054,0.057,0.108,0.103,0.164,0.146l7.112,5.076l0.64-1.816l-7.267-5.188L89.758,141.07 M106.35,152.898l-5.617-4.004
+ c-0.561-0.399-1.073-0.586-1.519-0.576c-0.447,0.011-0.826,0.214-1.123,0.589c-0.297,0.373-0.508,0.919-0.62,1.612
+ c-0.112,0.694-0.123,1.535-0.015,2.506c0.107,0.969,0.324,1.971,0.622,2.953c0.298,0.983,0.679,1.953,1.116,2.849
+ c0.438,0.896,0.932,1.719,1.457,2.421c0.525,0.701,1.081,1.279,1.642,1.679l4.817,3.44l0.643-1.818l-5.633-4.021
+ c-0.298-0.213-0.596-0.506-0.882-0.855c-0.285-0.354-0.561-0.762-0.815-1.215c-0.254-0.451-0.488-0.941-0.691-1.455
+ c-0.203-0.512-0.374-1.046-0.506-1.582l6.679,4.766l0.644-1.817l-7.597-5.419c0.025-0.42,0.098-0.776,0.212-1.063
+ c0.113-0.286,0.268-0.5,0.457-0.635c0.19-0.135,0.412-0.19,0.664-0.161c0.25,0.031,0.529,0.15,0.828,0.362l4.584,3.271
+ L106.35,152.898 M72.648,128.752c-0.1-0.07-0.194-0.119-0.282-0.145c-0.087-0.027-0.169-0.032-0.242-0.018
+ c-0.075,0.016-0.14,0.053-0.198,0.107c-0.058,0.057-0.107,0.132-0.146,0.227l-3.404,9.787l1.829,1.307l2.79-8.179l2.878,7.683
+ l-4.148-2.959l1.189,3.117l4.199,2.998l1.145,3.088l1.831,1.308l-6.404-16.808c-0.068-0.17-0.142-0.33-0.222-0.483
+ c-0.08-0.152-0.166-0.294-0.254-0.425s-0.181-0.248-0.275-0.352C72.838,128.906,72.743,128.82,72.648,128.752"/>
+ </g>
+ <path fill="#FFFFFF" d="M127.647,21.402L64.145,50.701l63.045,36.036l64.158-36.036L127.647,21.402z M127.253,81.504
+ L72.909,50.768l35.185-16.416l8.502,3.661L94.557,59.727l37.996-15.501l-13.538,15.697l24.135-10.007l-12.231,15.239
+ l12.886,7.128L127.253,81.504z M136.936,63.977l18.508-23.021l-23.217,9.484l12.947-15.172L110.25,49.59l12.425-12.426
+ l-10.397-4.773l15.369-7.194l55.001,25.571l-34.4,19.096L136.936,63.977z"/>
+ <g>
+ <path fill="#FFFFFF" d="M155.916,147.47l-1.908,1.384c-0.084,0.539-0.174,1.1-0.269,1.681c-0.099,0.577-0.198,1.18-0.306,1.803
+ c-0.105,0.622-0.222,1.267-0.338,1.929c-0.121,0.664-0.244,1.351-0.373,2.055c-0.134,0.705-0.263,1.388-0.388,2.045
+ c-0.123,0.658-0.246,1.295-0.364,1.908c-0.119,0.612-0.236,1.203-0.351,1.771c-0.111,0.565-0.224,1.112-0.33,1.634
+ c-0.06-0.505-0.114-1.013-0.17-1.523c-0.058-0.514-0.108-1.027-0.162-1.551c-0.053-0.52-0.104-1.045-0.151-1.574
+ c-0.052-0.529-0.099-1.063-0.144-1.604c-0.047-0.541-0.093-1.068-0.133-1.583c-0.039-0.515-0.074-1.015-0.109-1.502
+ c-0.03-0.489-0.062-0.963-0.088-1.424c-0.026-0.463-0.051-0.914-0.069-1.351l-2.175,1.576c0.046,0.646,0.097,1.284,0.146,1.92
+ c0.054,0.633,0.106,1.26,0.162,1.883c0.058,0.623,0.117,1.238,0.179,1.85c0.063,0.609,0.127,1.217,0.194,1.818
+ c0.064,0.599,0.138,1.205,0.209,1.816c0.068,0.612,0.146,1.229,0.228,1.854c0.078,0.625,0.16,1.255,0.246,1.888
+ c0.086,0.637,0.174,1.273,0.267,1.922l2.434-1.775c0.186-0.83,0.365-1.653,0.541-2.469c0.182-0.815,0.354-1.623,0.524-2.426
+ c0.169-0.8,0.337-1.593,0.499-2.375c0.161-0.783,0.319-1.563,0.475-2.33c0.154-0.771,0.309-1.537,0.457-2.308
+ c0.146-0.771,0.293-1.539,0.438-2.31c0.142-0.771,0.281-1.541,0.422-2.313C155.646,149.02,155.783,148.244,155.916,147.47"/>
+ <path fill="#FFFFFF" d="M159.123,149.271l-1.807,1.311l-0.031,0.525l-0.771,12.022l1.805-1.317L159.123,149.271 M158.57,144.455
+ c-0.094,0.064-0.178,0.135-0.256,0.208c-0.08,0.071-0.15,0.149-0.221,0.229c-0.063,0.08-0.125,0.164-0.181,0.25
+ c-0.056,0.088-0.103,0.18-0.146,0.272c-0.041,0.094-0.08,0.194-0.113,0.305c-0.032,0.11-0.063,0.229-0.09,0.354
+ s-0.049,0.259-0.063,0.4c-0.021,0.144-0.032,0.291-0.045,0.451c-0.009,0.162-0.015,0.311-0.015,0.442
+ c0,0.131,0.007,0.25,0.019,0.35c0.013,0.101,0.026,0.188,0.052,0.259c0.021,0.07,0.051,0.127,0.084,0.166
+ c0.032,0.039,0.071,0.065,0.12,0.078c0.05,0.012,0.104,0.012,0.166-0.002c0.062-0.015,0.134-0.042,0.209-0.082
+ c0.076-0.041,0.16-0.094,0.25-0.162c0.095-0.063,0.181-0.135,0.259-0.209c0.08-0.072,0.151-0.149,0.221-0.229
+ c0.066-0.081,0.129-0.164,0.182-0.25c0.055-0.086,0.104-0.178,0.145-0.27c0.041-0.096,0.08-0.197,0.113-0.31
+ c0.031-0.11,0.063-0.229,0.09-0.358c0.025-0.127,0.051-0.267,0.068-0.41c0.016-0.146,0.029-0.299,0.043-0.461
+ c0.01-0.164,0.014-0.311,0.014-0.44c0-0.132-0.008-0.248-0.018-0.349c-0.012-0.102-0.025-0.188-0.047-0.254
+ c-0.021-0.068-0.051-0.123-0.082-0.16c-0.031-0.039-0.072-0.064-0.121-0.076c-0.047-0.012-0.104-0.012-0.166,0.005
+ c-0.063,0.015-0.133,0.043-0.211,0.085C158.75,144.333,158.664,144.386,158.57,144.455"/>
+ <path fill="#FFFFFF" d="M165.184,144.634c-0.069,0.052-0.143,0.113-0.209,0.187c-0.067,0.07-0.139,0.154-0.203,0.248
+ c-0.069,0.095-0.137,0.197-0.203,0.313c-0.063,0.116-0.129,0.242-0.192,0.378c-0.063,0.137-0.135,0.289-0.211,0.463
+ c-0.074,0.174-0.153,0.366-0.24,0.577c-0.086,0.211-0.178,0.443-0.274,0.695c-0.099,0.251-0.199,0.521-0.31,0.813l-0.125-2.01
+ l-1.358,0.987l-0.808,12.532l1.813-1.32l0.516-8.008c0.078-0.208,0.151-0.404,0.223-0.588c0.072-0.184,0.141-0.354,0.203-0.514
+ c0.064-0.158,0.127-0.306,0.184-0.438c0.06-0.137,0.111-0.26,0.162-0.369c0.05-0.105,0.099-0.207,0.146-0.296
+ c0.051-0.089,0.096-0.168,0.145-0.239c0.046-0.068,0.091-0.131,0.136-0.182c0.043-0.053,0.088-0.094,0.131-0.123
+ c0.021-0.016,0.041-0.028,0.065-0.042c0.021-0.015,0.047-0.029,0.07-0.042c0.025-0.014,0.053-0.024,0.08-0.037
+ c0.025-0.014,0.057-0.023,0.086-0.036c0.031-0.008,0.063-0.019,0.092-0.022c0.027-0.012,0.06-0.021,0.092-0.027
+ c0.029-0.008,0.062-0.018,0.091-0.024c0.03-0.008,0.063-0.015,0.094-0.021l0.418-3.135c-0.034,0.011-0.067,0.021-0.101,0.029
+ c-0.03,0.012-0.063,0.021-0.09,0.031c-0.03,0.01-0.061,0.021-0.084,0.033c-0.026,0.011-0.053,0.021-0.076,0.033
+ c-0.022,0.012-0.049,0.021-0.069,0.033s-0.045,0.024-0.067,0.036c-0.021,0.015-0.041,0.026-0.063,0.04
+ C165.223,144.607,165.201,144.621,165.184,144.634"/>
+ <path fill="#FFFFFF" d="M169.717,137.949l-1.076,0.779c-0.07,0.399-0.145,0.791-0.221,1.172c-0.074,0.38-0.154,0.75-0.234,1.11
+ c-0.082,0.361-0.164,0.714-0.252,1.058c-0.084,0.342-0.176,0.676-0.268,0.998l-0.922,0.67l-0.143,2.169l0.91-0.662l-0.435,6.753
+ c-0.022,0.335-0.035,0.639-0.035,0.914c-0.002,0.273,0.006,0.52,0.024,0.736c0.019,0.215,0.05,0.401,0.084,0.563
+ c0.041,0.16,0.089,0.289,0.146,0.393c0.059,0.103,0.133,0.176,0.217,0.225c0.084,0.045,0.182,0.064,0.291,0.056
+ c0.109-0.007,0.23-0.043,0.365-0.104c0.139-0.063,0.283-0.152,0.445-0.271c0.074-0.053,0.15-0.112,0.227-0.177
+ c0.074-0.063,0.152-0.13,0.229-0.204c0.073-0.069,0.153-0.146,0.229-0.229c0.076-0.08,0.156-0.166,0.234-0.254
+ c0.08-0.088,0.154-0.179,0.23-0.271c0.073-0.09,0.146-0.183,0.217-0.274c0.07-0.093,0.139-0.188,0.203-0.281
+ c0.063-0.096,0.129-0.188,0.19-0.286l0.022-1.794c-0.043,0.044-0.086,0.087-0.125,0.125c-0.043,0.039-0.084,0.078-0.123,0.115
+ c-0.041,0.039-0.08,0.074-0.118,0.108c-0.037,0.036-0.078,0.067-0.115,0.101c-0.037,0.03-0.074,0.063-0.108,0.09
+ c-0.041,0.029-0.078,0.062-0.113,0.091c-0.037,0.026-0.074,0.057-0.111,0.087c-0.037,0.025-0.074,0.054-0.111,0.082
+ c-0.055,0.04-0.106,0.068-0.154,0.087c-0.047,0.021-0.092,0.024-0.131,0.021c-0.039-0.008-0.076-0.021-0.104-0.049
+ c-0.033-0.029-0.062-0.068-0.084-0.119s-0.043-0.106-0.062-0.173c-0.014-0.063-0.026-0.137-0.033-0.214
+ c-0.012-0.078-0.014-0.164-0.016-0.258c0-0.094,0.004-0.193,0.01-0.301l0.429-6.604l1.522-1.108l0.142-2.164l-1.523,1.104
+ L169.717,137.949"/>
+ <path fill="#FFFFFF" d="M178.785,134.994l-1.791,1.302l-0.615,9.468c-0.111,0.174-0.225,0.338-0.334,0.487
+ c-0.107,0.149-0.215,0.287-0.32,0.409c-0.104,0.125-0.209,0.234-0.309,0.333c-0.104,0.098-0.201,0.182-0.299,0.252
+ c-0.076,0.057-0.147,0.093-0.213,0.11c-0.063,0.018-0.121,0.014-0.174-0.008s-0.099-0.063-0.14-0.123
+ c-0.04-0.062-0.073-0.142-0.103-0.238c-0.026-0.104-0.049-0.235-0.063-0.402c-0.015-0.167-0.023-0.367-0.025-0.6
+ c-0.002-0.233,0.002-0.498,0.014-0.797s0.027-0.631,0.054-0.996l0.456-6.396l-1.797,1.306c-0.01,0.138-0.021,0.313-0.037,0.531
+ c-0.014,0.219-0.031,0.479-0.055,0.776c-0.021,0.3-0.047,0.64-0.076,1.02c-0.025,0.382-0.061,0.804-0.096,1.265
+ c-0.033,0.462-0.063,0.882-0.092,1.26c-0.025,0.379-0.053,0.716-0.073,1.013c-0.021,0.298-0.04,0.552-0.054,0.767
+ c-0.019,0.215-0.027,0.389-0.037,0.521c-0.031,0.512-0.053,0.979-0.063,1.402c-0.008,0.421-0.002,0.799,0.017,1.131
+ c0.016,0.332,0.045,0.62,0.088,0.863c0.043,0.242,0.1,0.438,0.166,0.595c0.065,0.153,0.146,0.272,0.238,0.356
+ c0.094,0.086,0.196,0.135,0.313,0.149c0.118,0.019,0.245-0.004,0.389-0.056c0.144-0.053,0.298-0.142,0.463-0.264
+ c0.091-0.063,0.181-0.14,0.271-0.224c0.091-0.085,0.185-0.183,0.278-0.285c0.094-0.104,0.188-0.222,0.287-0.348
+ c0.096-0.126,0.194-0.263,0.295-0.409c0.102-0.146,0.196-0.296,0.297-0.452c0.096-0.151,0.188-0.313,0.285-0.479
+ c0.092-0.162,0.186-0.33,0.276-0.504c0.093-0.172,0.185-0.352,0.272-0.533l0.104,1.271l1.389-1.014L178.785,134.994"/>
+ <path fill="#FFFFFF" d="M182.768,141.832c-0.063,0.05-0.127,0.087-0.188,0.115c-0.057,0.027-0.113,0.042-0.162,0.048
+ c-0.053,0.005-0.1,0-0.139-0.017c-0.043-0.019-0.078-0.047-0.113-0.086s-0.063-0.086-0.084-0.145
+ c-0.021-0.061-0.043-0.128-0.055-0.207c-0.014-0.08-0.021-0.168-0.023-0.27c-0.002-0.1,0-0.209,0.008-0.326
+ c0.01-0.143,0.025-0.28,0.051-0.418c0.021-0.137,0.049-0.27,0.086-0.399c0.035-0.131,0.078-0.26,0.125-0.386
+ c0.052-0.126,0.105-0.251,0.17-0.374c0.063-0.123,0.138-0.246,0.224-0.367c0.082-0.119,0.183-0.237,0.285-0.354
+ c0.106-0.115,0.226-0.23,0.354-0.344s0.268-0.224,0.416-0.334l0.521-0.382l-0.166,2.56c-0.059,0.104-0.113,0.203-0.17,0.3
+ c-0.053,0.095-0.109,0.186-0.162,0.271c-0.059,0.085-0.107,0.167-0.162,0.244c-0.052,0.077-0.104,0.146-0.154,0.215
+ c-0.053,0.067-0.104,0.133-0.158,0.191c-0.053,0.063-0.106,0.117-0.162,0.174c-0.055,0.054-0.108,0.104-0.166,0.154
+ C182.885,141.745,182.826,141.791,182.768,141.832 M184.02,130.961c-0.102,0.071-0.199,0.148-0.305,0.232
+ c-0.102,0.084-0.203,0.173-0.311,0.269c-0.105,0.097-0.213,0.196-0.322,0.306c-0.107,0.105-0.219,0.222-0.33,0.34
+ c-0.111,0.117-0.229,0.248-0.354,0.394s-0.254,0.301-0.39,0.472c-0.137,0.172-0.278,0.354-0.426,0.553
+ c-0.147,0.194-0.304,0.406-0.461,0.632l0.067,1.792c0.095-0.117,0.188-0.229,0.277-0.338c0.092-0.108,0.18-0.213,0.27-0.313
+ c0.089-0.1,0.173-0.196,0.257-0.288c0.084-0.091,0.161-0.179,0.241-0.261c0.08-0.083,0.158-0.164,0.24-0.242
+ c0.08-0.079,0.164-0.153,0.246-0.228c0.084-0.071,0.168-0.146,0.254-0.213c0.086-0.068,0.174-0.135,0.262-0.198
+ c0.099-0.072,0.191-0.132,0.281-0.181c0.084-0.047,0.168-0.082,0.246-0.104c0.076-0.022,0.148-0.033,0.215-0.031
+ c0.067,0.002,0.127,0.017,0.185,0.044c0.055,0.026,0.103,0.068,0.142,0.127c0.036,0.058,0.069,0.133,0.092,0.226
+ c0.023,0.092,0.035,0.198,0.041,0.323c0.008,0.123,0.006,0.266-0.006,0.422l-0.056,0.845l-0.483,0.353
+ c-0.295,0.215-0.57,0.439-0.832,0.676c-0.256,0.236-0.5,0.481-0.724,0.74c-0.224,0.258-0.43,0.524-0.618,0.805
+ c-0.187,0.277-0.355,0.565-0.513,0.867c-0.151,0.301-0.287,0.604-0.406,0.907c-0.12,0.306-0.223,0.611-0.312,0.919
+ c-0.086,0.309-0.153,0.615-0.209,0.926c-0.055,0.312-0.092,0.623-0.11,0.935c-0.039,0.601-0.028,1.093,0.031,1.479
+ c0.057,0.385,0.166,0.662,0.321,0.832c0.156,0.17,0.365,0.233,0.621,0.189c0.252-0.043,0.558-0.193,0.908-0.452
+ c0.098-0.067,0.189-0.147,0.287-0.235c0.094-0.088,0.188-0.187,0.283-0.293c0.098-0.106,0.189-0.225,0.284-0.349
+ c0.099-0.125,0.19-0.259,0.288-0.401c0.096-0.146,0.188-0.293,0.281-0.445c0.092-0.15,0.181-0.305,0.268-0.463
+ c0.086-0.157,0.17-0.318,0.252-0.481c0.082-0.164,0.16-0.33,0.236-0.498l0.104,1.269l1.311-0.957l0.566-8.646
+ c0.021-0.337,0.029-0.645,0.021-0.921c-0.002-0.276-0.021-0.524-0.053-0.742c-0.035-0.217-0.08-0.403-0.14-0.561
+ c-0.062-0.157-0.133-0.283-0.22-0.38c-0.086-0.099-0.19-0.161-0.309-0.195c-0.119-0.033-0.254-0.036-0.404-0.005
+ c-0.149,0.028-0.317,0.092-0.5,0.185C184.432,130.682,184.232,130.807,184.02,130.961"/>
+ <path fill="#FFFFFF" d="M191.021,121.232l-1.761,1.275l-0.987,15.075c-0.019,0.257-0.023,0.488-0.023,0.693
+ s0.011,0.387,0.025,0.543c0.02,0.156,0.045,0.287,0.078,0.393s0.074,0.188,0.127,0.246c0.051,0.057,0.108,0.093,0.18,0.113
+ c0.068,0.02,0.146,0.021,0.234,0.004c0.088-0.016,0.184-0.053,0.289-0.105c0.104-0.052,0.221-0.125,0.344-0.215
+ c0.104-0.077,0.212-0.162,0.32-0.256c0.106-0.094,0.219-0.196,0.33-0.307c0.112-0.11,0.227-0.23,0.342-0.358
+ c0.116-0.128,0.229-0.265,0.352-0.409l0.027-1.81l-0.426,0.332c-0.046,0.032-0.082,0.056-0.119,0.069s-0.07,0.018-0.099,0.012
+ c-0.028-0.004-0.053-0.019-0.073-0.041c-0.021-0.022-0.041-0.055-0.056-0.094c-0.015-0.041-0.022-0.099-0.03-0.168
+ c-0.007-0.07-0.013-0.156-0.015-0.259c-0.002-0.101,0-0.217,0.006-0.347c0.002-0.131,0.009-0.277,0.021-0.438L191.021,121.232"
+ />
+ <path fill="#FFFFFF" d="M195.492,132.34l-0.818,0.523l0.33-5.009l0.77-0.559c0.14-0.101,0.269-0.181,0.39-0.24
+ c0.121-0.06,0.229-0.098,0.332-0.114c0.101-0.017,0.19-0.013,0.271,0.012c0.084,0.024,0.152,0.072,0.215,0.139
+ c0.062,0.069,0.115,0.148,0.158,0.243c0.041,0.094,0.076,0.201,0.104,0.322c0.023,0.12,0.041,0.254,0.047,0.402
+ c0.006,0.149,0.006,0.31-0.006,0.485c-0.016,0.205-0.035,0.403-0.063,0.597c-0.029,0.192-0.063,0.379-0.107,0.563
+ c-0.045,0.185-0.094,0.36-0.153,0.534c-0.058,0.173-0.119,0.342-0.194,0.506c-0.07,0.164-0.151,0.318-0.242,0.469
+ c-0.088,0.148-0.187,0.289-0.293,0.422c-0.105,0.135-0.219,0.26-0.342,0.376C195.764,132.129,195.633,132.238,195.492,132.34
+ M195.164,125.433l0.277-4.249l0.852-0.618c0.098-0.069,0.186-0.122,0.27-0.158c0.082-0.036,0.158-0.056,0.228-0.06
+ c0.069-0.004,0.133,0.008,0.188,0.037c0.059,0.028,0.106,0.071,0.149,0.131c0.041,0.062,0.078,0.134,0.104,0.219
+ c0.029,0.085,0.056,0.18,0.068,0.287c0.018,0.107,0.027,0.226,0.029,0.355c0.004,0.129,0,0.27-0.012,0.423
+ c-0.01,0.155-0.025,0.309-0.051,0.461c-0.023,0.153-0.058,0.305-0.095,0.457c-0.038,0.15-0.084,0.301-0.137,0.45
+ c-0.054,0.149-0.115,0.298-0.183,0.445c-0.067,0.146-0.143,0.286-0.222,0.418c-0.081,0.132-0.167,0.257-0.261,0.374
+ c-0.094,0.116-0.191,0.225-0.299,0.326c-0.105,0.101-0.219,0.195-0.338,0.282L195.164,125.433 M196.744,117.89l-2.961,2.146
+ l-1.102,16.726l2.411-1.762c0.334-0.243,0.646-0.501,0.938-0.773c0.292-0.271,0.563-0.559,0.813-0.858
+ c0.25-0.302,0.479-0.617,0.688-0.948c0.206-0.331,0.395-0.676,0.561-1.037c0.164-0.357,0.313-0.72,0.443-1.086
+ c0.131-0.363,0.244-0.732,0.34-1.105c0.097-0.373,0.174-0.75,0.233-1.129c0.062-0.379,0.104-0.763,0.13-1.15
+ c0.014-0.209,0.018-0.409,0.01-0.598c-0.008-0.19-0.026-0.368-0.06-0.537c-0.026-0.168-0.067-0.326-0.119-0.474
+ c-0.053-0.147-0.114-0.285-0.188-0.412c-0.069-0.127-0.149-0.23-0.233-0.312c-0.086-0.082-0.176-0.14-0.275-0.176
+ c-0.1-0.036-0.201-0.05-0.313-0.04c-0.111,0.009-0.229,0.042-0.353,0.097c0.119-0.177,0.229-0.354,0.334-0.531
+ c0.104-0.177,0.201-0.353,0.291-0.53c0.09-0.177,0.172-0.354,0.248-0.533c0.075-0.177,0.146-0.356,0.207-0.535
+ c0.062-0.176,0.117-0.36,0.164-0.551c0.053-0.19,0.096-0.388,0.135-0.592c0.037-0.203,0.069-0.413,0.101-0.63
+ c0.024-0.217,0.045-0.438,0.063-0.668c0.021-0.313,0.023-0.598,0.016-0.856c-0.014-0.258-0.037-0.487-0.08-0.69
+ c-0.041-0.203-0.1-0.377-0.172-0.526c-0.074-0.148-0.164-0.269-0.268-0.362c-0.104-0.092-0.225-0.15-0.359-0.179
+ c-0.137-0.027-0.287-0.024-0.453,0.012s-0.348,0.104-0.545,0.205C197.188,117.591,196.973,117.724,196.744,117.89"/>
+ <path fill="#FFFFFF" d="M203.377,126.843c-0.111,0.082-0.217,0.139-0.311,0.17c-0.096,0.033-0.181,0.041-0.26,0.024
+ c-0.076-0.017-0.146-0.058-0.207-0.124c-0.063-0.066-0.115-0.157-0.16-0.273c-0.043-0.117-0.078-0.271-0.104-0.46
+ c-0.025-0.19-0.043-0.417-0.053-0.681c-0.008-0.263-0.008-0.564,0.002-0.902c0.01-0.337,0.027-0.712,0.057-1.124
+ c0.021-0.324,0.047-0.631,0.08-0.92c0.031-0.29,0.07-0.562,0.113-0.817c0.045-0.255,0.096-0.495,0.15-0.717
+ c0.057-0.221,0.115-0.425,0.184-0.614c0.064-0.188,0.137-0.358,0.213-0.517c0.076-0.159,0.158-0.305,0.246-0.438
+ c0.086-0.131,0.176-0.249,0.271-0.354c0.1-0.105,0.198-0.196,0.309-0.274c0.104-0.075,0.197-0.127,0.287-0.154
+ c0.092-0.026,0.172-0.028,0.246-0.005c0.072,0.023,0.139,0.071,0.197,0.145c0.057,0.072,0.106,0.17,0.149,0.293
+ c0.043,0.124,0.076,0.282,0.103,0.477c0.022,0.195,0.041,0.426,0.047,0.693c0.01,0.267,0.008,0.569-0.002,0.908
+ c-0.011,0.339-0.027,0.713-0.056,1.125c-0.022,0.323-0.049,0.629-0.081,0.916c-0.031,0.288-0.07,0.558-0.113,0.81
+ c-0.043,0.251-0.09,0.485-0.146,0.7c-0.054,0.217-0.113,0.414-0.179,0.593c-0.063,0.182-0.135,0.35-0.209,0.503
+ c-0.071,0.154-0.149,0.293-0.231,0.419c-0.082,0.125-0.17,0.239-0.26,0.338C203.57,126.685,203.477,126.771,203.377,126.843
+ M204.135,116.359c-0.248,0.18-0.484,0.385-0.709,0.616c-0.229,0.231-0.443,0.488-0.646,0.771
+ c-0.204,0.284-0.397,0.593-0.581,0.929c-0.185,0.335-0.357,0.698-0.521,1.087c-0.161,0.386-0.308,0.792-0.438,1.22
+ c-0.13,0.428-0.244,0.876-0.345,1.345c-0.1,0.469-0.186,0.958-0.25,1.469c-0.065,0.511-0.121,1.041-0.156,1.592
+ c-0.034,0.547-0.053,1.044-0.053,1.49c0,0.447,0.019,0.845,0.056,1.191c0.036,0.347,0.092,0.644,0.165,0.892
+ c0.074,0.247,0.164,0.444,0.275,0.594c0.109,0.146,0.238,0.254,0.383,0.318c0.146,0.066,0.308,0.09,0.484,0.074
+ c0.18-0.016,0.375-0.074,0.59-0.172c0.213-0.1,0.441-0.238,0.689-0.418c0.25-0.185,0.488-0.394,0.717-0.627
+ c0.229-0.234,0.447-0.498,0.652-0.785c0.207-0.287,0.402-0.6,0.59-0.94c0.186-0.34,0.359-0.707,0.523-1.101
+ c0.164-0.393,0.313-0.806,0.444-1.237c0.136-0.433,0.248-0.884,0.351-1.355c0.1-0.472,0.184-0.962,0.25-1.472
+ c0.067-0.511,0.123-1.041,0.158-1.59c0.034-0.516,0.049-0.986,0.047-1.412c-0.004-0.426-0.023-0.807-0.064-1.143
+ c-0.039-0.335-0.1-0.625-0.178-0.871c-0.076-0.246-0.174-0.445-0.287-0.6c-0.113-0.155-0.244-0.27-0.393-0.341
+ c-0.146-0.073-0.31-0.103-0.488-0.09c-0.18,0.01-0.375,0.063-0.584,0.157C204.604,116.047,204.377,116.183,204.135,116.359"/>
+ <path fill="#FFFFFF" d="M213.489,109.8l-1.823,1.325c-0.043,0.158-0.084,0.317-0.127,0.478c-0.043,0.16-0.084,0.321-0.129,0.482
+ c-0.043,0.162-0.088,0.324-0.133,0.487c-0.047,0.162-0.094,0.326-0.139,0.49c-0.046,0.163-0.091,0.322-0.138,0.478
+ c-0.045,0.156-0.09,0.31-0.135,0.458c-0.046,0.148-0.091,0.292-0.136,0.433s-0.086,0.277-0.129,0.409l-0.664-2.463l-2.035,1.479
+ l1.427,4.602l-2.521,7.942l1.875-1.37c0.053-0.206,0.105-0.413,0.16-0.619c0.053-0.207,0.107-0.414,0.162-0.621
+ c0.059-0.208,0.113-0.417,0.17-0.626c0.061-0.209,0.117-0.419,0.178-0.631c0.06-0.211,0.117-0.419,0.177-0.623
+ c0.059-0.204,0.116-0.404,0.176-0.601c0.06-0.196,0.116-0.389,0.174-0.579c0.058-0.189,0.115-0.375,0.172-0.558l0.857,3.234
+ l2.096-1.531l-1.696-5.256L213.489,109.8"/>
+ </g>
+ </g>
+ </g>
+</g>
+</svg>
diff --git a/src/VBox/ValidationKit/testmanager/htdocs/images/VirtualBox_64px.png b/src/VBox/ValidationKit/testmanager/htdocs/images/VirtualBox_64px.png Binary files differnew file mode 100644 index 00000000..d8849bdd --- /dev/null +++ b/src/VBox/ValidationKit/testmanager/htdocs/images/VirtualBox_64px.png diff --git a/src/VBox/ValidationKit/testmanager/htdocs/images/tmfavicon.ico b/src/VBox/ValidationKit/testmanager/htdocs/images/tmfavicon.ico Binary files differnew file mode 100644 index 00000000..72f7032d --- /dev/null +++ b/src/VBox/ValidationKit/testmanager/htdocs/images/tmfavicon.ico diff --git a/src/VBox/ValidationKit/testmanager/htdocs/js/Makefile.kup b/src/VBox/ValidationKit/testmanager/htdocs/js/Makefile.kup new file mode 100644 index 00000000..e69de29b --- /dev/null +++ b/src/VBox/ValidationKit/testmanager/htdocs/js/Makefile.kup diff --git a/src/VBox/ValidationKit/testmanager/htdocs/js/common.js b/src/VBox/ValidationKit/testmanager/htdocs/js/common.js new file mode 100644 index 00000000..52c4179c --- /dev/null +++ b/src/VBox/ValidationKit/testmanager/htdocs/js/common.js @@ -0,0 +1,1926 @@ +/* $Id: common.js $ */ +/** @file + * Common JavaScript functions + */ + +/* + * 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 + */ + + +/********************************************************************************************************************************* +* Global Variables * +*********************************************************************************************************************************/ +/** Same as WuiDispatcherBase.ksParamRedirectTo. */ +var g_ksParamRedirectTo = 'RedirectTo'; + +/** Days of the week in Date() style with Sunday first. */ +var g_kasDaysOfTheWeek = [ 'Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday' ]; + + +/** + * Detects the firefox browser. + */ +function isBrowserFirefox() +{ + return typeof InstallTrigger !== 'undefined'; +} + +/** + * Detects the google chrome browser. + * @note Might be confused with edge chromium + */ +function isBrowserChrome() +{ + var oChrome = window.chrome; + if (!oChrome) + return false; + return !!oChrome.runtime || !oChrome.webstore; +} + +/** + * Detects the chromium-based edge browser. + */ +function isBrowserEdgeChromium() +{ + if (!isBrowserChrome()) + return false; + return navigation.userAgent.indexOf('Edg') >= 0 +} + +/** + * Detects the chromium-based edge browser. + */ +function isBrowserInternetExplorer() +{ + /* documentMode is an IE only property. Values are 5,7,8,9,10 or 11 + according to google results. */ + if (typeof document.documentMode !== 'undefined') + { + if (document.documentMode) + return true; + } + /* IE only conditional compiling feature. Here, the 'true || ' part + will be included in the if when executing in IE: */ + if (/*@cc_on true || @*/false) + return true; + return false; +} + +/** + * Detects the safari browser (v3+). + */ +function isBrowserSafari() +{ + /* Check if window.HTMLElement is a function named 'HTMLElementConstructor()'? + Should work for older safari versions. */ + var sStr = window.HTMLElement.toString(); + if (/constructor/i.test(sStr)) + return true; + + /* Check the class name of window.safari.pushNotification. This works for current. */ + var oSafari = window['safari']; + if (oSafari) + { + if (typeof oSafari !== 'undefined') + { + var oPushNotify = oSafari.pushNotification; + if (oPushNotify) + { + sStr = oPushNotify.toString(); + if (/\[object Safari.*Notification\]/.test(sStr)) + return true; + } + } + } + return false; +} + +/** + * Checks if the given value is a decimal integer value. + * + * @returns true if it is, false if it's isn't. + * @param sValue The value to inspect. + */ +function isInteger(sValue) +{ + if (typeof sValue != 'undefined') + { + var intRegex = /^\d+$/; + if (intRegex.test(sValue)) + { + return true; + } + } + return false; +} + +/** + * Checks if @a oMemmber is present in aoArray. + * + * @returns true/false. + * @param aoArray The array to check. + * @param oMember The member to check for. + */ +function isMemberOfArray(aoArray, oMember) +{ + var i; + for (i = 0; i < aoArray.length; i++) + if (aoArray[i] == oMember) + return true; + return false; +} + +/** + * Parses a typical ISO timestamp, returing a Date object, reasonably + * forgiving, but will throw weird indexing/conversion errors if the input + * is malformed. + * + * @returns Date object. + * @param sTs The timestamp to parse. + * @sa parseIsoTimestamp() in utils.py. + */ +function parseIsoTimestamp(sTs) +{ + /* YYYY-MM-DD */ + var iYear = parseInt(sTs.substring(0, 4), 10); + console.assert(sTs.charAt(4) == '-'); + var iMonth = parseInt(sTs.substring(5, 7), 10); + console.assert(sTs.charAt(7) == '-'); + var iDay = parseInt(sTs.substring(8, 10), 10); + + /* Skip separator */ + var sTime = sTs.substring(10); + while ('Tt \t\n\r'.includes(sTime.charAt(0))) { + sTime = sTime.substring(1); + } + + /* HH:MM[:SS[.fraction] */ + var iHour = parseInt(sTime.substring(0, 2), 10); + console.assert(sTime.charAt(2) == ':'); + var iMin = parseInt(sTime.substring(3, 5), 10); + var iSec = 0; + var iMicroseconds = 0; + var offTime = 5; + if (sTime.charAt(5) == ':') + { + iSec = parseInt(sTime.substring(6, 8), 10); + + /* Fraction? */ + offTime = 8; + if (offTime < sTime.length && '.,'.includes(sTime.charAt(offTime))) + { + offTime += 1; + var cchFraction = 0; + while (offTime + cchFraction < sTime.length && '0123456789'.includes(sTime.charAt(offTime + cchFraction))) + cchFraction += 1; + if (cchFraction > 0) + { + iMicroseconds = parseInt(sTime.substring(offTime, offTime + cchFraction), 10); + offTime += cchFraction; + while (cchFraction < 6) + { + iMicroseconds *= 10; + cchFraction += 1; + } + while (cchFraction > 6) + { + iMicroseconds = iMicroseconds / 10; + cchFraction -= 1; + } + } + } + } + var iMilliseconds = (iMicroseconds + 499) / 1000; + + /* Naive? */ + var oDate = new Date(Date.UTC(iYear, iMonth - 1, iDay, iHour, iMin, iSec, iMilliseconds)); + if (offTime >= sTime.length) + return oDate; + + /* Zulu? */ + if (offTime >= sTime.length || 'Zz'.includes(sTime.charAt(offTime))) + return oDate; + + /* Some kind of offset afterwards. */ + var chSign = sTime.charAt(offTime); + if ('+-'.includes(chSign)) + { + offTime += 1; + var cMinTz = parseInt(sTime.substring(offTime, offTime + 2), 10) * 60; + offTime += 2; + if (offTime < sTime.length && sTime.charAt(offTime) == ':') + offTime += 1; + if (offTime + 2 <= sTime.length) + { + cMinTz += parseInt(sTime.substring(offTime, offTime + 2), 10); + offTime += 2; + } + console.assert(offTime == sTime.length); + if (chSign == '-') + cMinTz = -cMinTz; + + return new Date(oDate.getTime() - cMinTz * 60000); + } + console.assert(false); + return oDate; +} + +/** + * @param oDate Date object. + */ +function formatTimeHHMM(oDate, fNbsp) +{ + var sTime = oDate.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit'} ); + if (fNbsp === true) + sTime = sTime.replace(' ', '\u00a0'); + + /* Workaround for single digit hours in firefox with en_US (minutes works fine): */ + var iHours = oDate.getHours(); + if ((iHours % 12) < 10) + { + var ch1 = sTime.substr(0, 1); + var ch2 = sTime.substr(1, 1); + if ( ch1 == (iHours % 12).toString() + && !(ch2 >= '0' && ch2 <= '9')) + sTime = '0' + sTime; + } + return sTime; +} + +/** + * Escapes special characters to HTML-safe sequences, for element use. + * + * @returns Escaped string suitable for HTML. + * @param sText Plain text to escape. + */ +function escapeElem(sText) +{ + sText = sText.replace(/&/g, '&'); + sText = sText.replace(/>/g, '<'); + return sText.replace(/</g, '>'); +} + +/** + * Escapes special characters to HTML-safe sequences, for double quoted + * attribute use. + * + * @returns Escaped string suitable for HTML. + * @param sText Plain text to escape. + */ +function escapeAttr(sText) +{ + sText = sText.replace(/&/g, '&'); + sText = sText.replace(/</g, '<'); + sText = sText.replace(/>/g, '>'); + return sText.replace(/"/g, '"'); +} + +/** + * Removes the element with the specified ID. + */ +function removeHtmlNode(sContainerId) +{ + var oElement = document.getElementById(sContainerId); + if (oElement) + { + oElement.parentNode.removeChild(oElement); + } +} + +/** + * Sets the value of the element with id @a sInputId to the keys of aoItems + * (comma separated). + */ +function setElementValueToKeyList(sInputId, aoItems) +{ + var sKey; + var oElement = document.getElementById(sInputId); + oElement.value = ''; + + for (sKey in aoItems) + { + if (oElement.value.length > 0) + { + oElement.value += ','; + } + + oElement.value += sKey; + } +} + +/** + * Get the Window.devicePixelRatio in a safe way. + * + * @returns Floating point ratio. 1.0 means it's a 1:1 ratio. + */ +function getDevicePixelRatio() +{ + var fpRatio = 1.0; + if (window.devicePixelRatio) + { + fpRatio = window.devicePixelRatio; + if (fpRatio < 0.5 || fpRatio > 10.0) + fpRatio = 1.0; + } + return fpRatio; +} + +/** + * Tries to figure out the DPI of the device in the X direction. + * + * @returns DPI on success, null on failure. + */ +function getDeviceXDotsPerInch() +{ + if (window.deviceXDPI && window.deviceXDPI > 48 && window.deviceXDPI < 2048) + { + return window.deviceXDPI; + } + else if (window.devicePixelRatio && window.devicePixelRatio >= 0.5 && window.devicePixelRatio <= 10.0) + { + cDotsPerInch = Math.round(96 * window.devicePixelRatio); + } + else + { + cDotsPerInch = null; + } + return cDotsPerInch; +} + +/** + * Gets the width of the given element (downscaled). + * + * Useful when using the element to figure the size of a image + * or similar. + * + * @returns Number of pixels. null if oElement is bad. + * @param oElement The element (not ID). + */ +function getElementWidth(oElement) +{ + if (oElement && oElement.offsetWidth) + return oElement.offsetWidth; + return null; +} + +/** By element ID version of getElementWidth. */ +function getElementWidthById(sElementId) +{ + return getElementWidth(document.getElementById(sElementId)); +} + +/** + * Gets the real unscaled width of the given element. + * + * Useful when using the element to figure the size of a image + * or similar. + * + * @returns Number of screen pixels. null if oElement is bad. + * @param oElement The element (not ID). + */ +function getUnscaledElementWidth(oElement) +{ + if (oElement && oElement.offsetWidth) + return Math.round(oElement.offsetWidth * getDevicePixelRatio()); + return null; +} + +/** By element ID version of getUnscaledElementWidth. */ +function getUnscaledElementWidthById(sElementId) +{ + return getUnscaledElementWidth(document.getElementById(sElementId)); +} + +/** + * Gets the part of the URL needed for a RedirectTo parameter. + * + * @returns URL string. + */ +function getCurrentBrowerUrlPartForRedirectTo() +{ + var sWhere = window.location.href; + var offTmp; + var offPathKeep; + + /* Find the end of that URL 'path' component. */ + var offPathEnd = sWhere.indexOf('?'); + if (offPathEnd < 0) + offPathEnd = sWhere.indexOf('#'); + if (offPathEnd < 0) + offPathEnd = sWhere.length; + + /* Go backwards from the end of the and find the start of the last component. */ + offPathKeep = sWhere.lastIndexOf("/", offPathEnd); + offTmp = sWhere.lastIndexOf(":", offPathEnd); + if (offPathKeep < offTmp) + offPathKeep = offTmp; + offTmp = sWhere.lastIndexOf("\\", offPathEnd); + if (offPathKeep < offTmp) + offPathKeep = offTmp; + + return sWhere.substring(offPathKeep + 1); +} + +/** + * Adds the given sorting options to the URL and reloads. + * + * This will preserve previous sorting columns except for those + * given in @a aiColumns. + * + * @param sParam Sorting parameter. + * @param aiColumns Array of sorting columns. + */ +function ahrefActionSortByColumns(sParam, aiColumns) +{ + var sWhere = window.location.href; + + var offHash = sWhere.indexOf('#'); + if (offHash < 0) + offHash = sWhere.length; + + var offQm = sWhere.indexOf('?'); + if (offQm > offHash) + offQm = -1; + + var sNew = ''; + if (offQm > 0) + sNew = sWhere.substring(0, offQm); + + sNew += '?' + sParam + '=' + aiColumns[0]; + var i; + for (i = 1; i < aiColumns.length; i++) + sNew += '&' + sParam + '=' + aiColumns[i]; + + if (offQm >= 0 && offQm + 1 < offHash) + { + var sArgs = '&' + sWhere.substring(offQm + 1, offHash); + var off = 0; + while (off < sArgs.length) + { + var offMatch = sArgs.indexOf('&' + sParam + '=', off); + if (offMatch >= 0) + { + if (off < offMatch) + sNew += sArgs.substring(off, offMatch); + + var offValue = offMatch + 1 + sParam.length + 1; + offEnd = sArgs.indexOf('&', offValue); + if (offEnd < offValue) + offEnd = sArgs.length; + + var iColumn = parseInt(sArgs.substring(offValue, offEnd)); + if (!isMemberOfArray(aiColumns, iColumn) && !isMemberOfArray(aiColumns, -iColumn)) + sNew += sArgs.substring(offMatch, offEnd); + + off = offEnd; + } + else + { + sNew += sArgs.substring(off); + break; + } + } + } + + if (offHash < sWhere.length) + sNew = sWhere.substr(offHash); + + window.location.href = sNew; +} + +/** + * Sets the value of an input field element (give by ID). + * + * @returns Returns success indicator (true/false). + * @param sFieldId The field ID (required for updating). + * @param sValue The field value. + */ +function setInputFieldValue(sFieldId, sValue) +{ + var oInputElement = document.getElementById(sFieldId); + if (oInputElement) + { + oInputElement.value = sValue; + return true; + } + return false; +} + +/** + * Adds a hidden input field to a form. + * + * @returns The new input field element. + * @param oFormElement The form to append it to. + * @param sName The field name. + * @param sValue The field value. + * @param sFieldId The field ID (optional). + */ +function addHiddenInputFieldToForm(oFormElement, sName, sValue, sFieldId) +{ + var oNew = document.createElement('input'); + oNew.type = 'hidden'; + oNew.name = sName; + oNew.value = sValue; + if (sFieldId) + oNew.id = sFieldId; + oFormElement.appendChild(oNew); + return oNew; +} + +/** By element ID version of addHiddenInputFieldToForm. */ +function addHiddenInputFieldToFormById(sFormId, sName, sValue, sFieldId) +{ + return addHiddenInputFieldToForm(document.getElementById(sFormId), sName, sValue, sFieldId); +} + +/** + * Adds or updates a hidden input field to/on a form. + * + * @returns The new input field element. + * @param sFormId The ID of the form to amend. + * @param sName The field name. + * @param sValue The field value. + * @param sFieldId The field ID (required for updating). + */ +function addUpdateHiddenInputFieldToFormById(sFormId, sName, sValue, sFieldId) +{ + var oInputElement = null; + if (sFieldId) + { + oInputElement = document.getElementById(sFieldId); + } + if (oInputElement) + { + oInputElement.name = sName; + oInputElement.value = sValue; + } + else + { + oInputElement = addHiddenInputFieldToFormById(sFormId, sName, sValue, sFieldId); + } + return oInputElement; +} + +/** + * Adds a width and a dpi input to the given form element if possible to + * determine the values. + * + * This is normally employed in an onlick hook, but then you must specify IDs or + * the browser may end up adding it several times. + * + * @param sFormId The ID of the form to amend. + * @param sWidthSrcId The ID of the element to calculate the width + * value from. + * @param sWidthName The name of the width value. + * @param sDpiName The name of the dpi value. + */ +function addDynamicGraphInputs(sFormId, sWidthSrcId, sWidthName, sDpiName) +{ + var cx = getUnscaledElementWidthById(sWidthSrcId); + var cDotsPerInch = getDeviceXDotsPerInch(); + + if (cx) + { + addUpdateHiddenInputFieldToFormById(sFormId, sWidthName, cx, sFormId + '-' + sWidthName + '-id'); + } + + if (cDotsPerInch) + { + addUpdateHiddenInputFieldToFormById(sFormId, sDpiName, cDotsPerInch, sFormId + '-' + sDpiName + '-id'); + } + +} + +/** + * Adds the RedirecTo field with the current URL to the form. + * + * This is a 'onsubmit' action. + * + * @returns Returns success indicator (true/false). + * @param oForm The form being submitted. + */ +function addRedirectToInputFieldWithCurrentUrl(oForm) +{ + /* Constant used here is duplicated in WuiDispatcherBase.ksParamRedirectTo */ + return addHiddenInputFieldToForm(oForm, 'RedirectTo', getCurrentBrowerUrlPartForRedirectTo(), null); +} + +/** + * Adds the RedirecTo parameter to the href of the given anchor. + * + * This is a 'onclick' action. + * + * @returns Returns success indicator (true/false). + * @param oAnchor The anchor element being clicked on. + */ +function addRedirectToAnchorHref(oAnchor) +{ + var sRedirectToParam = g_ksParamRedirectTo + '=' + encodeURIComponent(getCurrentBrowerUrlPartForRedirectTo()); + var sHref = oAnchor.href; + if (sHref.indexOf(sRedirectToParam) < 0) + { + var sHash; + var offHash = sHref.indexOf('#'); + if (offHash >= 0) + sHash = sHref.substring(offHash); + else + { + sHash = ''; + offHash = sHref.length; + } + sHref = sHref.substring(0, offHash) + if (sHref.indexOf('?') >= 0) + sHref += '&'; + else + sHref += '?'; + sHref += sRedirectToParam; + sHref += sHash; + oAnchor.href = sHref; + } + return true; +} + + + +/** + * Clears one input element. + * + * @param oInput The input to clear. + */ +function resetInput(oInput) +{ + switch (oInput.type) + { + case 'checkbox': + case 'radio': + oInput.checked = false; + break; + + case 'text': + oInput.value = 0; + break; + } +} + + +/** + * Clears a form. + * + * @param sIdForm The ID of the form + */ +function clearForm(sIdForm) +{ + var oForm = document.getElementById(sIdForm); + if (oForm) + { + var aoInputs = oForm.getElementsByTagName('INPUT'); + var i; + for (i = 0; i < aoInputs.length; i++) + resetInput(aoInputs[i]) + + /* HTML5 allows inputs outside <form>, so scan the document. */ + aoInputs = document.getElementsByTagName('INPUT'); + for (i = 0; i < aoInputs.length; i++) + if (aoInputs.hasOwnProperty("form")) + if (aoInputs.form == sIdForm) + resetInput(aoInputs[i]) + } + + return true; +} + + +/** + * Used by the time navigation to update the hidden efficient date field when + * either of the date or time fields changes. + * + * @param oForm The form. + */ +function timeNavigationUpdateHiddenEffDate(oForm, sIdSuffix) +{ + var sDate = document.getElementById('EffDate' + sIdSuffix).value; + var sTime = document.getElementById('EffTime' + sIdSuffix).value; + + var oField = document.getElementById('EffDateTime' + sIdSuffix); + oField.value = sDate + 'T' + sTime + '.00Z'; +} + + +/** @name Collapsible / Expandable items + * @{ + */ + + +/** + * Toggles the collapsible / expandable state of a parent DD and DT uncle. + * + * @returns true + * @param oAnchor The anchor object. + */ +function toggleCollapsibleDtDd(oAnchor) +{ + var oParent = oAnchor.parentElement; + var sClass = oParent.className; + + /* Find the DD sibling tag */ + var oDdElement = oParent.nextSibling; + while (oDdElement != null && oDdElement.tagName != 'DD') + oDdElement = oDdElement.nextSibling; + + /* Determin the new class and arrow char. */ + var sNewClass; + var sNewChar; + if ( sClass.substr(-11) == 'collapsible') + { + sNewClass = sClass.substr(0, sClass.length - 11) + 'expandable'; + sNewChar = '\u25B6'; /* black right-pointing triangle */ + } + else if (sClass.substr(-10) == 'expandable') + { + sNewClass = sClass.substr(0, sClass.length - 10) + 'collapsible'; + sNewChar = '\u25BC'; /* black down-pointing triangle */ + } + else + { + console.log('toggleCollapsibleParent: Invalid class: ' + sClass); + return true; + } + + /* Update the parent (DT) class and anchor text. */ + oParent.className = sNewClass; + oAnchor.firstChild.textContent = sNewChar + oAnchor.firstChild.textContent.substr(1); + + /* Update the uncle (DD) class. */ + if (oDdElement) + oDdElement.className = sNewClass; + return true; +} + +/** + * Shows/hides a sub-category UL according to checkbox status. + * + * The checkbox is expected to be within a label element or something. + * + * @returns true + * @param oInput The input checkbox. + */ +function toggleCollapsibleCheckbox(oInput) +{ + var oParent = oInput.parentElement; + + /* Find the UL sibling element. */ + var oUlElement = oParent.nextSibling; + while (oUlElement != null && oUlElement.tagName != 'UL') + oUlElement = oUlElement.nextSibling; + + /* Change the visibility. */ + if (oInput.checked) + oUlElement.className = oUlElement.className.replace('expandable', 'collapsible'); + else + { + oUlElement.className = oUlElement.className.replace('collapsible', 'expandable'); + + /* Make sure all sub-checkboxes are now unchecked. */ + var aoSubInputs = oUlElement.getElementsByTagName('input'); + var i; + for (i = 0; i < aoSubInputs.length; i++) + aoSubInputs[i].checked = false; + } + return true; +} + +/** + * Toggles the sidebar size so filters can more easily manipulated. + */ +function toggleSidebarSize() +{ + var sLinkText; + if (document.body.className != 'tm-wide-side-menu') + { + document.body.className = 'tm-wide-side-menu'; + sLinkText = '\u00ab\u00ab'; + } + else + { + document.body.className = ''; + sLinkText = '\u00bb\u00bb'; + } + + var aoToggleLink = document.getElementsByClassName('tm-sidebar-size-link'); + var i; + for (i = 0; i < aoToggleLink.length; i++) + if ( aoToggleLink[i].textContent.indexOf('\u00bb') >= 0 + || aoToggleLink[i].textContent.indexOf('\u00ab') >= 0) + aoToggleLink[i].textContent = sLinkText; +} + +/** @} */ + + +/** @name Custom Tooltips + * @{ + */ + +/** Enables non-iframe tooltip code. */ +var g_fNewTooltips = true; + +/** Where we keep tooltip elements when not displayed. */ +var g_dTooltips = {}; +var g_oCurrentTooltip = null; +var g_idTooltipShowTimer = null; +var g_idTooltipHideTimer = null; +var g_cTooltipSvnRevisions = 12; + +/** + * Cancel showing/replacing/repositing a tooltip. + */ +function tooltipResetShowTimer() +{ + if (g_idTooltipShowTimer) + { + clearTimeout(g_idTooltipShowTimer); + g_idTooltipShowTimer = null; + } +} + +/** + * Cancel hiding of the current tooltip. + */ +function tooltipResetHideTimer() +{ + if (g_idTooltipHideTimer) + { + clearTimeout(g_idTooltipHideTimer); + g_idTooltipHideTimer = null; + } +} + +/** + * Really hide the tooltip. + */ +function tooltipReallyHide() +{ + if (g_oCurrentTooltip) + { + //console.log('tooltipReallyHide: ' + g_oCurrentTooltip); + g_oCurrentTooltip.oElm.style.display = 'none'; + g_oCurrentTooltip = null; + } +} + +/** + * Schedule the tooltip for hiding. + */ +function tooltipHide() +{ + function tooltipDelayedHide() + { + tooltipResetHideTimer(); + tooltipReallyHide(); + } + + /* + * Cancel any pending show and schedule hiding if necessary. + */ + tooltipResetShowTimer(); + if (g_oCurrentTooltip && !g_idTooltipHideTimer) + { + g_idTooltipHideTimer = setTimeout(tooltipDelayedHide, 700); + } + + return true; +} + +/** + * Function that is repositions the tooltip when it's shown. + * + * Used directly, via onload, and hackish timers to catch all browsers and + * whatnot. + * + * Will set several tooltip member variables related to position and space. + */ +function tooltipRepositionOnLoad() +{ + //console.log('tooltipRepositionOnLoad'); + if (g_oCurrentTooltip) + { + var oRelToRect = g_oCurrentTooltip.oRelToRect; + var cxNeeded = g_oCurrentTooltip.oElm.offsetWidth + 8; + var cyNeeded = g_oCurrentTooltip.oElm.offsetHeight + 8; + + var cyWindow = window.innerHeight; + var yScroll = window.pageYOffset || document.documentElement.scrollTop; + var yScrollBottom = yScroll + cyWindow; + var cxWindow = window.innerWidth; + var xScroll = window.pageXOffset || document.documentElement.scrollLeft; + var xScrollRight = xScroll + cxWindow; + + var cyAbove = Math.max(oRelToRect.top, 0); + var cyBelow = Math.max(cyWindow - oRelToRect.bottom, 0); + var cxLeft = Math.max(oRelToRect.left, 0); + var cxRight = Math.max(cxWindow - oRelToRect.right, 0); + + var xPos; + var yPos; + + //console.log('tooltipRepositionOnLoad: rect: x,y=' + oRelToRect.x + ',' + oRelToRect.y + // + ' cx,cy=' + oRelToRect.width + ',' + oRelToRect.height + ' top=' + oRelToRect.top + // + ' bottom=' + oRelToRect.bottom + ' left=' + oRelToRect.left + ' right=' + oRelToRect.right); + //console.log('tooltipRepositionOnLoad: yScroll=' + yScroll + ' yScrollBottom=' + yScrollBottom); + //console.log('tooltipRepositionOnLoad: cyAbove=' + cyAbove + ' cyBelow=' + cyBelow + ' cyNeeded=' + cyNeeded); + //console.log('tooltipRepositionOnLoad: xScroll=' + xScroll + ' xScrollRight=' + xScrollRight); + //console.log('tooltipRepositionOnLoad: cxLeft=' + cxLeft + ' cxRight=' + cxRight + ' cxNeeded=' + cxNeeded); + + /* + * Decide where to put the thing. + */ + if (cyNeeded < cyBelow) + { + yPos = yScroll + oRelToRect.top; + g_oCurrentTooltip.cyMax = cyBelow; + //console.log('tooltipRepositionOnLoad: #1'); + } + else if (cyBelow >= cyAbove) + { + yPos = yScrollBottom - cyNeeded; + g_oCurrentTooltip.cyMax = yScrollBottom - yPos; + //console.log('tooltipRepositionOnLoad: #2'); + } + else + { + yPos = yScroll + oRelToRect.bottom - cyNeeded; + g_oCurrentTooltip.cyMax = yScrollBottom - yPos; + //console.log('tooltipRepositionOnLoad: #3'); + } + if (yPos < yScroll) + { + yPos = yScroll; + g_oCurrentTooltip.cyMax = yScrollBottom - yPos; + //console.log('tooltipRepositionOnLoad: #4'); + } + g_oCurrentTooltip.yPos = yPos; + g_oCurrentTooltip.yScroll = yScroll; + g_oCurrentTooltip.cyMaxUp = yPos - yScroll; + //console.log('tooltipRepositionOnLoad: yPos=' + yPos + ' yScroll=' + yScroll + ' cyMaxUp=' + g_oCurrentTooltip.cyMaxUp); + + if (cxNeeded < cxRight) + { + xPos = xScroll + oRelToRect.right; + g_oCurrentTooltip.cxMax = cxRight; + //console.log('tooltipRepositionOnLoad: #5'); + } + else + { + xPos = xScroll + oRelToRect.left - cxNeeded; + if (xPos < xScroll) + xPos = xScroll; + g_oCurrentTooltip.cxMax = cxNeeded; + //console.log('tooltipRepositionOnLoad: #6'); + } + g_oCurrentTooltip.xPos = xPos; + g_oCurrentTooltip.xScroll = xScroll; + //console.log('tooltipRepositionOnLoad: xPos=' + xPos + ' xScroll=' + xScroll); + + g_oCurrentTooltip.oElm.style.top = yPos + 'px'; + g_oCurrentTooltip.oElm.style.left = xPos + 'px'; + } + return true; +} + + +/** + * Really show the tooltip. + * + * @param oTooltip The tooltip object. + * @param oRelTo What to put the tooltip adjecent to. + */ +function tooltipReallyShow(oTooltip, oRelTo) +{ + var oRect; + + tooltipResetShowTimer(); + tooltipResetHideTimer(); + + if (g_oCurrentTooltip == oTooltip) + { + //console.log('moving tooltip'); + } + else if (g_oCurrentTooltip) + { + //console.log('removing current tooltip and showing new'); + tooltipReallyHide(); + } + else + { + //console.log('showing tooltip'); + } + + //oTooltip.oElm.setAttribute('style', 'display: block; position: absolute;'); + oTooltip.oElm.style.position = 'absolute'; + oTooltip.oElm.style.display = 'block'; + oRect = oRelTo.getBoundingClientRect(); + oTooltip.oRelToRect = oRect; + + g_oCurrentTooltip = oTooltip; + + /* + * Do repositioning (again). + */ + tooltipRepositionOnLoad(); +} + +/** + * Tooltip onmouseenter handler . + */ +function tooltipElementOnMouseEnter() +{ + /*console.log('tooltipElementOnMouseEnter: arguments.length='+arguments.length+' [0]='+arguments[0]); + console.log('ENT: currentTarget='+arguments[0].currentTarget+' id='+arguments[0].currentTarget.id+' class='+arguments[0].currentTarget.className); */ + tooltipResetShowTimer(); + tooltipResetHideTimer(); + return true; +} + +/** + * Tooltip onmouseout handler. + * + * @remarks We only use this and onmouseenter for one tooltip element (iframe + * for svn, because chrome is sending onmouseout events after + * onmouseneter for the next element, which would confuse this simple + * code. + */ +function tooltipElementOnMouseOut() +{ + var oEvt = arguments[0]; + /*console.log('tooltipElementOnMouseOut: arguments.length='+arguments.length+' [0]='+oEvt); + console.log('OUT: currentTarget='+oEvt.currentTarget+' id='+oEvt.currentTarget.id+' class='+oEvt.currentTarget.className);*/ + + /* Ignore the event if leaving to a child element. */ + var oElm = oEvt.toElement || oEvt.relatedTarget; + if (oElm != this && oElm) + { + for (;;) + { + oElm = oElm.parentNode; + if (!oElm || oElm == window) + break; + if (oElm == this) + { + console.log('OUT: was to child! - ignore'); + return false; + } + } + } + + tooltipHide(); + return true; +} + +/** + * iframe.onload hook that repositions and resizes the tooltip. + * + * This is a little hacky and we're calling it one or three times too many to + * work around various browser differences too. + */ +function svnHistoryTooltipOldOnLoad() +{ + //console.log('svnHistoryTooltipOldOnLoad'); + + /* + * Resize the tooltip to better fit the content. + */ + tooltipRepositionOnLoad(); /* Sets cxMax and cyMax. */ + if (g_oCurrentTooltip && g_oCurrentTooltip.oIFrame.contentWindow) + { + var oIFrameElement = g_oCurrentTooltip.oIFrame; + var cxSpace = Math.max(oIFrameElement.offsetLeft * 2, 0); /* simplified */ + var cySpace = Math.max(oIFrameElement.offsetTop * 2, 0); /* simplified */ + var cxNeeded = oIFrameElement.contentWindow.document.body.scrollWidth + cxSpace; + var cyNeeded = oIFrameElement.contentWindow.document.body.scrollHeight + cySpace; + var cx = Math.min(cxNeeded, g_oCurrentTooltip.cxMax); + var cy; + + g_oCurrentTooltip.oElm.width = cx + 'px'; + oIFrameElement.width = (cx - cxSpace) + 'px'; + if (cx >= cxNeeded) + { + //console.log('svnHistoryTooltipOldOnLoad: overflowX -> hidden'); + oIFrameElement.style.overflowX = 'hidden'; + } + else + { + oIFrameElement.style.overflowX = 'scroll'; + } + + cy = Math.min(cyNeeded, g_oCurrentTooltip.cyMax); + if (cyNeeded > g_oCurrentTooltip.cyMax && g_oCurrentTooltip.cyMaxUp > 0) + { + var cyMove = Math.min(cyNeeded - g_oCurrentTooltip.cyMax, g_oCurrentTooltip.cyMaxUp); + g_oCurrentTooltip.cyMax += cyMove; + g_oCurrentTooltip.yPos -= cyMove; + g_oCurrentTooltip.oElm.style.top = g_oCurrentTooltip.yPos + 'px'; + cy = Math.min(cyNeeded, g_oCurrentTooltip.cyMax); + } + + g_oCurrentTooltip.oElm.height = cy + 'px'; + oIFrameElement.height = (cy - cySpace) + 'px'; + if (cy >= cyNeeded) + { + //console.log('svnHistoryTooltipOldOnLoad: overflowY -> hidden'); + oIFrameElement.style.overflowY = 'hidden'; + } + else + { + oIFrameElement.style.overflowY = 'scroll'; + } + + //console.log('cyNeeded='+cyNeeded+' cyMax='+g_oCurrentTooltip.cyMax+' cySpace='+cySpace+' cy='+cy); + //console.log('oIFrameElement.offsetTop='+oIFrameElement.offsetTop); + //console.log('svnHistoryTooltipOldOnLoad: cx='+cx+'cxMax='+g_oCurrentTooltip.cxMax+' cxNeeded='+cxNeeded+' cy='+cy+' cyMax='+g_oCurrentTooltip.cyMax); + + tooltipRepositionOnLoad(); + } + return true; +} + +/** + * iframe.onload hook that repositions and resizes the tooltip. + * + * This is a little hacky and we're calling it one or three times too many to + * work around various browser differences too. + */ +function svnHistoryTooltipNewOnLoad() +{ + //console.log('svnHistoryTooltipNewOnLoad'); + + /* + * Resize the tooltip to better fit the content. + */ + tooltipRepositionOnLoad(); /* Sets cxMax and cyMax. */ + oTooltip = g_oCurrentTooltip; + if (oTooltip) + { + var oElmInner = oTooltip.oInnerElm; + var cxSpace = Math.max(oElmInner.offsetLeft * 2, 0); /* simplified */ + var cySpace = Math.max(oElmInner.offsetTop * 2, 0); /* simplified */ + var cxNeeded = oElmInner.scrollWidth + cxSpace; + var cyNeeded = oElmInner.scrollHeight + cySpace; + var cx = Math.min(cxNeeded, oTooltip.cxMax); + + oTooltip.oElm.width = cx + 'px'; + oElmInner.width = (cx - cxSpace) + 'px'; + if (cx >= cxNeeded) + { + //console.log('svnHistoryTooltipNewOnLoad: overflowX -> hidden'); + oElmInner.style.overflowX = 'hidden'; + } + else + { + oElmInner.style.overflowX = 'scroll'; + } + + var cy = Math.min(cyNeeded, oTooltip.cyMax); + if (cyNeeded > oTooltip.cyMax && oTooltip.cyMaxUp > 0) + { + var cyMove = Math.min(cyNeeded - oTooltip.cyMax, oTooltip.cyMaxUp); + oTooltip.cyMax += cyMove; + oTooltip.yPos -= cyMove; + oTooltip.oElm.style.top = oTooltip.yPos + 'px'; + cy = Math.min(cyNeeded, oTooltip.cyMax); + } + + oTooltip.oElm.height = cy + 'px'; + oElmInner.height = (cy - cySpace) + 'px'; + if (cy >= cyNeeded) + { + //console.log('svnHistoryTooltipNewOnLoad: overflowY -> hidden'); + oElmInner.style.overflowY = 'hidden'; + } + else + { + oElmInner.style.overflowY = 'scroll'; + } + + //console.log('cyNeeded='+cyNeeded+' cyMax='+oTooltip.cyMax+' cySpace='+cySpace+' cy='+cy); + //console.log('oElmInner.offsetTop='+oElmInner.offsetTop); + //console.log('svnHistoryTooltipNewOnLoad: cx='+cx+'cxMax='+oTooltip.cxMax+' cxNeeded='+cxNeeded+' cy='+cy+' cyMax='+oTooltip.cyMax); + + tooltipRepositionOnLoad(); + } + return true; +} + + +function svnHistoryTooltipNewOnReadState(oTooltip, oRestReq, oParent) +{ + /*console.log('svnHistoryTooltipNewOnReadState: status=' + oRestReq.status + ' readyState=' + oRestReq.readyState);*/ + if (oRestReq.readyState != oRestReq.DONE) + { + oTooltip.oInnerElm.innerHTML = '<p>Loading ...(' + oRestReq.readyState + ')</p>'; + return true; + } + + /* + * Check the result and translate it to a javascript object (oResp). + */ + var oResp = null; + var sHtml; + if (oRestReq.status != 200) + { + console.log('svnHistoryTooltipNewOnReadState: status=' + oRestReq.status); + sHtml = '<p>error: status=' + oRestReq.status + '</p>'; + } + else + { + try + { + oResp = JSON.parse(oRestReq.responseText); + } + catch (oEx) + { + console.log('JSON.parse threw: ' + oEx.toString()); + console.log(oRestReq.responseText); + sHtml = '<p>error: JSON.parse threw: ' + oEx.toString() + '</p>'; + } + } + + /* + * Generate the HTML. + * + * Note! Make sure the highlighting code in svnHistoryTooltipNewDelayedShow + * continues to work after modifying this code. + */ + if (oResp) + { + sHtml = '<div class="tmvcstimeline tmvcstimelinetooltip">\n'; + + var aoCommits = oResp.aoCommits; + var cCommits = oResp.aoCommits.length; + var iCurDay = null; + var i; + for (i = 0; i < cCommits; i++) + { + var oCommit = aoCommits[i]; + var tsCreated = parseIsoTimestamp(oCommit.tsCreated); + var iCommitDay = Math.floor((tsCreated.getTime() + tsCreated.getTimezoneOffset()) / (24 * 60 * 60 * 1000)); + if (iCurDay === null || iCurDay != iCommitDay) + { + if (iCurDay !== null) + sHtml += ' </dl>\n'; + iCurDay = iCommitDay; + sHtml += ' <h2>' + tsCreated.toISOString().split('T')[0] + ' ' + g_kasDaysOfTheWeek[tsCreated.getDay()] + '</h2>\n'; + sHtml += ' <dl>\n'; + } + Date + + var sHighligh = ''; + if (oCommit.iRevision == oTooltip.iRevision) + sHighligh += ' class="tmvcstimeline-highlight"'; + + sHtml += ' <dt id="r' + oCommit.iRevision + '"' + sHighligh + '>'; + sHtml += '<a href="' + oResp.sTracChangesetUrlFmt.replace('%(iRevision)s', oCommit.iRevision.toString()); + sHtml += '" target="_blank">'; + sHtml += '<span class="tmvcstimeline-time">' + escapeElem(formatTimeHHMM(tsCreated, true)) + '</span>' + sHtml += ' Changeset <span class="tmvcstimeline-rev">[' + oCommit.iRevision + ']</span>'; + sHtml += ' by <span class="tmvcstimeline-author">' + escapeElem(oCommit.sAuthor) + '</span>'; + sHtml += '</a></dt>\n'; + sHtml += ' <dd' + sHighligh + '>' + escapeElem(oCommit.sMessage) + '</dd>\n'; + } + + if (iCurDay !== null) + sHtml += ' </dl>\n'; + sHtml += '</div>'; + } + + /*console.log('svnHistoryTooltipNewOnReadState: sHtml=' + sHtml);*/ + oTooltip.oInnerElm.innerHTML = sHtml; + + tooltipReallyShow(oTooltip, oParent); + svnHistoryTooltipNewOnLoad(); +} + +/** + * Calculates the last revision to get when showing a tooltip for @a iRevision. + * + * A tooltip covers several change log entries, both to limit the number of + * tooltips to load and to give context. The exact number is defined by + * g_cTooltipSvnRevisions. + * + * @returns Last revision in a tooltip. + * @param iRevision The revision number. + */ +function svnHistoryTooltipCalcLastRevision(iRevision) +{ + var iFirstRev = Math.floor(iRevision / g_cTooltipSvnRevisions) * g_cTooltipSvnRevisions; + return iFirstRev + g_cTooltipSvnRevisions - 1; +} + +/** + * Calculates a unique ID for the tooltip element. + * + * This is also used as dictionary index. + * + * @returns tooltip ID value (string). + * @param sRepository The repository name. + * @param iRevision The revision number. + */ +function svnHistoryTooltipCalcId(sRepository, iRevision) +{ + return 'svnHistoryTooltip_' + sRepository + '_' + svnHistoryTooltipCalcLastRevision(iRevision); +} + +/** + * The onmouseenter event handler for creating the tooltip. + * + * @param oEvt The event. + * @param sRepository The repository name. + * @param iRevision The revision number. + * @param sUrlPrefix URL prefix for non-testmanager use. + * + * @remarks onmouseout must be set to call tooltipHide. + */ +function svnHistoryTooltipShowEx(oEvt, sRepository, iRevision, sUrlPrefix) +{ + var sKey = svnHistoryTooltipCalcId(sRepository, iRevision); + var oParent = oEvt.currentTarget; + //console.log('svnHistoryTooltipShow ' + sRepository); + + function svnHistoryTooltipOldDelayedShow() + { + var sSrc; + + var oTooltip = g_dTooltips[sKey]; + //console.log('svnHistoryTooltipOldDelayedShow ' + sRepository + ' ' + oTooltip); + if (!oTooltip) + { + /* + * Create a new tooltip element. + */ + //console.log('creating ' + sKey); + oTooltip = {}; + oTooltip.oElm = document.createElement('div'); + oTooltip.oElm.setAttribute('id', sKey); + oTooltip.oElm.className = 'tmvcstooltip'; + //oTooltip.oElm.setAttribute('style', 'display:none; position: absolute;'); + oTooltip.oElm.style.display = 'none'; /* Note! Must stay hidden till loaded, or parent jumps with #rXXXX.*/ + oTooltip.oElm.style.position = 'absolute'; + oTooltip.oElm.style.zIndex = 6001; + oTooltip.xPos = 0; + oTooltip.yPos = 0; + oTooltip.cxMax = 0; + oTooltip.cyMax = 0; + oTooltip.cyMaxUp = 0; + oTooltip.xScroll = 0; + oTooltip.yScroll = 0; + oTooltip.iRevision = iRevision; /**< For :target/highlighting */ + + var oIFrameElement = document.createElement('iframe'); + oIFrameElement.setAttribute('id', sKey + '_iframe'); + oIFrameElement.style.position = 'relative'; + oIFrameElement.onmouseenter = tooltipElementOnMouseEnter; + //oIFrameElement.onmouseout = tooltipElementOnMouseOut; + oTooltip.oElm.appendChild(oIFrameElement); + oTooltip.oIFrame = oIFrameElement; + g_dTooltips[sKey] = oTooltip; + + document.body.appendChild(oTooltip.oElm); + + oIFrameElement.onload = function() { /* A slight delay here to give time for #rXXXX scrolling before we show it. */ + setTimeout(function(){ + /*console.log('iframe/onload');*/ + tooltipReallyShow(oTooltip, oParent); + svnHistoryTooltipOldOnLoad(); + }, isBrowserInternetExplorer() ? 256 : 128); + }; + + var sUrl = sUrlPrefix + 'index.py?Action=VcsHistoryTooltip&repo=' + sRepository + + '&rev=' + svnHistoryTooltipCalcLastRevision(iRevision) + + '&cEntries=' + g_cTooltipSvnRevisions + + '#r' + iRevision; + oIFrameElement.src = sUrl; + } + else + { + /* + * Show the existing one, possibly with different :target/highlighting. + */ + if (oTooltip.iRevision != iRevision) + { + //console.log('Changing revision ' + oTooltip.iRevision + ' -> ' + iRevision); + oTooltip.oIFrame.contentWindow.location.hash = '#r' + iRevision; + if (!isBrowserFirefox()) /* Chrome updates stuff like expected; Firefox OTOH doesn't change anything. */ + { + setTimeout(function() { /* Slight delay to make sure it scrolls before it's shown. */ + tooltipReallyShow(oTooltip, oParent); + svnHistoryTooltipOldOnLoad(); + }, isBrowserInternetExplorer() ? 256 : 64); + } + else + oTooltip.oIFrame.contentWindow.location.reload(); + } + else + { + tooltipReallyShow(oTooltip, oParent); + svnHistoryTooltipOldOnLoad(); + } + } + } + + function svnHistoryTooltipNewDelayedShow() + { + var sSrc; + + var oTooltip = g_dTooltips[sKey]; + /*console.log('svnHistoryTooltipNewDelayedShow: ' + sRepository + ' ' + oTooltip);*/ + if (!oTooltip) + { + /* + * Create a new tooltip element. + */ + /*console.log('creating ' + sKey);*/ + + var oElm = document.createElement('div'); + oElm.setAttribute('id', sKey); + oElm.className = 'tmvcstooltipnew'; + //oElm.setAttribute('style', 'display:none; position: absolute;'); + oElm.style.display = 'none'; /* Note! Must stay hidden till loaded, or parent jumps with #rXXXX.*/ + oElm.style.position = 'absolute'; + oElm.style.zIndex = 6001; + oElm.onmouseenter = tooltipElementOnMouseEnter; + oElm.onmouseout = tooltipElementOnMouseOut; + + var oInnerElm = document.createElement('div'); + oInnerElm.className = 'tooltip-inner'; + oElm.appendChild(oInnerElm); + + oTooltip = {}; + oTooltip.oElm = oElm; + oTooltip.oInnerElm = oInnerElm; + oTooltip.xPos = 0; + oTooltip.yPos = 0; + oTooltip.cxMax = 0; + oTooltip.cyMax = 0; + oTooltip.cyMaxUp = 0; + oTooltip.xScroll = 0; + oTooltip.yScroll = 0; + oTooltip.iRevision = iRevision; /**< For :target/highlighting */ + + oRestReq = new XMLHttpRequest(); + oRestReq.onreadystatechange = function() { svnHistoryTooltipNewOnReadState(oTooltip, this, oParent); } + oRestReq.open('GET', sUrlPrefix + 'rest.py?sPath=vcs/changelog/' + sRepository + + '/' + svnHistoryTooltipCalcLastRevision(iRevision) + '/' + g_cTooltipSvnRevisions); + oRestReq.setRequestHeader('Content-type', 'application/json'); + + document.body.appendChild(oTooltip.oElm); + g_dTooltips[sKey] = oTooltip; + + oRestReq.send(''); + } + else + { + /* + * Show the existing one, possibly with different highlighting. + * Note! Update this code when changing svnHistoryTooltipNewOnReadState. + */ + if (oTooltip.iRevision != iRevision) + { + //console.log('Changing revision ' + oTooltip.iRevision + ' -> ' + iRevision); + var oElmTimelineDiv = oTooltip.oInnerElm.firstElementChild; + var i; + for (i = 0; i < oElmTimelineDiv.children.length; i++) + { + var oElm = oElmTimelineDiv.children[i]; + //console.log('oElm='+oElm+' id='+oElm.id+' nodeName='+oElm.nodeName); + if (oElm.nodeName == 'DL') + { + var iCurRev = iRevision - 64; + var j; + for (j = 0; i < oElm.children.length; i++) + { + var oDlSubElm = oElm.children[i]; + //console.log(' oDlSubElm='+oDlSubElm+' id='+oDlSubElm.id+' nodeName='+oDlSubElm.nodeName+' className='+oDlSubElm.className); + if (oDlSubElm.id.length > 2) + iCurRev = parseInt(oDlSubElm.id.substring(1), 10); + if (iCurRev == iRevision) + oDlSubElm.className = 'tmvcstimeline-highlight'; + else + oDlSubElm.className = ''; + } + } + } + oTooltip.iRevision = iRevision; + } + + tooltipReallyShow(oTooltip, oParent); + svnHistoryTooltipNewOnLoad(); + } + } + + + /* + * Delay the change (in case the mouse moves on). + */ + tooltipResetShowTimer(); + if (g_fNewTooltips) + g_idTooltipShowTimer = setTimeout(svnHistoryTooltipNewDelayedShow, 512); + else + g_idTooltipShowTimer = setTimeout(svnHistoryTooltipOldDelayedShow, 512); +} + +/** + * The onmouseenter event handler for creating the tooltip. + * + * @param oEvt The event. + * @param sRepository The repository name. + * @param iRevision The revision number. + * + * @remarks onmouseout must be set to call tooltipHide. + */ +function svnHistoryTooltipShow(oEvt, sRepository, iRevision) +{ + return svnHistoryTooltipShowEx(oEvt, sRepository, iRevision, ''); +} + +/** @} */ + + +/** @name Debugging and Introspection + * @{ + */ + +/** + * Python-like dir() implementation. + * + * @returns Array of names associated with oObj. + * @param oObj The object under inspection. If not specified we'll + * look at the window object. + */ +function pythonlikeDir(oObj, fDeep) +{ + var aRet = []; + var dTmp = {}; + + if (!oObj) + { + oObj = window; + } + + for (var oCur = oObj; oCur; oCur = Object.getPrototypeOf(oCur)) + { + var aThis = Object.getOwnPropertyNames(oCur); + for (var i = 0; i < aThis.length; i++) + { + if (!(aThis[i] in dTmp)) + { + dTmp[aThis[i]] = 1; + aRet.push(aThis[i]); + } + } + } + + return aRet; +} + + +/** + * Python-like dir() implementation, shallow version. + * + * @returns Array of names associated with oObj. + * @param oObj The object under inspection. If not specified we'll + * look at the window object. + */ +function pythonlikeShallowDir(oObj, fDeep) +{ + var aRet = []; + var dTmp = {}; + + if (oObj) + { + for (var i in oObj) + { + aRet.push(i); + } + } + + return aRet; +} + + + +function dbgGetObjType(oObj) +{ + var sType = typeof oObj; + if (sType == "object" && oObj !== null) + { + if (oObj.constructor && oObj.constructor.name) + { + sType = oObj.constructor.name; + } + else + { + var fnToString = Object.prototype.toString; + var sTmp = fnToString.call(oObj); + if (sTmp.indexOf('[object ') === 0) + { + sType = sTmp.substring(8, sTmp.length); + } + } + } + return sType; +} + + +/** + * Dumps the given object to the console. + * + * @param oObj The object under inspection. + * @param sPrefix What to prefix the log output with. + */ +function dbgDumpObj(oObj, sName, sPrefix) +{ + var aMembers; + var sType; + + /* + * Defaults + */ + if (!oObj) + { + oObj = window; + } + + if (!sPrefix) + { + if (sName) + { + sPrefix = sName + ':'; + } + else + { + sPrefix = 'dbgDumpObj:'; + } + } + + if (!sName) + { + sName = ''; + } + + /* + * The object itself. + */ + sPrefix = sPrefix + ' '; + console.log(sPrefix + sName + ' ' + dbgGetObjType(oObj)); + + /* + * The members. + */ + sPrefix = sPrefix + ' '; + aMembers = pythonlikeDir(oObj); + for (i = 0; i < aMembers.length; i++) + { + console.log(sPrefix + aMembers[i]); + } + + return true; +} + +function dbgDumpObjWorker(sType, sName, oObj, sPrefix) +{ + var sRet; + switch (sType) + { + case 'function': + { + sRet = sPrefix + 'function ' + sName + '()' + '\n'; + break; + } + + case 'object': + { + sRet = sPrefix + 'var ' + sName + '(' + dbgGetObjType(oObj) + ') ='; + if (oObj !== null) + { + sRet += '\n'; + } + else + { + sRet += ' null\n'; + } + break; + } + + case 'string': + { + sRet = sPrefix + 'var ' + sName + '(string, ' + oObj.length + ')'; + if (oObj.length < 80) + { + sRet += ' = "' + oObj + '"\n'; + } + else + { + sRet += '\n'; + } + break; + } + + case 'Oops!': + sRet = sPrefix + sName + '(??)\n'; + break; + + default: + sRet = sPrefix + 'var ' + sName + '(' + sType + ')\n'; + break; + } + return sRet; +} + + +function dbgObjInArray(aoObjs, oObj) +{ + var i = aoObjs.length; + while (i > 0) + { + i--; + if (aoObjs[i] === oObj) + { + return true; + } + } + return false; +} + +function dbgDumpObjTreeWorker(oObj, sPrefix, aParentObjs, cMaxDepth) +{ + var sRet = ''; + var aMembers = pythonlikeShallowDir(oObj); + var i; + + for (i = 0; i < aMembers.length; i++) + { + //var sName = i; + var sName = aMembers[i]; + var oMember; + var sType; + var oEx; + + try + { + oMember = oObj[sName]; + sType = typeof oObj[sName]; + } + catch (oEx) + { + oMember = null; + sType = 'Oops!'; + } + + //sRet += '[' + i + '/' + aMembers.length + ']'; + sRet += dbgDumpObjWorker(sType, sName, oMember, sPrefix); + + if ( sType == 'object' + && oObj !== null) + { + + if (dbgObjInArray(aParentObjs, oMember)) + { + sRet += sPrefix + '! parent recursion\n'; + } + else if ( sName == 'previousSibling' + || sName == 'previousElement' + || sName == 'lastChild' + || sName == 'firstElementChild' + || sName == 'lastElementChild' + || sName == 'nextElementSibling' + || sName == 'prevElementSibling' + || sName == 'parentElement' + || sName == 'ownerDocument') + { + sRet += sPrefix + '! potentially dangerous element name\n'; + } + else if (aParentObjs.length >= cMaxDepth) + { + sRet = sRet.substring(0, sRet.length - 1); + sRet += ' <too deep>!\n'; + } + else + { + + aParentObjs.push(oMember); + if (i + 1 < aMembers.length) + { + sRet += dbgDumpObjTreeWorker(oMember, sPrefix + '| ', aParentObjs, cMaxDepth); + } + else + { + sRet += dbgDumpObjTreeWorker(oMember, sPrefix.substring(0, sPrefix.length - 2) + ' | ', aParentObjs, cMaxDepth); + } + aParentObjs.pop(); + } + } + } + return sRet; +} + +/** + * Dumps the given object and all it's subobjects to the console. + * + * @returns String dump of the object. + * @param oObj The object under inspection. + * @param sName The object name (optional). + * @param sPrefix What to prefix the log output with (optional). + * @param cMaxDepth The max depth, optional. + */ +function dbgDumpObjTree(oObj, sName, sPrefix, cMaxDepth) +{ + var sType; + var sRet; + var oEx; + + /* + * Defaults + */ + if (!sPrefix) + { + sPrefix = ''; + } + + if (!sName) + { + sName = '??'; + } + + if (!cMaxDepth) + { + cMaxDepth = 2; + } + + /* + * The object itself. + */ + try + { + sType = typeof oObj; + } + catch (oEx) + { + sType = 'Oops!'; + } + sRet = dbgDumpObjWorker(sType, sName, oObj, sPrefix); + if (sType == 'object' && oObj !== null) + { + var aParentObjs = Array(); + aParentObjs.push(oObj); + sRet += dbgDumpObjTreeWorker(oObj, sPrefix + '| ', aParentObjs, cMaxDepth); + } + + return sRet; +} + +function dbgLogString(sLongString) +{ + var aStrings = sLongString.split("\n"); + var i; + for (i = 0; i < aStrings.length; i++) + { + console.log(aStrings[i]); + } + console.log('dbgLogString - end - ' + aStrings.length + '/' + sLongString.length); + return true; +} + +function dbgLogObjTree(oObj, sName, sPrefix, cMaxDepth) +{ + return dbgLogString(dbgDumpObjTree(oObj, sName, sPrefix, cMaxDepth)); +} + +/** @} */ + diff --git a/src/VBox/ValidationKit/testmanager/htdocs/js/graphwiz.js b/src/VBox/ValidationKit/testmanager/htdocs/js/graphwiz.js new file mode 100644 index 00000000..90e1163d --- /dev/null +++ b/src/VBox/ValidationKit/testmanager/htdocs/js/graphwiz.js @@ -0,0 +1,126 @@ +/* $Id: graphwiz.js $ */ +/** @file + * JavaScript functions for the Graph Wizard. + */ + +/* + * 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 + */ + + +/******************************************************************************* +* Global Variables * +*******************************************************************************/ +/** The previous width of the div element that we measure. */ +var g_cxPreviousWidth = 0; + + +/** + * onload function that sets g_cxPreviousWidth to the width of @a sWidthSrcId. + * + * @returns true. + * @param sWidthSrcId The ID of the element which width we should measure. + */ +function graphwizOnLoadRememberWidth(sWidthSrcId) +{ + var cx = getUnscaledElementWidthById(sWidthSrcId); + if (cx) + { + g_cxPreviousWidth = cx; + } + return true; +} + + +/** + * onresize callback function that scales the given graph width input field + * value according to the resized element. + * + * @returns true. + * @param sWidthSrcId The ID of the element which width we should measure + * the resize effect on. + * @param sWidthInputId The ID of the input field which values should be + * scaled. + * + * @remarks Since we're likely to get several resize calls as part of one user + * resize operation, we're likely to suffer from some rounding + * artifacts. So, should the user abort or undo the resizing, the + * width value is unlikely to be restored to the exact value it had + * prior to the resizing. + */ +function graphwizOnResizeRecalcWidth(sWidthSrcId, sWidthInputId) +{ + var cx = getUnscaledElementWidthById(sWidthSrcId); + if (cx) + { + var oElement = document.getElementById(sWidthInputId); + if (oElement && g_cxPreviousWidth) + { + var cxOld = oElement.value; + if (isInteger(cxOld)) + { + var fpRatio = cxOld / g_cxPreviousWidth; + oElement.value = Math.round(cx * fpRatio); + } + } + g_cxPreviousWidth = cx; + } + + return true; +} + +/** + * Fills thegraph size (cx, cy) and dpi fields with default values. + * + * @returns false (for onclick). + * @param sWidthSrcId The ID of the element which width we should measure. + * @param sWidthInputId The ID of the graph width field (cx). + * @param sHeightInputId The ID of the graph height field (cy). + * @param sDpiInputId The ID of the graph DPI field. + */ +function graphwizSetDefaultSizeValues(sWidthSrcId, sWidthInputId, sHeightInputId, sDpiInputId) +{ + var cx = getUnscaledElementWidthById(sWidthSrcId); + var cDotsPerInch = getDeviceXDotsPerInch(); + + if (cx) + { + setInputFieldValue(sWidthInputId, cx); + setInputFieldValue(sHeightInputId, Math.round(cx * 5 / 16)); /* See wuimain.py. */ + } + + if (cDotsPerInch) + { + setInputFieldValue(sDpiInputId, cDotsPerInch); + } + + return false; +} + diff --git a/src/VBox/ValidationKit/testmanager/htdocs/js/vcsrevisions.js b/src/VBox/ValidationKit/testmanager/htdocs/js/vcsrevisions.js new file mode 100644 index 00000000..f7b7de7c --- /dev/null +++ b/src/VBox/ValidationKit/testmanager/htdocs/js/vcsrevisions.js @@ -0,0 +1,237 @@ +/* $Id: vcsrevisions.js $ */ +/** @file + * Common JavaScript functions + */ + +/* + * 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 + */ + + +/** + * @internal. + */ +function vcsRevisionFormatDate(tsDate) +{ + /*return tsDate.toLocaleDateString();*/ + return tsDate.toISOString().split('T')[0]; +} + +/** + * @internal. + */ +function vcsRevisionFormatTime(tsDate) +{ + return formatTimeHHMM(tsDate, true /*fNbsp*/); +} + +/** + * Called 'onclick' for the link/button used to show the detailed VCS + * revisions. + * @internal. + */ +function vcsRevisionShowDetails(oElmSource) +{ + document.getElementById('vcsrevisions-detailed').style.display = 'block'; + document.getElementById('vcsrevisions-brief').style.display = 'none'; + oElmSource.style.display = 'none'; + return false; +} + +/** + * Called when we've got the revision data. + * @internal + */ +function vcsRevisionsRender(sTestMgr, oElmDst, sBugTracker, oRestReq, sUrl) +{ + console.log('vcsRevisionsRender: status=' + oRestReq.status + ' readyState=' + oRestReq.readyState + ' url=' + sUrl); + if (oRestReq.readyState != oRestReq.DONE) + { + oElmDst.innerHTML = '<p>' + oRestReq.readyState + '</p>'; + return true; + } + + + /* + * Check the result and translate it to a javascript object (oResp). + */ + var oResp = null; + var sHtml; + if (oRestReq.status != 200) + { + /** @todo figure why this doesn't work (sPath to something random). */ + var sMsg = oRestReq.getResponseHeader('tm-error-message'); + console.log('vcsRevisionsRender: status=' + oRestReq.status + ' readyState=' + oRestReq.readyState + ' url=' + sUrl + ' msg=' + sMsg); + sHtml = '<p>error: status=' + oRestReq.status + 'readyState=' + oRestReq.readyState + ' url=' + sUrl; + if (sMsg) + sHtml += ' msg=' + sMsg; + sHtml += '</p>'; + } + else + { + try + { + oResp = JSON.parse(oRestReq.responseText); + } + catch (oEx) + { + console.log('JSON.parse threw: ' + oEx.toString()); + console.log(oRestReq.responseText); + sHtml = '<p>error: JSON.parse threw: ' + oEx.toString() + '</p>'; + } + } + + /* + * Do the rendering. + */ + if (oResp) + { + if (oResp.cCommits == 0) + { + sHtml = '<p>None.</p>'; + } + else + { + var aoCommits = oResp.aoCommits; + var cCommits = oResp.aoCommits.length; + var i; + + sHtml = ''; + /*sHtml = '<a href="#" onclick="return vcsRevisionShowDetails(this);" class="vcsrevisions-show-details">Show full VCS details...</a>\n';*/ + /*sHtml = '<button onclick="vcsRevisionShowDetails(this);" class="vcsrevisions-show-details">Show full VCS details...</button>\n';*/ + + /* Brief view (the default): */ + sHtml += '<p id="vcsrevisions-brief">'; + for (i = 0; i < cCommits; i++) + { + var oCommit = aoCommits[i]; + var sUrl = oResp.sTracChangesetUrlFmt.replace('%(sRepository)s', oCommit.sRepository).replace('%(iRevision)s', oCommit.iRevision.toString()); + var sTitle = oCommit.sAuthor + ': ' + oCommit.sMessage; + sHtml += ' <a href="' + escapeElem(sUrl) + '" title="' + escapeElem(sTitle) + '">r' + oCommit.iRevision + '</a> \n'; + } + sHtml += '</p>'; + sHtml += '<a href="#" onclick="return vcsRevisionShowDetails(this);" class="vcsrevisions-show-details-bottom">Show full VCS details...</a>\n'; + + /* Details view: */ + sHtml += '<div id="vcsrevisions-detailed" style="display:none;">\n'; + var iCurDay = null; + if (0) + { + /* Changelog variant: */ + for (i = 0; i < cCommits; i++) + { + var oCommit = aoCommits[i]; + var tsCreated = parseIsoTimestamp(oCommit.tsCreated); + var sUrl = oResp.sTracChangesetUrlFmt.replace('%(sRepository)s', oCommit.sRepository).replace('%(iRevision)s', oCommit.iRevision.toString()); + var iCommitDay = Math.floor((tsCreated.getTime() + tsCreated.getTimezoneOffset()) / (24 * 60 * 60 * 1000)); + if (iCurDay === null || iCurDay != iCommitDay) + { + if (iCurDay !== null) + sHtml += ' </dl>\n'; + iCurDay = iCommitDay; + sHtml += ' <h3>' + vcsRevisionFormatDate(tsCreated) + ' ' + g_kasDaysOfTheWeek[tsCreated.getDay()] + '</h3>\n'; + sHtml += ' <dl>\n'; + } + + sHtml += ' <dt id="r' + oCommit.iRevision + '">'; + sHtml += '<a href="' + oResp.sTracChangesetUrlFmt.replace('%(iRevision)s', oCommit.iRevision.toString()) + '">'; + /*sHtml += '<span class="vcsrevisions-time">' + escapeElem(vcsRevisionFormatTime(tsCreated)) + '</span>' + sHtml += ' Changeset <span class="vcsrevisions-rev">r' + oCommit.iRevision + '</span>'; + sHtml += ' by <span class="vcsrevisions-author">' + escapeElem(oCommit.sAuthor) + '</span>'; */ + sHtml += '<span class="vcsrevisions-time">' + escapeElem(vcsRevisionFormatTime(tsCreated)) + '</span>'; + sHtml += ' - <span class="vcsrevisions-rev">r' + oCommit.iRevision + '</span>'; + sHtml += ' - <span class="vcsrevisions-author">' + escapeElem(oCommit.sAuthor) + '</span>'; + sHtml += '</a></dt>\n'; + sHtml += ' <dd>' + escapeElem(oCommit.sMessage) + '</dd>\n'; + } + + if (iCurDay !== null) + sHtml += ' </dl>\n'; + } + else + { /* TABLE variant: */ + sHtml += '<table class="vcsrevisions-table">'; + var iAlt = 0; + for (i = 0; i < cCommits; i++) + { + var oCommit = aoCommits[i]; + var tsCreated = parseIsoTimestamp(oCommit.tsCreated); + var sUrl = oResp.sTracChangesetUrlFmt.replace('%(sRepository)s', oCommit.sRepository).replace('%(iRevision)s', oCommit.iRevision.toString()); + var iCommitDay = Math.floor((tsCreated.getTime() + tsCreated.getTimezoneOffset()) / (24 * 60 * 60 * 1000)); + if (iCurDay === null || iCurDay != iCommitDay) + { + iCurDay = iCommitDay; + sHtml += '<tr id="r' + oCommit.iRevision + '"><td colspan="4" class="vcsrevisions-tab-date">'; + sHtml += vcsRevisionFormatDate(tsCreated) + ' ' + g_kasDaysOfTheWeek[tsCreated.getDay()]; + sHtml += '</td></tr>\n'; + sHtml += '<tr>'; + iAlt = 0; + } + else + sHtml += '<tr id="r' + oCommit.iRevision + '">'; + var sAltCls = ''; + var sAltClsStmt = ''; + iAlt += 1; + if (iAlt & 1) + { + sAltCls = ' alt'; + sAltClsStmt = ' class="alt"'; + } + sHtml += '<td class="vcsrevisions-tab-time'+sAltCls+'"><a href="' + sUrl + '">' + + escapeElem(vcsRevisionFormatTime(tsCreated)) + '</a></td>'; + sHtml += '<td'+sAltClsStmt+'><a href="' + sUrl + '" class="vcsrevisions-rev' + sAltCls + '">r' + + oCommit.iRevision + '</a></td>'; + sHtml += '<td'+sAltClsStmt+'><a href="' + sUrl + '" class="vcsrevisions-author' + sAltCls + '">' + + escapeElem(oCommit.sAuthor) + '<a></td>'; + sHtml += '<td'+sAltClsStmt+'>' + escapeElem(oCommit.sMessage) + '</td></tr>\n'; + } + sHtml += '</table>\n'; + } + sHtml += '</div>\n'; + } + } + + oElmDst.innerHTML = sHtml; +} + +/** Called by the xtracker bugdetails page. */ +function VcsRevisionsLoad(sTestMgr, oElmDst, sBugTracker, lBugNo) +{ + oElmDst.innerHTML = '<p>Loading VCS revisions...</p>'; + + var sUrl = sTestMgr + 'rest.py?sPath=vcs/bugreferences/' + sBugTracker + '/' + lBugNo; + var oRestReq = new XMLHttpRequest(); + oRestReq.onreadystatechange = function() { vcsRevisionsRender(sTestMgr, oElmDst, sBugTracker, this, sUrl); } + oRestReq.open('GET', sUrl); + oRestReq.withCredentials = true; + /*oRestReq.setRequestHeader('Content-type', 'application/json'); - Causes CORS trouble. */ + oRestReq.send(); +} + diff --git a/src/VBox/ValidationKit/testmanager/misc/Makefile.kmk b/src/VBox/ValidationKit/testmanager/misc/Makefile.kmk new file mode 100644 index 00000000..74d882cc --- /dev/null +++ b/src/VBox/ValidationKit/testmanager/misc/Makefile.kmk @@ -0,0 +1,46 @@ +# $Id: Makefile.kmk $ +## @file +# VirtualBox Validation Kit. +# + +# +# Copyright (C) 2006-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 +# + +SUB_DEPTH = ../../../../.. +include $(KBUILD_PATH)/subheader.kmk + + +VBOX_VALIDATIONKIT_PYTHON_SOURCES += $(wildcard $(PATH_SUB_CURRENT)/*.py) + +$(evalcall def_vbox_validationkit_process_python_sources) +$(evalcall def_vbox_validationkit_process_js_sources) +include $(FILE_KBUILD_SUB_FOOTER) + diff --git a/src/VBox/ValidationKit/testmanager/misc/htpasswd-logout b/src/VBox/ValidationKit/testmanager/misc/htpasswd-logout new file mode 100644 index 00000000..8a36998b --- /dev/null +++ b/src/VBox/ValidationKit/testmanager/misc/htpasswd-logout @@ -0,0 +1 @@ +logout:$apr1$OqiMc/Uv$XylAjnIPla7gb57UMW0TK. diff --git a/src/VBox/ValidationKit/testmanager/misc/htpasswd-sample b/src/VBox/ValidationKit/testmanager/misc/htpasswd-sample new file mode 100644 index 00000000..6b6c1b33 --- /dev/null +++ b/src/VBox/ValidationKit/testmanager/misc/htpasswd-sample @@ -0,0 +1,2 @@ +admin:ZXHvyrLs.vCmw +test:ClO2uu6/D7jDg diff --git a/src/VBox/ValidationKit/testmanager/readme.txt b/src/VBox/ValidationKit/testmanager/readme.txt new file mode 100644 index 00000000..7211c7b6 --- /dev/null +++ b/src/VBox/ValidationKit/testmanager/readme.txt @@ -0,0 +1,125 @@ +$Id: readme.txt $ + +Directory descriptions: + ./ The Test Manager. + ./batch/ Batch scripts to be run via cron. + ./cgi/ CGI scripts (we'll use standard CGI at first). + ./core/ The core Test Manager logic (model). + ./htdocs/ Files to be served directly by the web server. + ./htdocs/css/ Style sheets. + ./htdocs/images/ Graphics. + ./webui/ The Web User Interface (WUI) bits. (Not sure if we will + do model-view-controller stuff, though. Time will show.) + +I. Running a Test Manager instance with Docker: + + - This way should be preferred to get a local Test Manager instance running + and is NOT meant for production use! + + - Install docker-ce and docker-compose on your Linux host (not tested on + Windows yet). Your user must be able to run the Docker CLI (see Docker documentation). + + - Type "kmk" to get the containers built, "kmk start|stop" to start/stop them + respectively. To start over, use "kmk clean". For having a peek into the container + logs, use "kmk logs". + + To administrate / develop the database, an Adminer instance is running at + http://localhost:8080 + + To access the actual Test Manager instance, go to http://localhost:8080/testmanager/ + + - There are two ways of doing development with this setup: + + a. The Test Manager source is stored inside a separate data volume called + "docker_vbox-testmgr-web". The source will be checked out automatically on + container initialization. Development then can take part within that data + container. The initialization script will automatically pull the sources + from the public OSE tree, so make sure this is what you want! + + b. Edit the (hidden) .env file in this directory and change VBOX_TESTMGR_DATA + to point to your checked out VBox root, e.g. VBOX_TESTMGR_DATA=/path/to/VBox/trunk + + +II. Steps for manually setting up a local Test Manager instance for development: + + - Install apache, postgresql, python, psycopg2 (python) and pylint. + + - Create the database by executing 'kmk load-testmanager-db' in + the './db/' subdirectory. The default psql parameters there + requies pg_hba.conf to specify 'trust' instead of 'peer' as the + authentication method for local connections. + + - Use ./db/partial-db-dump.py on the production system to extract a + partial database dump (last 14 days). + + - Use ./db/partial-db-dump.py with the --load-dump-into-database + parameter on the development box to load the dump. + + - Configure apache using the ./apache-template-2.4.conf (see top of + file for details), for example: + + Define TestManagerRootDir "/mnt/scratch/vbox/svn/trunk/src/VBox/ValidationKit/testmanager" + Define VBoxBuildOutputDir "/tmp" + Include "${TestManagerRootDir}/apache-template-2.4.conf" + + Make sure to enable cgi (a2enmod cgi && systemctl restart apache2). + + - Default htpasswd file has users a user 'admin' with password 'admin' and a + 'test' user with password 'test'. This isn't going to get you far if + you've loaded something from the production server as there is typically + no 'admin' user in the 'Users' table there. So, you will need to add your + user and a throwaway password to 'misc/htpasswd-sample' using the htpasswd + utility. + + - Try http://localhost/testmanager/ in a browser and see if it works. + + +III. OS X version of the above manual setup using MacPorts: + + - sudo ports install apache2 postgresql12 postgresql12-server py38-psycopg2 py38-pylint + sudo port select --set python python38 + sudo port select --set python3 python38 + sudo port select --set pylint pylint38 + + Note! Replace the python 38 with the most recent one you want to use. Same + for the 12 in relation to postgresql. + + - Do what the postgresql12-server notes says, at the time of writing: + sudo mkdir -p /opt/local/var/db/postgresql12/defaultdb + sudo chown postgres:postgres /opt/local/var/db/postgresql12/defaultdb + sudo su postgres -c 'cd /opt/local/var/db/postgresql12 && /opt/local/lib/postgresql12/bin/initdb -D /opt/local/var/db/postgresql12/defaultdb' + sudo port load postgresql12-server + + Note! The postgresql12-server's config is 'trust' already, so no need to + edit /opt/local/var/db/postgresql12/defaultdb/pg_hba.conf there. If + you use a different version, please check it. + + - kmk load-testmanager-db + + - Creating and loading a partial database dump as detailed above. + + - Configure apache: + - sudo joe /opt/local/etc/apache2/httpd.conf: + - Uncomment the line "LoadModule cgi_module...". + - At the end of the file add (edit paths): + Define TestManagerRootDir "/Users/bird/coding/vbox/svn/trunk/src/VBox/ValidationKit/testmanager" + Define VBoxBuildOutputDir "/tmp" + Include "${TestManagerRootDir}/apache-template-2.4.conf" + - Test the config: + /opt/local/sbin/apachectl -t + - So apache will find the right python add the following to + /opt/local/sbin/envvars: + PATH=/opt/local/bin:/opt/local/sbin:$PATH + export PATH + - Load the apache service (or reload it): + sudo port load apache2 + - Give apache access to read everything under TestManagerRootDir: + chmod -R a:rX /Users/bird/coding/vbox/svn/trunk/src/VBox/ValidationKit/testmanager + MYDIR=/Users/bird/coding/vbox/svn/trunk/src/VBox/ValidationKit; while [ '!' "$MYDIR" '<' "$HOME" ]; do \ + chmod a+x "$MYDIR"; MYDIR=`dirname $MYDIR`; done + + - Fix htpasswd file as detailed above and try the url (also above). + + +N.B. For developing tests (../tests/), setting up a local test manager will be + a complete waste of time. Just run the test drivers locally. diff --git a/src/VBox/ValidationKit/testmanager/selftest/st1-load.pgsql b/src/VBox/ValidationKit/testmanager/selftest/st1-load.pgsql new file mode 100644 index 00000000..af4d5ace --- /dev/null +++ b/src/VBox/ValidationKit/testmanager/selftest/st1-load.pgsql @@ -0,0 +1,164 @@ +-- $Id: st1-load.pgsql $ +--- @file +-- VBox Test Manager - Self Test #1 Database Load File. +-- + +-- +-- 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 +-- + + + +\set ON_ERROR_STOP 1 +\connect testmanager; + +BEGIN WORK; + + +INSERT INTO Users (uid, sUsername, sEmail, sFullName, sLoginName) + VALUES (1112223331, 'st1', 'st1@example.org', 'self test #1', 'st1'); + +INSERT INTO TestCases (uidAuthor, sName, fEnabled, cSecTimeout, sBaseCmd, sTestSuiteZips) + VALUES (1112223331, 'st1-test1', TRUE, 3600, 'validationkit/tests/selftests/tdSelfTest1.py', '@DOWNLOAD_BASE_URL@/VBoxValidationKit.zip'); + +INSERT INTO TestCaseArgs (idTestCase, uidAuthor, sArgs) + VALUES ((SELECT idTestCase FROM TestCases WHERE sName = 'st1-test1'), 1112223331, ''); + +INSERT INTO TestGroups (uidAuthor, sName) + VALUES (1112223331, 'st1-testgroup'); + +INSERT INTO TestGroupMembers (idTestGroup, idTestCase, uidAuthor) + VALUES ((SELECT idTestGroup FROM TestGroups WHERE sName = 'st1-testgroup'), + (SELECT idTestCase FROM TestCases WHERE sName = 'st1-test1'), + 1112223331); + +INSERT INTO BuildSources (uidAuthor, sName, sProduct, sBranch, asTypes, asOsArches) + VALUES (1112223331, 'st1-src', 'st1', 'trunk', + ARRAY['release', 'strict'], + ARRAY['win.x86', 'linux.noarch', 'solaris.amd64', 'os-agnostic.sparc64', 'os-agnostic.noarch']); + +INSERT INTO BuildCategories (sProduct, sBranch, sType, asOsArches) + VALUES ('st1', 'trunk', 'release', ARRAY['os-agnostic.noarch']); + +INSERT INTO Builds (uidAuthor, idBuildCategory, iRevision, sVersion, sBinaries) + VALUES (1112223331, + (SELECT idBuildCategory FROM BuildCategories WHERE sProduct = 'st1' AND sBranch = 'trunk'), + 1234, '1.0', ''); + +INSERT INTO SchedGroups (uidAuthor, sName, sDescription, fEnabled, idBuildSrc) + VALUES (1112223331, 'st1-group', 'test test #1', TRUE, + (SELECT idBuildSrc FROM BuildSources WHERE sName = 'st1-src') ); + +INSERT INTO SchedGroupMembers (idSchedGroup, idTestGroup, uidAuthor) + VALUES ((SELECT idSchedGroup FROM SchedGroups WHERE sName = 'st1-group'), + (SELECT idTestGroup FROM TestGroups WHERE sName = 'st1-testgroup'), + 1112223331); + + +-- The second test + +INSERT INTO TestCases (uidAuthor, sName, fEnabled, cSecTimeout, sBaseCmd, sTestSuiteZips) + VALUES (1112223331, 'st1-test2', TRUE, 3600, 'validationkit/tests/selftests/tdSelfTest2.py', '@DOWNLOAD_BASE_URL@/VBoxValidationKit.zip'); + +INSERT INTO TestCaseArgs (idTestCase, uidAuthor, sArgs) + VALUES ((SELECT idTestCase FROM TestCases WHERE sName = 'st1-test2'), 1112223331, ''); + +INSERT INTO TestGroupMembers (idTestGroup, idTestCase, uidAuthor) + VALUES ((SELECT idTestGroup FROM TestGroups WHERE sName = 'st1-testgroup'), + (SELECT idTestCase FROM TestCases WHERE sName = 'st1-test2'), + 1112223331); + +-- The third test + +INSERT INTO TestCases (uidAuthor, sName, fEnabled, cSecTimeout, sBaseCmd, sTestSuiteZips) + VALUES (1112223331, 'st1-test3', TRUE, 3600, 'validationkit/tests/selftests/tdSelfTest3.py', '@DOWNLOAD_BASE_URL@/VBoxValidationKit.zip'); + +INSERT INTO TestCaseArgs (idTestCase, uidAuthor, sArgs) + VALUES ((SELECT idTestCase FROM TestCases WHERE sName = 'st1-test3'), 1112223331, ''); + +INSERT INTO TestGroupMembers (idTestGroup, idTestCase, uidAuthor) + VALUES ((SELECT idTestGroup FROM TestGroups WHERE sName = 'st1-testgroup'), + (SELECT idTestCase FROM TestCases WHERE sName = 'st1-test3'), + 1112223331); + +-- The fourth thru eight tests + +INSERT INTO TestCases (uidAuthor, sName, fEnabled, cSecTimeout, sBaseCmd, sTestSuiteZips) + VALUES (1112223331, 'st1-test4-neg', TRUE, 3600, 'validationkit/tests/selftests/tdSelfTest4.py --test immediate-sub-tests', + '@DOWNLOAD_BASE_URL@/VBoxValidationKit.zip'); +INSERT INTO TestCaseArgs (idTestCase, uidAuthor, sArgs) + VALUES ((SELECT idTestCase FROM TestCases WHERE sName = 'st1-test4-neg'), 1112223331, ''); +INSERT INTO TestGroupMembers (idTestGroup, idTestCase, uidAuthor) + VALUES ((SELECT idTestGroup FROM TestGroups WHERE sName = 'st1-testgroup'), + (SELECT idTestCase FROM TestCases WHERE sName = 'st1-test4-neg'), + 1112223331); + +INSERT INTO TestCases (uidAuthor, sName, fEnabled, cSecTimeout, sBaseCmd, sTestSuiteZips) + VALUES (1112223331, 'st1-test5-neg', TRUE, 3600, 'validationkit/tests/selftests/tdSelfTest4.py --test total-sub-tests', + '@DOWNLOAD_BASE_URL@/VBoxValidationKit.zip'); +INSERT INTO TestCaseArgs (idTestCase, uidAuthor, sArgs) + VALUES ((SELECT idTestCase FROM TestCases WHERE sName = 'st1-test5-neg'), 1112223331, ''); +INSERT INTO TestGroupMembers (idTestGroup, idTestCase, uidAuthor) + VALUES ((SELECT idTestGroup FROM TestGroups WHERE sName = 'st1-testgroup'), + (SELECT idTestCase FROM TestCases WHERE sName = 'st1-test5-neg'), + 1112223331); + +INSERT INTO TestCases (uidAuthor, sName, fEnabled, cSecTimeout, sBaseCmd, sTestSuiteZips) + VALUES (1112223331, 'st1-test6-neg', TRUE, 3600, 'validationkit/tests/selftests/tdSelfTest4.py --test immediate-values', + '@DOWNLOAD_BASE_URL@/VBoxValidationKit.zip'); +INSERT INTO TestCaseArgs (idTestCase, uidAuthor, sArgs) + VALUES ((SELECT idTestCase FROM TestCases WHERE sName = 'st1-test6-neg'), 1112223331, ''); +INSERT INTO TestGroupMembers (idTestGroup, idTestCase, uidAuthor) + VALUES ((SELECT idTestGroup FROM TestGroups WHERE sName = 'st1-testgroup'), + (SELECT idTestCase FROM TestCases WHERE sName = 'st1-test6-neg'), + 1112223331); + +INSERT INTO TestCases (uidAuthor, sName, fEnabled, cSecTimeout, sBaseCmd, sTestSuiteZips) + VALUES (1112223331, 'st1-test7-neg', TRUE, 3600, 'validationkit/tests/selftests/tdSelfTest4.py --test total-values', + '@DOWNLOAD_BASE_URL@/VBoxValidationKit.zip'); +INSERT INTO TestCaseArgs (idTestCase, uidAuthor, sArgs) + VALUES ((SELECT idTestCase FROM TestCases WHERE sName = 'st1-test7-neg'), 1112223331, ''); +INSERT INTO TestGroupMembers (idTestGroup, idTestCase, uidAuthor) + VALUES ((SELECT idTestGroup FROM TestGroups WHERE sName = 'st1-testgroup'), + (SELECT idTestCase FROM TestCases WHERE sName = 'st1-test7-neg'), + 1112223331); + +INSERT INTO TestCases (uidAuthor, sName, fEnabled, cSecTimeout, sBaseCmd, sTestSuiteZips) + VALUES (1112223331, 'st1-test8-neg', TRUE, 3600, 'validationkit/tests/selftests/tdSelfTest4.py --test immediate-messages', + '@DOWNLOAD_BASE_URL@/VBoxValidationKit.zip'); +INSERT INTO TestCaseArgs (idTestCase, uidAuthor, sArgs) + VALUES ((SELECT idTestCase FROM TestCases WHERE sName = 'st1-test8-neg'), 1112223331, ''); +INSERT INTO TestGroupMembers (idTestGroup, idTestCase, uidAuthor) + VALUES ((SELECT idTestGroup FROM TestGroups WHERE sName = 'st1-testgroup'), + (SELECT idTestCase FROM TestCases WHERE sName = 'st1-test8-neg'), + 1112223331); + +COMMIT WORK; + diff --git a/src/VBox/ValidationKit/testmanager/selftest/st1-unload.pgsql b/src/VBox/ValidationKit/testmanager/selftest/st1-unload.pgsql new file mode 100644 index 00000000..5fe797c3 --- /dev/null +++ b/src/VBox/ValidationKit/testmanager/selftest/st1-unload.pgsql @@ -0,0 +1,87 @@ +-- $Id: st1-unload.pgsql $ +--- @file +-- VBox Test Manager - Self Test #1 Database Unload File. +-- + +-- +-- 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 +-- + + + +\set ON_ERROR_STOP 1 +\connect testmanager; + +BEGIN WORK; + +DELETE FROM TestBoxStatuses; +DELETE FROM SchedQueues; + +DELETE FROM SchedGroupMembers WHERE uidAuthor = 1112223331; +UPDATE TestBoxes SET idSchedGroup = 1 WHERE idSchedGroup IN ( SELECT idSchedGroup FROM SchedGroups WHERE uidAuthor = 1112223331 ); +DELETE FROM SchedGroups WHERE uidAuthor = 1112223331 OR sName = 'st1-group'; + +UPDATE TestSets SET idTestResult = NULL + WHERE idTestCase IN ( SELECT idTestCase FROM TestCases WHERE uidAuthor = 1112223331 ); + +DELETE FROM TestResultValues + WHERE idTestResult IN ( SELECT idTestResult FROM TestResults + WHERE idTestSet IN ( SELECT idTestSet FROM TestSets + WHERE idTestCase IN ( SELECT idTestCase FROM TestCases + WHERE uidAuthor = 1112223331 ) ) ); +DELETE FROM TestResultFiles + WHERE idTestResult IN ( SELECT idTestResult FROM TestResults + WHERE idTestSet IN ( SELECT idTestSet FROM TestSets + WHERE idTestCase IN ( SELECT idTestCase FROM TestCases + WHERE uidAuthor = 1112223331 ) ) ); +DELETE FROM TestResultMsgs + WHERE idTestResult IN ( SELECT idTestResult FROM TestResults + WHERE idTestSet IN ( SELECT idTestSet FROM TestSets + WHERE idTestCase IN ( SELECT idTestCase FROM TestCases + WHERE uidAuthor = 1112223331 ) ) ); +DELETE FROM TestResults + WHERE idTestSet IN ( SELECT idTestSet FROM TestSets + WHERE idTestCase IN ( SELECT idTestCase FROM TestCases WHERE uidAuthor = 1112223331 ) ); +DELETE FROM TestSets + WHERE idTestCase IN ( SELECT idTestCase FROM TestCases WHERE uidAuthor = 1112223331 ); + +DELETE FROM TestCases WHERE uidAuthor = 1112223331; +DELETE FROM TestCaseArgs WHERE uidAuthor = 1112223331; +DELETE FROM TestGroups WHERE uidAuthor = 1112223331 OR sName = 'st1-testgroup'; +DELETE FROM TestGroupMembers WHERE uidAuthor = 1112223331; + +DELETE FROM BuildSources WHERE uidAuthor = 1112223331; +DELETE FROM Builds WHERE uidAuthor = 1112223331; +DELETE FROM BuildCategories WHERE sProduct = 'st1'; + +DELETE FROM Users WHERE uid = 1112223331; + +COMMIT WORK; + diff --git a/src/VBox/ValidationKit/testmanager/webui/Makefile.kmk b/src/VBox/ValidationKit/testmanager/webui/Makefile.kmk new file mode 100644 index 00000000..5a9b58bd --- /dev/null +++ b/src/VBox/ValidationKit/testmanager/webui/Makefile.kmk @@ -0,0 +1,47 @@ +# $Id: Makefile.kmk $ +## @file +# VirtualBox Validation Kit. +# + +# +# Copyright (C) 2006-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 +# + +SUB_DEPTH = ../../../../.. +include $(KBUILD_PATH)/subheader.kmk + + +VBOX_VALIDATIONKIT_PYTHON_SOURCES += $(wildcard $(PATH_SUB_CURRENT)/*.py) +VBOX_VALIDATIONKIT_PYUNITTEST_EXCLUDE += $(PATH_SUB_CURRENT)/wuihlpgraphmatplotlib.py + +$(evalcall def_vbox_validationkit_process_python_sources) +$(evalcall def_vbox_validationkit_process_js_sources) +include $(FILE_KBUILD_SUB_FOOTER) + diff --git a/src/VBox/ValidationKit/testmanager/webui/__init__.py b/src/VBox/ValidationKit/testmanager/webui/__init__.py new file mode 100644 index 00000000..d00f5436 --- /dev/null +++ b/src/VBox/ValidationKit/testmanager/webui/__init__.py @@ -0,0 +1,40 @@ +# -*- coding: utf-8 -*- +# $Id: __init__.py $ + +""" +TestBox Script - WUI Presentation. +""" + +__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 $" + diff --git a/src/VBox/ValidationKit/testmanager/webui/template-details.html b/src/VBox/ValidationKit/testmanager/webui/template-details.html new file mode 100644 index 00000000..e7e16c0f --- /dev/null +++ b/src/VBox/ValidationKit/testmanager/webui/template-details.html @@ -0,0 +1,45 @@ +<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd"> +<html lang="en"> + <head> + <meta http-equiv="content-type" content="text/html; charset=UTF-8" /> + <meta http-equiv="content-language" content="en" /> + <meta name="language" content="en" /> + <link href="htdocs/images/tmfavicon.ico" rel="shortcut icon" type="image/x-icon" /> + <link href="htdocs/images/tmfavicon.ico" rel="icon" type="image/x-icon" /> + <link href="htdocs/css/common.css" rel="stylesheet" type="text/css" media="screen" /> + <link href="htdocs/css/tooltip.css" rel="stylesheet" type="text/css" media="screen" /> + <link href="htdocs/css/details.css" rel="stylesheet" type="text/css" media="screen" /> + <script type="text/javascript" src="htdocs/js/common.js"></script> + <title>@@PAGE_TITLE@@</title> + </head> + + <body> + <div id="wrap"> + <div id="head-wrap"> + <div id="logo"> + <img alt ="VirtualBox" src="htdocs/images/VirtualBox.svg"> + </div> + <div id="header"> + <h1>@@PAGE_TITLE@@</h1> + </div> + <div id="top-menu" class="tm-top-menu-wo-side"> + <ul> + @@TOP_MENU_ITEMS@@ + </ul> + </div> + <div id="login"> + <p><small> + Logged in as <b>@@USER_NAME@@</b>@@LOG_OUT@@ + </small></p> + </div> + </div> + + <div id="main"> + @@PAGE_BODY@@ + + @@DEBUG@@ + </div> + </div> + </body> +</html> + diff --git a/src/VBox/ValidationKit/testmanager/webui/template-graphwiz.html b/src/VBox/ValidationKit/testmanager/webui/template-graphwiz.html new file mode 100644 index 00000000..4e1dc0c8 --- /dev/null +++ b/src/VBox/ValidationKit/testmanager/webui/template-graphwiz.html @@ -0,0 +1,46 @@ +<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd"> +<html lang="en"> + <head> + <meta http-equiv="content-type" content="text/html; charset=UTF-8" /> + <meta http-equiv="content-language" content="en" /> + <meta name="language" content="en" /> + <link href="htdocs/images/tmfavicon.ico" rel="shortcut icon" type="image/x-icon" /> + <link href="htdocs/images/tmfavicon.ico" rel="icon" type="image/x-icon" /> + <link href="htdocs/css/common.css" rel="stylesheet" type="text/css" media="screen" /> + <link href="htdocs/css/tooltip.css" rel="stylesheet" type="text/css" media="screen" /> + <link href="htdocs/css/graphwiz.css" rel="stylesheet" type="text/css" media="screen" /> + <script type="text/javascript" src="htdocs/js/common.js"></script> + <script type="text/javascript" src="htdocs/js/graphwiz.js"></script> + <title>@@PAGE_TITLE@@</title> + </head> + + <body> + <div id="wrap"> + <div id="head-wrap"> + <div id="logo"> + <img alt ="VirtualBox" src="htdocs/images/VirtualBox.svg"> + </div> + <div id="header"> + <h1>@@PAGE_TITLE@@</h1> + </div> + <div id="top-menu" class="tm-top-menu-wo-side"> + <ul> + @@TOP_MENU_ITEMS@@ + </ul> + </div> + <div id="login"> + <p><small> + Logged in as <b>@@USER_NAME@@</b>@@LOG_OUT@@ + </small></p> + </div> + </div> + + <div id="main"> + @@PAGE_BODY@@ + + @@DEBUG@@ + </div> + </div> + </body> +</html> + diff --git a/src/VBox/ValidationKit/testmanager/webui/template-tooltip.html b/src/VBox/ValidationKit/testmanager/webui/template-tooltip.html new file mode 100644 index 00000000..7aa95d71 --- /dev/null +++ b/src/VBox/ValidationKit/testmanager/webui/template-tooltip.html @@ -0,0 +1,20 @@ +<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd"> +<html lang="en"> +<head> + <meta http-equiv="content-type" content="text/html; charset=UTF-8" /> + <meta http-equiv="content-language" content="en" /> + <meta name="language" content="en" /> + <link href="htdocs/css/common.css" rel="stylesheet" type="text/css" media="screen" /> + <link href="htdocs/css/tooltip.css" rel="stylesheet" type="text/css" media="screen" /> + <title>@@PAGE_TITLE@@</title> +</head> + +<body scroll="no"> +<div id="tooltip" class="tooltip-main"> +<div id="tooltip-inner" class="tooltip-inner"> +@@PAGE_BODY@@ +</div> +</div> +</body> +</html> + diff --git a/src/VBox/ValidationKit/testmanager/webui/template.html b/src/VBox/ValidationKit/testmanager/webui/template.html new file mode 100644 index 00000000..6480c20b --- /dev/null +++ b/src/VBox/ValidationKit/testmanager/webui/template.html @@ -0,0 +1,65 @@ +<!DOCTYPE HTML> +<html lang="en"> + <head> + <meta charset="UTF-8" /> + <meta http-equiv="content-type" content="text/html; charset=UTF-8" /> + <meta http-equiv="content-language" content="en" /> + <meta name="language" content="en" /> + <link href="htdocs/images/tmfavicon.ico" rel="shortcut icon" /> + <link href="htdocs/images/tmfavicon.ico" rel="icon" type="image/x-icon" /> + <link href="htdocs/css/common.css" rel="stylesheet" type="text/css" media="screen" /> + <link href="htdocs/css/tooltip.css" rel="stylesheet" type="text/css" media="screen" /> + <script type="text/javascript" src="htdocs/js/common.js"></script> + <title>@@PAGE_TITLE@@</title> + </head> + + <body> + <div id="wrap"> + <div id="head-wrap"> + <div id="logo"> + <img alt ="VirtualBox" src="htdocs/images/VirtualBox.svg"> + </div> + <div id="header"> + <h1>@@PAGE_TITLE@@</h1> + </div> + <div id="login"> + <p><small> + Logged in as <b>@@USER_NAME@@</b>@@LOG_OUT@@ + </small></p> + </div> + <div id="top-menu"> + <ul> + @@TOP_MENU_ITEMS@@ + </ul> + </div> + </div> + + <div id="side-menu-wrap"> + <div id="side-menu"> + <div id="side-menu-body"> + <form id="side-menu-form" @@SIDE_MENU_FORM_ATTRS@@> + <ul> + @@SIDE_MENU_ITEMS@@ + </ul> + @@SIDE_FILTER_CONTROL@@ + </form> + </div> + <!-- justify-content: space-between --> + <div id="side-footer"> + <p> + VBox Test Manager<br/>@@TESTMANAGER_VERSION@@r@@TESTMANAGER_REVISION@@ + </p> + <p>Copyright © 2012-2023 Oracle Corporation</p> + </div> + </div> + </div> + + <div id="main"> + @@PAGE_BODY@@ + + @@DEBUG@@ + </div> + </div> + </body> +</html> + diff --git a/src/VBox/ValidationKit/testmanager/webui/wuiadmin.py b/src/VBox/ValidationKit/testmanager/webui/wuiadmin.py new file mode 100755 index 00000000..ef5c7669 --- /dev/null +++ b/src/VBox/ValidationKit/testmanager/webui/wuiadmin.py @@ -0,0 +1,1270 @@ +# -*- coding: utf-8 -*- +# $Id: wuiadmin.py $ + +""" +Test Manager Core - WUI - Admin Main page. +""" + +__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 cgitb; +import sys; + +# Validation Kit imports. +from common import utils, webutils; +from testmanager import config; +from testmanager.webui.wuibase import WuiDispatcherBase, WuiException + + +class WuiAdmin(WuiDispatcherBase): + """ + WUI Admin main page. + """ + + ## The name of the script. + ksScriptName = 'admin.py' + + ## Number of days back. + ksParamDaysBack = 'cDaysBack'; + + ## @name Actions + ## @{ + ksActionSystemLogList = 'SystemLogList' + ksActionSystemChangelogList = 'SystemChangelogList' + ksActionSystemDbDump = 'SystemDbDump' + ksActionSystemDbDumpDownload = 'SystemDbDumpDownload' + + ksActionUserList = 'UserList' + ksActionUserAdd = 'UserAdd' + ksActionUserAddPost = 'UserAddPost' + ksActionUserEdit = 'UserEdit' + ksActionUserEditPost = 'UserEditPost' + ksActionUserDelPost = 'UserDelPost' + ksActionUserDetails = 'UserDetails' + + ksActionTestBoxList = 'TestBoxList' + ksActionTestBoxListPost = 'TestBoxListPost' + ksActionTestBoxAdd = 'TestBoxAdd' + ksActionTestBoxAddPost = 'TestBoxAddPost' + ksActionTestBoxEdit = 'TestBoxEdit' + ksActionTestBoxEditPost = 'TestBoxEditPost' + ksActionTestBoxDetails = 'TestBoxDetails' + ksActionTestBoxRemovePost = 'TestBoxRemove' + ksActionTestBoxesRegenQueues = 'TestBoxesRegenQueues'; + + ksActionTestCaseList = 'TestCaseList' + ksActionTestCaseAdd = 'TestCaseAdd' + ksActionTestCaseAddPost = 'TestCaseAddPost' + ksActionTestCaseClone = 'TestCaseClone' + ksActionTestCaseDetails = 'TestCaseDetails' + ksActionTestCaseEdit = 'TestCaseEdit' + ksActionTestCaseEditPost = 'TestCaseEditPost' + ksActionTestCaseDoRemove = 'TestCaseDoRemove' + + ksActionGlobalRsrcShowAll = 'GlobalRsrcShowAll' + ksActionGlobalRsrcShowAdd = 'GlobalRsrcShowAdd' + ksActionGlobalRsrcShowEdit = 'GlobalRsrcShowEdit' + ksActionGlobalRsrcAdd = 'GlobalRsrcAddPost' + ksActionGlobalRsrcEdit = 'GlobalRsrcEditPost' + ksActionGlobalRsrcDel = 'GlobalRsrcDelPost' + + ksActionBuildList = 'BuildList' + ksActionBuildAdd = 'BuildAdd' + ksActionBuildAddPost = 'BuildAddPost' + ksActionBuildClone = 'BuildClone' + ksActionBuildDetails = 'BuildDetails' + ksActionBuildDoRemove = 'BuildDoRemove' + ksActionBuildEdit = 'BuildEdit' + ksActionBuildEditPost = 'BuildEditPost' + + ksActionBuildBlacklist = 'BuildBlacklist'; + ksActionBuildBlacklistAdd = 'BuildBlacklistAdd'; + ksActionBuildBlacklistAddPost = 'BuildBlacklistAddPost'; + ksActionBuildBlacklistClone = 'BuildBlacklistClone'; + ksActionBuildBlacklistDetails = 'BuildBlacklistDetails'; + ksActionBuildBlacklistDoRemove = 'BuildBlacklistDoRemove'; + ksActionBuildBlacklistEdit = 'BuildBlacklistEdit'; + ksActionBuildBlacklistEditPost = 'BuildBlacklistEditPost'; + + ksActionFailureCategoryList = 'FailureCategoryList'; + ksActionFailureCategoryAdd = 'FailureCategoryAdd'; + ksActionFailureCategoryAddPost = 'FailureCategoryAddPost'; + ksActionFailureCategoryDetails = 'FailureCategoryDetails'; + ksActionFailureCategoryDoRemove = 'FailureCategoryDoRemove'; + ksActionFailureCategoryEdit = 'FailureCategoryEdit'; + ksActionFailureCategoryEditPost = 'FailureCategoryEditPost'; + + ksActionFailureReasonList = 'FailureReasonList' + ksActionFailureReasonAdd = 'FailureReasonAdd' + ksActionFailureReasonAddPost = 'FailureReasonAddPost' + ksActionFailureReasonDetails = 'FailureReasonDetails' + ksActionFailureReasonDoRemove = 'FailureReasonDoRemove' + ksActionFailureReasonEdit = 'FailureReasonEdit' + ksActionFailureReasonEditPost = 'FailureReasonEditPost' + + ksActionBuildSrcList = 'BuildSrcList' + ksActionBuildSrcAdd = 'BuildSrcAdd' + ksActionBuildSrcAddPost = 'BuildSrcAddPost' + ksActionBuildSrcClone = 'BuildSrcClone' + ksActionBuildSrcDetails = 'BuildSrcDetails' + ksActionBuildSrcEdit = 'BuildSrcEdit' + ksActionBuildSrcEditPost = 'BuildSrcEditPost' + ksActionBuildSrcDoRemove = 'BuildSrcDoRemove' + + ksActionBuildCategoryList = 'BuildCategoryList' + ksActionBuildCategoryAdd = 'BuildCategoryAdd' + ksActionBuildCategoryAddPost = 'BuildCategoryAddPost' + ksActionBuildCategoryClone = 'BuildCategoryClone'; + ksActionBuildCategoryDetails = 'BuildCategoryDetails'; + ksActionBuildCategoryDoRemove = 'BuildCategoryDoRemove'; + + ksActionTestGroupList = 'TestGroupList' + ksActionTestGroupAdd = 'TestGroupAdd' + ksActionTestGroupAddPost = 'TestGroupAddPost' + ksActionTestGroupClone = 'TestGroupClone' + ksActionTestGroupDetails = 'TestGroupDetails' + ksActionTestGroupDoRemove = 'TestGroupDoRemove' + ksActionTestGroupEdit = 'TestGroupEdit' + ksActionTestGroupEditPost = 'TestGroupEditPost' + ksActionTestCfgRegenQueues = 'TestCfgRegenQueues' + + ksActionSchedGroupList = 'SchedGroupList' + ksActionSchedGroupAdd = 'SchedGroupAdd'; + ksActionSchedGroupAddPost = 'SchedGroupAddPost'; + ksActionSchedGroupClone = 'SchedGroupClone'; + ksActionSchedGroupDetails = 'SchedGroupDetails'; + ksActionSchedGroupDoRemove = 'SchedGroupDel'; + ksActionSchedGroupEdit = 'SchedGroupEdit'; + ksActionSchedGroupEditPost = 'SchedGroupEditPost'; + ksActionSchedQueueList = 'SchedQueueList'; + ## @} + + def __init__(self, oSrvGlue): # pylint: disable=too-many-locals,too-many-statements + WuiDispatcherBase.__init__(self, oSrvGlue, self.ksScriptName); + self._sTemplate = 'template.html'; + + + # + # System actions. + # + self._dDispatch[self.ksActionSystemChangelogList] = self._actionSystemChangelogList; + self._dDispatch[self.ksActionSystemLogList] = self._actionSystemLogList; + self._dDispatch[self.ksActionSystemDbDump] = self._actionSystemDbDump; + self._dDispatch[self.ksActionSystemDbDumpDownload] = self._actionSystemDbDumpDownload; + + # + # User Account actions. + # + self._dDispatch[self.ksActionUserList] = self._actionUserList; + self._dDispatch[self.ksActionUserAdd] = self._actionUserAdd; + self._dDispatch[self.ksActionUserEdit] = self._actionUserEdit; + self._dDispatch[self.ksActionUserAddPost] = self._actionUserAddPost; + self._dDispatch[self.ksActionUserEditPost] = self._actionUserEditPost; + self._dDispatch[self.ksActionUserDetails] = self._actionUserDetails; + self._dDispatch[self.ksActionUserDelPost] = self._actionUserDelPost; + + # + # TestBox actions. + # + self._dDispatch[self.ksActionTestBoxList] = self._actionTestBoxList; + self._dDispatch[self.ksActionTestBoxListPost] = self._actionTestBoxListPost; + self._dDispatch[self.ksActionTestBoxAdd] = self._actionTestBoxAdd; + self._dDispatch[self.ksActionTestBoxAddPost] = self._actionTestBoxAddPost; + self._dDispatch[self.ksActionTestBoxDetails] = self._actionTestBoxDetails; + self._dDispatch[self.ksActionTestBoxEdit] = self._actionTestBoxEdit; + self._dDispatch[self.ksActionTestBoxEditPost] = self._actionTestBoxEditPost; + self._dDispatch[self.ksActionTestBoxRemovePost] = self._actionTestBoxRemovePost; + self._dDispatch[self.ksActionTestBoxesRegenQueues] = self._actionRegenQueuesCommon; + + # + # Test Case actions. + # + self._dDispatch[self.ksActionTestCaseList] = self._actionTestCaseList; + self._dDispatch[self.ksActionTestCaseAdd] = self._actionTestCaseAdd; + self._dDispatch[self.ksActionTestCaseAddPost] = self._actionTestCaseAddPost; + self._dDispatch[self.ksActionTestCaseClone] = self._actionTestCaseClone; + self._dDispatch[self.ksActionTestCaseDetails] = self._actionTestCaseDetails; + self._dDispatch[self.ksActionTestCaseEdit] = self._actionTestCaseEdit; + self._dDispatch[self.ksActionTestCaseEditPost] = self._actionTestCaseEditPost; + self._dDispatch[self.ksActionTestCaseDoRemove] = self._actionTestCaseDoRemove; + + # + # Global Resource actions + # + self._dDispatch[self.ksActionGlobalRsrcShowAll] = self._actionGlobalRsrcShowAll; + self._dDispatch[self.ksActionGlobalRsrcShowAdd] = self._actionGlobalRsrcShowAdd; + self._dDispatch[self.ksActionGlobalRsrcShowEdit] = self._actionGlobalRsrcShowEdit; + self._dDispatch[self.ksActionGlobalRsrcAdd] = self._actionGlobalRsrcAdd; + self._dDispatch[self.ksActionGlobalRsrcEdit] = self._actionGlobalRsrcEdit; + self._dDispatch[self.ksActionGlobalRsrcDel] = self._actionGlobalRsrcDel; + + # + # Build Source actions + # + self._dDispatch[self.ksActionBuildSrcList] = self._actionBuildSrcList; + self._dDispatch[self.ksActionBuildSrcAdd] = self._actionBuildSrcAdd; + self._dDispatch[self.ksActionBuildSrcAddPost] = self._actionBuildSrcAddPost; + self._dDispatch[self.ksActionBuildSrcClone] = self._actionBuildSrcClone; + self._dDispatch[self.ksActionBuildSrcDetails] = self._actionBuildSrcDetails; + self._dDispatch[self.ksActionBuildSrcDoRemove] = self._actionBuildSrcDoRemove; + self._dDispatch[self.ksActionBuildSrcEdit] = self._actionBuildSrcEdit; + self._dDispatch[self.ksActionBuildSrcEditPost] = self._actionBuildSrcEditPost; + + # + # Build actions + # + self._dDispatch[self.ksActionBuildList] = self._actionBuildList; + self._dDispatch[self.ksActionBuildAdd] = self._actionBuildAdd; + self._dDispatch[self.ksActionBuildAddPost] = self._actionBuildAddPost; + self._dDispatch[self.ksActionBuildClone] = self._actionBuildClone; + self._dDispatch[self.ksActionBuildDetails] = self._actionBuildDetails; + self._dDispatch[self.ksActionBuildDoRemove] = self._actionBuildDoRemove; + self._dDispatch[self.ksActionBuildEdit] = self._actionBuildEdit; + self._dDispatch[self.ksActionBuildEditPost] = self._actionBuildEditPost; + + # + # Build Black List actions + # + self._dDispatch[self.ksActionBuildBlacklist] = self._actionBuildBlacklist; + self._dDispatch[self.ksActionBuildBlacklistAdd] = self._actionBuildBlacklistAdd; + self._dDispatch[self.ksActionBuildBlacklistAddPost] = self._actionBuildBlacklistAddPost; + self._dDispatch[self.ksActionBuildBlacklistClone] = self._actionBuildBlacklistClone; + self._dDispatch[self.ksActionBuildBlacklistDetails] = self._actionBuildBlacklistDetails; + self._dDispatch[self.ksActionBuildBlacklistDoRemove] = self._actionBuildBlacklistDoRemove; + self._dDispatch[self.ksActionBuildBlacklistEdit] = self._actionBuildBlacklistEdit; + self._dDispatch[self.ksActionBuildBlacklistEditPost] = self._actionBuildBlacklistEditPost; + + # + # Failure Category actions + # + self._dDispatch[self.ksActionFailureCategoryList] = self._actionFailureCategoryList; + self._dDispatch[self.ksActionFailureCategoryAdd] = self._actionFailureCategoryAdd; + self._dDispatch[self.ksActionFailureCategoryAddPost] = self._actionFailureCategoryAddPost; + self._dDispatch[self.ksActionFailureCategoryDetails] = self._actionFailureCategoryDetails; + self._dDispatch[self.ksActionFailureCategoryDoRemove] = self._actionFailureCategoryDoRemove; + self._dDispatch[self.ksActionFailureCategoryEdit] = self._actionFailureCategoryEdit; + self._dDispatch[self.ksActionFailureCategoryEditPost] = self._actionFailureCategoryEditPost; + + # + # Failure Reason actions + # + self._dDispatch[self.ksActionFailureReasonList] = self._actionFailureReasonList; + self._dDispatch[self.ksActionFailureReasonAdd] = self._actionFailureReasonAdd; + self._dDispatch[self.ksActionFailureReasonAddPost] = self._actionFailureReasonAddPost; + self._dDispatch[self.ksActionFailureReasonDetails] = self._actionFailureReasonDetails; + self._dDispatch[self.ksActionFailureReasonDoRemove] = self._actionFailureReasonDoRemove; + self._dDispatch[self.ksActionFailureReasonEdit] = self._actionFailureReasonEdit; + self._dDispatch[self.ksActionFailureReasonEditPost] = self._actionFailureReasonEditPost; + + # + # Build Category actions + # + self._dDispatch[self.ksActionBuildCategoryList] = self._actionBuildCategoryList; + self._dDispatch[self.ksActionBuildCategoryAdd] = self._actionBuildCategoryAdd; + self._dDispatch[self.ksActionBuildCategoryAddPost] = self._actionBuildCategoryAddPost; + self._dDispatch[self.ksActionBuildCategoryClone] = self._actionBuildCategoryClone; + self._dDispatch[self.ksActionBuildCategoryDetails] = self._actionBuildCategoryDetails; + self._dDispatch[self.ksActionBuildCategoryDoRemove] = self._actionBuildCategoryDoRemove; + + # + # Test Group actions + # + self._dDispatch[self.ksActionTestGroupList] = self._actionTestGroupList; + self._dDispatch[self.ksActionTestGroupAdd] = self._actionTestGroupAdd; + self._dDispatch[self.ksActionTestGroupAddPost] = self._actionTestGroupAddPost; + self._dDispatch[self.ksActionTestGroupClone] = self._actionTestGroupClone; + self._dDispatch[self.ksActionTestGroupDetails] = self._actionTestGroupDetails; + self._dDispatch[self.ksActionTestGroupEdit] = self._actionTestGroupEdit; + self._dDispatch[self.ksActionTestGroupEditPost] = self._actionTestGroupEditPost; + self._dDispatch[self.ksActionTestGroupDoRemove] = self._actionTestGroupDoRemove; + self._dDispatch[self.ksActionTestCfgRegenQueues] = self._actionRegenQueuesCommon; + + # + # Scheduling Group and Queue actions + # + self._dDispatch[self.ksActionSchedGroupList] = self._actionSchedGroupList; + self._dDispatch[self.ksActionSchedGroupAdd] = self._actionSchedGroupAdd; + self._dDispatch[self.ksActionSchedGroupClone] = self._actionSchedGroupClone; + self._dDispatch[self.ksActionSchedGroupDetails] = self._actionSchedGroupDetails; + self._dDispatch[self.ksActionSchedGroupEdit] = self._actionSchedGroupEdit; + self._dDispatch[self.ksActionSchedGroupAddPost] = self._actionSchedGroupAddPost; + self._dDispatch[self.ksActionSchedGroupEditPost] = self._actionSchedGroupEditPost; + self._dDispatch[self.ksActionSchedGroupDoRemove] = self._actionSchedGroupDoRemove; + self._dDispatch[self.ksActionSchedQueueList] = self._actionSchedQueueList; + + + # + # Menus + # + self._aaoMenus = \ + [ + [ + 'Builds', self._sActionUrlBase + self.ksActionBuildList, + [ + [ 'Builds', self._sActionUrlBase + self.ksActionBuildList, False ], + [ 'Blacklist', self._sActionUrlBase + self.ksActionBuildBlacklist, False ], + [ 'Build sources', self._sActionUrlBase + self.ksActionBuildSrcList, False ], + [ 'Build categories', self._sActionUrlBase + self.ksActionBuildCategoryList, False ], + [ 'New build', self._sActionUrlBase + self.ksActionBuildAdd, True ], + [ 'New blacklisting', self._sActionUrlBase + self.ksActionBuildBlacklistAdd, True ], + [ 'New build source', self._sActionUrlBase + self.ksActionBuildSrcAdd, True ], + [ 'New build category', self._sActionUrlBase + self.ksActionBuildCategoryAdd, True ], + ] + ], + [ + 'Failure Reasons', self._sActionUrlBase + self.ksActionFailureReasonList, + [ + [ 'Failure categories', self._sActionUrlBase + self.ksActionFailureCategoryList, False ], + [ 'Failure reasons', self._sActionUrlBase + self.ksActionFailureReasonList, False ], + [ 'New failure category', self._sActionUrlBase + self.ksActionFailureCategoryAdd, True ], + [ 'New failure reason', self._sActionUrlBase + self.ksActionFailureReasonAdd, True ], + ] + ], + [ + 'System', self._sActionUrlBase + self.ksActionSystemChangelogList, + [ + [ 'Changelog', self._sActionUrlBase + self.ksActionSystemChangelogList, False ], + [ 'System log', self._sActionUrlBase + self.ksActionSystemLogList, False ], + [ 'Partial DB Dump', self._sActionUrlBase + self.ksActionSystemDbDump, False ], + [ 'User accounts', self._sActionUrlBase + self.ksActionUserList, False ], + [ 'New user', self._sActionUrlBase + self.ksActionUserAdd, True ], + ] + ], + [ + 'Testboxes', self._sActionUrlBase + self.ksActionTestBoxList, + [ + [ 'Testboxes', self._sActionUrlBase + self.ksActionTestBoxList, False ], + [ 'Scheduling groups', self._sActionUrlBase + self.ksActionSchedGroupList, False ], + [ 'New testbox', self._sActionUrlBase + self.ksActionTestBoxAdd, True ], + [ 'New scheduling group', self._sActionUrlBase + self.ksActionSchedGroupAdd, True ], + [ 'View scheduling queues', self._sActionUrlBase + self.ksActionSchedQueueList, False ], + [ 'Regenerate all scheduling queues', self._sActionUrlBase + self.ksActionTestBoxesRegenQueues, True ], + ] + ], + [ + 'Test Config', self._sActionUrlBase + self.ksActionTestGroupList, + [ + [ 'Test cases', self._sActionUrlBase + self.ksActionTestCaseList, False ], + [ 'Test groups', self._sActionUrlBase + self.ksActionTestGroupList, False ], + [ 'Global resources', self._sActionUrlBase + self.ksActionGlobalRsrcShowAll, False ], + [ 'New test case', self._sActionUrlBase + self.ksActionTestCaseAdd, True ], + [ 'New test group', self._sActionUrlBase + self.ksActionTestGroupAdd, True ], + [ 'New global resource', self._sActionUrlBase + self.ksActionGlobalRsrcShowAdd, True ], + [ 'Regenerate all scheduling queues', self._sActionUrlBase + self.ksActionTestCfgRegenQueues, True ], + ] + ], + [ + '> Test Results', 'index.py?' + webutils.encodeUrlParams(self._dDbgParams), [] + ], + ]; + + + def _actionDefault(self): + """Show the default admin page.""" + self._sAction = self.ksActionTestBoxList; + from testmanager.core.testbox import TestBoxLogic; + from testmanager.webui.wuiadmintestbox import WuiTestBoxList; + return self._actionGenericListing(TestBoxLogic, WuiTestBoxList); + + def _actionGenericDoDelOld(self, oCoreObjectLogic, sCoreObjectIdFieldName, sRedirectAction): + """ + Delete entry (using oLogicType.remove). + + @param oCoreObjectLogic A *Logic class + + @param sCoreObjectIdFieldName Name of HTTP POST variable that + contains object ID information + + @param sRedirectAction An action for redirect user to + in case of operation success + """ + iCoreDataObjectId = self.getStringParam(sCoreObjectIdFieldName) # STRING?!?! + self._checkForUnknownParameters() + + try: + self._sPageTitle = None + self._sPageBody = None + self._sRedirectTo = self._sActionUrlBase + sRedirectAction + return oCoreObjectLogic(self._oDb).remove(self._oCurUser.uid, iCoreDataObjectId) + except Exception as oXcpt: + self._oDb.rollback(); + self._sPageTitle = 'Unable to delete record' + self._sPageBody = str(oXcpt); + if config.g_kfDebugDbXcpt: + self._sPageBody += cgitb.html(sys.exc_info()); + self._sRedirectTo = None + + return False + + + # + # System Category. + # + + # System wide changelog actions. + + def _actionSystemChangelogList(self): + """ Action handler. """ + from testmanager.core.systemchangelog import SystemChangelogLogic; + from testmanager.webui.wuiadminsystemchangelog import WuiAdminSystemChangelogList; + + tsEffective = self.getEffectiveDateParam(); + cItemsPerPage = self.getIntParam(self.ksParamItemsPerPage, iMin = 2, iMax = 9999, iDefault = 384); + iPage = self.getIntParam(self.ksParamPageNo, iMin = 0, iMax = 999999, iDefault = 0); + cDaysBack = self.getIntParam(self.ksParamDaysBack, iMin = 1, iMax = 366, iDefault = 14); + self._checkForUnknownParameters(); + + aoEntries = SystemChangelogLogic(self._oDb).fetchForListingEx(iPage * cItemsPerPage, cItemsPerPage + 1, + tsEffective, cDaysBack); + oContent = WuiAdminSystemChangelogList(aoEntries, iPage, cItemsPerPage, tsEffective, + cDaysBack = cDaysBack, fnDPrint = self._oSrvGlue.dprint, oDisp = self); + (self._sPageTitle, self._sPageBody) = oContent.show(); + return True; + + # System Log actions. + + def _actionSystemLogList(self): + """ Action wrapper. """ + from testmanager.core.systemlog import SystemLogLogic; + from testmanager.webui.wuiadminsystemlog import WuiAdminSystemLogList; + return self._actionGenericListing(SystemLogLogic, WuiAdminSystemLogList) + + def _actionSystemDbDump(self): + """ Action handler. """ + from testmanager.webui.wuiadminsystemdbdump import WuiAdminSystemDbDumpForm; + + cDaysBack = self.getIntParam(self.ksParamDaysBack, iMin = config.g_kcTmDbDumpMinDays, + iMax = config.g_kcTmDbDumpMaxDays, iDefault = config.g_kcTmDbDumpDefaultDays); + self._checkForUnknownParameters(); + + oContent = WuiAdminSystemDbDumpForm(cDaysBack, oDisp = self); + (self._sPageTitle, self._sPageBody) = oContent.showForm(); + return True; + + def _actionSystemDbDumpDownload(self): + """ Action handler. """ + import datetime; + import os; + + cDaysBack = self.getIntParam(self.ksParamDaysBack, iMin = config.g_kcTmDbDumpMinDays, + iMax = config.g_kcTmDbDumpMaxDays, iDefault = config.g_kcTmDbDumpDefaultDays); + self._checkForUnknownParameters(); + + # + # Generate the dump. + # + # We generate a file name that's unique to a user is smart enough to only + # issue one of these requests at the time. This also makes sure we won't + # waste too much space should this code get interrupted and rerun. + # + oFile = None; + oNow = datetime.datetime.utcnow(); + sOutFile = config.g_ksTmDbDumpOutFileTmpl % (self._oCurUser.uid,); + sTmpFile = config.g_ksTmDbDumpTmpFileTmpl % (self._oCurUser.uid,); + sScript = os.path.join(config.g_ksTestManagerDir, 'db', 'partial-db-dump.py'); + try: + (iExitCode, sStdOut, sStdErr) = utils.processOutputUnchecked([ sScript, + '--days-to-dump', str(cDaysBack), + '-f', sOutFile, + '-t', sTmpFile, + ]); + if iExitCode != 0: + raise Exception('iExitCode=%s\n--- stderr ---\n%s\n--- stdout ---\n%s' % (iExitCode, sStdOut, sStdErr,)); + + # + # Open and send the dump. + # + oFile = open(sOutFile, 'rb'); # pylint: disable=consider-using-with + cbFile = os.fstat(oFile.fileno()).st_size; + + self._oSrvGlue.setHeaderField('Content-Type', 'application/zip'); + self._oSrvGlue.setHeaderField('Content-Disposition', + oNow.strftime('attachment; filename="partial-db-dump-%Y-%m-%dT%H-%M-%S.zip"')); + self._oSrvGlue.setHeaderField('Content-Length', str(cbFile)); + + while True: + abChunk = oFile.read(262144); + if not abChunk: + break; + self._oSrvGlue.writeRaw(abChunk); + + finally: + # Delete the file to save space. + if oFile: + try: oFile.close(); + except: pass; + utils.noxcptDeleteFile(sOutFile); + utils.noxcptDeleteFile(sTmpFile); + return self.ksDispatchRcAllDone; + + + # User Account actions. + + def _actionUserList(self): + """ Action wrapper. """ + from testmanager.core.useraccount import UserAccountLogic; + from testmanager.webui.wuiadminuseraccount import WuiUserAccountList; + return self._actionGenericListing(UserAccountLogic, WuiUserAccountList) + + def _actionUserAdd(self): + """ Action wrapper. """ + from testmanager.core.useraccount import UserAccountData; + from testmanager.webui.wuiadminuseraccount import WuiUserAccount; + return self._actionGenericFormAdd(UserAccountData, WuiUserAccount) + + def _actionUserDetails(self): + """ Action wrapper. """ + from testmanager.core.useraccount import UserAccountData, UserAccountLogic; + from testmanager.webui.wuiadminuseraccount import WuiUserAccount; + return self._actionGenericFormDetails(UserAccountData, UserAccountLogic, WuiUserAccount, 'uid'); + + def _actionUserEdit(self): + """ Action wrapper. """ + from testmanager.core.useraccount import UserAccountData; + from testmanager.webui.wuiadminuseraccount import WuiUserAccount; + return self._actionGenericFormEdit(UserAccountData, WuiUserAccount, UserAccountData.ksParam_uid); + + def _actionUserAddPost(self): + """ Action wrapper. """ + from testmanager.core.useraccount import UserAccountData, UserAccountLogic; + from testmanager.webui.wuiadminuseraccount import WuiUserAccount; + return self._actionGenericFormAddPost(UserAccountData, UserAccountLogic, WuiUserAccount, self.ksActionUserList) + + def _actionUserEditPost(self): + """ Action wrapper. """ + from testmanager.core.useraccount import UserAccountData, UserAccountLogic; + from testmanager.webui.wuiadminuseraccount import WuiUserAccount; + return self._actionGenericFormEditPost(UserAccountData, UserAccountLogic, WuiUserAccount, self.ksActionUserList) + + def _actionUserDelPost(self): + """ Action wrapper. """ + from testmanager.core.useraccount import UserAccountData, UserAccountLogic; + return self._actionGenericDoRemove(UserAccountLogic, UserAccountData.ksParam_uid, self.ksActionUserList) + + + # + # TestBox & Scheduling Category. + # + + def _actionTestBoxList(self): + """ Action wrapper. """ + from testmanager.core.testbox import TestBoxLogic + from testmanager.webui.wuiadmintestbox import WuiTestBoxList; + return self._actionGenericListing(TestBoxLogic, WuiTestBoxList); + + def _actionTestBoxListPost(self): + """Actions on a list of testboxes.""" + from testmanager.core.testbox import TestBoxData, TestBoxLogic + from testmanager.webui.wuiadmintestbox import WuiTestBoxList; + + # Parameters. + aidTestBoxes = self.getListOfIntParams(TestBoxData.ksParam_idTestBox, iMin = 1, aiDefaults = []); + sListAction = self.getStringParam(self.ksParamListAction); + if sListAction in [asDesc[0] for asDesc in WuiTestBoxList.kasTestBoxActionDescs]: + idAction = None; + else: + asActionPrefixes = [ 'setgroup-', ]; + i = 0; + while i < len(asActionPrefixes) and not sListAction.startswith(asActionPrefixes[i]): + i += 1; + if i >= len(asActionPrefixes): + raise WuiException('Parameter "%s" has an invalid value: "%s"' % (self.ksParamListAction, sListAction,)); + idAction = sListAction[len(asActionPrefixes[i]):]; + if not idAction.isdigit(): + raise WuiException('Parameter "%s" has an invalid value: "%s"' % (self.ksParamListAction, sListAction,)); + idAction = int(idAction); + sListAction = sListAction[:len(asActionPrefixes[i]) - 1]; + self._checkForUnknownParameters(); + + + # Take action. + if sListAction == 'none': + pass; + else: + oLogic = TestBoxLogic(self._oDb); + aoTestBoxes = [] + for idTestBox in aidTestBoxes: + aoTestBoxes.append(TestBoxData().initFromDbWithId(self._oDb, idTestBox)); + + if sListAction in [ 'enable', 'disable' ]: + fEnable = sListAction == 'enable'; + for oTestBox in aoTestBoxes: + if oTestBox.fEnabled != fEnable: + oTestBox.fEnabled = fEnable; + oLogic.editEntry(oTestBox, self._oCurUser.uid, fCommit = False); + else: + for oTestBox in aoTestBoxes: + if oTestBox.enmPendingCmd != sListAction: + oLogic.setCommand(oTestBox.idTestBox, oTestBox.enmPendingCmd, sListAction, self._oCurUser.uid, + fCommit = False); + self._oDb.commit(); + + # Re-display the list. + self._sPageTitle = None; + self._sPageBody = None; + self._sRedirectTo = self._sActionUrlBase + self.ksActionTestBoxList; + return True; + + def _actionTestBoxAdd(self): + """ Action wrapper. """ + from testmanager.core.testbox import TestBoxDataEx; + from testmanager.webui.wuiadmintestbox import WuiTestBox; + return self._actionGenericFormAdd(TestBoxDataEx, WuiTestBox); + + def _actionTestBoxAddPost(self): + """ Action wrapper. """ + from testmanager.core.testbox import TestBoxDataEx, TestBoxLogic; + from testmanager.webui.wuiadmintestbox import WuiTestBox; + return self._actionGenericFormAddPost(TestBoxDataEx, TestBoxLogic, WuiTestBox, self.ksActionTestBoxList); + + def _actionTestBoxDetails(self): + """ Action wrapper. """ + from testmanager.core.testbox import TestBoxDataEx, TestBoxLogic; + from testmanager.webui.wuiadmintestbox import WuiTestBox; + return self._actionGenericFormDetails(TestBoxDataEx, TestBoxLogic, WuiTestBox, 'idTestBox', 'idGenTestBox'); + + def _actionTestBoxEdit(self): + """ Action wrapper. """ + from testmanager.core.testbox import TestBoxDataEx; + from testmanager.webui.wuiadmintestbox import WuiTestBox; + return self._actionGenericFormEdit(TestBoxDataEx, WuiTestBox, TestBoxDataEx.ksParam_idTestBox); + + def _actionTestBoxEditPost(self): + """ Action wrapper. """ + from testmanager.core.testbox import TestBoxDataEx, TestBoxLogic; + from testmanager.webui.wuiadmintestbox import WuiTestBox; + return self._actionGenericFormEditPost(TestBoxDataEx, TestBoxLogic,WuiTestBox, self.ksActionTestBoxList); + + def _actionTestBoxRemovePost(self): + """ Action wrapper. """ + from testmanager.core.testbox import TestBoxData, TestBoxLogic; + return self._actionGenericDoRemove(TestBoxLogic, TestBoxData.ksParam_idTestBox, self.ksActionTestBoxList); + + + # Scheduling Group actions + + def _actionSchedGroupList(self): + """ Action wrapper. """ + from testmanager.core.schedgroup import SchedGroupLogic; + from testmanager.webui.wuiadminschedgroup import WuiAdminSchedGroupList; + return self._actionGenericListing(SchedGroupLogic, WuiAdminSchedGroupList); + + def _actionSchedGroupAdd(self): + """ Action wrapper. """ + from testmanager.core.schedgroup import SchedGroupDataEx; + from testmanager.webui.wuiadminschedgroup import WuiSchedGroup; + return self._actionGenericFormAdd(SchedGroupDataEx, WuiSchedGroup); + + def _actionSchedGroupClone(self): + """ Action wrapper. """ + from testmanager.core.schedgroup import SchedGroupDataEx; + from testmanager.webui.wuiadminschedgroup import WuiSchedGroup; + return self._actionGenericFormClone( SchedGroupDataEx, WuiSchedGroup, 'idSchedGroup'); + + def _actionSchedGroupDetails(self): + """ Action wrapper. """ + from testmanager.core.schedgroup import SchedGroupDataEx, SchedGroupLogic; + from testmanager.webui.wuiadminschedgroup import WuiSchedGroup; + return self._actionGenericFormDetails(SchedGroupDataEx, SchedGroupLogic, WuiSchedGroup, 'idSchedGroup'); + + def _actionSchedGroupEdit(self): + """ Action wrapper. """ + from testmanager.core.schedgroup import SchedGroupDataEx; + from testmanager.webui.wuiadminschedgroup import WuiSchedGroup; + return self._actionGenericFormEdit(SchedGroupDataEx, WuiSchedGroup, SchedGroupDataEx.ksParam_idSchedGroup); + + def _actionSchedGroupAddPost(self): + """ Action wrapper. """ + from testmanager.core.schedgroup import SchedGroupDataEx, SchedGroupLogic; + from testmanager.webui.wuiadminschedgroup import WuiSchedGroup; + return self._actionGenericFormAddPost(SchedGroupDataEx, SchedGroupLogic, WuiSchedGroup, self.ksActionSchedGroupList); + + def _actionSchedGroupEditPost(self): + """ Action wrapper. """ + from testmanager.core.schedgroup import SchedGroupDataEx, SchedGroupLogic; + from testmanager.webui.wuiadminschedgroup import WuiSchedGroup; + return self._actionGenericFormEditPost(SchedGroupDataEx, SchedGroupLogic, WuiSchedGroup, self.ksActionSchedGroupList); + + def _actionSchedGroupDoRemove(self): + """ Action wrapper. """ + from testmanager.core.schedgroup import SchedGroupData, SchedGroupLogic; + return self._actionGenericDoRemove(SchedGroupLogic, SchedGroupData.ksParam_idSchedGroup, self.ksActionSchedGroupList) + + def _actionSchedQueueList(self): + """ Action wrapper. """ + from testmanager.core.schedqueue import SchedQueueLogic; + from testmanager.webui.wuiadminschedqueue import WuiAdminSchedQueueList; + return self._actionGenericListing(SchedQueueLogic, WuiAdminSchedQueueList); + + def _actionRegenQueuesCommon(self): + """ + Common code for ksActionTestBoxesRegenQueues and ksActionTestCfgRegenQueues. + + Too lazy to put this in some separate place right now. + """ + from testmanager.core.schedgroup import SchedGroupLogic; + from testmanager.core.schedulerbase import SchedulerBase; + + self._checkForUnknownParameters(); + ## @todo should also be changed to a POST with a confirmation dialog preceeding it. + + self._sPageTitle = 'Regenerate All Scheduling Queues'; + if not self.isReadOnlyUser(): + self._sPageBody = ''; + aoGroups = SchedGroupLogic(self._oDb).getAll(); + for oGroup in aoGroups: + self._sPageBody += '<h3>%s (ID %#d)</h3>' % (webutils.escapeElem(oGroup.sName), oGroup.idSchedGroup); + try: + (aoErrors, asMessages) = SchedulerBase.recreateQueue(self._oDb, self._oCurUser.uid, oGroup.idSchedGroup, 2); + except Exception as oXcpt: + self._oDb.rollback(); + self._sPageBody += '<p>SchedulerBase.recreateQueue threw an exception: %s</p>' \ + % (webutils.escapeElem(str(oXcpt)),); + self._sPageBody += cgitb.html(sys.exc_info()); + else: + if not aoErrors: + self._sPageBody += '<p>Successfully regenerated.</p>'; + else: + for oError in aoErrors: + if oError[1] is None: + self._sPageBody += '<p>%s.</p>' % (webutils.escapeElem(oError[0]),); + ## @todo links. + #elif isinstance(oError[1], TestGroupData): + # self._sPageBody += '<p>%s.</p>' % (webutils.escapeElem(oError[0]),); + #elif isinstance(oError[1], TestGroupCase): + # self._sPageBody += '<p>%s.</p>' % (webutils.escapeElem(oError[0]),); + else: + self._sPageBody += '<p>%s. [Cannot link to %s]</p>' \ + % (webutils.escapeElem(oError[0]), webutils.escapeElem(str(oError[1])),); + for sMsg in asMessages: + self._sPageBody += '<p>%s<p>\n' % (webutils.escapeElem(sMsg),); + + # Remove leftovers from deleted scheduling groups. + self._sPageBody += '<h3>Cleanups</h3>\n'; + cOrphans = SchedulerBase.cleanUpOrphanedQueues(self._oDb); + self._sPageBody += '<p>Removed %s orphaned (deleted) queue%s.<p>\n' % (cOrphans, '' if cOrphans == 1 else 's', ); + else: + self._sPageBody = webutils.escapeElem('%s is a read only user and may not regenerate the scheduling queues!' + % (self._oCurUser.sUsername,)); + return True; + + + + # + # Test Config Category. + # + + # Test Cases + + def _actionTestCaseList(self): + """ Action wrapper. """ + from testmanager.core.testcase import TestCaseLogic; + from testmanager.webui.wuiadmintestcase import WuiTestCaseList; + return self._actionGenericListing(TestCaseLogic, WuiTestCaseList); + + def _actionTestCaseAdd(self): + """ Action wrapper. """ + from testmanager.core.testcase import TestCaseDataEx; + from testmanager.webui.wuiadmintestcase import WuiTestCase; + return self._actionGenericFormAdd(TestCaseDataEx, WuiTestCase); + + def _actionTestCaseAddPost(self): + """ Action wrapper. """ + from testmanager.core.testcase import TestCaseDataEx, TestCaseLogic; + from testmanager.webui.wuiadmintestcase import WuiTestCase; + return self._actionGenericFormAddPost(TestCaseDataEx, TestCaseLogic, WuiTestCase, self.ksActionTestCaseList); + + def _actionTestCaseClone(self): + """ Action wrapper. """ + from testmanager.core.testcase import TestCaseDataEx; + from testmanager.webui.wuiadmintestcase import WuiTestCase; + return self._actionGenericFormClone( TestCaseDataEx, WuiTestCase, 'idTestCase', 'idGenTestCase'); + + def _actionTestCaseDetails(self): + """ Action wrapper. """ + from testmanager.core.testcase import TestCaseDataEx, TestCaseLogic; + from testmanager.webui.wuiadmintestcase import WuiTestCase; + return self._actionGenericFormDetails(TestCaseDataEx, TestCaseLogic, WuiTestCase, 'idTestCase', 'idGenTestCase'); + + def _actionTestCaseEdit(self): + """ Action wrapper. """ + from testmanager.core.testcase import TestCaseDataEx; + from testmanager.webui.wuiadmintestcase import WuiTestCase; + return self._actionGenericFormEdit(TestCaseDataEx, WuiTestCase, TestCaseDataEx.ksParam_idTestCase); + + def _actionTestCaseEditPost(self): + """ Action wrapper. """ + from testmanager.core.testcase import TestCaseDataEx, TestCaseLogic; + from testmanager.webui.wuiadmintestcase import WuiTestCase; + return self._actionGenericFormEditPost(TestCaseDataEx, TestCaseLogic, WuiTestCase, self.ksActionTestCaseList); + + def _actionTestCaseDoRemove(self): + """ Action wrapper. """ + from testmanager.core.testcase import TestCaseData, TestCaseLogic; + return self._actionGenericDoRemove(TestCaseLogic, TestCaseData.ksParam_idTestCase, self.ksActionTestCaseList); + + # Test Group actions + + def _actionTestGroupList(self): + """ Action wrapper. """ + from testmanager.core.testgroup import TestGroupLogic; + from testmanager.webui.wuiadmintestgroup import WuiTestGroupList; + return self._actionGenericListing(TestGroupLogic, WuiTestGroupList); + def _actionTestGroupAdd(self): + """ Action wrapper. """ + from testmanager.core.testgroup import TestGroupDataEx; + from testmanager.webui.wuiadmintestgroup import WuiTestGroup; + return self._actionGenericFormAdd(TestGroupDataEx, WuiTestGroup); + def _actionTestGroupAddPost(self): + """ Action wrapper. """ + from testmanager.core.testgroup import TestGroupDataEx, TestGroupLogic; + from testmanager.webui.wuiadmintestgroup import WuiTestGroup; + return self._actionGenericFormAddPost(TestGroupDataEx, TestGroupLogic, WuiTestGroup, self.ksActionTestGroupList); + def _actionTestGroupClone(self): + """ Action wrapper. """ + from testmanager.core.testgroup import TestGroupDataEx; + from testmanager.webui.wuiadmintestgroup import WuiTestGroup; + return self._actionGenericFormClone(TestGroupDataEx, WuiTestGroup, 'idTestGroup'); + def _actionTestGroupDetails(self): + """ Action wrapper. """ + from testmanager.core.testgroup import TestGroupDataEx, TestGroupLogic; + from testmanager.webui.wuiadmintestgroup import WuiTestGroup; + return self._actionGenericFormDetails(TestGroupDataEx, TestGroupLogic, WuiTestGroup, 'idTestGroup'); + def _actionTestGroupEdit(self): + """ Action wrapper. """ + from testmanager.core.testgroup import TestGroupDataEx; + from testmanager.webui.wuiadmintestgroup import WuiTestGroup; + return self._actionGenericFormEdit(TestGroupDataEx, WuiTestGroup, TestGroupDataEx.ksParam_idTestGroup); + def _actionTestGroupEditPost(self): + """ Action wrapper. """ + from testmanager.core.testgroup import TestGroupDataEx, TestGroupLogic; + from testmanager.webui.wuiadmintestgroup import WuiTestGroup; + return self._actionGenericFormEditPost(TestGroupDataEx, TestGroupLogic, WuiTestGroup, self.ksActionTestGroupList); + def _actionTestGroupDoRemove(self): + """ Action wrapper. """ + from testmanager.core.testgroup import TestGroupDataEx, TestGroupLogic; + return self._actionGenericDoRemove(TestGroupLogic, TestGroupDataEx.ksParam_idTestGroup, self.ksActionTestGroupList) + + + # Global Resources + + def _actionGlobalRsrcShowAll(self): + """ Action wrapper. """ + from testmanager.core.globalresource import GlobalResourceLogic; + from testmanager.webui.wuiadminglobalrsrc import WuiGlobalResourceList; + return self._actionGenericListing(GlobalResourceLogic, WuiGlobalResourceList); + + def _actionGlobalRsrcShowAdd(self): + """ Action wrapper. """ + return self._actionGlobalRsrcShowAddEdit(WuiAdmin.ksActionGlobalRsrcAdd); + + def _actionGlobalRsrcShowEdit(self): + """ Action wrapper. """ + return self._actionGlobalRsrcShowAddEdit(WuiAdmin.ksActionGlobalRsrcEdit); + + def _actionGlobalRsrcAdd(self): + """ Action wrapper. """ + return self._actionGlobalRsrcAddEdit(WuiAdmin.ksActionGlobalRsrcAdd); + + def _actionGlobalRsrcEdit(self): + """ Action wrapper. """ + return self._actionGlobalRsrcAddEdit(WuiAdmin.ksActionGlobalRsrcEdit); + + def _actionGlobalRsrcDel(self): + """ Action wrapper. """ + from testmanager.core.globalresource import GlobalResourceData, GlobalResourceLogic; + return self._actionGenericDoDelOld(GlobalResourceLogic, GlobalResourceData.ksParam_idGlobalRsrc, + self.ksActionGlobalRsrcShowAll); + + def _actionGlobalRsrcShowAddEdit(self, sAction): # pylint: disable=invalid-name + """Show Global Resource creation or edit dialog""" + from testmanager.core.globalresource import GlobalResourceLogic, GlobalResourceData; + from testmanager.webui.wuiadminglobalrsrc import WuiGlobalResource; + + oGlobalResourceLogic = GlobalResourceLogic(self._oDb) + if sAction == WuiAdmin.ksActionGlobalRsrcEdit: + idGlobalRsrc = self.getIntParam(GlobalResourceData.ksParam_idGlobalRsrc, iDefault = -1) + oData = oGlobalResourceLogic.getById(idGlobalRsrc) + else: + oData = GlobalResourceData() + oData.convertToParamNull() + + self._checkForUnknownParameters() + + oContent = WuiGlobalResource(oData) + (self._sPageTitle, self._sPageBody) = oContent.showAddModifyPage(sAction) + + return True + + def _actionGlobalRsrcAddEdit(self, sAction): + """Add or modify Global Resource record""" + from testmanager.core.globalresource import GlobalResourceLogic, GlobalResourceData; + from testmanager.webui.wuiadminglobalrsrc import WuiGlobalResource; + + oData = GlobalResourceData() + oData.initFromParams(self, fStrict=True) + + self._checkForUnknownParameters() + + if self._oSrvGlue.getMethod() != 'POST': + raise WuiException('Expected "POST" request, got "%s"' % (self._oSrvGlue.getMethod(),)) + + oGlobalResourceLogic = GlobalResourceLogic(self._oDb) + dErrors = oData.validateAndConvert(self._oDb); + if not dErrors: + if sAction == WuiAdmin.ksActionGlobalRsrcAdd: + oGlobalResourceLogic.addGlobalResource(self._oCurUser.uid, oData) + elif sAction == WuiAdmin.ksActionGlobalRsrcEdit: + idGlobalRsrc = self.getStringParam(GlobalResourceData.ksParam_idGlobalRsrc) + oGlobalResourceLogic.editGlobalResource(self._oCurUser.uid, idGlobalRsrc, oData) + else: + raise WuiException('Invalid parameter.') + self._sPageTitle = None; + self._sPageBody = None; + self._sRedirectTo = self._sActionUrlBase + self.ksActionGlobalRsrcShowAll; + else: + oContent = WuiGlobalResource(oData) + (self._sPageTitle, self._sPageBody) = oContent.showAddModifyPage(sAction, dErrors=dErrors) + + return True + + + # + # Build Source actions + # + + def _actionBuildSrcList(self): + """ Action wrapper. """ + from testmanager.core.buildsource import BuildSourceLogic; + from testmanager.webui.wuiadminbuildsource import WuiAdminBuildSrcList; + return self._actionGenericListing(BuildSourceLogic, WuiAdminBuildSrcList); + + def _actionBuildSrcAdd(self): + """ Action wrapper. """ + from testmanager.core.buildsource import BuildSourceData; + from testmanager.webui.wuiadminbuildsource import WuiAdminBuildSrc; + return self._actionGenericFormAdd(BuildSourceData, WuiAdminBuildSrc); + + def _actionBuildSrcAddPost(self): + """ Action wrapper. """ + from testmanager.core.buildsource import BuildSourceData, BuildSourceLogic; + from testmanager.webui.wuiadminbuildsource import WuiAdminBuildSrc; + return self._actionGenericFormAddPost(BuildSourceData, BuildSourceLogic, WuiAdminBuildSrc, self.ksActionBuildSrcList); + + def _actionBuildSrcClone(self): + """ Action wrapper. """ + from testmanager.core.buildsource import BuildSourceData; + from testmanager.webui.wuiadminbuildsource import WuiAdminBuildSrc; + return self._actionGenericFormClone( BuildSourceData, WuiAdminBuildSrc, 'idBuildSrc'); + + def _actionBuildSrcDetails(self): + """ Action wrapper. """ + from testmanager.core.buildsource import BuildSourceData, BuildSourceLogic; + from testmanager.webui.wuiadminbuildsource import WuiAdminBuildSrc; + return self._actionGenericFormDetails(BuildSourceData, BuildSourceLogic, WuiAdminBuildSrc, 'idBuildSrc'); + + def _actionBuildSrcDoRemove(self): + """ Action wrapper. """ + from testmanager.core.buildsource import BuildSourceData, BuildSourceLogic; + return self._actionGenericDoRemove(BuildSourceLogic, BuildSourceData.ksParam_idBuildSrc, self.ksActionBuildSrcList); + + def _actionBuildSrcEdit(self): + """ Action wrapper. """ + from testmanager.core.buildsource import BuildSourceData; + from testmanager.webui.wuiadminbuildsource import WuiAdminBuildSrc; + return self._actionGenericFormEdit(BuildSourceData, WuiAdminBuildSrc, BuildSourceData.ksParam_idBuildSrc); + + def _actionBuildSrcEditPost(self): + """ Action wrapper. """ + from testmanager.core.buildsource import BuildSourceData, BuildSourceLogic; + from testmanager.webui.wuiadminbuildsource import WuiAdminBuildSrc; + return self._actionGenericFormEditPost(BuildSourceData, BuildSourceLogic, WuiAdminBuildSrc, self.ksActionBuildSrcList); + + + # + # Build actions + # + def _actionBuildList(self): + """ Action wrapper. """ + from testmanager.core.build import BuildLogic; + from testmanager.webui.wuiadminbuild import WuiAdminBuildList; + return self._actionGenericListing(BuildLogic, WuiAdminBuildList); + + def _actionBuildAdd(self): + """ Action wrapper. """ + from testmanager.core.build import BuildData; + from testmanager.webui.wuiadminbuild import WuiAdminBuild; + return self._actionGenericFormAdd(BuildData, WuiAdminBuild); + + def _actionBuildAddPost(self): + """ Action wrapper. """ + from testmanager.core.build import BuildData, BuildLogic; + from testmanager.webui.wuiadminbuild import WuiAdminBuild; + return self._actionGenericFormAddPost(BuildData, BuildLogic, WuiAdminBuild, self.ksActionBuildList); + + def _actionBuildClone(self): + """ Action wrapper. """ + from testmanager.core.build import BuildData; + from testmanager.webui.wuiadminbuild import WuiAdminBuild; + return self._actionGenericFormClone( BuildData, WuiAdminBuild, 'idBuild'); + + def _actionBuildDetails(self): + """ Action wrapper. """ + from testmanager.core.build import BuildData, BuildLogic; + from testmanager.webui.wuiadminbuild import WuiAdminBuild; + return self._actionGenericFormDetails(BuildData, BuildLogic, WuiAdminBuild, 'idBuild'); + + def _actionBuildDoRemove(self): + """ Action wrapper. """ + from testmanager.core.build import BuildData, BuildLogic; + return self._actionGenericDoRemove(BuildLogic, BuildData.ksParam_idBuild, self.ksActionBuildList); + + def _actionBuildEdit(self): + """ Action wrapper. """ + from testmanager.core.build import BuildData; + from testmanager.webui.wuiadminbuild import WuiAdminBuild; + return self._actionGenericFormEdit(BuildData, WuiAdminBuild, BuildData.ksParam_idBuild); + + def _actionBuildEditPost(self): + """ Action wrapper. """ + from testmanager.core.build import BuildData, BuildLogic; + from testmanager.webui.wuiadminbuild import WuiAdminBuild; + return self._actionGenericFormEditPost(BuildData, BuildLogic, WuiAdminBuild, self.ksActionBuildList) + + + # + # Build Category actions + # + def _actionBuildCategoryList(self): + """ Action wrapper. """ + from testmanager.core.build import BuildCategoryLogic; + from testmanager.webui.wuiadminbuildcategory import WuiAdminBuildCatList; + return self._actionGenericListing(BuildCategoryLogic, WuiAdminBuildCatList); + + def _actionBuildCategoryAdd(self): + """ Action wrapper. """ + from testmanager.core.build import BuildCategoryData; + from testmanager.webui.wuiadminbuildcategory import WuiAdminBuildCat; + return self._actionGenericFormAdd(BuildCategoryData, WuiAdminBuildCat); + + def _actionBuildCategoryAddPost(self): + """ Action wrapper. """ + from testmanager.core.build import BuildCategoryData, BuildCategoryLogic; + from testmanager.webui.wuiadminbuildcategory import WuiAdminBuildCat; + return self._actionGenericFormAddPost(BuildCategoryData, BuildCategoryLogic, WuiAdminBuildCat, + self.ksActionBuildCategoryList); + + def _actionBuildCategoryClone(self): + """ Action wrapper. """ + from testmanager.core.build import BuildCategoryData; + from testmanager.webui.wuiadminbuildcategory import WuiAdminBuildCat; + return self._actionGenericFormClone(BuildCategoryData, WuiAdminBuildCat, 'idBuildCategory'); + + def _actionBuildCategoryDetails(self): + """ Action wrapper. """ + from testmanager.core.build import BuildCategoryData, BuildCategoryLogic; + from testmanager.webui.wuiadminbuildcategory import WuiAdminBuildCat; + return self._actionGenericFormDetails(BuildCategoryData, BuildCategoryLogic, WuiAdminBuildCat, 'idBuildCategory'); + + def _actionBuildCategoryDoRemove(self): + """ Action wrapper. """ + from testmanager.core.build import BuildCategoryData, BuildCategoryLogic; + return self._actionGenericDoRemove(BuildCategoryLogic, BuildCategoryData.ksParam_idBuildCategory, + self.ksActionBuildCategoryList) + + + # + # Build Black List actions + # + def _actionBuildBlacklist(self): + """ Action wrapper. """ + from testmanager.core.buildblacklist import BuildBlacklistLogic; + from testmanager.webui.wuiadminbuildblacklist import WuiAdminListOfBlacklistItems; + return self._actionGenericListing(BuildBlacklistLogic, WuiAdminListOfBlacklistItems); + + def _actionBuildBlacklistAdd(self): + """ Action wrapper. """ + from testmanager.core.buildblacklist import BuildBlacklistData; + from testmanager.webui.wuiadminbuildblacklist import WuiAdminBuildBlacklist; + return self._actionGenericFormAdd(BuildBlacklistData, WuiAdminBuildBlacklist); + + def _actionBuildBlacklistAddPost(self): + """ Action wrapper. """ + from testmanager.core.buildblacklist import BuildBlacklistData, BuildBlacklistLogic; + from testmanager.webui.wuiadminbuildblacklist import WuiAdminBuildBlacklist; + return self._actionGenericFormAddPost(BuildBlacklistData, BuildBlacklistLogic, + WuiAdminBuildBlacklist, self.ksActionBuildBlacklist); + + def _actionBuildBlacklistClone(self): + """ Action wrapper. """ + from testmanager.core.buildblacklist import BuildBlacklistData; + from testmanager.webui.wuiadminbuildblacklist import WuiAdminBuildBlacklist; + return self._actionGenericFormClone(BuildBlacklistData, WuiAdminBuildBlacklist, 'idBlacklisting'); + + def _actionBuildBlacklistDetails(self): + """ Action wrapper. """ + from testmanager.core.buildblacklist import BuildBlacklistData, BuildBlacklistLogic; + from testmanager.webui.wuiadminbuildblacklist import WuiAdminBuildBlacklist; + return self._actionGenericFormDetails(BuildBlacklistData, BuildBlacklistLogic, WuiAdminBuildBlacklist, 'idBlacklisting'); + + def _actionBuildBlacklistDoRemove(self): + """ Action wrapper. """ + from testmanager.core.buildblacklist import BuildBlacklistData, BuildBlacklistLogic; + return self._actionGenericDoRemove(BuildBlacklistLogic, BuildBlacklistData.ksParam_idBlacklisting, + self.ksActionBuildBlacklist); + + def _actionBuildBlacklistEdit(self): + """ Action wrapper. """ + from testmanager.core.buildblacklist import BuildBlacklistData; + from testmanager.webui.wuiadminbuildblacklist import WuiAdminBuildBlacklist; + return self._actionGenericFormEdit(BuildBlacklistData, WuiAdminBuildBlacklist, BuildBlacklistData.ksParam_idBlacklisting); + + def _actionBuildBlacklistEditPost(self): + """ Action wrapper. """ + from testmanager.core.buildblacklist import BuildBlacklistData, BuildBlacklistLogic; + from testmanager.webui.wuiadminbuildblacklist import WuiAdminBuildBlacklist; + return self._actionGenericFormEditPost(BuildBlacklistData, BuildBlacklistLogic, WuiAdminBuildBlacklist, + self.ksActionBuildBlacklist) + + + # + # Failure Category actions + # + def _actionFailureCategoryList(self): + """ Action wrapper. """ + from testmanager.core.failurecategory import FailureCategoryLogic; + from testmanager.webui.wuiadminfailurecategory import WuiFailureCategoryList; + return self._actionGenericListing(FailureCategoryLogic, WuiFailureCategoryList); + + def _actionFailureCategoryAdd(self): + """ Action wrapper. """ + from testmanager.core.failurecategory import FailureCategoryData; + from testmanager.webui.wuiadminfailurecategory import WuiFailureCategory; + return self._actionGenericFormAdd(FailureCategoryData, WuiFailureCategory); + + def _actionFailureCategoryAddPost(self): + """ Action wrapper. """ + from testmanager.core.failurecategory import FailureCategoryData, FailureCategoryLogic; + from testmanager.webui.wuiadminfailurecategory import WuiFailureCategory; + return self._actionGenericFormAddPost(FailureCategoryData, FailureCategoryLogic, WuiFailureCategory, + self.ksActionFailureCategoryList) + + def _actionFailureCategoryDetails(self): + """ Action wrapper. """ + from testmanager.core.failurecategory import FailureCategoryData, FailureCategoryLogic; + from testmanager.webui.wuiadminfailurecategory import WuiFailureCategory; + return self._actionGenericFormDetails(FailureCategoryData, FailureCategoryLogic, WuiFailureCategory); + + + def _actionFailureCategoryDoRemove(self): + """ Action wrapper. """ + from testmanager.core.failurecategory import FailureCategoryData, FailureCategoryLogic; + return self._actionGenericDoRemove(FailureCategoryLogic, FailureCategoryData.ksParam_idFailureCategory, + self.ksActionFailureCategoryList); + + def _actionFailureCategoryEdit(self): + """ Action wrapper. """ + from testmanager.core.failurecategory import FailureCategoryData; + from testmanager.webui.wuiadminfailurecategory import WuiFailureCategory; + return self._actionGenericFormEdit(FailureCategoryData, WuiFailureCategory, + FailureCategoryData.ksParam_idFailureCategory); + + def _actionFailureCategoryEditPost(self): + """ Action wrapper. """ + from testmanager.core.failurecategory import FailureCategoryData, FailureCategoryLogic; + from testmanager.webui.wuiadminfailurecategory import WuiFailureCategory; + return self._actionGenericFormEditPost(FailureCategoryData, FailureCategoryLogic, WuiFailureCategory, + self.ksActionFailureCategoryList); + + # + # Failure Reason actions + # + def _actionFailureReasonList(self): + """ Action wrapper. """ + from testmanager.core.failurereason import FailureReasonLogic; + from testmanager.webui.wuiadminfailurereason import WuiAdminFailureReasonList; + return self._actionGenericListing(FailureReasonLogic, WuiAdminFailureReasonList) + + def _actionFailureReasonAdd(self): + """ Action wrapper. """ + from testmanager.core.failurereason import FailureReasonData; + from testmanager.webui.wuiadminfailurereason import WuiAdminFailureReason; + return self._actionGenericFormAdd(FailureReasonData, WuiAdminFailureReason); + + def _actionFailureReasonAddPost(self): + """ Action wrapper. """ + from testmanager.core.failurereason import FailureReasonData, FailureReasonLogic; + from testmanager.webui.wuiadminfailurereason import WuiAdminFailureReason; + return self._actionGenericFormAddPost(FailureReasonData, FailureReasonLogic, WuiAdminFailureReason, + self.ksActionFailureReasonList); + + def _actionFailureReasonDetails(self): + """ Action wrapper. """ + from testmanager.core.failurereason import FailureReasonData, FailureReasonLogic; + from testmanager.webui.wuiadminfailurereason import WuiAdminFailureReason; + return self._actionGenericFormDetails(FailureReasonData, FailureReasonLogic, WuiAdminFailureReason); + + def _actionFailureReasonDoRemove(self): + """ Action wrapper. """ + from testmanager.core.failurereason import FailureReasonData, FailureReasonLogic; + return self._actionGenericDoRemove(FailureReasonLogic, FailureReasonData.ksParam_idFailureReason, + self.ksActionFailureReasonList); + + def _actionFailureReasonEdit(self): + """ Action wrapper. """ + from testmanager.core.failurereason import FailureReasonData; + from testmanager.webui.wuiadminfailurereason import WuiAdminFailureReason; + return self._actionGenericFormEdit(FailureReasonData, WuiAdminFailureReason); + + + def _actionFailureReasonEditPost(self): + """ Action wrapper. """ + from testmanager.core.failurereason import FailureReasonData, FailureReasonLogic; + from testmanager.webui.wuiadminfailurereason import WuiAdminFailureReason; + return self._actionGenericFormEditPost(FailureReasonData, FailureReasonLogic, WuiAdminFailureReason, + self.ksActionFailureReasonList) + + + # + # Overrides. + # + + def _generatePage(self): + """Override parent handler in order to change page titte""" + if self._sPageTitle is not None: + self._sPageTitle = 'Test Manager Admin - ' + self._sPageTitle + + return WuiDispatcherBase._generatePage(self) diff --git a/src/VBox/ValidationKit/testmanager/webui/wuiadminbuild.py b/src/VBox/ValidationKit/testmanager/webui/wuiadminbuild.py new file mode 100755 index 00000000..17944ec3 --- /dev/null +++ b/src/VBox/ValidationKit/testmanager/webui/wuiadminbuild.py @@ -0,0 +1,154 @@ +# -*- coding: utf-8 -*- +# $Id: wuiadminbuild.py $ + +""" +Test Manager WUI - 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 $" + + +# Validation Kit imports. +from testmanager.webui.wuicontentbase import WuiFormContentBase, WuiListContentBase, WuiTmLink, WuiBuildLogLink, \ + WuiSvnLinkWithTooltip; +from testmanager.core.build import BuildData, BuildCategoryLogic; +from testmanager.core.buildblacklist import BuildBlacklistData; +from testmanager.core.db import isDbTimestampInfinity; + + +class WuiAdminBuild(WuiFormContentBase): + """ + WUI Build HTML content generator. + """ + + def __init__(self, oData, sMode, oDisp): + if sMode == WuiFormContentBase.ksMode_Add: + sTitle = 'Add Build' + elif sMode == WuiFormContentBase.ksMode_Edit: + sTitle = 'Modify Build - #%s' % (oData.idBuild,); + else: + assert sMode == WuiFormContentBase.ksMode_Show; + sTitle = 'Build - #%s' % (oData.idBuild,); + WuiFormContentBase.__init__(self, oData, sMode, 'Build', oDisp, sTitle); + + def _populateForm(self, oForm, oData): + oForm.addIntRO (BuildData.ksParam_idBuild, oData.idBuild, 'Build ID') + oForm.addTimestampRO(BuildData.ksParam_tsCreated, oData.tsCreated, 'Created') + oForm.addTimestampRO(BuildData.ksParam_tsEffective, oData.tsEffective, 'Last changed') + oForm.addTimestampRO(BuildData.ksParam_tsExpire, oData.tsExpire, 'Expires (excl)') + oForm.addIntRO (BuildData.ksParam_uidAuthor, oData.uidAuthor, 'Changed by UID') + + oForm.addComboBox (BuildData.ksParam_idBuildCategory, oData.idBuildCategory, 'Build category', + BuildCategoryLogic(self._oDisp.getDb()).fetchForCombo()); + + oForm.addInt (BuildData.ksParam_iRevision, oData.iRevision, 'Revision') + oForm.addText (BuildData.ksParam_sVersion, oData.sVersion, 'Version') + oForm.addWideText (BuildData.ksParam_sLogUrl, oData.sLogUrl, 'Log URL') + oForm.addWideText (BuildData.ksParam_sBinaries, oData.sBinaries, 'Binaries') + oForm.addCheckBox (BuildData.ksParam_fBinariesDeleted, oData.fBinariesDeleted, 'Binaries deleted') + + oForm.addSubmit() + return True; + + +class WuiAdminBuildList(WuiListContentBase): + """ + WUI Admin Build List Content Generator. + """ + + ksResultsSortByOs_Darwin = '' + def __init__(self, aoEntries, iPage, cItemsPerPage, tsEffective, fnDPrint, oDisp, aiSelectedSortColumns = None): + WuiListContentBase.__init__(self, aoEntries, iPage, cItemsPerPage, tsEffective, + sTitle = 'Builds', sId = 'builds', fnDPrint = fnDPrint, oDisp = oDisp, + aiSelectedSortColumns = aiSelectedSortColumns); + + self._asColumnHeaders = ['ID', 'Product', 'Branch', 'Version', + 'Type', 'OS(es)', 'Author', 'Added', + 'Files', 'Action' ]; + self._asColumnAttribs = ['align="right"', 'align="center"', 'align="center"', 'align="center"', + 'align="center"', 'align="center"', 'align="center"', 'align="center"', + '', 'align="center"']; + + def _formatListEntry(self, iEntry): + from testmanager.webui.wuiadmin import WuiAdmin + oEntry = self._aoEntries[iEntry]; + + aoActions = []; + if oEntry.sLogUrl is not None: + aoActions.append(WuiBuildLogLink(oEntry.sLogUrl, 'Build Log')); + + dParams = { WuiAdmin.ksParamAction: WuiAdmin.ksActionBuildBlacklistAdd, + BuildBlacklistData.ksParam_sProduct: oEntry.oCat.sProduct, + BuildBlacklistData.ksParam_sBranch: oEntry.oCat.sBranch, + BuildBlacklistData.ksParam_asTypes: oEntry.oCat.sType, + BuildBlacklistData.ksParam_asOsArches: oEntry.oCat.asOsArches, + BuildBlacklistData.ksParam_iFirstRevision: oEntry.iRevision, + BuildBlacklistData.ksParam_iLastRevision: oEntry.iRevision } + + if self._oDisp is None or not self._oDisp.isReadOnlyUser(): + aoActions += [ + WuiTmLink('Blacklist', WuiAdmin.ksScriptName, dParams), + WuiTmLink('Details', WuiAdmin.ksScriptName, + { WuiAdmin.ksParamAction: WuiAdmin.ksActionBuildDetails, + BuildData.ksParam_idBuild: oEntry.idBuild, + WuiAdmin.ksParamEffectiveDate: self._tsEffectiveDate, }), + WuiTmLink('Clone', WuiAdmin.ksScriptName, + { WuiAdmin.ksParamAction: WuiAdmin.ksActionBuildClone, + BuildData.ksParam_idBuild: oEntry.idBuild, + WuiAdmin.ksParamEffectiveDate: self._tsEffectiveDate, }), + ]; + if isDbTimestampInfinity(oEntry.tsExpire): + aoActions += [ + WuiTmLink('Modify', WuiAdmin.ksScriptName, + { WuiAdmin.ksParamAction: WuiAdmin.ksActionBuildEdit, + BuildData.ksParam_idBuild: oEntry.idBuild }), + WuiTmLink('Remove', WuiAdmin.ksScriptName, + { WuiAdmin.ksParamAction: WuiAdmin.ksActionBuildDoRemove, + BuildData.ksParam_idBuild: oEntry.idBuild }, + sConfirm = 'Are you sure you want to remove build #%d?' % (oEntry.idBuild,) ), + ]; + + return [ oEntry.idBuild, + oEntry.oCat.sProduct, + oEntry.oCat.sBranch, + WuiSvnLinkWithTooltip(oEntry.iRevision, oEntry.oCat.sRepository, + sName = '%s r%s' % (oEntry.sVersion, oEntry.iRevision,)), + oEntry.oCat.sType, + ' '.join(oEntry.oCat.asOsArches), + 'batch' if oEntry.uidAuthor is None else oEntry.uidAuthor, + self.formatTsShort(oEntry.tsCreated), + oEntry.sBinaries if not oEntry.fBinariesDeleted else '<Deleted>', + aoActions, + ]; + diff --git a/src/VBox/ValidationKit/testmanager/webui/wuiadminbuildblacklist.py b/src/VBox/ValidationKit/testmanager/webui/wuiadminbuildblacklist.py new file mode 100755 index 00000000..d265259b --- /dev/null +++ b/src/VBox/ValidationKit/testmanager/webui/wuiadminbuildblacklist.py @@ -0,0 +1,164 @@ +# -*- coding: utf-8 -*- +# $Id: wuiadminbuildblacklist.py $ + +""" +Test Manager WUI - Build Blacklist. +""" + +__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 $" + + +# Validation Kit imports. +from testmanager.webui.wuibase import WuiException +from testmanager.webui.wuicontentbase import WuiFormContentBase, WuiListContentBase, WuiTmLink +from testmanager.core.buildblacklist import BuildBlacklistData +from testmanager.core.failurereason import FailureReasonLogic +from testmanager.core.db import TMDatabaseConnection +from testmanager.core import coreconsts + + +class WuiAdminBuildBlacklist(WuiFormContentBase): + """ + WUI Build Black List Form. + """ + + def __init__(self, oData, sMode, oDisp): + """ + Prepare & initialize parent + """ + + if sMode == WuiFormContentBase.ksMode_Add: + sTitle = 'Add Build Blacklist Entry' + elif sMode == WuiFormContentBase.ksMode_Edit: + sTitle = 'Edit Build Blacklist Entry' + else: + assert sMode == WuiFormContentBase.ksMode_Show; + sTitle = 'Build Black'; + WuiFormContentBase.__init__(self, oData, sMode, 'BuildBlacklist', oDisp, sTitle); + + # + # Additional data. + # + self.asTypes = coreconsts.g_kasBuildTypesAll + self.asOsArches = coreconsts.g_kasOsDotCpusAll + + def _populateForm(self, oForm, oData): + """ + Construct an HTML form + """ + + aoFailureReasons = FailureReasonLogic(self._oDisp.getDb()).fetchForCombo() + if not aoFailureReasons: + from testmanager.webui.wuiadmin import WuiAdmin + raise WuiException('Please <a href="%s?%s=%s">add</a> some Failure Reasons first.' + % (WuiAdmin.ksScriptName, WuiAdmin.ksParamAction, WuiAdmin.ksActionFailureReasonAdd)); + + asTypes = self.getListOfItems(self.asTypes, oData.asTypes) + asOsArches = self.getListOfItems(self.asOsArches, oData.asOsArches) + + oForm.addIntRO (BuildBlacklistData.ksParam_idBlacklisting, oData.idBlacklisting, 'Blacklist item ID') + oForm.addTimestampRO(BuildBlacklistData.ksParam_tsEffective, oData.tsEffective, 'Last changed') + oForm.addTimestampRO(BuildBlacklistData.ksParam_tsExpire, oData.tsExpire, 'Expires (excl)') + oForm.addIntRO (BuildBlacklistData.ksParam_uidAuthor, oData.uidAuthor, 'Changed by UID') + + oForm.addComboBox (BuildBlacklistData.ksParam_idFailureReason, oData.idFailureReason, 'Failure Reason', + aoFailureReasons) + + oForm.addText (BuildBlacklistData.ksParam_sProduct, oData.sProduct, 'Product') + oForm.addText (BuildBlacklistData.ksParam_sBranch, oData.sBranch, 'Branch') + oForm.addListOfTypes(BuildBlacklistData.ksParam_asTypes, asTypes, 'Build types') + oForm.addListOfOsArches(BuildBlacklistData.ksParam_asOsArches, asOsArches, 'Target architectures') + oForm.addInt (BuildBlacklistData.ksParam_iFirstRevision, oData.iFirstRevision, 'First revision') + oForm.addInt (BuildBlacklistData.ksParam_iLastRevision, oData.iLastRevision, 'Last revision (incl)') + + oForm.addSubmit(); + + return True; + + +class WuiAdminListOfBlacklistItems(WuiListContentBase): + """ + WUI Admin Build Blacklist Content Generator. + """ + + def __init__(self, aoEntries, iPage, cItemsPerPage, tsEffective, fnDPrint, oDisp, aiSelectedSortColumns = None): + WuiListContentBase.__init__(self, aoEntries, iPage, cItemsPerPage, tsEffective, + sTitle = 'Build Blacklist', sId = 'buildsBlacklist', + fnDPrint = fnDPrint, oDisp = oDisp, aiSelectedSortColumns = aiSelectedSortColumns); + + self._asColumnHeaders = ['ID', 'Failure Reason', + 'Product', 'Branch', 'Type', + 'OS(es)', 'First Revision', 'Last Revision', + 'Actions' ] + self._asColumnAttribs = ['align="right"', 'align="center"', 'align="center"', 'align="center"', + 'align="center"', 'align="center"', 'align="center"', 'align="center"', + 'align="center"', 'align="center"', 'align="center"', 'align="center"', + 'align="center"' ] + + def _formatListEntry(self, iEntry): + from testmanager.webui.wuiadmin import WuiAdmin + oEntry = self._aoEntries[iEntry] + + sShortFailReason = FailureReasonLogic(TMDatabaseConnection()).getById(oEntry.idFailureReason).sShort + + aoActions = [ + WuiTmLink('Details', WuiAdmin.ksScriptName, + { WuiAdmin.ksParamAction: WuiAdmin.ksActionBuildBlacklistDetails, + BuildBlacklistData.ksParam_idBlacklisting: oEntry.idBlacklisting }), + ]; + if self._oDisp is None or not self._oDisp.isReadOnlyUser(): + aoActions += [ + WuiTmLink('Edit', WuiAdmin.ksScriptName, + { WuiAdmin.ksParamAction: WuiAdmin.ksActionBuildBlacklistEdit, + BuildBlacklistData.ksParam_idBlacklisting: oEntry.idBlacklisting }), + WuiTmLink('Clone', WuiAdmin.ksScriptName, + { WuiAdmin.ksParamAction: WuiAdmin.ksActionBuildBlacklistClone, + BuildBlacklistData.ksParam_idBlacklisting: oEntry.idBlacklisting, + WuiAdmin.ksParamEffectiveDate: oEntry.tsEffective, }), + WuiTmLink('Remove', WuiAdmin.ksScriptName, + { WuiAdmin.ksParamAction: WuiAdmin.ksActionBuildBlacklistDoRemove, + BuildBlacklistData.ksParam_idBlacklisting: oEntry.idBlacklisting }, + sConfirm = 'Are you sure you want to remove black list entry #%d?' % (oEntry.idBlacklisting,)), + ]; + + return [ oEntry.idBlacklisting, + sShortFailReason, + oEntry.sProduct, + oEntry.sBranch, + oEntry.asTypes, + oEntry.asOsArches, + oEntry.iFirstRevision, + oEntry.iLastRevision, + aoActions + ]; diff --git a/src/VBox/ValidationKit/testmanager/webui/wuiadminbuildcategory.py b/src/VBox/ValidationKit/testmanager/webui/wuiadminbuildcategory.py new file mode 100755 index 00000000..87d76fba --- /dev/null +++ b/src/VBox/ValidationKit/testmanager/webui/wuiadminbuildcategory.py @@ -0,0 +1,122 @@ +# -*- coding: utf-8 -*- +# $Id: wuiadminbuildcategory.py $ + +""" +Test Manager WUI - Build categories. +""" + +__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 $" + + +# Validation Kit imports. +from common import webutils; +from testmanager.webui.wuicontentbase import WuiListContentBase, WuiFormContentBase, WuiRawHtml, WuiTmLink; +from testmanager.core.build import BuildCategoryData +from testmanager.core import coreconsts; + + +class WuiAdminBuildCatList(WuiListContentBase): + """ + WUI Build Category List Content Generator. + """ + + def __init__(self, aoEntries, iPage, cItemsPerPage, tsEffective, fnDPrint, oDisp, aiSelectedSortColumns = None): + WuiListContentBase.__init__(self, aoEntries, iPage, cItemsPerPage, tsEffective, + sTitle = 'Build Categories', sId = 'buildcategories', + fnDPrint = fnDPrint, oDisp = oDisp, aiSelectedSortColumns = aiSelectedSortColumns); + self._asColumnHeaders = ([ 'ID', 'Product', 'Repository', 'Branch', 'Build Type', 'OS/Architectures', 'Actions' ]); + self._asColumnAttribs = (['align="right"', '', '', '', '', 'align="center"' ]); + + def _formatListEntry(self, iEntry): + from testmanager.webui.wuiadmin import WuiAdmin; + oEntry = self._aoEntries[iEntry]; + + aoActions = [ + WuiTmLink('Details', WuiAdmin.ksScriptName, + { WuiAdmin.ksParamAction: WuiAdmin.ksActionBuildCategoryDetails, + BuildCategoryData.ksParam_idBuildCategory: oEntry.idBuildCategory, }), + ]; + if self._oDisp is None or not self._oDisp.isReadOnlyUser(): + aoActions += [ + WuiTmLink('Clone', WuiAdmin.ksScriptName, + { WuiAdmin.ksParamAction: WuiAdmin.ksActionBuildCategoryClone, + BuildCategoryData.ksParam_idBuildCategory: oEntry.idBuildCategory, }), + WuiTmLink('Try Remove', WuiAdmin.ksScriptName, + { WuiAdmin.ksParamAction: WuiAdmin.ksActionBuildCategoryDoRemove, + BuildCategoryData.ksParam_idBuildCategory: oEntry.idBuildCategory, }), + ]; + + sHtml = '<ul class="tmshowall">\n'; + for sOsArch in oEntry.asOsArches: + sHtml += ' <li class="tmshowall">%s</li>\n' % (webutils.escapeElem(sOsArch),); + sHtml += '</ul>\n' + + return [ oEntry.idBuildCategory, + oEntry.sRepository, + oEntry.sProduct, + oEntry.sBranch, + oEntry.sType, + WuiRawHtml(sHtml), + aoActions, + ]; + + +class WuiAdminBuildCat(WuiFormContentBase): + """ + WUI Build Category Form Content Generator. + """ + def __init__(self, oData, sMode, oDisp): + if sMode == WuiFormContentBase.ksMode_Add: + sTitle = 'Create Build Category'; + elif sMode == WuiFormContentBase.ksMode_Edit: + assert False, 'not possible' + else: + assert sMode == WuiFormContentBase.ksMode_Show; + sTitle = 'Build Category- %s' % (oData.idBuildCategory,); + WuiFormContentBase.__init__(self, oData, sMode, 'BuildCategory', oDisp, sTitle, fEditable = False); + + def _populateForm(self, oForm, oData): + oForm.addIntRO( BuildCategoryData.ksParam_idBuildCategory, oData.idBuildCategory, 'Build Category ID') + oForm.addText( BuildCategoryData.ksParam_sRepository, oData.sRepository, 'VCS repository name'); + oForm.addText( BuildCategoryData.ksParam_sProduct, oData.sProduct, 'Product name') + oForm.addText( BuildCategoryData.ksParam_sBranch, oData.sBranch, 'Branch name') + oForm.addText( BuildCategoryData.ksParam_sType, oData.sType, 'Build type') + + aoOsArches = [[sOsArch, sOsArch in oData.asOsArches, sOsArch] for sOsArch in coreconsts.g_kasOsDotCpusAll]; + oForm.addListOfOsArches(BuildCategoryData.ksParam_asOsArches, aoOsArches, 'Target architectures'); + + if self._sMode != WuiFormContentBase.ksMode_Show: + oForm.addSubmit(); + return True; + diff --git a/src/VBox/ValidationKit/testmanager/webui/wuiadminbuildsource.py b/src/VBox/ValidationKit/testmanager/webui/wuiadminbuildsource.py new file mode 100755 index 00000000..52d05f07 --- /dev/null +++ b/src/VBox/ValidationKit/testmanager/webui/wuiadminbuildsource.py @@ -0,0 +1,160 @@ +# -*- coding: utf-8 -*- +# $Id: wuiadminbuildsource.py $ + +""" +Test Manager WUI - Build Sources. +""" + +__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 $" + + +# Validation Kit imports. +from common import utils, webutils; +from testmanager.webui.wuicontentbase import WuiFormContentBase, WuiListContentBase, WuiTmLink, WuiRawHtml; +from testmanager.core import coreconsts; +from testmanager.core.db import isDbTimestampInfinity; +from testmanager.core.buildsource import BuildSourceData; + + +class WuiAdminBuildSrc(WuiFormContentBase): + """ + WUI Build Sources HTML content generator. + """ + + def __init__(self, oData, sMode, oDisp): + assert isinstance(oData, BuildSourceData); + if sMode == WuiFormContentBase.ksMode_Add: + sTitle = 'New Build Source'; + elif sMode == WuiFormContentBase.ksMode_Edit: + sTitle = 'Edit Build Source - %s (#%s)' % (oData.sName, oData.idBuildSrc,); + else: + assert sMode == WuiFormContentBase.ksMode_Show; + sTitle = 'Build Source - %s (#%s)' % (oData.sName, oData.idBuildSrc,); + WuiFormContentBase.__init__(self, oData, sMode, 'BuildSrc', oDisp, sTitle); + + def _populateForm(self, oForm, oData): + oForm.addIntRO (BuildSourceData.ksParam_idBuildSrc, oData.idBuildSrc, 'Build Source item ID') + oForm.addTimestampRO(BuildSourceData.ksParam_tsEffective, oData.tsEffective, 'Last changed') + oForm.addTimestampRO(BuildSourceData.ksParam_tsExpire, oData.tsExpire, 'Expires (excl)') + oForm.addIntRO (BuildSourceData.ksParam_uidAuthor, oData.uidAuthor, 'Changed by UID') + oForm.addText (BuildSourceData.ksParam_sName, oData.sName, 'Name') + oForm.addText (BuildSourceData.ksParam_sDescription, oData.sDescription, 'Description') + oForm.addText (BuildSourceData.ksParam_sProduct, oData.sProduct, 'Product') + oForm.addText (BuildSourceData.ksParam_sBranch, oData.sBranch, 'Branch') + asTypes = self.getListOfItems(coreconsts.g_kasBuildTypesAll, oData.asTypes); + oForm.addListOfTypes(BuildSourceData.ksParam_asTypes, asTypes, 'Build types') + asOsArches = self.getListOfItems(coreconsts.g_kasOsDotCpusAll, oData.asOsArches); + oForm.addListOfOsArches(BuildSourceData.ksParam_asOsArches, asOsArches, 'Target architectures') + oForm.addInt (BuildSourceData.ksParam_iFirstRevision, oData.iFirstRevision, 'Starting from revision') + oForm.addInt (BuildSourceData.ksParam_iLastRevision, oData.iLastRevision, 'Ending by revision') + oForm.addLong (BuildSourceData.ksParam_cSecMaxAge, + utils.formatIntervalSeconds2(oData.cSecMaxAge) if oData.cSecMaxAge not in [-1, '', None] else '', + 'Max age in seconds'); + oForm.addSubmit(); + return True; + +class WuiAdminBuildSrcList(WuiListContentBase): + """ + WUI Build Source content generator. + """ + + def __init__(self, aoEntries, iPage, cItemsPerPage, tsEffective, fnDPrint, oDisp, aiSelectedSortColumns = None): + WuiListContentBase.__init__(self, aoEntries, iPage, cItemsPerPage, tsEffective, + sTitle = 'Registered Build Sources', sId = 'build sources', + fnDPrint = fnDPrint, oDisp = oDisp, aiSelectedSortColumns = aiSelectedSortColumns); + self._asColumnHeaders = ['ID', 'Name', 'Description', 'Product', + 'Branch', 'Build Types', 'OS/ARCH', 'First Revision', 'Last Revision', 'Max Age', + 'Actions' ]; + self._asColumnAttribs = ['align="center"', 'align="center"', 'align="center"', 'align="center"', 'align="center"', + 'align="left"', 'align="left"', 'align="center"', 'align="center"', 'align="center"', + 'align="center"' ]; + + def _getSubList(self, aList): + """ + Convert pythonic list into HTML list + """ + if aList not in (None, []): + sHtml = ' <ul class="tmshowall">\n' + for sTmp in aList: + sHtml += ' <li class="tmshowall">%s</a></li>\n' % (webutils.escapeElem(sTmp),); + sHtml += ' </ul>\n'; + else: + sHtml = '<ul class="tmshowall"><li class="tmshowall">Any</li></ul>\n'; + + return WuiRawHtml(sHtml); + + def _formatListEntry(self, iEntry): + """ + Format *show all* table entry + """ + + from testmanager.webui.wuiadmin import WuiAdmin + oEntry = self._aoEntries[iEntry] + + aoActions = [ + WuiTmLink('Details', WuiAdmin.ksScriptName, + { WuiAdmin.ksParamAction: WuiAdmin.ksActionBuildSrcDetails, + BuildSourceData.ksParam_idBuildSrc: oEntry.idBuildSrc, + WuiAdmin.ksParamEffectiveDate: self._tsEffectiveDate, }), + ]; + if self._oDisp is None or not self._oDisp.isReadOnlyUser(): + aoActions += [ + WuiTmLink('Clone', WuiAdmin.ksScriptName, + { WuiAdmin.ksParamAction: WuiAdmin.ksActionBuildSrcClone, + BuildSourceData.ksParam_idBuildSrc: oEntry.idBuildSrc, + WuiAdmin.ksParamEffectiveDate: self._tsEffectiveDate, }), + ]; + if isDbTimestampInfinity(oEntry.tsExpire): + aoActions += [ + WuiTmLink('Modify', WuiAdmin.ksScriptName, + { WuiAdmin.ksParamAction: WuiAdmin.ksActionBuildSrcEdit, + BuildSourceData.ksParam_idBuildSrc: oEntry.idBuildSrc } ), + WuiTmLink('Remove', WuiAdmin.ksScriptName, + { WuiAdmin.ksParamAction: WuiAdmin.ksActionBuildSrcDoRemove, + BuildSourceData.ksParam_idBuildSrc: oEntry.idBuildSrc }, + sConfirm = 'Are you sure you want to remove build source #%d?' % (oEntry.idBuildSrc,) ) + ]; + + return [ oEntry.idBuildSrc, + oEntry.sName, + oEntry.sDescription, + oEntry.sProduct, + oEntry.sBranch, + self._getSubList(oEntry.asTypes), + self._getSubList(oEntry.asOsArches), + oEntry.iFirstRevision, + oEntry.iLastRevision, + utils.formatIntervalSeconds2(oEntry.cSecMaxAge) if oEntry.cSecMaxAge is not None else None, + aoActions, + ] diff --git a/src/VBox/ValidationKit/testmanager/webui/wuiadminfailurecategory.py b/src/VBox/ValidationKit/testmanager/webui/wuiadminfailurecategory.py new file mode 100755 index 00000000..d02931de --- /dev/null +++ b/src/VBox/ValidationKit/testmanager/webui/wuiadminfailurecategory.py @@ -0,0 +1,155 @@ +# -*- coding: utf-8 -*- +# $Id: wuiadminfailurecategory.py $ + +""" +Test Manager WUI - Failure Categories Web content generator. +""" + +__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 $" + + +# Validation Kit imports. +from testmanager.webui.wuicontentbase import WuiFormContentBase, WuiContentBase, WuiListContentBase, WuiTmLink; +from testmanager.webui.wuiadminfailurereason import WuiAdminFailureReasonList; +from testmanager.core.failurecategory import FailureCategoryData; +from testmanager.core.failurereason import FailureReasonLogic; + + +class WuiFailureReasonCategoryLink(WuiTmLink): + """ Link to a failure category. """ + def __init__(self, idFailureCategory, sName = WuiContentBase.ksShortDetailsLink, sTitle = None, fBracketed = None): + if fBracketed is None: + fBracketed = len(sName) > 2; + from testmanager.webui.wuiadmin import WuiAdmin; + WuiTmLink.__init__(self, sName = sName, + sUrlBase = WuiAdmin.ksScriptName, + dParams = { WuiAdmin.ksParamAction: WuiAdmin.ksActionFailureCategoryDetails, + FailureCategoryData.ksParam_idFailureCategory: idFailureCategory, }, + fBracketed = fBracketed, + sTitle = sTitle); + self.idFailureCategory = idFailureCategory; + + + +class WuiFailureCategory(WuiFormContentBase): + """ + WUI Failure Category HTML content generator. + """ + + def __init__(self, oData, sMode, oDisp): + """ + Prepare & initialize parent + """ + + sTitle = 'Failure Category'; + if sMode == WuiFormContentBase.ksMode_Add: + sTitle = 'Add ' + sTitle; + elif sMode == WuiFormContentBase.ksMode_Edit: + sTitle = 'Edit ' + sTitle; + else: + assert sMode == WuiFormContentBase.ksMode_Show; + + WuiFormContentBase.__init__(self, oData, sMode, 'FailureCategory', oDisp, sTitle); + + def _populateForm(self, oForm, oData): + """ + Construct an HTML form + """ + + oForm.addIntRO (FailureCategoryData.ksParam_idFailureCategory, oData.idFailureCategory, 'Failure Category ID') + oForm.addTimestampRO(FailureCategoryData.ksParam_tsEffective, oData.tsEffective, 'Last changed') + oForm.addTimestampRO(FailureCategoryData.ksParam_tsExpire, oData.tsExpire, 'Expires (excl)') + oForm.addIntRO (FailureCategoryData.ksParam_uidAuthor, oData.uidAuthor, 'Changed by UID') + oForm.addText (FailureCategoryData.ksParam_sShort, oData.sShort, 'Short Description') + oForm.addText (FailureCategoryData.ksParam_sFull, oData.sFull, 'Full Description') + + oForm.addSubmit() + + return True; + + def _generatePostFormContent(self, oData): + """ + Adds a table with the category members below the form. + """ + if oData.idFailureCategory is not None and oData.idFailureCategory >= 0: + oLogic = FailureReasonLogic(self._oDisp.getDb()); + tsNow = self._oDisp.getNow(); + cMax = 4096; + aoEntries = oLogic.fetchForListingInCategory(0, cMax, tsNow, oData.idFailureCategory) + if aoEntries: + oList = WuiAdminFailureReasonList(aoEntries, 0, cMax, tsNow, fnDPrint = None, oDisp = self._oDisp); + return [ [ 'Members', oList.show(fShowNavigation = False)[1]], ]; + return []; + + + +class WuiFailureCategoryList(WuiListContentBase): + """ + WUI Admin Failure Category Content Generator. + """ + + def __init__(self, aoEntries, iPage, cItemsPerPage, tsEffective, fnDPrint, oDisp, aiSelectedSortColumns = None): + WuiListContentBase.__init__(self, aoEntries, iPage, cItemsPerPage, tsEffective, + sTitle = 'Failure Categories', sId = 'failureCategories', + fnDPrint = fnDPrint, oDisp = oDisp, aiSelectedSortColumns = aiSelectedSortColumns); + + self._asColumnHeaders = ['ID', 'Short Description', 'Full Description', 'Actions' ] + self._asColumnAttribs = ['align="right"', 'align="center"', 'align="center"', 'align="center"'] + + def _formatListEntry(self, iEntry): + from testmanager.webui.wuiadmin import WuiAdmin + oEntry = self._aoEntries[iEntry] + + aoActions = [ + WuiTmLink('Details', WuiAdmin.ksScriptName, + { WuiAdmin.ksParamAction: WuiAdmin.ksActionFailureCategoryDetails, + FailureCategoryData.ksParam_idFailureCategory: oEntry.idFailureCategory }), + ]; + if self._oDisp is None or not self._oDisp.isReadOnlyUser(): + aoActions += [ + WuiTmLink('Modify', WuiAdmin.ksScriptName, + { WuiAdmin.ksParamAction: WuiAdmin.ksActionFailureCategoryEdit, + FailureCategoryData.ksParam_idFailureCategory: oEntry.idFailureCategory }), + WuiTmLink('Remove', WuiAdmin.ksScriptName, + { WuiAdmin.ksParamAction: WuiAdmin.ksActionFailureCategoryDoRemove, + FailureCategoryData.ksParam_idFailureCategory: oEntry.idFailureCategory }, + sConfirm = 'Do you really want to remove failure cateogry #%d?' % (oEntry.idFailureCategory,)), + ] + + return [ oEntry.idFailureCategory, + oEntry.sShort, + oEntry.sFull, + aoActions, + ]; + diff --git a/src/VBox/ValidationKit/testmanager/webui/wuiadminfailurereason.py b/src/VBox/ValidationKit/testmanager/webui/wuiadminfailurereason.py new file mode 100755 index 00000000..5fa76227 --- /dev/null +++ b/src/VBox/ValidationKit/testmanager/webui/wuiadminfailurereason.py @@ -0,0 +1,175 @@ +# -*- coding: utf-8 -*- +# $Id: wuiadminfailurereason.py $ + +""" +Test Manager WUI - Failure Reasons Web content generator. +""" + +__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 $" + + +# Validation Kit imports. +from testmanager.webui.wuibase import WuiException +from testmanager.webui.wuicontentbase import WuiFormContentBase, WuiListContentBase, WuiContentBase, WuiTmLink; +from testmanager.core.failurereason import FailureReasonData; +from testmanager.core.failurecategory import FailureCategoryLogic; +from testmanager.core.db import TMDatabaseConnection; + + + +class WuiFailureReasonDetailsLink(WuiTmLink): + """ Short link to a failure reason. """ + def __init__(self, idFailureReason, sName = WuiContentBase.ksShortDetailsLink, sTitle = None, fBracketed = None): + if fBracketed is None: + fBracketed = len(sName) > 2; + from testmanager.webui.wuiadmin import WuiAdmin; + WuiTmLink.__init__(self, sName = sName, + sUrlBase = WuiAdmin.ksScriptName, + dParams = { WuiAdmin.ksParamAction: WuiAdmin.ksActionFailureReasonDetails, + FailureReasonData.ksParam_idFailureReason: idFailureReason, }, + fBracketed = fBracketed); + self.idFailureReason = idFailureReason; + + + +class WuiFailureReasonAddLink(WuiTmLink): + """ Link for adding a failure reason. """ + def __init__(self, sName = WuiContentBase.ksShortAddLink, sTitle = None, fBracketed = None): + if fBracketed is None: + fBracketed = len(sName) > 2; + from testmanager.webui.wuiadmin import WuiAdmin; + WuiTmLink.__init__(self, sName = sName, + sUrlBase = WuiAdmin.ksScriptName, + dParams = { WuiAdmin.ksParamAction: WuiAdmin.ksActionFailureReasonAdd, }, + fBracketed = fBracketed); + + + +class WuiAdminFailureReason(WuiFormContentBase): + """ + WUI Failure Reason HTML content generator. + """ + + def __init__(self, oFailureReasonData, sMode, oDisp): + """ + Prepare & initialize parent + """ + + sTitle = 'Failure Reason'; + if sMode == WuiFormContentBase.ksMode_Add: + sTitle = 'Add' + sTitle; + elif sMode == WuiFormContentBase.ksMode_Edit: + sTitle = 'Edit' + sTitle; + else: + assert sMode == WuiFormContentBase.ksMode_Show; + + WuiFormContentBase.__init__(self, oFailureReasonData, sMode, 'FailureReason', oDisp, sTitle); + + def _populateForm(self, oForm, oData): + """ + Construct an HTML form + """ + + aoFailureCategories = FailureCategoryLogic(TMDatabaseConnection()).getFailureCategoriesForCombo() + if not aoFailureCategories: + from testmanager.webui.wuiadmin import WuiAdmin + sExceptionMsg = 'Please <a href="%s?%s=%s">add</a> Failure Category first.' % \ + (WuiAdmin.ksScriptName, WuiAdmin.ksParamAction, WuiAdmin.ksActionFailureCategoryAdd) + + raise WuiException(sExceptionMsg) + + oForm.addIntRO (FailureReasonData.ksParam_idFailureReason, oData.idFailureReason, 'Failure Reason ID') + oForm.addTimestampRO (FailureReasonData.ksParam_tsEffective, oData.tsEffective, 'Last changed') + oForm.addTimestampRO (FailureReasonData.ksParam_tsExpire, oData.tsExpire, 'Expires (excl)') + oForm.addIntRO (FailureReasonData.ksParam_uidAuthor, oData.uidAuthor, 'Changed by UID') + + oForm.addComboBox (FailureReasonData.ksParam_idFailureCategory, oData.idFailureCategory, 'Failure Category', + aoFailureCategories) + + oForm.addText (FailureReasonData.ksParam_sShort, oData.sShort, 'Short Description') + oForm.addText (FailureReasonData.ksParam_sFull, oData.sFull, 'Full Description') + oForm.addInt (FailureReasonData.ksParam_iTicket, oData.iTicket, 'Ticket Number') + oForm.addMultilineText(FailureReasonData.ksParam_asUrls, oData.asUrls, 'Other URLs to reports ' + 'or discussions of the ' + 'observed symptoms') + oForm.addSubmit() + + return True + + +class WuiAdminFailureReasonList(WuiListContentBase): + """ + WUI Admin Failure Reasons Content Generator. + """ + + def __init__(self, aoEntries, iPage, cItemsPerPage, tsEffective, fnDPrint, oDisp, aiSelectedSortColumns = None): + WuiListContentBase.__init__(self, aoEntries, iPage, cItemsPerPage, tsEffective, + sTitle = 'Failure Reasons', sId = 'failureReasons', + fnDPrint = fnDPrint, oDisp = oDisp, aiSelectedSortColumns = aiSelectedSortColumns); + + self._asColumnHeaders = ['ID', 'Category', 'Short Description', + 'Full Description', 'Ticket', 'External References', 'Actions' ] + + self._asColumnAttribs = ['align="right"', 'align="center"', 'align="center"', + 'align="center"',' align="center"', 'align="center"', 'align="center"'] + + def _formatListEntry(self, iEntry): + from testmanager.webui.wuiadmin import WuiAdmin + from testmanager.webui.wuiadminfailurecategory import WuiFailureReasonCategoryLink; + oEntry = self._aoEntries[iEntry] + + aoActions = [ + WuiTmLink('Details', WuiAdmin.ksScriptName, + { WuiAdmin.ksParamAction: WuiAdmin.ksActionFailureReasonDetails, + FailureReasonData.ksParam_idFailureReason: oEntry.idFailureReason } ), + ]; + if self._oDisp is None or not self._oDisp.isReadOnlyUser(): + aoActions += [ + WuiTmLink('Modify', WuiAdmin.ksScriptName, + { WuiAdmin.ksParamAction: WuiAdmin.ksActionFailureReasonEdit, + FailureReasonData.ksParam_idFailureReason: oEntry.idFailureReason } ), + WuiTmLink('Remove', WuiAdmin.ksScriptName, + { WuiAdmin.ksParamAction: WuiAdmin.ksActionFailureReasonDoRemove, + FailureReasonData.ksParam_idFailureReason: oEntry.idFailureReason }, + sConfirm = 'Are you sure you want to remove failure reason #%d?' % (oEntry.idFailureReason,)), + ]; + + return [ oEntry.idFailureReason, + WuiFailureReasonCategoryLink(oEntry.idFailureCategory, sName = oEntry.oCategory.sShort, fBracketed = False), + oEntry.sShort, + oEntry.sFull, + oEntry.iTicket, + oEntry.asUrls, + aoActions, + ] diff --git a/src/VBox/ValidationKit/testmanager/webui/wuiadminglobalrsrc.py b/src/VBox/ValidationKit/testmanager/webui/wuiadminglobalrsrc.py new file mode 100755 index 00000000..b748ea2f --- /dev/null +++ b/src/VBox/ValidationKit/testmanager/webui/wuiadminglobalrsrc.py @@ -0,0 +1,130 @@ +# -*- coding: utf-8 -*- +# $Id: wuiadminglobalrsrc.py $ + +""" +Test Manager WUI - Global resources. +""" + +__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 $" + +# Validation Kit imports. +from testmanager.webui.wuibase import WuiException +from testmanager.webui.wuicontentbase import WuiContentBase +from testmanager.webui.wuihlpform import WuiHlpForm +from testmanager.core.globalresource import GlobalResourceData +from testmanager.webui.wuicontentbase import WuiListContentBase, WuiTmLink + + +class WuiGlobalResource(WuiContentBase): + """ + WUI global resources content generator. + """ + + def __init__(self, oData, fnDPrint = None): + """ + Do necessary initializations + """ + WuiContentBase.__init__(self, fnDPrint) + self._oData = oData + + def showAddModifyPage(self, sAction, dErrors = None): + """ + Render add global resource HTML form. + """ + from testmanager.webui.wuiadmin import WuiAdmin + + sFormActionUrl = '%s?%s=%s' % (WuiAdmin.ksScriptName, + WuiAdmin.ksParamAction, sAction) + if sAction == WuiAdmin.ksActionGlobalRsrcAdd: + sTitle = 'Add Global Resource' + elif sAction == WuiAdmin.ksActionGlobalRsrcEdit: + sTitle = 'Modify Global Resource' + sFormActionUrl += '&%s=%s' % (GlobalResourceData.ksParam_idGlobalRsrc, self._oData.idGlobalRsrc) + else: + raise WuiException('Invalid paraemter "%s"' % (sAction,)) + + oForm = WuiHlpForm('globalresourceform', + sFormActionUrl, + dErrors if dErrors is not None else {}) + + if sAction == WuiAdmin.ksActionGlobalRsrcAdd: + oForm.addIntRO (GlobalResourceData.ksParam_idGlobalRsrc, self._oData.idGlobalRsrc, 'Global Resource ID') + oForm.addTimestampRO(GlobalResourceData.ksParam_tsEffective, self._oData.tsEffective, 'Last changed') + oForm.addTimestampRO(GlobalResourceData.ksParam_tsExpire, self._oData.tsExpire, 'Expires (excl)') + oForm.addIntRO (GlobalResourceData.ksParam_uidAuthor, self._oData.uidAuthor, 'Changed by UID') + oForm.addText (GlobalResourceData.ksParam_sName, self._oData.sName, 'Name') + oForm.addText (GlobalResourceData.ksParam_sDescription, self._oData.sDescription, 'Description') + oForm.addCheckBox (GlobalResourceData.ksParam_fEnabled, self._oData.fEnabled, 'Enabled') + + oForm.addSubmit('Submit') + + return (sTitle, oForm.finalize()) + + +class WuiGlobalResourceList(WuiListContentBase): + """ + WUI Content Generator. + """ + + def __init__(self, aoEntries, iPage, cItemsPerPage, tsEffective, fnDPrint, oDisp, aiSelectedSortColumns = None): + WuiListContentBase.__init__(self, aoEntries, iPage, cItemsPerPage, tsEffective, + sTitle = 'Global Resources', sId = 'globalResources', + fnDPrint = fnDPrint, oDisp = oDisp, aiSelectedSortColumns = aiSelectedSortColumns); + + self._asColumnHeaders = ['ID', 'Name', 'Description', 'Enabled', 'Actions' ] + self._asColumnAttribs = ['align="right"', 'align="center"', 'align="center"', + 'align="center"', 'align="center"'] + + def _formatListEntry(self, iEntry): + from testmanager.webui.wuiadmin import WuiAdmin + oEntry = self._aoEntries[iEntry] + + aoActions = [ ]; + if self._oDisp is None or not self._oDisp.isReadOnlyUser(): + aoActions += [ + WuiTmLink('Modify', WuiAdmin.ksScriptName, + { WuiAdmin.ksParamAction: WuiAdmin.ksActionGlobalRsrcShowEdit, + GlobalResourceData.ksParam_idGlobalRsrc: oEntry.idGlobalRsrc }), + WuiTmLink('Remove', WuiAdmin.ksScriptName, + { WuiAdmin.ksParamAction: WuiAdmin.ksActionGlobalRsrcDel, + GlobalResourceData.ksParam_idGlobalRsrc: oEntry.idGlobalRsrc }, + sConfirm = 'Are you sure you want to remove global resource #%d?' % (oEntry.idGlobalRsrc,)), + ]; + + return [ oEntry.idGlobalRsrc, + oEntry.sName, + oEntry.sDescription, + oEntry.fEnabled, + aoActions, ]; + diff --git a/src/VBox/ValidationKit/testmanager/webui/wuiadminschedgroup.py b/src/VBox/ValidationKit/testmanager/webui/wuiadminschedgroup.py new file mode 100755 index 00000000..e7a43717 --- /dev/null +++ b/src/VBox/ValidationKit/testmanager/webui/wuiadminschedgroup.py @@ -0,0 +1,205 @@ +# -*- coding: utf-8 -*- +# $Id: wuiadminschedgroup.py $ + +""" +Test Manager WUI - Scheduling groups. +""" + +__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 $" + + +# Validation Kit imports. +from testmanager.core.buildsource import BuildSourceData, BuildSourceLogic; +from testmanager.core.db import isDbTimestampInfinity; +from testmanager.core.schedgroup import SchedGroupData, SchedGroupDataEx; +from testmanager.core.testgroup import TestGroupData, TestGroupLogic; +from testmanager.core.testbox import TestBoxLogic; +from testmanager.webui.wuicontentbase import WuiFormContentBase, WuiListContentBase, WuiTmLink, WuiRawHtml; +from testmanager.webui.wuiadmintestbox import WuiTestBoxDetailsLink; + + +class WuiSchedGroup(WuiFormContentBase): + """ + WUI Scheduling Groups HTML content generator. + """ + + def __init__(self, oData, sMode, oDisp): + assert isinstance(oData, SchedGroupData); + if sMode == WuiFormContentBase.ksMode_Add: + sTitle = 'New Scheduling Group'; + elif sMode == WuiFormContentBase.ksMode_Edit: + sTitle = 'Edit Scheduling Group' + else: + assert sMode == WuiFormContentBase.ksMode_Show; + sTitle = 'Scheduling Group'; + WuiFormContentBase.__init__(self, oData, sMode, 'SchedGroup', oDisp, sTitle); + + # Read additional bits form the DB, unless we're in + if sMode != WuiFormContentBase.ksMode_Show: + self._aoAllRelevantTestGroups = TestGroupLogic(oDisp.getDb()).getAll(); + self._aoAllRelevantTestBoxes = TestBoxLogic(oDisp.getDb()).getAll(); + else: + self._aoAllRelevantTestGroups = [oMember.oTestGroup for oMember in oData.aoMembers]; + self._aoAllRelevantTestBoxes = [oMember.oTestBox for oMember in oData.aoTestBoxes]; + + def _populateForm(self, oForm, oData): # type: (WuiHlpForm, SchedGroupDataEx) -> bool + """ + Construct an HTML form + """ + + oForm.addIntRO( SchedGroupData.ksParam_idSchedGroup, oData.idSchedGroup, 'ID') + oForm.addTimestampRO(SchedGroupData.ksParam_tsEffective, oData.tsEffective, 'Last changed') + oForm.addTimestampRO(SchedGroupData.ksParam_tsExpire, oData.tsExpire, 'Expires (excl)') + oForm.addIntRO( SchedGroupData.ksParam_uidAuthor, oData.uidAuthor, 'Changed by UID') + oForm.addText( SchedGroupData.ksParam_sName, oData.sName, 'Name') + oForm.addText( SchedGroupData.ksParam_sDescription, oData.sDescription, 'Description') + oForm.addCheckBox( SchedGroupData.ksParam_fEnabled, oData.fEnabled, 'Enabled') + + oForm.addComboBox( SchedGroupData.ksParam_enmScheduler, oData.enmScheduler, 'Scheduler type', + SchedGroupData.kasSchedulerDesc) + + aoBuildSrcIds = BuildSourceLogic(self._oDisp.getDb()).fetchForCombo(); + oForm.addComboBox( SchedGroupData.ksParam_idBuildSrc, oData.idBuildSrc, 'Build source', aoBuildSrcIds); + oForm.addComboBox( SchedGroupData.ksParam_idBuildSrcTestSuite, + oData.idBuildSrcTestSuite, 'Test suite', aoBuildSrcIds); + + oForm.addListOfSchedGroupMembers(SchedGroupDataEx.ksParam_aoMembers, + oData.aoMembers, self._aoAllRelevantTestGroups, 'Test groups', + oData.idSchedGroup, fReadOnly = self._sMode == WuiFormContentBase.ksMode_Show); + + oForm.addListOfSchedGroupBoxes(SchedGroupDataEx.ksParam_aoTestBoxes, + oData.aoTestBoxes, self._aoAllRelevantTestBoxes, 'Test boxes', + oData.idSchedGroup, fReadOnly = self._sMode == WuiFormContentBase.ksMode_Show); + + oForm.addMultilineText(SchedGroupData.ksParam_sComment, oData.sComment, 'Comment'); + oForm.addSubmit() + + return True; + +class WuiAdminSchedGroupList(WuiListContentBase): + """ + Content generator for the schedule group listing. + """ + + def __init__(self, aoEntries, iPage, cItemsPerPage, tsEffective, fnDPrint, oDisp, aiSelectedSortColumns = None): + WuiListContentBase.__init__(self, aoEntries, iPage, cItemsPerPage, tsEffective, + sTitle = 'Registered Scheduling Groups', sId = 'schedgroups', + fnDPrint = fnDPrint, oDisp = oDisp, aiSelectedSortColumns = aiSelectedSortColumns); + + self._asColumnHeaders = [ + 'ID', 'Name', 'Enabled', 'Scheduler Type', + 'Build Source', 'Validation Kit Source', 'Test Groups', 'TestBoxes', 'Note', 'Actions', + ]; + + self._asColumnAttribs = [ + 'align="right"', 'align="center"', 'align="center"', 'align="center"', + 'align="center"', 'align="center"', '', '', 'align="center"', 'align="center"', + ]; + + def _formatListEntry(self, iEntry): + """ + Format *show all* table entry + """ + from testmanager.webui.wuiadmin import WuiAdmin + oEntry = self._aoEntries[iEntry] # type: SchedGroupDataEx + + oBuildSrc = None; + if oEntry.idBuildSrc is not None: + oBuildSrc = WuiTmLink(oEntry.oBuildSrc.sName if oEntry.oBuildSrc else str(oEntry.idBuildSrc), + WuiAdmin.ksScriptName, + { WuiAdmin.ksParamAction: WuiAdmin.ksActionBuildSrcDetails, + BuildSourceData.ksParam_idBuildSrc: oEntry.idBuildSrc, }); + + oValidationKitSrc = None; + if oEntry.idBuildSrcTestSuite is not None: + oValidationKitSrc = WuiTmLink(oEntry.oBuildSrcValidationKit.sName if oEntry.oBuildSrcValidationKit + else str(oEntry.idBuildSrcTestSuite), + WuiAdmin.ksScriptName, + { WuiAdmin.ksParamAction: WuiAdmin.ksActionBuildSrcDetails, + BuildSourceData.ksParam_idBuildSrc: oEntry.idBuildSrcTestSuite, }); + + # Test groups + aoMembers = []; + for oMember in oEntry.aoMembers: + aoMembers.append(WuiTmLink(oMember.oTestGroup.sName, WuiAdmin.ksScriptName, + { WuiAdmin.ksParamAction: WuiAdmin.ksActionTestGroupDetails, + TestGroupData.ksParam_idTestGroup: oMember.idTestGroup, + WuiAdmin.ksParamEffectiveDate: self._tsEffectiveDate, }, + sTitle = '#%s' % (oMember.idTestGroup,) if oMember.oTestGroup.sDescription is None + else '#%s - %s' % (oMember.idTestGroup, oMember.oTestGroup.sDescription,) )); + + # Test boxes. + aoTestBoxes = []; + for oRelation in oEntry.aoTestBoxes: + oTestBox = oRelation.oTestBox; + if oTestBox: + aoTestBoxes.append(WuiTestBoxDetailsLink(oTestBox, fBracketed = True, tsNow = self._tsEffectiveDate)); + else: + aoTestBoxes.append(WuiRawHtml('#%s' % (oRelation.idTestBox,))); + + # Actions + aoActions = [ WuiTmLink('Details', WuiAdmin.ksScriptName, + { WuiAdmin.ksParamAction: WuiAdmin.ksActionSchedGroupDetails, + SchedGroupData.ksParam_idSchedGroup: oEntry.idSchedGroup, + WuiAdmin.ksParamEffectiveDate: self._tsEffectiveDate, } ),]; + if self._oDisp is None or not self._oDisp.isReadOnlyUser(): + + if isDbTimestampInfinity(oEntry.tsExpire): + aoActions.append(WuiTmLink('Modify', WuiAdmin.ksScriptName, + { WuiAdmin.ksParamAction: WuiAdmin.ksActionSchedGroupEdit, + SchedGroupData.ksParam_idSchedGroup: oEntry.idSchedGroup } )); + aoActions.append(WuiTmLink('Clone', WuiAdmin.ksScriptName, + { WuiAdmin.ksParamAction: WuiAdmin.ksActionSchedGroupClone, + SchedGroupData.ksParam_idSchedGroup: oEntry.idSchedGroup, + WuiAdmin.ksParamEffectiveDate: self._tsEffectiveDate, } )); + if isDbTimestampInfinity(oEntry.tsExpire): + aoActions.append(WuiTmLink('Remove', WuiAdmin.ksScriptName, + { WuiAdmin.ksParamAction: WuiAdmin.ksActionSchedGroupDoRemove, + SchedGroupData.ksParam_idSchedGroup: oEntry.idSchedGroup }, + sConfirm = 'Are you sure you want to remove scheduling group #%d?' + % (oEntry.idSchedGroup,))); + + return [ + oEntry.idSchedGroup, + oEntry.sName, + oEntry.fEnabled, + oEntry.enmScheduler, + oBuildSrc, + oValidationKitSrc, + aoMembers, + aoTestBoxes, + self._formatCommentCell(oEntry.sComment), + aoActions, + ]; + diff --git a/src/VBox/ValidationKit/testmanager/webui/wuiadminschedqueue.py b/src/VBox/ValidationKit/testmanager/webui/wuiadminschedqueue.py new file mode 100755 index 00000000..88a3475a --- /dev/null +++ b/src/VBox/ValidationKit/testmanager/webui/wuiadminschedqueue.py @@ -0,0 +1,79 @@ +# -*- coding: utf-8 -*- +# "$Id: wuiadminschedqueue.py $" + +""" +Test Manager WUI - Admin - Scheduling Queue. +""" + +__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 $" + + +# Validation Kit imports +from testmanager.webui.wuicontentbase import WuiListContentBase + + +class WuiAdminSchedQueueList(WuiListContentBase): + """ + WUI Scheduling Queue Content Generator. + """ + def __init__(self, aoEntries, iPage, cItemsPerPage, tsEffective, fnDPrint, oDisp, aiSelectedSortColumns = None): + tsEffective = None; # Not relevant, no history on the scheduling queue. + WuiListContentBase.__init__(self, aoEntries, iPage, cItemsPerPage, tsEffective, 'Scheduling Queue', + fnDPrint = fnDPrint, oDisp = oDisp, aiSelectedSortColumns = aiSelectedSortColumns, + fTimeNavigation = False); + self._asColumnHeaders = [ + 'Last Run', 'Scheduling Group', 'Test Group', 'Test Case', 'Config State', 'Item ID' + ]; + self._asColumnAttribs = [ + 'align="center"', 'align="center"', 'align="center"', 'align="center"', 'align="center"', 'align="center"' + ]; + self._iPrevPerSchedGroupRowNumber = 0; + + def _formatListEntry(self, iEntry): + oEntry = self._aoEntries[iEntry] # type: SchedQueueEntry + sState = 'up-to-date' if oEntry.fUpToDate else 'outdated'; + return [ oEntry.tsLastScheduled, oEntry.sSchedGroup, oEntry.sTestGroup, oEntry.sTestCase, sState, oEntry.idItem ]; + + def _formatListEntryHtml(self, iEntry): + sHtml = WuiListContentBase._formatListEntryHtml(self, iEntry); + + # Insert separator row? + if iEntry < len(self._aoEntries): + oEntry = self._aoEntries[iEntry] # type: SchedQueueEntry + if oEntry.iPerSchedGroupRowNumber != self._iPrevPerSchedGroupRowNumber: + if iEntry > 0 and iEntry + 1 < min(len(self._aoEntries), self._cItemsPerPage): + sHtml += '<tr class="tmseparator"><td colspan=%s> </td></tr>\n' % (len(self._asColumnHeaders),); + self._iPrevPerSchedGroupRowNumber = oEntry.iPerSchedGroupRowNumber; + return sHtml; + diff --git a/src/VBox/ValidationKit/testmanager/webui/wuiadminsystemchangelog.py b/src/VBox/ValidationKit/testmanager/webui/wuiadminsystemchangelog.py new file mode 100755 index 00000000..94057524 --- /dev/null +++ b/src/VBox/ValidationKit/testmanager/webui/wuiadminsystemchangelog.py @@ -0,0 +1,447 @@ +# -*- coding: utf-8 -*- +# $Id: wuiadminsystemchangelog.py $ + +""" +Test Manager WUI - Admin - System changelog. +""" + +__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 $" + + +from common import webutils; + +# Validation Kit imports. +from testmanager.webui.wuicontentbase import WuiListContentBase, WuiHtmlKeeper, WuiAdminLink, \ + WuiMainLink, WuiElementText, WuiHtmlBase; + +from testmanager.core.base import AttributeChangeEntryPre; +from testmanager.core.buildblacklist import BuildBlacklistLogic, BuildBlacklistData; +from testmanager.core.build import BuildLogic, BuildData; +from testmanager.core.buildsource import BuildSourceLogic, BuildSourceData; +from testmanager.core.globalresource import GlobalResourceLogic, GlobalResourceData; +from testmanager.core.failurecategory import FailureCategoryLogic, FailureCategoryData; +from testmanager.core.failurereason import FailureReasonLogic, FailureReasonData; +from testmanager.core.systemlog import SystemLogData; +from testmanager.core.systemchangelog import SystemChangelogLogic; +from testmanager.core.schedgroup import SchedGroupLogic, SchedGroupData; +from testmanager.core.testbox import TestBoxLogic, TestBoxData; +from testmanager.core.testcase import TestCaseLogic, TestCaseData; +from testmanager.core.testgroup import TestGroupLogic, TestGroupData; +from testmanager.core.testset import TestSetData; +from testmanager.core.useraccount import UserAccountLogic, UserAccountData; + + +class WuiAdminSystemChangelogList(WuiListContentBase): + """ + WUI System Changelog Content Generator. + """ + + def __init__(self, aoEntries, iPage, cItemsPerPage, tsEffective, fnDPrint, oDisp, cDaysBack, aiSelectedSortColumns = None): + WuiListContentBase.__init__(self, aoEntries, iPage, cItemsPerPage, tsEffective, 'System Changelog', + fnDPrint = fnDPrint, oDisp = oDisp, aiSelectedSortColumns = aiSelectedSortColumns); + self._asColumnHeaders = [ 'When', 'User', 'Event', 'Details' ]; + self._asColumnAttribs = [ 'align="center"', 'align="center"', '', '' ]; + self._oBuildBlacklistLogic = BuildBlacklistLogic(oDisp.getDb()); + self._oBuildLogic = BuildLogic(oDisp.getDb()); + self._oBuildSourceLogic = BuildSourceLogic(oDisp.getDb()); + self._oFailureCategoryLogic = FailureCategoryLogic(oDisp.getDb()); + self._oFailureReasonLogic = FailureReasonLogic(oDisp.getDb()); + self._oGlobalResourceLogic = GlobalResourceLogic(oDisp.getDb()); + self._oSchedGroupLogic = SchedGroupLogic(oDisp.getDb()); + self._oTestBoxLogic = TestBoxLogic(oDisp.getDb()); + self._oTestCaseLogic = TestCaseLogic(oDisp.getDb()); + self._oTestGroupLogic = TestGroupLogic(oDisp.getDb()); + self._oUserAccountLogic = UserAccountLogic(oDisp.getDb()); + self._sPrevDate = ''; + _ = cDaysBack; + + # oDetails = self._createBlacklistingDetailsLink(oEntry.idWhat, oEntry.tsEffective); + def _createBlacklistingDetailsLink(self, idBlacklisting, tsEffective): + """ Creates a link to the build source details. """ + oBlacklisting = self._oBuildBlacklistLogic.cachedLookup(idBlacklisting); + if oBlacklisting is not None: + from testmanager.webui.wuiadmin import WuiAdmin; + return WuiAdminLink('Blacklisting #%u' % (oBlacklisting.idBlacklisting,), + WuiAdmin.ksActionBuildBlacklistDetails, tsEffective, + { BuildBlacklistData.ksParam_idBlacklisting: oBlacklisting.idBlacklisting }, + fBracketed = False); + return WuiElementText('[blacklisting #%u not found]' % (idBlacklisting,)); + + def _createBuildDetailsLink(self, idBuild, tsEffective): + """ Creates a link to the build details. """ + oBuild = self._oBuildLogic.cachedLookup(idBuild); + if oBuild is not None: + from testmanager.webui.wuiadmin import WuiAdmin; + return WuiAdminLink('%s %sr%u' % ( oBuild.oCat.sProduct, oBuild.sVersion, oBuild.iRevision), + WuiAdmin.ksActionBuildDetails, tsEffective, + { BuildData.ksParam_idBuild: oBuild.idBuild }, + fBracketed = False, + sTitle = 'build #%u for %s, type %s' + % (oBuild.idBuild, ' & '.join(oBuild.oCat.asOsArches), oBuild.oCat.sType)); + return WuiElementText('[build #%u not found]' % (idBuild,)); + + def _createBuildSourceDetailsLink(self, idBuildSrc, tsEffective): + """ Creates a link to the build source details. """ + oBuildSource = self._oBuildSourceLogic.cachedLookup(idBuildSrc); + if oBuildSource is not None: + from testmanager.webui.wuiadmin import WuiAdmin; + return WuiAdminLink(oBuildSource.sName, WuiAdmin.ksActionBuildSrcDetails, tsEffective, + { BuildSourceData.ksParam_idBuildSrc: oBuildSource.idBuildSrc }, + fBracketed = False, + sTitle = 'Build source #%u' % (oBuildSource.idBuildSrc,)); + return WuiElementText('[build source #%u not found]' % (idBuildSrc,)); + + def _createFailureCategoryDetailsLink(self, idFailureCategory, tsEffective): + """ Creates a link to the failure category details. """ + oFailureCategory = self._oFailureCategoryLogic.cachedLookup(idFailureCategory); + if oFailureCategory is not None: + from testmanager.webui.wuiadmin import WuiAdmin; + return WuiAdminLink(oFailureCategory.sShort, WuiAdmin.ksActionFailureCategoryDetails, tsEffective, + { FailureCategoryData.ksParam_idFailureCategory: oFailureCategory.idFailureCategory }, + fBracketed = False, + sTitle = 'Failure category #%u' % (oFailureCategory.idFailureCategory,)); + return WuiElementText('[failure category #%u not found]' % (idFailureCategory,)); + + def _createFailureReasonDetailsLink(self, idFailureReason, tsEffective): + """ Creates a link to the failure reason details. """ + oFailureReason = self._oFailureReasonLogic.cachedLookup(idFailureReason); + if oFailureReason is not None: + from testmanager.webui.wuiadmin import WuiAdmin; + return WuiAdminLink(oFailureReason.sShort, WuiAdmin.ksActionFailureReasonDetails, tsEffective, + { FailureReasonData.ksParam_idFailureReason: oFailureReason.idFailureReason }, + fBracketed = False, + sTitle = 'Failure reason #%u, category %s' + % (oFailureReason.idFailureReason, oFailureReason.oCategory.sShort)); + return WuiElementText('[failure reason #%u not found]' % (idFailureReason,)); + + def _createGlobalResourceDetailsLink(self, idGlobalRsrc, tsEffective): + """ Creates a link to the global resource details. """ + oGlobalResource = self._oGlobalResourceLogic.cachedLookup(idGlobalRsrc); + if oGlobalResource is not None: + return WuiAdminLink(oGlobalResource.sName, '@todo', tsEffective, + { GlobalResourceData.ksParam_idGlobalRsrc: oGlobalResource.idGlobalRsrc }, + fBracketed = False, + sTitle = 'Global resource #%u' % (oGlobalResource.idGlobalRsrc,)); + return WuiElementText('[global resource #%u not found]' % (idGlobalRsrc,)); + + def _createSchedGroupDetailsLink(self, idSchedGroup, tsEffective): + """ Creates a link to the scheduling group details. """ + oSchedGroup = self._oSchedGroupLogic.cachedLookup(idSchedGroup); + if oSchedGroup is not None: + from testmanager.webui.wuiadmin import WuiAdmin; + return WuiAdminLink(oSchedGroup.sName, WuiAdmin.ksActionSchedGroupDetails, tsEffective, + { SchedGroupData.ksParam_idSchedGroup: oSchedGroup.idSchedGroup }, + fBracketed = False, + sTitle = 'Scheduling group #%u' % (oSchedGroup.idSchedGroup,)); + return WuiElementText('[scheduling group #%u not found]' % (idSchedGroup,)); + + def _createTestBoxDetailsLink(self, idTestBox, tsEffective): + """ Creates a link to the testbox details. """ + oTestBox = self._oTestBoxLogic.cachedLookup(idTestBox); + if oTestBox is not None: + from testmanager.webui.wuiadmin import WuiAdmin; + return WuiAdminLink(oTestBox.sName, WuiAdmin.ksActionTestBoxDetails, tsEffective, + { TestBoxData.ksParam_idTestBox: oTestBox.idTestBox }, + fBracketed = False, sTitle = 'Testbox #%u' % (oTestBox.idTestBox,)); + return WuiElementText('[testbox #%u not found]' % (idTestBox,)); + + def _createTestCaseDetailsLink(self, idTestCase, tsEffective): + """ Creates a link to the test case details. """ + oTestCase = self._oTestCaseLogic.cachedLookup(idTestCase); + if oTestCase is not None: + from testmanager.webui.wuiadmin import WuiAdmin; + return WuiAdminLink(oTestCase.sName, WuiAdmin.ksActionTestCaseDetails, tsEffective, + { TestCaseData.ksParam_idTestCase: oTestCase.idTestCase }, + fBracketed = False, sTitle = 'Test case #%u' % (oTestCase.idTestCase,)); + return WuiElementText('[test case #%u not found]' % (idTestCase,)); + + def _createTestGroupDetailsLink(self, idTestGroup, tsEffective): + """ Creates a link to the test group details. """ + oTestGroup = self._oTestGroupLogic.cachedLookup(idTestGroup); + if oTestGroup is not None: + from testmanager.webui.wuiadmin import WuiAdmin; + return WuiAdminLink(oTestGroup.sName, WuiAdmin.ksActionTestGroupDetails, tsEffective, + { TestGroupData.ksParam_idTestGroup: oTestGroup.idTestGroup }, + fBracketed = False, sTitle = 'Test group #%u' % (oTestGroup.idTestGroup,)); + return WuiElementText('[test group #%u not found]' % (idTestGroup,)); + + def _createTestSetResultsDetailsLink(self, idTestSet, tsEffective): + """ Creates a link to the test set results. """ + _ = tsEffective; + from testmanager.webui.wuimain import WuiMain; + return WuiMainLink('test set #%u' % idTestSet, WuiMain.ksActionTestSetDetails, + { TestSetData.ksParam_idTestSet: idTestSet }, fBracketed = False); + + def _createTestSetDetailsLinkByResult(self, idTestResult, tsEffective): + """ Creates a link to the test set results. """ + _ = tsEffective; + from testmanager.webui.wuimain import WuiMain; + return WuiMainLink('test result #%u' % idTestResult, WuiMain.ksActionTestSetDetailsFromResult, + { TestSetData.ksParam_idTestResult: idTestResult }, fBracketed = False); + + def _createUserAccountDetailsLink(self, uid, tsEffective): + """ Creates a link to the user account details. """ + oUser = self._oUserAccountLogic.cachedLookup(uid); + if oUser is not None: + return WuiAdminLink(oUser.sUsername, '@todo', tsEffective, { UserAccountData.ksParam_uid: oUser.uid }, + fBracketed = False, sTitle = '%s (#%u)' % (oUser.sFullName, oUser.uid)); + return WuiElementText('[user #%u not found]' % (uid,)); + + def _formatDescGeneric(self, sDesc, oEntry): + """ + Generically format system log the description. + """ + oRet = WuiHtmlKeeper(); + asWords = sDesc.split(); + for sWord in asWords: + offEqual = sWord.find('='); + if offEqual > 0: + sKey = sWord[:offEqual]; + try: idValue = int(sWord[offEqual+1:].rstrip('.,')); + except: pass; + else: + if sKey == 'idTestSet': + oRet.append(self._createTestSetResultsDetailsLink(idValue, oEntry.tsEffective)); + continue; + if sKey == 'idTestBox': + oRet.append(self._createTestBoxDetailsLink(idValue, oEntry.tsEffective)); + continue; + if sKey == 'idSchedGroup': + oRet.append(self._createSchedGroupDetailsLink(idValue, oEntry.tsEffective)); + continue; + + oRet.append(WuiElementText(sWord)); + return oRet; + + def _formatListEntryHtml(self, iEntry): # pylint: disable=too-many-statements + """ + Overridden parent method. + """ + oEntry = self._aoEntries[iEntry]; + sRowClass = 'tmodd' if (iEntry + 1) & 1 else 'tmeven'; + sHtml = u''; + + # + # Format the timestamp. + # + sDate = self.formatTsShort(oEntry.tsEffective); + if sDate[:10] != self._sPrevDate: + self._sPrevDate = sDate[:10]; + sHtml += ' <tr class="%s tmdaterow" align="left"><td colspan="7">%s</td></tr>\n' % (sRowClass, sDate[:10],); + sDate = sDate[11:] + + # + # System log events. + # pylint: disable=redefined-variable-type + # + aoChanges = None; + if oEntry.sEvent == SystemLogData.ksEvent_CmdNacked: + sEvent = 'Command not acknowleged'; + oDetails = oEntry.sDesc; + + elif oEntry.sEvent == SystemLogData.ksEvent_TestBoxUnknown: + sEvent = 'Unknown testbox'; + oDetails = oEntry.sDesc; + + elif oEntry.sEvent == SystemLogData.ksEvent_TestSetAbandoned: + sEvent = 'Abandoned ' if oEntry.sDesc.startswith('idTestSet') else 'Abandoned test set'; + oDetails = self._formatDescGeneric(oEntry.sDesc, oEntry); + + elif oEntry.sEvent == SystemLogData.ksEvent_UserAccountUnknown: + sEvent = 'Unknown user account'; + oDetails = oEntry.sDesc; + + elif oEntry.sEvent == SystemLogData.ksEvent_XmlResultMalformed: + sEvent = 'Malformed XML result'; + oDetails = oEntry.sDesc; + + elif oEntry.sEvent == SystemLogData.ksEvent_SchedQueueRecreate: + sEvent = 'Recreating scheduling queue'; + asWords = oEntry.sDesc.split(); + if len(asWords) > 3 and asWords[0] == 'User' and asWords[1][0] == '#': + try: idAuthor = int(asWords[1][1:]); + except: pass; + else: + oEntry.oAuthor = self._oUserAccountLogic.cachedLookup(idAuthor); + if oEntry.oAuthor is not None: + i = 2; + if asWords[i] == 'recreated': i += 1; + oEntry.sDesc = ' '.join(asWords[i:]); + oDetails = self._formatDescGeneric(oEntry.sDesc.replace('sched queue #', 'for scheduling group idSchedGroup='), + oEntry); + # + # System changelog events. + # + elif oEntry.sEvent == SystemChangelogLogic.ksWhat_Blacklisting: + sEvent = 'Modified blacklisting'; + oDetails = self._createBlacklistingDetailsLink(oEntry.idWhat, oEntry.tsEffective); + + elif oEntry.sEvent == SystemChangelogLogic.ksWhat_Build: + sEvent = 'Modified build'; + oDetails = self._createBuildDetailsLink(oEntry.idWhat, oEntry.tsEffective); + + elif oEntry.sEvent == SystemChangelogLogic.ksWhat_BuildSource: + sEvent = 'Modified build source'; + oDetails = self._createBuildSourceDetailsLink(oEntry.idWhat, oEntry.tsEffective); + + elif oEntry.sEvent == SystemChangelogLogic.ksWhat_GlobalRsrc: + sEvent = 'Modified global resource'; + oDetails = self._createGlobalResourceDetailsLink(oEntry.idWhat, oEntry.tsEffective); + + elif oEntry.sEvent == SystemChangelogLogic.ksWhat_FailureCategory: + sEvent = 'Modified failure category'; + oDetails = self._createFailureCategoryDetailsLink(oEntry.idWhat, oEntry.tsEffective); + (aoChanges, _) = self._oFailureCategoryLogic.fetchForChangeLog(oEntry.idWhat, 0, 1, oEntry.tsEffective); + + elif oEntry.sEvent == SystemChangelogLogic.ksWhat_FailureReason: + sEvent = 'Modified failure reason'; + oDetails = self._createFailureReasonDetailsLink(oEntry.idWhat, oEntry.tsEffective); + (aoChanges, _) = self._oFailureReasonLogic.fetchForChangeLog(oEntry.idWhat, 0, 1, oEntry.tsEffective); + + elif oEntry.sEvent == SystemChangelogLogic.ksWhat_SchedGroup: + sEvent = 'Modified scheduling group'; + oDetails = self._createSchedGroupDetailsLink(oEntry.idWhat, oEntry.tsEffective); + + elif oEntry.sEvent == SystemChangelogLogic.ksWhat_TestBox: + sEvent = 'Modified testbox'; + oDetails = self._createTestBoxDetailsLink(oEntry.idWhat, oEntry.tsEffective); + (aoChanges, _) = self._oTestBoxLogic.fetchForChangeLog(oEntry.idWhat, 0, 1, oEntry.tsEffective); + + elif oEntry.sEvent == SystemChangelogLogic.ksWhat_TestCase: + sEvent = 'Modified test case'; + oDetails = self._createTestCaseDetailsLink(oEntry.idWhat, oEntry.tsEffective); + (aoChanges, _) = self._oTestCaseLogic.fetchForChangeLog(oEntry.idWhat, 0, 1, oEntry.tsEffective); + + elif oEntry.sEvent == SystemChangelogLogic.ksWhat_TestGroup: + sEvent = 'Modified test group'; + oDetails = self._createTestGroupDetailsLink(oEntry.idWhat, oEntry.tsEffective); + + elif oEntry.sEvent == SystemChangelogLogic.ksWhat_TestResult: + sEvent = 'Modified test failure reason'; + oDetails = self._createTestSetDetailsLinkByResult(oEntry.idWhat, oEntry.tsEffective); + + elif oEntry.sEvent == SystemChangelogLogic.ksWhat_User: + sEvent = 'Modified user account'; + oDetails = self._createUserAccountDetailsLink(oEntry.idWhat, oEntry.tsEffective); + + else: + sEvent = '%s(%s)' % (oEntry.sEvent, oEntry.idWhat,); + oDetails = '!Unknown event!' + (oEntry.sDesc if oEntry.sDesc else ''); + + # + # Do the formatting. + # + + if aoChanges: + oChangeEntry = aoChanges[0]; + cAttribsChanged = len(oChangeEntry.aoChanges) + 1; + if oChangeEntry.oOldRaw is None and sEvent.startswith('Modified '): + sEvent = 'Created ' + sEvent[9:]; + + else: + oChangeEntry = None; + cAttribsChanged = -1; + + sHtml += u' <tr class="%s">\n' \ + u' <td rowspan="%d" align="center" >%s</td>\n' \ + u' <td rowspan="%d" align="center" >%s</td>\n' \ + u' <td colspan="5" class="%s%s">%s %s</td>\n' \ + u' </tr>\n' \ + % ( sRowClass, + 1 + cAttribsChanged + 1, sDate, + 1 + cAttribsChanged + 1, webutils.escapeElem(oEntry.oAuthor.sUsername if oEntry.oAuthor is not None else ''), + sRowClass, ' tmsyschlogevent' if oChangeEntry is not None else '', webutils.escapeElem(sEvent), + oDetails.toHtml() if isinstance(oDetails, WuiHtmlBase) else oDetails, + ); + + if oChangeEntry is not None: + sHtml += u' <tr class="%s tmsyschlogspacerrowabove">\n' \ + u' <td xrowspan="%d" style="border-right: 0px; border-bottom: 0px;"></td>\n' \ + u' <td colspan="3" style="border-right: 0px;"></td>\n' \ + u' <td rowspan="%d" class="%s tmsyschlogspacer"></td>\n' \ + u' </tr>\n' \ + % (sRowClass, cAttribsChanged + 1, cAttribsChanged + 1, sRowClass); + for j, oChange in enumerate(oChangeEntry.aoChanges): + fLastRow = j + 1 == len(oChangeEntry.aoChanges); + sHtml += u' <tr class="%s%s tmsyschlogattr%s">\n' \ + % ( sRowClass, 'odd' if j & 1 else 'even', ' tmsyschlogattrfinal' if fLastRow else '',); + if j == 0: + sHtml += u' <td class="%s tmsyschlogspacer" rowspan="%d"></td>\n' % (sRowClass, cAttribsChanged - 1,); + + if isinstance(oChange, AttributeChangeEntryPre): + sHtml += u' <td class="%s%s">%s</td>\n' \ + u' <td><div class="tdpre"><pre>%s</pre></div></td>\n' \ + u' <td class="%s%s"><div class="tdpre"><pre>%s</pre></div></td>\n' \ + % ( ' tmtopleft' if j == 0 else '', ' tmbottomleft' if fLastRow else '', + webutils.escapeElem(oChange.sAttr), + webutils.escapeElem(oChange.sOldText), + ' tmtopright' if j == 0 else '', ' tmbottomright' if fLastRow else '', + webutils.escapeElem(oChange.sNewText), ); + else: + sHtml += u' <td class="%s%s">%s</td>\n' \ + u' <td>%s</td>\n' \ + u' <td class="%s%s">%s</td>\n' \ + % ( ' tmtopleft' if j == 0 else '', ' tmbottomleft' if fLastRow else '', + webutils.escapeElem(oChange.sAttr), + webutils.escapeElem(oChange.sOldText), + ' tmtopright' if j == 0 else '', ' tmbottomright' if fLastRow else '', + webutils.escapeElem(oChange.sNewText), ); + sHtml += u' </tr>\n'; + + if oChangeEntry is not None: + sHtml += u' <tr class="%s tmsyschlogspacerrowbelow "><td colspan="5"></td></tr>\n\n' % (sRowClass,); + return sHtml; + + + def _generateTableHeaders(self): + """ + Overridden parent method. + """ + + sHtml = u'<thead class="tmheader">\n' \ + u' <tr>\n' \ + u' <th rowspan="2">When</th>\n' \ + u' <th rowspan="2">Who</th>\n' \ + u' <th colspan="5">Event</th>\n' \ + u' </tr>\n' \ + u' <tr>\n' \ + u' <th style="border-right: 0px;"></th>\n' \ + u' <th>Attribute</th>\n' \ + u' <th>Old</th>\n' \ + u' <th style="border-right: 0px;">New</th>\n' \ + u' <th></th>\n' \ + u' </tr>\n' \ + u'</thead>\n'; + return sHtml; + diff --git a/src/VBox/ValidationKit/testmanager/webui/wuiadminsystemdbdump.py b/src/VBox/ValidationKit/testmanager/webui/wuiadminsystemdbdump.py new file mode 100755 index 00000000..b0e6f99c --- /dev/null +++ b/src/VBox/ValidationKit/testmanager/webui/wuiadminsystemdbdump.py @@ -0,0 +1,72 @@ +# -*- coding: utf-8 -*- +# $Id: wuiadminsystemdbdump.py $ + +""" +Test Manager WUI - System DB - Partial Dumping +""" + +__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 $" + + +# Validation Kit imports. +from testmanager.core.base import ModelDataBase; +from testmanager.webui.wuicontentbase import WuiFormContentBase; + + +class WuiAdminSystemDbDumpForm(WuiFormContentBase): + """ + WUI Partial DB Dump HTML content generator. + """ + + def __init__(self, cDaysBack, oDisp): + WuiFormContentBase.__init__(self, ModelDataBase(), + WuiFormContentBase.ksMode_Edit, 'DbDump', oDisp, 'Partial DB Dump', + sSubmitAction = oDisp.ksActionSystemDbDumpDownload); + self._cDaysBack = cDaysBack; + + def _generateTopRowFormActions(self, oData): + _ = oData; + return []; + + def _populateForm(self, oForm, oData): # type: (WuiHlpForm, SchedGroupDataEx) -> bool + """ + Construct an HTML form + """ + _ = oData; + + oForm.addInt(self._oDisp.ksParamDaysBack, self._cDaysBack, 'How many days back to dump'); + oForm.addSubmit('Produce & Download'); + + return True; + diff --git a/src/VBox/ValidationKit/testmanager/webui/wuiadminsystemlog.py b/src/VBox/ValidationKit/testmanager/webui/wuiadminsystemlog.py new file mode 100755 index 00000000..6b3b7a45 --- /dev/null +++ b/src/VBox/ValidationKit/testmanager/webui/wuiadminsystemlog.py @@ -0,0 +1,85 @@ +# -*- coding: utf-8 -*- +# $Id: wuiadminsystemlog.py $ + +""" +Test Manager WUI - Admin - System Log. +""" + +__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 $" + + +# Validation Kit imports. +from testmanager.webui.wuicontentbase import WuiListContentBase, WuiTmLink; +from testmanager.core.testbox import TestBoxData; +from testmanager.core.systemlog import SystemLogData; +from testmanager.core.useraccount import UserAccountData; + + +class WuiAdminSystemLogList(WuiListContentBase): + """ + WUI System Log Content Generator. + """ + + def __init__(self, aoEntries, iPage, cItemsPerPage, tsEffective, fnDPrint, oDisp, aiSelectedSortColumns = None): + WuiListContentBase.__init__(self, aoEntries, iPage, cItemsPerPage, tsEffective, 'System Log', + fnDPrint = fnDPrint, oDisp = oDisp, aiSelectedSortColumns = aiSelectedSortColumns); + self._asColumnHeaders = ['Date', 'Event', 'Message', 'Action']; + self._asColumnAttribs = ['', '', '', 'align="center"']; + + def _formatListEntry(self, iEntry): + from testmanager.webui.wuiadmin import WuiAdmin; + oEntry = self._aoEntries[iEntry]; + + oAction = None + + if self._oDisp is None or not self._oDisp.isReadOnlyUser(): + if oEntry.sEvent == SystemLogData.ksEvent_TestBoxUnknown \ + and oEntry.sLogText.find('addr=') >= 0 \ + and oEntry.sLogText.find('uuid=') >= 0: + sUuid = (oEntry.sLogText[(oEntry.sLogText.find('uuid=') + 5):])[:36]; + sAddr = (oEntry.sLogText[(oEntry.sLogText.find('addr=') + 5):]).split(' ')[0]; + oAction = WuiTmLink('Add TestBox', WuiAdmin.ksScriptName, + { WuiAdmin.ksParamAction: WuiAdmin.ksActionTestBoxAdd, + TestBoxData.ksParam_uuidSystem: sUuid, + TestBoxData.ksParam_ip: sAddr }); + + elif oEntry.sEvent == SystemLogData.ksEvent_UserAccountUnknown: + sUserName = oEntry.sLogText[oEntry.sLogText.find('(') + 1: + oEntry.sLogText.find(')')] + oAction = WuiTmLink('Add User', WuiAdmin.ksScriptName, + { WuiAdmin.ksParamAction: WuiAdmin.ksActionUserAdd, + UserAccountData.ksParam_sLoginName: sUserName }); + + return [oEntry.tsCreated, oEntry.sEvent, oEntry.sLogText, oAction]; + diff --git a/src/VBox/ValidationKit/testmanager/webui/wuiadmintestbox.py b/src/VBox/ValidationKit/testmanager/webui/wuiadmintestbox.py new file mode 100755 index 00000000..a06e670c --- /dev/null +++ b/src/VBox/ValidationKit/testmanager/webui/wuiadmintestbox.py @@ -0,0 +1,490 @@ +# -*- coding: utf-8 -*- +# $Id: wuiadmintestbox.py $ + +""" +Test Manager WUI - TestBox. +""" + +__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 socket; + +# Validation Kit imports. +from common import utils, webutils; +from testmanager.webui.wuicontentbase import WuiContentBase, WuiListContentWithActionBase, WuiFormContentBase, WuiLinkBase, \ + WuiSvnLink, WuiTmLink, WuiSpanText, WuiRawHtml; +from testmanager.core.db import TMDatabaseConnection; +from testmanager.core.schedgroup import SchedGroupLogic, SchedGroupData; +from testmanager.core.testbox import TestBoxData, TestBoxDataEx, TestBoxLogic; +from testmanager.core.testset import TestSetData; +from testmanager.core.db import isDbTimestampInfinity; + + + +class WuiTestBoxDetailsLinkById(WuiTmLink): + """ Test box details link by ID. """ + + def __init__(self, idTestBox, sName = WuiContentBase.ksShortDetailsLink, fBracketed = False, tsNow = None, sTitle = None): + from testmanager.webui.wuiadmin import WuiAdmin; + dParams = { + WuiAdmin.ksParamAction: WuiAdmin.ksActionTestBoxDetails, + TestBoxData.ksParam_idTestBox: idTestBox, + }; + if tsNow is not None: + dParams[WuiAdmin.ksParamEffectiveDate] = tsNow; ## ?? + WuiTmLink.__init__(self, sName, WuiAdmin.ksScriptName, dParams, fBracketed = fBracketed, sTitle = sTitle); + self.idTestBox = idTestBox; + + +class WuiTestBoxDetailsLink(WuiTestBoxDetailsLinkById): + """ Test box details link by TestBoxData instance. """ + + def __init__(self, oTestBox, sName = None, fBracketed = False, tsNow = None): # (TestBoxData, str, bool, Any) -> None + WuiTestBoxDetailsLinkById.__init__(self, oTestBox.idTestBox, + sName if sName else oTestBox.sName, + fBracketed = fBracketed, + tsNow = tsNow, + sTitle = self.formatTitleText(oTestBox)); + self.oTestBox = oTestBox; + + @staticmethod + def formatTitleText(oTestBox): # (TestBoxData) -> str + """ + Formats the title text for a TestBoxData object. + """ + + # Note! Somewhat similar code is found in testresults.py + + # + # Collect field/value tuples. + # + aasTestBoxTitle = [ + (u'Identifier:', '#%u' % (oTestBox.idTestBox,),), + (u'Name:', oTestBox.sName,), + ]; + if oTestBox.sCpuVendor: + aasTestBoxTitle.append((u'CPU\u00a0vendor:', oTestBox.sCpuVendor, )); + if oTestBox.sCpuName: + aasTestBoxTitle.append((u'CPU\u00a0name:', u'\u00a0'.join(oTestBox.sCpuName.split()),)); + if oTestBox.cCpus: + aasTestBoxTitle.append((u'CPU\u00a0threads:', u'%s' % ( oTestBox.cCpus, ),)); + + asFeatures = []; + if oTestBox.fCpuHwVirt is True: + if oTestBox.sCpuVendor is None: + asFeatures.append(u'HW\u2011Virt'); + elif oTestBox.sCpuVendor in ['AuthenticAMD',]: + asFeatures.append(u'HW\u2011Virt(AMD\u2011V)'); + else: + asFeatures.append(u'HW\u2011Virt(VT\u2011x)'); + if oTestBox.fCpuNestedPaging is True: asFeatures.append(u'Nested\u2011Paging'); + if oTestBox.fCpu64BitGuest is True: asFeatures.append(u'64\u2011bit\u2011Guest'); + if oTestBox.fChipsetIoMmu is True: asFeatures.append(u'I/O\u2011MMU'); + aasTestBoxTitle.append((u'CPU\u00a0features:', u',\u00a0'.join(asFeatures),)); + + if oTestBox.cMbMemory: + aasTestBoxTitle.append((u'System\u00a0RAM:', u'%s MiB' % ( oTestBox.cMbMemory, ),)); + if oTestBox.sOs: + aasTestBoxTitle.append((u'OS:', oTestBox.sOs, )); + if oTestBox.sCpuArch: + aasTestBoxTitle.append((u'OS\u00a0arch:', oTestBox.sCpuArch,)); + if oTestBox.sOsVersion: + aasTestBoxTitle.append((u'OS\u00a0version:', u'\u00a0'.join(oTestBox.sOsVersion.split()),)); + if oTestBox.ip: + aasTestBoxTitle.append((u'IP\u00a0address:', u'%s' % ( oTestBox.ip, ),)); + + # + # Do a guestimation of the max field name width and pad short + # names when constructing the title text lines. + # + cchMaxWidth = 0; + for sEntry, _ in aasTestBoxTitle: + cchMaxWidth = max(WuiTestBoxDetailsLink.estimateStringWidth(sEntry), cchMaxWidth); + asTestBoxTitle = []; + for sEntry, sValue in aasTestBoxTitle: + asTestBoxTitle.append(u'%s%s\t\t%s' + % (sEntry, WuiTestBoxDetailsLink.getStringWidthPadding(sEntry, cchMaxWidth), sValue)); + + return u'\n'.join(asTestBoxTitle); + + +class WuiTestBoxDetailsLinkShort(WuiTestBoxDetailsLink): + """ Test box details link by TestBoxData instance, but with ksShortDetailsLink as default name. """ + + def __init__(self, oTestBox, sName = WuiContentBase.ksShortDetailsLink, fBracketed = False, + tsNow = None): # (TestBoxData, str, bool, Any) -> None + WuiTestBoxDetailsLink.__init__(self, oTestBox, sName = sName, fBracketed = fBracketed, tsNow = tsNow); + + +class WuiTestBox(WuiFormContentBase): + """ + WUI TestBox Form Content Generator. + """ + + def __init__(self, oData, sMode, oDisp): + if sMode == WuiFormContentBase.ksMode_Add: + sTitle = 'Create TextBox'; + if oData.uuidSystem is not None and len(oData.uuidSystem) > 10: + sTitle += ' - ' + oData.uuidSystem; + elif sMode == WuiFormContentBase.ksMode_Edit: + sTitle = 'Edit TestBox - %s (#%s)' % (oData.sName, oData.idTestBox); + else: + assert sMode == WuiFormContentBase.ksMode_Show; + sTitle = 'TestBox - %s (#%s)' % (oData.sName, oData.idTestBox); + WuiFormContentBase.__init__(self, oData, sMode, 'TestBox', oDisp, sTitle); + + # Try enter sName as hostname (no domain) when creating the testbox. + if sMode == WuiFormContentBase.ksMode_Add \ + and self._oData.sName in [None, ''] \ + and self._oData.ip not in [None, '']: + try: + (self._oData.sName, _, _) = socket.gethostbyaddr(self._oData.ip); + except: + pass; + offDot = self._oData.sName.find('.'); + if offDot > 0: + self._oData.sName = self._oData.sName[:offDot]; + + + def _populateForm(self, oForm, oData): + oForm.addIntRO( TestBoxData.ksParam_idTestBox, oData.idTestBox, 'TestBox ID'); + oForm.addIntRO( TestBoxData.ksParam_idGenTestBox, oData.idGenTestBox, 'TestBox generation ID'); + oForm.addTimestampRO(TestBoxData.ksParam_tsEffective, oData.tsEffective, 'Last changed'); + oForm.addTimestampRO(TestBoxData.ksParam_tsExpire, oData.tsExpire, 'Expires (excl)'); + oForm.addIntRO( TestBoxData.ksParam_uidAuthor, oData.uidAuthor, 'Changed by UID'); + + oForm.addText( TestBoxData.ksParam_ip, oData.ip, 'TestBox IP Address'); ## make read only?? + oForm.addUuid( TestBoxData.ksParam_uuidSystem, oData.uuidSystem, 'TestBox System/Firmware UUID'); + oForm.addText( TestBoxData.ksParam_sName, oData.sName, 'TestBox Name'); + oForm.addText( TestBoxData.ksParam_sDescription, oData.sDescription, 'TestBox Description'); + oForm.addCheckBox( TestBoxData.ksParam_fEnabled, oData.fEnabled, 'Enabled'); + oForm.addComboBox( TestBoxData.ksParam_enmLomKind, oData.enmLomKind, 'Lights-out-management', + TestBoxData.kaoLomKindDescs); + oForm.addText( TestBoxData.ksParam_ipLom, oData.ipLom, 'Lights-out-management IP Address'); + oForm.addInt( TestBoxData.ksParam_pctScaleTimeout, oData.pctScaleTimeout, 'Timeout scale factor (%)'); + + oForm.addListOfSchedGroupsForTestBox(TestBoxDataEx.ksParam_aoInSchedGroups, + oData.aoInSchedGroups, + SchedGroupLogic(TMDatabaseConnection()).fetchOrderedByName(), + 'Scheduling Group', oData.idTestBox); + # Command, comment and submit button. + if self._sMode == WuiFormContentBase.ksMode_Edit: + oForm.addComboBox(TestBoxData.ksParam_enmPendingCmd, oData.enmPendingCmd, 'Pending command', + TestBoxData.kaoTestBoxCmdDescs); + else: + oForm.addComboBoxRO(TestBoxData.ksParam_enmPendingCmd, oData.enmPendingCmd, 'Pending command', + TestBoxData.kaoTestBoxCmdDescs); + oForm.addMultilineText(TestBoxData.ksParam_sComment, oData.sComment, 'Comment'); + if self._sMode != WuiFormContentBase.ksMode_Show: + oForm.addSubmit('Create TestBox' if self._sMode == WuiFormContentBase.ksMode_Add else 'Change TestBox'); + + return True; + + + def _generatePostFormContent(self, oData): + from testmanager.webui.wuihlpform import WuiHlpForm; + + oForm = WuiHlpForm('testbox-machine-settable', '', fReadOnly = True); + oForm.addTextRO( TestBoxData.ksParam_sOs, oData.sOs, 'TestBox OS'); + oForm.addTextRO( TestBoxData.ksParam_sOsVersion, oData.sOsVersion, 'TestBox OS version'); + oForm.addTextRO( TestBoxData.ksParam_sCpuArch, oData.sCpuArch, 'TestBox OS kernel architecture'); + oForm.addTextRO( TestBoxData.ksParam_sCpuVendor, oData.sCpuVendor, 'TestBox CPU vendor'); + oForm.addTextRO( TestBoxData.ksParam_sCpuName, oData.sCpuName, 'TestBox CPU name'); + if oData.lCpuRevision: + oForm.addTextRO( TestBoxData.ksParam_lCpuRevision, '%#x' % (oData.lCpuRevision,), 'TestBox CPU revision', + sPostHtml = ' (family=%#x model=%#x stepping=%#x)' + % (oData.getCpuFamily(), oData.getCpuModel(), oData.getCpuStepping(),), + sSubClass = 'long'); + else: + oForm.addLongRO( TestBoxData.ksParam_lCpuRevision, oData.lCpuRevision, 'TestBox CPU revision'); + oForm.addIntRO( TestBoxData.ksParam_cCpus, oData.cCpus, 'Number of CPUs, cores and threads'); + oForm.addCheckBoxRO( TestBoxData.ksParam_fCpuHwVirt, oData.fCpuHwVirt, 'VT-x or AMD-V supported'); + oForm.addCheckBoxRO( TestBoxData.ksParam_fCpuNestedPaging, oData.fCpuNestedPaging, 'Nested paging supported'); + oForm.addCheckBoxRO( TestBoxData.ksParam_fCpu64BitGuest, oData.fCpu64BitGuest, '64-bit guest supported'); + oForm.addCheckBoxRO( TestBoxData.ksParam_fChipsetIoMmu, oData.fChipsetIoMmu, 'I/O MMU supported'); + oForm.addMultilineTextRO(TestBoxData.ksParam_sReport, oData.sReport, 'Hardware/software report'); + oForm.addLongRO( TestBoxData.ksParam_cMbMemory, oData.cMbMemory, 'Installed RAM size (MB)'); + oForm.addLongRO( TestBoxData.ksParam_cMbScratch, oData.cMbScratch, 'Available scratch space (MB)'); + oForm.addIntRO( TestBoxData.ksParam_iTestBoxScriptRev, oData.iTestBoxScriptRev, + 'TestBox Script SVN revision'); + sHexVer = oData.formatPythonVersion(); + oForm.addIntRO( TestBoxData.ksParam_iPythonHexVersion, oData.iPythonHexVersion, + 'Python version (hex)', sPostHtml = webutils.escapeElem(sHexVer)); + return [('Machine Only Settables', oForm.finalize()),]; + + + +class WuiTestBoxList(WuiListContentWithActionBase): + """ + WUI TestBox List Content Generator. + """ + + ## Descriptors for the combo box. + kasTestBoxActionDescs = \ + [ \ + [ 'none', 'Select an action...', '' ], + [ 'enable', 'Enable', '' ], + [ 'disable', 'Disable', '' ], + TestBoxData.kaoTestBoxCmdDescs[1], + TestBoxData.kaoTestBoxCmdDescs[2], + TestBoxData.kaoTestBoxCmdDescs[3], + TestBoxData.kaoTestBoxCmdDescs[4], + TestBoxData.kaoTestBoxCmdDescs[5], + ]; + + ## Boxes which doesn't report in for more than 15 min are considered dead. + kcSecMaxStatusDeltaAlive = 15*60 + + def __init__(self, aoEntries, iPage, cItemsPerPage, tsEffective, fnDPrint, oDisp, aiSelectedSortColumns = None): + # type: (list[TestBoxDataForListing], int, int, datetime.datetime, ignore, WuiAdmin) -> None + WuiListContentWithActionBase.__init__(self, aoEntries, iPage, cItemsPerPage, tsEffective, + sTitle = 'TestBoxes', sId = 'users', fnDPrint = fnDPrint, oDisp = oDisp, + aiSelectedSortColumns = aiSelectedSortColumns); + self._asColumnHeaders.extend([ 'Name', 'LOM', 'Status', 'Cmd', + 'Note', 'Script', 'Python', 'Group', + 'OS', 'CPU', 'Features', 'CPUs', 'RAM', 'Scratch', + 'Actions' ]); + self._asColumnAttribs.extend([ 'align="center"', 'align="center"', 'align="center"', 'align="center"' + 'align="center"', 'align="center"', 'align="center"', 'align="center"', + '', '', '', 'align="left"', 'align="right"', 'align="right"', 'align="right"', + 'align="center"' ]); + self._aaiColumnSorting.extend([ + (TestBoxLogic.kiSortColumn_sName,), + None, # LOM + (-TestBoxLogic.kiSortColumn_fEnabled, TestBoxLogic.kiSortColumn_enmState, -TestBoxLogic.kiSortColumn_tsUpdated,), + (TestBoxLogic.kiSortColumn_enmPendingCmd,), + None, # Note + (TestBoxLogic.kiSortColumn_iTestBoxScriptRev,), + (TestBoxLogic.kiSortColumn_iPythonHexVersion,), + None, # Group + (TestBoxLogic.kiSortColumn_sOs, TestBoxLogic.kiSortColumn_sOsVersion, TestBoxLogic.kiSortColumn_sCpuArch,), + (TestBoxLogic.kiSortColumn_sCpuVendor, TestBoxLogic.kiSortColumn_lCpuRevision,), + (TestBoxLogic.kiSortColumn_fCpuNestedPaging,), + (TestBoxLogic.kiSortColumn_cCpus,), + (TestBoxLogic.kiSortColumn_cMbMemory,), + (TestBoxLogic.kiSortColumn_cMbScratch,), + None, # Actions + ]); + assert len(self._aaiColumnSorting) == len(self._asColumnHeaders); + self._aoActions = list(self.kasTestBoxActionDescs); + self._sAction = oDisp.ksActionTestBoxListPost; + self._sCheckboxName = TestBoxData.ksParam_idTestBox; + + def show(self, fShowNavigation = True): + """ Adds some stats at the bottom of the page """ + (sTitle, sBody) = super(WuiTestBoxList, self).show(fShowNavigation); + + # Count boxes in interesting states. + if self._aoEntries: + cActive = 0; + cDead = 0; + for oTestBox in self._aoEntries: + if oTestBox.oStatus is not None: + oDelta = oTestBox.tsCurrent - oTestBox.oStatus.tsUpdated; + if oDelta.days <= 0 and oDelta.seconds <= self.kcSecMaxStatusDeltaAlive: + if oTestBox.fEnabled: + cActive += 1; + else: + cDead += 1; + else: + cDead += 1; + sBody += '<div id="testboxsummary"><p>\n' \ + '%s testboxes of which %s are active and %s dead' \ + '</p></div>\n' \ + % (len(self._aoEntries), cActive, cDead,) + return (sTitle, sBody); + + def _formatListEntry(self, iEntry): # pylint: disable=too-many-locals + from testmanager.webui.wuiadmin import WuiAdmin; + oEntry = self._aoEntries[iEntry]; + + # Lights outs managment. + if oEntry.enmLomKind == TestBoxData.ksLomKind_ILOM: + aoLom = [ WuiLinkBase('ILOM', 'https://%s/' % (oEntry.ipLom,), fBracketed = False), ]; + elif oEntry.enmLomKind == TestBoxData.ksLomKind_ELOM: + aoLom = [ WuiLinkBase('ELOM', 'http://%s/' % (oEntry.ipLom,), fBracketed = False), ]; + elif oEntry.enmLomKind == TestBoxData.ksLomKind_AppleXserveLom: + aoLom = [ 'Apple LOM' ]; + elif oEntry.enmLomKind == TestBoxData.ksLomKind_None: + aoLom = [ 'none' ]; + else: + aoLom = [ 'Unexpected enmLomKind value "%s"' % (oEntry.enmLomKind,) ]; + if oEntry.ipLom is not None: + if oEntry.enmLomKind in [ TestBoxData.ksLomKind_ILOM, TestBoxData.ksLomKind_ELOM ]: + aoLom += [ WuiLinkBase('(ssh)', 'ssh://%s' % (oEntry.ipLom,), fBracketed = False) ]; + aoLom += [ WuiRawHtml('<br>'), '%s' % (oEntry.ipLom,) ]; + + # State and Last seen. + if oEntry.oStatus is None: + oSeen = WuiSpanText('tmspan-offline', 'Never'); + oState = ''; + else: + oDelta = oEntry.tsCurrent - oEntry.oStatus.tsUpdated; + if oDelta.days <= 0 and oDelta.seconds <= self.kcSecMaxStatusDeltaAlive: + oSeen = WuiSpanText('tmspan-online', u'%s\u00a0s\u00a0ago' % (oDelta.days * 24 * 3600 + oDelta.seconds,)); + else: + oSeen = WuiSpanText('tmspan-offline', u'%s' % (self.formatTsShort(oEntry.oStatus.tsUpdated),)); + + if oEntry.oStatus.idTestSet is None: + oState = str(oEntry.oStatus.enmState); + else: + from testmanager.webui.wuimain import WuiMain; + oState = WuiTmLink(oEntry.oStatus.enmState, WuiMain.ksScriptName, # pylint: disable=redefined-variable-type + { WuiMain.ksParamAction: WuiMain.ksActionTestResultDetails, + TestSetData.ksParam_idTestSet: oEntry.oStatus.idTestSet, }, + sTitle = '#%u' % (oEntry.oStatus.idTestSet,), + fBracketed = False); + # Comment + oComment = self._formatCommentCell(oEntry.sComment); + + # Group links. + aoGroups = []; + for oInGroup in oEntry.aoInSchedGroups: + oSchedGroup = oInGroup.oSchedGroup; + aoGroups.append(WuiTmLink(oSchedGroup.sName, WuiAdmin.ksScriptName, + { WuiAdmin.ksParamAction: WuiAdmin.ksActionSchedGroupEdit, + SchedGroupData.ksParam_idSchedGroup: oSchedGroup.idSchedGroup, }, + sTitle = '#%u' % (oSchedGroup.idSchedGroup,), + fBracketed = len(oEntry.aoInSchedGroups) > 1)); + + # Reformat the OS version to take less space. + aoOs = [ 'N/A' ]; + if oEntry.sOs is not None and oEntry.sOsVersion is not None and oEntry.sCpuArch: + sOsVersion = oEntry.sOsVersion; + if sOsVersion[0] not in [ 'v', 'V', 'r', 'R'] \ + and sOsVersion[0].isdigit() \ + and sOsVersion.find('.') in range(4) \ + and oEntry.sOs in [ 'linux', 'solaris', 'darwin', ]: + sOsVersion = 'v' + sOsVersion; + + sVer1 = sOsVersion; + sVer2 = None; + if oEntry.sOs in ('linux', 'darwin'): + iSep = sOsVersion.find(' / '); + if iSep > 0: + sVer1 = sOsVersion[:iSep].strip(); + sVer2 = sOsVersion[iSep + 3:].strip(); + sVer2 = sVer2.replace('Red Hat Enterprise Linux Server', 'RHEL'); + sVer2 = sVer2.replace('Oracle Linux Server', 'OL'); + elif oEntry.sOs == 'solaris': + iSep = sOsVersion.find(' ('); + if iSep > 0 and sOsVersion[-1] == ')': + sVer1 = sOsVersion[:iSep].strip(); + sVer2 = sOsVersion[iSep + 2:-1].strip(); + elif oEntry.sOs == 'win': + iSep = sOsVersion.find('build'); + if iSep > 0: + sVer1 = sOsVersion[:iSep].strip(); + sVer2 = 'B' + sOsVersion[iSep + 1:].strip(); + aoOs = [ + WuiSpanText('tmspan-osarch', u'%s.%s' % (oEntry.sOs, oEntry.sCpuArch,)), + WuiSpanText('tmspan-osver1', sVer1.replace('-', u'\u2011'),), + ]; + if sVer2 is not None: + aoOs += [ WuiRawHtml('<br>'), WuiSpanText('tmspan-osver2', sVer2.replace('-', u'\u2011')), ]; + + # Format the CPU revision. + oCpu = None; + if oEntry.lCpuRevision is not None and oEntry.sCpuVendor is not None and oEntry.sCpuName is not None: + oCpu = [ + u'%s (fam:%xh\u00a0m:%xh\u00a0s:%xh)' + % (oEntry.sCpuVendor, oEntry.getCpuFamily(), oEntry.getCpuModel(), oEntry.getCpuStepping(),), + WuiRawHtml('<br>'), + oEntry.sCpuName, + ]; + else: + oCpu = []; + if oEntry.sCpuVendor is not None: + oCpu.append(oEntry.sCpuVendor); + if oEntry.lCpuRevision is not None: + oCpu.append('%#x' % (oEntry.lCpuRevision,)); + if oEntry.sCpuName is not None: + oCpu.append(oEntry.sCpuName); + + # Stuff cpu vendor and cpu/box features into one field. + asFeatures = [] + if oEntry.fCpuHwVirt is True: asFeatures.append(u'HW\u2011Virt'); + if oEntry.fCpuNestedPaging is True: asFeatures.append(u'Nested\u2011Paging'); + if oEntry.fCpu64BitGuest is True: asFeatures.append(u'64\u2011bit\u2011Guest'); + if oEntry.fChipsetIoMmu is True: asFeatures.append(u'I/O\u2011MMU'); + sFeatures = u' '.join(asFeatures) if asFeatures else u''; + + # Collection applicable actions. + aoActions = [ + WuiTmLink('Details', WuiAdmin.ksScriptName, + { WuiAdmin.ksParamAction: WuiAdmin.ksActionTestBoxDetails, + TestBoxData.ksParam_idTestBox: oEntry.idTestBox, + WuiAdmin.ksParamEffectiveDate: self._tsEffectiveDate, } ), + ] + + if self._oDisp is None or not self._oDisp.isReadOnlyUser(): + if isDbTimestampInfinity(oEntry.tsExpire): + aoActions += [ + WuiTmLink('Edit', WuiAdmin.ksScriptName, + { WuiAdmin.ksParamAction: WuiAdmin.ksActionTestBoxEdit, + TestBoxData.ksParam_idTestBox: oEntry.idTestBox, } ), + WuiTmLink('Remove', WuiAdmin.ksScriptName, + { WuiAdmin.ksParamAction: WuiAdmin.ksActionTestBoxRemovePost, + TestBoxData.ksParam_idTestBox: oEntry.idTestBox }, + sConfirm = 'Are you sure that you want to remove %s (%s)?' % (oEntry.sName, oEntry.ip) ), + ] + + if oEntry.sOs not in [ 'win', 'os2', ] and oEntry.ip is not None: + aoActions.append(WuiLinkBase('ssh', 'ssh://vbox@%s' % (oEntry.ip,),)); + + return [ self._getCheckBoxColumn(iEntry, oEntry.idTestBox), + [ WuiSpanText('tmspan-name', oEntry.sName), WuiRawHtml('<br>'), '%s' % (oEntry.ip,),], + aoLom, + [ + '' if oEntry.fEnabled else 'disabled / ', + oState, + WuiRawHtml('<br>'), + oSeen, + ], + oEntry.enmPendingCmd, + oComment, + WuiSvnLink(oEntry.iTestBoxScriptRev), + oEntry.formatPythonVersion(), + aoGroups, + aoOs, + oCpu, + sFeatures, + oEntry.cCpus if oEntry.cCpus is not None else 'N/A', + utils.formatNumberNbsp(oEntry.cMbMemory) + u'\u00a0MB' if oEntry.cMbMemory is not None else 'N/A', + utils.formatNumberNbsp(oEntry.cMbScratch) + u'\u00a0MB' if oEntry.cMbScratch is not None else 'N/A', + aoActions, + ]; + diff --git a/src/VBox/ValidationKit/testmanager/webui/wuiadmintestcase.py b/src/VBox/ValidationKit/testmanager/webui/wuiadmintestcase.py new file mode 100755 index 00000000..19fa86c9 --- /dev/null +++ b/src/VBox/ValidationKit/testmanager/webui/wuiadmintestcase.py @@ -0,0 +1,258 @@ +# -*- coding: utf-8 -*- +# $Id: wuiadmintestcase.py $ + +""" +Test Manager WUI - Test Cases. +""" + +__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 $" + + +# Validation Kit imports. +from common import utils, webutils; +from testmanager.webui.wuicontentbase import WuiFormContentBase, WuiListContentBase, WuiContentBase, WuiTmLink, WuiRawHtml; +from testmanager.core.db import isDbTimestampInfinity; +from testmanager.core.testcase import TestCaseDataEx, TestCaseData, TestCaseDependencyLogic; +from testmanager.core.globalresource import GlobalResourceData, GlobalResourceLogic; + + + +class WuiTestCaseDetailsLink(WuiTmLink): + """ Test case details link by ID. """ + + def __init__(self, idTestCase, sName = WuiContentBase.ksShortDetailsLink, fBracketed = False, tsNow = None): + from testmanager.webui.wuiadmin import WuiAdmin; + dParams = { + WuiAdmin.ksParamAction: WuiAdmin.ksActionTestCaseDetails, + TestCaseData.ksParam_idTestCase: idTestCase, + }; + if tsNow is not None: + dParams[WuiAdmin.ksParamEffectiveDate] = tsNow; ## ?? + WuiTmLink.__init__(self, sName, WuiAdmin.ksScriptName, dParams, fBracketed = fBracketed); + self.idTestCase = idTestCase; + + +class WuiTestCaseList(WuiListContentBase): + """ + WUI test case list content generator. + """ + + def __init__(self, aoEntries, iPage, cItemsPerPage, tsEffective, fnDPrint, oDisp, aiSelectedSortColumns = None): + WuiListContentBase.__init__(self, aoEntries, iPage, cItemsPerPage, tsEffective, sTitle = 'Test Cases', + fnDPrint = fnDPrint, oDisp = oDisp, aiSelectedSortColumns = aiSelectedSortColumns); + self._asColumnHeaders = \ + [ + 'Name', 'Active', 'Timeout', 'Base Command / Variations', 'Validation Kit Files', + 'Test Case Prereqs', 'Global Rsrces', 'Note', 'Actions' + ]; + self._asColumnAttribs = \ + [ + '', '', 'align="center"', '', '', + 'valign="top"', 'valign="top"', 'align="center"', 'align="center"' + ]; + + def _formatListEntry(self, iEntry): + oEntry = self._aoEntries[iEntry]; + from testmanager.webui.wuiadmin import WuiAdmin; + + aoRet = \ + [ + oEntry.sName.replace('-', u'\u2011'), + 'Enabled' if oEntry.fEnabled else 'Disabled', + utils.formatIntervalSeconds(oEntry.cSecTimeout), + ]; + + # Base command and variations. + fNoGang = True; + fNoSubName = True; + fAllDefaultTimeouts = True; + for oVar in oEntry.aoTestCaseArgs: + if fNoSubName and oVar.sSubName is not None and oVar.sSubName.strip(): + fNoSubName = False; + if oVar.cGangMembers > 1: + fNoGang = False; + if oVar.cSecTimeout is not None: + fAllDefaultTimeouts = False; + + sHtml = ' <table class="tminnertbl" width=100%>\n' \ + ' <tr>\n' \ + ' '; + if not fNoSubName: + sHtml += '<th class="tmtcasubname">Sub-name</th>'; + if not fNoGang: + sHtml += '<th class="tmtcagangsize">Gang Size</th>'; + if not fAllDefaultTimeouts: + sHtml += '<th class="tmtcatimeout">Timeout</th>'; + sHtml += '<th>Additional Arguments</b></th>\n' \ + ' </tr>\n' + for oTmp in oEntry.aoTestCaseArgs: + sHtml += '<tr>'; + if not fNoSubName: + sHtml += '<td>%s</td>' % (webutils.escapeElem(oTmp.sSubName) if oTmp.sSubName is not None else ''); + if not fNoGang: + sHtml += '<td>%d</td>' % (oTmp.cGangMembers,) + if not fAllDefaultTimeouts: + sHtml += '<td>%s</td>' \ + % (utils.formatIntervalSeconds(oTmp.cSecTimeout) if oTmp.cSecTimeout is not None else 'Default',) + sHtml += u'<td>%s</td></tr>' \ + % ( webutils.escapeElem(oTmp.sArgs.replace('-', u'\u2011')) if oTmp.sArgs else u'\u2011',); + sHtml += '</tr>\n'; + sHtml += ' </table>' + + aoRet.append([oEntry.sBaseCmd.replace('-', u'\u2011'), WuiRawHtml(sHtml)]); + + # Next. + aoRet += [ oEntry.sValidationKitZips if oEntry.sValidationKitZips is not None else '', ]; + + # Show dependency on other testcases + if oEntry.aoDepTestCases not in (None, []): + sHtml = ' <ul class="tmshowall">\n' + for sTmp in oEntry.aoDepTestCases: + sHtml += ' <li class="tmshowall"><a href="%s?%s=%s&%s=%s">%s</a></li>\n' \ + % (WuiAdmin.ksScriptName, + WuiAdmin.ksParamAction, WuiAdmin.ksActionTestCaseEdit, + TestCaseData.ksParam_idTestCase, sTmp.idTestCase, + sTmp.sName) + sHtml += ' </ul>\n' + else: + sHtml = '<ul class="tmshowall"><li class="tmshowall">None</li></ul>\n' + aoRet.append(WuiRawHtml(sHtml)); + + # Show dependency on global resources + if oEntry.aoDepGlobalResources not in (None, []): + sHtml = ' <ul class="tmshowall">\n' + for sTmp in oEntry.aoDepGlobalResources: + sHtml += ' <li class="tmshowall"><a href="%s?%s=%s&%s=%s">%s</a></li>\n' \ + % (WuiAdmin.ksScriptName, + WuiAdmin.ksParamAction, WuiAdmin.ksActionGlobalRsrcShowEdit, + GlobalResourceData.ksParam_idGlobalRsrc, sTmp.idGlobalRsrc, + sTmp.sName) + sHtml += ' </ul>\n' + else: + sHtml = '<ul class="tmshowall"><li class="tmshowall">None</li></ul>\n' + aoRet.append(WuiRawHtml(sHtml)); + + # Comment (note). + aoRet.append(self._formatCommentCell(oEntry.sComment)); + + # Show actions that can be taken. + aoActions = [ WuiTmLink('Details', WuiAdmin.ksScriptName, + { WuiAdmin.ksParamAction: WuiAdmin.ksActionTestCaseDetails, + TestCaseData.ksParam_idGenTestCase: oEntry.idGenTestCase }), ]; + if self._oDisp is None or not self._oDisp.isReadOnlyUser(): + if isDbTimestampInfinity(oEntry.tsExpire): + aoActions.append(WuiTmLink('Modify', WuiAdmin.ksScriptName, + { WuiAdmin.ksParamAction: WuiAdmin.ksActionTestCaseEdit, + TestCaseData.ksParam_idTestCase: oEntry.idTestCase })); + aoActions.append(WuiTmLink('Clone', WuiAdmin.ksScriptName, + { WuiAdmin.ksParamAction: WuiAdmin.ksActionTestCaseClone, + TestCaseData.ksParam_idGenTestCase: oEntry.idGenTestCase })); + if isDbTimestampInfinity(oEntry.tsExpire): + aoActions.append(WuiTmLink('Remove', WuiAdmin.ksScriptName, + { WuiAdmin.ksParamAction: WuiAdmin.ksActionTestCaseDoRemove, + TestCaseData.ksParam_idTestCase: oEntry.idTestCase }, + sConfirm = 'Are you sure you want to remove test case #%d?' % (oEntry.idTestCase,))); + aoRet.append(aoActions); + + return aoRet; + + +class WuiTestCase(WuiFormContentBase): + """ + WUI user account content generator. + """ + + def __init__(self, oData, sMode, oDisp): + assert isinstance(oData, TestCaseDataEx); + + if sMode == WuiFormContentBase.ksMode_Add: + sTitle = 'New Test Case'; + elif sMode == WuiFormContentBase.ksMode_Edit: + sTitle = 'Edit Test Case - %s (#%s)' % (oData.sName, oData.idTestCase); + else: + assert sMode == WuiFormContentBase.ksMode_Show; + sTitle = 'Test Case - %s (#%s)' % (oData.sName, oData.idTestCase); + WuiFormContentBase.__init__(self, oData, sMode, 'TestCase', oDisp, sTitle); + + # Read additional bits form the DB. + oDepLogic = TestCaseDependencyLogic(oDisp.getDb()); + self._aoAllTestCases = oDepLogic.getApplicableDepTestCaseData(-1 if oData.idTestCase is None else oData.idTestCase); + self._aoAllGlobalRsrcs = GlobalResourceLogic(oDisp.getDb()).getAll(); + + def _populateForm(self, oForm, oData): + oForm.addIntRO (TestCaseData.ksParam_idTestCase, oData.idTestCase, 'Test Case ID') + oForm.addTimestampRO(TestCaseData.ksParam_tsEffective, oData.tsEffective, 'Last changed') + oForm.addTimestampRO(TestCaseData.ksParam_tsExpire, oData.tsExpire, 'Expires (excl)') + oForm.addIntRO (TestCaseData.ksParam_uidAuthor, oData.uidAuthor, 'Changed by UID') + oForm.addIntRO (TestCaseData.ksParam_idGenTestCase, oData.idGenTestCase, 'Test Case generation ID') + oForm.addText (TestCaseData.ksParam_sName, oData.sName, 'Name') + oForm.addText (TestCaseData.ksParam_sDescription, oData.sDescription, 'Description') + oForm.addCheckBox (TestCaseData.ksParam_fEnabled, oData.fEnabled, 'Enabled') + oForm.addLong (TestCaseData.ksParam_cSecTimeout, + utils.formatIntervalSeconds2(oData.cSecTimeout), 'Default timeout') + oForm.addWideText (TestCaseData.ksParam_sTestBoxReqExpr, oData.sTestBoxReqExpr, 'TestBox requirements (python)'); + oForm.addWideText (TestCaseData.ksParam_sBuildReqExpr, oData.sBuildReqExpr, 'Build requirement (python)'); + oForm.addWideText (TestCaseData.ksParam_sBaseCmd, oData.sBaseCmd, 'Base command') + oForm.addText (TestCaseData.ksParam_sValidationKitZips, oData.sValidationKitZips, 'Test suite files') + + oForm.addListOfTestCaseArgs(TestCaseDataEx.ksParam_aoTestCaseArgs, oData.aoTestCaseArgs, 'Argument variations') + + aoTestCaseDeps = []; + for oTestCase in self._aoAllTestCases: + if oTestCase.idTestCase == oData.idTestCase: + continue; + fSelected = False; + for oDep in oData.aoDepTestCases: + if oDep.idTestCase == oTestCase.idTestCase: + fSelected = True; + break; + aoTestCaseDeps.append([oTestCase.idTestCase, fSelected, oTestCase.sName]); + oForm.addListOfTestCases(TestCaseDataEx.ksParam_aoDepTestCases, aoTestCaseDeps, 'Depends on test cases') + + aoGlobalResrcDeps = []; + for oGlobalRsrc in self._aoAllGlobalRsrcs: + fSelected = False; + for oDep in oData.aoDepGlobalResources: + if oDep.idGlobalRsrc == oGlobalRsrc.idGlobalRsrc: + fSelected = True; + break; + aoGlobalResrcDeps.append([oGlobalRsrc.idGlobalRsrc, fSelected, oGlobalRsrc.sName]); + oForm.addListOfResources(TestCaseDataEx.ksParam_aoDepGlobalResources, aoGlobalResrcDeps, 'Depends on resources') + + oForm.addMultilineText(TestCaseDataEx.ksParam_sComment, oData.sComment, 'Comment'); + + oForm.addSubmit(); + + return True; + diff --git a/src/VBox/ValidationKit/testmanager/webui/wuiadmintestgroup.py b/src/VBox/ValidationKit/testmanager/webui/wuiadmintestgroup.py new file mode 100755 index 00000000..8954afe9 --- /dev/null +++ b/src/VBox/ValidationKit/testmanager/webui/wuiadmintestgroup.py @@ -0,0 +1,197 @@ +# -*- coding: utf-8 -*- +# $Id: wuiadmintestgroup.py $ + +""" +Test Manager WUI - Test Groups. +""" + +__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 $" + +# Validation Kit imports. +from common import utils, webutils; +from testmanager.webui.wuicontentbase import WuiFormContentBase, WuiListContentBase, WuiTmLink, WuiRawHtml; +from testmanager.core.db import isDbTimestampInfinity; +from testmanager.core.testgroup import TestGroupData, TestGroupDataEx; +from testmanager.core.testcase import TestCaseData, TestCaseLogic; + + +class WuiTestGroup(WuiFormContentBase): + """ + WUI test group content generator. + """ + + def __init__(self, oData, sMode, oDisp): + assert isinstance(oData, TestGroupDataEx); + + if sMode == WuiFormContentBase.ksMode_Add: + sTitle = 'Add Test Group'; + elif sMode == WuiFormContentBase.ksMode_Edit: + sTitle = 'Modify Test Group'; + else: + assert sMode == WuiFormContentBase.ksMode_Show; + sTitle = 'Test Group'; + WuiFormContentBase.__init__(self, oData, sMode, 'TestGroup', oDisp, sTitle); + + # + # Fetch additional data. + # + if sMode in [WuiFormContentBase.ksMode_Add, WuiFormContentBase.ksMode_Edit]: + self.aoAllTestCases = TestCaseLogic(oDisp.getDb()).fetchForListing(0, 0x7fff, None); + else: + self.aoAllTestCases = [oMember.oTestCase for oMember in oData.aoMembers]; + + def _populateForm(self, oForm, oData): + oForm.addIntRO (TestGroupData.ksParam_idTestGroup, self._oData.idTestGroup, 'Test Group ID') + oForm.addTimestampRO (TestGroupData.ksParam_tsEffective, self._oData.tsEffective, 'Last changed') + oForm.addTimestampRO (TestGroupData.ksParam_tsExpire, self._oData.tsExpire, 'Expires (excl)') + oForm.addIntRO (TestGroupData.ksParam_uidAuthor, self._oData.uidAuthor, 'Changed by UID') + oForm.addText (TestGroupData.ksParam_sName, self._oData.sName, 'Name') + oForm.addText (TestGroupData.ksParam_sDescription, self._oData.sDescription, 'Description') + + oForm.addListOfTestGroupMembers(TestGroupDataEx.ksParam_aoMembers, + oData.aoMembers, self.aoAllTestCases, 'Test Case List', + fReadOnly = self._sMode == WuiFormContentBase.ksMode_Show); + + oForm.addMultilineText (TestGroupData.ksParam_sComment, self._oData.sComment, 'Comment'); + oForm.addSubmit(); + return True; + + +class WuiTestGroupList(WuiListContentBase): + """ + WUI test group list content generator. + """ + + def __init__(self, aoEntries, iPage, cItemsPerPage, tsEffective, fnDPrint, oDisp, aiSelectedSortColumns = None): + assert not aoEntries or isinstance(aoEntries[0], TestGroupDataEx) + + WuiListContentBase.__init__(self, aoEntries, iPage, cItemsPerPage, tsEffective, sTitle = 'Test Groups', + fnDPrint = fnDPrint, oDisp = oDisp, aiSelectedSortColumns = aiSelectedSortColumns); + self._asColumnHeaders = [ 'ID', 'Name', 'Description', 'Test Cases', 'Note', 'Actions' ]; + self._asColumnAttribs = [ 'align="right"', '', '', '', 'align="center"', 'align="center"' ]; + + + def _formatListEntry(self, iEntry): + oEntry = self._aoEntries[iEntry]; + from testmanager.webui.wuiadmin import WuiAdmin; + + # + # Test case list. + # + sHtml = ''; + if oEntry.aoMembers: + for oMember in oEntry.aoMembers: + sHtml += '<dl>\n' \ + ' <dd><strong>%s</strong> (priority: %d) %s %s</dd>\n' \ + % ( webutils.escapeElem(oMember.oTestCase.sName), + oMember.iSchedPriority, + WuiTmLink('Details', WuiAdmin.ksScriptName, + { WuiAdmin.ksParamAction: WuiAdmin.ksActionTestCaseDetails, + TestCaseData.ksParam_idGenTestCase: oMember.oTestCase.idGenTestCase, } ).toHtml(), + WuiTmLink('Edit', WuiAdmin.ksScriptName, + { WuiAdmin.ksParamAction: WuiAdmin.ksActionTestCaseEdit, + TestCaseData.ksParam_idTestCase: oMember.oTestCase.idTestCase, } ).toHtml() + if isDbTimestampInfinity(oMember.oTestCase.tsExpire) + and self._oDisp is not None + and not self._oDisp.isReadOnlyUser() else '', + ); + + sHtml += ' <dt>\n'; + + fNoGang = True; + for oVar in oMember.oTestCase.aoTestCaseArgs: + if oVar.cGangMembers > 1: + fNoGang = False + break; + + sHtml += ' <table class="tminnertbl" width="100%">\n' + if fNoGang: + sHtml += ' <tr><th>Timeout</th><th>Arguments</th></tr>\n'; + else: + sHtml += ' <tr><th>Gang Size</th><th>Timeout</th><th style="text-align:left;">Arguments</th></tr>\n'; + + cArgsIncluded = 0; + for oVar in oMember.oTestCase.aoTestCaseArgs: + if oMember.aidTestCaseArgs is None or oVar.idTestCaseArgs in oMember.aidTestCaseArgs: + cArgsIncluded += 1; + if fNoGang: + sHtml += ' <tr>'; + else: + sHtml += ' <tr><td>%s</td>' % (oVar.cGangMembers,); + sHtml += '<td>%s</td><td>%s</td></tr>\n' \ + % ( utils.formatIntervalSeconds(oMember.oTestCase.cSecTimeout if oVar.cSecTimeout is None + else oVar.cSecTimeout), + webutils.escapeElem(oVar.sArgs), ); + if cArgsIncluded == 0: + sHtml += ' <tr><td colspan="%u">No arguments selected.</td></tr>\n' % ( 2 if fNoGang else 3, ); + sHtml += ' </table>\n' \ + ' </dl>\n'; + oTestCases = WuiRawHtml(sHtml); + + # + # Actions. + # + aoActions = [ WuiTmLink('Details', WuiAdmin.ksScriptName, + { WuiAdmin.ksParamAction: WuiAdmin.ksActionTestGroupDetails, + TestGroupData.ksParam_idTestGroup: oEntry.idTestGroup, + WuiAdmin.ksParamEffectiveDate: self._tsEffectiveDate, }) ]; + if self._oDisp is None or not self._oDisp.isReadOnlyUser(): + + if isDbTimestampInfinity(oEntry.tsExpire): + aoActions.append(WuiTmLink('Modify', WuiAdmin.ksScriptName, + { WuiAdmin.ksParamAction: WuiAdmin.ksActionTestGroupEdit, + TestGroupData.ksParam_idTestGroup: oEntry.idTestGroup })); + aoActions.append(WuiTmLink('Clone', WuiAdmin.ksScriptName, + { WuiAdmin.ksParamAction: WuiAdmin.ksActionTestGroupClone, + TestGroupData.ksParam_idTestGroup: oEntry.idTestGroup, + WuiAdmin.ksParamEffectiveDate: self._tsEffectiveDate, })); + aoActions.append(WuiTmLink('Remove', WuiAdmin.ksScriptName, + { WuiAdmin.ksParamAction: WuiAdmin.ksActionTestGroupDoRemove, + TestGroupData.ksParam_idTestGroup: oEntry.idTestGroup }, + sConfirm = 'Do you really want to remove test group #%d?' % (oEntry.idTestGroup,))); + else: + aoActions.append(WuiTmLink('Clone', WuiAdmin.ksScriptName, + { WuiAdmin.ksParamAction: WuiAdmin.ksActionTestGroupClone, + TestGroupData.ksParam_idTestGroup: oEntry.idTestGroup, + WuiAdmin.ksParamEffectiveDate: self._tsEffectiveDate, })); + + + + return [ oEntry.idTestGroup, + oEntry.sName, + oEntry.sDescription if oEntry.sDescription is not None else '', + oTestCases, + self._formatCommentCell(oEntry.sComment, cMaxLines = max(3, len(oEntry.aoMembers) * 2)), + aoActions ]; + diff --git a/src/VBox/ValidationKit/testmanager/webui/wuiadminuseraccount.py b/src/VBox/ValidationKit/testmanager/webui/wuiadminuseraccount.py new file mode 100755 index 00000000..30d78cd4 --- /dev/null +++ b/src/VBox/ValidationKit/testmanager/webui/wuiadminuseraccount.py @@ -0,0 +1,110 @@ +# -*- coding: utf-8 -*- +# $Id: wuiadminuseraccount.py $ + +""" +Test Manager WUI - User accounts. +""" + +__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 $" + +# Validation Kit imports. +from testmanager.webui.wuicontentbase import WuiFormContentBase, WuiListContentBase, WuiTmLink; +from testmanager.core.useraccount import UserAccountData + + +class WuiUserAccount(WuiFormContentBase): + """ + WUI user account content generator. + """ + def __init__(self, oData, sMode, oDisp): + if sMode == WuiFormContentBase.ksMode_Add: + sTitle = 'Add User Account'; + elif sMode == WuiFormContentBase.ksMode_Edit: + sTitle = 'Modify User Account'; + else: + assert sMode == WuiFormContentBase.ksMode_Show; + sTitle = 'User Account'; + WuiFormContentBase.__init__(self, oData, sMode, 'User', oDisp, sTitle); + + def _populateForm(self, oForm, oData): + oForm.addIntRO( UserAccountData.ksParam_uid, oData.uid, 'User ID'); + oForm.addTimestampRO(UserAccountData.ksParam_tsEffective, oData.tsEffective, 'Effective Date'); + oForm.addTimestampRO(UserAccountData.ksParam_tsExpire, oData.tsExpire, 'Effective Date'); + oForm.addIntRO( UserAccountData.ksParam_uidAuthor, oData.uidAuthor, 'Changed by UID'); + oForm.addText( UserAccountData.ksParam_sLoginName, oData.sLoginName, 'Login name') + oForm.addText( UserAccountData.ksParam_sUsername, oData.sUsername, 'User name') + oForm.addText( UserAccountData.ksParam_sFullName, oData.sFullName, 'Full name') + oForm.addText( UserAccountData.ksParam_sEmail, oData.sEmail, 'E-mail') + oForm.addCheckBox( UserAccountData.ksParam_fReadOnly, oData.fReadOnly, 'Only read access') + if self._sMode != WuiFormContentBase.ksMode_Show: + oForm.addSubmit('Add User' if self._sMode == WuiFormContentBase.ksMode_Add else 'Change User'); + return True; + + +class WuiUserAccountList(WuiListContentBase): + """ + WUI user account list content generator. + """ + + def __init__(self, aoEntries, iPage, cItemsPerPage, tsEffective, fnDPrint, oDisp, aiSelectedSortColumns = None): + WuiListContentBase.__init__(self, aoEntries, iPage, cItemsPerPage, tsEffective, + sTitle = 'Registered User Accounts', sId = 'users', fnDPrint = fnDPrint, oDisp = oDisp, + aiSelectedSortColumns = aiSelectedSortColumns); + self._asColumnHeaders = ['User ID', 'Name', 'E-mail', 'Full Name', 'Login Name', 'Access', 'Actions']; + self._asColumnAttribs = ['align="center"', 'align="center"', 'align="center"', 'align="center"', 'align="center"', + 'align="center"', 'align="center"', ]; + + def _formatListEntry(self, iEntry): + from testmanager.webui.wuiadmin import WuiAdmin; + oEntry = self._aoEntries[iEntry]; + aoActions = [ + WuiTmLink('Details', WuiAdmin.ksScriptName, + { WuiAdmin.ksParamAction: WuiAdmin.ksActionUserDetails, + UserAccountData.ksParam_uid: oEntry.uid } ), + ]; + if self._oDisp is None or not self._oDisp.isReadOnlyUser(): + aoActions += [ + WuiTmLink('Modify', WuiAdmin.ksScriptName, + { WuiAdmin.ksParamAction: WuiAdmin.ksActionUserEdit, + UserAccountData.ksParam_uid: oEntry.uid } ), + WuiTmLink('Remove', WuiAdmin.ksScriptName, + { WuiAdmin.ksParamAction: WuiAdmin.ksActionUserDelPost, + UserAccountData.ksParam_uid: oEntry.uid }, + sConfirm = 'Are you sure you want to remove user #%d?' % (oEntry.uid,)), + ]; + + return [ oEntry.uid, oEntry.sUsername, oEntry.sEmail, oEntry.sFullName, oEntry.sLoginName, + 'read only' if oEntry.fReadOnly else 'full', + aoActions, ]; + diff --git a/src/VBox/ValidationKit/testmanager/webui/wuibase.py b/src/VBox/ValidationKit/testmanager/webui/wuibase.py new file mode 100755 index 00000000..feccf20a --- /dev/null +++ b/src/VBox/ValidationKit/testmanager/webui/wuibase.py @@ -0,0 +1,1245 @@ +# -*- coding: utf-8 -*- +# $Id: wuibase.py $ + +""" +Test Manager Web-UI - Base Classes. +""" + +__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 sys; +import string; + +# Validation Kit imports. +from common import webutils, utils; +from testmanager import config; +from testmanager.core.base import ModelDataBase, ModelLogicBase, TMExceptionBase; +from testmanager.core.db import TMDatabaseConnection; +from testmanager.core.systemlog import SystemLogLogic, SystemLogData; +from testmanager.core.useraccount import UserAccountLogic + +# Python 3 hacks: +if sys.version_info[0] >= 3: + unicode = str; # pylint: disable=redefined-builtin,invalid-name + long = int; # pylint: disable=redefined-builtin,invalid-name + + +class WuiException(TMExceptionBase): + """ + For exceptions raised by Web UI code. + """ + pass; # pylint: disable=unnecessary-pass + + +class WuiDispatcherBase(object): + """ + Base class for the Web User Interface (WUI) dispatchers. + + The dispatcher class defines the basics of the page (like base template, + menu items, action). It is also responsible for parsing requests and + dispatching them to action (POST) or/and content generators (GET+POST). + The content returned by the generator is merged into the template and sent + back to the webserver glue. + """ + + ## @todo possible that this should all go into presentation. + + ## The action parameter. + ksParamAction = 'Action'; + ## The name of the default action. + ksActionDefault = 'default'; + + ## The name of the current page number parameter used when displaying lists. + ksParamPageNo = 'PageNo'; + ## The name of the page length (list items) parameter when displaying lists. + ksParamItemsPerPage = 'ItemsPerPage'; + + ## The name of the effective date (timestamp) parameter. + ksParamEffectiveDate = 'EffectiveDate'; + + ## The name of the redirect-to (test manager relative url) parameter. + ksParamRedirectTo = 'RedirectTo'; + + ## The name of the list-action parameter (WuiListContentWithActionBase). + ksParamListAction = 'ListAction'; + + ## One or more columns to sort by. + ksParamSortColumns = 'SortBy'; + + ## The name of the change log enabled/disabled parameter. + ksParamChangeLogEnabled = 'ChangeLogEnabled'; + ## The name of the parmaeter indicating the change log page number. + ksParamChangeLogPageNo = 'ChangeLogPageNo'; + ## The name of the parameter indicate number of change log entries per page. + ksParamChangeLogEntriesPerPage = 'ChangeLogEntriesPerPage'; + ## The change log related parameters. + kasChangeLogParams = (ksParamChangeLogEnabled, ksParamChangeLogPageNo, ksParamChangeLogEntriesPerPage,); + + ## @name Dispatcher debugging parameters. + ## {@ + ksParamDbgSqlTrace = 'DbgSqlTrace'; + ksParamDbgSqlExplain = 'DbgSqlExplain'; + ## List of all debugging parameters. + kasDbgParams = (ksParamDbgSqlTrace, ksParamDbgSqlExplain,); + ## @} + + ## Special action return code for skipping _generatePage. Useful for + # download pages and the like that messes with the HTTP header and more. + ksDispatchRcAllDone = 'Done - Page has been rendered already'; + + + def __init__(self, oSrvGlue, sScriptName): + self._oSrvGlue = oSrvGlue; + self._oDb = TMDatabaseConnection(self.dprint if config.g_kfWebUiSqlDebug else None, oSrvGlue = oSrvGlue); + self._tsNow = None; # Set by getEffectiveDateParam. + self._asCheckedParams = []; + self._dParams = None; # Set by dispatchRequest. + self._sAction = None; # Set by dispatchRequest. + self._dDispatch = { self.ksActionDefault: self._actionDefault, }; + + # Template bits. + self._sTemplate = 'template-default.html'; + self._sPageTitle = '$$TODO$$'; # The page title. + self._aaoMenus = []; # List of [sName, sLink, [ [sSideName, sLink], .. ] tuples. + self._sPageFilter = ''; # The filter controls (optional). + self._sPageBody = '$$TODO$$'; # The body text. + self._dSideMenuFormAttrs = {}; # key/value with attributes for the side menu <form> tag. + self._sRedirectTo = None; + self._sDebug = ''; + + # Debugger bits. + self._fDbgSqlTrace = False; + self._fDbgSqlExplain = False; + self._dDbgParams = {}; + for sKey, sValue in oSrvGlue.getParameters().items(): + if sKey in self.kasDbgParams: + self._dDbgParams[sKey] = sValue; + if self._dDbgParams: + from testmanager.webui.wuicontentbase import WuiTmLink; + WuiTmLink.kdDbgParams = self._dDbgParams; + + # Determine currently logged in user credentials + self._oCurUser = UserAccountLogic(self._oDb).tryFetchAccountByLoginName(oSrvGlue.getLoginName()); + + # Calc a couple of URL base strings for this dispatcher. + self._sUrlBase = sScriptName + '?'; + if self._dDbgParams: + self._sUrlBase += webutils.encodeUrlParams(self._dDbgParams) + '&'; + self._sActionUrlBase = self._sUrlBase + self.ksParamAction + '='; + + + def _redirectPage(self): + """ + Redirects the page to the URL given in self._sRedirectTo. + """ + assert self._sRedirectTo; + assert self._sPageBody is None; + assert self._sPageTitle is None; + + self._oSrvGlue.setRedirect(self._sRedirectTo); + return True; + + def _isMenuMatch(self, sMenuUrl, sActionParam): + """ Overridable menu matcher. """ + return sMenuUrl is not None and sMenuUrl.find(sActionParam) > 0; + + def _isSideMenuMatch(self, sSideMenuUrl, sActionParam): + """ Overridable side menu matcher. """ + return sSideMenuUrl is not None and sSideMenuUrl.find(sActionParam) > 0; + + def _generateMenus(self): + """ + Generates the two menus, returning them as (sTopMenuItems, sSideMenuItems). + """ + fReadOnly = self.isReadOnlyUser(); + + # + # We use the action to locate the side menu. + # + aasSideMenu = None; + for cchAction in range(len(self._sAction), 1, -1): + sActionParam = '%s=%s' % (self.ksParamAction, self._sAction[:cchAction]); + for aoItem in self._aaoMenus: + if self._isMenuMatch(aoItem[1], sActionParam): + aasSideMenu = aoItem[2]; + break; + for asSubItem in aoItem[2]: + if self._isMenuMatch(asSubItem[1], sActionParam): + aasSideMenu = aoItem[2]; + break; + if aasSideMenu is not None: + break; + + # + # Top menu first. + # + sTopMenuItems = ''; + for aoItem in self._aaoMenus: + if aasSideMenu is aoItem[2]: + sTopMenuItems += '<li class="current_page_item">'; + else: + sTopMenuItems += '<li>'; + sTopMenuItems += '<a href="' + webutils.escapeAttr(aoItem[1]) + '">' \ + + webutils.escapeElem(aoItem[0]) + '</a></li>\n'; + + # + # Side menu (if found). + # + sActionParam = '%s=%s' % (self.ksParamAction, self._sAction); + sSideMenuItems = ''; + if aasSideMenu is not None: + for asSubItem in aasSideMenu: + if asSubItem[1] is not None: + if not asSubItem[2] or not fReadOnly: + if self._isSideMenuMatch(asSubItem[1], sActionParam): + sSideMenuItems += '<li class="current_page_item">'; + else: + sSideMenuItems += '<li>'; + sSideMenuItems += '<a href="' + webutils.escapeAttr(asSubItem[1]) + '">' \ + + webutils.escapeElem(asSubItem[0]) + '</a></li>\n'; + else: + sSideMenuItems += '<li class="subheader_item">' + webutils.escapeElem(asSubItem[0]) + '</li>'; + return (sTopMenuItems, sSideMenuItems); + + def _generatePage(self): + """ + Generates the page using _sTemplate, _sPageTitle, _aaoMenus, and _sPageBody. + """ + assert self._sRedirectTo is None; + + # + # Build the replacement string dictionary. + # + + # Provide basic auth log out for browsers that supports it. + sUserAgent = self._oSrvGlue.getUserAgent(); + if sUserAgent.startswith('Mozilla/') and sUserAgent.find('Firefox') > 0: + # Log in as the logout user in the same realm, the browser forgets + # the old login and the job is done. (see apache sample conf) + sLogOut = ' (<a href="%s://logout:logout@%s%slogout.py">logout</a>)' \ + % (self._oSrvGlue.getUrlScheme(), self._oSrvGlue.getUrlNetLoc(), self._oSrvGlue.getUrlBasePath()); + elif sUserAgent.startswith('Mozilla/') and sUserAgent.find('Safari') > 0: + # For a 401, causing the browser to forget the old login. Works + # with safari as well as the two above. Since safari consider the + # above method a phishing attempt and displays a warning to that + # effect, which when taken seriously aborts the logout, this method + # is preferable, even if it throws logon boxes in the user's face + # till he/she/it hits escape, because it always works. + sLogOut = ' (<a href="logout2.py">logout</a>)' + elif (sUserAgent.startswith('Mozilla/') and sUserAgent.find('MSIE') > 0) \ + or (sUserAgent.startswith('Mozilla/') and sUserAgent.find('Chrome') > 0): + ## There doesn't seem to be any way to make IE really log out + # without using a cookie and systematically 401 accesses based on + # some logout state associated with it. Not sure how secure that + # can be made and we really want to avoid cookies. So, perhaps, + # just avoid IE for now. :-) + ## Chrome/21.0 doesn't want to log out either. + sLogOut = '' + else: + sLogOut = '' + + # Prep Menus. + (sTopMenuItems, sSideMenuItems) = self._generateMenus(); + + # The dictionary (max variable length is 28 chars (see further down)). + dReplacements = { + '@@PAGE_TITLE@@': self._sPageTitle, + '@@LOG_OUT@@': sLogOut, + '@@TESTMANAGER_VERSION@@': config.g_ksVersion, + '@@TESTMANAGER_REVISION@@': config.g_ksRevision, + '@@BASE_URL@@': self._oSrvGlue.getBaseUrl(), + '@@TOP_MENU_ITEMS@@': sTopMenuItems, + '@@SIDE_MENU_ITEMS@@': sSideMenuItems, + '@@SIDE_FILTER_CONTROL@@': self._sPageFilter, + '@@SIDE_MENU_FORM_ATTRS@@': '', + '@@PAGE_BODY@@': self._sPageBody, + '@@DEBUG@@': '', + }; + + # Side menu form attributes. + if self._dSideMenuFormAttrs: + dReplacements['@@SIDE_MENU_FORM_ATTRS@@'] = ' '.join(['%s="%s"' % (sKey, webutils.escapeAttr(sValue)) + for sKey, sValue in self._dSideMenuFormAttrs.items()]); + + # Special current user handling. + if self._oCurUser is not None: + dReplacements['@@USER_NAME@@'] = self._oCurUser.sUsername; + else: + dReplacements['@@USER_NAME@@'] = 'unauthorized user "' + self._oSrvGlue.getLoginName() + '"'; + + # Prep debug section. + if self._sDebug == '': + if config.g_kfWebUiSqlTrace or self._fDbgSqlTrace or self._fDbgSqlExplain: + self._sDebug = '<h3>Processed in %s ns.</h3>\n%s\n' \ + % ( utils.formatNumber(utils.timestampNano() - self._oSrvGlue.tsStart,), + self._oDb.debugHtmlReport(self._oSrvGlue.tsStart)); + elif config.g_kfWebUiProcessedIn: + self._sDebug = '<h3>Processed in %s ns.</h3>\n' \ + % ( utils.formatNumber(utils.timestampNano() - self._oSrvGlue.tsStart,), ); + if config.g_kfWebUiDebugPanel: + self._sDebug += self._debugRenderPanel(); + if self._sDebug != '': + dReplacements['@@DEBUG@@'] = u'<div id="debug"><br><br><hr/>' \ + + (utils.toUnicode(self._sDebug, errors='ignore') if isinstance(self._sDebug, str) + else self._sDebug) \ + + u'</div>\n'; + + # + # Load the template. + # + with open(os.path.join(self._oSrvGlue.pathTmWebUI(), self._sTemplate)) as oFile: # pylint: disable=unspecified-encoding + sTmpl = oFile.read(); + + # + # Process the template, outputting each part we process. + # + offStart = 0; + offCur = 0; + while offCur < len(sTmpl): + # Look for a replacement variable. + offAtAt = sTmpl.find('@@', offCur); + if offAtAt < 0: + break; + offCur = offAtAt + 2; + if sTmpl[offCur] not in string.ascii_uppercase: + continue; + offEnd = sTmpl.find('@@', offCur, offCur+28); + if offEnd <= 0: + continue; + offCur = offEnd; + sReplacement = sTmpl[offAtAt:offEnd+2]; + if sReplacement in dReplacements: + # Got a match! Write out the previous chunk followed by the replacement text. + if offStart < offAtAt: + self._oSrvGlue.write(sTmpl[offStart:offAtAt]); + self._oSrvGlue.write(dReplacements[sReplacement]); + # Advance past the replacement point in the template. + offCur += 2; + offStart = offCur; + else: + assert False, 'Unknown replacement "%s" at offset %s in %s' % (sReplacement, offAtAt, self._sTemplate ); + + # The final chunk. + if offStart < len(sTmpl): + self._oSrvGlue.write(sTmpl[offStart:]); + + return True; + + # + # Interface for WuiContentBase classes. + # + + def getUrlNoParams(self): + """ + Returns the base URL without any parameters (no trailing '?' or &). + """ + return self._sUrlBase[:self._sUrlBase.rindex('?')]; + + def getUrlBase(self): + """ + Returns the base URL, ending with '?' or '&'. + This may already include some debug parameters. + """ + return self._sUrlBase; + + def getParameters(self): + """ + Returns a (shallow) copy of the request parameter dictionary. + """ + return self._dParams.copy(); + + def getDb(self): + """ + Returns the database connection. + """ + return self._oDb; + + def getNow(self): + """ + Returns the effective date. + """ + return self._tsNow; + + + # + # Parameter handling. + # + + def getStringParam(self, sName, asValidValues = None, sDefault = None, fAllowNull = False): + """ + Gets a string parameter. + Raises exception if not found and sDefault is None. + """ + if sName in self._dParams: + if sName not in self._asCheckedParams: + self._asCheckedParams.append(sName); + sValue = self._dParams[sName]; + if isinstance(sValue, list): + raise WuiException('%s parameter "%s" is given multiple times: "%s"' % (self._sAction, sName, sValue)); + sValue = sValue.strip(); + elif sDefault is None and fAllowNull is not True: + raise WuiException('%s is missing parameters: "%s"' % (self._sAction, sName,)); + else: + sValue = sDefault; + + if asValidValues is not None and sValue not in asValidValues: + raise WuiException('%s parameter %s value "%s" not in %s ' + % (self._sAction, sName, sValue, asValidValues)); + return sValue; + + def getBoolParam(self, sName, fDefault = None): + """ + Gets a boolean parameter. + Raises exception if not found and fDefault is None, or if not a valid boolean. + """ + sValue = self.getStringParam(sName, [ 'True', 'true', '1', 'False', 'false', '0'], + '0' if fDefault is None else str(fDefault)); + # HACK: Checkboxes doesn't return a value when unchecked, so we always + # provide a default when dealing with boolean parameters. + return sValue in ('True', 'true', '1',); + + def getIntParam(self, sName, iMin = None, iMax = None, iDefault = None): + """ + Gets a integer parameter. + Raises exception if not found and iDefault is None, if not a valid int, + or if outside the range defined by iMin and iMax. + """ + if iDefault is not None and sName not in self._dParams: + return iDefault; + + sValue = self.getStringParam(sName, None, None if iDefault is None else str(iDefault)); + try: + iValue = int(sValue); + except: + raise WuiException('%s parameter %s value "%s" cannot be convert to an integer' + % (self._sAction, sName, sValue)); + + if (iMin is not None and iValue < iMin) \ + or (iMax is not None and iValue > iMax): + raise WuiException('%s parameter %s value %d is out of range [%s..%s]' + % (self._sAction, sName, iValue, iMin, iMax)); + return iValue; + + def getLongParam(self, sName, lMin = None, lMax = None, lDefault = None): + """ + Gets a long integer parameter. + Raises exception if not found and lDefault is None, if not a valid long, + or if outside the range defined by lMin and lMax. + """ + if lDefault is not None and sName not in self._dParams: + return lDefault; + + sValue = self.getStringParam(sName, None, None if lDefault is None else str(lDefault)); + try: + lValue = long(sValue); + except: + raise WuiException('%s parameter %s value "%s" cannot be convert to an integer' + % (self._sAction, sName, sValue)); + + if (lMin is not None and lValue < lMin) \ + or (lMax is not None and lValue > lMax): + raise WuiException('%s parameter %s value %d is out of range [%s..%s]' + % (self._sAction, sName, lValue, lMin, lMax)); + return lValue; + + def getTsParam(self, sName, tsDefault = None, fRequired = True): + """ + Gets a timestamp parameter. + Raises exception if not found and fRequired is True. + """ + if fRequired is False and sName not in self._dParams: + return tsDefault; + + sValue = self.getStringParam(sName, None, None if tsDefault is None else str(tsDefault)); + (sValue, sError) = ModelDataBase.validateTs(sValue); + if sError is not None: + raise WuiException('%s parameter %s value "%s": %s' + % (self._sAction, sName, sValue, sError)); + return sValue; + + def getListOfIntParams(self, sName, iMin = None, iMax = None, aiDefaults = None): + """ + Gets parameter list. + Raises exception if not found and aiDefaults is None, or if any of the + values are not valid integers or outside the range defined by iMin and iMax. + """ + if sName in self._dParams: + if sName not in self._asCheckedParams: + self._asCheckedParams.append(sName); + + if isinstance(self._dParams[sName], list): + asValues = self._dParams[sName]; + else: + asValues = [self._dParams[sName],]; + aiValues = []; + for sValue in asValues: + try: + iValue = int(sValue); + except: + raise WuiException('%s parameter %s value "%s" cannot be convert to an integer' + % (self._sAction, sName, sValue)); + + if (iMin is not None and iValue < iMin) \ + or (iMax is not None and iValue > iMax): + raise WuiException('%s parameter %s value %d is out of range [%s..%s]' + % (self._sAction, sName, iValue, iMin, iMax)); + aiValues.append(iValue); + else: + aiValues = aiDefaults; + + return aiValues; + + def getListOfStrParams(self, sName, asDefaults = None): + """ + Gets parameter list. + Raises exception if not found and asDefaults is None. + """ + if sName in self._dParams: + if sName not in self._asCheckedParams: + self._asCheckedParams.append(sName); + + if isinstance(self._dParams[sName], list): + asValues = [str(s).strip() for s in self._dParams[sName]]; + else: + asValues = [str(self._dParams[sName]).strip(), ]; + elif asDefaults is None: + raise WuiException('%s is missing parameters: "%s"' % (self._sAction, sName,)); + else: + asValues = asDefaults; + + return asValues; + + def getListOfTestCasesParam(self, sName, asDefaults = None): # too many local vars - pylint: disable=too-many-locals + """Get list of test cases and their parameters""" + if sName in self._dParams: + if sName not in self._asCheckedParams: + self._asCheckedParams.append(sName) + + aoListOfTestCases = [] + + aiSelectedTestCaseIds = self.getListOfIntParams('%s[asCheckedTestCases]' % sName, aiDefaults=[]) + aiAllTestCases = self.getListOfIntParams('%s[asAllTestCases]' % sName, aiDefaults=[]) + + for idTestCase in aiAllTestCases: + aiCheckedTestCaseArgs = \ + self.getListOfIntParams( + '%s[%d][asCheckedTestCaseArgs]' % (sName, idTestCase), + aiDefaults=[]) + + aiAllTestCaseArgs = \ + self.getListOfIntParams( + '%s[%d][asAllTestCaseArgs]' % (sName, idTestCase), + aiDefaults=[]) + + oListEntryTestCaseArgs = [] + for idTestCaseArgs in aiAllTestCaseArgs: + fArgsChecked = idTestCaseArgs in aiCheckedTestCaseArgs; + + # Dry run + sPrefix = '%s[%d][%d]' % (sName, idTestCase, idTestCaseArgs,); + self.getIntParam(sPrefix + '[idTestCaseArgs]', iDefault = -1,) + + sArgs = self.getStringParam(sPrefix + '[sArgs]', sDefault = '') + cSecTimeout = self.getStringParam(sPrefix + '[cSecTimeout]', sDefault = '') + cGangMembers = self.getStringParam(sPrefix + '[cGangMembers]', sDefault = '') + cGangMembers = self.getStringParam(sPrefix + '[cGangMembers]', sDefault = '') + + oListEntryTestCaseArgs.append((fArgsChecked, idTestCaseArgs, sArgs, cSecTimeout, cGangMembers)) + + sTestCaseName = self.getStringParam('%s[%d][sName]' % (sName, idTestCase), sDefault='') + + oListEntryTestCase = ( + idTestCase, + idTestCase in aiSelectedTestCaseIds, + sTestCaseName, + oListEntryTestCaseArgs + ); + + aoListOfTestCases.append(oListEntryTestCase) + + if not aoListOfTestCases: + if asDefaults is None: + raise WuiException('%s is missing parameters: "%s"' % (self._sAction, sName)) + aoListOfTestCases = asDefaults + + return aoListOfTestCases + + def getEffectiveDateParam(self, sParamName = None): + """ + Gets the effective date parameter. + + Returns a timestamp suitable for database and url parameters. + Returns None if not found or empty. + + The first call with sParamName set to None will set the internal _tsNow + value upon successfull return. + """ + + sName = sParamName if sParamName is not None else WuiDispatcherBase.ksParamEffectiveDate + + if sName not in self._dParams: + return None; + + if sName not in self._asCheckedParams: + self._asCheckedParams.append(sName); + + sValue = self._dParams[sName]; + if isinstance(sValue, list): + raise WuiException('%s parameter "%s" is given multiple times: %s' % (self._sAction, sName, sValue)); + sValue = sValue.strip(); + if sValue == '': + return None; + + # + # Timestamp, just validate it and return. + # + if sValue[0] not in ['-', '+']: + (sValue, sError) = ModelDataBase.validateTs(sValue); + if sError is not None: + raise WuiException('%s parameter "%s" ("%s") is invalid: %s' % (self._sAction, sName, sValue, sError)); + if sParamName is None and self._tsNow is None: + self._tsNow = sValue; + return sValue; + + # + # Relative timestamp. Validate and convert it to a fixed timestamp. + # + chSign = sValue[0]; + (sValue, sError) = ModelDataBase.validateTs(sValue[1:], fRelative = True); + if sError is not None: + raise WuiException('%s parameter "%s" ("%s") is invalid: %s' % (self._sAction, sName, sValue, sError)); + if sValue[-6] in ['-', '+']: + raise WuiException('%s parameter "%s" ("%s") is a relative timestamp but incorrectly includes a time zone.' + % (self._sAction, sName, sValue)); + offTime = 11; + if sValue[offTime - 1] != ' ': + raise WuiException('%s parameter "%s" ("%s") incorrect format.' % (self._sAction, sName, sValue)); + sInterval = 'P' + sValue[:(offTime - 1)] + 'T' + sValue[offTime:]; + + self._oDb.execute('SELECT CURRENT_TIMESTAMP ' + chSign + ' \'' + sInterval + '\'::INTERVAL'); + oDate = self._oDb.fetchOne()[0]; + + sValue = str(oDate); + if sParamName is None and self._tsNow is None: + self._tsNow = sValue; + return sValue; + + def getRedirectToParameter(self, sDefault = None): + """ + Gets the special redirect to parameter if it exists, will Return default + if not, with None being a valid default. + + Makes sure the it doesn't got offsite. + Raises exception if invalid. + """ + if sDefault is not None or self.ksParamRedirectTo in self._dParams: + sValue = self.getStringParam(self.ksParamRedirectTo, sDefault = sDefault); + cch = sValue.find("?"); + if cch < 0: + cch = sValue.find("#"); + if cch < 0: + cch = len(sValue); + for ch in (':', '/', '\\', '..'): + if sValue.find(ch, 0, cch) >= 0: + raise WuiException('Invalid character (%c) in redirect-to url: %s' % (ch, sValue,)); + else: + sValue = None; + return sValue; + + + def _checkForUnknownParameters(self): + """ + Check if we've handled all parameters, raises exception if anything + unknown was found. + """ + + if len(self._asCheckedParams) != len(self._dParams): + sUnknownParams = ''; + for sKey in self._dParams: + if sKey not in self._asCheckedParams: + sUnknownParams += ' ' + sKey + '=' + str(self._dParams[sKey]); + raise WuiException('Unknown parameters: ' + sUnknownParams); + + return True; + + def _assertPostRequest(self): + """ + Makes sure that the request we're dispatching is a POST request. + Raises an exception of not. + """ + if self._oSrvGlue.getMethod() != 'POST': + raise WuiException('Expected "POST" request, got "%s"' % (self._oSrvGlue.getMethod(),)) + return True; + + # + # Client browser type. + # + + ## @name Browser types. + ## @{ + ksBrowserFamily_Unknown = 0; + ksBrowserFamily_Gecko = 1; + ksBrowserFamily_Webkit = 2; + ksBrowserFamily_Trident = 3; + ## @} + + ## @name Browser types. + ## @{ + ksBrowserType_FamilyMask = 0xff; + ksBrowserType_Unknown = 0; + ksBrowserType_Firefox = (1 << 8) | ksBrowserFamily_Gecko; + ksBrowserType_Chrome = (2 << 8) | ksBrowserFamily_Webkit; + ksBrowserType_Safari = (3 << 8) | ksBrowserFamily_Webkit; + ksBrowserType_IE = (4 << 8) | ksBrowserFamily_Trident; + ## @} + + def getBrowserType(self): + """ + Gets the browser type. + The browser family can be extracted from this using ksBrowserType_FamilyMask. + """ + sAgent = self._oSrvGlue.getUserAgent(); + if sAgent.find('AppleWebKit/') > 0: + if sAgent.find('Chrome/') > 0: + return self.ksBrowserType_Chrome; + if sAgent.find('Safari/') > 0: + return self.ksBrowserType_Safari; + return self.ksBrowserType_Unknown | self.ksBrowserFamily_Webkit; + if sAgent.find('Gecko/') > 0: + if sAgent.find('Firefox/') > 0: + return self.ksBrowserType_Firefox; + return self.ksBrowserType_Unknown | self.ksBrowserFamily_Gecko; + return self.ksBrowserType_Unknown | self.ksBrowserFamily_Unknown; + + def isBrowserGecko(self, sMinVersion = None): + """ Returns true if it's a gecko based browser. """ + if (self.getBrowserType() & self.ksBrowserType_FamilyMask) != self.ksBrowserFamily_Gecko: + return False; + if sMinVersion is not None: + sAgent = self._oSrvGlue.getUserAgent(); + sVersion = sAgent[sAgent.find('Gecko/')+6:].split()[0]; + if sVersion < sMinVersion: + return False; + return True; + + # + # User related stuff. + # + + def isReadOnlyUser(self): + """ Returns true if the logged in user is read-only or if no user is logged in. """ + return self._oCurUser is None or self._oCurUser.fReadOnly; + + + # + # Debugging + # + + def _debugProcessDispatch(self): + """ + Processes any debugging parameters in the request and adds them to + _asCheckedParams so they won't cause trouble in the action handler. + """ + + self._fDbgSqlTrace = self.getBoolParam(self.ksParamDbgSqlTrace, False); + self._fDbgSqlExplain = self.getBoolParam(self.ksParamDbgSqlExplain, False); + + if self._fDbgSqlExplain: + self._oDb.debugEnableExplain(); + + return True; + + def _debugRenderPanel(self): + """ + Renders a simple form for controlling WUI debugging. + + Returns the HTML for it. + """ + + sHtml = '<div id="debug-panel">\n' \ + ' <form id="debug-panel-form" method="get" action="#">\n'; + + for sKey, oValue in self._dParams.items(): + if sKey not in self.kasDbgParams: + if hasattr(oValue, 'startswith'): + sHtml += ' <input type="hidden" name="%s" value="%s"/>\n' \ + % (webutils.escapeAttr(sKey), webutils.escapeAttrToStr(oValue),); + else: + for oSubValue in oValue: + sHtml += ' <input type="hidden" name="%s" value="%s"/>\n' \ + % (webutils.escapeAttr(sKey), webutils.escapeAttrToStr(oSubValue),); + + for aoCheckBox in ( + [self.ksParamDbgSqlTrace, self._fDbgSqlTrace, 'SQL trace'], + [self.ksParamDbgSqlExplain, self._fDbgSqlExplain, 'SQL explain'], ): + sHtml += ' <input type="checkbox" name="%s" value="1"%s />%s\n' \ + % (aoCheckBox[0], ' checked' if aoCheckBox[1] else '', aoCheckBox[2]); + + sHtml += ' <button type="submit">Apply</button>\n'; + sHtml += ' </form>\n' \ + '</div>\n'; + return sHtml; + + + def _debugGetParameters(self): + """ + Gets a dictionary with the debug parameters. + + For use when links are constructed from scratch instead of self._dParams. + """ + return self._dDbgParams; + + # + # Dispatching + # + + def _actionDefault(self): + """The default action handler, always overridden. """ + raise WuiException('The child class shall override WuiBase.actionDefault().') + + def _actionGenericListing(self, oLogicType, oListContentType): + """ + Generic listing action. + + oLogicType implements fetchForListing. + oListContentType is a child of WuiListContentBase. + """ + tsEffective = self.getEffectiveDateParam(); + cItemsPerPage = self.getIntParam(self.ksParamItemsPerPage, iMin = 2, iMax = 9999, iDefault = 384); + iPage = self.getIntParam(self.ksParamPageNo, iMin = 0, iMax = 999999, iDefault = 0); + aiSortColumnsDup = self.getListOfIntParams(self.ksParamSortColumns, + iMin = -getattr(oLogicType, 'kcMaxSortColumns', 0) + 1, + iMax = getattr(oLogicType, 'kcMaxSortColumns', 0), aiDefaults = []); + aiSortColumns = []; + for iSortColumn in aiSortColumnsDup: + if iSortColumn not in aiSortColumns: + aiSortColumns.append(iSortColumn); + self._checkForUnknownParameters(); + + ## @todo fetchForListing could be made more useful if it returned a tuple + # that includes the total number of entries, thus making paging more user + # friendly (known number of pages). So, the return should be: + # (aoEntries, cAvailableEntries) + # + # In addition, we could add a new parameter to include deleted entries, + # making it easier to find old deleted testboxes/testcases/whatever and + # clone them back to life. The temporal navigation is pretty usless here. + # + aoEntries = oLogicType(self._oDb).fetchForListing(iPage * cItemsPerPage, cItemsPerPage + 1, tsEffective, aiSortColumns); + oContent = oListContentType(aoEntries, iPage, cItemsPerPage, tsEffective, + fnDPrint = self._oSrvGlue.dprint, oDisp = self, aiSelectedSortColumns = aiSortColumns); + (self._sPageTitle, self._sPageBody) = oContent.show(); + return True; + + def _actionGenericFormAdd(self, oDataType, oFormType, sRedirectTo = None): + """ + Generic add something form display request handler. + + oDataType is a ModelDataBase child class. + oFormType is a WuiFormContentBase child class. + """ + assert issubclass(oDataType, ModelDataBase); + from testmanager.webui.wuicontentbase import WuiFormContentBase; + assert issubclass(oFormType, WuiFormContentBase); + + oData = oDataType().initFromParams(oDisp = self, fStrict = False); + sRedirectTo = self.getRedirectToParameter(sRedirectTo); + self._checkForUnknownParameters(); + + oForm = oFormType(oData, oFormType.ksMode_Add, oDisp = self); + oForm.setRedirectTo(sRedirectTo); + (self._sPageTitle, self._sPageBody) = oForm.showForm(); + return True + + def _actionGenericFormDetails(self, oDataType, oLogicType, oFormType, sIdAttr = None, sGenIdAttr = None): # pylint: disable=too-many-locals + """ + Generic handler for showing a details form/page. + + oDataType is a ModelDataBase child class. + oLogicType may implement fetchForChangeLog. + oFormType is a WuiFormContentBase child class. + sIdParamName is the name of the ID parameter (not idGen!). + """ + # Input. + assert issubclass(oDataType, ModelDataBase); + assert issubclass(oLogicType, ModelLogicBase); + from testmanager.webui.wuicontentbase import WuiFormContentBase; + assert issubclass(oFormType, WuiFormContentBase); + + if sIdAttr is None: + sIdAttr = oDataType.ksIdAttr; + if sGenIdAttr is None: + sGenIdAttr = getattr(oDataType, 'ksGenIdAttr', None); + + # Parameters. + idGenObject = -1; + if sGenIdAttr is not None: + idGenObject = self.getIntParam(getattr(oDataType, 'ksParam_' + sGenIdAttr), 0, 0x7ffffffe, -1); + if idGenObject != -1: + idObject = tsNow = None; + else: + idObject = self.getIntParam(getattr(oDataType, 'ksParam_' + sIdAttr), 0, 0x7ffffffe, -1); + tsNow = self.getEffectiveDateParam(); + fChangeLog = self.getBoolParam(WuiDispatcherBase.ksParamChangeLogEnabled, True); + iChangeLogPageNo = self.getIntParam(WuiDispatcherBase.ksParamChangeLogPageNo, 0, 9999, 0); + cChangeLogEntriesPerPage = self.getIntParam(WuiDispatcherBase.ksParamChangeLogEntriesPerPage, 2, 9999, 4); + self._checkForUnknownParameters(); + + # Fetch item and display it. + if idGenObject == -1: + oData = oDataType().initFromDbWithId(self._oDb, idObject, tsNow); + else: + oData = oDataType().initFromDbWithGenId(self._oDb, idGenObject); + + oContent = oFormType(oData, oFormType.ksMode_Show, oDisp = self); + (self._sPageTitle, self._sPageBody) = oContent.showForm(); + + # Add change log if supported. + if fChangeLog and hasattr(oLogicType, 'fetchForChangeLog'): + (aoEntries, fMore) = oLogicType(self._oDb).fetchForChangeLog(getattr(oData, sIdAttr), + iChangeLogPageNo * cChangeLogEntriesPerPage, + cChangeLogEntriesPerPage , + tsNow); + self._sPageBody += oContent.showChangeLog(aoEntries, fMore, iChangeLogPageNo, cChangeLogEntriesPerPage, tsNow); + return True + + def _actionGenericDoRemove(self, oLogicType, sParamId, sRedirAction): + """ + Delete entry (using oLogicType.removeEntry). + + oLogicType is a class that implements addEntry. + + sParamId is the name (ksParam_...) of the HTTP variable hold the ID of + the database entry to delete. + + sRedirAction is what action to redirect to on success. + """ + import cgitb; + + idEntry = self.getIntParam(sParamId, iMin = 1, iMax = 0x7ffffffe) + fCascade = self.getBoolParam('fCascadeDelete', False); + sRedirectTo = self.getRedirectToParameter(self._sActionUrlBase + sRedirAction); + self._checkForUnknownParameters() + + try: + if self.isReadOnlyUser(): + raise Exception('"%s" is a read only user!' % (self._oCurUser.sUsername,)); + self._sPageTitle = None + self._sPageBody = None + self._sRedirectTo = sRedirectTo; + return oLogicType(self._oDb).removeEntry(self._oCurUser.uid, idEntry, fCascade = fCascade, fCommit = True); + except Exception as oXcpt: + self._oDb.rollback(); + self._sPageTitle = 'Unable to delete entry'; + self._sPageBody = str(oXcpt); + if config.g_kfDebugDbXcpt: + self._sPageBody += cgitb.html(sys.exc_info()); + self._sRedirectTo = None; + return False; + + def _actionGenericFormEdit(self, oDataType, oFormType, sIdParamName = None, sRedirectTo = None): + """ + Generic edit something form display request handler. + + oDataType is a ModelDataBase child class. + oFormType is a WuiFormContentBase child class. + sIdParamName is the name of the ID parameter (not idGen!). + """ + assert issubclass(oDataType, ModelDataBase); + from testmanager.webui.wuicontentbase import WuiFormContentBase; + assert issubclass(oFormType, WuiFormContentBase); + + if sIdParamName is None: + sIdParamName = getattr(oDataType, 'ksParam_' + oDataType.ksIdAttr); + assert len(sIdParamName) > 1; + + tsNow = self.getEffectiveDateParam(); + idObject = self.getIntParam(sIdParamName, 0, 0x7ffffffe); + sRedirectTo = self.getRedirectToParameter(sRedirectTo); + self._checkForUnknownParameters(); + oData = oDataType().initFromDbWithId(self._oDb, idObject, tsNow = tsNow); + + oContent = oFormType(oData, oFormType.ksMode_Edit, oDisp = self); + oContent.setRedirectTo(sRedirectTo); + (self._sPageTitle, self._sPageBody) = oContent.showForm(); + return True + + def _actionGenericFormEditL(self, oCoreObjectLogic, sCoreObjectIdFieldName, oWuiObjectLogic): + """ + Generic modify something form display request handler. + + @param oCoreObjectLogic A *Logic class + + @param sCoreObjectIdFieldName Name of HTTP POST variable that + contains object ID information + + @param oWuiObjectLogic Web interface renderer class + """ + + iCoreDataObjectId = self.getIntParam(sCoreObjectIdFieldName, 0, 0x7ffffffe, -1) + self._checkForUnknownParameters(); + + ## @todo r=bird: This will return a None object if the object wasn't found... Crash bang in the content generator + # code (that's not logic code btw.). + oData = oCoreObjectLogic(self._oDb).getById(iCoreDataObjectId) + + # Instantiate and render the MODIFY dialog form + oContent = oWuiObjectLogic(oData, oWuiObjectLogic.ksMode_Edit, oDisp=self) + + (self._sPageTitle, self._sPageBody) = oContent.showForm() + + return True + + def _actionGenericFormClone(self, oDataType, oFormType, sIdAttr, sGenIdAttr = None): + """ + Generic clone something form display request handler. + + oDataType is a ModelDataBase child class. + oFormType is a WuiFormContentBase child class. + sIdParamName is the name of the ID parameter. + sGenIdParamName is the name of the generation ID parameter, None if not applicable. + """ + # Input. + assert issubclass(oDataType, ModelDataBase); + from testmanager.webui.wuicontentbase import WuiFormContentBase; + assert issubclass(oFormType, WuiFormContentBase); + + # Parameters. + idGenObject = -1; + if sGenIdAttr is not None: + idGenObject = self.getIntParam(getattr(oDataType, 'ksParam_' + sGenIdAttr), 0, 0x7ffffffe, -1); + if idGenObject != -1: + idObject = tsNow = None; + else: + idObject = self.getIntParam(getattr(oDataType, 'ksParam_' + sIdAttr), 0, 0x7ffffffe, -1); + tsNow = self.getEffectiveDateParam(); + self._checkForUnknownParameters(); + + # Fetch data and clear identifying attributes not relevant to the clone. + if idGenObject != -1: + oData = oDataType().initFromDbWithGenId(self._oDb, idGenObject); + else: + oData = oDataType().initFromDbWithId(self._oDb, idObject, tsNow); + + setattr(oData, sIdAttr, None); + if sGenIdAttr is not None: + setattr(oData, sGenIdAttr, None); + oData.tsEffective = None; + oData.tsExpire = None; + + # Display form. + oContent = oFormType(oData, oFormType.ksMode_Add, oDisp = self); + (self._sPageTitle, self._sPageBody) = oContent.showForm() + return True + + + def _actionGenericFormPost(self, sMode, fnLogicAction, oDataType, oFormType, sRedirectTo, fStrict = True): + """ + Generic POST request handling from a WuiFormContentBase child. + + oDataType is a ModelDataBase child class. + oFormType is a WuiFormContentBase child class. + fnLogicAction is a method taking a oDataType instance and uidAuthor as arguments. + """ + assert issubclass(oDataType, ModelDataBase); + from testmanager.webui.wuicontentbase import WuiFormContentBase; + assert issubclass(oFormType, WuiFormContentBase); + + # + # Read and validate parameters. + # + oData = oDataType().initFromParams(oDisp = self, fStrict = fStrict); + sRedirectTo = self.getRedirectToParameter(sRedirectTo); + self._checkForUnknownParameters(); + self._assertPostRequest(); + if sMode == WuiFormContentBase.ksMode_Add and getattr(oData, 'kfIdAttrIsForForeign', False): + enmValidateFor = oData.ksValidateFor_AddForeignId; + elif sMode == WuiFormContentBase.ksMode_Add: + enmValidateFor = oData.ksValidateFor_Add; + else: + enmValidateFor = oData.ksValidateFor_Edit; + dErrors = oData.validateAndConvert(self._oDb, enmValidateFor); + + # Check that the user can do this. + sErrorMsg = None; + assert self._oCurUser is not None; + if self.isReadOnlyUser(): + sErrorMsg = 'User %s is not allowed to modify anything!' % (self._oCurUser.sUsername,) + + if not dErrors and not sErrorMsg: + oData.convertFromParamNull(); + + # + # Try do the job. + # + try: + fnLogicAction(oData, self._oCurUser.uid, fCommit = True); + except Exception as oXcpt: + self._oDb.rollback(); + oForm = oFormType(oData, sMode, oDisp = self); + oForm.setRedirectTo(sRedirectTo); + sErrorMsg = str(oXcpt) if not config.g_kfDebugDbXcpt else '\n'.join(utils.getXcptInfo(4)); + (self._sPageTitle, self._sPageBody) = oForm.showForm(sErrorMsg = sErrorMsg); + else: + # + # Worked, redirect to the specified page. + # + self._sPageTitle = None; + self._sPageBody = None; + self._sRedirectTo = sRedirectTo; + else: + oForm = oFormType(oData, sMode, oDisp = self); + oForm.setRedirectTo(sRedirectTo); + (self._sPageTitle, self._sPageBody) = oForm.showForm(dErrors = dErrors, sErrorMsg = sErrorMsg); + return True; + + def _actionGenericFormAddPost(self, oDataType, oLogicType, oFormType, sRedirAction, fStrict=True): + """ + Generic add entry POST request handling from a WuiFormContentBase child. + + oDataType is a ModelDataBase child class. + oLogicType is a class that implements addEntry. + oFormType is a WuiFormContentBase child class. + sRedirAction is what action to redirect to on success. + """ + assert issubclass(oDataType, ModelDataBase); + assert issubclass(oLogicType, ModelLogicBase); + from testmanager.webui.wuicontentbase import WuiFormContentBase; + assert issubclass(oFormType, WuiFormContentBase); + + oLogic = oLogicType(self._oDb); + return self._actionGenericFormPost(WuiFormContentBase.ksMode_Add, oLogic.addEntry, oDataType, oFormType, + '?' + webutils.encodeUrlParams({self.ksParamAction: sRedirAction}), fStrict=fStrict) + + def _actionGenericFormEditPost(self, oDataType, oLogicType, oFormType, sRedirAction, fStrict = True): + """ + Generic edit POST request handling from a WuiFormContentBase child. + + oDataType is a ModelDataBase child class. + oLogicType is a class that implements addEntry. + oFormType is a WuiFormContentBase child class. + sRedirAction is what action to redirect to on success. + """ + assert issubclass(oDataType, ModelDataBase); + assert issubclass(oLogicType, ModelLogicBase); + from testmanager.webui.wuicontentbase import WuiFormContentBase; + assert issubclass(oFormType, WuiFormContentBase); + + oLogic = oLogicType(self._oDb); + return self._actionGenericFormPost(WuiFormContentBase.ksMode_Edit, oLogic.editEntry, oDataType, oFormType, + '?' + webutils.encodeUrlParams({self.ksParamAction: sRedirAction}), + fStrict = fStrict); + + def _unauthorizedUser(self): + """ + Displays the unauthorized user message (corresponding record is not + present in DB). + """ + + sLoginName = self._oSrvGlue.getLoginName(); + + # Report to system log + oSystemLogLogic = SystemLogLogic(self._oDb); + oSystemLogLogic.addEntry(SystemLogData.ksEvent_UserAccountUnknown, + 'Unknown user (%s) attempts to access from %s' + % (sLoginName, self._oSrvGlue.getClientAddr()), + 24, fCommit = True) + + # Display message. + self._sPageTitle = 'User not authorized' + self._sPageBody = """ + <p>Access denied for user <b>%s</b>. + Please contact an admin user to set up your access.</p> + """ % (sLoginName,) + return True; + + def dispatchRequest(self): + """ + Dispatches a request. + """ + + # + # Get the parameters and checks for duplicates. + # + try: + dParams = self._oSrvGlue.getParameters(); + except Exception as oXcpt: + raise WuiException('Error retriving parameters: %s' % (oXcpt,)); + + for sKey in dParams.keys(): + + # Take care about strings which may contain unicode characters: convert percent-encoded symbols back to unicode. + for idxItem, _ in enumerate(dParams[sKey]): + dParams[sKey][idxItem] = utils.toUnicode(dParams[sKey][idxItem], 'utf-8'); + + if not len(dParams[sKey]) > 1: + dParams[sKey] = dParams[sKey][0]; + self._dParams = dParams; + + # + # Figure out the requested action and validate it. + # + if self.ksParamAction in self._dParams: + self._sAction = self._dParams[self.ksParamAction]; + self._asCheckedParams.append(self.ksParamAction); + else: + self._sAction = self.ksActionDefault; + + if isinstance(self._sAction, list) or self._sAction not in self._dDispatch: + raise WuiException('Unknown action "%s" requested' % (self._sAction,)); + + # + # Call action handler and generate the page (if necessary). + # + if self._oCurUser is not None: + self._debugProcessDispatch(); + if self._dDispatch[self._sAction]() is self.ksDispatchRcAllDone: + return True; + else: + self._unauthorizedUser(); + + if self._sRedirectTo is None: + self._generatePage(); + else: + self._redirectPage(); + return True; + + + def dprint(self, sText): + """ Debug printing. """ + if config.g_kfWebUiDebug: + self._oSrvGlue.dprint(sText); diff --git a/src/VBox/ValidationKit/testmanager/webui/wuicontentbase.py b/src/VBox/ValidationKit/testmanager/webui/wuicontentbase.py new file mode 100755 index 00000000..c91e6036 --- /dev/null +++ b/src/VBox/ValidationKit/testmanager/webui/wuicontentbase.py @@ -0,0 +1,1290 @@ +# -*- coding: utf-8 -*- +# $Id: wuicontentbase.py $ + +""" +Test Manager Web-UI - Content Base Classes. +""" + +__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 copy; +import sys; + +# Validation Kit imports. +from common import utils, webutils; +from testmanager import config; +from testmanager.webui.wuibase import WuiDispatcherBase, WuiException; +from testmanager.webui.wuihlpform import WuiHlpForm; +from testmanager.core import db; +from testmanager.core.base import AttributeChangeEntryPre; + +# Python 3 hacks: +if sys.version_info[0] >= 3: + unicode = str; # pylint: disable=redefined-builtin,invalid-name + + +class WuiHtmlBase(object): # pylint: disable=too-few-public-methods + """ + Base class for HTML objects. + """ + + def __init__(self): + """Dummy init to shut up pylint.""" + pass; # pylint: disable=unnecessary-pass + + def toHtml(self): + + """ + Must be overridden by sub-classes. + """ + assert False; + return ''; + + def __str__(self): + """ String representation is HTML, simplifying formatting and such. """ + return self.toHtml(); + + +class WuiLinkBase(WuiHtmlBase): # pylint: disable=too-few-public-methods + """ + For passing links from WuiListContentBase._formatListEntry. + """ + + def __init__(self, sName, sUrlBase, dParams = None, sConfirm = None, sTitle = None, + sFragmentId = None, fBracketed = True, sExtraAttrs = ''): + WuiHtmlBase.__init__(self); + self.sName = sName + self.sUrl = sUrlBase + self.sConfirm = sConfirm; + self.sTitle = sTitle; + self.fBracketed = fBracketed; + self.sExtraAttrs = sExtraAttrs; + + if dParams: + # Do some massaging of None arguments. + dParams = dict(dParams); + for sKey in dParams: + if dParams[sKey] is None: + dParams[sKey] = ''; + self.sUrl += '?' + webutils.encodeUrlParams(dParams); + + if sFragmentId is not None: + self.sUrl += '#' + sFragmentId; + + def setBracketed(self, fBracketed): + """Changes the bracketing style.""" + self.fBracketed = fBracketed; + return True; + + def toHtml(self): + """ + Returns a simple HTML anchor element. + """ + sExtraAttrs = self.sExtraAttrs; + if self.sConfirm is not None: + sExtraAttrs += 'onclick=\'return confirm("%s");\' ' % (webutils.escapeAttr(self.sConfirm),); + if self.sTitle is not None: + sExtraAttrs += 'title="%s" ' % (webutils.escapeAttr(self.sTitle),); + if sExtraAttrs and sExtraAttrs[-1] != ' ': + sExtraAttrs += ' '; + + sFmt = '[<a %shref="%s">%s</a>]'; + if not self.fBracketed: + sFmt = '<a %shref="%s">%s</a>'; + return sFmt % (sExtraAttrs, webutils.escapeAttr(self.sUrl), webutils.escapeElem(self.sName)); + + @staticmethod + def estimateStringWidth(sString): + """ + Takes a string and estimate it's width so the caller can pad with + U+2002 before tab in a title text. This is very very rough. + """ + cchWidth = 0; + for ch in sString: + if ch.isupper() or ch in u'wm\u2007\u2003\u2001\u3000': + cchWidth += 2; + else: + cchWidth += 1; + return cchWidth; + + @staticmethod + def getStringWidthPaddingEx(cchWidth, cchMaxWidth): + """ Works with estiamteStringWidth(). """ + if cchWidth + 2 <= cchMaxWidth: + return u'\u2002' * ((cchMaxWidth - cchWidth) * 2 // 3) + return u''; + + @staticmethod + def getStringWidthPadding(sString, cchMaxWidth): + """ Works with estiamteStringWidth(). """ + return WuiLinkBase.getStringWidthPaddingEx(WuiLinkBase.estimateStringWidth(sString), cchMaxWidth); + + @staticmethod + def padStringToWidth(sString, cchMaxWidth): + """ Works with estimateStringWidth. """ + cchWidth = WuiLinkBase.estimateStringWidth(sString); + if cchWidth < cchMaxWidth: + return sString + WuiLinkBase.getStringWidthPaddingEx(cchWidth, cchMaxWidth); + return sString; + + +class WuiTmLink(WuiLinkBase): # pylint: disable=too-few-public-methods + """ Local link to the test manager. """ + + kdDbgParams = []; + + def __init__(self, sName, sUrlBase, dParams = None, sConfirm = None, sTitle = None, + sFragmentId = None, fBracketed = True): + + # Add debug parameters if necessary. + if self.kdDbgParams: + if not dParams: + dParams = dict(self.kdDbgParams); + else: + dParams = dict(dParams); + for sKey in self.kdDbgParams: + if sKey not in dParams: + dParams[sKey] = self.kdDbgParams[sKey]; + + WuiLinkBase.__init__(self, sName, sUrlBase, dParams, sConfirm, sTitle, sFragmentId, fBracketed); + + +class WuiAdminLink(WuiTmLink): # pylint: disable=too-few-public-methods + """ Local link to the test manager's admin portion. """ + + def __init__(self, sName, sAction, tsEffectiveDate = None, dParams = None, sConfirm = None, sTitle = None, + sFragmentId = None, fBracketed = True): + from testmanager.webui.wuiadmin import WuiAdmin; + if not dParams: + dParams = {}; + else: + dParams = dict(dParams); + if sAction is not None: + dParams[WuiAdmin.ksParamAction] = sAction; + if tsEffectiveDate is not None: + dParams[WuiAdmin.ksParamEffectiveDate] = tsEffectiveDate; + WuiTmLink.__init__(self, sName, WuiAdmin.ksScriptName, dParams = dParams, sConfirm = sConfirm, sTitle = sTitle, + sFragmentId = sFragmentId, fBracketed = fBracketed); + +class WuiMainLink(WuiTmLink): # pylint: disable=too-few-public-methods + """ Local link to the test manager's main portion. """ + + def __init__(self, sName, sAction, dParams = None, sConfirm = None, sTitle = None, sFragmentId = None, fBracketed = True): + if not dParams: + dParams = {}; + else: + dParams = dict(dParams); + from testmanager.webui.wuimain import WuiMain; + if sAction is not None: + dParams[WuiMain.ksParamAction] = sAction; + WuiTmLink.__init__(self, sName, WuiMain.ksScriptName, dParams = dParams, sConfirm = sConfirm, sTitle = sTitle, + sFragmentId = sFragmentId, fBracketed = fBracketed); + +class WuiSvnLink(WuiLinkBase): # pylint: disable=too-few-public-methods + """ + For linking to a SVN revision. + """ + def __init__(self, iRevision, sName = None, fBracketed = True, sExtraAttrs = ''): + if sName is None: + sName = 'r%s' % (iRevision,); + WuiLinkBase.__init__(self, sName, config.g_ksTracLogUrlPrefix, { 'rev': iRevision,}, + fBracketed = fBracketed, sExtraAttrs = sExtraAttrs); + +class WuiSvnLinkWithTooltip(WuiSvnLink): # pylint: disable=too-few-public-methods + """ + For linking to a SVN revision with changelog tooltip. + """ + def __init__(self, iRevision, sRepository, sName = None, fBracketed = True): + sExtraAttrs = ' onmouseover="return svnHistoryTooltipShow(event,\'%s\',%s);" onmouseout="return tooltipHide();"' \ + % ( sRepository, iRevision, ); + WuiSvnLink.__init__(self, iRevision, sName = sName, fBracketed = fBracketed, sExtraAttrs = sExtraAttrs); + +class WuiBuildLogLink(WuiLinkBase): + """ + For linking to a build log. + """ + def __init__(self, sUrl, sName = None, fBracketed = True): + assert sUrl; + if sName is None: + sName = 'Build log'; + if not webutils.hasSchema(sUrl): + WuiLinkBase.__init__(self, sName, config.g_ksBuildLogUrlPrefix + sUrl, fBracketed = fBracketed); + else: + WuiLinkBase.__init__(self, sName, sUrl, fBracketed = fBracketed); + +class WuiRawHtml(WuiHtmlBase): # pylint: disable=too-few-public-methods + """ + For passing raw html from WuiListContentBase._formatListEntry. + """ + def __init__(self, sHtml): + self.sHtml = sHtml; + WuiHtmlBase.__init__(self); + + def toHtml(self): + return self.sHtml; + +class WuiHtmlKeeper(WuiHtmlBase): # pylint: disable=too-few-public-methods + """ + For keeping a list of elements, concatenating their toHtml output together. + """ + def __init__(self, aoInitial = None, sSep = ' '): + WuiHtmlBase.__init__(self); + self.sSep = sSep; + self.aoKept = []; + if aoInitial is not None: + if isinstance(aoInitial, WuiHtmlBase): + self.aoKept.append(aoInitial); + else: + self.aoKept.extend(aoInitial); + + def append(self, oObject): + """ Appends one objects. """ + self.aoKept.append(oObject); + + def extend(self, aoObjects): + """ Appends a list of objects. """ + self.aoKept.extend(aoObjects); + + def toHtml(self): + return self.sSep.join(oObj.toHtml() for oObj in self.aoKept); + +class WuiSpanText(WuiRawHtml): # pylint: disable=too-few-public-methods + """ + Outputs the given text within a span of the given CSS class. + """ + def __init__(self, sSpanClass, sText, sTitle = None): + if sTitle is None: + WuiRawHtml.__init__(self, + u'<span class="%s">%s</span>' + % ( webutils.escapeAttr(sSpanClass), webutils.escapeElem(sText),)); + else: + WuiRawHtml.__init__(self, + u'<span class="%s" title="%s">%s</span>' + % ( webutils.escapeAttr(sSpanClass), webutils.escapeAttr(sTitle), webutils.escapeElem(sText),)); + +class WuiElementText(WuiRawHtml): # pylint: disable=too-few-public-methods + """ + Outputs the given element text. + """ + def __init__(self, sText): + WuiRawHtml.__init__(self, webutils.escapeElem(sText)); + + +class WuiContentBase(object): # pylint: disable=too-few-public-methods + """ + Base for the content classes. + """ + + ## The text/symbol for a very short add link. + ksShortAddLink = u'\u2795' + ## HTML hex entity string for ksShortAddLink. + ksShortAddLinkHtml = '➕;' + ## The text/symbol for a very short edit link. + ksShortEditLink = u'\u270D' + ## HTML hex entity string for ksShortDetailsLink. + ksShortEditLinkHtml = '✍' + ## The text/symbol for a very short details link. + ksShortDetailsLink = u'\U0001f6c8\ufe0e' + ## HTML hex entity string for ksShortDetailsLink. + ksShortDetailsLinkHtml = '🛈;︎' + ## The text/symbol for a very short change log / details / previous page link. + ksShortChangeLogLink = u'\u2397' + ## HTML hex entity string for ksShortDetailsLink. + ksShortChangeLogLinkHtml = '⎗' + ## The text/symbol for a very short reports link. + ksShortReportLink = u'\U0001f4ca\ufe0e' + ## HTML hex entity string for ksShortReportLink. + ksShortReportLinkHtml = '📊︎' + ## The text/symbol for a very short test results link. + ksShortTestResultsLink = u'\U0001f5d0\ufe0e' + + + def __init__(self, fnDPrint = None, oDisp = None): + self._oDisp = oDisp; # WuiDispatcherBase. + self._fnDPrint = fnDPrint; + if fnDPrint is None and oDisp is not None: + self._fnDPrint = oDisp.dprint; + + def dprint(self, sText): + """ Debug printing. """ + if self._fnDPrint: + self._fnDPrint(sText); + + @staticmethod + def formatTsShort(oTs): + """ + Formats a timestamp (db rep) into a short form. + """ + oTsZulu = db.dbTimestampToZuluDatetime(oTs); + sTs = oTsZulu.strftime('%Y-%m-%d %H:%M:%SZ'); + return unicode(sTs).replace('-', u'\u2011').replace(' ', u'\u00a0'); + + def getNowTs(self): + """ Gets a database compatible current timestamp from python. See db.dbTimestampPythonNow(). """ + return db.dbTimestampPythonNow(); + + def formatIntervalShort(self, oInterval): + """ + Formats an interval (db rep) into a short form. + """ + # default formatting for negative intervals. + if oInterval.days < 0: + return str(oInterval); + + # Figure the hour, min and sec counts. + cHours = oInterval.seconds // 3600; + cMinutes = (oInterval.seconds % 3600) // 60; + cSeconds = oInterval.seconds - cHours * 3600 - cMinutes * 60; + + # Tailor formatting to the interval length. + if oInterval.days > 0: + if oInterval.days > 1: + return '%d days, %d:%02d:%02d' % (oInterval.days, cHours, cMinutes, cSeconds); + return '1 day, %d:%02d:%02d' % (cHours, cMinutes, cSeconds); + if cMinutes > 0 or cSeconds >= 30 or cHours > 0: + return '%d:%02d:%02d' % (cHours, cMinutes, cSeconds); + if cSeconds >= 10: + return '%d.%ds' % (cSeconds, oInterval.microseconds // 100000); + if cSeconds > 0: + return '%d.%02ds' % (cSeconds, oInterval.microseconds // 10000); + return '%d ms' % (oInterval.microseconds // 1000,); + + @staticmethod + def genericPageWalker(iCurItem, cItems, sHrefFmt, cWidth = 11, iBase = 1, sItemName = 'page'): + """ + Generic page walker generator. + + sHrefFmt has three %s sequences: + 1. The first is the page number link parameter (0-based). + 2. The title text, iBase-based number or text. + 3. The link text, iBase-based number or text. + """ + + # Calc display range. + iStart = 0 if iCurItem - cWidth // 2 <= cWidth // 4 else iCurItem - cWidth // 2; + iEnd = iStart + cWidth; + if iEnd > cItems: + iEnd = cItems; + if cItems > cWidth: + iStart = cItems - cWidth; + + sHtml = u''; + + # Previous page (using << >> because « and » are too tiny). + if iCurItem > 0: + sHtml += '%s ' % sHrefFmt % (iCurItem - 1, 'previous ' + sItemName, '<<'); + else: + sHtml += '<< '; + + # 1 2 3 4... + if iStart > 0: + sHtml += '%s ... \n' % (sHrefFmt % (0, 'first %s' % (sItemName,), 0 + iBase),); + + sHtml += ' \n'.join(sHrefFmt % (i, '%s %d' % (sItemName, i + iBase), i + iBase) if i != iCurItem + else unicode(i + iBase) + for i in range(iStart, iEnd)); + if iEnd < cItems: + sHtml += ' ... %s\n' % (sHrefFmt % (cItems - 1, 'last %s' % (sItemName,), cItems - 1 + iBase)); + + # Next page. + if iCurItem + 1 < cItems: + sHtml += ' %s' % sHrefFmt % (iCurItem + 1, 'next ' + sItemName, '>>'); + else: + sHtml += ' >>'; + + return sHtml; + +class WuiSingleContentBase(WuiContentBase): # pylint: disable=too-few-public-methods + """ + Base for the content classes working on a single data object (oData). + """ + def __init__(self, oData, oDisp = None, fnDPrint = None): + WuiContentBase.__init__(self, oDisp = oDisp, fnDPrint = fnDPrint); + self._oData = oData; # Usually ModelDataBase. + + +class WuiFormContentBase(WuiSingleContentBase): # pylint: disable=too-few-public-methods + """ + Base class for simple input form content classes (single data object). + """ + + ## @name Form mode. + ## @{ + ksMode_Add = 'add'; + ksMode_Edit = 'edit'; + ksMode_Show = 'show'; + ## @} + + ## Default action mappings. + kdSubmitActionMappings = { + ksMode_Add: 'AddPost', + ksMode_Edit: 'EditPost', + }; + + def __init__(self, oData, sMode, sCoreName, oDisp, sTitle, sId = None, fEditable = True, sSubmitAction = None): + WuiSingleContentBase.__init__(self, copy.copy(oData), oDisp); + assert sMode in [self.ksMode_Add, self.ksMode_Edit, self.ksMode_Show]; + assert len(sTitle) > 1; + assert sId is None or sId; + + self._sMode = sMode; + self._sCoreName = sCoreName; + self._sActionBase = 'ksAction' + sCoreName; + self._sTitle = sTitle; + self._sId = sId if sId is not None else (type(oData).__name__.lower() + 'form'); + self._fEditable = fEditable and (oDisp is None or not oDisp.isReadOnlyUser()) + self._sSubmitAction = sSubmitAction; + if sSubmitAction is None and sMode != self.ksMode_Show: + self._sSubmitAction = getattr(oDisp, self._sActionBase + self.kdSubmitActionMappings[sMode]); + self._sRedirectTo = None; + + + def _populateForm(self, oForm, oData): + """ + Populates the form. oData has parameter NULL values. + This must be reimplemented by the child. + """ + _ = oForm; _ = oData; + raise Exception('Reimplement me!'); + + def _generatePostFormContent(self, oData): + """ + Generate optional content that comes below the form. + Returns a list of tuples, where the first tuple element is the title + and the second the content. I.e. similar to show() output. + """ + _ = oData; + return []; + + @staticmethod + def _calcChangeLogEntryLinks(aoEntries, iEntry): + """ + Returns an array of links to go with the change log entry. + """ + _ = aoEntries; _ = iEntry; + ## @todo detect deletion and recreation. + ## @todo view details link. + ## @todo restore link (need new action) + ## @todo clone link. + return []; + + @staticmethod + def _guessChangeLogEntryDescription(aoEntries, iEntry): + """ + Guesses the action + author that caused the change log entry. + Returns descriptive string. + """ + oEntry = aoEntries[iEntry]; + + # Figure the author of the change. + if oEntry.sAuthor is not None: + sAuthor = '%s (#%s)' % (oEntry.sAuthor, oEntry.uidAuthor,); + elif oEntry.uidAuthor is not None: + sAuthor = '#%d (??)' % (oEntry.uidAuthor,); + else: + sAuthor = None; + + # Figure the action. + if oEntry.oOldRaw is None: + if sAuthor is None: + return 'Created by batch job.'; + return 'Created by %s.' % (sAuthor,); + + if sAuthor is None: + return 'Automatically updated.' + return 'Modified by %s.' % (sAuthor,); + + @staticmethod + def formatChangeLogEntry(aoEntries, iEntry, sUrl, dParams): + """ + Formats one change log entry into one or more HTML table rows. + + The sUrl and dParams arguments are used to construct links to historical + data using the tsEffective value. If no links wanted, they'll both be None. + + Note! The parameters are given as array + index in case someone wishes + to access adjacent entries later in order to generate better + change descriptions. + """ + oEntry = aoEntries[iEntry]; + + # Turn the effective date into a URL if we can: + if sUrl: + dParams[WuiDispatcherBase.ksParamEffectiveDate] = oEntry.tsEffective; + sEffective = WuiLinkBase(WuiFormContentBase.formatTsShort(oEntry.tsEffective), sUrl, + dParams, fBracketed = False).toHtml(); + else: + sEffective = webutils.escapeElem(WuiFormContentBase.formatTsShort(oEntry.tsEffective)) + + # The primary row. + sRowClass = 'tmodd' if (iEntry + 1) & 1 else 'tmeven'; + sContent = ' <tr class="%s">\n' \ + ' <td rowspan="%d">%s</td>\n' \ + ' <td rowspan="%d">%s</td>\n' \ + ' <td colspan="3">%s%s</td>\n' \ + ' </tr>\n' \ + % ( sRowClass, + len(oEntry.aoChanges) + 1, sEffective, + len(oEntry.aoChanges) + 1, webutils.escapeElem(WuiFormContentBase.formatTsShort(oEntry.tsExpire)), + WuiFormContentBase._guessChangeLogEntryDescription(aoEntries, iEntry), + ' '.join(oLink.toHtml() for oLink in WuiFormContentBase._calcChangeLogEntryLinks(aoEntries, iEntry)),); + + # Additional rows for each changed attribute. + j = 0; + for oChange in oEntry.aoChanges: + if isinstance(oChange, AttributeChangeEntryPre): + sContent += ' <tr class="%s%s"><td>%s</td>'\ + '<td><div class="tdpre">%s%s%s</div></td>' \ + '<td><div class="tdpre">%s%s%s</div></td></tr>\n' \ + % ( sRowClass, 'odd' if j & 1 else 'even', + webutils.escapeElem(oChange.sAttr), + '<pre>' if oChange.sOldText else '', + webutils.escapeElem(oChange.sOldText), + '</pre>' if oChange.sOldText else '', + '<pre>' if oChange.sNewText else '', + webutils.escapeElem(oChange.sNewText), + '</pre>' if oChange.sNewText else '', ); + else: + sContent += ' <tr class="%s%s"><td>%s</td><td>%s</td><td>%s</td></tr>\n' \ + % ( sRowClass, 'odd' if j & 1 else 'even', + webutils.escapeElem(oChange.sAttr), + webutils.escapeElem(oChange.sOldText), + webutils.escapeElem(oChange.sNewText), ); + j += 1; + + return sContent; + + def _showChangeLogNavi(self, fMoreEntries, iPageNo, cEntriesPerPage, tsNow, sWhere): + """ + Returns the HTML for the change log navigator. + Note! See also _generateNavigation. + """ + sNavigation = '<div class="tmlistnav-%s">\n' % sWhere; + sNavigation += ' <table class="tmlistnavtab">\n' \ + ' <tr>\n'; + dParams = self._oDisp.getParameters(); + dParams[WuiDispatcherBase.ksParamChangeLogEntriesPerPage] = cEntriesPerPage; + dParams[WuiDispatcherBase.ksParamChangeLogPageNo] = iPageNo; + if tsNow is not None: + dParams[WuiDispatcherBase.ksParamEffectiveDate] = tsNow; + + # Prev and combo box in one cell. Both inside the form for formatting reasons. + sNavigation += ' <td>\n' \ + ' <form name="ChangeLogEntriesPerPageForm" method="GET">\n' + + # Prev + if iPageNo > 0: + dParams[WuiDispatcherBase.ksParamChangeLogPageNo] = iPageNo - 1; + sNavigation += '<a href="?%s#tmchangelog">Previous</a>\n' \ + % (webutils.encodeUrlParams(dParams),); + dParams[WuiDispatcherBase.ksParamChangeLogPageNo] = iPageNo; + else: + sNavigation += 'Previous\n'; + + # Entries per page selector. + del dParams[WuiDispatcherBase.ksParamChangeLogEntriesPerPage]; + sNavigation += ' \n' \ + ' <select name="%s" onchange="window.location=\'?%s&%s=\' + ' \ + 'this.options[this.selectedIndex].value + \'#tmchangelog\';" ' \ + 'title="Max change log entries per page">\n' \ + % (WuiDispatcherBase.ksParamChangeLogEntriesPerPage, + webutils.encodeUrlParams(dParams), + WuiDispatcherBase.ksParamChangeLogEntriesPerPage); + dParams[WuiDispatcherBase.ksParamChangeLogEntriesPerPage] = cEntriesPerPage; + + for iEntriesPerPage in [2, 4, 8, 16, 32, 64, 128, 256, 384, 512, 768, 1024, 1536, 2048, 3072, 4096, 8192]: + sNavigation += ' <option value="%d" %s>%d entries per page</option>\n' \ + % ( iEntriesPerPage, + 'selected="selected"' if iEntriesPerPage == cEntriesPerPage else '', + iEntriesPerPage ); + sNavigation += ' </select>\n'; + + # End of cell (and form). + sNavigation += ' </form>\n' \ + ' </td>\n'; + + # Next + if fMoreEntries: + dParams[WuiDispatcherBase.ksParamChangeLogPageNo] = iPageNo + 1; + sNavigation += ' <td><a href="?%s#tmchangelog">Next</a></td>\n' \ + % (webutils.encodeUrlParams(dParams),); + else: + sNavigation += ' <td>Next</td>\n'; + + sNavigation += ' </tr>\n' \ + ' </table>\n' \ + '</div>\n'; + return sNavigation; + + def setRedirectTo(self, sRedirectTo): + """ + For setting the hidden redirect-to field. + """ + self._sRedirectTo = sRedirectTo; + return True; + + def showChangeLog(self, aoEntries, fMoreEntries, iPageNo, cEntriesPerPage, tsNow, fShowNavigation = True): + """ + Render the change log, returning raw HTML. + aoEntries is an array of ChangeLogEntry. + """ + sContent = '\n' \ + '<hr>\n' \ + '<div id="tmchangelog">\n' \ + ' <h3>Change Log </h3>\n'; + if fShowNavigation: + sContent += self._showChangeLogNavi(fMoreEntries, iPageNo, cEntriesPerPage, tsNow, 'top'); + sContent += ' <table class="tmtable tmchangelog">\n' \ + ' <thead class="tmheader">' \ + ' <tr>' \ + ' <th rowspan="2">When</th>\n' \ + ' <th rowspan="2">Expire (excl)</th>\n' \ + ' <th colspan="3">Changes</th>\n' \ + ' </tr>\n' \ + ' <tr>\n' \ + ' <th>Attribute</th>\n' \ + ' <th>Old value</th>\n' \ + ' <th>New value</th>\n' \ + ' </tr>\n' \ + ' </thead>\n' \ + ' <tbody>\n'; + + if self._sMode == self.ksMode_Show: + sUrl = self._oDisp.getUrlNoParams(); + dParams = self._oDisp.getParameters(); + else: + sUrl = None; + dParams = None; + + for iEntry, _ in enumerate(aoEntries): + sContent += self.formatChangeLogEntry(aoEntries, iEntry, sUrl, dParams); + + sContent += ' </tbody>\n' \ + ' </table>\n'; + if fShowNavigation and len(aoEntries) >= 8: + sContent += self._showChangeLogNavi(fMoreEntries, iPageNo, cEntriesPerPage, tsNow, 'bottom'); + sContent += '</div>\n\n'; + return sContent; + + def _generateTopRowFormActions(self, oData): + """ + Returns a list of WuiTmLinks. + """ + aoActions = []; + if self._sMode == self.ksMode_Show and self._fEditable: + # Remove _idGen and effective date since we're always editing the current data, + # and make sure the primary ID is present. Also remove change log stuff. + dParams = self._oDisp.getParameters(); + if hasattr(oData, 'ksIdGenAttr'): + sIdGenParam = getattr(oData, 'ksParam_' + oData.ksIdGenAttr); + if sIdGenParam in dParams: + del dParams[sIdGenParam]; + for sParam in [ WuiDispatcherBase.ksParamEffectiveDate, ] + list(WuiDispatcherBase.kasChangeLogParams): + if sParam in dParams: + del dParams[sParam]; + dParams[getattr(oData, 'ksParam_' + oData.ksIdAttr)] = getattr(oData, oData.ksIdAttr); + + dParams[WuiDispatcherBase.ksParamAction] = getattr(self._oDisp, self._sActionBase + 'Edit'); + aoActions.append(WuiTmLink('Edit', '', dParams)); + + # Add clone operation if available. This uses the same data selection as for showing details. No change log. + if hasattr(self._oDisp, self._sActionBase + 'Clone'): + dParams = self._oDisp.getParameters(); + for sParam in WuiDispatcherBase.kasChangeLogParams: + if sParam in dParams: + del dParams[sParam]; + dParams[WuiDispatcherBase.ksParamAction] = getattr(self._oDisp, self._sActionBase + 'Clone'); + aoActions.append(WuiTmLink('Clone', '', dParams)); + + elif self._sMode == self.ksMode_Edit: + # Details views the details at a given time, so we need either idGen or an effecive date + regular id. + dParams = {}; + if hasattr(oData, 'ksIdGenAttr'): + sIdGenParam = getattr(oData, 'ksParam_' + oData.ksIdGenAttr); + dParams[sIdGenParam] = getattr(oData, oData.ksIdGenAttr); + elif hasattr(oData, 'tsEffective'): + dParams[WuiDispatcherBase.ksParamEffectiveDate] = oData.tsEffective; + dParams[getattr(oData, 'ksParam_' + oData.ksIdAttr)] = getattr(oData, oData.ksIdAttr); + dParams[WuiDispatcherBase.ksParamAction] = getattr(self._oDisp, self._sActionBase + 'Details'); + aoActions.append(WuiTmLink('Details', '', dParams)); + + # Add delete operation if available. + if hasattr(self._oDisp, self._sActionBase + 'DoRemove'): + dParams = self._oDisp.getParameters(); + dParams[WuiDispatcherBase.ksParamAction] = getattr(self._oDisp, self._sActionBase + 'DoRemove'); + dParams[getattr(oData, 'ksParam_' + oData.ksIdAttr)] = getattr(oData, oData.ksIdAttr); + aoActions.append(WuiTmLink('Delete', '', dParams, sConfirm = "Are you absolutely sure?")); + + return aoActions; + + def showForm(self, dErrors = None, sErrorMsg = None): + """ + Render the form. + """ + oForm = WuiHlpForm(self._sId, + '?' + webutils.encodeUrlParams({WuiDispatcherBase.ksParamAction: self._sSubmitAction}), + dErrors if dErrors is not None else {}, + fReadOnly = self._sMode == self.ksMode_Show); + + self._oData.convertToParamNull(); + + # If form cannot be constructed due to some reason we + # need to show this reason + try: + self._populateForm(oForm, self._oData); + if self._sRedirectTo is not None: + oForm.addTextHidden(self._oDisp.ksParamRedirectTo, self._sRedirectTo); + except WuiException as oXcpt: + sContent = unicode(oXcpt) + else: + sContent = oForm.finalize(); + + # Add any post form content. + atPostFormContent = self._generatePostFormContent(self._oData); + if atPostFormContent: + for iSection, tSection in enumerate(atPostFormContent): + (sSectionTitle, sSectionContent) = tSection; + sContent += u'<div id="postform-%d" class="tmformpostsection">\n' % (iSection,); + if sSectionTitle: + sContent += '<h3 class="tmformpostheader">%s</h3>\n' % (webutils.escapeElem(sSectionTitle),); + sContent += u' <div id="postform-%d-content" class="tmformpostcontent">\n' % (iSection,); + sContent += sSectionContent; + sContent += u' </div>\n' \ + u'</div>\n'; + + # Add action to the top. + aoActions = self._generateTopRowFormActions(self._oData); + if aoActions: + sActionLinks = '<p>%s</p>' % (' '.join(unicode(oLink) for oLink in aoActions)); + sContent = sActionLinks + sContent; + + # Add error info to the top. + if sErrorMsg is not None: + sContent = '<p class="tmerrormsg">' + webutils.escapeElem(sErrorMsg) + '</p>\n' + sContent; + + return (self._sTitle, sContent); + + def getListOfItems(self, asListItems = tuple(), asSelectedItems = tuple()): + """ + Format generic list which should be used by HTML form + """ + aoRet = [] + for sListItem in asListItems: + fEnabled = sListItem in asSelectedItems; + aoRet.append((sListItem, fEnabled, sListItem)) + return aoRet + + +class WuiListContentBase(WuiContentBase): + """ + Base for the list content classes. + """ + + def __init__(self, aoEntries, iPage, cItemsPerPage, tsEffectiveDate, sTitle, # pylint: disable=too-many-arguments + sId = None, fnDPrint = None, oDisp = None, aiSelectedSortColumns = None, fTimeNavigation = True): + WuiContentBase.__init__(self, fnDPrint = fnDPrint, oDisp = oDisp); + self._aoEntries = aoEntries; ## @todo should replace this with a Logic object and define methods for querying. + self._iPage = iPage; + self._cItemsPerPage = cItemsPerPage; + self._tsEffectiveDate = tsEffectiveDate; + self._fTimeNavigation = fTimeNavigation; + self._sTitle = sTitle; assert len(sTitle) > 1; + if sId is None: + sId = sTitle.strip().replace(' ', '').lower(); + assert sId.strip(); + self._sId = sId; + self._asColumnHeaders = []; + self._asColumnAttribs = []; + self._aaiColumnSorting = []; ##< list of list of integers + self._aiSelectedSortColumns = aiSelectedSortColumns; ##< list of integers + + def _formatCommentCell(self, sComment, cMaxLines = 3, cchMaxLine = 63): + """ + Helper functions for formatting comment cell. + Returns None or WuiRawHtml instance. + """ + # Nothing to do for empty comments. + if sComment is None: + return None; + sComment = sComment.strip(); + if not sComment: + return None; + + # Restrict the text if necessary, making the whole text available thru mouse-over. + ## @todo this would be better done by java script or smth, so it could automatically adjust to the table size. + if len(sComment) > cchMaxLine or sComment.count('\n') >= cMaxLines: + sShortHtml = ''; + for iLine, sLine in enumerate(sComment.split('\n')): + if iLine >= cMaxLines: + break; + if iLine > 0: + sShortHtml += '<br>\n'; + if len(sLine) > cchMaxLine: + sShortHtml += webutils.escapeElem(sLine[:(cchMaxLine - 3)]); + sShortHtml += '...'; + else: + sShortHtml += webutils.escapeElem(sLine); + return WuiRawHtml('<span class="tmcomment" title="%s">%s</span>' % (webutils.escapeAttr(sComment), sShortHtml,)); + + return WuiRawHtml('<span class="tmcomment">%s</span>' % (webutils.escapeElem(sComment).replace('\n', '<br>'),)); + + def _formatListEntry(self, iEntry): + """ + Formats the specified list entry as a list of column values. + Returns HTML for a table row. + + The child class really need to override this! + """ + # ASSUMES ModelDataBase children. + asRet = []; + for sAttr in self._aoEntries[0].getDataAttributes(): + asRet.append(getattr(self._aoEntries[iEntry], sAttr)); + return asRet; + + def _formatListEntryHtml(self, iEntry): + """ + Formats the specified list entry as HTML. + Returns HTML for a table row. + + The child class can override this to + """ + if (iEntry + 1) & 1: + sRow = u' <tr class="tmodd">\n'; + else: + sRow = u' <tr class="tmeven">\n'; + + aoValues = self._formatListEntry(iEntry); + assert len(aoValues) == len(self._asColumnHeaders), '%s vs %s' % (len(aoValues), len(self._asColumnHeaders)); + + for i, _ in enumerate(aoValues): + if i < len(self._asColumnAttribs) and self._asColumnAttribs[i]: + sRow += u' <td ' + self._asColumnAttribs[i] + '>'; + else: + sRow += u' <td>'; + + if isinstance(aoValues[i], WuiHtmlBase): + sRow += aoValues[i].toHtml(); + elif isinstance(aoValues[i], list): + if aoValues[i]: + for oElement in aoValues[i]: + if isinstance(oElement, WuiHtmlBase): + sRow += oElement.toHtml(); + elif db.isDbTimestamp(oElement): + sRow += webutils.escapeElem(self.formatTsShort(oElement)); + else: + sRow += webutils.escapeElem(unicode(oElement)); + sRow += ' '; + elif db.isDbTimestamp(aoValues[i]): + sRow += webutils.escapeElem(self.formatTsShort(aoValues[i])); + elif db.isDbInterval(aoValues[i]): + sRow += webutils.escapeElem(self.formatIntervalShort(aoValues[i])); + elif aoValues[i] is not None: + sRow += webutils.escapeElem(unicode(aoValues[i])); + + sRow += u'</td>\n'; + + return sRow + u' </tr>\n'; + + @staticmethod + def generateTimeNavigationComboBox(sWhere, dParams, tsEffective): + """ + Generates the HTML for the xxxx ago combo box form. + """ + sNavigation = '<form name="TmTimeNavCombo-%s" method="GET">\n' % (sWhere,); + sNavigation += ' <select name="%s" onchange="window.location=' % (WuiDispatcherBase.ksParamEffectiveDate); + sNavigation += '\'?%s&%s=\' + ' % (webutils.encodeUrlParams(dParams), WuiDispatcherBase.ksParamEffectiveDate) + sNavigation += 'this.options[this.selectedIndex].value;" title="Effective date">\n'; + + aoWayBackPoints = [ + ('+0000-00-00 00:00:00.00', 'Now', ' title="Present Day. Present Time."'), # lain :) + + ('-0000-00-00 01:00:00.00', '1 hour ago', ''), + ('-0000-00-00 02:00:00.00', '2 hours ago', ''), + ('-0000-00-00 03:00:00.00', '3 hours ago', ''), + + ('-0000-00-01 00:00:00.00', '1 day ago', ''), + ('-0000-00-02 00:00:00.00', '2 days ago', ''), + ('-0000-00-03 00:00:00.00', '3 days ago', ''), + + ('-0000-00-07 00:00:00.00', '1 week ago', ''), + ('-0000-00-14 00:00:00.00', '2 weeks ago', ''), + ('-0000-00-21 00:00:00.00', '3 weeks ago', ''), + + ('-0000-01-00 00:00:00.00', '1 month ago', ''), + ('-0000-02-00 00:00:00.00', '2 months ago', ''), + ('-0000-03-00 00:00:00.00', '3 months ago', ''), + ('-0000-04-00 00:00:00.00', '4 months ago', ''), + ('-0000-05-00 00:00:00.00', '5 months ago', ''), + ('-0000-06-00 00:00:00.00', 'Half a year ago', ''), + + ('-0001-00-00 00:00:00.00', '1 year ago', ''), + ] + fSelected = False; + for sTimestamp, sWayBackPointCaption, sExtraAttrs in aoWayBackPoints: + if sTimestamp == tsEffective: + fSelected = True; + sNavigation += ' <option value="%s"%s%s>%s</option>\n' \ + % (webutils.quoteUrl(sTimestamp), + ' selected="selected"' if sTimestamp == tsEffective else '', + sExtraAttrs, sWayBackPointCaption, ); + if not fSelected and tsEffective != '': + sNavigation += ' <option value="%s" selected>%s</option>\n' \ + % (webutils.quoteUrl(tsEffective), WuiContentBase.formatTsShort(tsEffective)) + + sNavigation += ' </select>\n' \ + '</form>\n'; + return sNavigation; + + @staticmethod + def generateTimeNavigationDateTime(sWhere, dParams, sNow): + """ + Generates HTML for a form with date + time input fields. + + Note! Modifies dParams! + """ + + # + # Date + time input fields. We use a java script helper to combine the two + # into a hidden field as there is no portable datetime input field type. + # + sNavigation = '<form method="get" action="?" onchange="timeNavigationUpdateHiddenEffDate(this,\'%s\')">' % (sWhere,); + if sNow is None: + sNow = utils.getIsoTimestamp(); + else: + sNow = utils.normalizeIsoTimestampToZulu(sNow); + asSplit = sNow.split('T'); + sNavigation += ' <input type="date" value="%s" id="EffDate%s"/> ' % (asSplit[0], sWhere, ); + sNavigation += ' <input type="time" value="%s" id="EffTime%s"/> ' % (asSplit[1][:8], sWhere,); + sNavigation += ' <input type="hidden" name="%s" value="%s" id="EffDateTime%s"/>' \ + % (WuiDispatcherBase.ksParamEffectiveDate, webutils.escapeAttr(sNow), sWhere); + for sKey in dParams: + sNavigation += ' <input type="hidden" name="%s" value="%s"/>' \ + % (webutils.escapeAttr(sKey), webutils.escapeAttrToStr(dParams[sKey])); + sNavigation += ' <input type="submit" value="Set"/>\n' \ + '</form>\n'; + return sNavigation; + + ## @todo move to better place! WuiMain uses it. + @staticmethod + def generateTimeNavigation(sWhere, dParams, tsEffectiveAbs, sPreamble = '', sPostamble = '', fKeepPageNo = False): + """ + Returns HTML for time navigation. + + Note! Modifies dParams! + Note! Views without a need for a timescale just stubs this method. + """ + sNavigation = '<div class="tmtimenav-%s tmtimenav">%s' % (sWhere, sPreamble,); + + # + # Prepare the URL parameters. + # + if WuiDispatcherBase.ksParamPageNo in dParams: # Forget about page No when changing a period + del dParams[WuiDispatcherBase.ksParamPageNo] + if not fKeepPageNo and WuiDispatcherBase.ksParamEffectiveDate in dParams: + tsEffectiveParam = dParams[WuiDispatcherBase.ksParamEffectiveDate]; + del dParams[WuiDispatcherBase.ksParamEffectiveDate]; + else: + tsEffectiveParam = '' + + # + # Generate the individual parts. + # + sNavigation += WuiListContentBase.generateTimeNavigationDateTime(sWhere, dParams, tsEffectiveAbs); + sNavigation += WuiListContentBase.generateTimeNavigationComboBox(sWhere, dParams, tsEffectiveParam); + + sNavigation += '%s</div>' % (sPostamble,); + return sNavigation; + + def _generateTimeNavigation(self, sWhere, sPreamble = '', sPostamble = ''): + """ + Returns HTML for time navigation. + + Note! Views without a need for a timescale just stubs this method. + """ + return self.generateTimeNavigation(sWhere, self._oDisp.getParameters(), self._oDisp.getEffectiveDateParam(), + sPreamble, sPostamble) + + @staticmethod + def generateItemPerPageSelector(sWhere, dParams, cCurItemsPerPage): + """ + Generate HTML code for items per page selector. + Note! Modifies dParams! + """ + + # Drop the current page count parameter. + if WuiDispatcherBase.ksParamItemsPerPage in dParams: + del dParams[WuiDispatcherBase.ksParamItemsPerPage]; + + # Remove the current page number. + if WuiDispatcherBase.ksParamPageNo in dParams: + del dParams[WuiDispatcherBase.ksParamPageNo]; + + sHtmlItemsPerPageSelector = '<form name="TmItemsPerPageForm-%s" method="GET" class="tmitemsperpage-%s tmitemsperpage">\n'\ + ' <select name="%s" onchange="window.location=\'?%s&%s=\' + ' \ + 'this.options[this.selectedIndex].value;" title="Max items per page">\n' \ + % (sWhere, WuiDispatcherBase.ksParamItemsPerPage, sWhere, + webutils.encodeUrlParams(dParams), + WuiDispatcherBase.ksParamItemsPerPage) + + acItemsPerPage = [16, 32, 64, 128, 256, 384, 512, 768, 1024, 1536, 2048, 3072, 4096]; + for cItemsPerPage in acItemsPerPage: + sHtmlItemsPerPageSelector += ' <option value="%d" %s>%d per page</option>\n' \ + % (cItemsPerPage, + 'selected="selected"' if cItemsPerPage == cCurItemsPerPage else '', + cItemsPerPage) + sHtmlItemsPerPageSelector += ' </select>\n' \ + '</form>\n'; + + return sHtmlItemsPerPageSelector + + + def _generateNavigation(self, sWhere): + """ + Return HTML for navigation. + """ + + # + # ASSUMES the dispatcher/controller code fetches one entry more than + # needed to fill the page to indicate further records. + # + sNavigation = '<div class="tmlistnav-%s">\n' % sWhere; + sNavigation += ' <table class="tmlistnavtab">\n' \ + ' <tr>\n'; + dParams = self._oDisp.getParameters(); + dParams[WuiDispatcherBase.ksParamItemsPerPage] = self._cItemsPerPage; + dParams[WuiDispatcherBase.ksParamPageNo] = self._iPage; + if self._tsEffectiveDate is not None: + dParams[WuiDispatcherBase.ksParamEffectiveDate] = self._tsEffectiveDate; + + # Prev + if self._iPage > 0: + dParams[WuiDispatcherBase.ksParamPageNo] = self._iPage - 1; + sNavigation += ' <td align="left"><a href="?%s">Previous</a></td>\n' % (webutils.encodeUrlParams(dParams),); + else: + sNavigation += ' <td></td>\n'; + + # Time scale. + if self._fTimeNavigation: + sNavigation += '<td align="center" class="tmtimenav">'; + sNavigation += self._generateTimeNavigation(sWhere); + sNavigation += '</td>'; + + # page count and next. + sNavigation += '<td align="right" class="tmnextanditemsperpage">\n'; + + if len(self._aoEntries) > self._cItemsPerPage: + dParams[WuiDispatcherBase.ksParamPageNo] = self._iPage + 1; + sNavigation += ' <a href="?%s">Next</a>\n' % (webutils.encodeUrlParams(dParams),); + sNavigation += self.generateItemPerPageSelector(sWhere, dParams, self._cItemsPerPage); + sNavigation += '</td>\n'; + sNavigation += ' </tr>\n' \ + ' </table>\n' \ + '</div>\n'; + return sNavigation; + + def _checkSortingByColumnAscending(self, aiColumns): + """ + Checks if we're sorting by this column. + + Returns 0 if not sorting by this, negative if descending, positive if ascending. The + value indicates the priority (nearer to 0 is higher). + """ + if len(aiColumns) <= len(self._aiSelectedSortColumns): + aiColumns = list(aiColumns); + aiNegColumns = list([-i for i in aiColumns]); # pylint: disable=consider-using-generator + i = 0; + while i + len(aiColumns) <= len(self._aiSelectedSortColumns): + aiSub = list(self._aiSelectedSortColumns[i : i + len(aiColumns)]); + if aiSub == aiColumns: + return 1 + i; + if aiSub == aiNegColumns: + return -1 - i; + i += 1; + return 0; + + def _generateTableHeaders(self): + """ + Generate table headers. + Returns raw html string. + Overridable. + """ + + sHtml = ' <thead class="tmheader"><tr>'; + for iHeader, oHeader in enumerate(self._asColumnHeaders): + if isinstance(oHeader, WuiHtmlBase): + sHtml += '<th>' + oHeader.toHtml() + '</th>'; + elif iHeader < len(self._aaiColumnSorting) and self._aaiColumnSorting[iHeader] is not None: + sHtml += '<th>' + iSorting = self._checkSortingByColumnAscending(self._aaiColumnSorting[iHeader]); + if iSorting > 0: + sDirection = ' ▴' if iSorting == 1 else '<small> ▵</small>'; + sSortParams = ','.join([str(-i) for i in self._aaiColumnSorting[iHeader]]); + else: + sDirection = ''; + if iSorting < 0: + sDirection = ' ▾' if iSorting == -1 else '<small> ▿</small>' + sSortParams = ','.join([str(i) for i in self._aaiColumnSorting[iHeader]]); + sHtml += '<a href="javascript:ahrefActionSortByColumns(\'%s\',[%s]);">' \ + % (WuiDispatcherBase.ksParamSortColumns, sSortParams); + sHtml += webutils.escapeElem(oHeader) + '</a>' + sDirection + '</th>'; + else: + sHtml += '<th>' + webutils.escapeElem(oHeader) + '</th>'; + sHtml += '</tr><thead>\n'; + return sHtml + + def _generateTable(self): + """ + show worker that just generates the table. + """ + + # + # Create a table. + # If no colum headers are provided, fall back on database field + # names, ASSUMING that the entries are ModelDataBase children. + # Note! the cellspacing is for IE8. + # + sPageBody = '<table class="tmtable" id="' + self._sId + '" cellspacing="0">\n'; + + if not self._asColumnHeaders: + self._asColumnHeaders = self._aoEntries[0].getDataAttributes(); + + sPageBody += self._generateTableHeaders(); + + # + # Format the body and close the table. + # + sPageBody += ' <tbody>\n'; + for iEntry in range(min(len(self._aoEntries), self._cItemsPerPage)): + sPageBody += self._formatListEntryHtml(iEntry); + sPageBody += ' </tbody>\n' \ + '</table>\n'; + return sPageBody; + + def _composeTitle(self): + """Composes the title string (return value).""" + sTitle = self._sTitle; + if self._iPage != 0: + sTitle += ' (page ' + unicode(self._iPage + 1) + ')' + if self._tsEffectiveDate is not None: + sTitle += ' as per ' + unicode(self.formatTsShort(self._tsEffectiveDate)); + return sTitle; + + + def show(self, fShowNavigation = True): + """ + Displays the list. + Returns (Title, HTML) on success, raises exception on error. + """ + + sPageBody = '' + if fShowNavigation: + sPageBody += self._generateNavigation('top'); + + if self._aoEntries: + sPageBody += self._generateTable(); + if fShowNavigation: + sPageBody += self._generateNavigation('bottom'); + else: + sPageBody += '<p>No entries.</p>' + + return (self._composeTitle(), sPageBody); + + +class WuiListContentWithActionBase(WuiListContentBase): + """ + Base for the list content with action classes. + """ + + def __init__(self, aoEntries, iPage, cItemsPerPage, tsEffectiveDate, sTitle, # pylint: disable=too-many-arguments + sId = None, fnDPrint = None, oDisp = None, aiSelectedSortColumns = None): + WuiListContentBase.__init__(self, aoEntries, iPage, cItemsPerPage, tsEffectiveDate, sTitle, sId = sId, + fnDPrint = fnDPrint, oDisp = oDisp, aiSelectedSortColumns = aiSelectedSortColumns); + self._aoActions = None; # List of [ oValue, sText, sHover ] provided by the child class. + self._sAction = None; # Set by the child class. + self._sCheckboxName = None; # Set by the child class. + self._asColumnHeaders = [ WuiRawHtml('<input type="checkbox" onClick="toggle%s(this)">' + % ('' if sId is None else sId)), ]; + self._asColumnAttribs = [ 'align="center"', ]; + self._aaiColumnSorting = [ None, ]; + + def _getCheckBoxColumn(self, iEntry, sValue): + """ + Used by _formatListEntry implementations, returns a WuiRawHtmlBase object. + """ + _ = iEntry; + return WuiRawHtml('<input type="checkbox" name="%s" value="%s">' + % (webutils.escapeAttr(self._sCheckboxName), webutils.escapeAttr(unicode(sValue)))); + + def show(self, fShowNavigation=True): + """ + Displays the list. + Returns (Title, HTML) on success, raises exception on error. + """ + assert self._aoActions is not None; + assert self._sAction is not None; + + sPageBody = '<script language="JavaScript">\n' \ + 'function toggle%s(oSource) {\n' \ + ' aoCheckboxes = document.getElementsByName(\'%s\');\n' \ + ' for(var i in aoCheckboxes)\n' \ + ' aoCheckboxes[i].checked = oSource.checked;\n' \ + '}\n' \ + '</script>\n' \ + % ('' if self._sId is None else self._sId, self._sCheckboxName,); + if fShowNavigation: + sPageBody += self._generateNavigation('top'); + if self._aoEntries: + + sPageBody += '<form action="?%s" method="post" class="tmlistactionform">\n' \ + % (webutils.encodeUrlParams({WuiDispatcherBase.ksParamAction: self._sAction,}),); + sPageBody += self._generateTable(); + + sPageBody += ' <label>Actions</label>\n' \ + ' <select name="%s" id="%s-action-combo" class="tmlistactionform-combo">\n' \ + % (webutils.escapeAttr(WuiDispatcherBase.ksParamListAction), webutils.escapeAttr(self._sId),); + for oValue, sText, _ in self._aoActions: + sPageBody += ' <option value="%s">%s</option>\n' \ + % (webutils.escapeAttr(unicode(oValue)), webutils.escapeElem(sText), ); + sPageBody += ' </select>\n'; + sPageBody += ' <input type="submit"></input>\n'; + sPageBody += '</form>\n'; + if fShowNavigation: + sPageBody += self._generateNavigation('bottom'); + else: + sPageBody += '<p>No entries.</p>' + + return (self._composeTitle(), sPageBody); + diff --git a/src/VBox/ValidationKit/testmanager/webui/wuigraphwiz.py b/src/VBox/ValidationKit/testmanager/webui/wuigraphwiz.py new file mode 100755 index 00000000..040b3217 --- /dev/null +++ b/src/VBox/ValidationKit/testmanager/webui/wuigraphwiz.py @@ -0,0 +1,660 @@ +# -*- coding: utf-8 -*- +# $Id: wuigraphwiz.py $ + +""" +Test Manager WUI - Graph Wizard +""" + +__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 $" + +# Python imports. +import functools; + +# Validation Kit imports. +from testmanager.webui.wuimain import WuiMain; +from testmanager.webui.wuihlpgraph import WuiHlpLineGraphErrorbarY, WuiHlpGraphDataTableEx; +from testmanager.webui.wuireport import WuiReportBase; + +from common import utils, webutils; +from common import constants; + + +class WuiGraphWiz(WuiReportBase): + """Construct a graph for analyzing test results (values) across builds and testboxes.""" + + ## @name Series name parts. + ## @{ + kfSeriesName_TestBox = 1; + kfSeriesName_Product = 2; + kfSeriesName_Branch = 4; + kfSeriesName_BuildType = 8; + kfSeriesName_OsArchs = 16; + kfSeriesName_TestCase = 32; + kfSeriesName_TestCaseArgs = 64; + kfSeriesName_All = 127; + ## @} + + + def __init__(self, oModel, dParams, fSubReport = False, fnDPrint = None, oDisp = None): + WuiReportBase.__init__(self, oModel, dParams, fSubReport = fSubReport, fnDPrint = fnDPrint, oDisp = oDisp); + + # Select graph implementation. + if dParams[WuiMain.ksParamGraphWizImpl] == 'charts': + from testmanager.webui.wuihlpgraphgooglechart import WuiHlpLineGraphErrorbarY as MyGraph; + self.oGraphClass = MyGraph; + elif dParams[WuiMain.ksParamGraphWizImpl] == 'matplotlib': + from testmanager.webui.wuihlpgraphmatplotlib import WuiHlpLineGraphErrorbarY as MyGraph; + self.oGraphClass = MyGraph; + else: + self.oGraphClass = WuiHlpLineGraphErrorbarY; + + + # + def _figureSeriesNameBits(self, aoSeries): + """ Figures out the method (bitmask) to use when naming series. """ + if len(aoSeries) <= 1: + return WuiGraphWiz.kfSeriesName_TestBox; + + # Start with all and drop unnecessary specs one-by-one. + fRet = WuiGraphWiz.kfSeriesName_All; + + if [oSrs.idTestBox for oSrs in aoSeries].count(aoSeries[0].idTestBox) == len(aoSeries): + fRet &= ~WuiGraphWiz.kfSeriesName_TestBox; + + if [oSrs.idBuildCategory for oSrs in aoSeries].count(aoSeries[0].idBuildCategory) == len(aoSeries): + fRet &= ~WuiGraphWiz.kfSeriesName_Product; + fRet &= ~WuiGraphWiz.kfSeriesName_Branch; + fRet &= ~WuiGraphWiz.kfSeriesName_BuildType; + fRet &= ~WuiGraphWiz.kfSeriesName_OsArchs; + else: + if [oSrs.oBuildCategory.sProduct for oSrs in aoSeries].count(aoSeries[0].oBuildCategory.sProduct) == len(aoSeries): + fRet &= ~WuiGraphWiz.kfSeriesName_Product; + if [oSrs.oBuildCategory.sBranch for oSrs in aoSeries].count(aoSeries[0].oBuildCategory.sBranch) == len(aoSeries): + fRet &= ~WuiGraphWiz.kfSeriesName_Branch; + if [oSrs.oBuildCategory.sType for oSrs in aoSeries].count(aoSeries[0].oBuildCategory.sType) == len(aoSeries): + fRet &= ~WuiGraphWiz.kfSeriesName_BuildType; + + # Complicated. + fRet &= ~WuiGraphWiz.kfSeriesName_OsArchs; + daTestBoxes = {}; + for oSeries in aoSeries: + if oSeries.idTestBox in daTestBoxes: + daTestBoxes[oSeries.idTestBox].append(oSeries); + else: + daTestBoxes[oSeries.idTestBox] = [oSeries,]; + for aoSeriesPerTestBox in daTestBoxes.values(): + if len(aoSeriesPerTestBox) >= 0: + asOsArches = aoSeriesPerTestBox[0].oBuildCategory.asOsArches; + for i in range(1, len(aoSeriesPerTestBox)): + if aoSeriesPerTestBox[i].oBuildCategory.asOsArches != asOsArches: + fRet |= WuiGraphWiz.kfSeriesName_OsArchs; + break; + + if aoSeries[0].oTestCaseArgs is None: + fRet &= ~WuiGraphWiz.kfSeriesName_TestCaseArgs; + if [oSrs.idTestCase for oSrs in aoSeries].count(aoSeries[0].idTestCase) == len(aoSeries): + fRet &= ~WuiGraphWiz.kfSeriesName_TestCase; + else: + fRet &= ~WuiGraphWiz.kfSeriesName_TestCase; + if [oSrs.idTestCaseArgs for oSrs in aoSeries].count(aoSeries[0].idTestCaseArgs) == len(aoSeries): + fRet &= ~WuiGraphWiz.kfSeriesName_TestCaseArgs; + + return fRet; + + def _getSeriesNameFromBits(self, oSeries, fBits): + """ Creates a series name from bits (kfSeriesName_xxx). """ + assert fBits != 0; + sName = ''; + + if fBits & WuiGraphWiz.kfSeriesName_Product: + if sName: sName += ' / '; + sName += oSeries.oBuildCategory.sProduct; + + if fBits & WuiGraphWiz.kfSeriesName_Branch: + if sName: sName += ' / '; + sName += oSeries.oBuildCategory.sBranch; + + if fBits & WuiGraphWiz.kfSeriesName_BuildType: + if sName: sName += ' / '; + sName += oSeries.oBuildCategory.sType; + + if fBits & WuiGraphWiz.kfSeriesName_OsArchs: + if sName: sName += ' / '; + sName += ' & '.join(oSeries.oBuildCategory.asOsArches); + + if fBits & WuiGraphWiz.kfSeriesName_TestCaseArgs: + if sName: sName += ' / '; + if oSeries.idTestCaseArgs is not None: + sName += oSeries.oTestCase.sName + ':#' + str(oSeries.idTestCaseArgs); + else: + sName += oSeries.oTestCase.sName; + elif fBits & WuiGraphWiz.kfSeriesName_TestCase: + if sName: sName += ' / '; + sName += oSeries.oTestCase.sName; + + if fBits & WuiGraphWiz.kfSeriesName_TestBox: + if sName: sName += ' / '; + sName += oSeries.oTestBox.sName; + + return sName; + + def _calcGraphName(self, oSeries, fSeriesName, sSampleName): + """ Constructs a name for the graph. """ + fGraphName = ~fSeriesName & ( WuiGraphWiz.kfSeriesName_TestBox + | WuiGraphWiz.kfSeriesName_Product + | WuiGraphWiz.kfSeriesName_Branch + | WuiGraphWiz.kfSeriesName_BuildType + ); + sName = self._getSeriesNameFromBits(oSeries, fGraphName); + if sName: sName += ' - '; + sName += sSampleName; + return sName; + + def _calcSampleName(self, oCollection): + """ Constructs a name for a sample source (collection). """ + if oCollection.sValue is not None: + asSampleName = [oCollection.sValue, 'in',]; + elif oCollection.sType == self._oModel.ksTypeElapsed: + asSampleName = ['Elapsed time', 'for', ]; + elif oCollection.sType == self._oModel.ksTypeResult: + asSampleName = ['Error count', 'for',]; + else: + return 'Invalid collection type: "%s"' % (oCollection.sType,); + + sTestName = ', '.join(oCollection.asTests if oCollection.asTests[0] else oCollection.asTests[1:]); + if sTestName == '': + # Use the testcase name if there is only one for all series. + if not oCollection.aoSeries: + return asSampleName[0]; + if len(oCollection.aoSeries) > 1: + idTestCase = oCollection.aoSeries[0].idTestCase; + for oSeries in oCollection.aoSeries: + if oSeries.idTestCase != idTestCase: + return asSampleName[0]; + sTestName = oCollection.aoSeries[0].oTestCase.sName; + return ' '.join(asSampleName) + ' ' + sTestName; + + + def _splitSeries(self, aoSeries): + """ + Splits the data series (ReportGraphModel.DataSeries) into one or more graphs. + + Returns an array of data series arrays. + """ + # Must be at least two series for something to be splittable. + if len(aoSeries) <= 1: + if not aoSeries: + return []; + return [aoSeries,]; + + # Split on unit. + dUnitSeries = {}; + for oSeries in aoSeries: + if oSeries.iUnit not in dUnitSeries: + dUnitSeries[oSeries.iUnit] = []; + dUnitSeries[oSeries.iUnit].append(oSeries); + + # Sort the per-unit series since the build category was only sorted by ID. + for iUnit in dUnitSeries: + def mycmp(oSelf, oOther): + """ __cmp__ like function. """ + iCmp = utils.stricmp(oSelf.oBuildCategory.sProduct, oOther.oBuildCategory.sProduct); + if iCmp != 0: + return iCmp; + iCmp = utils.stricmp(oSelf.oBuildCategory.sBranch, oOther.oBuildCategory.sBranch); + if iCmp != 0: + return iCmp; + iCmp = utils.stricmp(oSelf.oBuildCategory.sType, oOther.oBuildCategory.sType); + if iCmp != 0: + return iCmp; + iCmp = utils.stricmp(oSelf.oTestBox.sName, oOther.oTestBox.sName); + if iCmp != 0: + return iCmp; + return 0; + dUnitSeries[iUnit] = sorted(dUnitSeries[iUnit], key = functools.cmp_to_key(mycmp)); + + # Split the per-unit series up if necessary. + cMaxPerGraph = self._dParams[WuiMain.ksParamGraphWizMaxPerGraph]; + aaoRet = []; + for aoUnitSeries in dUnitSeries.values(): + while len(aoUnitSeries) > cMaxPerGraph: + aaoRet.append(aoUnitSeries[:cMaxPerGraph]); + aoUnitSeries = aoUnitSeries[cMaxPerGraph:]; + if aoUnitSeries: + aaoRet.append(aoUnitSeries); + + return aaoRet; + + def _configureGraph(self, oGraph): + """ + Configures oGraph according to user parameters and other config settings. + + Returns oGraph. + """ + oGraph.setWidth(self._dParams[WuiMain.ksParamGraphWizWidth]) + oGraph.setHeight(self._dParams[WuiMain.ksParamGraphWizHeight]) + oGraph.setDpi(self._dParams[WuiMain.ksParamGraphWizDpi]) + oGraph.setErrorBarY(self._dParams[WuiMain.ksParamGraphWizErrorBarY]); + oGraph.setFontSize(self._dParams[WuiMain.ksParamGraphWizFontSize]); + if hasattr(oGraph, 'setXkcdStyle'): + oGraph.setXkcdStyle(self._dParams[WuiMain.ksParamGraphWizXkcdStyle]); + + return oGraph; + + def _generateInteractiveForm(self): + """ + Generates the HTML for the interactive form. + Returns (sTopOfForm, sEndOfForm) + """ + + # + # The top of the form. + # + sTop = '<form action="#" method="get" id="graphwiz-form">\n' \ + ' <input type="hidden" name="%s" value="%s"/>\n' \ + ' <input type="hidden" name="%s" value="%u"/>\n' \ + % ( WuiMain.ksParamAction, WuiMain.ksActionGraphWiz, + WuiMain.ksParamGraphWizSrcTestSetId, self._dParams[WuiMain.ksParamGraphWizSrcTestSetId], + ); + + sTop += ' <div id="graphwiz-nav">\n'; + sTop += ' <script type="text/javascript">\n' \ + ' window.onresize = function(){ return graphwizOnResizeRecalcWidth("graphwiz-nav", "%s"); }\n' \ + ' window.onload = function(){ return graphwizOnLoadRememberWidth("graphwiz-nav"); }\n' \ + ' </script>\n' \ + % ( WuiMain.ksParamGraphWizWidth, ); + + # + # Top: First row. + # + sTop += ' <div id="graphwiz-top-1">\n'; + + # time. + sNow = self._dParams[WuiMain.ksParamEffectiveDate]; + if sNow is None: sNow = ''; + sTop += ' <div id="graphwiz-time">\n'; + sTop += ' <label for="%s">Starting:</label>\n' \ + ' <input type="text" name="%s" id="%s" value="%s" class="graphwiz-time-input"/>\n' \ + % ( WuiMain.ksParamEffectiveDate, + WuiMain.ksParamEffectiveDate, WuiMain.ksParamEffectiveDate, sNow, ); + + sTop += ' <input type="hidden" name="%s" value="%u"/>\n' % ( WuiMain.ksParamReportPeriods, 1, ); + sTop += ' <label for="%s"> Going back:\n' \ + ' <input type="text" name="%s" id="%s" value="%s" class="graphwiz-period-input"/>\n' \ + % ( WuiMain.ksParamReportPeriodInHours, + WuiMain.ksParamReportPeriodInHours, WuiMain.ksParamReportPeriodInHours, + utils.formatIntervalHours(self._dParams[WuiMain.ksParamReportPeriodInHours]) ); + sTop += ' </div>\n'; + + # Graph options top row. + sTop += ' <div id="graphwiz-top-options-1">\n'; + + # graph type. + sTop += ' <label for="%s">Graph:</label>\n' \ + ' <select name="%s" id="%s">\n' \ + % ( WuiMain.ksParamGraphWizImpl, WuiMain.ksParamGraphWizImpl, WuiMain.ksParamGraphWizImpl, ); + for (sImpl, sDesc) in WuiMain.kaasGraphWizImplCombo: + sTop += ' <option value="%s"%s>%s</option>\n' \ + % (sImpl, ' selected' if sImpl == self._dParams[WuiMain.ksParamGraphWizImpl] else '', sDesc); + sTop += ' </select>\n'; + + # graph size. + sTop += ' <label for="%s">Graph size:</label>\n' \ + ' <input type="text" name="%s" id="%s" value="%s" class="graphwiz-pixel-input"> x\n' \ + ' <input type="text" name="%s" id="%s" value="%s" class="graphwiz-pixel-input">\n' \ + ' <label for="%s">Dpi:</label>'\ + ' <input type="text" name="%s" id="%s" value="%s" class="graphwiz-dpi-input">\n' \ + ' <button type="button" onclick="%s">Defaults</button>\n' \ + % ( WuiMain.ksParamGraphWizWidth, + WuiMain.ksParamGraphWizWidth, WuiMain.ksParamGraphWizWidth, self._dParams[WuiMain.ksParamGraphWizWidth], + WuiMain.ksParamGraphWizHeight, WuiMain.ksParamGraphWizHeight, self._dParams[WuiMain.ksParamGraphWizHeight], + WuiMain.ksParamGraphWizDpi, + WuiMain.ksParamGraphWizDpi, WuiMain.ksParamGraphWizDpi, self._dParams[WuiMain.ksParamGraphWizDpi], + webutils.escapeAttr('return graphwizSetDefaultSizeValues("graphwiz-nav", "%s", "%s", "%s");' + % ( WuiMain.ksParamGraphWizWidth, WuiMain.ksParamGraphWizHeight, + WuiMain.ksParamGraphWizDpi )), + ); + + sTop += ' </div>\n'; # (options row 1) + + sTop += ' </div>\n'; # (end of row 1) + + # + # Top: Second row. + # + sTop += ' <div id="graphwiz-top-2">\n'; + + # Submit + sFormButton = '<button type="submit">Refresh</button>\n'; + sTop += ' <div id="graphwiz-top-submit">' + sFormButton + '</div>\n'; + + + # Options. + sTop += ' <div id="graphwiz-top-options-2">\n'; + + sTop += ' <input type="checkbox" name="%s" id="%s" value="1"%s/>\n' \ + ' <label for="%s">Tabular data</label>\n' \ + % ( WuiMain.ksParamGraphWizTabular, WuiMain.ksParamGraphWizTabular, + ' checked' if self._dParams[WuiMain.ksParamGraphWizTabular] else '', + WuiMain.ksParamGraphWizTabular); + + if hasattr(self.oGraphClass, 'setXkcdStyle'): + sTop += ' <input type="checkbox" name="%s" id="%s" value="1"%s/>\n' \ + ' <label for="%s">xkcd-style</label>\n' \ + % ( WuiMain.ksParamGraphWizXkcdStyle, WuiMain.ksParamGraphWizXkcdStyle, + ' checked' if self._dParams[WuiMain.ksParamGraphWizXkcdStyle] else '', + WuiMain.ksParamGraphWizXkcdStyle); + elif self._dParams[WuiMain.ksParamGraphWizXkcdStyle]: + sTop += ' <input type="hidden" name="%s" id="%s" value="1"/>\n' \ + % ( WuiMain.ksParamGraphWizXkcdStyle, WuiMain.ksParamGraphWizXkcdStyle, ); + + if not hasattr(self.oGraphClass, 'kfNoErrorBarsSupport'): + sTop += ' <input type="checkbox" name="%s" id="%s" value="1"%s title="%s"/>\n' \ + ' <label for="%s">Error bars,</label>\n' \ + ' <label for="%s">max: </label>\n' \ + ' <input type="text" name="%s" id="%s" value="%s" class="graphwiz-maxerrorbar-input" title="%s"/>\n' \ + % ( WuiMain.ksParamGraphWizErrorBarY, WuiMain.ksParamGraphWizErrorBarY, + ' checked' if self._dParams[WuiMain.ksParamGraphWizErrorBarY] else '', + 'Error bars shows some of the max and min results on the Y-axis.', + WuiMain.ksParamGraphWizErrorBarY, + WuiMain.ksParamGraphWizMaxErrorBarY, + WuiMain.ksParamGraphWizMaxErrorBarY, WuiMain.ksParamGraphWizMaxErrorBarY, + self._dParams[WuiMain.ksParamGraphWizMaxErrorBarY], + 'Maximum number of Y-axis error bar per graph. (Too many makes it unreadable.)' + ); + else: + if self._dParams[WuiMain.ksParamGraphWizErrorBarY]: + sTop += '<input type="hidden" name="%s" id="%s" value="1">\n' \ + % ( WuiMain.ksParamGraphWizErrorBarY, WuiMain.ksParamGraphWizErrorBarY, ); + sTop += '<input type="hidden" name="%s" id="%s" value="%u">\n' \ + % ( WuiMain.ksParamGraphWizMaxErrorBarY, WuiMain.ksParamGraphWizMaxErrorBarY, + self._dParams[WuiMain.ksParamGraphWizMaxErrorBarY], ); + + sTop += ' <label for="%s">Font size: </label>\n' \ + ' <input type="text" name="%s" id="%s" value="%s" class="graphwiz-fontsize-input"/>\n' \ + % ( WuiMain.ksParamGraphWizFontSize, + WuiMain.ksParamGraphWizFontSize, WuiMain.ksParamGraphWizFontSize, + self._dParams[WuiMain.ksParamGraphWizFontSize], ); + + sTop += ' <label for="%s">Data series: </label>\n' \ + ' <input type="text" name="%s" id="%s" value="%s" class="graphwiz-maxpergraph-input" title="%s"/>\n' \ + % ( WuiMain.ksParamGraphWizMaxPerGraph, + WuiMain.ksParamGraphWizMaxPerGraph, WuiMain.ksParamGraphWizMaxPerGraph, + self._dParams[WuiMain.ksParamGraphWizMaxPerGraph], + 'Max data series per graph.' ); + + sTop += ' </div>\n'; # (options row 2) + + sTop += ' </div>\n'; # (end of row 2) + + sTop += ' </div>\n'; # end of top. + + # + # The end of the page selection. + # + sEnd = ' <div id="graphwiz-end-selection">\n'; + + # + # Testbox selection + # + aidTestBoxes = list(self._dParams[WuiMain.ksParamGraphWizTestBoxIds]); + sEnd += ' <div id="graphwiz-testboxes" class="graphwiz-end-selection-group">\n' \ + ' <h3>TestBox Selection:</h3>\n' \ + ' <ol class="tmgraph-testboxes">\n'; + + # Get a list of eligible testboxes from the DB. + for oTestBox in self._oModel.getEligibleTestBoxes(): + try: aidTestBoxes.remove(oTestBox.idTestBox); + except: sChecked = ''; + else: sChecked = ' checked'; + sEnd += ' <li><input type="checkbox" name="%s" value="%s" id="gw-tb-%u"%s/>' \ + '<label for="gw-tb-%u">%s</label></li>\n' \ + % ( WuiMain.ksParamGraphWizTestBoxIds, oTestBox.idTestBox, oTestBox.idTestBox, sChecked, + oTestBox.idTestBox, oTestBox.sName); + + # List testboxes that have been checked in a different period or something. + for idTestBox in aidTestBoxes: + oTestBox = self._oModel.oCache.getTestBox(idTestBox); + sEnd += ' <li><input type="checkbox" name="%s" value="%s" id="gw-tb-%u" checked/>' \ + '<label for="gw-tb-%u">%s</label></li>\n' \ + % ( WuiMain.ksParamGraphWizTestBoxIds, oTestBox.idTestBox, oTestBox.idTestBox, + oTestBox.idTestBox, oTestBox.sName); + + sEnd += ' </ol>\n' \ + ' </div>\n'; + + # + # Build category selection. + # + aidBuildCategories = list(self._dParams[WuiMain.ksParamGraphWizBuildCatIds]); + sEnd += ' <div id="graphwiz-buildcategories" class="graphwiz-end-selection-group">\n' \ + ' <h3>Build Category Selection:</h3>\n' \ + ' <ol class="tmgraph-buildcategories">\n'; + for oBuildCat in self._oModel.getEligibleBuildCategories(): + try: aidBuildCategories.remove(oBuildCat.idBuildCategory); + except: sChecked = ''; + else: sChecked = ' checked'; + sEnd += ' <li><input type="checkbox" name="%s" value="%s" id="gw-bc-%u" %s/>' \ + '<label for="gw-bc-%u">%s / %s / %s / %s</label></li>\n' \ + % ( WuiMain.ksParamGraphWizBuildCatIds, oBuildCat.idBuildCategory, oBuildCat.idBuildCategory, sChecked, + oBuildCat.idBuildCategory, + oBuildCat.sProduct, oBuildCat.sBranch, oBuildCat.sType, ' & '.join(oBuildCat.asOsArches) ); + assert not aidBuildCategories; # SQL should return all currently selected. + + sEnd += ' </ol>\n' \ + ' </div>\n'; + + # + # Testcase variations. + # + sEnd += ' <div id="graphwiz-testcase-variations" class="graphwiz-end-selection-group">\n' \ + ' <h3>Miscellaneous:</h3>\n' \ + ' <ol>'; + + sEnd += ' <li>\n' \ + ' <input type="checkbox" id="%s" name="%s" value="1"%s/>\n' \ + ' <label for="%s">Separate by testcase variation.</label>\n' \ + ' </li>\n' \ + % ( WuiMain.ksParamGraphWizSepTestVars, WuiMain.ksParamGraphWizSepTestVars, + ' checked' if self._dParams[WuiMain.ksParamGraphWizSepTestVars] else '', + WuiMain.ksParamGraphWizSepTestVars ); + + + sEnd += ' <li>\n' \ + ' <lable for="%s">Test case ID:</label>\n' \ + ' <input type="text" id="%s" name="%s" value="%s" readonly/>\n' \ + ' </li>\n' \ + % ( WuiMain.ksParamGraphWizTestCaseIds, + WuiMain.ksParamGraphWizTestCaseIds, WuiMain.ksParamGraphWizTestCaseIds, + ','.join([str(i) for i in self._dParams[WuiMain.ksParamGraphWizTestCaseIds]]), ); + + sEnd += ' </ol>\n' \ + ' </div>\n'; + + #sEnd += ' <h3> </h3>\n'; + + # + # Finish up the form. + # + sEnd += ' <div id="graphwiz-end-submit"><p>' + sFormButton + '</p></div>\n'; + sEnd += ' </div>\n' \ + '</form>\n'; + + return (sTop, sEnd); + + def generateReportBody(self): + fInteractive = not self._fSubReport; + + # Quick mockup. + self._sTitle = 'Graph Wizzard'; + + sHtml = ''; + sHtml += '<h2>Incomplete code - no complaints yet, thank you!!</h2>\n'; + + # + # Create a form for altering the data we're working with. + # + if fInteractive: + (sTopOfForm, sEndOfForm) = self._generateInteractiveForm(); + sHtml += sTopOfForm; + del sTopOfForm; + + # + # Emit the graphs. At least one per sample source. + # + sHtml += ' <div id="graphwiz-graphs">\n'; + iGraph = 0; + aoCollections = self._oModel.fetchGraphData(); + for iCollection, oCollection in enumerate(aoCollections): + # Name the graph and add a checkbox for removing it. + sSampleName = self._calcSampleName(oCollection); + sHtml += ' <div class="graphwiz-collection" id="graphwiz-source-%u">\n' % (iCollection,); + if fInteractive: + sHtml += ' <div class="graphwiz-src-select">\n' \ + ' <input type="checkbox" name="%s" id="%s" value="%s:%s%s" checked class="graphwiz-src-input">\n' \ + ' <label for="%s">%s</label>\n' \ + ' </div>\n' \ + % ( WuiMain.ksParamReportSubjectIds, WuiMain.ksParamReportSubjectIds, oCollection.sType, + ':'.join([str(idStr) for idStr in oCollection.aidStrTests]), + ':%u' % oCollection.idStrValue if oCollection.idStrValue else '', + WuiMain.ksParamReportSubjectIds, sSampleName ); + + if oCollection.aoSeries: + # + # Split the series into sub-graphs as needed and produce SVGs. + # + aaoSeries = self._splitSeries(oCollection.aoSeries); + for aoSeries in aaoSeries: + # Gather the data for this graph. (Most big stuff is passed by + # reference, so there shouldn't be any large memory penalty for + # repacking the data here.) + sYUnit = None; + if aoSeries[0].iUnit < len(constants.valueunit.g_asNames) and aoSeries[0].iUnit > 0: + sYUnit = constants.valueunit.g_asNames[aoSeries[0].iUnit]; + oData = WuiHlpGraphDataTableEx(sXUnit = 'Build revision', sYUnit = sYUnit); + + fSeriesName = self._figureSeriesNameBits(aoSeries); + for oSeries in aoSeries: + sSeriesName = self._getSeriesNameFromBits(oSeries, fSeriesName); + asHtmlTooltips = None; + if len(oSeries.aoRevInfo) == len(oSeries.aiRevisions): + asHtmlTooltips = []; + for i, oRevInfo in enumerate(oSeries.aoRevInfo): + sPlusMinus = ''; + if oSeries.acSamples[i] > 1: + sPlusMinus = ' (+%s/-%s; %u samples)' \ + % ( utils.formatNumber(oSeries.aiErrorBarAbove[i]), + utils.formatNumber(oSeries.aiErrorBarBelow[i]), + oSeries.acSamples[i]) + sTooltip = '<table class=\'graphwiz-tt\'><tr><td>%s:</td><td>%s %s %s</td></tr>'\ + '<tr><td>Rev:</td><td>r%s</td></tr>' \ + % ( sSeriesName, + utils.formatNumber(oSeries.aiValues[i]), + sYUnit, sPlusMinus, + oSeries.aiRevisions[i], + ); + if oRevInfo.sAuthor is not None: + sMsg = oRevInfo.sMessage[:80].strip(); + #if sMsg.find('\n') >= 0: + # sMsg = sMsg[:sMsg.find('\n')].strip(); + sTooltip += '<tr><td>Author:</td><td>%s</td></tr>' \ + '<tr><td>Date:</td><td>%s</td><tr>' \ + '<tr><td>Message:</td><td>%s%s</td></tr>' \ + % ( oRevInfo.sAuthor, + self.formatTsShort(oRevInfo.tsCreated), + sMsg, '...' if len(oRevInfo.sMessage) > len(sMsg) else ''); + sTooltip += '</table>'; + asHtmlTooltips.append(sTooltip); + oData.addDataSeries(sSeriesName, oSeries.aiRevisions, oSeries.aiValues, asHtmlTooltips, + oSeries.aiErrorBarBelow, oSeries.aiErrorBarAbove); + # Render the data into a graph. + oGraph = self.oGraphClass('tmgraph-%u' % (iGraph,), oData, self._oDisp); + self._configureGraph(oGraph); + + oGraph.setTitle(self._calcGraphName(aoSeries[0], fSeriesName, sSampleName)); + sHtml += ' <div class="graphwiz-graph" id="graphwiz-graph-%u">\n' % (iGraph,); + sHtml += oGraph.renderGraph(); + sHtml += '\n </div>\n'; + iGraph += 1; + + # + # Emit raw tabular data if requested. + # + if self._dParams[WuiMain.ksParamGraphWizTabular]: + sHtml += ' <div class="graphwiz-tab-div" id="graphwiz-tab-%u">\n' \ + ' <table class="tmtable graphwiz-tab">\n' \ + % (iCollection, ); + for aoSeries in aaoSeries: + if aoSeries[0].iUnit < len(constants.valueunit.g_asNames) and aoSeries[0].iUnit > 0: + sUnit = constants.valueunit.g_asNames[aoSeries[0].iUnit]; + else: + sUnit = str(aoSeries[0].iUnit); + + for iSeries, oSeries in enumerate(aoSeries): + sColor = self.oGraphClass.calcSeriesColor(iSeries); + + sHtml += '<thead class="tmheader">\n' \ + ' <tr class="graphwiz-tab graphwiz-tab-new-series-row">\n' \ + ' <th colspan="5"><span style="background-color:%s;"> </span> %s</th>\n' \ + ' </tr>\n' \ + ' <tr class="graphwiz-tab graphwiz-tab-col-hdr-row">\n' \ + ' <th>Revision</th><th>Value (%s)</th><th>Δmax</th><th>Δmin</th>' \ + '<th>Samples</th>\n' \ + ' </tr>\n' \ + '</thead>\n' \ + % ( sColor, + self._getSeriesNameFromBits(oSeries, self.kfSeriesName_All & ~self.kfSeriesName_OsArchs), + sUnit ); + + for i, iRevision in enumerate(oSeries.aiRevisions): + sHtml += ' <tr class="%s"><td>r%s</td><td>%s</td><td>+%s</td><td>-%s</td><td>%s</td></tr>\n' \ + % ( 'tmodd' if i & 1 else 'tmeven', + iRevision, oSeries.aiValues[i], + oSeries.aiErrorBarAbove[i], oSeries.aiErrorBarBelow[i], + oSeries.acSamples[i]); + sHtml += ' </table>\n' \ + ' </div>\n'; + else: + sHtml += '<i>No results.</i>\n'; + sHtml += ' </div>\n' + sHtml += ' </div>\n'; + + # + # Finish the form. + # + if fInteractive: + sHtml += sEndOfForm; + + return sHtml; + diff --git a/src/VBox/ValidationKit/testmanager/webui/wuihlpform.py b/src/VBox/ValidationKit/testmanager/webui/wuihlpform.py new file mode 100755 index 00000000..38015e83 --- /dev/null +++ b/src/VBox/ValidationKit/testmanager/webui/wuihlpform.py @@ -0,0 +1,1111 @@ +# -*- coding: utf-8 -*- +# $Id: wuihlpform.py $ + +""" +Test Manager Web-UI - Form Helpers. +""" + +__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 copy; +import sys; + +# Validation Kit imports. +from common import utils; +from common.webutils import escapeAttr, escapeElem; +from testmanager import config; +from testmanager.core.schedgroup import SchedGroupMemberData, SchedGroupDataEx; +from testmanager.core.testcaseargs import TestCaseArgsData; +from testmanager.core.testgroup import TestGroupMemberData, TestGroupDataEx; +from testmanager.core.testbox import TestBoxDataForSchedGroup; + +# Python 3 hacks: +if sys.version_info[0] >= 3: + unicode = str; # pylint: disable=redefined-builtin,invalid-name + + +class WuiHlpForm(object): + """ + Helper for constructing a form. + """ + + ksItemsList = 'ksItemsList' + + ksOnSubmit_AddReturnToFieldWithCurrentUrl = '+AddReturnToFieldWithCurrentUrl+'; + + def __init__(self, sId, sAction, dErrors = None, fReadOnly = False, sOnSubmit = None): + self._fFinalized = False; + self._fReadOnly = fReadOnly; + self._dErrors = dErrors if dErrors is not None else {}; + + if sOnSubmit == self.ksOnSubmit_AddReturnToFieldWithCurrentUrl: + sOnSubmit = u'return addRedirectToInputFieldWithCurrentUrl(this)'; + if sOnSubmit is None: sOnSubmit = u''; + else: sOnSubmit = u' onsubmit=\"%s\"' % (escapeAttr(sOnSubmit),); + + self._sBody = u'\n' \ + u'<div id="%s" class="tmform">\n' \ + u' <form action="%s" method="post"%s>\n' \ + u' <ul>\n' \ + % (sId, sAction, sOnSubmit); + + def _add(self, sText): + """Internal worker for appending text to the body.""" + assert not self._fFinalized; + if not self._fFinalized: + self._sBody += utils.toUnicode(sText, errors='ignore'); + return True; + return False; + + def _escapeErrorText(self, sText): + """Escapes error text, preserving some predefined HTML tags.""" + if sText.find('<br>') >= 0: + asParts = sText.split('<br>'); + for i, _ in enumerate(asParts): + asParts[i] = escapeElem(asParts[i].strip()); + sText = '<br>\n'.join(asParts); + else: + sText = escapeElem(sText); + return sText; + + def _addLabel(self, sName, sLabel, sDivSubClass = 'normal'): + """Internal worker for adding a label.""" + if sName in self._dErrors: + sError = self._dErrors[sName]; + if utils.isString(sError): # List error trick (it's an associative array). + return self._add(u' <li>\n' + u' <div class="tmform-field"><div class="tmform-field-%s">\n' + u' <label for="%s" class="tmform-error-label">%s\n' + u' <span class="tmform-error-desc">%s</span>\n' + u' </label>\n' + % (escapeAttr(sDivSubClass), escapeAttr(sName), escapeElem(sLabel), + self._escapeErrorText(sError), ) ); + return self._add(u' <li>\n' + u' <div class="tmform-field"><div class="tmform-field-%s">\n' + u' <label for="%s">%s</label>\n' + % (escapeAttr(sDivSubClass), escapeAttr(sName), escapeElem(sLabel)) ); + + + def finalize(self): + """ + Finalizes the form and returns the body. + """ + if not self._fFinalized: + self._add(u' </ul>\n' + u' </form>\n' + u'</div>\n' + u'<div class="clear"></div>\n' ); + return self._sBody; + + def addTextHidden(self, sName, sValue, sExtraAttribs = ''): + """Adds a hidden text input.""" + return self._add(u' <div class="tmform-field-hidden">\n' + u' <input name="%s" id="%s" type="text" hidden%s value="%s" class="tmform-hidden">\n' + u' </div>\n' + u' </li>\n' + % ( escapeAttr(sName), escapeAttr(sName), sExtraAttribs, escapeElem(str(sValue)) )); + # + # Non-input stuff. + # + def addNonText(self, sValue, sLabel, sName = 'non-text', sPostHtml = ''): + """Adds a read-only text input.""" + self._addLabel(sName, sLabel, 'string'); + if sValue is None: sValue = ''; + return self._add(u' <p>%s%s</p>\n' + u' </div></div>\n' + u' </li>\n' + % (escapeElem(unicode(sValue)), sPostHtml )); + + def addRawHtml(self, sRawHtml, sLabel, sName = 'raw-html'): + """Adds a read-only text input.""" + self._addLabel(sName, sLabel, 'string'); + self._add(sRawHtml); + return self._add(u' </div></div>\n' + u' </li>\n'); + + + # + # Text input fields. + # + def addText(self, sName, sValue, sLabel, sSubClass = 'string', sExtraAttribs = '', sPostHtml = ''): + """Adds a text input.""" + if self._fReadOnly: + return self.addTextRO(sName, sValue, sLabel, sSubClass, sExtraAttribs); + if sSubClass not in ('int', 'long', 'string', 'uuid', 'timestamp', 'wide'): raise Exception(sSubClass); + self._addLabel(sName, sLabel, sSubClass); + if sValue is None: sValue = ''; + return self._add(u' <input name="%s" id="%s" type="text"%s value="%s">%s\n' + u' </div></div>\n' + u' </li>\n' + % ( escapeAttr(sName), escapeAttr(sName), sExtraAttribs, escapeAttr(unicode(sValue)), sPostHtml )); + + def addTextRO(self, sName, sValue, sLabel, sSubClass = 'string', sExtraAttribs = '', sPostHtml = ''): + """Adds a read-only text input.""" + if sSubClass not in ('int', 'long', 'string', 'uuid', 'timestamp', 'wide'): raise Exception(sSubClass); + self._addLabel(sName, sLabel, sSubClass); + if sValue is None: sValue = ''; + return self._add(u' <input name="%s" id="%s" type="text" readonly%s value="%s" class="tmform-input-readonly">' + u'%s\n' + u' </div></div>\n' + u' </li>\n' + % ( escapeAttr(sName), escapeAttr(sName), sExtraAttribs, escapeAttr(unicode(sValue)), sPostHtml )); + + def addWideText(self, sName, sValue, sLabel, sExtraAttribs = '', sPostHtml = ''): + """Adds a wide text input.""" + return self.addText(sName, sValue, sLabel, 'wide', sExtraAttribs, sPostHtml = sPostHtml); + + def addWideTextRO(self, sName, sValue, sLabel, sExtraAttribs = '', sPostHtml = ''): + """Adds a wide read-only text input.""" + return self.addTextRO(sName, sValue, sLabel, 'wide', sExtraAttribs, sPostHtml = sPostHtml); + + def _adjustMultilineTextAttribs(self, sExtraAttribs, sValue): + """ Internal helper for setting good default sizes for textarea based on content.""" + if sExtraAttribs.find('cols') < 0 and sExtraAttribs.find('width') < 0: + sExtraAttribs = 'cols="96%" ' + sExtraAttribs; + + if sExtraAttribs.find('rows') < 0 and sExtraAttribs.find('width') < 0: + if sValue is None: sValue = ''; + else: sValue = sValue.strip(); + + cRows = sValue.count('\n') + (not sValue.endswith('\n')); + if cRows * 80 < len(sValue): + cRows += 2; + cRows = max(min(cRows, 16), 2); + sExtraAttribs = ('rows="%s" ' % (cRows,)) + sExtraAttribs; + + return sExtraAttribs; + + def addMultilineText(self, sName, sValue, sLabel, sSubClass = 'string', sExtraAttribs = ''): + """Adds a multiline text input.""" + if self._fReadOnly: + return self.addMultilineTextRO(sName, sValue, sLabel, sSubClass, sExtraAttribs); + if sSubClass not in ('int', 'long', 'string', 'uuid', 'timestamp'): raise Exception(sSubClass) + self._addLabel(sName, sLabel, sSubClass) + if sValue is None: sValue = ''; + sNewValue = unicode(sValue) if not isinstance(sValue, list) else '\n'.join(sValue) + return self._add(u' <textarea name="%s" id="%s" %s>%s</textarea>\n' + u' </div></div>\n' + u' </li>\n' + % ( escapeAttr(sName), escapeAttr(sName), self._adjustMultilineTextAttribs(sExtraAttribs, sNewValue), + escapeElem(sNewValue))) + + def addMultilineTextRO(self, sName, sValue, sLabel, sSubClass = 'string', sExtraAttribs = ''): + """Adds a multiline read-only text input.""" + if sSubClass not in ('int', 'long', 'string', 'uuid', 'timestamp'): raise Exception(sSubClass) + self._addLabel(sName, sLabel, sSubClass) + if sValue is None: sValue = ''; + sNewValue = unicode(sValue) if not isinstance(sValue, list) else '\n'.join(sValue) + return self._add(u' <textarea name="%s" id="%s" readonly %s>%s</textarea>\n' + u' </div></div>\n' + u' </li>\n' + % ( escapeAttr(sName), escapeAttr(sName), self._adjustMultilineTextAttribs(sExtraAttribs, sNewValue), + escapeElem(sNewValue))) + + def addInt(self, sName, iValue, sLabel, sExtraAttribs = '', sPostHtml = ''): + """Adds an integer input.""" + return self.addText(sName, unicode(iValue), sLabel, 'int', sExtraAttribs, sPostHtml = sPostHtml); + + def addIntRO(self, sName, iValue, sLabel, sExtraAttribs = '', sPostHtml = ''): + """Adds an integer input.""" + return self.addTextRO(sName, unicode(iValue), sLabel, 'int', sExtraAttribs, sPostHtml = sPostHtml); + + def addLong(self, sName, lValue, sLabel, sExtraAttribs = '', sPostHtml = ''): + """Adds a long input.""" + return self.addText(sName, unicode(lValue), sLabel, 'long', sExtraAttribs, sPostHtml = sPostHtml); + + def addLongRO(self, sName, lValue, sLabel, sExtraAttribs = '', sPostHtml = ''): + """Adds a long input.""" + return self.addTextRO(sName, unicode(lValue), sLabel, 'long', sExtraAttribs, sPostHtml = sPostHtml); + + def addUuid(self, sName, uuidValue, sLabel, sExtraAttribs = '', sPostHtml = ''): + """Adds an UUID input.""" + return self.addText(sName, unicode(uuidValue), sLabel, 'uuid', sExtraAttribs, sPostHtml = sPostHtml); + + def addUuidRO(self, sName, uuidValue, sLabel, sExtraAttribs = '', sPostHtml = ''): + """Adds a read-only UUID input.""" + return self.addTextRO(sName, unicode(uuidValue), sLabel, 'uuid', sExtraAttribs, sPostHtml = sPostHtml); + + def addTimestampRO(self, sName, sTimestamp, sLabel, sExtraAttribs = '', sPostHtml = ''): + """Adds a read-only database string timstamp input.""" + return self.addTextRO(sName, sTimestamp, sLabel, 'timestamp', sExtraAttribs, sPostHtml = sPostHtml); + + + # + # Text areas. + # + + + # + # Combo boxes. + # + def addComboBox(self, sName, sSelected, sLabel, aoOptions, sExtraAttribs = '', sPostHtml = ''): + """Adds a combo box.""" + if self._fReadOnly: + return self.addComboBoxRO(sName, sSelected, sLabel, aoOptions, sExtraAttribs, sPostHtml); + self._addLabel(sName, sLabel, 'combobox'); + self._add(' <select name="%s" id="%s" class="tmform-combobox"%s>\n' + % (escapeAttr(sName), escapeAttr(sName), sExtraAttribs)); + sSelected = unicode(sSelected); + for iValue, sText, _ in aoOptions: + sValue = unicode(iValue); + self._add(' <option value="%s"%s>%s</option>\n' + % (escapeAttr(sValue), ' selected' if sValue == sSelected else '', + escapeElem(sText))); + return self._add(u' </select>' + sPostHtml + '\n' + u' </div></div>\n' + u' </li>\n'); + + def addComboBoxRO(self, sName, sSelected, sLabel, aoOptions, sExtraAttribs = '', sPostHtml = ''): + """Adds a read-only combo box.""" + self.addTextHidden(sName, sSelected); + self._addLabel(sName, sLabel, 'combobox-readonly'); + self._add(u' <select name="%s" id="%s" disabled class="tmform-combobox"%s>\n' + % (escapeAttr(sName), escapeAttr(sName), sExtraAttribs)); + sSelected = unicode(sSelected); + for iValue, sText, _ in aoOptions: + sValue = unicode(iValue); + self._add(' <option value="%s"%s>%s</option>\n' + % (escapeAttr(sValue), ' selected' if sValue == sSelected else '', + escapeElem(sText))); + return self._add(u' </select>' + sPostHtml + '\n' + u' </div></div>\n' + u' </li>\n'); + + # + # Check boxes. + # + @staticmethod + def _reinterpretBool(fValue): + """Reinterprets a value as a boolean type.""" + if fValue is not type(True): + if fValue is None: + fValue = False; + elif str(fValue) in ('True', 'true', '1'): + fValue = True; + else: + fValue = False; + return fValue; + + def addCheckBox(self, sName, fChecked, sLabel, sExtraAttribs = ''): + """Adds an check box.""" + if self._fReadOnly: + return self.addCheckBoxRO(sName, fChecked, sLabel, sExtraAttribs); + self._addLabel(sName, sLabel, 'checkbox'); + fChecked = self._reinterpretBool(fChecked); + return self._add(u' <input name="%s" id="%s" type="checkbox"%s%s value="1" class="tmform-checkbox">\n' + u' </div></div>\n' + u' </li>\n' + % (escapeAttr(sName), escapeAttr(sName), ' checked' if fChecked else '', sExtraAttribs)); + + def addCheckBoxRO(self, sName, fChecked, sLabel, sExtraAttribs = ''): + """Adds an readonly check box.""" + self._addLabel(sName, sLabel, 'checkbox'); + fChecked = self._reinterpretBool(fChecked); + # Hack Alert! The onclick and onkeydown are for preventing editing and fake readonly/disabled. + return self._add(u' <input name="%s" id="%s" type="checkbox"%s readonly%s value="1" class="readonly"\n' + u' onclick="return false" onkeydown="return false">\n' + u' </div></div>\n' + u' </li>\n' + % (escapeAttr(sName), escapeAttr(sName), ' checked' if fChecked else '', sExtraAttribs)); + + # + # List of items to check + # + def _addList(self, sName, aoRows, sLabel, fUseTable = False, sId = 'dummy', sExtraAttribs = ''): + """ + Adds a list of items to check. + + @param sName Name of HTML form element + @param aoRows List of [sValue, fChecked, sName] sub-arrays. + @param sLabel Label of HTML form element + """ + fReadOnly = self._fReadOnly; ## @todo add this as a parameter. + if fReadOnly: + sExtraAttribs += ' readonly onclick="return false" onkeydown="return false"'; + + self._addLabel(sName, sLabel, 'list'); + if not aoRows: + return self._add('No items</div></div></li>') + sNameEscaped = escapeAttr(sName); + + self._add(' <div class="tmform-checkboxes-container" id="%s">\n' % (escapeAttr(sId),)); + if fUseTable: + self._add(' <table>\n'); + for asRow in aoRows: + assert len(asRow) == 3; # Don't allow sloppy input data! + fChecked = self._reinterpretBool(asRow[1]) + self._add(u' <tr>\n' + u' <td><input type="checkbox" name="%s" value="%s"%s%s></td>\n' + u' <td>%s</td>\n' + u' </tr>\n' + % ( sNameEscaped, escapeAttr(unicode(asRow[0])), ' checked' if fChecked else '', sExtraAttribs, + escapeElem(unicode(asRow[2])), )); + self._add(u' </table>\n'); + else: + for asRow in aoRows: + assert len(asRow) == 3; # Don't allow sloppy input data! + fChecked = self._reinterpretBool(asRow[1]) + self._add(u' <div class="tmform-checkbox-holder">' + u'<input type="checkbox" name="%s" value="%s"%s%s> %s</input></div>\n' + % ( sNameEscaped, escapeAttr(unicode(asRow[0])), ' checked' if fChecked else '', sExtraAttribs, + escapeElem(unicode(asRow[2])),)); + return self._add(u' </div></div></div>\n' + u' </li>\n'); + + + def addListOfOsArches(self, sName, aoOsArches, sLabel, sExtraAttribs = ''): + """ + List of checkboxes for OS/ARCH selection. + asOsArches is a list of [sValue, fChecked, sName] sub-arrays. + """ + return self._addList(sName, aoOsArches, sLabel, fUseTable = False, sId = 'tmform-checkbox-list-os-arches', + sExtraAttribs = sExtraAttribs); + + def addListOfTypes(self, sName, aoTypes, sLabel, sExtraAttribs = ''): + """ + List of checkboxes for build type selection. + aoTypes is a list of [sValue, fChecked, sName] sub-arrays. + """ + return self._addList(sName, aoTypes, sLabel, fUseTable = False, sId = 'tmform-checkbox-list-build-types', + sExtraAttribs = sExtraAttribs); + + def addListOfTestCases(self, sName, aoTestCases, sLabel, sExtraAttribs = ''): + """ + List of checkboxes for test box (dependency) selection. + aoTestCases is a list of [sValue, fChecked, sName] sub-arrays. + """ + return self._addList(sName, aoTestCases, sLabel, fUseTable = False, sId = 'tmform-checkbox-list-testcases', + sExtraAttribs = sExtraAttribs); + + def addListOfResources(self, sName, aoTestCases, sLabel, sExtraAttribs = ''): + """ + List of checkboxes for resource selection. + aoTestCases is a list of [sValue, fChecked, sName] sub-arrays. + """ + return self._addList(sName, aoTestCases, sLabel, fUseTable = False, sId = 'tmform-checkbox-list-resources', + sExtraAttribs = sExtraAttribs); + + def addListOfTestGroups(self, sName, aoTestGroups, sLabel, sExtraAttribs = ''): + """ + List of checkboxes for test group selection. + aoTestGroups is a list of [sValue, fChecked, sName] sub-arrays. + """ + return self._addList(sName, aoTestGroups, sLabel, fUseTable = False, sId = 'tmform-checkbox-list-testgroups', + sExtraAttribs = sExtraAttribs); + + def addListOfTestCaseArgs(self, sName, aoVariations, sLabel): # pylint: disable=too-many-statements + """ + Adds a list of test case argument variations to the form. + + @param sName Name of HTML form element + @param aoVariations List of TestCaseArgsData instances. + @param sLabel Label of HTML form element + """ + self._addLabel(sName, sLabel); + + sTableId = u'TestArgsExtendingListRoot'; + fReadOnly = self._fReadOnly; ## @todo argument? + sReadOnlyAttr = u' readonly class="tmform-input-readonly"' if fReadOnly else ''; + + sHtml = u'<li>\n' + + # + # Define javascript function for extending the list of test case + # variations. Doing it here so we can use the python constants. This + # also permits multiple argument lists on one page should that ever be + # required... + # + if not fReadOnly: + sHtml += u'<script type="text/javascript">\n' + sHtml += u'\n'; + sHtml += u'g_%s_aItems = { %s };\n' % (sName, ', '.join(('%s: 1' % (i,)) for i in range(len(aoVariations))),); + sHtml += u'g_%s_cItems = %s;\n' % (sName, len(aoVariations),); + sHtml += u'g_%s_iIdMod = %s;\n' % (sName, len(aoVariations) + 32); + sHtml += u'\n'; + sHtml += u'function %s_removeEntry(sId)\n' % (sName,); + sHtml += u'{\n'; + sHtml += u' if (g_%s_cItems > 1)\n' % (sName,); + sHtml += u' {\n'; + sHtml += u' g_%s_cItems--;\n' % (sName,); + sHtml += u' delete g_%s_aItems[sId];\n' % (sName,); + sHtml += u' setElementValueToKeyList(\'%s\', g_%s_aItems);\n' % (sName, sName); + sHtml += u'\n'; + for iInput in range(8): + sHtml += u' removeHtmlNode(\'%s[\' + sId + \'][%s]\');\n' % (sName, iInput,); + sHtml += u' }\n'; + sHtml += u'}\n'; + sHtml += u'\n'; + sHtml += u'function %s_extendListEx(sSubName, cGangMembers, cSecTimeout, sArgs, sTestBoxReqExpr, sBuildReqExpr)\n' \ + % (sName,); + sHtml += u'{\n'; + sHtml += u' var oElement = document.getElementById(\'%s\');\n' % (sTableId,); + sHtml += u' var oTBody = document.createElement(\'tbody\');\n'; + sHtml += u' var sHtml = \'\';\n'; + sHtml += u' var sId;\n'; + sHtml += u'\n'; + sHtml += u' g_%s_iIdMod += 1;\n' % (sName,); + sHtml += u' sId = g_%s_iIdMod.toString();\n' % (sName,); + + oVarDefaults = TestCaseArgsData(); + oVarDefaults.convertToParamNull(); + sHtml += u'\n'; + sHtml += u' sHtml += \'<tr class="tmform-testcasevars-first-row">\';\n'; + sHtml += u' sHtml += \' <td>Sub-Name:</td>\';\n'; + sHtml += u' sHtml += \' <td class="tmform-field-subname">' \ + '<input name="%s[\' + sId + \'][%s]" id="%s[\' + sId + \'][0]" value="\' + sSubName + \'"></td>\';\n' \ + % (sName, TestCaseArgsData.ksParam_sSubName, sName,); + sHtml += u' sHtml += \' <td>Gang Members:</td>\';\n'; + sHtml += u' sHtml += \' <td class="tmform-field-tiny-int">' \ + '<input name="%s[\' + sId + \'][%s]" id="%s[\' + sId + \'][0]" value="\' + cGangMembers + \'"></td>\';\n' \ + % (sName, TestCaseArgsData.ksParam_cGangMembers, sName,); + sHtml += u' sHtml += \' <td>Timeout:</td>\';\n'; + sHtml += u' sHtml += \' <td class="tmform-field-int">' \ + u'<input name="%s[\' + sId + \'][%s]" id="%s[\' + sId + \'][1]" value="\'+ cSecTimeout + \'"></td>\';\n' \ + % (sName, TestCaseArgsData.ksParam_cSecTimeout, sName,); + sHtml += u' sHtml += \' <td><a href="#" onclick="%s_removeEntry(\\\'\' + sId + \'\\\');"> Remove</a></td>\';\n' \ + % (sName, ); + sHtml += u' sHtml += \' <td></td>\';\n'; + sHtml += u' sHtml += \'</tr>\';\n' + sHtml += u'\n'; + sHtml += u' sHtml += \'<tr class="tmform-testcasevars-inner-row">\';\n'; + sHtml += u' sHtml += \' <td>Arguments:</td>\';\n'; + sHtml += u' sHtml += \' <td class="tmform-field-wide100" colspan="6">' \ + u'<input name="%s[\' + sId + \'][%s]" id="%s[\' + sId + \'][2]" value="\' + sArgs + \'"></td>\';\n' \ + % (sName, TestCaseArgsData.ksParam_sArgs, sName,); + sHtml += u' sHtml += \' <td></td>\';\n'; + sHtml += u' sHtml += \'</tr>\';\n' + sHtml += u'\n'; + sHtml += u' sHtml += \'<tr class="tmform-testcasevars-inner-row">\';\n'; + sHtml += u' sHtml += \' <td>TestBox Reqs:</td>\';\n'; + sHtml += u' sHtml += \' <td class="tmform-field-wide100" colspan="6">' \ + u'<input name="%s[\' + sId + \'][%s]" id="%s[\' + sId + \'][2]" value="\' + sTestBoxReqExpr' \ + u' + \'"></td>\';\n' \ + % (sName, TestCaseArgsData.ksParam_sTestBoxReqExpr, sName,); + sHtml += u' sHtml += \' <td></td>\';\n'; + sHtml += u' sHtml += \'</tr>\';\n' + sHtml += u'\n'; + sHtml += u' sHtml += \'<tr class="tmform-testcasevars-final-row">\';\n'; + sHtml += u' sHtml += \' <td>Build Reqs:</td>\';\n'; + sHtml += u' sHtml += \' <td class="tmform-field-wide100" colspan="6">' \ + u'<input name="%s[\' + sId + \'][%s]" id="%s[\' + sId + \'][2]" value="\' + sBuildReqExpr + \'"></td>\';\n' \ + % (sName, TestCaseArgsData.ksParam_sBuildReqExpr, sName,); + sHtml += u' sHtml += \' <td></td>\';\n'; + sHtml += u' sHtml += \'</tr>\';\n' + sHtml += u'\n'; + sHtml += u' oTBody.id = \'%s[\' + sId + \'][6]\';\n' % (sName,); + sHtml += u' oTBody.innerHTML = sHtml;\n'; + sHtml += u'\n'; + sHtml += u' oElement.appendChild(oTBody);\n'; + sHtml += u'\n'; + sHtml += u' g_%s_aItems[sId] = 1;\n' % (sName,); + sHtml += u' g_%s_cItems++;\n' % (sName,); + sHtml += u' setElementValueToKeyList(\'%s\', g_%s_aItems);\n' % (sName, sName); + sHtml += u'}\n'; + sHtml += u'function %s_extendList()\n' % (sName,); + sHtml += u'{\n'; + sHtml += u' %s_extendListEx("%s", "%s", "%s", "%s", "%s", "%s");\n' % (sName, + escapeAttr(unicode(oVarDefaults.sSubName)), escapeAttr(unicode(oVarDefaults.cGangMembers)), + escapeAttr(unicode(oVarDefaults.cSecTimeout)), escapeAttr(oVarDefaults.sArgs), + escapeAttr(oVarDefaults.sTestBoxReqExpr), escapeAttr(oVarDefaults.sBuildReqExpr), ); + sHtml += u'}\n'; + if config.g_kfVBoxSpecific: + sSecTimeoutDef = escapeAttr(unicode(oVarDefaults.cSecTimeout)); + sHtml += u'function vbox_%s_add_uni()\n' % (sName,); + sHtml += u'{\n'; + sHtml += u' %s_extendListEx("1-raw", "1", "%s", "--cpu-counts 1 --virt-modes raw", ' \ + u' "", "");\n' % (sName, sSecTimeoutDef); + sHtml += u' %s_extendListEx("1-hw", "1", "%s", "--cpu-counts 1 --virt-modes hwvirt", ' \ + u' "fCpuHwVirt is True", "");\n' % (sName, sSecTimeoutDef); + sHtml += u' %s_extendListEx("1-np", "1", "%s", "--cpu-counts 1 --virt-modes hwvirt-np", ' \ + u' "fCpuNestedPaging is True", "");\n' % (sName, sSecTimeoutDef); + sHtml += u'}\n'; + sHtml += u'function vbox_%s_add_uni_amd64()\n' % (sName,); + sHtml += u'{\n'; + sHtml += u' %s_extendListEx("1-hw", "1", "%s", "--cpu-counts 1 --virt-modes hwvirt", ' \ + u' "fCpuHwVirt is True", "");\n' % (sName, sSecTimeoutDef); + sHtml += u' %s_extendListEx("1-np", "%s", "--cpu-counts 1 --virt-modes hwvirt-np", ' \ + u' "fCpuNestedPaging is True", "");\n' % (sName, sSecTimeoutDef); + sHtml += u'}\n'; + sHtml += u'function vbox_%s_add_smp()\n' % (sName,); + sHtml += u'{\n'; + sHtml += u' %s_extendListEx("2-hw", "1", "%s", "--cpu-counts 2 --virt-modes hwvirt",' \ + u' "fCpuHwVirt is True and cCpus >= 2", "");\n' % (sName, sSecTimeoutDef); + sHtml += u' %s_extendListEx("2-np", "1", "%s", "--cpu-counts 2 --virt-modes hwvirt-np",' \ + u' "fCpuNestedPaging is True and cCpus >= 2", "");\n' % (sName, sSecTimeoutDef); + sHtml += u' %s_extendListEx("3-hw", "1", "%s", "--cpu-counts 3 --virt-modes hwvirt",' \ + u' "fCpuHwVirt is True and cCpus >= 3", "");\n' % (sName, sSecTimeoutDef); + sHtml += u' %s_extendListEx("4-np", "1", "%s", "--cpu-counts 4 --virt-modes hwvirt-np ",' \ + u' "fCpuNestedPaging is True and cCpus >= 4", "");\n' % (sName, sSecTimeoutDef); + #sHtml += u' %s_extendListEx("6-hw", "1", "%s", "--cpu-counts 6 --virt-modes hwvirt",' \ + # u' "fCpuHwVirt is True and cCpus >= 6", "");\n' % (sName, sSecTimeoutDef); + #sHtml += u' %s_extendListEx("8-np", "1", "%s", "--cpu-counts 8 --virt-modes hwvirt-np",' \ + # u' "fCpuNestedPaging is True and cCpus >= 8", "");\n' % (sName, sSecTimeoutDef); + sHtml += u'}\n'; + sHtml += u'</script>\n'; + + + # + # List current entries. + # + sHtml += u'<input type="hidden" name="%s" id="%s" value="%s">\n' \ + % (sName, sName, ','.join(unicode(i) for i in range(len(aoVariations))), ); + sHtml += u' <table id="%s" class="tmform-testcasevars">\n' % (sTableId,) + if not fReadOnly: + sHtml += u' <caption>\n' \ + u' <a href="#" onClick="%s_extendList()">Add</a>\n' % (sName,); + if config.g_kfVBoxSpecific: + sHtml += u' [<a href="#" onClick="vbox_%s_add_uni()">Single CPU Variations</a>\n' % (sName,); + sHtml += u' <a href="#" onClick="vbox_%s_add_uni_amd64()">amd64</a>]\n' % (sName,); + sHtml += u' [<a href="#" onClick="vbox_%s_add_smp()">SMP Variations</a>]\n' % (sName,); + sHtml += u' </caption>\n'; + + dSubErrors = {}; + if sName in self._dErrors and isinstance(self._dErrors[sName], dict): + dSubErrors = self._dErrors[sName]; + + for iVar, _ in enumerate(aoVariations): + oVar = copy.copy(aoVariations[iVar]); + oVar.convertToParamNull(); + + sHtml += u'<tbody id="%s[%s][6]">\n' % (sName, iVar,) + sHtml += u' <tr class="tmform-testcasevars-first-row">\n' \ + u' <td>Sub-name:</td>' \ + u' <td class="tmform-field-subname"><input name="%s[%s][%s]" id="%s[%s][1]" value="%s"%s></td>\n' \ + u' <td>Gang Members:</td>' \ + u' <td class="tmform-field-tiny-int"><input name="%s[%s][%s]" id="%s[%s][1]" value="%s"%s></td>\n' \ + u' <td>Timeout:</td>' \ + u' <td class="tmform-field-int"><input name="%s[%s][%s]" id="%s[%s][2]" value="%s"%s></td>\n' \ + % ( sName, iVar, TestCaseArgsData.ksParam_sSubName, sName, iVar, oVar.sSubName, sReadOnlyAttr, + sName, iVar, TestCaseArgsData.ksParam_cGangMembers, sName, iVar, oVar.cGangMembers, sReadOnlyAttr, + sName, iVar, TestCaseArgsData.ksParam_cSecTimeout, sName, iVar, + utils.formatIntervalSeconds2(oVar.cSecTimeout), sReadOnlyAttr, ); + if not fReadOnly: + sHtml += u' <td><a href="#" onclick="%s_removeEntry(\'%s\');">Remove</a></td>\n' \ + % (sName, iVar); + else: + sHtml += u' <td></td>\n'; + sHtml += u' <td class="tmform-testcasevars-stupid-border-column"></td>\n' \ + u' </tr>\n'; + + sHtml += u' <tr class="tmform-testcasevars-inner-row">\n' \ + u' <td>Arguments:</td>' \ + u' <td class="tmform-field-wide100" colspan="6">' \ + u'<input name="%s[%s][%s]" id="%s[%s][3]" value="%s"%s></td>\n' \ + u' <td></td>\n' \ + u' </tr>\n' \ + % ( sName, iVar, TestCaseArgsData.ksParam_sArgs, sName, iVar, escapeAttr(oVar.sArgs), sReadOnlyAttr) + + sHtml += u' <tr class="tmform-testcasevars-inner-row">\n' \ + u' <td>TestBox Reqs:</td>' \ + u' <td class="tmform-field-wide100" colspan="6">' \ + u'<input name="%s[%s][%s]" id="%s[%s][4]" value="%s"%s></td>\n' \ + u' <td></td>\n' \ + u' </tr>\n' \ + % ( sName, iVar, TestCaseArgsData.ksParam_sTestBoxReqExpr, sName, iVar, + escapeAttr(oVar.sTestBoxReqExpr), sReadOnlyAttr) + + sHtml += u' <tr class="tmform-testcasevars-final-row">\n' \ + u' <td>Build Reqs:</td>' \ + u' <td class="tmform-field-wide100" colspan="6">' \ + u'<input name="%s[%s][%s]" id="%s[%s][5]" value="%s"%s></td>\n' \ + u' <td></td>\n' \ + u' </tr>\n' \ + % ( sName, iVar, TestCaseArgsData.ksParam_sBuildReqExpr, sName, iVar, + escapeAttr(oVar.sBuildReqExpr), sReadOnlyAttr) + + + if iVar in dSubErrors: + sHtml += u' <tr><td colspan="4"><p align="left" class="tmform-error-desc">%s</p></td></tr>\n' \ + % (self._escapeErrorText(dSubErrors[iVar]),); + + sHtml += u'</tbody>\n'; + sHtml += u' </table>\n' + sHtml += u'</li>\n' + + return self._add(sHtml) + + def addListOfTestGroupMembers(self, sName, aoTestGroupMembers, aoAllTestCases, sLabel, # pylint: disable=too-many-locals + fReadOnly = True): + """ + For WuiTestGroup. + """ + assert len(aoTestGroupMembers) <= len(aoAllTestCases); + self._addLabel(sName, sLabel); + if not aoAllTestCases: + return self._add('<li>No testcases.</li>\n') + + self._add(u'<input name="%s" type="hidden" value="%s">\n' + % ( TestGroupDataEx.ksParam_aidTestCases, + ','.join([unicode(oTestCase.idTestCase) for oTestCase in aoAllTestCases]), )); + + self._add(u'<table class="tmformtbl">\n' + u' <thead>\n' + u' <tr>\n' + u' <th rowspan="2"></th>\n' + u' <th rowspan="2">Test Case</th>\n' + u' <th rowspan="2">All Vars</th>\n' + u' <th rowspan="2">Priority [0..31]</th>\n' + u' <th colspan="4" align="center">Variations</th>\n' + u' </tr>\n' + u' <tr>\n' + u' <th>Included</th>\n' + u' <th>Gang size</th>\n' + u' <th>Timeout</th>\n' + u' <th>Arguments</th>\n' + u' </tr>\n' + u' </thead>\n' + u' <tbody>\n' + ); + + if self._fReadOnly: + fReadOnly = True; + sCheckBoxAttr = ' readonly onclick="return false" onkeydown="return false"' if fReadOnly else ''; + + oDefMember = TestGroupMemberData(); + aoTestGroupMembers = list(aoTestGroupMembers); # Copy it so we can pop. + for iTestCase, _ in enumerate(aoAllTestCases): + oTestCase = aoAllTestCases[iTestCase]; + + # Is it a member? + oMember = None; + for i, _ in enumerate(aoTestGroupMembers): + if aoTestGroupMembers[i].oTestCase.idTestCase == oTestCase.idTestCase: + oMember = aoTestGroupMembers.pop(i); + break; + + # Start on the rows... + sPrefix = u'%s[%d]' % (sName, oTestCase.idTestCase,); + self._add(u' <tr class="%s">\n' + u' <td rowspan="%d">\n' + u' <input name="%s[%s]" type="hidden" value="%s">\n' # idTestCase + u' <input name="%s[%s]" type="hidden" value="%s">\n' # idTestGroup + u' <input name="%s[%s]" type="hidden" value="%s">\n' # tsExpire + u' <input name="%s[%s]" type="hidden" value="%s">\n' # tsEffective + u' <input name="%s[%s]" type="hidden" value="%s">\n' # uidAuthor + u' <input name="%s" type="checkbox"%s%s value="%d" class="tmform-checkbox" title="#%d - %s">\n' #(list) + u' </td>\n' + % ( 'tmodd' if iTestCase & 1 else 'tmeven', + len(oTestCase.aoTestCaseArgs), + sPrefix, TestGroupMemberData.ksParam_idTestCase, oTestCase.idTestCase, + sPrefix, TestGroupMemberData.ksParam_idTestGroup, -1 if oMember is None else oMember.idTestGroup, + sPrefix, TestGroupMemberData.ksParam_tsExpire, '' if oMember is None else oMember.tsExpire, + sPrefix, TestGroupMemberData.ksParam_tsEffective, '' if oMember is None else oMember.tsEffective, + sPrefix, TestGroupMemberData.ksParam_uidAuthor, '' if oMember is None else oMember.uidAuthor, + TestGroupDataEx.ksParam_aoMembers, '' if oMember is None else ' checked', sCheckBoxAttr, + oTestCase.idTestCase, oTestCase.idTestCase, escapeElem(oTestCase.sName), + )); + self._add(u' <td rowspan="%d" align="left">%s</td>\n' + % ( len(oTestCase.aoTestCaseArgs), escapeElem(oTestCase.sName), )); + + self._add(u' <td rowspan="%d" title="Include all variations (checked) or choose a set?">\n' + u' <input name="%s[%s]" type="checkbox"%s%s value="-1">\n' + u' </td>\n' + % ( len(oTestCase.aoTestCaseArgs), + sPrefix, TestGroupMemberData.ksParam_aidTestCaseArgs, + ' checked' if oMember is None or oMember.aidTestCaseArgs is None else '', sCheckBoxAttr, )); + + self._add(u' <td rowspan="%d" align="center">\n' + u' <input name="%s[%s]" type="text" value="%s" style="max-width:3em;" %s>\n' + u' </td>\n' + % ( len(oTestCase.aoTestCaseArgs), + sPrefix, TestGroupMemberData.ksParam_iSchedPriority, + (oMember if oMember is not None else oDefMember).iSchedPriority, + ' readonly class="tmform-input-readonly"' if fReadOnly else '', )); + + # Argument variations. + aidTestCaseArgs = [] if oMember is None or oMember.aidTestCaseArgs is None else oMember.aidTestCaseArgs; + for iVar, oVar in enumerate(oTestCase.aoTestCaseArgs): + if iVar > 0: + self._add(' <tr class="%s">\n' % ('tmodd' if iTestCase & 1 else 'tmeven',)); + self._add(u' <td align="center">\n' + u' <input name="%s[%s]" type="checkbox"%s%s value="%d">' + u' </td>\n' + % ( sPrefix, TestGroupMemberData.ksParam_aidTestCaseArgs, + ' checked' if oVar.idTestCaseArgs in aidTestCaseArgs else '', sCheckBoxAttr, oVar.idTestCaseArgs, + )); + self._add(u' <td align="center">%s</td>\n' + u' <td align="center">%s</td>\n' + u' <td align="left">%s</td>\n' + % ( oVar.cGangMembers, + 'Default' if oVar.cSecTimeout is None else oVar.cSecTimeout, + escapeElem(oVar.sArgs) )); + + self._add(u' </tr>\n'); + + + + if not oTestCase.aoTestCaseArgs: + self._add(u' <td></td> <td></td> <td></td> <td></td>\n' + u' </tr>\n'); + return self._add(u' </tbody>\n' + u'</table>\n'); + + def addListOfSchedGroupMembers(self, sName, aoSchedGroupMembers, aoAllRelevantTestGroups, # pylint: disable=too-many-locals + sLabel, idSchedGroup, fReadOnly = True): + """ + For WuiAdminSchedGroup. + """ + if fReadOnly is None or self._fReadOnly: + fReadOnly = self._fReadOnly; + assert len(aoSchedGroupMembers) <= len(aoAllRelevantTestGroups); + self._addLabel(sName, sLabel); + if not aoAllRelevantTestGroups: + return self._add(u'<li>No test groups.</li>\n') + + self._add(u'<input name="%s" type="hidden" value="%s">\n' + % ( SchedGroupDataEx.ksParam_aidTestGroups, + ','.join([unicode(oTestGroup.idTestGroup) for oTestGroup in aoAllRelevantTestGroups]), )); + + self._add(u'<table class="tmformtbl tmformtblschedgroupmembers">\n' + u' <thead>\n' + u' <tr>\n' + u' <th></th>\n' + u' <th>Test Group</th>\n' + u' <th>Priority [0..31]</th>\n' + u' <th>Prerequisite Test Group</th>\n' + u' <th>Weekly schedule</th>\n' + u' </tr>\n' + u' </thead>\n' + u' <tbody>\n' + ); + + sCheckBoxAttr = u' readonly onclick="return false" onkeydown="return false"' if fReadOnly else ''; + sComboBoxAttr = u' disabled' if fReadOnly else ''; + + oDefMember = SchedGroupMemberData(); + aoSchedGroupMembers = list(aoSchedGroupMembers); # Copy it so we can pop. + for iTestGroup, _ in enumerate(aoAllRelevantTestGroups): + oTestGroup = aoAllRelevantTestGroups[iTestGroup]; + + # Is it a member? + oMember = None; + for i, _ in enumerate(aoSchedGroupMembers): + if aoSchedGroupMembers[i].oTestGroup.idTestGroup == oTestGroup.idTestGroup: + oMember = aoSchedGroupMembers.pop(i); + break; + + # Start on the rows... + sPrefix = u'%s[%d]' % (sName, oTestGroup.idTestGroup,); + self._add(u' <tr class="%s">\n' + u' <td>\n' + u' <input name="%s[%s]" type="hidden" value="%s">\n' # idTestGroup + u' <input name="%s[%s]" type="hidden" value="%s">\n' # idSchedGroup + u' <input name="%s[%s]" type="hidden" value="%s">\n' # tsExpire + u' <input name="%s[%s]" type="hidden" value="%s">\n' # tsEffective + u' <input name="%s[%s]" type="hidden" value="%s">\n' # uidAuthor + u' <input name="%s" type="checkbox"%s%s value="%d" class="tmform-checkbox" title="#%d - %s">\n' #(list) + u' </td>\n' + % ( 'tmodd' if iTestGroup & 1 else 'tmeven', + sPrefix, SchedGroupMemberData.ksParam_idTestGroup, oTestGroup.idTestGroup, + sPrefix, SchedGroupMemberData.ksParam_idSchedGroup, idSchedGroup, + sPrefix, SchedGroupMemberData.ksParam_tsExpire, '' if oMember is None else oMember.tsExpire, + sPrefix, SchedGroupMemberData.ksParam_tsEffective, '' if oMember is None else oMember.tsEffective, + sPrefix, SchedGroupMemberData.ksParam_uidAuthor, '' if oMember is None else oMember.uidAuthor, + SchedGroupDataEx.ksParam_aoMembers, '' if oMember is None else ' checked', sCheckBoxAttr, + oTestGroup.idTestGroup, oTestGroup.idTestGroup, escapeElem(oTestGroup.sName), + )); + self._add(u' <td>%s</td>\n' % ( escapeElem(oTestGroup.sName), )); + + self._add(u' <td>\n' + u' <input name="%s[%s]" type="text" value="%s" style="max-width:3em;" %s>\n' + u' </td>\n' + % ( sPrefix, SchedGroupMemberData.ksParam_iSchedPriority, + (oMember if oMember is not None else oDefMember).iSchedPriority, + ' readonly class="tmform-input-readonly"' if fReadOnly else '', )); + + self._add(u' <td>\n' + u' <select name="%s[%s]" id="%s[%s]" class="tmform-combobox"%s>\n' + u' <option value="-1"%s>None</option>\n' + % ( sPrefix, SchedGroupMemberData.ksParam_idTestGroupPreReq, + sPrefix, SchedGroupMemberData.ksParam_idTestGroupPreReq, + sComboBoxAttr, + ' selected' if oMember is None or oMember.idTestGroupPreReq is None else '', + )); + for oTestGroup2 in aoAllRelevantTestGroups: + if oTestGroup2 != oTestGroup: + fSelected = oMember is not None and oTestGroup2.idTestGroup == oMember.idTestGroupPreReq; + self._add(' <option value="%s"%s>%s</option>\n' + % ( oTestGroup2.idTestGroup, ' selected' if fSelected else '', escapeElem(oTestGroup2.sName), )); + self._add(u' </select>\n' + u' </td>\n'); + + self._add(u' <td>\n' + u' Todo<input name="%s[%s]" type="hidden" value="%s">\n' + u' </td>\n' + % ( sPrefix, SchedGroupMemberData.ksParam_bmHourlySchedule, + '' if oMember is None else oMember.bmHourlySchedule, )); + + self._add(u' </tr>\n'); + return self._add(u' </tbody>\n' + u'</table>\n'); + + def addListOfSchedGroupBoxes(self, sName, aoSchedGroupBoxes, # pylint: disable=too-many-locals + aoAllRelevantTestBoxes, sLabel, idSchedGroup, fReadOnly = True, + fUseTable = False): # (str, list[TestBoxDataEx], list[TestBoxDataEx], str, bool, bool) -> str + """ + For WuiAdminSchedGroup. + """ + if fReadOnly is None or self._fReadOnly: + fReadOnly = self._fReadOnly; + assert len(aoSchedGroupBoxes) <= len(aoAllRelevantTestBoxes); + self._addLabel(sName, sLabel); + if not aoAllRelevantTestBoxes: + return self._add(u'<li>No test boxes.</li>\n') + + self._add(u'<input name="%s" type="hidden" value="%s">\n' + % ( SchedGroupDataEx.ksParam_aidTestBoxes, + ','.join([unicode(oTestBox.idTestBox) for oTestBox in aoAllRelevantTestBoxes]), )); + + sCheckBoxAttr = u' readonly onclick="return false" onkeydown="return false"' if fReadOnly else ''; + oDefMember = TestBoxDataForSchedGroup(); + aoSchedGroupBoxes = list(aoSchedGroupBoxes); # Copy it so we can pop. + + from testmanager.webui.wuiadmintestbox import WuiTestBoxDetailsLink; + + if not fUseTable: + # + # Non-table version (see also addListOfOsArches). + # + self._add(' <div class="tmform-checkboxes-container">\n'); + + for iTestBox, oTestBox in enumerate(aoAllRelevantTestBoxes): + # Is it a member? + oMember = None; + for i, _ in enumerate(aoSchedGroupBoxes): + if aoSchedGroupBoxes[i].oTestBox and aoSchedGroupBoxes[i].oTestBox.idTestBox == oTestBox.idTestBox: + oMember = aoSchedGroupBoxes.pop(i); + break; + + # Start on the rows... + sPrf = u'%s[%d]' % (sName, oTestBox.idTestBox,); + self._add(u' <div class="tmform-checkbox-holder tmshade%u">\n' + u' <input name="%s[%s]" type="hidden" value="%s">\n' # idTestBox + u' <input name="%s[%s]" type="hidden" value="%s">\n' # idSchedGroup + u' <input name="%s[%s]" type="hidden" value="%s">\n' # tsExpire + u' <input name="%s[%s]" type="hidden" value="%s">\n' # tsEffective + u' <input name="%s[%s]" type="hidden" value="%s">\n' # uidAuthor + u' <input name="%s" type="checkbox"%s%s value="%d" class="tmform-checkbox" title="#%d - %s">\n' #(list) + % ( iTestBox & 7, + sPrf, TestBoxDataForSchedGroup.ksParam_idTestBox, oTestBox.idTestBox, + sPrf, TestBoxDataForSchedGroup.ksParam_idSchedGroup, idSchedGroup, + sPrf, TestBoxDataForSchedGroup.ksParam_tsExpire, '' if oMember is None else oMember.tsExpire, + sPrf, TestBoxDataForSchedGroup.ksParam_tsEffective, '' if oMember is None else oMember.tsEffective, + sPrf, TestBoxDataForSchedGroup.ksParam_uidAuthor, '' if oMember is None else oMember.uidAuthor, + SchedGroupDataEx.ksParam_aoTestBoxes, '' if oMember is None else ' checked', sCheckBoxAttr, + oTestBox.idTestBox, oTestBox.idTestBox, escapeElem(oTestBox.sName), + )); + + self._add(u' <span class="tmform-priority tmform-testbox-priority">' + u'<input name="%s[%s]" type="text" value="%s" style="max-width:3em;" %s title="%s"></span>\n' + % ( sPrf, TestBoxDataForSchedGroup.ksParam_iSchedPriority, + (oMember if oMember is not None else oDefMember).iSchedPriority, + ' readonly class="tmform-input-readonly"' if fReadOnly else '', + escapeAttr("Priority [0..31]. Higher value means run more often.") )); + + self._add(u' <span class="tmform-testbox-name">%s</span>\n' + % ( WuiTestBoxDetailsLink(oTestBox, sName = '%s (%s)' % (oTestBox.sName, oTestBox.sOs,)),)); + self._add(u' </div>\n'); + return self._add(u' </div></div></div>\n' + u' </li>\n'); + + # + # Table version. + # + self._add(u'<table class="tmformtbl">\n' + u' <thead>\n' + u' <tr>\n' + u' <th></th>\n' + u' <th>Test Box</th>\n' + u' <th>Priority [0..31]</th>\n' + u' </tr>\n' + u' </thead>\n' + u' <tbody>\n' + ); + + for iTestBox, oTestBox in enumerate(aoAllRelevantTestBoxes): + # Is it a member? + oMember = None; + for i, _ in enumerate(aoSchedGroupBoxes): + if aoSchedGroupBoxes[i].oTestBox and aoSchedGroupBoxes[i].oTestBox.idTestBox == oTestBox.idTestBox: + oMember = aoSchedGroupBoxes.pop(i); + break; + + # Start on the rows... + sPrefix = u'%s[%d]' % (sName, oTestBox.idTestBox,); + self._add(u' <tr class="%s">\n' + u' <td>\n' + u' <input name="%s[%s]" type="hidden" value="%s">\n' # idTestBox + u' <input name="%s[%s]" type="hidden" value="%s">\n' # idSchedGroup + u' <input name="%s[%s]" type="hidden" value="%s">\n' # tsExpire + u' <input name="%s[%s]" type="hidden" value="%s">\n' # tsEffective + u' <input name="%s[%s]" type="hidden" value="%s">\n' # uidAuthor + u' <input name="%s" type="checkbox"%s%s value="%d" class="tmform-checkbox" title="#%d - %s">\n' #(list) + u' </td>\n' + % ( 'tmodd' if iTestBox & 1 else 'tmeven', + sPrefix, TestBoxDataForSchedGroup.ksParam_idTestBox, oTestBox.idTestBox, + sPrefix, TestBoxDataForSchedGroup.ksParam_idSchedGroup, idSchedGroup, + sPrefix, TestBoxDataForSchedGroup.ksParam_tsExpire, '' if oMember is None else oMember.tsExpire, + sPrefix, TestBoxDataForSchedGroup.ksParam_tsEffective, '' if oMember is None else oMember.tsEffective, + sPrefix, TestBoxDataForSchedGroup.ksParam_uidAuthor, '' if oMember is None else oMember.uidAuthor, + SchedGroupDataEx.ksParam_aoTestBoxes, '' if oMember is None else ' checked', sCheckBoxAttr, + oTestBox.idTestBox, oTestBox.idTestBox, escapeElem(oTestBox.sName), + )); + self._add(u' <td align="left">%s</td>\n' % ( escapeElem(oTestBox.sName), )); + + self._add(u' <td align="center">\n' + u' <input name="%s[%s]" type="text" value="%s" style="max-width:3em;" %s>\n' + u' </td>\n' + % ( sPrefix, + TestBoxDataForSchedGroup.ksParam_iSchedPriority, + (oMember if oMember is not None else oDefMember).iSchedPriority, + ' readonly class="tmform-input-readonly"' if fReadOnly else '', )); + + self._add(u' </tr>\n'); + return self._add(u' </tbody>\n' + u'</table>\n'); + + def addListOfSchedGroupsForTestBox(self, sName, aoInSchedGroups, aoAllSchedGroups, sLabel, # pylint: disable=too-many-locals + idTestBox, fReadOnly = None): + # type: (str, TestBoxInSchedGroupDataEx, SchedGroupData, str, bool) -> str + """ + For WuiTestGroup. + """ + from testmanager.core.testbox import TestBoxInSchedGroupData, TestBoxDataEx; + + if fReadOnly is None or self._fReadOnly: + fReadOnly = self._fReadOnly; + assert len(aoInSchedGroups) <= len(aoAllSchedGroups); + + # Only show selected groups in read-only mode. + if fReadOnly: + aoAllSchedGroups = [oCur.oSchedGroup for oCur in aoInSchedGroups] + + self._addLabel(sName, sLabel); + if not aoAllSchedGroups: + return self._add('<li>No scheduling groups.</li>\n') + + # Add special parameter with all the scheduling group IDs in the form. + self._add(u'<input name="%s" type="hidden" value="%s">\n' + % ( TestBoxDataEx.ksParam_aidSchedGroups, + ','.join([unicode(oSchedGroup.idSchedGroup) for oSchedGroup in aoAllSchedGroups]), )); + + # Table header. + self._add(u'<table class="tmformtbl">\n' + u' <thead>\n' + u' <tr>\n' + u' <th rowspan="2"></th>\n' + u' <th rowspan="2">Schedulding Group</th>\n' + u' <th rowspan="2">Priority [0..31]</th>\n' + u' </tr>\n' + u' </thead>\n' + u' <tbody>\n' + ); + + # Table body. + if self._fReadOnly: + fReadOnly = True; + sCheckBoxAttr = ' readonly onclick="return false" onkeydown="return false"' if fReadOnly else ''; + + oDefMember = TestBoxInSchedGroupData(); + aoInSchedGroups = list(aoInSchedGroups); # Copy it so we can pop. + for iSchedGroup, oSchedGroup in enumerate(aoAllSchedGroups): + + # Is it a member? + oMember = None; + for i, _ in enumerate(aoInSchedGroups): + if aoInSchedGroups[i].idSchedGroup == oSchedGroup.idSchedGroup: + oMember = aoInSchedGroups.pop(i); + break; + + # Start on the rows... + sPrefix = u'%s[%d]' % (sName, oSchedGroup.idSchedGroup,); + self._add(u' <tr class="%s">\n' + u' <td>\n' + u' <input name="%s[%s]" type="hidden" value="%s">\n' # idSchedGroup + u' <input name="%s[%s]" type="hidden" value="%s">\n' # idTestBox + u' <input name="%s[%s]" type="hidden" value="%s">\n' # tsExpire + u' <input name="%s[%s]" type="hidden" value="%s">\n' # tsEffective + u' <input name="%s[%s]" type="hidden" value="%s">\n' # uidAuthor + u' <input name="%s" type="checkbox"%s%s value="%d" class="tmform-checkbox" title="#%d - %s">\n' #(list) + u' </td>\n' + % ( 'tmodd' if iSchedGroup & 1 else 'tmeven', + sPrefix, TestBoxInSchedGroupData.ksParam_idSchedGroup, oSchedGroup.idSchedGroup, + sPrefix, TestBoxInSchedGroupData.ksParam_idTestBox, idTestBox, + sPrefix, TestBoxInSchedGroupData.ksParam_tsExpire, '' if oMember is None else oMember.tsExpire, + sPrefix, TestBoxInSchedGroupData.ksParam_tsEffective, '' if oMember is None else oMember.tsEffective, + sPrefix, TestBoxInSchedGroupData.ksParam_uidAuthor, '' if oMember is None else oMember.uidAuthor, + TestBoxDataEx.ksParam_aoInSchedGroups, '' if oMember is None else ' checked', sCheckBoxAttr, + oSchedGroup.idSchedGroup, oSchedGroup.idSchedGroup, escapeElem(oSchedGroup.sName), + )); + self._add(u' <td align="left">%s</td>\n' % ( escapeElem(oSchedGroup.sName), )); + + self._add(u' <td align="center">\n' + u' <input name="%s[%s]" type="text" value="%s" style="max-width:3em;" %s>\n' + u' </td>\n' + % ( sPrefix, TestBoxInSchedGroupData.ksParam_iSchedPriority, + (oMember if oMember is not None else oDefMember).iSchedPriority, + ' readonly class="tmform-input-readonly"' if fReadOnly else '', )); + self._add(u' </tr>\n'); + + return self._add(u' </tbody>\n' + u'</table>\n'); + + + # + # Buttons. + # + def addSubmit(self, sLabel = 'Submit'): + """Adds the submit button to the form.""" + if self._fReadOnly: + return True; + return self._add(u' <li>\n' + u' <br>\n' + u' <div class="tmform-field"><div class="tmform-field-submit">\n' + u' <label> </label>\n' + u' <input type="submit" value="%s">\n' + u' </div></div>\n' + u' </li>\n' + % (escapeElem(sLabel),)); + + def addReset(self): + """Adds a reset button to the form.""" + if self._fReadOnly: + return True; + return self._add(u' <li>\n' + u' <div class="tmform-button"><div class="tmform-button-reset">\n' + u' <input type="reset" value="%s">\n' + u' </div></div>\n' + u' </li>\n'); + diff --git a/src/VBox/ValidationKit/testmanager/webui/wuihlpgraph.py b/src/VBox/ValidationKit/testmanager/webui/wuihlpgraph.py new file mode 100755 index 00000000..ed08bb0f --- /dev/null +++ b/src/VBox/ValidationKit/testmanager/webui/wuihlpgraph.py @@ -0,0 +1,128 @@ +# -*- coding: utf-8 -*- +# $Id: wuihlpgraph.py $ + +""" +Test Manager Web-UI - Graph Helpers. +""" + +__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 $" + + +class WuiHlpGraphDataTable(object): # pylint: disable=too-few-public-methods + """ + Data table container. + """ + + class Row(object): # pylint: disable=too-few-public-methods + """A row.""" + def __init__(self, sGroup, aoValues, asValues = None): + self.sName = sGroup; + self.aoValues = aoValues; + if asValues is None: + self.asValues = [str(oVal) for oVal in aoValues]; + else: + assert len(asValues) == len(aoValues); + self.asValues = asValues; + + def __init__(self, sGroupLable, asMemberLabels): + self.aoTable = [ WuiHlpGraphDataTable.Row(sGroupLable, asMemberLabels), ]; + self.fHasStringValues = False; + + def addRow(self, sGroup, aoValues, asValues = None): + """Adds a row to the data table.""" + if asValues: + self.fHasStringValues = True; + self.aoTable.append(WuiHlpGraphDataTable.Row(sGroup, aoValues, asValues)); + return True; + + def getGroupCount(self): + """Gets the number of data groups (rows).""" + return len(self.aoTable) - 1; + + +class WuiHlpGraphDataTableEx(object): # pylint: disable=too-few-public-methods + """ + Data container for an table/graph with optional error bars on the Y values. + """ + + class DataSeries(object): # pylint: disable=too-few-public-methods + """ + A data series. + + The aoXValues, aoYValues and aoYErrorBars are parallel arrays, making a + series of (X,Y,Y-err-above-delta,Y-err-below-delta) points. + + The error bars are optional. + """ + def __init__(self, sName, aoXValues, aoYValues, asHtmlTooltips = None, aoYErrorBarBelow = None, aoYErrorBarAbove = None): + self.sName = sName; + self.aoXValues = aoXValues; + self.aoYValues = aoYValues; + self.asHtmlTooltips = asHtmlTooltips; + self.aoYErrorBarBelow = aoYErrorBarBelow; + self.aoYErrorBarAbove = aoYErrorBarAbove; + + def __init__(self, sXUnit, sYUnit): + self.sXUnit = sXUnit; + self.sYUnit = sYUnit; + self.aoSeries = []; + + def addDataSeries(self, sName, aoXValues, aoYValues, asHtmlTooltips = None, aoYErrorBarBelow = None, aoYErrorBarAbove = None): + """Adds an data series to the table.""" + self.aoSeries.append(WuiHlpGraphDataTableEx.DataSeries(sName, aoXValues, aoYValues, asHtmlTooltips, + aoYErrorBarBelow, aoYErrorBarAbove)); + return True; + + def getDataSeriesCount(self): + """Gets the number of data series.""" + return len(self.aoSeries); + + +# +# Dynamically choose implementation. +# +if True: # pylint: disable=using-constant-test + from testmanager.webui import wuihlpgraphgooglechart as GraphImplementation; +else: + try: + import matplotlib; # pylint: disable=unused-import,import-error,import-error,wrong-import-order + from testmanager.webui import wuihlpgraphmatplotlib as GraphImplementation; # pylint: disable=ungrouped-imports + except: + from testmanager.webui import wuihlpgraphsimple as GraphImplementation; + +# pylint: disable=invalid-name +WuiHlpBarGraph = GraphImplementation.WuiHlpBarGraph; +WuiHlpLineGraph = GraphImplementation.WuiHlpLineGraph; +WuiHlpLineGraphErrorbarY = GraphImplementation.WuiHlpLineGraphErrorbarY; + diff --git a/src/VBox/ValidationKit/testmanager/webui/wuihlpgraphbase.py b/src/VBox/ValidationKit/testmanager/webui/wuihlpgraphbase.py new file mode 100755 index 00000000..7ab6c6b8 --- /dev/null +++ b/src/VBox/ValidationKit/testmanager/webui/wuihlpgraphbase.py @@ -0,0 +1,131 @@ +# -*- coding: utf-8 -*- +# $Id: wuihlpgraphbase.py $ + +""" +Test Manager Web-UI - Graph Helpers - Base Class. +""" + +__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 $" + + +class WuiHlpGraphBase(object): + """ + Base class for the Graph helpers. + """ + + ## Set of colors that can be used by child classes to color data series. + kasColors = \ + [ + '#0000ff', # Blue + '#00ff00', # Green + '#ff0000', # Red + '#000000', # Black + + '#00ffff', # Cyan/Aqua + '#ff00ff', # Magenta/Fuchsia + '#ffff00', # Yellow + '#8b4513', # SaddleBrown + + '#7b68ee', # MediumSlateBlue + '#ffc0cb', # Pink + '#bdb76b', # DarkKhaki + '#008080', # Teal + + '#bc8f8f', # RosyBrown + '#000080', # Navy(Blue) + '#dc143c', # Crimson + '#800080', # Purple + + '#daa520', # Goldenrod + '#40e0d0', # Turquoise + '#00bfff', # DeepSkyBlue + '#c0c0c0', # Silver + ]; + + + def __init__(self, sId, oData, oDisp): + self._sId = sId; + self._oData = oData; + self._oDisp = oDisp; + # Graph output dimensions. + self._cxGraph = 1024; + self._cyGraph = 448; + self._cDpiGraph = 96; + # Other graph attributes + self._sTitle = None; + self._cPtFont = 8; + + def headerContent(self): + """ + Returns content that goes into the HTML header. + """ + return ''; + + def renderGraph(self): + """ + Renders the graph. + Returning HTML. + """ + return '<p>renderGraph needs to be overridden by the child class!</p>'; + + def setTitle(self, sTitle): + """ Sets the graph title. """ + self._sTitle = sTitle; + return True; + + def setWidth(self, cx): + """ Sets the graph width. """ + self._cxGraph = cx; + return True; + + def setHeight(self, cy): + """ Sets the graph height. """ + self._cyGraph = cy; + return True; + + def setDpi(self, cDotsPerInch): + """ Sets the graph DPI. """ + self._cDpiGraph = cDotsPerInch; + return True; + + def setFontSize(self, cPtFont): + """ Sets the default font size. """ + self._cPtFont = cPtFont; + return True; + + + @staticmethod + def calcSeriesColor(iSeries): + """ Returns a #rrggbb color code for the given series. """ + return WuiHlpGraphBase.kasColors[iSeries % len(WuiHlpGraphBase.kasColors)]; diff --git a/src/VBox/ValidationKit/testmanager/webui/wuihlpgraphgooglechart.py b/src/VBox/ValidationKit/testmanager/webui/wuihlpgraphgooglechart.py new file mode 100755 index 00000000..b6861603 --- /dev/null +++ b/src/VBox/ValidationKit/testmanager/webui/wuihlpgraphgooglechart.py @@ -0,0 +1,376 @@ +# -*- coding: utf-8 -*- +# $Id: wuihlpgraphgooglechart.py $ + +""" +Test Manager Web-UI - Graph Helpers - Implemented using Google Charts. +""" + +__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 $" + +# Validation Kit imports. +from common import utils, webutils; +from testmanager.webui.wuihlpgraphbase import WuiHlpGraphBase; + + +#******************************************************************************* +#* Global Variables * +#******************************************************************************* +g_cGraphs = 0; + +class WuiHlpGraphGoogleChartsBase(WuiHlpGraphBase): + """ Base class for the Google Charts graphs. """ + pass; # pylint: disable=unnecessary-pass + + +class WuiHlpBarGraph(WuiHlpGraphGoogleChartsBase): + """ + Bar graph. + """ + + def __init__(self, sId, oData, oDisp = None): + WuiHlpGraphGoogleChartsBase.__init__(self, sId, oData, oDisp); + self.fpMax = None; + self.fpMin = 0.0; + self.fYInverted = False; + + def setRangeMax(self, fpMax): + """ Sets the max range.""" + self.fpMax = float(fpMax); + return None; + + def invertYDirection(self): + """ Inverts the direction of the Y-axis direction. """ + self.fYInverted = True; + return None; + + def renderGraph(self): + aoTable = self._oData.aoTable # type: WuiHlpGraphDataTable + + # Seems material (google.charts.Bar) cannot change the direction on the Y-axis, + # so we cannot get bars growing downwards from the top like we want for the + # reports. The classic charts OTOH cannot put X-axis labels on the top, but + # we just drop them all together instead, saving a little space. + fUseMaterial = False; + + # Unique on load function. + global g_cGraphs; + iGraph = g_cGraphs; + g_cGraphs += 1; + + sHtml = '<div id="%s">\n' % ( self._sId, ); + sHtml += '<script type="text/javascript" src="https://www.gstatic.com/charts/loader.js"></script>\n' \ + '<script type="text/javascript">\n' \ + 'google.charts.load("current", { packages: ["corechart", "bar"] });\n' \ + 'google.setOnLoadCallback(tmDrawBarGraph%u);\n' \ + 'function tmDrawBarGraph%u()\n' \ + '{\n' \ + ' var oGraph;\n' \ + ' var dGraphOptions = \n' \ + ' {\n' \ + ' "title": "%s",\n' \ + ' "hAxis": {\n' \ + ' "title": "%s",\n' \ + ' },\n' \ + ' "vAxis": {\n' \ + ' "direction": %s,\n' \ + ' },\n' \ + % ( iGraph, + iGraph, + webutils.escapeAttrJavaScriptStringDQ(self._sTitle) if self._sTitle is not None else '', + webutils.escapeAttrJavaScriptStringDQ(aoTable[0].sName) if aoTable and aoTable[0].sName else '', + '-1' if self.fYInverted else '1', + ); + if fUseMaterial and self.fYInverted: + sHtml += ' "axes": { "x": { 0: { "side": "top" } }, "y": { "0": { "direction": -1, }, }, },\n'; + sHtml += ' };\n'; + + # The data. + if self._oData.fHasStringValues and len(aoTable) > 1: + sHtml += ' var oData = new google.visualization.DataTable();\n'; + # Column definitions. + sHtml += ' oData.addColumn("string", "%s");\n' \ + % (webutils.escapeAttrJavaScriptStringDQ(aoTable[0].sName) if aoTable[0].sName else '',); + for iValue, oValue in enumerate(aoTable[0].aoValues): + oSampleValue = aoTable[1].aoValues[iValue]; + if utils.isString(oSampleValue): + sHtml += ' oData.addColumn("string", "%s");\n' % (webutils.escapeAttrJavaScriptStringDQ(oValue),); + else: + sHtml += ' oData.addColumn("number", "%s");\n' % (webutils.escapeAttrJavaScriptStringDQ(oValue),); + sHtml += ' oData.addColumn({type: "string", role: "annotation"});\n'; + # The data rows. + sHtml += ' oData.addRows([\n'; + for oRow in aoTable[1:]: + if oRow.sName: + sRow = ' [ "%s"' % (webutils.escapeAttrJavaScriptStringDQ(oRow.sName),); + else: + sRow = ' [ null'; + for iValue, oValue in enumerate(oRow.aoValues): + if not utils.isString(oValue): + sRow += ', %s' % (oValue,); + else: + sRow += ', "%s"' % (webutils.escapeAttrJavaScriptStringDQ(oValue),); + if oRow.asValues[iValue]: + sRow += ', "%s"' % (webutils.escapeAttrJavaScriptStringDQ(oRow.asValues[iValue]),); + else: + sRow += ', null'; + sHtml += sRow + '],\n'; + sHtml += ' ]);\n'; + else: + sHtml += ' var oData = google.visualization.arrayToDataTable([\n'; + for oRow in aoTable: + sRow = ' [ "%s"' % (webutils.escapeAttrJavaScriptStringDQ(oRow.sName),); + for oValue in oRow.aoValues: + if utils.isString(oValue): + sRow += ', "%s"' % (webutils.escapeAttrJavaScriptStringDQ(oValue),); + else: + sRow += ', %s' % (oValue,); + sHtml += sRow + '],\n'; + sHtml += ' ]);\n'; + + # Create and draw. + if not fUseMaterial: + sHtml += ' oGraph = new google.visualization.ColumnChart(document.getElementById("%s"));\n' \ + ' oGraph.draw(oData, dGraphOptions);\n' \ + % ( self._sId, ); + else: + sHtml += ' oGraph = new google.charts.Bar(document.getElementById("%s"));\n' \ + ' oGraph.draw(oData, google.charts.Bar.convertOptions(dGraphOptions));\n' \ + % ( self._sId, ); + + # clean and return. + sHtml += ' oData = null;\n' \ + ' return true;\n' \ + '};\n'; + + sHtml += '</script>\n' \ + '</div>\n'; + return sHtml; + + +class WuiHlpLineGraph(WuiHlpGraphGoogleChartsBase): + """ + Line graph. + """ + + ## @todo implement error bars. + kfNoErrorBarsSupport = True; + + def __init__(self, sId, oData, oDisp = None, fErrorBarY = False): + # oData must be a WuiHlpGraphDataTableEx like object. + WuiHlpGraphGoogleChartsBase.__init__(self, sId, oData, oDisp); + self._cMaxErrorBars = 12; + self._fErrorBarY = fErrorBarY; + + def setErrorBarY(self, fEnable): + """ Enables or Disables error bars, making this work like a line graph. """ + self._fErrorBarY = fEnable; + return True; + + def renderGraph(self): # pylint: disable=too-many-locals + fSlideFilter = True; + + # Tooltips? + cTooltips = 0; + for oSeries in self._oData.aoSeries: + cTooltips += oSeries.asHtmlTooltips is not None; + + # Unique on load function. + global g_cGraphs; + iGraph = g_cGraphs; + g_cGraphs += 1; + + sHtml = '<div id="%s">\n' % ( self._sId, ); + if fSlideFilter: + sHtml += ' <table><tr><td><div id="%s_graph"/></td></tr><tr><td><div id="%s_filter"/></td></tr></table>\n' \ + % ( self._sId, self._sId, ); + + sHtml += '<script type="text/javascript" src="https://www.google.com/jsapi"></script>\n' \ + '<script type="text/javascript">\n' \ + 'google.load("visualization", "1.0", { packages: ["corechart"%s] });\n' \ + 'google.setOnLoadCallback(tmDrawLineGraph%u);\n' \ + 'function tmDrawLineGraph%u()\n' \ + '{\n' \ + ' var fnResize;\n' \ + ' var fnRedraw;\n' \ + ' var idRedrawTimer = null;\n' \ + ' var cxCur = getElementWidthById("%s") - 20;\n' \ + ' var oGraph;\n' \ + ' var oData = new google.visualization.DataTable();\n' \ + ' var fpXYRatio = %u / %u;\n' \ + ' var dGraphOptions = \n' \ + ' {\n' \ + ' "title": "%s",\n' \ + ' "width": cxCur,\n' \ + ' "height": Math.round(cxCur / fpXYRatio),\n' \ + ' "pointSize": 2,\n' \ + ' "fontSize": %u,\n' \ + ' "hAxis": { "title": "%s", "minorGridlines": { count: 5 }},\n' \ + ' "vAxis": { "title": "%s", "minorGridlines": { count: 5 }},\n' \ + ' "theme": "maximized",\n' \ + ' "tooltip": { "isHtml": %s }\n' \ + ' };\n' \ + % ( ', "controls"' if fSlideFilter else '', + iGraph, + iGraph, + self._sId, + self._cxGraph, self._cyGraph, + self._sTitle if self._sTitle is not None else '', + self._cPtFont * self._cDpiGraph / 72, # fudge + self._oData.sXUnit if self._oData.sXUnit else '', + self._oData.sYUnit if self._oData.sYUnit else '', + 'true' if cTooltips > 0 else 'false', + ); + if fSlideFilter: + sHtml += ' var oDashboard = new google.visualization.Dashboard(document.getElementById("%s"));\n' \ + ' var oSlide = new google.visualization.ControlWrapper({\n' \ + ' "controlType": "NumberRangeFilter",\n' \ + ' "containerId": "%s_filter",\n' \ + ' "options": {\n' \ + ' "filterColumnIndex": 0,\n' \ + ' "ui": { "width": getElementWidthById("%s") / 2 }, \n' \ + ' }\n' \ + ' });\n' \ + % ( self._sId, + self._sId, + self._sId,); + + # Data variables. + for iSeries, oSeries in enumerate(self._oData.aoSeries): + sHtml += ' var aSeries%u = [\n' % (iSeries,); + if oSeries.asHtmlTooltips is None: + sHtml += '[%s,%s]' % ( oSeries.aoXValues[0], oSeries.aoYValues[0],); + for i in range(1, len(oSeries.aoXValues)): + if (i & 16) == 0: sHtml += '\n'; + sHtml += ',[%s,%s]' % ( oSeries.aoXValues[i], oSeries.aoYValues[i], ); + else: + sHtml += '[%s,%s,"%s"]' \ + % ( oSeries.aoXValues[0], oSeries.aoYValues[0], + webutils.escapeAttrJavaScriptStringDQ(oSeries.asHtmlTooltips[0]),); + for i in range(1, len(oSeries.aoXValues)): + if (i & 16) == 0: sHtml += '\n'; + sHtml += ',[%s,%s,"%s"]' \ + % ( oSeries.aoXValues[i], oSeries.aoYValues[i], + webutils.escapeAttrJavaScriptStringDQ(oSeries.asHtmlTooltips[i]),); + + sHtml += '];\n' + + sHtml += ' oData.addColumn("number", "%s");\n' % (self._oData.sXUnit if self._oData.sXUnit else '',); + cVColumns = 0; + for oSeries in self._oData.aoSeries: + sHtml += ' oData.addColumn("number", "%s");\n' % (oSeries.sName,); + if oSeries.asHtmlTooltips: + sHtml += ' oData.addColumn({"type": "string", "role": "tooltip", "p": {"html": true}});\n'; + cVColumns += 1; + cVColumns += 1; + sHtml += 'var i;\n' + + cVColumsDone = 0; + for iSeries, oSeries in enumerate(self._oData.aoSeries): + sVar = 'aSeries%u' % (iSeries,); + sHtml += ' for (i = 0; i < %s.length; i++)\n' \ + ' {\n' \ + ' oData.addRow([%s[i][0]%s,%s[i][1]%s%s]);\n' \ + % ( sVar, + sVar, + ',null' * cVColumsDone, + sVar, + '' if oSeries.asHtmlTooltips is None else ',%s[i][2]' % (sVar,), + ',null' * (cVColumns - cVColumsDone - 1 - (oSeries.asHtmlTooltips is not None)), + ); + sHtml += ' }\n' \ + ' %s = null\n' \ + % (sVar,); + cVColumsDone += 1 + (oSeries.asHtmlTooltips is not None); + + # Create and draw. + if fSlideFilter: + sHtml += ' oGraph = new google.visualization.ChartWrapper({\n' \ + ' "chartType": "LineChart",\n' \ + ' "containerId": "%s_graph",\n' \ + ' "options": dGraphOptions\n' \ + ' });\n' \ + ' oDashboard.bind(oSlide, oGraph);\n' \ + ' oDashboard.draw(oData);\n' \ + % ( self._sId, ); + else: + sHtml += ' oGraph = new google.visualization.LineChart(document.getElementById("%s"));\n' \ + ' oGraph.draw(oData, dGraphOptions);\n' \ + % ( self._sId, ); + + # Register a resize handler for redrawing the graph, using a timer to delay it. + sHtml += ' fnRedraw = function() {\n' \ + ' var cxNew = getElementWidthById("%s") - 6;\n' \ + ' if (Math.abs(cxNew - cxCur) > 8)\n' \ + ' {\n' \ + ' cxCur = cxNew;\n' \ + ' dGraphOptions["width"] = cxNew;\n' \ + ' dGraphOptions["height"] = Math.round(cxNew / fpXYRatio);\n' \ + ' oGraph.draw(oData, dGraphOptions);\n' \ + ' }\n' \ + ' clearTimeout(idRedrawTimer);\n' \ + ' idRedrawTimer = null;\n' \ + ' return true;\n' \ + ' };\n' \ + ' fnResize = function() {\n' \ + ' if (idRedrawTimer != null) { clearTimeout(idRedrawTimer); } \n' \ + ' idRedrawTimer = setTimeout(fnRedraw, 512);\n' \ + ' return true;\n' \ + ' };\n' \ + ' if (window.attachEvent)\n' \ + ' { window.attachEvent("onresize", fnResize); }\n' \ + ' else if (window.addEventListener)\n' \ + ' { window.addEventListener("resize", fnResize, true); }\n' \ + % ( self._sId, ); + + # clean up what the callbacks don't need. + sHtml += ' oData = null;\n' \ + ' aaaSeries = null;\n'; + + # done; + sHtml += ' return true;\n' \ + '};\n'; + + sHtml += '</script>\n' \ + '</div>\n'; + return sHtml; + + +class WuiHlpLineGraphErrorbarY(WuiHlpLineGraph): + """ + Line graph with an errorbar for the Y axis. + """ + + def __init__(self, sId, oData, oDisp = None): + WuiHlpLineGraph.__init__(self, sId, oData, fErrorBarY = True); + diff --git a/src/VBox/ValidationKit/testmanager/webui/wuihlpgraphmatplotlib.py b/src/VBox/ValidationKit/testmanager/webui/wuihlpgraphmatplotlib.py new file mode 100755 index 00000000..cb5cf893 --- /dev/null +++ b/src/VBox/ValidationKit/testmanager/webui/wuihlpgraphmatplotlib.py @@ -0,0 +1,341 @@ +# -*- coding: utf-8 -*- +# $Id: wuihlpgraphmatplotlib.py $ + +""" +Test Manager Web-UI - Graph Helpers - Implemented using matplotlib. +""" + +__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 Import and extensions installed on the system. +import re; +import sys; +if sys.version_info[0] >= 3: + from io import StringIO as StringIO; # pylint: disable=import-error,no-name-in-module,useless-import-alias +else: + from StringIO import StringIO as StringIO; # pylint: disable=import-error,no-name-in-module,useless-import-alias + +import matplotlib; # pylint: disable=import-error +matplotlib.use('Agg'); # Force backend. +import matplotlib.pyplot; # pylint: disable=import-error +from numpy import arange as numpy_arange; # pylint: disable=no-name-in-module,import-error,wrong-import-order + +# Validation Kit imports. +from testmanager.webui.wuihlpgraphbase import WuiHlpGraphBase; + + +class WuiHlpGraphMatplotlibBase(WuiHlpGraphBase): + """ Base class for the matplotlib graphs. """ + + def __init__(self, sId, oData, oDisp): + WuiHlpGraphBase.__init__(self, sId, oData, oDisp); + self._fXkcdStyle = True; + + def setXkcdStyle(self, fEnabled = True): + """ Enables xkcd style graphs for implementations that supports it. """ + self._fXkcdStyle = fEnabled; + return True; + + def _createFigure(self): + """ + Wrapper around matplotlib.pyplot.figure that feeds the figure the + basic graph configuration. + """ + if self._fXkcdStyle and matplotlib.__version__ > '1.2.9': + matplotlib.pyplot.xkcd(); # pylint: disable=no-member + matplotlib.rcParams.update({'font.size': self._cPtFont}); + + oFigure = matplotlib.pyplot.figure(figsize = (float(self._cxGraph) / self._cDpiGraph, + float(self._cyGraph) / self._cDpiGraph), + dpi = self._cDpiGraph); + return oFigure; + + def _produceSvg(self, oFigure, fTightLayout = True): + """ Creates an SVG string from the given figure. """ + oOutput = StringIO(); + if fTightLayout: + oFigure.tight_layout(); + oFigure.savefig(oOutput, format = 'svg'); + + if self._oDisp and self._oDisp.isBrowserGecko('20100101'): + # This browser will stretch images to fit if no size or width is given. + sSubstitute = r'\1 \3 reserveAspectRatio="xMidYMin meet"'; + else: + # Chrome and IE likes to have the sizes as well as the viewBox. + sSubstitute = r'\1 \3 reserveAspectRatio="xMidYMin meet" \2 \4'; + return re.sub(r'(<svg) (height="\d+pt") (version="\d+.\d+" viewBox="\d+ \d+ \d+ \d+") (width="\d+pt")', + sSubstitute, + oOutput.getvalue().decode('utf8'), + count = 1); + +class WuiHlpBarGraph(WuiHlpGraphMatplotlibBase): + """ + Bar graph. + """ + + def __init__(self, sId, oData, oDisp = None): + WuiHlpGraphMatplotlibBase.__init__(self, sId, oData, oDisp); + self.fpMax = None; + self.fpMin = 0.0; + self.cxBarWidth = None; + + def setRangeMax(self, fpMax): + """ Sets the max range.""" + self.fpMax = float(fpMax); + return None; + + def invertYDirection(self): + """ Inverts the direction of the Y-axis direction. """ + ## @todo self.fYInverted = True; + return None; + + def renderGraph(self): # pylint: disable=too-many-locals + aoTable = self._oData.aoTable; + + # + # Extract/structure the required data. + # + aoSeries = []; + for j in range(len(aoTable[1].aoValues)): + aoSeries.append([]); + asNames = []; + oXRange = numpy_arange(self._oData.getGroupCount()); + fpMin = self.fpMin; + fpMax = self.fpMax; + if self.fpMax is None: + fpMax = float(aoTable[1].aoValues[0]); + + for i in range(1, len(aoTable)): + asNames.append(aoTable[i].sName); + for j, oValue in enumerate(aoTable[i].aoValues): + fpValue = float(oValue); + aoSeries[j].append(fpValue); + if fpValue < fpMin: + fpMin = fpValue; + if fpValue > fpMax: + fpMax = fpValue; + + fpMid = fpMin + (fpMax - fpMin) / 2.0; + + if self.cxBarWidth is None: + self.cxBarWidth = 1.0 / (len(aoTable[0].asValues) + 1.1); + + # Render the PNG. + oFigure = self._createFigure(); + oSubPlot = oFigure.add_subplot(1, 1, 1); + + aoBars = []; + for i, _ in enumerate(aoSeries): + sColor = self.calcSeriesColor(i); + aoBars.append(oSubPlot.bar(oXRange + self.cxBarWidth * i, + aoSeries[i], + self.cxBarWidth, + color = sColor, + align = 'edge')); + + #oSubPlot.set_title('Title') + #oSubPlot.set_xlabel('X-axis') + #oSubPlot.set_xticks(oXRange + self.cxBarWidth); + oSubPlot.set_xticks(oXRange); + oLegend = oSubPlot.legend(aoTable[0].asValues, loc = 'best', fancybox = True); + oLegend.get_frame().set_alpha(0.5); + oSubPlot.set_xticklabels(asNames, ha = "left"); + #oSubPlot.set_ylabel('Y-axis') + oSubPlot.set_yticks(numpy_arange(fpMin, fpMax + (fpMax - fpMin) / 10 * 0, fpMax / 10)); + oSubPlot.grid(True); + fpPadding = (fpMax - fpMin) * 0.02; + for i, _ in enumerate(aoBars): + aoRects = aoBars[i] + for j, _ in enumerate(aoRects): + oRect = aoRects[j]; + fpValue = float(aoTable[j + 1].aoValues[i]); + if fpValue <= fpMid: + oSubPlot.text(oRect.get_x() + oRect.get_width() / 2.0, + oRect.get_height() + fpPadding, + aoTable[j + 1].asValues[i], + ha = 'center', va = 'bottom', rotation = 'vertical', alpha = 0.6, fontsize = 'small'); + else: + oSubPlot.text(oRect.get_x() + oRect.get_width() / 2.0, + oRect.get_height() - fpPadding, + aoTable[j + 1].asValues[i], + ha = 'center', va = 'top', rotation = 'vertical', alpha = 0.6, fontsize = 'small'); + + return self._produceSvg(oFigure); + + + + +class WuiHlpLineGraph(WuiHlpGraphMatplotlibBase): + """ + Line graph. + """ + + def __init__(self, sId, oData, oDisp = None, fErrorBarY = False): + # oData must be a WuiHlpGraphDataTableEx like object. + WuiHlpGraphMatplotlibBase.__init__(self, sId, oData, oDisp); + self._cMaxErrorBars = 12; + self._fErrorBarY = fErrorBarY; + + def setErrorBarY(self, fEnable): + """ Enables or Disables error bars, making this work like a line graph. """ + self._fErrorBarY = fEnable; + return True; + + def renderGraph(self): # pylint: disable=too-many-locals + aoSeries = self._oData.aoSeries; + + oFigure = self._createFigure(); + oSubPlot = oFigure.add_subplot(1, 1, 1); + if self._oData.sYUnit is not None: + oSubPlot.set_ylabel(self._oData.sYUnit); + if self._oData.sXUnit is not None: + oSubPlot.set_xlabel(self._oData.sXUnit); + + cSeriesNames = 0; + cYMin = 1000; + cYMax = 0; + for iSeries, oSeries in enumerate(aoSeries): + sColor = self.calcSeriesColor(iSeries); + cYMin = min(cYMin, min(oSeries.aoYValues)); + cYMax = max(cYMax, max(oSeries.aoYValues)); + if not self._fErrorBarY: + oSubPlot.errorbar(oSeries.aoXValues, oSeries.aoYValues, color = sColor); + elif len(oSeries.aoXValues) > self._cMaxErrorBars: + if matplotlib.__version__ < '1.3.0': + oSubPlot.errorbar(oSeries.aoXValues, oSeries.aoYValues, color = sColor); + else: + oSubPlot.errorbar(oSeries.aoXValues, oSeries.aoYValues, + yerr = [oSeries.aoYErrorBarBelow, oSeries.aoYErrorBarAbove], + errorevery = len(oSeries.aoXValues) / self._cMaxErrorBars, + color = sColor ); + else: + oSubPlot.errorbar(oSeries.aoXValues, oSeries.aoYValues, + yerr = [oSeries.aoYErrorBarBelow, oSeries.aoYErrorBarAbove], + color = sColor); + cSeriesNames += oSeries.sName is not None; + + if cYMin != 0 or cYMax != 0: + oSubPlot.set_ylim(bottom = 0); + + if cSeriesNames > 0: + oLegend = oSubPlot.legend([oSeries.sName for oSeries in aoSeries], loc = 'best', fancybox = True); + oLegend.get_frame().set_alpha(0.5); + + if self._sTitle is not None: + oSubPlot.set_title(self._sTitle); + + if self._cxGraph >= 256: + oSubPlot.minorticks_on(); + oSubPlot.grid(True, 'major', axis = 'both'); + oSubPlot.grid(True, 'both', axis = 'x'); + + if True: # pylint: disable=using-constant-test + # oSubPlot.axis('off'); + #oSubPlot.grid(True, 'major', axis = 'none'); + #oSubPlot.grid(True, 'both', axis = 'none'); + matplotlib.pyplot.setp(oSubPlot, xticks = [], yticks = []); + + return self._produceSvg(oFigure); + + +class WuiHlpLineGraphErrorbarY(WuiHlpLineGraph): + """ + Line graph with an errorbar for the Y axis. + """ + + def __init__(self, sId, oData, oDisp = None): + WuiHlpLineGraph.__init__(self, sId, oData, fErrorBarY = True); + + +class WuiHlpMiniSuccessRateGraph(WuiHlpGraphMatplotlibBase): + """ + Mini rate graph. + """ + + def __init__(self, sId, oData, oDisp = None): + """ + oData must be a WuiHlpGraphDataTableEx like object, but only aoSeries, + aoSeries[].aoXValues, and aoSeries[].aoYValues will be used. The + values are expected to be a percentage, i.e. values between 0 and 100. + """ + WuiHlpGraphMatplotlibBase.__init__(self, sId, oData, oDisp); + self.setFontSize(6); + + def renderGraph(self): # pylint: disable=too-many-locals + assert len(self._oData.aoSeries) == 1; + oSeries = self._oData.aoSeries[0]; + + # hacking + #self.setWidth(512); + #self.setHeight(128); + # end + + oFigure = self._createFigure(); + from mpl_toolkits.axes_grid.axislines import SubplotZero; # pylint: disable=import-error + oAxis = SubplotZero(oFigure, 111); + oFigure.add_subplot(oAxis); + + # Disable all the normal axis. + oAxis.axis['right'].set_visible(False) + oAxis.axis['top'].set_visible(False) + oAxis.axis['bottom'].set_visible(False) + oAxis.axis['left'].set_visible(False) + + # Use the zero axis instead. + oAxis.axis['yzero'].set_axisline_style('-|>'); + oAxis.axis['yzero'].set_visible(True); + oAxis.axis['xzero'].set_axisline_style('-|>'); + oAxis.axis['xzero'].set_visible(True); + + if oSeries.aoYValues[-1] == 100: + sColor = 'green'; + elif oSeries.aoYValues[-1] > 75: + sColor = 'yellow'; + else: + sColor = 'red'; + oAxis.plot(oSeries.aoXValues, oSeries.aoYValues, '.-', color = sColor, linewidth = 3); + oAxis.fill_between(oSeries.aoXValues, oSeries.aoYValues, facecolor = sColor, alpha = 0.5) + + oAxis.set_xlim(left = -0.01); + oAxis.set_xticklabels([]); + oAxis.set_xmargin(1); + + oAxis.set_ylim(bottom = 0, top = 100); + oAxis.set_yticks([0, 50, 100]); + oAxis.set_ylabel('%'); + #oAxis.set_yticklabels([]); + oAxis.set_yticklabels(['', '%', '']); + + return self._produceSvg(oFigure, False); + diff --git a/src/VBox/ValidationKit/testmanager/webui/wuihlpgraphsimple.py b/src/VBox/ValidationKit/testmanager/webui/wuihlpgraphsimple.py new file mode 100755 index 00000000..9be0b565 --- /dev/null +++ b/src/VBox/ValidationKit/testmanager/webui/wuihlpgraphsimple.py @@ -0,0 +1,163 @@ +# -*- coding: utf-8 -*- +# $Id: wuihlpgraphsimple.py $ + +""" +Test Manager Web-UI - Graph Helpers - Simple/Stub Implementation. +""" + +__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 $" + +# Validation Kit imports. +from common.webutils import escapeAttr, escapeElem; +from testmanager.webui.wuihlpgraphbase import WuiHlpGraphBase; + + + +class WuiHlpBarGraph(WuiHlpGraphBase): + """ + Bar graph. + """ + + def __init__(self, sId, oData, oDisp = None): + WuiHlpGraphBase.__init__(self, sId, oData, oDisp); + self.cxMaxBar = 480; + self.fpMax = None; + self.fpMin = 0.0; + + def setRangeMax(self, fpMax): + """ Sets the max range.""" + self.fpMax = float(fpMax); + return None; + + def invertYDirection(self): + """ Not supported. """ + return None; + + def renderGraph(self): + aoTable = self._oData.aoTable; + sReport = '<div class="tmbargraph">\n'; + + # Figure the range. + fpMin = self.fpMin; + fpMax = self.fpMax; + if self.fpMax is None: + fpMax = float(aoTable[1].aoValues[0]); + for i in range(1, len(aoTable)): + for oValue in aoTable[i].aoValues: + fpValue = float(oValue); + if fpValue < fpMin: + fpMin = fpValue; + if fpValue > fpMax: + fpMax = fpValue; + assert fpMin >= 0; + + # Format the data. + sReport += '<table class="tmbargraphl1" border="1" id="%s">\n' % (escapeAttr(self._sId),); + for i in range(1, len(aoTable)): + oRow = aoTable[i]; + sReport += ' <tr>\n' \ + ' <td>%s</td>\n' \ + ' <td height="100%%" width="%spx">\n' \ + ' <table class="tmbargraphl2" height="100%%" width="100%%" ' \ + 'border="0" cellspacing="0" cellpadding="0">\n' \ + % (escapeElem(oRow.sName), escapeAttr(str(self.cxMaxBar + 2))); + for j, oValue in enumerate(oRow.aoValues): + cPct = int(float(oValue) * 100 / fpMax); + cxBar = int(float(oValue) * self.cxMaxBar / fpMax); + sValue = escapeElem(oRow.asValues[j]); + sColor = self.kasColors[j % len(self.kasColors)]; + sInvColor = 'white'; + if sColor[0] == '#' and len(sColor) == 7: + sInvColor = '#%06x' % (~int(sColor[1:],16) & 0xffffff,); + + sReport += ' <tr><td>\n' \ + ' <table class="tmbargraphl3" height="100%%" border="0" cellspacing="0" cellpadding="0">\n' \ + ' <tr>\n'; + if cPct >= 99: + sReport += ' <td width="%spx" nowrap bgcolor="%s" align="right" style="color:%s;">' \ + '%s </td>\n' \ + % (cxBar, sColor, sInvColor, sValue); + elif cPct < 1: + sReport += ' <td width="%spx" nowrap style="color:%s;">%s</td>\n' \ + % (self.cxMaxBar - cxBar, sColor, sValue); + elif cPct >= 50: + sReport += ' <td width="%spx" nowrap bgcolor="%s" align="right" style="color:%s;">' \ + '%s </td>\n' \ + ' <td width="%spx" nowrap><div> </div></td>\n' \ + % (cxBar, sColor, sInvColor, sValue, self.cxMaxBar - cxBar); + else: + sReport += ' <td width="%spx" nowrap bgcolor="%s"></td>\n' \ + ' <td width="%spx" nowrap> %s</td>\n' \ + % (cxBar, sColor, self.cxMaxBar - cxBar, sValue); + sReport += ' </tr>\n' \ + ' </table>\n' \ + ' </td></tr>\n' + sReport += ' </table>\n' \ + ' </td>\n' \ + ' </tr>\n'; + if i + 1 < len(aoTable) and len(oRow.aoValues) > 1: + sReport += ' <tr></tr>\n' + + sReport += '</table>\n'; + + sReport += '<div class="tmgraphlegend">\n' \ + ' <p>Legend:\n'; + for j, sValue in enumerate(aoTable[0].asValues): + sColor = self.kasColors[j % len(self.kasColors)]; + sReport += ' <font color="%s">■ %s</font>\n' % (sColor, escapeElem(sValue),); + sReport += ' </p>\n' \ + '</div>\n'; + + sReport += '</div>\n'; + return sReport; + + + + +class WuiHlpLineGraph(WuiHlpGraphBase): + """ + Line graph. + """ + + def __init__(self, sId, oData, oDisp): + WuiHlpGraphBase.__init__(self, sId, oData, oDisp); + + +class WuiHlpLineGraphErrorbarY(WuiHlpLineGraph): + """ + Line graph with an errorbar for the Y axis. + """ + + pass; # pylint: disable=unnecessary-pass + diff --git a/src/VBox/ValidationKit/testmanager/webui/wuilogviewer.py b/src/VBox/ValidationKit/testmanager/webui/wuilogviewer.py new file mode 100755 index 00000000..45a847ed --- /dev/null +++ b/src/VBox/ValidationKit/testmanager/webui/wuilogviewer.py @@ -0,0 +1,251 @@ +# -*- coding: utf-8 -*- +# $Id: wuilogviewer.py $ + +""" +Test Manager WUI - Log viewer +""" + +__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 $" + +# Validation Kit imports. +from common import webutils; +from testmanager.core.testset import TestSetData; +from testmanager.webui.wuicontentbase import WuiContentBase, WuiTmLink; +from testmanager.webui.wuimain import WuiMain; + + +class WuiLogViewer(WuiContentBase): + """Log viewer.""" + + def __init__(self, oTestSet, oLogFile, cbChunk, iChunk, aoTimestamps, oDisp = None, fnDPrint = None): + WuiContentBase.__init__(self, oDisp = oDisp, fnDPrint = fnDPrint); + self._oTestSet = oTestSet; + self._oLogFile = oLogFile; + self._cbChunk = cbChunk; + self._iChunk = iChunk; + self._aoTimestamps = aoTimestamps; + + def _generateNavigation(self, cbFile): + """Generate the HTML for the log navigation.""" + + dParams = { + WuiMain.ksParamAction: WuiMain.ksActionViewLog, + WuiMain.ksParamLogSetId: self._oTestSet.idTestSet, + WuiMain.ksParamLogFileId: self._oLogFile.idTestResultFile, + WuiMain.ksParamLogChunkSize: self._cbChunk, + WuiMain.ksParamLogChunkNo: self._iChunk, + }; + + # + # The page walker. + # + dParams2 = dict(dParams); + del dParams2[WuiMain.ksParamLogChunkNo]; + sHrefFmt = '<a href="?%s&%s=%%s" title="%%s">%%s</a>' \ + % (webutils.encodeUrlParams(dParams2).replace('%', '%%'), WuiMain.ksParamLogChunkNo,); + sHtmlWalker = self.genericPageWalker(self._iChunk, (cbFile + self._cbChunk - 1) // self._cbChunk, + sHrefFmt, 11, 0, 'chunk'); + + # + # The chunk size selector. + # + + dParams2 = dict(dParams); + del dParams2[WuiMain.ksParamLogChunkSize]; + sHtmlSize = '<form name="ChunkSizeForm" method="GET">\n' \ + ' Max <select name="%s" onchange="window.location=\'?%s&%s=\' + ' \ + 'this.options[this.selectedIndex].value;" title="Max items per page">\n' \ + % ( WuiMain.ksParamLogChunkSize, webutils.encodeUrlParams(dParams2), WuiMain.ksParamLogChunkSize,); + + for cbChunk in [ 256, 512, 1024, 2048, 4096, 8192, 16384, 32768, 65536, 131072, 262144, 524288, 1048576, 2097152, + 4194304, 8388608, 16777216 ]: + sHtmlSize += ' <option value="%d" %s>%d bytes</option>\n' \ + % (cbChunk, 'selected="selected"' if cbChunk == self._cbChunk else '', cbChunk); + sHtmlSize += ' </select> per page\n' \ + '</form>\n' + + # + # Download links. + # + oRawLink = WuiTmLink('View Raw', '', + { WuiMain.ksParamAction: WuiMain.ksActionGetFile, + WuiMain.ksParamGetFileSetId: self._oTestSet.idTestSet, + WuiMain.ksParamGetFileId: self._oLogFile.idTestResultFile, + WuiMain.ksParamGetFileDownloadIt: False, + }, + sTitle = '%u MiB' % ((cbFile + 1048576 - 1) // 1048576,) ); + oDownloadLink = WuiTmLink('Download Log', '', + { WuiMain.ksParamAction: WuiMain.ksActionGetFile, + WuiMain.ksParamGetFileSetId: self._oTestSet.idTestSet, + WuiMain.ksParamGetFileId: self._oLogFile.idTestResultFile, + WuiMain.ksParamGetFileDownloadIt: True, + }, + sTitle = '%u MiB' % ((cbFile + 1048576 - 1) // 1048576,) ); + oTestSetLink = WuiTmLink('Test Set', '', + { WuiMain.ksParamAction: WuiMain.ksActionTestResultDetails, + TestSetData.ksParam_idTestSet: self._oTestSet.idTestSet, + }); + + + # + # Combine the elements and return. + # + return '<div class="tmlogviewernavi">\n' \ + ' <table width=100%>\n' \ + ' <tr>\n' \ + ' <td width=20%>\n' \ + ' ' + oTestSetLink.toHtml() + '\n' \ + ' ' + oRawLink.toHtml() + '\n' \ + ' ' + oDownloadLink.toHtml() + '\n' \ + ' </td>\n' \ + ' <td width=60% align=center>' + sHtmlWalker + '</td>' \ + ' <td width=20% align=right>' + sHtmlSize + '</td>\n' \ + ' </tr>\n' \ + ' </table>\n' \ + '</div>\n'; + + def _displayLog(self, oFile, offFile, cbFile, aoTimestamps): + """Displays the current section of the log file.""" + from testmanager.core import db; + + def prepCurTs(): + """ Formats the current timestamp. """ + if iCurTs < len(aoTimestamps): + oTsZulu = db.dbTimestampToZuluDatetime(aoTimestamps[iCurTs]); + return (oTsZulu.strftime('%H:%M:%S.%f'), oTsZulu.strftime('%H_%M_%S_%f')); + return ('~~|~~|~~|~~~~~~', '~~|~~|~~|~~~~~~'); # ASCII chars with high values. Limit hits. + + def isCurLineAtOrAfterCurTs(): + """ Checks if the current line starts with a timestamp that is after the current one. """ + if len(sLine) >= 15 \ + and sLine[2] == ':' \ + and sLine[5] == ':' \ + and sLine[8] == '.' \ + and sLine[14] in '0123456789': + if sLine[:15] >= sCurTs and iCurTs < len(aoTimestamps): + return True; + return False; + + # Figure the end offset. + offEnd = offFile + self._cbChunk; + offEnd = min(offEnd, cbFile); + + # + # Here is an annoying thing, we cannot seek in zip file members. So, + # since we have to read from the start, we can just as well count line + # numbers while we're at it. + # + iCurTs = 0; + (sCurTs, sCurId) = prepCurTs(); + offCur = 0; + iLine = 0; + while True: + sLine = oFile.readline().decode('utf-8', 'replace'); + offLine = offCur; + iLine += 1; + offCur += len(sLine); + if offCur >= offFile or not sLine: + break; + while isCurLineAtOrAfterCurTs(): + iCurTs += 1; + (sCurTs, sCurId) = prepCurTs(); + + # + # Got to where we wanted, format the chunk. + # + asLines = ['\n<div class="tmlog">\n<pre>\n', ]; + while True: + # The timestamp IDs. + sPrevTs = ''; + while isCurLineAtOrAfterCurTs(): + if sPrevTs != sCurTs: + asLines.append('<a id="%s"></a>' % (sCurId,)); + iCurTs += 1; + (sCurTs, sCurId) = prepCurTs(); + + # The line. + asLines.append('<a id="L%d" href="#L%d">%05d</a><a id="O%d"></a>%s\n' \ + % (iLine, iLine, iLine, offLine, webutils.escapeElem(sLine.rstrip()))); + + # next + if offCur >= offEnd: + break; + sLine = oFile.readline().decode('utf-8', 'replace'); + offLine = offCur; + iLine += 1; + offCur += len(sLine); + if not sLine: + break; + asLines.append('<pre/></div>\n'); + return ''.join(asLines); + + + def show(self): + """Shows the log.""" + + if self._oLogFile.sDescription not in [ '', None ]: + sTitle = '%s - %s' % (self._oLogFile.sFile, self._oLogFile.sDescription); + else: + sTitle = '%s' % (self._oLogFile.sFile,); + + # + # Open the log file. No universal line endings here. + # + (oFile, oSizeOrError, _) = self._oTestSet.openFile(self._oLogFile.sFile, 'rb'); + if oFile is None: + return (sTitle, '<p>%s</p>\n' % (webutils.escapeElem(oSizeOrError),),); + cbFile = oSizeOrError; + + # + # Generate the page. + # + + # Start with a focus hack. + sHtml = '<div id="tmlogoutdiv" tabindex="0">\n' \ + '<script lang="text/javascript">\n' \ + 'document.getElementById(\'tmlogoutdiv\').focus();\n' \ + '</script>\n'; + + sNaviHtml = self._generateNavigation(cbFile); + sHtml += sNaviHtml; + + offFile = self._iChunk * self._cbChunk; + if offFile < cbFile: + sHtml += self._displayLog(oFile, offFile, cbFile, self._aoTimestamps); + sHtml += sNaviHtml; + else: + sHtml += '<p>End Of File</p>'; + + return (sTitle, sHtml); + diff --git a/src/VBox/ValidationKit/testmanager/webui/wuimain.py b/src/VBox/ValidationKit/testmanager/webui/wuimain.py new file mode 100755 index 00000000..3f059775 --- /dev/null +++ b/src/VBox/ValidationKit/testmanager/webui/wuimain.py @@ -0,0 +1,1344 @@ +# -*- coding: utf-8 -*- +# $Id: wuimain.py $ + +""" +Test Manager Core - WUI - The Main page. +""" + +__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. + +# Validation Kit imports. +from testmanager import config; +from testmanager.core.base import TMExceptionBase, TMTooManyRows; +from testmanager.webui.wuibase import WuiDispatcherBase, WuiException; +from testmanager.webui.wuicontentbase import WuiTmLink; +from common import webutils, utils; + + + +class WuiMain(WuiDispatcherBase): + """ + WUI Main page. + + Note! All cylic dependency avoiance stuff goes here in the dispatcher code, + not in the action specific code. This keeps the uglyness in one place + and reduces load time dependencies in the more critical code path. + """ + + ## The name of the script. + ksScriptName = 'index.py' + + ## @name Actions + ## @{ + ksActionResultsUnGrouped = 'ResultsUnGrouped' + ksActionResultsGroupedBySchedGroup = 'ResultsGroupedBySchedGroup' + ksActionResultsGroupedByTestGroup = 'ResultsGroupedByTestGroup' + ksActionResultsGroupedByBuildRev = 'ResultsGroupedByBuildRev' + ksActionResultsGroupedByBuildCat = 'ResultsGroupedByBuildCat' + ksActionResultsGroupedByTestBox = 'ResultsGroupedByTestBox' + ksActionResultsGroupedByTestCase = 'ResultsGroupedByTestCase' + ksActionResultsGroupedByOS = 'ResultsGroupedByOS' + ksActionResultsGroupedByArch = 'ResultsGroupedByArch' + ksActionTestSetDetails = 'TestSetDetails'; + ksActionTestResultDetails = ksActionTestSetDetails; + ksActionTestSetDetailsFromResult = 'TestSetDetailsFromResult' + ksActionTestResultFailureDetails = 'TestResultFailureDetails' + ksActionTestResultFailureAdd = 'TestResultFailureAdd' + ksActionTestResultFailureAddPost = 'TestResultFailureAddPost' + ksActionTestResultFailureEdit = 'TestResultFailureEdit' + ksActionTestResultFailureEditPost = 'TestResultFailureEditPost' + ksActionTestResultFailureDoRemove = 'TestResultFailureDoRemove' + ksActionViewLog = 'ViewLog' + ksActionGetFile = 'GetFile' + ksActionReportSummary = 'ReportSummary'; + ksActionReportRate = 'ReportRate'; + ksActionReportTestCaseFailures = 'ReportTestCaseFailures'; + ksActionReportTestBoxFailures = 'ReportTestBoxFailures'; + ksActionReportFailureReasons = 'ReportFailureReasons'; + ksActionGraphWiz = 'GraphWiz'; + ksActionVcsHistoryTooltip = 'VcsHistoryTooltip'; ##< Hardcoded in common.js. + ## @} + + ## @name Standard report parameters + ## @{ + ksParamReportPeriods = 'cPeriods'; + ksParamReportPeriodInHours = 'cHoursPerPeriod'; + ksParamReportSubject = 'sSubject'; + ksParamReportSubjectIds = 'SubjectIds'; + ## @} + + ## @name Graph Wizard parameters + ## Common parameters: ksParamReportPeriods, ksParamReportPeriodInHours, ksParamReportSubjectIds, + ## ksParamReportSubject, ksParamEffectivePeriod, and ksParamEffectiveDate. + ## @{ + ksParamGraphWizTestBoxIds = 'aidTestBoxes'; + ksParamGraphWizBuildCatIds = 'aidBuildCats'; + ksParamGraphWizTestCaseIds = 'aidTestCases'; + ksParamGraphWizSepTestVars = 'fSepTestVars'; + ksParamGraphWizImpl = 'enmImpl'; + ksParamGraphWizWidth = 'cx'; + ksParamGraphWizHeight = 'cy'; + ksParamGraphWizDpi = 'dpi'; + ksParamGraphWizFontSize = 'cPtFont'; + ksParamGraphWizErrorBarY = 'fErrorBarY'; + ksParamGraphWizMaxErrorBarY = 'cMaxErrorBarY'; + ksParamGraphWizMaxPerGraph = 'cMaxPerGraph'; + ksParamGraphWizXkcdStyle = 'fXkcdStyle'; + ksParamGraphWizTabular = 'fTabular'; + ksParamGraphWizSrcTestSetId = 'idSrcTestSet'; + ## @} + + ## @name Graph implementations values for ksParamGraphWizImpl. + ## @{ + ksGraphWizImpl_Default = 'default'; + ksGraphWizImpl_Matplotlib = 'matplotlib'; + ksGraphWizImpl_Charts = 'charts'; + kasGraphWizImplValid = [ ksGraphWizImpl_Default, ksGraphWizImpl_Matplotlib, ksGraphWizImpl_Charts]; + kaasGraphWizImplCombo = [ + ( ksGraphWizImpl_Default, 'Default' ), + ( ksGraphWizImpl_Matplotlib, 'Matplotlib (server)' ), + ( ksGraphWizImpl_Charts, 'Google Charts (client)'), + ]; + ## @} + + ## @name Log Viewer parameters. + ## @{ + ksParamLogSetId = 'LogViewer_idTestSet'; + ksParamLogFileId = 'LogViewer_idFile'; + ksParamLogChunkSize = 'LogViewer_cbChunk'; + ksParamLogChunkNo = 'LogViewer_iChunk'; + ## @} + + ## @name File getter parameters. + ## @{ + ksParamGetFileSetId = 'GetFile_idTestSet'; + ksParamGetFileId = 'GetFile_idFile'; + ksParamGetFileDownloadIt = 'GetFile_fDownloadIt'; + ## @} + + ## @name VCS history parameters. + ## @{ + ksParamVcsHistoryRepository = 'repo'; + ksParamVcsHistoryRevision = 'rev'; + ksParamVcsHistoryEntries = 'cEntries'; + ## @} + + ## @name Test result listing parameters. + ## @{ + ## If this param is specified, then show only results for this member when results grouped by some parameter. + ksParamGroupMemberId = 'GroupMemberId' + ## Optional parameter for indicating whether to restrict the listing to failures only. + ksParamOnlyFailures = 'OnlyFailures'; + ## The sheriff parameter for getting failures needing a reason or two assigned to them. + ksParamOnlyNeedingReason = 'OnlyNeedingReason'; + ## Result listing sorting. + ksParamTestResultsSortBy = 'enmSortBy' + ## @} + + ## Effective time period. one of the first column values in kaoResultPeriods. + ksParamEffectivePeriod = 'sEffectivePeriod' + + ## Test result period values. + kaoResultPeriods = [ + ( '1 hour', '1 hour', 1 ), + ( '2 hours', '2 hours', 2 ), + ( '3 hours', '3 hours', 3 ), + ( '6 hours', '6 hours', 6 ), + ( '12 hours', '12 hours', 12 ), + + ( '1 day', '1 day', 24 ), + ( '2 days', '2 days', 48 ), + ( '3 days', '3 days', 72 ), + + ( '1 week', '1 week', 168 ), + ( '2 weeks', '2 weeks', 336 ), + ( '3 weeks', '3 weeks', 504 ), + + ( '1 month', '1 month', 31 * 24 ), # The approx hour count varies with the start date. + ( '2 months', '2 months', (31 + 31) * 24 ), # Using maximum values. + ( '3 months', '3 months', (31 + 30 + 31) * 24 ), + + ( '6 months', '6 months', (31 + 31 + 30 + 31 + 30 + 31) * 24 ), + + ( '1 year', '1 year', 365 * 24 ), + ]; + ## The default test result period. + ksResultPeriodDefault = '6 hours'; + + + + def __init__(self, oSrvGlue): + WuiDispatcherBase.__init__(self, oSrvGlue, self.ksScriptName); + self._sTemplate = 'template.html' + + # + # Populate the action dispatcher dictionary. + # Lambda is forbidden because of readability, speed and reducing number of imports. + # + self._dDispatch[self.ksActionResultsUnGrouped] = self._actionResultsUnGrouped; + self._dDispatch[self.ksActionResultsGroupedByTestGroup] = self._actionResultsGroupedByTestGroup; + self._dDispatch[self.ksActionResultsGroupedByBuildRev] = self._actionResultsGroupedByBuildRev; + self._dDispatch[self.ksActionResultsGroupedByBuildCat] = self._actionResultsGroupedByBuildCat; + self._dDispatch[self.ksActionResultsGroupedByTestBox] = self._actionResultsGroupedByTestBox; + self._dDispatch[self.ksActionResultsGroupedByTestCase] = self._actionResultsGroupedByTestCase; + self._dDispatch[self.ksActionResultsGroupedByOS] = self._actionResultsGroupedByOS; + self._dDispatch[self.ksActionResultsGroupedByArch] = self._actionResultsGroupedByArch; + self._dDispatch[self.ksActionResultsGroupedBySchedGroup] = self._actionResultsGroupedBySchedGroup; + + self._dDispatch[self.ksActionTestSetDetails] = self._actionTestSetDetails; + self._dDispatch[self.ksActionTestSetDetailsFromResult] = self._actionTestSetDetailsFromResult; + + self._dDispatch[self.ksActionTestResultFailureAdd] = self._actionTestResultFailureAdd; + self._dDispatch[self.ksActionTestResultFailureAddPost] = self._actionTestResultFailureAddPost; + self._dDispatch[self.ksActionTestResultFailureDetails] = self._actionTestResultFailureDetails; + self._dDispatch[self.ksActionTestResultFailureDoRemove] = self._actionTestResultFailureDoRemove; + self._dDispatch[self.ksActionTestResultFailureEdit] = self._actionTestResultFailureEdit; + self._dDispatch[self.ksActionTestResultFailureEditPost] = self._actionTestResultFailureEditPost; + + self._dDispatch[self.ksActionViewLog] = self._actionViewLog; + self._dDispatch[self.ksActionGetFile] = self._actionGetFile; + + self._dDispatch[self.ksActionReportSummary] = self._actionReportSummary; + self._dDispatch[self.ksActionReportRate] = self._actionReportRate; + self._dDispatch[self.ksActionReportTestCaseFailures] = self._actionReportTestCaseFailures; + self._dDispatch[self.ksActionReportFailureReasons] = self._actionReportFailureReasons; + self._dDispatch[self.ksActionGraphWiz] = self._actionGraphWiz; + + self._dDispatch[self.ksActionVcsHistoryTooltip] = self._actionVcsHistoryTooltip; + + # Legacy. + self._dDispatch['TestResultDetails'] = self._dDispatch[self.ksActionTestSetDetails]; + + + # + # Popupate the menus. + # + + # Additional URL parameters keeping for time navigation. + sExtraTimeNav = '' + dCurParams = oSrvGlue.getParameters() + if dCurParams is not None: + for sExtraParam in [ self.ksParamItemsPerPage, self.ksParamEffectiveDate, self.ksParamEffectivePeriod, ]: + if sExtraParam in dCurParams: + sExtraTimeNav += '&%s' % (webutils.encodeUrlParams({sExtraParam: dCurParams[sExtraParam]}),) + + # Additional URL parameters for reports + sExtraReports = ''; + if dCurParams is not None: + for sExtraParam in [ self.ksParamReportPeriods, self.ksParamReportPeriodInHours, self.ksParamEffectiveDate, ]: + if sExtraParam in dCurParams: + sExtraReports += '&%s' % (webutils.encodeUrlParams({sExtraParam: dCurParams[sExtraParam]}),) + + # Shorthand to keep within margins. + sActUrlBase = self._sActionUrlBase; + sOnlyFailures = '&%s%s' % ( webutils.encodeUrlParams({self.ksParamOnlyFailures: True}), sExtraTimeNav, ); + sSheriff = '&%s%s' % ( webutils.encodeUrlParams({self.ksParamOnlyNeedingReason: True}), sExtraTimeNav, ); + + self._aaoMenus = \ + [ + [ + 'Sheriff', sActUrlBase + self.ksActionResultsUnGrouped + sSheriff, + [ + [ 'Grouped by', None ], + [ 'Ungrouped', sActUrlBase + self.ksActionResultsUnGrouped + sSheriff, False ], + [ 'Sched group', sActUrlBase + self.ksActionResultsGroupedBySchedGroup + sSheriff, False ], + [ 'Test group', sActUrlBase + self.ksActionResultsGroupedByTestGroup + sSheriff, False ], + [ 'Test case', sActUrlBase + self.ksActionResultsGroupedByTestCase + sSheriff, False ], + [ 'Testbox', sActUrlBase + self.ksActionResultsGroupedByTestBox + sSheriff, False ], + [ 'OS', sActUrlBase + self.ksActionResultsGroupedByOS + sSheriff, False ], + [ 'Architecture', sActUrlBase + self.ksActionResultsGroupedByArch + sSheriff, False ], + [ 'Revision', sActUrlBase + self.ksActionResultsGroupedByBuildRev + sSheriff, False ], + [ 'Build category', sActUrlBase + self.ksActionResultsGroupedByBuildCat + sSheriff, False ], + ] + ], + [ + 'Reports', sActUrlBase + self.ksActionReportSummary, + [ + [ 'Summary', sActUrlBase + self.ksActionReportSummary + sExtraReports, False ], + [ 'Success rate', sActUrlBase + self.ksActionReportRate + sExtraReports, False ], + [ 'Test case failures', sActUrlBase + self.ksActionReportTestCaseFailures + sExtraReports, False ], + [ 'Testbox failures', sActUrlBase + self.ksActionReportTestBoxFailures + sExtraReports, False ], + [ 'Failure reasons', sActUrlBase + self.ksActionReportFailureReasons + sExtraReports, False ], + ] + ], + [ + 'Test Results', sActUrlBase + self.ksActionResultsUnGrouped + sExtraTimeNav, + [ + [ 'Grouped by', None ], + [ 'Ungrouped', sActUrlBase + self.ksActionResultsUnGrouped + sExtraTimeNav, False ], + [ 'Sched group', sActUrlBase + self.ksActionResultsGroupedBySchedGroup + sExtraTimeNav, False ], + [ 'Test group', sActUrlBase + self.ksActionResultsGroupedByTestGroup + sExtraTimeNav, False ], + [ 'Test case', sActUrlBase + self.ksActionResultsGroupedByTestCase + sExtraTimeNav, False ], + [ 'Testbox', sActUrlBase + self.ksActionResultsGroupedByTestBox + sExtraTimeNav, False ], + [ 'OS', sActUrlBase + self.ksActionResultsGroupedByOS + sExtraTimeNav, False ], + [ 'Architecture', sActUrlBase + self.ksActionResultsGroupedByArch + sExtraTimeNav, False ], + [ 'Revision', sActUrlBase + self.ksActionResultsGroupedByBuildRev + sExtraTimeNav, False ], + [ 'Build category', sActUrlBase + self.ksActionResultsGroupedByBuildCat + sExtraTimeNav, False ], + ] + ], + [ + 'Test Failures', sActUrlBase + self.ksActionResultsUnGrouped + sOnlyFailures, + [ + [ 'Grouped by', None ], + [ 'Ungrouped', sActUrlBase + self.ksActionResultsUnGrouped + sOnlyFailures, False ], + [ 'Sched group', sActUrlBase + self.ksActionResultsGroupedBySchedGroup + sOnlyFailures, False ], + [ 'Test group', sActUrlBase + self.ksActionResultsGroupedByTestGroup + sOnlyFailures, False ], + [ 'Test case', sActUrlBase + self.ksActionResultsGroupedByTestCase + sOnlyFailures, False ], + [ 'Testbox', sActUrlBase + self.ksActionResultsGroupedByTestBox + sOnlyFailures, False ], + [ 'OS', sActUrlBase + self.ksActionResultsGroupedByOS + sOnlyFailures, False ], + [ 'Architecture', sActUrlBase + self.ksActionResultsGroupedByArch + sOnlyFailures, False ], + [ 'Revision', sActUrlBase + self.ksActionResultsGroupedByBuildRev + sOnlyFailures, False ], + [ 'Build category', sActUrlBase + self.ksActionResultsGroupedByBuildCat + sOnlyFailures, False ], + ] + ], + [ + '> Admin', 'admin.py?' + webutils.encodeUrlParams(self._dDbgParams), [] + ], + ]; + + + # + # Overriding parent methods. + # + + def _generatePage(self): + """Override parent handler in order to change page title.""" + if self._sPageTitle is not None: + self._sPageTitle = 'Test Results - ' + self._sPageTitle + + return WuiDispatcherBase._generatePage(self) + + def _actionDefault(self): + """Show the default admin page.""" + from testmanager.webui.wuitestresult import WuiGroupedResultList; + from testmanager.core.testresults import TestResultLogic, TestResultFilter; + self._sAction = self.ksActionResultsUnGrouped + return self._actionGroupedResultsListing(TestResultLogic.ksResultsGroupingTypeNone, + TestResultLogic, TestResultFilter, WuiGroupedResultList); + + def _isMenuMatch(self, sMenuUrl, sActionParam): + if super(WuiMain, self)._isMenuMatch(sMenuUrl, sActionParam): + fOnlyNeedingReason = self.getBoolParam(self.ksParamOnlyNeedingReason, fDefault = False); + if fOnlyNeedingReason: + return (sMenuUrl.find(self.ksParamOnlyNeedingReason) > 0); + fOnlyFailures = self.getBoolParam(self.ksParamOnlyFailures, fDefault = False); + return (sMenuUrl.find(self.ksParamOnlyFailures) > 0) == fOnlyFailures \ + and sMenuUrl.find(self.ksParamOnlyNeedingReason) < 0; + return False; + + + # + # Navigation bar stuff + # + + def _generateSortBySelector(self, dParams, sPreamble, sPostamble): + """ + Generate HTML code for the sort by selector. + """ + from testmanager.core.testresults import TestResultLogic; + + if self.ksParamTestResultsSortBy in dParams: + enmResultSortBy = dParams[self.ksParamTestResultsSortBy]; + del dParams[self.ksParamTestResultsSortBy]; + else: + enmResultSortBy = TestResultLogic.ksResultsSortByRunningAndStart; + + sHtmlSortBy = '<form name="TimeForm" method="GET"> Sort by\n'; + sHtmlSortBy += sPreamble; + sHtmlSortBy += '\n <select name="%s" onchange="window.location=' % (self.ksParamTestResultsSortBy,); + sHtmlSortBy += '\'?%s&%s=\' + ' % (webutils.encodeUrlParams(dParams), self.ksParamTestResultsSortBy) + sHtmlSortBy += 'this.options[this.selectedIndex].value;" title="Sorting by">\n' + + fSelected = False; + for enmCode, sTitle in TestResultLogic.kaasResultsSortByTitles: + if enmCode == enmResultSortBy: + fSelected = True; + sHtmlSortBy += ' <option value="%s"%s>%s</option>\n' \ + % (enmCode, ' selected="selected"' if enmCode == enmResultSortBy else '', sTitle,); + assert fSelected; + sHtmlSortBy += ' </select>\n'; + sHtmlSortBy += sPostamble; + sHtmlSortBy += '\n</form>\n' + return sHtmlSortBy; + + def _generateStatusSelector(self, dParams, fOnlyFailures): + """ + Generate HTML code for the status code selector. Currently very simple. + """ + dParams[self.ksParamOnlyFailures] = not fOnlyFailures; + return WuiTmLink('Show all results' if fOnlyFailures else 'Only show failed tests', '', dParams, + fBracketed = False).toHtml(); + + def _generateTimeWalker(self, dParams, tsEffective, sCurPeriod): + """ + Generates HTML code for walking back and forth in time. + """ + # Have to do some math here. :-/ + if tsEffective is None: + self._oDb.execute('SELECT CURRENT_TIMESTAMP - \'' + sCurPeriod + '\'::interval'); + tsNext = None; + tsPrev = self._oDb.fetchOne()[0]; + else: + self._oDb.execute('SELECT %s::TIMESTAMP - \'' + sCurPeriod + '\'::interval,\n' + ' %s::TIMESTAMP + \'' + sCurPeriod + '\'::interval', + (tsEffective, tsEffective,)); + tsPrev, tsNext = self._oDb.fetchOne(); + + # Forget about page No when changing a period + if WuiDispatcherBase.ksParamPageNo in dParams: + del dParams[WuiDispatcherBase.ksParamPageNo] + + # Format. + dParams[WuiDispatcherBase.ksParamEffectiveDate] = str(tsPrev); + sPrev = '<a href="?%s" title="One period earlier"><<</a> ' \ + % (webutils.encodeUrlParams(dParams),); + + if tsNext is not None: + dParams[WuiDispatcherBase.ksParamEffectiveDate] = str(tsNext); + sNext = ' <a href="?%s" title="One period later">>></a>' \ + % (webutils.encodeUrlParams(dParams),); + else: + sNext = ' >>'; + + from testmanager.webui.wuicontentbase import WuiListContentBase; ## @todo move to better place. + return WuiListContentBase.generateTimeNavigation('top', self.getParameters(), self.getEffectiveDateParam(), + sPrev, sNext, False); + + def _generateResultPeriodSelector(self, dParams, sCurPeriod): + """ + Generate HTML code for result period selector. + """ + + if self.ksParamEffectivePeriod in dParams: + del dParams[self.ksParamEffectivePeriod]; + + # Forget about page No when changing a period + if WuiDispatcherBase.ksParamPageNo in dParams: + del dParams[WuiDispatcherBase.ksParamPageNo] + + sHtmlPeriodSelector = '<form name="PeriodForm" method="GET">\n' + sHtmlPeriodSelector += ' Period is\n' + sHtmlPeriodSelector += ' <select name="%s" onchange="window.location=' % self.ksParamEffectivePeriod + sHtmlPeriodSelector += '\'?%s&%s=\' + ' % (webutils.encodeUrlParams(dParams), self.ksParamEffectivePeriod) + sHtmlPeriodSelector += 'this.options[this.selectedIndex].value;">\n' + + for sPeriodValue, sPeriodCaption, _ in self.kaoResultPeriods: + sHtmlPeriodSelector += ' <option value="%s"%s>%s</option>\n' \ + % (webutils.quoteUrl(sPeriodValue), + ' selected="selected"' if sPeriodValue == sCurPeriod else '', + sPeriodCaption) + + sHtmlPeriodSelector += ' </select>\n' \ + '</form>\n' + + return sHtmlPeriodSelector + + def _generateGroupContentSelector(self, aoGroupMembers, iCurrentMember, sAltAction): + """ + Generate HTML code for group content selector. + """ + + dParams = self.getParameters() + + if self.ksParamGroupMemberId in dParams: + del dParams[self.ksParamGroupMemberId] + + if sAltAction is not None: + if self.ksParamAction in dParams: + del dParams[self.ksParamAction]; + dParams[self.ksParamAction] = sAltAction; + + sHtmlSelector = '<form name="GroupContentForm" method="GET">\n' + sHtmlSelector += ' <select name="%s" onchange="window.location=' % self.ksParamGroupMemberId + sHtmlSelector += '\'?%s&%s=\' + ' % (webutils.encodeUrlParams(dParams), self.ksParamGroupMemberId) + sHtmlSelector += 'this.options[this.selectedIndex].value;">\n' + + sHtmlSelector += '<option value="-1">All</option>\n' + + for iGroupMemberId, sGroupMemberName in aoGroupMembers: + if iGroupMemberId is not None: + sHtmlSelector += ' <option value="%s"%s>%s</option>\n' \ + % (iGroupMemberId, + ' selected="selected"' if iGroupMemberId == iCurrentMember else '', + sGroupMemberName) + + sHtmlSelector += ' </select>\n' \ + '</form>\n' + + return sHtmlSelector + + def _generatePagesSelector(self, dParams, cItems, cItemsPerPage, iPage): + """ + Generate HTML code for pages (1, 2, 3 ... N) selector + """ + + if WuiDispatcherBase.ksParamPageNo in dParams: + del dParams[WuiDispatcherBase.ksParamPageNo] + + sHrefPtr = '<a href="?%s&%s=' % (webutils.encodeUrlParams(dParams).replace('%', '%%'), + WuiDispatcherBase.ksParamPageNo) + sHrefPtr += '%d">%s</a>' + + cNumOfPages = (cItems + cItemsPerPage - 1) // cItemsPerPage; + cPagesToDisplay = 10 + cPagesRangeStart = iPage - cPagesToDisplay // 2 \ + if not iPage - cPagesToDisplay / 2 < 0 else 0 + cPagesRangeEnd = cPagesRangeStart + cPagesToDisplay \ + if not cPagesRangeStart + cPagesToDisplay > cNumOfPages else cNumOfPages + # Adjust pages range + if cNumOfPages < cPagesToDisplay: + cPagesRangeStart = 0 + cPagesRangeEnd = cNumOfPages + + # 1 2 3 4... + sHtmlPager = ' \n'.join(sHrefPtr % (x, str(x + 1)) if x != iPage else str(x + 1) + for x in range(cPagesRangeStart, cPagesRangeEnd)) + if cPagesRangeStart > 0: + sHtmlPager = '%s ... \n' % (sHrefPtr % (0, str(1))) + sHtmlPager + if cPagesRangeEnd < cNumOfPages: + sHtmlPager += ' ... %s\n' % (sHrefPtr % (cNumOfPages, str(cNumOfPages + 1))) + + # Prev/Next (using << >> because « and » are too tiny). + if iPage > 0: + dParams[WuiDispatcherBase.ksParamPageNo] = iPage - 1 + sHtmlPager = ('<a title="Previous page" href="?%s"><<</a> \n' + % (webutils.encodeUrlParams(dParams), )) \ + + sHtmlPager; + else: + sHtmlPager = '<< \n' + sHtmlPager + + if iPage + 1 < cNumOfPages: + dParams[WuiDispatcherBase.ksParamPageNo] = iPage + 1 + sHtmlPager += '\n <a title="Next page" href="?%s">>></a>\n' % (webutils.encodeUrlParams(dParams),) + else: + sHtmlPager += '\n >>\n' + + return sHtmlPager + + def _generateItemPerPageSelector(self, dParams, cItemsPerPage): + """ + Generate HTML code for items per page selector + Note! Modifies dParams! + """ + + from testmanager.webui.wuicontentbase import WuiListContentBase; ## @todo move to better place. + return WuiListContentBase.generateItemPerPageSelector('top', dParams, cItemsPerPage); + + def _generateResultNavigation(self, cItems, cItemsPerPage, iPage, tsEffective, sCurPeriod, fOnlyFailures, + sHtmlMemberSelector): + """ Make custom time navigation bar for the results. """ + + # Generate the elements. + sHtmlStatusSelector = self._generateStatusSelector(self.getParameters(), fOnlyFailures); + sHtmlSortBySelector = self._generateSortBySelector(self.getParameters(), '', sHtmlStatusSelector); + sHtmlPeriodSelector = self._generateResultPeriodSelector(self.getParameters(), sCurPeriod) + sHtmlTimeWalker = self._generateTimeWalker(self.getParameters(), tsEffective, sCurPeriod); + + if cItems > 0: + sHtmlPager = self._generatePagesSelector(self.getParameters(), cItems, cItemsPerPage, iPage) + sHtmlItemsPerPageSelector = self._generateItemPerPageSelector(self.getParameters(), cItemsPerPage) + else: + sHtmlPager = '' + sHtmlItemsPerPageSelector = '' + + # Generate navigation bar + sHtml = '<table width=100%>\n' \ + '<tr>\n' \ + ' <td width=30%>' + sHtmlMemberSelector + '</td>\n' \ + ' <td width=40% align=center>' + sHtmlTimeWalker + '</td>' \ + ' <td width=30% align=right>\n' + sHtmlPeriodSelector + '</td>\n' \ + '</tr>\n' \ + '<tr>\n' \ + ' <td width=30%>' + sHtmlSortBySelector + '</td>\n' \ + ' <td width=40% align=center>\n' + sHtmlPager + '</td>\n' \ + ' <td width=30% align=right>\n' + sHtmlItemsPerPageSelector + '</td>\n'\ + '</tr>\n' \ + '</table>\n' + + return sHtml + + def _generateReportNavigation(self, tsEffective, cHoursPerPeriod, cPeriods): + """ Make time navigation bar for the reports. """ + + # The period length selector. + dParams = self.getParameters(); + if WuiMain.ksParamReportPeriodInHours in dParams: + del dParams[WuiMain.ksParamReportPeriodInHours]; + sHtmlPeriodLength = ''; + sHtmlPeriodLength += '<form name="ReportPeriodInHoursForm" method="GET">\n' \ + ' Period length <select name="%s" onchange="window.location=\'?%s&%s=\' + ' \ + 'this.options[this.selectedIndex].value;" title="Statistics period length in hours.">\n' \ + % (WuiMain.ksParamReportPeriodInHours, + webutils.encodeUrlParams(dParams), + WuiMain.ksParamReportPeriodInHours) + for cHours in [ 1, 2, 3, 4, 5, 6, 7, 8, 9, 12, 18, 24, 48, 72, 96, 120, 144, 168 ]: + sHtmlPeriodLength += ' <option value="%d"%s>%d hour%s</option>\n' \ + % (cHours, 'selected="selected"' if cHours == cHoursPerPeriod else '', cHours, + 's' if cHours > 1 else ''); + sHtmlPeriodLength += ' </select>\n' \ + '</form>\n' + + # The period count selector. + dParams = self.getParameters(); + if WuiMain.ksParamReportPeriods in dParams: + del dParams[WuiMain.ksParamReportPeriods]; + sHtmlCountOfPeriods = ''; + sHtmlCountOfPeriods += '<form name="ReportPeriodsForm" method="GET">\n' \ + ' Periods <select name="%s" onchange="window.location=\'?%s&%s=\' + ' \ + 'this.options[this.selectedIndex].value;" title="Statistics periods to report.">\n' \ + % (WuiMain.ksParamReportPeriods, + webutils.encodeUrlParams(dParams), + WuiMain.ksParamReportPeriods) + for cCurPeriods in range(2, 43): + sHtmlCountOfPeriods += ' <option value="%d"%s>%d</option>\n' \ + % (cCurPeriods, 'selected="selected"' if cCurPeriods == cPeriods else '', cCurPeriods); + sHtmlCountOfPeriods += ' </select>\n' \ + '</form>\n' + + # The time walker. + sHtmlTimeWalker = self._generateTimeWalker(self.getParameters(), tsEffective, '%d hours' % (cHoursPerPeriod)); + + # Combine them all. + sHtml = '<table width=100%>\n' \ + ' <tr>\n' \ + ' <td width=30% align="center">\n' + sHtmlPeriodLength + '</td>\n' \ + ' <td width=40% align="center">\n' + sHtmlTimeWalker + '</td>' \ + ' <td width=30% align="center">\n' + sHtmlCountOfPeriods + '</td>\n' \ + ' </tr>\n' \ + '</table>\n'; + return sHtml; + + + # + # The rest of stuff + # + + def _actionGroupedResultsListing( #pylint: disable=too-many-locals + self, + enmResultsGroupingType, + oResultsLogicType, + oResultFilterType, + oResultsListContentType): + """ + Override generic listing action. + + oResultsLogicType implements getEntriesCount, fetchResultsForListing and more. + oResultFilterType is a child of ModelFilterBase. + oResultsListContentType is a child of WuiListContentBase. + """ + from testmanager.core.testresults import TestResultLogic; + + cItemsPerPage = self.getIntParam(self.ksParamItemsPerPage, iMin = 2, iMax = 9999, iDefault = 128); + iPage = self.getIntParam(self.ksParamPageNo, iMin = 0, iMax = 999999, iDefault = 0); + tsEffective = self.getEffectiveDateParam(); + iGroupMemberId = self.getIntParam(self.ksParamGroupMemberId, iMin = -1, iMax = 999999, iDefault = -1); + fOnlyFailures = self.getBoolParam(self.ksParamOnlyFailures, fDefault = False); + fOnlyNeedingReason = self.getBoolParam(self.ksParamOnlyNeedingReason, fDefault = False); + enmResultSortBy = self.getStringParam(self.ksParamTestResultsSortBy, + asValidValues = TestResultLogic.kasResultsSortBy, + sDefault = TestResultLogic.ksResultsSortByRunningAndStart); + oFilter = oResultFilterType().initFromParams(self); + + # Get testing results period and validate it + asValidValues = [x for (x, _, _) in self.kaoResultPeriods] + sCurPeriod = self.getStringParam(self.ksParamEffectivePeriod, asValidValues = asValidValues, + sDefault = self.ksResultPeriodDefault) + assert sCurPeriod != ''; # Impossible! + + self._checkForUnknownParameters() + + # + # Fetch the group members. + # + # If no grouping is selected, we'll fill the grouping combo with + # testboxes just to avoid having completely useless combo box. + # + oTrLogic = TestResultLogic(self._oDb); + sAltSelectorAction = None; + if enmResultsGroupingType in (TestResultLogic.ksResultsGroupingTypeNone, TestResultLogic.ksResultsGroupingTypeTestBox,): + aoTmp = oTrLogic.getTestBoxes(tsNow = tsEffective, sPeriod = sCurPeriod) + aoGroupMembers = sorted(list({(x.idTestBox, '%s (%s)' % (x.sName, str(x.ip))) for x in aoTmp }), + reverse = False, key = lambda asData: asData[1]) + + if enmResultsGroupingType == TestResultLogic.ksResultsGroupingTypeTestBox: + self._sPageTitle = 'Grouped by Test Box'; + else: + self._sPageTitle = 'Ungrouped results'; + sAltSelectorAction = self.ksActionResultsGroupedByTestBox; + aoGroupMembers.insert(0, [None, None]); # The "All" member. + + elif enmResultsGroupingType == TestResultLogic.ksResultsGroupingTypeTestGroup: + aoTmp = oTrLogic.getTestGroups(tsNow = tsEffective, sPeriod = sCurPeriod); + aoGroupMembers = sorted(list({ (x.idTestGroup, x.sName ) for x in aoTmp }), + reverse = False, key = lambda asData: asData[1]) + self._sPageTitle = 'Grouped by Test Group' + + elif enmResultsGroupingType == TestResultLogic.ksResultsGroupingTypeBuildRev: + aoTmp = oTrLogic.getBuilds(tsNow = tsEffective, sPeriod = sCurPeriod) + aoGroupMembers = sorted(list({ (x.iRevision, '%s.%d' % (x.oCat.sBranch, x.iRevision)) for x in aoTmp }), + reverse = True, key = lambda asData: asData[0]) + self._sPageTitle = 'Grouped by Build' + + elif enmResultsGroupingType == TestResultLogic.ksResultsGroupingTypeBuildCat: + aoTmp = oTrLogic.getBuildCategories(tsNow = tsEffective, sPeriod = sCurPeriod) + aoGroupMembers = sorted(list({ (x.idBuildCategory, + '%s / %s / %s / %s' % ( x.sProduct, x.sBranch, ', '.join(x.asOsArches), x.sType) ) + for x in aoTmp }), + reverse = True, key = lambda asData: asData[1]); + self._sPageTitle = 'Grouped by Build Category' + + elif enmResultsGroupingType == TestResultLogic.ksResultsGroupingTypeTestCase: + aoTmp = oTrLogic.getTestCases(tsNow = tsEffective, sPeriod = sCurPeriod) + aoGroupMembers = sorted(list({ (x.idTestCase, '%s' % x.sName) for x in aoTmp }), + reverse = False, key = lambda asData: asData[1]) + self._sPageTitle = 'Grouped by Test Case' + + elif enmResultsGroupingType == TestResultLogic.ksResultsGroupingTypeOS: + aoTmp = oTrLogic.getOSes(tsNow = tsEffective, sPeriod = sCurPeriod) + aoGroupMembers = sorted(list(set(aoTmp)), reverse = False, key = lambda asData: asData[1]); + self._sPageTitle = 'Grouped by OS' + + elif enmResultsGroupingType == TestResultLogic.ksResultsGroupingTypeArch: + aoTmp = oTrLogic.getArchitectures(tsNow = tsEffective, sPeriod = sCurPeriod) + aoGroupMembers = sorted(list(set(aoTmp)), reverse = False, key = lambda asData: asData[1]); + self._sPageTitle = 'Grouped by Architecture' + + elif enmResultsGroupingType == TestResultLogic.ksResultsGroupingTypeSchedGroup: + aoTmp = oTrLogic.getSchedGroups(tsNow = tsEffective, sPeriod = sCurPeriod) + aoGroupMembers = sorted(list({ (x.idSchedGroup, '%s' % x.sName) for x in aoTmp }), + reverse = False, key = lambda asData: asData[1]) + self._sPageTitle = 'Grouped by Scheduling Group' + + else: + raise TMExceptionBase('Unknown grouping type') + + _sPageBody = '' + oContent = None + cEntriesMax = 0 + _dParams = self.getParameters() + oResultLogic = oResultsLogicType(self._oDb); + for idMember, sMemberName in aoGroupMembers: + # + # Count and fetch entries to be displayed. + # + + # Skip group members that were not specified. + if idMember != iGroupMemberId \ + and ( (idMember is not None and enmResultsGroupingType == TestResultLogic.ksResultsGroupingTypeNone) + or (iGroupMemberId > 0 and enmResultsGroupingType != TestResultLogic.ksResultsGroupingTypeNone) ): + continue + + cEntries = oResultLogic.getEntriesCount(tsNow = tsEffective, + sInterval = sCurPeriod, + oFilter = oFilter, + enmResultsGroupingType = enmResultsGroupingType, + iResultsGroupingValue = idMember, + fOnlyFailures = fOnlyFailures, + fOnlyNeedingReason = fOnlyNeedingReason); + if cEntries == 0: # Do not display empty groups + continue + aoEntries = oResultLogic.fetchResultsForListing(iPage * cItemsPerPage, + cItemsPerPage, + tsNow = tsEffective, + sInterval = sCurPeriod, + oFilter = oFilter, + enmResultSortBy = enmResultSortBy, + enmResultsGroupingType = enmResultsGroupingType, + iResultsGroupingValue = idMember, + fOnlyFailures = fOnlyFailures, + fOnlyNeedingReason = fOnlyNeedingReason); + cEntriesMax = max(cEntriesMax, cEntries) + + # + # Format them. + # + oContent = oResultsListContentType(aoEntries, + cEntries, + iPage, + cItemsPerPage, + tsEffective, + fnDPrint = self._oSrvGlue.dprint, + oDisp = self) + + (_, sHtml) = oContent.show(fShowNavigation = False) + if sMemberName is not None: + _sPageBody += '<table width=100%><tr><td>' + + _dParams[self.ksParamGroupMemberId] = idMember + sLink = WuiTmLink(sMemberName, '', _dParams, fBracketed = False).toHtml() + + _sPageBody += '<h2>%s (%d)</h2></td>' % (sLink, cEntries) + _sPageBody += '<td><br></td>' + _sPageBody += '</tr></table>' + _sPageBody += sHtml + _sPageBody += '<br>' + + # + # Complete the page by slapping navigation controls at the top and + # bottom of it. + # + sHtmlNavigation = self._generateResultNavigation(cEntriesMax, cItemsPerPage, iPage, + tsEffective, sCurPeriod, fOnlyFailures, + self._generateGroupContentSelector(aoGroupMembers, iGroupMemberId, + sAltSelectorAction)); + if cEntriesMax > 0: + self._sPageBody = sHtmlNavigation + _sPageBody + sHtmlNavigation; + else: + self._sPageBody = sHtmlNavigation + '<p align="center"><i>No data to display</i></p>\n'; + + # + # Now, generate a filter control panel for the side bar. + # + if hasattr(oFilter, 'kiBranches'): + oFilter.aCriteria[oFilter.kiBranches].fExpanded = True; + if hasattr(oFilter, 'kiTestStatus'): + oFilter.aCriteria[oFilter.kiTestStatus].fExpanded = True; + self._sPageFilter = self._generateResultFilter(oFilter, oResultLogic, tsEffective, sCurPeriod, + enmResultsGroupingType = enmResultsGroupingType, + aoGroupMembers = aoGroupMembers, + fOnlyFailures = fOnlyFailures, + fOnlyNeedingReason = fOnlyNeedingReason); + return True; + + def _generateResultFilter(self, oFilter, oResultLogic, tsNow, sPeriod, enmResultsGroupingType = None, aoGroupMembers = None, + fOnlyFailures = False, fOnlyNeedingReason = False): + """ + Generates the result filter for the left hand side. + """ + _ = enmResultsGroupingType; _ = aoGroupMembers; _ = fOnlyFailures; _ = fOnlyNeedingReason; + oResultLogic.fetchPossibleFilterOptions(oFilter, tsNow, sPeriod) + + # Add non-filter parameters as hidden fields so we can use 'GET' and have URLs to bookmark. + self._dSideMenuFormAttrs['method'] = 'GET'; + sHtml = u''; + for sKey, oValue in self._oSrvGlue.getParameters().items(): + if len(sKey) > 3: + if hasattr(oValue, 'startswith'): + sHtml += u'<input type="hidden" name="%s" value="%s"/>\n' \ + % (webutils.escapeAttr(sKey), webutils.escapeAttr(oValue),); + else: + for oSubValue in oValue: + sHtml += u'<input type="hidden" name="%s" value="%s"/>\n' \ + % (webutils.escapeAttr(sKey), webutils.escapeAttr(oSubValue),); + + # Generate the filter panel. + sHtml += u'<div id="side-filters">\n' \ + u' <p>Filters' \ + u' <span class="tm-side-filter-title-buttons"><input type="submit" value="Apply" />\n' \ + u' <a href="javascript:toggleSidebarSize();" class="tm-sidebar-size-link">»»</a></span></p>\n'; + sHtml += u' <dl>\n'; + for oCrit in oFilter.aCriteria: + if oCrit.aoPossible or oCrit.sType == oCrit.ksType_Ranges: + if ( oCrit.oSub is None \ + and ( oCrit.sState == oCrit.ksState_Selected \ + or (len(oCrit.aoPossible) <= 2 and oCrit.sType != oCrit.ksType_Ranges))) \ + or ( oCrit.oSub is not None \ + and ( oCrit.sState == oCrit.ksState_Selected \ + or oCrit.oSub.sState == oCrit.ksState_Selected \ + or (len(oCrit.aoPossible) <= 2 and len(oCrit.oSub.aoPossible) <= 2))) \ + or oCrit.fExpanded is True: + sClass = 'sf-collapsible'; + sChar = '▼'; + else: + sClass = 'sf-expandable'; + sChar = '▶'; + + sHtml += u' <dt class="%s"><a href="javascript:void(0)" onclick="toggleCollapsibleDtDd(this);">%s %s</a> ' \ + % (sClass, sChar, webutils.escapeElem(oCrit.sName),); + sHtml += u'<span class="tm-side-filter-dt-buttons">'; + if oCrit.sInvVarNm is not None: + sHtml += u'<input id="sf-union-%s" class="tm-side-filter-union-input" ' \ + u'name="%s" value="1" type="checkbox"%s />' \ + u'<label for="sf-union-%s" class="tm-side-filter-union-input"></label>' \ + % ( oCrit.sInvVarNm, oCrit.sInvVarNm, ' checked' if oCrit.fInverted else '', oCrit.sInvVarNm,); + sHtml += u' <input type="submit" value="Apply" />'; + sHtml += u'</span>'; + sHtml += u'</dt>\n' \ + u' <dd class="%s">\n' \ + u' <ul>\n' \ + % (sClass); + + if oCrit.sType == oCrit.ksType_Ranges: + assert not oCrit.oSub; + assert not oCrit.aoPossible; + asValues = []; + for tRange in oCrit.aoSelected: + if tRange[0] == tRange[1]: + asValues.append('%s' % (tRange[0],)); + else: + asValues.append('%s-%s' % (tRange[0] if tRange[0] is not None else 'inf', + tRange[1] if tRange[1] is not None else 'inf')); + sHtml += u' <li title="%s"><input type="text" name="%s" value="%s"/></li>\n' \ + % ( webutils.escapeAttr('comma separate list of numerical ranges'), oCrit.sVarNm, + ', '.join(asValues), ); + else: + for oDesc in oCrit.aoPossible: + fChecked = oDesc.oValue in oCrit.aoSelected; + sHtml += u' <li%s%s><label><input type="checkbox" name="%s" value="%s"%s%s/>%s%s</label>\n' \ + % ( ' class="side-filter-irrelevant"' if oDesc.fIrrelevant else '', + (' title="%s"' % (webutils.escapeAttr(oDesc.sHover,)) if oDesc.sHover is not None else ''), + oCrit.sVarNm, + oDesc.oValue, + ' checked' if fChecked else '', + ' onclick="toggleCollapsibleCheckbox(this);"' if oDesc.aoSubs is not None else '', + webutils.escapeElem(oDesc.sDesc), + '<span class="side-filter-count"> [%u]</span>' % (oDesc.cTimes) if oDesc.cTimes is not None + else '', ); + if oDesc.aoSubs is not None: + sHtml += u' <ul class="sf-checkbox-%s">\n' % ('collapsible' if fChecked else 'expandable', ); + for oSubDesc in oDesc.aoSubs: + fSubChecked = oSubDesc.oValue in oCrit.oSub.aoSelected; + sHtml += u' <li%s%s><label><input type="checkbox" name="%s" value="%s"%s/>%s%s</label>\n' \ + % ( ' class="side-filter-irrelevant"' if oSubDesc.fIrrelevant else '', + ' title="%s"' % ( webutils.escapeAttr(oSubDesc.sHover,) if oSubDesc.sHover is not None + else ''), + oCrit.oSub.sVarNm, oSubDesc.oValue, ' checked' if fSubChecked else '', + webutils.escapeElem(oSubDesc.sDesc), + '<span class="side-filter-count"> [%u]</span>' % (oSubDesc.cTimes) + if oSubDesc.cTimes is not None else '', ); + + sHtml += u' </ul>\n'; + sHtml += u' </li>'; + + sHtml += u' </ul>\n' \ + u' </dd>\n'; + + sHtml += u' </dl>\n'; + sHtml += u' <input type="submit" value="Apply"/>\n'; + sHtml += u' <input type="reset" value="Reset"/>\n'; + sHtml += u' <button type="button" onclick="clearForm(\'side-menu-form\');">Clear</button>\n'; + sHtml += u'</div>\n'; + return sHtml; + + def _actionResultsUnGrouped(self): + """ Action wrapper. """ + from testmanager.webui.wuitestresult import WuiGroupedResultList; + from testmanager.core.testresults import TestResultLogic, TestResultFilter; + #return self._actionResultsListing(TestResultLogic, WuiGroupedResultList)? + return self._actionGroupedResultsListing(TestResultLogic.ksResultsGroupingTypeNone, + TestResultLogic, TestResultFilter, WuiGroupedResultList); + + def _actionResultsGroupedByTestGroup(self): + """ Action wrapper. """ + from testmanager.webui.wuitestresult import WuiGroupedResultList; + from testmanager.core.testresults import TestResultLogic, TestResultFilter; + return self._actionGroupedResultsListing(TestResultLogic.ksResultsGroupingTypeTestGroup, + TestResultLogic, TestResultFilter, WuiGroupedResultList); + + def _actionResultsGroupedByBuildRev(self): + """ Action wrapper. """ + from testmanager.webui.wuitestresult import WuiGroupedResultList; + from testmanager.core.testresults import TestResultLogic, TestResultFilter; + return self._actionGroupedResultsListing(TestResultLogic.ksResultsGroupingTypeBuildRev, + TestResultLogic, TestResultFilter, WuiGroupedResultList); + + def _actionResultsGroupedByBuildCat(self): + """ Action wrapper. """ + from testmanager.webui.wuitestresult import WuiGroupedResultList; + from testmanager.core.testresults import TestResultLogic, TestResultFilter; + return self._actionGroupedResultsListing(TestResultLogic.ksResultsGroupingTypeBuildCat, + TestResultLogic, TestResultFilter, WuiGroupedResultList); + + def _actionResultsGroupedByTestBox(self): + """ Action wrapper. """ + from testmanager.webui.wuitestresult import WuiGroupedResultList; + from testmanager.core.testresults import TestResultLogic, TestResultFilter; + return self._actionGroupedResultsListing(TestResultLogic.ksResultsGroupingTypeTestBox, + TestResultLogic, TestResultFilter, WuiGroupedResultList); + + def _actionResultsGroupedByTestCase(self): + """ Action wrapper. """ + from testmanager.webui.wuitestresult import WuiGroupedResultList; + from testmanager.core.testresults import TestResultLogic, TestResultFilter; + return self._actionGroupedResultsListing(TestResultLogic.ksResultsGroupingTypeTestCase, + TestResultLogic, TestResultFilter, WuiGroupedResultList); + + def _actionResultsGroupedByOS(self): + """ Action wrapper. """ + from testmanager.webui.wuitestresult import WuiGroupedResultList; + from testmanager.core.testresults import TestResultLogic, TestResultFilter; + return self._actionGroupedResultsListing(TestResultLogic.ksResultsGroupingTypeOS, + TestResultLogic, TestResultFilter, WuiGroupedResultList); + + def _actionResultsGroupedByArch(self): + """ Action wrapper. """ + from testmanager.webui.wuitestresult import WuiGroupedResultList; + from testmanager.core.testresults import TestResultLogic, TestResultFilter; + return self._actionGroupedResultsListing(TestResultLogic.ksResultsGroupingTypeArch, + TestResultLogic, TestResultFilter, WuiGroupedResultList); + + def _actionResultsGroupedBySchedGroup(self): + """ Action wrapper. """ + from testmanager.webui.wuitestresult import WuiGroupedResultList; + from testmanager.core.testresults import TestResultLogic, TestResultFilter; + return self._actionGroupedResultsListing(TestResultLogic.ksResultsGroupingTypeSchedGroup, + TestResultLogic, TestResultFilter, WuiGroupedResultList); + + + def _actionTestSetDetailsCommon(self, idTestSet): + """Show test case execution result details.""" + from testmanager.core.build import BuildDataEx; + from testmanager.core.testbox import TestBoxData; + from testmanager.core.testcase import TestCaseDataEx; + from testmanager.core.testcaseargs import TestCaseArgsDataEx; + from testmanager.core.testgroup import TestGroupData; + from testmanager.core.testresults import TestResultLogic; + from testmanager.core.testset import TestSetData; + from testmanager.webui.wuitestresult import WuiTestResult; + + self._sTemplate = 'template-details.html'; + self._checkForUnknownParameters() + + oTestSetData = TestSetData().initFromDbWithId(self._oDb, idTestSet); + try: + (oTestResultTree, _) = TestResultLogic(self._oDb).fetchResultTree(idTestSet); + except TMTooManyRows: + (oTestResultTree, _) = TestResultLogic(self._oDb).fetchResultTree(idTestSet, 2); + oBuildDataEx = BuildDataEx().initFromDbWithId(self._oDb, oTestSetData.idBuild, oTestSetData.tsCreated); + try: oBuildValidationKitDataEx = BuildDataEx().initFromDbWithId(self._oDb, oTestSetData.idBuildTestSuite, + oTestSetData.tsCreated); + except: oBuildValidationKitDataEx = None; + oTestBoxData = TestBoxData().initFromDbWithGenId(self._oDb, oTestSetData.idGenTestBox); + oTestGroupData = TestGroupData().initFromDbWithId(self._oDb, ## @todo This bogus time wise. Bad DB design? + oTestSetData.idTestGroup, oTestSetData.tsCreated); + oTestCaseDataEx = TestCaseDataEx().initFromDbWithGenId(self._oDb, oTestSetData.idGenTestCase, + oTestSetData.tsConfig); + oTestCaseArgsDataEx = TestCaseArgsDataEx().initFromDbWithGenIdEx(self._oDb, oTestSetData.idGenTestCaseArgs, + oTestSetData.tsConfig); + + oContent = WuiTestResult(oDisp = self, fnDPrint = self._oSrvGlue.dprint); + (self._sPageTitle, self._sPageBody) = oContent.showTestCaseResultDetails(oTestResultTree, + oTestSetData, + oBuildDataEx, + oBuildValidationKitDataEx, + oTestBoxData, + oTestGroupData, + oTestCaseDataEx, + oTestCaseArgsDataEx); + return True + + def _actionTestSetDetails(self): + """Show test case execution result details.""" + from testmanager.core.testset import TestSetData; + + idTestSet = self.getIntParam(TestSetData.ksParam_idTestSet); + return self._actionTestSetDetailsCommon(idTestSet); + + def _actionTestSetDetailsFromResult(self): + """Show test case execution result details.""" + from testmanager.core.testresults import TestResultData; + from testmanager.core.testset import TestSetData; + idTestResult = self.getIntParam(TestSetData.ksParam_idTestResult); + oTestResultData = TestResultData().initFromDbWithId(self._oDb, idTestResult); + return self._actionTestSetDetailsCommon(oTestResultData.idTestSet); + + + def _actionTestResultFailureAdd(self): + """ Pro forma. """ + from testmanager.core.testresultfailures import TestResultFailureData; + from testmanager.webui.wuitestresultfailure import WuiTestResultFailure; + return self._actionGenericFormAdd(TestResultFailureData, WuiTestResultFailure); + + def _actionTestResultFailureAddPost(self): + """Add test result failure result""" + from testmanager.core.testresultfailures import TestResultFailureLogic, TestResultFailureData; + from testmanager.webui.wuitestresultfailure import WuiTestResultFailure; + if self.ksParamRedirectTo not in self._dParams: + raise WuiException('Missing parameter ' + self.ksParamRedirectTo); + + return self._actionGenericFormAddPost(TestResultFailureData, TestResultFailureLogic, + WuiTestResultFailure, self.ksActionResultsUnGrouped); + + def _actionTestResultFailureDoRemove(self): + """ Action wrapper. """ + from testmanager.core.testresultfailures import TestResultFailureData, TestResultFailureLogic; + return self._actionGenericDoRemove(TestResultFailureLogic, TestResultFailureData.ksParam_idTestResult, + self.ksActionResultsUnGrouped); + + def _actionTestResultFailureDetails(self): + """ Pro forma. """ + from testmanager.core.testresultfailures import TestResultFailureLogic, TestResultFailureData; + from testmanager.webui.wuitestresultfailure import WuiTestResultFailure; + return self._actionGenericFormDetails(TestResultFailureData, TestResultFailureLogic, + WuiTestResultFailure, 'idTestResult'); + + def _actionTestResultFailureEdit(self): + """ Pro forma. """ + from testmanager.core.testresultfailures import TestResultFailureData; + from testmanager.webui.wuitestresultfailure import WuiTestResultFailure; + return self._actionGenericFormEdit(TestResultFailureData, WuiTestResultFailure, + TestResultFailureData.ksParam_idTestResult); + + def _actionTestResultFailureEditPost(self): + """Edit test result failure result""" + from testmanager.core.testresultfailures import TestResultFailureLogic, TestResultFailureData; + from testmanager.webui.wuitestresultfailure import WuiTestResultFailure; + return self._actionGenericFormEditPost(TestResultFailureData, TestResultFailureLogic, + WuiTestResultFailure, self.ksActionResultsUnGrouped); + + def _actionViewLog(self): + """ + Log viewer action. + """ + from testmanager.core.testresults import TestResultLogic, TestResultFileDataEx; + from testmanager.core.testset import TestSetData, TestSetLogic; + from testmanager.webui.wuilogviewer import WuiLogViewer; + + self._sTemplate = 'template-details.html'; ## @todo create new template (background color, etc) + idTestSet = self.getIntParam(self.ksParamLogSetId, iMin = 1); + idLogFile = self.getIntParam(self.ksParamLogFileId, iMin = 0, iDefault = 0); + cbChunk = self.getIntParam(self.ksParamLogChunkSize, iMin = 256, iMax = 16777216, iDefault = 1024*1024); + iChunk = self.getIntParam(self.ksParamLogChunkNo, iMin = 0, + iMax = config.g_kcMbMaxMainLog * 1048576 / cbChunk, iDefault = 0); + self._checkForUnknownParameters(); + + oTestSet = TestSetData().initFromDbWithId(self._oDb, idTestSet); + if idLogFile == 0: + oTestFile = TestResultFileDataEx().initFakeMainLog(oTestSet); + aoTimestamps = TestResultLogic(self._oDb).fetchTimestampsForLogViewer(idTestSet); + else: + oTestFile = TestSetLogic(self._oDb).getFile(idTestSet, idLogFile); + aoTimestamps = []; + if oTestFile.sMime not in [ 'text/plain',]: + raise WuiException('The log view does not display files of type: %s' % (oTestFile.sMime,)); + + oContent = WuiLogViewer(oTestSet, oTestFile, cbChunk, iChunk, aoTimestamps, + oDisp = self, fnDPrint = self._oSrvGlue.dprint); + (self._sPageTitle, self._sPageBody) = oContent.show(); + return True; + + def _actionGetFile(self): + """ + Get file action. + """ + from testmanager.core.testset import TestSetData, TestSetLogic; + from testmanager.core.testresults import TestResultFileDataEx; + + idTestSet = self.getIntParam(self.ksParamGetFileSetId, iMin = 1); + idFile = self.getIntParam(self.ksParamGetFileId, iMin = 0, iDefault = 0); + fDownloadIt = self.getBoolParam(self.ksParamGetFileDownloadIt, fDefault = True); + self._checkForUnknownParameters(); + + # + # Get the file info and open it. + # + oTestSet = TestSetData().initFromDbWithId(self._oDb, idTestSet); + if idFile == 0: + oTestFile = TestResultFileDataEx().initFakeMainLog(oTestSet); + else: + oTestFile = TestSetLogic(self._oDb).getFile(idTestSet, idFile); + + (oFile, oSizeOrError, _) = oTestSet.openFile(oTestFile.sFile, 'rb'); + if oFile is None: + raise Exception(oSizeOrError); + + # + # Send the file. + # + self._oSrvGlue.setHeaderField('Content-Type', oTestFile.getMimeWithEncoding()); + if fDownloadIt: + self._oSrvGlue.setHeaderField('Content-Disposition', 'attachment; filename="TestSet-%d-%s"' + % (idTestSet, oTestFile.sFile,)); + while True: + abChunk = oFile.read(262144); + if not abChunk: + break; + self._oSrvGlue.writeRaw(abChunk); + return self.ksDispatchRcAllDone; + + def _actionGenericReport(self, oModelType, oFilterType, oReportType): + """ + Generic report action. + oReportType is a child of WuiReportContentBase. + oFilterType is a child of ModelFilterBase. + oModelType is a child of ReportModelBase. + """ + from testmanager.core.report import ReportModelBase; + + tsEffective = self.getEffectiveDateParam(); + cPeriods = self.getIntParam(self.ksParamReportPeriods, iMin = 2, iMax = 99, iDefault = 7); + cHoursPerPeriod = self.getIntParam(self.ksParamReportPeriodInHours, iMin = 1, iMax = 168, iDefault = 24); + sSubject = self.getStringParam(self.ksParamReportSubject, ReportModelBase.kasSubjects, + ReportModelBase.ksSubEverything); + if sSubject == ReportModelBase.ksSubEverything: + aidSubjects = self.getListOfIntParams(self.ksParamReportSubjectIds, aiDefaults = []); + else: + aidSubjects = self.getListOfIntParams(self.ksParamReportSubjectIds, iMin = 1); + if aidSubjects is None: + raise WuiException('Missing parameter %s' % (self.ksParamReportSubjectIds,)); + + aiSortColumnsDup = self.getListOfIntParams(self.ksParamSortColumns, + iMin = -getattr(oReportType, 'kcMaxSortColumns', cPeriods) + 1, + iMax = getattr(oReportType, 'kcMaxSortColumns', cPeriods), aiDefaults = []); + aiSortColumns = []; + for iSortColumn in aiSortColumnsDup: + if iSortColumn not in aiSortColumns: + aiSortColumns.append(iSortColumn); + + oFilter = oFilterType().initFromParams(self); + self._checkForUnknownParameters(); + + dParams = \ + { + self.ksParamEffectiveDate: tsEffective, + self.ksParamReportPeriods: cPeriods, + self.ksParamReportPeriodInHours: cHoursPerPeriod, + self.ksParamReportSubject: sSubject, + self.ksParamReportSubjectIds: aidSubjects, + }; + ## @todo oFilter. + + oModel = oModelType(self._oDb, tsEffective, cPeriods, cHoursPerPeriod, sSubject, aidSubjects, oFilter); + oContent = oReportType(oModel, dParams, fSubReport = False, aiSortColumns = aiSortColumns, + fnDPrint = self._oSrvGlue.dprint, oDisp = self); + (self._sPageTitle, self._sPageBody) = oContent.show(); + sNavi = self._generateReportNavigation(tsEffective, cHoursPerPeriod, cPeriods); + self._sPageBody = sNavi + self._sPageBody; + + if hasattr(oFilter, 'kiBranches'): + oFilter.aCriteria[oFilter.kiBranches].fExpanded = True; + self._sPageFilter = self._generateResultFilter(oFilter, oModel, tsEffective, '%s hours' % (cHoursPerPeriod * cPeriods,)); + return True; + + def _actionReportSummary(self): + """ Action wrapper. """ + from testmanager.core.report import ReportLazyModel, ReportFilter; + from testmanager.webui.wuireport import WuiReportSummary; + return self._actionGenericReport(ReportLazyModel, ReportFilter, WuiReportSummary); + + def _actionReportRate(self): + """ Action wrapper. """ + from testmanager.core.report import ReportLazyModel, ReportFilter; + from testmanager.webui.wuireport import WuiReportSuccessRate; + return self._actionGenericReport(ReportLazyModel, ReportFilter, WuiReportSuccessRate); + + def _actionReportTestCaseFailures(self): + """ Action wrapper. """ + from testmanager.core.report import ReportLazyModel, ReportFilter; + from testmanager.webui.wuireport import WuiReportTestCaseFailures; + return self._actionGenericReport(ReportLazyModel, ReportFilter, WuiReportTestCaseFailures); + + def _actionReportFailureReasons(self): + """ Action wrapper. """ + from testmanager.core.report import ReportLazyModel, ReportFilter; + from testmanager.webui.wuireport import WuiReportFailureReasons; + return self._actionGenericReport(ReportLazyModel, ReportFilter, WuiReportFailureReasons); + + def _actionGraphWiz(self): + """ + Graph wizard action. + """ + from testmanager.core.report import ReportModelBase, ReportGraphModel; + from testmanager.webui.wuigraphwiz import WuiGraphWiz; + self._sTemplate = 'template-graphwiz.html'; + + tsEffective = self.getEffectiveDateParam(); + cPeriods = self.getIntParam(self.ksParamReportPeriods, iMin = 1, iMax = 1, iDefault = 1); # Not needed yet. + sTmp = self.getStringParam(self.ksParamReportPeriodInHours, sDefault = '3 weeks'); + (cHoursPerPeriod, sError) = utils.parseIntervalHours(sTmp); + if sError is not None: raise WuiException(sError); + asSubjectIds = self.getListOfStrParams(self.ksParamReportSubjectIds); + sSubject = self.getStringParam(self.ksParamReportSubject, [ReportModelBase.ksSubEverything], + ReportModelBase.ksSubEverything); # dummy + aidTestBoxes = self.getListOfIntParams(self.ksParamGraphWizTestBoxIds, iMin = 1, aiDefaults = []); + aidBuildCats = self.getListOfIntParams(self.ksParamGraphWizBuildCatIds, iMin = 1, aiDefaults = []); + aidTestCases = self.getListOfIntParams(self.ksParamGraphWizTestCaseIds, iMin = 1, aiDefaults = []); + fSepTestVars = self.getBoolParam(self.ksParamGraphWizSepTestVars, fDefault = False); + + enmGraphImpl = self.getStringParam(self.ksParamGraphWizImpl, asValidValues = self.kasGraphWizImplValid, + sDefault = self.ksGraphWizImpl_Default); + cx = self.getIntParam(self.ksParamGraphWizWidth, iMin = 128, iMax = 8192, iDefault = 1280); + cy = self.getIntParam(self.ksParamGraphWizHeight, iMin = 128, iMax = 8192, iDefault = int(cx * 5 / 16) ); + cDotsPerInch = self.getIntParam(self.ksParamGraphWizDpi, iMin = 64, iMax = 512, iDefault = 96); + cPtFont = self.getIntParam(self.ksParamGraphWizFontSize, iMin = 6, iMax = 32, iDefault = 8); + fErrorBarY = self.getBoolParam(self.ksParamGraphWizErrorBarY, fDefault = False); + cMaxErrorBarY = self.getIntParam(self.ksParamGraphWizMaxErrorBarY, iMin = 8, iMax = 9999999, iDefault = 18); + cMaxPerGraph = self.getIntParam(self.ksParamGraphWizMaxPerGraph, iMin = 1, iMax = 24, iDefault = 8); + fXkcdStyle = self.getBoolParam(self.ksParamGraphWizXkcdStyle, fDefault = False); + fTabular = self.getBoolParam(self.ksParamGraphWizTabular, fDefault = False); + idSrcTestSet = self.getIntParam(self.ksParamGraphWizSrcTestSetId, iDefault = None); + self._checkForUnknownParameters(); + + dParams = \ + { + self.ksParamEffectiveDate: tsEffective, + self.ksParamReportPeriods: cPeriods, + self.ksParamReportPeriodInHours: cHoursPerPeriod, + self.ksParamReportSubject: sSubject, + self.ksParamReportSubjectIds: asSubjectIds, + self.ksParamGraphWizTestBoxIds: aidTestBoxes, + self.ksParamGraphWizBuildCatIds: aidBuildCats, + self.ksParamGraphWizTestCaseIds: aidTestCases, + self.ksParamGraphWizSepTestVars: fSepTestVars, + + self.ksParamGraphWizImpl: enmGraphImpl, + self.ksParamGraphWizWidth: cx, + self.ksParamGraphWizHeight: cy, + self.ksParamGraphWizDpi: cDotsPerInch, + self.ksParamGraphWizFontSize: cPtFont, + self.ksParamGraphWizErrorBarY: fErrorBarY, + self.ksParamGraphWizMaxErrorBarY: cMaxErrorBarY, + self.ksParamGraphWizMaxPerGraph: cMaxPerGraph, + self.ksParamGraphWizXkcdStyle: fXkcdStyle, + self.ksParamGraphWizTabular: fTabular, + self.ksParamGraphWizSrcTestSetId: idSrcTestSet, + }; + + oModel = ReportGraphModel(self._oDb, tsEffective, cPeriods, cHoursPerPeriod, sSubject, asSubjectIds, + aidTestBoxes, aidBuildCats, aidTestCases, fSepTestVars); + oContent = WuiGraphWiz(oModel, dParams, fSubReport = False, fnDPrint = self._oSrvGlue.dprint, oDisp = self); + (self._sPageTitle, self._sPageBody) = oContent.show(); + return True; + + def _actionVcsHistoryTooltip(self): + """ + Version control system history. + """ + from testmanager.webui.wuivcshistory import WuiVcsHistoryTooltip; + from testmanager.core.vcsrevisions import VcsRevisionLogic; + + self._sTemplate = 'template-tooltip.html'; + iRevision = self.getIntParam(self.ksParamVcsHistoryRevision, iMin = 0, iMax = 999999999); + sRepository = self.getStringParam(self.ksParamVcsHistoryRepository); + cEntries = self.getIntParam(self.ksParamVcsHistoryEntries, iMin = 1, iMax = 1024, iDefault = 8); + self._checkForUnknownParameters(); + + aoEntries = VcsRevisionLogic(self._oDb).fetchTimeline(sRepository, iRevision, cEntries); + oContent = WuiVcsHistoryTooltip(aoEntries, sRepository, iRevision, cEntries, + fnDPrint = self._oSrvGlue.dprint, oDisp = self); + (self._sPageTitle, self._sPageBody) = oContent.show(); + return True; + diff --git a/src/VBox/ValidationKit/testmanager/webui/wuireport.py b/src/VBox/ValidationKit/testmanager/webui/wuireport.py new file mode 100755 index 00000000..f1625176 --- /dev/null +++ b/src/VBox/ValidationKit/testmanager/webui/wuireport.py @@ -0,0 +1,817 @@ +# -*- coding: utf-8 -*- +# $Id: wuireport.py $ + +""" +Test Manager WUI - Reports. +""" + +__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 $" + + +# Validation Kit imports. +from common import webutils; +from testmanager.webui.wuicontentbase import WuiContentBase, WuiTmLink, WuiSvnLinkWithTooltip; +from testmanager.webui.wuihlpgraph import WuiHlpGraphDataTable, WuiHlpBarGraph; +from testmanager.webui.wuitestresult import WuiTestSetLink, WuiTestResultsForTestCaseLink, WuiTestResultsForTestBoxLink; +from testmanager.webui.wuiadmintestcase import WuiTestCaseDetailsLink; +from testmanager.webui.wuiadmintestbox import WuiTestBoxDetailsLinkShort; +from testmanager.core.report import ReportModelBase, ReportFilter; +from testmanager.core.testresults import TestResultFilter; + + +class WuiReportSummaryLink(WuiTmLink): + """ Generic report summary link. """ + + def __init__(self, sSubject, aIdSubjects, sName = WuiContentBase.ksShortReportLink, + tsNow = None, cPeriods = None, cHoursPerPeriod = None, fBracketed = False, dExtraParams = None): + from testmanager.webui.wuimain import WuiMain; + dParams = { + WuiMain.ksParamAction: WuiMain.ksActionReportSummary, + WuiMain.ksParamReportSubject: sSubject, + WuiMain.ksParamReportSubjectIds: aIdSubjects, + }; + if dExtraParams is not None: + dParams.update(dExtraParams); + if tsNow is not None: + dParams[WuiMain.ksParamEffectiveDate] = tsNow; + if cPeriods is not None: + dParams[WuiMain.ksParamReportPeriods] = cPeriods; + if cPeriods is not None: + dParams[WuiMain.ksParamReportPeriodInHours] = cHoursPerPeriod; + WuiTmLink.__init__(self, sName, WuiMain.ksScriptName, dParams, fBracketed = fBracketed); + + +class WuiReportBase(WuiContentBase): + """ + Base class for the reports. + """ + + def __init__(self, oModel, dParams, fSubReport = False, aiSortColumns = None, fnDPrint = None, oDisp = None): + WuiContentBase.__init__(self, fnDPrint = fnDPrint, oDisp = oDisp); + self._oModel = oModel; + self._dParams = dParams; + self._fSubReport = fSubReport; + self._sTitle = None; + self._aiSortColumns = aiSortColumns; + + # Additional URL parameters for reports: + from testmanager.webui.wuimain import WuiMain; + self._dExtraParams = ReportFilter().strainParameters({} if oDisp is None else oDisp.getParameters(), + (WuiMain.ksParamReportPeriods, + WuiMain.ksParamReportPeriodInHours, + WuiMain.ksParamEffectiveDate,)); + # Additional URL parameters for test results: + self._dExtraTestResultsParams = TestResultFilter().strainParameters(oDisp.getParameters(), + (WuiMain.ksParamEffectiveDate,)); + self._dExtraTestResultsParams[WuiMain.ksParamEffectivePeriod] = self.getPeriodForTestResults(); + + + def generateNavigator(self, sWhere): + """ + Generates the navigator (manipulate _dParams). + Returns HTML. + """ + assert sWhere in ('top', 'bottom',); + + return ''; + + def generateReportBody(self): + """ + This is overridden by the child class to generate the report. + Returns HTML. + """ + return '<h3>Must override generateReportBody!</h3>'; + + def show(self): + """ + Generate the report. + Returns (sTitle, HTML). + """ + + sTitle = self._sTitle if self._sTitle is not None else type(self).__name__; + sReport = self.generateReportBody(); + if not self._fSubReport: + sReport = self.generateNavigator('top') + sReport + self.generateNavigator('bottom'); + sTitle = self._oModel.sSubject + ' - ' + sTitle; ## @todo add subject to title in a proper way! + + sReport += '\n\n<!-- HEYYOU: sSubject=%s aidSubjects=%s -->\n\n' % (self._oModel.sSubject, self._oModel.aidSubjects); + return (sTitle, sReport); + + # + # Utility methods + # + + def getPeriodForTestResults(self): + """ + Takes the report period length and count and translates it into a + reasonable test result period (value). + """ + from testmanager.webui.wuimain import WuiMain; + cHours = self._oModel.cPeriods * self._oModel.cHoursPerPeriod; + if cHours > 7*24: + cHours = cHours // 2; + for sPeriodValue, _, cPeriodHours in WuiMain.kaoResultPeriods: + sPeriod = sPeriodValue; + if cPeriodHours >= cHours: + return sPeriod; + return sPeriod; + + @staticmethod + def fmtPct(cHits, cTotal): + """ + Formats a percent number. + Returns a string. + """ + uPct = cHits * 100 // cTotal; + if uPct >= 10 and (uPct > 103 or uPct <= 95): + return '%s%%' % (uPct,); + return '%.1f%%' % (cHits * 100.0 / cTotal,); + + @staticmethod + def fmtPctWithHits(cHits, cTotal): + """ + Formats a percent number with total in parentheses. + Returns a string. + """ + return '%s (%s)' % (WuiReportBase.fmtPct(cHits, cTotal), cHits); + + @staticmethod + def fmtPctWithHitsAndTotal(cHits, cTotal): + """ + Formats a percent number with total in parentheses. + Returns a string. + """ + return '%s (%s/%s)' % (WuiReportBase.fmtPct(cHits, cTotal), cHits, cTotal); + + + +class WuiReportSuccessRate(WuiReportBase): + """ + Generates a report displaying the success rate over time. + """ + + def generateReportBody(self): + self._sTitle = 'Success rate'; + fTailoredForGoogleCharts = True; + + # + # Get the data and check if we have anything in the 'skipped' category. + # + adPeriods = self._oModel.getSuccessRates(); + + cTotalSkipped = 0; + for dStatuses in adPeriods: + cTotalSkipped += dStatuses[ReportModelBase.ksTestStatus_Skipped]; + + # + # Output some general stats before the graphs. + # + cTotalNow = adPeriods[0][ReportModelBase.ksTestStatus_Success]; + cTotalNow += adPeriods[0][ReportModelBase.ksTestStatus_Skipped]; + cSuccessNow = cTotalNow; + cTotalNow += adPeriods[0][ReportModelBase.ksTestStatus_Failure]; + + sReport = '<p>Current success rate: '; + if cTotalNow > 0: + cSkippedNow = adPeriods[0][ReportModelBase.ksTestStatus_Skipped]; + if cSkippedNow > 0: + sReport += '%s (thereof %s skipped)</p>\n' \ + % (self.fmtPct(cSuccessNow, cTotalNow), self.fmtPct(cSkippedNow, cTotalNow),); + else: + sReport += '%s (none skipped)</p>\n' % (self.fmtPct(cSuccessNow, cTotalNow),); + else: + sReport += 'N/A</p>\n' + + # + # Create the data table. + # + if fTailoredForGoogleCharts: + if cTotalSkipped > 0: + oTable = WuiHlpGraphDataTable(None, [ 'Succeeded', 'Skipped', 'Failed' ]); + else: + oTable = WuiHlpGraphDataTable(None, [ 'Succeeded', 'Failed' ]); + else: + if cTotalSkipped > 0: + oTable = WuiHlpGraphDataTable('When', [ 'Succeeded', 'Skipped', 'Failed' ]); + else: + oTable = WuiHlpGraphDataTable('When', [ 'Succeeded', 'Failed' ]); + + for i, dStatuses in enumerate(adPeriods): + cSuccesses = dStatuses[ReportModelBase.ksTestStatus_Success]; + cFailures = dStatuses[ReportModelBase.ksTestStatus_Failure]; + cSkipped = dStatuses[ReportModelBase.ksTestStatus_Skipped]; + + cSuccess = cSuccesses + cSkipped; + cTotal = cSuccess + cFailures; + sPeriod = self._oModel.getPeriodDesc(i); + if fTailoredForGoogleCharts: + if cTotalSkipped > 0: + oTable.addRow(sPeriod, + [ cSuccesses * 100 // cTotal if cTotal else 0, + cSkipped * 100 // cTotal if cTotal else 0, + cFailures * 100 // cTotal if cTotal else 0, ], + [ self.fmtPct(cSuccesses, cTotal) if cSuccesses else None, + self.fmtPct(cSkipped, cTotal) if cSkipped else None, + self.fmtPct(cFailures, cTotal) if cFailures else None, ]); + else: + oTable.addRow(sPeriod, + [ cSuccesses * 100 // cTotal if cTotal else 0, + cFailures * 100 // cTotal if cTotal else 0, ], + [ self.fmtPct(cSuccesses, cTotal) if cSuccesses else None, + self.fmtPct(cFailures, cTotal) if cFailures else None, ]); + elif cTotal > 0: + if cTotalSkipped > 0: + oTable.addRow(sPeriod, + [ cSuccesses * 100 // cTotal, + cSkipped * 100 // cTotal, + cFailures * 100 // cTotal, ], + [ self.fmtPctWithHits(cSuccesses, cTotal), + self.fmtPctWithHits(cSkipped, cTotal), + self.fmtPctWithHits(cFailures, cTotal), ]); + else: + oTable.addRow(sPeriod, + [ cSuccesses * 100 // cTotal, + cFailures * 100 // cTotal, ], + [ self.fmtPctWithHits(cSuccesses, cTotal), + self.fmtPctWithHits(cFailures, cTotal), ]); + elif cTotalSkipped > 0: + oTable.addRow(sPeriod, [ 0, 0, 0 ], [ '0%', '0%', '0%' ]); + else: + oTable.addRow(sPeriod, [ 0, 0 ], [ '0%', '0%' ]); + + # + # Render the graph. + # + oGraph = WuiHlpBarGraph('success-rate', oTable, self._oDisp); + oGraph.setRangeMax(100); + sReport += oGraph.renderGraph(); + + # + # Graph with absolute counts. + # + if fTailoredForGoogleCharts: + if cTotalSkipped > 0: + oTable = WuiHlpGraphDataTable(None, [ 'Succeeded', 'Skipped', 'Failed' ]); + else: + oTable = WuiHlpGraphDataTable(None, [ 'Succeeded', 'Failed' ]); + for i, dStatuses in enumerate(adPeriods): + cSuccesses = dStatuses[ReportModelBase.ksTestStatus_Success]; + cFailures = dStatuses[ReportModelBase.ksTestStatus_Failure]; + cSkipped = dStatuses[ReportModelBase.ksTestStatus_Skipped]; + + if cTotalSkipped > 0: + oTable.addRow(None, #self._oModel.getPeriodDesc(i), + [ cSuccesses, cSkipped, cFailures, ], + [ str(cSuccesses) if cSuccesses > 0 else None, + str(cSkipped) if cSkipped > 0 else None, + str(cFailures) if cFailures > 0 else None, ]); + else: + oTable.addRow(None, #self._oModel.getPeriodDesc(i), + [ cSuccesses, cFailures, ], + [ str(cSuccesses) if cSuccesses > 0 else None, + str(cFailures) if cFailures > 0 else None, ]); + oGraph = WuiHlpBarGraph('success-numbers', oTable, self._oDisp); + oGraph.invertYDirection(); + sReport += oGraph.renderGraph(); + + return sReport; + + +class WuiReportFailuresBase(WuiReportBase): + """ + Common parent of WuiReportFailureReasons and WuiReportTestCaseFailures. + """ + + def _splitSeriesIntoMultipleGraphs(self, aidSorted, cMaxSeriesPerGraph = 8): + """ + Splits the ID array into one or more arrays, making sure we don't + have too many series per graph. + Returns array of ID arrays. + """ + if len(aidSorted) <= cMaxSeriesPerGraph + 2: + return [aidSorted,]; + cGraphs = len(aidSorted) // cMaxSeriesPerGraph + (len(aidSorted) % cMaxSeriesPerGraph != 0); + cPerGraph = len(aidSorted) // cGraphs + (len(aidSorted) % cGraphs != 0); + + aaoRet = []; + cLeft = len(aidSorted); + iSrc = 0; + while cLeft > 0: + cThis = cPerGraph; + if cLeft <= cPerGraph + 2: + cThis = cLeft; + elif cLeft <= cPerGraph * 2 + 4: + cThis = cLeft // 2; + aaoRet.append(aidSorted[iSrc : iSrc + cThis]); + iSrc += cThis; + cLeft -= cThis; + return aaoRet; + + def _formatEdgeOccurenceSubject(self, oTransient): + """ + Worker for _formatEdgeOccurence that child classes overrides to format + their type of subject data in the best possible way. + """ + _ = oTransient; + assert False; + return ''; + + def _formatEdgeOccurence(self, oTransient): + """ + Helper for formatting the transients. + oTransient is of type ReportFailureReasonTransient or ReportTestCaseFailureTransient. + """ + sHtml = u'<li>'; + if oTransient.fEnter: sHtml += 'Since '; + else: sHtml += 'Until '; + sHtml += WuiSvnLinkWithTooltip(oTransient.iRevision, oTransient.sRepository, fBracketed = 'False').toHtml(); + sHtml += u', %s: ' % (WuiTestSetLink(oTransient.idTestSet, self.formatTsShort(oTransient.tsDone), + fBracketed = False).toHtml(), ) + sHtml += self._formatEdgeOccurenceSubject(oTransient); + sHtml += u'</li>\n'; + return sHtml; + + def _generateTransitionList(self, oSet): + """ + Generates the enter and leave lists. + """ + # Skip this if we're looking at builds. + if self._oModel.sSubject in [self._oModel.ksSubBuild,] and len(self._oModel.aidSubjects) in [1, 2]: + return u''; + + sHtml = u'<h4>Movements:</h4>\n' \ + u'<ul>\n'; + if not oSet.aoEnterInfo and not oSet.aoLeaveInfo: + sHtml += u'<li>No changes</li>\n'; + else: + for oTransient in oSet.aoEnterInfo: + sHtml += self._formatEdgeOccurence(oTransient); + for oTransient in oSet.aoLeaveInfo: + sHtml += self._formatEdgeOccurence(oTransient); + sHtml += u'</ul>\n'; + + return sHtml; + + + def _formatSeriesNameColumnHeadersForTable(self): + """ Formats the series name column for the HTML table. """ + return '<th>Subject Name</th>'; + + def _formatSeriesNameForTable(self, oSet, idKey): + """ Formats the series name for the HTML table. """ + _ = oSet; + return '<td>%d</td>' % (idKey,); + + def _formatRowValueForTable(self, oRow, oPeriod, cColsPerSeries): + """ Formats a row value for the HTML table. """ + _ = oPeriod; + if oRow is None: + return u'<td colspan="%d"> </td>' % (cColsPerSeries,); + if cColsPerSeries == 2: + return u'<td align="right">%u%%</td><td align="center">%u / %u</td>' \ + % (oRow.cHits * 100 // oRow.cTotal, oRow.cHits, oRow.cTotal); + return u'<td align="center">%u</td>' % (oRow.cHits,); + + def _formatSeriesTotalForTable(self, oSet, idKey, cColsPerSeries): + """ Formats the totals cell for a data series in the HTML table. """ + dcTotalPerId = getattr(oSet, 'dcTotalPerId', None); + if cColsPerSeries == 2: + return u'<td align="right">%u%%</td><td align="center">%u/%u</td>' \ + % (oSet.dcHitsPerId[idKey] * 100 // dcTotalPerId[idKey], oSet.dcHitsPerId[idKey], dcTotalPerId[idKey]); + return u'<td align="center">%u</td>' % (oSet.dcHitsPerId[idKey],); + + def _generateTableForSet(self, oSet, aidSorted = None, iSortColumn = 0, + fWithTotals = True, cColsPerSeries = None): + """ + Turns the set into a table. + + Returns raw html. + """ + sHtml = u'<table class="tmtbl-report-set" width="100%%">\n'; + if cColsPerSeries is None: + cColsPerSeries = 2 if hasattr(oSet, 'dcTotalPerId') else 1; + + # Header row. + sHtml += u' <tr><thead><th>#</th>'; + sHtml += self._formatSeriesNameColumnHeadersForTable(); + for iPeriod, oPeriod in enumerate(reversed(oSet.aoPeriods)): + sHtml += u'<th colspan="%d"><a href="javascript:ahrefActionSortByColumns(\'%s\',[%s]);">%s</a>%s</th>' \ + % ( cColsPerSeries, self._oDisp.ksParamSortColumns, iPeriod, webutils.escapeElem(oPeriod.sDesc), + '▼' if iPeriod == iSortColumn else ''); + if fWithTotals: + sHtml += u'<th colspan="%d"><a href="javascript:ahrefActionSortByColumns(\'%s\',[%s]);">Total</a>%s</th>' \ + % ( cColsPerSeries, self._oDisp.ksParamSortColumns, len(oSet.aoPeriods), + '▼' if iSortColumn == len(oSet.aoPeriods) else ''); + sHtml += u'</thead></td>\n'; + + # Each data series. + if aidSorted is None: + aidSorted = oSet.dSubjects.keys(); + sHtml += u' <tbody>\n'; + for iRow, idKey in enumerate(aidSorted): + sHtml += u' <tr class="%s">' % ('tmodd' if iRow & 1 else 'tmeven',); + sHtml += u'<td align="left">#%u</td>' % (iRow + 1,); + sHtml += self._formatSeriesNameForTable(oSet, idKey); + for oPeriod in reversed(oSet.aoPeriods): + oRow = oPeriod.dRowsById.get(idKey, None); + sHtml += self._formatRowValueForTable(oRow, oPeriod, cColsPerSeries); + if fWithTotals: + sHtml += self._formatSeriesTotalForTable(oSet, idKey, cColsPerSeries); + sHtml += u' </tr>\n'; + sHtml += u' </tbody>\n'; + sHtml += u'</table>\n'; + return sHtml; + + +class WuiReportFailuresWithTotalBase(WuiReportFailuresBase): + """ + For ReportPeriodSetWithTotalBase. + """ + + def _formatSeriedNameForGraph(self, oSubject): + """ + Format the subject name for the graph. + """ + return str(oSubject); + + def _getSortedIds(self, oSet): + """ + Get default sorted subject IDs and which column. + """ + + # Figure the sorting column. + if self._aiSortColumns is not None \ + and self._aiSortColumns \ + and abs(self._aiSortColumns[0]) <= len(oSet.aoPeriods): + iSortColumn = abs(self._aiSortColumns[0]); + fByTotal = iSortColumn >= len(oSet.aoPeriods); # pylint: disable=unused-variable + elif oSet.cMaxTotal < 10: + iSortColumn = len(oSet.aoPeriods); + else: + iSortColumn = 0; + + if iSortColumn >= len(oSet.aoPeriods): + # Sort the total. + aidSortedRaw = sorted(oSet.dSubjects, + key = lambda idKey: oSet.dcHitsPerId[idKey] * 10000 // oSet.dcTotalPerId[idKey], + reverse = True); + else: + # Sort by NOW column. + dTmp = {}; + for idKey in oSet.dSubjects: + oRow = oSet.aoPeriods[-1 - iSortColumn].dRowsById.get(idKey, None); + if oRow is None: dTmp[idKey] = 0; + else: dTmp[idKey] = oRow.cHits * 10000 // max(1, oRow.cTotal); + aidSortedRaw = sorted(dTmp, key = lambda idKey: dTmp[idKey], reverse = True); + return (aidSortedRaw, iSortColumn); + + def _generateGraph(self, oSet, sIdBase, aidSortedRaw): + """ + Generates graph. + """ + sHtml = u''; + fGenerateGraph = len(aidSortedRaw) <= 6 and len(aidSortedRaw) > 0; ## Make this configurable. + if fGenerateGraph: + # Figure the graph width for all of them. + uPctMax = max(oSet.uMaxPct, oSet.cMaxHits * 100 // oSet.cMaxTotal); + uPctMax = max(uPctMax + 2, 10); + + for _, aidSorted in enumerate(self._splitSeriesIntoMultipleGraphs(aidSortedRaw, 8)): + asNames = []; + for idKey in aidSorted: + oSubject = oSet.dSubjects[idKey]; + asNames.append(self._formatSeriedNameForGraph(oSubject)); + + oTable = WuiHlpGraphDataTable('Period', asNames); + + for _, oPeriod in enumerate(reversed(oSet.aoPeriods)): + aiValues = []; + asValues = []; + + for idKey in aidSorted: + oRow = oPeriod.dRowsById.get(idKey, None); + if oRow is not None: + aiValues.append(oRow.cHits * 100 // oRow.cTotal); + asValues.append(self.fmtPctWithHitsAndTotal(oRow.cHits, oRow.cTotal)); + else: + aiValues.append(0); + asValues.append('0'); + + oTable.addRow(oPeriod.sDesc, aiValues, asValues); + + if True: # pylint: disable=using-constant-test + aiValues = []; + asValues = []; + for idKey in aidSorted: + uPct = oSet.dcHitsPerId[idKey] * 100 // oSet.dcTotalPerId[idKey]; + aiValues.append(uPct); + asValues.append(self.fmtPctWithHitsAndTotal(oSet.dcHitsPerId[idKey], oSet.dcTotalPerId[idKey])); + oTable.addRow('Totals', aiValues, asValues); + + oGraph = WuiHlpBarGraph(sIdBase, oTable, self._oDisp); + oGraph.setRangeMax(uPctMax); + sHtml += '<br>\n'; + sHtml += oGraph.renderGraph(); + return sHtml; + + + +class WuiReportFailureReasons(WuiReportFailuresBase): + """ + Generates a report displaying the failure reasons over time. + """ + + def _formatEdgeOccurenceSubject(self, oTransient): + return u'%s / %s' % ( webutils.escapeElem(oTransient.oReason.oCategory.sShort), + webutils.escapeElem(oTransient.oReason.sShort),); + + def _formatSeriesNameColumnHeadersForTable(self): + return '<th>Failure Reason</th>'; + + def _formatSeriesNameForTable(self, oSet, idKey): + oReason = oSet.dSubjects[idKey]; + sHtml = u'<td>'; + sHtml += u'%s / %s' % ( webutils.escapeElem(oReason.oCategory.sShort), webutils.escapeElem(oReason.sShort),); + sHtml += u'</td>'; + return sHtml; + + + def generateReportBody(self): + self._sTitle = 'Failure reasons'; + + # + # Get the data and sort the data series in descending order of badness. + # + oSet = self._oModel.getFailureReasons(); + aidSortedRaw = sorted(oSet.dSubjects, key = lambda idReason: oSet.dcHitsPerId[idReason], reverse = True); + + # + # Generate table and transition list. These are the most useful ones with the current graph machinery. + # + sHtml = self._generateTableForSet(oSet, aidSortedRaw, len(oSet.aoPeriods)); + sHtml += self._generateTransitionList(oSet); + + # + # Check if most of the stuff is without any assign reason, if so, skip + # that part of the graph so it doesn't offset the interesting bits. + # + fIncludeWithoutReason = True; + for oPeriod in reversed(oSet.aoPeriods): + if oPeriod.cWithoutReason > oSet.cMaxHits * 4: + fIncludeWithoutReason = False; + sHtml += '<p>Warning: Many failures without assigned reason!</p>\n'; + break; + + # + # Generate the graph. + # + fGenerateGraph = len(aidSortedRaw) <= 9 and len(aidSortedRaw) > 0; ## Make this configurable. + if fGenerateGraph: + aidSorted = aidSortedRaw; + + asNames = []; + for idReason in aidSorted: + oReason = oSet.dSubjects[idReason]; + asNames.append('%s / %s' % (oReason.oCategory.sShort, oReason.sShort,) ) + if fIncludeWithoutReason: + asNames.append('No reason'); + + oTable = WuiHlpGraphDataTable('Period', asNames); + + cMax = oSet.cMaxHits; + for _, oPeriod in enumerate(reversed(oSet.aoPeriods)): + aiValues = []; + + for idReason in aidSorted: + oRow = oPeriod.dRowsById.get(idReason, None); + iValue = oRow.cHits if oRow is not None else 0; + aiValues.append(iValue); + + if fIncludeWithoutReason: + aiValues.append(oPeriod.cWithoutReason); + if oPeriod.cWithoutReason > cMax: + cMax = oPeriod.cWithoutReason; + + oTable.addRow(oPeriod.sDesc, aiValues); + + oGraph = WuiHlpBarGraph('failure-reason', oTable, self._oDisp); + oGraph.setRangeMax(max(cMax + 1, 3)); + sHtml += oGraph.renderGraph(); + return sHtml; + + +class WuiReportTestCaseFailures(WuiReportFailuresWithTotalBase): + """ + Generates a report displaying the failure reasons over time. + """ + + def _formatEdgeOccurenceSubject(self, oTransient): + sHtml = u'%s ' % ( webutils.escapeElem(oTransient.oSubject.sName),); + sHtml += WuiTestCaseDetailsLink(oTransient.oSubject.idTestCase, fBracketed = False).toHtml(); + return sHtml; + + def _formatSeriesNameColumnHeadersForTable(self): + return '<th>Test Case</th>'; + + def _formatSeriesNameForTable(self, oSet, idKey): + oTestCase = oSet.dSubjects[idKey]; + return u'<td>%s %s %s</td>' % \ + ( WuiTestResultsForTestCaseLink(idKey, oTestCase.sName, self._dExtraTestResultsParams).toHtml(), + WuiTestCaseDetailsLink(oTestCase.idTestCase).toHtml(), + WuiReportSummaryLink(ReportModelBase.ksSubTestCase, oTestCase.idTestCase, + dExtraParams = self._dExtraParams).toHtml(),); + + def _formatSeriedNameForGraph(self, oSubject): + return oSubject.sName; + + def generateReportBody(self): + self._sTitle = 'Test Case Failures'; + oSet = self._oModel.getTestCaseFailures(); + (aidSortedRaw, iSortColumn) = self._getSortedIds(oSet); + + sHtml = self._generateTableForSet(oSet, aidSortedRaw, iSortColumn); + sHtml += self._generateTransitionList(oSet); + sHtml += self._generateGraph(oSet, 'testcase-graph', aidSortedRaw); + return sHtml; + + +class WuiReportTestCaseArgsFailures(WuiReportFailuresWithTotalBase): + """ + Generates a report displaying the failure reasons over time. + """ + + def __init__(self, oModel, dParams, fSubReport = False, aiSortColumns = None, fnDPrint = None, oDisp = None): + WuiReportFailuresWithTotalBase.__init__(self, oModel, dParams, fSubReport = fSubReport, + aiSortColumns = aiSortColumns, fnDPrint = fnDPrint, oDisp = oDisp); + self.oTestCaseCrit = TestResultFilter().aCriteria[TestResultFilter.kiTestCases] # type: FilterCriterion + + @staticmethod + def _formatName(oTestCaseArgs): + """ Internal helper for formatting the testcase name. """ + if oTestCaseArgs.sSubName: + sName = u'%s / %s' % ( oTestCaseArgs.oTestCase.sName, oTestCaseArgs.sSubName, ); + else: + sName = u'%s / #%u' % ( oTestCaseArgs.oTestCase.sName, oTestCaseArgs.idTestCaseArgs, ); + return sName; + + def _formatEdgeOccurenceSubject(self, oTransient): + sHtml = u'%s ' % ( webutils.escapeElem(self._formatName(oTransient.oSubject)),); + sHtml += WuiTestCaseDetailsLink(oTransient.oSubject.idTestCase, fBracketed = False).toHtml(); + return sHtml; + + def _formatSeriesNameColumnHeadersForTable(self): + return '<th>Test Case / Variation</th>'; + + def _formatSeriesNameForTable(self, oSet, idKey): + oTestCaseArgs = oSet.dSubjects[idKey]; + sHtml = u'<td>'; + dParams = dict(self._dExtraTestResultsParams); + dParams[self.oTestCaseCrit.sVarNm] = oTestCaseArgs.idTestCase; + dParams[self.oTestCaseCrit.oSub.sVarNm] = idKey; + sHtml += WuiTestResultsForTestCaseLink(oTestCaseArgs.idTestCase, self._formatName(oTestCaseArgs), dParams).toHtml(); + sHtml += u' '; + sHtml += WuiTestCaseDetailsLink(oTestCaseArgs.idTestCase).toHtml(); + #sHtml += u' '; + #sHtml += WuiReportSummaryLink(ReportModelBase.ksSubTestCaseArgs, oTestCaseArgs.idTestCaseArgs, + # sName = self._formatName(oTestCaseArgs), dExtraParams = self._dExtraParams).toHtml(); + sHtml += u'</td>'; + return sHtml; + + def _formatSeriedNameForGraph(self, oSubject): + return self._formatName(oSubject); + + def generateReportBody(self): + self._sTitle = 'Test Case Variation Failures'; + oSet = self._oModel.getTestCaseVariationFailures(); + (aidSortedRaw, iSortColumn) = self._getSortedIds(oSet); + + sHtml = self._generateTableForSet(oSet, aidSortedRaw, iSortColumn); + sHtml += self._generateTransitionList(oSet); + sHtml += self._generateGraph(oSet, 'testcasearg-graph', aidSortedRaw); + return sHtml; + + + +class WuiReportTestBoxFailures(WuiReportFailuresWithTotalBase): + """ + Generates a report displaying the failure reasons over time. + """ + + def _formatEdgeOccurenceSubject(self, oTransient): + sHtml = u'%s ' % ( webutils.escapeElem(oTransient.oSubject.sName),); + sHtml += WuiTestBoxDetailsLinkShort(oTransient.oSubject).toHtml(); + return sHtml; + + def _formatSeriesNameColumnHeadersForTable(self): + return '<th colspan="5">Test Box</th>'; + + def _formatSeriesNameForTable(self, oSet, idKey): + oTestBox = oSet.dSubjects[idKey]; + sHtml = u'<td>'; + sHtml += WuiTestResultsForTestBoxLink(idKey, oTestBox.sName, self._dExtraTestResultsParams).toHtml() + sHtml += u' '; + sHtml += WuiTestBoxDetailsLinkShort(oTestBox).toHtml(); + sHtml += u' '; + sHtml += WuiReportSummaryLink(ReportModelBase.ksSubTestBox, oTestBox.idTestBox, + dExtraParams = self._dExtraParams).toHtml(); + sHtml += u'</td>'; + sOsAndVer = '%s %s' % (oTestBox.sOs, oTestBox.sOsVersion.strip(),); + if len(sOsAndVer) < 22: + sHtml += u'<td>%s</td>' % (webutils.escapeElem(sOsAndVer),); + else: # wonder if td.title works.. + sHtml += u'<td title="%s" width="1%%" style="white-space:nowrap;">%s...</td>' \ + % (webutils.escapeAttr(sOsAndVer), webutils.escapeElem(sOsAndVer[:20])); + sHtml += u'<td>%s</td>' % (webutils.escapeElem(oTestBox.getArchBitString()),); + sHtml += u'<td>%s</td>' % (webutils.escapeElem(oTestBox.getPrettyCpuVendor()),); + sHtml += u'<td>%s' % (oTestBox.getPrettyCpuVersion(),); + if oTestBox.fCpuNestedPaging: sHtml += u', np'; + elif oTestBox.fCpuHwVirt: sHtml += u', hw'; + else: sHtml += u', raw'; + if oTestBox.fCpu64BitGuest: sHtml += u', 64'; + sHtml += u'</td>'; + return sHtml; + + def _formatSeriedNameForGraph(self, oSubject): + return oSubject.sName; + + def generateReportBody(self): + self._sTitle = 'Test Box Failures'; + oSet = self._oModel.getTestBoxFailures(); + (aidSortedRaw, iSortColumn) = self._getSortedIds(oSet); + + sHtml = self._generateTableForSet(oSet, aidSortedRaw, iSortColumn); + sHtml += self._generateTransitionList(oSet); + sHtml += self._generateGraph(oSet, 'testbox-graph', aidSortedRaw); + return sHtml; + + +class WuiReportSummary(WuiReportBase): + """ + Summary report. + """ + + def generateReportBody(self): + self._sTitle = 'Summary'; + sHtml = '<p>This will display several reports and listings useful to get an overview of %s (id=%s).</p>' \ + % (self._oModel.sSubject, self._oModel.aidSubjects,); + + aoReports = []; + + aoReports.append(WuiReportSuccessRate( self._oModel, self._dParams, fSubReport = True, + aiSortColumns = self._aiSortColumns, + fnDPrint = self._fnDPrint, oDisp = self._oDisp)); + aoReports.append(WuiReportTestCaseFailures(self._oModel, self._dParams, fSubReport = True, + aiSortColumns = self._aiSortColumns, + fnDPrint = self._fnDPrint, oDisp = self._oDisp)); + if self._oModel.sSubject == ReportModelBase.ksSubTestCase: + aoReports.append(WuiReportTestCaseArgsFailures(self._oModel, self._dParams, fSubReport = True, + aiSortColumns = self._aiSortColumns, + fnDPrint = self._fnDPrint, oDisp = self._oDisp)); + aoReports.append(WuiReportTestBoxFailures( self._oModel, self._dParams, fSubReport = True, + aiSortColumns = self._aiSortColumns, + fnDPrint = self._fnDPrint, oDisp = self._oDisp)); + aoReports.append(WuiReportFailureReasons( self._oModel, self._dParams, fSubReport = True, + aiSortColumns = self._aiSortColumns, + fnDPrint = self._fnDPrint, oDisp = self._oDisp)); + + for oReport in aoReports: + (sTitle, sContent) = oReport.show(); + sHtml += '<br>'; # drop this layout hack + sHtml += '<div>'; + sHtml += '<h3>%s</h3>\n' % (webutils.escapeElem(sTitle),); + sHtml += sContent; + sHtml += '</div>'; + + return sHtml; + diff --git a/src/VBox/ValidationKit/testmanager/webui/wuitestresult.py b/src/VBox/ValidationKit/testmanager/webui/wuitestresult.py new file mode 100755 index 00000000..1edb03d5 --- /dev/null +++ b/src/VBox/ValidationKit/testmanager/webui/wuitestresult.py @@ -0,0 +1,965 @@ +# -*- coding: utf-8 -*- +# $Id: wuitestresult.py $ + +""" +Test Manager WUI - Test Results. +""" + +__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 $" + +# Python imports. +import datetime; + +# Validation Kit imports. +from testmanager.webui.wuicontentbase import WuiContentBase, WuiListContentBase, WuiHtmlBase, WuiTmLink, WuiLinkBase, \ + WuiSvnLink, WuiSvnLinkWithTooltip, WuiBuildLogLink, WuiRawHtml, \ + WuiHtmlKeeper; +from testmanager.webui.wuimain import WuiMain; +from testmanager.webui.wuihlpform import WuiHlpForm; +from testmanager.webui.wuiadminfailurereason import WuiFailureReasonAddLink, WuiFailureReasonDetailsLink; +from testmanager.webui.wuitestresultfailure import WuiTestResultFailureDetailsLink; +from testmanager.core.failurereason import FailureReasonData, FailureReasonLogic; +from testmanager.core.report import ReportGraphModel, ReportModelBase; +from testmanager.core.testbox import TestBoxData; +from testmanager.core.testcase import TestCaseData; +from testmanager.core.testset import TestSetData; +from testmanager.core.testgroup import TestGroupData; +from testmanager.core.testresultfailures import TestResultFailureData; +from testmanager.core.build import BuildData; +from testmanager.core import db; +from testmanager import config; +from common import webutils, utils; + + +class WuiTestSetLink(WuiTmLink): + """ Test set link. """ + + def __init__(self, idTestSet, sName = WuiContentBase.ksShortDetailsLink, fBracketed = False): + WuiTmLink.__init__(self, sName, WuiMain.ksScriptName, + { WuiMain.ksParamAction: WuiMain.ksActionTestResultDetails, + TestSetData.ksParam_idTestSet: idTestSet, }, fBracketed = fBracketed); + self.idTestSet = idTestSet; + +class WuiTestResultsForSomethingLink(WuiTmLink): + """ Test results link for a grouping. """ + + def __init__(self, sGroupedBy, idGroupMember, sName = WuiContentBase.ksShortTestResultsLink, + dExtraParams = None, fBracketed = False): + dParams = dict(dExtraParams) if dExtraParams else {}; + dParams[WuiMain.ksParamAction] = sGroupedBy; + dParams[WuiMain.ksParamGroupMemberId] = idGroupMember; + WuiTmLink.__init__(self, sName, WuiMain.ksScriptName, dParams, fBracketed = fBracketed); + + +class WuiTestResultsForTestBoxLink(WuiTestResultsForSomethingLink): + """ Test results link for a given testbox. """ + + def __init__(self, idTestBox, sName = WuiContentBase.ksShortTestResultsLink, dExtraParams = None, fBracketed = False): + WuiTestResultsForSomethingLink.__init__(self, WuiMain.ksActionResultsGroupedByTestBox, idTestBox, + sName = sName, dExtraParams = dExtraParams, fBracketed = fBracketed); + + +class WuiTestResultsForTestCaseLink(WuiTestResultsForSomethingLink): + """ Test results link for a given testcase. """ + + def __init__(self, idTestCase, sName = WuiContentBase.ksShortTestResultsLink, dExtraParams = None, fBracketed = False): + WuiTestResultsForSomethingLink.__init__(self, WuiMain.ksActionResultsGroupedByTestCase, idTestCase, + sName = sName, dExtraParams = dExtraParams, fBracketed = fBracketed); + + +class WuiTestResultsForBuildRevLink(WuiTestResultsForSomethingLink): + """ Test results link for a given build revision. """ + + def __init__(self, iRevision, sName = WuiContentBase.ksShortTestResultsLink, dExtraParams = None, fBracketed = False): + WuiTestResultsForSomethingLink.__init__(self, WuiMain.ksActionResultsGroupedByBuildRev, iRevision, + sName = sName, dExtraParams = dExtraParams, fBracketed = fBracketed); + + +class WuiTestResult(WuiContentBase): + """Display test case result""" + + def __init__(self, fnDPrint = None, oDisp = None): + WuiContentBase.__init__(self, fnDPrint = fnDPrint, oDisp = oDisp); + + # Cyclic import hacks. + from testmanager.webui.wuiadmin import WuiAdmin; + self.oWuiAdmin = WuiAdmin; + + def _toHtml(self, oObject): + """Translate some object to HTML.""" + if isinstance(oObject, WuiHtmlBase): + return oObject.toHtml(); + if db.isDbTimestamp(oObject): + return webutils.escapeElem(self.formatTsShort(oObject)); + if db.isDbInterval(oObject): + return webutils.escapeElem(self.formatIntervalShort(oObject)); + if utils.isString(oObject): + return webutils.escapeElem(oObject); + return webutils.escapeElem(str(oObject)); + + def _htmlTable(self, aoTableContent): + """Generate HTML code for table""" + sHtml = u' <table class="tmtbl-testresult-details" width="100%%">\n'; + + for aoSubRows in aoTableContent: + if not aoSubRows: + continue; # Can happen if there is no testsuit. + oCaption = aoSubRows[0]; + sHtml += u' \n' \ + u' <tr class="tmtbl-result-details-caption">\n' \ + u' <td colspan="2">%s</td>\n' \ + u' </tr>\n' \ + % (self._toHtml(oCaption),); + + iRow = 0; + for aoRow in aoSubRows[1:]: + iRow += 1; + sHtml += u' <tr class="%s">\n' % ('tmodd' if iRow & 1 else 'tmeven',); + if len(aoRow) == 1: + sHtml += u' <td class="tmtbl-result-details-subcaption" colspan="2">%s</td>\n' \ + % (self._toHtml(aoRow[0]),); + else: + sHtml += u' <th scope="row">%s</th>\n' % (webutils.escapeElem(aoRow[0]),); + if len(aoRow) > 2: + sHtml += u' <td>%s</td>\n' % (aoRow[2](aoRow[1]),); + else: + sHtml += u' <td>%s</td>\n' % (self._toHtml(aoRow[1]),); + sHtml += u' </tr>\n'; + + sHtml += u' </table>\n'; + + return sHtml + + def _highlightStatus(self, sStatus): + """Return sStatus string surrounded by HTML highlight code """ + sTmp = '<font color=%s><b>%s</b></font>' \ + % ('red' if sStatus == 'failure' else 'green', webutils.escapeElem(sStatus.upper())) + return sTmp + + def _anchorAndAppendBinaries(self, sBinaries, aoRows): + """ Formats each binary (if any) into a row with a download link. """ + if sBinaries is not None: + for sBinary in sBinaries.split(','): + if not webutils.hasSchema(sBinary): + sBinary = config.g_ksBuildBinUrlPrefix + sBinary; + aoRows.append([WuiLinkBase(webutils.getFilename(sBinary), sBinary, fBracketed = False),]); + return aoRows; + + + def _formatEventTimestampHtml(self, tsEvent, tsLog, idEvent, oTestSet): + """ Formats an event timestamp with a main log link. """ + tsEvent = db.dbTimestampToZuluDatetime(tsEvent); + #sFormattedTimestamp = u'%04u\u2011%02u\u2011%02u\u00a0%02u:%02u:%02uZ' \ + # % ( tsEvent.year, tsEvent.month, tsEvent.day, + # tsEvent.hour, tsEvent.minute, tsEvent.second,); + sFormattedTimestamp = u'%02u:%02u:%02uZ' \ + % ( tsEvent.hour, tsEvent.minute, tsEvent.second,); + sTitle = u'#%u - %04u\u2011%02u\u2011%02u\u00a0%02u:%02u:%02u.%06uZ' \ + % ( idEvent, tsEvent.year, tsEvent.month, tsEvent.day, + tsEvent.hour, tsEvent.minute, tsEvent.second, tsEvent.microsecond, ); + tsLog = db.dbTimestampToZuluDatetime(tsLog); + sFragment = u'%02u_%02u_%02u_%06u' % ( tsLog.hour, tsLog.minute, tsLog.second, tsLog.microsecond); + return WuiTmLink(sFormattedTimestamp, '', + { WuiMain.ksParamAction: WuiMain.ksActionViewLog, + WuiMain.ksParamLogSetId: oTestSet.idTestSet, }, + sFragmentId = sFragment, sTitle = sTitle, fBracketed = False, ).toHtml(); + + def _recursivelyGenerateEvents(self, oTestResult, sParentName, sLineage, iRow, + iFailure, oTestSet, iDepth): # pylint: disable=too-many-locals + """ + Recursively generate event table rows for the result set. + + oTestResult is an object of the type TestResultDataEx. + """ + # Hack: Replace empty outer test result name with (pretty) command line. + if iRow == 1: + sName = ''; + sDisplayName = sParentName; + else: + sName = oTestResult.sName if sParentName == '' else '%s, %s' % (sParentName, oTestResult.sName,); + sDisplayName = webutils.escapeElem(sName); + + # Format error count. + sErrCnt = ''; + if oTestResult.cErrors > 0: + sErrCnt = ' (1 error)' if oTestResult.cErrors == 1 else ' (%d errors)' % oTestResult.cErrors; + + # Format bits for adding or editing the failure reason. Level 0 is handled at the top of the page. + sChangeReason = ''; + if oTestResult.cErrors > 0 and iDepth > 0 and self._oDisp is not None and not self._oDisp.isReadOnlyUser(): + dTmp = { + self._oDisp.ksParamAction: self._oDisp.ksActionTestResultFailureAdd if oTestResult.oReason is None else + self._oDisp.ksActionTestResultFailureEdit, + TestResultFailureData.ksParam_idTestResult: oTestResult.idTestResult, + }; + sChangeReason = ' <a href="?%s" class="tmtbl-edit-reason" onclick="addRedirectToAnchorHref(this)">%s</a> ' \ + % ( webutils.encodeUrlParams(dTmp), WuiContentBase.ksShortEditLinkHtml ); + + # Format the include in graph checkboxes. + sLineage += ':%u' % (oTestResult.idStrName,); + sResultGraph = '<input type="checkbox" name="%s" value="%s%s" title="Include result in graph."/>' \ + % (WuiMain.ksParamReportSubjectIds, ReportGraphModel.ksTypeResult, sLineage,); + sElapsedGraph = ''; + if oTestResult.tsElapsed is not None: + sElapsedGraph = '<input type="checkbox" name="%s" value="%s%s" title="Include elapsed time in graph."/>' \ + % ( WuiMain.ksParamReportSubjectIds, ReportGraphModel.ksTypeElapsed, sLineage); + + + if not oTestResult.aoChildren \ + and len(oTestResult.aoValues) + len(oTestResult.aoMsgs) + len(oTestResult.aoFiles) == 0: + # Leaf - single row. + tsEvent = oTestResult.tsCreated; + if oTestResult.tsElapsed is not None: + tsEvent += oTestResult.tsElapsed; + sHtml = ' <tr class="%s tmtbl-events-leaf tmtbl-events-lvl%s tmstatusrow-%s" id="S%u">\n' \ + ' <td id="E%u">%s</td>\n' \ + ' <td>%s</td>\n' \ + ' <td>%s</td>\n' \ + ' <td>%s</td>\n' \ + ' <td colspan="2"%s>%s%s%s</td>\n' \ + ' <td>%s</td>\n' \ + ' </tr>\n' \ + % ( 'tmodd' if iRow & 1 else 'tmeven', iDepth, oTestResult.enmStatus, oTestResult.idTestResult, + oTestResult.idTestResult, + self._formatEventTimestampHtml(tsEvent, oTestResult.tsCreated, oTestResult.idTestResult, oTestSet), + sElapsedGraph, + webutils.escapeElem(self.formatIntervalShort(oTestResult.tsElapsed)) if oTestResult.tsElapsed is not None + else '', + sDisplayName, + ' id="failure-%u"' % (iFailure,) if oTestResult.isFailure() else '', + webutils.escapeElem(oTestResult.enmStatus), webutils.escapeElem(sErrCnt), + sChangeReason if oTestResult.oReason is None else '', + sResultGraph ); + iRow += 1; + else: + # Multiple rows. + sHtml = ' <tr class="%s tmtbl-events-first tmtbl-events-lvl%s ">\n' \ + ' <td>%s</td>\n' \ + ' <td></td>\n' \ + ' <td></td>\n' \ + ' <td>%s</td>\n' \ + ' <td colspan="2">%s</td>\n' \ + ' <td></td>\n' \ + ' </tr>\n' \ + % ( 'tmodd' if iRow & 1 else 'tmeven', iDepth, + self._formatEventTimestampHtml(oTestResult.tsCreated, oTestResult.tsCreated, + oTestResult.idTestResult, oTestSet), + sDisplayName, + 'running' if oTestResult.tsElapsed is None else '', ); + iRow += 1; + + # Depth. Check if our error count is just reflecting the one of our children. + cErrorsBelow = 0; + for oChild in oTestResult.aoChildren: + (sChildHtml, iRow, iFailure) = self._recursivelyGenerateEvents(oChild, sName, sLineage, + iRow, iFailure, oTestSet, iDepth + 1); + sHtml += sChildHtml; + cErrorsBelow += oChild.cErrors; + + # Messages. + for oMsg in oTestResult.aoMsgs: + sHtml += ' <tr class="%s tmtbl-events-message tmtbl-events-lvl%s">\n' \ + ' <td>%s</td>\n' \ + ' <td></td>\n' \ + ' <td></td>\n' \ + ' <td colspan="3">%s: %s</td>\n' \ + ' <td></td>\n' \ + ' </tr>\n' \ + % ( 'tmodd' if iRow & 1 else 'tmeven', iDepth, + self._formatEventTimestampHtml(oMsg.tsCreated, oMsg.tsCreated, oMsg.idTestResultMsg, oTestSet), + webutils.escapeElem(oMsg.enmLevel), + webutils.escapeElem(oMsg.sMsg), ); + iRow += 1; + + # Values. + for oValue in oTestResult.aoValues: + sHtml += ' <tr class="%s tmtbl-events-value tmtbl-events-lvl%s">\n' \ + ' <td>%s</td>\n' \ + ' <td></td>\n' \ + ' <td></td>\n' \ + ' <td>%s</td>\n' \ + ' <td class="tmtbl-events-number">%s</td>\n' \ + ' <td class="tmtbl-events-unit">%s</td>\n' \ + ' <td><input type="checkbox" name="%s" value="%s%s:%u" title="Include value in graph."></td>\n' \ + ' </tr>\n' \ + % ( 'tmodd' if iRow & 1 else 'tmeven', iDepth, + self._formatEventTimestampHtml(oValue.tsCreated, oValue.tsCreated, oValue.idTestResultValue, oTestSet), + webutils.escapeElem(oValue.sName), + utils.formatNumber(oValue.lValue).replace(' ', ' '), + webutils.escapeElem(oValue.sUnit), + WuiMain.ksParamReportSubjectIds, ReportGraphModel.ksTypeValue, sLineage, oValue.idStrName, ); + iRow += 1; + + # Files. + for oFile in oTestResult.aoFiles: + if oFile.sMime in [ 'text/plain', ]: + aoLinks = [ + WuiTmLink('%s (%s)' % (oFile.sFile, oFile.sKind), '', + { self._oDisp.ksParamAction: self._oDisp.ksActionViewLog, + self._oDisp.ksParamLogSetId: oTestSet.idTestSet, + self._oDisp.ksParamLogFileId: oFile.idTestResultFile, }, + sTitle = oFile.sDescription), + WuiTmLink('View Raw', '', + { self._oDisp.ksParamAction: self._oDisp.ksActionGetFile, + self._oDisp.ksParamGetFileSetId: oTestSet.idTestSet, + self._oDisp.ksParamGetFileId: oFile.idTestResultFile, + self._oDisp.ksParamGetFileDownloadIt: False, }, + sTitle = oFile.sDescription), + ] + else: + aoLinks = [ + WuiTmLink('%s (%s)' % (oFile.sFile, oFile.sKind), '', + { self._oDisp.ksParamAction: self._oDisp.ksActionGetFile, + self._oDisp.ksParamGetFileSetId: oTestSet.idTestSet, + self._oDisp.ksParamGetFileId: oFile.idTestResultFile, + self._oDisp.ksParamGetFileDownloadIt: False, }, + sTitle = oFile.sDescription), + ] + aoLinks.append(WuiTmLink('Download', '', + { self._oDisp.ksParamAction: self._oDisp.ksActionGetFile, + self._oDisp.ksParamGetFileSetId: oTestSet.idTestSet, + self._oDisp.ksParamGetFileId: oFile.idTestResultFile, + self._oDisp.ksParamGetFileDownloadIt: True, }, + sTitle = oFile.sDescription)); + + sHtml += ' <tr class="%s tmtbl-events-file tmtbl-events-lvl%s">\n' \ + ' <td>%s</td>\n' \ + ' <td></td>\n' \ + ' <td></td>\n' \ + ' <td>%s</td>\n' \ + ' <td></td>\n' \ + ' <td></td>\n' \ + ' <td></td>\n' \ + ' </tr>\n' \ + % ( 'tmodd' if iRow & 1 else 'tmeven', iDepth, + self._formatEventTimestampHtml(oFile.tsCreated, oFile.tsCreated, oFile.idTestResultFile, oTestSet), + '\n'.join(oLink.toHtml() for oLink in aoLinks),); + iRow += 1; + + # Done? + if oTestResult.tsElapsed is not None: + tsEvent = oTestResult.tsCreated + oTestResult.tsElapsed; + sHtml += ' <tr class="%s tmtbl-events-final tmtbl-events-lvl%s tmstatusrow-%s" id="E%d">\n' \ + ' <td>%s</td>\n' \ + ' <td>%s</td>\n' \ + ' <td>%s</td>\n' \ + ' <td>%s</td>\n' \ + ' <td colspan="2"%s>%s%s%s</td>\n' \ + ' <td>%s</td>\n' \ + ' </tr>\n' \ + % ( 'tmodd' if iRow & 1 else 'tmeven', iDepth, oTestResult.enmStatus, oTestResult.idTestResult, + self._formatEventTimestampHtml(tsEvent, tsEvent, oTestResult.idTestResult, oTestSet), + sElapsedGraph, + webutils.escapeElem(self.formatIntervalShort(oTestResult.tsElapsed)), + sDisplayName, + ' id="failure-%u"' % (iFailure,) if oTestResult.isFailure() else '', + webutils.escapeElem(oTestResult.enmStatus), webutils.escapeElem(sErrCnt), + sChangeReason if cErrorsBelow < oTestResult.cErrors and oTestResult.oReason is None else '', + sResultGraph); + iRow += 1; + + # Failure reason. + if oTestResult.oReason is not None: + sReasonText = '%s / %s' % ( oTestResult.oReason.oFailureReason.oCategory.sShort, + oTestResult.oReason.oFailureReason.sShort, ); + sCommentHtml = ''; + if oTestResult.oReason.sComment and oTestResult.oReason.sComment.strip(): + sCommentHtml = '<br>' + webutils.escapeElem(oTestResult.oReason.sComment.strip()); + sCommentHtml = sCommentHtml.replace('\n', '<br>'); + + sDetailedReason = ' <a href="?%s" class="tmtbl-show-reason">%s</a>' \ + % ( webutils.encodeUrlParams({ self._oDisp.ksParamAction: + self._oDisp.ksActionTestResultFailureDetails, + TestResultFailureData.ksParam_idTestResult: + oTestResult.idTestResult,}), + WuiContentBase.ksShortDetailsLinkHtml,); + + sHtml += ' <tr class="%s tmtbl-events-reason tmtbl-events-lvl%s">\n' \ + ' <td>%s</td>\n' \ + ' <td colspan="2">%s</td>\n' \ + ' <td colspan="3">%s%s%s%s</td>\n' \ + ' <td>%s</td>\n' \ + ' </tr>\n' \ + % ( 'tmodd' if iRow & 1 else 'tmeven', iDepth, + webutils.escapeElem(self.formatTsShort(oTestResult.oReason.tsEffective)), + oTestResult.oReason.oAuthor.sUsername, + webutils.escapeElem(sReasonText), sDetailedReason, sChangeReason, + sCommentHtml, + 'todo'); + iRow += 1; + + if oTestResult.isFailure(): + iFailure += 1; + + return (sHtml, iRow, iFailure); + + + def _generateMainReason(self, oTestResultTree, oTestSet): + """ + Generates the form for displaying and updating the main failure reason. + + oTestResultTree is an instance TestResultDataEx. + oTestSet is an instance of TestSetData. + + """ + _ = oTestSet; + sHtml = ' '; + + if oTestResultTree.isFailure() or oTestResultTree.cErrors > 0: + sHtml += ' <h2>Failure Reason:</h2>\n'; + oData = oTestResultTree.oReason; + + # We need the failure reasons for the combobox. + aoFailureReasons = FailureReasonLogic(self._oDisp.getDb()).fetchForCombo('Test Sheriff, you figure out why!'); + assert aoFailureReasons; + + # For now we'll use the standard form helper. + sFormActionUrl = '%s?%s=%s' % ( self._oDisp.ksScriptName, self._oDisp.ksParamAction, + WuiMain.ksActionTestResultFailureAddPost if oData is None else + WuiMain.ksActionTestResultFailureEditPost ) + fReadOnly = not self._oDisp or self._oDisp.isReadOnlyUser(); + oForm = WuiHlpForm('failure-reason', sFormActionUrl, + sOnSubmit = WuiHlpForm.ksOnSubmit_AddReturnToFieldWithCurrentUrl, fReadOnly = fReadOnly); + oForm.addTextHidden(TestResultFailureData.ksParam_idTestResult, oTestResultTree.idTestResult); + oForm.addTextHidden(TestResultFailureData.ksParam_idTestSet, oTestSet.idTestSet); + if oData is not None: + oForm.addComboBox(TestResultFailureData.ksParam_idFailureReason, oData.idFailureReason, 'Reason', + aoFailureReasons, + sPostHtml = u' ' + WuiFailureReasonDetailsLink(oData.idFailureReason).toHtml() + + (u' ' + WuiFailureReasonAddLink('New', fBracketed = False).toHtml() + if not fReadOnly else u'')); + oForm.addMultilineText(TestResultFailureData.ksParam_sComment, oData.sComment, 'Comment') + + oForm.addNonText(u'%s (%s), %s' + % ( oData.oAuthor.sUsername, oData.oAuthor.sUsername, + self.formatTsShort(oData.tsEffective),), + 'Sheriff', + sPostHtml = ' ' + WuiTestResultFailureDetailsLink(oData.idTestResult, "Show Details").toHtml() ) + + oForm.addTextHidden(TestResultFailureData.ksParam_tsEffective, oData.tsEffective); + oForm.addTextHidden(TestResultFailureData.ksParam_tsExpire, oData.tsExpire); + oForm.addTextHidden(TestResultFailureData.ksParam_uidAuthor, oData.uidAuthor); + oForm.addSubmit('Change Reason'); + else: + oForm.addComboBox(TestResultFailureData.ksParam_idFailureReason, -1, 'Reason', aoFailureReasons, + sPostHtml = ' ' + WuiFailureReasonAddLink('New').toHtml() if not fReadOnly else ''); + oForm.addMultilineText(TestResultFailureData.ksParam_sComment, '', 'Comment'); + oForm.addTextHidden(TestResultFailureData.ksParam_tsEffective, ''); + oForm.addTextHidden(TestResultFailureData.ksParam_tsExpire, ''); + oForm.addTextHidden(TestResultFailureData.ksParam_uidAuthor, ''); + oForm.addSubmit('Add Reason'); + + sHtml += oForm.finalize(); + return sHtml; + + + def showTestCaseResultDetails(self, # pylint: disable=too-many-locals,too-many-statements + oTestResultTree, + oTestSet, + oBuildEx, + oValidationKitEx, + oTestBox, + oTestGroup, + oTestCaseEx, + oTestVarEx): + """Show detailed result""" + def getTcDepsHtmlList(aoTestCaseData): + """Get HTML <ul> list of Test Case name items""" + if aoTestCaseData: + sTmp = '<ul>' + for oTestCaseData in aoTestCaseData: + sTmp += '<li>%s</li>' % (webutils.escapeElem(oTestCaseData.sName),); + sTmp += '</ul>' + else: + sTmp = 'No items' + return sTmp + + def getGrDepsHtmlList(aoGlobalResourceData): + """Get HTML <ul> list of Global Resource name items""" + if aoGlobalResourceData: + sTmp = '<ul>' + for oGlobalResourceData in aoGlobalResourceData: + sTmp += '<li>%s</li>' % (webutils.escapeElem(oGlobalResourceData.sName),); + sTmp += '</ul>' + else: + sTmp = 'No items' + return sTmp + + + asHtml = [] + + from testmanager.webui.wuireport import WuiReportSummaryLink; + tsReportEffectiveDate = None; + if oTestSet.tsDone is not None: + tsReportEffectiveDate = oTestSet.tsDone + datetime.timedelta(days = 4); + if tsReportEffectiveDate >= self.getNowTs(): + tsReportEffectiveDate = None; + + # Test result + test set details. + aoResultRows = [ + WuiHtmlKeeper([ WuiTmLink(oTestCaseEx.sName, self.oWuiAdmin.ksScriptName, + { self.oWuiAdmin.ksParamAction: self.oWuiAdmin.ksActionTestCaseDetails, + TestCaseData.ksParam_idTestCase: oTestCaseEx.idTestCase, + self.oWuiAdmin.ksParamEffectiveDate: oTestSet.tsConfig, }, + fBracketed = False), + WuiTestResultsForTestCaseLink(oTestCaseEx.idTestCase), + WuiReportSummaryLink(ReportModelBase.ksSubTestCase, oTestCaseEx.idTestCase, + tsNow = tsReportEffectiveDate, fBracketed = False), + ]), + ]; + if oTestCaseEx.sDescription: + aoResultRows.append([oTestCaseEx.sDescription,]); + aoResultRows.append([ 'Status:', WuiRawHtml('<span class="tmspan-status-%s">%s</span>' + % (oTestResultTree.enmStatus, oTestResultTree.enmStatus,))]); + if oTestResultTree.cErrors > 0: + aoResultRows.append(( 'Errors:', oTestResultTree.cErrors )); + aoResultRows.append([ 'Elapsed:', oTestResultTree.tsElapsed ]); + cSecCfgTimeout = oTestCaseEx.cSecTimeout if oTestVarEx.cSecTimeout is None else oTestVarEx.cSecTimeout; + cSecEffTimeout = cSecCfgTimeout * oTestBox.pctScaleTimeout / 100; + aoResultRows.append([ 'Timeout:', + '%s (%s sec)' % (utils.formatIntervalSeconds2(cSecEffTimeout), cSecEffTimeout,) ]); + if cSecEffTimeout != cSecCfgTimeout: + aoResultRows.append([ 'Cfg Timeout:', + '%s (%s sec)' % (utils.formatIntervalSeconds(cSecCfgTimeout), cSecCfgTimeout,) ]); + aoResultRows += [ + ( 'Started:', WuiTmLink(self.formatTsShort(oTestSet.tsCreated), WuiMain.ksScriptName, + { WuiMain.ksParamAction: WuiMain.ksActionResultsUnGrouped, + WuiMain.ksParamEffectiveDate: oTestSet.tsCreated, }, + fBracketed = False) ), + ]; + if oTestSet.tsDone is not None: + aoResultRows += [ ( 'Done:', + WuiTmLink(self.formatTsShort(oTestSet.tsDone), WuiMain.ksScriptName, + { WuiMain.ksParamAction: WuiMain.ksActionResultsUnGrouped, + WuiMain.ksParamEffectiveDate: oTestSet.tsDone, }, + fBracketed = False) ) ]; + else: + aoResultRows += [( 'Done:', 'Still running...')]; + aoResultRows += [( 'Config:', oTestSet.tsConfig )]; + if oTestVarEx.cGangMembers > 1: + aoResultRows.append([ 'Member No:', '#%s (of %s)' % (oTestSet.iGangMemberNo, oTestVarEx.cGangMembers) ]); + + aoResultRows += [ + ( 'Test Group:', + WuiHtmlKeeper([ WuiTmLink(oTestGroup.sName, self.oWuiAdmin.ksScriptName, + { self.oWuiAdmin.ksParamAction: self.oWuiAdmin.ksActionTestGroupDetails, + TestGroupData.ksParam_idTestGroup: oTestGroup.idTestGroup, + self.oWuiAdmin.ksParamEffectiveDate: oTestSet.tsConfig, }, + fBracketed = False), + WuiReportSummaryLink(ReportModelBase.ksSubTestGroup, oTestGroup.idTestGroup, + tsNow = tsReportEffectiveDate, fBracketed = False), + ]), ), + ]; + if oTestVarEx.sTestBoxReqExpr is not None: + aoResultRows.append([ 'TestBox reqs:', oTestVarEx.sTestBoxReqExpr ]); + elif oTestCaseEx.sTestBoxReqExpr is not None or oTestVarEx.sTestBoxReqExpr is not None: + aoResultRows.append([ 'TestBox reqs:', oTestCaseEx.sTestBoxReqExpr ]); + if oTestVarEx.sBuildReqExpr is not None: + aoResultRows.append([ 'Build reqs:', oTestVarEx.sBuildReqExpr ]); + elif oTestCaseEx.sBuildReqExpr is not None or oTestVarEx.sBuildReqExpr is not None: + aoResultRows.append([ 'Build reqs:', oTestCaseEx.sBuildReqExpr ]); + if oTestCaseEx.sValidationKitZips is not None and oTestCaseEx.sValidationKitZips != '@VALIDATIONKIT_ZIP@': + aoResultRows.append([ 'Validation Kit:', oTestCaseEx.sValidationKitZips ]); + if oTestCaseEx.aoDepTestCases: + aoResultRows.append([ 'Prereq. Test Cases:', oTestCaseEx.aoDepTestCases, getTcDepsHtmlList ]); + if oTestCaseEx.aoDepGlobalResources: + aoResultRows.append([ 'Global Resources:', oTestCaseEx.aoDepGlobalResources, getGrDepsHtmlList ]); + + # Builds. + aoBuildRows = []; + if oBuildEx is not None: + aoBuildRows += [ + WuiHtmlKeeper([ WuiTmLink('Build', self.oWuiAdmin.ksScriptName, + { self.oWuiAdmin.ksParamAction: self.oWuiAdmin.ksActionBuildDetails, + BuildData.ksParam_idBuild: oBuildEx.idBuild, + self.oWuiAdmin.ksParamEffectiveDate: oTestSet.tsCreated, }, + fBracketed = False), + WuiTestResultsForBuildRevLink(oBuildEx.iRevision), + WuiReportSummaryLink(ReportModelBase.ksSubBuild, oBuildEx.idBuild, + tsNow = tsReportEffectiveDate, fBracketed = False), ]), + ]; + self._anchorAndAppendBinaries(oBuildEx.sBinaries, aoBuildRows); + aoBuildRows += [ + ( 'Revision:', WuiSvnLinkWithTooltip(oBuildEx.iRevision, oBuildEx.oCat.sRepository, + fBracketed = False) ), + ( 'Product:', oBuildEx.oCat.sProduct ), + ( 'Branch:', oBuildEx.oCat.sBranch ), + ( 'Type:', oBuildEx.oCat.sType ), + ( 'Version:', oBuildEx.sVersion ), + ( 'Created:', oBuildEx.tsCreated ), + ]; + if oBuildEx.uidAuthor is not None: + aoBuildRows += [ ( 'Author ID:', oBuildEx.uidAuthor ), ]; + if oBuildEx.sLogUrl is not None: + aoBuildRows += [ ( 'Log:', WuiBuildLogLink(oBuildEx.sLogUrl, fBracketed = False) ), ]; + + aoValidationKitRows = []; + if oValidationKitEx is not None: + aoValidationKitRows += [ + WuiTmLink('Validation Kit', self.oWuiAdmin.ksScriptName, + { self.oWuiAdmin.ksParamAction: self.oWuiAdmin.ksActionBuildDetails, + BuildData.ksParam_idBuild: oValidationKitEx.idBuild, + self.oWuiAdmin.ksParamEffectiveDate: oTestSet.tsCreated, }, + fBracketed = False), + ]; + self._anchorAndAppendBinaries(oValidationKitEx.sBinaries, aoValidationKitRows); + aoValidationKitRows += [ ( 'Revision:', WuiSvnLink(oValidationKitEx.iRevision, fBracketed = False) ) ]; + if oValidationKitEx.oCat.sProduct != 'VBox TestSuite': + aoValidationKitRows += [ ( 'Product:', oValidationKitEx.oCat.sProduct ), ]; + if oValidationKitEx.oCat.sBranch != 'trunk': + aoValidationKitRows += [ ( 'Product:', oValidationKitEx.oCat.sBranch ), ]; + if oValidationKitEx.oCat.sType != 'release': + aoValidationKitRows += [ ( 'Type:', oValidationKitEx.oCat.sType), ]; + if oValidationKitEx.sVersion != '0.0.0': + aoValidationKitRows += [ ( 'Version:', oValidationKitEx.sVersion ), ]; + aoValidationKitRows += [ + ( 'Created:', oValidationKitEx.tsCreated ), + ]; + if oValidationKitEx.uidAuthor is not None: + aoValidationKitRows += [ ( 'Author ID:', oValidationKitEx.uidAuthor ), ]; + if oValidationKitEx.sLogUrl is not None: + aoValidationKitRows += [ ( 'Log:', WuiBuildLogLink(oValidationKitEx.sLogUrl, fBracketed = False) ), ]; + + # TestBox. + aoTestBoxRows = [ + WuiHtmlKeeper([ WuiTmLink(oTestBox.sName, self.oWuiAdmin.ksScriptName, + { self.oWuiAdmin.ksParamAction: self.oWuiAdmin.ksActionTestBoxDetails, + TestBoxData.ksParam_idGenTestBox: oTestSet.idGenTestBox, }, + fBracketed = False), + WuiTestResultsForTestBoxLink(oTestBox.idTestBox), + WuiReportSummaryLink(ReportModelBase.ksSubTestBox, oTestSet.idTestBox, + tsNow = tsReportEffectiveDate, fBracketed = False), + ]), + ]; + if oTestBox.sDescription: + aoTestBoxRows.append([oTestBox.sDescription, ]); + aoTestBoxRows += [ + ( 'IP:', oTestBox.ip ), + #( 'UUID:', oTestBox.uuidSystem ), + #( 'Enabled:', oTestBox.fEnabled ), + #( 'Lom Kind:', oTestBox.enmLomKind ), + #( 'Lom IP:', oTestBox.ipLom ), + ( 'OS/Arch:', '%s.%s' % (oTestBox.sOs, oTestBox.sCpuArch) ), + ( 'OS Version:', oTestBox.sOsVersion ), + ( 'CPUs:', oTestBox.cCpus ), + ]; + if oTestBox.sCpuName is not None: + aoTestBoxRows.append(['CPU Name', oTestBox.sCpuName.replace(' ', ' ')]); + if oTestBox.lCpuRevision is not None: + sMarch = oTestBox.queryCpuMicroarch(); + if sMarch is not None: + aoTestBoxRows.append( ('CPU Microarch', sMarch) ); + uFamily = oTestBox.getCpuFamily(); + uModel = oTestBox.getCpuModel(); + uStepping = oTestBox.getCpuStepping(); + aoTestBoxRows += [ + ( 'CPU Family', '%u (%#x)' % ( uFamily, uFamily, ) ), + ( 'CPU Model', '%u (%#x)' % ( uModel, uModel, ) ), + ( 'CPU Stepping', '%u (%#x)' % ( uStepping, uStepping, ) ), + ]; + asFeatures = [ oTestBox.sCpuVendor, ]; + if oTestBox.fCpuHwVirt is True: asFeatures.append(u'HW\u2011Virt'); + if oTestBox.fCpuNestedPaging is True: asFeatures.append(u'Nested\u2011Paging'); + if oTestBox.fCpu64BitGuest is True: asFeatures.append(u'64\u2011bit\u2011Guest'); + if oTestBox.fChipsetIoMmu is True: asFeatures.append(u'I/O\u2011MMU'); + aoTestBoxRows += [ + ( 'Features:', u' '.join(asFeatures) ), + ( 'RAM size:', '%s MB' % (oTestBox.cMbMemory,) ), + ( 'Scratch Size:', '%s MB' % (oTestBox.cMbScratch,) ), + ( 'Scale Timeout:', '%s%%' % (oTestBox.pctScaleTimeout,) ), + ( 'Script Rev:', WuiSvnLink(oTestBox.iTestBoxScriptRev, fBracketed = False) ), + ( 'Python:', oTestBox.formatPythonVersion() ), + ( 'Pending Command:', oTestBox.enmPendingCmd ), + ]; + + aoRows = [ + aoResultRows, + aoBuildRows, + aoValidationKitRows, + aoTestBoxRows, + ]; + + asHtml.append(self._htmlTable(aoRows)); + + # + # Convert the tree to a list of events, values, message and files. + # + sHtmlEvents = ''; + sHtmlEvents += '<table class="tmtbl-events" id="tmtbl-events" width="100%">\n'; + sHtmlEvents += ' <tr class="tmheader">\n' \ + ' <th>When</th>\n' \ + ' <th></th>\n' \ + ' <th>Elapsed</th>\n' \ + ' <th>Event name</th>\n' \ + ' <th colspan="2">Value (status)</th>' \ + ' <th></th>\n' \ + ' </tr>\n'; + sPrettyCmdLine = ' \\<br> \n'.join(webutils.escapeElem(oTestCaseEx.sBaseCmd + + ' ' + + oTestVarEx.sArgs).split() ); + (sTmp, _, cFailures) = self._recursivelyGenerateEvents(oTestResultTree, sPrettyCmdLine, '', 1, 0, oTestSet, 0); + sHtmlEvents += sTmp; + + sHtmlEvents += '</table>\n' + + # + # Put it all together. + # + sHtml = '<table class="tmtbl-testresult-details-base" width="100%">\n'; + sHtml += ' <tr>\n' + sHtml += ' <td valign="top" width="20%%">\n%s\n</td>\n' % ' <br>\n'.join(asHtml); + + sHtml += ' <td valign="top" width="80%" style="padding-left:6px">\n'; + sHtml += self._generateMainReason(oTestResultTree, oTestSet); + + sHtml += ' <h2>Events:</h2>\n'; + sHtml += ' <form action="#" method="get" id="graph-form">\n' \ + ' <input type="hidden" name="%s" value="%s"/>\n' \ + ' <input type="hidden" name="%s" value="%u"/>\n' \ + ' <input type="hidden" name="%s" value="%u"/>\n' \ + ' <input type="hidden" name="%s" value="%u"/>\n' \ + ' <input type="hidden" name="%s" value="%u"/>\n' \ + % ( WuiMain.ksParamAction, WuiMain.ksActionGraphWiz, + WuiMain.ksParamGraphWizTestBoxIds, oTestBox.idTestBox, + WuiMain.ksParamGraphWizBuildCatIds, oBuildEx.idBuildCategory, + WuiMain.ksParamGraphWizTestCaseIds, oTestSet.idTestCase, + WuiMain.ksParamGraphWizSrcTestSetId, oTestSet.idTestSet, + ); + if oTestSet.tsDone is not None: + sHtml += ' <input type="hidden" name="%s" value="%s"/>\n' \ + % ( WuiMain.ksParamEffectiveDate, oTestSet.tsDone, ); + sHtml += ' <p>\n'; + sFormButton = '<button type="submit" onclick="%s">Show graphs</button>' \ + % ( webutils.escapeAttr('addDynamicGraphInputs("graph-form", "main", "%s", "%s");' + % (WuiMain.ksParamGraphWizWidth, WuiMain.ksParamGraphWizDpi, )) ); + sHtml += ' ' + sFormButton + '\n'; + sHtml += ' %s %s %s\n' \ + % ( WuiTmLink('Log File', '', + { WuiMain.ksParamAction: WuiMain.ksActionViewLog, + WuiMain.ksParamLogSetId: oTestSet.idTestSet, + }), + WuiTmLink('Raw Log', '', + { WuiMain.ksParamAction: WuiMain.ksActionGetFile, + WuiMain.ksParamGetFileSetId: oTestSet.idTestSet, + WuiMain.ksParamGetFileDownloadIt: False, + }), + WuiTmLink('Download Log', '', + { WuiMain.ksParamAction: WuiMain.ksActionGetFile, + WuiMain.ksParamGetFileSetId: oTestSet.idTestSet, + WuiMain.ksParamGetFileDownloadIt: True, + }), + ); + sHtml += ' </p>\n'; + if cFailures == 1: + sHtml += ' <p>%s</p>\n' % ( WuiTmLink('Jump to failure', '#failure-0'), ) + elif cFailures > 1: + sHtml += ' <p>Jump to failure: '; + if cFailures <= 13: + for iFailure in range(0, cFailures): + sHtml += ' ' + WuiTmLink('#%u' % (iFailure,), '#failure-%u' % (iFailure,)).toHtml(); + else: + for iFailure in range(0, 6): + sHtml += ' ' + WuiTmLink('#%u' % (iFailure,), '#failure-%u' % (iFailure,)).toHtml(); + sHtml += ' ... '; + for iFailure in range(cFailures - 6, cFailures): + sHtml += ' ' + WuiTmLink('#%u' % (iFailure,), '#failure-%u' % (iFailure,)).toHtml(); + sHtml += ' </p>\n'; + + sHtml += sHtmlEvents; + sHtml += ' <p>' + sFormButton + '</p>\n'; + sHtml += ' </form>\n'; + sHtml += ' </td>\n'; + + sHtml += ' </tr>\n'; + sHtml += '</table>\n'; + + return ('Test Case result details', sHtml) + + +class WuiGroupedResultList(WuiListContentBase): + """ + WUI results content generator. + """ + + def __init__(self, aoEntries, cEntriesCount, iPage, cItemsPerPage, tsEffective, fnDPrint, oDisp, + aiSelectedSortColumns = None): + """Override initialization""" + WuiListContentBase.__init__(self, aoEntries, iPage, cItemsPerPage, tsEffective, + sTitle = 'Ungrouped (%d)' % cEntriesCount, sId = 'results', + fnDPrint = fnDPrint, oDisp = oDisp, aiSelectedSortColumns = aiSelectedSortColumns); + + self._cEntriesCount = cEntriesCount + + self._asColumnHeaders = [ + 'Start', + 'Product Build', + 'Kit', + 'Box', + 'OS.Arch', + 'Test Case', + 'Elapsed', + 'Result', + 'Reason', + ]; + self._asColumnAttribs = ['align="center"', 'align="center"', 'align="center"', + 'align="center"', 'align="center"', 'align="center"', + 'align="center"', 'align="center"', 'align="center"', + 'align="center"', 'align="center"', 'align="center"', + 'align="center"', ]; + + + # Prepare parameter lists. + self._dTestBoxLinkParams = self._oDisp.getParameters(); + self._dTestBoxLinkParams[WuiMain.ksParamAction] = WuiMain.ksActionResultsGroupedByTestBox; + + self._dTestCaseLinkParams = self._oDisp.getParameters(); + self._dTestCaseLinkParams[WuiMain.ksParamAction] = WuiMain.ksActionResultsGroupedByTestCase; + + self._dRevLinkParams = self._oDisp.getParameters(); + self._dRevLinkParams[WuiMain.ksParamAction] = WuiMain.ksActionResultsGroupedByBuildRev; + + + + def _formatListEntry(self, iEntry): + """ + Format *show all* table entry + """ + oEntry = self._aoEntries[iEntry]; + + from testmanager.webui.wuiadmin import WuiAdmin; + from testmanager.webui.wuireport import WuiReportSummaryLink; + + oValidationKit = None; + if oEntry.idBuildTestSuite is not None: + oValidationKit = WuiTmLink('r%s' % (oEntry.iRevisionTestSuite,), + WuiAdmin.ksScriptName, + { WuiAdmin.ksParamAction: WuiAdmin.ksActionBuildDetails, + BuildData.ksParam_idBuild: oEntry.idBuildTestSuite }, + fBracketed = False); + + aoTestSetLinks = []; + aoTestSetLinks.append(WuiTmLink(oEntry.enmStatus, + WuiMain.ksScriptName, + { WuiMain.ksParamAction: WuiMain.ksActionTestResultDetails, + TestSetData.ksParam_idTestSet: oEntry.idTestSet }, + fBracketed = False)); + if oEntry.cErrors > 0: + aoTestSetLinks.append(WuiRawHtml('-')); + aoTestSetLinks.append(WuiTmLink('%d error%s' % (oEntry.cErrors, '' if oEntry.cErrors == 1 else 's', ), + WuiMain.ksScriptName, + { WuiMain.ksParamAction: WuiMain.ksActionTestResultDetails, + TestSetData.ksParam_idTestSet: oEntry.idTestSet }, + sFragmentId = 'failure-0', fBracketed = False)); + + + self._dTestBoxLinkParams[WuiMain.ksParamGroupMemberId] = oEntry.idTestBox; + self._dTestCaseLinkParams[WuiMain.ksParamGroupMemberId] = oEntry.idTestCase; + self._dRevLinkParams[WuiMain.ksParamGroupMemberId] = oEntry.iRevision; + + sTestBoxTitle = u''; + if oEntry.sCpuVendor is not None: + sTestBoxTitle += 'CPU vendor:\t%s\n' % ( oEntry.sCpuVendor, ); + if oEntry.sCpuName is not None: + sTestBoxTitle += 'CPU name:\t%s\n' % ( ' '.join(oEntry.sCpuName.split()), ); + if oEntry.sOsVersion is not None: + sTestBoxTitle += 'OS version:\t%s\n' % ( oEntry.sOsVersion, ); + asFeatures = []; + if oEntry.fCpuHwVirt is True: + if oEntry.sCpuVendor is None: + asFeatures.append(u'HW\u2011Virt'); + elif oEntry.sCpuVendor in ['AuthenticAMD',]: + asFeatures.append(u'HW\u2011Virt(AMD\u2011V)'); + else: + asFeatures.append(u'HW\u2011Virt(VT\u2011x)'); + if oEntry.fCpuNestedPaging is True: asFeatures.append(u'Nested\u2011Paging'); + if oEntry.fCpu64BitGuest is True: asFeatures.append(u'64\u2011bit\u2011Guest'); + #if oEntry.fChipsetIoMmu is True: asFeatures.append(u'I/O\u2011MMU'); + sTestBoxTitle += u'CPU features:\t' + u', '.join(asFeatures); + + # Testcase + if oEntry.sSubName: + sTestCaseName = '%s / %s' % (oEntry.sTestCaseName, oEntry.sSubName,); + else: + sTestCaseName = oEntry.sTestCaseName; + + # Reason: + aoReasons = []; + for oIt in oEntry.aoFailureReasons: + sReasonTitle = 'Reason: \t%s\n' % ( oIt.oFailureReason.sShort, ); + sReasonTitle += 'Category:\t%s\n' % ( oIt.oFailureReason.oCategory.sShort, ); + sReasonTitle += 'Assigned:\t%s\n' % ( self.formatTsShort(oIt.tsFailureReasonAssigned), ); + sReasonTitle += 'By User: \t%s\n' % ( oIt.oFailureReasonAssigner.sUsername, ); + if oIt.sFailureReasonComment: + sReasonTitle += 'Comment: \t%s\n' % ( oIt.sFailureReasonComment, ); + if oIt.oFailureReason.iTicket is not None and oIt.oFailureReason.iTicket > 0: + sReasonTitle += 'xTracker:\t#%s\n' % ( oIt.oFailureReason.iTicket, ); + for i, sUrl in enumerate(oIt.oFailureReason.asUrls): + sUrl = sUrl.strip(); + if sUrl: + sReasonTitle += 'URL#%u: \t%s\n' % ( i, sUrl, ); + aoReasons.append(WuiTmLink(oIt.oFailureReason.sShort, WuiAdmin.ksScriptName, + { WuiAdmin.ksParamAction: WuiAdmin.ksActionFailureReasonDetails, + FailureReasonData.ksParam_idFailureReason: oIt.oFailureReason.idFailureReason }, + sTitle = sReasonTitle)); + + return [ + oEntry.tsCreated, + [ WuiTmLink('%s %s (%s)' % (oEntry.sProduct, oEntry.sVersion, oEntry.sType,), + WuiMain.ksScriptName, self._dRevLinkParams, sTitle = '%s' % (oEntry.sBranch,), fBracketed = False), + WuiSvnLinkWithTooltip(oEntry.iRevision, 'vbox'), ## @todo add sRepository TestResultListingData + WuiTmLink(self.ksShortDetailsLink, WuiAdmin.ksScriptName, + { WuiAdmin.ksParamAction: WuiAdmin.ksActionBuildDetails, + BuildData.ksParam_idBuild: oEntry.idBuild }, + fBracketed = False), + ], + oValidationKit, + [ WuiTmLink(oEntry.sTestBoxName, WuiMain.ksScriptName, self._dTestBoxLinkParams, fBracketed = False, + sTitle = sTestBoxTitle), + WuiTmLink(self.ksShortDetailsLink, WuiAdmin.ksScriptName, + { WuiAdmin.ksParamAction: WuiAdmin.ksActionTestBoxDetails, + TestBoxData.ksParam_idTestBox: oEntry.idTestBox }, + fBracketed = False), + WuiReportSummaryLink(ReportModelBase.ksSubTestBox, oEntry.idTestBox, fBracketed = False), ], + '%s.%s' % (oEntry.sOs, oEntry.sArch), + [ WuiTmLink(sTestCaseName, WuiMain.ksScriptName, self._dTestCaseLinkParams, fBracketed = False, + sTitle = (oEntry.sBaseCmd + ' ' + oEntry.sArgs) if oEntry.sArgs else oEntry.sBaseCmd), + WuiTmLink(self.ksShortDetailsLink, WuiAdmin.ksScriptName, + { WuiAdmin.ksParamAction: WuiAdmin.ksActionTestCaseDetails, + TestCaseData.ksParam_idTestCase: oEntry.idTestCase }, + fBracketed = False), + WuiReportSummaryLink(ReportModelBase.ksSubTestCase, oEntry.idTestCase, fBracketed = False), ], + oEntry.tsElapsed, + aoTestSetLinks, + aoReasons + ]; diff --git a/src/VBox/ValidationKit/testmanager/webui/wuitestresultfailure.py b/src/VBox/ValidationKit/testmanager/webui/wuitestresultfailure.py new file mode 100755 index 00000000..9ffd5518 --- /dev/null +++ b/src/VBox/ValidationKit/testmanager/webui/wuitestresultfailure.py @@ -0,0 +1,110 @@ +# -*- coding: utf-8 -*- +# $Id: wuitestresultfailure.py $ + +""" +Test Manager WUI - Dummy Test Result Failure Reason Edit Dialog - just for error handling! +""" + +__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 $" + +# Validation Kit imports. +from testmanager.webui.wuicontentbase import WuiFormContentBase, WuiContentBase, WuiTmLink; +from testmanager.webui.wuimain import WuiMain; +from testmanager.webui.wuiadminfailurereason import WuiFailureReasonDetailsLink, WuiFailureReasonAddLink; +from testmanager.core.testresultfailures import TestResultFailureData; +from testmanager.core.testset import TestSetData; +from testmanager.core.failurereason import FailureReasonLogic; + + + +class WuiTestResultFailureDetailsLink(WuiTmLink): + """ Link for adding a failure reason. """ + def __init__(self, idTestResult, sName = WuiContentBase.ksShortDetailsLink, sTitle = None, fBracketed = None): + if fBracketed is None: + fBracketed = len(sName) > 2; + WuiTmLink.__init__(self, sName = sName, + sUrlBase = WuiMain.ksScriptName, + dParams = { WuiMain.ksParamAction: WuiMain.ksActionTestResultFailureDetails, + TestResultFailureData.ksParam_idTestResult: idTestResult, }, + fBracketed = fBracketed, + sTitle = sTitle); + self.idTestResult = idTestResult; + + + +class WuiTestResultFailure(WuiFormContentBase): + """ + WUI test result failure error form generator. + """ + def __init__(self, oData, sMode, oDisp): + if sMode == WuiFormContentBase.ksMode_Add: + sTitle = 'Add Test Result Failure Reason'; + elif sMode == WuiFormContentBase.ksMode_Edit: + sTitle = 'Modify Test Result Failure Reason'; + else: + assert sMode == WuiFormContentBase.ksMode_Show; + sTitle = 'Test Result Failure Reason'; + ## submit access. + WuiFormContentBase.__init__(self, oData, sMode, 'TestResultFailure', oDisp, sTitle); + + def _populateForm(self, oForm, oData): + + aoFailureReasons = FailureReasonLogic(self._oDisp.getDb()).fetchForCombo('Todo: Figure out why'); + sPostHtml = ''; + if oData.idFailureReason is not None and oData.idFailureReason >= 0: + sPostHtml += u' ' + WuiFailureReasonDetailsLink(oData.idFailureReason).toHtml(); + sPostHtml += u' ' + WuiFailureReasonAddLink('New', fBracketed = False).toHtml(); + oForm.addComboBox(TestResultFailureData.ksParam_idFailureReason, oData.idFailureReason, + 'Reason', aoFailureReasons, sPostHtml = sPostHtml); + oForm.addMultilineText(TestResultFailureData.ksParam_sComment, oData.sComment, 'Comment'); + oForm.addIntRO( TestResultFailureData.ksParam_idTestResult, oData.idTestResult, 'Test Result ID'); + oForm.addIntRO( TestResultFailureData.ksParam_idTestSet, oData.idTestSet, 'Test Set ID'); + oForm.addTimestampRO(TestResultFailureData.ksParam_tsEffective, oData.tsEffective, 'Effective Date'); + oForm.addTimestampRO(TestResultFailureData.ksParam_tsExpire, oData.tsExpire, 'Expire (excl)'); + oForm.addIntRO( TestResultFailureData.ksParam_uidAuthor, oData.uidAuthor, 'Changed by UID'); + if self._sMode != WuiFormContentBase.ksMode_Show: + oForm.addSubmit('Add' if self._sMode == WuiFormContentBase.ksMode_Add else 'Modify'); + return True; + + def _generateTopRowFormActions(self, oData): + """ + We add a way to get back to the test set to the actions. + """ + aoActions = super(WuiTestResultFailure, self)._generateTopRowFormActions(oData); + if oData and oData.idTestResult and int(oData.idTestResult) > 0: + aoActions.append(WuiTmLink('Associated Test Set', WuiMain.ksScriptName, + { WuiMain.ksParamAction: WuiMain.ksActionTestSetDetailsFromResult, + TestSetData.ksParam_idTestResult: oData.idTestResult } + )); + return aoActions; diff --git a/src/VBox/ValidationKit/testmanager/webui/wuivcshistory.py b/src/VBox/ValidationKit/testmanager/webui/wuivcshistory.py new file mode 100755 index 00000000..1df9bbb2 --- /dev/null +++ b/src/VBox/ValidationKit/testmanager/webui/wuivcshistory.py @@ -0,0 +1,101 @@ +# -*- coding: utf-8 -*- +# $Id: wuivcshistory.py $ + +""" +Test Manager WUI - VCS history +""" + +__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 $" + +# Python imports. +#import datetime; + +# Validation Kit imports. +from testmanager import config; +from testmanager.core import db; +from testmanager.webui.wuicontentbase import WuiContentBase; +from common import webutils; + +class WuiVcsHistoryTooltip(WuiContentBase): + """ + WUI VCS history tooltip generator. + """ + + def __init__(self, aoEntries, sRepository, iRevision, cEntries, fnDPrint, oDisp): + """Override initialization""" + WuiContentBase.__init__(self, fnDPrint = fnDPrint, oDisp = oDisp); + self.aoEntries = aoEntries; + self.sRepository = sRepository; + self.iRevision = iRevision; + self.cEntries = cEntries; + + + def show(self): + """ + Generates the tooltip. + Returns (sTitle, HTML). + """ + sHtml = '<div class="tmvcstimeline tmvcstimelinetooltip">\n'; + + oCurDate = None; + for oEntry in self.aoEntries: + oTsZulu = db.dbTimestampToZuluDatetime(oEntry.tsCreated); + if oCurDate is None or oCurDate != oTsZulu.date(): + if oCurDate is not None: + sHtml += ' </dl>\n' + oCurDate = oTsZulu.date(); + sHtml += ' <h2>%s:</h2>\n' \ + ' <dl>\n' \ + % (oTsZulu.strftime('%Y-%m-%d'),); + + sEntry = ' <dt id="r%s">' % (oEntry.iRevision, ); + sEntry += '<a href="%s" target="_blank">' \ + % ( webutils.escapeAttr(config.g_ksTracChangsetUrlFmt + % { 'iRevision': oEntry.iRevision, 'sRepository': oEntry.sRepository,}), ); + + sEntry += '<span class="tmvcstimeline-time">%s</span>' % ( oTsZulu.strftime('%H:%MZ'), ); + sEntry += ' Changeset <span class="tmvcstimeline-rev">[%s]</span>' % ( oEntry.iRevision, ); + sEntry += ' by <span class="tmvcstimeline-author">%s</span>' % ( webutils.escapeElem(oEntry.sAuthor), ); + sEntry += '</a>\n'; + sEntry += '</dt>\n'; + sEntry += ' <dd>%s</dd>\n' % ( webutils.escapeElem(oEntry.sMessage), ); + + sHtml += sEntry; + + if oCurDate is not None: + sHtml += ' </dl>\n'; + sHtml += '</div>\n'; + + return ('VCS History Tooltip', sHtml); + |