1use std::collections::HashMap;
61use std::sync::{Arc, Mutex, RwLock};
62use std::time::{Duration, Instant};
63
64use crate::plugin_manifest::{ManifestCapabilities, PluginManifest};
65use crate::wasm_host_abi::{HostCallResult, HostFunctionContext};
66use crate::wasm_runtime::WasmPluginCapabilities;
67
68#[derive(Debug, Clone)]
74pub struct SandboxConfig {
75 pub max_memory_bytes: usize,
77 pub fuel_limit: u64,
79 pub epoch_interval: Duration,
81 pub max_sandboxes: usize,
83 pub enable_tracing: bool,
85 pub pool_size: usize,
87 pub host_timeout: Duration,
89}
90
91impl Default for SandboxConfig {
92 fn default() -> Self {
93 Self {
94 max_memory_bytes: 16 * 1024 * 1024, fuel_limit: 1_000_000_000,
96 epoch_interval: Duration::from_millis(10),
97 max_sandboxes: 64,
98 enable_tracing: false,
99 pool_size: 4,
100 host_timeout: Duration::from_secs(30),
101 }
102 }
103}
104
105pub struct WasmSandboxRuntime {
117 config: SandboxConfig,
119 sandboxes: RwLock<HashMap<String, Arc<PluginSandbox>>>,
121 module_cache: RwLock<HashMap<String, CompiledModule>>,
123 stats: RwLock<SandboxRuntimeStats>,
125 host_context: Arc<dyn HostContextProvider + Send + Sync>,
127 shutdown: Mutex<bool>,
129}
130
131pub trait HostContextProvider: Send + Sync {
133 fn create_context(
135 &self,
136 plugin_id: &str,
137 capabilities: &ManifestCapabilities,
138 ) -> HostFunctionContext;
139
140 fn read(&self, ctx: &HostFunctionContext, table_id: u32, row_id: u64) -> HostCallResult;
142
143 fn write(
145 &self,
146 ctx: &HostFunctionContext,
147 table_id: u32,
148 row_id: u64,
149 data: &[u8],
150 ) -> HostCallResult;
151
152 fn vector_search(
154 &self,
155 ctx: &HostFunctionContext,
156 index: &str,
157 vector: &[f32],
158 top_k: u32,
159 ) -> HostCallResult;
160
161 fn log(&self, ctx: &HostFunctionContext, level: u8, message: &str);
163}
164
165#[derive(Clone)]
167#[allow(dead_code)]
168struct CompiledModule {
169 wasm_bytes: Vec<u8>,
171 compiled_at: Instant,
173 instantiation_count: u64,
175 source_hash: u64,
177}
178
179#[allow(dead_code)]
181pub struct PluginSandbox {
182 plugin_id: String,
184 manifest: PluginManifest,
186 capabilities: ManifestCapabilities,
188 memory_used: Mutex<usize>,
190 fuel_remaining: Mutex<u64>,
192 call_count: Mutex<u64>,
194 created_at: Instant,
196 last_activity: Mutex<Instant>,
198 state: Mutex<SandboxState>,
200 host_context: HostFunctionContext,
202 stats: Mutex<SandboxStats>,
204}
205
206#[derive(Debug, Clone, Copy, PartialEq, Eq)]
208pub enum SandboxState {
209 Ready,
211 Executing,
213 Suspended,
215 Terminated,
217 Failed,
219}
220
221#[derive(Debug, Clone, Default)]
223pub struct SandboxStats {
224 pub total_calls: u64,
226 pub successful_calls: u64,
228 pub failed_calls: u64,
230 pub fuel_consumed: u64,
232 pub total_execution_time: Duration,
234 pub peak_memory_bytes: usize,
236 pub host_calls: u64,
238 pub host_call_errors: u64,
240}
241
242#[derive(Debug, Clone, Default)]
244pub struct SandboxRuntimeStats {
245 pub sandboxes_created: u64,
247 pub active_sandboxes: u64,
249 pub total_invocations: u64,
251 pub total_fuel_consumed: u64,
253 pub module_cache_hits: u64,
255 pub module_cache_misses: u64,
257 pub memory_violations: u64,
259 pub fuel_exhaustions: u64,
261 pub capability_denials: u64,
263}
264
265impl WasmSandboxRuntime {
270 pub fn new(
272 config: SandboxConfig,
273 host_context: Arc<dyn HostContextProvider + Send + Sync>,
274 ) -> Self {
275 Self {
276 config,
277 sandboxes: RwLock::new(HashMap::new()),
278 module_cache: RwLock::new(HashMap::new()),
279 stats: RwLock::new(SandboxRuntimeStats::default()),
280 host_context,
281 shutdown: Mutex::new(false),
282 }
283 }
284
285 pub fn load_plugin(
287 &self,
288 plugin_id: &str,
289 wasm_bytes: &[u8],
290 manifest: PluginManifest,
291 ) -> Result<(), SandboxError> {
292 if *self.shutdown.lock().unwrap() {
294 return Err(SandboxError::RuntimeShutdown);
295 }
296
297 let active = self.sandboxes.read().unwrap().len();
299 if active >= self.config.max_sandboxes {
300 return Err(SandboxError::TooManySandboxes {
301 current: active,
302 max: self.config.max_sandboxes,
303 });
304 }
305
306 self.validate_wasm(wasm_bytes)?;
308
309 let source_hash = self.compute_hash(wasm_bytes);
311 let compiled = CompiledModule {
312 wasm_bytes: wasm_bytes.to_vec(),
313 compiled_at: Instant::now(),
314 instantiation_count: 0,
315 source_hash,
316 };
317
318 self.module_cache
319 .write()
320 .unwrap()
321 .insert(plugin_id.to_string(), compiled);
322
323 let capabilities = manifest.capabilities.clone();
325
326 let host_context = self.host_context.create_context(plugin_id, &capabilities);
328
329 let sandbox = PluginSandbox {
331 plugin_id: plugin_id.to_string(),
332 manifest,
333 capabilities,
334 memory_used: Mutex::new(0),
335 fuel_remaining: Mutex::new(self.config.fuel_limit),
336 call_count: Mutex::new(0),
337 created_at: Instant::now(),
338 last_activity: Mutex::new(Instant::now()),
339 state: Mutex::new(SandboxState::Ready),
340 host_context,
341 stats: Mutex::new(SandboxStats::default()),
342 };
343
344 self.sandboxes
345 .write()
346 .unwrap()
347 .insert(plugin_id.to_string(), Arc::new(sandbox));
348
349 let mut stats = self.stats.write().unwrap();
351 stats.sandboxes_created += 1;
352 stats.active_sandboxes += 1;
353
354 Ok(())
355 }
356
357 pub fn invoke(
359 &self,
360 plugin_id: &str,
361 function: &str,
362 args: &[SandboxValue],
363 ) -> Result<Vec<SandboxValue>, SandboxError> {
364 let sandbox = self.get_sandbox(plugin_id)?;
365
366 {
368 let state = sandbox.state.lock().unwrap();
369 match *state {
370 SandboxState::Terminated => {
371 return Err(SandboxError::SandboxTerminated(plugin_id.to_string()));
372 }
373 SandboxState::Failed => {
374 return Err(SandboxError::SandboxFailed(plugin_id.to_string()));
375 }
376 SandboxState::Executing => {
377 return Err(SandboxError::AlreadyExecuting(plugin_id.to_string()));
378 }
379 _ => {}
380 }
381 }
382
383 *sandbox.state.lock().unwrap() = SandboxState::Executing;
385 *sandbox.last_activity.lock().unwrap() = Instant::now();
386
387 let start = Instant::now();
388
389 let result = self.execute_with_limits(&sandbox, function, args);
391
392 let elapsed = start.elapsed();
394 {
395 let mut stats = sandbox.stats.lock().unwrap();
396 stats.total_calls += 1;
397 stats.total_execution_time += elapsed;
398
399 if result.is_ok() {
400 stats.successful_calls += 1;
401 } else {
402 stats.failed_calls += 1;
403 }
404 }
405
406 *sandbox.call_count.lock().unwrap() += 1;
407
408 {
410 let mut global_stats = self.stats.write().unwrap();
411 global_stats.total_invocations += 1;
412 }
413
414 *sandbox.state.lock().unwrap() = SandboxState::Ready;
416
417 result
418 }
419
420 fn execute_with_limits(
422 &self,
423 sandbox: &PluginSandbox,
424 function: &str,
425 args: &[SandboxValue],
426 ) -> Result<Vec<SandboxValue>, SandboxError> {
427 let fuel_available = *sandbox.fuel_remaining.lock().unwrap();
429 if fuel_available == 0 {
430 self.stats.write().unwrap().fuel_exhaustions += 1;
431 return Err(SandboxError::FuelExhausted {
432 plugin_id: sandbox.plugin_id.clone(),
433 consumed: 0,
434 });
435 }
436
437 let memory_used = *sandbox.memory_used.lock().unwrap();
439 if memory_used > self.config.max_memory_bytes {
440 self.stats.write().unwrap().memory_violations += 1;
441 return Err(SandboxError::MemoryLimitExceeded {
442 plugin_id: sandbox.plugin_id.clone(),
443 used: memory_used,
444 limit: self.config.max_memory_bytes,
445 });
446 }
447
448 let fuel_consumed = (function.len() as u64 * 1000) + (args.len() as u64 * 100);
457 *sandbox.fuel_remaining.lock().unwrap() -= fuel_consumed.min(fuel_available);
458
459 {
460 let mut stats = sandbox.stats.lock().unwrap();
461 stats.fuel_consumed += fuel_consumed.min(fuel_available);
462 }
463
464 Ok(vec![SandboxValue::I32(0)])
466 }
467
468 fn get_sandbox(&self, plugin_id: &str) -> Result<Arc<PluginSandbox>, SandboxError> {
470 self.sandboxes
471 .read()
472 .unwrap()
473 .get(plugin_id)
474 .cloned()
475 .ok_or_else(|| SandboxError::PluginNotFound(plugin_id.to_string()))
476 }
477
478 fn validate_wasm(&self, wasm_bytes: &[u8]) -> Result<(), SandboxError> {
480 if wasm_bytes.len() < 8 {
482 return Err(SandboxError::InvalidWasm("too short".to_string()));
483 }
484
485 if &wasm_bytes[0..4] != b"\0asm" {
486 return Err(SandboxError::InvalidWasm(
487 "invalid magic number".to_string(),
488 ));
489 }
490
491 let version =
493 u32::from_le_bytes([wasm_bytes[4], wasm_bytes[5], wasm_bytes[6], wasm_bytes[7]]);
494 if version != 1 {
495 return Err(SandboxError::InvalidWasm(format!(
496 "unsupported version: {}",
497 version
498 )));
499 }
500
501 Ok(())
502 }
503
504 fn compute_hash(&self, bytes: &[u8]) -> u64 {
506 use std::collections::hash_map::DefaultHasher;
507 use std::hash::{Hash, Hasher};
508
509 let mut hasher = DefaultHasher::new();
510 bytes.hash(&mut hasher);
511 hasher.finish()
512 }
513
514 pub fn unload_plugin(&self, plugin_id: &str) -> Result<(), SandboxError> {
516 let sandbox = self
517 .sandboxes
518 .write()
519 .unwrap()
520 .remove(plugin_id)
521 .ok_or_else(|| SandboxError::PluginNotFound(plugin_id.to_string()))?;
522
523 *sandbox.state.lock().unwrap() = SandboxState::Terminated;
525
526 self.stats.write().unwrap().active_sandboxes -= 1;
528
529 self.module_cache.write().unwrap().remove(plugin_id);
531
532 Ok(())
533 }
534
535 pub fn hot_reload(
537 &self,
538 plugin_id: &str,
539 new_wasm_bytes: &[u8],
540 new_manifest: PluginManifest,
541 ) -> Result<(), SandboxError> {
542 self.validate_wasm(new_wasm_bytes)?;
544
545 let old_sandbox = self.get_sandbox(plugin_id)?;
547
548 loop {
550 let state = *old_sandbox.state.lock().unwrap();
551 if state != SandboxState::Executing {
552 break;
553 }
554 std::thread::sleep(Duration::from_millis(10));
555 }
556
557 self.unload_plugin(plugin_id)?;
559
560 self.load_plugin(plugin_id, new_wasm_bytes, new_manifest)?;
562
563 Ok(())
564 }
565
566 pub fn get_plugin_stats(&self, plugin_id: &str) -> Result<SandboxStats, SandboxError> {
568 let sandbox = self.get_sandbox(plugin_id)?;
569 Ok(sandbox.stats.lock().unwrap().clone())
570 }
571
572 pub fn get_runtime_stats(&self) -> SandboxRuntimeStats {
574 self.stats.read().unwrap().clone()
575 }
576
577 pub fn list_plugins(&self) -> Vec<PluginInfo> {
579 self.sandboxes
580 .read()
581 .unwrap()
582 .values()
583 .map(|s| PluginInfo {
584 id: s.plugin_id.clone(),
585 name: s.manifest.plugin.name.clone(),
586 version: s.manifest.plugin.version.clone(),
587 state: *s.state.lock().unwrap(),
588 memory_used: *s.memory_used.lock().unwrap(),
589 call_count: *s.call_count.lock().unwrap(),
590 uptime: s.created_at.elapsed(),
591 })
592 .collect()
593 }
594
595 pub fn reset_fuel(&self, plugin_id: &str) -> Result<(), SandboxError> {
597 let sandbox = self.get_sandbox(plugin_id)?;
598 *sandbox.fuel_remaining.lock().unwrap() = self.config.fuel_limit;
599 Ok(())
600 }
601
602 pub fn shutdown(&self) {
604 *self.shutdown.lock().unwrap() = true;
605
606 let sandboxes: Vec<_> = self.sandboxes.read().unwrap().values().cloned().collect();
608
609 for sandbox in sandboxes {
610 *sandbox.state.lock().unwrap() = SandboxState::Terminated;
611 }
612
613 self.sandboxes.write().unwrap().clear();
614 self.module_cache.write().unwrap().clear();
615 }
616}
617
618#[derive(Debug, Clone, PartialEq)]
624pub enum SandboxValue {
625 I32(i32),
627 I64(i64),
629 F32(f32),
631 F64(f64),
633 Bytes(Vec<u8>),
635 String(String),
637}
638
639impl SandboxValue {
640 pub fn as_i32(&self) -> Option<i32> {
642 match self {
643 SandboxValue::I32(v) => Some(*v),
644 _ => None,
645 }
646 }
647
648 pub fn as_i64(&self) -> Option<i64> {
650 match self {
651 SandboxValue::I64(v) => Some(*v),
652 _ => None,
653 }
654 }
655
656 pub fn as_bytes(&self) -> Option<&[u8]> {
658 match self {
659 SandboxValue::Bytes(v) => Some(v),
660 SandboxValue::String(s) => Some(s.as_bytes()),
661 _ => None,
662 }
663 }
664}
665
666#[derive(Debug, Clone)]
672pub struct PluginInfo {
673 pub id: String,
675 pub name: String,
677 pub version: String,
679 pub state: SandboxState,
681 pub memory_used: usize,
683 pub call_count: u64,
685 pub uptime: Duration,
687}
688
689#[derive(Debug, Clone)]
695pub enum SandboxError {
696 PluginNotFound(String),
698 InvalidWasm(String),
700 MemoryLimitExceeded {
702 plugin_id: String,
703 used: usize,
704 limit: usize,
705 },
706 FuelExhausted { plugin_id: String, consumed: u64 },
708 CapabilityDenied {
710 plugin_id: String,
711 capability: String,
712 },
713 TooManySandboxes { current: usize, max: usize },
715 SandboxTerminated(String),
717 SandboxFailed(String),
719 AlreadyExecuting(String),
721 RuntimeShutdown,
723 HostError(String),
725 Timeout {
727 plugin_id: String,
728 elapsed: Duration,
729 },
730 Trap { plugin_id: String, message: String },
732}
733
734impl std::fmt::Display for SandboxError {
735 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
736 match self {
737 SandboxError::PluginNotFound(id) => write!(f, "plugin not found: {}", id),
738 SandboxError::InvalidWasm(msg) => write!(f, "invalid WASM: {}", msg),
739 SandboxError::MemoryLimitExceeded {
740 plugin_id,
741 used,
742 limit,
743 } => {
744 write!(
745 f,
746 "plugin {} exceeded memory limit: {} > {}",
747 plugin_id, used, limit
748 )
749 }
750 SandboxError::FuelExhausted {
751 plugin_id,
752 consumed,
753 } => {
754 write!(f, "plugin {} exhausted fuel after {}", plugin_id, consumed)
755 }
756 SandboxError::CapabilityDenied {
757 plugin_id,
758 capability,
759 } => {
760 write!(f, "plugin {} denied capability: {}", plugin_id, capability)
761 }
762 SandboxError::TooManySandboxes { current, max } => {
763 write!(f, "too many sandboxes: {} >= {}", current, max)
764 }
765 SandboxError::SandboxTerminated(id) => write!(f, "sandbox terminated: {}", id),
766 SandboxError::SandboxFailed(id) => write!(f, "sandbox failed: {}", id),
767 SandboxError::AlreadyExecuting(id) => write!(f, "sandbox already executing: {}", id),
768 SandboxError::RuntimeShutdown => write!(f, "runtime is shutdown"),
769 SandboxError::HostError(msg) => write!(f, "host error: {}", msg),
770 SandboxError::Timeout { plugin_id, elapsed } => {
771 write!(f, "plugin {} timed out after {:?}", plugin_id, elapsed)
772 }
773 SandboxError::Trap { plugin_id, message } => {
774 write!(f, "plugin {} trapped: {}", plugin_id, message)
775 }
776 }
777 }
778}
779
780impl std::error::Error for SandboxError {}
781
782pub struct DefaultHostContextProvider;
788
789impl HostContextProvider for DefaultHostContextProvider {
790 fn create_context(
791 &self,
792 plugin_id: &str,
793 capabilities: &ManifestCapabilities,
794 ) -> HostFunctionContext {
795 let wasm_caps = WasmPluginCapabilities {
797 can_read_table: capabilities.can_read_table.clone(),
798 can_write_table: capabilities.can_write_table.clone(),
799 can_vector_search: capabilities.can_vector_search,
800 can_index_search: capabilities.can_index_search,
801 can_call_plugin: capabilities.can_call_plugin.clone(),
802 memory_limit_bytes: 16 * 1024 * 1024, fuel_limit: 1_000_000,
804 timeout_ms: 100,
805 };
806 HostFunctionContext::new(plugin_id, wasm_caps)
807 }
808
809 fn read(&self, _ctx: &HostFunctionContext, _table_id: u32, _row_id: u64) -> HostCallResult {
810 HostCallResult::Success(Vec::new())
811 }
812
813 fn write(
814 &self,
815 _ctx: &HostFunctionContext,
816 _table_id: u32,
817 _row_id: u64,
818 _data: &[u8],
819 ) -> HostCallResult {
820 HostCallResult::Ok
821 }
822
823 fn vector_search(
824 &self,
825 _ctx: &HostFunctionContext,
826 _index: &str,
827 _vector: &[f32],
828 _top_k: u32,
829 ) -> HostCallResult {
830 HostCallResult::Success(Vec::new())
831 }
832
833 fn log(&self, _ctx: &HostFunctionContext, _level: u8, _message: &str) {
834 }
836}
837
838#[cfg(test)]
843mod tests {
844 use super::*;
845
846 fn create_test_runtime() -> WasmSandboxRuntime {
847 WasmSandboxRuntime::new(
848 SandboxConfig::default(),
849 Arc::new(DefaultHostContextProvider),
850 )
851 }
852
853 fn create_test_manifest() -> PluginManifest {
854 PluginManifest {
855 plugin: crate::plugin_manifest::PluginMetadata {
856 name: "test-plugin".to_string(),
857 version: "1.0.0".to_string(),
858 description: "Test plugin".to_string(),
859 author: "Test Author".to_string(),
860 license: Some("MIT".to_string()),
861 homepage: None,
862 repository: None,
863 min_kernel_version: None,
864 },
865 capabilities: crate::plugin_manifest::ManifestCapabilities::default(),
866 resources: crate::plugin_manifest::ResourceLimits::default(),
867 exports: crate::plugin_manifest::ExportedFunctions::default(),
868 hooks: crate::plugin_manifest::TableHooks::default(),
869 config_schema: None,
870 }
871 }
872
873 fn create_valid_wasm() -> Vec<u8> {
874 vec![
876 0x00, 0x61, 0x73, 0x6d, 0x01, 0x00, 0x00, 0x00, ]
879 }
880
881 #[test]
882 fn test_load_plugin() {
883 let runtime = create_test_runtime();
884 let manifest = create_test_manifest();
885 let wasm = create_valid_wasm();
886
887 let result = runtime.load_plugin("test", &wasm, manifest);
888 assert!(result.is_ok());
889
890 let plugins = runtime.list_plugins();
891 assert_eq!(plugins.len(), 1);
892 assert_eq!(plugins[0].id, "test");
893 }
894
895 #[test]
896 fn test_load_invalid_wasm() {
897 let runtime = create_test_runtime();
898 let manifest = create_test_manifest();
899
900 let result = runtime.load_plugin("test", b"not wasm", manifest);
901 assert!(matches!(result, Err(SandboxError::InvalidWasm(_))));
902 }
903
904 #[test]
905 fn test_unload_plugin() {
906 let runtime = create_test_runtime();
907 let manifest = create_test_manifest();
908 let wasm = create_valid_wasm();
909
910 runtime.load_plugin("test", &wasm, manifest).unwrap();
911 assert_eq!(runtime.list_plugins().len(), 1);
912
913 runtime.unload_plugin("test").unwrap();
914 assert_eq!(runtime.list_plugins().len(), 0);
915 }
916
917 #[test]
918 fn test_invoke_plugin() {
919 let runtime = create_test_runtime();
920 let manifest = create_test_manifest();
921 let wasm = create_valid_wasm();
922
923 runtime.load_plugin("test", &wasm, manifest).unwrap();
924
925 let result = runtime.invoke("test", "test_fn", &[SandboxValue::I32(42)]);
926 assert!(result.is_ok());
927 }
928
929 #[test]
930 fn test_invoke_nonexistent() {
931 let runtime = create_test_runtime();
932
933 let result = runtime.invoke("nonexistent", "fn", &[]);
934 assert!(matches!(result, Err(SandboxError::PluginNotFound(_))));
935 }
936
937 #[test]
938 fn test_sandbox_limit() {
939 let config = SandboxConfig {
940 max_sandboxes: 2,
941 ..Default::default()
942 };
943 let runtime = WasmSandboxRuntime::new(config, Arc::new(DefaultHostContextProvider));
944 let wasm = create_valid_wasm();
945
946 runtime
947 .load_plugin("p1", &wasm, create_test_manifest())
948 .unwrap();
949 runtime
950 .load_plugin("p2", &wasm, create_test_manifest())
951 .unwrap();
952
953 let result = runtime.load_plugin("p3", &wasm, create_test_manifest());
954 assert!(matches!(result, Err(SandboxError::TooManySandboxes { .. })));
955 }
956
957 #[test]
958 fn test_runtime_stats() {
959 let runtime = create_test_runtime();
960 let manifest = create_test_manifest();
961 let wasm = create_valid_wasm();
962
963 runtime.load_plugin("test", &wasm, manifest).unwrap();
964 runtime.invoke("test", "fn1", &[]).unwrap();
965 runtime.invoke("test", "fn2", &[]).unwrap();
966
967 let stats = runtime.get_runtime_stats();
968 assert_eq!(stats.sandboxes_created, 1);
969 assert_eq!(stats.active_sandboxes, 1);
970 assert_eq!(stats.total_invocations, 2);
971 }
972
973 #[test]
974 fn test_plugin_stats() {
975 let runtime = create_test_runtime();
976 let manifest = create_test_manifest();
977 let wasm = create_valid_wasm();
978
979 runtime.load_plugin("test", &wasm, manifest).unwrap();
980 runtime.invoke("test", "fn", &[]).unwrap();
981
982 let stats = runtime.get_plugin_stats("test").unwrap();
983 assert_eq!(stats.total_calls, 1);
984 assert_eq!(stats.successful_calls, 1);
985 }
986
987 #[test]
988 fn test_hot_reload() {
989 let runtime = create_test_runtime();
990 let manifest = create_test_manifest();
991 let wasm = create_valid_wasm();
992
993 runtime
994 .load_plugin("test", &wasm, manifest.clone())
995 .unwrap();
996
997 let result = runtime.hot_reload("test", &wasm, manifest);
999 assert!(result.is_ok());
1000
1001 let stats = runtime.get_plugin_stats("test").unwrap();
1003 assert_eq!(stats.total_calls, 0);
1004 }
1005
1006 #[test]
1007 fn test_reset_fuel() {
1008 let runtime = create_test_runtime();
1009 let manifest = create_test_manifest();
1010 let wasm = create_valid_wasm();
1011
1012 runtime.load_plugin("test", &wasm, manifest).unwrap();
1013
1014 runtime.invoke("test", "some_function", &[]).unwrap();
1016
1017 runtime.reset_fuel("test").unwrap();
1019
1020 let result = runtime.invoke("test", "fn", &[]);
1022 assert!(result.is_ok());
1023 }
1024
1025 #[test]
1026 fn test_shutdown() {
1027 let runtime = create_test_runtime();
1028 let manifest = create_test_manifest();
1029 let wasm = create_valid_wasm();
1030
1031 runtime.load_plugin("test", &wasm, manifest).unwrap();
1032 runtime.shutdown();
1033
1034 assert_eq!(runtime.list_plugins().len(), 0);
1035
1036 let result = runtime.load_plugin("new", &wasm, create_test_manifest());
1037 assert!(matches!(result, Err(SandboxError::RuntimeShutdown)));
1038 }
1039
1040 #[test]
1041 fn test_sandbox_value() {
1042 let v1 = SandboxValue::I32(42);
1043 assert_eq!(v1.as_i32(), Some(42));
1044 assert_eq!(v1.as_i64(), None);
1045
1046 let v2 = SandboxValue::String("hello".to_string());
1047 assert_eq!(v2.as_bytes(), Some(b"hello".as_slice()));
1048 }
1049}