2
\$\begingroup\$

A common feature of basic loops is the break functionality, allowing the user to specify break points for their loop to stop. I've been creating a keyword to enable nesting loops in Robot Framework. This Nestable For Loop for Robot Framework includes the break functionality.

The basic code for a nestable Robot Framework For Loop is located here. This is a strict upgrade, in that it includes the break functionality in the form of the keyword Exit If.

Exit If requires a single argument: a Boolean expression written in a string. Loops.py includes two methods to work with this kind of expression: _is_boolean_string(string_in) and _evaluate_boolean_string(condition). _is_boolean_string returns True if the expression is a Boolean string (including unsupported Boolean expressions), and _evaluate_boolean_string returns True if the expression would logically evaluate to True.

Because the nature of this code is to be nestable, I need it to be as fast and efficient as possible, so I'm looking for performance and algorithm-based suggestions.

from robot.libraries.BuiltIn import BuiltIn

# TODO: Create new types of For Loops
# TODO: Implement While Loop
# TODO: Create Do-While Loops


class Loops(object):

    def __init__(self):
        self.selenium_lib = BuiltIn().get_library_instance('ExtendedSelenium2Library')
        self.internal_variables = {}

    def for_loop(self, loop_type, start, end, index_var, *keywords):
        #   Format the keywords
        keywords = self._format_loop(*keywords)
        #   If I'm given a range of numbers...
        if loop_type == 'IN RANGE':
            #   Clean out the internal variables from previous iterations
            self.internal_variables = {}

            #   This is the actual looping part
            for loop_iteration in range(int(start), int(end)):
                keyword_set = self._index_var_swap(loop_iteration, index_var, *keywords)
                #   If it's a one-keyword list with no arguments, then I can use the fastest possible keyword to run it
                if len(keyword_set) == 1:
                    BuiltIn().run_keyword(keyword_set)
                #   If it's a one-keyword list with arguments, then I can use a faster keyword to run it
                elif 'AND' not in keyword_set:
                    #   If the keyword isn't Exit If, then I can just run it normally.
                    if keyword_set[0].lower() != 'exit if':
                        BuiltIn().run_keyword(*keyword_set)
                    #   If the keyword is Exit If, then I need to evaluate the keyword differently..
                    elif BuiltIn().run_keyword(*keyword_set):
                        break
                #   If it's a multiple-keyword list, then I have to use Run Keywords
                else:
                    temp = self._run_keywords(*keyword_set)
                    if not temp:
                        break

    def exit_if(self, condition):
        return self._evaluate_boolean_string(condition)

    def _format_loop(self, *keywords):
        keywords = list(keywords)   # I need to format the keywords as a list.
        changed = False             # Whether or not I changed anything in the previous iteration.
        index = 0                   # The item index I'm at in the list of keywords
        del_list = []               # The list of items I need to delete
        swap_list = []              # The list of items i need to swap to AND for the use of Run Keywords

        def _new_variable():
            #   Default to a variable declaration of 'name='
            t = 1
            #   If my variable declaration is 'name ='
            if x[-2:] == ' =':
                #   Reflect that in the value of t
                t = 2

            #   Count the number of cells until the end of the line
            length = self._deliminator_search(index, x, *keywords)

            if length == 3 and not BuiltIn().run_keyword_and_return_status("Keyword Should Exist", keywords[index + 1]):
                #   If I'm assigning a value to my variable
                self._assign_internal_variable(x[:-t], str(keywords[index + 1]))

            elif length == 3:
                #   If I'm assigning the result of a keyword without any arguments
                self._assign_internal_variable_to_keyword(keywords[index][:-t], str(keywords[index + 1]))
            else:
                #   If I'm assigning the result of a keyword with arguments
                self._assign_internal_variable_to_keyword(keywords[index][:-t], keywords[index + 1],
                                                          keywords[index + 2:index + (length - 1)])

            #   Add the variable declaration code to the delete list.
            del_list.extend(range(index - 1, index + length))

        #   For each argument
        for x in keywords:
            #   Format it to a string
            x = str(x)
            #   Assign new variables
            if x[-1:] == '=':
                _new_variable()

            #   If the previous element was not changed...
            if not changed:
                #   If the current item is not the last one on the list...
                if x != len(keywords) - 1:
                    #   If the current item is a deliminator...
                    if x == '\\':
                        #   If the next item is a deliminator, delete this item and set changed to True
                        if keywords[int(index) + 1] == '\\':
                            del_list.append(index)
                            changed = True
                        #   If the next item is not a deliminator...
                        else:
                            #   If this isn't the first deliminator on the list, swap it to an 'AND'
                            if index != 0:
                                swap_list.append(index)
                                changed = True
                            #   If this deliminator is in position index=0, just delete it
                            else:
                                del_list.append(index)
                                changed = True
                    #   If the current element is not a deliminator, then I don't need to touch anything.
                #   If the current element is the last one, then I don't need to touch anything
            #   If the previous element was changed, then I don't need to "change" this one...
            elif changed:
                changed = False
                #   ...but if it's a deliminator then I do need to set it up for the inner for loop it means.
                if keywords[index] == '\\':
                    keywords[index] = keywords[index]*2
            index = index + 1   # Advance the index

        # These actually do the swapping and deleting
        for thing in swap_list:
            keywords[thing] = 'AND'
        del_list.reverse()
        for item in del_list:
            del keywords[item]

        # I also need to activate my variables for this set of keywords to run.
        keywords = self._activate_variables(*keywords)

        return keywords

    def _assign_internal_variable(self, variable_name, assignment):
        # This keyword works like any other keyword so that it can be activated by BuiltIn.run_keywords
        self.internal_variables[variable_name] = assignment

    def _assign_internal_variable_to_keyword(self, variable_name, keyword, *arguments):
        # Uses assign_internal_variable to simplify code.
        self._assign_internal_variable(variable_name, BuiltIn().run_keyword(keyword, *arguments))

    def _activate_variables(self, *keywords):
        #   Initialize variables
        keywords = list(keywords)   # Cast keywords as a List
        index = 0                   # The index of the keyword I'm looking at

        #   For each keyword
        for keyword in keywords:
            keyword = str(keyword)  # Cast keyword as a String
            assignment = False      # Whether or not the found variable name is in a variable assignment
            for key in self.internal_variables.keys():
                key = str(key)      # Cast key as a String
                #   If I can find the key in the keyword and it's not an assignment...
                if keyword.find(key) > -1 and not assignment:
                    #   ...replace the text of the key in the keyword.
                    keywords[index] = keyword.replace(str(key), str(self.internal_variables[key]))
                #   If the keyword I'm looking at is an assignment...
                if keyword.lower() == 'assign internal variable'\
                        and keyword.lower() != 'assign internal variable to keyword':
                    #   ...then my next keyword is going to definitely be a known variable, so I don't want to touch it.
                    assignment = True
                #   If the keyword I'm looking at is not an assignment...
                else:
                    #   ...set assignment to False just in case the previous one happened to be an assignment.
                    assignment = False
            index = index + 1   # Advance the index
            #   NOTE: Replaces the EXACT text, even if it's in another keyword or variable, so be very careful
        return keywords     # Return the list of keywords to be used in the format loop

    @staticmethod
    def _is_boolean_string(string_in):
        # For all of the possible Boolean parameters...
        for param in ['!', '<', '>', '=', '==']:
            # Return whether or not the parameter is in the string.
            if str(param) in str(string_in):
                return True
        return False

    @staticmethod
    def _evaluate_boolean_string(condition):
        def _find_bool():
            begin = len(condition)  # Initialize the starting index as the last index in condition
            # For all of the parameters...
            for index in t[2]:
                # Find the location of the start of the boolean parameters
                temp = condition.find(str(t[1][int(index)]))
                # If the location exists and is less than start...
                if temp <= begin and temp != -1:
                    # Set start to the location
                    begin = temp
                    t[0][int(index)] = True
            # If the input was bad, return -1
            if sum(t[0]) > 2 or sum(t[0]) == 0 or begin == len(condition):
                return -1
            else:
                return begin

        def _eval(arg_1, arg_2):
            if (t[0][1] or t[0][2]) and not (t[0][1] and t[0][2]):  # If it has either > or < in it, but not both
                if t[0][3]:  # If it has = in it
                    if t[0][1]:  # If it's >=
                        return arg_1 >= arg_2
                    else:  # If it's <=
                        return arg_1 <= arg_2
                else:
                    if t[0][1]:  # If it's >
                        return arg_1 > arg_2
                    else:  # If it's <
                        return arg_1 < arg_2
            elif t[0][4]:  # If it's ==
                return arg_1 == arg_2
            elif t[0][0] and t[0][3]:  # If it's !=
                return arg_1 != arg_2
            else:  # In case of Tester
                return False

        # Cast the condition as a string with no whitespaces
        condition = str(condition).replace(" ", "")
        # Initialize the t-table with default values
        t = [[False, False, False, False, False],
             ['!', '<', '>', '=', '=='],
             [0, 1, 2, 3, 4]]
        # Find the start of the Boolean expression
        start = _find_bool()
        # Evaluate the expression
        return _eval(condition[:start], condition[start + sum(t[0]):])

    @staticmethod
    def _run_keywords(*keys):
        #   Find the end of the current keyword
        def _and_search(start, key='no key yet'):
            # I'm starting with a false key that is never 'AND'
            and_index = 1       # I never want to start on an 'AND'
            #   While the current key isn't 'AND' and I'm not at the end of the list of keywords...
            while key != 'AND' and and_index + start != len(key_list):
                #   Set the current key equal to the next key in the list.
                key = key_list[int(start) + and_index]
                and_index = and_index + 1   # Advance the index

            #   If the final key is an 'AND'...
            if key == 'AND':
                return and_index - 1    # Return the keyword length minus the 'AND'
            #   Otherwise...
            else:
                return and_index    # Return the keyword length

        def _split_keyword_list():
            first = True    # We always start at the first cell of a keyword/argument set.
            index = 0       # The item index I'm at in the list of keywords
            #   For each word in the list of keywords/arguments...
            for word in key_list:
                #   If it's the first word...
                if first:
                    #   Append the keyword
                    keywords.append(key_list[int(index):(index + _and_search(index))])
                    first = False   # Set first to False
                #   If it's any other word I don't need to append it, but...
                else:
                    #   If it's the last word in the keyword...
                    if _and_search(index, word) == 0:
                        #   The next keyword must be the first.
                        first = True
                index = index + 1  # Advance the index

        # I need to format the keywords as a list and instantiate the array of keywords as an empty array.
        key_list, keywords = list(keys), []

        #   Split list into keyword/arg sets
        _split_keyword_list()

        #   For each key/arg item in the list...
        for keyword in keywords:
            #   Run the keyword with its arguments
            if keyword[0].lower() != 'exit if':
                BuiltIn().run_keyword(keyword[0], *keyword[1:])
            elif Loops()._is_boolean_string(keyword[1]) and BuiltIn().run_keyword(keyword[0], *keyword[1:]):
                return False
        return True

    @staticmethod
    def _index_var_swap(loop_iteration, index_var, *keywords):
        #   Format the keywords as a list for iteration
        keywords = list(keywords)
        index = 0
        #   For every line in keywords
        for line in keywords:
            #   Replace all instances of the index_var in the string with the loop iteration as a string
            keywords[index] = str(line).replace(str(index_var), str(loop_iteration))
            index = index + 1
        return keywords

    @staticmethod
    def _deliminator_search(start, keyword, *keywords):
        index = 0
        while keyword != '\\' and keyword != '\\\\':
            keyword = keywords[int(start) + index]
            index = index + 1
        return index

