PK!sFnarmock/__init__.py__version__ = "0.2.0" PK!or&&narmock/__main__.pyfrom narmock.cli import main main() PK!narmock/api.py"""High-level public interface.""" __all__ = ["generate_mocks", "collect_linker_flags"] from .inspect import collect_mocked_functions from .generator import GeneratedMock, FileGenerator def generate_mocks(expanded_code, directory): """Identify mocked functions and generate the source and header files.""" generator = FileGenerator() for function in collect_mocked_functions(expanded_code): generator.add_mock(function) generator.write_to_directory(directory) def collect_linker_flags(directory): """Return the flags needed for wrapping the declared mocked functions.""" try: return GeneratedMock.extract_flags(FileGenerator.read_declarations(directory)) except FileNotFoundError: return [] PK!/GUUnarmock/cli.py"""Commande-line interface.""" __all__ = ["main"] import sys from argparse import ArgumentParser, FileType from .api import generate_mocks, collect_linker_flags parser = ArgumentParser( prog="narmock", description="A minimal mocking utility for C projects." ) group = parser.add_mutually_exclusive_group(required=True) group.add_argument( "-g", metavar="", nargs="?", type=FileType("r"), const=sys.stdin, help="generate mocks", ) group.add_argument("-f", action="store_true", help="output linker flags") parser.add_argument("-d", metavar="", default=".", help="mocks directory") def main(): args = parser.parse_args() if args.g: generate_mocks(expanded_code=args.g.read(), directory=args.d) elif args.f: print(" ".join(collect_linker_flags(directory=args.d)).strip()) PK!Anarmock/generator.py"""Module in charge of generating mocks.""" __all__ = ["GeneratedMock", "FileGenerator"] import os import re from copy import deepcopy from jinja2 import Environment, PackageLoader from pycparser import c_ast as node from pycparser.c_generator import CGenerator from . import __version__ def decl(name, type): return node.Decl(name, [], [], [], type, None, None) def function_ptr_decl(name, return_type, parameters): return decl( name, node.PtrDecl([], node.FuncDecl(node.ParamList(parameters), return_type)) ) def rename_return_type(return_type, name): return_type = deepcopy(return_type) type_decl = return_type while not isinstance(type_decl, node.TypeDecl): type_decl = type_decl.type type_decl.declname = name return return_type def get_guard_name(filename): slug = re.sub(r"[^a-zA-Z0-9]", "_", os.path.normpath(os.path.relpath(filename))) return re.sub(r"_+", "_", slug).upper().strip("_") def generate_includes(system_includes, local_includes, directory): return "\n\n".join( includes for includes in ( "\n".join(f"#include <{path}>" for path in sorted(system_includes)), "\n".join( f'#include "{os.path.relpath(path, directory)}"' for path in sorted(local_includes) ), ) if includes ) class GeneratedMock: DECL_MARKER = "// NARMOCK_DECLARATION" IMPL_MARKER = "// NARMOCK_IMPLEMENTATION" FLAGS_MARKER = "// NARMOCK_LINKER_FLAGS" FLAGS_REGEX = re.compile(fr"^{FLAGS_MARKER}\s+(.+)$", re.MULTILINE) @classmethod def extract_flags(cls, mock_declarations): return cls.FLAGS_REGEX.findall(mock_declarations) def __init__(self, function): self.function = function self.func_name = function.name self.wrapped_func = f"__wrap_{self.func_name}" self.real_func = f"__real_{self.func_name}" self.linker_flags = f"-Wl,--wrap={self.func_name}" self.state_name = f"_narmock_state_for_{self.func_name}" self.state_type = f"_narmock_state_type_for_{self.func_name}" self.private_state_type = f"_narmock_private_state_type_for_{self.func_name}" self.func_decl = self.function.declaration.type self.func_params = self.func_decl.args.params if self.func_decl.args else [] self.forward_args = ", ".join( param.name for param in self.func_params if param.name ) return_type = self.func_decl.type self.return_value = ( None if isinstance(return_type, node.TypeDecl) and isinstance(return_type.type, node.IdentifierType) and return_type.type.names[0] == "void" else "return_value" ) self.return_value_decl = decl( self.return_value, rename_return_type(return_type, self.return_value) ) self.implementation_decl = function_ptr_decl( "implementation", rename_return_type(return_type, "implementation"), self.func_params, ) self.mock_return_decl = self.state_function( "mock_return", [self.return_value_decl] ) self.mock_implementation_decl = self.state_function( "mock_implementation", [self.implementation_decl] ) self.disable_mock_decl = self.state_function( "disable_mock", [decl(None, node.TypeDecl(None, [], node.IdentifierType(["void"])))], ) self.real_decl = self.rename_function(self.real_func) self.wrapped_decl = self.rename_function(self.wrapped_func) def state_function(self, name, parameters): return function_ptr_decl( name, node.PtrDecl( [], node.TypeDecl(name, [], node.IdentifierType([self.state_type])) ), parameters, ) def rename_function(self, name): return decl( name, node.FuncDecl( self.func_decl.args, rename_return_type(self.func_decl.type, name) ), ) class FileGenerator: SOURCE_FILE = "__mocks__.c" HEADER_FILE = "__mocks__.h" def __init__(self): self.code_generator = CGenerator() self.jinja_env = Environment( loader=PackageLoader("narmock", "templates"), trim_blocks=True, lstrip_blocks=True, ) self.jinja_env.filters["render"] = self.code_generator.visit self.source_template = self.jinja_env.get_template(f"{self.SOURCE_FILE}.jinja2") self.header_template = self.jinja_env.get_template(f"{self.HEADER_FILE}.jinja2") self.mocks = [] self.system_includes = set() self.local_includes = set() def add_mock(self, mocked_function): self.mocks.append(GeneratedMock(mocked_function)) if mocked_function.include: if mocked_function.include.system: self.system_includes.add(mocked_function.include.path) else: self.local_includes.add(mocked_function.include.path) def write_to_directory(self, directory): source_filename = os.path.join(directory, self.SOURCE_FILE) header_filename = os.path.join(directory, self.HEADER_FILE) mocks = list(sorted(self.mocks, key=lambda m: m.func_name)) source_code = self.source_template.render( narmock_version=__version__, includes=generate_includes([], [header_filename], directory), mocks=mocks, ) header_code = self.header_template.render( narmock_version=__version__, guard_name=get_guard_name(header_filename), includes=generate_includes( self.system_includes, self.local_includes, directory ), mocks=mocks, ) with open(source_filename, "w") as source_file: source_file.write(source_code.strip() + "\n") with open(header_filename, "w") as header_file: header_file.write(header_code.strip() + "\n") @classmethod def read_declarations(cls, directory): with open(os.path.join(directory, cls.HEADER_FILE)) as header_file: return header_file.read() PK!L!L!narmock/inspect.py"""Module in charge of detecting mock usage in expanded source code.""" __all__ = [ "collect_mocked_functions", "IncludeDirective", "MockedFunction", "Token", "ForgivingDeclarationParser", ] import os import sys import re from collections import defaultdict from typing import NamedTuple, List, Tuple from pycparser import c_ast as node from pycparser.c_parser import CParser from pycparser.plyparser import ParseError GETTER_REGEX = re.compile( r"(_narmock_state_type_for_[A-Za-z0-9_]+\s*\*\s*)?\b" + fr"_narmock_get_mock_for_([A-Za-z0-9_]+)\s*\(" ) def collect_mocked_functions(expanded_source_code): """Yield all the mocked functions used in the expanded source code.""" functions = set() for match in GETTER_REGEX.finditer(expanded_source_code): return_type, function_name = match.groups() if not return_type: functions.add(function_name) yield from ForgivingDeclarationParser(expanded_source_code, functions) if functions: for function in functions: print("error:", f"'{function}' undeclared", file=sys.stderr) sys.exit(1) def rename_arguments(function_declaration): if not function_declaration.type.args: return function_declaration for i, param in enumerate(function_declaration.type.args.params): param_type = param.type if ( not param.name and isinstance(param_type.type, node.IdentifierType) and param_type.type.names == ["void"] ): continue param.name = f"arg{i + 1}" while not isinstance(param_type, node.TypeDecl): param_type = param_type.type param_type.declname = param.name return function_declaration class IncludeDirective(NamedTuple): path: str system: bool @classmethod def from_source_context(cls, source_context): if not source_context: return None for filename in source_context: if not os.path.isabs(filename): continue dirname, basename = os.path.split(filename) fullname = basename while True: dirname, basename = os.path.split(dirname) if "include" in basename.lower(): return cls(fullname, True) if not basename: break fullname = os.path.join(basename, fullname) return cls(os.path.abspath(source_context[-1]), False) def __str__(self): return "#include " + (f"<{self.path}>" if self.system else f'"{self.path}"') class MockedFunction(NamedTuple): name: str declaration: str include: IncludeDirective class Token(NamedTuple): type: str value: str span: Tuple[int, int] def is_punctuation(self, *symbols): return self.type == "PUNCTUATION" and self.value in symbols def is_keyword(self, *keywords): return self.type == "KEYWORD" and self.value in keywords @property def is_prefix(self): return self.type == "KEYWORD" and self.value in ( "typedef", "extern", "static", "auto", "register", "__extension__", ) class ForgivingDeclarationParser: linemarker = re.compile(r'^# \d+ "((?:\\.|[^\\"])*)"((?: [1234])*)$') tokens = { "LINEMARKER": r"^#.*$", "KEYWORD": ( "\\b(?:auto|break|case|char|const|continue|default|do|double|else|enum|extern|float" "|for|goto|if|int|long|register|return|short|signed|sizeof|static|struct|switch" "|typedef|union|unsigned|void|volatile|while|__extension__)\\b" ), "IDENTIFIER": r"\b[a-zA-Z_](?:[a-zA-Z_0-9])*\b", "CHARACTER": r"L?'(?:\\.|[^\\'])+'", "STRING": r'L?"(?:\\.|[^\\"])*"', "INTEGER": r"(?:0[xX][a-fA-F0-9]+|[0-9]+)[uUlL]*", "FLOAT": ( r"(?:[0-9]+[Ee][+-]?[0-9]+|[0-9]*\.[0-9]+(?:[Ee][+-]?[0-9]+)?|[0-9]+\.[0-9]*(?:[Ee][+-]?[0-9]+)?)[fFlL]?" ), "PUNCTUATION": ( r"\.\.\.|>>=|<<=|\+=|-=|\*=|/=|%=|&=|\^=|\|=|>>|<<|\+\+|--|->|&&|\|\||<=|>=|" r"==|!=|;|\{|\}|,|:|=|\(|\)|\[|\]|\.|&|!|~|-|\+|\*|/|%|<|>|\^|\||\?" ), "SPACE": r"[ \t\v\n\f]*", "IGNORE": r".+?", } ignored_tokens = "SPACE", "IGNORE" regex = re.compile( "|".join(f"(?P<{token}>{pattern})" for token, pattern in tokens.items()), flags=re.MULTILINE, ) def __init__(self, source_code, functions=None): self.source_code = source_code self.functions = functions self.token_stream = self.tokenize(source_code) self.previous = None self.current = None self.bracket_stack = [] self.source_context = [] self.typedefs = ["typedef int __builtin_va_list;"] self.cparser = CParser() @classmethod def tokenize(cls, source_code): for match in cls.regex.finditer(source_code): if match.lastgroup not in cls.ignored_tokens: yield Token(match.lastgroup, match.group().strip(), match.span()) def __iter__(self): while self.next(): if self.current.is_keyword("typedef"): self.parse_typedef() function = self.parse_function_declaration() if function is not None: yield function if self.functions is not None and not self.functions: break while self.current and not ( self.current.is_punctuation(";", "}") and not self.bracket_stack ): self.next() def next(self): self.previous = self.current self.current = next(self.token_stream, None) if not self.current: return None if self.current.type == "PUNCTUATION": if self.current.value in "({[": self.bracket_stack.append(")}]"["({[".index(self.current.value)]) elif self.bracket_stack and self.current.value == self.bracket_stack[-1]: self.bracket_stack.pop() elif self.current.type == "LINEMARKER": filename, flags = self.linemarker.match(self.current.value).groups() if "1" in flags: self.source_context.append(filename) elif "2" in flags: self.source_context.pop() try: self.source_context[-1] = filename except IndexError: self.source_context.append(filename) self.next() return self.current def parse_typedef(self): start_index = self.current.span[0] while self.current and not ( self.current.is_punctuation(";") and not self.bracket_stack ): self.next() self.typedefs.append(self.source_code[start_index : self.current.span[1]]) def parse_function_declaration(self): if self.bracket_stack: return None while self.current and self.current.is_prefix: self.next() start_index = self.current.span[0] return_type = [] while ( self.current and not self.current.is_punctuation("(") or self.next() and self.current.is_punctuation("*") ): if not self.bracket_stack and self.current.is_punctuation(";"): return None return_type.append(self.current.value) self.next() if not return_type: return None func_name = return_type.pop() if self.functions is not None and func_name not in self.functions: return None while ( self.current and self.bracket_stack or self.next() and self.current.is_punctuation("(") ): self.next() signature = self.source_code[start_index : self.previous.span[1]] + ";" code = "\n".join(self.typedefs) + "\n" + signature try: file_ast = self.cparser.parse(code) except ParseError: return None else: if self.functions is not None: self.functions.remove(func_name) return MockedFunction( func_name, rename_arguments(file_ast.ext[-1]), IncludeDirective.from_source_context(self.source_context), ) PK!narmock/templates/__init__.pyPK!LT  $narmock/templates/__mocks__.c.jinja2/* Mocks source file Generated with Narmock v{{ narmock_version }} (https://github.com/vberlier/narmock) Do not edit manually */ {{ includes }} {% for mock in mocks %} {{ mock.IMPL_MARKER }} {{ mock.func_name }} {{ mock.real_decl | render }}; typedef struct {{ mock.private_state_type }} {{ mock.private_state_type }}; struct {{ mock.private_state_type }} { {{ mock.state_type }} public; int mode; {% if mock.return_value %} {{ mock.return_value_decl | render }}; {% endif %} {{ mock.implementation_decl | render }}; }; {{ mock.state_type }} *_narmock_mock_return_function_for_{{ mock.func_name }}({% if mock.return_value %}{{ mock.return_value_decl | render }}{% endif %}); {{ mock.state_type }} *_narmock_mock_implementation_function_for_{{ mock.func_name }}({{ mock.implementation_decl | render }}); {{ mock.state_type }} *_narmock_disable_mock_function_for_{{ mock.func_name }}(); {{ mock.private_state_type }} {{ mock.state_name }} = { .public = { .mock_return = _narmock_mock_return_function_for_{{ mock.func_name }}, .mock_implementation = _narmock_mock_implementation_function_for_{{ mock.func_name }}, .disable_mock = _narmock_disable_mock_function_for_{{ mock.func_name }} }, .mode = 0 }; {{ mock.wrapped_decl | render }} { switch ({{ mock.state_name }}.mode) { case 1: return{% if mock.return_value %} {{ mock.state_name }}.return_value{% endif %}; case 2: return {{ mock.state_name }}.implementation({{ mock.forward_args }}); default: return {{ mock.real_func }}({{ mock.forward_args }}); } } {{ mock.state_type }} *_narmock_mock_return_function_for_{{ mock.func_name }}({% if mock.return_value %}{{ mock.return_value_decl | render }}{% endif %}) { {{ mock.state_name }}.mode = 1; {% if mock.return_value %} {{ mock.state_name }}.return_value = return_value; {% endif %} return &{{ mock.state_name }}.public; } {{ mock.state_type }} *_narmock_mock_implementation_function_for_{{ mock.func_name }}({{ mock.implementation_decl | render }}) { {{ mock.state_name }}.mode = 2; {{ mock.state_name }}.implementation = implementation; return &{{ mock.state_name }}.public; } {{ mock.state_type }} *_narmock_disable_mock_function_for_{{ mock.func_name }}() { {{ mock.state_name }}.mode = 0; return &{{ mock.state_name }}.public; } {{ mock.state_type }} *_narmock_get_mock_for_{{ mock.func_name }}(void *function) { (void)function; return &{{ mock.state_name }}.public; } {% endfor %} PK!$narmock/templates/__mocks__.h.jinja2/* Mocks header file Generated with Narmock v{{ narmock_version }} (https://github.com/vberlier/narmock) Do not edit manually */ #ifndef {{ guard_name }} #define {{ guard_name }} #ifndef MOCK #define MOCK(function) (_narmock_get_mock_for_##function((void *)&function)) #endif {{ includes }} {% for mock in mocks %} {{ mock.DECL_MARKER }} {{ mock.func_name }} {{ mock.FLAGS_MARKER }} {{ mock.linker_flags }} typedef struct {{ mock.state_type }} {{ mock.state_type }}; struct {{ mock.state_type }} { {{ mock.mock_return_decl | render }}; {{ mock.mock_implementation_decl | render }}; {{ mock.disable_mock_decl | render }}; }; {{ mock.state_type }} *_narmock_get_mock_for_{{ mock.func_name }}(void *function); {% endfor %} #endif PK!HqS(,(narmock-0.2.0.dist-info/entry_points.txtN+I/N.,()K,Oζz9Vy\\PK!11narmock-0.2.0.dist-info/LICENSEMIT License Copyright (c) 2019 Valentin Berlier Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. PK!HڽTUnarmock-0.2.0.dist-info/WHEEL A н#Z;/"d&F[xzw@Zpy3Fv]\fi4WZ^EgM_-]#0(q7PK!H@~ narmock-0.2.0.dist-info/METADATAXrۺSlsG\'3g8nm=Nt:nƂHDL,J}>@oE/z 8X/a 3,+-dW[VTL20:d] %+DAcD!L Tp yltAnpfE`v<:m\3bbW.=/FM1=*Ky-0R;X2h:ik̹ vM{OwQ$p6äf[ A^gY)ViGd:2t.xHyb66#/Nl녰KmnNLSpЦ-U/Z[$=<'B{ <[fj= 8~B_?IuX5hu?d#TTsH*&iKQ`{*W\1mE4lS` Nr=QEQ(?gww]?Z= ʺoR7 IQ8>{swG+5ytvx ^1Wf sy`B)MBSX5:0cVasF#zVD`bKj7 eų` 4s {{p^iʶDT#yK t斵. +N|j=78 K/,D{vQ l$ r@zY3s`7)=e+H#Q#N@"5"b;)A-睆00#t %ؤcj E"mDI2۴3Ü5.vD(EI:}:qeB)>15AG~ˬ(E֯t$>9*lbEֶܑ/8=Z-KLf*(>VB[z-N#y-;=OQvn%Xlmo~]FŚ-$}N$<XIrA<]~#I`\ vS`UO5ZDի֚G;"ll;^XyVa=- KRhPk!k 3"#8:+ZԭF),H[:iʭwv}-d ^B]HdNp~2J5qjT^;Jja@Zqvciw>c⊥12t/|7)EdB!彳72Ĕlga:e. |_%+ *%ѵg 2Ū/n恗C/]e 袵7}jrlkV4M]5ƴQ&Z* !l?ϝx`( jQg_g~.ᝧӋÝֆP4ER;ciӬ&~anx8AƆ&S ڙ[On69O͌Wmkd$누zVKK (A0kn /Lyُ.k$кnMl!+N RBZbWk&wbNO)5AccpK";)Q8a~Bt(x! vTG_m"w*voP;hDf4e8_C[l}9p~p gt&1Mx >\gSYr5~=r!3pPK!Hn=gInarmock-0.2.0.dist-info/RECORD}KH}Ap1 nȆDH$~8]='ވG]qCB u ZZNẁjp d`u\rʄu#( ;tݛ3t]n>?"c+| ,|98SVESV840(JѾ*iHfٱ 88[l3XsCܖJzQ}Fh0`[ٰ;wRZbNbt2f4bJ{fAm, eA^ vraLמJ}B 3)ѵH9iA^Tm$cftzأLepAc_(ux)i0Cu{ľ!.ZB: vgwC'HoHH hc*eVj9bݻ#B+gegS"Yq+fu!>w?KL%vaEfkzẁ{aݍ(mk'o||aq GXJkl @]WGo$endޢ)aǸ`m"T2m.Ϳ[Y'MhK 'GkYd)\j]p8((^o(nRԏPK!sFnarmock/__init__.pyPK!or&&Gnarmock/__main__.pyPK!narmock/api.pyPK!/GUUnarmock/cli.pyPK!A<narmock/generator.pyPK!L!L! narmock/inspect.pyPK!Anarmock/templates/__init__.pyPK!LT  $Anarmock/templates/__mocks__.c.jinja2PK!$(Lnarmock/templates/__mocks__.h.jinja2PK!HqS(,(VOnarmock-0.2.0.dist-info/entry_points.txtPK!11Onarmock-0.2.0.dist-info/LICENSEPK!HڽTU2Tnarmock-0.2.0.dist-info/WHEELPK!H@~ Tnarmock-0.2.0.dist-info/METADATAPK!Hn=gI\narmock-0.2.0.dist-info/RECORDPKi_