diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 22697d8..de5bf8a 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -22,3 +22,5 @@ jobs: run: cargo clippy --all-features --all-targets -- -D warnings - name: Check run: cargo check --release --all --all-features + - name: Test + run: cargo test --all --features mock-ffi diff --git a/Cargo.toml b/Cargo.toml index ecd5922..a9e2388 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,3 +19,4 @@ serde_json = "1.0" [features] default = ["serde"] serde = ["dep:serde"] +mock-ffi = [] diff --git a/README.md b/README.md index 4d59916..8e7d9dc 100644 --- a/README.md +++ b/README.md @@ -87,3 +87,23 @@ cargo build --release --target wasm32-wasip1 --example llm-mcp | [httpbin](./examples/httpbin.rs) | HTTP to query anything from httpbin | ✅ | ✅ | | [llm](./examples/llm.rs) | LLM to chat with `Llama-3.1-8B-Instruct-q4f32_1-MLC` and `SmolLM2-1.7B-Instruct-q4f16_1-MLC` models | ✅ | ✅ | | [llm-mcp](./examples/llm-mcp.rs) | LLM with MCP (Model Control Protocol) demonstrating tool integration using SSE endpoints | ✅ | ✅ | + + +## Testing + +The SDK uses FFI (Foreign Function Interface) calls that are only available in the Blockless WASM runtime environment. +To run tests without host runtime, use the `mock-ffi` feature which provides mock implementations: + +```bash +cargo test --all --features mock-ffi +``` + +This feature enables mock implementations of all FFI functions, allowing you to: +- Test SDK struct creation and configuration +- Test error handling logic +- Verify API contracts without needing the runtime +- Run unit tests in CI/CD pipelines + +Note: +- The mocks return predictable test data and don't perform actual network requests or system calls. +- Only one implementation of the FFI functions is allowed to be mocked. diff --git a/src/cgi.rs b/src/cgi.rs index 45c74e0..c4ffc4a 100644 --- a/src/cgi.rs +++ b/src/cgi.rs @@ -2,6 +2,7 @@ use crate::CGIErrorKind; use json::{object::Object, JsonValue}; use std::fmt::{Debug, Display}; +#[cfg(not(feature = "mock-ffi"))] #[link(wasm_import_module = "blockless_cgi")] extern "C" { #[link_name = "cgi_open"] @@ -27,6 +28,61 @@ extern "C" { pub(crate) fn cgi_list_read(handle: u32, buf: *mut u8, buf_len: u32, num: *mut u32) -> u32; } +#[cfg(feature = "mock-ffi")] +#[allow(unused_variables)] +mod mock_ffi { + pub unsafe extern "C" fn cgi_open( + _opts: *const u8, + _opts_len: u32, + cgi_handle: *mut u32, + ) -> u32 { + unimplemented!() + } + + pub unsafe extern "C" fn cgi_stdout_read( + _handle: u32, + buf: *mut u8, + buf_len: u32, + num: *mut u32, + ) -> u32 { + unimplemented!() + } + + pub unsafe extern "C" fn cgi_stderr_read( + _handle: u32, + buf: *mut u8, + buf_len: u32, + num: *mut u32, + ) -> u32 { + unimplemented!() + } + + #[allow(dead_code)] + pub unsafe extern "C" fn cgi_stdin_write( + _handle: u32, + _buf: *const u8, + buf_len: u32, + num: *mut u32, + ) -> u32 { + unimplemented!() + } + + pub unsafe fn cgi_close(_handle: u32) -> u32 { + unimplemented!() + } + + pub unsafe fn cgi_list_exec(cgi_handle: *mut u32) -> u32 { + unimplemented!() + } + + pub unsafe fn cgi_list_read(_handle: u32, buf: *mut u8, buf_len: u32, num: *mut u32) -> u32 { + unimplemented!() + } +} + +#[cfg(feature = "mock-ffi")] +use mock_ffi::*; + #[derive(Debug)] pub struct CGIExtensions { pub file_name: String, diff --git a/src/http.rs b/src/http.rs index 03195ac..6543378 100644 --- a/src/http.rs +++ b/src/http.rs @@ -2,6 +2,7 @@ use crate::error::HttpErrorKind; use json::JsonValue; use std::{cmp::Ordering, collections::BTreeMap}; +#[cfg(not(feature = "mock-ffi"))] #[link(wasm_import_module = "blockless_http")] extern "C" { #[link_name = "http_req"] @@ -31,6 +32,44 @@ extern "C" { pub(crate) fn http_close(handle: u32) -> u32; } +#[cfg(feature = "mock-ffi")] +#[allow(unused_variables)] +mod mock_ffi { + + pub unsafe fn http_open( + _url: *const u8, + _url_len: u32, + _opts: *const u8, + _opts_len: u32, + fd: *mut u32, + status: *mut u32, + ) -> u32 { + unimplemented!() + } + + pub unsafe fn http_read_header( + _handle: u32, + _header: *const u8, + _header_len: u32, + buf: *mut u8, + buf_len: u32, + num: *mut u32, + ) -> u32 { + unimplemented!() + } + + pub unsafe fn http_read_body(_handle: u32, buf: *mut u8, buf_len: u32, num: *mut u32) -> u32 { + unimplemented!() + } + + pub unsafe fn http_close(_handle: u32) -> u32 { + unimplemented!() + } +} + +#[cfg(feature = "mock-ffi")] +use mock_ffi::*; + type Handle = u32; type ExitCode = u32; diff --git a/src/llm.rs b/src/llm.rs index 952104f..87b7421 100644 --- a/src/llm.rs +++ b/src/llm.rs @@ -4,6 +4,7 @@ use std::{str::FromStr, string::ToString}; type Handle = u32; type ExitCode = u8; +#[cfg(not(feature = "mock-ffi"))] #[link(wasm_import_module = "blockless_llm")] extern "C" { fn llm_set_model_request(h: *mut Handle, model_ptr: *const u8, model_len: u8) -> ExitCode; @@ -34,6 +35,70 @@ extern "C" { fn llm_close(h: Handle) -> ExitCode; } +#[cfg(feature = "mock-ffi")] +#[allow(unused_variables)] +mod mock_ffi { + use super::*; + + pub unsafe fn llm_set_model_request( + h: *mut Handle, + _model_ptr: *const u8, + _model_len: u8, + ) -> ExitCode { + unimplemented!() + } + + pub unsafe fn llm_get_model_response( + _h: Handle, + buf: *mut u8, + buf_len: u8, + bytes_written: *mut u8, + ) -> ExitCode { + unimplemented!() + } + + pub unsafe fn llm_set_model_options_request( + _h: Handle, + _options_ptr: *const u8, + _options_len: u16, + ) -> ExitCode { + unimplemented!() + } + + pub unsafe fn llm_get_model_options( + _h: Handle, + buf: *mut u8, + buf_len: u16, + bytes_written: *mut u16, + ) -> ExitCode { + unimplemented!() + } + + pub unsafe fn llm_prompt_request( + _h: Handle, + _prompt_ptr: *const u8, + _prompt_len: u16, + ) -> ExitCode { + unimplemented!() + } + + pub unsafe fn llm_read_prompt_response( + _h: Handle, + buf: *mut u8, + buf_len: u16, + bytes_written: *mut u16, + ) -> ExitCode { + unimplemented!() + } + + pub unsafe fn llm_close(_h: Handle) -> ExitCode { + unimplemented!() + } +} + +#[cfg(feature = "mock-ffi")] +use mock_ffi::*; + #[derive(Debug, Clone)] pub enum Models { Llama321BInstruct(Option), diff --git a/src/memory.rs b/src/memory.rs index e502f81..d3b3c37 100644 --- a/src/memory.rs +++ b/src/memory.rs @@ -1,3 +1,4 @@ +#[cfg(not(feature = "mock-ffi"))] #[link(wasm_import_module = "blockless_memory")] extern "C" { #[link_name = "memory_read"] @@ -6,6 +7,21 @@ extern "C" { pub(crate) fn env_var_read(buf: *mut u8, len: u32, num: *mut u32) -> u32; } +#[cfg(feature = "mock-ffi")] +#[allow(unused_variables)] +mod mock_ffi { + pub unsafe fn memory_read(buf: *mut u8, len: u32, num: *mut u32) -> u32 { + unimplemented!() + } + + pub unsafe fn env_var_read(buf: *mut u8, len: u32, num: *mut u32) -> u32 { + unimplemented!() + } +} + +#[cfg(feature = "mock-ffi")] +use mock_ffi::*; + pub fn read_stdin(buf: &mut [u8]) -> std::io::Result { let mut len = 0; let errno = unsafe { memory_read(buf.as_mut_ptr(), buf.len() as _, &mut len) }; diff --git a/src/socket.rs b/src/socket.rs index 9e832e8..6a31e0e 100644 --- a/src/socket.rs +++ b/src/socket.rs @@ -1,5 +1,6 @@ use crate::SocketErrorKind; +#[cfg(not(feature = "mock-ffi"))] #[link(wasm_import_module = "blockless_socket")] extern "C" { #[link_name = "create_tcp_bind_socket"] @@ -10,6 +11,20 @@ extern "C" { ) -> u32; } +#[cfg(feature = "mock-ffi")] +mod mock_ffi { + pub unsafe fn create_tcp_bind_socket_native( + _addr: *const u8, + _addr_len: u32, + _fd: *mut u32, + ) -> u32 { + unimplemented!() + } +} + +#[cfg(feature = "mock-ffi")] +use mock_ffi::*; + pub fn create_tcp_bind_socket(addr: &str) -> Result { unsafe { let addr_ptr = addr.as_ptr();