• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

gripmock / grpctestify-rust / 24902954483

24 Apr 2026 05:29PM UTC coverage: 78.02% (+0.3%) from 77.729%
24902954483

Pull #43

github

web-flow
Merge b18cb9c15 into 017e47d15
Pull Request #43: new command gen grpcurl & call

741 of 993 new or added lines in 24 files covered. (74.62%)

3 existing lines in 3 files now uncovered.

19594 of 25114 relevant lines covered (78.02%)

39580.43 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

87.03
/src/plugins/mod.rs
1
//! Assertion plugins — built-in functions for gRPC test assertions.
2
//!
3
//! Plugins extend the assertion engine with custom validation logic.
4
//! Each plugin implements the [`Plugin`] trait and is registered with [`PluginManager`].
5
//!
6
//! # Type System
7
//!
8
//! Plugin signatures include full type information (`TypeInfo`, `ArgTypeInfo`)
9
//! that is consumed by:
10
//! - **Optimizer**: type-aware rewrites (e.g., `@len(.x) >= 0 → true`)
11
//! - **LSP**: hover information, completion, signature help
12
//! - **Semantics**: type-checking assertion expressions
13
//! - **Explain/Inspect**: human-readable type information
14
//!
15
//! # Available Plugins
16
//!
17
//! | Plugin | Purpose | Returns |
18
//! |--------|---------|---------|
19
//! | `@uuid` | Validate UUID format | bool |
20
//! | `@email` | Validate email format | bool |
21
//! | `@ip` | Validate IP address | bool |
22
//! | `@url` | Validate URL format | bool |
23
//! | `@timestamp` | Validate Unix timestamp | bool |
24
//! | `@regex` | Regex matching | bool |
25
//! | `@len` / `@empty` | Length/emptiness checks | non-negative integer / bool |
26
//! | `@header` / `@has_header` | HTTP header extraction/checks | string|null / bool |
27
//! | `@trailer` / `@has_trailer` | gRPC trailer extraction/checks | string|null / bool |
28
//! | `@env` | Environment variable (with optional default) | string|null |
29
//! | `@elapsed_ms` / `@total_elapsed_ms` | Timing assertions | non-negative integer |
30
//! | `@scope_message_count` / `@scope_index` | Streaming scope info | non-negative integer |
31

32
pub mod email;
33
pub mod empty;
34
pub mod env;
35
pub mod header_extract;
36
pub mod ip;
37
pub mod len;
38
pub mod macros;
39
pub mod regex;
40
pub mod timestamp;
41
pub mod timing;
42
pub mod trailer_extract;
43
pub mod type_info;
44
pub mod url;
45
pub mod uuid;
46

47
pub use type_info::{ArgTypeInfo, TypeInfo, TypedPluginSignature};
48

49
use anyhow::Result;
50
use serde_json::Value;
51
use std::collections::HashMap;
52
use std::sync::{Arc, LazyLock, RwLock};
53

54
use crate::assert::engine::AssertionResult;
55

56
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
57
pub enum PluginPurity {
58
    Pure,
59
    ContextDependent,
60
    Impure,
61
}
62

63
#[derive(Debug, Clone, Copy)]
64
pub struct PluginSignature {
65
    /// Extended return type — the single source of truth for return type information.
66
    /// Used by optimizer, LSP hover, and semantics.
67
    pub return_type: TypeInfo,
68
    /// Type information for each argument (for LSP signature help).
69
    pub arg_types: &'static [ArgTypeInfo],
70
    pub purity: PluginPurity,
71
    pub deterministic: bool,
72
    pub idempotent: bool,
73
    pub safe_for_rewrite: bool,
74
    /// Human-readable argument names for signature display.
75
    pub arg_names: &'static [&'static str],
76
}
77

78
impl Default for PluginSignature {
79
    fn default() -> Self {
×
80
        Self {
×
81
            return_type: TypeInfo::Any,
×
82
            arg_types: &[],
×
83
            purity: PluginPurity::Impure,
×
84
            deterministic: false,
×
85
            idempotent: false,
×
86
            safe_for_rewrite: false,
×
87
            arg_names: &[],
×
88
        }
×
89
    }
×
90
}
91

