|
| 1 | +"""Fixtures to define test suites for generated code Python guest code. |
| 2 | +
|
| 3 | +These tests work by allowing you to write a WIT file, implement the guest |
| 4 | +code in Python via componentize-py, and then test the generated Python |
| 5 | +bindings. To add a new test, first create the needed fixtures: |
| 6 | +
|
| 7 | +* Create a new sub directory. |
| 8 | +* Within that directory create a `.wit` file. |
| 9 | +* Create an `app.py` file in that directory implementing the guest code. |
| 10 | +
|
| 11 | +Then to write the test itself: |
| 12 | +
|
| 13 | +* Create a `test_<name>.py` in the same directory. |
| 14 | +* Use the `bindgest_testcase` in your test to create the wasm component |
| 15 | + and generate python bindings for this component. |
| 16 | +
|
| 17 | +## Example |
| 18 | +
|
| 19 | +Given this directory: |
| 20 | +
|
| 21 | +``` |
| 22 | +bare_funcs/ |
| 23 | +├── app.py <-- guest code implementation |
| 24 | +├── barefuncs <-- componentize-py bindings |
| 25 | +│ ├── __init__.py |
| 26 | +│ └── types.py |
| 27 | +├── component.wit <-- test .wit file |
| 28 | +└── test_mycomp.py <-- pytest test case of bindings |
| 29 | +``` |
| 30 | +
|
| 31 | +With a `component.wit` file of: |
| 32 | +
|
| 33 | +```wit |
| 34 | +package component:barefuncs; |
| 35 | +
|
| 36 | +world barefuncs { |
| 37 | + export foo: func(a: s32) -> s32; |
| 38 | +} |
| 39 | +``` |
| 40 | +
|
| 41 | +And guest code of: |
| 42 | +
|
| 43 | +```python |
| 44 | +class Barefuncs: |
| 45 | + def foo(self, a: int) -> int: |
| 46 | + return a + 1 |
| 47 | +``` |
| 48 | +
|
| 49 | +You can write a testcase for this using: |
| 50 | +
|
| 51 | +```python |
| 52 | +from pathlib import Path |
| 53 | +
|
| 54 | +
|
| 55 | +def test_bare_funcs(bindgen_testcase): |
| 56 | + testcase = bindgen_testcase( |
| 57 | + guest_code_dir=Path(__file__).parent, |
| 58 | + world_name='barefuncs', |
| 59 | + ) |
| 60 | + store, root = generate_bindings(testcase) |
| 61 | + assert root.foo(store, 10) == 11 |
| 62 | +``` |
| 63 | +
|
| 64 | +""" |
| 65 | +from pathlib import Path |
| 66 | +from dataclasses import dataclass, field |
| 67 | +import importlib |
| 68 | +import tempfile |
| 69 | +import subprocess |
| 70 | +import shutil |
| 71 | + |
| 72 | +from pytest import fixture |
| 73 | + |
| 74 | +import wasmtime |
| 75 | +from wasmtime.bindgen import generate |
| 76 | + |
| 77 | + |
| 78 | +TEST_ROOT = Path(__file__).parent |
| 79 | +BINDGEN_DIR = TEST_ROOT / 'generated' |
| 80 | + |
| 81 | + |
| 82 | +@dataclass |
| 83 | +class BindgenTestCase: |
| 84 | + guest_code_dir: Path |
| 85 | + world_name: str |
| 86 | + wit_filename: str = 'component.wit' |
| 87 | + app_dir: Path = field(init=False) |
| 88 | + app_name: str = field(init=False, default='app', repr=False) |
| 89 | + |
| 90 | + def __post_init__(self): |
| 91 | + self.app_dir = Path(self.guest_code_dir).resolve() |
| 92 | + |
| 93 | + @property |
| 94 | + def wit_full_path(self): |
| 95 | + return self.guest_code_dir.joinpath(self.wit_filename) |
| 96 | + |
| 97 | + @property |
| 98 | + def testsuite_name(self): |
| 99 | + # The name of the directory that contains the |
| 100 | + # guest Python code is used as the identifier for |
| 101 | + # package names, etc. |
| 102 | + return self.guest_code_dir.name |
| 103 | + |
| 104 | + |
| 105 | +def generate_bindings(guest_code_dir: Path, |
| 106 | + world_name: str, |
| 107 | + wit_filename: str = 'component.wit'): |
| 108 | + tc = BindgenTestCase( |
| 109 | + guest_code_dir=guest_code_dir, |
| 110 | + world_name=world_name, |
| 111 | + wit_filename=wit_filename) |
| 112 | + return _generate_bindings(tc) |
| 113 | + |
| 114 | + |
| 115 | +def _generate_bindings(testcase: BindgenTestCase): |
| 116 | + wit_path = testcase.wit_full_path |
| 117 | + componentize_py = shutil.which('componentize-py') |
| 118 | + if componentize_py is None: |
| 119 | + raise RuntimeError("Could not find componentize-py executable.") |
| 120 | + with tempfile.NamedTemporaryFile('w') as f: |
| 121 | + output_wasm = str(f.name + '.wasm') |
| 122 | + subprocess.run([ |
| 123 | + componentize_py, '-d', str(wit_path), '-w', testcase.world_name, |
| 124 | + 'componentize', '--stub-wasi', testcase.app_name, |
| 125 | + '-o', output_wasm |
| 126 | + ], check=True, cwd=testcase.guest_code_dir) |
| 127 | + # Once we've done that now generate the python bindings. |
| 128 | + testsuite_name = testcase.testsuite_name |
| 129 | + with open(output_wasm, 'rb') as out: |
| 130 | + # Mapping of filename -> content_bytes |
| 131 | + results = generate(testsuite_name, out.read()) |
| 132 | + for filename, contents in results.items(): |
| 133 | + path = BINDGEN_DIR / testsuite_name / filename |
| 134 | + path.parent.mkdir(parents=True, exist_ok=True) |
| 135 | + path.write_bytes(contents) |
| 136 | + # Return an instantiated module for the caller to test. |
| 137 | + pkg = importlib.import_module(f'.generated.{testsuite_name}', |
| 138 | + package=__package__) |
| 139 | + store = wasmtime.Store() |
| 140 | + root = pkg.Root(store) |
| 141 | + return store, root |
| 142 | + |
| 143 | + |
| 144 | +@fixture |
| 145 | +def bindgen_testcase(): |
| 146 | + return generate_bindings |
0 commit comments