diff --git a/pytest.ini b/pytest.ini index 270b5f5d..ff73daef 100644 --- a/pytest.ini +++ b/pytest.ini @@ -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/* diff --git a/tests/bindgen/__init__.py b/tests/bindgen/__init__.py index cd6991b8..e69de29b 100644 --- a/tests/bindgen/__init__.py +++ b/tests/bindgen/__init__.py @@ -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 diff --git a/tests/bindgen/bare_funcs/test_bare_funcs.py b/tests/bindgen/bare_funcs/test_bare_funcs.py new file mode 100644 index 00000000..bd551447 --- /dev/null +++ b/tests/bindgen/bare_funcs/test_bare_funcs.py @@ -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 diff --git a/tests/bindgen/conftest.py b/tests/bindgen/conftest.py new file mode 100644 index 00000000..6eb29760 --- /dev/null +++ b/tests/bindgen/conftest.py @@ -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 diff --git a/tests/bindgen/export_resources/test_export_resources.py b/tests/bindgen/export_resources/test_export_resources.py new file mode 100644 index 00000000..2fd9d7f8 --- /dev/null +++ b/tests/bindgen/export_resources/test_export_resources.py @@ -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!' diff --git a/tests/bindgen/list_types/test_lists.py b/tests/bindgen/list_types/test_lists.py new file mode 100644 index 00000000..130c38e0 --- /dev/null +++ b/tests/bindgen/list_types/test_lists.py @@ -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'] diff --git a/tests/bindgen/test_bindgen.py b/tests/bindgen/test_bindgen.py deleted file mode 100644 index 380f81d8..00000000 --- a/tests/bindgen/test_bindgen.py +++ /dev/null @@ -1,49 +0,0 @@ -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']