/ ferris-proof-plugins / tests / plugin_system_tests.rs
plugin_system_tests.rs
  1  use ferris_proof_core::plugins::{PluginManager, ValidationStatus, VerificationPlugin};
  2  use ferris_proof_plugins::sandbox::{NetworkPolicy, ResourceLimits};
  3  use ferris_proof_plugins::{ProptestPlugin, SandboxedExecutor, TlaPlusPlugin};
  4  use serde_json::json;
  5  use std::collections::HashMap;
  6  use std::time::Duration;
  7  
  8  #[cfg(test)]
  9  mod plugin_system_tests {
 10      use super::*;
 11  
 12      #[test]
 13      /// Test plugin loading and version compatibility
 14      /// Validates: Requirements 9.11, 9.16
 15      fn test_plugin_loading_and_version_compatibility() {
 16          let mut plugin_manager = PluginManager::new();
 17  
 18          // Test loading TLA+ plugin
 19          let tla_plugin = Box::new(TlaPlusPlugin::new());
 20          let plugin_name = tla_plugin.name().to_string();
 21          let plugin_version = tla_plugin.version().to_string();
 22  
 23          // Plugin should load successfully
 24          let load_result = plugin_manager.register_plugin(tla_plugin);
 25          assert!(load_result.is_ok(), "TLA+ plugin should load successfully");
 26  
 27          // Check that plugin is registered
 28          let plugin_metadata = plugin_manager.plugin_metadata(&plugin_name);
 29          assert!(
 30              plugin_metadata.is_some(),
 31              "Plugin metadata should be available"
 32          );
 33  
 34          let metadata = plugin_metadata.unwrap();
 35          assert_eq!(metadata.name, plugin_name);
 36          assert_eq!(metadata.version, plugin_version);
 37  
 38          // Test loading Proptest plugin
 39          let proptest_plugin = Box::new(ProptestPlugin::new());
 40          let proptest_name = proptest_plugin.name().to_string();
 41  
 42          let load_result = plugin_manager.register_plugin(proptest_plugin);
 43          assert!(
 44              load_result.is_ok(),
 45              "Proptest plugin should load successfully"
 46          );
 47  
 48          // Verify both plugins are listed
 49          let all_plugins = plugin_manager.list_plugins();
 50          assert!(
 51              all_plugins.len() >= 2,
 52              "Should have at least 2 plugins registered"
 53          );
 54  
 55          let plugin_names: Vec<String> = all_plugins.iter().map(|p| p.name.clone()).collect();
 56          assert!(
 57              plugin_names.contains(&plugin_name),
 58              "TLA+ plugin should be in list"
 59          );
 60          assert!(
 61              plugin_names.contains(&proptest_name),
 62              "Proptest plugin should be in list"
 63          );
 64      }
 65  
 66      #[test]
 67      /// Test tool discovery and availability checking
 68      /// Validates: Requirements 9.11, 9.16
 69      fn test_tool_discovery_and_availability() {
 70          let mut plugin_manager = PluginManager::new();
 71  
 72          // Register plugins
 73          let tla_plugin = Box::new(TlaPlusPlugin::new());
 74          let proptest_plugin = Box::new(ProptestPlugin::new());
 75  
 76          plugin_manager
 77              .register_plugin(tla_plugin)
 78              .expect("TLA+ plugin should register");
 79          plugin_manager
 80              .register_plugin(proptest_plugin)
 81              .expect("Proptest plugin should register");
 82  
 83          // Validate tools
 84          let validation_results = plugin_manager
 85              .validate_tools()
 86              .expect("Tool validation should succeed");
 87  
 88          assert!(
 89              !validation_results.is_empty(),
 90              "Should have validation results"
 91          );
 92  
 93          for result in &validation_results {
 94              // Each plugin should have a validation result
 95              assert!(
 96                  !result.plugin_name.is_empty(),
 97                  "Plugin name should not be empty"
 98              );
 99  
100              match result.status {
101                  ValidationStatus::Valid => {
102                      assert!(
103                          result.tool_info.is_some(),
104                          "Valid tools should have tool info"
105                      );
106                      let tool_info = result.tool_info.as_ref().unwrap();
107                      assert!(
108                          tool_info.available,
109                          "Valid tools should be marked as available"
110                      );
111                      assert!(
112                          !tool_info.capabilities.is_empty(),
113                          "Valid tools should have capabilities"
114                      );
115                  }
116                  ValidationStatus::Unavailable => {
117                      // Tool not available - this is acceptable in test environments
118                      assert!(
119                          !result.issues.is_empty(),
120                          "Unavailable tools should have issues listed"
121                      );
122                  }
123                  ValidationStatus::VersionIncompatible => {
124                      // Version incompatible - should have tool info but marked as incompatible
125                      assert!(
126                          !result.issues.is_empty(),
127                          "Incompatible tools should have issues listed"
128                      );
129                  }
130                  ValidationStatus::Error => {
131                      // Error occurred during validation
132                      assert!(
133                          !result.issues.is_empty(),
134                          "Error status should have issues listed"
135                      );
136                  }
137              }
138          }
139      }
140  
141      #[test]
142      /// Test plugin metadata and capabilities
143      /// Validates: Requirements 9.15
144      fn test_plugin_metadata_and_capabilities() {
145          let tla_plugin = TlaPlusPlugin::new();
146          let proptest_plugin = ProptestPlugin::new();
147  
148          // Test TLA+ plugin metadata
149          let tla_metadata = tla_plugin.metadata();
150          assert_eq!(tla_metadata.name, "tla-plus");
151          assert!(!tla_metadata.version.is_empty());
152          assert!(!tla_metadata.description.is_empty());
153          assert!(!tla_metadata.techniques.is_empty());
154          assert!(tla_metadata
155              .techniques
156              .contains(&ferris_proof_core::types::Technique::FormalSpecs));
157          assert!(tla_metadata
158              .techniques
159              .contains(&ferris_proof_core::types::Technique::ModelChecking));
160  
161          // Test Proptest plugin metadata
162          let proptest_metadata = proptest_plugin.metadata();
163          assert_eq!(proptest_metadata.name, "proptest");
164          assert!(!proptest_metadata.version.is_empty());
165          assert!(!proptest_metadata.description.is_empty());
166          assert!(!proptest_metadata.techniques.is_empty());
167          assert!(proptest_metadata
168              .techniques
169              .contains(&ferris_proof_core::types::Technique::PropertyTests));
170  
171          // Test supported techniques
172          let tla_techniques = tla_plugin.supported_techniques();
173          assert!(!tla_techniques.is_empty());
174  
175          let proptest_techniques = proptest_plugin.supported_techniques();
176          assert!(!proptest_techniques.is_empty());
177  
178          // Test version ranges
179          let tla_versions = tla_plugin.supported_versions();
180          assert!(
181              tla_versions.min.is_some()
182                  || tla_versions.max.is_some()
183                  || tla_versions.requires_exact.is_some()
184          );
185  
186          let proptest_versions = proptest_plugin.supported_versions();
187          assert!(
188              proptest_versions.min.is_some()
189                  || proptest_versions.max.is_some()
190                  || proptest_versions.requires_exact.is_some()
191          );
192      }
193  
194      #[test]
195      /// Test plugin initialization and cleanup
196      /// Validates: Requirements 9.15
197      fn test_plugin_initialization_and_cleanup() {
198          let mut tla_plugin = TlaPlusPlugin::new();
199          let mut proptest_plugin = ProptestPlugin::new();
200  
201          // Test initialization with empty config
202          let empty_config = json!({});
203  
204          // TLA+ plugin initialization might fail if TLC is not available, which is acceptable
205          let _tla_init_result = tla_plugin.initialize(&empty_config);
206          // Don't assert success since TLC might not be installed
207  
208          // Proptest plugin initialization might fail if Rust toolchain is not available
209          let _proptest_init_result = proptest_plugin.initialize(&empty_config);
210          // Don't assert success since Rust might not be installed
211  
212          // Test initialization with specific config
213          let tla_config = json!({
214              "tla_plus": {
215                  "tlc_path": "/usr/local/bin/tlc"
216              }
217          });
218  
219          let proptest_config = json!({
220              "proptest": {
221                  "path": "cargo"
222              }
223          });
224  
225          // These might fail if tools are not available, which is acceptable in tests
226          let _ = tla_plugin.initialize(&tla_config);
227          let _ = proptest_plugin.initialize(&proptest_config);
228  
229          // Test cleanup - should always succeed
230          let tla_cleanup = tla_plugin.cleanup();
231          assert!(tla_cleanup.is_ok(), "TLA+ plugin cleanup should succeed");
232  
233          let proptest_cleanup = proptest_plugin.cleanup();
234          assert!(
235              proptest_cleanup.is_ok(),
236              "Proptest plugin cleanup should succeed"
237          );
238      }
239  
240      #[tokio::test]
241      /// Test sandboxed execution with resource limits
242      /// Validates: Requirements 12.4
243      async fn test_sandboxed_execution_with_resource_limits() {
244          let executor = SandboxedExecutor::new()
245              .with_network_policy(NetworkPolicy::Denied)
246              .with_limits(ResourceLimits {
247                  max_memory: 50 * 1024 * 1024, // 50MB
248                  max_cpu_time: 5,              // 5 seconds
249                  max_file_descriptors: 32,
250                  max_processes: 2,
251                  max_file_size: 1024, // 1KB
252              })
253              .with_timeout(Duration::from_secs(3));
254  
255          // Test executing a simple command
256          let result = executor
257              .execute("echo", &["Hello, World!"], HashMap::new(), None)
258              .await;
259  
260          match result {
261              Ok(output) => {
262                  assert_eq!(output.exit_code, 0, "Echo command should succeed");
263                  assert!(
264                      output.stdout.contains("Hello, World!"),
265                      "Output should contain expected text"
266                  );
267                  assert!(!output.timeout_occurred, "Command should not timeout");
268              }
269              Err(e) => {
270                  // Command might not be available in test environment
271                  let error_msg = e.to_string();
272                  assert!(
273                      error_msg.contains("not found")
274                          || error_msg.contains("No such file")
275                          || error_msg.contains("Failed to spawn process"),
276                      "Error should be due to command not found, got: {}",
277                      error_msg
278                  );
279              }
280          }
281      }
282  
283      #[tokio::test]
284      /// Test sandboxed execution timeout handling
285      /// Validates: Requirements 12.4
286      async fn test_sandboxed_execution_timeout_handling() {
287          let executor = SandboxedExecutor::new()
288              .with_network_policy(NetworkPolicy::Denied)
289              .with_limits(ResourceLimits::default())
290              .with_timeout(Duration::from_millis(100)); // Very short timeout
291  
292          // Test with a command that should timeout (sleep)
293          let result = executor
294              .execute(
295                  "sleep",
296                  &["5"], // Sleep for 5 seconds, but timeout is 100ms
297                  HashMap::new(),
298                  None,
299              )
300              .await;
301  
302          match result {
303              Ok(output) => {
304                  // If sleep command is available, it should timeout
305                  if output.timeout_occurred {
306                      assert_ne!(
307                          output.exit_code, 0,
308                          "Timed out command should have non-zero exit code"
309                      );
310                      assert!(
311                          output.stderr.contains("timeout"),
312                          "Error should mention timeout"
313                      );
314                  } else {
315                      // Command might have completed very quickly or failed to start
316                      // This is acceptable in test environments
317                  }
318              }
319              Err(e) => {
320                  // Command might not be available in test environment
321                  let error_msg = e.to_string();
322                  assert!(
323                      error_msg.contains("not found")
324                          || error_msg.contains("No such file")
325                          || error_msg.contains("Failed to spawn process"),
326                      "Error should be due to command not found, got: {}",
327                      error_msg
328                  );
329              }
330          }
331      }
332  
333      #[test]
334      /// Test plugin manager with multiple plugins
335      /// Validates: Requirements 9.11, 9.15, 9.16
336      fn test_plugin_manager_multiple_plugins() {
337          let mut plugin_manager = PluginManager::new();
338  
339          // Register multiple plugins
340          let tla_plugin = Box::new(TlaPlusPlugin::new());
341          let proptest_plugin = Box::new(ProptestPlugin::new());
342  
343          plugin_manager
344              .register_plugin(tla_plugin)
345              .expect("TLA+ plugin should register");
346          plugin_manager
347              .register_plugin(proptest_plugin)
348              .expect("Proptest plugin should register");
349  
350          // Test getting plugins for specific techniques
351          let formal_plugins =
352              plugin_manager.plugins_for_technique(&ferris_proof_core::types::Technique::FormalSpecs);
353          assert!(
354              !formal_plugins.is_empty(),
355              "Should have plugins for formal specs"
356          );
357  
358          let property_plugins = plugin_manager
359              .plugins_for_technique(&ferris_proof_core::types::Technique::PropertyTests);
360          assert!(
361              !property_plugins.is_empty(),
362              "Should have plugins for property tests"
363          );
364  
365          // Test plugin initialization
366          let config = json!({
367              "tla-plus": {
368                  "tlc_path": "/usr/local/bin/tlc"
369              },
370              "proptest": {
371                  "path": "cargo"
372              }
373          });
374  
375          let init_result = plugin_manager.initialize_plugins(&config);
376          assert!(init_result.is_ok(), "Plugin initialization should succeed");
377  
378          // Test plugin cleanup
379          let cleanup_result = plugin_manager.cleanup_plugins();
380          assert!(cleanup_result.is_ok(), "Plugin cleanup should succeed");
381      }
382  
383      #[test]
384      /// Test resource limits configuration
385      /// Validates: Requirements 12.4
386      fn test_resource_limits_configuration() {
387          let limits = ResourceLimits {
388              max_memory: 100 * 1024 * 1024, // 100MB
389              max_cpu_time: 30,              // 30 seconds
390              max_file_descriptors: 64,
391              max_processes: 5,
392              max_file_size: 10 * 1024 * 1024, // 10MB
393          };
394  
395          let _executor = SandboxedExecutor::new().with_limits(limits.clone());
396  
397          // Verify limits are set correctly
398          // Note: We can't directly access private fields, but we can test behavior
399          // Resource limits should be configurable
400  
401          // Test default limits
402          let default_limits = ResourceLimits::default();
403          assert!(
404              default_limits.max_memory > 0,
405              "Default memory limit should be positive"
406          );
407          assert!(
408              default_limits.max_cpu_time > 0,
409              "Default CPU time limit should be positive"
410          );
411          assert!(
412              default_limits.max_file_descriptors > 0,
413              "Default file descriptor limit should be positive"
414          );
415          assert!(
416              default_limits.max_processes > 0,
417              "Default process limit should be positive"
418          );
419          assert!(
420              default_limits.max_file_size > 0,
421              "Default file size limit should be positive"
422          );
423      }
424  
425      #[test]
426      /// Test network policy configuration
427      /// Validates: Requirements 12.1
428      fn test_network_policy_configuration() {
429          // Test denied policy
430          let _denied_executor = SandboxedExecutor::new().with_network_policy(NetworkPolicy::Denied);
431  
432          // Test allowlist policy
433          let allowed_hosts = vec!["example.com".to_string(), "trusted.org".to_string()];
434          let _allowlist_executor = SandboxedExecutor::new()
435              .with_network_policy(NetworkPolicy::AllowList(allowed_hosts.clone()));
436  
437          // Test unrestricted policy with consent
438          let _unrestricted_executor = SandboxedExecutor::new()
439              .with_network_policy(NetworkPolicy::Unrestricted { user_consent: true });
440  
441          // Test unrestricted policy without consent
442          let _no_consent_executor =
443              SandboxedExecutor::new().with_network_policy(NetworkPolicy::Unrestricted {
444                  user_consent: false,
445              });
446  
447          // Verify policies can be configured
448          // Network policies should be configurable
449  
450          // Test policy equality
451          let policy1 = NetworkPolicy::Denied;
452          let policy2 = NetworkPolicy::Denied;
453          assert_eq!(policy1, policy2, "Denied policies should be equal");
454  
455          let policy3 = NetworkPolicy::AllowList(vec!["test.com".to_string()]);
456          let policy4 = NetworkPolicy::AllowList(vec!["test.com".to_string()]);
457          assert_eq!(
458              policy3, policy4,
459              "Identical allowlist policies should be equal"
460          );
461  
462          let policy5 = NetworkPolicy::Unrestricted { user_consent: true };
463          let policy6 = NetworkPolicy::Unrestricted { user_consent: true };
464          assert_eq!(
465              policy5, policy6,
466              "Identical unrestricted policies should be equal"
467          );
468      }
469  }