Skip to content

Commit

Permalink
Updates for error handling
Browse files Browse the repository at this point in the history
DataDrivenLearning is now guaranteed to receive valid paths
Parsing Plugins is now with required parsing
Using ast to check and ensure integrity of dict from config
  ValueError when not a dict or empty dict
  FileNotFoundError when path for Plugin is invalid
  • Loading branch information
asgibson committed Sep 14, 2023
1 parent 6eb773d commit a794f99
Show file tree
Hide file tree
Showing 3 changed files with 198 additions and 71 deletions.
15 changes: 5 additions & 10 deletions onair/src/data_driven_components/data_driven_learning.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,16 +21,11 @@ def __init__(self, headers, _ai_plugins={}):
self.headers = headers
self.ai_constructs = []
for module_name in list(_ai_plugins.keys()):
try:
spec = importlib.util.spec_from_file_location(module_name, _ai_plugins[module_name])
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
self.ai_constructs.append(module.Plugin(module_name,headers))
except AttributeError:
raise Exception(f'Path passed for module {module_name} is not pointing to a usable module file. Double check that the config points to a Python file with a class Plugin.')



spec = importlib.util.spec_from_file_location(module_name, _ai_plugins[module_name])
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
self.ai_constructs.append(module.Plugin(module_name,headers))

def update(self, curr_data, status):
input_data = curr_data
output_data = status_to_oneHot(status)
Expand Down
31 changes: 17 additions & 14 deletions onair/src/run_scripts/execution_engine.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,48 +64,51 @@ def __init__(self, config_file='', run_name='', save_flag=False):
self.setup_sim()

def parse_configs(self, config_filepath):
# print("Using config file: {}".format(config_filepath))

config = configparser.ConfigParser()

if len(config.read(config_filepath)) == 0:
raise FileNotFoundError(f"Config file at '{config_filepath}' could not be read.")

try:
## Sort Required Data: Telementry Data & Configuration
## Parse Required Data: Telementry Data & Configuration
self.dataFilePath = config['DEFAULT']['TelemetryDataFilePath']
self.metadataFilePath = config['DEFAULT']['TelemetryMetadataFilePath']
self.metaFiles = config['DEFAULT']['MetaFiles'] # Config for vehicle telemetry
self.telemetryFiles = config['DEFAULT']['TelemetryFiles'] # Vehicle telemetry data

## Sort Required Data: Names
## Parse Required Data: Names
self.parser_file_name = config['DEFAULT']['ParserFileName']
self.parser_name = config['DEFAULT']['ParserName']
self.sim_name = config['DEFAULT']['SimName']

## Parse Required Data: Plugin name to path dict
config_plugin_list = config['DEFAULT']['PluginList']
ast_plugin_list = ast.parse(config_plugin_list, mode='eval')
if isinstance(ast_plugin_list.body, ast.Dict) and len(ast_plugin_list.body.keys) > 0:
temp_plugin_list = ast.literal_eval(config_plugin_list)
else:
raise ValueError(f"{config_plugin_list} is an invalid PluginList. It must be a dict of at least 1 key/value pair.")
for plugin_name in temp_plugin_list.values():
if not(os.path.exists(plugin_name)):
raise FileNotFoundError(f"In config file '{config_filepath}', path '{plugin_name}' does not exist or is formatted incorrectly.")
self.plugin_list = temp_plugin_list
except KeyError as e:
new_message = f"Config file: '{config_filepath}', missing key: {e.args[0]}"
raise KeyError(new_message) from e

## Sort Optional Data: Flags
## Parse Optional Data: Flags
self.IO_Flag = config['RUN_FLAGS'].getboolean('IO_Flag')
self.Dev_Flag = config['RUN_FLAGS'].getboolean('Dev_Flag')
self.SBN_Flag = config['RUN_FLAGS'].getboolean('SBN_Flag')
self.Viz_Flag = config['RUN_FLAGS'].getboolean('Viz_Flag')

