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