92
/// Context passed to plugins during execution
93
pub struct PluginContext<'a> {
94
    pub response: &'a Value,
95
    pub headers: Option<&'a HashMap<String, String>>,
96
    pub trailers: Option<&'a HashMap<String, String>>,
97
    pub timing: Option<&'a AssertionTiming>,
98
}
99

100
impl<'a> PluginContext<'a> {
101
    pub fn new(response: &'a Value) -> Self {
309✔
102
        Self {
309✔
103
            response,
309✔
104
            headers: None,
309✔
105
            trailers: None,
309✔
106
            timing: None,
309✔
107
        }
309✔
108
    }
309✔
109

110
    pub fn with_headers(mut self, headers: Option<&'a HashMap<String, String>>) -> Self {
248✔
111
        self.headers = headers;
248✔
112
        self
248✔
113
    }
248✔
114

115
    pub fn with_trailers(mut self, trailers: Option<&'a HashMap<String, String>>) -> Self {
248✔
116
        self.trailers = trailers;
248✔
117
        self
248✔
118
    }
248✔
119

120
    pub fn with_timing(mut self, timing: Option<&'a AssertionTiming>) -> Self {
224✔
121
        self.timing = timing;
224✔
122
        self
224✔
123
    }
224✔
124
}
125

126
/// Timing context available for assertion plugins.
127
#[derive(Debug, Clone, PartialEq, Eq)]
128
pub struct AssertionTiming {
129
    /// Duration for current assertion scope (message or batch), in milliseconds.
130
    pub elapsed_ms: u64,
131
    /// Cumulative duration across all completed assertion scopes, in milliseconds.
132
    pub total_elapsed_ms: u64,
133
    /// Number of messages in current scope.
134
    pub scope_message_count: usize,
135
    /// Monotonic index of current scope (1-based).
136
    pub scope_index: usize,
137
}
138

139
/// Result of a plugin execution
140
#[derive(Debug, Clone)]
141
pub enum PluginResult {
142
    /// The plugin performed an assertion (pass/fail)
143
    Assertion(AssertionResult),
144
    /// The plugin computed a value to be used in further expressions
145
    Value(Value),
146
}
147

148
/// Trait for all plugins
149
pub trait Plugin: Send + Sync {
150
    /// unique name of the plugin (e.g., "uuid", "len")
151
    fn name(&self) -> &str;
152
    /// Description of what the plugin does
153
    fn description(&self) -> &str;
154
    /// Execute the plugin logic
155
    fn execute(&self, args: &[Value], context: &PluginContext) -> Result<PluginResult>;
156

157
    /// Static plugin signature used by optimizer/LSP.
158
    fn signature(&self) -> PluginSignature {
×
159
        PluginSignature::default()
×
160
    }
×
161
}
162

163
pub fn normalize_plugin_name(name: &str) -> &str {
455✔
164
    let trimmed = name.trim();
455✔
165
    trimmed.strip_prefix('@').unwrap_or(trimmed)
455✔
166
}
455✔
167

168
pub fn extract_plugin_call_name(expr: &str) -> Option<String> {
398✔
169
    let e = expr.trim();
398✔
170
    if !e.starts_with('@') || !e.ends_with(')') {
398✔
171
        return None;
228✔
172
    }
170✔
173

174
    let open = e.find('(')?;
170✔
175
    if open <= 1 {
170✔
176
        return None;
×
177
    }
170✔
178

179
    Some(e[1..open].trim().to_string())
170✔
180
}
398✔
181

182
pub fn plugin_signature_map() -> HashMap<String, PluginSignature> {
34✔
183
    PluginManager::new()
34✔
184
        .list()
34✔
185
        .into_iter()
34✔
186
        .map(|plugin| (plugin.name().to_string(), plugin.signature()))
578✔
187
        .collect()
34✔
188
}
34✔
189

190
/// Cached plugin signatures — single source of truth for all modules.
191
pub static PLUGIN_SIGNATURES: LazyLock<HashMap<String, PluginSignature>> =
192
    LazyLock::new(plugin_signature_map);
193