## Sort Optional Data: Benchmarks
## Parse Optional Data: Benchmarks
try:
self.benchmarkFilePath = config['DEFAULT']['BenchmarkFilePath']
self.benchmarkFiles = config['DEFAULT']['BenchmarkFiles'] # Vehicle telemetry data
self.benchmarkIndices = config['DEFAULT']['BenchmarkIndices']
except:
pass
## Plugins
temp_plugin_list = ast.literal_eval(config['DEFAULT']['PluginList'])
if len(temp_plugin_list.keys()) == 0:
raise AttributeError(f'No plugins have been specified in the config. Please specify a plugin in the PluginList dictionary with key of form plugin_name and value of form path/to/plugin')
for plugin_name in temp_plugin_list.keys():
if not(os.path.exists(temp_plugin_list[plugin_name])):
raise FileNotFoundError(f'Path given for {plugin_name} does not exist or is formatted incorrectly.')
self.plugin_list = temp_plugin_list


def parse_data(self, parser_name, parser_file_name, dataFilePath, metadataFilePath, subsystems_breakdown=False):
Expand Down
223 changes: 176 additions & 47 deletions test/onair/src/run_scripts/test_execution_engine.py
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,152 @@ def test_ExecutionEngine_parse_configs_raises_FileNotFoundError_when_config_cann
# Assert
assert e_info.match(f"Config file at '{arg_bad_config_filepath}' could not be read.")

def test_ExecutionEngine_parse_configs_raises_KeyError_with_config_file_info_when_a_required_key_is_not_in_config(mocker):
# Arrange
arg_config_filepath = MagicMock()

fake_default = {'TelemetryDataFilePath':MagicMock(),
'TelemetryMetadataFilePath':MagicMock(),
'MetaFiles':MagicMock(),
'TelemetryFiles':MagicMock(),
'BenchmarkFilePath':MagicMock(),
'BenchmarkFiles':MagicMock(),
'BenchmarkIndices':MagicMock(),
'ParserFileName':MagicMock(),
'ParserName':MagicMock(),
'SimName':MagicMock(),
'PluginList':MagicMock()
}
required_keys = [item for item in list(fake_default.keys()) if 'Benchmark' not in item]
missing_key = pytest.gen.choice(required_keys)
del fake_default[missing_key]
fake_run_flags = MagicMock()
fake_dict_for_Config = {'DEFAULT':fake_default, 'RUN_FLAGS':fake_run_flags}
fake_config = MagicMock()
fake_config.__getitem__.side_effect = fake_dict_for_Config.__getitem__
fake_config_read_result = MagicMock()
fake_config_read_result.__len__.return_value = 1

cut = ExecutionEngine.__new__(ExecutionEngine)

mocker.patch(execution_engine.__name__ + '.configparser.ConfigParser', return_value=fake_config)
mocker.patch.object(fake_config, 'read', return_value=fake_config_read_result)

# Act
with pytest.raises(KeyError) as e_info:
cut.parse_configs(arg_config_filepath)

# Assert
assert e_info.match(f"Config file: '{arg_config_filepath}', missing key: {missing_key}")

def test_ExecutionEngine_parse_configs_raises_ValueError_when_PluginList_from_config_is_not_dict(mocker):
# Arrange
arg_config_filepath = MagicMock()

fake_config = MagicMock()
fake_plugin_list = MagicMock()
fake_plugin_list.body = MagicMock()
fake_default_item = MagicMock()
fake_config.__getitem__.return_value = fake_default_item
fake_default_item.__getitem__.side_effect = [None] * 7 + [fake_plugin_list]
fake_config_read_result = MagicMock()
fake_config_read_result.__len__.return_value = 1

cut = ExecutionEngine.__new__(ExecutionEngine)

mocker.patch(execution_engine.__name__ + '.configparser.ConfigParser', return_value=fake_config)
mocker.patch.object(fake_config, 'read', return_value=fake_config_read_result)
mocker.patch(execution_engine.__name__ + '.ast.parse', return_value=fake_plugin_list)
mocker.patch(execution_engine.__name__ + '.isinstance', return_value=False)

# Act
with pytest.raises(ValueError) as e_info:
cut.parse_configs(arg_config_filepath)

