Back to home page

EIC code displayed by LXR

 
 

    


File indexing completed on 2026-06-16 07:48:12

0001 """Linker-isolation regression tests for libActsPluginArrow.
0002 
0003 When the plugin is built with ACTS_ARROW_ISOLATED=ON, the arrow/parquet
0004 static archives are absorbed into the plugin .dylib/.so with hidden
0005 visibility plus an exported-symbols allowlist (see
0006 Plugins/Arrow/cmake/exported_symbols.{txt,ld}). The intent is that the
0007 plugin presents NO arrow or parquet symbols to the rest of the process,
0008 so a co-loaded pyarrow (which has its own libarrow) cannot collide with
0009 ACTS's bundled archives under any dlopen mode.
0010 
0011 These tests verify the resulting library actually upholds that contract:
0012 
0013   - no LC_LOAD_DYLIB / NEEDED entry for libarrow*, libparquet*, or
0014     libarrow_dataset*;
0015   - no externally-defined symbol whose top-level namespace (after
0016     demangling) is `arrow::`, `parquet::`, or `arrow_vendored::`.
0017 
0018 When isolation is off the plugin links arrow dynamically and the test
0019 skips — both checks are vacuously meaningless in that mode.
0020 """
0021 
0022 import os
0023 import re
0024 import shutil
0025 import subprocess
0026 import sys
0027 from pathlib import Path
0028 
0029 import pytest
0030 
0031 from helpers import arrowEnabled
0032 
0033 pytestmark = pytest.mark.skipif(
0034     not arrowEnabled, reason="Arrow/Parquet bindings not built"
0035 )
0036 
0037 
0038 _REPO_ROOT = Path(__file__).resolve().parents[3]
0039 _LIB_NAME = "libActsPluginArrow"
0040 
0041 
0042 def _plugin_lib() -> Path:
0043     """Locate the built plugin library in build/lib."""
0044     libdir = _REPO_ROOT / "build" / "lib"
0045     suffix = ".dylib" if sys.platform == "darwin" else ".so"
0046     path = libdir / f"{_LIB_NAME}{suffix}"
0047     if not path.exists():
0048         pytest.skip(f"{path} does not exist")
0049     return path
0050 
0051 
0052 def _direct_deps(lib: Path) -> list[str]:
0053     """Return the direct dynamic-linker dependencies of the library."""
0054     if sys.platform == "darwin":
0055         out = subprocess.check_output(["otool", "-L", str(lib)], text=True)
0056         deps: list[str] = []
0057         # First line is the library's own path; remainder are deps.
0058         for line in out.splitlines()[1:]:
0059             line = line.strip()
0060             if not line:
0061                 continue
0062             # Format: "<path> (compatibility version ...)"
0063             deps.append(line.split(" ", 1)[0])
0064         return deps
0065     out = subprocess.check_output(["readelf", "-d", str(lib)], text=True)
0066     deps = []
0067     for line in out.splitlines():
0068         m = re.search(r"\(NEEDED\).*Shared library:\s*\[(.+)\]", line)
0069         if m:
0070             deps.append(m.group(1))
0071     return deps
0072 
0073 
0074 def _is_arrow_lib(name: str) -> bool:
0075     base = os.path.basename(name)
0076     return base.startswith(("libarrow", "libparquet"))
0077 
0078 
0079 def _exported_symbols_demangled(lib: Path) -> list[str]:
0080     """Return demangled names of externally-defined symbols in the library."""
0081     if sys.platform == "darwin":
0082         # -g external, -U skip undefined, -j just the name
0083         argv = ["nm", "-gUj", str(lib)]
0084     else:
0085         argv = ["nm", "-D", "--defined-only", "--format=just-symbols", str(lib)]
0086     raw = subprocess.check_output(argv, text=True, stderr=subprocess.DEVNULL)
0087     mangled = [line.strip() for line in raw.splitlines() if line.strip()]
0088     if not mangled:
0089         return []
0090     cxxfilt = shutil.which("c++filt") or shutil.which("llvm-cxxfilt")
0091     if cxxfilt is None:
0092         pytest.skip("c++filt / llvm-cxxfilt not available")
0093     proc = subprocess.run(
0094         [cxxfilt],
0095         input="\n".join(mangled),
0096         capture_output=True,
0097         text=True,
0098         check=True,
0099     )
0100     return proc.stdout.splitlines()
0101 
0102 
0103 def _isolation_active(lib: Path) -> bool:
0104     """True iff the build appears to use ACTS_ARROW_ISOLATED=ON.
0105 
0106     Heuristic: in isolated mode the plugin has absorbed the arrow/parquet
0107     archives and so should not list libarrow*/libparquet* in its direct
0108     deps. In non-isolated mode it links those .dylibs/.sos directly.
0109     """
0110     return not any(_is_arrow_lib(d) for d in _direct_deps(lib))
0111 
0112 
0113 # Top-level namespaces that must not leak out of the plugin.
0114 _FORBIDDEN_NS_RE = re.compile(r"^(arrow|parquet|arrow_vendored)::")
0115 
0116 
0117 def test_no_arrow_dynamic_dependencies():
0118     """The plugin must not pull libarrow* / libparquet* into the process
0119     via its own dynamic-link table."""
0120     lib = _plugin_lib()
0121     if not _isolation_active(lib):
0122         pytest.skip("ACTS_ARROW_ISOLATED appears OFF; isolation check N/A")
0123     arrow_deps = [d for d in _direct_deps(lib) if _is_arrow_lib(d)]
0124     assert not arrow_deps, (
0125         f"{lib.name} unexpectedly references arrow/parquet shared "
0126         f"libraries: {arrow_deps}"
0127     )
0128 
0129 
0130 def test_no_exported_arrow_symbols():
0131     """No externally-defined symbol's top-level namespace may be
0132     arrow::, parquet::, or arrow_vendored::. Substring matches against
0133     'arrow' inside ACTS-namespaced symbols (e.g. signatures taking
0134     arrow::Table) are filtered out by checking the demangled prefix."""
0135     lib = _plugin_lib()
0136     if not _isolation_active(lib):
0137         pytest.skip("ACTS_ARROW_ISOLATED appears OFF; symbol-leak check N/A")
0138 
0139     demangled = _exported_symbols_demangled(lib)
0140     assert demangled, f"{lib.name} exports no symbols at all (suspicious)"
0141 
0142     leaks = [s for s in demangled if _FORBIDDEN_NS_RE.match(s)]
0143     assert not leaks, (
0144         f"{lib.name} leaks {len(leaks)} arrow/parquet symbol(s) "
0145         f"out of {len(demangled)} exported. First few:\n  " + "\n  ".join(leaks[:10])
0146     )
0147 
0148 
0149 def test_acts_symbols_are_exported():
0150     """Sanity counterpart: at least one symbol in the Acts/ActsPlugins/
0151     ActsExamples namespace should be exported. Catches the failure mode
0152     where the export allowlist is so strict it hides everything."""
0153     lib = _plugin_lib()
0154     demangled = _exported_symbols_demangled(lib)
0155     acts_syms = [
0156         s for s in demangled if re.match(r"^(Acts|ActsPlugins|ActsExamples)::", s)
0157     ]
0158     assert acts_syms, (
0159         f"{lib.name} exports no Acts*::* symbols (export allowlist "
0160         f"too strict?). Total exports: {len(demangled)}"
0161     )