diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..96ef6c0 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +/target +Cargo.lock diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..24a9299 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "proxy-wasm-test-framework" +version = "0.1.0" +authors = ["Christopher Agia "] +edition = "2018" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +wasmtime = { git = "https://github.com/bytecodealliance/wasmtime", rev = "2482bd80c24a6b23ac1a259df77c61d5cc9c93a9" } +anyhow = "1.0.31" +lazy_static = "1.4.0" +more-asserts = "0.2.1" +rand = "0.7.3" diff --git a/README.md b/README.md index cb0635f..e08e542 100644 --- a/README.md +++ b/README.md @@ -1 +1,31 @@ # WebAssembly for Proxies (test framework) + +The Proxy-Wasm ABI and associated SDKs enable developers to build extensions in +any of the supported languages and deliver the plugins as WebAssembly modules at +run-time. This repository contains a standalone runner which serves as a test +harness and simulator for Proxy-Wasm extensions, enabling quick testing in a +controlled environment. + +### Examples + +The basic usage of this test-framework is provided in the examples/ folder which +contains mocking of proxy-wasm modules provided in the proxy-wasm-rust-sdk +examples/. + +## Supported + +- Low-level expectation setting over most host-side functions that are consumed + immediately +- Checking of residual low-level expectations after wasm-function call has + completed +- Various high-level (simulator defaults) expectations that persist across + several host-function calls as opposed to being immediately consumed +- Expectation setting over returns from functions exposed on the proxy-wasm + module + +## In Progress + +- Complete default implementation for all host-side functions +- Low-level expectation setting for the remaining host-side functions +- High-level expectation setting for the remaining host-side functions +- Integration test examples diff --git a/examples/abi_boundary.rs b/examples/abi_boundary.rs new file mode 100644 index 0000000..e65629e --- /dev/null +++ b/examples/abi_boundary.rs @@ -0,0 +1,26 @@ +// Copyright 2020 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use anyhow::Result; +use proxy_wasm_test_framework::utility; +use std::env; + +fn main() -> Result<()> { + let args: Vec = env::args().collect(); + assert_eq!(args.len(), 2); + + let proxy_wasm_module_path = &args[1]; + utility::print_boundary(proxy_wasm_module_path)?; + return Ok(()); +} diff --git a/examples/hello_world.rs b/examples/hello_world.rs new file mode 100644 index 0000000..a865e9f --- /dev/null +++ b/examples/hello_world.rs @@ -0,0 +1,51 @@ +// Copyright 2020 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use anyhow::Result; +use proxy_wasm_test_framework::{tester, types::*}; +use std::env; + +fn main() -> Result<()> { + let args: Vec = env::args().collect(); + assert_eq!(args.len(), 2); + let hello_world_path = &args[1]; + let mut hello_world_test = tester::test(hello_world_path)?; + + hello_world_test + .call_start() + .execute_and_expect(ReturnType::None)?; + + let root_context = 1; + hello_world_test + .call_proxy_on_context_create(root_context, 0) + .execute_and_expect(ReturnType::None)?; + + hello_world_test + .call_proxy_on_vm_start(root_context, 0) + .expect_log(LogLevel::Info, "Hello, World!") + .expect_set_tick_period_millis(5 * 10u64.pow(3)) + .execute_and_expect(ReturnType::Bool(true))?; + + hello_world_test + .call_proxy_on_tick(root_context) + .expect_get_current_time_nanos() + .returning(0 * 10u64.pow(9)) + .execute_and_expect(ReturnType::None)?; + + hello_world_test + .call_proxy_on_tick(root_context) + .execute_and_expect(ReturnType::None)?; + + return Ok(()); +} diff --git a/examples/http_auth_random.rs b/examples/http_auth_random.rs new file mode 100644 index 0000000..2da5ade --- /dev/null +++ b/examples/http_auth_random.rs @@ -0,0 +1,74 @@ +// Copyright 2020 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use anyhow::Result; +use proxy_wasm_test_framework::{tester, types::*}; +use std::env; + +fn main() -> Result<()> { + let args: Vec = env::args().collect(); + assert_eq!(args.len(), 2); + let http_auth_random_path = &args[1]; + let mut http_auth_random = tester::test(http_auth_random_path)?; + + http_auth_random + .call_start() + .execute_and_expect(ReturnType::None)?; + + let root_context = 1; + http_auth_random + .call_proxy_on_context_create(root_context, 0) + .execute_and_expect(ReturnType::None)?; + + let http_context = 2; + http_auth_random + .call_proxy_on_context_create(http_context, root_context) + .execute_and_expect(ReturnType::None)?; + + http_auth_random + .call_proxy_on_request_headers(http_context, 0) + .expect_http_call( + "httpbin", + vec![ + (":method", "GET"), + (":path", "/bytes/1"), + (":authority", "httpbin.org"), + ], + None, + vec![], + 5 * 10u64.pow(3), + ) + .returning(0) + .execute_and_expect(ReturnType::Action(Action::Pause))?; + + let buffer_data = "custom_developer_body"; + http_auth_random + .call_proxy_on_http_call_response(http_context, 0, 0, buffer_data.len() as i32, 0) + .expect_get_buffer_bytes(BufferType::HttpCallResponseBody) + .returning(buffer_data) + .expect_send_local_response( + 403, + Some("Access forbidden.\n"), + vec![("Powered-By", "proxy-wasm")], + -1, + ) + .execute_and_expect(ReturnType::None)?; + + http_auth_random + .call_proxy_on_response_headers(http_context, 0) + .expect_replace_header_map_value(MapType::HttpResponseHeaders, "Powered-By", "proxy-wasm") + .execute_and_expect(ReturnType::Action(Action::Continue))?; + + return Ok(()); +} diff --git a/examples/http_headers.rs b/examples/http_headers.rs new file mode 100644 index 0000000..1a04507 --- /dev/null +++ b/examples/http_headers.rs @@ -0,0 +1,71 @@ +// Copyright 2020 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use anyhow::Result; +use proxy_wasm_test_framework::{tester, types::*}; +use std::env; + +fn main() -> Result<()> { + let args: Vec = env::args().collect(); + assert_eq!(args.len(), 2); + let http_headers_path = &args[1]; + let mut http_headers_test = tester::test(http_headers_path)?; + + http_headers_test + .call_start() + .execute_and_expect(ReturnType::None)?; + + let root_context = 1; + http_headers_test + .call_proxy_on_context_create(root_context, 0) + .execute_and_expect(ReturnType::None)?; + + let http_context = 2; + http_headers_test + .call_proxy_on_context_create(http_context, root_context) + .execute_and_expect(ReturnType::None)?; + + http_headers_test + .call_proxy_on_request_headers(http_context, 0) + .expect_get_header_map_pairs(MapType::HttpRequestHeaders) + .returning(vec![ + (":method", "GET"), + (":path", "/hello"), + (":authority", "developer"), + ]) + .expect_get_header_map_value(MapType::HttpRequestHeaders, ":path") + .returning("/hello") + .expect_send_local_response( + 200, + Some("Hello, World!\n"), + vec![("Hello", "World"), ("Powered-By", "proxy-wasm")], + -1, + ) + .execute_and_expect(ReturnType::Action(Action::Pause))?; + + http_headers_test + .call_proxy_on_response_headers(http_context, 0) + .expect_get_header_map_pairs(MapType::HttpResponseHeaders) + .returning(vec![(":status", "200"), ("Powered-By", "proxy-wasm")]) + .expect_log(LogLevel::Trace, "#2 <- :status: 200") + .expect_log(LogLevel::Trace, "#2 <- Powered-By: proxy-wasm") + .execute_and_expect(ReturnType::Action(Action::Continue))?; + + http_headers_test + .call_proxy_on_log(http_context) + .expect_log(LogLevel::Trace, "#2 completed.") + .execute_and_expect(ReturnType::None)?; + + return Ok(()); +} diff --git a/src/expect_interface.rs b/src/expect_interface.rs new file mode 100644 index 0000000..14f4b85 --- /dev/null +++ b/src/expect_interface.rs @@ -0,0 +1,173 @@ +// Copyright 2020 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use crate::tester::Tester; + +// As of now, the following expectations do not require "fn returning()" implementations and hence +// no structure is provided for them. Setting of these expectations are built directly into tester.rs: +// proxy_log(), proxy_set_tick_period_millis(), proxy_set_buffer_bytes(), proxy_replace_header_map_value(), +// proxy_remove_header_map_value(), proxy_add_header_map_value(), proxy_send_local_response(), etc. + +pub struct ExpectGetCurrentTimeNanos<'a> { + tester: &'a mut Tester, +} + +impl<'a> ExpectGetCurrentTimeNanos<'a> { + pub fn expecting(tester: &'a mut Tester) -> ExpectGetCurrentTimeNanos { + ExpectGetCurrentTimeNanos { tester: tester } + } + + pub fn returning(&mut self, current_time_nanos: u64) -> &mut Tester { + self.tester + .get_expect_handle() + .staged + .set_expect_get_current_time_nanos(current_time_nanos); + self.tester + } +} + +pub struct ExpectGetBufferBytes<'a> { + tester: &'a mut Tester, + buffer_type: i32, +} + +impl<'a> ExpectGetBufferBytes<'a> { + pub fn expecting(tester: &'a mut Tester, buffer_type: i32) -> ExpectGetBufferBytes { + ExpectGetBufferBytes { + tester: tester, + buffer_type: buffer_type, + } + } + + pub fn returning(&mut self, buffer_data: &str) -> &mut Tester { + self.tester + .get_expect_handle() + .staged + .set_expect_get_buffer_bytes(self.buffer_type, buffer_data); + self.tester + } +} + +pub struct ExpectGetHeaderMapPairs<'a> { + tester: &'a mut Tester, + map_type: i32, +} + +impl<'a> ExpectGetHeaderMapPairs<'a> { + pub fn expecting(tester: &'a mut Tester, map_type: i32) -> ExpectGetHeaderMapPairs { + ExpectGetHeaderMapPairs { + tester: tester, + map_type: map_type, + } + } + + pub fn returning(&mut self, header_map_pairs: Vec<(&str, &str)>) -> &mut Tester { + self.tester + .get_expect_handle() + .staged + .set_expect_get_header_map_pairs(self.map_type, header_map_pairs); + self.tester + } +} + +pub struct ExpectSetHeaderMapPairs<'a> { + tester: &'a mut Tester, + map_type: i32, +} + +impl<'a> ExpectSetHeaderMapPairs<'a> { + pub fn expecting(tester: &'a mut Tester, map_type: i32) -> ExpectSetHeaderMapPairs { + ExpectSetHeaderMapPairs { + tester: tester, + map_type: map_type, + } + } + + pub fn returning(&mut self, header_map_pairs: Vec<(&str, &str)>) -> &mut Tester { + self.tester + .get_expect_handle() + .staged + .set_expect_set_header_map_pairs(self.map_type, header_map_pairs); + self.tester + } +} + +pub struct ExpectGetHeaderMapValue<'a> { + tester: &'a mut Tester, + map_type: i32, + header_map_key: &'static str, +} + +impl<'a> ExpectGetHeaderMapValue<'a> { + pub fn expecting( + tester: &'a mut Tester, + map_type: i32, + header_map_key: &'static str, + ) -> ExpectGetHeaderMapValue<'a> { + ExpectGetHeaderMapValue { + tester: tester, + map_type: map_type, + header_map_key: header_map_key, + } + } + + pub fn returning(&mut self, header_map_value: &str) -> &mut Tester { + self.tester + .get_expect_handle() + .staged + .set_expect_get_header_map_value(self.map_type, self.header_map_key, header_map_value); + self.tester + } +} + +pub struct ExpectHttpCall<'a> { + tester: &'a mut Tester, + upstream: &'a str, + headers: Option>, + body: Option<&'a str>, + trailers: Option>, + timeout: u64, +} + +impl<'a> ExpectHttpCall<'a> { + pub fn expecting( + tester: &'a mut Tester, + upstream: &'a str, + headers: Vec<(&'a str, &'a str)>, + body: Option<&'a str>, + trailers: Vec<(&'a str, &'a str)>, + timeout: u64, + ) -> ExpectHttpCall<'a> { + ExpectHttpCall { + tester: tester, + upstream: upstream, + headers: Some(headers), + body: body, + trailers: Some(trailers), + timeout: timeout, + } + } + + pub fn returning(&mut self, token_id: u32) -> &mut Tester { + self.tester.get_expect_handle().staged.set_expect_http_call( + self.upstream, + self.headers.take().unwrap(), + self.body, + self.trailers.take().unwrap(), + self.timeout, + token_id, + ); + self.tester + } +} diff --git a/src/expectations.rs b/src/expectations.rs new file mode 100644 index 0000000..9625453 --- /dev/null +++ b/src/expectations.rs @@ -0,0 +1,417 @@ +// Copyright 2020 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use crate::hostcalls::serial_utils::serialize_map; +use crate::types::*; + +use std::time::{Duration, SystemTime, UNIX_EPOCH}; + +// Global structure for handling low-level expectation structure (staged) +pub struct ExpectHandle { + pub staged: Expect, +} + +impl ExpectHandle { + pub fn new() -> ExpectHandle { + ExpectHandle { + staged: Expect::new(), + } + } + + pub fn update_stage(&mut self) { + self.staged = Expect::new(); + } + + pub fn assert_stage(&self) { + if self.staged.expect_count != 0 { + panic!( + "Function call failed to consume all expectations - total remaining: {}", + self.staged.expect_count + ); + } + } + + pub fn print_staged(&self) { + println!("{:?}", self.staged); + } +} + +// Structure for setting low-level expectations over specific host functions +#[derive(Debug)] +pub struct Expect { + pub expect_count: i32, + log_message: Vec<(i32, String)>, + tick_period_millis: Vec, + current_time_nanos: Vec, + get_buffer_bytes: Vec<(i32, Bytes)>, + set_buffer_bytes: Vec<(i32, Bytes)>, + get_header_map_pairs: Vec<(i32, Bytes)>, + set_header_map_pairs: Vec<(i32, Bytes)>, + get_header_map_value: Vec<(i32, String, String)>, + replace_header_map_value: Vec<(i32, String, String)>, + remove_header_map_value: Vec<(i32, String)>, + add_header_map_value: Vec<(i32, String, String)>, + + send_local_response: Vec<(i32, Option, Bytes, i32)>, + http_call: Vec<(String, Bytes, Option, Bytes, Duration, u32)>, +} + +impl Expect { + pub fn new() -> Expect { + Expect { + expect_count: 0, + log_message: vec![], + tick_period_millis: vec![], + current_time_nanos: vec![], + get_buffer_bytes: vec![], + set_buffer_bytes: vec![], + get_header_map_pairs: vec![], + set_header_map_pairs: vec![], + get_header_map_value: vec![], + replace_header_map_value: vec![], + remove_header_map_value: vec![], + add_header_map_value: vec![], + send_local_response: vec![], + http_call: vec![], + } + } + + pub fn set_expect_log(&mut self, log_level: i32, log_string: &str) { + self.expect_count += 1; + self.log_message.push((log_level, log_string.to_string())); + } + + pub fn get_expect_log(&mut self, log_level: i32) -> Option { + match self.log_message.len() { + 0 => None, + _ => { + self.expect_count -= 1; + assert_eq!(log_level, self.log_message[0].0); + Some(self.log_message.remove(0).1) + } + } + } + + pub fn set_expect_set_tick_period_millis(&mut self, tick_period_millis: u64) { + self.expect_count += 1; + self.tick_period_millis + .push(Duration::from_millis(tick_period_millis)); + } + + pub fn get_expect_set_tick_period_millis(&mut self) -> Option { + match self.tick_period_millis.len() { + 0 => None, + _ => { + self.expect_count -= 1; + Some(self.tick_period_millis.remove(0).as_millis()) + } + } + } + + pub fn set_expect_get_current_time_nanos(&mut self, current_time_nanos: u64) { + self.expect_count += 1; + self.current_time_nanos + .push(UNIX_EPOCH + Duration::from_nanos(current_time_nanos)); + } + + pub fn get_expect_get_current_time_nanos(&mut self) -> Option { + match self.current_time_nanos.len() { + 0 => None, + _ => { + self.expect_count -= 1; + Some( + self.current_time_nanos + .remove(0) + .duration_since(UNIX_EPOCH) + .unwrap() + .as_nanos(), + ) + } + } + } + + pub fn set_expect_get_buffer_bytes(&mut self, buffer_type: i32, buffer_data: &str) { + self.expect_count += 1; + self.get_buffer_bytes + .push((buffer_type, buffer_data.as_bytes().to_vec())); + } + + pub fn get_expect_get_buffer_bytes(&mut self, buffer_type: i32) -> Option { + match self.get_buffer_bytes.len() { + 0 => None, + _ => { + self.expect_count -= 1; + assert_eq!(buffer_type, self.get_buffer_bytes[0].0); + Some(self.get_buffer_bytes.remove(0).1) + } + } + } + + pub fn set_expect_set_buffer_bytes(&mut self, buffer_type: i32, buffer_data: &str) { + self.expect_count += 1; + self.set_buffer_bytes + .push((buffer_type, buffer_data.as_bytes().to_vec())); + } + + pub fn get_expect_set_buffer_bytes(&mut self, buffer_type: i32, buffer_data: &[u8]) { + match self.set_buffer_bytes.len() { + 0 => {} + _ => { + self.expect_count -= 1; + let expect_buffer = self.set_buffer_bytes.remove(0); + assert_eq!(buffer_type, expect_buffer.0); + assert_eq!(buffer_data, &expect_buffer.1[..]) + } + } + } + + pub fn set_expect_get_header_map_pairs( + &mut self, + map_type: i32, + header_map_pairs: Vec<(&str, &str)>, + ) { + self.expect_count += 1; + self.get_header_map_pairs + .push((map_type, serialize_map(header_map_pairs))); + } + + pub fn get_expect_get_header_map_pairs(&mut self, map_type: i32) -> Option { + match self.get_header_map_pairs.len() { + 0 => None, + _ => { + self.expect_count -= 1; + assert_eq!(map_type, self.get_header_map_pairs[0].0); + Some(self.get_header_map_pairs.remove(0).1) + } + } + } + + pub fn set_expect_set_header_map_pairs( + &mut self, + map_type: i32, + header_map_pairs: Vec<(&str, &str)>, + ) { + self.expect_count += 1; + self.set_header_map_pairs + .push((map_type, serialize_map(header_map_pairs))); + } + + pub fn get_expect_set_header_map_pairs(&mut self, map_type: i32) -> Option { + match self.set_header_map_pairs.len() { + 0 => None, + _ => { + self.expect_count -= 1; + assert_eq!(map_type, self.set_header_map_pairs[0].0); + Some(self.set_header_map_pairs.remove(0).1) + } + } + } + + pub fn set_expect_get_header_map_value( + &mut self, + map_type: i32, + header_map_key: &str, + header_map_value: &str, + ) { + self.expect_count += 1; + self.get_header_map_value.push(( + map_type, + header_map_key.to_string(), + header_map_value.to_string(), + )); + } + + pub fn get_expect_get_header_map_value( + &mut self, + map_type: i32, + header_map_key: &str, + ) -> Option { + match self.get_header_map_value.len() { + 0 => None, + _ => { + self.expect_count -= 1; + assert_eq!(map_type, self.get_header_map_value[0].0); + assert_eq!(header_map_key, &self.get_header_map_value[0].1); + Some(self.get_header_map_value.remove(0).2) + } + } + } + + pub fn set_expect_replace_header_map_value( + &mut self, + map_type: i32, + header_map_key: &str, + header_map_value: &str, + ) { + self.expect_count += 1; + self.replace_header_map_value.push(( + map_type, + header_map_key.to_string(), + header_map_value.to_string(), + )); + } + + pub fn get_expect_replace_header_map_value( + &mut self, + map_type: i32, + header_map_key: &str, + header_map_value: &str, + ) { + match self.replace_header_map_value.len() { + 0 => {} + _ => { + self.expect_count -= 1; + let header_map_tuple = self.replace_header_map_value.remove(0); + assert_eq!(map_type, header_map_tuple.0); + assert_eq!(header_map_key, &header_map_tuple.1); + assert_eq!(header_map_value, &header_map_tuple.2); + } + } + } + + pub fn set_expect_remove_header_map_value(&mut self, map_type: i32, header_map_key: &str) { + self.expect_count += 1; + self.remove_header_map_value + .push((map_type, header_map_key.to_string())); + } + + pub fn get_expect_remove_header_map_value(&mut self, map_type: i32, header_map_key: &str) { + match self.remove_header_map_value.len() { + 0 => {} + _ => { + self.expect_count -= 1; + let header_map_tuple = self.remove_header_map_value.remove(0); + assert_eq!(map_type, header_map_tuple.0); + assert_eq!(header_map_key, &header_map_tuple.1); + } + } + } + + pub fn set_expect_add_header_map_value( + &mut self, + map_type: i32, + header_map_key: &str, + header_map_value: &str, + ) { + self.expect_count += 1; + self.add_header_map_value.push(( + map_type, + header_map_key.to_string(), + header_map_value.to_string(), + )); + } + + pub fn get_expect_add_header_map_value( + &mut self, + map_type: i32, + header_map_key: &str, + header_map_value: &str, + ) { + match self.add_header_map_value.len() { + 0 => {} + _ => { + self.expect_count -= 1; + let header_map_tuple = self.add_header_map_value.remove(0); + assert_eq!(map_type, header_map_tuple.0); + assert_eq!(header_map_key, &header_map_tuple.1); + assert_eq!(header_map_value, &header_map_tuple.2); + } + } + } + + pub fn set_expect_send_local_response( + &mut self, + status_code: i32, + body: Option<&str>, + headers: Vec<(&str, &str)>, + grpc_status: i32, + ) { + self.expect_count += 1; + self.send_local_response.push(( + status_code, + body.map(|data| data.to_string()), + serialize_map(headers), + grpc_status, + )) + } + + pub fn get_expect_send_local_response( + &mut self, + status_code: i32, + body: Option<&str>, + headers: Bytes, + grpc_status: i32, + ) { + match self.send_local_response.len() { + 0 => {} + _ => { + self.expect_count -= 1; + let local_response_tuple = self.send_local_response.remove(0); + assert_eq!(status_code, local_response_tuple.0); + assert_eq!( + body.unwrap_or("default"), + &local_response_tuple.1.unwrap_or(String::from("default")) + ); + assert_eq!(headers, local_response_tuple.2); + assert_eq!(grpc_status, local_response_tuple.3); + } + } + } + + pub fn set_expect_http_call( + &mut self, + upstream: &str, + headers: Vec<(&str, &str)>, + body: Option<&str>, + trailers: Vec<(&str, &str)>, + timeout: u64, + token_id: u32, + ) { + self.expect_count += 1; + self.http_call.push(( + upstream.to_string(), + serialize_map(headers), + body.map(|data| data.to_string()), + serialize_map(trailers), + Duration::from_millis(timeout), + token_id, + )); + } + + pub fn get_expect_http_call( + &mut self, + upstream: &str, + headers: &[u8], + body: Option<&str>, + trailers: &[u8], + timeout: i32, + ) -> Option { + match self.http_call.len() { + 0 => None, + _ => { + self.expect_count -= 1; + let http_call_tuple = self.http_call.remove(0); + assert_eq!(upstream, &http_call_tuple.0); + assert_eq!(headers, &http_call_tuple.1[..]); + assert_eq!( + body.unwrap_or("default"), + &http_call_tuple.2.unwrap_or(String::from("default")) + ); + assert_eq!(trailers, &http_call_tuple.3[..]); + assert_eq!(timeout, http_call_tuple.4.as_millis() as i32); + Some(http_call_tuple.5) + } + } + } +} diff --git a/src/host_settings.rs b/src/host_settings.rs new file mode 100644 index 0000000..a27fff8 --- /dev/null +++ b/src/host_settings.rs @@ -0,0 +1,247 @@ +// Copyright 2020 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use crate::hostcalls::serial_utils::serialize_map; +use crate::types::*; + +use std::collections::HashMap; +use std::time::Duration; + +// Global structure for handling default host behaviour (and high-level expectation setting) +pub struct HostHandle { + pub staged: HostSettings, +} + +impl HostHandle { + pub fn new() -> HostHandle { + HostHandle { + staged: HostSettings::new(AbiVersion::None), + } + } + + pub fn reset(&mut self, abi_version: AbiVersion) { + self.staged = HostSettings::new(abi_version); + } + + pub fn print_staged(&self) { + println!("{:?}", self.staged); + } +} + +// Global struct for host environment default settings +#[derive(Debug)] +pub struct HostSettings { + abi_version: AbiVersion, + tick_period_millis: Duration, + header_map_pairs: HashMap>, + buffer_bytes: HashMap, +} + +impl HostSettings { + pub fn new(abi_version: AbiVersion) -> HostSettings { + HostSettings { + abi_version: abi_version, + tick_period_millis: Duration::new(0, 0), + header_map_pairs: default_header_map_pairs(), + buffer_bytes: default_buffer_bytes(), + } + } + + pub fn set_abi_version(&mut self, abi_version: AbiVersion) { + self.abi_version = abi_version; + } + + pub fn get_abi_version(&mut self) -> AbiVersion { + self.abi_version + } + + pub fn reset_tick_period_millis(&mut self) { + self.tick_period_millis = Duration::from_millis(0u64); + } + + pub fn set_tick_period_millis(&mut self, tick_period_millis: u64) { + self.tick_period_millis = Duration::from_millis(tick_period_millis); + } + + pub fn get_tick_period_millis(&self) -> u128 { + self.tick_period_millis.as_millis() + } + + pub fn reset_buffer_bytes(&mut self) { + self.buffer_bytes = default_buffer_bytes(); + } + + pub fn set_buffer_bytes(&mut self, buffer_type: i32, buffer_data: &str) { + self.buffer_bytes + .insert(buffer_type, buffer_data.as_bytes().to_vec()); + } + + pub fn get_buffer_bytes(&self, buffer_type: i32) -> Bytes { + let buffer_data = self.buffer_bytes.get(&buffer_type).unwrap().clone(); + buffer_data + } + + pub fn reset_header_map_pairs(&mut self) { + self.header_map_pairs = default_header_map_pairs(); + } + + pub fn set_header_map_pairs(&mut self, map_type: i32, header_map_pairs: Vec<(&str, &str)>) { + let mut header_map = HashMap::new(); + for (header_map_key, header_map_value) in header_map_pairs.into_iter() { + header_map.insert(header_map_key.to_string(), header_map_value.to_string()); + } + self.header_map_pairs.insert(map_type, header_map); + } + + pub fn get_header_map_pairs(&self, map_type: i32) -> Bytes { + let header_map_pairs = self.header_map_pairs.get(&map_type).unwrap(); + let header_map_pairs = header_map_pairs + .iter() + .map(|(k, v)| (k as &str, v as &str)) + .collect(); + serialize_map(header_map_pairs) + } + + pub fn get_header_map_value(&self, map_type: i32, header_map_key: &str) -> Option { + let header_map = self.header_map_pairs.get(&map_type).unwrap(); + let header_map_value = header_map.get(header_map_key); + header_map_value.map(|str_val| str_val.to_string()) + } + + pub fn replace_header_map_value( + &mut self, + map_type: i32, + header_map_key: &str, + header_map_value: &str, + ) { + let header_map = self.header_map_pairs.get_mut(&map_type).unwrap(); + header_map.insert(header_map_key.to_string(), header_map_value.to_string()); + } + + pub fn remove_header_map_value(&mut self, map_type: i32, header_map_key: &str) { + let header_map = self.header_map_pairs.get_mut(&map_type).unwrap(); + header_map.remove(header_map_key); + } + + pub fn add_header_map_value( + &mut self, + map_type: i32, + header_map_key: &str, + header_map_value: &str, + ) { + let header_map = self.header_map_pairs.get_mut(&map_type).unwrap(); + header_map.insert(header_map_key.to_string(), header_map_value.to_string()); + } +} + +// functions to retrieve default values +pub fn default_header_map_pairs() -> HashMap> { + let mut default_header_maps = HashMap::new(); + + let mut http_on_request_headers = HashMap::new(); + http_on_request_headers.insert(":method".to_string(), "GET".to_string()); + http_on_request_headers.insert( + ":path".to_string(), + "/default/request/headers/path".to_string(), + ); + http_on_request_headers.insert(":authority".to_string(), "abi_test_harness".to_string()); + default_header_maps.insert(MapType::HttpRequestHeaders as i32, http_on_request_headers); + + let mut http_on_request_trailers = HashMap::new(); + http_on_request_trailers.insert(":method".to_string(), "GET".to_string()); + http_on_request_trailers.insert( + ":path".to_string(), + "/default/request/trailers/path".to_string(), + ); + http_on_request_trailers.insert(":authority".to_string(), "abi_test_harness".to_string()); + default_header_maps.insert( + MapType::HttpRequestTrailers as i32, + http_on_request_trailers, + ); + + let mut http_on_response_headers = HashMap::new(); + http_on_response_headers.insert(":method".to_string(), "GET".to_string()); + http_on_response_headers.insert( + ":path".to_string(), + "/default/response/headers/path".to_string(), + ); + http_on_response_headers.insert(":authority".to_string(), "abi_test_harness".to_string()); + default_header_maps.insert( + MapType::HttpResponseHeaders as i32, + http_on_response_headers, + ); + + let mut http_on_response_trailers = HashMap::new(); + http_on_response_trailers.insert(":method".to_string(), "GET".to_string()); + http_on_response_trailers.insert( + ":path".to_string(), + "/default/response/trailers/path".to_string(), + ); + http_on_response_trailers.insert(":authority".to_string(), "abi_test_harness".to_string()); + default_header_maps.insert( + MapType::HttpResponseTrailers as i32, + http_on_response_trailers, + ); + + let mut http_call_response_headers = HashMap::new(); + http_call_response_headers.insert(":method".to_string(), "GET".to_string()); + http_call_response_headers.insert( + ":path".to_string(), + "/default/call/response/headers/path".to_string(), + ); + http_call_response_headers.insert(":authority".to_string(), "abi_test_harness".to_string()); + default_header_maps.insert( + MapType::HttpCallResponseHeaders as i32, + http_call_response_headers, + ); + + let mut http_call_response_trailers = HashMap::new(); + http_call_response_trailers.insert(":method".to_string(), "GET".to_string()); + http_call_response_trailers.insert( + ":path".to_string(), + "/default/call/response/trailers/path".to_string(), + ); + http_call_response_trailers.insert(":authority".to_string(), "abi_test_harness".to_string()); + default_header_maps.insert( + MapType::HttpCallResponseTrailers as i32, + http_call_response_trailers, + ); + + default_header_maps +} + +pub fn default_buffer_bytes() -> HashMap { + let mut default_bytes = HashMap::new(); + default_bytes.insert( + BufferType::HttpRequestBody as i32, + "default_http_request_body".as_bytes().to_vec(), + ); + default_bytes.insert( + BufferType::HttpResponseBody as i32, + "default_http_response_body".as_bytes().to_vec(), + ); + default_bytes.insert( + BufferType::DownstreamData as i32, + "default_downstream_data".as_bytes().to_vec(), + ); + default_bytes.insert( + BufferType::UpstreamData as i32, + "default_upstream_data".as_bytes().to_vec(), + ); + default_bytes.insert( + BufferType::HttpCallResponseBody as i32, + "default_call_response_body".as_bytes().to_vec(), + ); + default_bytes +} diff --git a/src/hostcalls.rs b/src/hostcalls.rs new file mode 100644 index 0000000..5701929 --- /dev/null +++ b/src/hostcalls.rs @@ -0,0 +1,1089 @@ +// Copyright 2020 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use crate::expectations::ExpectHandle; +use crate::host_settings::HostHandle; +use crate::types::*; + +use lazy_static::lazy_static; +use more_asserts::*; +use std::sync::{Arc, Mutex}; +use std::time::{SystemTime, UNIX_EPOCH}; +use wasmtime::*; + +lazy_static! { + static ref HOST: Arc> = Arc::new(Mutex::new(HostHandle::new())); + static ref EXPECT: Arc> = Arc::new(Mutex::new(ExpectHandle::new())); +} + +pub fn get_abi_version(module: &Module) -> AbiVersion { + if module.get_export("proxy_abi_version_0_1_0") != None { + AbiVersion::ProxyAbiVersion0_1_0 + } else if module.get_export("proxy_abi_version_0_2_0") != None { + AbiVersion::ProxyAbiVersion0_2_0 + } else { + panic!("test-framework does not support proxy-wasm modules of this abi version"); + } +} + +pub fn generate_import_list( + store: &Store, + module: &Module, + func_vec: Arc>>, +) -> (Arc>, Arc>) { + let abi_version = get_abi_version(module); + HOST.lock().unwrap().staged.set_abi_version(abi_version); + let imports = module.imports(); + for import in imports { + match get_hostfunc(&store, abi_version, &import) { + Some(func) => (*func_vec).lock().unwrap().push(func.into()), + None => panic!("Failed to acquire \"{}\" from get_hostfunc() in src/hostcalls.rs --> check configuration", import.name()) + } + } + (HOST.clone(), EXPECT.clone()) +} + +fn get_hostfunc(store: &Store, _abi_version: AbiVersion, import: &ImportType) -> Option { + match import.name() { + "proxy_log" => { + Some(Func::wrap( + &store, + |caller: Caller<'_>, level: i32, message_data: i32, message_size: i32| -> i32 { + // Default Function: retrieve and display log message from proxy-wasm module + // Expectation: ensure the log level and the message data are as expected + let mem = match caller.get_export("memory") { + Some(Extern::Memory(mem)) => mem, + _ => { + println!("=> Error: proxy_log cannot get_export \"memory\""); + return Status::InternalFailure as i32; + } + }; + + unsafe { + let data = mem + .data_unchecked() + .get(message_data as u32 as usize..) + .and_then(|arr| arr.get(..message_size as u32 as usize)); + + let string_msg = + data.map(|string_msg| std::str::from_utf8(string_msg).unwrap()); + let string_msg = match string_msg { + Some(s) => s, + _ => "invalid utf-8 slice", + }; + + if let Some(expect_log_string) = + EXPECT.lock().unwrap().staged.get_expect_log(level) + { + assert_eq!(string_msg.to_string(), expect_log_string); + } + + println!( + "=> proxy_log | Level: {} | Message: {}", + level, string_msg + ); + } + return Status::Ok as i32; + }, + )) + } + + "proxy_get_current_time_nanoseconds" => { + Some(Func::wrap( + &store, + |caller: Caller<'_>, return_time: i32| -> i32 { + // Default Function: respond to proxy-wasm module with the current time + // Expectation: respond with a pre-set expected time + let mem = match caller.get_export("memory") { + Some(Extern::Memory(mem)) => mem, + _ => { + println!("=> Error: proxy_get_current_time_nanoseconds cannot get export \"memory\""); + return Status::InternalFailure as i32; + } + }; + + let time = match EXPECT + .lock() + .unwrap() + .staged + .get_expect_get_current_time_nanos() + { + Some(current_time_nanos) => current_time_nanos as u64, + None => SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_nanos() as u64, + }; + + unsafe { + let data = mem.data_unchecked_mut().get_unchecked_mut( + return_time as u32 as usize..return_time as u32 as usize + 8, + ); + + data.copy_from_slice(&time.to_le_bytes()); + } + return Status::Ok as i32; + }, + )) + } + + "proxy_set_tick_period_milliseconds" => { + Some(Func::wrap( + &store, + |_caller: Caller<'_>, period: i32| -> i32 { + // Default Function: receive and store tick period from proxy-wasm module + // Expectation: assert received tick period is equal to expected + HOST.lock() + .unwrap() + .staged + .set_tick_period_millis(period as u64); + if let Some(tick_period_millis) = EXPECT + .lock() + .unwrap() + .staged + .get_expect_set_tick_period_millis() + { + assert_eq!(tick_period_millis, period as u128); + } + return Status::Ok as i32; + }, + )) + } + + "proxy_get_configuration" => { + Some(Func::wrap( + &store, + |_caller: Caller<'_>, _return_buffer_data: i32, _return_buffer_size: i32| -> i32 { + // Default Function: + // Expectation: + println!("- proxy_get_configuration | "); + + return Status::InternalFailure as i32; + }, + )) + } + + "proxy_get_buffer_bytes" => { + Some(Func::wrap( + &store, + |caller: Caller<'_>, + buffer_type: i32, + start: i32, + max_size: i32, + return_buffer_data: i32, + return_buffer_size: i32| + -> i32 { + // Default Function: generate and return random buffer_bytes of length max_size - start + // Expectation: return buffer bytes set in expectation + let mem = match caller.get_export("memory") { + Some(Extern::Memory(mem)) => mem, + _ => { + println!( + "=> Error: proxy_get_buffer_bytes cannot get export \"memory\"" + ); + return Status::InternalFailure as i32; + } + }; + + let malloc = match caller.get_export("malloc") { + Some(Extern::Func(func)) => func.get1::().unwrap(), + _ => { + println!( + "=> Error: proxy_get_buffer_bytes cannot get export \"malloc\"" + ); + return Status::InternalFailure as i32; + } + }; + + let response_body = match EXPECT + .lock() + .unwrap() + .staged + .get_expect_get_buffer_bytes(buffer_type) + { + Some(expect_buffer_bytes) => { + assert_le!(expect_buffer_bytes.len(), (max_size - start) as usize); + expect_buffer_bytes + } + None => { + let buffer_bytes: Bytes; + let host_buffer_bytes = + HOST.lock().unwrap().staged.get_buffer_bytes(buffer_type); + if host_buffer_bytes.len() == (max_size - start) as usize { + buffer_bytes = host_buffer_bytes; + } else { + buffer_bytes = serial_utils::generate_random_string( + (max_size - start) as usize, + ) + .as_bytes() + .to_vec(); + } + buffer_bytes + } + }; + + unsafe { + let return_buffer_size_ptr = mem.data_unchecked_mut().get_unchecked_mut( + return_buffer_size as u32 as usize + ..return_buffer_size as u32 as usize + 4, + ); + + let return_buffer_data_ptr = mem.data_unchecked_mut().get_unchecked_mut( + return_buffer_data as u32 as usize + ..return_buffer_data as u32 as usize + 4, + ); + + // allocate memory and store buffer bytes + let buffer_data_add = + malloc(response_body.len() as i32).unwrap() as u32 as usize; + let buffer_data_ptr = mem.data_unchecked_mut().get_unchecked_mut( + buffer_data_add..buffer_data_add + response_body.len(), + ); + buffer_data_ptr.copy_from_slice(&response_body); + + return_buffer_size_ptr + .copy_from_slice(&(response_body.len() as u32).to_le_bytes()); + return_buffer_data_ptr + .copy_from_slice(&(buffer_data_add as u32).to_le_bytes()); + } + return Status::Ok as i32; + }, + )) + } + + "proxy_set_buffer_bytes" => { + Some(Func::wrap( + &store, + |caller: Caller<'_>, + buffer_type: i32, + start: i32, + size: i32, + buffer_data: i32, + buffer_size: i32| + -> i32 { + // Default Function: set received buffer data as default + // Expectation: assert that the received buffer bytes is as expected + let mem = match caller.get_export("memory") { + Some(Extern::Memory(mem)) => mem, + _ => { + println!( + "=> Error: proxy_set_buffer_bytes cannot get export \"memory\"" + ); + return Status::InternalFailure as i32; + } + }; + + unsafe { + let buffer_data_ptr = mem.data_unchecked().get_unchecked( + buffer_data as u32 as usize + ..(buffer_data + buffer_size) as u32 as usize, + ); + assert_ge!(buffer_data_ptr.len(), (start + size) as usize); + + EXPECT.lock().unwrap().staged.get_expect_set_buffer_bytes( + buffer_type, + &buffer_data_ptr[start as usize..(start + size) as usize], + ); + HOST.lock().unwrap().staged.set_buffer_bytes( + buffer_type, + std::str::from_utf8( + &buffer_data_ptr[start as usize..(start + size) as usize], + ) + .unwrap(), + ); + } + return Status::Ok as i32; + }, + )) + } + + "proxy_get_header_map_pairs" => { + Some(Func::wrap( + &store, + |caller: Caller<'_>, + map_type: i32, + return_map_data: i32, + return_map_size: i32| + -> i32 { + // Default Function: respond with default header map pairs depending on map_type + // Expectation: respond with set expected header map pairs + let mem = match caller.get_export("memory") { + Some(Extern::Memory(mem)) => mem, + _ => { + println!("=> Error: proxy_get_header_map_pairs cannot get export \"memory\""); + return Status::InternalFailure as i32; + } + }; + + let malloc = match caller.get_export("malloc") { + Some(Extern::Func(func)) => func.get1::().unwrap(), + _ => { + println!("=> Error: proxy_get_header_map_pairs cannot get export \"malloc\""); + return Status::InternalFailure as i32; + } + }; + + let serial_map = match EXPECT + .lock() + .unwrap() + .staged + .get_expect_get_header_map_pairs(map_type) + { + Some(header_map_pairs) => header_map_pairs, + None => HOST.lock().unwrap().staged.get_header_map_pairs(map_type), + }; + let serial_map_size = serial_map.len(); + + unsafe { + let return_map_size_ptr = mem.data_unchecked_mut().get_unchecked_mut( + return_map_size as u32 as usize..return_map_size as u32 as usize + 4, + ); + + let return_map_data_ptr = mem.data_unchecked_mut().get_unchecked_mut( + return_map_data as u32 as usize..return_map_data as u32 as usize + 4, + ); + + let map_data_add = malloc(serial_map_size as i32).unwrap() as u32 as usize; + let map_data_ptr = mem + .data_unchecked_mut() + .get_unchecked_mut(map_data_add..map_data_add + serial_map_size); + map_data_ptr.copy_from_slice(&serial_map); + + return_map_data_ptr.copy_from_slice(&(map_data_add as u32).to_le_bytes()); + return_map_size_ptr + .copy_from_slice(&(serial_map_size as u32).to_le_bytes()); + } + return Status::Ok as i32; + }, + )) + } + + "proxy_set_header_map_pairs" => { + Some(Func::wrap( + &store, + |caller: Caller<'_>, map_type: i32, map_data: i32, map_size: i32| -> i32 { + // Default Function: Reads and sets the according header map as the simulator default for the given map type + // Expectation: asserts that the received header map and header map type corresponds to the expected one + let mem = match caller.get_export("memory") { + Some(Extern::Memory(mem)) => mem, + _ => { + println!("=> Error: proxy_set_header_map_pairs cannot get export \"memory\""); + return Status::InternalFailure as i32; + } + }; + + unsafe { + let header_map_ptr = mem.data_unchecked().get_unchecked( + map_data as u32 as usize..(map_data + map_size) as u32 as usize, + ); + + HOST.lock().unwrap().staged.set_header_map_pairs( + map_type, + serial_utils::deserialize_map(header_map_ptr) + .iter() + .map(|(k, v)| (k as &str, v as &str)) + .collect(), + ); + + if let Some(expect_header_map) = EXPECT + .lock() + .unwrap() + .staged + .get_expect_set_header_map_pairs(map_type) + { + assert_eq!(expect_header_map, header_map_ptr.to_vec()) + } + } + return Status::Ok as i32; + }, + )) + } + + "proxy_get_header_map_value" => { + Some(Func::wrap( + &store, + |caller: Caller<'_>, + map_type: i32, + key_data: i32, + key_size: i32, + return_value_data: i32, + return_value_size: i32| + -> i32 { + // Default Function: respond with a default header map value corresponding to map_type (if exists) + // Expectation: respond with set expected header map value for the given key and map_type + // Panics if there is no header map value in expectation or host simulator for the provided map_type and key + let mem = match caller.get_export("memory") { + Some(Extern::Memory(mem)) => mem, + _ => { + println!("=> Error: proxy_get_header_map_value cannot get export \"memory\""); + return Status::InternalFailure as i32; + } + }; + + let malloc = match caller.get_export("malloc") { + Some(Extern::Func(func)) => func.get1::().unwrap(), + _ => { + println!("=> Error: proxy_get_header_map_value cannot get export \"malloc\""); + return Status::InternalFailure as i32; + } + }; + + unsafe { + let key_data_ptr = mem + .data_unchecked() + .get(key_data as u32 as usize..) + .and_then(|arr| arr.get(..key_size as u32 as usize)); + let string_key = key_data_ptr + .map(|string_msg| std::str::from_utf8(string_msg).unwrap()) + .unwrap(); + + let string_value = match EXPECT + .lock() + .unwrap() + .staged + .get_expect_get_header_map_value(map_type, string_key) + { + Some(expect_string_value) => expect_string_value, + None => { + match HOST.lock().unwrap().staged.get_header_map_value(map_type, &string_key) { + Some(host_string_value) => host_string_value, + None => panic!("=> Error: proxy_get_header_map_value | no header map value for key {}", string_key)} + } + }; + + let value_data_add = + malloc(string_value.len() as i32).unwrap() as u32 as usize; + let value_data_ptr = mem + .data_unchecked_mut() + .get_unchecked_mut(value_data_add..value_data_add + string_value.len()); + value_data_ptr.copy_from_slice((&string_value).as_bytes()); + + let return_value_data_ptr = mem.data_unchecked_mut().get_unchecked_mut( + return_value_data as u32 as usize + ..return_value_data as u32 as usize + 4, + ); + + let return_value_size_ptr = mem.data_unchecked_mut().get_unchecked_mut( + return_value_size as u32 as usize + ..return_value_size as u32 as usize + 4, + ); + + return_value_data_ptr + .copy_from_slice(&(value_data_add as u32).to_le_bytes()); + return_value_size_ptr + .copy_from_slice(&(string_value.len() as u32).to_le_bytes()); + } + + return Status::Ok as i32; + }, + )) + } + + "proxy_replace_header_map_value" => { + Some(Func::wrap( + &store, + |caller: Caller<'_>, + map_type: i32, + key_data: i32, + key_size: i32, + value_data: i32, + value_size: i32| + -> i32 { + // Default Function: replace the specified key-value pair in the default host environment if it exists + // Expectation: assert that the received key-value pair are as expected + let mem = match caller.get_export("memory") { + Some(Extern::Memory(mem)) => mem, + _ => { + println!("=> Error: proxy_replace_header_map_value cannot get export \"memory\""); + return Status::InternalFailure as i32; + } + }; + + unsafe { + let key_data_ptr = mem + .data_unchecked() + .get(key_data as u32 as usize..) + .and_then(|arr| arr.get(..key_size as u32 as usize)); + let string_key = key_data_ptr + .map(|string_msg| std::str::from_utf8(string_msg).unwrap()) + .unwrap(); + + let value_data_ptr = mem + .data_unchecked() + .get(value_data as u32 as usize..) + .and_then(|arr| arr.get(..value_size as u32 as usize)); + let string_value = value_data_ptr + .map(|string_msg| std::str::from_utf8(string_msg).unwrap()) + .unwrap(); + + EXPECT + .lock() + .unwrap() + .staged + .get_expect_replace_header_map_value( + map_type, + string_key, + string_value, + ); + HOST.lock().unwrap().staged.replace_header_map_value( + map_type, + string_key, + string_value, + ); + } + + return Status::Ok as i32; + }, + )) + } + + "proxy_remove_header_map_value" => { + Some(Func::wrap( + &store, + |caller: Caller<'_>, map_type: i32, key_data: i32, key_size: i32| -> i32 { + // Default Function: remove the specified key-value pair in the default host environment if it exists + // Expectation: assert that the received key is as expected + let mem = match caller.get_export("memory") { + Some(Extern::Memory(mem)) => mem, + _ => { + println!("=> Error: proxy_remove_header_map_value cannot get export \"memory\""); + return Status::InternalFailure as i32; + } + }; + + unsafe { + let key_data_ptr = mem + .data_unchecked() + .get(key_data as u32 as usize..) + .and_then(|arr| arr.get(..key_size as u32 as usize)); + let string_key = key_data_ptr + .map(|string_msg| std::str::from_utf8(string_msg).unwrap()) + .unwrap(); + + EXPECT + .lock() + .unwrap() + .staged + .get_expect_remove_header_map_value(map_type, string_key); + HOST.lock() + .unwrap() + .staged + .remove_header_map_value(map_type, string_key); + } + + return Status::Ok as i32; + }, + )) + } + + "proxy_add_header_map_value" => { + Some(Func::wrap( + &store, + |caller: Caller<'_>, + map_type: i32, + key_data: i32, + key_size: i32, + value_data: i32, + value_size: i32| + -> i32 { + // Default Function: add the specified key-value pair in the default host environment if it exists + // Expectation: assert that the received key-value pair are as expected + let mem = match caller.get_export("memory") { + Some(Extern::Memory(mem)) => mem, + _ => { + println!("=> Error: proxy_add_header_map_value cannot get export \"memory\""); + return Status::InternalFailure as i32; + } + }; + + unsafe { + let key_data_ptr = mem + .data_unchecked() + .get(key_data as u32 as usize..) + .and_then(|arr| arr.get(..key_size as u32 as usize)); + let string_key = key_data_ptr + .map(|string_msg| std::str::from_utf8(string_msg).unwrap()) + .unwrap(); + + let value_data_ptr = mem + .data_unchecked() + .get(value_data as u32 as usize..) + .and_then(|arr| arr.get(..value_size as u32 as usize)); + let string_value = value_data_ptr + .map(|string_msg| std::str::from_utf8(string_msg).unwrap()) + .unwrap(); + + EXPECT + .lock() + .unwrap() + .staged + .get_expect_add_header_map_value(map_type, string_key, string_value); + HOST.lock().unwrap().staged.add_header_map_value( + map_type, + string_key, + string_value, + ); + } + + return Status::Ok as i32; + }, + )) + } + + "proxy_get_property" => { + Some(Func::wrap( + &store, + |_caller: Caller<'_>, + _path_data: i32, + _path_size: i32, + _return_value_data: i32, + _return_value_size: i32| + -> i32 { + // Default Function: + // Expectation: + println!("=> proxy_get_property | "); + return Status::InternalFailure as i32; + }, + )) + } + + "proxy_set_property" => { + Some(Func::wrap( + &store, + |_caller: Caller<'_>, + _path_data: i32, + _path_size: i32, + _value_data: i32, + _value_size: i32| + -> i32 { + // Default Function: + // Expectation: + println!("=> proxy_set_property | "); + return Status::InternalFailure as i32; + }, + )) + } + + "proxy_get_shared_data" => { + Some(Func::wrap( + &store, + |_caller: Caller<'_>, + _key_data: i32, + _key_size: i32, + _return_value_data: i32, + _return_value_size: i32, + _return_cas: i32| + -> i32 { + // Default Function: + // Expectation: + println!("=> proxy_get_shared_data | "); + return Status::InternalFailure as i32; + }, + )) + } + + "proxy_set_shared_data" => { + Some(Func::wrap( + &store, + |_caller: Caller<'_>, + _key_data: i32, + _key_size: i32, + _value_data: i32, + _value_size: i32, + _cas: i32| + -> i32 { + // Default Function: + // Expectation: + println!("=> proxy_set_shared_data | "); + return Status::InternalFailure as i32; + }, + )) + } + + "proxy_register_shared_queue" => { + Some(Func::wrap( + &store, + |_caller: Caller<'_>, _name_data: i32, _name_size: i32, _return_id: i32| -> i32 { + // Default Function: + // Expectation: + println!("=> proxy_register_shared_queue | "); + return Status::InternalFailure as i32; + }, + )) + } + + "proxy_resolve_shared_queue" => { + Some(Func::wrap( + &store, + |_caller: Caller<'_>, + _vm_id_data: i32, + _vm_id_size: i32, + _name_data: i32, + _name_size: i32, + _return_id: i32| + -> i32 { + // Default Function: + // Expectation: + println!("=> proxy_resolve_shared_queue | "); + return Status::InternalFailure as i32; + }, + )) + } + + "proxy_dequeue_shared_queue" => { + Some(Func::wrap( + &store, + |_caller: Caller<'_>, + _queue_id: i32, + _payload_data: i32, + _payload_size: i32| + -> i32 { + // Default Function: + // Expectation: + println!("=> proxy_dequeue_shared_queue |"); + return Status::InternalFailure as i32; + }, + )) + } + + "proxy_enqueue_shared_queue" => { + Some(Func::wrap( + &store, + |_caller: Caller<'_>, _queue_id: i32, _value_data: i32, _value_size: i32| -> i32 { + // Default Function: + // Expectation: + println!("=> proxy_enqueue_shared_queue | "); + return Status::InternalFailure as i32; + }, + )) + } + + "proxy_continue_stream" => { + Some(Func::wrap(&store, |_caller: Caller<'_>| -> i32 { + // Default Function: + // Expectation: + println!("=> proxy_continue_stream | continuing stream"); + return Status::Ok as i32; + })) + } + + "proxy_close_stream" => { + Some(Func::wrap(&store, |_caller: Caller<'_>| -> i32 { + // Default Function: + // Expectation: + println!("=> proxy_close_stream | closing stream"); + return Status::Ok as i32; + })) + } + + "proxy_continue_request" => { + Some(Func::wrap(&store, |_caller: Caller<'_>| -> i32 { + // Default Function: + // Expectation: + println!("=> proxy_continue_request | continuing request"); + return Status::Ok as i32; + })) + } + + "proxy_continue_response" => { + Some(Func::wrap(&store, |_caller: Caller<'_>| -> i32 { + // Default Function: + // Expectation: + println!("=> proxy_continue_response | continuing reponse"); + return Status::Ok as i32; + })) + } + + "proxy_send_local_response" => { + Some(Func::wrap( + &store, + |caller: Caller<'_>, + status_code: i32, + _status_code_details_data: i32, + _status_code_details_size: i32, + body_data: i32, + body_size: i32, + headers_data: i32, + headers_size: i32, + grpc_status: i32| + -> i32 { + // Default Function: receive and display local response + // Expectation: assert equal the received local response with the expected one + let mem = match caller.get_export("memory") { + Some(Extern::Memory(mem)) => mem, + _ => { + println!("=> Error: proxy_send_local_response cannot get export \"memory\""); + return Status::InternalFailure as i32; + } + }; + + unsafe { + let mut string_body: Option<&str> = None; + if body_size > 0 { + let body_data_ptr = mem + .data_unchecked() + .get(body_data as u32 as usize..) + .and_then(|arr| arr.get(..body_size as u32 as usize)); + string_body = body_data_ptr + .map(|string_msg| std::str::from_utf8(string_msg).unwrap()); + } + + let header_data_ptr = mem.data_unchecked().get_unchecked( + headers_data as u32 as usize + ..headers_data as u32 as usize + headers_size as u32 as usize, + ); + let deserialized_header = serial_utils::deserialize_map(header_data_ptr); + + EXPECT + .lock() + .unwrap() + .staged + .get_expect_send_local_response( + status_code, + string_body, + header_data_ptr.to_vec(), + grpc_status, + ); + + println!( + "=> proxy_send_local_response | status_code: {}", + status_code + ); + println!( + " | body_data: {}", + string_body.unwrap_or("None") + ); + println!( + " | headers_data: {:?}", + deserialized_header + ); + println!( + " | grpc_status: {}", + grpc_status + ); + } + return Status::Ok as i32; + }, + )) + } + + "proxy_clear_route_cache" => { + Some(Func::wrap(&store, |_caller: Caller<'_>| -> i32 { + // Default Function: + // Expectation: + println!("=> proxy_clear_route_cache | "); + return Status::InternalFailure as i32; + })) + } + + "proxy_http_call" => { + Some(Func::wrap( + &store, + |caller: Caller<'_>, + upstream_data: i32, + upstream_size: i32, + headers_data: i32, + headers_size: i32, + body_data: i32, + body_size: i32, + trailers_data: i32, + trailers_size: i32, + timeout: i32, + return_token: i32| + -> i32 { + // Default Function: receives and displays http call from proxy-wasm module + // Expectation: asserts equal the receieved http call with the expected one + let mem = match caller.get_export("memory") { + Some(Extern::Memory(mem)) => mem, + _ => { + println!("=> Error: proxy_http_call cannot get export \"memory\""); + return Status::InternalFailure as i32; + } + }; + + // expectation description not implemented yet + unsafe { + let upstream_data_ptr = mem + .data_unchecked() + .get(upstream_data as u32 as usize..) + .and_then(|arr| arr.get(..upstream_size as u32 as usize)); + let string_upstream = upstream_data_ptr + .map(|string_msg| std::str::from_utf8(string_msg).unwrap()) + .unwrap(); + + let mut string_body: Option<&str> = None; + if body_size > 0 { + let body_data_ptr = mem + .data_unchecked() + .get(body_data as u32 as usize..) + .and_then(|arr| arr.get(..body_size as u32 as usize)); + string_body = body_data_ptr + .map(|string_msg| std::str::from_utf8(string_msg).unwrap()); + } + + let header_data_ptr = mem.data_unchecked().get_unchecked( + headers_data as u32 as usize + ..headers_data as u32 as usize + headers_size as u32 as usize, + ); + let deserialized_header = serial_utils::deserialize_map(header_data_ptr); + + let trailer_data_ptr = mem.data_unchecked().get_unchecked( + trailers_data as u32 as usize + ..trailers_data as u32 as usize + trailers_size as u32 as usize, + ); + let deserialized_trailer = serial_utils::deserialize_map(trailer_data_ptr); + + let token_id = match EXPECT.lock().unwrap().staged.get_expect_http_call( + string_upstream, + header_data_ptr, + string_body, + trailer_data_ptr, + timeout, + ) { + Some(expect_token) => expect_token, + None => 0, + }; + + let return_token_add = mem.data_unchecked_mut().get_unchecked_mut( + return_token as u32 as usize..return_token as u32 as usize + 4, + ); + return_token_add.copy_from_slice(&token_id.to_le_bytes()); + + println!("=> proxy_http_call | upstream_data: {}", string_upstream); + println!( + " | headers_data: {:?}", + deserialized_header + ); + println!( + " | body_data: {}", + string_body.unwrap_or("None") + ); + println!( + " | trailers_data: {:?}", + deserialized_trailer + ); + println!(" | timeout: {:?}", timeout); + } + return Status::Ok as i32; + }, + )) + } + + "proxy_set_effective_context" => { + Some(Func::wrap( + &store, + |_caller: Caller<'_>, context_id: i32| -> i32 { + // Default Function: + // Expectation: + println!("=> proxy_set_effective_context | {}", context_id); + return Status::Ok as i32; + }, + )) + } + + "proxy_done" => { + Some(Func::wrap(&store, |_caller: Caller<'_>| -> i32 { + // Default Function: + // Expectation: + println!("=> proxy_done | "); + return Status::InternalFailure as i32; + })) + } + + _ => None, + } +} + +pub mod serial_utils { + + type Bytes = Vec; + use rand::Rng; + use std::convert::TryFrom; + + const CHARSET: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZ\ + abcdefghijklmnopqrstuvwxyz\ + 0123456789)(*&^%$#@!~"; + + pub fn _serialize_property_path(path: Vec<&str>) -> Bytes { + if path.is_empty() { + return Vec::new(); + } + let mut size: usize = 0; + for part in &path { + size += part.len() + 1; + } + let mut bytes: Bytes = Vec::with_capacity(size); + for part in &path { + bytes.extend_from_slice(&part.as_bytes()); + bytes.push(0); + } + bytes.pop(); + bytes + } + + pub fn serialize_map(map: Vec<(&str, &str)>) -> Bytes { + let mut size: usize = 4; + for (name, value) in &map { + size += name.len() + value.len() + 10; + } + let mut bytes: Bytes = Vec::with_capacity(size); + bytes.extend_from_slice(&(map.len() as u32).to_le_bytes()); + for (name, value) in &map { + bytes.extend_from_slice(&(name.len() as u32).to_le_bytes()); + bytes.extend_from_slice(&(value.len() as u32).to_le_bytes()); + } + for (name, value) in &map { + bytes.extend_from_slice(&name.as_bytes()); + bytes.push(0); + bytes.extend_from_slice(&value.as_bytes()); + bytes.push(0); + } + bytes + } + + pub fn deserialize_map(bytes: &[u8]) -> Vec<(String, String)> { + let mut map = Vec::new(); + if bytes.is_empty() { + return map; + } + let size = u32::from_le_bytes(<[u8; 4]>::try_from(&bytes[0..4]).unwrap()) as usize; + let mut p = 4 + size * 8; + for n in 0..size { + let s = 4 + n * 8; + let size = u32::from_le_bytes(<[u8; 4]>::try_from(&bytes[s..s + 4]).unwrap()) as usize; + let key = bytes[p..p + size].to_vec(); + p += size + 1; + let size = + u32::from_le_bytes(<[u8; 4]>::try_from(&bytes[s + 4..s + 8]).unwrap()) as usize; + let value = bytes[p..p + size].to_vec(); + p += size + 1; + map.push(( + String::from_utf8(key).unwrap(), + String::from_utf8(value).unwrap(), + )); + } + map + } + + pub fn generate_random_string(string_len: usize) -> String { + let mut rng = rand::thread_rng(); + let random_string: String = (0..string_len) + .map(|_| { + let idx = rng.gen_range(0, CHARSET.len()); + CHARSET[idx] as char + }) + .collect(); + random_string + } +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..ee8b6de --- /dev/null +++ b/src/lib.rs @@ -0,0 +1,26 @@ +// Copyright 2020 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#![crate_type = "lib"] +#![crate_name = "proxy_wasm_test_framework"] + +pub mod tester; +pub mod types; +pub mod utility; + +mod expect_interface; +mod expectations; +mod host_settings; +mod hostcalls; +mod settings_interface; diff --git a/src/settings_interface.rs b/src/settings_interface.rs new file mode 100644 index 0000000..1d0ea54 --- /dev/null +++ b/src/settings_interface.rs @@ -0,0 +1,59 @@ +// Copyright 2020 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use crate::tester::Tester; + +pub struct DefaultBufferBytes<'a> { + tester: &'a mut Tester, + buffer_type: i32, +} + +impl<'a> DefaultBufferBytes<'a> { + pub fn expecting(tester: &'a mut Tester, buffer_type: i32) -> DefaultBufferBytes { + DefaultBufferBytes { + tester: tester, + buffer_type: buffer_type, + } + } + + pub fn returning(&mut self, buffer_data: &str) -> &mut Tester { + self.tester + .get_settings_handle() + .staged + .set_buffer_bytes(self.buffer_type, buffer_data); + self.tester + } +} + +pub struct DefaultHeaderMapPairs<'a> { + tester: &'a mut Tester, + map_type: i32, +} + +impl<'a> DefaultHeaderMapPairs<'a> { + pub fn expecting(tester: &'a mut Tester, map_type: i32) -> DefaultHeaderMapPairs { + DefaultHeaderMapPairs { + tester: tester, + map_type: map_type, + } + } + + pub fn returning(&mut self, header_map_pairs: Vec<(&str, &str)>) -> &mut Tester { + self.tester + .get_settings_handle() + .staged + .set_header_map_pairs(self.map_type, header_map_pairs); + self.tester + } +} diff --git a/src/tester.rs b/src/tester.rs new file mode 100644 index 0000000..1b3604f --- /dev/null +++ b/src/tester.rs @@ -0,0 +1,869 @@ +// Copyright 2020 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use crate::expect_interface::*; +use crate::expectations::ExpectHandle; +use crate::host_settings::HostHandle; +use crate::hostcalls::{generate_import_list, get_abi_version}; +use crate::settings_interface::*; +use crate::types::*; + +use anyhow::Result; +use std::sync::{Arc, Mutex, MutexGuard}; +use wasmtime::*; + +pub fn test(wasm_file: &str) -> Result { + // initialize wasm engine and shared cache + let store = Store::default(); + let module = Module::from_file(store.engine(), wasm_file)?; + + // generate and link host function implementations + let abi_version = get_abi_version(&module); + let imports: Arc>> = Arc::new(Mutex::new(Vec::new())); + let (host_settings, expectations): (Arc>, Arc>) = + generate_import_list(&store, &module, imports.clone()); + let instance = Instance::new(&store, &module, &(*imports).lock().unwrap()[..])?; + + // create mock test proxy-wasm object + let tester = Tester::new(abi_version, instance, host_settings, expectations); + return Ok(tester); +} + +#[derive(Debug, PartialEq)] +enum FunctionCall { + FunctionNotSet, + Start(), + ProxyOnContextCreate(i32, i32), + ProxyOnLog(i32), + ProxyOnDone(i32), + ProxyOnDelete(i32), + ProxyOnVmStart(i32, i32), + ProxyOnConfigure(i32, i32), + ProxyOnTick(i32), + ProxyOnQueueReady(i32, i32), + ProxyOnNewConnection(i32), + ProxyOnDownstreamData(i32, i32, i32), + ProxyOnDownstreamConnectionClose(i32, i32), + ProxyOnUpstreamData(i32, i32, i32), + ProxyOnUpstreamConnectionClose(i32, i32), + ProxyOnRequestHeaders(i32, i32), + ProxyOnRequestBody(i32, i32, i32), + ProxyOnRequestTrailers(i32, i32), + ProxyOnResponseHeaders(i32, i32), + ProxyOnResponseBody(i32, i32, i32), + ProxyOnResponseTrailers(i32, i32), + ProxyOnHttpCallResponse(i32, i32, i32, i32, i32), +} + +#[derive(Debug, PartialEq)] +enum FunctionType { + ReturnNotSet, + ReturnEmpty, + ReturnBool, + ReturnAction, +} + +pub struct Tester { + abi_version: AbiVersion, + instance: Instance, + defaults: Arc>, + expect: Arc>, + function_call: FunctionCall, + function_type: FunctionType, +} + +impl Tester { + fn new( + abi_version: AbiVersion, + instance: Instance, + host_settings: Arc>, + expect: Arc>, + ) -> Tester { + Tester { + abi_version: abi_version, + instance: instance, + defaults: host_settings, + expect: expect, + function_call: FunctionCall::FunctionNotSet, + function_type: FunctionType::ReturnNotSet, + } + } + + /* ------------------------------------- Low-level Expectation Setting ------------------------------------- */ + + pub fn expect_log(&mut self, log_level: LogLevel, log_msg: &str) -> &mut Self { + self.get_expect_handle() + .staged + .set_expect_log(log_level as i32, log_msg); + self + } + + pub fn expect_set_tick_period_millis(&mut self, tick_period_millis: u64) -> &mut Self { + self.get_expect_handle() + .staged + .set_expect_set_tick_period_millis(tick_period_millis); + self + } + + pub fn expect_get_current_time_nanos(&mut self) -> ExpectGetCurrentTimeNanos { + ExpectGetCurrentTimeNanos::expecting(self) + } + + pub fn expect_get_buffer_bytes(&mut self, buffer_type: BufferType) -> ExpectGetBufferBytes { + ExpectGetBufferBytes::expecting(self, buffer_type as i32) + } + + pub fn expect_set_buffer_bytes( + &mut self, + buffer_type: BufferType, + buffer_data: &str, + ) -> &mut Self { + self.get_expect_handle() + .staged + .set_expect_set_buffer_bytes(buffer_type as i32, buffer_data); + self + } + + pub fn expect_get_header_map_pairs(&mut self, map_type: MapType) -> ExpectGetHeaderMapPairs { + ExpectGetHeaderMapPairs::expecting(self, map_type as i32) + } + + pub fn expect_set_header_map_pairs(&mut self, map_type: MapType) -> ExpectSetHeaderMapPairs { + ExpectSetHeaderMapPairs::expecting(self, map_type as i32) + } + + pub fn expect_get_header_map_value( + &mut self, + map_type: MapType, + header_map_key: &'static str, + ) -> ExpectGetHeaderMapValue { + ExpectGetHeaderMapValue::expecting(self, map_type as i32, header_map_key) + } + + pub fn expect_replace_header_map_value( + &mut self, + map_type: MapType, + header_map_key: &str, + header_map_value: &str, + ) -> &mut Self { + self.get_expect_handle() + .staged + .set_expect_replace_header_map_value(map_type as i32, header_map_key, header_map_value); + self + } + + pub fn expect_remove_header_map_value( + &mut self, + map_type: MapType, + header_map_key: &str, + ) -> &mut Self { + self.get_expect_handle() + .staged + .set_expect_remove_header_map_value(map_type as i32, header_map_key); + self + } + + pub fn expect_add_header_map_value( + &mut self, + map_type: MapType, + header_map_key: &str, + header_map_value: &str, + ) -> &mut Self { + self.get_expect_handle() + .staged + .set_expect_add_header_map_value(map_type as i32, header_map_key, header_map_value); + self + } + + pub fn expect_send_local_response( + &mut self, + status_code: i32, + body: Option<&str>, + headers: Vec<(&str, &str)>, + grpc_status: i32, + ) -> &mut Self { + self.get_expect_handle() + .staged + .set_expect_send_local_response(status_code, body, headers, grpc_status); + self + } + + pub fn expect_http_call( + &mut self, + upstream: &'static str, + headers: Vec<(&'static str, &'static str)>, + body: Option<&'static str>, + trailers: Vec<(&'static str, &'static str)>, + timeout: u64, + ) -> ExpectHttpCall { + ExpectHttpCall::expecting(self, upstream, headers, body, trailers, timeout) + } + + /* ------------------------------------- High-level Expectation Setting ------------------------------------- */ + + pub fn reset_default_tick_period_millis(&mut self) -> &mut Self { + self.get_settings_handle().staged.reset_tick_period_millis(); + self + } + + pub fn set_default_tick_period_millis(&mut self, tick_period_millis: u64) -> &mut Self { + self.get_settings_handle() + .staged + .set_tick_period_millis(tick_period_millis); + self + } + + pub fn reset_default_buffer_bytes(&mut self) -> &mut Self { + self.get_settings_handle().staged.reset_buffer_bytes(); + self + } + + pub fn set_default_buffer_bytes(&mut self, buffer_type: BufferType) -> DefaultBufferBytes { + DefaultBufferBytes::expecting(self, buffer_type as i32) + } + + pub fn reset_default_header_map_pairs(&mut self) -> &mut Self { + self.get_settings_handle().staged.reset_header_map_pairs(); + self + } + + pub fn set_default_header_map_pairs(&mut self, map_type: MapType) -> DefaultHeaderMapPairs { + DefaultHeaderMapPairs::expecting(self, map_type as i32) + } + + /* ------------------------------------- Utility Functions ------------------------------------- */ + + pub fn get_expect_handle(&self) -> MutexGuard { + self.expect.lock().unwrap() + } + + pub fn print_expectations(&self) { + self.expect.lock().unwrap().print_staged(); + } + + fn update_expect_stage(&mut self) { + self.expect.lock().unwrap().update_stage(); + } + + fn assert_expect_stage(&mut self) { + self.expect.lock().unwrap().assert_stage(); + } + + pub fn get_settings_handle(&self) -> MutexGuard { + self.defaults.lock().unwrap() + } + + pub fn print_host_settings(&self) { + self.defaults.lock().unwrap().print_staged(); + } + + pub fn reset_host_settings(&mut self) { + self.defaults.lock().unwrap().reset(self.abi_version); + } + + /* ------------------------------------- Wasm Function Executation ------------------------------------- */ + + pub fn execute_and_expect(&mut self, expect_wasm: ReturnType) -> Result<()> { + assert_ne!(self.function_call, FunctionCall::FunctionNotSet); + assert_ne!(self.function_type, FunctionType::ReturnNotSet); + + let mut return_wasm: Option = None; + match self.function_call { + FunctionCall::Start() => { + let _start = self + .instance + .get_func("_start") + .ok_or(anyhow::format_err!( + "failed to find `_start` function export" + ))? + .get0::<()>()?; + _start()?; + } + + FunctionCall::ProxyOnContextCreate(root_context_id, parent_context_id) => { + let proxy_on_context_create = self + .instance + .get_func("proxy_on_context_create") + .ok_or(anyhow::format_err!( + "failed to find `proxy_on_context_create` function export" + ))? + .get2::()?; + proxy_on_context_create(root_context_id, parent_context_id)?; + } + + FunctionCall::ProxyOnDone(context_id) => { + let proxy_on_done = self + .instance + .get_func("proxy_on_done") + .ok_or(anyhow::format_err!( + "failed to find 'proxy_on_done' function export" + ))? + .get1::()?; + let is_done = proxy_on_done(context_id)?; + println!("RETURN: is_done -> {}", is_done); + return_wasm = Some(is_done); + } + + FunctionCall::ProxyOnLog(context_id) => { + let proxy_on_log = self + .instance + .get_func("proxy_on_log") + .ok_or(anyhow::format_err!( + "failed to find `proxy_on_log` function export" + ))? + .get1::()?; + proxy_on_log(context_id)?; + } + + FunctionCall::ProxyOnDelete(context_id) => { + let proxy_on_delete = self + .instance + .get_func("proxy_on_delete") + .ok_or(anyhow::format_err!( + "failed to find 'proxy_on_delete' function export" + ))? + .get1::()?; + proxy_on_delete(context_id)?; + } + + FunctionCall::ProxyOnVmStart(context_id, vm_configuration_size) => { + let proxy_on_vm_start = self + .instance + .get_func("proxy_on_vm_start") + .ok_or(anyhow::format_err!( + "failed to find `proxy_on_vm_start` function export" + ))? + .get2::()?; + let success = proxy_on_vm_start(context_id, vm_configuration_size)?; + println!("RETURN: success -> {}", success); + return_wasm = Some(success); + } + + FunctionCall::ProxyOnConfigure(context_id, plugin_configuration_size) => { + let proxy_on_configure = self + .instance + .get_func("proxy_on_configure") + .ok_or(anyhow::format_err!( + "failed to find 'proxy_on_configure' function export" + ))? + .get2::()?; + let success = proxy_on_configure(context_id, plugin_configuration_size)?; + println!("RETURN: success -> {}", success); + return_wasm = Some(success); + } + + FunctionCall::ProxyOnTick(context_id) => { + let proxy_on_tick = self + .instance + .get_func("proxy_on_tick") + .ok_or(anyhow::format_err!( + "failed to find `proxy_on_tick` function export" + ))? + .get1::()?; + proxy_on_tick(context_id)?; + } + + FunctionCall::ProxyOnQueueReady(context_id, queue_id) => { + let proxy_on_queue_ready = self + .instance + .get_func("proxy_on_queue_ready") + .ok_or(anyhow::format_err!( + "failed to find 'proxy_on_queue_ready' function export" + ))? + .get2::()?; + proxy_on_queue_ready(context_id, queue_id)?; + } + + FunctionCall::ProxyOnNewConnection(context_id) => { + let proxy_on_new_connection = self + .instance + .get_func("proxy_on_new_connection") + .ok_or(anyhow::format_err!( + "failed to find 'proxy_on_new_connection' function export" + ))? + .get1::()?; + let action = proxy_on_new_connection(context_id)?; + println!("RETURN: action -> {}", action); + return_wasm = Some(action); + } + + FunctionCall::ProxyOnDownstreamData(context_id, data_size, end_of_stream) => { + let proxy_on_downstream_data = self + .instance + .get_func("proxy_on_downstream_data") + .ok_or(anyhow::format_err!( + "failed to find 'proxy_on_downstream_data' function export" + ))? + .get3::()?; + let action = proxy_on_downstream_data(context_id, data_size, end_of_stream)?; + println!("RETURN: action -> {}", action); + return_wasm = Some(action); + } + + FunctionCall::ProxyOnDownstreamConnectionClose(context_id, peer_type) => { + let proxy_on_downstream_connection_close = self + .instance + .get_func("proxy_on_downstream_connection_close") + .ok_or(anyhow::format_err!( + "failed to find 'proxy_on_downstream_connection_close' function export" + ))? + .get2::()?; + proxy_on_downstream_connection_close(context_id, peer_type)?; + } + + FunctionCall::ProxyOnUpstreamData(context_id, data_size, end_of_stream) => { + let proxy_on_upstream_data = self + .instance + .get_func("proxy_on_upstream_data") + .ok_or(anyhow::format_err!( + "failed to find 'proxy_on_upstream_data' function export" + ))? + .get3::()?; + let action = proxy_on_upstream_data(context_id, data_size, end_of_stream)?; + println!("RETURN: action -> {}", action); + return_wasm = Some(action); + } + + FunctionCall::ProxyOnUpstreamConnectionClose(context_id, peer_type) => { + let proxy_on_upstream_connection_close = self + .instance + .get_func("proxy_on_upstream_connection_close") + .ok_or(anyhow::format_err!( + "failed to find 'proxy_on_upstream_connection_close' function export" + ))? + .get2::()?; + proxy_on_upstream_connection_close(context_id, peer_type)?; + } + + FunctionCall::ProxyOnRequestHeaders(context_id, num_headers) => { + let proxy_on_request_headers = self + .instance + .get_func("proxy_on_request_headers") + .ok_or(anyhow::format_err!( + "failed to find `proxy_on_request_headers` function export" + ))? + .get2::()?; + let action = proxy_on_request_headers(context_id, num_headers)?; + println!("RETURN: action -> {}", action); + return_wasm = Some(action); + } + + FunctionCall::ProxyOnRequestBody(context_id, body_size, end_of_stream) => { + let proxy_on_request_body = self + .instance + .get_func("proxy_on_request_body") + .ok_or(anyhow::format_err!( + "failed to find 'proxy_on_request_body' function export" + ))? + .get3::()?; + let action = proxy_on_request_body(context_id, body_size, end_of_stream)?; + println!("RETURN: action -> {}", action); + return_wasm = Some(action); + } + + FunctionCall::ProxyOnRequestTrailers(context_id, num_trailers) => { + let proxy_on_request_trailers = self + .instance + .get_func("proxy_on_request_trailers") + .ok_or(anyhow::format_err!( + "failed to find `proxy_on_request_trailers` function export" + ))? + .get2::()?; + let action = proxy_on_request_trailers(context_id, num_trailers)?; + println!("RETURN: action -> {}", action); + return_wasm = Some(action); + } + + FunctionCall::ProxyOnResponseHeaders(context_id, num_headers) => { + let proxy_on_response_headers = self + .instance + .get_func("proxy_on_response_headers") + .ok_or(anyhow::format_err!( + "failed to find `proxy_on_response_headers` function export" + ))? + .get2::()?; + let action = proxy_on_response_headers(context_id, num_headers)?; + println!("RETURN: action -> {}", action); + return_wasm = Some(action); + } + + FunctionCall::ProxyOnResponseBody(context_id, body_size, end_of_stream) => { + let proxy_on_response_body = self + .instance + .get_func("proxy_on_response_body") + .ok_or(anyhow::format_err!( + "failed to find 'proxy_on_response_body' function export" + ))? + .get3::()?; + let action = proxy_on_response_body(context_id, body_size, end_of_stream)?; + println!("RETURN: action -> {}", action); + return_wasm = Some(action); + } + + FunctionCall::ProxyOnResponseTrailers(context_id, num_trailers) => { + let proxy_on_response_trailers = self + .instance + .get_func("proxy_on_response_trailers") + .ok_or(anyhow::format_err!( + "failed to find `proxy_on_response_trailers` function export" + ))? + .get2::()?; + let action = proxy_on_response_trailers(context_id, num_trailers)?; + println!("RETURN: action -> {}", action); + return_wasm = Some(action); + } + + FunctionCall::ProxyOnHttpCallResponse( + context_id, + callout_id, + num_headers, + body_size, + num_trailers, + ) => { + let proxy_on_http_call_response = self + .instance + .get_func("proxy_on_http_call_response") + .ok_or(anyhow::format_err!( + "failed to find `proxy_on_http_call_response` function export" + ))? + .get5::()?; + proxy_on_http_call_response( + context_id, + callout_id, + num_headers, + body_size, + num_trailers, + )?; + } + + _ => panic!("No function with name: {:?}", self.function_call), + } + + match expect_wasm { + ReturnType::None => { + assert_eq!(self.function_type, FunctionType::ReturnEmpty); + assert_eq!(return_wasm.is_none(), true); + } + ReturnType::Bool(expect_bool) => { + assert_eq!(self.function_type, FunctionType::ReturnBool); + assert_eq!(expect_bool as i32, return_wasm.unwrap_or(-1)); + } + ReturnType::Action(expect_action) => { + assert_eq!(self.function_type, FunctionType::ReturnAction); + assert_eq!(expect_action as i32, return_wasm.unwrap_or(-1)) + } + } + + self.function_call = FunctionCall::FunctionNotSet; + self.function_type = FunctionType::ReturnNotSet; + self.assert_expect_stage(); + self.update_expect_stage(); + println!("\n"); + return Ok(()); + } + + /* ------------------------------------- Call Setting ------------------------------------- */ + + pub fn call_start(&mut self) -> &mut Self { + println!("CALL TO: _start"); + self.function_call = FunctionCall::Start(); + self.function_type = FunctionType::ReturnEmpty; + self + } + + pub fn call_proxy_on_context_create( + &mut self, + root_context_id: i32, + parent_context_id: i32, + ) -> &mut Self { + println!("CALL TO: proxy_on_context_create"); + println!( + "ARGS: root_context_id -> {}, parent_context_id -> {}", + root_context_id, parent_context_id + ); + self.function_call = FunctionCall::ProxyOnContextCreate(root_context_id, parent_context_id); + self.function_type = FunctionType::ReturnEmpty; + self + } + + pub fn call_proxy_on_done(&mut self, context_id: i32) -> &mut Self { + println!("CALL TO: proxy_on_done"); + println!("ARGS: context_id -> {}", context_id); + self.function_call = FunctionCall::ProxyOnDone(context_id); + self.function_type = FunctionType::ReturnBool; + self + } + + pub fn call_proxy_on_log(&mut self, context_id: i32) -> &mut Self { + println!("CALL TO: proxy_on_log"); + println!("ARGS: context_id -> {}", context_id); + self.function_call = FunctionCall::ProxyOnLog(context_id); + self.function_type = FunctionType::ReturnEmpty; + self + } + + pub fn call_proxy_on_delete(&mut self, context_id: i32) -> &mut Self { + println!("CALL TO: proxy_on_delete"); + println!("ARGS: context_id -> {}", context_id); + self.function_call = FunctionCall::ProxyOnDelete(context_id); + self.function_type = FunctionType::ReturnEmpty; + self + } + + pub fn call_proxy_on_vm_start( + &mut self, + context_id: i32, + vm_configuration_size: i32, + ) -> &mut Self { + println!("CALL TO: proxy_on_vm_start"); + println!( + "ARGS: context_id -> {}, vm_configuration_size -> {}", + context_id, vm_configuration_size + ); + self.function_call = FunctionCall::ProxyOnVmStart(context_id, vm_configuration_size); + self.function_type = FunctionType::ReturnBool; + self + } + + pub fn call_proxy_on_configure( + &mut self, + context_id: i32, + plugin_configuration_size: i32, + ) -> &mut Self { + println!("CALL TO: proxy_on_configure"); + println!( + "ARGS: context_id -> {}, plugin_configuration_size -> {}", + context_id, plugin_configuration_size + ); + self.function_call = FunctionCall::ProxyOnConfigure(context_id, plugin_configuration_size); + self.function_type = FunctionType::ReturnBool; + self + } + + pub fn call_proxy_on_tick(&mut self, context_id: i32) -> &mut Self { + println!("CALL TO: proxy_on_tick"); + println!("ARGS: context_id -> {}", context_id); + self.function_call = FunctionCall::ProxyOnTick(context_id); + self.function_type = FunctionType::ReturnEmpty; + self + } + + pub fn call_proxy_on_queue_ready(&mut self, context_id: i32, queue_id: i32) -> &mut Self { + println!("CALL TO: proxy_on_queue_ready"); + println!( + "ARGS: context_id -> {}, queue_id -> {}", + context_id, queue_id + ); + self.function_call = FunctionCall::ProxyOnQueueReady(context_id, queue_id); + self.function_type = FunctionType::ReturnEmpty; + self + } + + pub fn call_proxy_on_new_connection(&mut self, context_id: i32) -> &mut Self { + println!("CALL TO: proxy_on_new_connection"); + println!("ARGS: context_id -> {}", context_id); + self.function_call = FunctionCall::ProxyOnNewConnection(context_id); + self.function_type = FunctionType::ReturnAction; + self + } + + pub fn call_proxy_on_downstream_data( + &mut self, + context_id: i32, + data_size: i32, + end_of_stream: i32, + ) -> &mut Self { + println!("CALL TO: proxy_on_downstream_data"); + println!( + "ARGS: context_id -> {}, data_size -> {}, end_of_stream -> {}", + context_id, data_size, end_of_stream + ); + self.function_call = + FunctionCall::ProxyOnDownstreamData(context_id, data_size, end_of_stream); + self.function_type = FunctionType::ReturnAction; + self + } + + pub fn call_proxy_on_downstream_connection_close( + &mut self, + context_id: i32, + peer_type: PeerType, + ) -> &mut Self { + println!("CALL TO: proxy_on_downstream_connection_close"); + println!( + "ARGS: context_id -> {}, peer_data -> {}", + context_id, peer_type as i32 + ); + self.function_call = + FunctionCall::ProxyOnDownstreamConnectionClose(context_id, peer_type as i32); + self.function_type = FunctionType::ReturnEmpty; + self + } + + pub fn call_proxy_on_upstream_data( + &mut self, + context_id: i32, + data_size: i32, + end_of_stream: i32, + ) -> &mut Self { + println!("CALL TO: proxy_on_upstream_data"); + println!( + "ARGS: context_id -> {}, data_size -> {}, end_of_stream -> {}", + context_id, data_size, end_of_stream + ); + self.function_call = + FunctionCall::ProxyOnUpstreamData(context_id, data_size, end_of_stream); + self.function_type = FunctionType::ReturnAction; + self + } + + pub fn call_proxy_on_upstream_connection_close( + &mut self, + context_id: i32, + peer_type: PeerType, + ) -> &mut Self { + println!("CALL TO: proxy_on_upstream_connection_close"); + println!( + "ARGS: context_id -> {}, peer_data -> {}", + context_id, peer_type as i32 + ); + self.function_call = + FunctionCall::ProxyOnUpstreamConnectionClose(context_id, peer_type as i32); + self.function_type = FunctionType::ReturnEmpty; + self + } + + pub fn call_proxy_on_request_headers( + &mut self, + context_id: i32, + num_headers: i32, + ) -> &mut Self { + println!("CALL TO: proxy_on_request_headers"); + println!( + "ARGS: context_id -> {}, num_headers -> {}", + context_id, num_headers + ); + self.function_call = FunctionCall::ProxyOnRequestHeaders(context_id, num_headers); + self.function_type = FunctionType::ReturnAction; + self + } + + pub fn call_proxy_on_request_body( + &mut self, + context_id: i32, + body_size: i32, + end_of_stream: i32, + ) -> &mut Self { + println!("CALL TO: proxy_on_request_body"); + println!( + "ARGS: context_id -> {}, body_size -> {}, end_of_stream -> {}", + context_id, body_size, end_of_stream + ); + self.function_call = FunctionCall::ProxyOnRequestBody(context_id, body_size, end_of_stream); + self.function_type = FunctionType::ReturnAction; + self + } + + pub fn call_proxy_on_request_trailers( + &mut self, + context_id: i32, + num_trailers: i32, + ) -> &mut Self { + println!("CALL TO: proxy_on_request_trailers"); + println!( + "ARGS: context_id -> {}, num_trailers -> {}", + context_id, num_trailers + ); + self.function_call = FunctionCall::ProxyOnRequestTrailers(context_id, num_trailers); + self.function_type = FunctionType::ReturnAction; + self + } + + pub fn call_proxy_on_response_headers( + &mut self, + context_id: i32, + num_headers: i32, + ) -> &mut Self { + println!("CALL TO: proxy_on_response_headers"); + println!( + "ARGS: context_id -> {}, num_headers -> {}", + context_id, num_headers + ); + self.function_call = FunctionCall::ProxyOnResponseHeaders(context_id, num_headers); + self.function_type = FunctionType::ReturnAction; + self + } + + pub fn call_proxy_on_response_body( + &mut self, + context_id: i32, + body_size: i32, + end_of_stream: i32, + ) -> &mut Self { + println!("CALL TO: proxy_on_response_body"); + println!( + "ARGS: context_id -> {}, body_size -> {}, end_of_stream -> {}", + context_id, body_size, end_of_stream + ); + self.function_call = + FunctionCall::ProxyOnResponseBody(context_id, body_size, end_of_stream); + self.function_type = FunctionType::ReturnAction; + self + } + + pub fn call_proxy_on_response_trailers( + &mut self, + context_id: i32, + num_trailers: i32, + ) -> &mut Self { + println!("CALL TO: proxy_on_response_trailers"); + println!( + "ARGS: context_id -> {}, num_trailers -> {}", + context_id, num_trailers + ); + self.function_call = FunctionCall::ProxyOnResponseTrailers(context_id, num_trailers); + self.function_type = FunctionType::ReturnAction; + self + } + + pub fn call_proxy_on_http_call_response( + &mut self, + context_id: i32, + callout_id: i32, + num_headers: i32, + body_size: i32, + num_trailers: i32, + ) -> &mut Self { + println!("CALL TO: proxy_on_http_call_response"); + println!( + "ARGS: context_id -> {}, callout_id -> {}", + context_id, callout_id + ); + println!( + " num_headers -> {}, body_size -> {}, num_trailers: {}", + num_headers, body_size, num_trailers + ); + self.function_call = FunctionCall::ProxyOnHttpCallResponse( + context_id, + callout_id, + num_headers, + body_size, + num_trailers, + ); + self.function_type = FunctionType::ReturnEmpty; + self + } + + /* ---------------------------------- Combination Calls ---------------------------------- */ +} diff --git a/src/types.rs b/src/types.rs new file mode 100644 index 0000000..27b40e8 --- /dev/null +++ b/src/types.rs @@ -0,0 +1,87 @@ +// Copyright 2020 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#[repr(u32)] +#[derive(Debug)] +pub enum LogLevel { + Trace = 0, + Debug = 1, + Info = 2, + Warn = 3, + Error = 4, + Critical = 5, +} + +#[repr(u32)] +#[derive(Debug, Clone, Copy)] +pub enum Action { + Continue = 0, + Pause = 1, +} + +#[repr(u32)] +#[derive(Debug)] +pub enum Status { + Ok = 0, + NotFound = 1, + BadArgument = 2, + Empty = 7, + CasMismatch = 8, + InternalFailure = 10, +} + +#[repr(u32)] +#[derive(Debug)] +pub enum BufferType { + HttpRequestBody = 0, + HttpResponseBody = 1, + DownstreamData = 2, + UpstreamData = 3, + HttpCallResponseBody = 4, +} + +#[repr(u32)] +#[derive(Debug)] +pub enum MapType { + HttpRequestHeaders = 0, + HttpRequestTrailers = 1, + HttpResponseHeaders = 2, + HttpResponseTrailers = 3, + HttpCallResponseHeaders = 6, + HttpCallResponseTrailers = 7, +} + +#[repr(u32)] +#[derive(Debug, Clone, Copy)] +pub enum PeerType { + Unknown = 0, + Local = 1, + Remote = 2, +} + +#[derive(Debug)] +pub enum ReturnType { + None, + Bool(bool), + Action(Action), +} + +#[derive(Debug, Clone, Copy)] +pub enum AbiVersion { + None, + ProxyAbiVersion0_1_0, + ProxyAbiVersion0_2_0, +} + +pub type Bytes = Vec; diff --git a/src/utility.rs b/src/utility.rs new file mode 100644 index 0000000..0519a75 --- /dev/null +++ b/src/utility.rs @@ -0,0 +1,52 @@ +// Copyright 2020 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use anyhow::Result; +use wasmtime::*; + +pub fn print_boundary(wasm_file: &str) -> Result<()> { + let store = Store::default(); + let module = Module::from_file(store.engine(), wasm_file)?; + print_imports(&module); + print_exports(&module); + return Ok(()); +} + +pub fn print_imports(module: &Module) { + let imports = module.imports(); + println!("This module requires {} imports", imports.len()); + println!("-----------------------------------------------------------------"); + // get details of all imports (in order) + for (c, item) in imports.enumerate() { + println!( + "Import {}: {} -- {} -- {:?}", + c + 1, + item.module(), + item.name(), + item.ty() + ); + } + println!("-----------------------------------------------------------------") +} + +pub fn print_exports(module: &Module) { + let exports = module.exports(); + println!("This module requires {} exports", exports.len()); + println!("-----------------------------------------------------------------"); + // get details of all imports (in order) + for (c, item) in exports.enumerate() { + println!("Export {}: {} -- {:?}", c + 1, item.name(), item.ty()); + } + println!("-----------------------------------------------------------------") +}