diff options
Diffstat (limited to 'tools/interface_generator/bin')
13 files changed, 342 insertions, 0 deletions
diff --git a/tools/interface_generator/bin/BUILD.bazel b/tools/interface_generator/bin/BUILD.bazel new file mode 100644 index 0000000..04843b9 --- /dev/null +++ b/tools/interface_generator/bin/BUILD.bazel @@ -0,0 +1,45 @@ +load("@pip//:requirements.bzl", "requirement") +load("//private:defs.bzl", "py_pytest") + +""" interface declaration """ + +py_library( + name = "interface_declaration", + srcs = ["interface_declaration.py"], +) + +""" templates """ + +py_library( + name = "templates", + srcs = ["templates.py"], + data = glob(["templates/*"]), +) + +""" interface_generator """ + +py_binary( + name = "interface_generator", + srcs = [ + "interface_generator.py", + "templates.py", + ], + imports = ["."], + visibility = ["//visibility:public"], + deps = [ + ":interface_declaration", + ":templates", + requirement("mako"), + ], +) + +""" pytest """ + +py_pytest( + name = "pytest", + srcs = glob(["*.py"]), + data = glob(["templates/*"]), + deps = [ + requirement("mako"), + ], +) diff --git a/tools/interface_generator/bin/interface_declaration.py b/tools/interface_generator/bin/interface_declaration.py new file mode 100644 index 0000000..4560bbd --- /dev/null +++ b/tools/interface_generator/bin/interface_declaration.py @@ -0,0 +1,58 @@ +""" +interface_declaration.py +""" + +from dataclasses import dataclass, asdict +from pathlib import Path + + +@dataclass +class InterfaceDeclaration: + """interface declaration class""" + + name: str + license_hdr: str + system_includes: list[str] + types: list[dict] + functions: list[dict] + + def read_license(self, path: Path): + """read and starrify a license""" + if path is None: + self.license_hdr = "" + return + + with open(path, encoding="utf-8") as license_file: + self.license_hdr = "".join( + [ + f" * { line.rstrip().ljust(72)[:72] } * \n" + for line in license_file.readlines() + ] + ).rstrip() + + def __init__(self, name: str, license_path: Path): + self.name = name if name is not None else "kstdio" + self.read_license(license_path) + self.system_includes = ["stdarg.h"] + self.types = [ + { + "name": "File", + "members": [ + "int fd", + "int (*putc)(const struct kstdio_File*, const char)", + "int (*puts)(const struct kstdio_File*, const char*)", + ], + }, + ] + self.functions = [ + { + "name": "printf", + "return": "int", + "arguments": ["const char* format"], + "argument_names": ["format"], + }, + ] + + def into_dict(self) -> dict: + """create a dictionary for use in mako""" + return asdict(self) diff --git a/tools/interface_generator/bin/interface_declaration_unittest.py b/tools/interface_generator/bin/interface_declaration_unittest.py new file mode 100644 index 0000000..b2ad80c --- /dev/null +++ b/tools/interface_generator/bin/interface_declaration_unittest.py @@ -0,0 +1,19 @@ +""" +interface declaration unit tests +""" + +import unittest +from interface_declaration import InterfaceDeclaration + + +class InterfaceDeclarationUnittest(unittest.TestCase): + """interface_declaration unit tests""" + + def test_interfacedeclaration_class_asdict(self): + """test mock interface""" + interface = InterfaceDeclaration("kstdio", None) + interface_dict = interface.into_dict() + + self.assertEqual(interface.name, "kstdio") + self.assertEqual(len(interface.license_hdr), 0) + self.assertEqual(interface_dict["name"], "kstdio") diff --git a/tools/interface_generator/bin/interface_generator.py b/tools/interface_generator/bin/interface_generator.py new file mode 100755 index 0000000..eab93d7 --- /dev/null +++ b/tools/interface_generator/bin/interface_generator.py @@ -0,0 +1,86 @@ +#!/usr/bin/env python3 + +""" +interface_generator.py +""" + +from argparse import ArgumentParser +from pathlib import Path +import sys +from mako.lookup import TemplateLookup +from interface_declaration import InterfaceDeclaration +from templates import get_templates, get_templates_dir + +PROG = { + "name": "interface_generator", + "version": "0.1", +} + + +def generate_file( + template: Path, templates: Path, output, interface: InterfaceDeclaration +): + """generate file using a tempalte and write it to the output location""" + lookup = TemplateLookup(directories=[".", templates.absolute()]) + mako_template = lookup.get_template(str(template.relative_to(templates))) + output_name = template.stem.replace("interface", interface.name) + print(f"{interface.name} via {template.name} => {output_name}") + + result = mako_template.render(**interface.into_dict(), PROG=PROG) + + if isinstance(output, Path): + # print(f"writing to {(output / output_name).absolute()}") + with open(output / output_name, "w", encoding="utf-8") as output_file: + print(result, file=output_file) + else: + print(result, file=output) + + +def main(): + """main function""" + parser = ArgumentParser( + prog="interface_generator", + description="Generate C header and mock files from an interface declaration", + ) + parser.add_argument( + "-i", + "--interface", + type=Path, + # required=True, + help="path to interface file", + ) + parser.add_argument( + "-t", + "--templates", + type=Path, + default=get_templates_dir(), + help="templates location", + ) + parser.add_argument( + "-l", + "--license", + type=Path, + required=True, + help="path to license file", + ) + parser.add_argument( + "-o", + "--output", + type=Path, + default=sys.stdout, + help="path to output, stdout by default", + ) + + args = parser.parse_args() + # print(args) + + interface = InterfaceDeclaration(args.interface, args.license) + # print(interface) + + for template in get_templates(args.templates): + # print(template) + generate_file(template, args.templates, args.output, interface) + + +if __name__ == "__main__": + main() diff --git a/tools/interface_generator/bin/templates.py b/tools/interface_generator/bin/templates.py new file mode 100644 index 0000000..9f7539e --- /dev/null +++ b/tools/interface_generator/bin/templates.py @@ -0,0 +1,19 @@ +"""template helper functions""" + +from pathlib import Path +import re + + +TEMPLATE_PATTERN = re.compile(r"^[^_]\S+\.mako$") + + +def get_templates_dir(name="templates") -> Path: + """get the templates directory""" + return Path(__file__).parent / name + + +def get_templates(path: Path) -> list[Path]: + """list templates in given path""" + result = list(path.glob("*.mako")) + result = [item for item in result if TEMPLATE_PATTERN.match(item.name)] + return result diff --git a/tools/interface_generator/bin/templates/__c_functions.mako b/tools/interface_generator/bin/templates/__c_functions.mako new file mode 100644 index 0000000..2e40119 --- /dev/null +++ b/tools/interface_generator/bin/templates/__c_functions.mako @@ -0,0 +1,4 @@ +/* Functions */ +% for fn in functions: +${fn['return']} ${name}_${fn['name']}(${ ", ".join(fn['arguments']) }); +% endfor diff --git a/tools/interface_generator/bin/templates/__c_system_include.mako b/tools/interface_generator/bin/templates/__c_system_include.mako new file mode 100644 index 0000000..d6a9d09 --- /dev/null +++ b/tools/interface_generator/bin/templates/__c_system_include.mako @@ -0,0 +1,4 @@ +/* System includes */ +% for path in system_includes: +#include <${path}> +% endfor diff --git a/tools/interface_generator/bin/templates/__c_types.mako b/tools/interface_generator/bin/templates/__c_types.mako new file mode 100644 index 0000000..ce6b6b5 --- /dev/null +++ b/tools/interface_generator/bin/templates/__c_types.mako @@ -0,0 +1,6 @@ +/* Types */ +% for type in types: +typedef struct ${name}_${type['name']} { +${ "\n".join([ " {};".format(member) for member in type['members'] ]) } +} ${name}_${type['name']}; +% endfor diff --git a/tools/interface_generator/bin/templates/__header.mako b/tools/interface_generator/bin/templates/__header.mako new file mode 100644 index 0000000..43ce1e7 --- /dev/null +++ b/tools/interface_generator/bin/templates/__header.mako @@ -0,0 +1,5 @@ +/* This file is generated by ${PROG['name']} v${PROG['version']} */ + +/****************************************************************************** +${ license_hdr } + ******************************************************************************/ diff --git a/tools/interface_generator/bin/templates/interface.h.mako b/tools/interface_generator/bin/templates/interface.h.mako new file mode 100644 index 0000000..47ea940 --- /dev/null +++ b/tools/interface_generator/bin/templates/interface.h.mako @@ -0,0 +1,8 @@ +<%include file="__header.mako" /> +#ifndef ${ name.upper() } +#define ${ name.upper() } + +<%include file="__c_system_include.mako" /> +<%include file="__c_types.mako" /> +<%include file="__c_functions.mako" /> +#endif /* ${ name.upper() } */ diff --git a/tools/interface_generator/bin/templates/interface_mock.cpp.mako b/tools/interface_generator/bin/templates/interface_mock.cpp.mako new file mode 100644 index 0000000..721f2c4 --- /dev/null +++ b/tools/interface_generator/bin/templates/interface_mock.cpp.mako @@ -0,0 +1,32 @@ +<%include file="__header.mako" /> + +#include <stdexcept> +#include "${name}_mock.hpp" + +static I${name}_mock *s_instance = nullptr; + +I${name}_mock::I${name}_mock() +{ + if(s_instance != nullptr) + { + throw std::runtime_error("Creating a second instance of mock object"); + } + s_instance = this; +} + +I${name}_mock::~I${name}_mock() +{ + // destructors shouldn't throw exceptions + s_instance = nullptr; +} + +% for fn in functions: +${fn['return']} ${name}_${fn['name']}(${ ", ".join(fn['arguments']) }) +{ + if(s_instance == nullptr) + { + throw std::runtime_error("No mock created to handle function ${name}_${fn['name']}"); + } + return s_instance->${fn['name']}(${ ", ".join(fn['argument_names']) }); +} +% endfor diff --git a/tools/interface_generator/bin/templates/interface_mock.hpp.mako b/tools/interface_generator/bin/templates/interface_mock.hpp.mako new file mode 100644 index 0000000..e33d50e --- /dev/null +++ b/tools/interface_generator/bin/templates/interface_mock.hpp.mako @@ -0,0 +1,26 @@ +<%include file="__header.mako" /> +#ifndef ${ name.upper() }_MOCK +#define ${ name.upper() }_MOCK + +extern "C" { +#include "${ name }.h" +} + +#include <gtest/gtest.h> +#include <gmock/gmock.h> + +class I${name}_mock +{ +public: + I${name}_mock(); + ~I${name}_mock(); + + /* Functions */ +% for fn in functions: + MOCK_METHOD(${fn['return']}, ${fn['name']}, (${ ", ".join(fn['arguments']) })); +% endfor +}; + +using ${name}_mock = ::testing::NiceMock<I${name}_mock>; + +#endif /* ${ name.upper() }_MOCK */ diff --git a/tools/interface_generator/bin/templates_unittest.py b/tools/interface_generator/bin/templates_unittest.py new file mode 100644 index 0000000..acb1685 --- /dev/null +++ b/tools/interface_generator/bin/templates_unittest.py @@ -0,0 +1,30 @@ +""" +templates unit tests +""" + +import unittest +from pathlib import Path +import templates + + +class TemplatesUnittest(unittest.TestCase): + """templates unit tests""" + + def test_get_templates_dir_is_valid(self): + """verify that default templates dir is valid""" + path = templates.get_templates_dir() + + self.assertTrue(isinstance(path, Path)) + self.assertTrue(path.exists()) + self.assertTrue(path.is_dir()) + + def test_get_templates(self): + """verify that templates are available by default""" + path = templates.get_templates_dir() + result = templates.get_templates(path) + + self.assertGreater(len(result), 0) + for template in result: + self.assertTrue(isinstance(template, Path)) + self.assertTrue(template.exists()) + self.assertTrue(template.is_file()) |