Skip to content

Commit

Permalink
added INFER option to WAIT WITH statements and added tests and docume…
Browse files Browse the repository at this point in the history
…ntation.
  • Loading branch information
FIREdog5 committed May 31, 2021
1 parent ba26d7c commit 1dc0c02
Show file tree
Hide file tree
Showing 5 changed files with 89 additions and 42 deletions.
104 changes: 68 additions & 36 deletions shepherd/Tester.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import queue
import time
from typing import Any
from keyword import iskeyword
from Utils import *
from LCM import *

Expand Down Expand Up @@ -68,15 +69,15 @@ def parse_header(header):
def execute_python(script):
"""
A helper function that executes a python expression in the context of the
local scipt enviroment.
local scipt environment.
"""
global LOCALVARS
exec(script, LOCALVARS)


def evaluate_python(token):
"""
A helper function that evaluates a token against the local scipt enviroment.
A helper function that evaluates a token against the local scipt environment.
"""
global LOCALVARS
return eval(token, LOCALVARS)
Expand Down Expand Up @@ -470,21 +471,14 @@ def with_function_wait(expression, data):
"""
Takes in a WITH statement found in a WAIT statement, and the data that was
present in the header that triggered the processing of this WAIT statement,
and modifies the local script enviroment accordingly.
and modifies the local script environment accordingly.
Also handles syntax checking of the WITH statement.
"""
parts = expression.split('=')
if len(parts) != 2:
raise Exception('WITH statement: {} is invalid.'.format(expression))
parts[0] = remove_outer_spaces(parts[0])
parts[1] = remove_outer_spaces(parts[1])
if parts[1][0] != "'" or parts[1][-1] != "'":
raise Exception(
"expected second argument of WITH statement: {} to be wrapped in '.".format(expression))
parts = parse_with_function_wait(expression)
ex = None
try:
global LOCALVARS
LOCALVARS[parts[0]] = data[parts[1][1:-1]]
LOCALVARS[parts[0]] = data[parts[1]]
except ValueError:
ex = Exception("{} is undefined".format(parts[0]))
except Exception:
Expand All @@ -493,6 +487,46 @@ def with_function_wait(expression, data):
if ex:
raise ex

def parse_with_function_wait(expression):
"""
Helper function used in a few places to parse and syntax check a WITH
statement in a WAIT statement.
Returns a tuple of form (var_name, data_key)
"""
parts = expression.split('=')
if len(parts) != 2:
raise Exception('WITH statement: {} is invalid.'.format(expression))
parts[0] = remove_outer_spaces(parts[0])
parts[1] = remove_outer_spaces(parts[1])
if parts[1][0] != "'" or parts[1][-1] != "'":
raise Exception(
"expected second argument of WITH statement: {} to be wrapped in '.".format(expression))
return (parts[0], parts[1][1:-1])

def with_infer_function(withs, data):
"""
Called when a WITH statement found in a WAIT statement that uses an INFER is
encountered, and takes in all WITH statements as well as the recieved data.
Modifies the local script environment to store any unused data keys (not
found in other WAIT statements) in variables of the exact same name.
Ensures that valid python variable naming conventions are used.
"""

def is_valid_variable_name(name):
"""
Quick helper function to check if variable names are valid.
"""
if name[0] == '_':
return False
return name.isidentifier() and not iskeyword(name)

global LOCALVARS
with_keys = [parse_with_function_wait(w)[1] for w in withs if not 'INFER' in w]
for var in data.keys():
if not var in with_keys:
if not is_valid_variable_name(var):
raise Exception(f"{var} is not a valid python variable name, and therefore cannot be used in INFER. Use WITH <valid_name> = '{var}' to specify a valid name.")
LOCALVARS[var] = data[var]

def with_function_emit(expression, data):
"""
Expand Down Expand Up @@ -550,15 +584,22 @@ def check_received_headers():
def execute_header(header, data):
"""
Takes in a header data structure and the data from the LCM call and will
modify the local enviroment accordingly.
modify the local environment accordingly.
Processes all SET and WITH statements in the header individually, and with
no guarantee on order.
In this implementation, all WITH statements are processed first, from
left to right, and then all SET statements, from left to right.
"""
global LOCALVARS
inferred = False
for with_statement in header['header']['with_statements']:
with_function_wait(with_statement, data)
if with_statement == 'INFER':
if inferred:
continue
with_infer_function(header['header']['with_statements'], data)
inferred = True
else:
with_function_wait(with_statement, data)
for set_statement in header['header']['set_statements']:
local_arg = remove_outer_spaces(set_statement.split('=')[0])
python_expression = remove_outer_spaces(set_statement.split('=')[1])
Expand Down Expand Up @@ -611,33 +652,24 @@ def start():
if TARGET == 'unassigned':
raise Exception("READ needs to be called before the first WAIT.")

# def run():
# i = 0
# while True:
# time.sleep(.5)
# print(f"running! {i}")
# i += 1

# test_thread = threading.Thread(target=run)
# test_thread.start()

while True:
time.sleep(0.1)
payload = EVENTS.get(True)
accept_header(payload)
# a quick try block to ensure that errors in WAIT statements get line #
ex = None
try:
accept_header(payload)
# pylint: disable=broad-except
except Exception as exx:
ex = Exception(
'an error occured on line {}:\n{}'.format(LINE + 1, exx))
finally:
if ex:
raise ex
if(check_received_headers()):
CURRENT_HEADERS = []
run_until_wait()


# class worker(Thread):
# def run(self):
# for i in range(0, 11):
# print(x)
# time.sleep(1)
# worker().start()


def main():
"""
Reads the whole file in and places it in a python list on the heap.
Expand Down Expand Up @@ -684,8 +716,8 @@ def main():
WAITING = False
"""
A dictionary that is populated by the script's execution. This is used as an
enviroment for python execution in RUN and in the WAIT, SET, and PRINTP
statements. Unlike normal python enviroments, there are no further frames opened
environment for python execution in RUN and in the WAIT, SET, and PRINTP
statements. Unlike normal python environments, there are no further frames opened
for code blocks, and this is a facsimile of dynamic typing.
"""
LOCALVARS = {}
Expand Down
10 changes: 4 additions & 6 deletions shepherd/tests/TESTING_DOCS.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,17 +60,13 @@ Usage: `PRINTP <python expression>`

