Skip to content

Commit

Permalink
Restructure bindgen testcases
Browse files Browse the repository at this point in the history
* Each test case is now in the same directory with the corresponding
  WIT file and guest code (app.py).
* The helper functions for generating wasm components and host bindings
  is moved to a pytest fixture to avoid relative package issues when
  using direct imports.
* Remove the use of changing the cwd in the test process.
  • Loading branch information
jamesls committed May 14, 2024
1 parent 95a22ab commit a429569
Show file tree
Hide file tree
Showing 7 changed files with 195 additions and 191 deletions.
4 changes: 2 additions & 2 deletions pytest.ini
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
[pytest]
addopts = --doctest-modules --mypy
addopts = --doctest-modules --mypy --ignore-glob=tests/bindgen/*/app.py
norecursedirs =
tests/bindgen/*
tests/bindgen/generated/*
140 changes: 0 additions & 140 deletions tests/bindgen/__init__.py
Original file line number Diff line number Diff line change
@@ -1,140 +0,0 @@
"""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
9 changes: 9 additions & 0 deletions tests/bindgen/bare_funcs/test_bare_funcs.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
from pathlib import Path


def test_bare_funcs(bindgen_testcase):
store, root = bindgen_testcase(
guest_code_dir=Path(__file__).parent,
world_name='barefuncs',
)
assert root.foo(store, 10) == 11
146 changes: 146 additions & 0 deletions tests/bindgen/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
"""Fixtures to define test suites for generated code Python guest code.
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
import importlib
import tempfile
import subprocess
import shutil

from pytest import fixture

import wasmtime
from wasmtime.bindgen import generate


TEST_ROOT = Path(__file__).parent
BINDGEN_DIR = TEST_ROOT / 'generated'


@dataclass
class BindgenTestCase:
guest_code_dir: Path
world_name: str
wit_filename: str = 'component.wit'
app_dir: Path = field(init=False)
app_name: str = field(init=False, default='app', repr=False)

def __post_init__(self):
self.app_dir = Path(self.guest_code_dir).resolve()

@property
def wit_full_path(self):
return self.guest_code_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.guest_code_dir.name


def generate_bindings(guest_code_dir: Path,
world_name: str,
wit_filename: str = 'component.wit'):
tc = BindgenTestCase(
guest_code_dir=guest_code_dir,
world_name=world_name,
wit_filename=wit_filename)
return _generate_bindings(tc)


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')
subprocess.run([
componentize_py, '-d', str(wit_path), '-w', testcase.world_name,
'componentize', '--stub-wasi', testcase.app_name,
'-o', output_wasm
], check=True, cwd=testcase.guest_code_dir)
# 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


@fixture
def bindgen_testcase():
return generate_bindings
12 changes: 12 additions & 0 deletions tests/bindgen/export_resources/test_export_resources.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
from pathlib import Path


def test_bare_funcs(bindgen_testcase):
store, root = bindgen_testcase(
guest_code_dir=Path(__file__).parent,
world_name='testworld',
)
interface = root.my_interface_name()
instance = interface.DemoResourceClass(store, 'myname')
result = instance.greet(store, 'Hello there')
assert result == 'Hello there, myname!'
26 changes: 26 additions & 0 deletions tests/bindgen/list_types/test_lists.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
from pathlib import Path


def test_lists(bindgen_testcase):
store, root = bindgen_testcase(
guest_code_dir=Path(__file__).parent,
world_name='lists',
)
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']
49 changes: 0 additions & 49 deletions tests/bindgen/test_bindgen.py

This file was deleted.

0 comments on commit a429569

Please sign in to comment.