Skip to content

Commit

Permalink
Merge remote-tracking branch 'origin/develop' into feature/41-durable…
Browse files Browse the repository at this point in the history
…_mockup
  • Loading branch information
theHolgi committed Mar 20, 2024
2 parents c8d1b22 + 7b0d938 commit a09a99b
Show file tree
Hide file tree
Showing 3 changed files with 140 additions and 50 deletions.
11 changes: 11 additions & 0 deletions hammocking/hammocking.ini
Original file line number Diff line number Diff line change
@@ -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]
144 changes: 109 additions & 35 deletions hammocking/hammocking.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,42 @@
import re
from argparse import ArgumentParser
from pathlib import Path
from typing import List, Union, Tuple, Iterator, Iterable
from clang.cindex import Index, TranslationUnit, Cursor, CursorKind, Config
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)

# Config.set_library_file('libclang-14.so.1')

Expand All @@ -28,13 +60,14 @@ def get_definition(self) -> str:


class Function:
def __init__(self, type: str, name: str, params: List[Variable]) -> None:
def __init__(self, type: str, name: str, params: List[Variable], is_variadic: bool = False) -> None:
self.type = type
self.name = name
self.params = params
self.is_variadic = is_variadic

def get_signature(self) -> str:
return f"{self.type} {self.name}({self._collect_arguments(True)})"
return f"{self.type} {self.name}({self._collect_arguments(True)}{', ...' if self.is_variadic else ''})"

def _collect_arguments(self, with_types: bool) -> str:
unnamed_index = 1
Expand Down Expand Up @@ -85,10 +118,10 @@ def add_variable(self, type: str, name: str, size: int = 0) -> None:
logging.info(f"HammocKing: Create mockup for variable {name}")
self.variables.append(Variable(type, name, size))

def add_function(self, type: str, name: str, params: List[Tuple[str, str]] = []) -> None:
def add_function(self, type: str, name: str, params: List[Tuple[str, str]] = [], is_variadic: bool = False) -> None:
"""Add a variable definition"""
logging.info(f"Create mockup for function {name}")
self.functions.append(Function(type, name, [Variable(param[0], param[1]) for param in params]))
self.functions.append(Function(type, name, [Variable(param[0], param[1]) for param in params], is_variadic))

def get_mockup(self, file: str) -> str:
return self.render(Path(file + '.j2'))
Expand All @@ -115,7 +148,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
Expand Down Expand Up @@ -167,23 +200,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()],
)
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:
Expand All @@ -195,31 +233,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')
Expand All @@ -234,18 +303,23 @@ 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("--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)

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()

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)
h.add_excludes(args.excludes)
h.add_excludes(args.exclude_pathes)
h.read(args.sources)
h.write(args.outdir)

Expand Down
35 changes: 20 additions & 15 deletions tests/hammocking_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@

from hammocking.hammocking import *

# Apply default config
ConfigReader()

class TestVariable:
def test_creation(self):
Expand Down Expand Up @@ -220,25 +222,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):
Expand Down Expand Up @@ -356,6 +352,15 @@ def test_langmode_override(self):
assert len(mock.writer.functions) == 1, "Mockup shall have a function"
assert mock.writer.functions[0].get_signature() == "_Bool bool_status()", "Function shall be created with C99 bool type"

def test_variadic_function(self):
"""Mock a variadic function"""
mock = Hammock(["printf"])
mock.parse("extern int printf(const char * format, ...);")
assert mock.done, "Should be done now"
assert len(mock.writer.functions) == 1, "Mockup shall have a function"
self.assertEqual(
mock.writer.functions[0].get_signature(), "int printf(const char * format, ...)", "Function shall be created in the mockup"
)

if __name__ == "__main__":
unittest.main()

0 comments on commit a09a99b

Please sign in to comment.