diff --git a/Cargo.lock b/Cargo.lock index 7800786..1cf9608 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -363,7 +363,7 @@ checksum = "0b6a852b24ab71dffc585bcb46eaf7959d175cb865a7152e35b348d1b2960422" [[package]] name = "conformance-tests" version = "0.1.0" -source = "git+https://github.com/fermyon/conformance-tests#84680472e1a3e46fe06c2462fa95b4466164cf8c" +source = "git+https://github.com/fermyon/conformance-tests#8549e0fa0cb7ed41263335cb895eb509c1b61a49" dependencies = [ "anyhow", "flate2", @@ -2530,7 +2530,7 @@ dependencies = [ [[package]] name = "test-environment" version = "0.1.0" -source = "git+https://github.com/fermyon/conformance-tests#84680472e1a3e46fe06c2462fa95b4466164cf8c" +source = "git+https://github.com/fermyon/conformance-tests#8549e0fa0cb7ed41263335cb895eb509c1b61a49" dependencies = [ "anyhow", "fslock", diff --git a/conformance-tests/src/main.rs b/conformance-tests/src/main.rs index 0c14ce0..90a910e 100644 --- a/conformance-tests/src/main.rs +++ b/conformance-tests/src/main.rs @@ -1,316 +1,51 @@ -use std::collections::HashMap; +mod runtime; -use anyhow::anyhow; -use anyhow::Context as _; -use bindings::{exports::wasi::http::types::HeaderError, VirtualizedApp}; -use test_environment::http::Response; +use runtime::SpinTest; -mod bindings { - wasmtime::component::bindgen!({ - world: "virtualized-app", - path: "../host-wit", - with: { - "wasi:io": wasmtime_wasi::bindings::io, - "wasi:clocks": wasmtime_wasi::bindings::clocks, - } - }); -} +// We don't use port 80 because URL parsing treats port 80 special +// and will not include it in the URL string representation which breaks the test +const HTTP_PORT: u16 = 1234; fn main() -> anyhow::Result<()> { - let tests_dir = conformance_tests::download_tests()?; - - for test in conformance_tests::tests(&tests_dir)? { - let engine = wasmtime::Engine::default(); - let manifest = String::from_utf8(std::fs::read(test.manifest)?)?; - let mut store = wasmtime::Store::new(&engine, StoreData::new(manifest)); - let mut linker = wasmtime::component::Linker::new(&engine); - let component = spin_test::Component::from_file(test.component)?; - let component = spin_test::virtualize_app(component).context("failed to virtualize app")?; - - let component = wasmtime::component::Component::new(&engine, component)?; - wasmtime_wasi::add_to_linker_sync(&mut linker)?; - bindings::VirtualizedApp::add_to_linker(&mut linker, |x| x)?; - - let (instance, _) = bindings::VirtualizedApp::instantiate(&mut store, &component, &linker)?; + // let tests_dir = conformance_tests::download_tests()?; + let tests_dir = std::path::Path::new("../../conformance-test/conformance-tests"); + + for test in conformance_tests::tests(tests_dir)? { + println!("Test: {}", test.name); + let mut manifest = + test_environment::manifest_template::EnvTemplate::from_file(&test.manifest).unwrap(); + let env_config = test_environment::TestEnvironmentConfig { + create_runtime: Box::new(|_env| { + manifest.substitute_value("port", |port| { + (port == "80").then(|| HTTP_PORT.to_string()) + })?; + SpinTest::new(manifest.into_contents(), test.component) + }), + // Services are not needed in `spin-test` since everything stays in the guest + services_config: test_environment::services::ServicesConfig::none(), + }; + let mut env = test_environment::TestEnvironment::up(env_config, |_| Ok(())).unwrap(); + if test.config.services.iter().any(|s| s == "http-echo") { + env.runtime_mut() + .set_echo_response(format!("http://localhost:{HTTP_PORT}").as_str())?; + } for invocation in test.config.invocations { - let conformance_tests::config::Invocation::Http(invocation) = invocation; - invocation.run(|request| { - let request = to_outgoing_request(&instance, &mut store, request)?; - let response = to_incoming_response(&instance, &mut store, request)?; - from_incoming_response(&mut store, response) - })?; + let conformance_tests::config::Invocation::Http(mut invocation) = invocation; + invocation + .request + .substitute(|key, value| { + Ok(match (key, value) { + ("port", "80") => Some(HTTP_PORT.to_string()), + _ => None, + }) + }) + .unwrap(); + invocation.run(|request| env.runtime_mut().make_http_request(request))?; } } Ok(()) } -/// Convert a test_environment::http::Request into a wasi::http::types::IncomingRequest -fn to_outgoing_request<'a>( - instance: &'a VirtualizedApp, - store: &mut wasmtime::Store, - request: test_environment::http::Request, -) -> anyhow::Result> { - let fields = Fields::new(instance, store)?; - for (n, v) in request.headers { - fields.append(store, &(*n).to_owned(), &(*v).to_owned().into_bytes())??; - } - let outgoing_request = OutgoingRequest::new(instance, store, fields)?; - let method = match request.method { - test_environment::http::Method::Get => bindings::exports::wasi::http::types::Method::Get, - test_environment::http::Method::Post => bindings::exports::wasi::http::types::Method::Post, - test_environment::http::Method::Put => bindings::exports::wasi::http::types::Method::Put, - test_environment::http::Method::Patch => { - bindings::exports::wasi::http::types::Method::Patch - } - test_environment::http::Method::Delete => { - bindings::exports::wasi::http::types::Method::Delete - } - }; - outgoing_request - .set_method(store, &method)? - .map_err(|_| anyhow!("invalid request method"))?; - outgoing_request - .set_path_with_query(store, Some(request.path))? - .map_err(|_| anyhow!("invalid request path"))?; - // TODO: set the body - IncomingRequest::new(instance, store, outgoing_request) -} - -/// Call the incoming handler with the request and return the response -fn to_incoming_response<'a>( - instance: &'a VirtualizedApp, - store: &mut wasmtime::Store, - request: IncomingRequest<'a>, -) -> anyhow::Result> { - let (out, rx) = new_response(instance, &mut *store)?; - instance - .wasi_http_incoming_handler() - .call_handle(&mut *store, request.resource, out)?; - rx.get(&mut *store)?.context("no response found") -} - -/// Convert a wasi::http::types::IncomingResponse into a test_environment::http::Response -fn from_incoming_response( - store: &mut wasmtime::Store, - response: IncomingResponse, -) -> anyhow::Result { - let status = response.status(store)?; - let headers = response - .headers(store)? - .entries(store)? - .into_iter() - .map(|(k, v)| Ok((k, String::from_utf8(v)?))) - .collect::>>()?; - let body = response - .consume(store)? - .map_err(|_| anyhow!("response body already consumed"))? - .stream(store)? - .map_err(|_| anyhow!("response body stream already consumed"))? - .blocking_read(store, u64::MAX)??; - Ok(Response::full(status, headers, body)) -} - -struct Fields<'a> { - guest: bindings::exports::wasi::http::types::GuestFields<'a>, - resource: wasmtime::component::ResourceAny, -} - -impl<'a> Fields<'a> { - pub fn new( - instance: &'a VirtualizedApp, - store: &mut wasmtime::Store, - ) -> anyhow::Result { - let guest = instance.wasi_http_types().fields(); - let resource = guest.call_constructor(store)?; - Ok(Self { guest, resource }) - } - - pub fn append( - &self, - store: &mut wasmtime::Store, - name: &String, - value: &Vec, - ) -> anyhow::Result> { - self.guest.call_append(store, self.resource, name, value) - } - - fn entries( - &self, - store: &mut wasmtime::Store, - ) -> wasmtime::Result)>> { - self.guest.call_entries(store, self.resource) - } -} - -struct OutgoingRequest<'a> { - guest: bindings::exports::wasi::http::types::GuestOutgoingRequest<'a>, - resource: wasmtime::component::ResourceAny, -} - -impl<'a> OutgoingRequest<'a> { - pub fn new( - instance: &'a VirtualizedApp, - store: &mut wasmtime::Store, - fields: Fields, - ) -> anyhow::Result { - let guest = instance.wasi_http_types().outgoing_request(); - let resource = guest.call_constructor(store, fields.resource)?; - Ok(Self { guest, resource }) - } - - pub fn set_method( - &self, - store: &mut wasmtime::Store, - method: &bindings::exports::wasi::http::types::Method, - ) -> anyhow::Result> { - self.guest.call_set_method(store, self.resource, method) - } - - pub fn set_path_with_query( - &self, - store: &mut wasmtime::Store, - path: Option<&str>, - ) -> anyhow::Result> { - self.guest - .call_set_path_with_query(store, self.resource, path) - } -} - -struct IncomingRequest<'a> { - #[allow(dead_code)] - guest: bindings::exports::wasi::http::types::GuestIncomingRequest<'a>, - resource: wasmtime::component::ResourceAny, -} - -impl<'a> IncomingRequest<'a> { - fn new( - instance: &'a VirtualizedApp, - store: &mut wasmtime::Store, - outgoing_request: OutgoingRequest, - ) -> anyhow::Result { - let guest = instance.fermyon_spin_wasi_virt_http_helper(); - let resource = guest.call_new_request(store, outgoing_request.resource, None)?; - let guest = instance.wasi_http_types().incoming_request(); - Ok(Self { guest, resource }) - } -} - -fn new_response<'a, T>( - instance: &'a VirtualizedApp, - store: &mut wasmtime::Store, -) -> anyhow::Result<(wasmtime::component::ResourceAny, ResponseReceiver<'a>)> { - let guest = instance.fermyon_spin_wasi_virt_http_helper(); - let (out_param, rx) = guest.call_new_response(store)?; - let rx_guest = instance - .fermyon_spin_wasi_virt_http_helper() - .response_receiver(); - - Ok(( - out_param, - ResponseReceiver { - instance, - resource: rx, - guest: rx_guest, - }, - )) -} - -struct ResponseReceiver<'a> { - instance: &'a VirtualizedApp, - guest: bindings::exports::fermyon::spin_wasi_virt::http_helper::GuestResponseReceiver<'a>, - resource: wasmtime::component::ResourceAny, -} - -impl<'a> ResponseReceiver<'a> { - fn get( - &self, - store: &mut wasmtime::Store, - ) -> anyhow::Result>> { - let Some(resource) = self.guest.call_get(store, self.resource)? else { - return Ok(None); - }; - Ok(Some(IncomingResponse { - instance: self.instance, - guest: self.instance.wasi_http_types().incoming_response(), - resource, - })) - } -} - -struct IncomingResponse<'a> { - instance: &'a VirtualizedApp, - guest: bindings::exports::wasi::http::types::GuestIncomingResponse<'a>, - resource: wasmtime::component::ResourceAny, -} - -impl<'a> IncomingResponse<'a> { - fn status(&self, store: &mut wasmtime::Store) -> anyhow::Result { - self.guest.call_status(store, self.resource) - } - - fn headers(&self, store: &mut wasmtime::Store) -> anyhow::Result { - let fields = self.guest.call_headers(store, self.resource)?; - Ok(Fields { - guest: self.instance.wasi_http_types().fields(), - resource: fields, - }) - } - - fn consume( - &self, - store: &mut wasmtime::Store, - ) -> wasmtime::Result, ()>> { - let Ok(resource) = self.guest.call_consume(store, self.resource)? else { - return Ok(Err(())); - }; - let guest = self.instance.wasi_http_types().incoming_body(); - Ok(Ok(IncomingBody { - instance: self.instance, - guest, - resource, - })) - } -} - -struct IncomingBody<'a> { - instance: &'a VirtualizedApp, - guest: bindings::exports::wasi::http::types::GuestIncomingBody<'a>, - resource: wasmtime::component::ResourceAny, -} - -impl<'a> IncomingBody<'a> { - fn stream( - &self, - store: &mut wasmtime::Store, - ) -> wasmtime::Result> { - let Ok(resource) = self.guest.call_stream(store, self.resource)? else { - return Ok(Err(())); - }; - Ok(Ok(InputStream { - instance: self.instance, - resource, - })) - } -} - -struct InputStream<'a> { - instance: &'a VirtualizedApp, - resource: wasmtime::component::ResourceAny, -} - -impl<'a> InputStream<'a> { - fn blocking_read( - &self, - store: &mut wasmtime::Store, - max_bytes: u64, - ) -> wasmtime::Result, bindings::exports::wasi::io::streams::StreamError>> { - self.instance - .wasi_io_streams() - .input_stream() - .call_blocking_read(store, self.resource, max_bytes) - } -} - struct StoreData { manifest: String, ctx: wasmtime_wasi::WasiCtx, @@ -339,7 +74,7 @@ impl wasmtime_wasi::WasiView for StoreData { } } -impl bindings::VirtualizedAppImports for StoreData { +impl runtime::VirtualizedAppImports for StoreData { fn get_manifest(&mut self) -> String { self.manifest.clone() } diff --git a/conformance-tests/src/runtime.rs b/conformance-tests/src/runtime.rs new file mode 100644 index 0000000..8aa6fbb --- /dev/null +++ b/conformance-tests/src/runtime.rs @@ -0,0 +1,416 @@ +// Some of the bindings code might not be currently used, but we're +// leaving it here since this code might change often and we may want +// to use some of the dead code in the near future. +#![allow(dead_code)] + +use anyhow::anyhow; +use anyhow::Context as _; +use bindings::exports::fermyon::spin_wasi_virt::http_handler::ResponseHandler; +use bindings::{exports::wasi::http::types::HeaderError, VirtualizedApp}; +use std::collections::HashMap; +use std::path::PathBuf; +use test_environment::http::Response; + +mod bindings { + wasmtime::component::bindgen!({ + world: "virtualized-app", + path: "../host-wit", + with: { + "wasi:io": wasmtime_wasi::bindings::io, + "wasi:clocks": wasmtime_wasi::bindings::clocks, + } + }); +} + +pub use bindings::VirtualizedAppImports; + +/// The `spin-test` runtime +pub(crate) struct SpinTest { + instance: VirtualizedApp, + store: wasmtime::Store, +} + +impl SpinTest { + pub fn new(manifest: String, component_path: PathBuf) -> anyhow::Result { + let engine = wasmtime::Engine::default(); + let mut store = wasmtime::Store::new(&engine, super::StoreData::new(manifest)); + let mut linker = wasmtime::component::Linker::new(&engine); + let component = spin_test::Component::from_file(component_path)?; + let component = spin_test::virtualize_app(component).context("failed to virtualize app")?; + + let component = wasmtime::component::Component::new(&engine, component)?; + wasmtime_wasi::add_to_linker_sync(&mut linker)?; + bindings::VirtualizedApp::add_to_linker(&mut linker, |x| x)?; + + let (instance, _) = bindings::VirtualizedApp::instantiate(&mut store, &component, &linker)?; + + Ok(Self { instance, store }) + } + /// Make an HTTP request against the `spin-test` runtime + pub fn make_http_request( + &mut self, + request: test_environment::http::Request, + ) -> anyhow::Result { + let request = to_outgoing_request(&self.instance, &mut self.store, request) + .context("failed to create outgoing-request")?; + let response = to_incoming_response(&self.instance, &mut self.store, request) + .context("failed to get incoming-response")?; + from_incoming_response(&mut self.store, response) + .context("failed to convert to `Response` from incoming-response") + } + + pub fn set_echo_response(&mut self, url: &str) -> anyhow::Result<()> { + self.instance + .fermyon_spin_wasi_virt_http_handler() + .call_set_response(&mut self.store, url, ResponseHandler::Echo) + } +} + +impl test_environment::Runtime for SpinTest { + fn error(&mut self) -> anyhow::Result<()> { + Ok(()) + } +} + +/// Convert a test_environment::http::Request into a wasi::http::types::IncomingRequest +fn to_outgoing_request<'a>( + instance: &'a VirtualizedApp, + store: &mut wasmtime::Store, + request: test_environment::http::Request, +) -> anyhow::Result> { + let fields = Fields::new(instance, store)?; + for (n, v) in request.headers { + fields.append(store, &(*n).to_owned(), &(*v).to_owned().into_bytes())??; + } + let outgoing_request = OutgoingRequest::new(instance, store, fields)?; + let method = match request.method { + test_environment::http::Method::Get => bindings::exports::wasi::http::types::Method::Get, + test_environment::http::Method::Post => bindings::exports::wasi::http::types::Method::Post, + test_environment::http::Method::Put => bindings::exports::wasi::http::types::Method::Put, + test_environment::http::Method::Patch => { + bindings::exports::wasi::http::types::Method::Patch + } + test_environment::http::Method::Delete => { + bindings::exports::wasi::http::types::Method::Delete + } + }; + outgoing_request + .set_method(store, &method)? + .map_err(|_| anyhow!("invalid request method"))?; + outgoing_request + .set_path_with_query(store, Some(request.path))? + .map_err(|_| anyhow!("invalid request path"))?; + // TODO: set the body + IncomingRequest::new(instance, store, outgoing_request) +} + +/// Call the incoming handler with the request and return the response +fn to_incoming_response<'a>( + instance: &'a VirtualizedApp, + store: &mut wasmtime::Store, + request: IncomingRequest<'a>, +) -> anyhow::Result> { + let (out, rx) = new_response(instance, &mut *store) + .context("failed to create out-response and response-receiver")?; + instance + .wasi_http_incoming_handler() + .call_handle(&mut *store, request.resource, out) + .context("call to incoming-handler failed")?; + rx.get(&mut *store)?.context("no response found") +} + +/// Convert a wasi::http::types::IncomingResponse into a test_environment::http::Response +fn from_incoming_response( + store: &mut wasmtime::Store, + response: IncomingResponse, +) -> anyhow::Result { + let status = response.status(store)?; + let headers = response + .headers(store)? + .entries(store)? + .into_iter() + .map(|(k, v)| Ok((k, String::from_utf8(v)?))) + .collect::>>()?; + let body = response + .consume(store)? + .map_err(|_| anyhow!("response body already consumed"))? + .stream(store)? + .map_err(|_| anyhow!("response body stream already consumed"))? + .blocking_read(store, u64::MAX)??; + Ok(Response::full(status, headers, body)) +} + +struct Fields<'a> { + guest: bindings::exports::wasi::http::types::GuestFields<'a>, + resource: wasmtime::component::ResourceAny, +} + +impl<'a> Fields<'a> { + pub fn new( + instance: &'a VirtualizedApp, + store: &mut wasmtime::Store, + ) -> anyhow::Result { + let guest = instance.wasi_http_types().fields(); + let resource = guest.call_constructor(store)?; + Ok(Self { guest, resource }) + } + + pub fn append( + &self, + store: &mut wasmtime::Store, + name: &String, + value: &Vec, + ) -> anyhow::Result> { + self.guest.call_append(store, self.resource, name, value) + } + + fn entries( + &self, + store: &mut wasmtime::Store, + ) -> wasmtime::Result)>> { + self.guest.call_entries(store, self.resource) + } +} + +struct OutgoingRequest<'a> { + guest: bindings::exports::wasi::http::types::GuestOutgoingRequest<'a>, + resource: wasmtime::component::ResourceAny, +} + +impl<'a> OutgoingRequest<'a> { + pub fn new( + instance: &'a VirtualizedApp, + store: &mut wasmtime::Store, + fields: Fields, + ) -> anyhow::Result { + let guest = instance.wasi_http_types().outgoing_request(); + let resource = guest.call_constructor(store, fields.resource)?; + Ok(Self { guest, resource }) + } + + pub fn set_method( + &self, + store: &mut wasmtime::Store, + method: &bindings::exports::wasi::http::types::Method, + ) -> anyhow::Result> { + self.guest.call_set_method(store, self.resource, method) + } + + pub fn set_path_with_query( + &self, + store: &mut wasmtime::Store, + path: Option<&str>, + ) -> anyhow::Result> { + self.guest + .call_set_path_with_query(store, self.resource, path) + } +} + +struct OutgoingResponse<'a> { + instance: &'a VirtualizedApp, + guest: bindings::exports::wasi::http::types::GuestOutgoingResponse<'a>, + resource: wasmtime::component::ResourceAny, +} + +impl<'a> OutgoingResponse<'a> { + pub fn new( + instance: &'a VirtualizedApp, + store: &mut wasmtime::Store, + fields: Fields, + ) -> anyhow::Result { + let guest = instance.wasi_http_types().outgoing_response(); + let resource = guest.call_constructor(store, fields.resource)?; + Ok(Self { + instance, + guest, + resource, + }) + } + + pub fn body( + &self, + store: &mut wasmtime::Store, + ) -> anyhow::Result> { + let resource = match self.guest.call_body(store, self.resource)? { + Ok(r) => r, + Err(()) => return Ok(Err(())), + }; + Ok(Ok(OutgoingBody { + instance: self.instance, + guest: self.instance.wasi_http_types().outgoing_body(), + resource, + })) + } +} + +struct IncomingRequest<'a> { + #[allow(dead_code)] + guest: bindings::exports::wasi::http::types::GuestIncomingRequest<'a>, + resource: wasmtime::component::ResourceAny, +} + +impl<'a> IncomingRequest<'a> { + fn new( + instance: &'a VirtualizedApp, + store: &mut wasmtime::Store, + outgoing_request: OutgoingRequest, + ) -> anyhow::Result { + let guest = instance.fermyon_spin_wasi_virt_http_helper(); + let resource = guest.call_new_request(store, outgoing_request.resource, None)?; + let guest = instance.wasi_http_types().incoming_request(); + Ok(Self { guest, resource }) + } +} + +fn new_response<'a, T>( + instance: &'a VirtualizedApp, + store: &mut wasmtime::Store, +) -> anyhow::Result<(wasmtime::component::ResourceAny, ResponseReceiver<'a>)> { + let guest = instance.fermyon_spin_wasi_virt_http_helper(); + let (out_param, rx) = guest.call_new_response(store)?; + let rx_guest = instance + .fermyon_spin_wasi_virt_http_helper() + .response_receiver(); + + Ok(( + out_param, + ResponseReceiver { + instance, + resource: rx, + guest: rx_guest, + }, + )) +} + +struct ResponseReceiver<'a> { + instance: &'a VirtualizedApp, + guest: bindings::exports::fermyon::spin_wasi_virt::http_helper::GuestResponseReceiver<'a>, + resource: wasmtime::component::ResourceAny, +} + +impl<'a> ResponseReceiver<'a> { + fn get( + &self, + store: &mut wasmtime::Store, + ) -> anyhow::Result>> { + let Some(resource) = self.guest.call_get(store, self.resource)? else { + return Ok(None); + }; + Ok(Some(IncomingResponse { + instance: self.instance, + guest: self.instance.wasi_http_types().incoming_response(), + resource, + })) + } +} + +struct IncomingResponse<'a> { + instance: &'a VirtualizedApp, + guest: bindings::exports::wasi::http::types::GuestIncomingResponse<'a>, + resource: wasmtime::component::ResourceAny, +} + +impl<'a> IncomingResponse<'a> { + fn status(&self, store: &mut wasmtime::Store) -> anyhow::Result { + self.guest.call_status(store, self.resource) + } + + fn headers(&self, store: &mut wasmtime::Store) -> anyhow::Result { + let fields = self.guest.call_headers(store, self.resource)?; + Ok(Fields { + guest: self.instance.wasi_http_types().fields(), + resource: fields, + }) + } + + fn consume( + &self, + store: &mut wasmtime::Store, + ) -> wasmtime::Result, ()>> { + let Ok(resource) = self.guest.call_consume(store, self.resource)? else { + return Ok(Err(())); + }; + let guest = self.instance.wasi_http_types().incoming_body(); + Ok(Ok(IncomingBody { + instance: self.instance, + guest, + resource, + })) + } +} + +struct IncomingBody<'a> { + instance: &'a VirtualizedApp, + guest: bindings::exports::wasi::http::types::GuestIncomingBody<'a>, + resource: wasmtime::component::ResourceAny, +} + +impl<'a> IncomingBody<'a> { + fn stream( + &self, + store: &mut wasmtime::Store, + ) -> wasmtime::Result> { + let Ok(resource) = self.guest.call_stream(store, self.resource)? else { + return Ok(Err(())); + }; + Ok(Ok(InputStream { + instance: self.instance, + resource, + })) + } +} + +struct InputStream<'a> { + instance: &'a VirtualizedApp, + resource: wasmtime::component::ResourceAny, +} + +impl<'a> InputStream<'a> { + fn blocking_read( + &self, + store: &mut wasmtime::Store, + max_bytes: u64, + ) -> wasmtime::Result, bindings::exports::wasi::io::streams::StreamError>> { + self.instance + .wasi_io_streams() + .input_stream() + .call_blocking_read(store, self.resource, max_bytes) + } +} + +struct OutgoingBody<'a> { + instance: &'a VirtualizedApp, + guest: bindings::exports::wasi::http::types::GuestOutgoingBody<'a>, + resource: wasmtime::component::ResourceAny, +} + +impl<'a> OutgoingBody<'a> { + fn write(&self, store: &mut wasmtime::Store) -> anyhow::Result> { + let stream = match self.guest.call_write(store, self.resource)? { + Ok(s) => s, + Err(()) => return Ok(Err(())), + }; + Ok(Ok(OutputStream { + instance: self.instance, + resource: stream, + })) + } +} + +struct OutputStream<'a> { + instance: &'a VirtualizedApp, + resource: wasmtime::component::ResourceAny, +} + +impl<'a> OutputStream<'a> { + fn blocking_write_and_flush( + &self, + store: &mut wasmtime::Store, + data: &[u8], + ) -> wasmtime::Result> { + self.instance + .wasi_io_streams() + .output_stream() + .call_blocking_write_and_flush(store, self.resource, data) + } +} diff --git a/crates/router/src/bindings.rs b/crates/router/src/bindings.rs index 9f8c526..f79061c 100644 --- a/crates/router/src/bindings.rs +++ b/crates/router/src/bindings.rs @@ -1,4 +1,4 @@ -// Generated by `wit-bindgen` 0.24.0. DO NOT EDIT! +// Generated by `wit-bindgen` 0.25.0. DO NOT EDIT! // Options used: pub type OutgoingRequest = wasi::http::types::OutgoingRequest; pub type IncomingRequest = wasi::http::types::IncomingRequest; @@ -7488,6 +7488,7 @@ mod _rt { /// drop a resource. /// /// This generally is implemented by generated code, not user-facing code. + #[allow(clippy::missing_safety_doc)] pub unsafe trait WasmResource { /// Invokes the `[resource-drop]...` intrinsic. unsafe fn drop(handle: u32); @@ -7719,7 +7720,7 @@ macro_rules! __export_router_impl { pub(crate) use __export_router_impl as export; #[cfg(target_arch = "wasm32")] -#[link_section = "component-type:wit-bindgen:0.24.0:router:encoded world"] +#[link_section = "component-type:wit-bindgen:0.25.0:router:encoded world"] #[doc(hidden)] pub static __WIT_BINDGEN_COMPONENT_TYPE: [u8; 6758] = *b"\ \0asm\x0d\0\x01\0\0\x19\x16wit-component-encoding\x04\0\x07\xe93\x01A\x02\x01A&\x01\ @@ -7856,7 +7857,7 @@ ng-body\x17\0\x18\x03\0\x0bnew-request\x01\x19\x01B\x08\x02\x03\x02\x01\x0a\x04\ \x02\x01i\x01\x01i\x03\x01@\x02\x07request\x04\x0cresponse-out\x05\x01\0\x04\0\x06\ handle\x01\x06\x04\x01\x20wasi:http/incoming-handler@0.2.0\x05\x1a\x04\x01\x15fe\ rmyon:router/router\x04\0\x0b\x0c\x01\0\x06router\x03\0\0\0G\x09producers\x01\x0c\ -processed-by\x02\x0dwit-component\x070.202.0\x10wit-bindgen-rust\x060.24.0"; +processed-by\x02\x0dwit-component\x070.208.1\x10wit-bindgen-rust\x060.25.0"; #[inline(never)] #[doc(hidden)] diff --git a/crates/spin-test-virt/src/bindings.rs b/crates/spin-test-virt/src/bindings.rs index 9712d71..4b8fe44 100644 --- a/crates/spin-test-virt/src/bindings.rs +++ b/crates/spin-test-virt/src/bindings.rs @@ -1,4 +1,4 @@ -// Generated by `wit-bindgen` 0.24.0. DO NOT EDIT! +// Generated by `wit-bindgen` 0.25.0. DO NOT EDIT! // Options used: #[allow(unused_unsafe, clippy::all)] pub fn get_manifest() -> _rt::String { @@ -10776,7 +10776,8 @@ pub mod exports { } impl DbDataType { - pub(crate) unsafe fn _lift(val: u8) -> DbDataType { + #[doc(hidden)] + pub unsafe fn _lift(val: u8) -> DbDataType { if !cfg!(debug_assertions) { return ::core::mem::transmute(val); } @@ -12122,7 +12123,8 @@ pub mod exports { } impl Qos { - pub(crate) unsafe fn _lift(val: u8) -> Qos { + #[doc(hidden)] + pub unsafe fn _lift(val: u8) -> Qos { if !cfg!(debug_assertions) { return ::core::mem::transmute(val); } @@ -15402,22 +15404,47 @@ pub mod exports { super::super::super::super::exports::wasi::http::types::OutgoingResponseBorrow< 'a, >; + pub enum ResponseHandler { + Echo, + Response(OutgoingResponse), + } + impl ::core::fmt::Debug for ResponseHandler { + fn fmt(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result { + match self { + ResponseHandler::Echo => { + f.debug_tuple("ResponseHandler::Echo").finish() + } + ResponseHandler::Response(e) => { + f.debug_tuple("ResponseHandler::Response").field(e).finish() + } + } + } + } #[doc(hidden)] #[allow(non_snake_case)] pub unsafe fn _export_set_response_cabi( arg0: *mut u8, arg1: usize, arg2: i32, + arg3: i32, ) { #[cfg(target_arch = "wasm32")] _rt::run_ctors_once(); let len0 = arg1; let bytes0 = _rt::Vec::from_raw_parts(arg0.cast(), len0, len0); - T::set_response(_rt::string_lift(bytes0), super::super::super::super::exports::wasi::http::types::OutgoingResponse::from_handle(arg2 as u32)); + let v1 = match arg2 { + 0 => ResponseHandler::Echo, + n => { + debug_assert_eq!(n, 1, "invalid enum discriminant"); + let e1 = super::super::super::super::exports::wasi::http::types::OutgoingResponse::from_handle(arg3 as u32); + ResponseHandler::Response(e1) + } + }; + T::set_response(_rt::string_lift(bytes0), v1); } pub trait Guest { /// Set a response for a given url - fn set_response(url: _rt::String, response: OutgoingResponse); + fn set_response(url: _rt::String, response: ResponseHandler); } #[doc(hidden)] @@ -15425,8 +15452,8 @@ pub mod exports { ($ty:ident with_types_in $($path_to_types:tt)*) => (const _: () = { #[export_name = "fermyon:spin-wasi-virt/http-handler#set-response"] - unsafe extern "C" fn export_set_response(arg0: *mut u8,arg1: usize,arg2: i32,) { - $($path_to_types)*::_export_set_response_cabi::<$ty>(arg0, arg1, arg2) + unsafe extern "C" fn export_set_response(arg0: *mut u8,arg1: usize,arg2: i32,arg3: i32,) { + $($path_to_types)*::_export_set_response_cabi::<$ty>(arg0, arg1, arg2, arg3) } };); } @@ -17064,7 +17091,8 @@ pub mod exports { } impl DescriptorType { - pub(crate) unsafe fn _lift(val: u8) -> DescriptorType { + #[doc(hidden)] + pub unsafe fn _lift(val: u8) -> DescriptorType { if !cfg!(debug_assertions) { return ::core::mem::transmute(val); } @@ -17422,7 +17450,8 @@ pub mod exports { impl std::error::Error for ErrorCode {} impl ErrorCode { - pub(crate) unsafe fn _lift(val: u8) -> ErrorCode { + #[doc(hidden)] + pub unsafe fn _lift(val: u8) -> ErrorCode { if !cfg!(debug_assertions) { return ::core::mem::transmute(val); } @@ -17508,7 +17537,8 @@ pub mod exports { } impl Advice { - pub(crate) unsafe fn _lift(val: u8) -> Advice { + #[doc(hidden)] + pub unsafe fn _lift(val: u8) -> Advice { if !cfg!(debug_assertions) { return ::core::mem::transmute(val); } @@ -29987,7 +30017,8 @@ pub mod exports { impl std::error::Error for ErrorCode {} impl ErrorCode { - pub(crate) unsafe fn _lift(val: u8) -> ErrorCode { + #[doc(hidden)] + pub unsafe fn _lift(val: u8) -> ErrorCode { if !cfg!(debug_assertions) { return ::core::mem::transmute(val); } @@ -30042,7 +30073,8 @@ pub mod exports { } impl IpAddressFamily { - pub(crate) unsafe fn _lift(val: u8) -> IpAddressFamily { + #[doc(hidden)] + pub unsafe fn _lift(val: u8) -> IpAddressFamily { if !cfg!(debug_assertions) { return ::core::mem::transmute(val); } @@ -32068,7 +32100,8 @@ pub mod exports { } impl ShutdownType { - pub(crate) unsafe fn _lift(val: u8) -> ShutdownType { + #[doc(hidden)] + pub unsafe fn _lift(val: u8) -> ShutdownType { if !cfg!(debug_assertions) { return ::core::mem::transmute(val); } @@ -34006,6 +34039,7 @@ mod _rt { /// drop a resource. /// /// This generally is implemented by generated code, not user-facing code. + #[allow(clippy::missing_safety_doc)] pub unsafe trait WasmResource { /// Invokes the `[resource-drop]...` intrinsic. unsafe fn drop(handle: u32); @@ -34323,10 +34357,10 @@ macro_rules! __export_env_impl { pub(crate) use __export_env_impl as export; #[cfg(target_arch = "wasm32")] -#[link_section = "component-type:wit-bindgen:0.24.0:env:encoded world"] +#[link_section = "component-type:wit-bindgen:0.25.0:env:encoded world"] #[doc(hidden)] -pub static __WIT_BINDGEN_COMPONENT_TYPE: [u8; 27955] = *b"\ -\0asm\x0d\0\x01\0\0\x19\x16wit-component-encoding\x04\0\x07\xb8\xd9\x01\x01A\x02\ +pub static __WIT_BINDGEN_COMPONENT_TYPE: [u8; 27999] = *b"\ +\0asm\x0d\0\x01\0\0\x19\x16wit-component-encoding\x04\0\x07\xe4\xd9\x01\x01A\x02\ \x01A\xa0\x01\x01B\x0a\x04\0\x08pollable\x03\x01\x01h\0\x01@\x01\x04self\x01\0\x7f\ \x04\0\x16[method]pollable.ready\x01\x02\x01@\x01\x04self\x01\x01\0\x04\0\x16[me\ thod]pollable.block\x01\x03\x01p\x01\x01py\x01@\x01\x02in\x04\0\x05\x04\0\x04pol\ @@ -34877,23 +34911,24 @@ d]future-incoming-response.get\x01\x89\x01\x01h\x07\x01k\x1b\x01@\x01\x03err\x8a V\x04\0\x0aerror-code\x03\0\x06\x01i\x01\x01i\x03\x01k\x09\x01i\x05\x01j\x01\x0b\ \x01\x07\x01@\x02\x07request\x08\x07options\x0a\0\x0c\x04\0\x06handle\x01\x0d\x04\ \x01\x20wasi:http/outgoing-handler@0.2.0\x05W\x02\x03\09\x11outgoing-response\x01\ -B\x05\x02\x03\x02\x01X\x04\0\x11outgoing-response\x03\0\0\x01i\x01\x01@\x02\x03u\ -rls\x08response\x02\x01\0\x04\0\x0cset-response\x01\x03\x04\x01#fermyon:spin-was\ -i-virt/http-handler\x05Y\x02\x03\09\x10incoming-request\x02\x03\09\x11incoming-r\ -esponse\x02\x03\09\x11response-outparam\x02\x03\09\x0dincoming-body\x01B\x1f\x02\ -\x03\x02\x01Z\x04\0\x10incoming-request\x03\0\0\x02\x03\x02\x01[\x04\0\x11incomi\ -ng-response\x03\0\x02\x02\x03\x02\x01X\x04\0\x11outgoing-response\x03\0\x04\x02\x03\ -\x02\x01S\x04\0\x10outgoing-request\x03\0\x06\x02\x03\x02\x01\\\x04\0\x11respons\ -e-outparam\x03\0\x08\x02\x03\x02\x01U\x04\0\x18future-incoming-response\x03\0\x0a\ -\x02\x03\x02\x01]\x04\0\x0dincoming-body\x03\0\x0c\x04\0\x11response-receiver\x03\ -\x01\x01h\x0e\x01i\x03\x01k\x10\x01@\x01\x04self\x0f\0\x11\x04\0\x1d[method]resp\ -onse-receiver.get\x01\x12\x01i\x07\x01i\x0d\x01k\x14\x01i\x01\x01@\x02\x07reques\ -t\x13\x0dincoming-body\x15\0\x16\x04\0\x0bnew-request\x01\x17\x01i\x09\x01i\x0e\x01\ -o\x02\x18\x19\x01@\0\0\x1a\x04\0\x0cnew-response\x01\x1b\x04\x01\"fermyon:spin-w\ -asi-virt/http-helper\x05^\x01B\x03\x01p}\x01@\x02\x04paths\x08contents\0\x01\0\x04\ -\0\x08add-file\x01\x01\x04\x01!fermyon:spin-wasi-virt/fs-handler\x05_\x04\x01\x1a\ -fermyon:spin-test-virt/env\x04\0\x0b\x09\x01\0\x03env\x03\0\0\0G\x09producers\x01\ -\x0cprocessed-by\x02\x0dwit-component\x070.202.0\x10wit-bindgen-rust\x060.24.0"; +B\x07\x02\x03\x02\x01X\x04\0\x11outgoing-response\x03\0\0\x01i\x01\x01q\x02\x04e\ +cho\0\0\x08response\x01\x02\0\x04\0\x10response-handler\x03\0\x03\x01@\x02\x03ur\ +ls\x08response\x04\x01\0\x04\0\x0cset-response\x01\x05\x04\x01#fermyon:spin-wasi\ +-virt/http-handler\x05Y\x02\x03\09\x10incoming-request\x02\x03\09\x11incoming-re\ +sponse\x02\x03\09\x11response-outparam\x02\x03\09\x0dincoming-body\x01B\x1f\x02\x03\ +\x02\x01Z\x04\0\x10incoming-request\x03\0\0\x02\x03\x02\x01[\x04\0\x11incoming-r\ +esponse\x03\0\x02\x02\x03\x02\x01X\x04\0\x11outgoing-response\x03\0\x04\x02\x03\x02\ +\x01S\x04\0\x10outgoing-request\x03\0\x06\x02\x03\x02\x01\\\x04\0\x11response-ou\ +tparam\x03\0\x08\x02\x03\x02\x01U\x04\0\x18future-incoming-response\x03\0\x0a\x02\ +\x03\x02\x01]\x04\0\x0dincoming-body\x03\0\x0c\x04\0\x11response-receiver\x03\x01\ +\x01h\x0e\x01i\x03\x01k\x10\x01@\x01\x04self\x0f\0\x11\x04\0\x1d[method]response\ +-receiver.get\x01\x12\x01i\x07\x01i\x0d\x01k\x14\x01i\x01\x01@\x02\x07request\x13\ +\x0dincoming-body\x15\0\x16\x04\0\x0bnew-request\x01\x17\x01i\x09\x01i\x0e\x01o\x02\ +\x18\x19\x01@\0\0\x1a\x04\0\x0cnew-response\x01\x1b\x04\x01\"fermyon:spin-wasi-v\ +irt/http-helper\x05^\x01B\x03\x01p}\x01@\x02\x04paths\x08contents\0\x01\0\x04\0\x08\ +add-file\x01\x01\x04\x01!fermyon:spin-wasi-virt/fs-handler\x05_\x04\x01\x1afermy\ +on:spin-test-virt/env\x04\0\x0b\x09\x01\0\x03env\x03\0\0\0G\x09producers\x01\x0c\ +processed-by\x02\x0dwit-component\x070.208.1\x10wit-bindgen-rust\x060.25.0"; #[inline(never)] #[doc(hidden)] diff --git a/crates/spin-test-virt/src/wasi/http.rs b/crates/spin-test-virt/src/wasi/http.rs index d385aeb..a4a46ac 100644 --- a/crates/spin-test-virt/src/wasi/http.rs +++ b/crates/spin-test-virt/src/wasi/http.rs @@ -8,6 +8,7 @@ use std::{ pub use crate::bindings::exports::wasi::http as exports; pub use crate::bindings::wasi::http as imports; +use crate::bindings::exports::fermyon::spin_wasi_virt::http_handler; use crate::Component; use super::io; @@ -389,7 +390,7 @@ impl exports::types::GuestFields for Fields { } fn clone(&self) -> exports::types::Fields { - todo!() + exports::types::Fields::new(Clone::clone(self)) } } @@ -486,9 +487,8 @@ impl exports::types::GuestFutureTrailers for FutureTrailers { } } -pub static RESPONSES: std::sync::OnceLock< - Mutex>, -> = std::sync::OnceLock::new(); +pub static RESPONSES: std::sync::OnceLock>> = + std::sync::OnceLock::new(); impl exports::outgoing_handler::Guest for Component { fn handle( @@ -519,7 +519,7 @@ impl exports::outgoing_handler::Guest for Component { exports::outgoing_handler::ErrorCode::InternalError(Some(format!("{e}"))) })?; if !url_allowed { - (exports::outgoing_handler::ErrorCode::HttpRequestDenied); + return Err(exports::outgoing_handler::ErrorCode::HttpRequestDenied); } let response = RESPONSES .get_or_init(Default::default) @@ -527,7 +527,7 @@ impl exports::outgoing_handler::Guest for Component { .unwrap() .remove(&url); match response { - Some(r) => { + Some(http_handler::ResponseHandler::Response(r)) => { let r: OutgoingResponse = r.into_inner(); Ok(exports::types::FutureIncomingResponse::new( FutureIncomingResponse::new(Ok(IncomingResponse { @@ -537,15 +537,24 @@ impl exports::outgoing_handler::Guest for Component { })), )) } + Some(http_handler::ResponseHandler::Echo) => { + Ok(exports::types::FutureIncomingResponse::new( + FutureIncomingResponse::new(Ok(IncomingResponse { + status: 200, + headers: Fields::default(), + body: request.body.unconsume().map(Into::into), + })), + )) + } None => Err(exports::outgoing_handler::ErrorCode::InternalError(Some( - format!("unrecognized url: {url}"), + format!("mocking error - unrecognized url: {url}"), ))), } } } -impl crate::bindings::exports::fermyon::spin_wasi_virt::http_handler::Guest for Component { - fn set_response(url: String, response: exports::types::OutgoingResponse) { +impl http_handler::Guest for Component { + fn set_response(url: String, response: http_handler::ResponseHandler) { RESPONSES .get_or_init(Default::default) .lock() diff --git a/examples/test-rs/src/lib.rs b/examples/test-rs/src/lib.rs index f527f83..4bfbdcb 100644 --- a/examples/test-rs/src/lib.rs +++ b/examples/test-rs/src/lib.rs @@ -29,7 +29,10 @@ fn cache_miss() { let response = http::types::OutgoingResponse::new(http::types::Headers::new()); response.write_body(user_json.as_bytes()); - http_handler::set_response("https://my.api.com?user_id=123", response); + http_handler::set_response( + "https://my.api.com?user_id=123", + http_handler::ResponseHandler::Response(response), + ); // Configure the test make_request(user_json); diff --git a/host-wit/deps/spin-wasi-virt/world.wit b/host-wit/deps/spin-wasi-virt/world.wit index 747eaef..a997eca 100644 --- a/host-wit/deps/spin-wasi-virt/world.wit +++ b/host-wit/deps/spin-wasi-virt/world.wit @@ -85,8 +85,13 @@ interface http-helper { interface http-handler { use wasi:http/types@0.2.0.{outgoing-response}; + variant response-handler { + echo, + response(outgoing-response) + } + /// Set a response for a given url - set-response: func(url: string, response: outgoing-response); + set-response: func(url: string, response: response-handler); } interface fs-handler { diff --git a/host-wit/world.wit b/host-wit/world.wit index 228fe58..ca803e8 100644 --- a/host-wit/world.wit +++ b/host-wit/world.wit @@ -35,6 +35,7 @@ world virtualized-app { export wasi:http/types@0.2.0; export wasi:http/incoming-handler@0.2.0; export fermyon:spin-wasi-virt/http-helper; + export fermyon:spin-wasi-virt/http-handler; } /// A test runner that can run a `spin-test` test composition diff --git a/src/lib.rs b/src/lib.rs index b24e279..8d5b39e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -102,6 +102,7 @@ pub fn virtualize_app(app_component: Component) -> anyhow::Result> { composition.export(instance, name).unwrap(); }; export("fermyon:spin-wasi-virt/http-helper"); + export("fermyon:spin-wasi-virt/http-handler"); export("wasi:http/types@0.2.0"); export("wasi:clocks/monotonic-clock@0.2.0"); export("wasi:io/streams@0.2.0");