-
Notifications
You must be signed in to change notification settings - Fork 59
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Proof of concept of bindgen tests using componentize-py
The basic idea is that you define a wit file along with guest code in Python. We can then generate a wasm component using componentize-py, host bindings using `wasmtime.bindgen`, and then test that the generated bindings work as expected.
- Loading branch information
Showing
12 changed files
with
270 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -14,3 +14,4 @@ wasmtime/win32-* | |
wasmtime/include | ||
wasmtime/bindgen/generated | ||
tests/codegen/generated | ||
tests/bindgen/generated |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,2 +1,4 @@ | ||
[pytest] | ||
addopts = --doctest-modules --mypy | ||
norecursedirs = | ||
tests/bindgen/* |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -42,6 +42,7 @@ | |
'pytest', | ||
'pycparser', | ||
'pytest-mypy', | ||
'componentize-py', | ||
], | ||
}, | ||
classifiers=[ | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,140 @@ | ||
"""Test suite for testing generated code with guest code written in Python. | ||
These tests work by allowing you to write a WIT file, implement the guest | ||
code in Python via componentize-py, and then test the generated Python | ||
bindings. To add a new test, first create the needed fixtures: | ||
* Create a new sub directory. | ||
* Within that directory create a `.wit` file. | ||
* Create an `app.py` file in that directory implementing the guest code. | ||
Then to write the test itself: | ||
* Define the params of the testcase with `BindgenTestCase`. | ||
* Generate the Python bindings using `generate_bindings()`. This will also | ||
build the `app.wasm` file using `componentize-py`. | ||
* `generate_bindings()` returns the store and the instantiated `Root` object | ||
which you can then test. | ||
## Example | ||
Given this directory: | ||
``` | ||
bare_funcs/ | ||
├── app.py <-- guest code implementation | ||
├── barefuncs <-- componentize-py bindings | ||
│ ├── __init__.py | ||
│ └── types.py | ||
└── component.wit <-- test .wit file | ||
``` | ||
With a `component.wit` file of: | ||
```wit | ||
package component:barefuncs; | ||
world barefuncs { | ||
export foo: func(a: s32) -> s32; | ||
} | ||
``` | ||
And guest code of: | ||
```python | ||
class Barefuncs: | ||
def foo(self, a: int) -> int: | ||
return a + 1 | ||
``` | ||
You can write a testcase for this using: | ||
```python | ||
def test_bare_funcs(): | ||
testcase = BindgenTestCase( | ||
guest_code_dir='bare_funcs', | ||
world_name='barefuncs', | ||
) | ||
store, root = generate_bindings(testcase) | ||
assert root.foo(store, 10) == 11 | ||
``` | ||
""" | ||
import os | ||
from pathlib import Path | ||
from dataclasses import dataclass, field | ||
from wasmtime.bindgen import generate | ||
import wasmtime | ||
import contextlib | ||
import importlib | ||
import tempfile | ||
import subprocess | ||
import shutil | ||
|
||
|
||
TEST_ROOT = Path(__file__).parent | ||
BINDGEN_DIR = TEST_ROOT / 'generated' | ||
|
||
|
||
@contextlib.contextmanager | ||
def chdir(dirname: Path): | ||
original = os.getcwd() | ||
try: | ||
os.chdir(str(dirname)) | ||
yield | ||
finally: | ||
os.chdir(original) | ||
|
||
|
||
@dataclass | ||
class BindgenTestCase: | ||
world_name: str | ||
guest_code_dir: str | ||
app_dir: Path = field(init=False) | ||
wit_filename: str = 'component.wit' | ||
app_name: str = 'app' | ||
|
||
def __post_init__(self): | ||
self.app_dir = TEST_ROOT / self.guest_code_dir | ||
|
||
@property | ||
def wit_full_path(self): | ||
return self.app_dir.joinpath(self.wit_filename) | ||
|
||
@property | ||
def testsuite_name(self): | ||
# The name of the directory that contains the | ||
# guest Python code is used as the identifier for | ||
# package names, etc. | ||
return self.app_dir.name | ||
|
||
|
||
def generate_bindings(testcase: BindgenTestCase): | ||
wit_path = testcase.wit_full_path | ||
componentize_py = shutil.which('componentize-py') | ||
if componentize_py is None: | ||
raise RuntimeError("Could not find componentize-py executable.") | ||
with tempfile.NamedTemporaryFile('w') as f: | ||
output_wasm = str(f.name + '.wasm') | ||
with chdir(testcase.app_dir): | ||
subprocess.run([ | ||
componentize_py, '-d', str(wit_path), '-w', testcase.world_name, | ||
'componentize', '--stub-wasi', testcase.app_name, | ||
'-o', output_wasm | ||
], check=True) | ||
# Once we've done that now generate the python bindings. | ||
testsuite_name = testcase.testsuite_name | ||
with open(output_wasm, 'rb') as out: | ||
# Mapping of filename -> content_bytes | ||
results = generate(testsuite_name, out.read()) | ||
for filename, contents in results.items(): | ||
path = BINDGEN_DIR / testsuite_name / filename | ||
path.parent.mkdir(parents=True, exist_ok=True) | ||
path.write_bytes(contents) | ||
# Return an instantiated module for the caller to test. | ||
pkg = importlib.import_module(f'.generated.{testsuite_name}', | ||
package=__package__) | ||
store = wasmtime.Store() | ||
root = pkg.Root(store) | ||
return store, root |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
class Barefuncs: | ||
def foo(self, a): | ||
return a + 1 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
package component:barefuncs; | ||
|
||
world barefuncs { | ||
export foo: func(a: s32) -> s32; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
import sys | ||
from types import ModuleType | ||
|
||
|
||
class MyInterfaceName: | ||
def interface_func(self, foo: str) -> str: | ||
return f"hello {foo}" | ||
|
||
|
||
# componentize-py expects that resources within an interface are defined | ||
# as a class in a separate module that matches the interface name. | ||
# | ||
# Normally, you'd want to go the more typical route of running | ||
# | ||
# componentize-py -d component.wit -w testworld bindings . | ||
# | ||
# to generate the types and protocols to help you write guest code, | ||
# and then split the code into multiple files, but we're taking a | ||
# shortcut here so we can write all the guest code in a single file. | ||
class DemoResourceClass: | ||
def __init__(self, name: str) -> None: | ||
self.name = name | ||
|
||
def greet(self, greeting: str) -> str: | ||
return f'{greeting}, {self.name}!' | ||
|
||
|
||
mod = ModuleType("my_interface_name") | ||
mod.DemoResourceClass = DemoResourceClass | ||
sys.modules['my_interface_name'] = mod |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
package component:basicresource; | ||
|
||
interface my-interface-name { | ||
interface-func: func(foo: string) -> string; | ||
resource demo-resource-class { | ||
constructor(name: string); | ||
greet: func(greeting: string) -> string; | ||
} | ||
} | ||
|
||
world testworld { | ||
export my-interface-name; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
from typing import List | ||
|
||
|
||
class Lists: | ||
def strings(self, a: str) -> str: | ||
return a | ||
|
||
def bytes(self, a: bytes) -> bytes: | ||
return a | ||
|
||
def ints(self, a: List[int]) -> List[int]: | ||
return a | ||
|
||
def string_list(self, a: List[str]) -> List[str]: | ||
return a |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
package component:lists; | ||
|
||
world lists { | ||
export strings: func(a: string) -> string; | ||
export bytes: func(a: list<u8>) -> list<u8>; | ||
export ints: func(a: list<u32>) -> list<u32>; | ||
export string-list: func(a: list<string>) -> list<string>; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,49 @@ | ||
from . import BindgenTestCase, generate_bindings | ||
|
||
def test_bare_funcs(): | ||
testcase = BindgenTestCase( | ||
guest_code_dir='bare_funcs', | ||
world_name='barefuncs', | ||
) | ||
store, root = generate_bindings(testcase) | ||
assert root.foo(store, 10) == 11 | ||
|
||
|
||
# This test works, but needs wasmtime-py#232 merged to pass. | ||
# | ||
# def test_export_resources(): | ||
# testcase = BindgenTestCase( | ||
# guest_code_dir='export_resources', | ||
# world_name='testworld', | ||
# ) | ||
# store, root = generate_bindings(testcase) | ||
# interface = root.my_interface_name() | ||
# instance = interface.DemoResourceClass(store, 'myname') | ||
# result = instance.greet(store, 'Hello there') | ||
# assert result == 'Hello there, myname!' | ||
|
||
|
||
def test_lists(): | ||
testcase = BindgenTestCase( | ||
guest_code_dir='list_types', | ||
world_name='lists' | ||
) | ||
store, root = generate_bindings(testcase) | ||
assert root.strings(store, '') == '' | ||
assert root.strings(store, 'a') == 'a' | ||
assert root.strings(store, 'hello world') == 'hello world' | ||
assert root.strings(store, 'hello ⚑ world') == 'hello ⚑ world' | ||
|
||
assert root.bytes(store, b'') == b'' | ||
assert root.bytes(store, b'a') == b'a' | ||
assert root.bytes(store, b'\x01\x02') == b'\x01\x02' | ||
|
||
assert root.ints(store, []) == [] | ||
assert root.ints(store, [1]) == [1] | ||
assert root.ints(store, [1, 2, 100, 10000]) == [1, 2, 100, 10000] | ||
|
||
assert root.string_list(store, []) == [] | ||
assert root.string_list(store, ['']) == [''] | ||
assert root.string_list( | ||
store, ['a', 'b', '', 'd', 'hello'] | ||
) == ['a', 'b', '', 'd', 'hello'] |