Minimal pycapnp
This commit is contained in:
@@ -1,39 +0,0 @@
|
||||
@0xc39aee9191aedcf3;
|
||||
|
||||
const qux :UInt32 = 123;
|
||||
|
||||
struct Person {
|
||||
id @0 :UInt32;
|
||||
name @1 :Text;
|
||||
email @2 :Text;
|
||||
phones @3 :List(PhoneNumber);
|
||||
|
||||
struct PhoneNumber {
|
||||
number @0 :Text;
|
||||
type @1 :Type;
|
||||
|
||||
enum Type {
|
||||
mobile @0;
|
||||
home @1;
|
||||
work @2;
|
||||
}
|
||||
}
|
||||
|
||||
employment :union {
|
||||
unemployed @4 :Void;
|
||||
employer @5 :Employer;
|
||||
school @6 :Text;
|
||||
selfEmployed @7 :Void;
|
||||
# We assume that a person is only one of these.
|
||||
}
|
||||
}
|
||||
|
||||
struct Employer {
|
||||
name @0 :Text;
|
||||
boss @1 :Person;
|
||||
}
|
||||
|
||||
struct AddressBook {
|
||||
people @0 :List(Person);
|
||||
}
|
||||
|
||||
@@ -1,39 +0,0 @@
|
||||
@0xd33206731939e03b;
|
||||
|
||||
const qux :UInt32 = 123;
|
||||
|
||||
struct Person {
|
||||
id @0 :UInt32;
|
||||
name @1 :Text;
|
||||
email @2 :Text;
|
||||
phones @3 :List(PhoneNumber);
|
||||
|
||||
struct PhoneNumber {
|
||||
number @0 :Text;
|
||||
type @1 :Type;
|
||||
|
||||
enum Type {
|
||||
mobile @0;
|
||||
home @1;
|
||||
work @2;
|
||||
}
|
||||
}
|
||||
|
||||
employment :union {
|
||||
unemployed @4 :Void;
|
||||
employer @5 :Employer;
|
||||
school @6 :Text;
|
||||
selfEmployed @7 :Void;
|
||||
# We assume that a person is only one of these.
|
||||
}
|
||||
}
|
||||
|
||||
struct Employer {
|
||||
name @0 :Text;
|
||||
boss @1 :Person;
|
||||
}
|
||||
|
||||
struct AddressBook {
|
||||
people @0 :List(Person);
|
||||
}
|
||||
|
||||
Binary file not shown.
@@ -1,17 +0,0 @@
|
||||
@0xfb9a160831eee9bb;
|
||||
|
||||
struct AnnotationStruct {
|
||||
test @0: Int32;
|
||||
}
|
||||
|
||||
annotation test1(*): Text;
|
||||
annotation test2(*): AnnotationStruct;
|
||||
annotation test3(*): List(AnnotationStruct);
|
||||
annotation test4(*): List(UInt16);
|
||||
|
||||
$test1("TestFile");
|
||||
|
||||
struct TestAnnotationOne $test1("Test") { }
|
||||
struct TestAnnotationTwo $test2(test = 100) { }
|
||||
struct TestAnnotationThree $test3([(test=100), (test=101)]) { }
|
||||
struct TestAnnotationFour $test4([200, 201]) { }
|
||||
@@ -1,5 +0,0 @@
|
||||
@0x9afc0f7513269df3;
|
||||
|
||||
struct Child {
|
||||
name @0 :Text;
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
@0x95c41c96183b9c2f;
|
||||
|
||||
using import "/schemas/child.capnp".Child;
|
||||
|
||||
struct Parent {
|
||||
child @0 :List(Child);
|
||||
}
|
||||
@@ -1,108 +0,0 @@
|
||||
"""
|
||||
Regression test for use-after-free bug in async write with large payloads.
|
||||
|
||||
This test reproduces a bug where pipelining multiple RPC calls with large data
|
||||
payloads (>~4000 bytes) causes memory corruption. The root cause was that
|
||||
_PyAsyncIoStreamProtocol.write_loop() passed a memoryview pointing to C++
|
||||
memory to transport.write(), then called fulfill() which freed the C++ memory.
|
||||
Since transport.write() is non-blocking and buffers data asynchronously,
|
||||
the data could be corrupted before being sent.
|
||||
|
||||
The fix is to copy the data to Python bytes before passing to transport.write().
|
||||
|
||||
See: https://github.com/capnproto/pycapnp/pull/392
|
||||
"""
|
||||
|
||||
import pytest
|
||||
import socket
|
||||
|
||||
import capnp
|
||||
import test_capability_capnp
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
async def kj_loop():
|
||||
async with capnp.kj_loop():
|
||||
yield
|
||||
|
||||
|
||||
class LargeResponseServer(test_capability_capnp.TestInterface.Server):
|
||||
"""
|
||||
Server that returns large text responses to trigger the use-after-free bug.
|
||||
|
||||
The bug manifests when response messages are >~4000 bytes.
|
||||
"""
|
||||
|
||||
async def foo(self, i, j, **kwargs):
|
||||
# Generate a large response string based on input
|
||||
# The size is controlled by the input parameter 'i'
|
||||
size = i
|
||||
# Create a deterministic pattern that can be verified
|
||||
pattern = "".join(chr(65 + (k % 26)) for k in range(size))
|
||||
return pattern
|
||||
|
||||
|
||||
async def test_large_response_sequential():
|
||||
"""
|
||||
Test that large RPC responses are received correctly when sent sequentially.
|
||||
|
||||
Tests various payload sizes including those >4000 bytes which trigger the bug.
|
||||
"""
|
||||
read_sock, write_sock = socket.socketpair()
|
||||
read_stream = await capnp.AsyncIoStream.create_connection(sock=read_sock)
|
||||
write_stream = await capnp.AsyncIoStream.create_connection(sock=write_sock)
|
||||
|
||||
_ = capnp.TwoPartyServer(write_stream, bootstrap=LargeResponseServer())
|
||||
client = capnp.TwoPartyClient(read_stream)
|
||||
cap = client.bootstrap().cast_as(test_capability_capnp.TestInterface)
|
||||
|
||||
# Test various sizes, including sizes that trigger the bug (>~4000 bytes)
|
||||
test_sizes = [100, 1000, 4000, 5000, 8000]
|
||||
|
||||
for size in test_sizes:
|
||||
response = await cap.foo(i=size, j=False)
|
||||
|
||||
# Verify the response has the correct length
|
||||
assert len(response.x) == size, f"Size mismatch for {size}: expected {size}, got {len(response.x)}"
|
||||
|
||||
# Verify the pattern is correct (not corrupted)
|
||||
expected = "".join(chr(65 + (k % 26)) for k in range(size))
|
||||
assert response.x == expected, (
|
||||
f"Data corruption detected for {size} bytes payload! "
|
||||
f"First 50 chars: expected '{expected[:50]}', got '{response.x[:50]}'"
|
||||
)
|
||||
|
||||
|
||||
async def test_large_response_pipelined():
|
||||
"""
|
||||
Test that pipelining multiple RPC calls with large responses works correctly.
|
||||
|
||||
This is a more aggressive test that sends multiple requests without awaiting,
|
||||
then collects all results. This pattern is more likely to trigger the
|
||||
use-after-free bug because multiple messages are queued in the write buffer.
|
||||
"""
|
||||
read_sock, write_sock = socket.socketpair()
|
||||
read_stream = await capnp.AsyncIoStream.create_connection(sock=read_sock)
|
||||
write_stream = await capnp.AsyncIoStream.create_connection(sock=write_sock)
|
||||
|
||||
_ = capnp.TwoPartyServer(write_stream, bootstrap=LargeResponseServer())
|
||||
client = capnp.TwoPartyClient(read_stream)
|
||||
cap = client.bootstrap().cast_as(test_capability_capnp.TestInterface)
|
||||
|
||||
# Test sizes that trigger the bug - send 3 pipelined requests
|
||||
test_sizes = [5000, 6000, 8000]
|
||||
|
||||
# Send all requests without awaiting (pipelining)
|
||||
promises = []
|
||||
for size in test_sizes:
|
||||
promise = cap.foo(i=size, j=False)
|
||||
promises.append((size, promise))
|
||||
|
||||
# Now await all responses and verify
|
||||
for size, promise in promises:
|
||||
response = await promise
|
||||
|
||||
assert len(response.x) == size, f"Size mismatch for {size}"
|
||||
|
||||
expected = "".join(chr(65 + (k % 26)) for k in range(size))
|
||||
assert response.x == expected, f"Data corruption detected for {size} bytes payload!"
|
||||
@@ -1,97 +0,0 @@
|
||||
# Copyright (c) 2013, Kenton Varda <temporal@gmail.com>
|
||||
# All rights reserved.
|
||||
#
|
||||
# Redistribution and use in source and binary forms, with or without
|
||||
# modification, are permitted provided that the following conditions are met:
|
||||
#
|
||||
# 1. Redistributions of source code must retain the above copyright notice, this
|
||||
# list of conditions and the following disclaimer.
|
||||
# 2. Redistributions in binary form must reproduce the above copyright notice,
|
||||
# this list of conditions and the following disclaimer in the documentation
|
||||
# and/or other materials provided with the distribution.
|
||||
#
|
||||
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
||||
# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
||||
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
||||
# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
|
||||
# ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
|
||||
# (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
|
||||
# LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
|
||||
# ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
|
||||
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
|
||||
# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
||||
|
||||
@0xd508eefec2dc42b8;
|
||||
|
||||
interface TestInterface {
|
||||
foo @0 (i :UInt32, j :Bool) -> (x: Text);
|
||||
bar @1 () -> ();
|
||||
buz @2 (i: TestSturdyRefHostId) -> (x: Text);
|
||||
bam @3 (i :UInt32, j :Bool) -> (x: Text, i:UInt32);
|
||||
bak1 @4 () -> (i:List(UInt32));
|
||||
bak2 @5 (i:List(UInt32)) -> ();
|
||||
# baz @2 (s: TestAllTypes);
|
||||
}
|
||||
|
||||
interface TestExtends extends(TestInterface) {
|
||||
qux @0 ();
|
||||
}
|
||||
|
||||
interface TestPipeline {
|
||||
getCap @0 (n: UInt32, inCap :TestInterface) -> (s: Text, outBox :Box);
|
||||
testPointers @1 (cap :TestInterface, obj :AnyPointer, list :List(TestInterface)) -> ();
|
||||
|
||||
struct Box {
|
||||
cap @0 :TestInterface;
|
||||
}
|
||||
}
|
||||
|
||||
struct TestSturdyRefHostId {
|
||||
host @0 :Text;
|
||||
}
|
||||
|
||||
struct TestSturdyRefObjectId {
|
||||
tag @0 :Tag;
|
||||
enum Tag {
|
||||
testInterface @0;
|
||||
testExtends @1;
|
||||
testPipeline @2;
|
||||
}
|
||||
}
|
||||
|
||||
interface TestCallOrder {
|
||||
getCallSequence @0 (expected: UInt32) -> (n: UInt32);
|
||||
# First call returns 0, next returns 1, ...
|
||||
#
|
||||
# The input `expected` is ignored but useful for disambiguating debug logs.
|
||||
}
|
||||
|
||||
interface TestTailCallee {
|
||||
struct TailResult {
|
||||
i @0 :UInt32;
|
||||
t @1 :Text;
|
||||
c @2 :TestCallOrder;
|
||||
}
|
||||
|
||||
foo @0 (i :Int32, t :Text) -> TailResult;
|
||||
}
|
||||
|
||||
interface TestTailCaller {
|
||||
foo @0 (i :Int32, callee :TestTailCallee) -> TestTailCallee.TailResult;
|
||||
}
|
||||
|
||||
interface TestPassedCap {
|
||||
foo @0 (cap :TestInterface) -> (x: Text);
|
||||
}
|
||||
|
||||
interface TestStructArg {
|
||||
bar @0 BarParams -> (c: Text);
|
||||
}
|
||||
struct BarParams {
|
||||
a @0 :Text;
|
||||
b @1 :Int32;
|
||||
}
|
||||
|
||||
interface TestGeneric(MyObject) {
|
||||
foo @0 (a :MyObject) -> (b: Text);
|
||||
}
|
||||
@@ -1,411 +0,0 @@
|
||||
import pytest
|
||||
import asyncio
|
||||
|
||||
import capnp
|
||||
import test_capability_capnp as capability
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
async def kj_loop():
|
||||
async with capnp.kj_loop():
|
||||
yield
|
||||
|
||||
|
||||
class Server(capability.TestInterface.Server):
|
||||
def __init__(self, val=1):
|
||||
self.val = val
|
||||
|
||||
async def foo(self, i, j, **kwargs):
|
||||
extra = 0
|
||||
if j:
|
||||
extra = 1
|
||||
return str(i * 5 + extra + self.val)
|
||||
|
||||
async def buz(self, i, **kwargs):
|
||||
return i.host + "_test"
|
||||
|
||||
async def bam(self, i, **kwargs):
|
||||
return str(i) + "_test", i
|
||||
|
||||
async def bak1(self, **kwargs):
|
||||
return [1, 2, 3, 4, 5]
|
||||
|
||||
async def bak2(self, i, **kwargs):
|
||||
assert i[4] == 5
|
||||
|
||||
|
||||
class PipelineServer(capability.TestPipeline.Server):
|
||||
async def getCap(self, n, inCap, _context, **kwargs):
|
||||
response = await inCap.foo(i=n)
|
||||
_results = _context.results
|
||||
_results.s = response.x + "_foo"
|
||||
_results.outBox.cap = Server(100)
|
||||
|
||||
|
||||
async def test_client():
|
||||
client = capability.TestInterface._new_client(Server())
|
||||
|
||||
req = client._request("foo")
|
||||
req.i = 5
|
||||
|
||||
remote = req.send()
|
||||
response = await remote
|
||||
|
||||
assert response.x == "26"
|
||||
|
||||
req = client.foo_request()
|
||||
req.i = 5
|
||||
|
||||
remote = req.send()
|
||||
response = await remote
|
||||
|
||||
assert response.x == "26"
|
||||
|
||||
with pytest.raises(AttributeError):
|
||||
client.foo2_request()
|
||||
|
||||
req = client.foo_request()
|
||||
|
||||
with pytest.raises(Exception):
|
||||
req.i = "foo"
|
||||
|
||||
req = client.foo_request()
|
||||
|
||||
with pytest.raises(AttributeError):
|
||||
req.baz = 1
|
||||
|
||||
resp = await client.bak1()
|
||||
# Used to fail with
|
||||
# capnp.lib.capnp.KjException: Tried to set field: 'i' with a value of: '[1, 2, 3, 4, 5]'
|
||||
# which is an unsupported type: '<class 'capnp.lib.capnp._DynamicListReader'>'
|
||||
await client.bak2(resp.i)
|
||||
|
||||
|
||||
async def test_simple_client():
|
||||
client = capability.TestInterface._new_client(Server())
|
||||
|
||||
remote = client._send("foo", i=5)
|
||||
response = await remote
|
||||
|
||||
assert response.x == "26"
|
||||
|
||||
remote = client.foo(i=5)
|
||||
response = await remote
|
||||
|
||||
assert response.x == "26"
|
||||
|
||||
remote = client.foo(i=5, j=True)
|
||||
response = await remote
|
||||
|
||||
assert response.x == "27"
|
||||
|
||||
remote = client.foo(5)
|
||||
response = await remote
|
||||
|
||||
assert response.x == "26"
|
||||
|
||||
remote = client.foo(5, True)
|
||||
response = await remote
|
||||
|
||||
assert response.x == "27"
|
||||
|
||||
remote = client.foo(5, j=True)
|
||||
response = await remote
|
||||
|
||||
assert response.x == "27"
|
||||
|
||||
remote = client.buz(capability.TestSturdyRefHostId.new_message(host="localhost"))
|
||||
response = await remote
|
||||
|
||||
assert response.x == "localhost_test"
|
||||
|
||||
remote = client.bam(i=5)
|
||||
response = await remote
|
||||
|
||||
assert response.x == "5_test"
|
||||
assert response.i == 5
|
||||
|
||||
with pytest.raises(Exception):
|
||||
remote = client.foo(5, 10)
|
||||
|
||||
with pytest.raises(Exception):
|
||||
remote = client.foo(5, True, 100)
|
||||
|
||||
with pytest.raises(Exception):
|
||||
remote = client.foo(i="foo")
|
||||
|
||||
with pytest.raises(AttributeError):
|
||||
remote = client.foo2(i=5)
|
||||
|
||||
with pytest.raises(Exception):
|
||||
remote = client.foo(baz=5)
|
||||
|
||||
|
||||
async def test_pipeline():
|
||||
client = capability.TestPipeline._new_client(PipelineServer())
|
||||
foo_client = capability.TestInterface._new_client(Server())
|
||||
|
||||
remote = client.getCap(n=5, inCap=foo_client)
|
||||
|
||||
outCap = remote.outBox.cap
|
||||
pipelinePromise = outCap.foo(i=10)
|
||||
|
||||
response = await pipelinePromise
|
||||
assert response.x == "150"
|
||||
|
||||
response = await remote
|
||||
assert response.s == "26_foo"
|
||||
|
||||
|
||||
class BadServer(capability.TestInterface.Server):
|
||||
def __init__(self, val=1):
|
||||
self.val = val
|
||||
|
||||
async def foo(self, i, j, **kwargs):
|
||||
extra = 0
|
||||
if j:
|
||||
extra = 1
|
||||
return str(i * 5 + extra + self.val), 10 # returning too many args
|
||||
|
||||
|
||||
async def test_exception_client():
|
||||
client = capability.TestInterface._new_client(BadServer())
|
||||
|
||||
remote = client._send("foo", i=5)
|
||||
with pytest.raises(capnp.KjException):
|
||||
await remote
|
||||
|
||||
|
||||
class BadPipelineServer(capability.TestPipeline.Server):
|
||||
async def getCap(self, n, inCap, _context, **kwargs):
|
||||
try:
|
||||
await inCap.foo(i=n)
|
||||
except capnp.KjException:
|
||||
raise Exception("test was a success")
|
||||
|
||||
|
||||
async def test_exception_chain():
|
||||
client = capability.TestPipeline._new_client(BadPipelineServer())
|
||||
foo_client = capability.TestInterface._new_client(BadServer())
|
||||
|
||||
remote = client.getCap(n=5, inCap=foo_client)
|
||||
|
||||
try:
|
||||
await remote
|
||||
except Exception as e:
|
||||
assert "test was a success" in str(e)
|
||||
|
||||
|
||||
async def test_pipeline_exception():
|
||||
client = capability.TestPipeline._new_client(BadPipelineServer())
|
||||
foo_client = capability.TestInterface._new_client(BadServer())
|
||||
|
||||
remote = client.getCap(n=5, inCap=foo_client)
|
||||
|
||||
outCap = remote.outBox.cap
|
||||
pipelinePromise = outCap.foo(i=10)
|
||||
|
||||
with pytest.raises(Exception):
|
||||
await pipelinePromise
|
||||
|
||||
with pytest.raises(Exception):
|
||||
await remote
|
||||
|
||||
|
||||
async def test_casting():
|
||||
client = capability.TestExtends._new_client(Server())
|
||||
client2 = client.upcast(capability.TestInterface)
|
||||
_ = client2.cast_as(capability.TestInterface)
|
||||
|
||||
with pytest.raises(Exception):
|
||||
client.upcast(capability.TestPipeline)
|
||||
|
||||
|
||||
class TailCallOrder(capability.TestCallOrder.Server):
|
||||
def __init__(self):
|
||||
self.count = -1
|
||||
|
||||
async def getCallSequence(self, expected, **kwargs):
|
||||
self.count += 1
|
||||
return self.count
|
||||
|
||||
|
||||
class TailCaller(capability.TestTailCaller.Server):
|
||||
def __init__(self):
|
||||
self.count = 0
|
||||
|
||||
async def foo(self, i, callee, _context, **kwargs):
|
||||
self.count += 1
|
||||
|
||||
tail = callee.foo_request(i=i, t="from TailCaller")
|
||||
return await _context.tail_call(tail)
|
||||
|
||||
|
||||
class TailCallee(capability.TestTailCallee.Server):
|
||||
def __init__(self):
|
||||
self.count = 0
|
||||
|
||||
async def foo(self, i, t, _context, **kwargs):
|
||||
self.count += 1
|
||||
|
||||
results = _context.results
|
||||
results.i = i
|
||||
results.t = t
|
||||
results.c = TailCallOrder()
|
||||
|
||||
|
||||
async def test_tail_call():
|
||||
callee_server = TailCallee()
|
||||
caller_server = TailCaller()
|
||||
|
||||
callee = capability.TestTailCallee._new_client(callee_server)
|
||||
caller = capability.TestTailCaller._new_client(caller_server)
|
||||
|
||||
promise = caller.foo(i=456, callee=callee)
|
||||
dependent_call1 = promise.c.getCallSequence()
|
||||
|
||||
response = await promise
|
||||
|
||||
assert response.i == 456
|
||||
assert response.i == 456
|
||||
|
||||
dependent_call2 = response.c.getCallSequence()
|
||||
dependent_call3 = response.c.getCallSequence()
|
||||
|
||||
result = await dependent_call1
|
||||
assert result.n == 0
|
||||
result = await dependent_call2
|
||||
assert result.n == 1
|
||||
result = await dependent_call3
|
||||
assert result.n == 2
|
||||
|
||||
assert callee_server.count == 1
|
||||
assert caller_server.count == 1
|
||||
|
||||
|
||||
async def test_cancel():
|
||||
client = capability.TestInterface._new_client(Server())
|
||||
|
||||
req = client._request("foo")
|
||||
req.i = 5
|
||||
|
||||
remote = req.send()
|
||||
remote.cancel()
|
||||
|
||||
with pytest.raises(Exception):
|
||||
await remote
|
||||
|
||||
req = client.foo(5)
|
||||
await req
|
||||
req.cancel() # Cancel a promise that was already consumed
|
||||
|
||||
req = client.foo(5)
|
||||
req.cancel()
|
||||
with pytest.raises(Exception):
|
||||
await req
|
||||
|
||||
req = client.foo(5)
|
||||
assert (await req).x == "26"
|
||||
with pytest.raises(Exception):
|
||||
await req
|
||||
|
||||
|
||||
async def test_double_send():
|
||||
client = capability.TestInterface._new_client(Server())
|
||||
|
||||
req = client._request("foo")
|
||||
req.i = 5
|
||||
|
||||
await req.send()
|
||||
with pytest.raises(Exception):
|
||||
await req.send()
|
||||
|
||||
|
||||
class PromiseJoinServer(capability.TestPipeline.Server):
|
||||
async def getCap(self, n, inCap, _context, **kwargs):
|
||||
res = await inCap.foo(i=n)
|
||||
response = await inCap.foo(i=int(res.x) + 1)
|
||||
_results = _context.results
|
||||
_results.s = response.x + "_bar"
|
||||
_results.outBox.cap = inCap
|
||||
|
||||
|
||||
async def test_promise_joining():
|
||||
client = capability.TestPipeline._new_client(PromiseJoinServer())
|
||||
foo_client = capability.TestInterface._new_client(Server())
|
||||
|
||||
remote = client.getCap(n=5, inCap=foo_client)
|
||||
assert (await remote).s == "136_bar"
|
||||
|
||||
|
||||
class ExtendsServer(Server):
|
||||
async def qux(self, **kwargs):
|
||||
pass
|
||||
|
||||
|
||||
async def test_inheritance():
|
||||
client = capability.TestExtends._new_client(ExtendsServer())
|
||||
await client.qux()
|
||||
|
||||
remote = client.foo(i=5)
|
||||
response = await remote
|
||||
|
||||
assert response.x == "26"
|
||||
|
||||
|
||||
class PassedCapTest(capability.TestPassedCap.Server):
|
||||
async def foo(self, cap, _context, **kwargs):
|
||||
res = await cap.foo(5)
|
||||
_context.results.x = res.x
|
||||
|
||||
|
||||
async def test_null_cap():
|
||||
client = capability.TestPassedCap._new_client(PassedCapTest())
|
||||
assert (await client.foo(Server())).x == "26"
|
||||
|
||||
with pytest.raises(capnp.KjException):
|
||||
await client.foo()
|
||||
|
||||
|
||||
class StructArgTest(capability.TestStructArg.Server):
|
||||
async def bar(self, a, b, **kwargs):
|
||||
return a + str(b)
|
||||
|
||||
|
||||
async def test_struct_args():
|
||||
client = capability.TestStructArg._new_client(StructArgTest())
|
||||
assert (await client.bar(a="test", b=1)).c == "test1"
|
||||
with pytest.raises(capnp.KjException):
|
||||
assert (await client.bar("test", 1)).c == "test1"
|
||||
|
||||
|
||||
class GenericTest(capability.TestGeneric.Server):
|
||||
async def foo(self, a, **kwargs):
|
||||
return a.as_text() + "test"
|
||||
|
||||
|
||||
async def test_generic():
|
||||
client = capability.TestGeneric._new_client(GenericTest())
|
||||
|
||||
obj = capnp._MallocMessageBuilder().get_root_as_any()
|
||||
obj.set_as_text("anypointer_")
|
||||
assert (await client.foo(obj)).b == "anypointer_test"
|
||||
|
||||
|
||||
class CancelServer(capability.TestInterface.Server):
|
||||
def __init__(self, val=1):
|
||||
self.val = val
|
||||
|
||||
async def foo(self, i, j, **kwargs):
|
||||
with pytest.raises(asyncio.CancelledError):
|
||||
await asyncio.sleep(10)
|
||||
|
||||
|
||||
async def test_cancel2():
|
||||
client = capability.TestInterface._new_client(CancelServer())
|
||||
|
||||
task = asyncio.ensure_future(client.foo(1, True))
|
||||
await asyncio.sleep(0) # Make sure that the task runs
|
||||
task.cancel()
|
||||
with pytest.raises(asyncio.CancelledError):
|
||||
await task
|
||||
@@ -1,258 +0,0 @@
|
||||
import pytest
|
||||
|
||||
import capnp
|
||||
import test_capability_capnp as capability
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
async def kj_loop():
|
||||
async with capnp.kj_loop():
|
||||
yield
|
||||
|
||||
|
||||
class Server(capability.TestInterface.Server):
|
||||
def __init__(self, val=1):
|
||||
self.val = val
|
||||
|
||||
async def foo_context(self, context):
|
||||
extra = 0
|
||||
if context.params.j:
|
||||
extra = 1
|
||||
context.results.x = str(context.params.i * 5 + extra + self.val)
|
||||
|
||||
async def buz_context(self, context):
|
||||
context.results.x = context.params.i.host + "_test"
|
||||
|
||||
|
||||
class PipelineServer(capability.TestPipeline.Server):
|
||||
async def getCap_context(self, context):
|
||||
response = await context.params.inCap.foo(i=context.params.n)
|
||||
context.results.s = response.x + "_foo"
|
||||
context.results.outBox.cap = Server(100)
|
||||
|
||||
|
||||
async def test_client_context():
|
||||
client = capability.TestInterface._new_client(Server())
|
||||
|
||||
req = client._request("foo")
|
||||
req.i = 5
|
||||
|
||||
remote = req.send()
|
||||
response = await remote
|
||||
|
||||
assert response.x == "26"
|
||||
|
||||
req = client.foo_request()
|
||||
req.i = 5
|
||||
|
||||
remote = req.send()
|
||||
response = await remote
|
||||
|
||||
assert response.x == "26"
|
||||
|
||||
with pytest.raises(AttributeError):
|
||||
client.foo2_request()
|
||||
|
||||
req = client.foo_request()
|
||||
|
||||
with pytest.raises(Exception):
|
||||
req.i = "foo"
|
||||
|
||||
req = client.foo_request()
|
||||
|
||||
with pytest.raises(AttributeError):
|
||||
req.baz = 1
|
||||
|
||||
|
||||
async def test_simple_client_context():
|
||||
client = capability.TestInterface._new_client(Server())
|
||||
|
||||
remote = client._send("foo", i=5)
|
||||
response = await remote
|
||||
|
||||
assert response.x == "26"
|
||||
|
||||
remote = client.foo(i=5)
|
||||
response = await remote
|
||||
|
||||
assert response.x == "26"
|
||||
|
||||
remote = client.foo(i=5, j=True)
|
||||
response = await remote
|
||||
|
||||
assert response.x == "27"
|
||||
|
||||
remote = client.foo(5)
|
||||
response = await remote
|
||||
|
||||
assert response.x == "26"
|
||||
|
||||
remote = client.foo(5, True)
|
||||
response = await remote
|
||||
|
||||
assert response.x == "27"
|
||||
|
||||
remote = client.foo(5, j=True)
|
||||
response = await remote
|
||||
|
||||
assert response.x == "27"
|
||||
|
||||
remote = client.buz(capability.TestSturdyRefHostId.new_message(host="localhost"))
|
||||
response = await remote
|
||||
|
||||
assert response.x == "localhost_test"
|
||||
|
||||
with pytest.raises(Exception):
|
||||
remote = client.foo(5, 10)
|
||||
|
||||
with pytest.raises(Exception):
|
||||
remote = client.foo(5, True, 100)
|
||||
|
||||
with pytest.raises(Exception):
|
||||
remote = client.foo(i="foo")
|
||||
|
||||
with pytest.raises(AttributeError):
|
||||
remote = client.foo2(i=5)
|
||||
|
||||
with pytest.raises(Exception):
|
||||
remote = client.foo(baz=5)
|
||||
|
||||
|
||||
async def test_pipeline_context():
|
||||
client = capability.TestPipeline._new_client(PipelineServer())
|
||||
foo_client = capability.TestInterface._new_client(Server())
|
||||
|
||||
remote = client.getCap(n=5, inCap=foo_client)
|
||||
|
||||
outCap = remote.outBox.cap
|
||||
pipelinePromise = outCap.foo(i=10)
|
||||
|
||||
response = await pipelinePromise
|
||||
assert response.x == "150"
|
||||
|
||||
response = await remote
|
||||
assert response.s == "26_foo"
|
||||
|
||||
|
||||
class BadServer(capability.TestInterface.Server):
|
||||
def __init__(self, val=1):
|
||||
self.val = val
|
||||
|
||||
async def foo_context(self, context):
|
||||
context.results.x = str(context.params.i * 5 + self.val)
|
||||
context.results.x2 = 5 # raises exception
|
||||
|
||||
|
||||
async def test_exception_client_context():
|
||||
client = capability.TestInterface._new_client(BadServer())
|
||||
|
||||
remote = client._send("foo", i=5)
|
||||
with pytest.raises(capnp.KjException):
|
||||
await remote
|
||||
|
||||
|
||||
class BadPipelineServer(capability.TestPipeline.Server):
|
||||
async def getCap_context(self, context):
|
||||
try:
|
||||
await context.params.inCap.foo(i=context.params.n)
|
||||
except capnp.KjException:
|
||||
raise Exception("test was a success")
|
||||
|
||||
|
||||
async def test_exception_chain_context():
|
||||
client = capability.TestPipeline._new_client(BadPipelineServer())
|
||||
foo_client = capability.TestInterface._new_client(BadServer())
|
||||
|
||||
remote = client.getCap(n=5, inCap=foo_client)
|
||||
|
||||
try:
|
||||
await remote
|
||||
except Exception as e:
|
||||
assert "test was a success" in str(e)
|
||||
|
||||
|
||||
async def test_pipeline_exception_context():
|
||||
client = capability.TestPipeline._new_client(BadPipelineServer())
|
||||
foo_client = capability.TestInterface._new_client(BadServer())
|
||||
|
||||
remote = client.getCap(n=5, inCap=foo_client)
|
||||
|
||||
outCap = remote.outBox.cap
|
||||
pipelinePromise = outCap.foo(i=10)
|
||||
|
||||
with pytest.raises(Exception):
|
||||
await pipelinePromise
|
||||
|
||||
with pytest.raises(Exception):
|
||||
await remote
|
||||
|
||||
|
||||
async def test_casting_context():
|
||||
client = capability.TestExtends._new_client(Server())
|
||||
client2 = client.upcast(capability.TestInterface)
|
||||
_ = client2.cast_as(capability.TestInterface)
|
||||
|
||||
with pytest.raises(Exception):
|
||||
client.upcast(capability.TestPipeline)
|
||||
|
||||
|
||||
class TailCallOrder(capability.TestCallOrder.Server):
|
||||
def __init__(self):
|
||||
self.count = -1
|
||||
|
||||
async def getCallSequence_context(self, context):
|
||||
self.count += 1
|
||||
context.results.n = self.count
|
||||
|
||||
|
||||
class TailCaller(capability.TestTailCaller.Server):
|
||||
def __init__(self):
|
||||
self.count = 0
|
||||
|
||||
async def foo_context(self, context):
|
||||
self.count += 1
|
||||
|
||||
tail = context.params.callee.foo_request(i=context.params.i, t="from TailCaller")
|
||||
await context.tail_call(tail)
|
||||
|
||||
|
||||
class TailCallee(capability.TestTailCallee.Server):
|
||||
def __init__(self):
|
||||
self.count = 0
|
||||
|
||||
async def foo_context(self, context):
|
||||
self.count += 1
|
||||
|
||||
results = context.results
|
||||
results.i = context.params.i
|
||||
results.t = context.params.t
|
||||
results.c = TailCallOrder()
|
||||
|
||||
|
||||
async def test_tail_call():
|
||||
callee_server = TailCallee()
|
||||
caller_server = TailCaller()
|
||||
|
||||
callee = capability.TestTailCallee._new_client(callee_server)
|
||||
caller = capability.TestTailCaller._new_client(caller_server)
|
||||
|
||||
promise = caller.foo(i=456, callee=callee)
|
||||
dependent_call1 = promise.c.getCallSequence()
|
||||
|
||||
response = await promise
|
||||
|
||||
assert response.i == 456
|
||||
assert response.i == 456
|
||||
|
||||
dependent_call2 = response.c.getCallSequence()
|
||||
dependent_call3 = response.c.getCallSequence()
|
||||
|
||||
result = await dependent_call1
|
||||
assert result.n == 0
|
||||
result = await dependent_call2
|
||||
assert result.n == 1
|
||||
result = await dependent_call3
|
||||
assert result.n == 2
|
||||
|
||||
assert callee_server.count == 1
|
||||
assert caller_server.count == 1
|
||||
@@ -1,241 +0,0 @@
|
||||
import pytest
|
||||
import asyncio
|
||||
import socket
|
||||
|
||||
import capnp
|
||||
import test_capability
|
||||
import test_capability_capnp as capability
|
||||
|
||||
|
||||
async def test_two_kj_one_asyncio():
|
||||
async with capnp.kj_loop():
|
||||
pass
|
||||
async with capnp.kj_loop():
|
||||
pass
|
||||
|
||||
|
||||
def test_two_kj_two_asyncio():
|
||||
async def do():
|
||||
async with capnp.kj_loop():
|
||||
pass
|
||||
|
||||
asyncio.run(do())
|
||||
asyncio.run(do())
|
||||
|
||||
|
||||
async def test_nested_kj():
|
||||
with pytest.raises(RuntimeError) as exninfo:
|
||||
async with capnp.kj_loop():
|
||||
async with capnp.kj_loop():
|
||||
pass
|
||||
assert "The KJ event-loop is already running" in str(exninfo)
|
||||
|
||||
|
||||
async def test_kj_loop_leak_new_client():
|
||||
async with capnp.kj_loop():
|
||||
client = capability.TestInterface._new_client(test_capability.Server())
|
||||
with pytest.raises(RuntimeError) as exninfo:
|
||||
await client.foo(5, True)
|
||||
assert "The KJ event-loop is not running" in str(exninfo)
|
||||
|
||||
|
||||
async def test_kj_loop_leak_client():
|
||||
read, write = socket.socketpair()
|
||||
async with capnp.kj_loop():
|
||||
read = await capnp.AsyncIoStream.create_connection(sock=read)
|
||||
write = await capnp.AsyncIoStream.create_connection(sock=write)
|
||||
_ = capnp.TwoPartyServer(write, bootstrap=test_capability.Server())
|
||||
client = capnp.TwoPartyClient(read)
|
||||
cap = client.bootstrap().cast_as(capability.TestInterface)
|
||||
with pytest.raises(RuntimeError) as exninfo:
|
||||
await cap.foo(5, True)
|
||||
assert "The KJ event-loop is not running" in str(exninfo)
|
||||
|
||||
|
||||
async def test_kj_loop_leak_client2():
|
||||
read, write = socket.socketpair()
|
||||
async with capnp.kj_loop():
|
||||
read = await capnp.AsyncIoStream.create_connection(sock=read)
|
||||
write = await capnp.AsyncIoStream.create_connection(sock=write)
|
||||
_ = capnp.TwoPartyServer(write, bootstrap=test_capability.Server())
|
||||
client = capnp.TwoPartyClient(read)
|
||||
with pytest.raises(RuntimeError) as exninfo:
|
||||
client.bootstrap().cast_as(capability.TestInterface)
|
||||
assert "This client is closed" in str(exninfo)
|
||||
|
||||
|
||||
async def test_kj_loop_leak_client3():
|
||||
read, write = socket.socketpair()
|
||||
async with capnp.kj_loop():
|
||||
read = await capnp.AsyncIoStream.create_connection(sock=read)
|
||||
write = await capnp.AsyncIoStream.create_connection(sock=write)
|
||||
_ = capnp.TwoPartyServer(write, bootstrap=test_capability.Server())
|
||||
client = capnp.TwoPartyClient(read).bootstrap()
|
||||
with pytest.raises(RuntimeError) as exninfo:
|
||||
cap = client.cast_as(capability.TestInterface)
|
||||
await cap.foo(5, True)
|
||||
assert "The KJ event-loop is not running" in str(exninfo)
|
||||
|
||||
|
||||
async def test_no_kj_loop():
|
||||
read, write = socket.socketpair()
|
||||
with pytest.raises(RuntimeError) as exninfo:
|
||||
await capnp.AsyncIoStream.create_connection(sock=read)
|
||||
assert "The KJ event-loop is not running" in str(exninfo)
|
||||
with pytest.raises(RuntimeError) as exninfo:
|
||||
await capnp.AsyncIoStream.create_connection(sock=write)
|
||||
assert "The KJ event-loop is not running" in str(exninfo)
|
||||
with pytest.raises(RuntimeError) as exninfo:
|
||||
capability.TestPipeline._new_client(test_capability.PipelineServer())
|
||||
assert "The KJ event-loop is not running" in str(exninfo)
|
||||
|
||||
|
||||
async def test_promise_leaking1():
|
||||
async with capnp.kj_loop():
|
||||
client = capability.TestInterface._new_client(test_capability.Server())
|
||||
remote = client.foo(5, True)
|
||||
task = asyncio.ensure_future(remote)
|
||||
await asyncio.sleep(0)
|
||||
with pytest.raises(capnp.KjException):
|
||||
await task
|
||||
|
||||
|
||||
async def test_promise_leaking2():
|
||||
async with capnp.kj_loop():
|
||||
client = capability.TestInterface._new_client(test_capability.Server())
|
||||
remote = client.foo(5, True)
|
||||
task = asyncio.ensure_future(remote)
|
||||
with pytest.raises(RuntimeError) as exninfo:
|
||||
await task
|
||||
assert "The KJ event-loop is not running" in str(exninfo)
|
||||
|
||||
|
||||
async def test_promise_leaking3():
|
||||
async with capnp.kj_loop():
|
||||
client = capability.TestInterface._new_client(test_capability.Server())
|
||||
remote = client.foo(5, True)
|
||||
with pytest.raises(RuntimeError) as exninfo:
|
||||
await remote
|
||||
assert "The KJ event-loop is not running" in str(exninfo)
|
||||
|
||||
|
||||
async def test_promise_leaking4():
|
||||
read, _ = socket.socketpair()
|
||||
async with capnp.kj_loop():
|
||||
connection = await capnp.AsyncIoStream.create_connection(sock=read)
|
||||
client = capnp.TwoPartyClient(connection)
|
||||
cap = client.bootstrap().cast_as(capability.TestInterface)
|
||||
res = asyncio.ensure_future(cap.foo(5, True))
|
||||
await asyncio.sleep(0)
|
||||
with pytest.raises(capnp.KjException):
|
||||
await res
|
||||
|
||||
|
||||
async def test_promise_leaking5():
|
||||
read, _ = socket.socketpair()
|
||||
async with capnp.kj_loop():
|
||||
connection = await capnp.AsyncIoStream.create_connection(sock=read)
|
||||
client = capnp.TwoPartyClient(connection)
|
||||
cap = client.bootstrap().cast_as(capability.TestInterface)
|
||||
res = asyncio.ensure_future(cap.foo(5, True))
|
||||
with pytest.raises(RuntimeError) as exninfo:
|
||||
await res
|
||||
assert "The KJ event-loop is not running" in str(exninfo)
|
||||
|
||||
|
||||
async def test_promise_leaking6():
|
||||
read, _ = socket.socketpair()
|
||||
async with capnp.kj_loop():
|
||||
connection = await capnp.AsyncIoStream.create_connection(sock=read)
|
||||
client = capnp.TwoPartyClient(connection)
|
||||
cap = client.bootstrap().cast_as(capability.TestInterface)
|
||||
res = cap.foo(5, True)
|
||||
with pytest.raises(RuntimeError) as exninfo:
|
||||
await res
|
||||
assert "The KJ event-loop is not running" in str(exninfo)
|
||||
|
||||
|
||||
async def test_kj_loop_read_message_after_close():
|
||||
read, _ = socket.socketpair()
|
||||
async with capnp.kj_loop():
|
||||
read = await capnp.AsyncIoStream.create_connection(sock=read)
|
||||
with pytest.raises(RuntimeError) as exninfo:
|
||||
await capability.TestSturdyRefHostId.read_async(read)
|
||||
assert "The KJ event-loop is not running" in str(exninfo)
|
||||
|
||||
|
||||
async def test_kj_loop_partial_read_message_after_close():
|
||||
read, _ = socket.socketpair()
|
||||
async with capnp.kj_loop():
|
||||
read = await capnp.AsyncIoStream.create_connection(sock=read)
|
||||
message = capability.TestSturdyRefHostId.read_async(read)
|
||||
with pytest.raises(RuntimeError) as exninfo:
|
||||
await message
|
||||
assert "The KJ event-loop is not running" in str(exninfo)
|
||||
|
||||
|
||||
async def test_kj_loop_write_message_after_close():
|
||||
_, write = socket.socketpair()
|
||||
async with capnp.kj_loop():
|
||||
write = await capnp.AsyncIoStream.create_connection(sock=write)
|
||||
message = capability.TestSturdyRefHostId.new_message()
|
||||
with pytest.raises(RuntimeError) as exninfo:
|
||||
await message.write_async(write)
|
||||
assert "The KJ event-loop is not running" in str(exninfo)
|
||||
|
||||
|
||||
async def test_kj_loop_partial_write_message_after_close():
|
||||
_, write = socket.socketpair()
|
||||
async with capnp.kj_loop():
|
||||
write = await capnp.AsyncIoStream.create_connection(sock=write)
|
||||
message = capability.TestSturdyRefHostId.new_message()
|
||||
send = message.write_async(write)
|
||||
with pytest.raises(RuntimeError) as exninfo:
|
||||
await send
|
||||
assert "The KJ event-loop is not running" in str(exninfo)
|
||||
|
||||
|
||||
async def test_client_on_disconnect_memory():
|
||||
read, _ = socket.socketpair()
|
||||
async with capnp.kj_loop():
|
||||
read = await capnp.AsyncIoStream.create_connection(sock=read)
|
||||
client = capnp.TwoPartyClient(read)
|
||||
with pytest.raises(RuntimeError) as exninfo:
|
||||
await client.on_disconnect()
|
||||
assert "This client is closed" in str(exninfo)
|
||||
|
||||
|
||||
async def test_server_on_disconnect_memory():
|
||||
_, write = socket.socketpair()
|
||||
async with capnp.kj_loop():
|
||||
write = await capnp.AsyncIoStream.create_connection(sock=write)
|
||||
server = capnp.TwoPartyServer(write, bootstrap=test_capability.Server())
|
||||
with pytest.raises(RuntimeError) as exninfo:
|
||||
await server.on_disconnect()
|
||||
assert "This server is closed" in str(exninfo)
|
||||
|
||||
|
||||
@pytest.mark.xfail(
|
||||
strict=True,
|
||||
reason="Fails because the promisefulfiller got destroyed. Possibly a bug in the C++ library.",
|
||||
)
|
||||
async def test_client_on_disconnect_memory2():
|
||||
"""
|
||||
E capnp.lib.capnp.KjException: kj/async.c++:2813: failed:
|
||||
PromiseFulfiller was destroyed without fulfilling the promise.
|
||||
"""
|
||||
read, _ = socket.socketpair()
|
||||
async with capnp.kj_loop():
|
||||
read = await capnp.AsyncIoStream.create_connection(sock=read)
|
||||
client = capnp.TwoPartyClient(read)
|
||||
disc = client.on_disconnect()
|
||||
await disc
|
||||
|
||||
|
||||
async def test_server_on_disconnect_memory2():
|
||||
_, write = socket.socketpair()
|
||||
async with capnp.kj_loop():
|
||||
write = await capnp.AsyncIoStream.create_connection(sock=write)
|
||||
server = capnp.TwoPartyServer(write, bootstrap=test_capability.Server())
|
||||
disc = server.on_disconnect()
|
||||
await disc
|
||||
@@ -1,161 +0,0 @@
|
||||
import os
|
||||
import pytest
|
||||
import socket
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
|
||||
examples_dir = os.path.join(os.path.dirname(__file__), "..", "examples")
|
||||
hostname = "localhost"
|
||||
|
||||
|
||||
processes = []
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def cleanup():
|
||||
yield
|
||||
for p in processes:
|
||||
p.kill()
|
||||
|
||||
|
||||
def run_subprocesses(address, server, client, wildcard_server=False, ipv4_force=True): # noqa
|
||||
server_attempt = 0
|
||||
server_attempts = 2
|
||||
done = False
|
||||
addr, port = address.split(":")
|
||||
c_address = address
|
||||
s_address = address
|
||||
while not done:
|
||||
assert server_attempt < server_attempts, "Failed {} server attempts".format(server_attempts)
|
||||
server_attempt += 1
|
||||
|
||||
# Force ipv4 for tests (known issues on GitHub Actions with IPv6 for some targets)
|
||||
if "unix" not in addr and ipv4_force:
|
||||
addr = socket.gethostbyname(addr)
|
||||
c_address = "{}:{}".format(addr, port)
|
||||
s_address = c_address
|
||||
if wildcard_server:
|
||||
s_address = "*:{}".format(port) # Use wildcard address for server
|
||||
print("Forcing ipv4 -> {} => {} {}".format(address, c_address, s_address))
|
||||
|
||||
# Start server
|
||||
cmd = [sys.executable, os.path.join(examples_dir, server), s_address]
|
||||
serverp = subprocess.Popen(cmd, stdout=sys.stdout, stderr=sys.stderr)
|
||||
print("Server started (Attempt #{})".format(server_attempt))
|
||||
processes.append(serverp)
|
||||
retries = 300
|
||||
# Loop until we have a socket connection to the server (with timeout)
|
||||
while True:
|
||||
try:
|
||||
if "unix" in address:
|
||||
sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
||||
result = sock.connect_ex(port)
|
||||
if result == 0:
|
||||
break
|
||||
else:
|
||||
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
result = sock.connect_ex((addr, int(port)))
|
||||
if result == 0:
|
||||
break
|
||||
sock = socket.socket(socket.AF_INET6, socket.SOCK_STREAM)
|
||||
result = sock.connect_ex((addr, int(port)))
|
||||
if result == 0:
|
||||
break
|
||||
except socket.gaierror as err:
|
||||
print("gaierror: {}".format(err))
|
||||
# Give the server some small amount of time to start listening
|
||||
time.sleep(0.1)
|
||||
retries -= 1
|
||||
if retries == 0:
|
||||
serverp.kill()
|
||||
print("Timed out waiting for server to start")
|
||||
break
|
||||
|
||||
if serverp.poll() is not None:
|
||||
print("Server exited prematurely: {}".format(serverp.returncode))
|
||||
break
|
||||
|
||||
# 3 tries per server try
|
||||
client_attempt = 0
|
||||
client_attempts = 3
|
||||
while not done:
|
||||
if client_attempt >= client_attempts:
|
||||
print("Failed {} client attempts".format(client_attempts))
|
||||
break
|
||||
client_attempt += 1
|
||||
|
||||
# Start client
|
||||
cmd = [sys.executable, os.path.join(examples_dir, client), c_address]
|
||||
clientp = subprocess.Popen(cmd, stdout=sys.stdout, stderr=sys.stderr)
|
||||
print("Client started (Attempt #{})".format(client_attempt))
|
||||
processes.append(clientp)
|
||||
|
||||
retries = 30 * 10
|
||||
# Loop until the client is finished (with timeout)
|
||||
while True:
|
||||
if clientp.poll() == 0:
|
||||
done = True
|
||||
break
|
||||
|
||||
if clientp.poll() is not None:
|
||||
print("Client exited prematurely: {}".format(clientp.returncode))
|
||||
break
|
||||
time.sleep(0.1)
|
||||
retries -= 1
|
||||
if retries == 0:
|
||||
print("Timed out waiting for client to finish")
|
||||
clientp.kill()
|
||||
break
|
||||
|
||||
serverp.kill()
|
||||
|
||||
serverp.kill()
|
||||
|
||||
|
||||
def test_async_calculator_example(unused_tcp_port, cleanup):
|
||||
address = "{}:{}".format(hostname, unused_tcp_port)
|
||||
server = "async_calculator_server.py"
|
||||
client = "async_calculator_client.py"
|
||||
run_subprocesses(address, server, client)
|
||||
|
||||
|
||||
def test_addressbook_example(cleanup):
|
||||
proc = subprocess.Popen([sys.executable, os.path.join(examples_dir, "addressbook.py")])
|
||||
ret = proc.wait()
|
||||
assert ret == 0
|
||||
|
||||
|
||||
def test_async_example(unused_tcp_port, cleanup):
|
||||
address = "{}:{}".format(hostname, unused_tcp_port)
|
||||
server = "async_server.py"
|
||||
client = "async_client.py"
|
||||
run_subprocesses(address, server, client)
|
||||
|
||||
|
||||
def test_ssl_async_example(unused_tcp_port, cleanup):
|
||||
address = "{}:{}".format(hostname, unused_tcp_port)
|
||||
server = "async_ssl_server.py"
|
||||
client = "async_ssl_client.py"
|
||||
run_subprocesses(address, server, client, ipv4_force=False)
|
||||
|
||||
|
||||
def test_ssl_reconnecting_async_example(unused_tcp_port, cleanup):
|
||||
address = "{}:{}".format(hostname, unused_tcp_port)
|
||||
server = "async_ssl_server.py"
|
||||
client = "async_reconnecting_ssl_client.py"
|
||||
run_subprocesses(address, server, client, ipv4_force=False)
|
||||
|
||||
|
||||
def test_async_ssl_calculator_example(unused_tcp_port, cleanup):
|
||||
address = "{}:{}".format(hostname, unused_tcp_port)
|
||||
server = "async_ssl_calculator_server.py"
|
||||
client = "async_ssl_calculator_client.py"
|
||||
run_subprocesses(address, server, client, ipv4_force=False)
|
||||
|
||||
|
||||
def test_async_socket_message_example(unused_tcp_port, cleanup):
|
||||
address = "{}:{}".format(hostname, unused_tcp_port)
|
||||
server = "async_socket_message_server.py"
|
||||
client = "async_socket_message_client.py"
|
||||
run_subprocesses(address, server, client)
|
||||
@@ -1,292 +0,0 @@
|
||||
import os
|
||||
import tempfile
|
||||
import weakref
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
import capnp
|
||||
import sys
|
||||
import gc
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def all_types():
|
||||
"""Load the standard all_types.capnp schema."""
|
||||
directory = os.path.dirname(__file__)
|
||||
return capnp.load(os.path.join(directory, "all_types.capnp"))
|
||||
|
||||
|
||||
def test_set_bytes_get_bytes(all_types):
|
||||
"""
|
||||
Scenario 1: Set Byte -> Get Byte
|
||||
Verify standard behavior: writing bytes results in reading bytes.
|
||||
"""
|
||||
msg = all_types.TestAllTypes.new_message()
|
||||
input_data = b"hello_world"
|
||||
|
||||
# Set
|
||||
msg.dataField = input_data
|
||||
|
||||
# Get
|
||||
output_data = msg.dataField
|
||||
|
||||
# Verify
|
||||
assert isinstance(output_data, bytes)
|
||||
assert output_data == input_data
|
||||
|
||||
|
||||
def test_set_view_get_bytes(all_types):
|
||||
"""
|
||||
Scenario 2: Set View -> Get Byte
|
||||
Verify compatibility: Passing a memoryview sets the data,
|
||||
but standard attribute access returns a bytes copy.
|
||||
"""
|
||||
msg = all_types.TestAllTypes.new_message()
|
||||
|
||||
# Create a memoryview source
|
||||
raw_source = bytearray(b"view_source")
|
||||
view = memoryview(raw_source)
|
||||
|
||||
# Set via memoryview
|
||||
msg.dataField = view
|
||||
|
||||
# Get via standard attribute
|
||||
output_data = msg.dataField
|
||||
|
||||
# Verify
|
||||
assert isinstance(output_data, bytes)
|
||||
assert output_data == b"view_source"
|
||||
|
||||
|
||||
def test_set_bytes_get_view_and_modify(all_types):
|
||||
"""
|
||||
Scenario 3: Set Byte -> Get View
|
||||
Verify the high-performance API get_data_as_view.
|
||||
The view must be writable and modifications must reflect in the message.
|
||||
"""
|
||||
msg = all_types.TestAllTypes.new_message()
|
||||
|
||||
# Initial write
|
||||
msg.dataField = b"ABCDE"
|
||||
|
||||
# Get view via new API
|
||||
view = msg.get_data_as_view("dataField")
|
||||
|
||||
# Verify view properties
|
||||
assert isinstance(view, memoryview)
|
||||
assert view.readonly is False
|
||||
assert view.tobytes() == b"ABCDE"
|
||||
|
||||
# Verify in-place modification
|
||||
view[0] = ord("Z") # Change 'A' to 'Z'
|
||||
|
||||
# Verify modification is reflected in standard access
|
||||
assert msg.dataField == b"ZBCDE"
|
||||
|
||||
|
||||
def test_reader_vs_builder_view(all_types):
|
||||
"""
|
||||
Verify that Builder views are writable, but Reader views are read-only.
|
||||
"""
|
||||
# 1. Builder phase
|
||||
builder = all_types.TestAllTypes.new_message()
|
||||
builder.dataField = b"test_rw"
|
||||
|
||||
builder_view = builder.get_data_as_view("dataField")
|
||||
assert builder_view.readonly is False
|
||||
builder_view[0] = ord("T") # Modification allowed
|
||||
|
||||
# 2. Reader phase
|
||||
reader = builder.as_reader()
|
||||
|
||||
# Standard Get
|
||||
assert reader.dataField == b"Test_rw"
|
||||
|
||||
# Reader get_data_as_view
|
||||
reader_view = reader.get_data_as_view("dataField")
|
||||
assert isinstance(reader_view, memoryview)
|
||||
assert reader_view.readonly is True
|
||||
|
||||
# Attempting to modify Reader view should raise TypeError
|
||||
with pytest.raises(TypeError):
|
||||
reader_view[0] = ord("X")
|
||||
|
||||
|
||||
def test_nested_struct_data(all_types):
|
||||
"""
|
||||
Verify that get_data_as_view works correctly on nested structs.
|
||||
"""
|
||||
msg = all_types.TestAllTypes.new_message()
|
||||
|
||||
# Initialize nested struct
|
||||
inner = msg.init("structField")
|
||||
inner.int32Field = 100
|
||||
inner.dataField = b"nested_data"
|
||||
|
||||
# 1. Verify standard access
|
||||
assert msg.structField.dataField == b"nested_data"
|
||||
|
||||
# 2. Verify nested get_data_as_view
|
||||
view = msg.structField.get_data_as_view("dataField")
|
||||
|
||||
assert isinstance(view, memoryview)
|
||||
assert view.tobytes() == b"nested_data"
|
||||
|
||||
# Modify nested data
|
||||
view[0] = ord("N")
|
||||
assert msg.structField.dataField == b"Nested_data"
|
||||
|
||||
|
||||
def test_corner_cases_values(all_types):
|
||||
"""
|
||||
Test edge cases: Empty bytes and binary data with nulls.
|
||||
"""
|
||||
msg = all_types.TestAllTypes.new_message()
|
||||
|
||||
# Case A: Empty Bytes
|
||||
msg.dataField = b""
|
||||
assert msg.dataField == b""
|
||||
view = msg.get_data_as_view("dataField")
|
||||
assert len(view) == 0
|
||||
|
||||
# Case B: Binary data containing null bytes
|
||||
binary_data = b"\x00\xff\x00\x01"
|
||||
msg.dataField = binary_data
|
||||
assert msg.dataField == binary_data
|
||||
assert msg.get_data_as_view("dataField").tobytes() == binary_data
|
||||
|
||||
|
||||
def test_uninitialized_data_get_view(all_types):
|
||||
"""
|
||||
Default DATA fields should expose an empty memoryview instead of failing on a NULL buffer pointer.
|
||||
"""
|
||||
builder = all_types.TestAllTypes.new_message()
|
||||
builder_view = builder.get_data_as_view("dataField")
|
||||
|
||||
assert isinstance(builder_view, memoryview)
|
||||
assert builder_view.readonly is False
|
||||
assert len(builder_view) == 0
|
||||
assert builder_view.tobytes() == b""
|
||||
|
||||
reader = all_types.TestAllTypes.new_message().as_reader()
|
||||
reader_view = reader.get_data_as_view("dataField")
|
||||
|
||||
assert isinstance(reader_view, memoryview)
|
||||
assert reader_view.readonly is True
|
||||
assert len(reader_view) == 0
|
||||
assert reader_view.tobytes() == b""
|
||||
|
||||
with pytest.raises(IndexError):
|
||||
builder_view[0] = 0xFF
|
||||
|
||||
with pytest.raises(ValueError):
|
||||
builder_view[0:1] = b"\xff"
|
||||
|
||||
|
||||
def test_error_wrong_type(all_types):
|
||||
"""
|
||||
Test error handling: Calling get_data_as_view on non-Data fields.
|
||||
"""
|
||||
msg = all_types.TestAllTypes.new_message()
|
||||
msg.int32Field = 123
|
||||
msg.textField = "I am text"
|
||||
|
||||
# Attempt on Int field
|
||||
with pytest.raises(TypeError) as excinfo:
|
||||
msg.get_data_as_view("int32Field")
|
||||
assert "not a DATA field" in str(excinfo.value)
|
||||
|
||||
# Attempt on Text field
|
||||
with pytest.raises(TypeError) as excinfo:
|
||||
msg.get_data_as_view("textField")
|
||||
assert "not a DATA field" in str(excinfo.value)
|
||||
|
||||
|
||||
def test_error_missing_field(all_types):
|
||||
"""
|
||||
Test error handling: Accessing a non-existent field name.
|
||||
"""
|
||||
msg = all_types.TestAllTypes.new_message()
|
||||
|
||||
# Accessing a missing field should raise AttributeError (standard Python behavior)
|
||||
with pytest.raises(AttributeError) as excinfo:
|
||||
msg.get_data_as_view("non_existent_field")
|
||||
|
||||
# Optional: Verify the error message contains the field name
|
||||
assert "non_existent_field" in str(excinfo.value)
|
||||
|
||||
|
||||
def test_view_keeps_message_alive(all_types):
|
||||
"""
|
||||
Verify that a View keeps messages alive.
|
||||
"""
|
||||
msg = all_types.TestAllTypes.new_message()
|
||||
expected_data = b"persistence_check"
|
||||
msg.dataField = expected_data
|
||||
|
||||
initial_ref_count = sys.getrefcount(msg)
|
||||
view = msg.get_data_as_view("dataField")
|
||||
new_ref_count = sys.getrefcount(msg)
|
||||
|
||||
assert new_ref_count > initial_ref_count, (
|
||||
f"View failed to hold reference to Message! (Old: {initial_ref_count}, New: {new_ref_count})"
|
||||
)
|
||||
print(f"\n[Ref Check] Success: Ref count increased from {initial_ref_count} to {new_ref_count}")
|
||||
|
||||
del msg
|
||||
gc.collect()
|
||||
|
||||
assert view.tobytes() == expected_data
|
||||
|
||||
|
||||
def test_data_view_exports_through_buffer_exporter(all_types):
|
||||
"""Returned memoryviews should pin an internal exporter, not bare pointers."""
|
||||
msg = all_types.TestAllTypes.new_message()
|
||||
msg.dataField = b"exporter_check"
|
||||
view = msg.get_data_as_view("dataField")
|
||||
|
||||
assert isinstance(view, memoryview)
|
||||
assert view.obj is not None
|
||||
assert len(view.obj) == len(view)
|
||||
|
||||
|
||||
def test_data_view_survives_del_builder(all_types):
|
||||
msg = all_types.TestAllTypes.new_message()
|
||||
msg.dataField = b"persistence_check"
|
||||
view = msg.get_data_as_view("dataField")
|
||||
|
||||
del msg
|
||||
gc.collect()
|
||||
|
||||
assert view.tobytes() == b"persistence_check"
|
||||
|
||||
|
||||
def test_data_view_releases_packed_payload():
|
||||
schema_text = """
|
||||
@0x9d7d4f087df9b6e1;
|
||||
struct BlobMsg {
|
||||
data @0 :Data;
|
||||
}
|
||||
"""
|
||||
|
||||
class Payload(bytearray):
|
||||
pass
|
||||
|
||||
td = tempfile.TemporaryDirectory()
|
||||
path = Path(td.name) / "blob.capnp"
|
||||
path.write_text(schema_text)
|
||||
schema = capnp.load(str(path))
|
||||
try:
|
||||
payload = Payload(schema.BlobMsg.new_message(data=b"x" * 4096).to_bytes_packed())
|
||||
payload_ref = weakref.ref(payload)
|
||||
|
||||
reader = schema.BlobMsg.from_bytes_packed(payload)
|
||||
view = reader.get_data_as_view("data")
|
||||
view.release()
|
||||
|
||||
del view, reader, payload
|
||||
gc.collect()
|
||||
|
||||
assert payload_ref() is None
|
||||
finally:
|
||||
td.cleanup()
|
||||
@@ -24,27 +24,15 @@ def test_large_read(test_capnp):
|
||||
for i in range(len(values)):
|
||||
values[i] = i
|
||||
|
||||
array.write_packed(f)
|
||||
f.write(array.to_bytes())
|
||||
f.seek(0)
|
||||
|
||||
array = test_capnp.MultiArray.read_packed(f)
|
||||
with test_capnp.MultiArray.from_bytes(f.read()) as reader:
|
||||
array = reader
|
||||
del f
|
||||
assert array.rows[0].values[9000] == 9000
|
||||
|
||||
|
||||
def test_large_read_multiple(test_capnp):
|
||||
f = tempfile.TemporaryFile()
|
||||
msg1 = test_capnp.Msg.new_message()
|
||||
msg1.data = [0x41] * 8192
|
||||
msg1.write(f)
|
||||
msg2 = test_capnp.Msg.new_message()
|
||||
msg2.write(f)
|
||||
f.seek(0)
|
||||
|
||||
for m in test_capnp.Msg.read_multiple(f):
|
||||
pass
|
||||
|
||||
|
||||
def get_two_adjacent_messages(test_capnp):
|
||||
msg1 = test_capnp.Msg.new_message()
|
||||
msg1.data = [0x41] * 8192
|
||||
|
||||
60
test/test_lifetime.py
Normal file
60
test/test_lifetime.py
Normal file
@@ -0,0 +1,60 @@
|
||||
"""Lifetimes used by realtime message producers and retained log readers."""
|
||||
|
||||
import gc
|
||||
from pathlib import Path
|
||||
|
||||
import capnp
|
||||
import pytest
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def schema():
|
||||
return capnp.load(str(Path(__file__).with_name("addressbook.capnp")))
|
||||
|
||||
|
||||
def builder_count():
|
||||
return sum(type(obj) is capnp._MallocMessageBuilder for obj in gc.get_objects())
|
||||
|
||||
|
||||
def test_kwargs_release_without_gc(schema):
|
||||
# Warm lazy schema state before counting. String fields must not create
|
||||
# schema/field cycles that keep whole arenas alive until the next GC pass.
|
||||
schema.Person.new_message(name="warmup")
|
||||
gc.collect()
|
||||
enabled = gc.isenabled()
|
||||
gc.disable()
|
||||
try:
|
||||
before = builder_count()
|
||||
for _ in range(1000):
|
||||
msg = schema.Person.new_message(name="Alice", phones=[{"type": "mobile"}])
|
||||
assert msg.name == "Alice"
|
||||
assert msg.phones[0].type == "mobile"
|
||||
del msg
|
||||
assert builder_count() == before
|
||||
finally:
|
||||
if enabled:
|
||||
gc.enable()
|
||||
|
||||
|
||||
def test_unknown_string_field_raises(schema):
|
||||
with pytest.raises(capnp.KjException):
|
||||
schema.Person.new_message(notAField="Alice")
|
||||
|
||||
|
||||
def test_readers_outlive_input_and_iterator(schema):
|
||||
data = b"".join(schema.Person.new_message(name=name).to_bytes() for name in ("Alice", "Bob"))
|
||||
messages = schema.Person.read_multiple_bytes(data)
|
||||
alice = next(messages)
|
||||
bob = next(messages)
|
||||
del data, messages
|
||||
gc.collect()
|
||||
assert (alice.name, bob.name) == ("Alice", "Bob")
|
||||
|
||||
|
||||
def test_nested_reader_outlives_root(schema):
|
||||
data = schema.AddressBook.new_message(people=[{"name": "Alice"}]).to_bytes()
|
||||
with schema.AddressBook.from_bytes(data) as root:
|
||||
person = root.people[0]
|
||||
del data, root
|
||||
gc.collect()
|
||||
assert person.name == "Alice"
|
||||
@@ -1,7 +1,6 @@
|
||||
import pytest
|
||||
import capnp
|
||||
import os
|
||||
import sys
|
||||
|
||||
this_dir = os.path.dirname(__file__)
|
||||
|
||||
@@ -62,93 +61,3 @@ def test_failed_import():
|
||||
|
||||
with pytest.raises(Exception):
|
||||
bar.foo = foo
|
||||
|
||||
|
||||
def test_defualt_import_hook():
|
||||
# Make sure any previous imports of addressbook_capnp are gone
|
||||
capnp.cleanup_global_schema_parser()
|
||||
|
||||
import addressbook_capnp # noqa: F401
|
||||
|
||||
|
||||
def test_dash_import():
|
||||
import addressbook_with_dashes_capnp # noqa: F401
|
||||
|
||||
|
||||
def test_spaces_import():
|
||||
import addressbook_with_spaces_capnp # noqa: F401
|
||||
|
||||
|
||||
def test_add_import_hook():
|
||||
capnp.add_import_hook()
|
||||
|
||||
# Make sure any previous imports of addressbook_capnp are gone
|
||||
capnp.cleanup_global_schema_parser()
|
||||
|
||||
import addressbook_capnp
|
||||
|
||||
addressbook_capnp.AddressBook.new_message()
|
||||
|
||||
|
||||
def test_multiple_add_import_hook():
|
||||
capnp.add_import_hook()
|
||||
capnp.add_import_hook()
|
||||
|
||||
# Make sure any previous imports of addressbook_capnp are gone
|
||||
capnp.cleanup_global_schema_parser()
|
||||
|
||||
import addressbook_capnp
|
||||
|
||||
addressbook_capnp.AddressBook.new_message()
|
||||
|
||||
|
||||
def test_remove_import_hook():
|
||||
capnp.add_import_hook()
|
||||
capnp.remove_import_hook()
|
||||
|
||||
if "addressbook_capnp" in sys.modules:
|
||||
# hack to deal with it being imported already
|
||||
del sys.modules["addressbook_capnp"]
|
||||
|
||||
with pytest.raises(ImportError):
|
||||
import addressbook_capnp # noqa: F401
|
||||
|
||||
|
||||
def test_bundled_import_hook():
|
||||
# stream.capnp should be bundled, or provided by the system capnproto
|
||||
capnp.add_import_hook()
|
||||
from capnp import stream_capnp # noqa: F401
|
||||
|
||||
|
||||
def test_nested_import():
|
||||
import schemas.parent_capnp # noqa: F401
|
||||
import schemas.child_capnp # noqa: F401
|
||||
|
||||
|
||||
async def test_load_capnp(foo):
|
||||
# test dynamically loading
|
||||
loader = capnp.SchemaLoader()
|
||||
loader.load(foo.Baz.schema.get_proto())
|
||||
loader.load_dynamic(foo.Qux.schema.get_proto().node)
|
||||
|
||||
schema = loader.get(foo.Baz.schema.get_proto().node.id).as_struct()
|
||||
assert "text" in schema.fieldnames
|
||||
assert "qux" in schema.fieldnames
|
||||
assert schema.fields["qux"].proto.slot.type.which == "struct"
|
||||
|
||||
class Wrapper(foo.Wrapper.Server):
|
||||
async def wrapped(self, object, **kwargs):
|
||||
assert isinstance(object, capnp.lib.capnp._DynamicObjectReader)
|
||||
baz_ = object.as_struct(schema)
|
||||
assert baz_.text == "test"
|
||||
assert baz_.qux.id == 2
|
||||
|
||||
# test calling into the wrapper with a Baz message.
|
||||
baz_ = foo.Baz.new_message()
|
||||
baz_.text = "test"
|
||||
baz_.qux.id = 2
|
||||
|
||||
async with capnp.kj_loop():
|
||||
wrapper = foo.Wrapper._new_client(Wrapper())
|
||||
remote = wrapper.wrapped(baz_)
|
||||
await remote
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
from types import coroutine
|
||||
import pytest
|
||||
import socket
|
||||
import gc
|
||||
|
||||
import capnp
|
||||
import test_capability
|
||||
import test_capability_capnp as capability
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
async def kj_loop():
|
||||
async with capnp.kj_loop():
|
||||
yield
|
||||
|
||||
|
||||
@coroutine
|
||||
def wrap(p):
|
||||
return (yield from p)
|
||||
|
||||
|
||||
async def test_kj_loop_await_attach():
|
||||
read, write = socket.socketpair()
|
||||
read = await capnp.AsyncIoStream.create_connection(sock=read)
|
||||
write = await capnp.AsyncIoStream.create_connection(sock=write)
|
||||
_ = capnp.TwoPartyServer(write, bootstrap=test_capability.Server())
|
||||
client = capnp.TwoPartyClient(read).bootstrap().cast_as(capability.TestInterface)
|
||||
t = wrap(client.foo(5, True).__await__())
|
||||
del client
|
||||
del read
|
||||
gc.collect()
|
||||
await t
|
||||
@@ -1,51 +0,0 @@
|
||||
import pytest
|
||||
import capnp
|
||||
import os
|
||||
|
||||
this_dir = os.path.dirname(__file__)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def addressbook():
|
||||
return capnp.load(os.path.join(this_dir, "addressbook.capnp"))
|
||||
|
||||
|
||||
def test_object_basic(addressbook):
|
||||
obj = capnp._MallocMessageBuilder().get_root_as_any()
|
||||
person = obj.as_struct(addressbook.Person)
|
||||
person.name = "test"
|
||||
person.id = 1000
|
||||
|
||||
same_person = obj.as_struct(addressbook.Person)
|
||||
assert same_person.name == "test"
|
||||
assert same_person.id == 1000
|
||||
|
||||
obj_r = obj.as_reader()
|
||||
same_person = obj_r.as_struct(addressbook.Person)
|
||||
assert same_person.name == "test"
|
||||
assert same_person.id == 1000
|
||||
|
||||
|
||||
def test_object_list(addressbook):
|
||||
obj = capnp._MallocMessageBuilder().get_root_as_any()
|
||||
listSchema = capnp._ListSchema(addressbook.Person)
|
||||
people = obj.init_as_list(listSchema, 2)
|
||||
person = people[0]
|
||||
person.name = "test"
|
||||
person.id = 1000
|
||||
person = people[1]
|
||||
person.name = "test2"
|
||||
person.id = 1001
|
||||
|
||||
same_person = obj.as_list(listSchema)
|
||||
assert same_person[0].name == "test"
|
||||
assert same_person[0].id == 1000
|
||||
assert same_person[1].name == "test2"
|
||||
assert same_person[1].id == 1001
|
||||
|
||||
obj_r = obj.as_reader()
|
||||
same_person = obj_r.as_list(listSchema)
|
||||
assert same_person[0].name == "test"
|
||||
assert same_person[0].id == 1000
|
||||
assert same_person[1].name == "test2"
|
||||
assert same_person[1].id == 1001
|
||||
97
test/test_openpilot.py
Normal file
97
test/test_openpilot.py
Normal file
@@ -0,0 +1,97 @@
|
||||
"""Optional integration checks against a local openpilot checkout.
|
||||
|
||||
Set OPENPILOT_PATH and run with openpilot's dependencies available. These tests
|
||||
use the checkout's real schemas; they never copy or modify them.
|
||||
"""
|
||||
|
||||
import os
|
||||
import pickle
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import capnp
|
||||
import pytest
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def openpilot():
|
||||
path = os.environ.get("OPENPILOT_PATH")
|
||||
if path is None:
|
||||
pytest.skip("set OPENPILOT_PATH to run openpilot integration tests")
|
||||
sys.path.insert(0, str(Path(path).resolve()))
|
||||
from openpilot.cereal import log
|
||||
|
||||
return log
|
||||
|
||||
|
||||
def test_event_types(openpilot):
|
||||
from openpilot.cereal import messaging
|
||||
from openpilot.cereal.services import SERVICE_LIST
|
||||
|
||||
for event in openpilot.Event.schema.union_fields:
|
||||
if event not in SERVICE_LIST:
|
||||
continue
|
||||
try:
|
||||
msg = messaging.new_message(event)
|
||||
except capnp.KjException:
|
||||
msg = messaging.new_message(event, 2)
|
||||
with openpilot.Event.from_bytes(msg.to_bytes()) as reader:
|
||||
assert reader.which() == event
|
||||
assert reader.logMonoTime == msg.logMonoTime
|
||||
assert reader.valid == msg.valid
|
||||
|
||||
|
||||
def test_can_fields(openpilot):
|
||||
from openpilot.selfdrive.pandad.pandad_api_impl import can_capnp_to_list, can_list_to_can_capnp
|
||||
|
||||
frames = [(0x123, b"\x00\xff\x01", 0), (0x456, b"\x02\x03", 1)]
|
||||
raw = can_list_to_can_capnp(frames)
|
||||
# Exercise both the writer's and reader's cached schema field paths.
|
||||
decoded = can_capnp_to_list([raw])
|
||||
assert decoded[0][1] == frames
|
||||
with openpilot.Event.from_bytes(raw) as reader:
|
||||
assert [(f.address, f.dat, f.src) for f in reader.can] == frames
|
||||
|
||||
|
||||
def test_schema_reflection(openpilot):
|
||||
from openpilot.system.webrtc.schema import generate_struct
|
||||
|
||||
from opendbc.car.structs import car
|
||||
|
||||
schema = generate_struct(car.CarState.schema)
|
||||
assert schema["vEgo"] == "float32"
|
||||
assert schema["gearShifter"] == "text"
|
||||
assert isinstance(schema["wheelSpeeds"], dict)
|
||||
assert (
|
||||
car.CarParams.schema.fields["safetyConfigs"].schema.elementType.node.id
|
||||
== car.CarParams.SafetyConfig.schema.node.id
|
||||
)
|
||||
|
||||
|
||||
def test_logreader_and_pickle(openpilot):
|
||||
from openpilot.cereal import messaging
|
||||
from openpilot.tools.lib.logreader import LogReader
|
||||
|
||||
msgs = []
|
||||
for i in range(20):
|
||||
msg = messaging.new_message("carState")
|
||||
msg.carState.vEgo = float(i)
|
||||
msgs.append(msg.to_bytes())
|
||||
readers = list(LogReader.from_bytes(b"".join(msgs)))
|
||||
assert [msg.carState.vEgo for msg in readers] == list(range(20))
|
||||
restored = pickle.loads(pickle.dumps(readers[3]))
|
||||
assert restored.which() == "carState"
|
||||
assert restored.carState.vEgo == 3.0
|
||||
|
||||
|
||||
def test_fuzzy_messages_and_replay_comparison(openpilot):
|
||||
from openpilot.common.fuzzy import Fuzzy, capnp_random_dict
|
||||
from openpilot.selfdrive.test.process_replay.compare_logs import compare_logs
|
||||
|
||||
for event in ("carState", "carControl", "carParams", "modelV2", "can", "extrinsicsCalibration"):
|
||||
data = capnp_random_dict(Fuzzy(42, 51), openpilot.Event.schema, event, real_floats=True)
|
||||
builder = openpilot.Event.new_message(**data)
|
||||
with openpilot.Event.from_bytes(builder.to_bytes()) as reader:
|
||||
assert reader.which() == event
|
||||
assert reader.to_dict() == builder.to_dict()
|
||||
assert compare_logs([reader], [reader.as_builder().as_reader()]) == []
|
||||
@@ -1,79 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import pytest
|
||||
import capnp # noqa: F401
|
||||
import os
|
||||
|
||||
this_dir = os.path.dirname(__file__)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def all_types():
|
||||
return capnp.load(os.path.join(this_dir, "all_types.capnp"))
|
||||
|
||||
|
||||
def test_bytearray_allocator(all_types):
|
||||
class Allocator:
|
||||
def __init__(self):
|
||||
self.cur_size = 0
|
||||
self.last_size = 0
|
||||
|
||||
def __call__(self, minimum_size: int) -> bytearray:
|
||||
actual_size = max(minimum_size, self.cur_size)
|
||||
self.last_size = actual_size
|
||||
self.cur_size += actual_size
|
||||
WORD_SIZE = 8
|
||||
byte_count = actual_size * WORD_SIZE
|
||||
return bytearray(byte_count)
|
||||
|
||||
allocator = Allocator()
|
||||
assert allocator.cur_size == 0
|
||||
assert allocator.last_size == 0
|
||||
msg_builder = capnp._PyCustomMessageBuilder(allocator, 1024)
|
||||
struct_builder = msg_builder.init_root(all_types.TestAllTypes)
|
||||
assert allocator.cur_size == 1024
|
||||
assert allocator.last_size == 1024
|
||||
|
||||
struct_builder.init("dataField", 5)
|
||||
assert bytes(struct_builder._get("dataField")) == b"\x00\x00\x00\x00\x00"
|
||||
|
||||
struct_builder.dataField = b"hello"
|
||||
assert bytes(struct_builder._get("dataField")) == b"hello"
|
||||
|
||||
struct_reader = struct_builder.as_reader()
|
||||
assert bytes(struct_reader._get("dataField")) == b"hello"
|
||||
|
||||
|
||||
def test_memoryview_allocator(all_types):
|
||||
class MemoryViewAllocator:
|
||||
def __init__(self):
|
||||
self.cur_size = 0
|
||||
self.last_size = 0
|
||||
self.buffers = []
|
||||
|
||||
def __call__(self, minimum_size: int) -> memoryview:
|
||||
actual_size = max(minimum_size, self.cur_size)
|
||||
self.last_size = actual_size
|
||||
self.cur_size += actual_size
|
||||
WORD_SIZE = 8
|
||||
byte_count = actual_size * WORD_SIZE
|
||||
buffer = bytearray(byte_count)
|
||||
self.buffers.append(buffer)
|
||||
return memoryview(buffer)
|
||||
|
||||
allocator = MemoryViewAllocator()
|
||||
assert allocator.cur_size == 0
|
||||
assert allocator.last_size == 0
|
||||
msg_builder = capnp._PyCustomMessageBuilder(allocator, 1024)
|
||||
struct_builder = msg_builder.init_root(all_types.TestAllTypes)
|
||||
assert allocator.cur_size == 1024
|
||||
assert allocator.last_size == 1024
|
||||
|
||||
struct_builder.init("dataField", 5)
|
||||
assert bytes(struct_builder._get("dataField")) == b"\x00\x00\x00\x00\x00"
|
||||
|
||||
struct_builder.dataField = b"hello"
|
||||
assert bytes(struct_builder._get("dataField")) == b"hello"
|
||||
|
||||
struct_reader = struct_builder.as_reader()
|
||||
assert bytes(struct_reader._get("dataField")) == b"hello"
|
||||
@@ -20,7 +20,7 @@ def addressbook():
|
||||
|
||||
|
||||
def test_addressbook_message_classes(addressbook):
|
||||
def writeAddressBook(fd):
|
||||
def writeAddressBook(file):
|
||||
message = capnp._MallocMessageBuilder()
|
||||
addressBook = message.init_root(addressbook.AddressBook)
|
||||
people = addressBook.init("people", 2)
|
||||
@@ -45,11 +45,11 @@ def test_addressbook_message_classes(addressbook):
|
||||
bobPhones[1].type = "work"
|
||||
bob.employment.unemployed = None
|
||||
|
||||
capnp._write_packed_message_to_fd(fd, message)
|
||||
file.write(addressBook.to_bytes())
|
||||
|
||||
def printAddressBook(fd):
|
||||
message = capnp._PackedFdMessageReader(f)
|
||||
addressBook = message.get_root(addressbook.AddressBook)
|
||||
def printAddressBook(file):
|
||||
with addressbook.AddressBook.from_bytes(file.read()) as reader:
|
||||
addressBook = reader
|
||||
|
||||
people = addressBook.people
|
||||
|
||||
@@ -73,11 +73,11 @@ def test_addressbook_message_classes(addressbook):
|
||||
assert bobPhones[1].type == "work"
|
||||
assert bob.employment.unemployed is None
|
||||
|
||||
f = open("example", "w")
|
||||
writeAddressBook(f.fileno())
|
||||
f = open("example", "wb")
|
||||
writeAddressBook(f)
|
||||
|
||||
f = open("example", "r")
|
||||
printAddressBook(f.fileno())
|
||||
f = open("example", "rb")
|
||||
printAddressBook(f)
|
||||
|
||||
|
||||
def test_addressbook(addressbook):
|
||||
@@ -105,10 +105,11 @@ def test_addressbook(addressbook):
|
||||
bobPhones[1].type = "work"
|
||||
bob.employment.unemployed = None
|
||||
|
||||
addresses.write(file)
|
||||
file.write(addresses.to_bytes())
|
||||
|
||||
def printAddressBook(file):
|
||||
addresses = addressbook.AddressBook.read(file)
|
||||
with addressbook.AddressBook.from_bytes(file.read()) as reader:
|
||||
addresses = reader
|
||||
|
||||
people = addresses.people
|
||||
|
||||
@@ -132,71 +133,10 @@ def test_addressbook(addressbook):
|
||||
assert bobPhones[1].type == "work"
|
||||
assert bob.employment.unemployed is None
|
||||
|
||||
f = open("example", "w")
|
||||
f = open("example", "wb")
|
||||
writeAddressBook(f)
|
||||
|
||||
f = open("example", "r")
|
||||
printAddressBook(f)
|
||||
|
||||
|
||||
def test_addressbook_resizable(addressbook):
|
||||
def writeAddressBook(file):
|
||||
addresses = addressbook.AddressBook.new_message()
|
||||
people = addresses.init_resizable_list("people")
|
||||
|
||||
alice = people.add()
|
||||
alice.id = 123
|
||||
alice.name = "Alice"
|
||||
alice.email = "alice@example.com"
|
||||
alicePhones = alice.init("phones", 1)
|
||||
alicePhones[0].number = "555-1212"
|
||||
alicePhones[0].type = "mobile"
|
||||
alice.employment.school = "MIT"
|
||||
|
||||
bob = people.add()
|
||||
bob.id = 456
|
||||
bob.name = "Bob"
|
||||
bob.email = "bob@example.com"
|
||||
bobPhones = bob.init("phones", 2)
|
||||
bobPhones[0].number = "555-4567"
|
||||
bobPhones[0].type = "home"
|
||||
bobPhones[1].number = "555-7654"
|
||||
bobPhones[1].type = "work"
|
||||
bob.employment.unemployed = None
|
||||
|
||||
people.finish()
|
||||
|
||||
addresses.write(file)
|
||||
|
||||
def printAddressBook(file):
|
||||
addresses = addressbook.AddressBook.read(file)
|
||||
|
||||
people = addresses.people
|
||||
|
||||
alice = people[0]
|
||||
assert alice.id == 123
|
||||
assert alice.name == "Alice"
|
||||
assert alice.email == "alice@example.com"
|
||||
alicePhones = alice.phones
|
||||
assert alicePhones[0].number == "555-1212"
|
||||
assert alicePhones[0].type == "mobile"
|
||||
assert alice.employment.school == "MIT"
|
||||
|
||||
bob = people[1]
|
||||
assert bob.id == 456
|
||||
assert bob.name == "Bob"
|
||||
assert bob.email == "bob@example.com"
|
||||
bobPhones = bob.phones
|
||||
assert bobPhones[0].number == "555-4567"
|
||||
assert bobPhones[0].type == "home"
|
||||
assert bobPhones[1].number == "555-7654"
|
||||
assert bobPhones[1].type == "work"
|
||||
assert bob.employment.unemployed is None
|
||||
|
||||
f = open("example", "w")
|
||||
writeAddressBook(f)
|
||||
|
||||
f = open("example", "r")
|
||||
f = open("example", "rb")
|
||||
printAddressBook(f)
|
||||
|
||||
|
||||
@@ -230,10 +170,11 @@ def test_addressbook_explicit_fields(addressbook):
|
||||
employment = bob._get_by_field(person_fields["employment"])
|
||||
employment._set_by_field(addressbook.Person.Employment.schema.fields["unemployed"], None)
|
||||
|
||||
addresses.write(file)
|
||||
file.write(addresses.to_bytes())
|
||||
|
||||
def printAddressBook(file):
|
||||
addresses = addressbook.AddressBook.read(file)
|
||||
with addressbook.AddressBook.from_bytes(file.read()) as reader:
|
||||
addresses = reader
|
||||
address_fields = addressbook.AddressBook.schema.fields
|
||||
person_fields = addressbook.Person.schema.fields
|
||||
phone_fields = addressbook.Person.PhoneNumber.schema.fields
|
||||
@@ -262,10 +203,10 @@ def test_addressbook_explicit_fields(addressbook):
|
||||
employment = bob._get_by_field(person_fields["employment"])
|
||||
employment._get_by_field(addressbook.Person.Employment.schema.fields["unemployed"]) is None
|
||||
|
||||
f = open("example", "w")
|
||||
f = open("example", "wb")
|
||||
writeAddressBook(f)
|
||||
|
||||
f = open("example", "r")
|
||||
f = open("example", "rb")
|
||||
printAddressBook(f)
|
||||
|
||||
|
||||
@@ -515,8 +456,9 @@ def test_build_first_segment_size(all_types):
|
||||
|
||||
|
||||
def test_binary_read(all_types):
|
||||
f = open(os.path.join(this_dir, "all-types.binary"), "r", encoding="utf8")
|
||||
root = all_types.TestAllTypes.read(f)
|
||||
f = open(os.path.join(this_dir, "all-types.binary"), "rb")
|
||||
with all_types.TestAllTypes.from_bytes(f.read()) as reader:
|
||||
root = reader
|
||||
check_all_types(root)
|
||||
|
||||
expectedText = open(os.path.join(this_dir, "all-types.txt"), "r", encoding="utf8").read()
|
||||
@@ -532,26 +474,10 @@ def test_binary_read(all_types):
|
||||
check_all_types(builder2.get_root(all_types.TestAllTypes))
|
||||
|
||||
|
||||
def test_packed_read(all_types):
|
||||
f = open(os.path.join(this_dir, "all-types.packed"), "r", encoding="utf8")
|
||||
root = all_types.TestAllTypes.read_packed(f)
|
||||
check_all_types(root)
|
||||
|
||||
expectedText = open(os.path.join(this_dir, "all-types.txt"), "r", encoding="utf8").read()
|
||||
assert str(root) + "\n" == expectedText
|
||||
|
||||
|
||||
def test_binary_write(all_types):
|
||||
root = all_types.TestAllTypes.new_message()
|
||||
init_all_types(root)
|
||||
root.write(open("example", "w"))
|
||||
open("example", "wb").write(root.to_bytes())
|
||||
|
||||
check_all_types(all_types.TestAllTypes.read(open("example", "r")))
|
||||
|
||||
|
||||
def test_packed_write(all_types):
|
||||
root = all_types.TestAllTypes.new_message()
|
||||
init_all_types(root)
|
||||
root.write_packed(open("example", "w"))
|
||||
|
||||
check_all_types(all_types.TestAllTypes.read_packed(open("example", "r")))
|
||||
with all_types.TestAllTypes.from_bytes(open("example", "rb").read()) as reader:
|
||||
check_all_types(reader)
|
||||
|
||||
@@ -1,13 +0,0 @@
|
||||
@0x84249be5c3bff005;
|
||||
|
||||
interface Foo {
|
||||
foo @0 () -> (val :UInt32);
|
||||
}
|
||||
|
||||
struct Bar {
|
||||
foo @0 :Foo;
|
||||
}
|
||||
|
||||
interface Baz {
|
||||
grault @0 () -> (bar: Bar);
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
import pytest
|
||||
|
||||
import capnp
|
||||
import test_response_capnp
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
async def kj_loop():
|
||||
async with capnp.kj_loop():
|
||||
yield
|
||||
|
||||
|
||||
class FooServer(test_response_capnp.Foo.Server):
|
||||
def __init__(self, val=1):
|
||||
self.val = val
|
||||
|
||||
async def foo(self, **kwargs):
|
||||
return 1
|
||||
|
||||
|
||||
class BazServer(test_response_capnp.Baz.Server):
|
||||
def __init__(self, val=1):
|
||||
self.val = val
|
||||
|
||||
async def grault(self, **kwargs):
|
||||
return {"foo": FooServer()}
|
||||
|
||||
|
||||
async def test_response_reference():
|
||||
baz = test_response_capnp.Baz._new_client(BazServer())
|
||||
|
||||
bar = (await baz.grault()).bar
|
||||
|
||||
foo = bar.foo
|
||||
# This used to cause an exception about invalid pointers because the response got garbage collected
|
||||
assert (await foo.foo()).val == 1
|
||||
|
||||
|
||||
async def test_response_reference2():
|
||||
baz = test_response_capnp.Baz._new_client(BazServer())
|
||||
|
||||
bar = (await baz.grault()).bar
|
||||
|
||||
# This always worked since it saved the intermediate response object
|
||||
response = await baz.grault()
|
||||
bar = response.bar
|
||||
foo = bar.foo
|
||||
assert (await foo.foo()).val == 1
|
||||
@@ -1,64 +0,0 @@
|
||||
"""
|
||||
rpc test
|
||||
"""
|
||||
|
||||
import pytest
|
||||
import capnp
|
||||
import socket
|
||||
|
||||
import test_capability_capnp
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
async def kj_loop():
|
||||
async with capnp.kj_loop():
|
||||
yield
|
||||
|
||||
|
||||
class Server(test_capability_capnp.TestInterface.Server):
|
||||
def __init__(self, val=100):
|
||||
self.val = val
|
||||
|
||||
async def foo(self, i, j, **kwargs):
|
||||
return str(i * 5 + self.val)
|
||||
|
||||
|
||||
async def test_simple_rpc_with_options():
|
||||
read, write = socket.socketpair()
|
||||
read = await capnp.AsyncIoStream.create_connection(sock=read)
|
||||
write = await capnp.AsyncIoStream.create_connection(sock=write)
|
||||
|
||||
_ = capnp.TwoPartyServer(write, bootstrap=Server())
|
||||
# This traversal limit is too low to receive the response in, so we expect
|
||||
# an exception during the call.
|
||||
client = capnp.TwoPartyClient(read, traversal_limit_in_words=1)
|
||||
|
||||
with pytest.raises(capnp.KjException):
|
||||
cap = client.bootstrap().cast_as(test_capability_capnp.TestInterface)
|
||||
|
||||
remote = cap.foo(i=5)
|
||||
_ = remote.wait()
|
||||
|
||||
|
||||
async def test_simple_rpc_bootstrap():
|
||||
read, write = socket.socketpair()
|
||||
read = await capnp.AsyncIoStream.create_connection(sock=read)
|
||||
write = await capnp.AsyncIoStream.create_connection(sock=write)
|
||||
|
||||
_ = capnp.TwoPartyServer(write, bootstrap=Server(100))
|
||||
client = capnp.TwoPartyClient(read)
|
||||
|
||||
cap = client.bootstrap()
|
||||
cap = cap.cast_as(test_capability_capnp.TestInterface)
|
||||
|
||||
# Check not only that the methods are there, but also that they are listed
|
||||
# as expected.
|
||||
assert "foo" in dir(cap)
|
||||
assert "bar" in dir(cap)
|
||||
assert "buz" in dir(cap)
|
||||
assert "bam" in dir(cap)
|
||||
|
||||
remote = cap.foo(i=5)
|
||||
response = await remote
|
||||
|
||||
assert response.x == "125"
|
||||
@@ -1,50 +0,0 @@
|
||||
import gc
|
||||
import os
|
||||
import socket
|
||||
import sys # add examples dir to sys.path
|
||||
import pytest
|
||||
|
||||
import capnp
|
||||
|
||||
examples_dir = os.path.join(os.path.dirname(__file__), "..", "examples")
|
||||
sys.path.append(examples_dir)
|
||||
|
||||
import async_calculator_client # noqa: E402
|
||||
import async_calculator_server # noqa: E402
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
async def kj_loop():
|
||||
async with capnp.kj_loop():
|
||||
yield
|
||||
|
||||
|
||||
async def test_calculator():
|
||||
read, write = socket.socketpair()
|
||||
read = await capnp.AsyncIoStream.create_connection(sock=read)
|
||||
write = await capnp.AsyncIoStream.create_connection(sock=write)
|
||||
|
||||
_ = capnp.TwoPartyServer(write, bootstrap=async_calculator_server.CalculatorImpl())
|
||||
await async_calculator_client.main(read)
|
||||
|
||||
|
||||
async def test_calculator_gc():
|
||||
def new_evaluate_impl(old_evaluate_impl):
|
||||
def call(*args, **kwargs):
|
||||
gc.collect()
|
||||
return old_evaluate_impl(*args, **kwargs)
|
||||
|
||||
return call
|
||||
|
||||
read, write = socket.socketpair()
|
||||
read = await capnp.AsyncIoStream.create_connection(sock=read)
|
||||
write = await capnp.AsyncIoStream.create_connection(sock=write)
|
||||
|
||||
# inject a gc.collect to the beginning of every evaluate_impl call
|
||||
evaluate_impl_orig = async_calculator_server.evaluate_impl
|
||||
async_calculator_server.evaluate_impl = new_evaluate_impl(evaluate_impl_orig)
|
||||
|
||||
_ = capnp.TwoPartyServer(write, bootstrap=async_calculator_server.CalculatorImpl())
|
||||
await async_calculator_client.main(read)
|
||||
|
||||
async_calculator_server.evaluate_impl = evaluate_impl_orig
|
||||
@@ -10,11 +10,6 @@ def addressbook():
|
||||
return capnp.load(os.path.join(this_dir, "addressbook.capnp"))
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def annotations():
|
||||
return capnp.load(os.path.join(this_dir, "annotations.capnp"))
|
||||
|
||||
|
||||
def test_basic_schema(addressbook):
|
||||
assert addressbook.Person.schema.fieldnames[0] == "id"
|
||||
|
||||
@@ -24,27 +19,3 @@ def test_list_schema(addressbook):
|
||||
personType = peopleField.schema.elementType
|
||||
|
||||
assert personType.node.id == addressbook.Person.schema.node.id
|
||||
|
||||
personListSchema = capnp._ListSchema(addressbook.Person)
|
||||
|
||||
assert personListSchema.elementType.node.id == addressbook.Person.schema.node.id
|
||||
|
||||
|
||||
def test_annotations(annotations):
|
||||
assert annotations.schema.node.annotations[0].value.text == "TestFile"
|
||||
|
||||
annotation = annotations.TestAnnotationOne.schema.node.annotations[0]
|
||||
assert annotation.value.text == "Test"
|
||||
|
||||
annotation = annotations.TestAnnotationTwo.schema.node.annotations[0]
|
||||
assert annotation.value.struct.as_struct(annotations.AnnotationStruct).test == 100
|
||||
|
||||
annotation = annotations.TestAnnotationThree.schema.node.annotations[0]
|
||||
annotation_list = annotation.value.list.as_list(capnp._ListSchema(annotations.AnnotationStruct))
|
||||
assert annotation_list[0].test == 100
|
||||
assert annotation_list[1].test == 101
|
||||
|
||||
annotation = annotations.TestAnnotationFour.schema.node.annotations[0]
|
||||
annotation_list = annotation.value.list.as_list(capnp._ListSchema(capnp.types.UInt16))
|
||||
assert annotation_list[0] == 200
|
||||
assert annotation_list[1] == 201
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
import warnings
|
||||
from contextlib import contextmanager
|
||||
|
||||
import gc
|
||||
import pytest
|
||||
import capnp
|
||||
import os
|
||||
import platform
|
||||
import test_regression
|
||||
import tempfile
|
||||
import pickle
|
||||
@@ -20,28 +18,6 @@ def all_types():
|
||||
return capnp.load(os.path.join(this_dir, "all_types.capnp"))
|
||||
|
||||
|
||||
def test_roundtrip_file(all_types):
|
||||
f = tempfile.TemporaryFile()
|
||||
msg = all_types.TestAllTypes.new_message()
|
||||
test_regression.init_all_types(msg)
|
||||
msg.write(f)
|
||||
|
||||
f.seek(0)
|
||||
msg = all_types.TestAllTypes.read(f)
|
||||
test_regression.check_all_types(msg)
|
||||
|
||||
|
||||
def test_roundtrip_file_packed(all_types):
|
||||
f = tempfile.TemporaryFile()
|
||||
msg = all_types.TestAllTypes.new_message()
|
||||
test_regression.init_all_types(msg)
|
||||
msg.write_packed(f)
|
||||
|
||||
f.seek(0)
|
||||
msg = all_types.TestAllTypes.read_packed(f)
|
||||
test_regression.check_all_types(msg)
|
||||
|
||||
|
||||
def test_roundtrip_bytes(all_types):
|
||||
msg = all_types.TestAllTypes.new_message()
|
||||
test_regression.init_all_types(msg)
|
||||
@@ -51,105 +27,6 @@ def test_roundtrip_bytes(all_types):
|
||||
test_regression.check_all_types(msg)
|
||||
|
||||
|
||||
@pytest.mark.skipif(
|
||||
platform.python_implementation() == "PyPy",
|
||||
reason="TODO: Investigate why this works on CPython but fails on PyPy.",
|
||||
)
|
||||
def test_roundtrip_segments(all_types):
|
||||
msg = all_types.TestAllTypes.new_message()
|
||||
test_regression.init_all_types(msg)
|
||||
segments = msg.to_segments()
|
||||
msg = all_types.TestAllTypes.from_segments(segments)
|
||||
test_regression.check_all_types(msg)
|
||||
|
||||
|
||||
@pytest.mark.skipif(
|
||||
platform.python_implementation() == "PyPy",
|
||||
reason="TODO: Investigate segmented serialization support on PyPy.",
|
||||
)
|
||||
def test_segment_views_are_read_only_buffers(all_types):
|
||||
msg = all_types.TestAllTypes.new_message()
|
||||
test_regression.init_all_types(msg)
|
||||
|
||||
segments = msg.to_segments()
|
||||
segment_views = msg.to_segment_views()
|
||||
|
||||
assert len(segment_views) == len(segments)
|
||||
assert len(segment_views) >= 1
|
||||
|
||||
for segment_view, segment_bytes in zip(segment_views, segments):
|
||||
assert not isinstance(segment_view, bytes)
|
||||
view = memoryview(segment_view)
|
||||
try:
|
||||
assert view.readonly is True
|
||||
assert view.tobytes() == segment_bytes
|
||||
finally:
|
||||
view.release()
|
||||
|
||||
|
||||
@pytest.mark.skipif(
|
||||
platform.python_implementation() == "PyPy",
|
||||
reason="TODO: Investigate segmented serialization support on PyPy.",
|
||||
)
|
||||
def test_roundtrip_segment_views(all_types):
|
||||
msg = all_types.TestAllTypes.new_message()
|
||||
test_regression.init_all_types(msg)
|
||||
|
||||
segment_views = msg.to_segment_views()
|
||||
msg = all_types.TestAllTypes.from_segments(segment_views)
|
||||
test_regression.check_all_types(msg)
|
||||
|
||||
|
||||
@pytest.mark.skipif(
|
||||
platform.python_implementation() == "PyPy",
|
||||
reason="TODO: Investigate segmented serialization support on PyPy.",
|
||||
)
|
||||
def test_segment_views_are_not_writable(all_types):
|
||||
msg = all_types.TestAllTypes.new_message()
|
||||
test_regression.init_all_types(msg)
|
||||
|
||||
segment_views = msg.to_segment_views()
|
||||
view = memoryview(segment_views[0])
|
||||
try:
|
||||
assert len(view) > 0
|
||||
with pytest.raises(TypeError):
|
||||
view[0] = 0
|
||||
finally:
|
||||
view.release()
|
||||
|
||||
|
||||
@pytest.mark.skipif(
|
||||
platform.python_implementation() == "PyPy",
|
||||
reason="TODO: Investigate segmented serialization support on PyPy.",
|
||||
)
|
||||
def test_segment_view_keeps_message_alive(all_types):
|
||||
msg = all_types.TestAllTypes.new_message()
|
||||
test_regression.init_all_types(msg)
|
||||
|
||||
segment_views = msg.to_segment_views()
|
||||
segment_view = segment_views[0]
|
||||
view = memoryview(segment_view)
|
||||
expected = view.tobytes()
|
||||
|
||||
del msg
|
||||
del segment_views
|
||||
del segment_view
|
||||
gc.collect()
|
||||
|
||||
try:
|
||||
assert view.tobytes() == expected
|
||||
finally:
|
||||
view.release()
|
||||
|
||||
|
||||
def test_segment_views_require_root_struct(all_types):
|
||||
msg = all_types.TestAllTypes.new_message()
|
||||
nested = msg.init("structField")
|
||||
|
||||
with pytest.raises(capnp.KjException):
|
||||
nested.to_segment_views()
|
||||
|
||||
|
||||
@pytest.mark.skipif(
|
||||
sys.version_info[0] < 3,
|
||||
reason="mmap doesn't implement the buffer interface under python 2.",
|
||||
@@ -159,7 +36,7 @@ def test_roundtrip_bytes_mmap(all_types):
|
||||
test_regression.init_all_types(msg)
|
||||
|
||||
with tempfile.TemporaryFile() as f:
|
||||
msg.write(f)
|
||||
f.write(msg.to_bytes())
|
||||
length = f.tell()
|
||||
|
||||
f.seek(0)
|
||||
@@ -188,19 +65,6 @@ def test_roundtrip_bytes_fail(all_types):
|
||||
pass
|
||||
|
||||
|
||||
@pytest.mark.skipif(
|
||||
platform.python_implementation() == "PyPy",
|
||||
reason="This works in PyPy 4.0.1 but travisci's version of PyPy has some bug that fails this test.",
|
||||
)
|
||||
def test_roundtrip_bytes_packed(all_types):
|
||||
msg = all_types.TestAllTypes.new_message()
|
||||
test_regression.init_all_types(msg)
|
||||
message_bytes = msg.to_bytes_packed()
|
||||
|
||||
msg = all_types.TestAllTypes.from_bytes_packed(message_bytes)
|
||||
test_regression.check_all_types(msg)
|
||||
|
||||
|
||||
@contextmanager
|
||||
def _warnings(expected_count=2, expected_text="This message has already been written once."):
|
||||
with warnings.catch_warnings(record=True) as w:
|
||||
@@ -211,23 +75,6 @@ def _warnings(expected_count=2, expected_text="This message has already been wri
|
||||
assert all(expected_text in str(x.message) for x in w), w
|
||||
|
||||
|
||||
def test_roundtrip_file_multiple(all_types):
|
||||
f = tempfile.TemporaryFile()
|
||||
msg = all_types.TestAllTypes.new_message()
|
||||
test_regression.init_all_types(msg)
|
||||
msg.write(f)
|
||||
with _warnings(2):
|
||||
msg.write(f)
|
||||
msg.write(f)
|
||||
|
||||
f.seek(0)
|
||||
i = 0
|
||||
for msg in all_types.TestAllTypes.read_multiple(f):
|
||||
test_regression.check_all_types(msg)
|
||||
i += 1
|
||||
assert i == 3
|
||||
|
||||
|
||||
def test_roundtrip_bytes_multiple(all_types):
|
||||
msg = all_types.TestAllTypes.new_message()
|
||||
test_regression.init_all_types(msg)
|
||||
@@ -244,63 +91,6 @@ def test_roundtrip_bytes_multiple(all_types):
|
||||
assert i == 3
|
||||
|
||||
|
||||
def test_roundtrip_file_multiple_packed(all_types):
|
||||
f = tempfile.TemporaryFile()
|
||||
msg = all_types.TestAllTypes.new_message()
|
||||
test_regression.init_all_types(msg)
|
||||
msg.write_packed(f)
|
||||
with _warnings(2):
|
||||
msg.write_packed(f)
|
||||
msg.write_packed(f)
|
||||
|
||||
f.seek(0)
|
||||
i = 0
|
||||
for msg in all_types.TestAllTypes.read_multiple_packed(f):
|
||||
test_regression.check_all_types(msg)
|
||||
i += 1
|
||||
assert i == 3
|
||||
|
||||
|
||||
def test_roundtrip_bytes_multiple_packed(all_types):
|
||||
msg = all_types.TestAllTypes.new_message()
|
||||
test_regression.init_all_types(msg)
|
||||
|
||||
msgs = msg.to_bytes_packed()
|
||||
with _warnings(2):
|
||||
msgs += msg.to_bytes_packed()
|
||||
msgs += msg.to_bytes_packed()
|
||||
|
||||
i = 0
|
||||
for msg in all_types.TestAllTypes.read_multiple_bytes_packed(msgs):
|
||||
test_regression.check_all_types(msg)
|
||||
i += 1
|
||||
assert i == 3
|
||||
|
||||
|
||||
def test_file_and_bytes(all_types):
|
||||
f = tempfile.TemporaryFile()
|
||||
msg = all_types.TestAllTypes.new_message()
|
||||
test_regression.init_all_types(msg)
|
||||
msg.write(f)
|
||||
|
||||
f.seek(0)
|
||||
|
||||
with _warnings(1):
|
||||
assert f.read() == msg.to_bytes()
|
||||
|
||||
|
||||
def test_file_and_bytes_packed(all_types):
|
||||
f = tempfile.TemporaryFile()
|
||||
msg = all_types.TestAllTypes.new_message()
|
||||
test_regression.init_all_types(msg)
|
||||
msg.write_packed(f)
|
||||
|
||||
f.seek(0)
|
||||
|
||||
with _warnings(1):
|
||||
assert f.read() == msg.to_bytes_packed()
|
||||
|
||||
|
||||
def test_pickle(all_types):
|
||||
msg = all_types.TestAllTypes.new_message()
|
||||
test_regression.init_all_types(msg)
|
||||
@@ -326,22 +116,6 @@ def test_from_bytes_traversal_limit(all_types):
|
||||
assert msg.structList[i].uInt8Field == 0
|
||||
|
||||
|
||||
def test_from_bytes_packed_traversal_limit(all_types):
|
||||
size = 1024
|
||||
bld = all_types.TestAllTypes.new_message()
|
||||
bld.init("structList", size)
|
||||
data = bld.to_bytes_packed()
|
||||
|
||||
msg = all_types.TestAllTypes.from_bytes_packed(data)
|
||||
with pytest.raises(capnp.KjException):
|
||||
for i in range(0, size):
|
||||
msg.structList[i].uInt8Field == 0
|
||||
|
||||
msg = all_types.TestAllTypes.from_bytes_packed(data, traversal_limit_in_words=2**62)
|
||||
for i in range(0, size):
|
||||
assert msg.structList[i].uInt8Field == 0
|
||||
|
||||
|
||||
def test_malformed_text_field_reraise():
|
||||
SCHEMA = "@0xdbb9ad1f14bf0b36;\nstruct Person { name @0 :Text; age @1 :UInt32; }\n"
|
||||
with tempfile.NamedTemporaryFile(suffix=".capnp", mode="w", delete=False) as f:
|
||||
@@ -353,10 +127,7 @@ def test_malformed_text_field_reraise():
|
||||
buf = bytearray(Person.new_message(name="alice", age=30).to_bytes())
|
||||
buf[37] ^= 0xFF
|
||||
|
||||
# The process should raise an exception, not SIGSEGV
|
||||
try:
|
||||
with Person.from_bytes(bytes(buf), traversal_limit_in_words=2**20) as r:
|
||||
_ = str(r.name)
|
||||
except Exception:
|
||||
# Success: We caught an exception cleanly
|
||||
pass
|
||||
# An invalid UTF-8 error description may itself raise UnicodeDecodeError.
|
||||
with pytest.raises((capnp.KjException, UnicodeDecodeError)):
|
||||
with Person.from_bytes(bytes(buf), traversal_limit_in_words=2**20) as reader:
|
||||
_ = reader.name
|
||||
|
||||
@@ -50,7 +50,7 @@ def test_which_builder(addressbook):
|
||||
|
||||
|
||||
def test_which_reader(addressbook):
|
||||
def writeAddressBook(fd):
|
||||
def writeAddressBook(file):
|
||||
message = capnp._MallocMessageBuilder()
|
||||
addressBook = message.init_root(addressbook.AddressBook)
|
||||
people = addressBook.init("people", 2)
|
||||
@@ -61,13 +61,14 @@ def test_which_reader(addressbook):
|
||||
bob = people[1]
|
||||
bob.employment.unemployed = None
|
||||
|
||||
capnp._write_packed_message_to_fd(fd, message)
|
||||
file.write(addressBook.to_bytes())
|
||||
|
||||
f = tempfile.TemporaryFile()
|
||||
writeAddressBook(f.fileno())
|
||||
writeAddressBook(f)
|
||||
f.seek(0)
|
||||
|
||||
addresses = addressbook.AddressBook.read_packed(f)
|
||||
with addressbook.AddressBook.from_bytes(f.read()) as reader:
|
||||
addresses = reader
|
||||
|
||||
people = addresses.people
|
||||
|
||||
|
||||
@@ -1,27 +0,0 @@
|
||||
@0x9dafb673e5609df6;
|
||||
|
||||
|
||||
enum FruitId {
|
||||
apple @0;
|
||||
banana @1;
|
||||
cherry @2;
|
||||
}
|
||||
|
||||
struct UnknownFruit {
|
||||
fruitId @0: FruitId;
|
||||
}
|
||||
|
||||
struct Apple {
|
||||
fruitId @0: FruitId;
|
||||
color @1: Text;
|
||||
}
|
||||
|
||||
struct Banana {
|
||||
fruitId @0: FruitId;
|
||||
length @1: Float32;
|
||||
}
|
||||
|
||||
struct Cherry {
|
||||
fruitId @0: FruitId;
|
||||
sweetness @1: UInt8;
|
||||
}
|
||||
@@ -1,93 +0,0 @@
|
||||
import os
|
||||
|
||||
import pytest
|
||||
|
||||
import capnp
|
||||
|
||||
this_dir = os.path.dirname(__file__)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def message_schemas():
|
||||
return capnp.load(os.path.join(this_dir, "test_structs_sequence.capnp"))
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def make_apple(message_schemas):
|
||||
def _make_apple(color: str):
|
||||
apple = message_schemas.Apple.new_message()
|
||||
apple.fruitId = message_schemas.FruitId.apple
|
||||
apple.color = color
|
||||
return apple
|
||||
|
||||
return _make_apple
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def red_apple(make_apple):
|
||||
return make_apple("Red")
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def green_apple(make_apple):
|
||||
return make_apple("Green")
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def banana(message_schemas):
|
||||
banana_ = message_schemas.Banana.new_message()
|
||||
banana_.fruitId = message_schemas.FruitId.banana
|
||||
banana_.length = 12.345
|
||||
return banana_
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def cherry(message_schemas):
|
||||
cherry_ = message_schemas.Cherry.new_message()
|
||||
cherry_.fruitId = message_schemas.FruitId.cherry
|
||||
cherry_.sweetness = 64
|
||||
return cherry_
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def fruit_basket(cherry, red_apple, banana, green_apple):
|
||||
return [cherry, red_apple, banana, green_apple]
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def fruit_basket_encoded(fruit_basket):
|
||||
return b"".join(fruit.to_bytes_packed() for fruit in fruit_basket)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def expected(fruit_basket):
|
||||
return [fruit.to_dict() for fruit in fruit_basket]
|
||||
|
||||
|
||||
def test_parse_structs_sequence(message_schemas, fruit_basket_encoded, expected):
|
||||
# ARRANGE
|
||||
reader = capnp.read_multiple_bytes_packed(fruit_basket_encoded)
|
||||
|
||||
def _parse_fruit(any_):
|
||||
unknown_fruit = any_.as_struct(message_schemas.UnknownFruit)
|
||||
if unknown_fruit.fruitId == message_schemas.FruitId.apple:
|
||||
return any_.as_struct(message_schemas.Apple)
|
||||
|
||||
if unknown_fruit.fruitId == message_schemas.FruitId.banana:
|
||||
return any_.as_struct(message_schemas.Banana)
|
||||
|
||||
if unknown_fruit.fruitId == message_schemas.FruitId.cherry:
|
||||
return any_.as_struct(message_schemas.Cherry)
|
||||
|
||||
return unknown_fruit
|
||||
|
||||
# ACT
|
||||
parsed = [_parse_fruit(any_).to_dict() for any_ in reader]
|
||||
|
||||
# ASSERT
|
||||
assert parsed == expected
|
||||
|
||||
|
||||
def test_empty_sequence():
|
||||
reader = capnp.read_multiple_bytes_packed(b"")
|
||||
assert len(list(reader)) == 0
|
||||
Reference in New Issue
Block a user