Repository: incubator-ariatosca Updated Branches: refs/heads/ARIA-174-Refactor-instantiation-phase 50d4c1de5 -> a0e776fa8
fixed all of the tests, still remain fix the dump_types, figure out the relationship between the context issues and the handler issues, is the handler as a singleton is realy necessary, figure out the local imports Project: http://git-wip-us.apache.org/repos/asf/incubator-ariatosca/repo Commit: http://git-wip-us.apache.org/repos/asf/incubator-ariatosca/commit/a0e776fa Tree: http://git-wip-us.apache.org/repos/asf/incubator-ariatosca/tree/a0e776fa Diff: http://git-wip-us.apache.org/repos/asf/incubator-ariatosca/diff/a0e776fa Branch: refs/heads/ARIA-174-Refactor-instantiation-phase Commit: a0e776fa86d36912c3e598aeadd25c7c1ee212f6 Parents: 50d4c1d Author: max-orlov <ma...@gigaspaces.com> Authored: Wed Jul 26 18:03:26 2017 +0300 Committer: max-orlov <ma...@gigaspaces.com> Committed: Wed Jul 26 18:03:26 2017 +0300 ---------------------------------------------------------------------- aria/orchestrator/topology/__init__.py | 12 ++- aria/orchestrator/topology/instance.py | 136 ++++++++++--------------- aria/orchestrator/topology/template.py | 51 ++++------ aria/parser/consumption/consumer.py | 4 +- aria/parser/consumption/modeling.py | 6 +- aria/parser/modeling/context.py | 6 +- aria/parser/validation/context.py | 59 +---------- aria/parser/validation/issue.py | 68 ++++++++++++- tests/instantiation/test_configuration.py | 13 +-- 9 files changed, 165 insertions(+), 190 deletions(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/incubator-ariatosca/blob/a0e776fa/aria/orchestrator/topology/__init__.py ---------------------------------------------------------------------- diff --git a/aria/orchestrator/topology/__init__.py b/aria/orchestrator/topology/__init__.py index 5c7747b..afa5334 100644 --- a/aria/orchestrator/topology/__init__.py +++ b/aria/orchestrator/topology/__init__.py @@ -14,6 +14,8 @@ # limitations under the License. from StringIO import StringIO +from ...parser.validation import issue +from ...parser.consumption.style import Style from ...modeling import models from ...utils import console from . import ( @@ -23,7 +25,7 @@ from . import ( ) -class Handler(object): +class Handler(issue.Reporter): _init_map = { models.ServiceTemplate: models.Service, @@ -52,8 +54,7 @@ class Handler(object): class TopologyStylizer(object): def __init__(self): - from aria.parser.consumption import style - self._style = style.Style() + self._style = Style() self._str = StringIO() def write(self, str_): @@ -71,9 +72,10 @@ class Handler(object): except AttributeError: return super(Handler.TopologyStylizer, self).__getattribute__(item) - def __init__(self, model_storage=None): + def __init__(self, model_storage=None, *args, **kwargs): # TODO: model storage is required only for the list of plugins, can we get it # somewhere else? + super(Handler, self).__init__(*args, **kwargs) self._model_storage = model_storage self._handlers = dict(self._init_handlers(instance), **self._init_handlers(template)) @@ -217,7 +219,7 @@ class Handler(object): def find_hosts(self, service): for node in service.nodes.values(): - service.host = self._find_host(node) + node.host = self._find_host(node) def configure_operations(self, model, **kwargs): if isinstance(model, dict): http://git-wip-us.apache.org/repos/asf/incubator-ariatosca/blob/a0e776fa/aria/orchestrator/topology/instance.py ---------------------------------------------------------------------- diff --git a/aria/orchestrator/topology/instance.py b/aria/orchestrator/topology/instance.py index 5990792..e160d2b 100644 --- a/aria/orchestrator/topology/instance.py +++ b/aria/orchestrator/topology/instance.py @@ -13,14 +13,13 @@ # See the License for the specific language governing permissions and # limitations under the License. +from ... parser.modeling import context from ... modeling import models +from ... utils import formatting from .. import execution_plugin from .. import decorators from . import common -# TODO: this should this be here? -from aria.utils import formatting - class Artifact(common._InstanceHandler): @@ -129,16 +128,12 @@ class Node(common._OperatorHolderHandler): **kwargs) def validate(self, **kwargs): - # TODO: fix the context - # context = ConsumptionContext.get_thread_local() - # if len(self._template.name) > context.modeling.id_max_length: - # pass - # context.validation.report('"{0}" has an ID longer than the limit of {1:d} characters: ' - # '{2:d}'.format( - # self.name, - # context.modeling.id_max_length, - # len(self.name)), - # level=validation.Issue.BETWEEN_INSTANCES) + if len(self._model.name) > context.ID_MAX_LENGTH: + pass + self._topology.report('"{0}" has an ID longer than the limit of {1:d} characters: ' + '{2:d}'.format( + self._model.name, context.ID_MAX_LENGTH, len(self._model.name)), + level=self._topology.Issue.BETWEEN_INSTANCES) self._validate(self._model.properties, self._model.attributes, @@ -166,18 +161,13 @@ class Node(common._OperatorHolderHandler): self._topology.configure_operations(relationship) def validate_capabilities(self): - # TODO: fix - # context = ConsumptionContext.get_thread_local() satisfied = False for capability in self._model.capabilities.itervalues(): if not capability.has_enough_relationships: - # context.validation.report('capability "{0}" of node "{1}" requires at least {2:d} ' - # 'relationships but has {3:d}'.format( - # capability.name, - # self.name, - # capability.min_occurrences, - # capability.occurrences), - # level=validation.Issue.BETWEEN_INSTANCES) + self._topology.report('capability "{0}" of node "{1}" requires at least {2:d} ' + 'relationships but has {3:d}'.format( + capability.name, self._model.name, capability.min_occurrences, capability.occurrences), + level=self._topology.Issue.BETWEEN_INSTANCES) satisfied = False return satisfied @@ -188,22 +178,16 @@ class Node(common._OperatorHolderHandler): target_node_template, target_node_capability = self._find_target(requirement_template) if target_node_template is not None: satisfied = self._satisfy_capability( - target_node_capability, target_node_template, - requirement_template) + target_node_capability, target_node_template, requirement_template) else: - # TODO: fix - - # context = ConsumptionContext.get_thread_local() - # context.validation.report('requirement "{0}" of node "{1}" has no target node ' - # 'template'.format(requirement_template.name, self.name), - # level=validation.Issue.BETWEEN_INSTANCES) + self._topology.report('requirement "{0}" of node "{1}" has no target node template'. + format(requirement_template.name, self._model.name), + level=self._topology.Issue.BETWEEN_INSTANCES) satisfied = False return satisfied def _satisfy_capability(self, target_node_capability, target_node_template, requirement_template): - # TODO: fix reporting - # context = ConsumptionContext.get_thread_local() # Find target nodes target_nodes = target_node_template.nodes if target_nodes: @@ -236,38 +220,32 @@ class Node(common._OperatorHolderHandler): self._model.outbound_relationships.append(relationship_model) return True else: - # context.validation.report('requirement "{0}" of node "{1}" targets node ' - # 'template "{2}" but its instantiated nodes do not ' - # 'have enough capacity'.format( - # requirement_template.name, - # self.name, - # target_node_template.name), - # level=validation.Issue.BETWEEN_INSTANCES) + self._topology.report('requirement "{0}" of node "{1}" targets node ' + 'template "{2}" but its instantiated nodes do not ' + 'have enough capacity'.format( + requirement_template.name, self._model.name, target_node_template.name), + level=self._topology.Issue.BETWEEN_INSTANCES) return False else: - # context.validation.report('requirement "{0}" of node "{1}" targets node template ' - # '"{2}" but it has no instantiated nodes'.format( - # requirement_template.name, - # self.name, - # target_node_template.name), - # level=validation.Issue.BETWEEN_INSTANCES) + self._topology.report('requirement "{0}" of node "{1}" targets node template ' + '"{2}" but it has no instantiated nodes'.format( + requirement_template.name, self._model.name, target_node_template.name), + level=self._topology.Issue.BETWEEN_INSTANCES) return False def _find_target(self, requirement_template): # We might already have a specific node template, so we'll just verify it if requirement_template.target_node_template is not None: if not self._model.node_template.is_target_node_template_valid(requirement_template.target_node_template): - # TODO: fix - pass - # context.validation.report('requirement "{0}" of node template "{1}" is for node ' - # 'template "{2}" but it does not match constraints'.format( - # self.name, - # self.target_node_template.name, - # source_node_template.name), - # level=validation.Issue.BETWEEN_TYPES) + self._topology.report('requirement "{0}" of node template "{1}" is for node ' + 'template "{2}" but it does not match constraints'.format( + requirement_template.name, + requirement_template.target_node_template.name, + self._model.node_template.name), + level=self._topology.Issue.BETWEEN_TYPES) if (requirement_template.target_capability_type is not None or requirement_template.target_capability_name is not None): - target_node_capability = self._get_capability_from_requirement(requirement_template) + target_node_capability = self._get_capability(requirement_template) if target_node_capability is None: return None, None else: @@ -285,22 +263,27 @@ class Node(common._OperatorHolderHandler): if not self._model.node_template.is_target_node_template_valid(target_node_template): continue - if requirement_template.target_node_template: - target_node_capability = self._get_capability_from_requirement(requirement_template) - if target_node_capability is None: - continue - else: - return target_node_template, target_node_capability + target_node_capability = self._get_capability(requirement_template, + target_node_template) + + if target_node_capability is None: + continue + + return target_node_template, target_node_capability return None, None - def _get_capability_from_requirement(self, requirement_template): - for capability_template in requirement_template.target_node_template.capability_templates.values(): - if self._satisfies_requirement(capability_template, requirement_template): + def _get_capability(self, requirement_template, target_node_template=None): + target_node_template = target_node_template or requirement_template.target_node_template + + for capability_template in target_node_template.capability_templates.values(): + if self._satisfies_requirement( + capability_template, requirement_template, target_node_template): return capability_template + return None - def _satisfies_requirement(self, capability_template, requirement_template): + def _satisfies_requirement(self, capability_template, requirement_template, target_node_template): # Do we match the required capability type? if (requirement_template.target_capability_type and requirement_template.target_capability_type.get_descendant( @@ -318,7 +301,7 @@ class Node(common._OperatorHolderHandler): if requirement_template.target_node_template_constraints: for node_template_constraint in requirement_template.target_node_template_constraints: if not node_template_constraint.matches( - self._model.node_template, requirement_template.target_node_template): + self._model.node_template, target_node_template): return False return True @@ -396,13 +379,10 @@ class Operation(common._OperatorHolderHandler): # Check for reserved arguments used_reserved_names = decorators.OPERATION_DECORATOR_RESERVED_ARGUMENTS.intersection( self._model.arguments.keys()) - # if used_reserved_names: - # # context = ConsumptionContext.get_thread_local() - # context.validation.report('using reserved arguments in operation "{0}": {1}' - # .format( - # self.name, - # formatting.string_list_as_string(used_reserved_names)), - # level=validation.Issue.EXTERNAL) + if used_reserved_names: + self._topology.report('using reserved arguments in operation "{0}": {1}'.format( + self._model.name, formatting.string_list_as_string(used_reserved_names)), + level=self._topology.Issue.EXTERNAL) class Policy(common._InstanceHandler): @@ -535,15 +515,11 @@ class Substitution(common._InstanceHandler): class SubstitutionMapping(common._InstanceHandler): def validate(self, **kwargs): - # context = ConsumptionContext.get_thread_local() if (self._model.capability is None) and (self._model.requirement_template is None): - pass - # TODO: handler reports - # context.validation.report('mapping "{0}" refers to neither capability nor a requirement' - # ' in node: {1}'.format( - # self.name, - # formatting.safe_repr(self.node.name)), - # level=validation.Issue.BETWEEN_TYPES) + self._topology.report('mapping "{0}" refers to neither capability nor a requirement' + ' in node: {1}'.format( + self._model.name, formatting.safe_repr(self._model.node.name)), + level=self._topology.Issue.BETWEEN_TYPES) def dump(self, console): if self._model.capability is not None: http://git-wip-us.apache.org/repos/asf/incubator-ariatosca/blob/a0e776fa/aria/orchestrator/topology/template.py ---------------------------------------------------------------------- diff --git a/aria/orchestrator/topology/template.py b/aria/orchestrator/topology/template.py index 2d1319a..917f7a8 100644 --- a/aria/orchestrator/topology/template.py +++ b/aria/orchestrator/topology/template.py @@ -15,6 +15,7 @@ from datetime import datetime +from ...utils import formatting from ...modeling import utils as modeling_utils from . import utils, common @@ -60,10 +61,8 @@ class ServiceTemplate(common._TemplateHandler): plugin = plugin_specification.plugin service.plugins[plugin.name] = plugin else: - # TODO: fix the context report usage - pass - # self._context.validation.report('specified plugin not found: {0}'.format( - # plugin_specification.name), level=validation.Issue.EXTERNAL) + self._topology.report('specified plugin not found: {0}'.format( + plugin_specification.name), level=self._topology.Issue.EXTERNAL) service.meta_data = self._topology.instantiate(self._model.meta_data) for node_template in self._model.node_templates.itervalues(): @@ -81,8 +80,7 @@ class ServiceTemplate(common._TemplateHandler): return service - @staticmethod - def _scaling(node_template): + def _scaling(self, node_template): scaling = {} def extract_property(properties, name): @@ -125,16 +123,10 @@ class ServiceTemplate(common._TemplateHandler): scaling['max_instances'] < scaling['min_instances'] or scaling['default_instances'] < scaling['min_instances'] or scaling['default_instances'] > scaling['max_instances']): - pass - # TODO: fix this - # context = ConsumptionContext.get_thread_local() - # context.validation.report('invalid scaling parameters for node template "{0}": ' - # 'min={1}, max={2}, default={3}'.format( - # self.name, - # scaling['min_instances'], - # scaling['max_instances'], - # scaling['default_instances']), - # level=validation.Issue.BETWEEN_TYPES) + self._topology.report( + 'invalid scaling parameters for node template "{0}": min={min_instances}, max=' + '{max_instances}, default={default_instances}'.format(self._model.name, **scaling), + level=self._topology.Issue.BETWEEN_TYPES) return scaling @@ -475,12 +467,11 @@ class SubstitutionTemplateMapping(common._TemplateHandler): node_template = self._model.requirement_template.node_template nodes = node_template.nodes if len(nodes) == 0: - # TODO: manage the context report - # self._context.validation.report( - # 'mapping "{0}" refers to node template "{1}" but there are no node instances'. - # format(self._template.mapped_name, - # self._template.node_template.name), - # level=validation.Issue.BETWEEN_INSTANCES) + self._topology.report( + 'mapping "{0}" refers to node template "{1}" but there are no node instances'. + format(self._model.mapped_name, + self._model.node_template.name), + level=self._topology.Issue.BETWEEN_INSTANCES) return None # The TOSCA spec does not provide a way to choose the node, # so we will just pick the first one @@ -494,18 +485,14 @@ class SubstitutionTemplateMapping(common._TemplateHandler): return substitution_mapping def validate(self): - # context = ConsumptionContext.get_thread_local() if all([ self._model.capability_template is None, self._model.requirement_template is None ]): - pass - # TODO: handle reporting - # context.validation.report('mapping "{0}" refers to neither capability nor a requirement' - # ' in node template: {1}'.format( - # self.name, - # formatting.safe_repr(self.node_template.name)), - # level=validation.Issue.BETWEEN_TYPES) + self._topology.report('mapping "{0}" refers to neither capability nor a requirement ' + 'in node template: {1}'.format( + self._model.name, formatting.safe_repr(self._model.node_template.name)), + level=self._topology.Issue.BETWEEN_TYPES) class RelationshipTemplate(common._TemplateHandler): @@ -535,9 +522,7 @@ class RelationshipTemplate(common._TemplateHandler): return relationship def validate(self): - # TODO: either type or name must be set - self._validate(self._model.properties, - self._model.interface_templates) + self._validate(self._model.properties, self._model.interface_templates) class OperationTemplate(common._TemplateHandler): http://git-wip-us.apache.org/repos/asf/incubator-ariatosca/blob/a0e776fa/aria/parser/consumption/consumer.py ---------------------------------------------------------------------- diff --git a/aria/parser/consumption/consumer.py b/aria/parser/consumption/consumer.py index 4c79aab..8acbf31 100644 --- a/aria/parser/consumption/consumer.py +++ b/aria/parser/consumption/consumer.py @@ -13,8 +13,6 @@ # See the License for the specific language governing permissions and # limitations under the License. -from aria.orchestrator import topology - from ...exceptions import AriaException from ...utils.exceptions import print_exception @@ -29,6 +27,8 @@ class Consumer(object): """ def __init__(self, context): + from aria.orchestrator import topology + self.handler = topology.handler self.context = context http://git-wip-us.apache.org/repos/asf/incubator-ariatosca/blob/a0e776fa/aria/parser/consumption/modeling.py ---------------------------------------------------------------------- diff --git a/aria/parser/consumption/modeling.py b/aria/parser/consumption/modeling.py index 8216816..828cc30 100644 --- a/aria/parser/consumption/modeling.py +++ b/aria/parser/consumption/modeling.py @@ -13,8 +13,6 @@ # See the License for the specific language governing permissions and # limitations under the License. -from aria.orchestrator import topology - from .consumer import Consumer, ConsumerChain from ...utils.formatting import json_dumps, yaml_dumps from ... import exceptions @@ -108,7 +106,7 @@ class InstantiateServiceInstance(Consumer): self.context.validation.report('InstantiateServiceInstance consumer: missing service ' 'template') return - self.context.modeling.instance = topology.handler.instantiate( + self.context.modeling.instance = self.handler.instantiate( self.context.modeling.template, inputs=dict(self.context.modeling.inputs) ) @@ -125,7 +123,7 @@ class InstantiateServiceInstance(Consumer): CoerceServiceInstanceValues )).consume() - if self.context.validation.dump_issues(): + if self.handler.dump_issues(): raise exceptions.InstantiationError('Failed to instantiate service template `{0}`' .format(self.context.modeling.template.name)) http://git-wip-us.apache.org/repos/asf/incubator-ariatosca/blob/a0e776fa/aria/parser/modeling/context.py ---------------------------------------------------------------------- diff --git a/aria/parser/modeling/context.py b/aria/parser/modeling/context.py index 3d75617..286ab60 100644 --- a/aria/parser/modeling/context.py +++ b/aria/parser/modeling/context.py @@ -19,6 +19,10 @@ from ...utils.collections import StrictDict, prune from ...utils.uuid import generate_uuid +# See: http://www.faqs.org/rfcs/rfc1035.html +ID_MAX_LENGTH = 63 + +1 class IdType(object): LOCAL_SERIAL = 0 """ @@ -61,7 +65,7 @@ class ModelingContext(object): #self.id_type = IdType.LOCAL_SERIAL #self.id_type = IdType.LOCAL_RANDOM self.id_type = IdType.UNIVERSAL_RANDOM - self.id_max_length = 63 # See: http://www.faqs.org/rfcs/rfc1035.html + self.id_max_length = ID_MAX_LENGTH self.inputs = StrictDict(key_class=basestring) self._serial_id_counter = itertools.count(1) http://git-wip-us.apache.org/repos/asf/incubator-ariatosca/blob/a0e776fa/aria/parser/validation/context.py ---------------------------------------------------------------------- diff --git a/aria/parser/validation/context.py b/aria/parser/validation/context.py index ef641bd..a245518 100644 --- a/aria/parser/validation/context.py +++ b/aria/parser/validation/context.py @@ -13,15 +13,10 @@ # See the License for the specific language governing permissions and # limitations under the License. -from .issue import Issue -from ...utils.threading import LockedList -from ...utils.collections import FrozenList -from ...utils.exceptions import print_exception -from ...utils.console import puts, Colored, indent -from ...utils.formatting import as_raw +from . import issue -class ValidationContext(object): +class ValidationContext(issue.Reporter): """ Validation context. @@ -35,53 +30,7 @@ class ValidationContext(object): :vartype max_level: int """ - def __init__(self): + def __init__(self, *args, **kwargs): + super(ValidationContext, self).__init__(*args, **kwargs) self.allow_unknown_fields = False self.allow_primitive_coersion = False - self.max_level = Issue.ALL - - self._issues = LockedList() - - def report(self, message=None, exception=None, location=None, line=None, - column=None, locator=None, snippet=None, level=Issue.PLATFORM, issue=None): - if issue is None: - issue = Issue(message, exception, location, line, column, locator, snippet, level) - - # Avoid duplicate issues - with self._issues: - for i in self._issues: - if str(i) == str(issue): - return - - self._issues.append(issue) - - @property - def has_issues(self): - return len(self._issues) > 0 - - @property - def issues(self): - issues = [i for i in self._issues if i.level <= self.max_level] - issues.sort(key=lambda i: (i.level, i.location, i.line, i.column, i.message)) - return FrozenList(issues) - - @property - def issues_as_raw(self): - return [as_raw(i) for i in self.issues] - - def dump_issues(self): - issues = self.issues - if issues: - puts(Colored.blue('Validation issues:', bold=True)) - with indent(2): - for issue in issues: - puts(Colored.blue(issue.heading_as_str)) - details = issue.details_as_str - if details: - with indent(3): - puts(details) - if issue.exception is not None: - with indent(3): - print_exception(issue.exception) - return True - return False http://git-wip-us.apache.org/repos/asf/incubator-ariatosca/blob/a0e776fa/aria/parser/validation/issue.py ---------------------------------------------------------------------- diff --git a/aria/parser/validation/issue.py b/aria/parser/validation/issue.py index db8065d..f4304b7 100644 --- a/aria/parser/validation/issue.py +++ b/aria/parser/validation/issue.py @@ -15,8 +15,14 @@ from __future__ import absolute_import # so we can import standard 'collections' -from ...utils.collections import OrderedDict -from ...utils.type import full_type_name +from ...utils import ( + collections, + type, + threading, + exceptions, + console, + formatting +) class Issue(object): @@ -82,14 +88,14 @@ class Issue(object): @property def as_raw(self): - return OrderedDict(( + return collections.OrderedDict(( ('level', self.level), ('message', self.message), ('location', self.location), ('line', self.line), ('column', self.column), ('snippet', self.snippet), - ('exception', full_type_name(self.exception) if self.exception else None))) + ('exception', type.full_type_name(self.exception) if self.exception else None))) @property def locator_as_str(self): @@ -124,3 +130,57 @@ class Issue(object): if details: heading_str += ', ' + details return heading_str + + +class Reporter(object): + + Issue = Issue + + def __init__(self, *args, **kwargs): + super(Reporter, self).__init__(*args, **kwargs) + self._issues = threading.LockedList() + self.max_level = self.Issue.ALL + + def report(self, message=None, exception=None, location=None, line=None, + column=None, locator=None, snippet=None, level=Issue.PLATFORM, issue=None): + if issue is None: + issue = self.Issue(message, exception, location, line, column, locator, snippet, level) + + # Avoid duplicate issues + with self._issues: + for i in self._issues: + if str(i) == str(issue): + return + + self._issues.append(issue) + + @property + def has_issues(self): + return len(self._issues) > 0 + + @property + def issues(self): + issues = [i for i in self._issues if i.level <= self.max_level] + issues.sort(key=lambda i: (i.level, i.location, i.line, i.column, i.message)) + return collections.FrozenList(issues) + + @property + def issues_as_raw(self): + return [formatting.as_raw(i) for i in self.issues] + + def dump_issues(self): + issues = self.issues + if issues: + console.puts(console.Colored.blue('Validation issues:', bold=True)) + with console.indent(2): + for issue in issues: + console.puts(console.Colored.blue(issue.heading_as_str)) + details = issue.details_as_str + if details: + with console.indent(3): + console.puts(details) + if issue.exception is not None: + with console.indent(3): + exceptions.print_exception(issue.exception) + return True + return False \ No newline at end of file http://git-wip-us.apache.org/repos/asf/incubator-ariatosca/blob/a0e776fa/tests/instantiation/test_configuration.py ---------------------------------------------------------------------- diff --git a/tests/instantiation/test_configuration.py b/tests/instantiation/test_configuration.py index 659e334..2dcfd63 100644 --- a/tests/instantiation/test_configuration.py +++ b/tests/instantiation/test_configuration.py @@ -17,6 +17,7 @@ import pytest from tests.parser.service_templates import consume_literal from aria.modeling.utils import parameters_as_values +from aria.orchestrator.topology import handler TEMPLATE = """ @@ -165,9 +166,9 @@ def test_remote(service): def test_reserved_arguments(broken_service_issues): - assert len(broken_service_issues) == 2 - assert any( - all([issue.message.startswith('using reserved arguments in operation "operation":'), - 'ctx' in issue.message, - 'toolbelt' in issue.message]) - for issue in broken_service_issues) + assert len(broken_service_issues) == 1 + assert len(handler.issues) == 1 + issue = handler.issues[0].message + assert all([issue.startswith('using reserved arguments in operation "operation" :'), + 'ctx' in issue, + 'toolbelt' in issue])