122 lines
5.6 KiB
Python
122 lines
5.6 KiB
Python
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)
|