pinakes-core: add plugin integration tests and test fixtures

Signed-off-by: NotAShelf <raf@notashelf.dev>
Change-Id: If4372ea33b93306486170353f9edf4a76a6a6964
This commit is contained in:
raf 2026-03-08 14:56:31 +03:00
commit 7d3c2052c2
Signed by: NotAShelf
GPG key ID: 29D95B64378DB4BF
6 changed files with 404 additions and 2 deletions

View file

@ -0,0 +1,2 @@
/target/
Cargo.lock

View file

@ -0,0 +1,14 @@
[package]
name = "test-plugin"
version = "1.0.0"
edition = "2024"
[lib]
crate-type = ["cdylib"]
[dependencies]
dlmalloc = { version = "0.2", features = ["global"] }
[profile.release]
opt-level = "s"
lto = true

View file

@ -0,0 +1,17 @@
[plugin]
name = "test-plugin"
version = "1.0.0"
api_version = "1.0"
description = "Test fixture plugin for integration tests"
kind = ["media_type", "metadata_extractor", "thumbnail_generator", "event_handler"]
priority = 50
[plugin.binary]
wasm = "test_plugin.wasm"
[capabilities]
network = false
[capabilities.filesystem]
read = ["/tmp"]
write = ["/tmp"]

View file

@ -0,0 +1,181 @@
//! Test fixture plugin for integration tests.
//!
//! Exercises all extension points: MediaTypeProvider, MetadataExtractor,
//! ThumbnailGenerator, and EventHandler.
//!
//! To build in the project directory, you might need to set `RUSTFLAGS=""` or
//! explicitly specify a non-Clang linking pipeline. Below command is the
//! easiest way to build this:
//!
//! ```
//! $ RUSTFLAGS="" cargo build --target wasm32-unknown-unknown --release`
//! ```
#![no_std]
extern crate alloc;
use alloc::{format, vec::Vec};
use core::alloc::Layout;
// Global allocator for no_std WASM
#[global_allocator]
static ALLOC: dlmalloc::GlobalDlmalloc = dlmalloc::GlobalDlmalloc;
#[panic_handler]
fn panic_handler(_info: &core::panic::PanicInfo) -> ! {
core::arch::wasm32::unreachable()
}
// Host functions provided by the runtime
unsafe extern "C" {
fn host_set_result(ptr: i32, len: i32);
fn host_log(level: i32, ptr: i32, len: i32);
}
/// Write a JSON response back to the host via host_set_result.
fn set_response(json: &[u8]) {
unsafe {
host_set_result(json.as_ptr() as i32, json.len() as i32);
}
}
/// Log at info level.
fn log_info(msg: &str) {
unsafe {
host_log(2, msg.as_ptr() as i32, msg.len() as i32);
}
}
/// Read the JSON request bytes from memory at (ptr, len).
unsafe fn read_request(ptr: i32, len: i32) -> Vec<u8> {
if ptr < 0 || len <= 0 {
return Vec::new();
}
let slice =
unsafe { core::slice::from_raw_parts(ptr as *const u8, len as usize) };
slice.to_vec()
}
/// JSON string value extraction, without Serde.
/// Finds `"key": "value"` and returns `value`.
/// FIXME: this is a hack, I need to look into serde without std...
fn json_get_str<'a>(json: &'a [u8], key: &str) -> Option<&'a str> {
let json_str = core::str::from_utf8(json).ok()?;
let pattern = format!("\"{}\"", key);
let key_pos = json_str.find(&pattern)?;
let after_key = &json_str[key_pos + pattern.len()..];
// Skip `: ` or `:`
let after_colon = after_key.trim_start().strip_prefix(':')?;
let after_colon = after_colon.trim_start();
if after_colon.starts_with('"') {
let value_start = 1;
let value_end = after_colon[value_start..].find('"')?;
Some(&after_colon[value_start..value_start + value_end])
} else {
None
}
}
/// Allocate memory for the host to write request data into.
#[unsafe(no_mangle)]
pub extern "C" fn alloc(size: i32) -> i32 {
if size <= 0 {
return 0;
}
unsafe {
let layout = Layout::from_size_align(size as usize, 1).unwrap();
let ptr = alloc::alloc::alloc(layout);
if ptr.is_null() {
return -1;
}
ptr as i32
}
}
// Lifecycle
#[unsafe(no_mangle)]
pub extern "C" fn initialize() -> i32 {
log_info("test-plugin initialized");
0
}
#[unsafe(no_mangle)]
pub extern "C" fn shutdown() -> i32 {
log_info("test-plugin shutdown");
0
}
/// Returns custom media type definitions: a `.testfile` type.
#[unsafe(no_mangle)]
pub extern "C" fn supported_media_types(_ptr: i32, _len: i32) {
let response = br#"[{"id":"testfile","name":"Test File","category":"document","extensions":["testfile"],"mime_types":["application/x-testfile"]}]"#;
set_response(response);
}
/// Check if this plugin can handle a given file path.
#[unsafe(no_mangle)]
pub extern "C" fn can_handle(ptr: i32, len: i32) {
let req = unsafe { read_request(ptr, len) };
let path = json_get_str(&req, "path").unwrap_or("");
let can = path.ends_with(".testfile");
if can {
set_response(br#"{"can_handle":true}"#);
} else {
set_response(br#"{"can_handle":false}"#);
}
}
/// Returns the list of media types this extractor supports.
#[unsafe(no_mangle)]
pub extern "C" fn supported_types(_ptr: i32, _len: i32) {
set_response(br#"["testfile"]"#);
}
/// Extract metadata from a file.
#[unsafe(no_mangle)]
pub extern "C" fn extract_metadata(ptr: i32, len: i32) {
let req = unsafe { read_request(ptr, len) };
let path = json_get_str(&req, "path").unwrap_or("unknown");
// Extract filename from path
let filename = path.rsplit('/').next().unwrap_or(path);
let response = format!(
r#"{{"title":"{}","artist":"test-plugin","description":"Extracted by test-plugin","extra":{{"plugin_source":"test-plugin"}}}}"#,
filename,
);
set_response(response.as_bytes());
}
/// Generate a thumbnail (returns a fixed path for testing).
#[unsafe(no_mangle)]
pub extern "C" fn generate_thumbnail(ptr: i32, len: i32) {
let req = unsafe { read_request(ptr, len) };
let output =
json_get_str(&req, "output_path").unwrap_or("/tmp/test-thumb.jpg");
let response = format!(
r#"{{"path":"{}","width":320,"height":320,"format":"jpeg"}}"#,
output
);
set_response(response.as_bytes());
}
/// Returns the event types this handler is interested in.
#[unsafe(no_mangle)]
pub extern "C" fn interested_events(_ptr: i32, _len: i32) {
set_response(br#"["MediaImported","MediaUpdated","MediaDeleted"]"#);
}
/// Handle an event.
#[unsafe(no_mangle)]
pub extern "C" fn handle_event(ptr: i32, len: i32) {
let req = unsafe { read_request(ptr, len) };
let event_type = json_get_str(&req, "event_type").unwrap_or("unknown");
let msg = format!("test-plugin handled event: {}", event_type);
log_info(&msg);
set_response(b"{}");
}