# Assert
assert e_info.match(f"{fake_plugin_list} is an invalid PluginList. It must be a dict of at least 1 key/value pair.")
assert execution_engine.ast.parse.call_count == 1
assert execution_engine.ast.parse.call_args_list[0].args == (fake_plugin_list,)
assert execution_engine.ast.parse.call_args_list[0].kwargs == {'mode':'eval'}
assert execution_engine.isinstance.call_count == 1
assert execution_engine.isinstance.call_args_list[0].args == (fake_plugin_list.body, execution_engine.ast.Dict, )

def test_ExecutionEngine_parse_configs_raises_ValueError_when_PluginList_from_config_is_empty_dict(mocker):
# Arrange
arg_config_filepath = MagicMock()

fake_config = MagicMock()
fake_plugin_list = MagicMock()
fake_plugin_list.body = MagicMock()
fake_plugin_list.body.keys = MagicMock()
fake_plugin_list.body.keys.__len__.return_value = 0
fake_default_item = MagicMock()
fake_config.__getitem__.return_value = fake_default_item
fake_default_item.__getitem__.side_effect = [None] * 7 + [fake_plugin_list]
fake_config_read_result = MagicMock()
fake_config_read_result.__len__.return_value = 1


cut = ExecutionEngine.__new__(ExecutionEngine)

mocker.patch(execution_engine.__name__ + '.configparser.ConfigParser', return_value=fake_config)
mocker.patch.object(fake_config, 'read', return_value=fake_config_read_result)
mocker.patch(execution_engine.__name__ + '.ast.parse', return_value=fake_plugin_list)
mocker.patch(execution_engine.__name__ + '.isinstance', return_value=True)

# Act
with pytest.raises(ValueError) as e_info:
cut.parse_configs(arg_config_filepath)

# Assert
assert e_info.match(f"{fake_plugin_list} is an invalid PluginList. It must be a dict of at least 1 key/value pair.")
assert execution_engine.ast.parse.call_count == 1
assert execution_engine.ast.parse.call_args_list[0].args == (fake_plugin_list,)
assert execution_engine.ast.parse.call_args_list[0].kwargs == {'mode':'eval'}
assert execution_engine.isinstance.call_count == 1
assert execution_engine.isinstance.call_args_list[0].args == (fake_plugin_list.body, execution_engine.ast.Dict, )

def test_ExecutionEngine_parse_configs_raises_FileNotFoundError_when_given_plugin_path_is_not_valid(mocker):
# Arrange
arg_config_filepath = MagicMock()

fake_config = MagicMock()
fake_plugin_list = MagicMock()
fake_plugin_list.body = MagicMock()
fake_plugin_list.body.keys = MagicMock()
fake_plugin_list.body.keys.__len__.return_value = 1
fake_temp_plugin_list = MagicMock()
fake_plugin_name = MagicMock()
fake_temp_iter = iter([fake_plugin_name])
fake_default_item = MagicMock()
fake_config.__getitem__.return_value = fake_default_item
fake_default_item.__getitem__.side_effect = [None] * 7 + [fake_plugin_list]
fake_config_read_result = MagicMock()
fake_config_read_result.__len__.return_value = 1


cut = ExecutionEngine.__new__(ExecutionEngine)

mocker.patch(execution_engine.__name__ + '.configparser.ConfigParser', return_value=fake_config)
mocker.patch.object(fake_config, 'read', return_value=fake_config_read_result)
mocker.patch(execution_engine.__name__ + '.ast.parse', return_value=fake_plugin_list)
mocker.patch(execution_engine.__name__ + '.isinstance', return_value=True)
mocker.patch(execution_engine.__name__ + '.ast.literal_eval', return_value=fake_temp_plugin_list)
mocker.patch.object(fake_temp_plugin_list, 'values', return_value=fake_temp_iter)
mocker.patch(execution_engine.__name__ + '.os.path.exists', return_value=False)
# Act
with pytest.raises(FileNotFoundError) as e_info:
cut.parse_configs(arg_config_filepath)

