Complex Resources

Sometimes we want our resources to contain sub-resources or sub-services (the difference is that sub-resources have ResourceData models in the DB and services does not). This can easily be achieved with Rotest.

Creating sub-resource model

In case we want to create a sub-resource, to Calcuator for example, we first need to point to it in the CalculatorData model.

(Skip this part if you want a sub-service, i.e. you don’t need to hold data on the sub-resource in the server’s DB, like when all its data is derived from the containing resource’s data)

from django.db import models
from rotest.management.models.resource_data import ResourceData


class SubCalculatorData(ResourceData):
    class Meta:
        app_label = "resources"

    process_id = models.IntegerField()


class CalculatorData(ResourceData):
    class Meta:
        app_label = "resources"

    ip_address = models.IPAddressField()
    sub_process = models.ForeignKey(SubCalculatorData)

In this example we created the ResourceData model for the sub-resource (like we’d do to any new resource), and pointed to it in the original CalculatorData model, declaring we intend to use a sub-resource here.

Don’t forget to add a reference to the model and the new field in admin.py:

from rotest.management.admin import register_resource_to_admin

from . import models

register_resource_to_admin(models.SubCalculatorData, attr_list=['process_id'])
register_resource_to_admin(models.CalculatorData, attr_list=['ip_address'],
                                                  link_list=['sub_process'])

Note that we used the link_list to point to the sub-resource and not attr-list, since its a model and not a regular field.

Don’t forget to run makemigrations and migrate again after changing the models!

Declaring sub-resources

Let’s continue to modify the Calculator resource, where we want to add sub-resources.

For now, let’s assume we already wrote the sub-resource under resources/sub_process.py.

Now, edit the file resources/calculator.py:

import rpyc
from rotest.management.base_resource import BaseResource

from .models import CalculatorData
from .sub_process import SubProcess


class Calculator(BaseResource):
    DATA_CLASS = CalculatorData

    PORT = 1357

    sub_process = SubProcess.request(data=CalculatorData.sub_process)

    def connect(self):
        super(Calculator, self).connect()
        self._rpyc = rpyc.classic.connect(self.data.ip_address, self.PORT)

    def finalize(self):
        super(Calculator, self).finalize()
        if self._rpyc is not None:
            self._rpyc.close()
            self._rpyc = None

    def calculate(self, expression):
        return self._rpyc.eval(expression)

    def get_sub_process_id(self, expression):
        return self.sub_process.data.process_id

Note the following:

  • Declaring the sub-resource:

    sub_process = SubProcess.request(data=CalculatorData.sub_process)
    

    The syntax is the same as requesting resources for a test.

    We assigned the SubCalculatorData model instance (pointed from the containing resource’s CalculatorData) as the data for out sub-resource.

    Alternatively, in case SubProcess was a service and not a full-fledged resource, we could have passed parameters to it in a similar way:

    sub_process = SubProcess.request(ip_address=CalculatorData.ip_address,
                                     process_id=5)
    
  • The usage of the sub-resource

    def get_sub_process_id(self, expression):
        return self.sub_process.process_id
    

    Once the sub-resource or service is declared, it can be accessed from any of the containing resource’s methods, using the assigned name (in this case, the declaration line name it sub_process).

Lastly, let’s show the sub-resource under resources/sub_process.py:

from rotest.management.base_resource import BaseResource

from .models import SubCalculatorData


class SubProcess(BaseResource):
    DATA_CLASS = SubCalculatorData

    def container_calculate(self, expression):
        return self.parent.calculate(expression)

    def get_ip_address(self):
        return self.parent.data.ip_address

Note that we have access to the containing resource via parent.

This also applies when we write sub-services, which can use the parent’s methods, data, and even fields (e.g. self.parent._rpyc).

When writing sub-resources and services, remember two things:

  • Always call super when overriding BaseResource’s methods (connect, initialize, validate, finalize, store_state), since the basic method propagate the call to sub-resources.
  • It is ok to use self.parent and self.<sub-resource-name> , but mind the context. E.g. self.parent._rpyc in the above example is accessible from the sub-resource, but only after the connect() method (since firstly the sub-resource connects, and only afterwards the containing resource connects). The same applies for the other basic methods (first the sub-resources initialize, then the containing).