Minimal pycapnp

This commit is contained in:
Adeeb Shihadeh
2026-09-21 18:57:37 -07:00
parent a0cb5cdf06
commit 5befba15f3
125 changed files with 423 additions and 14793 deletions

View File

@@ -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);
}

View File

@@ -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.

View File

@@ -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]) { }

View File

@@ -1,5 +0,0 @@
@0x9afc0f7513269df3;
struct Child {
name @0 :Text;
}

View File

@@ -1,7 +0,0 @@
@0x95c41c96183b9c2f;
using import "/schemas/child.capnp".Child;
struct Parent {
child @0 :List(Child);
}

View File

@@ -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!"

View File

@@ -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);
}

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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)

View File

@@ -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()

View File

@@ -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
View 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"

View File

@@ -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

View File

@@ -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

View File

@@ -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
View 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()]) == []

View File

@@ -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"

View File

@@ -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)

View File

@@ -1,13 +0,0 @@
@0x84249be5c3bff005;
interface Foo {
foo @0 () -> (val :UInt32);
}
struct Bar {
foo @0 :Foo;
}
interface Baz {
grault @0 () -> (bar: Bar);
}

View File

@@ -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

View File

@@ -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"

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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;
}

View File

@@ -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