# Assert
assert e_info.match(f"In config file '{arg_config_filepath}', path '{fake_plugin_name}' does not exist or is formatted incorrectly.")
assert execution_engine.ast.parse.call_count == 1
assert execution_engine.ast.parse.call_args_list[0].args == (fake_plugin_list,)
assert execution_engine.ast.parse.call_args_list[0].kwargs == {'mode':'eval'}
assert execution_engine.isinstance.call_count == 1
assert execution_engine.isinstance.call_args_list[0].args == (fake_plugin_list.body, execution_engine.ast.Dict, )


def test_ExecutionEngine_parse_configs_sets_all_items_without_error(mocker):
# Arrange
arg_config_filepath = MagicMock()
Expand All @@ -133,9 +279,16 @@ def test_ExecutionEngine_parse_configs_sets_all_items_without_error(mocker):
'ParserFileName':MagicMock(),
'ParserName':MagicMock(),
'SimName':MagicMock(),
'PluginList':dict(MagicMock())
'PluginList':"{fake_name:fake_path}"
}
fake_run_flags = MagicMock()
fake_plugin_list = MagicMock()
fake_plugin_list.body = MagicMock()
fake_plugin_list.body.keys = MagicMock()
fake_plugin_list.body.keys.__len__.return_value = 1
fake_temp_plugin_list = MagicMock()
fake_plugin_name = MagicMock()
fake_temp_iter = iter([fake_plugin_name])
fake_dict_for_Config = {'DEFAULT':fake_default, 'RUN_FLAGS':fake_run_flags}
fake_config = MagicMock()
fake_config.__getitem__.side_effect = fake_dict_for_Config.__getitem__
Expand All @@ -145,15 +298,26 @@ def test_ExecutionEngine_parse_configs_sets_all_items_without_error(mocker):
fake_Dev_flags = MagicMock()
fake_SBN_flags = MagicMock()
fake_Viz_flags = MagicMock()
fake_plugin_list = dict(MagicMock())

fake_plugin_dict= MagicMock()
fake_keys = MagicMock()
fake_plugin = MagicMock()
fake_path = MagicMock()

fake_keys.__len__.return_value = 1
fake_keys.__iter__.return_value = iter([str(fake_plugin)])

cut = ExecutionEngine.__new__(ExecutionEngine)

mocker.patch(execution_engine.__name__ + '.configparser.ConfigParser', return_value=fake_config)
mocker.patch.object(fake_config, 'read', return_value=fake_config_read_result)
mocker.patch.object(fake_run_flags, 'getboolean', side_effect=[fake_IO_flags, fake_Dev_flags, fake_SBN_flags, fake_Viz_flags])
mocker.patch('ast.literal_eval',return_value=fake_plugin_list)
mocker.patch(execution_engine.__name__ + '.ast.parse', return_value=fake_plugin_list)
mocker.patch(execution_engine.__name__ + '.isinstance', return_value=True)
mocker.patch(execution_engine.__name__ + '.ast.literal_eval', return_value=fake_temp_plugin_list)
mocker.patch.object(fake_temp_plugin_list, 'values', return_value=fake_temp_iter)
mocker.patch(execution_engine.__name__ + '.os.path.exists', return_value=True)
mocker.patch.object(fake_plugin_dict, 'keys', return_value=fake_keys)
mocker.patch.object(fake_plugin_dict, '__getitem__', return_value=fake_path)

# Act
cut.parse_configs(arg_config_filepath)
Expand All @@ -171,7 +335,7 @@ def test_ExecutionEngine_parse_configs_sets_all_items_without_error(mocker):
assert cut.parser_file_name == fake_default['ParserFileName']
assert cut.parser_name == fake_default['ParserName']
assert cut.sim_name == fake_default['SimName']
assert cut.plugin_list == fake_default['PluginList']
assert cut.plugin_list == fake_temp_plugin_list
assert fake_run_flags.getboolean.call_count == 4
assert fake_run_flags.getboolean.call_args_list[0].args == ('IO_Flag', )
assert cut.IO_Flag == fake_IO_flags
Expand All @@ -194,7 +358,7 @@ def test_ExecutionEngine_parse_configs_bypasses_benchmarks_when_access_raises_er
'ParserFileName':MagicMock(),
'ParserName':MagicMock(),
'SimName':MagicMock(),
'PluginList':MagicMock()
'PluginList':"{fake_name:fake_path}"
}
fake_run_flags = MagicMock()
fake_dict_for_Config = {'DEFAULT':fake_default, 'RUN_FLAGS':fake_run_flags}
Expand All @@ -207,20 +371,22 @@ def test_ExecutionEngine_parse_configs_bypasses_benchmarks_when_access_raises_er
fake_SBN_flags = MagicMock()
fake_Viz_flags = MagicMock()
fake_plugin_dict = MagicMock()

