Short-name access to TestContext devices

Description

When a device server is launched via the DeviceTestContext or MultiDeviceTestContext, it is run without a Tango database. This means that access to any of its devices must be done via a fully-qualified Tango Resource Locator (FQTRL). I.e., we need something like tango://172.17.0.3:48493/test/device/1#dbase=no, instead of just the short name: test/device/1. Requiring the FQTRL is inconvenient.

A simple example:

from tango import AttrWriteType, DeviceProxy
from tango.server import Device, attribute
from tango.test_context import MultiDeviceTestContext

class Device1(Device):
    _attr1 = 100

    @attribute(access=AttrWriteType.READ_WRITE)
    def attr1(self):
        return self._attr1

    def write_attr1(self, value):
        self._attr1 = value

def test_multi_short_name_device_proxy_access_without_tango_db():
    devices_info = ({"class": Device1, "devices": [{"name": "test/device1/1"}]},)

    with MultiDeviceTestContext(devices_info):
        proxy1 = tango.DeviceProxy("test/device1/1")
        assert proxy1.name() == "test/device1/1"
        assert proxy1.attr1 == 100

Previously, we would have had to use a call like context.get_device_access("test/device1/1") to get the FQTRL, before instantiating a DeviceProxy.

A more detailed example is available.

Limitations

  • Group patterns (* wildcard) are not supported.

  • Group device names will be FQTRLs, rather than the short name that was originally supplied.

  • Launching two TestContexts in the same process will not work correctly without FQTRLs. The TestContext that is started second will overwrite the global variable set by the first.

Implementation details

From PyTango 9.5.0, the test device server’s FQTRL is stored in in a global variable while the TextContext is active. Then, in order to make device access with short names transparent, the DeviceProxy, AttributeProxy and Group classes have been modified to take this global variable into account. If it is set, and a short (i.e., unresolved) TRL is used for the proxy constructor or group add method, then the name is rewritten to a fully qualified (resolved) TRL. If this FQTRL is not accessible, the short name is tried again (except for Group) - this allows real devices to be accessed via short TRLs, if necessary.

There should not be a noticeable performance degradation for this outside of test environments. It it just a single variable lookup per proxy constructor or group add invocation.

This new TestContext functionality is enabled by default. It can be disabled by setting the enable_test_context_tango_host_override attribute to False before starting the TestContext.

Example:

def test_multi_short_name_access_fails_if_override_disabled():
    devices_info = ({"class": Device1, "devices": [{"name": "test/device1/a"}]},)

    context = MultiDeviceTestContext(devices_info)
    context.enable_test_context_tango_host_override = False
    context.start()
    try:
        with pytest.raises(DevFailed):
            _ = tango.DeviceProxy("test/device1/a")
    finally:
        context.stop()

Port detection and pre_init_callback

As before, if the user doesn’t specify a port number when instantiating the TestContext, omniORB will select an ephemeral port automatically when the device server starts. We need to determine this port number before any device calls its init_device method, in case it want to create clients (DeviceProxy, etc.) at that early stage.

The solution to this is the addition of a pre_init_callback in the run() and run_server() methods. This is called after Util.init() (when omniORB has started and bound to a TCP port for the GIOP traffic, and the event system has bound two ports for ZeroMQ traffic), but before Util.server_init() (which initialises all classes in the device server).

During pre_init_callback the TestContext probes the process’s open ports to determine the GIOP port number. We didn’t find a way to determine the port number without probing each port.