Files

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)