mirror of
https://github.com/outbackdingo/optimclaw.git
synced 2026-09-01 09:09:19 +00:00
fix(tests): replace hardcoded /tmp paths with tempdir + add 300 unit tests (#659)
* test: add unit tests across 20 modules for coverage push Add 300+ unit tests covering config, context, evaluation, extensions, LLM, secrets, tools/builder, and tools/mcp modules. All tests are pure unit tests (no mocks) exercising serde roundtrips, edge cases, error paths, and business logic. Co-Authored-By: Claude Opus 4.6 <[email protected]> * fix(tests): replace hardcoded /tmp paths with tempfile::tempdir The e2e_metrics_test::test_metrics_collected_from_tool_trace test was failing because setup_test_dir() created /tmp/ironclaw_metrics_test but the fixture referenced /tmp/ironclaw_e2e_test/hello.txt (path mismatch). Added LlmTrace::replace_paths() to substitute fixture paths at runtime, then converted all 12 test files from hardcoded /tmp/ironclaw_* paths to tempfile::tempdir(). Tests are now isolated, parallel-safe, and leave no debris on disk. Regression test: test_metrics_collected_from_tool_trace now passes consistently regardless of prior /tmp state. Co-Authored-By: Claude Opus 4.6 <[email protected]> --------- Co-authored-by: Claude Opus 4.6 <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
8fbb782090
commit
cf96a3253c
+368
-8
@@ -1026,24 +1026,384 @@ impl Tool for BuildSoftwareTool {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::tools::builder::core::*;
|
||||
|
||||
#[test]
|
||||
fn test_language_extensions() {
|
||||
fn test_language_extension_all_variants() {
|
||||
assert_eq!(Language::Rust.extension(), "rs");
|
||||
assert_eq!(Language::Python.extension(), "py");
|
||||
assert_eq!(Language::TypeScript.extension(), "ts");
|
||||
assert_eq!(Language::JavaScript.extension(), "js");
|
||||
assert_eq!(Language::Go.extension(), "go");
|
||||
assert_eq!(Language::Bash.extension(), "sh");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_build_commands() {
|
||||
assert!(Language::Rust.build_command("/tmp/project").is_some());
|
||||
assert!(Language::Python.build_command("/tmp/project").is_none());
|
||||
fn test_language_build_command_compiled_returns_some() {
|
||||
let dir = "/tmp/project";
|
||||
let rust_cmd = Language::Rust.build_command(dir);
|
||||
assert!(rust_cmd.is_some());
|
||||
assert!(rust_cmd.unwrap().contains("cargo build"));
|
||||
|
||||
let ts_cmd = Language::TypeScript.build_command(dir);
|
||||
assert!(ts_cmd.is_some());
|
||||
assert!(ts_cmd.unwrap().contains("npm run build"));
|
||||
|
||||
let go_cmd = Language::Go.build_command(dir);
|
||||
assert!(go_cmd.is_some());
|
||||
assert!(go_cmd.unwrap().contains("go build"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_software_type_serialization() {
|
||||
let json = serde_json::to_string(&SoftwareType::WasmTool).unwrap();
|
||||
assert_eq!(json, "\"wasm_tool\"");
|
||||
fn test_language_build_command_interpreted_returns_none() {
|
||||
let dir = "/tmp/project";
|
||||
assert!(Language::Python.build_command(dir).is_none());
|
||||
assert!(Language::JavaScript.build_command(dir).is_none());
|
||||
assert!(Language::Bash.build_command(dir).is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_language_build_command_includes_project_dir() {
|
||||
let dir = "/home/user/my_project";
|
||||
for lang in [Language::Rust, Language::TypeScript, Language::Go] {
|
||||
let cmd = lang.build_command(dir);
|
||||
assert!(
|
||||
cmd.as_ref().unwrap().contains(dir),
|
||||
"{:?} build command should contain project dir",
|
||||
lang
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_language_test_command_all_variants_non_empty() {
|
||||
let dir = "/tmp/project";
|
||||
let all_languages = [
|
||||
Language::Rust,
|
||||
Language::Python,
|
||||
Language::TypeScript,
|
||||
Language::JavaScript,
|
||||
Language::Go,
|
||||
Language::Bash,
|
||||
];
|
||||
for lang in all_languages {
|
||||
let cmd = lang.test_command(dir);
|
||||
assert!(
|
||||
!cmd.is_empty(),
|
||||
"{:?} test command should not be empty",
|
||||
lang
|
||||
);
|
||||
assert!(
|
||||
cmd.contains(dir),
|
||||
"{:?} test command should contain project dir",
|
||||
lang
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_language_test_command_specific_tools() {
|
||||
let dir = "/tmp/p";
|
||||
assert!(Language::Rust.test_command(dir).contains("cargo test"));
|
||||
assert!(Language::Python.test_command(dir).contains("pytest"));
|
||||
assert!(Language::TypeScript.test_command(dir).contains("npm test"));
|
||||
assert!(Language::JavaScript.test_command(dir).contains("npm test"));
|
||||
assert!(Language::Go.test_command(dir).contains("go test"));
|
||||
assert!(Language::Bash.test_command(dir).contains("shellcheck"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_software_type_serde_roundtrip() {
|
||||
let variants = [
|
||||
SoftwareType::WasmTool,
|
||||
SoftwareType::CliBinary,
|
||||
SoftwareType::Library,
|
||||
SoftwareType::Script,
|
||||
SoftwareType::WebService,
|
||||
];
|
||||
let expected_strings = [
|
||||
"\"wasm_tool\"",
|
||||
"\"cli_binary\"",
|
||||
"\"library\"",
|
||||
"\"script\"",
|
||||
"\"web_service\"",
|
||||
];
|
||||
for (variant, expected) in variants.iter().zip(expected_strings.iter()) {
|
||||
let json = serde_json::to_string(variant).unwrap();
|
||||
assert_eq!(&json, expected, "serialization mismatch for {:?}", variant);
|
||||
let deserialized: SoftwareType = serde_json::from_str(&json).unwrap();
|
||||
assert_eq!(
|
||||
&deserialized, variant,
|
||||
"roundtrip mismatch for {:?}",
|
||||
variant
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_language_serde_roundtrip() {
|
||||
let variants = [
|
||||
Language::Rust,
|
||||
Language::Python,
|
||||
Language::TypeScript,
|
||||
Language::JavaScript,
|
||||
Language::Go,
|
||||
Language::Bash,
|
||||
];
|
||||
let expected_strings = [
|
||||
"\"rust\"",
|
||||
"\"python\"",
|
||||
"\"type_script\"",
|
||||
"\"java_script\"",
|
||||
"\"go\"",
|
||||
"\"bash\"",
|
||||
];
|
||||
for (variant, expected) in variants.iter().zip(expected_strings.iter()) {
|
||||
let json = serde_json::to_string(variant).unwrap();
|
||||
assert_eq!(&json, expected, "serialization mismatch for {:?}", variant);
|
||||
let deserialized: Language = serde_json::from_str(&json).unwrap();
|
||||
assert_eq!(
|
||||
&deserialized, variant,
|
||||
"roundtrip mismatch for {:?}",
|
||||
variant
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_build_requirement_serde_roundtrip() {
|
||||
let req = BuildRequirement {
|
||||
name: "my_tool".into(),
|
||||
description: "A tool that does stuff".into(),
|
||||
software_type: SoftwareType::WasmTool,
|
||||
language: Language::Rust,
|
||||
input_spec: Some("JSON object with 'query' field".into()),
|
||||
output_spec: Some("JSON object with 'result' field".into()),
|
||||
dependencies: vec!["serde".into(), "reqwest".into()],
|
||||
capabilities: vec!["http".into(), "workspace".into()],
|
||||
};
|
||||
let json = serde_json::to_string(&req).unwrap();
|
||||
let deserialized: BuildRequirement = serde_json::from_str(&json).unwrap();
|
||||
assert_eq!(deserialized.name, req.name);
|
||||
assert_eq!(deserialized.description, req.description);
|
||||
assert_eq!(deserialized.software_type, req.software_type);
|
||||
assert_eq!(deserialized.language, req.language);
|
||||
assert_eq!(deserialized.input_spec, req.input_spec);
|
||||
assert_eq!(deserialized.output_spec, req.output_spec);
|
||||
assert_eq!(deserialized.dependencies, req.dependencies);
|
||||
assert_eq!(deserialized.capabilities, req.capabilities);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_build_requirement_serde_optional_fields_none() {
|
||||
let req = BuildRequirement {
|
||||
name: "minimal".into(),
|
||||
description: "Bare minimum".into(),
|
||||
software_type: SoftwareType::Script,
|
||||
language: Language::Bash,
|
||||
input_spec: None,
|
||||
output_spec: None,
|
||||
dependencies: vec![],
|
||||
capabilities: vec![],
|
||||
};
|
||||
let json = serde_json::to_string(&req).unwrap();
|
||||
let deserialized: BuildRequirement = serde_json::from_str(&json).unwrap();
|
||||
assert!(deserialized.input_spec.is_none());
|
||||
assert!(deserialized.output_spec.is_none());
|
||||
assert!(deserialized.dependencies.is_empty());
|
||||
assert!(deserialized.capabilities.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_builder_config_default_sensible_values() {
|
||||
let config = BuilderConfig::default();
|
||||
assert!(config.max_iterations > 0, "max_iterations must be positive");
|
||||
assert!(!config.timeout.is_zero(), "timeout must be non-zero");
|
||||
assert!(
|
||||
config.timeout.as_secs() >= 60,
|
||||
"timeout should be at least 60 seconds"
|
||||
);
|
||||
assert!(config.validate_wasm, "validate_wasm should default to true");
|
||||
assert!(config.run_tests, "run_tests should default to true");
|
||||
assert!(config.auto_register, "auto_register should default to true");
|
||||
assert!(
|
||||
!config.cleanup_on_failure,
|
||||
"cleanup_on_failure should default to false for debugging"
|
||||
);
|
||||
assert!(
|
||||
config.wasm_output_dir.is_none(),
|
||||
"wasm_output_dir should default to None"
|
||||
);
|
||||
assert!(
|
||||
config
|
||||
.build_dir
|
||||
.to_string_lossy()
|
||||
.contains("ironclaw-builds"),
|
||||
"build_dir should contain 'ironclaw-builds'"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_build_phase_serde_roundtrip() {
|
||||
let variants = [
|
||||
BuildPhase::Analyzing,
|
||||
BuildPhase::Scaffolding,
|
||||
BuildPhase::Implementing,
|
||||
BuildPhase::Building,
|
||||
BuildPhase::Testing,
|
||||
BuildPhase::Fixing,
|
||||
BuildPhase::Validating,
|
||||
BuildPhase::Registering,
|
||||
BuildPhase::Packaging,
|
||||
BuildPhase::Complete,
|
||||
BuildPhase::Failed,
|
||||
];
|
||||
for variant in &variants {
|
||||
let json = serde_json::to_string(variant).unwrap();
|
||||
let deserialized: BuildPhase = serde_json::from_str(&json).unwrap();
|
||||
assert_eq!(
|
||||
&deserialized, variant,
|
||||
"roundtrip mismatch for {:?}",
|
||||
variant
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_build_result_serde_success() {
|
||||
let result = BuildResult {
|
||||
build_id: Uuid::nil(),
|
||||
requirement: BuildRequirement {
|
||||
name: "test_tool".into(),
|
||||
description: "test".into(),
|
||||
software_type: SoftwareType::WasmTool,
|
||||
language: Language::Rust,
|
||||
input_spec: None,
|
||||
output_spec: None,
|
||||
dependencies: vec![],
|
||||
capabilities: vec![],
|
||||
},
|
||||
artifact_path: PathBuf::from("/tmp/test.wasm"),
|
||||
logs: vec![],
|
||||
success: true,
|
||||
error: None,
|
||||
started_at: Utc::now(),
|
||||
completed_at: Utc::now(),
|
||||
iterations: 3,
|
||||
validation_warnings: vec![],
|
||||
tests_passed: 5,
|
||||
tests_failed: 0,
|
||||
registered: true,
|
||||
};
|
||||
let json = serde_json::to_string(&result).unwrap();
|
||||
let deserialized: BuildResult = serde_json::from_str(&json).unwrap();
|
||||
assert!(deserialized.success);
|
||||
assert!(deserialized.error.is_none());
|
||||
assert_eq!(deserialized.iterations, 3);
|
||||
assert_eq!(deserialized.tests_passed, 5);
|
||||
assert_eq!(deserialized.tests_failed, 0);
|
||||
assert!(deserialized.registered);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_build_result_serde_failure() {
|
||||
let result = BuildResult {
|
||||
build_id: Uuid::nil(),
|
||||
requirement: BuildRequirement {
|
||||
name: "broken".into(),
|
||||
description: "fails".into(),
|
||||
software_type: SoftwareType::CliBinary,
|
||||
language: Language::Go,
|
||||
input_spec: None,
|
||||
output_spec: None,
|
||||
dependencies: vec![],
|
||||
capabilities: vec![],
|
||||
},
|
||||
artifact_path: PathBuf::from("/tmp/broken"),
|
||||
logs: vec![],
|
||||
success: false,
|
||||
error: Some("compilation error: undefined reference".into()),
|
||||
started_at: Utc::now(),
|
||||
completed_at: Utc::now(),
|
||||
iterations: 10,
|
||||
validation_warnings: vec!["missing export".into()],
|
||||
tests_passed: 2,
|
||||
tests_failed: 3,
|
||||
registered: false,
|
||||
};
|
||||
let json = serde_json::to_string(&result).unwrap();
|
||||
let deserialized: BuildResult = serde_json::from_str(&json).unwrap();
|
||||
assert!(!deserialized.success);
|
||||
assert_eq!(
|
||||
deserialized.error.as_deref(),
|
||||
Some("compilation error: undefined reference")
|
||||
);
|
||||
assert_eq!(deserialized.iterations, 10);
|
||||
assert_eq!(deserialized.validation_warnings.len(), 1);
|
||||
assert_eq!(deserialized.tests_passed, 2);
|
||||
assert_eq!(deserialized.tests_failed, 3);
|
||||
assert!(!deserialized.registered);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_build_result_default_fields_from_json() {
|
||||
// Verify #[serde(default)] fields can be omitted in JSON
|
||||
let json = serde_json::json!({
|
||||
"build_id": "00000000-0000-0000-0000-000000000000",
|
||||
"requirement": {
|
||||
"name": "x",
|
||||
"description": "y",
|
||||
"software_type": "script",
|
||||
"language": "bash",
|
||||
"input_spec": null,
|
||||
"output_spec": null,
|
||||
"dependencies": [],
|
||||
"capabilities": []
|
||||
},
|
||||
"artifact_path": "/tmp/x.sh",
|
||||
"logs": [],
|
||||
"success": true,
|
||||
"error": null,
|
||||
"started_at": "2025-01-01T00:00:00Z",
|
||||
"completed_at": "2025-01-01T00:01:00Z",
|
||||
"iterations": 1
|
||||
});
|
||||
let result: BuildResult = serde_json::from_value(json).unwrap();
|
||||
assert_eq!(result.validation_warnings, Vec::<String>::new());
|
||||
assert_eq!(result.tests_passed, 0);
|
||||
assert_eq!(result.tests_failed, 0);
|
||||
assert!(!result.registered);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_build_log_serde_roundtrip() {
|
||||
let log = BuildLog {
|
||||
timestamp: Utc::now(),
|
||||
phase: BuildPhase::Building,
|
||||
message: "Running cargo build".into(),
|
||||
details: Some("cargo build --release 2>&1".into()),
|
||||
};
|
||||
let json = serde_json::to_string(&log).unwrap();
|
||||
let deserialized: BuildLog = serde_json::from_str(&json).unwrap();
|
||||
assert_eq!(deserialized.phase, BuildPhase::Building);
|
||||
assert_eq!(deserialized.message, "Running cargo build");
|
||||
assert_eq!(
|
||||
deserialized.details.as_deref(),
|
||||
Some("cargo build --release 2>&1")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_build_log_serde_details_none() {
|
||||
let log = BuildLog {
|
||||
timestamp: Utc::now(),
|
||||
phase: BuildPhase::Complete,
|
||||
message: "Done".into(),
|
||||
details: None,
|
||||
};
|
||||
let json = serde_json::to_string(&log).unwrap();
|
||||
let deserialized: BuildLog = serde_json::from_str(&json).unwrap();
|
||||
assert!(deserialized.details.is_none());
|
||||
assert_eq!(deserialized.phase, BuildPhase::Complete);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -498,4 +498,163 @@ mod tests {
|
||||
assert_eq!(template.name, "WASM HTTP Tool");
|
||||
assert!(!template.files.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_render_no_variables() {
|
||||
let engine = TemplateEngine::new();
|
||||
let input = "Hello, world! No placeholders here.";
|
||||
assert_eq!(engine.render(input), input);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_render_variable_not_found() {
|
||||
let mut engine = TemplateEngine::new();
|
||||
engine.set("name", "ironclaw");
|
||||
let input = "Name: {{name}}, Missing: {{missing}}";
|
||||
assert_eq!(engine.render(input), "Name: ironclaw, Missing: {{missing}}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_render_multiple_replacements_of_same_variable() {
|
||||
let mut engine = TemplateEngine::new();
|
||||
engine.set("x", "42");
|
||||
assert_eq!(engine.render("{{x}} + {{x}} = 2*{{x}}"), "42 + 42 = 2*42");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_set_overwrites_existing_variable() {
|
||||
let mut engine = TemplateEngine::new();
|
||||
engine.set("color", "red");
|
||||
assert_eq!(engine.render("{{color}}"), "red");
|
||||
engine.set("color", "blue");
|
||||
assert_eq!(engine.render("{{color}}"), "blue");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_render_template_all_files() {
|
||||
let mut engine = TemplateEngine::new();
|
||||
engine.set("name", "my_tool");
|
||||
engine.set("description", "does stuff");
|
||||
|
||||
let template = Template::get(TemplateType::CliBinary);
|
||||
let rendered = engine.render_template(&template);
|
||||
|
||||
assert_eq!(rendered.len(), template.files.len());
|
||||
// Paths should have variables substituted
|
||||
for (path, _content) in &rendered {
|
||||
assert!(!path.contains("{{name}}"));
|
||||
}
|
||||
// Content should have variables substituted
|
||||
for (_path, content) in &rendered {
|
||||
assert!(!content.contains("{{name}}"));
|
||||
assert!(!content.contains("{{description}}"));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_all_template_types_return_non_empty() {
|
||||
let all_types = [
|
||||
TemplateType::WasmHttpTool,
|
||||
TemplateType::WasmTransformTool,
|
||||
TemplateType::WasmComputeTool,
|
||||
TemplateType::CliBinary,
|
||||
TemplateType::PythonScript,
|
||||
TemplateType::BashScript,
|
||||
];
|
||||
for tt in all_types {
|
||||
let t = Template::get(tt);
|
||||
assert!(!t.name.is_empty(), "{:?} has empty name", tt);
|
||||
assert!(!t.description.is_empty(), "{:?} has empty description", tt);
|
||||
assert!(!t.files.is_empty(), "{:?} has no files", tt);
|
||||
for f in &t.files {
|
||||
assert!(
|
||||
!f.content.is_empty(),
|
||||
"{:?} file {:?} has empty content",
|
||||
tt,
|
||||
f.path
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_template_type_serde_roundtrip() {
|
||||
let all_types = [
|
||||
TemplateType::WasmHttpTool,
|
||||
TemplateType::WasmTransformTool,
|
||||
TemplateType::WasmComputeTool,
|
||||
TemplateType::CliBinary,
|
||||
TemplateType::PythonScript,
|
||||
TemplateType::BashScript,
|
||||
];
|
||||
for tt in all_types {
|
||||
let json = serde_json::to_string(&tt).unwrap();
|
||||
let back: TemplateType = serde_json::from_str(&json).unwrap();
|
||||
assert_eq!(back, tt, "roundtrip failed for {:?} (json: {})", tt, json);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_each_template_has_at_least_one_required_file() {
|
||||
let all_types = [
|
||||
TemplateType::WasmHttpTool,
|
||||
TemplateType::WasmTransformTool,
|
||||
TemplateType::WasmComputeTool,
|
||||
TemplateType::CliBinary,
|
||||
TemplateType::PythonScript,
|
||||
TemplateType::BashScript,
|
||||
];
|
||||
for tt in all_types {
|
||||
let t = Template::get(tt);
|
||||
let required_count = t.files.iter().filter(|f| f.is_required).count();
|
||||
assert!(required_count >= 1, "{:?} has no required files", tt);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_template_file_extensions() {
|
||||
// WASM and CLI templates should have Cargo.toml and .rs files
|
||||
for tt in [
|
||||
TemplateType::WasmHttpTool,
|
||||
TemplateType::WasmTransformTool,
|
||||
TemplateType::WasmComputeTool,
|
||||
TemplateType::CliBinary,
|
||||
] {
|
||||
let t = Template::get(tt);
|
||||
let paths: Vec<&str> = t.files.iter().map(|f| f.path).collect();
|
||||
assert!(
|
||||
paths.iter().any(|p| p.ends_with("Cargo.toml")),
|
||||
"{:?} missing Cargo.toml",
|
||||
tt
|
||||
);
|
||||
assert!(
|
||||
paths.iter().any(|p| p.ends_with(".rs")),
|
||||
"{:?} missing .rs file",
|
||||
tt
|
||||
);
|
||||
}
|
||||
|
||||
// Python template should have a .py file
|
||||
let py = Template::get(TemplateType::PythonScript);
|
||||
assert!(py.files.iter().any(|f| f.path.ends_with(".py")));
|
||||
|
||||
// Bash template should have a .sh file
|
||||
let bash = Template::get(TemplateType::BashScript);
|
||||
assert!(bash.files.iter().any(|f| f.path.ends_with(".sh")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_python_and_bash_templates_have_name_in_path() {
|
||||
let py = Template::get(TemplateType::PythonScript);
|
||||
assert!(
|
||||
py.files.iter().any(|f| f.path.contains("{{name}}")),
|
||||
"PythonScript template should have {{{{name}}}} in a file path"
|
||||
);
|
||||
|
||||
let bash = Template::get(TemplateType::BashScript);
|
||||
assert!(
|
||||
bash.files.iter().any(|f| f.path.contains("{{name}}")),
|
||||
"BashScript template should have {{{{name}}}} in a file path"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -315,5 +315,163 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
// Note: Full WASM parsing tests would require actual WASM binaries
|
||||
#[test]
|
||||
fn test_validate_bytes_invalid_bytes() {
|
||||
let validator = WasmValidator::new();
|
||||
let garbage = b"this is not a wasm module at all";
|
||||
let result = validator.validate_bytes(garbage).unwrap();
|
||||
assert!(!result.is_valid);
|
||||
assert!(
|
||||
result
|
||||
.errors
|
||||
.iter()
|
||||
.any(|e| matches!(e, ValidationError::InvalidModule(_)))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_validate_bytes_empty() {
|
||||
let validator = WasmValidator::new();
|
||||
let result = validator.validate_bytes(b"").unwrap();
|
||||
assert!(!result.is_valid);
|
||||
assert!(
|
||||
result
|
||||
.errors
|
||||
.iter()
|
||||
.any(|e| matches!(e, ValidationError::InvalidModule(_)))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_validate_bytes_minimal_wasm_missing_run_export() {
|
||||
let validator = WasmValidator::new();
|
||||
// Minimal valid WASM: magic number + version
|
||||
let minimal_wasm = b"\x00asm\x01\x00\x00\x00";
|
||||
let result = validator.validate_bytes(minimal_wasm).unwrap();
|
||||
assert!(!result.is_valid);
|
||||
assert!(
|
||||
result
|
||||
.errors
|
||||
.iter()
|
||||
.any(|e| matches!(e, ValidationError::MissingExport(name) if name == "run"))
|
||||
);
|
||||
assert_eq!(result.size_bytes, 8);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_validation_result_is_valid_when_no_errors() {
|
||||
let result = ValidationResult {
|
||||
is_valid: true,
|
||||
errors: vec![],
|
||||
warnings: vec!["some warning".to_string()],
|
||||
exports: vec![],
|
||||
imports: vec![],
|
||||
size_bytes: 0,
|
||||
};
|
||||
assert!(result.is_valid);
|
||||
assert!(result.errors.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_validation_result_is_invalid_when_errors_present() {
|
||||
let result = ValidationResult {
|
||||
is_valid: false,
|
||||
errors: vec![ValidationError::MissingExport("run".to_string())],
|
||||
warnings: vec![],
|
||||
exports: vec![],
|
||||
imports: vec![],
|
||||
size_bytes: 0,
|
||||
};
|
||||
assert!(!result.is_valid);
|
||||
assert_eq!(result.errors.len(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_validation_error_display() {
|
||||
let io_err =
|
||||
ValidationError::IoError(std::io::Error::new(std::io::ErrorKind::NotFound, "gone"));
|
||||
assert!(io_err.to_string().contains("Failed to read WASM file"));
|
||||
|
||||
let invalid = ValidationError::InvalidModule("bad magic".to_string());
|
||||
assert!(invalid.to_string().contains("Invalid WASM module"));
|
||||
assert!(invalid.to_string().contains("bad magic"));
|
||||
|
||||
let missing = ValidationError::MissingExport("run".to_string());
|
||||
assert!(missing.to_string().contains("Missing required export"));
|
||||
assert!(missing.to_string().contains("run"));
|
||||
|
||||
let sig = ValidationError::InvalidSignature {
|
||||
name: "run".to_string(),
|
||||
expected: "() -> i32".to_string(),
|
||||
actual: "() -> ()".to_string(),
|
||||
};
|
||||
assert!(sig.to_string().contains("Invalid export signature"));
|
||||
assert!(sig.to_string().contains("run"));
|
||||
|
||||
let disallowed = ValidationError::DisallowedImport {
|
||||
module: "evil".to_string(),
|
||||
name: "hack".to_string(),
|
||||
};
|
||||
assert!(disallowed.to_string().contains("disallowed import"));
|
||||
assert!(disallowed.to_string().contains("evil::hack"));
|
||||
|
||||
let too_large = ValidationError::TooLarge {
|
||||
size: 200,
|
||||
max: 100,
|
||||
};
|
||||
assert!(too_large.to_string().contains("200"));
|
||||
assert!(too_large.to_string().contains("100"));
|
||||
|
||||
let other = ValidationError::Other("something broke".to_string());
|
||||
assert!(other.to_string().contains("something broke"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_export_kind_equality() {
|
||||
assert_eq!(ExportKind::Function, ExportKind::Function);
|
||||
assert_eq!(ExportKind::Memory, ExportKind::Memory);
|
||||
assert_eq!(ExportKind::Table, ExportKind::Table);
|
||||
assert_eq!(ExportKind::Global, ExportKind::Global);
|
||||
assert_ne!(ExportKind::Function, ExportKind::Memory);
|
||||
assert_ne!(ExportKind::Table, ExportKind::Global);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_import_kind_equality() {
|
||||
assert_eq!(ImportKind::Function, ImportKind::Function);
|
||||
assert_eq!(ImportKind::Memory, ImportKind::Memory);
|
||||
assert_eq!(ImportKind::Table, ImportKind::Table);
|
||||
assert_eq!(ImportKind::Global, ImportKind::Global);
|
||||
assert_ne!(ImportKind::Function, ImportKind::Global);
|
||||
assert_ne!(ImportKind::Memory, ImportKind::Table);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_validate_bytes_exceeds_max_size() {
|
||||
let validator = WasmValidator::new().with_max_size(4);
|
||||
// 8 bytes, over the 4-byte limit
|
||||
let minimal_wasm = b"\x00asm\x01\x00\x00\x00";
|
||||
let result = validator.validate_bytes(minimal_wasm).unwrap();
|
||||
assert!(!result.is_valid);
|
||||
assert!(
|
||||
result
|
||||
.errors
|
||||
.iter()
|
||||
.any(|e| matches!(e, ValidationError::TooLarge { size: 8, max: 4 }))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_with_max_size_then_validate_over_limit() {
|
||||
let validator = WasmValidator::new().with_max_size(16);
|
||||
let oversized = vec![0u8; 32];
|
||||
let result = validator.validate_bytes(&oversized).unwrap();
|
||||
assert!(!result.is_valid);
|
||||
assert!(
|
||||
result
|
||||
.errors
|
||||
.iter()
|
||||
.any(|e| matches!(e, ValidationError::TooLarge { size: 32, max: 16 }))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user