diff --git a/scripts/ccpp_capgen.py b/scripts/ccpp_capgen.py index af6443b0..984c0260 100755 --- a/scripts/ccpp_capgen.py +++ b/scripts/ccpp_capgen.py @@ -440,17 +440,17 @@ def check_fortran_against_metadata(meta_headers, fort_headers, # end if # end while if fort_headers: - errmsg = "" - sep = "" + errmsgs = [] + estr = "No matching metadata header found for {} in {}" for fheader in fort_headers: if fheader.has_variables: - errmsg += sep + "No matching metadata header found for {} in {}" - errmsg = errmsg.format(fheader.title, mfilename) - sep = "\n" + errmsgs.append(estr.format(fheader.title, mfilename)) # end if # end for - if errmsg: - raise CCPPError(errmsg) + if errmsgs: + mheads = ', '.join([x.name for x in meta_headers]) + errmsgs.append(f'Metadata headers in file: {mheads}') + raise CCPPError('\n'.join(errmsgs)) # end if # end if # We have a one-to-one set, compare headers diff --git a/scripts/fortran_tools/parse_fortran.py b/scripts/fortran_tools/parse_fortran.py index d71e3624..4b3605c1 100644 --- a/scripts/fortran_tools/parse_fortran.py +++ b/scripts/fortran_tools/parse_fortran.py @@ -671,37 +671,36 @@ def parse_fortran_var_decl(line, source, run_env, imports=None): >>> _DUMMY_RUN_ENV = CCPPFrameworkEnv(None, ndict={'host_files':'', \ 'scheme_files':'', \ 'suites':''}) - >>> parse_fortran_var_decl("integer :: foo", ParseSource('foo.F90', 'module', ParseContext()), _DUMMY_RUN_ENV)[0].get_prop_value('local_name') + >>> parse_fortran_var_decl("integer :: foo", ParseSource('foo.F90', 'module', ParseContext()), _DUMMY_RUN_ENV)[0][0].get_prop_value('local_name') 'foo' - >>> parse_fortran_var_decl("integer :: foo = 0", ParseSource('foo.F90', 'module', ParseContext()), _DUMMY_RUN_ENV)[0].get_prop_value('local_name') + >>> parse_fortran_var_decl("integer :: foo = 0", ParseSource('foo.F90', 'module', ParseContext()), _DUMMY_RUN_ENV)[0][0].get_prop_value('local_name') 'foo' - >>> parse_fortran_var_decl("integer :: foo", ParseSource('foo.F90', 'module', ParseContext()), _DUMMY_RUN_ENV)[0].get_prop_value('optional') + >>> parse_fortran_var_decl("integer :: foo", ParseSource('foo.F90', 'module', ParseContext()), _DUMMY_RUN_ENV)[0][0].get_prop_value('optional') False - >>> parse_fortran_var_decl("integer, optional :: foo", ParseSource('foo.F90', 'module', ParseContext()), _DUMMY_RUN_ENV)[0].get_prop_value('optional') + >>> parse_fortran_var_decl("integer, optional :: foo", ParseSource('foo.F90', 'module', ParseContext()), _DUMMY_RUN_ENV)[0][0].get_prop_value('optional') 'True' - >>> parse_fortran_var_decl("integer, dimension(:) :: foo", ParseSource('foo.F90', 'module', ParseContext()), _DUMMY_RUN_ENV)[0].get_prop_value('dimensions') + >>> parse_fortran_var_decl("integer, dimension(:) :: foo", ParseSource('foo.F90', 'module', ParseContext()), _DUMMY_RUN_ENV)[0][0].get_prop_value('dimensions') '(:)' - >>> parse_fortran_var_decl("integer, dimension(:) :: foo(bar)", ParseSource('foo.F90', 'module', ParseContext()), _DUMMY_RUN_ENV)[0].get_prop_value('dimensions') + >>> parse_fortran_var_decl("integer, dimension(:) :: foo(bar)", ParseSource('foo.F90', 'module', ParseContext()), _DUMMY_RUN_ENV)[0][0].get_prop_value('dimensions') '(bar)' - >>> parse_fortran_var_decl("integer, dimension(:) :: foo(:,:), baz", ParseSource('foo.F90', 'module', ParseContext()), _DUMMY_RUN_ENV)[0].get_prop_value('dimensions') + >>> parse_fortran_var_decl("integer, dimension(:) :: foo(:,:), baz", ParseSource('foo.F90', 'module', ParseContext()), _DUMMY_RUN_ENV)[0][0].get_prop_value('dimensions') '(:,:)' - >>> parse_fortran_var_decl("integer, dimension(:) :: foo(:,:), baz", ParseSource('foo.F90', 'module', ParseContext()), _DUMMY_RUN_ENV)[1].get_prop_value('dimensions') + >>> parse_fortran_var_decl("integer, dimension(:) :: foo(:,:), baz", ParseSource('foo.F90', 'module', ParseContext()), _DUMMY_RUN_ENV)[0][1].get_prop_value('dimensions') '(:)' - >>> parse_fortran_var_decl("real (kind=kind_phys), pointer :: phii (:,:) => null() !< interface geopotential height", ParseSource('foo.F90', 'module', ParseContext()), _DUMMY_RUN_ENV)[0].get_prop_value('dimensions') + >>> parse_fortran_var_decl("real (kind=kind_phys), pointer :: phii (:,:) => null() !< interface geopotential height", ParseSource('foo.F90', 'module', ParseContext()), _DUMMY_RUN_ENV)[0][0].get_prop_value('dimensions') '(:,:)' - >>> parse_fortran_var_decl("real(kind=kind_phys), dimension(im, levs, ntrac), intent(in) :: qgrs", ParseSource('foo.F90', 'scheme', ParseContext()), _DUMMY_RUN_ENV)[0].get_prop_value('dimensions') + >>> parse_fortran_var_decl("real(kind=kind_phys), dimension(im, levs, ntrac), intent(in) :: qgrs", ParseSource('foo.F90', 'scheme', ParseContext()), _DUMMY_RUN_ENV)[0][0].get_prop_value('dimensions') '(im, levs, ntrac)' - >>> parse_fortran_var_decl("character(len=*), intent(out) :: errmsg", ParseSource('foo.F90', 'scheme', ParseContext()), _DUMMY_RUN_ENV)[0].get_prop_value('local_name') + >>> parse_fortran_var_decl("character(len=*), intent(out) :: errmsg", ParseSource('foo.F90', 'scheme', ParseContext()), _DUMMY_RUN_ENV)[0][0].get_prop_value('local_name') 'errmsg' - >>> parse_fortran_var_decl("character(len=512), intent(out) :: errmsg", ParseSource('foo.F90', 'scheme', ParseContext()), _DUMMY_RUN_ENV)[0].get_prop_value('kind') + >>> parse_fortran_var_decl("character(len=512), intent(out) :: errmsg", ParseSource('foo.F90', 'scheme', ParseContext()), _DUMMY_RUN_ENV)[0][0].get_prop_value('kind') 'len=512' - >>> parse_fortran_var_decl("real(kind_phys), intent(out) :: foo(8)", ParseSource('foo.F90', 'scheme', ParseContext()), _DUMMY_RUN_ENV)[0].get_prop_value('dimensions') + >>> parse_fortran_var_decl("real(kind_phys), intent(out) :: foo(8)", ParseSource('foo.F90', 'scheme', ParseContext()), _DUMMY_RUN_ENV)[0][0].get_prop_value('dimensions') '(8)' - >>> parse_fortran_var_decl("real(kind_phys), intent(out) :: foo(8)", ParseSource('foo.F90', 'scheme', ParseContext()), _DUMMY_RUN_ENV)[0].get_dimensions() + >>> parse_fortran_var_decl("real(kind_phys), intent(out) :: foo(8)", ParseSource('foo.F90', 'scheme', ParseContext()), _DUMMY_RUN_ENV)[0][0].get_dimensions() ['8'] - >>> parse_fortran_var_decl("character(len=*), intent(out) :: errmsg", ParseSource('foo.F90', 'module', ParseContext()), _DUMMY_RUN_ENV)[0].get_prop_value('local_name') #doctest: +IGNORE_EXCEPTION_DETAIL - Traceback (most recent call last): - ParseSyntaxError: Invalid variable declaration, character(len=*), intent(out) :: errmsg, intent not allowed in module variable, in + >>> parse_fortran_var_decl("character(len=*), intent(out) :: errmsg", ParseSource('foo.F90', 'module', ParseContext()), _DUMMY_RUN_ENV)[1][0] + 'Syntax error: Invalid variable declaration, character(len=*), intent(out) :: errmsg, intent not allowed in module variable, in ' ## NB: Expressions (including function calls) not currently supported here #>>> parse_fortran_var_decl("real(kind_phys), intent(out) :: foo(size(bar))", ParseSource('foo.F90', 'scheme', ParseContext()), _DUMMY_RUN_ENV)[0].get_prop_value('dimensions') @@ -714,7 +713,9 @@ def parse_fortran_var_decl(line, source, run_env, imports=None): sline = sline[0:sline.index('!')].rstrip() # end if tobject = ftype_factory(sline, context) - newvars = list() + newvars = [] + errors = [] + errtyp = "Syntax error" if tobject is not None: varprops = sline[tobject.type_len:].strip() def_dims = None # Default dimensions @@ -728,17 +729,14 @@ def parse_fortran_var_decl(line, source, run_env, imports=None): if prop[0:6] == 'intent': if source.ptype != 'scheme': typ = source.ptype - errmsg = 'Invalid variable declaration, {}, intent' - errmsg = errmsg + ' not allowed in {} variable' + ctx = context_string(context) + emsg1 = f"Invalid variable declaration, {sline}, " + emsg2 = f"intent not allowed in {typ} variable" + errmsg = f"{errtyp}: {emsg1}{emsg2}{ctx}" if run_env.logger is not None: - ctx = context_string(context) - errmsg = "WARNING: " + errmsg + "{}" - run_env.logger.warning(errmsg.format(sline, - typ, ctx)) - else: - raise ParseSyntaxError(errmsg.format(sline, typ), - context=context) + run_env.logger.warning(errmsg) # end if + errors.append(errmsg) else: intent = prop[6:].strip()[1:-1].strip() # end if @@ -771,14 +769,12 @@ def parse_fortran_var_decl(line, source, run_env, imports=None): varname = var[0:ploc].strip() begin, end = check_balanced_paren(var) if (begin < 0) or (end < 0): + ctx = context_string(context) + errmsg = f"{errtyp}: Invalid variable declaration, {var}{ctx}" if run_env.logger is not None: - ctx = context_string(context) - errmsg = "WARNING: Invalid variable declaration, {}{}" - run_env.logger.warning(errmsg.format(var, ctx)) - else: - raise ParseSyntaxError('variable declaration', - token=var, context=context) + run_env.logger.warning(errmsg) # end if + errors.append(errmsg) else: dimspec = var[begin:end+1] # end if @@ -813,12 +809,17 @@ def parse_fortran_var_decl(line, source, run_env, imports=None): # XXgoldyXX: I am nervous about allowing invalid Var objects here # Also, this tends to cause an exception that ends up back here # which is not a good idea. - var = FortranVar(prop_dict, source, run_env, fortran_imports=imports) - newvars.append(var) + try: + var = FortranVar(prop_dict, source, run_env, + fortran_imports=imports) + newvars.append(var) + except ParseSyntaxError as perr: + errors.append(perr) + # end try # end for # No else (not a variable declaration) # end if - return newvars + return newvars, errors ######################################################################## diff --git a/scripts/fortran_tools/parse_fortran_file.py b/scripts/fortran_tools/parse_fortran_file.py index 1b722c38..9808f6c8 100644 --- a/scripts/fortran_tools/parse_fortran_file.py +++ b/scripts/fortran_tools/parse_fortran_file.py @@ -73,6 +73,14 @@ def line_statements(line): [" ! This is a comment statement; y'all;"] >>> line_statements("!! ") ['!! '] + >>> line_statements("real(kind_phys), intent(in) :: good_arr2(:,:)") + ['real(kind_phys), intent(in) :: good_arr2(:,:)'] + >>> line_statements("real(kind_phys), intent(in) :: bad_arr1(:,;)") + ['real(kind_phys), intent(in) :: bad_arr1(:,;)'] + >>> line_statements("real(kind_phys), intent(in), dimension(;,:) :: bad_arr2") + ['real(kind_phys), intent(in), dimension(;,:) :: bad_arr2'] + >>> line_statements("real(kind_phys), intent(in), dimension(:,;) :: bad_arr3") + ['real(kind_phys), intent(in), dimension(:,;) :: bad_arr3'] """ statements = list() ind_start = 0 @@ -80,6 +88,7 @@ def line_statements(line): line_len = len(line) in_single_char = False in_double_char = False + in_paren = 0 while ind_end < line_len: if in_single_char: if line[ind_end] == "'": @@ -94,9 +103,13 @@ def line_statements(line): elif line[ind_end] == '"': in_double_char = True elif line[ind_end] == '!': - # Commend in non-character context, suck in rest of line + # Comment in non-character context, suck in rest of line ind_end = line_len - 1 - elif line[ind_end] == ';': + elif line[ind_end] == '(': + in_paren += 1 + elif line[ind_end] == ')': + in_paren = max(in_paren - 1, 0) + elif (line[ind_end] == ';') and (in_paren < 1): # The whole reason for this routine, the statement separator if ind_end > ind_start: statements.append(line[ind_start:ind_end]) @@ -495,6 +508,7 @@ def parse_type_def(statements, type_def, mod_name, pobj, run_env, imports=None): mheader = None var_dict = VarDictionary(type_def[0], run_env) inspec = True + errors = [] while inspec and (statements is not None): while len(statements) > 0: statement = statements.pop(0) @@ -512,8 +526,10 @@ def parse_type_def(statements, type_def, mod_name, pobj, run_env, imports=None): # Comment of variable if ((not is_comment_statement(statement)) and (not parse_use_statement(statement, run_env.logger))): - dvars = parse_fortran_var_decl(statement, psrc, run_env, - imports=imports) + dvars, errs = parse_fortran_var_decl(statement, psrc, + run_env, + imports=imports) + errors.extend(errs) for var in dvars: var_dict.add_variable(var, run_env) # end for @@ -527,16 +543,19 @@ def parse_type_def(statements, type_def, mod_name, pobj, run_env, imports=None): statements = read_statements(pobj) # end if # end while - return statements, mheader + return statements, mheader, errors ######################################################################## def parse_preamble_data(statements, pobj, spec_name, endmatch, imports, run_env): """Parse module variables or DDT definitions from a module preamble or parse program variables from the beginning of a program. + Returns remaining statements, parsed metadata headers, and + any accumulated errors """ inspec = True - mheaders = list() + mheaders = [] + errors = [] var_dict = VarDictionary(spec_name, run_env) psrc = ParseSource(spec_name, 'MODULE', pobj) active_table = None @@ -577,9 +596,10 @@ def parse_preamble_data(statements, pobj, spec_name, endmatch, imports, run_env) statements.insert(0, statement) if ((active_table is not None) and (type_def[0].lower() == active_table.lower())): - statements, ddt = parse_type_def(statements, type_def, - spec_name, pobj, run_env, - imports=imports) + statements, ddt, errors = parse_type_def(statements, + type_def, spec_name, + pobj, run_env, + imports=imports) if ddt is None: ctx = context_string(pobj, nodir=True) msg = "No DDT found at '{}'{}" @@ -603,7 +623,9 @@ def parse_preamble_data(statements, pobj, spec_name, endmatch, imports, run_env) if ((not is_comment_statement(statement)) and (not parse_use_statement(statement, run_env.logger)) and (active_table.lower() == spec_name.lower())): - dvars = parse_fortran_var_decl(statement, psrc, run_env) + dvars, errs = parse_fortran_var_decl(statement, + psrc, run_env) + errors.extend(errs) for var in dvars: var_dict.add_variable(var, run_env) # end for @@ -614,7 +636,7 @@ def parse_preamble_data(statements, pobj, spec_name, endmatch, imports, run_env) statements = read_statements(pobj) # end if # end while - return statements, mheaders + return statements, mheaders, errors ######################################################################## @@ -624,6 +646,8 @@ def parse_scheme_metadata(statements, pobj, spec_name, table_name, run_env): mheader = None var_dict = None scheme_name = None + errors = [] + etyp = "Syntax error" # Find the subroutine line, should be first executable statement inpreamble = False insub = True @@ -683,9 +707,10 @@ def parse_scheme_metadata(statements, pobj, spec_name, table_name, run_env): raise ParseInternalError(errmsg.format(pobj)) # end if if arg in vdict: - errmsg = 'Duplicate dummy argument, {}' - raise ParseSyntaxError(errmsg.format(arg), - context=pobj) + ctx = context_string(pobj) + errors.append(f"Duplicate dummy argument, {arg}{ctx}") + else: + vdict[arg] = None # end if vdict[arg] = None # end for @@ -701,19 +726,25 @@ def parse_scheme_metadata(statements, pobj, spec_name, table_name, run_env): ((not is_comment_statement(statement)) and (not parse_use_statement(statement, run_env)) and is_dummy_argument_statement(statement))): - dvars = parse_fortran_var_decl(statement, psrc, run_env) + dvars, errs = parse_fortran_var_decl(statement, + psrc, run_env) + for err in errs: + # err might be an Exception instead of a string + errors.append(str(err)) + # end for for var in dvars: lname = var.get_prop_value('local_name').lower() if lname in vdict: if vdict[lname] is not None: - emsg = "Error: duplicate dummy argument, {}" - raise ParseSyntaxError(emsg.format(lname), - context=pobj) + ctx = context_string(pobj) + errors.append(f"ERROR: Duplicate dummy argument, {lname}{ctx}") + else: + vdict[lname] = var # end if - vdict[lname] = var else: - raise ParseSyntaxError('dummy argument', - token=lname, context=pobj) + ctx = context_string(pobj) + emsg = f"{etyp}: Invalid dummy argument, '{lname}'{ctx}" + errors.append(emsg) # end if # end for # end if @@ -724,7 +755,7 @@ def parse_scheme_metadata(statements, pobj, spec_name, table_name, run_env): # end if # end while # Check for missing declarations - missing = list() + missing = [] if vdict is None: errmsg = 'Subroutine, {}, not found{}' raise CCPPError(errmsg.format(scheme_name, ctx)) @@ -734,9 +765,12 @@ def parse_scheme_metadata(statements, pobj, spec_name, table_name, run_env): missing.append(lname) # end if # end for + for lname in missing: + del vdict[lname] + # end for if len(missing) > 0: - errmsg = 'Missing local_variables, {} in {}' - raise CCPPError(errmsg.format(missing, scheme_name)) + errmsg = f"Missing local_variables, {missing} in {scheme_name}" + errors.append(errmsg) # end if var_dict = VarDictionary(scheme_name, run_env, variables=vdict) if (scheme_name is not None) and (var_dict is not None): @@ -744,7 +778,7 @@ def parse_scheme_metadata(statements, pobj, spec_name, table_name, run_env): table_type_in='scheme', module=spec_name, var_dict=var_dict) # end if - return statements, mheader + return statements, mheader, errors ######################################################################## @@ -792,7 +826,8 @@ def parse_specification(pobj, statements, imports, run_env, mod_name=None, # end if inspec = True - mtables = list() + mtables = [] + errors = [] while inspec and (statements is not None): while len(statements) > 0: statement = statements.pop(0) @@ -808,24 +843,26 @@ def parse_specification(pobj, statements, imports, run_env, mod_name=None, elif asmatch is not None: # Put table statement back to re-read statements.insert(0, statement) - statements, new_tbls = parse_preamble_data(statements, - pobj, spec_name, - endmatch, imports, - run_env) + statements, new_tbls, errors = parse_preamble_data(statements, + pobj, + spec_name, + endmatch, + imports, + run_env) for tbl in new_tbls: title = tbl.table_name if title in mtables: - errmsg = duplicate_header(mtables[title], tbl) - raise CCPPError(errmsg) - # end if - if run_env.verbose: - ctx = tbl.start_context() - mtype = tbl.table_type - msg = "Adding metadata from {}, {}{}" - run_env.logger.debug(msg.format(mtype, title, ctx)) + errors.append(duplicate_header(mtables[title], tbl)) + else: + if run_env.verbose: + ctx = tbl.start_context() + mtype = tbl.table_type + msg = "Adding metadata from {}, {}{}" + run_env.logger.debug(msg.format(mtype, title, ctx)) + # End if + mtables.append(tbl) # end if - mtables.append(tbl) - # end if + # end for inspec = pobj.in_region('MODULE', region_name=mod_name) break elif type_def: @@ -851,7 +888,7 @@ def parse_specification(pobj, statements, imports, run_env, mod_name=None, statements = read_statements(pobj) # end if # end while - return statements, mtables + return statements, mtables, errors ######################################################################## @@ -872,8 +909,12 @@ def parse_program(pobj, statements, run_env): # end if # After the program name is the specification part imports = set() - statements, mtables = parse_specification(pobj, statements[1:], imports, - run_env, prog_name=prog_name) + statements, mtables, errors = parse_specification(pobj, statements[1:], + imports, run_env, + prog_name=prog_name) + if errors: + raise CCPPError('\n'.join(errors)) + # end if # We really cannot have tables inside a program's executable section # Just read until end statements = read_statements(pobj, statements) @@ -900,6 +941,8 @@ def parse_program(pobj, statements, run_env): def parse_module(pobj, statements, run_env): """Parse a Fortran MODULE and return any leftover statements and metadata tables encountered in the MODULE.""" + # Collect errors for more efficient error reporting + errors = [] # Collect any imported typedef (and other) names imports = set() # The first statement should be a module statement, grab the name @@ -915,8 +958,12 @@ def parse_module(pobj, statements, run_env): run_env.logger.debug(msg.format(mod_name, ctx)) # end if # After the module name is the specification part - statements, mtables = parse_specification(pobj, statements[1:], imports, - run_env, mod_name=mod_name) + statements, mtables, errs = parse_specification(pobj, statements[1:], + imports, run_env, + mod_name=mod_name) + if errs: + errors.extend(errs) + # end if # Look for metadata tables statements = read_statements(pobj, statements) inmodule = pobj.in_region('MODULE', region_name=mod_name) @@ -942,10 +989,12 @@ def parse_module(pobj, statements, run_env): inmodule = False break elif active_table is not None: - statements, mheader = parse_scheme_metadata(statements, pobj, - mod_name, - active_table, - run_env) + statements, mheader, errs = parse_scheme_metadata(statements, + pobj, + mod_name, + active_table, + run_env) + errors.extend(errs) if mheader is not None: title = mheader.table_name if title in mtables: @@ -983,6 +1032,9 @@ def parse_module(pobj, statements, run_env): statements = read_statements(pobj) # end if # end while + if errors: + raise CCPPError('\n'.join(errors)) + # end if return statements, mtables, additional_subroutines ######################################################################## diff --git a/test/unit_tests/sample_files/fortran_files/array_parsing_test.F90 b/test/unit_tests/sample_files/fortran_files/array_parsing_test.F90 new file mode 100644 index 00000000..493ab43d --- /dev/null +++ b/test/unit_tests/sample_files/fortran_files/array_parsing_test.F90 @@ -0,0 +1,33 @@ +!Test array specifications +! + +MODULE array_spec_test + + USE ccpp_kinds, ONLY: kind_phys + + IMPLICIT NONE + PRIVATE + + PUBLIC :: array_spec_test_run + +CONTAINS + + !> \section arg_table_array_spec_test_run Argument Table + !! \htmlinclude arg_table_array_spec_test_run.html + !! + SUBROUTINE array_spec_test_run(ncol, lev, good_arr1, good_arr2, good_arr3, & + good_arr4, good_arr5, bad_arr1, bad_arr2, bad_arr3) + + integer, intent(in) :: ncol, lev + real(kind_phys), intent(in) :: good_arr1(ncol,lev) + real(kind_phys), intent(in) :: good_arr2(:,:) + real(kind_phys), intent(in), dimension(ncol,lev) :: good_arr3 + real(kind_phys), intent(in), dimension(:,:) :: good_arr4 + real(kind_phys), intent(in) :: good_arr5(ncol,:) + real(kind_phys), intent(in) :: bad_arr1(:,;) + real(kind_phys), intent(in), dimension(;,:) :: bad_arr2 + real(kind_phys), intent(in), dimension(:,;) :: bad_arr3 + + END SUBROUTINE array_spec_test_run + +END MODULE array_spec_test diff --git a/test/unit_tests/test_fortran_parse.py b/test/unit_tests/test_fortran_parse.py new file mode 100644 index 00000000..52696cd5 --- /dev/null +++ b/test/unit_tests/test_fortran_parse.py @@ -0,0 +1,101 @@ +#! /usr/bin/env python3 +""" +----------------------------------------------------------------------- + Description: Contains unit tests for parsing Fortran + in scripts files scripts/fortran_tools/parse_fortran_file.py and + scripts/fortran_tools/parse_fortran.py + + Assumptions: + + Command line arguments: none + + Usage: python3 test_fortran_parse.py # run the unit tests +----------------------------------------------------------------------- +""" + +import os +import sys +import logging +import unittest + +_TEST_DIR = os.path.dirname(os.path.abspath(__file__)) +_SCRIPTS_DIR = os.path.abspath(os.path.join(_TEST_DIR, os.pardir, + os.pardir, "scripts")) +_SAMPLE_FILES_DIR = os.path.join(_TEST_DIR, "sample_files", "fortran_files") +_PRE_TMP_DIR = os.path.join(_TEST_DIR, "tmp") +_TMP_DIR = os.path.join(_PRE_TMP_DIR, "fortran_files") + +if not os.path.exists(_SCRIPTS_DIR): + raise ImportError(f"Cannot find scripts directory, {_SCRIPTS_DIR}") + +sys.path.append(_SCRIPTS_DIR) + +# pylint: disable=wrong-import-position +from fortran_tools import parse_fortran_file +from framework_env import CCPPFrameworkEnv +# pylint: enable=wrong-import-position + +############################################################################### +def remove_files(file_list): +############################################################################### + """Remove files in if they exist""" + if isinstance(file_list, str): + file_list = [file_list] + # end if + for fpath in file_list: + if os.path.exists(fpath): + os.remove(fpath) + # End if + # End for + +class FortranParseTestCase(unittest.TestCase): + + """Tests for `parse_fortran_file`.""" + + _run_env = None + + @classmethod + def setUpClass(cls): + """Clean output directory (tmp) before running tests""" + # Does "tmp" directory exist? If not then create it: + if not os.path.exists(_PRE_TMP_DIR): + os.makedirs(_PRE_TMP_DIR) + # end if + + # We need a run environment + logger = logging.getLogger(cls.__name__) + cls._run_env = CCPPFrameworkEnv(logger, ndict={'host_files':'', + 'scheme_files':'', + 'suites':''}) + + # Run inherited setup method: + super().setUpClass() + + def test_array_parsing(self): + """Test that the Fortran parser outputs an informative + error message for a badly formatted array specification. + Also, test that allowed specification strings are allowed. + """ + # Setup + testname = "array_parsing_test" + source = os.path.join(_SAMPLE_FILES_DIR, f"{testname}.F90") + # Exercise + header = "Test of parsing of Fortran array specification" + + with self.assertRaises(Exception) as context: + # Parse the file + _ = parse_fortran_file(source, self._run_env) + # end if + + # Check exception for expected error messages + self.assertTrue("bad_arr1: ';' is not a valid Fortran identifier" + in str(context.exception)) + self.assertTrue("bad_arr2: ';' is not a valid Fortran identifier" + in str(context.exception)) + self.assertTrue("bad_arr3: ';' is not a valid Fortran identifier" + in str(context.exception)) + self.assertTrue("Missing local_variables, ['bad_arr1', 'bad_arr2', 'bad_arr3'] in array_spec_test_run" + in str(context.exception)) + +if __name__ == "__main__": + unittest.main() diff --git a/test/unit_tests/test_fortran_write.py b/test/unit_tests/test_fortran_write.py index 558db73d..c2248896 100644 --- a/test/unit_tests/test_fortran_write.py +++ b/test/unit_tests/test_fortran_write.py @@ -47,7 +47,7 @@ def remove_files(file_list): # End if # End for -class MetadataTableTestCase(unittest.TestCase): +class FortranWriterTestCase(unittest.TestCase): """Tests for `FortranWriter`."""