"""Verify unmatched allowlist diagnostics from an existing CLI executable."""
from __future__ import annotations
import argparse
import os
import subprocess
import tempfile
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
FIXTURE = ROOT / "scripts/testdata/allowlist-diagnostics.hpp"
FASTCALL_FIXTURE = ROOT / "scripts/testdata/mangling-win32.hpp"
MSVC_SYMBOL_FIXTURE = ROOT / "scripts/testdata/method-mangling.hpp"
GENERIC_ALIAS_FIXTURE = ROOT / "scripts/testdata/unsupported-generic-alias.hpp"
PATTERNS = {
"--allowlist-type": "MissingType",
"--allowlist-function": "MissingFunction",
"--allowlist-var": "MISSING_VAR",
"--allowlist-item": "MissingItem",
}
def default_cli() -> Path:
executable = "cjbind_cli.exe" if os.name == "nt" else "cjbind_cli"
candidates = [
ROOT / "target/release/bin" / executable,
ROOT / "target/debug/bin" / executable,
]
for candidate in candidates:
if candidate.is_file():
return candidate
locations = ", ".join(str(candidate) for candidate in candidates)
raise RuntimeError(
f"an existing static CLI executable is required; checked: {locations}"
)
def invoke(cli: Path, *, no_record_matches: bool) -> str:
with tempfile.TemporaryDirectory(prefix="cjbind-cli-diagnostics-") as temp:
output = Path(temp) / "bindings.cj"
command = [
str(cli),
str(FIXTURE),
"-o",
str(output),
"--package",
"cjbind_diagnostics",
"--no-detect-include-path",
"--allowlist-type",
"MatchedType",
]
for flag, pattern in PATTERNS.items():
command.extend([flag, pattern])
command.extend(["--allowlist-file", "MissingHeader"])
if no_record_matches:
command.append("--no-record-matches")
command.extend(["--", "-x", "c++", "-std=c++14"])
result = subprocess.run(
command,
cwd=ROOT,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
encoding="utf-8",
errors="replace",
check=False,
)
diagnostics = result.stdout + result.stderr
if result.returncode != 0:
raise AssertionError(
f"CLI exited with {result.returncode}:\n{diagnostics}"
)
return diagnostics
def assert_recorded_diagnostics(diagnostics: str) -> None:
expected = [
f"Warning: {flag} pattern '{pattern}' did not match any item"
for flag, pattern in PATTERNS.items()
]
for message in expected:
count = diagnostics.count(message)
if count != 1:
raise AssertionError(
f"expected exactly one diagnostic {message!r}, found {count}:\n"
f"{diagnostics}"
)
if "pattern 'MatchedType' did not match any item" in diagnostics:
raise AssertionError(f"matched type was reported as unmatched:\n{diagnostics}")
file_message = (
"Warning: --allowlist-file pattern 'MissingHeader' did not match any item"
)
if file_message in diagnostics:
raise AssertionError(f"allowlist-file was reported as unmatched:\n{diagnostics}")
if diagnostics.count("did not match any item") != len(expected):
raise AssertionError(f"unexpected unmatched diagnostics:\n{diagnostics}")
def assert_suppressed_diagnostics(diagnostics: str) -> None:
if "did not match any item" in diagnostics:
raise AssertionError(
f"--no-record-matches did not suppress diagnostics:\n{diagnostics}"
)
def invoke_unsupported_abi(cli: Path, fixture: Path, target: str) -> str:
with tempfile.TemporaryDirectory(prefix="cjbind-unsupported-abi-") as temp:
output = Path(temp) / "bindings.cj"
result = subprocess.run(
[
str(cli),
str(fixture),
"-o",
str(output),
"--package",
"cjbind_unsupported_abi",
"--no-detect-include-path",
"--",
"-x",
"c++",
"-std=c++14",
f"--target={target}",
],
cwd=ROOT,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
encoding="utf-8",
errors="replace",
check=False,
)
diagnostics = result.stdout + result.stderr
if result.returncode != 0:
raise AssertionError(
f"CLI exited with {result.returncode}:\n{diagnostics}"
)
return diagnostics
def assert_diagnostic(diagnostics: str, expected: str) -> None:
if expected not in diagnostics:
raise AssertionError(
f"missing unsupported ABI diagnostic {expected!r}:\n{diagnostics}"
)
def main() -> None:
parser = argparse.ArgumentParser()
parser.add_argument(
"--cli",
type=Path,
help="path to an existing cjbind_cli executable",
)
args = parser.parse_args()
cli = args.cli.resolve() if args.cli is not None else default_cli()
if not cli.is_file():
raise RuntimeError(f"CLI executable does not exist: {cli}")
if not FIXTURE.is_file():
raise RuntimeError(f"diagnostic fixture does not exist: {FIXTURE}")
for fixture in [FASTCALL_FIXTURE, MSVC_SYMBOL_FIXTURE, GENERIC_ALIAS_FIXTURE]:
if not fixture.is_file():
raise RuntimeError(f"diagnostic fixture does not exist: {fixture}")
assert_recorded_diagnostics(invoke(cli, no_record_matches=False))
assert_suppressed_diagnostics(invoke(cli, no_record_matches=True))
assert_diagnostic(
invoke_unsupported_abi(cli, FASTCALL_FIXTURE, "i686-pc-win32"),
"calling convention 'fastcall' cannot be represented by the current Cangjie FFI",
)
assert_diagnostic(
invoke_unsupported_abi(cli, MSVC_SYMBOL_FIXTURE, "x86_64-pc-windows-msvc"),
"MSVC-decorated ABI symbols cannot be represented by the current Cangjie FFI",
)
assert_diagnostic(
invoke_unsupported_abi(cli, GENERIC_ALIAS_FIXTURE, "i686-pc-win32"),
"Error: skipping C++ template alias 'FastCallback' because calling convention 'fastcall' cannot be represented by the current Cangjie FFI",
)
print("CLI diagnostic regression test passed")
if __name__ == "__main__":
main()