diff --git a/parser/parser.py b/parser/parser.py index a41b790..094a828 100644 --- a/parser/parser.py +++ b/parser/parser.py @@ -98,6 +98,24 @@ def merge_meta(idl: dict, meta_path: Path) -> dict: return idl +#: The directories of MobilityDB's header tree holding a stand-in for the header of a +#: library a build may lack. MobilityDB's build reads `geos-none/geos_c.h` only where GEOS +#: is absent, and installs it then in place of GEOS's own `geos_c.h` +#: (`meos/CMakeLists.txt`, under `if(NOT GEOS)`), so no installed header tree holds both. +#: What a stand-in declares is that library's API rather than MEOS's, and read beside the +#: library's own header, which `meos_internal_geo.h` includes, every type it declares is a +#: redefinition clang drops: its enums would enter the catalog nameless and valueless. +STAND_IN_DIRS = frozenset({"geos-none"}) + + +def meos_headers(headers_dir: Path) -> list[Path]: + """The headers of the tree the parse reads, in order: those an install of MEOS ships, + the tree's own less its stand-ins (#STAND_IN_DIRS) for a library's header. It is the one + list #build_entry_point includes and #parse_meos owns, so the two read the same tree.""" + return sorted(h for h in headers_dir.glob("**/*.h") + if not STAND_IN_DIRS & set(h.relative_to(headers_dir).parts[:-1])) + + def parse_meos(entry: Path, include_dir: Path, extra_headers: tuple[Path, ...] = ()) -> dict: index = clang.cindex.Index.create() @@ -129,7 +147,7 @@ def parse_meos(entry: Path, include_dir: Path, options=clang.cindex.TranslationUnit.PARSE_DETAILED_PROCESSING_RECORD) # Collect all .h files belonging to the project - own_files = {str(p.resolve()) for p in include_dir.glob("**/*.h")} + own_files = {str(p.resolve()) for p in meos_headers(include_dir)} own_files.update(str(h.resolve()) for h in extra_headers) # First pass: build a mapping "anonymous struct location -> typedef name", and @@ -226,7 +244,7 @@ def _dedup_structs(items: list) -> list: def build_entry_point(headers_dir: Path, extra_headers: tuple[Path, ...] = ()) -> str: lines = [] - for h in sorted(headers_dir.glob("**/*.h")): + for h in meos_headers(headers_dir): lines.append(f'#include "{h.resolve()}"') for h in extra_headers: lines.append(f'#include "{h.resolve()}"') diff --git a/tests/test_stand_in_headers.py b/tests/test_stand_in_headers.py new file mode 100644 index 0000000..abdd8ef --- /dev/null +++ b/tests/test_stand_in_headers.py @@ -0,0 +1,63 @@ +"""Regression tests for the headers the catalog parse reads. + +MobilityDB's header tree holds `geos-none/geos_c.h`, a stand-in its build reads only where +GEOS is absent and installs then in place of GEOS's own `geos_c.h`. Parsed beside the +library's header, which `meos_internal_geo.h` includes, every enum it declares is a +redefinition clang drops: the catalog carried `enum (unnamed at .../geos_c.h:84:6)` with no +value, and a binding writing one Java enum per catalog enum (JMEOS `FunctionsGenerator`) +failed opening a file of that name. #meos_headers of parser/parser.py leaves the stand-in +out, as an install of MEOS does, so the catalog states neither its enums nor its functions. + +The catalog half mirrors #StructLayoutTests of tests/test_struct_layout.py: it reads the +generated IDL and skips where there is none; run ``python run.py`` first. +""" +import json +import re +import tempfile +import unittest +from pathlib import Path + +from parser.parser import meos_headers, build_entry_point + +IDL = Path(__file__).resolve().parents[1] / "output" / "meos-idl.json" +C_IDENTIFIER = re.compile(r"^[A-Za-z_][A-Za-z0-9_]*$") + + +class StandInHeaderTests(unittest.TestCase): + def test_the_stand_in_directory_is_not_read(self): + with tempfile.TemporaryDirectory() as d: + root = Path(d) + for rel in ("meos.h", "geo/postgis_funcs.h", "geos-none/geos_c.h"): + (root / rel).parent.mkdir(parents=True, exist_ok=True) + (root / rel).write_text("") + read = [h.relative_to(root).as_posix() for h in meos_headers(root)] + self.assertEqual(read, ["geo/postgis_funcs.h", "meos.h"]) + self.assertNotIn("geos-none", build_entry_point(root)) + + def test_a_header_named_like_the_stand_in_elsewhere_is_read(self): + with tempfile.TemporaryDirectory() as d: + root = Path(d) + (root / "geo").mkdir() + (root / "geo" / "geos-none.h").write_text("") + self.assertEqual([h.name for h in meos_headers(root)], ["geos-none.h"]) + + +class CatalogEnumNameTests(unittest.TestCase): + def setUp(self): + if not IDL.exists(): + self.skipTest(f"{IDL} not generated; run `python run.py` first") + self.idl = json.loads(IDL.read_text()) + + def test_every_enum_is_named_and_carries_values(self): + for e in self.idl["enums"]: + self.assertRegex(e["name"], C_IDENTIFIER, e["name"]) + self.assertTrue(e["values"], e["name"]) + + def test_no_declaration_comes_from_the_stand_in(self): + for section in ("functions", "structs", "enums"): + files = {x.get("file") for x in self.idl[section]} + self.assertNotIn("geos_c.h", files, section) + + +if __name__ == "__main__": + unittest.main()