Skip to content

Commit 6849f3c

Browse files
committed
Test: Add 29 unit and integration tests for cache, response, schema, logging, and integration
1 parent d56733f commit 6849f3c

File tree

5 files changed

+515
-0
lines changed

5 files changed

+515
-0
lines changed

tests/cache_test.rs

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
//! Tests for the cache module
2+
3+
use nexuscore_mcp::utils::cache::{ResultCache, file_hash, get_cache};
4+
use serde_json::json;
5+
use std::time::Duration;
6+
use std::thread;
7+
8+
#[test]
9+
fn test_cache_insert_and_get() {
10+
let mut cache = ResultCache::new(10, 60);
11+
12+
let key = "test:abc123".to_string();
13+
let value = json!({"result": "test_data"});
14+
15+
cache.insert(key.clone(), value.clone());
16+
17+
let retrieved = cache.get(&key);
18+
assert!(retrieved.is_some());
19+
assert_eq!(retrieved.unwrap()["result"], "test_data");
20+
}
21+
22+
#[test]
23+
fn test_cache_miss() {
24+
let cache = ResultCache::new(10, 60);
25+
26+
let result = cache.get("nonexistent:key");
27+
assert!(result.is_none());
28+
}
29+
30+
#[test]
31+
fn test_cache_expiry() {
32+
let mut cache = ResultCache::new(10, 1); // 1 second TTL
33+
34+
let key = "test:expiry".to_string();
35+
let value = json!({"data": "will_expire"});
36+
37+
cache.insert(key.clone(), value);
38+
39+
// Should exist immediately
40+
assert!(cache.get(&key).is_some());
41+
42+
// Wait for expiry
43+
thread::sleep(Duration::from_secs(2));
44+
45+
// Should be expired now
46+
assert!(cache.get(&key).is_none());
47+
}
48+
49+
#[test]
50+
fn test_cache_eviction() {
51+
let mut cache = ResultCache::new(3, 60); // Max 3 entries
52+
53+
cache.insert("key1".to_string(), json!(1));
54+
cache.insert("key2".to_string(), json!(2));
55+
cache.insert("key3".to_string(), json!(3));
56+
57+
// Add 4th entry - should evict oldest
58+
cache.insert("key4".to_string(), json!(4));
59+
60+
// key1 should be evicted
61+
assert!(cache.get("key1").is_none());
62+
assert!(cache.get("key4").is_some());
63+
}
64+
65+
#[test]
66+
fn test_cache_cleanup() {
67+
let mut cache = ResultCache::new(10, 1);
68+
69+
cache.insert("key1".to_string(), json!(1));
70+
cache.insert("key2".to_string(), json!(2));
71+
72+
thread::sleep(Duration::from_secs(2));
73+
74+
cache.cleanup();
75+
76+
// All entries should be removed after cleanup
77+
assert!(cache.get("key1").is_none());
78+
assert!(cache.get("key2").is_none());
79+
}
80+
81+
#[test]
82+
fn test_global_cache() {
83+
let cache = get_cache();
84+
let mut guard = cache.lock().unwrap();
85+
86+
guard.insert("global:test".to_string(), json!({"global": true}));
87+
88+
let result = guard.get("global:test");
89+
assert!(result.is_some());
90+
}
91+
92+
#[cfg(test)]
93+
mod file_hash_tests {
94+
use super::*;
95+
use std::fs;
96+
use std::io::Write;
97+
98+
#[test]
99+
fn test_file_hash_consistency() {
100+
// Create temp file
101+
let path = "test_hash_file.tmp";
102+
let mut file = fs::File::create(path).unwrap();
103+
file.write_all(b"test content for hashing").unwrap();
104+
drop(file);
105+
106+
// Hash should be consistent
107+
let hash1 = file_hash(path).unwrap();
108+
let hash2 = file_hash(path).unwrap();
109+
110+
assert_eq!(hash1, hash2);
111+
assert_eq!(hash1.len(), 64); // SHA256 = 64 hex chars
112+
113+
// Cleanup
114+
fs::remove_file(path).unwrap();
115+
}
116+
117+
#[test]
118+
fn test_file_hash_nonexistent() {
119+
let result = file_hash("nonexistent_file.xyz");
120+
assert!(result.is_err());
121+
}
122+
}

