diff --git a/setup.py b/setup.py index 6b915b67..47fc9470 100644 --- a/setup.py +++ b/setup.py @@ -1,7 +1,7 @@ """Setup file for handling packaging and distribution.""" from setuptools import setup, find_packages -__version__ = "4.0.2" +__version__ = "4.1.0" result_handlers = [ "db = rotest.core.result.handlers.db_handler:DBHandler", diff --git a/src/rotest/common/utils.py b/src/rotest/common/utils.py index 1f5ca794..2e39c946 100644 --- a/src/rotest/common/utils.py +++ b/src/rotest/common/utils.py @@ -72,3 +72,31 @@ def get_work_dir(base_dir, test_name, test_item): os.makedirs(work_dir) return work_dir + + +def get_class_fields(cls, field_type): + """Get all fields of the class that inherit from the given type. + + * This method searches also in the parent classes. + * Fields of sons override fields of parents. + * Ignores fields starting with '_'. + + Args: + cls (type): class to search. + field_type (type): field type to find. + + Yields: + tuple. all found (field name, field value). + """ + if cls is object: + return + + for parent_class in cls.__bases__: + for (field_name, field) in get_class_fields(parent_class, field_type): + yield (field_name, field) + + for field_name in cls.__dict__: + if not field_name.startswith("_"): + field = getattr(cls, field_name) + if isinstance(field, field_type): + yield (field_name, field) diff --git a/src/rotest/core/abstract_test.py b/src/rotest/core/abstract_test.py index 0dd6ebc2..f55f973c 100644 --- a/src/rotest/core/abstract_test.py +++ b/src/rotest/core/abstract_test.py @@ -13,6 +13,7 @@ from ipdbugger import debug from attrdict import AttrDict +from rotest.common.utils import get_class_fields from rotest.core.models.case_data import TestOutcome from rotest.management.base_resource import BaseResource from rotest.management.client.manager import ResourceRequest @@ -101,23 +102,6 @@ def release_resource_loggers(self): for resource in self.all_resources.itervalues(): resource.release_logger(self.logger) - @classmethod - def get_resource_requests_fields(cls): - """Yield tuples of all the resource request fields of this test. - - Yields: - tuple. (requests name, request field) tuples of the test class. - """ - checked_class = cls - while checked_class is not AbstractTest: - for field_name in checked_class.__dict__: - if not field_name.startswith("_"): - field = getattr(checked_class, field_name) - if isinstance(field, BaseResource): - yield (field_name, field) - - checked_class = checked_class.__bases__[0] - @classmethod def get_resource_requests(cls): """Return a list of all the resource requests this test makes. @@ -130,7 +114,7 @@ def get_resource_requests(cls): list. resource requests of the test class. """ all_requests = list(cls.resources) - for (field_name, field) in cls.get_resource_requests_fields(): + for (field_name, field) in get_class_fields(cls, BaseResource): new_request = request(field_name, field.__class__, **field.kwargs) diff --git a/src/rotest/core/block.py b/src/rotest/core/block.py index 94591727..b7b14751 100755 --- a/src/rotest/core/block.py +++ b/src/rotest/core/block.py @@ -2,6 +2,7 @@ # pylint: disable=dangerous-default-value,too-many-arguments from itertools import count +from rotest.common.utils import get_class_fields from rotest.common.config import ROTEST_WORK_DIR from rotest.core.flow_component import (AbstractFlowComponent, MODE_OPTIONAL, MODE_FINALLY, MODE_CRITICAL, @@ -105,40 +106,21 @@ def get_name(cls): @classmethod def get_inputs(cls): - """Return a list of all the input instances of this block. + """Return a dict of all the input instances of this block. Returns: dict. block's inputs (name: input placeholder instance). """ - all_inputs = {} - checked_class = cls - while checked_class is not TestBlock: - all_inputs.update({key: value for (key, value) in - checked_class.__dict__.iteritems() - if (isinstance(value, BlockInput) and - key not in all_inputs)}) - - checked_class = checked_class.__bases__[0] - - return all_inputs + return dict(get_class_fields(cls, BlockInput)) @classmethod def get_outputs(cls): - """Return a list of all the input instances of this block. + """Return a dict of all the input instances of this block. Returns: dict. block's inputs (name: input placeholder instance). """ - all_outputs = {} - checked_class = cls - while checked_class is not TestBlock: - all_outputs.update({key: value for (key, value) in - checked_class.__dict__.iteritems() - if isinstance(value, BlockOutput)}) - - checked_class = checked_class.__bases__[0] - - return all_outputs + return dict(get_class_fields(cls, BlockOutput)) def _share_outputs(self): """Share all the declared outputs of the block.""" diff --git a/src/rotest/management/base_resource.py b/src/rotest/management/base_resource.py index d84230de..12e56639 100644 --- a/src/rotest/management/base_resource.py +++ b/src/rotest/management/base_resource.py @@ -10,7 +10,8 @@ from attrdict import AttrDict from rotest.common import core_log -from rotest.common.utils import get_work_dir +from rotest.common.utils import get_work_dir, get_class_fields +from rotest.management.models.resource_data import ResourceData class ConvertToKwargsMeta(type): @@ -28,6 +29,7 @@ def __call__(cls, *args, **kwargs): "positional arguments") resource = type.__call__(cls, *args, **kwargs) + kwargs.pop('data', None) resource.kwargs = kwargs for field_name, field_value in kwargs.iteritems(): setattr(resource, field_name, field_value) @@ -77,8 +79,9 @@ def __init__(self, data=None, **kwargs): if data is not None: self.data = data - for field_name, field_value in self.data.get_fields().iteritems(): - setattr(self, field_name, field_value) + if isinstance(data, ResourceData): + for field_name, field_value in data.get_fields().iteritems(): + setattr(self, field_name, field_value) else: self.data = AttrDict() @@ -95,13 +98,33 @@ def __init__(self, data=None, **kwargs): def create_sub_resources(self): """Create and return the sub resources if needed. + By default, this method searches for sub-resources declared as + class fields, where the 'data' attribute in the declaration points + to the name of the sub-resource's data field under the current's data. + Override and assign sub-resources to fields in the current resource, using the 'data' object. Returns: iterable. sub-resources created. """ - return () + sub_resources = [] + for sub_name, sub_placeholder in get_class_fields(self.__class__, + BaseResource): + + sub_class = sub_placeholder.__class__ + if sub_class.DATA_CLASS is None: + sub_data = None + + else: + sub_data = getattr(self.data, sub_placeholder.data) + + sub_resource = sub_class(data=sub_data, **sub_placeholder.kwargs) + + setattr(self, sub_name, sub_resource) + sub_resources.append(sub_resource) + + return sub_resources def is_available(self, user_name=""): """Return whether resource is available for the given user. diff --git a/tests/management/test_resource_manager.py b/tests/management/test_resource_manager.py index 00fd5d79..4e63e8cf 100644 --- a/tests/management/test_resource_manager.py +++ b/tests/management/test_resource_manager.py @@ -13,6 +13,7 @@ from django.contrib.auth.models import User from swaggapi.api.builder.client import requester +from rotest.management import BaseResource from rotest.management.common.utils import LOCALHOST from rotest.management.client.manager import (ClientResourceManager, ResourceRequest) @@ -573,8 +574,8 @@ def test_lock_release_complex_resource(self): "Sub-resource %r should be locked but " "found available" % sub_resource.name) - resource_instace = DemoResource(data=resources.get()) - self.client._release_resources(resources=[resource_instace]) + resource_instance = DemoResource(data=resources.get()) + self.client._release_resources(resources=[resource_instance]) resources = DemoComplexResourceData.objects.filter( name=self.COMPLEX_NAME) @@ -1260,3 +1261,158 @@ def test_threaded_initialize(self): self.assertEqual(len(ThreadedResource.THREADS), 2, "%d threads were created instead of 2" % len(ThreadedResource.THREADS)) + + def test_lock_alter_complex_resource(self): + """Lock complex resource with the default 'create_sub_resources'. + + * Validates the DB initial state. + * Requests an existing complex resource, using resource client. + * Validates that 1 resource returned. + * Validates the name of the returned resource. + * Validates the type of the returned resource. + * Validates the amount of sub-resources it has. + * Validates the resource and its subs were locked and initialized. + * Releases the locked resource, using resource client. + * Validates the above resource and it sub-resources are now available. + """ + class AlterDemoComplexResource(BaseResource): + """Fake complex resource class, used in resource manager tests.""" + DATA_CLASS = DemoComplexResourceData + demo1 = DemoResource(data='demo1') + demo2 = DemoResource(data='demo2') + + def initialize(self): + """Turns on the initialization flag.""" + super(AlterDemoComplexResource, self).initialize() + self.data.initialization_flag = True + self.data.save() + + resources = DemoComplexResourceData.objects.filter( + name=self.COMPLEX_NAME) + + resources_num = len(resources) + self.assertEqual(resources_num, 1, "Expected 1 complex " + "resource with name %r in DB, found %d" + % (self.COMPLEX_NAME, resources_num)) + + resource, = resources + self.assertTrue(resource.is_available(), "Expected available " + "complex resource with name %r in DB, found %d" + % (self.COMPLEX_NAME, resources_num)) + + request = ResourceRequest('res1', AlterDemoComplexResource, + name=self.COMPLEX_NAME) + + resources = self.client.request_resources(requests=[request]).values() + + resources_num = len(resources) + self.assertEquals(resources_num, 1, "Expected list with 1 " + "resource in it but found %d" % resources_num) + + resource, = resources + self.assertEquals(resource.name, self.COMPLEX_NAME, + "Expected resource with name %r but got %r" + % (self.COMPLEX_NAME, resource.name)) + + self.assertIsInstance(resource, request.type, + "Expected resource of type %r, but got %r" + % (request.type.__name__, + resource.__class__.__name__)) + + self.assertEquals(len(list(resource.get_sub_resources())), 2, + "Expected to have 2 sub-resources, found %r" + % resource.get_sub_resources()) + + self.assertTrue(resource.data.initialization_flag, + "Resource %r should have been initialized" % + resource.name) + + for sub_resource in resource.get_sub_resources(): + self.assertFalse(sub_resource in AlterDemoComplexResource.__dict__, + "Sub-resource %r is still a placeholder" % + sub_resource.name) + + sub_data = DemoResourceData.objects.get(name=sub_resource.name) + self.assertFalse(sub_data.is_available(), + "Sub-resource %r should be locked but " + "found available" % sub_resource.name) + + self.assertIsInstance(sub_resource, DemoResource, + "Expected sub-resource of type %r, got %r" + % (DemoResource.__name__, + sub_resource.__class__.__name__)) + + self.assertTrue(sub_resource.data.initialization_flag, + "Sub-resource %r should have been initialized" % + sub_resource.name) + + resources_data = request.type.DATA_CLASS.objects.filter(~Q(owner=""), + name=self.COMPLEX_NAME) + + resources_num = len(resources_data) + self.assertEquals(resources_num, 1, "Expected 1 locked " + "resource with name %r in DB, found %d" + % (self.COMPLEX_NAME, resources_num)) + + self.client.release_resources(resources=[resource]) + + def test_lock_alter_complex_service(self): + """Lock complex service with the default 'create_sub_resources'. + + * Validates the DB initial state. + * Requests an existing complex resource, using resource client. + * Validates that 1 resource returned. + * Validates the name of the returned resource. + * Validates the type of the returned resource. + * Validates the amount of sub-resources it has. + * Validates the resource was initialized. + * Validates the above resource and it sub-resources were created. + * Releases the locked resource, using resource client. + """ + class AlterDemoComplexService(BaseResource): + """Fake complex service class, used in resource manager tests.""" + DATA_CLASS = None + demo1 = DemoService() + demo2 = DemoService() + + initialized = False + + def initialize(self): + """Turns on the initialization flag.""" + super(AlterDemoComplexService, self).initialize() + self.initialized = True + + request = ResourceRequest('res1', AlterDemoComplexService, + name=self.COMPLEX_NAME) + + resources = self.client.request_resources(requests=[request]).values() + + resource, = resources + self.assertEquals(resource.name, self.COMPLEX_NAME, + "Expected resource with name %r but got %r" + % (self.COMPLEX_NAME, resource.name)) + + self.assertIsInstance(resource, request.type, + "Expected resource of type %r, but got %r" + % (request.type.__name__, + resource.__class__.__name__)) + + self.assertEquals(len(list(resource.get_sub_resources())), 2, + "Expected to have 2 sub-resources, found %r" + % resource.get_sub_resources()) + + self.assertTrue(resource.initialized, + "Resource %r should have been initialized" % + resource.name) + + for sub_resource in resource.get_sub_resources(): + self.assertFalse(sub_resource in AlterDemoComplexService.__dict__, + "Sub-resource %r is still a placeholder" % + sub_resource.name) + + self.assertIsInstance(sub_resource, DemoService, + "Expected sub-resource of type %r, got %r" + % (DemoResource.__name__, + sub_resource.__class__.__name__)) + + self.client.release_resources(resources=[resource])