mirror of
https://github.com/nasa/trick.git
synced 2025-02-07 11:20:24 +00:00
Merge pull request #1160 from nasa/1159-robustify-yaml-parsing
Robustify YAML config file validation
This commit is contained in:
commit
c88faa04df
@ -185,7 +185,7 @@ class TrickWorkflow(WorkflowCommon):
|
|||||||
return sim
|
return sim
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def get_sims(self, labels):
|
def get_sims(self, labels=None):
|
||||||
"""
|
"""
|
||||||
Get a list of Sim() instances by label or labels listed in self.config_file
|
Get a list of Sim() instances by label or labels listed in self.config_file
|
||||||
|
|
||||||
@ -194,8 +194,9 @@ class TrickWorkflow(WorkflowCommon):
|
|||||||
|
|
||||||
Parameters
|
Parameters
|
||||||
----------
|
----------
|
||||||
labels : str or list
|
labels : str or list or None
|
||||||
label or labels that each sim must have to be returned by this function
|
label or labels that each sim must have to be returned by this functionr.
|
||||||
|
If None, return all sims
|
||||||
|
|
||||||
Returns
|
Returns
|
||||||
-------
|
-------
|
||||||
@ -208,6 +209,9 @@ class TrickWorkflow(WorkflowCommon):
|
|||||||
TypeError
|
TypeError
|
||||||
If labels is not a str or list
|
If labels is not a str or list
|
||||||
"""
|
"""
|
||||||
|
# If no labels given, just return the full list
|
||||||
|
if not labels:
|
||||||
|
return self.sims
|
||||||
sims_found = []
|
sims_found = []
|
||||||
ls = []
|
ls = []
|
||||||
if type(labels) == str:
|
if type(labels) == str:
|
||||||
@ -242,7 +246,7 @@ class TrickWorkflow(WorkflowCommon):
|
|||||||
y = yaml.load(file)
|
y = yaml.load(file)
|
||||||
return y
|
return y
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
tprint("Unable to parse config file: %s\nError: %s" % (config_file,e), 'DARK_RED')
|
tprint("Unable to parse config file: %s\nERROR: %s" % (config_file,e), 'DARK_RED')
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def _validate_config(self):
|
def _validate_config(self):
|
||||||
@ -293,13 +297,30 @@ class TrickWorkflow(WorkflowCommon):
|
|||||||
RuntimeError
|
RuntimeError
|
||||||
If self.config has insufficient content
|
If self.config has insufficient content
|
||||||
"""
|
"""
|
||||||
# All error messages should trigger self.config_errors to be True
|
# All error messages in config validation will trigger self.config_errors to be True
|
||||||
def cprint(msg, color):
|
def cprint(msg, color):
|
||||||
self.config_errors = True
|
self.config_errors = True
|
||||||
tprint(msg, color)
|
tprint(msg, color)
|
||||||
|
|
||||||
|
# Utility method for ensuring a variable is the expected type. Returns True if type
|
||||||
|
# matches expected type, False otherwise. If fatal=True, raise RuntimeError.
|
||||||
|
def type_expected(var, expected_type, fatal=False, extra_msg=''):
|
||||||
|
if type(var) != expected_type:
|
||||||
|
prepend = "FATAL: " if fatal else "ERROR: "
|
||||||
|
msg =(prepend + "Entry resembling %s expected to be %s, but got %s instead. Please"
|
||||||
|
" look for errors in %s. " %
|
||||||
|
( str(var), expected_type, type(var), self.config_file) + extra_msg)
|
||||||
|
cprint(msg, 'DARK_RED')
|
||||||
|
if fatal:
|
||||||
|
raise RuntimeError(msg)
|
||||||
|
else:
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
c = copy.deepcopy(self.config) # Make a copy for extra saftey
|
c = copy.deepcopy(self.config) # Make a copy for extra saftey
|
||||||
if not c: # If entire config is empty
|
if not c: # If entire config is empty
|
||||||
msg = "ERROR Config file %s is empty. Cannot continue." % (self.config_file)
|
msg =("ERROR: Config file %s could not be loaded. Make sure file exists and is valid YAML syntax."
|
||||||
|
" Cannot continue." % (self.config_file))
|
||||||
cprint(msg, 'DARK_RED')
|
cprint(msg, 'DARK_RED')
|
||||||
self._cleanup()
|
self._cleanup()
|
||||||
raise RuntimeError(msg)
|
raise RuntimeError(msg)
|
||||||
@ -316,12 +337,14 @@ class TrickWorkflow(WorkflowCommon):
|
|||||||
if 'parallel_safety' not in c['globals'] or not c['globals']['parallel_safety']:
|
if 'parallel_safety' not in c['globals'] or not c['globals']['parallel_safety']:
|
||||||
self.parallel_safety = 'loose'
|
self.parallel_safety = 'loose'
|
||||||
elif c['globals']['parallel_safety'] != 'loose' and c['globals']['parallel_safety'] != 'strict':
|
elif c['globals']['parallel_safety'] != 'loose' and c['globals']['parallel_safety'] != 'strict':
|
||||||
cprint( "ERROR parallel_safety value of %s in config file %s is unsupported. Defaulting to"
|
cprint( "ERROR: parallel_safety value of %s in config file %s is unsupported. Defaulting to"
|
||||||
" 'loose' and continuing..." % (c['globals']['parallel_safety'], self.config_file),
|
" 'loose' and continuing..." % (c['globals']['parallel_safety'], self.config_file),
|
||||||
'DARK_RED')
|
'DARK_RED')
|
||||||
self.parallel_safety = 'loose'
|
self.parallel_safety = 'loose'
|
||||||
else:
|
else:
|
||||||
self.parallel_safety = c['globals']['parallel_safety']
|
self.parallel_safety = c['globals']['parallel_safety']
|
||||||
|
if not type_expected(c, expected_type=dict, fatal=True, extra_msg='Cannot continue.'):
|
||||||
|
pass # Unreachable, type_expected will raise
|
||||||
c.pop('globals', None) # Remove to iterate on the rest of the non-global content
|
c.pop('globals', None) # Remove to iterate on the rest of the non-global content
|
||||||
all_sim_paths = [] # Keep a list of all paths for uniqueness check
|
all_sim_paths = [] # Keep a list of all paths for uniqueness check
|
||||||
# Check sub-parameters of SIM entries
|
# Check sub-parameters of SIM entries
|
||||||
@ -334,11 +357,9 @@ class TrickWorkflow(WorkflowCommon):
|
|||||||
# If the structure doesn't start with SIM, ignore it and move on
|
# If the structure doesn't start with SIM, ignore it and move on
|
||||||
if not s.lower().startswith('sim'):
|
if not s.lower().startswith('sim'):
|
||||||
continue
|
continue
|
||||||
if type(c[s]) is not dict:
|
# If what's stored under SIM_..: is not itself a dict
|
||||||
cprint("ERROR: SIM entry %s is not a dict!. SIM definitions must start with SIM, end"
|
if not type_expected(c[s], expected_type=dict, extra_msg="SIM definitions must start with SIM, end"
|
||||||
" with :, and contain the path: <path/to/SIM> key-value pair. Continuing but"
|
" with :, and contain the path: <path/to/SIM> key-value pair. Skipping over %s." % c[s]):
|
||||||
" ignoring this entry from %s."
|
|
||||||
% (s, self.config_file), 'DARK_RED')
|
|
||||||
self.config.pop(s)
|
self.config.pop(s)
|
||||||
continue
|
continue
|
||||||
# If optional entries missing, or None, set defaults
|
# If optional entries missing, or None, set defaults
|
||||||
@ -361,8 +382,14 @@ class TrickWorkflow(WorkflowCommon):
|
|||||||
% (s, self.config_file), 'DARK_RED')
|
% (s, self.config_file), 'DARK_RED')
|
||||||
self.config.pop(s)
|
self.config.pop(s)
|
||||||
continue
|
continue
|
||||||
if 'labels' not in c[s] or not c[s]['labels']:
|
# Ensure labels is a list of strings
|
||||||
self.config[s]['labels'] = []
|
if ('labels' not in c[s] or not c[s]['labels'] or not type_expected(
|
||||||
|
c[s]['labels'], expected_type=list, extra_msg='Ignoring labels.')):
|
||||||
|
self.config[s]['labels'] = []
|
||||||
|
else:
|
||||||
|
sanitized_labels =( [l for l in c[s]['labels'] if type_expected(
|
||||||
|
l, expected_type=str, extra_msg='Ignoring label "%s".' % l) ] )
|
||||||
|
self.config[s]['labels'] = sanitized_labels
|
||||||
# Create internal object to be populated with runs, valgrind runs, etc
|
# Create internal object to be populated with runs, valgrind runs, etc
|
||||||
thisSim = TrickWorkflow.Sim(name=s, sim_dir=self.config[s]['path'],
|
thisSim = TrickWorkflow.Sim(name=s, sim_dir=self.config[s]['path'],
|
||||||
description=self.config[s]['description'], labels=self.config[s]['labels'],
|
description=self.config[s]['description'], labels=self.config[s]['labels'],
|
||||||
@ -371,19 +398,16 @@ class TrickWorkflow(WorkflowCommon):
|
|||||||
all_sim_paths.append(c[s]['path'])
|
all_sim_paths.append(c[s]['path'])
|
||||||
# RUN sanity checks
|
# RUN sanity checks
|
||||||
if 'runs' in c[s]: # If it's there...
|
if 'runs' in c[s]: # If it's there...
|
||||||
if not c[s]['runs']: # but None, remove it
|
if not type_expected(c[s]['runs'], expected_type=dict,
|
||||||
|
extra_msg='Skipping entire run: section in %s' % c[s]):
|
||||||
self.config[s].pop('runs')
|
self.config[s].pop('runs')
|
||||||
elif type( c[s]['runs']) is not dict: # but None, remove it
|
|
||||||
cprint("ERROR: Run %s is not a dict! Make sure the 'RUN...:' ends with a : in config file %s."
|
|
||||||
" Continuing but skipping this run: section..."
|
|
||||||
% (c[s]['runs'], self.config_file), 'DARK_RED')
|
|
||||||
self.config[s].pop('runs')
|
|
||||||
continue
|
|
||||||
else: # If it's there and a valid list, check paths
|
else: # If it's there and a valid list, check paths
|
||||||
all_run_paths = [] # Keep a list of all paths for uniqueness check
|
all_run_paths = [] # Keep a list of all paths for uniqueness check
|
||||||
for r in c[s]['runs']:
|
for r in c[s]['runs']:
|
||||||
just_RUN = r.split(' ')[0]
|
if not type_expected(r, expected_type=str, extra_msg='Skipping this run.'):
|
||||||
just_RUN_dir = r.split('/')[0]
|
continue
|
||||||
|
just_RUN = r.split()[0]
|
||||||
|
just_RUN_dir = os.path.dirname(just_RUN)
|
||||||
if not os.path.exists(os.path.join(self.project_top_level, c[s]['path'], just_RUN)):
|
if not os.path.exists(os.path.join(self.project_top_level, c[s]['path'], just_RUN)):
|
||||||
cprint("ERROR: %s's 'run' path %s not found. Continuing but skipping this run "
|
cprint("ERROR: %s's 'run' path %s not found. Continuing but skipping this run "
|
||||||
"from %s." % (s, just_RUN, self.config_file), 'DARK_RED')
|
"from %s." % (s, just_RUN, self.config_file), 'DARK_RED')
|
||||||
@ -406,8 +430,12 @@ class TrickWorkflow(WorkflowCommon):
|
|||||||
c[s]['runs'][r] = dict(self.config[s]['runs'][r])
|
c[s]['runs'][r] = dict(self.config[s]['runs'][r])
|
||||||
elif 'returns' not in c[s]['runs'][r]:
|
elif 'returns' not in c[s]['runs'][r]:
|
||||||
self.config[s]['runs'][r]['returns'] = 0
|
self.config[s]['runs'][r]['returns'] = 0
|
||||||
elif (type(c[s]['runs'][r]['returns']) != int or
|
|
||||||
c[s]['runs'][r]['returns'] < 0 or c[s]['runs'][r]['returns'] > 255):
|
elif (not type_expected(c[s]['runs'][r]['returns'], expected_type=int,
|
||||||
|
extra_msg='Continuing but ignoring %s %s "returns:" value "%s"' %
|
||||||
|
(s, r, c[s]['runs'][r]['returns']))):
|
||||||
|
self.config[s]['runs'][r]['returns'] = 0 # Default to zero
|
||||||
|
elif (c[s]['runs'][r]['returns'] < 0 or c[s]['runs'][r]['returns'] > 255):
|
||||||
cprint("ERROR: %s's run '%s' has invalid 'returns' value (must be 0-255). "
|
cprint("ERROR: %s's run '%s' has invalid 'returns' value (must be 0-255). "
|
||||||
"Continuing but assuming this run is expected to return 0 in %s." % (s, r, self.config_file),
|
"Continuing but assuming this run is expected to return 0 in %s." % (s, r, self.config_file),
|
||||||
'DARK_RED')
|
'DARK_RED')
|
||||||
@ -424,6 +452,9 @@ class TrickWorkflow(WorkflowCommon):
|
|||||||
pass # Deliberately leave open for workflows to extend how comparisons are defined
|
pass # Deliberately leave open for workflows to extend how comparisons are defined
|
||||||
else: # If it's a list, make sure it fits the 'path vs. path' format
|
else: # If it's a list, make sure it fits the 'path vs. path' format
|
||||||
for cmp in c[s]['runs'][r]['compare']:
|
for cmp in c[s]['runs'][r]['compare']:
|
||||||
|
if not type_expected(cmp, expected_type=str,
|
||||||
|
extra_msg='Continuing but ignoring comparison %s' % cmp):
|
||||||
|
continue
|
||||||
if ' vs. ' not in cmp:
|
if ' vs. ' not in cmp:
|
||||||
cprint("ERROR: %s's run %s comparison '%s' does not match expected pattern. Must be of "
|
cprint("ERROR: %s's run %s comparison '%s' does not match expected pattern. Must be of "
|
||||||
"form: 'path/to/log1 vs. path/to/log2'. Continuing but ignoring this comparison in %s."
|
"form: 'path/to/log1 vs. path/to/log2'. Continuing but ignoring this comparison in %s."
|
||||||
@ -443,12 +474,8 @@ class TrickWorkflow(WorkflowCommon):
|
|||||||
thisSim.add_run(thisRun)
|
thisSim.add_run(thisRun)
|
||||||
# SIM's valgrind RUN path checks
|
# SIM's valgrind RUN path checks
|
||||||
if 'valgrind' in c[s]: # If it's there...
|
if 'valgrind' in c[s]: # If it's there...
|
||||||
if not c[s]['valgrind']: # but None, remove it
|
if not type_expected(c[s]['valgrind'], expected_type=dict,
|
||||||
self.config[s].pop('valgrind')
|
extra_msg='Skipping entire valgrind: section in %s' % c[s]):
|
||||||
elif type( c[s]['valgrind']) is not dict: # If it's the wrong type
|
|
||||||
cprint("ERROR: Valgrind entry %s is not a dict! Make sure 'valgrind:' ends with a : in "
|
|
||||||
"config file %s. Continuing but skipping this valgrind: section..."
|
|
||||||
% (c[s]['valgrind'], self.config_file), 'DARK_RED')
|
|
||||||
self.config[s].pop('valgrind')
|
self.config[s].pop('valgrind')
|
||||||
else: # If it's there and a valid dict
|
else: # If it's there and a valid dict
|
||||||
if self.this_os == 'darwin':
|
if self.this_os == 'darwin':
|
||||||
@ -460,13 +487,14 @@ class TrickWorkflow(WorkflowCommon):
|
|||||||
if 'flags' not in c[s]['valgrind']:
|
if 'flags' not in c[s]['valgrind']:
|
||||||
self.config[s]['valgrind']['flags'] = ''
|
self.config[s]['valgrind']['flags'] = ''
|
||||||
if 'runs' in c[s]['valgrind']: # If it's there...
|
if 'runs' in c[s]['valgrind']: # If it's there...
|
||||||
if not c[s]['valgrind']['runs']: # but None, remove entire valgrind section
|
if not type_expected(c[s]['valgrind']['runs'], expected_type=list,
|
||||||
cprint("ERROR: %s's valgrind section has no 'run' paths. Continuing but skipping "
|
extra_msg='Skipping this valgrind runs: section for %s' % c[s]):
|
||||||
"this valgrind section from %s." % (s, self.config_file), 'DARK_RED')
|
|
||||||
self.config[s].pop('valgrind')
|
self.config[s].pop('valgrind')
|
||||||
else:
|
else:
|
||||||
for r in c[s]['valgrind']['runs']: # If it's there and a valid list, check paths
|
for r in c[s]['valgrind']['runs']: # If it's there and a valid list, check paths
|
||||||
just_RUN = r.split(' ')[0]
|
if not type_expected(r, expected_type=str, extra_msg='Skipping this valgrind run.'):
|
||||||
|
continue
|
||||||
|
just_RUN = r.split()[0]
|
||||||
if not os.path.exists(os.path.join(self.project_top_level, c[s]['path'], just_RUN)):
|
if not os.path.exists(os.path.join(self.project_top_level, c[s]['path'], just_RUN)):
|
||||||
cprint("ERROR: %s's valgrind 'run' path %s not found. Continuing but skipping "
|
cprint("ERROR: %s's valgrind 'run' path %s not found. Continuing but skipping "
|
||||||
"this run from %s." % (s, just_RUN, self.config_file), 'DARK_RED')
|
"this run from %s." % (s, just_RUN, self.config_file), 'DARK_RED')
|
||||||
@ -873,6 +901,17 @@ class TrickWorkflow(WorkflowCommon):
|
|||||||
"""
|
"""
|
||||||
return self.runs
|
return self.runs
|
||||||
|
|
||||||
|
def get_valgrind_runs(self):
|
||||||
|
"""
|
||||||
|
Get all Run() instances associated with this sim
|
||||||
|
|
||||||
|
>>> s = TrickWorkflow.Sim(name='alloc', sim_dir=os.path.join(this_trick, 'test/SIM_alloc_test'))
|
||||||
|
>>> s.get_valgrind_runs() # This sim has no runs
|
||||||
|
[]
|
||||||
|
|
||||||
|
"""
|
||||||
|
return self.valgrind_runs
|
||||||
|
|
||||||
def add_run( self, run):
|
def add_run( self, run):
|
||||||
"""
|
"""
|
||||||
Append a new Run() instance to the internal run lists. Appends to valgrind
|
Append a new Run() instance to the internal run lists. Appends to valgrind
|
||||||
|
@ -1,33 +0,0 @@
|
|||||||
# YAML file with many errors for unit testing
|
|
||||||
# Global configuration parameters
|
|
||||||
|
|
||||||
globals:
|
|
||||||
parallel_safety: unsupported_value
|
|
||||||
|
|
||||||
extension_example:
|
|
||||||
should: be ignored by this framework
|
|
||||||
|
|
||||||
# This sim exists, but has duplicate run entries which is an error
|
|
||||||
SIM_ball_L1:
|
|
||||||
path: trick_sims/Ball/SIM_ball_L1
|
|
||||||
size: 6000
|
|
||||||
runs:
|
|
||||||
RUN_test/input.py:
|
|
||||||
RUN_test/input.py:
|
|
||||||
|
|
||||||
# This sim exists, but it's RUN does not
|
|
||||||
SIM_alloc_test:
|
|
||||||
path: test/SIM_alloc_test
|
|
||||||
runs:
|
|
||||||
RUN_buddy/input.py:
|
|
||||||
|
|
||||||
# This one is has duplicate non-unique path which is an error
|
|
||||||
SIM_L1_ball:
|
|
||||||
path: trick_sims/Ball/SIM_ball_L1
|
|
||||||
|
|
||||||
# This sim doesn't exist
|
|
||||||
SIM_foobar:
|
|
||||||
path: test/SIM_foobar
|
|
||||||
runs:
|
|
||||||
RUN_hi/input.py:
|
|
||||||
|
|
7
share/trick/trickops/tests/errors_fatal1.yml
Normal file
7
share/trick/trickops/tests/errors_fatal1.yml
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
# YAML file with fatal error used for unit testing
|
||||||
|
# The runs: section has invalid YAML syntax
|
||||||
|
SIM_alloc_test:
|
||||||
|
path: test/SIM_alloc_test
|
||||||
|
runs:
|
||||||
|
- RUN_test/input.py:
|
||||||
|
RUN_test/input.py:
|
4
share/trick/trickops/tests/errors_fatal2.yml
Normal file
4
share/trick/trickops/tests/errors_fatal2.yml
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
# YAML file with fatal error used for unit testing
|
||||||
|
# Doesn't conform to expected dict format
|
||||||
|
SIM_alloc_test
|
||||||
|
path - test/SIM_alloc_test
|
79
share/trick/trickops/tests/errors_nonfatal.yml
Normal file
79
share/trick/trickops/tests/errors_nonfatal.yml
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
# YAML file with many errors for unit testing
|
||||||
|
# Global configuration parameters
|
||||||
|
|
||||||
|
globals:
|
||||||
|
parallel_safety: unsupported_value
|
||||||
|
|
||||||
|
extension_example:
|
||||||
|
should: be ignored by this framework
|
||||||
|
|
||||||
|
# This sim exists, but has duplicate run entries which is an error
|
||||||
|
SIM_ball_L1:
|
||||||
|
path: trick_sims/Ball/SIM_ball_L1
|
||||||
|
size: 6000
|
||||||
|
runs:
|
||||||
|
RUN_test/input.py:
|
||||||
|
RUN_test/input.py:
|
||||||
|
valgrind:
|
||||||
|
runs:
|
||||||
|
RUN_test/input.py: # Wrong type, dict
|
||||||
|
- RUN_test/input.py: # Should be list of str not list of dict
|
||||||
|
|
||||||
|
# This sim exists, but its valgrind section is empty and its runs have problems
|
||||||
|
SIM_alloc_test:
|
||||||
|
path: test/SIM_alloc_test
|
||||||
|
valgrind:
|
||||||
|
runs:
|
||||||
|
RUN_buddy/input.py: # Doesn't exist
|
||||||
|
RUN_test/input.py: # Does exist
|
||||||
|
extension: foobar # Should be retained in self.config but not used
|
||||||
|
compare:
|
||||||
|
- RUN_test/log.trk: # Should be list of str not list of dict
|
||||||
|
- share/trick/trickops/tests/testdata/log_a.csv vs. share/trick/trickops/tests/baselinedata/log_a.csv # OK
|
||||||
|
|
||||||
|
# This sim exists but has problems with types of internal variables
|
||||||
|
SIM_events:
|
||||||
|
path: test/SIM_events
|
||||||
|
labels:
|
||||||
|
- fine1
|
||||||
|
- broke1: # Should be list of str not list of dict
|
||||||
|
- broke2: # Should be list of str not list of dict
|
||||||
|
- fine2
|
||||||
|
runs:
|
||||||
|
- RUN_test/input.py: # List of dicts, should be ignored
|
||||||
|
- RUN_test/unit_test.py: # List of dicts, should be ignored
|
||||||
|
valgrind: # Empty so should be ignored
|
||||||
|
runs:
|
||||||
|
|
||||||
|
# Sim exists, but runs have bad return values
|
||||||
|
SIM_threads:
|
||||||
|
path: test/SIM_threads
|
||||||
|
runs:
|
||||||
|
RUN_test/sched.py:
|
||||||
|
returns: # Empty so should print error and be ignored
|
||||||
|
RUN_test/amf.py:
|
||||||
|
returns: 270 # Invalid range
|
||||||
|
RUN_test/async.py:
|
||||||
|
returns: hello # Invalid type
|
||||||
|
RUN_test/unit_test.py:
|
||||||
|
returns: 7 # Valid type and range
|
||||||
|
|
||||||
|
# Missing the required path: so should be ignored
|
||||||
|
SIM_demo_inputfile:
|
||||||
|
|
||||||
|
# This sim has duplicate non-unique path which is an error
|
||||||
|
SIM_L1_ball:
|
||||||
|
path: trick_sims/Ball/SIM_ball_L1
|
||||||
|
|
||||||
|
# This sim doesn't exist
|
||||||
|
SIM_foobar:
|
||||||
|
path: test/SIM_foobar
|
||||||
|
runs:
|
||||||
|
RUN_hi/input.py:
|
||||||
|
|
||||||
|
# Sim exists but path is bad yaml syntax (no space)
|
||||||
|
SIM_parachute:
|
||||||
|
path:trick_sims/SIM_parachute
|
||||||
|
|
||||||
|
# Should be ignored
|
||||||
|
999:
|
@ -1,4 +1,4 @@
|
|||||||
import os, sys
|
import os, sys, shutil
|
||||||
import unittest
|
import unittest
|
||||||
import pdb
|
import pdb
|
||||||
from testconfig import this_trick, tests_dir
|
from testconfig import this_trick, tests_dir
|
||||||
@ -17,8 +17,9 @@ class TrickWorkflowTestCase(unittest.TestCase):
|
|||||||
quiet=True)
|
quiet=True)
|
||||||
|
|
||||||
def tearDown(self):
|
def tearDown(self):
|
||||||
self.instance._cleanup() # Remove the log file this instance creates
|
if self.instance:
|
||||||
del self.instance
|
self.instance._cleanup() # Remove the log file this instance creates
|
||||||
|
del self.instance
|
||||||
self.instance = None
|
self.instance = None
|
||||||
|
|
||||||
def setUpWithEmptyConfig(self):
|
def setUpWithEmptyConfig(self):
|
||||||
@ -26,10 +27,10 @@ class TrickWorkflowTestCase(unittest.TestCase):
|
|||||||
trick_dir=this_trick, config_file=os.path.join(tests_dir,"empty.yml"),
|
trick_dir=this_trick, config_file=os.path.join(tests_dir,"empty.yml"),
|
||||||
quiet=True)
|
quiet=True)
|
||||||
|
|
||||||
def setUpWithErrorConfig(self):
|
def setUpWithErrorConfig(self, file):
|
||||||
self.tearDown() # Cleanup the instance we get by default
|
self.tearDown() # Cleanup the instance we get by default
|
||||||
self.instance = TrickWorkflow(project_top_level=this_trick, log_dir='/tmp/',
|
self.instance = TrickWorkflow(project_top_level=this_trick, log_dir='/tmp/',
|
||||||
trick_dir=this_trick, config_file=os.path.join(tests_dir,"errors.yml"),
|
trick_dir=this_trick, config_file=os.path.join(tests_dir,file),
|
||||||
quiet=True)
|
quiet=True)
|
||||||
|
|
||||||
def test_init_nominal(self):
|
def test_init_nominal(self):
|
||||||
@ -47,11 +48,34 @@ class TrickWorkflowTestCase(unittest.TestCase):
|
|||||||
with self.assertRaises(RuntimeError):
|
with self.assertRaises(RuntimeError):
|
||||||
self.setUpWithEmptyConfig()
|
self.setUpWithEmptyConfig()
|
||||||
|
|
||||||
|
def test_init_bad_yaml_so_raises(self):
|
||||||
|
with self.assertRaises(RuntimeError):
|
||||||
|
self.setUpWithErrorConfig("errors_fatal1.yml")
|
||||||
|
with self.assertRaises(RuntimeError):
|
||||||
|
self.setUpWithErrorConfig("errors_fatal2.yml")
|
||||||
|
|
||||||
def test_init_errors_but_no_raise(self):
|
def test_init_errors_but_no_raise(self):
|
||||||
self.setUpWithErrorConfig()
|
self.setUpWithErrorConfig("errors_nonfatal.yml")
|
||||||
self.assertTrue(self.instance.config_errors)
|
self.assertTrue(self.instance.config_errors)
|
||||||
self.assertEqual(self.instance.parallel_safety , 'loose')
|
self.assertEqual(self.instance.parallel_safety , 'loose')
|
||||||
self.assertEqual(len(self.instance.sims), 2)
|
self.assertEqual(len(self.instance.sims), 4)
|
||||||
|
self.assertEqual(len(self.instance.get_sim('SIM_ball_L1').get_runs()), 1)
|
||||||
|
self.assertEqual(len(self.instance.get_sim('SIM_ball_L1').get_valgrind_runs()), 0)
|
||||||
|
self.assertEqual(len(self.instance.get_sim('SIM_alloc_test').get_runs()), 1)
|
||||||
|
self.assertEqual(len(self.instance.get_sim('SIM_alloc_test').get_valgrind_runs()), 0)
|
||||||
|
self.assertEqual(self.instance.config['SIM_alloc_test']['runs']['RUN_test/input.py']['extension'], 'foobar')
|
||||||
|
self.assertEqual(len(self.instance.get_sim('SIM_events').get_runs()), 0)
|
||||||
|
self.assertTrue('fine1' in self.instance.get_sim('SIM_events').labels)
|
||||||
|
self.assertTrue('fine2' in self.instance.get_sim('SIM_events').labels)
|
||||||
|
self.assertEqual(self.instance.get_sim('SIM_threads').get_run('RUN_test/sched.py').returns, 0)
|
||||||
|
self.assertEqual(self.instance.get_sim('SIM_threads').get_run('RUN_test/amf.py').returns, 0)
|
||||||
|
self.assertEqual(self.instance.get_sim('SIM_threads').get_run('RUN_test/async.py').returns, 0)
|
||||||
|
self.assertEqual(self.instance.get_sim('SIM_threads').get_run('RUN_test/unit_test.py').returns, 7)
|
||||||
|
self.assertTrue(self.instance.get_sim('SIM_demo_inputfile') is None)
|
||||||
|
self.assertTrue(self.instance.get_sim('SIM_L1_ball') is None)
|
||||||
|
self.assertTrue(self.instance.get_sim('SIM_foobar') is None)
|
||||||
|
self.assertTrue(self.instance.get_sim('SIM_parachute') is None)
|
||||||
|
|
||||||
self.assertTrue(self.instance.config['extension_example'])
|
self.assertTrue(self.instance.config['extension_example'])
|
||||||
self.instance.report()
|
self.instance.report()
|
||||||
|
|
||||||
@ -232,3 +256,80 @@ class TrickWorkflowTestCase(unittest.TestCase):
|
|||||||
baseline_data = 'share/trick/trickops/tests/baselinedata/log_a.csv'
|
baseline_data = 'share/trick/trickops/tests/baselinedata/log_a.csv'
|
||||||
r.add_comparison(test_data, baseline_data)
|
r.add_comparison(test_data, baseline_data)
|
||||||
self.assertEqual(r.compare(), 1)
|
self.assertEqual(r.compare(), 1)
|
||||||
|
|
||||||
|
def setup_deep_directory_structure(self, runs, parallel_safety='loose'):
|
||||||
|
'''
|
||||||
|
Build a temporary dir structure to test deep pathing
|
||||||
|
runs is a list of runs relative to SIM_fake directory
|
||||||
|
returns: str/path of config file for this deep setup
|
||||||
|
'''
|
||||||
|
deep_root = os.path.join(tests_dir, 'deep/')
|
||||||
|
SIM_root = os.path.join(deep_root, 'SIM_fake/')
|
||||||
|
for run in runs:
|
||||||
|
just_RUN_rel = os.path.dirname(run)
|
||||||
|
just_RUN_root = os.path.join(SIM_root, just_RUN_rel)
|
||||||
|
SIM_root_rel = os.path.relpath(SIM_root, this_trick)
|
||||||
|
os.makedirs(just_RUN_root, exist_ok=True)
|
||||||
|
Path(os.path.join(SIM_root,run)).touch()
|
||||||
|
yml_content=textwrap.dedent("""
|
||||||
|
globals:
|
||||||
|
parallel_safety: """ + parallel_safety + """
|
||||||
|
SIM_fake:
|
||||||
|
path: """ + SIM_root_rel + """
|
||||||
|
runs:
|
||||||
|
""")
|
||||||
|
for run in runs:
|
||||||
|
yml_content += " " + run + ":\n"
|
||||||
|
|
||||||
|
config_file = os.path.join(deep_root, "config.yml")
|
||||||
|
f = open(config_file, "w")
|
||||||
|
f.write(yml_content)
|
||||||
|
f.close()
|
||||||
|
return(config_file)
|
||||||
|
def teardown_deep_directory_structure(self):
|
||||||
|
# Clean up the dirs we created
|
||||||
|
deep_root = os.path.join(tests_dir, 'deep/')
|
||||||
|
shutil.rmtree(deep_root)
|
||||||
|
|
||||||
|
def test_deep_directory_structure_nominal(self):
|
||||||
|
'''
|
||||||
|
Set up a deep directory structure for sims and runs which will exercise
|
||||||
|
some of the path logic in _validate_config(). Nominal with no errors
|
||||||
|
'''
|
||||||
|
self.tearDown() # Cleanup the instance we get by default, don't need it
|
||||||
|
config_file = self.setup_deep_directory_structure(runs=['SET_fake/RUN_fake/input.py'])
|
||||||
|
self.instance = TrickWorkflow(project_top_level=this_trick, log_dir='/tmp/',
|
||||||
|
trick_dir=this_trick, config_file=config_file, quiet=True)
|
||||||
|
self.assertEqual(len(self.instance.get_sims()), 1)
|
||||||
|
self.assertEqual(len(self.instance.get_sim('SIM_fake').get_runs()), 1)
|
||||||
|
self.teardown_deep_directory_structure()
|
||||||
|
|
||||||
|
def test_deep_directory_structure_loose(self):
|
||||||
|
'''
|
||||||
|
Set up a deep directory structure for sims and runs which will exercise
|
||||||
|
some of the path logic in _validate_config(). Duplicate runs with
|
||||||
|
parallel safety = loose, should not error.
|
||||||
|
'''
|
||||||
|
self.tearDown() # Cleanup the instance we get by default, don't need it
|
||||||
|
config_file = self.setup_deep_directory_structure(runs=['SET_fake/RUN_fake/input.py',
|
||||||
|
'SET_fake/RUN_fake/test.py'])
|
||||||
|
self.instance = TrickWorkflow(project_top_level=this_trick, log_dir='/tmp/',
|
||||||
|
trick_dir=this_trick, config_file=config_file, quiet=True)
|
||||||
|
self.assertEqual(len(self.instance.get_sims()), 1)
|
||||||
|
self.assertEqual(len(self.instance.get_sim('SIM_fake').get_runs()), 2)
|
||||||
|
self.teardown_deep_directory_structure()
|
||||||
|
|
||||||
|
def test_deep_directory_structure_strict_error(self):
|
||||||
|
'''
|
||||||
|
Set up a deep directory structure for sims and runs which will exercise
|
||||||
|
some of the path logic in _validate_config(). Duplicate runs with
|
||||||
|
parallel safety = strict, should non-fatal error.
|
||||||
|
'''
|
||||||
|
self.tearDown() # Cleanup the instance we get by default, don't need it
|
||||||
|
config_file = self.setup_deep_directory_structure(runs=['SET_fake/RUN_fake/input.py',
|
||||||
|
'SET_fake/RUN_fake/test.py'], parallel_safety='strict')
|
||||||
|
self.instance = TrickWorkflow(project_top_level=this_trick, log_dir='/tmp/',
|
||||||
|
trick_dir=this_trick, config_file=config_file, quiet=True)
|
||||||
|
self.assertEqual(len(self.instance.get_sims()), 1)
|
||||||
|
self.assertEqual(len(self.instance.get_sim('SIM_fake').get_runs()), 1)
|
||||||
|
self.teardown_deep_directory_structure()
|
||||||
|
Loading…
x
Reference in New Issue
Block a user