tests/integration_test.rs

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
//! Integration tests for NexusCore MCP
2+
3+
use nexuscore_mcp::tools::Tool;
4+
use nexuscore_mcp::utils::response::StandardResponse;
5+
use serde_json::json;
6+
7+
/// Helper to run async tests
8+
fn block_on<F: std::future::Future>(f: F) -> F::Output {
9+
tokio::runtime::Runtime::new().unwrap().block_on(f)
10+
}
11+
12+
/// Test that all tools return valid StandardResponse format
13+
#[cfg(test)]
14+
mod response_format_tests {
15+
use super::*;
16+
17+
fn validate_response(result: serde_json::Value) {
18+
// Must have these fields
19+
assert!(result.get("tool").is_some(), "Missing 'tool' field");
20+
assert!(result.get("status").is_some(), "Missing 'status' field");
21+
assert!(result.get("timestamp").is_some(), "Missing 'timestamp' field");
22+
23+
// Status must be one of: success, error, partial
24+
let status = result["status"].as_str().unwrap();
25+
assert!(
26+
["success", "error", "partial"].contains(&status),
27+
"Invalid status: {}", status
28+
);
29+
30+
// If error, must have error field
31+
if status == "error" {
32+
assert!(result.get("error").is_some(), "Error response missing 'error' field");
33+
}
34+
}
35+
36+
#[test]
37+
fn test_success_response_format() {
38+
let result = StandardResponse::success("test", json!({"data": 1}));
39+
validate_response(result.clone());
40+
assert_eq!(result["status"], "success");
41+
}
42+
43+
#[test]
44+
fn test_error_response_format() {
45+
let result = StandardResponse::error("test", "fail");
46+
validate_response(result.clone());
47+
assert_eq!(result["status"], "error");
48+
}
49+
50+
#[test]
51+
fn test_cached_response_format() {
52+
let result = StandardResponse::success_cached("test", json!({}));
53+
validate_response(result.clone());
54+
assert_eq!(result["metadata"]["cached"], true);
55+
}
56+
}
57+
58+
/// Schema validation tests
59+
#[cfg(test)]
60+
mod schema_validation_tests {
61+
use super::*;
62+
use nexuscore_mcp::tools::{ToolSchema, ParamDef};
63+
64+
fn validate_json_schema(schema: serde_json::Value) {
65+
assert_eq!(schema["type"], "object");
66+
assert!(schema.get("properties").is_some());
67+
assert!(schema.get("required").is_some());
68+
}
69+
70+
#[test]
71+
fn test_empty_schema_valid() {
72+
let schema = ToolSchema::empty();
73+
validate_json_schema(schema.to_json());
74+
}
75+
76+
#[test]
77+
fn test_schema_with_required_params() {
78+
let schema = ToolSchema::new(vec![
79+
ParamDef::new("pid", "number", true, "PID"),
80+
ParamDef::new("path", "string", true, "Path"),
81+
]);
82+
83+
let json = schema.to_json();
84+
validate_json_schema(json.clone());
85+
86+
let required = json["required"].as_array().unwrap();
87+
assert_eq!(required.len(), 2);
88+
}
89+
}
90+
91+
/// Tool naming convention tests
92+
#[cfg(test)]
93+
mod naming_convention_tests {
94+
use super::*;
95+
use nexuscore_mcp::tools::common::metrics::MetricsTool;
96+
97+
#[test]
98+
fn test_tool_name_snake_case() {
99+
let tool = MetricsTool;
100+
let name = tool.name();
101+
102+
// Name should be snake_case
103+
assert!(name.chars().all(|c| c.is_lowercase() || c == '_' || c.is_numeric()));
104+
assert!(!name.contains('-'));
105+
assert!(!name.contains(' '));
106+
}
107+
108+
#[test]
109+
fn test_tool_has_description() {
110+
let tool = MetricsTool;
111+
let desc = tool.description();
112+
113+
assert!(!desc.is_empty());
114+
assert!(desc.len() > 10); // Should be meaningful
115+
}
116+
}
117+
118+
/// Metrics tool integration test
119+
#[cfg(test)]
120+
mod metrics_integration_tests {
121+
use super::*;
122+
use nexuscore_mcp::tools::common::metrics::MetricsTool;
123+
124+
#[test]
125+
fn test_metrics_tool_execution() {
126+
let tool = MetricsTool;
127+
128+
let result = block_on(tool.execute(json!({})));
129+
assert!(result.is_ok());
130+
131+
let response = result.unwrap();
132+
assert_eq!(response["status"], "success");
133+
assert!(response["data"].get("total_calls").is_some());
134+
assert!(response["data"].get("cache_hit_rate").is_some());
135+
}
136+
}

tests/logging_test.rs

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
//! Tests for the logging module
2+
3+
use nexuscore_mcp::utils::logging::{get_metrics, PerfMetrics};
4+
use serde_json::json;
5+
6+
#[test]
7+
fn test_perf_metrics_new() {
8+
let metrics = PerfMetrics::new();
9+
let stats = metrics.get_stats();
10+
11+
assert_eq!(stats["total_calls"], 0);
12+
assert_eq!(stats["cache_hits"], 0);
13+
assert_eq!(stats["cache_misses"], 0);
14+
assert_eq!(stats["total_duration_ms"], 0);
15+
}
16+
17+
#[test]
18+
fn test_perf_metrics_record_call() {
19+
let metrics = PerfMetrics::new();
20+
21+
metrics.record_call(100, false); // cache miss
22+
metrics.record_call(50, true); // cache hit
23+
metrics.record_call(75, false); // cache miss
24+
25+
let stats = metrics.get_stats();
26+
27+
assert_eq!(stats["total_calls"], 3);
28+
assert_eq!(stats["cache_hits"], 1);
29+
assert_eq!(stats["cache_misses"], 2);
30+
assert_eq!(stats["total_duration_ms"], 225);
31+
}
32+
33+
#[test]
34+
fn test_cache_hit_rate_calculation() {
35+
let metrics = PerfMetrics::new();
36+
37+
// 2 hits, 2 misses = 50% hit rate
38+
metrics.record_call(10, true);
39+
metrics.record_call(10, true);
40+
metrics.record_call(10, false);
41+
metrics.record_call(10, false);
42+
43+
let stats = metrics.get_stats();
44+
let hit_rate = stats["cache_hit_rate"].as_f64().unwrap();
45+
46+
assert!((hit_rate - 50.0).abs() < 0.01);
47+
}
48+
49+
#[test]
50+
fn test_cache_hit_rate_zero_calls() {
51+
let metrics = PerfMetrics::new();
52+
let stats = metrics.get_stats();
53+
54+
// Should be 0 when no calls recorded
55+
assert_eq!(stats["cache_hit_rate"], 0.0);
56+
}
57+
58+
#[test]
59+
fn test_global_metrics() {
60+
let metrics = get_metrics();
61+
62+
// Should always return the same instance
63+
let metrics2 = get_metrics();
64+
65+
metrics.record_call(10, false);
66+
67+
// Both references should see the same data
68+
let stats = metrics2.get_stats();
69+
assert!(stats["total_calls"].as_u64().unwrap() > 0);
70+
}
71+
72+
#[test]
73+
fn test_metrics_thread_safety() {
74+
use std::thread;
75+
use std::sync::Arc;
76+
77+
let metrics = Arc::new(PerfMetrics::new());
78+
let mut handles = vec![];
79+
80+
for _ in 0..10 {
81+
let m = Arc::clone(&metrics);
82+
handles.push(thread::spawn(move || {
83+
for _ in 0..100 {
84+
m.record_call(1, false);
85+
}
86+
}));
87+
}
88+
89+
for handle in handles {
90+
handle.join().unwrap();
91+
}
92+
93+
let stats = metrics.get_stats();
94+
assert_eq!(stats["total_calls"], 1000);
95+
}

0 commit comments

Comments
 (0)