From 7ba57ede25fd24b6fb26555d2dfa0cfd88b477de Mon Sep 17 00:00:00 2001 From: Mayank Chetan Date: Mon, 9 Sep 2024 15:18:06 +0000 Subject: [PATCH] new tests for lib robustness, verifying fst_vt --- openfast_io/openfast_io/FAST_reader.py | 94 ++++++++++++--- openfast_io/openfast_io/FAST_writer.py | 56 ++++++--- .../openfast_io/tests/test_of_io_pytest.py | 107 ++++++++++++++++-- 3 files changed, 217 insertions(+), 40 deletions(-) diff --git a/openfast_io/openfast_io/FAST_reader.py b/openfast_io/openfast_io/FAST_reader.py index 7d19d866e..2e05752ea 100644 --- a/openfast_io/openfast_io/FAST_reader.py +++ b/openfast_io/openfast_io/FAST_reader.py @@ -14,15 +14,38 @@ def readline_filterComments(f): - read = True - while read: - line = f.readline().strip() - if len(line)>0: - if line[0] != '!': - read = False - return line + """ + Filter out comments and empty lines from a file + + Args: + f: file handle + + Returns: + line: next line in the file that is not a comment or empty + """ + read = True + while read: + line = f.readline().strip() + if len(line)>0: + if line[0] != '!': + read = False + return line def read_array(f,len,split_val=None,array_type=str): + """ + Read an array of values from a line in a file + + Args: + f: file handle + len: number of values to read + split_val: value to stop reading at + array_type: type of values to return + + Returns: + arr: list of values read from the file line with the specified type + """ + + strings = re.split(',| ',f.readline().strip()) while '' in strings: # remove empties strings.remove('') @@ -54,7 +77,15 @@ def read_array(f,len,split_val=None,array_type=str): return arr def fix_path(name): - """ split a path, then reconstruct it using os.path.join """ + """ + split a path, then reconstruct it using os.path.join + + Args: + name: path to fix + + Returns: + new: reconstructed path + """ name = re.split("\\|/", name) new = name[0] for i in range(1,len(name)): @@ -62,7 +93,15 @@ def fix_path(name): return new def bool_read(text): - # convert true/false strings to boolean + """ + Read a boolean value from a string + + Args: + text: string to read + + Returns: + True if the string is 'true', False otherwise + """ if 'default' in text.lower(): return str(text) else: @@ -72,7 +111,15 @@ def bool_read(text): return False def float_read(text): - # return float with error handing for "default" values + """ + Read a float value from a string, with error handling for 'default' values + + Args: + text: string to read + + Returns: + float value if the string can be converted, string otherwise + """ if 'default' in text.lower(): return str(text) else: @@ -82,7 +129,15 @@ def float_read(text): return str(text) def int_read(text): - # return int with error handing for "default" values + """ + Read an integer value from a string, with error handling for 'default' values + + Args: + text: string to read + + Returns: + int value if the string can be converted, string otherwise + """ if 'default' in text.lower(): return str(text) else: @@ -92,7 +147,16 @@ def int_read(text): return str(text) def quoted_read(text): - # read a line, if the first part is quoted, return the quoted part, otherwise return the unquoted part + """ + Read a quoted value from a string (i.e. a value between quotes) + + Args: + text: string to read + + Returns: + quoted value if the string is quoted, unquoted value otherwise + + """ if '"' in text: return text.split('"')[1] elif "'" in text: @@ -2555,11 +2619,11 @@ def read_ExtPtfm(self, ep_file): self.fst_vt['ExtPtfm']['Red_FileName'] = os.path.join(os.path.dirname(ep_file), quoted_read(f.readline().split()[0])) self.fst_vt['ExtPtfm']['RedCst_FileName'] = os.path.join(os.path.dirname(ep_file), quoted_read(f.readline().split()[0])) self.fst_vt['ExtPtfm']['NActiveDOFList'] = int_read(f.readline().split()[0]) - self.fst_vt['ExtPtfm']['ActiveDOFList'] = [idx.strip() for idx in f.readline().split('ActiveDOFList')[0].split(',')] + self.fst_vt['ExtPtfm']['ActiveDOFList'] = read_array(f,None,split_val='ActiveDOFList',array_type=int) self.fst_vt['ExtPtfm']['NInitPosList'] = int_read(f.readline().split()[0]) - self.fst_vt['ExtPtfm']['InitPosList'] = [idx.strip() for idx in f.readline().split('InitPosList')[0].split(',')] + self.fst_vt['ExtPtfm']['InitPosList'] = read_array(f,None,split_val='InitPosList',array_type=float) self.fst_vt['ExtPtfm']['NInitVelList'] = int_read(f.readline().split()[0]) - self.fst_vt['ExtPtfm']['InitVelList'] = [idx.strip() for idx in f.readline().split('InitVelList')[0].split(',')] + self.fst_vt['ExtPtfm']['InitVelList'] = read_array(f,None,split_val='InitVelList',array_type=float) f.readline() # Output diff --git a/openfast_io/openfast_io/FAST_writer.py b/openfast_io/openfast_io/FAST_writer.py index 5906afa2e..7fa0083d5 100644 --- a/openfast_io/openfast_io/FAST_writer.py +++ b/openfast_io/openfast_io/FAST_writer.py @@ -15,7 +15,14 @@ def auto_format(f, var): - # Error handling for variables with 'Default' options + """ + Error handling for variables with 'Default' options + + args: + f: file object + var: variable to write to file + + """ if isinstance(var, str): f.write('{:}\n'.format(var)) elif isinstance(var, int): @@ -24,21 +31,46 @@ def auto_format(f, var): f.write('{: 2.15e}\n'.format(var)) def float_default_out(val): - # formatted float output when 'default' is an option + """ + Formatted float output when 'default' is an option + + args: + val: value to be formatted + + returns: + formatted value + """ if type(val) is float: return '{: 22f}'.format(val) else: return '{:<22}'.format(val) def int_default_out(val): - # formatted int output when 'default' is an option + """ + Formatted int output when 'default' is an option + + args: + val: value to be formatted + + returns: + formatted value + """ if type(val) is float: return '{:<22d}'.format(val) else: return '{:<22}'.format(val) -# given a list of nested dictionary keys, return the dict at that point def get_dict(vartree, branch): + """ + Given a list of nested dictionary keys, return the dictionary at that point + + args: + vartree: dictionary to search + branch: list of keys to search + + returns: + dictionary at the specified branch + """ return reduce(operator.getitem, branch, vartree) class InputWriter_OpenFAST(object): @@ -2102,21 +2134,11 @@ def write_ExtPtfm(self): f.write('{!s:<22} {:<11} {:}'.format(self.fst_vt['ExtPtfm']['Red_FileName'], 'Red_FileName', '- Path of the file containing Guyan/Craig-Bampton inputs (-)\n')) f.write('{!s:<22} {:<11} {:}'.format(self.fst_vt['ExtPtfm']['RedCst_FileName'], 'RedCst_FileName', '- Path of the file containing Guyan/Craig-Bampton constant inputs (-) (currently unused)\n')) f.write('{:<22d} {:<11} {:}'.format(self.fst_vt['ExtPtfm']['NActiveDOFList'], 'NActiveDOFList', '- Number of active CB mode listed in ActiveDOFList, use -1 for all modes (integer)\n')) - # f.write('{:<22} {:<11} {:}'.format(", ".join([str(i) for i in self.fst_vt['ExtPtfm']['ActiveDOFList']]), 'ActiveDOFList', '- List of CB modes index that are active, [unused if NActiveDOFList<=0]\n')) - if self.fst_vt['ExtPtfm']['NActiveDOFList'] > -1: - f.write('{:<22} {:<11} {:}'.format(', '.join(self.fst_vt['ExtPtfm']['ActiveDOFList'][:self.fst_vt['ExtPtfm']['NActiveDOFList']]), 'ActiveDOFList', '- List of CB modes index that are active, [unused if NActiveDOFList<=0]\n')) - else: - f.write('{:<22} {:<11} {:}'.format(', '.join(self.fst_vt['ExtPtfm']['ActiveDOFList']), 'ActiveDOFList', '- List of CB modes index that are active, [unused if NActiveDOFList<=0]\n')) + f.write('{:<22} {:<11} {:}'.format(', '.join([f'{val}' for val in self.fst_vt['ExtPtfm']['ActiveDOFList']]), 'ActiveDOFList', '- List of CB modes index that are active, [unused if NActiveDOFList<=0]\n')) f.write('{:<22d} {:<11} {:}'.format(self.fst_vt['ExtPtfm']['NInitPosList'], 'NInitPosList', '- Number of initial positions listed in InitPosList, using 0 implies all DOF initialized to 0 (integer)\n')) - if self.fst_vt['ExtPtfm']['NInitPosList'] > 0: - f.write('{:<22} {:<11} {:}'.format(', '.join(self.fst_vt['ExtPtfm']['InitPosList'][:self.fst_vt['ExtPtfm']['NInitPosList']]), 'InitPosList', '- List of initial positions for the CB modes [unused if NInitPosList<=0 or EquilStart=True]\n')) - else: - f.write('{:<22d} {:<11} {:}'.format(0, 'InitPosList', '- List of initial positions for the CB modes [unused if NInitPosList<=0 or EquilStart=True]\n')) + f.write('{:<22} {:<11} {:}'.format(', '.join([f'{val}' for val in self.fst_vt['ExtPtfm']['InitPosList']]), 'InitPosList', '- List of initial positions for the CB modes [unused if NInitPosList<=0 or EquilStart=True]\n')) f.write('{:<22d} {:<11} {:}'.format(self.fst_vt['ExtPtfm']['NInitVelList'], 'NInitVelList', '- Number of initial positions listed in InitVelList, using 0 implies all DOF initialized to 0 (integer)\n')) - if self.fst_vt['ExtPtfm']['NInitVelList'] > 0: - f.write('{:<22} {:<11} {:}'.format(', '.join(self.fst_vt['ExtPtfm']['InitVelList'][:self.fst_vt['ExtPtfm']['NInitVelList']]), 'InitVelList', '- List of initial velocities for the CB modes [unused if NInitVelPosList<=0 or EquilStart=True]\n')) - else: - f.write('{:<22d} {:<11} {:}'.format(0, 'InitVelList', '- List of initial velocities for the CB modes [unused if NInitVelPosList<=0 or EquilStart=True]\n')) + f.write('{:<22} {:<11} {:}'.format(', '.join([f'{val}' for val in self.fst_vt['ExtPtfm']['InitVelList']]), 'InitVelList', '- List of initial velocities for the CB modes [unused if NInitVelPosList<=0 or EquilStart=True]\n')) f.write('---------------------- OUTPUT --------------------------------------------------\n') f.write('{!s:<22} {:<11} {:}'.format(self.fst_vt['ExtPtfm']['SumPrint'], 'SumPrint', '- Print summary data to .sum (flag)\n')) diff --git a/openfast_io/openfast_io/tests/test_of_io_pytest.py b/openfast_io/openfast_io/tests/test_of_io_pytest.py index b7353344c..c8ad87742 100644 --- a/openfast_io/openfast_io/tests/test_of_io_pytest.py +++ b/openfast_io/openfast_io/tests/test_of_io_pytest.py @@ -62,6 +62,18 @@ def getPaths(OF_PATH = OF_PATH, REPOSITORY_ROOT = REPOSITORY_ROOT, BUILD_DIR = BUILD_DIR): + """ + Function to get the paths for the OpenFAST executable, source directory, build directory, r-test directory, and test data directory + + Args: + OF_PATH (str): Path to the OpenFAST executable + REPOSITORY_ROOT (str): Path to the OpenFAST repository + BUILD_DIR (str): Path to the build directory + + Returns: + dict: Dictionary containing the paths + """ + return { "executable": OF_PATH, "source_dir": REPOSITORY_ROOT, @@ -72,6 +84,17 @@ def getPaths(OF_PATH = OF_PATH, REPOSITORY_ROOT = REPOSITORY_ROOT, BUILD_DIR = B } def read_action(folder, path_dict = getPaths()): + """ + Function to read the input deck for a given folder + + Args: + folder (str): r-test folder name + path_dict (dict): Dictionary containing the paths. Default is getPaths() + + Returns: + dict: OpenFAST VariableTree + + """ print(f"Reading from {folder}") # Read input deck @@ -83,6 +106,15 @@ def read_action(folder, path_dict = getPaths()): return fast_reader.fst_vt def write_action(folder, fst_vt, path_dict = getPaths()): + """ + Function to write the input deck for a given folder + + Args: + folder (str): r-test folder name + fst_vt (dict): OpenFAST VariableTree + path_dict (dict): Dictionary containing the paths. Default is getPaths() + + """ print(f"Writing to {folder}, with TMax = 2.0") # check if the folder exists, if not, mostly being called not from cmake, so create it @@ -107,7 +139,14 @@ def write_action(folder, fst_vt, path_dict = getPaths()): fast_writer.execute() def run_action(folder, path_dict = getPaths()): - # Placeholder for the actual run action + """ + Function to run the simulation for a given folder + + Args: + folder (str): r-test folder name + path_dict (dict): Dictionary containing the paths. Default is getPaths() + + """ print(f"Running simulation for {folder}") command = [f"{path_dict['executable']}", f"{osp.join(path_dict['build_dir'],'openfast_io', folder, f'{folder}.fst')}"] with open(osp.join(path_dict['build_dir'],'openfast_io', folder, f'{folder}.log'), 'w') as f: @@ -115,18 +154,39 @@ def run_action(folder, path_dict = getPaths()): f.close() def check_ascii_out(folder, path_dict = getPaths()): - # Placeholder for the actual check action + """ + Function to read the ASCII output for a given folder + + Args: + folder (str): r-test folder name + path_dict (dict): Dictionary containing the paths. Default is getPaths() + """ print(f"Checking ASCII output for {folder}") asciiOutput = osp.join(path_dict['build_dir'],'openfast_io', folder, f"{folder}.out") fast_outout = FASTOutputFile(filename=asciiOutput) def check_binary_out(folder, path_dict = getPaths()): - # Placeholder for the actual check action + """ + Function to read the binary output for a given folder + + Args: + folder (str): r-test folder name + path_dict (dict): Dictionary containing the paths. Default is getPaths() + """ print(f"Checking binary output for {folder}") binaryOutput = osp.join(path_dict['build_dir'],'openfast_io', folder, f"{folder}.outb") fast_outout = FASTOutputFile(filename=binaryOutput) def check_fst_vt_with_source(folder, path_dict = getPaths()): + + """ + Function to check the fst_vt with the source for a given folder + + Args: + folder (str): r-test folder name + path_dict (dict): Dictionary containing the paths. Default is getPaths() + """ + print(f"Checking the fst_vt with the source for {folder}") # creating the two InputReader_OpenFAST objects @@ -144,7 +204,7 @@ def check_fst_vt_with_source(folder, path_dict = getPaths()): acceptable_diff = [ 'TMax', # TMax is updated in the write_action 'TStart', # TStart is updated in the write_action - 'OutFileFmt', # OutFileFmt is updated in the write_action # TODO check why its not being removed + 'OutFileFmt', # OutFileFmt is updated in the write_action ] @@ -159,6 +219,12 @@ def check_fst_vt_with_source(folder, path_dict = getPaths()): # Begining of the test def test_rtest_cloned(request): + """ + Function to check if the r-tests are cloned properly + + Args: + request (fixture): pytest request + """ REPOSITORY_ROOT = osp.join(request.config.getoption("--source_dir")) path_dict = getPaths(REPOSITORY_ROOT=REPOSITORY_ROOT) @@ -170,6 +236,12 @@ def test_rtest_cloned(request): sys.exit(1) def test_DLLs_exist(request): + """ + Function to check if the DISCON.dll file exists + + Args: + request (fixture): pytest request + """ path_dict = getPaths(OF_PATH=osp.join(request.config.getoption("--build_dir"))) @@ -182,6 +254,12 @@ def test_DLLs_exist(request): sys.exit(1) def test_openfast_executable_exists(request): + """ + Function to check if the OpenFAST executable exists + + Args: + request (fixture): pytest request + """ path_dict = getPaths(OF_PATH=osp.join(request.config.getoption("--executable"))) @@ -195,8 +273,14 @@ def test_openfast_executable_exists(request): # Parameterize the test function to run for each folder and action @pytest.mark.parametrize("folder", FOLDERS_TO_RUN) -# @pytest.mark.parametrize("action_name, action_func", actions) -def test_openfast_io_read_write_run_outRead(folder, request): +def test_openfast_io_read_write_run_readOut_verify(folder, request): + """ + Function to test the read, write, run, check ASCII, check binary, and check fst_vt for a given folder + + Args: + folder (str): r-test folder name + request (fixture): pytest request + """ path_dict = getPaths(OF_PATH=osp.join(request.config.getoption("--executable")), REPOSITORY_ROOT=osp.join(request.config.getoption("--source_dir")), @@ -204,7 +288,6 @@ def test_openfast_io_read_write_run_outRead(folder, request): try: - # action_func(folder) action_name = "read" fst_vt = read_action(folder, path_dict = path_dict) @@ -227,13 +310,17 @@ def test_openfast_io_read_write_run_outRead(folder, request): pytest.fail(f"Action '{action_name}' for folder '{folder}' failed with exception: {e}") def main(): + """ + Main function to run the test for all the folders + """ + # Initialize any necessary setup here for folder in FOLDERS_TO_RUN: print(" ") print(f"Processing folder: {folder}") - # Assuming read_action, write_action, run_action, and check_action are defined elsewhere + # Assuming read_action, write_action, run_action, check_ascii_out, check_binary_out, and check_fst_vt_with_source are defined elsewhere data = read_action(folder) write_action(folder, data) run_action(folder) @@ -243,4 +330,8 @@ def main(): print(f"Successfully processed folder: {folder}") if __name__ == "__main__": + """ + Run the main function if the script is run directly or through VSCode debugger + """ + main() \ No newline at end of file