diff --git a/parser/outparam.py b/parser/outparam.py index b1d499e..05544d4 100644 --- a/parser/outparam.py +++ b/parser/outparam.py @@ -21,8 +21,19 @@ from pathlib import Path # Doxygen block immediately followed by a function definition (mirrors nullable.py). +# The doxygen block and the function it labels can be separated by preprocessor +# guards (`#if MEOS` … `#endif`) and blank lines — the shape the vendored pgtypes +# base sources use to guard the MEOS-build twin of a symbol (e.g. +# `json_array_elements` in pgtypes/utils/jsonfuncs.c). Skip any run of such lines +# before the optional return-type line. `[^\S\n]` is horizontal whitespace only +# (matches `\r`, so CRLF sources work). This is a line-based subset of +# doxygroup.py._SKIP: the multi-line `/* … */` alternative is deliberately omitted +# here because, combined with this module's whole-doc-block `_FUNC` match, a DOTALL +# `.*?` inside the skip run backtracks catastrophically (doxygroup runs a bounded +# two-step search, so it can afford it); no vendored out-param carrier needs it. +_SKIP = r"(?:[^\S\n]*(?:\#[^\n]*|//[^\n]*)?[^\S\n]*\n)*" _FUNC = re.compile( - r'/\*\*(?P.*?)\*/\s*\n' + r'/\*\*(?P.*?)\*/[^\S\n]*\n' + _SKIP + r'(?:[A-Za-z_][\w\s\*]*?\n)?' # optional return-type line r'(?P[a-z][a-z0-9_]*)\s*\(' r'(?P[^;{]*?)\)\s*\{', @@ -49,6 +60,10 @@ def extract_param_names(meos_root: str | Path) -> dict[str, set]: out: dict[str, set] = {} files = glob.glob(str(root / "src/**/*.c"), recursive=True) files += glob.glob(str(root / "include/**/*.h"), recursive=True) + # The vendored PostgreSQL base-type layer (json, date/interval, …) lives in the + # sibling `pgtypes/` tree, outside meos/, and carries its `@param[out]` tags on + # the `.c` definitions there — scan them too (mirrors doxygroup/run.py pgtypes). + files += glob.glob(str(root.parent / "pgtypes/**/*.c"), recursive=True) for f in files: txt = Path(f).read_text(errors="ignore") for m in _FUNC.finditer(txt): @@ -67,6 +82,10 @@ def extract_outparams(meos_root: str | Path) -> dict[str, list[str]]: out: dict[str, list[str]] = {} files = glob.glob(str(root / "src/**/*.c"), recursive=True) files += glob.glob(str(root / "include/**/*.h"), recursive=True) + # The vendored PostgreSQL base-type layer (json, date/interval, …) lives in the + # sibling `pgtypes/` tree, outside meos/, and carries its `@param[out]` tags on + # the `.c` definitions there — scan them too (mirrors doxygroup/run.py pgtypes). + files += glob.glob(str(root.parent / "pgtypes/**/*.c"), recursive=True) for f in files: txt = Path(f).read_text(errors="ignore") for m in _FUNC.finditer(txt): diff --git a/tests/test_outparam.py b/tests/test_outparam.py new file mode 100644 index 0000000..de2d6ff --- /dev/null +++ b/tests/test_outparam.py @@ -0,0 +1,131 @@ +"""Unit tests for parser/outparam.py. + +Runs without libclang or pytest: python3 tests/test_outparam.py + +Focuses on the two behaviours that let the catalog carry `@param[out]` from the +vendored PostgreSQL base-type layer: (1) `_FUNC` steps over the `#if MEOS` guard +that separates a base function's doxygen block from its definition, and (2) the +extractors scan the sibling `pgtypes/` tree in addition to `meos/src` + +`meos/include` (mirrors doxygroup.py / run.py). +""" + +import sys +import tempfile +import unittest +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parents[1])) + +from parser.outparam import (extract_outparams, extract_param_names, + merge_outparams) + + +def _write(root, rel, text): + p = Path(root) / rel + p.parent.mkdir(parents=True, exist_ok=True) + p.write_bytes(text.encode()) + return p + + +# Plain meos/src function: return type on its own line, name right after. +PLAIN = """\ +/** + * @ingroup meos_temporal_analytics_tile + * @brief doc + * @param[in] temp Temporal value + * @param[out] count Number of elements in the output array + */ +TBox * +tint_value_boxes(const Temporal *temp, int vsize, int vorigin, int *count) +{ + return NULL; +} +""" + +# A base function guarded by `#if MEOS` between the doxygen close and the +# definition — the exact shape of `json_array_elements` in pgtypes/utils/jsonfuncs.c. +GUARDED = """\ +/** + * @ingroup meos_json_base_accessor + * @brief doc + * @param[in] js JSON value + * @param[out] count Number of elements in the output array + */ +#if MEOS +text ** +json_array_elements(const text *js, int *count) +{ + return NULL; +} +#endif /* MEOS */ +""" + +# A @param[out] tag on a by-VALUE parameter: the cross-check must drop it and +# report drift, never fold a non-pointer as an out-param. +BYVALUE = """\ +/** + * @ingroup meos_base + * @brief doc + * @param[out] n Not really an out-param + */ +int +some_fn(int n) +{ + return n; +} +""" + + +class TestExtract(unittest.TestCase): + def test_plain_meos_src(self): + with tempfile.TemporaryDirectory() as root: + _write(root, "meos/src/temporal_tile_meos.c", PLAIN) + out = extract_outparams(Path(root) / "meos") + self.assertEqual(out.get("tint_value_boxes"), ["count"]) + + def test_pgtypes_guarded_twin(self): + # The base function lives in the sibling pgtypes/ tree AND is behind a + # `#if MEOS` guard — both must be handled to pick up its out-param. + with tempfile.TemporaryDirectory() as root: + _write(root, "pgtypes/utils/jsonfuncs.c", GUARDED) + out = extract_outparams(Path(root) / "meos") + self.assertEqual(out.get("json_array_elements"), ["count"]) + + def test_param_names_include_pgtypes(self): + with tempfile.TemporaryDirectory() as root: + _write(root, "pgtypes/utils/jsonfuncs.c", GUARDED) + names = extract_param_names(Path(root) / "meos") + self.assertEqual(names.get("json_array_elements"), {"js", "count"}) + + def test_missing_pgtypes_is_skipped(self): + # A meos-only tree (no sibling pgtypes/) must not raise. + with tempfile.TemporaryDirectory() as root: + _write(root, "meos/src/foo.c", PLAIN) + out = extract_outparams(Path(root) / "meos") + self.assertEqual(out.get("tint_value_boxes"), ["count"]) + + +class TestMergeCrossCheck(unittest.TestCase): + def test_byvalue_tag_is_dropped_as_drift(self): + with tempfile.TemporaryDirectory() as root: + _write(root, "pgtypes/foo.c", BYVALUE) + idl = {"functions": [ + {"name": "some_fn", "params": [{"name": "n", "canonical": "int"}]}]} + idl, n, drift = merge_outparams(idl, Path(root) / "meos") + self.assertEqual(n, 0) + self.assertNotIn("shape", idl["functions"][0]) + self.assertTrue(any(d[0] == "some_fn" for d in drift)) + + def test_nonconst_pointer_is_folded(self): + with tempfile.TemporaryDirectory() as root: + _write(root, "pgtypes/utils/jsonfuncs.c", GUARDED) + idl = {"functions": [{"name": "json_array_elements", "params": [ + {"name": "js", "canonical": "const text *"}, + {"name": "count", "canonical": "int *"}]}]} + idl, n, drift = merge_outparams(idl, Path(root) / "meos") + self.assertEqual(n, 1) + self.assertEqual(idl["functions"][0]["shape"]["outParams"], ["count"]) + + +if __name__ == "__main__": + unittest.main()