Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 20 additions & 1 deletion parser/outparam.py
Original file line number Diff line number Diff line change
Expand Up @@ -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<doc>.*?)\*/\s*\n'
r'/\*\*(?P<doc>.*?)\*/[^\S\n]*\n' + _SKIP +
r'(?:[A-Za-z_][\w\s\*]*?\n)?' # optional return-type line
r'(?P<name>[a-z][a-z0-9_]*)\s*\('
r'(?P<params>[^;{]*?)\)\s*\{',
Expand All @@ -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):
Expand All @@ -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):
Expand Down
131 changes: 131 additions & 0 deletions tests/test_outparam.py
Original file line number Diff line number Diff line change
@@ -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()
Loading