Skip to content

Commit

Permalink
Test and lint examples (#117)
Browse files Browse the repository at this point in the history
* Add test runs for examples

* Add mypy linting
  • Loading branch information
benbrandt authored Oct 31, 2024
1 parent c5deb00 commit 707b508
Show file tree
Hide file tree
Showing 10 changed files with 159 additions and 33 deletions.
42 changes: 42 additions & 0 deletions .github/workflows/lint_examples.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
#!/bin/bash

set -euo pipefail

export COMPONENTIZE_PY_TEST_COUNT=0
export COMPONENTIZE_PY_TEST_SEED=bc6ad1950594f1fe477144ef5b3669dd5962e49de4f3b666e5cbf9072507749a
export WASMTIME_BACKTRACE_DETAILS=1

cargo build --release

# CLI
(cd examples/cli \
&& rm -rf command || true \
&& ../../target/release/componentize-py -d ../../wit -w wasi:cli/[email protected] bindings . \
&& mypy --strict .)

# HTTP
# poll_loop.py has many errors that might not be worth adjusting at the moment, so ignore for now
(cd examples/http \
&& rm -rf proxy || true \
&& ../../target/release/componentize-py -d ../../wit -w wasi:http/[email protected] bindings . \
&& mypy --strict --ignore-missing-imports -m app -p proxy)

# # Matrix Math
(cd examples/matrix-math \
&& rm -rf matrix_math || true \
&& curl -OL https://github.com/dicej/wasi-wheels/releases/download/v0.0.1/numpy-wasi.tar.gz \
&& tar xf numpy-wasi.tar.gz \
&& ../../target/release/componentize-py -d ../../wit -w matrix-math bindings . \
&& mypy --strict --follow-imports silent -m app -p matrix_math)

# Sandbox
(cd examples/sandbox \
&& rm -rf sandbox || true \
&& ../../target/release/componentize-py -d sandbox.wit bindings . \
&& mypy --strict -m guest -p sandbox)

# TCP
(cd examples/tcp \
&& rm -rf command || true \
&& ../../target/release/componentize-py -d ../../wit -w wasi:cli/[email protected] bindings . \
&& mypy --strict .)
14 changes: 14 additions & 0 deletions .github/workflows/test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -82,3 +82,17 @@ jobs:
- name: Test
shell: bash
run: COMPONENTIZE_PY_TEST_COUNT=20 PROPTEST_MAX_SHRINK_ITERS=0 cargo test --release

- uses: taiki-e/install-action@v2
with:
tool: wasmtime-cli
- uses: actions/setup-python@v5
with:
python-version: "3.12"
- run: pip install wasmtime mypy
- name: Test examples
shell: bash
run: bash .github/workflows/test_examples.sh
- name: Lint examples
shell: bash
run: bash .github/workflows/lint_examples.sh
37 changes: 37 additions & 0 deletions .github/workflows/test_examples.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
#!/bin/bash

set -euo pipefail

export COMPONENTIZE_PY_TEST_COUNT=0
export COMPONENTIZE_PY_TEST_SEED=bc6ad1950594f1fe477144ef5b3669dd5962e49de4f3b666e5cbf9072507749a
export WASMTIME_BACKTRACE_DETAILS=1

cargo build --release

# CLI
(cd examples/cli \
&& ../../target/release/componentize-py -d ../../wit -w wasi:cli/[email protected] componentize app -o cli.wasm \
&& wasmtime run cli.wasm)

# HTTP
# Just compiling for now
(cd examples/http \
&& ../../target/release/componentize-py -d ../../wit -w wasi:http/[email protected] componentize app -o http.wasm)

# Matrix Math
(cd examples/matrix-math \
&& curl -OL https://github.com/dicej/wasi-wheels/releases/download/v0.0.1/numpy-wasi.tar.gz \
&& tar xf numpy-wasi.tar.gz \
&& ../../target/release/componentize-py -d ../../wit -w matrix-math componentize app -o matrix-math.wasm \
&& wasmtime run matrix-math.wasm '[[1, 2], [4, 5], [6, 7]]' '[[1, 2, 3], [4, 5, 6]]')

# Sandbox
(cd examples/sandbox \
&& ../../target/release/componentize-py -d sandbox.wit componentize --stub-wasi guest -o sandbox.wasm \
&& python -m wasmtime.bindgen sandbox.wasm --out-dir sandbox \
&& python host.py "2 + 2")

# TCP
# Just compiling for now
(cd examples/tcp \
&& ../../target/release/componentize-py -d ../../wit -w wasi:cli/[email protected] componentize app -o tcp.wasm)
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
target
__pycache__
.mypy_cache
.venv
bacon.toml
dist
Expand All @@ -9,7 +10,11 @@ examples/matrix-math/matrix_math
examples/matrix-math/wasmtime-py
examples/http/.spin
examples/http/http.wasm
examples/http/proxy
examples/http/poll_loop.py
examples/tcp/tcp.wasm
examples/tcp/command
examples/cli/cli.wasm
examples/cli/command
examples/sandbox/sandbox
examples/sandbox/sandbox.wasm
3 changes: 2 additions & 1 deletion examples/cli/app.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from command import exports


class Run(exports.Run):
def run(self):
def run(self) -> None:
print("Hello, world!")
38 changes: 29 additions & 9 deletions examples/http/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,27 +13,39 @@
from proxy.types import Ok
from proxy.imports import types
from proxy.imports.types import (
Method_Get, Method_Post, Scheme, Scheme_Http, Scheme_Https, Scheme_Other, IncomingRequest, ResponseOutparam,
OutgoingResponse, Fields, OutgoingBody, OutgoingRequest
Method_Get,
Method_Post,
Scheme,
Scheme_Http,
Scheme_Https,
Scheme_Other,
IncomingRequest,
ResponseOutparam,
OutgoingResponse,
Fields,
OutgoingBody,
OutgoingRequest,
)
from poll_loop import Stream, Sink, PollLoop
from typing import Tuple
from urllib import parse


class IncomingHandler(exports.IncomingHandler):
"""Implements the `export`ed portion of the `wasi-http` `proxy` world."""

def handle(self, request: IncomingRequest, response_out: ResponseOutparam):
"""Handle the specified `request`, sending the response to `response_out`.
"""
def handle(self, request: IncomingRequest, response_out: ResponseOutparam) -> None:
"""Handle the specified `request`, sending the response to `response_out`."""
# Dispatch the request using `asyncio`, backed by a custom event loop
# based on WASI's `poll_oneoff` function.
loop = PollLoop()
asyncio.set_event_loop(loop)
loop.run_until_complete(handle_async(request, response_out))

async def handle_async(request: IncomingRequest, response_out: ResponseOutparam):

async def handle_async(
request: IncomingRequest, response_out: ResponseOutparam
) -> None:
"""Handle the specified `request`, sending the response to `response_out`."""

method = request.method()
Expand All @@ -46,7 +58,10 @@ async def handle_async(request: IncomingRequest, response_out: ResponseOutparam)
# buffering the response bodies), and stream the results back to the
# client as they become available.

urls = map(lambda pair: str(pair[1], "utf-8"), filter(lambda pair: pair[0] == "url", headers))
urls = map(
lambda pair: str(pair[1], "utf-8"),
filter(lambda pair: pair[0] == "url", headers),
)

response = OutgoingResponse(Fields.from_list([("content-type", b"text/plain")]))

Expand All @@ -64,7 +79,11 @@ async def handle_async(request: IncomingRequest, response_out: ResponseOutparam)
elif isinstance(method, Method_Post) and path == "/echo":
# Echo the request body back to the client without buffering.

response = OutgoingResponse(Fields.from_list(list(filter(lambda pair: pair[0] == "content-type", headers))))
response = OutgoingResponse(
Fields.from_list(
list(filter(lambda pair: pair[0] == "content-type", headers))
)
)

response_body = response.body()

Expand All @@ -87,6 +106,7 @@ async def handle_async(request: IncomingRequest, response_out: ResponseOutparam)
ResponseOutparam.set(response_out, Ok(response))
OutgoingBody.finish(body, None)


async def sha256(url: str) -> Tuple[str, str]:
"""Download the contents of the specified URL, computing the SHA-256
incrementally as the response body arrives.
Expand Down
9 changes: 5 additions & 4 deletions examples/matrix-math/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,17 +7,18 @@
from matrix_math import exports
from matrix_math.types import Err


class MatrixMath(matrix_math.MatrixMath):
def multiply(self, a: list[list[float]], b: list[list[float]]) -> list[list[float]]:
print(f"matrix_multiply received arguments {a} and {b}")
return numpy.matmul(a, b).tolist()
return numpy.matmul(a, b).tolist() # type: ignore


class Run(exports.Run):
def run(self):
def run(self) -> None:
args = sys.argv[1:]
if len(args) != 2:
print(f"usage: matrix-math <matrix> <matrix>", file=sys.stderr)
print("usage: matrix-math <matrix> <matrix>", file=sys.stderr)
exit(-1)

print(MatrixMath().multiply(eval(args[0]), eval(args[1])))

16 changes: 9 additions & 7 deletions examples/sandbox/guest.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,22 +2,24 @@
from sandbox.types import Err
import json

def handle(e: Exception):

def handle(e: Exception) -> Err[str]:
message = str(e)
if message == '':
raise Err(f"{type(e).__name__}")
if message == "":
return Err(f"{type(e).__name__}")
else:
raise Err(f"{type(e).__name__}: {message}")
return Err(f"{type(e).__name__}: {message}")


class Sandbox(sandbox.Sandbox):
def eval(self, expression: str) -> str:
try:
return json.dumps(eval(expression))
except Exception as e:
handle(e)
raise handle(e)

def exec(self, statements: str):
def exec(self, statements: str) -> None:
try:
exec(statements)
except Exception as e:
handle(e)
raise handle(e)
17 changes: 10 additions & 7 deletions examples/tcp/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,24 +5,28 @@
from command import exports
from typing import Tuple


class Run(exports.Run):
def run(self):
def run(self) -> None:
args = sys.argv[1:]
if len(args) != 1:
print(f"usage: tcp <address>:<port>", file=sys.stderr)
print("usage: tcp <address>:<port>", file=sys.stderr)
exit(-1)

address, port = parse_address_and_port(args[0])
asyncio.run(send_and_receive(address, port))


IPAddress = IPv4Address | IPv6Address



def parse_address_and_port(address_and_port: str) -> Tuple[IPAddress, int]:
ip, separator, port = address_and_port.rpartition(':')
ip, separator, port = address_and_port.rpartition(":")
assert separator
return (ipaddress.ip_address(ip.strip("[]")), int(port))

async def send_and_receive(address: IPAddress, port: int):


async def send_and_receive(address: IPAddress, port: int) -> None:
rx, tx = await asyncio.open_connection(str(address), port)

tx.write(b"hello, world!")
Expand All @@ -33,4 +37,3 @@ async def send_and_receive(address: IPAddress, port: int):

tx.close()
await tx.wait_closed()

11 changes: 6 additions & 5 deletions src/summary.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1348,14 +1348,14 @@ class {camel}(Flag):
if stub_runtime_calls {
format!(
"
def {snake}({params}):
def {snake}({params}){return_type}:
{docs}{NOT_IMPLEMENTED}
"
)
} else {
format!(
"
def {snake}({params}):
def {snake}({params}){return_type}:
{docs}tmp = componentize_py_runtime.call_import({index}, [{args}], {result_count})[0]
(_, func, args, _) = tmp.finalizer.detach()
self.handle = tmp.handle
Expand Down Expand Up @@ -1403,21 +1403,21 @@ class {camel}(Flag):
let docs =
format!(r#""""{newline}{indent}{doc}{newline}{indent}"""{newline}{indent}"#);
let enter = r#"
def __enter__(self):
def __enter__(self) -> Self:
"""Returns self"""
return self
"#;
if stub_runtime_calls {
format!(
"{enter}
def __exit__(self, *args):
def __exit__(self, exc_type: type[BaseException] | None, exc_value: BaseException | None, traceback: TracebackType | None) -> bool | None:
{docs}{NOT_IMPLEMENTED}
"
)
} else {
format!(
"{enter}
def __exit__(self, *args):
def __exit__(self, exc_type: type[BaseException] | None, exc_value: BaseException | None, traceback: TracebackType | None) -> bool | None:
{docs}(_, func, args, _) = self.finalizer.detach()
self.handle = None
func(args[0], args[1])
Expand Down Expand Up @@ -1746,6 +1746,7 @@ def {snake}({params}){return_type}:

let python_imports =
"from typing import TypeVar, Generic, Union, Optional, Protocol, Tuple, List, Any, Self
from types import TracebackType
from enum import Flag, Enum, auto
from dataclasses import dataclass
from abc import abstractmethod
Expand Down

0 comments on commit 707b508

Please sign in to comment.