Tree-based organization of keywords within a Robot Framework test library?

I have an existing Python I/O library that communicates with a piece of test equipment. I have been tasked with modifying it to add Robot Framework hooks such as keyword decorators, listeners, etc.

The Python library implements ~800 commands, each as a Python method. They’re organized into a few child classes, based on the command group. For example, in Python, I might use it like this:

import FooLibrary
foo = FooLibrary()
foo.group1.reset()
foo.group1.start()
foo.group1.doSomething()
foo.group1.stop()
foo.group2.reset()
foo.group2.start()
foo.group2.subgroup1.bar()
foo.group2.subgroup1.baz()
foo.group2.stop()

Is it even possible to keep the tree-based child class structure like this in a Robot Framework library? I’ve read the RF user guide, and didn’t see anything, so I assume the answer is “no”. But either way, I need to make this library usable within Robot Framework. So is there any advice for naming and organizing keywords for a library this big?

I can’t name the keywords something simple like “Reset”, “Start”, and “Stop”, since there are multiple command groups with those names.

And I can’t name them “Reset Group1”, “Reset Group2” etc because the API docs sort alphabetically, that makes it a huge mess since all the groups are out-of-order.

And I can’t make each group a separate Robot Framework library since they share an I/O communication session to the test equipment.

The only solution I can come up with is to have a giant monolithic test library with all 800+ keywords shoved into it, and prefix them with the group name, e.g.: “Group 1 Reset” “Group 2 Reset” and so on.

This doesn’t seem very elegant to me. Is there a better way to structure and name the keywords in my test library?

“Is it even possible to keep the tree-based child class structure like this in a Robot Framework library?”
Yes. The basic idea would be to have an Abstract Base Class (ABC) or protocol which defines the structure a group should have - i.e. that any class should expose & implement/inherit start, stop, reset etc. methods.

Group1 and Group2 would be classes setup to implement/inherit this specification.

FooLibrary would then setup group1 and group2 variables for the types (classes) Group1 and Group2 & expose them. There is a name for this ease-of-use, moving up & exposing of a shortcutting class from the main class in Martin Fowler’s book, Refactoring but the name escapes me now.

A collection could probably serve here. If all the groups have the same definition (start, stop, reset etc.) then keep the ABC/protocol/duck typing setup described above, but instead of setting up a variable for group1, group2 etc. in FooLibrary, instead build a collection of Group types. This will shortern the code & make it far more resiliant to expansion + maintenance changes later on.

Also you could go back to the drawing board here and look into both the (i) iterator & composite design patterns together and (ii) command design pattern which might likely better fit your needs. :slight_smile:

the groups don’t have identical method names unfortunately. A few overlap, but most don’t.

I can’t rewrite the library. I am only trying to annotate it with Robot Framework keywords.

The library is already setup to expose group1 and group2 and it all works fine in Python. My question is whether or not I can nest keywords as shown in the code example below.

FooLibrary.py:

from robot.api.deco import library, keyword
from robot.api import logger

__all__ = ['FooLibrary']


@library(scope='TEST')
class FooLibrary:

    def __init__(self):
        logger.info(f'FooLibrary constructor id={id(self)}')
        self._group1 = Group1()
        self._group2 = Group2()

    @keyword("Initialize")
    def initialize(self):
        logger.info(f'FooLibrary Initialize id={id(self)}')

    @keyword("Close")
    def close(self):
        logger.info(f'FooLibrary Close id={id(self)}')

    @property
    @keyword("Group1")
    def group1(self):
        return self._group1

    @property
    @keyword("Group2")
    def group2(self):
        return self._group1


class Group1:

    def __init__(self):
        logger.info(f'Group1 constructor id={id(self)}')

    @keyword("Start")
    def start(self):
        logger.info(f'Group1 Start id={id(self)}')

    @keyword("Stop")
    def stop(self):
        logger.info(f'Group1 Stop id={id(self)}')

    @keyword("Do Something")
    def doSomething(self):
        logger.info(f'Group1 Do Something id={id(self)}')


class Group2:

    def __init__(self):
        logger.info(f'Group2 constructor id={id(self)}')

    @keyword("Start")
    def start(self):
        logger.info(f'Group2 Start id={id(self)}')

    @keyword("Stop")
    def stop(self):
        logger.info(f'Group2 Stop id={id(self)}')

test.robot:

*** Settings ***
Library               FooLibrary


*** Test Cases ***

FooTest
    Initialize
    Group1  # [FAIL INSIDE[ No keyword with name 'Group1' found.
    Group1.Start  # [FAIL INSIDE[ No keyword with name 'Group1' found.
    Close

ok working example1, this with properties not keywords:
*.py code

    @library(scope='TEST')
    class FooLibrary:
    ......
        @property
        def group1a(self):
            logger.info(f'group1a method running')
            return self._group1

    class Group1:
    ......
        @property
        def starta(self):
            logger.info(f'Group1 Start id={id(self)}')

*.robot code

${FooLibrary}    Get Library Instance    FooLibrary
${group1a}=    Set Variable    ${FooLibrary.group1a}
${group1a_start}=    Set Variable    ${FooLibrary.group1a.starta}

working example2, this with keywords:

*.py code

@library(scope='TEST')
class FooLibrary:
......
@keyword("group1b")
def group1b(self):
    logger.info(f'group1b method running')
    return self._group1

class Group1:
......
@keyword("startb")    
def startb(self):
    logger.info(f'Group1 Start id={id(self)}')

*.robot code

Library    OperatingSystem
......
*** Test Cases ***
FooTest
    ${group1b}=    Run Keyword    group1b
    BuiltIn.Call Method    ${group1b}    startb

@neil Did you ever solve this? I’m trying to do the exact same thing. I want to be able to namespace my keywords, and haven’t figured out a good way yet.

No, I just ended up prefixing the keyword names with their category name, for example “Foo - Start” and “Baz - Start”. It’s ugly, but it was the least ugly approach.