import argparse import json from pathlib import Path import struct import subprocess import tempfile import unittest COMPILER = None ROOT = Path(__file__).resolve().parents[2] MANIFEST = { "port": "example.Native", "adapter": "example.C", "schema": "ffi-test-v1", "capabilities": [], "operations": ["triple", "reject", "fault", "bad_length"], } HELLO = MANIFEST | {"kind": "hello", "version": "dever-component-1", "setting": {}} def frame(message): data = json.dumps(message, separators=(",", ":")).encode() return struct.pack(">I", len(data)) + data def responses(data): values = [] while data: if len(data) < 4: raise AssertionError("truncated frame") size = struct.unpack(">I", data[:4])[0] if not 0 < size <= 16 * 1024 * 1024 or size > len(data) - 4: raise AssertionError("invalid frame size") values.append(json.loads(data[4:4 + size])) data = data[4 + size:] return values class NativeFfi(unittest.TestCase): @classmethod def setUpClass(cls): cls.temporary = tempfile.TemporaryDirectory(prefix="dever-c-ffi-") cls.addClassCleanup(cls.temporary.cleanup) cls.directory = Path(cls.temporary.name) (cls.directory / "lib").mkdir() library = cls.directory / "library.c" library.write_text("long long fixture_triple(long long value) { return value * 3; }\n") subprocess.run([COMPILER, "-shared", "-fPIC", "-Wl,-soname,libfixture.so", str(library), "-o", str(cls.directory / "lib/libfixture.so")], check=True, timeout=30) cls.binary = cls.directory / "worker" subprocess.run([ COMPILER, "-std=c11", "-Wall", "-Wextra", "-Werror", "-I", str(ROOT / "sdk/c"), str(ROOT / "sdk/c/dever_ffi.c"), str(ROOT / "test/external-sdk/c_ffi_adapter.c"), "-L", str(cls.directory / "lib"), "-lfixture", "-Wl,-rpath,$ORIGIN/lib", "-o", str(cls.binary), ], check=True, timeout=30) (cls.directory / "contract.json").write_text(json.dumps(MANIFEST, indent=2)) def execute(self, messages, suffix=b""): result = subprocess.run([str(self.binary)], input=b"".join(frame(message) for message in messages) + suffix, cwd=self.directory, stdout=subprocess.PIPE, stderr=subprocess.PIPE, timeout=5, env={}) return result, responses(result.stdout) def test_c_abi_calls_a_shared_library_and_preserves_protocol_lifecycle(self): result, values = self.execute([ HELLO, {"kind": "health", "id": 0}, {"kind": "call", "id": 1, "operation": "triple", "payload": {"value": 7}}, {"kind": "cancel", "id": 1}, {"kind": "call", "id": 2, "operation": "reject", "payload": {}}, {"kind": "call", "id": 3, "operation": "triple", "payload": {"value": -9}}, {"kind": "shutdown"}, ]) self.assertEqual(result.returncode, 0, result.stderr) self.assertEqual(values, [ MANIFEST | {"kind": "ready", "version": "dever-component-1"}, {"kind": "health", "id": 0}, {"kind": "result", "id": 1, "payload": {"value": 21}}, {"kind": "error", "id": 2, "error": "example.Rejected", "payload": None}, {"kind": "result", "id": 3, "payload": {"value": -27}}, {"kind": "shutdown"}, ]) self.assertEqual(result.stderr, b"closed\n") def test_identity_cannot_be_supplied_by_the_caller(self): for changed in [{"schema": "wrong"}, {"capabilities": ["gpu"]}, {"port": "wrong"}, {"operations": ["other"]}]: with self.subTest(changed=changed): result, values = self.execute([HELLO | changed]) self.assertNotEqual(result.returncode, 0) self.assertEqual(values, []) self.assertNotIn(b"closed", result.stderr) def test_invalid_calls_and_native_results_fail_closed(self): for call in [ {"id": 2, "operation": "triple"}, {"id": 1, "operation": "missing"}, {"id": 1, "operation": "bad_length"}, {"id": 1, "operation": "fault"}, {"id": -1, "operation": "triple"}, {"id": 9223372036854775808, "operation": "triple"}, ]: with self.subTest(call=call): result, values = self.execute([HELLO, {"kind": "call", "payload": {"value": 7}} | call]) self.assertNotEqual(result.returncode, 0) self.assertEqual(len(values), 1) self.assertIn(b"closed\n", result.stderr) def test_truncated_oversized_and_malformed_frames_fail_closed(self): for suffix in [b"\x00", struct.pack(">I", 0), struct.pack(">I", 16777217), struct.pack(">I", 5) + b"{}", frame({"kind": "shutdown", "extra": 1})]: with self.subTest(suffix=suffix): result, values = self.execute([HELLO], suffix) self.assertNotEqual(result.returncode, 0) self.assertEqual(len(values), 1) for malformed in [b"{", b'{"kind":"call","id":1e}', b'{"kind":"\\q"}', b"[" * 65 + b"]" * 65, b'{"kind":"shutdown"}\x00']: result, values = self.execute([HELLO], struct.pack(">I", len(malformed)) + malformed) self.assertNotEqual(result.returncode, 0) self.assertEqual(len(values), 1) if __name__ == "__main__": parser = argparse.ArgumentParser() parser.add_argument("--cc", required=True) options = parser.parse_args() if not Path(options.cc).is_absolute() or not Path(options.cc).is_file(): parser.error("--cc must be an explicit absolute compiler file") COMPILER = options.cc unittest.main(argv=[__file__], verbosity=2)