def test_equality(self): test_unit = CSymbolID('salt', Path('pepper')) with pytest.raises(TypeError): _ = test_unit == 'Not a CSymbolID' other = CSymbolID('salt', Path('pepper')) assert test_unit == other assert other == test_unit other = CSymbolID('stew', Path('dumplings')) assert test_unit != other assert other != test_unit
def test_equality(self): test_unit \ = CInfo(CSymbolID('argle', Path('bargle/wargle.gargle')), ['beef', 'cheese']) with pytest.raises(TypeError): _ = test_unit == 'not a CInfo' other = CInfo(CSymbolID('argle', Path('bargle/wargle.gargle')), ['beef', 'cheese']) assert test_unit == other assert other == test_unit other = CInfo(CSymbolID('argle', Path('bargle/wargle.gargle'))) assert test_unit != other assert other != test_unit
def test_default_constructor(self): test_unit \ = CInfo(CSymbolID('argle', Path('bargle/wargle.gargle'))) assert test_unit.symbol.name == 'argle' assert test_unit.symbol.found_in == Path('bargle/wargle.gargle') assert test_unit.depends_on == []
def test_prereq_constructor(self): test_unit \ = CInfo(CSymbolID('argle', Path('bargle/wargle.gargle')), ['cheese']) assert test_unit.symbol.name == 'argle' assert test_unit.symbol.found_in == Path('bargle/wargle.gargle') assert test_unit.depends_on == ['cheese']
def test_add_prerequisite(self): test_unit \ = CInfo(CSymbolID('argle', Path('bargle/wargle.gargle'))) assert test_unit.depends_on == [] test_unit.add_prerequisite('cheese') assert test_unit.depends_on == ['cheese']
def test_analyser_symbols(self, caplog, tmp_path): """ Tests that symbols are identified, and calls are picked up provided they come from internal headers. """ caplog.set_level(logging.DEBUG) test_file: Path = tmp_path / 'test.c' test_file.write_text( dedent(''' #pragma FAB UsrIncludeStart void foo(); #pragma FAB UsrIncludeEnd #pragma FAB UsrIncludeStart void bar(); #pragma FAB UsrIncludeEnd #pragma FAB SysIncludeStart void baz(); #pragma FAB SysIncludeEnd void foo() { bar(); baz(); } ''')) database: SqliteStateDatabase = SqliteStateDatabase(tmp_path) test_unit = CAnalyser(tmp_path) test_artifact = Artifact(test_file, CSource, Raw) output_artifacts = test_unit.run([test_artifact]) # Confirm database is updated working_state = CWorkingState(database) assert list(working_state) \ == [CInfo(CSymbolID('foo', test_file), ['bar'])] # Confirm returned Artifact is updated assert len(output_artifacts) == 1 assert output_artifacts[0].defines == ['foo'] assert output_artifacts[0].depends_on == ['bar'] assert output_artifacts[0].location == test_file assert output_artifacts[0].filetype is CSource assert output_artifacts[0].state is Analysed
def test_hash(self): test_unit = CSymbolID('grumper', Path('bumper')) similar = CSymbolID('grumper', Path('bumper')) different = CSymbolID('bumper', Path('grumper')) assert hash(test_unit) == hash(similar) assert hash(test_unit) != hash(different)
def test_constructor(self): test_unit = CSymbolID('beef', Path('cheese')) assert test_unit.name == 'beef' assert test_unit.found_in == Path('cheese')
def test_get_symbol(self, tmp_path: Path): database = SqliteStateDatabase(tmp_path) test_unit = CWorkingState(database) # Test on an empty list # with pytest.raises(WorkingStateException): _ = test_unit.get_symbol('tigger') # Test we can retrieve an item from a single element list test_unit.add_c_symbol(CSymbolID('tigger', Path('tigger.c'))) assert test_unit.get_symbol('tigger') \ == [CInfo(CSymbolID('tigger', Path('tigger.c')))] with pytest.raises(WorkingStateException): _ = test_unit.get_symbol('eeor') # Test retrieval from a multi-element list and with prerequisites. # test_unit.add_c_symbol(CSymbolID('eeor', Path('eeor.c'))) test_unit.add_c_dependency(CSymbolID('eeor', Path('eeor.c')), 'pooh') test_unit.add_c_dependency(CSymbolID('eeor', Path('eeor.c')), 'piglet') assert test_unit.get_symbol('tigger') \ == [CInfo(CSymbolID('tigger', Path('tigger.c')))] assert test_unit.get_symbol('eeor') \ == [CInfo(CSymbolID('eeor', Path('eeor.c')), ['piglet', 'pooh'])] with pytest.raises(WorkingStateException): _ = test_unit.get_symbol('pooh') # Test a multiply defined program unit. # test_unit.add_c_symbol(CSymbolID('tigger', Path('hundred.c'))) assert test_unit.get_symbol('tigger') \ == [CInfo(CSymbolID('tigger', Path('hundred.c'))), CInfo(CSymbolID('tigger', Path('tigger.c')))] assert test_unit.get_symbol('eeor') \ == [CInfo(CSymbolID('eeor', Path('eeor.c')), ['piglet', 'pooh'])] with pytest.raises(WorkingStateException): _ = test_unit.get_symbol('pooh')
def test_add_remove_sequence(self, tmp_path: Path): database = SqliteStateDatabase(tmp_path) test_unit = CWorkingState(database) assert list(iter(test_unit)) == [] # Add a file containing a program unit and an unsatisfied dependency. # test_unit.add_c_symbol(CSymbolID('foo', Path('foo.c'))) test_unit.add_c_dependency(CSymbolID('foo', Path('foo.c')), 'bar') assert list(iter(test_unit)) \ == [CInfo(CSymbolID('foo', Path('foo.c')), ['bar'])] assert list(test_unit.depends_on(CSymbolID('foo', Path('foo.c')))) \ == [CSymbolUnresolvedID('bar')] # Add a second file containing a second program unit. # # This satisfies the previously dangling dependency and adds a new # one. # test_unit.add_c_symbol(CSymbolID('bar', Path('bar.c'))) test_unit.add_c_dependency(CSymbolID('bar', Path('bar.c')), 'baz') assert list(iter(test_unit)) \ == [CInfo(CSymbolID('bar', Path('bar.c')), ['baz']), CInfo(CSymbolID('foo', Path('foo.c')), ['bar'])] assert list(test_unit.depends_on(CSymbolID('foo', Path('foo.c')))) \ == [CSymbolID('bar', Path('bar.c'))] assert list(test_unit.depends_on(CSymbolID('bar', Path('bar.c')))) \ == [CSymbolUnresolvedID('baz')] # Add a third file also containing a third program unit and another # copy of the first. # # The new unit depends on two other units. # test_unit.add_c_symbol(CSymbolID('baz', Path('baz.c'))) test_unit.add_c_symbol(CSymbolID('foo', Path('baz.c'))) test_unit.add_c_dependency(CSymbolID('baz', Path('baz.c')), 'qux') test_unit.add_c_dependency(CSymbolID('baz', Path('baz.c')), 'cheese') assert list(iter(test_unit)) \ == [CInfo(CSymbolID('bar', Path('bar.c')), ['baz']), CInfo(CSymbolID('baz', Path('baz.c')), ['cheese', 'qux']), CInfo(CSymbolID('foo', Path('baz.c'))), CInfo(CSymbolID('foo', Path('foo.c')), ['bar'])] assert list(test_unit.depends_on(CSymbolID('foo', Path('foo.c')))) \ == [CSymbolID('bar', Path('bar.c'))] assert list(test_unit.depends_on(CSymbolID('foo', Path('baz.c')))) \ == [] assert list(test_unit.depends_on(CSymbolID('bar', Path('bar.c')))) \ == [CSymbolID('baz', Path('baz.c'))] assert list(test_unit.depends_on(CSymbolID('baz', Path('baz.c')))) \ == [CSymbolUnresolvedID('qux'), CSymbolUnresolvedID('cheese')] # Remove a previously added file # test_unit.remove_c_file(Path('baz.c')) assert list(iter(test_unit)) \ == [CInfo(CSymbolID('bar', Path('bar.c')), ['baz']), CInfo(CSymbolID('foo', Path('foo.c')), ['bar'])] assert list(test_unit.depends_on(CSymbolID('foo', Path('foo.c')))) \ == [CSymbolID('bar', Path('bar.c'))] assert list(test_unit.depends_on(CSymbolID('bar', Path('bar.c')))) \ == [CSymbolUnresolvedID('baz')]
def run(self, artifacts: List[Artifact]) -> List[Artifact]: logger = logging.getLogger(__name__) if len(artifacts) == 1: artifact = artifacts[0] else: msg = ('Fortran Analyser expects only one Artifact, ' f'but was given {len(artifacts)}') raise TaskException(msg) reader = FileTextReader(artifact.location) new_artifact = Artifact(artifact.location, artifact.filetype, Analysed) state = FortranWorkingState(self.database) state.remove_fortran_file(reader.filename) logger.debug('Analysing: %s', reader.filename) # If this file defines any C symbol bindings it may also # end up with an entry in the C part of the database cstate = CWorkingState(self.database) cstate.remove_c_file(reader.filename) normalised_source = FortranNormaliser(reader) scope: List[Tuple[str, str]] = [] for line in normalised_source.line_by_line(): logger.debug(scope) logger.debug('Considering: %s', line) if len(scope) == 0: unit_match: Optional[Match] \ = self._program_unit_pattern.match(line) if unit_match is not None: unit_type: str = unit_match.group(1).lower() unit_name: str = unit_match.group(2).lower() logger.debug('Found %s called "%s"', unit_type, unit_name) unit_id = FortranUnitID(unit_name, reader.filename) state.add_fortran_program_unit(unit_id) new_artifact.add_definition(unit_name) scope.append((unit_type, unit_name)) continue use_match: Optional[Match] \ = self._use_pattern.match(line) if use_match is not None: use_name: str = use_match.group(3).lower() if use_name in self._intrinsic_modules: logger.debug('Ignoring intrinsic module "%s"', use_name) else: if len(scope) == 0: use_message \ = '"use" statement found outside program unit' raise TaskException(use_message) logger.debug('Found usage of "%s"', use_name) unit_id = FortranUnitID(scope[0][1], reader.filename) state.add_fortran_dependency(unit_id, use_name) new_artifact.add_dependency(use_name) continue block_match: Optional[Match] = self._scoping_pattern.match(line) if block_match is not None: # Beware we want the value of a different group to the one we # check the presence of. # block_name: str = block_match.group(1) \ and block_match.group(2).lower() block_nature: str = block_match.group(3).lower() logger.debug('Found %s called "%s"', block_nature, block_name) scope.append((block_nature, block_name)) continue proc_match: Optional[Match] \ = self._procedure_pattern.match(line) if proc_match is not None: proc_nature = proc_match.group(1).lower() proc_name = proc_match.group(2).lower() logger.debug('Found %s called "%s"', proc_nature, proc_name) scope.append((proc_nature, proc_name)) # Check for the procedure being symbol-bound to C cbind_match: Optional[Match] \ = self._cbind_pattern.match(line) if cbind_match is not None: cbind_name = cbind_match.group(2) # The name keyword on the bind statement is optional. # If it doesn't exist, the procedure name is used if cbind_name is None: cbind_name = proc_name cbind_name = cbind_name.lower().strip("'\"") logger.debug('Bound to C symbol "%s"', cbind_name) # A bind within an interface block means this is # exposure of a C-defined function to Fortran, # otherwise it is going the other way (allowing C # code to call the Fortran procedure) if any([stype == "interface" for stype, _ in scope]): # TODO: This is sort of hijacking the mechanism used # for Fortran module dependencies, only using the # symbol name. Longer term we probably need a more # elegant solution logger.debug('In an interface block; so a dependency') unit_id = FortranUnitID(scope[0][1], reader.filename) state.add_fortran_dependency(unit_id, cbind_name) new_artifact.add_dependency(cbind_name) else: # Add to the C database logger.debug('Not an interface block; so a definition') symbol_id = CSymbolID(cbind_name, reader.filename) cstate.add_c_symbol(symbol_id) new_artifact.add_definition(cbind_name) continue cbind_match = self._cbind_pattern.match(line) if cbind_match is not None: # This should be a line binding from C to a variable definition # (procedure binds are dealt with above) cbind_name = cbind_match.group(2) # The name keyword on the bind statement is optional. # If it doesn't exist, the Fortran variable name is used if cbind_name is None: var_search = re.search(r'.*::\s*(\w+)', line) if var_search: cbind_name = var_search.group(1) else: cbind_message \ = 'failed to find variable name ' \ 'on C bound variable' raise TaskException(cbind_message) cbind_name = cbind_name.lower().strip("'\"") logger.debug('Found C bound variable called "%s"', cbind_name) # Add to the C database symbol_id = CSymbolID(cbind_name, reader.filename) cstate.add_c_symbol(symbol_id) new_artifact.add_definition(cbind_name) iface_match: Optional[Match] = self._interface_pattern.match(line) if iface_match is not None: iface_name = iface_match.group(1) \ and iface_match.group(1).lower() logger.debug('Found interface called "%s"', iface_name) scope.append(('interface', iface_name)) continue type_match: Optional[Match] = self._type_pattern.match(line) if type_match is not None: type_name = type_match.group(3).lower() logger.debug('Found type called "%s"', type_name) scope.append(('type', type_name)) continue end_match: Optional[Match] = self._end_block_pattern.match(line) if end_match is not None: end_nature: str = end_match.group(1) \ and end_match.group(1).lower() end_name: str = end_match.group(2) \ and end_match.group(2).lower() logger.debug('Found end of %s called %s', end_nature, end_name) exp: Tuple[str, str] = scope.pop() if end_nature is not None: if end_nature != exp[0]: end_message = 'Expected end of {exp} "{name}" ' \ 'but found {found}' end_values = { 'exp': exp[0], 'name': exp[1], 'found': end_nature } raise TaskException(end_message.format(**end_values)) if end_name is not None: if end_name != exp[1]: end_message = 'Expected end of {exp} "{name}" ' \ 'but found end of {found}' end_values = { 'exp': exp[0], 'name': exp[1], 'found': end_name } raise TaskException(end_message.format(**end_values)) return [new_artifact]
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