diff --git a/product/ERP5/Tool/TaskDistributionTool.py b/product/ERP5/Tool/TaskDistributionTool.py new file mode 100644 index 0000000000000000000000000000000000000000..86bc6e6a8c92bda4e503d1bc70423bc3686ca512 --- /dev/null +++ b/product/ERP5/Tool/TaskDistributionTool.py @@ -0,0 +1,258 @@ +############################################################################## +# +# Copyright (c) 2009 Nexedi SA and Contributors. All Rights Reserved. +# Julien Muchembled <jm@nexedi.com> +# +# WARNING: This program as such is intended to be used by professional +# programmers who take the whole responsibility of assessing all potential +# consequences resulting from its eventual inadequacies and bugs +# End users who are looking for a ready-to-use solution with commercial +# guarantees and support are strongly adviced to contract a Free Software +# Service Company +# +# 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; either version 2 +# of the License, or (at your option) any later version. +# +# 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, write to the Free Software +# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. +# +############################################################################## + +import random +from AccessControl import ClassSecurityInfo +from Products.ERP5Type import Permissions, PropertySheet, Constraint, interfaces +from Products.ERP5Type.Tool.BaseTool import BaseTool +from zLOG import LOG +from xmlrpclib import Binary + +class TaskDistributionTool(BaseTool): + """ + A Task distribution tool (used for ERP5 unit test runs). + """ + + id = 'portal_task_distribution' + meta_type = 'ERP5 Task Distribution Tool' + portal_type = 'Task Distribution Tool' + allowed_types = () + + security = ClassSecurityInfo() + security.declareObjectProtected(Permissions.AccessContentsInformation) + + def __init__(self, *args, **kw): + BaseTool.__init__(self, *args, **kw) + # XXX Cache information about running test results, because the protocol + # is synchronous and we can't rely on the catalog. + # This is a hack until a better (and asynchronous) protocol is + # implemented. + self.test_result_dict = {} + + security.declarePublic('getProtocolRevision') + def getProtocolRevision(self): + """ + """ + return 1 + + def _getTestResultNode(self, test_result, node_title): + node_list = [x for x in test_result.objectValues( + portal_type='Test Result Node') if x.getTitle() == node_title] + node_list_len = len(node_list) + assert node_list_len in (0, 1) + node = None + if len(node_list): + node = node_list[0] + return node + + security.declarePublic('createTestResult') + def createTestResult(self, name, revision, test_name_list, allow_restart, + test_title=None, node_title=None, project_title=None): + """(temporary) + - name (string) + - revision (string representation of an integer) + - test_name_list (list of strings) + - allow_restart (boolean) + + XXX 'revision' should be a string representing the full revision + of the tested code, because some projects are tested with different + revisions of ERP5. + + -> (test_result_path, revision) or None if already completed + """ + LOG('createTestResult', 0, (name, revision, test_title, project_title)) + self._p_changed = 1 + portal = self.getPortalObject() + if test_title is None: + test_title = name + test_result_path, line_dict = self.test_result_dict.get( + test_title, ('', {})) + duration_dict = {} + def createNode(test_result, node_title): + if node_title is not None: + node = self._getTestResultNode(test_result, node_title) + if node is None: + node = test_result.newContent(portal_type='Test Result Node', + title=node_title) + node.start() + def createTestResultLineList(test_result, test_name_list, line_dict): + previous_test_result_list = portal.test_result_module.searchFolder( + title='=%s' % test_result.getTitle(), + sort_on=[('creation_date','descending')], + simulation_state='stopped', + limit=1) + if len(previous_test_result_list): + previous_test_result = previous_test_result_list[0].getObject() + for line in previous_test_result.objectValues(): + if line.getSimulationState() == 'stopped': + duration_dict[line.getTitle()] = line.getProperty('duration') + for test_name in test_name_list: + line = test_result.newContent(portal_type='Test Result Line', + title=test_name) + line_dict[line.getId()] = duration_dict.get(test_name) + reference_list_string = None + if type(revision) is str and '=' in revision: + reference_list_string = revision + int_index, reference = None, revision + elif type(revision) is str: + # backward compatibility + int_index, reference = revision, None + else: + # backward compatibility + int_index, reference = revision + test_result = None + if test_result_path: + test_result = portal.unrestrictedTraverse(test_result_path, None) + if test_result is None or test_result.getSimulationState() in \ + ('cancelled', 'failed'): + del self.test_result_dict[test_title] + line_dict = {} + else: + last_state = test_result.getSimulationState() + last_revision = str(test_result.getIntIndex()) + if last_state == 'started': + createNode(test_result, node_title) + reference = test_result.getReference() + if reference_list_string: + last_revision = reference + elif reference: + last_revision = last_revision, reference + if len(line_dict) == 0 and len(test_name_list): + createTestResultLineList(test_result, test_name_list, line_dict) + return test_result_path, last_revision + if last_state == 'stopped': + if reference_list_string is not None: + if reference_list_string == test_result.getReference(): + return + elif last_revision == int_index and not allow_restart: + return + test_result = portal.test_result_module.newContent( + portal_type='Test Result', + title=test_title, + reference=reference, + predecessor=test_result_path) + if int_index is not None: + test_result.setIntIndex(int_index) + if project_title is not None: + project_list = portal.portal_catalog(portal_type='Project', + title='="%s"' % project_title) + if len(project_list) == 1: + test_result.setSourceProjectValue(project_list[0].getObject()) + else: + raise ValueError('found this list of project : %r for title %r' % \ + ([x.path for x in project_list], project_title)) + else: + # Backward compatibility + project = portal.ERP5Site_getProjectFromTestSuite(name) + test_result.setSourceProjectValue(project) + test_result.updateLocalRolesOnSecurityGroups() # XXX + test_result_path = test_result.getRelativeUrl() + self.test_result_dict[test_title] = test_result_path, line_dict + test_result.start() + createTestResultLineList(test_result, test_name_list, line_dict) + createNode(test_result, node_title) + return test_result_path, revision + + security.declarePublic('startUnitTest') + def startUnitTest(self, test_result_path, exclude_list=()): + """(temporary) + - test_result_path (string) + - exclude_list (list of strings) + + -> test_path (string), test_name (string) + or None if finished + """ + portal = self.getPortalObject() + test_result = portal.restrictedTraverse(test_result_path) + if test_result.getSimulationState() != 'started': + return + path, line_dict = self.test_result_dict[test_result.getTitle()] + assert path == test_result_path + started_list = [] + for line_id, duration in sorted(line_dict.iteritems(), + key=lambda x: x[1], reverse=1): + line = test_result[line_id] + test = line.getTitle() + if test not in exclude_list: + state = line.getSimulationState() + test = line.getRelativeUrl(), test + if state == 'draft': + line.start() + return test + # XXX Make sure we finish all tests. + if state == 'started': + started_list.append(test) + if started_list: + return random.choice(started_list) + + security.declarePublic('stopUnitTest') + def stopUnitTest(self, test_path, status_dict): + """(temporary) + - test_path (string) + - status_dict (dict) + """ + status_dict = self._extractXMLRPCDict(status_dict) + LOG("TaskDistributionTool.stopUnitTest", 0, repr((test_path,status_dict))) + portal = self.getPortalObject() + line = portal.restrictedTraverse(test_path) + test_result = line.getParentValue() + path, line_dict = self.test_result_dict[test_result.getTitle()] + if test_result.getSimulationState() == 'started': + assert path == test_result.getRelativeUrl() + line_id = line.getId() + if line_id in line_dict: + line.stop(**status_dict) + del line_dict[line_id] + self._p_changed = 1 + if not line_dict: + test_result.stop() + + def _extractXMLRPCDict(self, xmlrpc_dict): + """ + extract all xmlrpclib.Binary instance + """ + return dict([(x,isinstance(y, Binary) and y.data or y) \ + for (x, y) in xmlrpc_dict.iteritems()]) + + security.declarePublic('reportTaskFailure') + def reportTaskFailure(self, test_result_path, status_dict, node_title): + """report failure when a node can not handle task + """ + status_dict = self._extractXMLRPCDict(status_dict) + LOG("TaskDistributionTool.reportTaskFailure", 0, repr((test_result_path, + status_dict))) + portal = self.getPortalObject() + test_result = portal.restrictedTraverse(test_result_path) + node = self._getTestResultNode(test_result, node_title) + assert node is not None + node.fail(**status_dict) + for node in test_result.objectValues(portal_type='Test Result Node'): + if node.getSimulationState() != 'failed': + break + else: + test_result.fail() diff --git a/product/ERP5/__init__.py b/product/ERP5/__init__.py index 83d59253960ce0e3c01ea49d3006674be1b4346d..cdd8bca6481ce552d3ca0bc1ecd2ac7e98f2c1e1 100644 --- a/product/ERP5/__init__.py +++ b/product/ERP5/__init__.py @@ -51,7 +51,7 @@ from Tool import CategoryTool, SimulationTool, RuleTool, IdTool, TemplateTool,\ GadgetTool, ContributionRegistryTool, IntrospectionTool,\ AcknowledgementTool, SolverTool, SolverProcessTool,\ ConversionTool, RoundingTool, UrlRegistryTool,\ - CertificateAuthorityTool, InotifyTool + CertificateAuthorityTool, InotifyTool, TaskDistributionTool import ERP5Site from Document import PythonScript object_classes = ( ERP5Site.ERP5Site, @@ -82,6 +82,7 @@ portal_tools = ( CategoryTool.CategoryTool, UrlRegistryTool.UrlRegistryTool, CertificateAuthorityTool.CertificateAuthorityTool, InotifyTool.InotifyTool, + TaskDistributionTool.TaskDistributionTool, ) content_classes = () content_constructors = ()