def test_harvested_data(self, caplog, tmp_path): """ Checks that the analyser deals with rescanning a file. """ caplog.set_level(logging.DEBUG) first_file: Path = tmp_path / 'other.F90' first_file.write_text( dedent(''' program betty use barney_mod, only :: dino implicit none end program betty module barney_mod end module barney_mod ''')) second_file: Path = tmp_path / 'test.f90' second_file.write_text( dedent(''' module barney_mod end module barney_mod ''')) database: SqliteStateDatabase = SqliteStateDatabase(tmp_path) test_unit = FortranAnalyser(tmp_path) first_artifact = Artifact(first_file, FortranSource, Raw) second_artifact = Artifact(second_file, FortranSource, Raw) # Not going to test returned objects this time _ = test_unit.run([first_artifact]) _ = test_unit.run([second_artifact]) # Confirm the database has been updated fdb = FortranWorkingState(database) assert list(iter(fdb)) \ == [FortranInfo(FortranUnitID('barney_mod', first_file)), FortranInfo(FortranUnitID('barney_mod', second_file)), FortranInfo(FortranUnitID('betty', first_file), ['barney_mod'])] assert list(fdb.depends_on(FortranUnitID('betty', first_file))) \ == [FortranUnitID('barney_mod', tmp_path / 'other.F90'), FortranUnitID('barney_mod', tmp_path / 'test.f90')] # Repeat the scan of second_file, there should be no change. # _ = test_unit.run([second_artifact]) fdb = FortranWorkingState(database) assert list(iter(fdb)) \ == [FortranInfo(FortranUnitID('barney_mod', first_file)), FortranInfo(FortranUnitID('barney_mod', second_file)), FortranInfo(FortranUnitID('betty', first_file), ['barney_mod'])] assert list(fdb.depends_on(FortranUnitID('betty', first_file))) \ == [FortranUnitID('barney_mod', tmp_path / 'other.F90'), FortranUnitID('barney_mod', tmp_path / 'test.f90')]
def test_mismatched_end_name(self, tmp_path: Path): """ Ensure that the analyser handles mismatched block end names correctly. """ test_file: Path = tmp_path / 'test.f90' test_file.write_text( dedent(''' module wibble_mod type :: thing_type end type blasted_type end module wibble_mod ''')) test_unit = FortranAnalyser(tmp_path) test_artifact = Artifact(test_file, FortranSource, Raw) with pytest.raises(TaskException): test_unit.run([test_artifact])
def test_empty_file(self): mock_tree = Mock(content=[None]) with mock.patch('fab.tasks.fortran.FortranAnalyser._parse_file', return_value=mock_tree): result = FortranAnalyser().run( HashedFile(fpath=None, file_hash=None)) assert type(result) is EmptySourceFile
def __init__(self, workspace: Path, target: str, exec_name: str, fpp_flags: str, fc_flags: str, ld_flags: str, n_procs: int): self._workspace = workspace if not workspace.exists(): workspace.mkdir(parents=True) self._state = SqliteStateDatabase(workspace) # Path maps tell the engine what filetype and starting state # the Artifacts representing any files encountered by the # initial descent should have path_maps = [ PathMap(r'.*\.f90', FortranSource, Raw), PathMap(r'.*\.F90', FortranSource, Seen), PathMap(r'.*\.c', CSource, Seen), PathMap(r'.*\.h', CHeader, Seen), ] # Initialise the required Tasks, providing them with any static # properties such as flags to use, workspace location etc # TODO: Eventually the tasks may instead access many of these # properties via the configuration (at Task runtime, to allow for # file-specific overrides?) fortran_preprocessor = FortranPreProcessor( 'cpp', ['-traditional-cpp', '-P'] + fpp_flags.split(), workspace) fortran_analyser = FortranAnalyser(workspace) fortran_compiler = FortranCompiler( 'gfortran', ['-c', '-J', str(workspace)] + fc_flags.split(), workspace) header_analyser = HeaderAnalyser(workspace) c_pragma_injector = CPragmaInjector(workspace) c_preprocessor = CPreProcessor('cpp', [], workspace) c_analyser = CAnalyser(workspace) c_compiler = CCompiler('gcc', ['-c'], workspace) linker = Linker('gcc', ['-lc', '-lgfortran'] + ld_flags.split(), workspace, exec_name) # The Task map tells the engine what Task it should be using # to deal with Artifacts depending on their type and state task_map = { (FortranSource, Seen): fortran_preprocessor, (FortranSource, Raw): fortran_analyser, (FortranSource, Analysed): fortran_compiler, (CSource, Seen): header_analyser, (CHeader, Seen): header_analyser, (CSource, HeadersAnalysed): c_pragma_injector, (CHeader, HeadersAnalysed): c_pragma_injector, (CSource, Modified): c_preprocessor, (CSource, Raw): c_analyser, (CSource, Analysed): c_compiler, (BinaryObject, Compiled): linker, } engine = Engine(workspace, target, path_maps, task_map) self._queue = QueueManager(n_procs - 1, engine)
def test_naked_use(self, tmp_path): """ Ensures that an exception is raised if a "use" is found outside a program unit. """ test_file: Path = tmp_path / 'test.f90' test_file.write_text( dedent(''' use beef_mod module test_mod end module test_mod ''')) test_unit = FortranAnalyser(tmp_path) test_artifact = Artifact(test_file, FortranSource, Raw) with pytest.raises(TaskException): test_unit.run([test_artifact])
def __init__( self, source: ArtefactsGetter = None, root_symbol: Optional[Union[ str, List[str]]] = None, # todo: iterable is more correct std="f2008", special_measure_analysis_results=None, unreferenced_deps=None, ignore_mod_deps=None, name='analyser'): """ If no artefact getter is specified in *source*, a default is used which provides input files from multiple artefact collections, including the default C and Fortran preprocessor outputs and any source files with a 'little' *.f90* extension. A build tree is produced for every root symbol specified in *root_symbol*, which can be a string or list of. This is how we create executable files. If no root symbol is specified, a single tree of the entire source is produced (with a root symbol of `None`). This is how we create shared and static libraries. :param source: An :class:`~fab.util.ArtefactsGetter` to get the source files. :param root_symbol: When building an executable, provide the Fortran Program name(s), or 'main' for C. If None, build tree extraction will not be performed and the entire source will be used as the build tree - for building a shared or static library. :param std: The fortran standard, passed through to fparser2. Defaults to 'f2008'. :param special_measure_analysis_results: When fparser2 cannot parse a "valid" Fortran file, we can manually provide the expected analysis results with this argument. Only the symbol definitions and dependencies need be provided. :param unreferenced_deps: A list of symbols which are needed for the build, but which cannot be automatically determined. For example, functions that are called without a module use statement. Assuming the files containing these symbols are present and will be analysed, those files and all their dependencies will be added to the build tree(s). :param ignore_mod_deps: Third party Fortran module names to be ignored. :param name: Human friendly name for logger output, with sensible default. """ super().__init__(name) self.source_getter = source or DEFAULT_SOURCE_GETTER self.root_symbols: Optional[List[str]] = [root_symbol] if isinstance( root_symbol, str) else root_symbol self.special_measure_analysis_results: List[ AnalysedFile] = special_measure_analysis_results or [] self.unreferenced_deps: List[str] = unreferenced_deps or [] # todo: these seem more like functions self.fortran_analyser = FortranAnalyser( std=std, ignore_mod_deps=ignore_mod_deps) self.c_analyser = CAnalyser()
def test_program_file(self, module_fpath, module_expected): # same as test_real_file() but replacing MODULE with PROGRAM with NamedTemporaryFile(mode='w+t', suffix='.f90') as tmp_file: tmp_file.write(module_fpath.open().read().replace( "MODULE", "PROGRAM")) tmp_file.flush() result = FortranAnalyser().run( HashedFile(fpath=Path(tmp_file.name), file_hash=None)) module_expected.fpath = Path(tmp_file.name) module_expected.module_defs = set() module_expected.symbol_defs.update( {'internal_sub', 'internal_func'}) assert result == module_expected
def test_analyser_scope(self, caplog, tmp_path): """ Tests that the analyser is able to track scope correctly. """ caplog.set_level(logging.DEBUG) test_file: Path = tmp_path / 'test.f90' test_file.write_text( dedent(''' program fred implicit none if (something) then named: do i=1, 10 end do named endif contains subroutine yabadabadoo() end end program module barney implicit none type betty_type integer :: property contains procedure inspect end type interface betty_type procedure betty_constructor end contains function inspect(this) class(betty_type), intent(in) :: this integer :: inspect inspect = this%property end function inspect end module ''')) database: SqliteStateDatabase = SqliteStateDatabase(tmp_path) test_unit = FortranAnalyser(tmp_path) test_artifact = Artifact(test_file, FortranSource, Raw) output_artifacts = test_unit.run([test_artifact]) # Confirm database is updated working_state = FortranWorkingState(database) assert list(working_state) \ == [FortranInfo(FortranUnitID('barney', tmp_path/'test.f90'), []), FortranInfo(FortranUnitID('fred', tmp_path/'test.f90'), [])] # Confirm returned Artifact is updated assert len(output_artifacts) == 1 assert output_artifacts[0].defines == ['fred', 'barney'] assert output_artifacts[0].depends_on == [] assert output_artifacts[0].location == test_file assert output_artifacts[0].filetype is FortranSource assert output_artifacts[0].state is Analysed
def test_analyser_program_units(self, caplog, tmp_path): """ Tests that program units and the "uses" they contain are correctly identified. """ caplog.set_level(logging.DEBUG) test_file: Path = tmp_path / 'test.f90' test_file.write_text( dedent(''' program foo use iso_fortran_env, only : output use, intrinsic :: ios_c_binding use beef_mod implicit none end program foo module bar use iso_fortran_env, only : output use, intrinsic :: ios_c_binding use cheese_mod, only : bits_n_bobs implicit none end module bar function baz(first, second) use iso_fortran_env, only : output use, intrinsic :: ios_c_binding use teapot_mod implicit none end function baz subroutine qux() use iso_fortran_env, only : output use, intrinsic :: ios_c_binding use wibble_mod use wubble_mod, only: stuff_n_nonsense implicit none end subroutine qux ''')) database: SqliteStateDatabase = SqliteStateDatabase(tmp_path) test_unit = FortranAnalyser(tmp_path) test_artifact = Artifact(test_file, FortranSource, Raw) output_artifacts = test_unit.run([test_artifact]) # Confirm database is updated working_state = FortranWorkingState(database) assert list(working_state) \ == [FortranInfo(FortranUnitID('bar', tmp_path/'test.f90'), ['cheese_mod']), FortranInfo(FortranUnitID('baz', tmp_path/'test.f90'), ['teapot_mod']), FortranInfo(FortranUnitID('foo', tmp_path/'test.f90'), ['beef_mod']), FortranInfo(FortranUnitID('qux', tmp_path/'test.f90'), ['wibble_mod', 'wubble_mod'])] # Confirm returned Artifact is updated assert len(output_artifacts) == 1 assert output_artifacts[0].defines == ['foo', 'bar', 'baz', 'qux'] assert output_artifacts[0].depends_on == [ 'beef_mod', 'cheese_mod', 'teapot_mod', 'wibble_mod', 'wubble_mod' ] assert output_artifacts[0].location == test_file assert output_artifacts[0].filetype is FortranSource assert output_artifacts[0].state is Analysed
def test_module_file(self, module_fpath, module_expected): result = FortranAnalyser().run( HashedFile(fpath=module_fpath, file_hash=None)) assert result == module_expected
def test_analyser_cbinding(self, caplog, tmp_path): """ Tests that C bind procedures are correctly detected. """ caplog.set_level(logging.DEBUG) test_file: Path = tmp_path / 'test.f90' test_file.write_text( dedent(''' module foo integer, bind(c), target, save :: quuz real, bind(c, name="corge"), target, save :: varname function bar() bind(c, name="bar_c") implicit none end function bar subroutine baz(), bind(c) implicit none end subroutine baz interface function qux() bind(c, name="qux_c") implicit none end function qux subroutine quux() bind(c) implicit none end subroutine quux end interface end module foo ''')) database: SqliteStateDatabase = SqliteStateDatabase(tmp_path) test_unit = FortranAnalyser(tmp_path) test_artifact = Artifact(test_file, FortranSource, Raw) output_artifacts = test_unit.run([test_artifact]) # Confirm database is updated # Fortran part working_state = FortranWorkingState(database) assert list(working_state) \ == [FortranInfo(FortranUnitID('foo', tmp_path/'test.f90'), ['quux', 'qux_c'])] # C part cworking_state = CWorkingState(database) assert list(cworking_state) \ == [CInfo(CSymbolID('bar_c', tmp_path/'test.f90'), []), CInfo(CSymbolID('baz', tmp_path/'test.f90'), []), CInfo(CSymbolID('corge', tmp_path/'test.f90'), []), CInfo(CSymbolID('quuz', tmp_path/'test.f90'), [])] # Confirm returned Artifact is updated assert len(output_artifacts) == 1 assert output_artifacts[0].defines \ == ['foo', 'quuz', 'corge', 'bar_c', 'baz'] assert output_artifacts[0].depends_on == ['qux_c', 'quux'] assert output_artifacts[0].location == test_file assert output_artifacts[0].filetype is FortranSource assert output_artifacts[0].state is Analysed