### SLEEP

The SLEEP statement is used in order to pause the execution of the .shepherd interpreter for a specified amount of time. Any LCM messages received
while the interpreter is paused will still be recorded and may be processed by the next WAIT statement that the interpreter encounters. The sleep
time may be a decimal, and is in terms of seconds. SLEEP may take a python expression as an argument, so long as it evaluates to a float.
The SLEEP statement is used in order to pause the execution of the .shepherd interpreter for a specified amount of time. Any LCM messages received while the interpreter is paused will still be recorded and may be processed by the next WAIT statement that the interpreter encounters. The sleep time may be a decimal, and is in terms of seconds. SLEEP may take a python expression as an argument, so long as it evaluates to a float.

Usage: `SLEEP <time / python expression>`

### DISCARD

The DISCARD statement clears the LCM / YDL of any messages that have been received so far, but have not been processed, ensuring that they will not
be processed. This is helpful after a SLEEP statement, to ensure that any messages that were received during the sleep would be ignored, if that is
the desired functionality.
The DISCARD statement clears the LCM / YDL of any messages that have been received so far, but have not been processed, ensuring that they will not be processed. This is helpful after a SLEEP statement, to ensure that any messages that were received during the sleep would be ignored, if that is the desired functionality.

Usage: `DISCARD`

Expand Down Expand Up @@ -134,6 +130,8 @@ The WAIT statement is used to pause code execution until a specific LCM message

- WITH can then be specified, to store arguments from the header into the namespace. This must follow the target specified by FROM. The WITH statement looks something like this, `WITH argument = 'argument in header'`. The name of the argument in the header must be surrounded by single quotes. If the argument is not present in the header, an error will be thrown.

- WITH INFER is a special kind of WITH statement which instructs the testing script to place all of the data found in the LCM message into the namespace with the variable name inferred as the name of the key in the data dictionary from the header. Any key in the data dictionary that is used in another WITH statement will not be inferred, and therefore will not be copied into the namespace a second time. The WITH INFER statement does not need to go in any particular place and will look both ahead and behind when calculating what statements to infer. This means it can go anywhere a normal WITH statement can go. It is important to note that any header keys that are inferred that are not valid python variable names, or that begin with an underscore will cause an error. Therefore you must explicitly name and store any such key values in their own WITH statements in order to give them valid names and cause WITH INFER to skip over them.

- SET can then be specified, which will execute a line of python code when the header is received. The SET statement looks something like this, `SET test = True`.

- Any number or WITH and SET statements can follow the FROM statement, and they may be arranged in any order (WITH does not need to come first). Likewise, there are no guarantees about the order that the WITH and SET statements will be executed in, so they should not rely on the execution of one another.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
TEST with_infer_client.shepherd
TEST with_infer_server.shepherd
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
READ LCM_TARGETS.SCOREBOARD
RUN a = 1
RUN b = 1
RUN c = 1
WAIT SCOREBOARD_HEADER.ALL_INFO FROM LCM_TARGETS.SCOREBOARD WITH c2 = 'c' WITH INFER WITH a2 = 'a'
FAIL a != 1
FAIL c != 1
FAIL b != 2
WAIT SCOREBOARD_HEADER.ALL_INFO FROM LCM_TARGETS.SCOREBOARD WITH INFER SET d = 4
FAIL a != 3
FAIL c != 3
FAIL b != 3
PASS
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
EMIT SCOREBOARD_HEADER.ALL_INFO TO LCM_TARGETS.SCOREBOARD WITH 'a' = 2 WITH 'b' = 2 WITH 'c' = 2
EMIT SCOREBOARD_HEADER.ALL_INFO TO LCM_TARGETS.SCOREBOARD WITH 'a' = 3 WITH 'b' = 3 WITH 'c' = 3

1 comment on commit 1dc0c02

@FIREdog5
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

#47

Please sign in to comment.