fake_keys = MagicMock()
fake_keys2 = MagicMock()
fake_plugin = MagicMock()
fake_path = MagicMock()

fake_keys.__len__.return_value = 1
fake_keys2.__iter__.return_value = iter(list([0,0]))
fake_keys.__iter__.return_value = iter([str(fake_plugin)])

cut = ExecutionEngine.__new__(ExecutionEngine)

mocker.patch(execution_engine.__name__ + '.configparser.ConfigParser', return_value=fake_config)
mocker.patch.object(fake_config, 'read', return_value=fake_config_read_result)
mocker.patch.object(fake_run_flags, 'getboolean', side_effect=[fake_IO_flags, fake_Dev_flags, fake_SBN_flags, fake_Viz_flags])
mocker.patch('ast.literal_eval',return_value=fake_plugin_dict)
mocker.patch.object(fake_plugin_dict, 'keys', return_value=fake_keys2)
mocker.patch.object(fake_plugin_dict, 'keys', return_value=fake_keys)
mocker.patch.object(fake_plugin_dict, '__getitem__', return_value=fake_path)
mocker.patch('os.path.exists', return_value=True)


# Act
Expand All @@ -231,43 +397,6 @@ def test_ExecutionEngine_parse_configs_bypasses_benchmarks_when_access_raises_er
assert hasattr(cut, 'benchmarkFiles') == False
assert hasattr(cut, 'benchmarkIndices') == False

def test_ExecutionEngine_parse_configs_raises_KeyError_with_config_file_info_when_a_required_key_is_not_in_config(mocker):
# Arrange
arg_config_filepath = MagicMock()

fake_default = {'TelemetryDataFilePath':MagicMock(),
'TelemetryMetadataFilePath':MagicMock(),
'MetaFiles':MagicMock(),
'TelemetryFiles':MagicMock(),
'BenchmarkFilePath':MagicMock(),
'BenchmarkFiles':MagicMock(),
'BenchmarkIndices':MagicMock(),
'ParserFileName':MagicMock(),
'ParserName':MagicMock(),
'SimName':MagicMock(),
}
required_keys = [item for item in list(fake_default.keys()) if 'Benchmark' not in item]
missing_key = pytest.gen.choice(required_keys)
del fake_default[missing_key]
fake_run_flags = MagicMock()
fake_dict_for_Config = {'DEFAULT':fake_default, 'RUN_FLAGS':fake_run_flags}
fake_config = MagicMock()
fake_config.__getitem__.side_effect = fake_dict_for_Config.__getitem__
fake_config_read_result = MagicMock()
fake_config_read_result.__len__.return_value = 1

cut = ExecutionEngine.__new__(ExecutionEngine)

mocker.patch(execution_engine.__name__ + '.configparser.ConfigParser', return_value=fake_config)
mocker.patch.object(fake_config, 'read', return_value=fake_config_read_result)

# Act
with pytest.raises(KeyError) as e_info:
cut.parse_configs(arg_config_filepath)

# Assert
assert e_info.match(f"Config file: '{arg_config_filepath}', missing key: {missing_key}")

# parse_data tests
def test_ExecutionEngine_parse_data_sets_the_processedSimData_to_the_TimeSynchronizer_which_was_given_the_sim_data_received_from_parsed_data(mocker):
# Arrange
Expand Down

0 comments on commit a794f99

Please sign in to comment.