diff --git a/.github/workflows/lint_examples.sh b/.github/workflows/lint_examples.sh deleted file mode 100755 index 7b85c40..0000000 --- a/.github/workflows/lint_examples.sh +++ /dev/null @@ -1,42 +0,0 @@ -#!/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/command@0.2.0 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/proxy@0.2.0 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/command@0.2.0 bindings . \ - && mypy --strict .) diff --git a/.github/workflows/test.yaml b/.github/workflows/test.yaml index 1cdb4b0..8cba0b0 100644 --- a/.github/workflows/test.yaml +++ b/.github/workflows/test.yaml @@ -79,20 +79,14 @@ jobs: shell: bash run: bash .github/workflows/lint.sh - - 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 + + - name: Test shell: bash - run: bash .github/workflows/lint_examples.sh + run: COMPONENTIZE_PY_TEST_COUNT=20 PROPTEST_MAX_SHRINK_ITERS=0 cargo test --release diff --git a/.github/workflows/test_examples.sh b/.github/workflows/test_examples.sh deleted file mode 100755 index dddd905..0000000 --- a/.github/workflows/test_examples.sh +++ /dev/null @@ -1,37 +0,0 @@ -#!/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/command@0.2.0 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/proxy@0.2.0 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/command@0.2.0 componentize app -o tcp.wasm) diff --git a/Cargo.lock b/Cargo.lock index 9f450e0..2b2f30d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -126,6 +126,22 @@ version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7d5a26814d8dcb93b0e5a0ff3c6d80a8843bafb21b39e8e18a6f05471870e110" +[[package]] +name = "assert_cmd" +version = "2.0.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc1835b7f27878de8525dc71410b5a31cdcc5f230aed5ba5df968e09c201b23d" +dependencies = [ + "anstyle", + "bstr", + "doc-comment", + "libc", + "predicates", + "predicates-core", + "predicates-tree", + "wait-timeout", +] + [[package]] name = "async-trait" version = "0.1.83" @@ -212,6 +228,17 @@ dependencies = [ "generic-array", ] +[[package]] +name = "bstr" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40723b8fb387abc38f4f4a37c09073622e41dd12327033091ef8950659e6dc0c" +dependencies = [ + "memchr", + "regex-automata", + "serde", +] + [[package]] name = "bumpalo" version = "3.16.0" @@ -394,6 +421,7 @@ name = "componentize-py" version = "0.15.2" dependencies = [ "anyhow", + "assert_cmd", "async-trait", "bincode", "bytes", @@ -401,12 +429,14 @@ dependencies = [ "clap", "component-init", "componentize-py-shared", + "fs_extra", "futures", "heck 0.5.0", "hex", "im-rc", "indexmap", "once_cell", + "predicates", "pretty_env_logger", "proptest", "pyo3", @@ -643,6 +673,12 @@ dependencies = [ "uuid", ] +[[package]] +name = "difflib" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6184e33543162437515c2e2b48714794e37845ec9851711914eec9d308f6ebe8" + [[package]] name = "digest" version = "0.10.7" @@ -694,6 +730,12 @@ dependencies = [ "winapi", ] +[[package]] +name = "doc-comment" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10" + [[package]] name = "either" version = "1.13.0" @@ -785,6 +827,15 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "float-cmp" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "98de4bbd547a563b716d8dfa9aad1cb19bfab00f4fa09a6a4ed21dbcf44ce9c4" +dependencies = [ + "num-traits", +] + [[package]] name = "fnv" version = "1.0.7" @@ -817,6 +868,12 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "fs_extra" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" + [[package]] name = "futures" version = "0.3.31" @@ -1290,6 +1347,12 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "normalize-line-endings" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61807f77802ff30975e01f4f071c8ba10c022052f98b3294119f3e615d13e5be" + [[package]] name = "num-bigint" version = "0.4.6" @@ -1394,6 +1457,36 @@ dependencies = [ "zerocopy", ] +[[package]] +name = "predicates" +version = "3.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e9086cc7640c29a356d1a29fd134380bee9d8f79a17410aa76e7ad295f42c97" +dependencies = [ + "anstyle", + "difflib", + "float-cmp", + "normalize-line-endings", + "predicates-core", + "regex", +] + +[[package]] +name = "predicates-core" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae8177bee8e75d6846599c6b9ff679ed51e882816914eec639944d7c9aa11931" + +[[package]] +name = "predicates-tree" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41b740d195ed3166cd147c8047ec98db0e22ec019eb8eeb76d343b795304fb13" +dependencies = [ + "predicates-core", + "termtree", +] + [[package]] name = "pretty_env_logger" version = "0.5.0" @@ -1919,6 +2012,12 @@ dependencies = [ "winapi-util", ] +[[package]] +name = "termtree" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3369f5ac52d5eb6ab48c6b4ffdc8efbcad6b89c765749064ba298f2c68a16a76" + [[package]] name = "test-generator" version = "0.1.0" diff --git a/Cargo.toml b/Cargo.toml index 8f1e12c..a0ed2f3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -49,8 +49,11 @@ toml = "0.8.19" semver = "1.0.23" [dev-dependencies] -proptest = "1.5.0" +assert_cmd = "2.0.16" +fs_extra = "1.3.0" hex = "0.4.3" +predicates = "3.1.2" +proptest = "1.5.0" tempfile = "3.13.0" [build-dependencies] diff --git a/bundled/poll_loop.py b/bundled/poll_loop.py index 4a08f4a..d6d8911 100644 --- a/bundled/poll_loop.py +++ b/bundled/poll_loop.py @@ -13,7 +13,12 @@ from proxy.types import Ok, Err from proxy.imports import types, streams, poll, outgoing_handler -from proxy.imports.types import IncomingBody, OutgoingBody, OutgoingRequest, IncomingResponse +from proxy.imports.types import ( + IncomingBody, + OutgoingBody, + OutgoingRequest, + IncomingResponse, +) from proxy.imports.streams import StreamError_Closed, InputStream from proxy.imports.poll import Pollable from typing import Optional, cast @@ -21,9 +26,10 @@ # Maximum number of bytes to read at a time READ_SIZE: int = 16 * 1024 + async def send(request: OutgoingRequest) -> IncomingResponse: """Send the specified request and wait asynchronously for the response.""" - + future = outgoing_handler.handle(request, None) while True: @@ -39,8 +45,10 @@ async def send(request: OutgoingRequest) -> IncomingResponse: else: raise response + class Stream: """Reader abstraction over `wasi:http/types#incoming-body`.""" + def __init__(self, body: IncomingBody): self.body: Optional[IncomingBody] = body self.stream: Optional[InputStream] = body.stream() @@ -57,13 +65,16 @@ async def next(self) -> Optional[bytes]: else: buffer = self.stream.read(READ_SIZE) if len(buffer) == 0: - await register(cast(PollLoop, asyncio.get_event_loop()), self.stream.subscribe()) + await register( + cast(PollLoop, asyncio.get_event_loop()), + self.stream.subscribe(), + ) else: return buffer except Err as e: if isinstance(e.value, StreamError_Closed): if self.stream is not None: - self.stream.__exit__() + self.stream.__exit__(None, None, None) self.stream = None if self.body is not None: IncomingBody.finish(self.body) @@ -71,8 +82,10 @@ async def next(self) -> Optional[bytes]: else: raise e + class Sink: """Writer abstraction over `wasi:http/types#outgoing-body`.""" + def __init__(self, body: OutgoingBody): self.body = body self.stream = body.write() @@ -87,7 +100,9 @@ async def send(self, chunk: bytes): while True: count = self.stream.check_write() if count == 0: - await register(cast(PollLoop, asyncio.get_event_loop()), self.stream.subscribe()) + await register( + cast(PollLoop, asyncio.get_event_loop()), self.stream.subscribe() + ) elif offset == len(chunk): if flushing: return @@ -96,20 +111,21 @@ async def send(self, chunk: bytes): flushing = True else: count = min(count, len(chunk) - offset) - self.stream.write(chunk[offset:offset+count]) + self.stream.write(chunk[offset : offset + count]) offset += count def close(self): """Close the stream, indicating no further data will be written.""" - self.stream.__exit__() + self.stream.__exit__(None, None, None) self.stream = None OutgoingBody.finish(self.body, None) self.body = None - + + class PollLoop(asyncio.AbstractEventLoop): """Custom `asyncio` event loop backed by `wasi:io/poll#poll`.""" - + def __init__(self): self.wakers = [] self.running = False @@ -130,18 +146,18 @@ def run_until_complete(self, future): for handle in handles: if not handle._cancelled: handle._run() - + if self.wakers: [pollables, wakers] = list(map(list, zip(*self.wakers))) - + new_wakers = [] ready = [False] * len(pollables) for index in poll.poll(pollables): ready[index] = True - + for (ready, pollable), waker in zip(zip(ready, pollables), wakers): if ready: - pollable.__exit__() + pollable.__exit__(None, None, None) waker.set_result(None) else: new_wakers.append((pollable, waker)) @@ -150,7 +166,7 @@ def run_until_complete(self, future): if self.exception is not None: raise self.exception - + return future.result() def is_running(self): @@ -169,7 +185,7 @@ def shutdown_asyncgens(self): pass def call_exception_handler(self, context): - self.exception = context.get('exception', None) + self.exception = context.get("exception", None) def call_soon(self, callback, *args, context=None): handle = asyncio.Handle(callback, args, self, context) @@ -211,72 +227,119 @@ def run_in_executor(self, executor, func, *args): def set_default_executor(self, executor): raise NotImplementedError - async def getaddrinfo(self, host, port, *, - family=0, type=0, proto=0, flags=0): + async def getaddrinfo(self, host, port, *, family=0, type=0, proto=0, flags=0): raise NotImplementedError async def getnameinfo(self, sockaddr, flags=0): raise NotImplementedError async def create_connection( - self, protocol_factory, host=None, port=None, - *, ssl=None, family=0, proto=0, - flags=0, sock=None, local_addr=None, - server_hostname=None, - ssl_handshake_timeout=None, - ssl_shutdown_timeout=None, - happy_eyeballs_delay=None, interleave=None): + self, + protocol_factory, + host=None, + port=None, + *, + ssl=None, + family=0, + proto=0, + flags=0, + sock=None, + local_addr=None, + server_hostname=None, + ssl_handshake_timeout=None, + ssl_shutdown_timeout=None, + happy_eyeballs_delay=None, + interleave=None, + ): raise NotImplementedError async def create_server( - self, protocol_factory, host=None, port=None, - *, family=socket.AF_UNSPEC, - flags=socket.AI_PASSIVE, sock=None, backlog=100, - ssl=None, reuse_address=None, reuse_port=None, - ssl_handshake_timeout=None, - ssl_shutdown_timeout=None, - start_serving=True): - raise NotImplementedError - - async def sendfile(self, transport, file, offset=0, count=None, - *, fallback=True): - raise NotImplementedError - - async def start_tls(self, transport, protocol, sslcontext, *, - server_side=False, - server_hostname=None, - ssl_handshake_timeout=None, - ssl_shutdown_timeout=None): + self, + protocol_factory, + host=None, + port=None, + *, + family=socket.AF_UNSPEC, + flags=socket.AI_PASSIVE, + sock=None, + backlog=100, + ssl=None, + reuse_address=None, + reuse_port=None, + ssl_handshake_timeout=None, + ssl_shutdown_timeout=None, + start_serving=True, + ): + raise NotImplementedError + + async def sendfile(self, transport, file, offset=0, count=None, *, fallback=True): + raise NotImplementedError + + async def start_tls( + self, + transport, + protocol, + sslcontext, + *, + server_side=False, + server_hostname=None, + ssl_handshake_timeout=None, + ssl_shutdown_timeout=None, + ): raise NotImplementedError async def create_unix_connection( - self, protocol_factory, path=None, *, - ssl=None, sock=None, - server_hostname=None, - ssl_handshake_timeout=None, - ssl_shutdown_timeout=None): + self, + protocol_factory, + path=None, + *, + ssl=None, + sock=None, + server_hostname=None, + ssl_handshake_timeout=None, + ssl_shutdown_timeout=None, + ): raise NotImplementedError async def create_unix_server( - self, protocol_factory, path=None, *, - sock=None, backlog=100, ssl=None, - ssl_handshake_timeout=None, - ssl_shutdown_timeout=None, - start_serving=True): + self, + protocol_factory, + path=None, + *, + sock=None, + backlog=100, + ssl=None, + ssl_handshake_timeout=None, + ssl_shutdown_timeout=None, + start_serving=True, + ): raise NotImplementedError async def connect_accepted_socket( - self, protocol_factory, sock, - *, ssl=None, - ssl_handshake_timeout=None, - ssl_shutdown_timeout=None): - raise NotImplementedError - - async def create_datagram_endpoint(self, protocol_factory, - local_addr=None, remote_addr=None, *, - family=0, proto=0, flags=0, - reuse_address=None, reuse_port=None, - allow_broadcast=None, sock=None): + self, + protocol_factory, + sock, + *, + ssl=None, + ssl_handshake_timeout=None, + ssl_shutdown_timeout=None, + ): + raise NotImplementedError + + async def create_datagram_endpoint( + self, + protocol_factory, + local_addr=None, + remote_addr=None, + *, + family=0, + proto=0, + flags=0, + reuse_address=None, + reuse_port=None, + allow_broadcast=None, + sock=None, + ): raise NotImplementedError async def connect_read_pipe(self, protocol_factory, pipe): @@ -285,18 +348,27 @@ async def connect_read_pipe(self, protocol_factory, pipe): async def connect_write_pipe(self, protocol_factory, pipe): raise NotImplementedError - async def subprocess_shell(self, protocol_factory, cmd, *, - stdin=subprocess.PIPE, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - **kwargs): + async def subprocess_shell( + self, + protocol_factory, + cmd, + *, + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + **kwargs, + ): raise NotImplementedError - async def subprocess_exec(self, protocol_factory, *args, - stdin=subprocess.PIPE, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - **kwargs): + async def subprocess_exec( + self, + protocol_factory, + *args, + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + **kwargs, + ): raise NotImplementedError def add_reader(self, fd, callback, *args): @@ -335,8 +407,7 @@ async def sock_connect(self, sock, address): async def sock_accept(self, sock): raise NotImplementedError - async def sock_sendfile(self, sock, file, offset=0, count=None, - *, fallback=None): + async def sock_sendfile(self, sock, file, offset=0, count=None, *, fallback=None): raise NotImplementedError def add_signal_handler(self, sig, callback, *args): @@ -363,6 +434,7 @@ def default_exception_handler(self, context): def set_debug(self, enabled): raise NotImplementedError + async def register(loop: PollLoop, pollable: Pollable): waker = loop.create_future() loop.wakers.append((pollable, waker)) diff --git a/tests/bindings.rs b/tests/bindings.rs new file mode 100644 index 0000000..01f591f --- /dev/null +++ b/tests/bindings.rs @@ -0,0 +1,183 @@ +use std::{ + ffi::OsStr, + path::{Path, PathBuf}, +}; + +use assert_cmd::{assert::Assert, Command}; +use fs_extra::dir::CopyOptions; +use predicates::{prelude::predicate, Predicate}; + +#[test] +fn lint_cli_bindings() -> anyhow::Result<()> { + let dir = tempfile::tempdir()?; + fs_extra::copy_items( + &["./examples/cli", "./wit"], + dir.path(), + &CopyOptions::new(), + )?; + let path = dir.path().join("cli"); + + generate_bindings(&path, "wasi:cli/command@0.2.0")?; + + assert!(predicate::path::is_dir().eval(&path.join("command"))); + + mypy_check(&path, ["--strict", "."]); + + Ok(()) +} + +#[test] +fn lint_http_bindings() -> anyhow::Result<()> { + let dir = tempfile::tempdir()?; + fs_extra::copy_items( + &["./examples/http", "./wit"], + dir.path(), + &CopyOptions::new(), + )?; + let path = dir.path().join("http"); + + generate_bindings(&path, "wasi:http/proxy@0.2.0")?; + + assert!(predicate::path::is_dir().eval(&path.join("proxy"))); + + mypy_check( + &path, + [ + "--strict", + // poll_loop.py has many errors that might not be worth adjusting at the moment, so ignore for now + "--ignore-missing-imports", + "-m", + "app", + "-p", + "proxy", + ], + ); + + Ok(()) +} + +#[test] +fn lint_matrix_math_bindings() -> anyhow::Result<()> { + let dir = tempfile::tempdir()?; + fs_extra::copy_items( + &["./examples/matrix-math", "./wit"], + dir.path(), + &CopyOptions::new(), + )?; + let path = dir.path().join("matrix-math"); + + install_numpy(&path); + + generate_bindings(&path, "matrix-math")?; + + assert!(predicate::path::is_dir().eval(&path.join("matrix_math"))); + + mypy_check( + &path, + [ + "--strict", + // numpy doesn't pass + "--follow-imports", + "silent", + "-m", + "app", + "-p", + "matrix_math", + ], + ); + + Ok(()) +} + +#[test] +fn lint_sandbox_bindings() -> anyhow::Result<()> { + let dir = tempfile::tempdir()?; + fs_extra::copy_items(&["./examples/sandbox"], dir.path(), &CopyOptions::new())?; + let path = dir.path().join("sandbox"); + + Command::cargo_bin("componentize-py")? + .current_dir(&path) + .args(["-d", "sandbox.wit", "bindings", "."]) + .assert() + .success(); + + assert!(predicate::path::is_dir().eval(&path.join("sandbox"))); + + mypy_check(&path, ["--strict", "-m", "guest", "-p", "sandbox"]); + + Ok(()) +} + +#[test] +fn lint_tcp_bindings() -> anyhow::Result<()> { + let dir = tempfile::tempdir()?; + fs_extra::copy_items( + &["./examples/tcp", "./wit"], + dir.path(), + &CopyOptions::new(), + )?; + let path = dir.path().join("tcp"); + + generate_bindings(&path, "wasi:cli/command@0.2.0")?; + + assert!(predicate::path::is_dir().eval(&path.join("command"))); + + mypy_check(&path, ["--strict", "."]); + + Ok(()) +} + +fn generate_bindings(path: &Path, world: &str) -> Result { + Ok(Command::cargo_bin("componentize-py")? + .current_dir(path) + .args(["-d", "../wit", "-w", world, "bindings", "."]) + .assert() + .success()) +} + +fn mypy_check(path: &Path, args: I) -> Assert +where + I: IntoIterator, + S: AsRef, +{ + Command::new("python3") + .current_dir(path) + .args(["-m", "venv", ".venv"]) + .assert() + .success(); + + Command::new(venv_path(path).join("pip")) + .current_dir(path) + .args(["install", "mypy"]) + .assert() + .success(); + + Command::new(venv_path(path).join("mypy")) + .current_dir(path) + .args(args) + .assert() + .success() + .stdout(predicate::str::is_match("Success: no issues found in \\d+ source files").unwrap()) +} + +fn venv_path(path: &Path) -> PathBuf { + path.join(".venv") + .join(if cfg!(windows) { "Scripts" } else { "bin" }) +} + +fn install_numpy(path: &Path) { + Command::new("curl") + .current_dir(path) + .args([ + "-OL", + "https://github.com/dicej/wasi-wheels/releases/download/v0.0.1/numpy-wasi.tar.gz", + ]) + .assert() + .success(); + + Command::new("tar") + .current_dir(path) + .args(["xf", "numpy-wasi.tar.gz"]) + .assert() + .success(); +} diff --git a/tests/componentize.rs b/tests/componentize.rs new file mode 100644 index 0000000..8f854c9 --- /dev/null +++ b/tests/componentize.rs @@ -0,0 +1,310 @@ +use std::{ + io::Write, + path::{Path, PathBuf}, + process::Stdio, +}; + +use assert_cmd::Command; +use fs_extra::dir::CopyOptions; +use predicates::prelude::{predicate, PredicateBooleanExt}; + +#[test] +fn cli_example() -> anyhow::Result<()> { + let dir = tempfile::tempdir()?; + fs_extra::copy_items( + &["./examples/cli", "./wit"], + dir.path(), + &CopyOptions::new(), + )?; + let path = dir.path().join("cli"); + + Command::cargo_bin("componentize-py")? + .current_dir(&path) + .args([ + "-d", + "../wit", + "-w", + "wasi:cli/command@0.2.0", + "componentize", + "app", + "-o", + "cli.wasm", + ]) + .assert() + .success() + .stdout("Component built successfully\n"); + + Command::new("wasmtime") + .current_dir(&path) + .args(["run", "cli.wasm"]) + .assert() + .success() + .stdout("Hello, world!\n"); + + Ok(()) +} + +#[test] +fn http_example() -> anyhow::Result<()> { + let dir = tempfile::tempdir()?; + fs_extra::copy_items( + &["./examples/http", "./wit"], + dir.path(), + &CopyOptions::new(), + )?; + let path = dir.path().join("http"); + + Command::cargo_bin("componentize-py")? + .current_dir(&path) + .args([ + "-d", + "../wit", + "-w", + "wasi:http/proxy@0.2.0", + "componentize", + "app", + "-o", + "http.wasm", + ]) + .assert() + .success() + .stdout("Component built successfully\n"); + + let mut handle = std::process::Command::new("wasmtime") + .current_dir(&path) + .args(["serve", "--wasi", "common", "http.wasm"]) + .spawn()?; + + let content = "’Twas brillig, and the slithy toves + Did gyre and gimble in the wabe: +All mimsy were the borogoves, + And the mome raths outgrabe. +"; + + Command::new("curl") + .current_dir(&path) + .args([ + "-i", + "-H", + "content-type: text/plain", + "--retry-connrefused", + "--retry", + "5", + "--data-binary", + "@-", + "http://127.0.0.1:8080/echo", + ]) + .write_stdin(content) + .assert() + .success() + .stdout(predicate::str::ends_with(content)); + + Command::new("curl") + .current_dir(&path) + .args([ + "-i", + "-H", + "url: https://webassembly.github.io/spec/core/", + "-H", + "url: https://www.w3.org/groups/wg/wasm/", + "-H", + "url: https://bytecodealliance.org/", + "--retry-connrefused", + "--retry", + "5", + "http://127.0.0.1:8080/hash-all", + ]) + .assert() + .success() + .stdout( + predicate::str::contains("https://webassembly.github.io/spec/core/:").and( + predicate::str::contains("https://bytecodealliance.org/:").and( + predicate::str::contains("https://www.w3.org/groups/wg/wasm/:"), + ), + ), + ); + + handle.kill()?; + + Ok(()) +} + +#[test] +fn matrix_math_example() -> anyhow::Result<()> { + let dir = tempfile::tempdir()?; + fs_extra::copy_items( + &["./examples/matrix-math", "./wit"], + dir.path(), + &CopyOptions::new(), + )?; + let path = dir.path().join("matrix-math"); + + install_numpy(&path); + + Command::cargo_bin("componentize-py")? + .current_dir(&path) + .args([ + "-d", + "../wit", + "-w", + "matrix-math", + "componentize", + "app", + "-o", + "matrix-math.wasm", + ]) + .assert() + .success() + .stdout("Component built successfully\n"); + + Command::new("wasmtime") + .current_dir(&path) + .args([ + "run", + "matrix-math.wasm", + "[[1, 2], [4, 5], [6, 7]]", + "[[1, 2, 3], [4, 5, 6]]", + ]) + .assert() + .success() + .stdout("matrix_multiply received arguments [[1, 2], [4, 5], [6, 7]] and [[1, 2, 3], [4, 5, 6]]\n[[9, 12, 15], [24, 33, 42], [34, 47, 60]]\n"); + + Ok(()) +} + +#[test] +fn sandbox_example() -> anyhow::Result<()> { + let dir = tempfile::tempdir()?; + fs_extra::copy_items(&["./examples/sandbox"], dir.path(), &CopyOptions::new())?; + let path = dir.path().join("sandbox"); + + Command::cargo_bin("componentize-py")? + .current_dir(&path) + .args([ + "-d", + "sandbox.wit", + "componentize", + "--stub-wasi", + "guest", + "-o", + "sandbox.wasm", + ]) + .assert() + .success() + .stdout("Component built successfully\n"); + + Command::new("python3") + .current_dir(&path) + .args(["-m", "venv", ".venv"]) + .assert() + .success(); + + Command::new(venv_path(&path).join("pip")) + .current_dir(&path) + .args(["install", "wasmtime"]) + .assert() + .success(); + + Command::new(venv_path(&path).join("python")) + .current_dir(&path) + .args([ + "-m", + "wasmtime.bindgen", + "sandbox.wasm", + "--out-dir", + "sandbox", + ]) + .assert() + .success(); + + Command::new(venv_path(&path).join("python")) + .current_dir(&path) + .args(["host.py", "2 + 2"]) + .assert() + .success() + .stdout(predicate::str::contains("result: 4")); + + Ok(()) +} + +#[cfg(unix)] +#[test] +fn tcp_example() -> anyhow::Result<()> { + let dir = tempfile::tempdir()?; + fs_extra::copy_items( + &["./examples/tcp", "./wit"], + dir.path(), + &CopyOptions::new(), + )?; + let path = dir.path().join("tcp"); + + Command::cargo_bin("componentize-py")? + .current_dir(&path) + .args([ + "-d", + "../wit", + "-w", + "wasi:cli/command@0.2.0", + "componentize", + "app", + "-o", + "tcp.wasm", + ]) + .assert() + .success() + .stdout("Component built successfully\n"); + + let mut nc_handle = std::process::Command::new("nc") + .current_dir(&path) + .args(["-l", "127.0.0.1", "3456"]) + .stdin(Stdio::piped()) + .spawn()?; + + let tcp_handle = std::process::Command::new("wasmtime") + .current_dir(&path) + .args([ + "run", + "--wasi", + "inherit-network", + "tcp.wasm", + "127.0.0.1:3456", + ]) + .stdout(Stdio::piped()) + .spawn()?; + + let mut nc_std_in = nc_handle.stdin.take().unwrap(); + + nc_std_in.write_all(b"hello")?; + + let output = tcp_handle.wait_with_output()?; + nc_handle.kill()?; + + assert_eq!( + String::from_utf8_lossy(&output.stdout), + "received: b'hello'\n" + ); + + Ok(()) +} + +fn venv_path(path: &Path) -> PathBuf { + path.join(".venv") + .join(if cfg!(windows) { "Scripts" } else { "bin" }) +} + +fn install_numpy(path: &Path) { + Command::new("curl") + .current_dir(path) + .args([ + "-OL", + "https://github.com/dicej/wasi-wheels/releases/download/v0.0.1/numpy-wasi.tar.gz", + ]) + .assert() + .success(); + + Command::new("tar") + .current_dir(path) + .args(["xf", "numpy-wasi.tar.gz"]) + .assert() + .success(); +}