From a90dd90c98800394ca93bb6727fd8d524e59078f Mon Sep 17 00:00:00 2001 From: Holger Lamm Date: Wed, 20 Mar 2024 17:08:05 +0100 Subject: [PATCH 1/2] Put OS-dependent stuff in .ini file --- hammocking/hammocking.ini | 11 ++++ hammocking/hammocking.py | 131 +++++++++++++++++++++++++++++--------- tests/hammocking_test.py | 26 ++++---- 3 files changed, 122 insertions(+), 46 deletions(-) create mode 100644 hammocking/hammocking.ini diff --git a/hammocking/hammocking.ini b/hammocking/hammocking.ini new file mode 100644 index 0000000..80aab0f --- /dev/null +++ b/hammocking/hammocking.ini @@ -0,0 +1,11 @@ +[hammocking] +# nm=nm +# include_pattern=.... +exclude_pattern=^(_|llvm_|memset|bzero) +[hammocking.darwin] +ignore_path=/Applications/Xcode.app +clang_lib_path=/Library/Developer/CommandLineTools/usr/lib +[hammocking.linux] +ignore_path=/usr/include +clang_lib_file=libclang-14.so.1 +[hammocking.win32] diff --git a/hammocking/hammocking.py b/hammocking/hammocking.py index 63562b8..039ecce 100755 --- a/hammocking/hammocking.py +++ b/hammocking/hammocking.py @@ -10,10 +10,42 @@ import re from argparse import ArgumentParser from pathlib import Path -from typing import List, Union, Tuple, Iterator, Iterable +from typing import List, Set, Union, Tuple, Iterator, Iterable, Optional from clang.cindex import Index, TranslationUnit, Cursor, CursorKind, Config, TypeKind from jinja2 import Environment, FileSystemLoader import logging +import configparser + +class ConfigReader: + section = "hammocking" + configfile = Path(__file__).parent / (section + ".ini") + def __init__(self, configfile: Path = None): + if configfile is None or configfile == Path(""): + configfile = ConfigReader.configfile + self.exclude_pathes = [] + if not configfile.exists(): + return + config = configparser.ConfigParser() + config.read_string(configfile.read_text()) + # Read generic settings + self._scan(config.items(section=self.section)) + # Read OS-specific settings + self._scan(config.items(section=f"{self.section}.{sys.platform}")) + + def _scan(self, items: Iterator[Tuple[str, str]]) -> None: + for item, value in items: + if item == "clang_lib_file": + Config.set_library_file(value) + if item == "clang_lib_path": + Config.set_library_path(value) + if item == "nm": + NmWrapper.set_nm_path(value) + if item == "ignore_path": + self.exclude_pathes = value.split(",") + if item == "include_pattern": + NmWrapper.set_include_pattern(value) + if item == "exclude_pattern": + NmWrapper.set_exclude_pattern(value) class Variable: @@ -115,7 +147,7 @@ def default_language_mode(self) -> str: class Hammock: - def __init__(self, symbols: List[str], cmd_args: List[str] = [], mockup_style="gmock", suffix=None): + def __init__(self, symbols: Set[str], cmd_args: List[str] = [], mockup_style="gmock", suffix=None): self.logger = logging.getLogger(self.__class__.__name__) self.symbols = symbols self.cmd_args = cmd_args @@ -167,24 +199,28 @@ def parse(self, input: Union[Path, str]) -> None: self.logger.debug(f"Command arguments: {parseOpts['args']}") for child in self.iter_children(translation_unit.cursor): if child.spelling in self.symbols: - in_header = child.location.file.name != translation_unit.spelling - if in_header: # We found it in the Source itself. Better not include the whole source! - self.writer.add_header(str(child.location.file)) - if child.kind == CursorKind.VAR_DECL: - child_type_array_size = child.type.get_array_size() - if child_type_array_size > 0: - self.writer.add_variable(child.type.element_type.spelling, child.spelling, child_type_array_size) - else: - self.writer.add_variable(child.type.spelling, child.spelling) - elif child.kind == CursorKind.FUNCTION_DECL: - self.writer.add_function( - child.type.get_result().spelling, - child.spelling, - [(arg.type.spelling, arg.spelling) for arg in child.get_arguments()], - is_variadic = child.type.is_function_variadic() if child.type.kind == TypeKind.FUNCTIONPROTO else False - ) + if any(map(lambda prefix: child.location.file.name.startswith(prefix), self.exclude_pathes)): + self.logger.debug("Not mocking symbol " + child.spelling) else: - self.logger.warning(f"Unknown kind of symbol: {child.kind}") + self.logger.debug(f"Found {child.spelling} in {child.location.file}") + in_header = child.location.file.name != translation_unit.spelling + if in_header: # We found it in the Source itself. Better not include the whole source! + self.writer.add_header(str(child.location.file)) + if child.kind == CursorKind.VAR_DECL: + child_type_array_size = child.type.get_array_size() + if child_type_array_size > 0: + self.writer.add_variable(child.type.element_type.spelling, child.spelling, child_type_array_size) + else: + self.writer.add_variable(child.type.spelling, child.spelling) + elif child.kind == CursorKind.FUNCTION_DECL: + self.writer.add_function( + child.type.get_result().spelling, + child.spelling, + [(arg.type.spelling, arg.spelling) for arg in child.get_arguments()], + is_variadic = child.type.is_function_variadic() if child.type.kind == TypeKind.FUNCTIONPROTO else False + ) + else: + self.logger.warning(f"Unknown kind of symbol: {child.kind}") self.symbols.remove(child.spelling) def write(self, outdir: Path) -> None: @@ -196,31 +232,62 @@ def done(self) -> bool: class NmWrapper: - regex = r"\s*U\s+((?!_|llvm_)\S*)" - nmpath = "llvm-nm" + nmpath = "nm" + includepattern = None + excludepattern = r"^__gcov" + if sys.platform == 'darwin': # Mac objects have an additional _ + pattern = r"\s*U\s+_(\S*)" + else: + pattern = r"\s*U\s+(\S*)" def __init__(self, plink: Path): self.plink = plink self.undefined_symbols = [] self.__process() - def get_undefined_symbols(self) -> List[str]: - return self.undefined_symbols + @classmethod + def set_nm_path(cls, path: str) -> None: + cls.nmpath = path + + @classmethod + def set_include_pattern(cls, pattern: str) -> None: + cls.includepattern = re.compile(pattern) + + @classmethod + def set_exclude_pattern(cls, pattern: str) -> None: + cls.excludepattern = re.compile(pattern) + + def get_undefined_symbols(self) -> Set[str]: + return set(self.undefined_symbols) def __process(self): with Popen( - [NmWrapper.nmpath, "--undefined-only", self.plink], + [NmWrapper.nmpath, self.plink], stdout=PIPE, stderr=PIPE, bufsize=1, universal_newlines=True, ) as p: for line in p.stdout: - match = re.match(self.regex, line) - if match: - self.undefined_symbols.append(match.group(1)) + symbol = self.mock_it(line) + if symbol is not None: + self.undefined_symbols.append(symbol) assert p.returncode is None + @classmethod + def mock_it(cls, symbol: str) -> Optional[str]: + if match := re.match(cls.pattern, symbol): + symbol = match.group(1) + if cls.includepattern is not None and re.match(cls.includepattern, symbol) is not None: + logging.debug(symbol + " to be mocked (via include pattern)") + return symbol + elif cls.excludepattern is None or re.match(cls.excludepattern, symbol) is None: + logging.debug(symbol + " to be mocked") + return symbol + else: + logging.debug(symbol + " is excluded") + return None + def main(pargv): arg = ArgumentParser(fromfile_prefix_chars="@", prog='hammocking') @@ -235,18 +302,20 @@ def main(pargv): arg.add_argument("--style", "-t", help="Mockup style to output", required=False, default="gmock") arg.add_argument("--suffix", help="Suffix to be added to the generated files", required=False) - arg.add_argument("--except", help="Path prefixes that should not be mocked", nargs="*", dest="excludes", default=["/usr/include"]) + arg.add_argument("--except", help="Path prefixes that should not be mocked", nargs="*", dest="exclude_pathes", default=["/usr/include"]) + arg.add_argument("--config", help="Configuration file", required=False, default="") args, cmd_args = arg.parse_known_args(args=pargv) - if args.debug: - logging.basicConfig(level=logging.DEBUG) + logging.basicConfig(level=logging.DEBUG if args.debug else logging.INFO) + config = ConfigReader(Path(args.config)) + args.exclude_pathes += config.exclude_pathes if not args.symbols: args.symbols = NmWrapper(args.plink).get_undefined_symbols() logging.debug("Extra arguments: %s" % cmd_args) h = Hammock(symbols=args.symbols, cmd_args=cmd_args, mockup_style=args.style, suffix=args.suffix) - h.add_excludes(args.excludes) + h.add_excludes(args.exclude_pathes) h.read(args.sources) h.write(args.outdir) diff --git a/tests/hammocking_test.py b/tests/hammocking_test.py index 86c9411..56529f0 100644 --- a/tests/hammocking_test.py +++ b/tests/hammocking_test.py @@ -6,6 +6,8 @@ from hammocking.hammocking import * +# Apply default config +ConfigReader() class TestVariable: def test_creation(self): @@ -202,25 +204,19 @@ def test_languagemode(self): class TestNmWrapper(unittest.TestCase): - regex = NmWrapper.regex def test_regex(self): - line = 'some_func' - match = re.match(self.regex, line) - assert not match + assert not NmWrapper.mock_it('some_func') - line = ' U some_func' - match = re.match(self.regex, line) - assert 'some_func' == match.group(1) - - line = '__gcov_exit' - match = re.match(self.regex, line) - assert not match - - line = ' U __gcov_exit' - match = re.match(self.regex, line) - assert not match + assert 'some_func' == NmWrapper.mock_it(' U some_func') + assert not NmWrapper.mock_it('__gcov_exit') + assert not NmWrapper.mock_it(' U __gcov_exit') + def test_custom_regex(self): + NmWrapper.set_exclude_pattern('^_') + NmWrapper.set_include_pattern('^_(xyz)') + assert not NmWrapper.mock_it(' U _abc') # Every underline function is now excluded + assert '_xyz' == NmWrapper.mock_it(' U _xyz') # ... except _xyz class TestHammock(unittest.TestCase): def test_variable(self): From 906c0800f410dd66739f58f74241ab18c4653adf Mon Sep 17 00:00:00 2001 From: Holger Lamm Date: Wed, 20 Mar 2024 17:13:49 +0100 Subject: [PATCH 2/2] #43: Can explicitly exclude symbols --- hammocking/hammocking.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/hammocking/hammocking.py b/hammocking/hammocking.py index 039ecce..b3d87b0 100755 --- a/hammocking/hammocking.py +++ b/hammocking/hammocking.py @@ -303,6 +303,7 @@ def main(pargv): arg.add_argument("--style", "-t", help="Mockup style to output", required=False, default="gmock") arg.add_argument("--suffix", help="Suffix to be added to the generated files", required=False) arg.add_argument("--except", help="Path prefixes that should not be mocked", nargs="*", dest="exclude_pathes", default=["/usr/include"]) + arg.add_argument("--exclude", help="Symbols that should not be mocked", nargs="*", default=[]) arg.add_argument("--config", help="Configuration file", required=False, default="") args, cmd_args = arg.parse_known_args(args=pargv) @@ -312,6 +313,8 @@ def main(pargv): if not args.symbols: args.symbols = NmWrapper(args.plink).get_undefined_symbols() + args.symbols -= set(args.exclude) + logging.debug("Extra arguments: %s" % cmd_args) h = Hammock(symbols=args.symbols, cmd_args=cmd_args, mockup_style=args.style, suffix=args.suffix)