194
/// Manager to register and retrieve plugins
195
pub struct PluginManager {
196
    plugins: RwLock<HashMap<String, Arc<dyn Plugin>>>,
197
}
198

199
impl PluginManager {
200
    pub fn new() -> Self {
224✔
201
        let mut manager = Self {
224✔
202
            plugins: RwLock::new(HashMap::new()),
224✔
203
        };
224✔
204
        manager.register_defaults();
224✔
205
        manager
224✔
206
    }
224✔
207

208
    fn register_defaults(&mut self) {
224✔
209
        self.register(Arc::new(uuid::UuidPlugin));
224✔
210
        self.register(Arc::new(email::EmailPlugin));
224✔
211
        self.register(Arc::new(empty::EmptyPlugin));
224✔
212
        self.register(Arc::new(ip::IpPlugin));
224✔
213
        self.register(Arc::new(url::UrlPlugin));
224✔
214
        self.register(Arc::new(timestamp::TimestampPlugin));
224✔
215
        self.register(Arc::new(header_extract::HeaderExtractPlugin));
224✔
216
        self.register(Arc::new(header_extract::HasHeaderPlugin));
224✔
217
        self.register(Arc::new(trailer_extract::TrailerExtractPlugin));
224✔
218
        self.register(Arc::new(trailer_extract::HasTrailerPlugin));
224✔
219
        self.register(Arc::new(len::LenPlugin));
224✔
220
        self.register(Arc::new(env::EnvPlugin));
224✔
221
        self.register(Arc::new(regex::RegexPlugin));
224✔
222
        self.register(Arc::new(timing::ElapsedMsPlugin));
224✔
223
        self.register(Arc::new(timing::TotalElapsedMsPlugin));
224✔
224
        self.register(Arc::new(timing::ScopeMessageCountPlugin));
224✔
225
        self.register(Arc::new(timing::ScopeIndexPlugin));
224✔
226
    }
224✔
227

228
    pub fn register(&mut self, plugin: Arc<dyn Plugin>) {
3,809✔
229
        self.plugins
3,809✔
230
            .write()
3,809✔
231
            .unwrap_or_else(|e| e.into_inner())
3,809✔
232
            .insert(plugin.name().to_string(), plugin);
3,809✔
233
    }
3,809✔
234

235
    pub fn register_with_name(&mut self, name: &str, plugin: Arc<dyn Plugin>) {
×
236
        self.plugins
×
237
            .write()
×
NEW
238
            .unwrap_or_else(|e| e.into_inner())
×
239
            .insert(name.to_string(), plugin);
×
240
    }
×
241

242
    pub fn get(&self, name: &str) -> Option<Arc<dyn Plugin>> {
233✔
243
        let normalized = normalize_plugin_name(name);
233✔
244
        self.plugins
233✔
245
            .read()
233✔
246
            .unwrap_or_else(|e| e.into_inner())
233✔
247
            .get(normalized)
233✔
248
            .cloned()
233✔
249
    }
233✔
250

251
    pub fn list(&self) -> Vec<Arc<dyn Plugin>> {
37✔
252
        self.plugins
37✔
253
            .read()
37✔
254
            .unwrap_or_else(|e| e.into_inner())
37✔
255
            .values()
37✔
256
            .cloned()
37✔
257
            .collect()
37✔
258
    }
37✔
259
}
260

261
impl Default for PluginManager {
262
    fn default() -> Self {
×
263
        Self::new()
×
264
    }
×
265
}
266