And the Robot Framework code that tests it:

*** Variables ***
${blue_squadron} =      Blue
${gold_squadron} =      Gold
${green_squadron} =     Green
${red_squadron} =       Red

*** Test Cases ***
Test For Loop IN RANGE
    For Loop    IN RANGE    0   1   INDEX0
    ...     \\  For Loop    IN RANGE    1   6   INDEX1
    ...     \\  \\  {standing_by}=      standing by
    ...     \\  \\  Run Keyword If      INDEX1 == 1     Log to Console  This is ${red_squadron} Leader standing by
    ...     \\  \\  Run Keyword Unless  INDEX1 == 1     Log to Console  ${red_squadron} INDEX1 {standing_by}
    ...     \\  For Loop    IN RANGE    1   6   INDEX2
    ...     \\  \\  standing_by_2 =     standing by
    ...     \\  \\  Run Keyword If      INDEX2 == 1     Log to Console  This is ${gold_squadron} Leader standing by
    ...     \\  \\  Run Keyword Unless  INDEX2 == 1     Log to Console  ${gold_squadron} INDEX2 standing_by_2
    ...     \\  For Loop    IN RANGE    1   6   INDEX3
    ...     \\  \\  standing_by_3=      Get Blue Squadron
    ...     \\  \\  Run Keyword If      INDEX3 == 1     Log to Console  This is ${blue_squadron} Leader standing by
    ...     \\  \\  Run Keyword Unless  INDEX3 == 1     Log to Console  ${blue_squadron} INDEX3 standing_by_3
    ...     \\  For Loop    IN RANGE    1   6   INDEX4
    ...     \\  \\  standing_by_4 =     Get Green Squadron   null input
    ...     \\  \\  Run Keyword If      INDEX4 == 1     Log to Console  This is ${green_squadron} Leader standing by
    ...     \\  \\  Run Keyword Unless  INDEX4 == 1     Log to Console  ${green_squadron} INDEX4 standing_by_4

Test IN RANGE Edge Case 1 - Single Keyword with Single Argument
    For Loop    IN RANGE    0   1   INDEX0
    ...     \\  Log to Console  testlog

Test For Loop Exit
    For Loop    IN RANGE    0   3   INDEX0
    ...     \\  Log to Console  INDEX0
    ...     \\  Exit If     INDEX0 == 1

*** Keywords ***
Get Blue Squadron
    [Return]    standing by

Get Green Squadron
    [Arguments]     ${text}
    [Return]        standing by
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Browse other questions tagged or ask your own question.