diff --git a/setup.py b/setup.py index e0c1bd03..54cb426f 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.2.2" +__version__ = "4.3.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..f44585c0 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 in (object, type): + 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 33704662..fd7ec0d5 100644 --- a/src/rotest/core/abstract_test.py +++ b/src/rotest/core/abstract_test.py @@ -14,6 +14,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 @@ -102,23 +103,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. @@ -131,7 +115,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/__init__.py b/src/rotest/management/__init__.py index 13ddedd4..c7c31c64 100644 --- a/src/rotest/management/__init__.py +++ b/src/rotest/management/__init__.py @@ -1,3 +1,3 @@ -from models import ResourceData -from base_resource import BaseResource -from client.manager import ClientResourceManager +from models import ResourceData +from base_resource import BaseResource +from client.manager import ClientResourceManager diff --git a/src/rotest/management/base_resource.py b/src/rotest/management/base_resource.py index d84230de..f56ba360 100644 --- a/src/rotest/management/base_resource.py +++ b/src/rotest/management/base_resource.py @@ -8,9 +8,12 @@ from ipdbugger import debug from attrdict import AttrDict +from django.db.models.fields.related import \ + ReverseSingleRelatedObjectDescriptor 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, DataPointer class ConvertToKwargsMeta(type): @@ -77,8 +80,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 +99,34 @@ 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__ + actual_kwargs = sub_placeholder.kwargs.copy() + for key, value in sub_placeholder.kwargs.iteritems(): + if isinstance(value, ReverseSingleRelatedObjectDescriptor): + actual_kwargs[key] = getattr(self.data, value.field.name) + + elif isinstance(value, DataPointer): + actual_kwargs[key] = getattr(self.data, value.field_name) + + sub_resource = sub_class(**actual_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/src/rotest/management/models/__init__.py b/src/rotest/management/models/__init__.py index 30489ad9..cca225a8 100644 --- a/src/rotest/management/models/__init__.py +++ b/src/rotest/management/models/__init__.py @@ -1,10 +1,10 @@ -"""Define Rotest's common models. - -The Django infrastructure expects a models.py file containing all the models -definitions for each application. This folder is a workaround used in order -to separate the different common application models into different files. -""" -# pylint: disable=unused-import -from rotest.management.models.resource_data import ResourceData -from rotest.management.models.ut_models import \ - DemoResourceData, DemoComplexResourceData +"""Define Rotest's common models. + +The Django infrastructure expects a models.py file containing all the models +definitions for each application. This folder is a workaround used in order +to separate the different common application models into different files. +""" +# pylint: disable=unused-import +from rotest.management.models.resource_data import ResourceData +from rotest.management.models.ut_models import \ + DemoResourceData, DemoComplexResourceData diff --git a/src/rotest/management/models/resource_data.py b/src/rotest/management/models/resource_data.py index b0b19958..d19a6a1f 100644 --- a/src/rotest/management/models/resource_data.py +++ b/src/rotest/management/models/resource_data.py @@ -9,6 +9,8 @@ from datetime import datetime from django.db import models +from django.utils import six +from django.db.models.base import ModelBase from django.contrib.auth import models as auth_models from django.core.exceptions import ObjectDoesNotExist, ValidationError @@ -17,7 +19,24 @@ from rotest.common.django_utils import get_sub_model, linked_unicode -class ResourceData(models.Model): +class DataPointer(object): + """Pointer to a field in the resource's data.""" + def __init__(self, field_name): + self.field_name = field_name + + +class DataBase(ModelBase): + """Metaclass that creates data pointers for django fields.""" + def __getattr__(cls, key): + if hasattr(cls, '_meta') and \ + key in (field.name for field in cls._meta.fields): + + return DataPointer(key) + + raise AttributeError(key) + + +class ResourceData(six.with_metaclass(DataBase, models.Model)): """Represent a container for a resource's global data. Inheriting resource datas may add more fields, specific to the resource. diff --git a/tests/management/test_resource_manager.py b/tests/management/test_resource_manager.py index 00fd5d79..dc8670da 100644 --- a/tests/management/test_resource_manager.py +++ b/tests/management/test_resource_manager.py @@ -13,7 +13,9 @@ 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.models.resource_data import DataPointer from rotest.management.client.manager import (ClientResourceManager, ResourceRequest) from rotest.management.common.resource_descriptor import \ @@ -573,8 +575,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 +1262,253 @@ def test_threaded_initialize(self): self.assertEqual(len(ThreadedResource.THREADS), 2, "%d threads were created instead of 2" % len(ThreadedResource.THREADS)) + + def test_lock_alternative_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=DemoComplexResourceData.demo1) + demo2 = DemoResource(data=DemoComplexResourceData.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_alternative_complex_resource_with_service(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 = DemoResourceData + demo1 = DemoService(name=DemoComplexResourceData.name) + + def initialize(self): + """Turns on the initialization flag.""" + super(AlterDemoComplexResource, self).initialize() + self.data.initialization_flag = True + self.data.save() + + resources = DemoResourceData.objects.filter(name=self.FREE1_NAME) + + resources_num = len(resources) + self.assertEqual(resources_num, 1, "Expected 1 complex " + "resource with name %r in DB, found %d" + % (self.FREE1_NAME, resources_num)) + + resource, = resources + self.assertTrue(resource.is_available(), "Expected available " + "complex resource with name %r in DB, found %d" + % (self.FREE1_NAME, resources_num)) + + request = ResourceRequest('res1', AlterDemoComplexResource, + name=self.FREE1_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.FREE1_NAME, + "Expected resource with name %r but got %r" + % (self.FREE1_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())), 1, + "Expected to have 1 sub-resources, found %r" + % resource.get_sub_resources()) + + self.assertEquals(resource.name, resource.demo1.name, + "Expected sub-service with name %r but got %r" + % (resource.name, resource.demo1.name)) + + 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) + + self.assertIsInstance(sub_resource, DemoService, + "Expected sub-resource of type %r, got %r" + % (DemoResource.__name__, + sub_resource.__class__.__name__)) + + resources_data = request.type.DATA_CLASS.objects.filter(~Q(owner=""), + name=self.FREE1_NAME) + + resources_num = len(resources_data) + self.assertEquals(resources_num, 1, "Expected 1 locked " + "resource with name %r in DB, found %d" + % (self.FREE1_NAME, resources_num)) + + self.client.release_resources(resources=[resource]) + + def test_lock_alternative_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(name=DataPointer('name')) + + 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.assertEquals(resource.name, resource.demo2.name, + "Expected sub-service with name %r but got %r" + % (resource.name, resource.demo2.name)) + + self.assertNotEquals(resource.name, resource.demo1.name, + "Expected sub-service with name different than %r" + % resource.name) + + 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])