267
#[cfg(test)]
268
mod tests {
269
    use super::*;
270

271
    #[test]
272
    fn test_plugin_manager_new() {
1✔
273
        let manager = PluginManager::new();
1✔
274
        // PluginManager registers defaults on creation
275
        let plugins = manager.plugins.read().unwrap();
1✔
276
        // Should have default plugins registered
277
        assert!(!plugins.is_empty());
1✔
278
    }
1✔
279

280
    #[test]
281
    fn test_plugin_manager_register() {
1✔
282
        let mut manager = PluginManager::new();
1✔
283
        // Test registration
284
        let plugin = Arc::new(uuid::UuidPlugin);
1✔
285
        manager.register(plugin);
1✔
286
        let plugins = manager.plugins.read().unwrap();
1✔
287
        assert!(plugins.contains_key("uuid"));
1✔
288
    }
1✔
289

290
    #[test]
291
    fn test_plugin_manager_get() {
1✔
292
        let manager = PluginManager::new();
1✔
293
        // Test retrieval of registered plugin
294
        let plugin = manager.get("uuid");
1✔
295
        assert!(plugin.is_some());
1✔
296
        assert_eq!(plugin.unwrap().name(), "uuid");
1✔
297
    }
1✔
298

299
    #[test]
300
    fn test_plugin_manager_get_accepts_at_prefix() {
1✔
301
        let manager = PluginManager::new();
1✔
302
        let plugin = manager.get("@uuid");
1✔
303
        assert!(plugin.is_some());
1✔
304
        assert_eq!(plugin.unwrap().name(), "uuid");
1✔
305
    }
1✔
306

307
    #[test]
308
    fn test_plugin_manager_list() {
1✔
309
        let manager = PluginManager::new();
1✔
310
        let plugins = manager.list();
1✔
311
        // Should have at least the default plugins
312
        assert!(plugins.len() >= 8); // uuid, email, ip, url, timestamp, header, trailer, len
1✔
313
    }
1✔
314

315
    #[test]
316
    fn test_plugin_manager_execute_plugin() {
1✔
317
        let manager = PluginManager::new();
1✔
318
        // Test execution with real plugin (uuid)
319
        let plugin = manager.get("uuid").unwrap();
1✔
320
        let context = PluginContext::new(&Value::Null);
1✔
321
        let result = plugin.execute(&[Value::String("test".to_string())], &context);
1✔
322
        // UUID plugin should return a value
323
        assert!(result.is_ok());
1✔
324
    }
1✔
325

326
    #[test]
327
    fn test_plugin_manager_has_header_registered() {
1✔
328
        let manager = PluginManager::new();
1✔
329
        let plugin = manager.get("has_header");
1✔
330
        assert!(plugin.is_some(), "has_header plugin should be registered");
1✔
331
        assert_eq!(plugin.unwrap().name(), "has_header");
1✔
332
    }
1✔
333

334
    #[test]
335
    fn test_plugin_manager_empty_registered() {
1✔
336
        let manager = PluginManager::new();
1✔
337
        let plugin = manager.get("empty");
1✔
338
        assert!(plugin.is_some(), "empty plugin should be registered");
1✔
339
        assert_eq!(plugin.unwrap().name(), "empty");
1✔
340
    }
1✔
341

342
    #[test]
343
    fn test_plugin_manager_has_trailer_registered() {
1✔
344
        let manager = PluginManager::new();
1✔
345
        let plugin = manager.get("has_trailer");
1✔
346
        assert!(plugin.is_some(), "has_trailer plugin should be registered");
1✔
347
        assert_eq!(plugin.unwrap().name(), "has_trailer");
1✔
348
    }
1✔
349

350
    #[test]
351
    fn test_signature_metadata_empty() {
1✔
352
        let manager = PluginManager::new();
1✔
353
        let signature = manager.get("empty").unwrap().signature();
1✔
354
        assert_eq!(signature.return_type, TypeInfo::Bool);
1✔
355
        assert_eq!(signature.purity, PluginPurity::Pure);
1✔
356
        assert!(signature.deterministic);
1✔
357
        assert!(signature.idempotent);
1✔
358
        assert!(signature.safe_for_rewrite);
1✔
359
    }
1✔
360

361
    #[test]
362
    fn test_signature_metadata_env() {
1✔
363
        let manager = PluginManager::new();
1✔
364
        let signature = manager.get("env").unwrap().signature();
1✔
365
        assert_eq!(signature.return_type, TypeInfo::StringOrNull);
1✔
366
        assert_eq!(signature.purity, PluginPurity::Impure);
1✔
367
        assert!(!signature.deterministic);
1✔
368
        assert!(!signature.idempotent);
1✔
369
        assert!(!signature.safe_for_rewrite);
1✔
370
    }
1✔
371
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc