I'm implementing things in Robot Framework that it does not support by default, including nestable For Loops and, now, While Loops. I'm looking for readability, algorithm, methodology, and/or performance suggestions.
The While Loop is not as advanced as the For loop is yet. I'm eventually looking to implement nesting the While Loop and breaking it on a command, but at present it will take any Robot Framework variable (assuming it's an integer) and loop until it reaches the specified breaking point. The custom keyword Increment plays the part of a line of internal code incrementing the value by a specified number. At present, Increment only supports +
and -
with any integer, but I can easily make it support *
and /
for more advanced increments.
The following code works, but as (still) a relative novice in Python I don't know all the tricks that could be used to optimize this loop structure and logic. The general idea, for those who have not read my other two RobotNest posts, is to allow the user to write nestable For and While loops in Robot Framework.
My basic methodology is as follows:
- The user writes the Loop as a single keyword with a short list of required arguments followed by whatever keywords they want, using
...
at the beginning of the line followed by\\
, once for every inner loop. - The program loops through the list of keywords and re-formats the Loop into a format usable by Robot Framework's
run_keywords
method located under the BuiltIn library, swapping\\
instances forAND
. - The program then runs the keywords through a custom
run_keywords
method (mirrorsBuiltIn().run_keywords
) so that I can control what keywords are executed when and break the loop if I need to. This method loops through the list of keywords and runs them based on their arguments individually. - Within the main loop, the program dynamically writes the index variable (differs in execution between For and While variants) to all keyword arguments that call it.
This methodology seems very inefficient to me. By the end of the program, I've looped through my list of keywords once statically and around 4 or 5 for every iteration of the loop itself. This is why I'm looking for algorithm suggestions.
In addition, I'd like to get away from using \\
to delineate lines, but unless I figure out how the compiler takes Robot Framework's code and says "this is what it equates to in Python", that probably won't be happening.
Here's my code as it currently stands:
from robot.libraries.BuiltIn import BuiltIn
# TODO: Create new types of For Loops
# TODO: Expand 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)
# Clean out the internal variables from previous iterations
self.internal_variables = {}
# If I'm given a range of numbers...
if loop_type == 'IN RANGE':
# 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 while_loop(self, comparison, *keywords):
# Format the input
comparison = str(comparison).replace(" ", "")
# Clean out the internal variables from previous iterations
self.internal_variables = {}
# Create an internal variable to deal with a changing index
self._assign_internal_variable("while_loop_var", comparison[:Loops()._find_bool(comparison)])
comp = comparison
while Loops()._evaluate_boolean_string(comp):
# Format the keywords
k_words = self._format_loop(*keywords)
self._while_var_swap(*k_words)
self._run_keywords(*k_words)
comp = str(self.internal_variables["while_loop_var"]) + str(comparison[Loops()._find_bool(comparison):])
def exit_if(self, condition):
return self._evaluate_boolean_string(condition)
def increment(self, inc):
operator = inc[0]
if self._is_number(inc[1:]):
if operator == '+':
self.internal_variables["while_loop_var"] =\
int(self.internal_variables["while_loop_var"]) + int(inc[1:])
elif operator == '-':
self.internal_variables["while_loop_var"] =\
int(self.internal_variables["while_loop_var"]) - int(inc[1:])
return False
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
def _while_var_swap(self, *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('while_loop_var', str(self.internal_variables["while_loop_var"]))
index = index + 1
return keywords
@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 _eval(arg_1, arg_2):
if (t[2][1] or t[2][2]) and not (t[2][1] and t[2][2]): # If it has either > or < in it, but not both
if t[2][3]: # If it has = in it
if t[2][1]: # If it's >=
return arg_1 >= arg_2
else: # If it's <=
return arg_1 <= arg_2
else:
if t[2][1]: # If it's <
return arg_1 < arg_2
else: # If it's >
return arg_1 > arg_2
elif t[2][4]: # If it's ==
return arg_1 == arg_2
elif t[2][0] and t[2][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 = [[0, 1, 2, 3, 4],
['!', '<', '>', '=', '=='],
[False, False, False, False, False]]
# Find the start of the Boolean expression
start, t = Loops()._find_bool(condition, t)
# Evaluate the expression
return _eval(condition[:start], condition[start + sum(t[2]):])
@staticmethod
def _find_bool(comparison, tin=None):
if tin is None:
t = [[0, 1, 2, 3, 4],
['!', '<', '>', '=', '=='],
[False, False, False, False, False]]
else:
t = tin
# Initialize the starting index as the last index in condition
begin = len(comparison)
# For all of the parameters...
for index in t[0]:
# Find the location of the start of the boolean parameters
temp = comparison.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[2][int(index)] = True
# If the input was bad, return -1 and don't change the t table
if sum(t[2]) > 2 or sum(t[2]) == 0 or begin == len(comparison):
if tin is None:
return -1
else:
return -1, tin
else:
if tin is None:
return begin
else:
return begin, t
@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 _var_swap(old_var, new_var, keywords):
index = 0
# For every line in keywords
for line in keywords:
# Replace the all instance of the oldVar in the string with newVar as a string
keywords[index] = str(line).replace(str(old_var), str(new_var))
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
@staticmethod
def _is_number(s):
try:
float(s)
return True
except ValueError:
return False
And here's the working Robot Framework code that I use to test it:
*** Settings ***
Library ExtendedSelenium2Library
Library Loops.py
*** Variables ***
${blue_squadron} = Blue
${gold_squadron} = Gold
${green_squadron} = Green
${red_squadron} = Red
${test_index_1} = 0
${test_index_2} = 3
*** 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
Test While Loop
While Loop ${test_index_1} < 3
... \\ Log to Console Index: while_loop_var
... \\ Increment +1
While Loop ${test_index_2} > 0
... \\ Log to Console Index: while_loop_var
... \\ Increment -1
*** Keywords ***
Get Blue Squadron
[Return] standing by
Get Green Squadron
[Arguments] ${text}
[Return] standing by
Language: Python 2.7.13, Robot Framework 3.0.2 (Python 2.7.13 on win32)
Dependencies: BuiltIn Library, ExtendedSelenium2Library
:
or::
in a cell by itself? From a human factors perspective, those are easier to type and read. Also, why are you using expressions likeINDEX == 1
rather than${INDEX} == 1
? \$\endgroup\$\`
and I was reading that as a cell into a language that interprets it as an escape character, I had to use two. Recently I've started looking into using theBuiltIn().keyword_should_exist
method to find lines, but I'm running into issues with that so far. I'm using the Python-like expressions because Robot Framework-style variables are compiled to their numbers as they're being sent to Python to be run, while non-Robot Framework syntax can work in the loop. \$\endgroup\$::
in a cell by itself, but for some reason Robot Framework or Python converts:: ::
to::::
, which causes a lot more problems. \$\endgroup\$::
before the next cell? \$\endgroup\$|
and_
. \$\endgroup\$