grpctestify 1.5.0

gRPC testing utility written in Rust
Documentation
use anyhow::{Result, anyhow};
use serde_json::Value;

use crate::plugins::{
    Plugin, PluginContext, PluginPurity, PluginResult, PluginSignature, TypeInfo,
};

pub struct ElapsedMsPlugin;

impl Plugin for ElapsedMsPlugin {
    fn name(&self) -> &str {
        "elapsed_ms"
    }

    fn description(&self) -> &str {
        "Returns elapsed duration in milliseconds for the current assertion scope"
    }

    fn execute(&self, _args: &[Value], context: &PluginContext) -> Result<PluginResult> {
        let timing = context
            .timing
            .ok_or_else(|| anyhow!("timing context is unavailable for @elapsed_ms()"))?;

        Ok(PluginResult::Value(Value::from(timing.elapsed_ms)))
    }

    fn signature(&self) -> PluginSignature {
        PluginSignature {
            return_type: TypeInfo::UInt,
            arg_types: &[],
            purity: PluginPurity::ContextDependent,
            deterministic: true,
            idempotent: true,
            safe_for_rewrite: true,
            arg_names: &[],
        }
    }
}

pub struct TotalElapsedMsPlugin;

impl Plugin for TotalElapsedMsPlugin {
    fn name(&self) -> &str {
        "total_elapsed_ms"
    }

    fn description(&self) -> &str {
        "Returns cumulative elapsed duration in milliseconds across assertion scopes"
    }

    fn execute(&self, _args: &[Value], context: &PluginContext) -> Result<PluginResult> {
        let timing = context
            .timing
            .ok_or_else(|| anyhow!("timing context is unavailable for @total_elapsed_ms()"))?;

        Ok(PluginResult::Value(Value::from(timing.total_elapsed_ms)))
    }

    fn signature(&self) -> PluginSignature {
        PluginSignature {
            return_type: TypeInfo::UInt,
            arg_types: &[],
            purity: PluginPurity::ContextDependent,
            deterministic: true,
            idempotent: true,
            safe_for_rewrite: true,
            arg_names: &[],
        }
    }
}

pub struct ScopeMessageCountPlugin;

impl Plugin for ScopeMessageCountPlugin {
    fn name(&self) -> &str {
        "scope_message_count"
    }

    fn description(&self) -> &str {
        "Returns number of response messages in the current assertion scope"
    }

    fn execute(&self, _args: &[Value], context: &PluginContext) -> Result<PluginResult> {
        let timing = context
            .timing
            .ok_or_else(|| anyhow!("timing context is unavailable for @scope_message_count()"))?;

        Ok(PluginResult::Value(Value::from(timing.scope_message_count)))
    }

    fn signature(&self) -> PluginSignature {
        PluginSignature {
            return_type: TypeInfo::UInt,
            arg_types: &[],
            purity: PluginPurity::ContextDependent,
            deterministic: true,
            idempotent: true,
            safe_for_rewrite: true,
            arg_names: &[],
        }
    }
}

pub struct ScopeIndexPlugin;

impl Plugin for ScopeIndexPlugin {
    fn name(&self) -> &str {
        "scope_index"
    }

    fn description(&self) -> &str {
        "Returns current assertion scope index (1-based)"
    }

    fn execute(&self, _args: &[Value], context: &PluginContext) -> Result<PluginResult> {
        let timing = context
            .timing
            .ok_or_else(|| anyhow!("timing context is unavailable for @scope_index()"))?;

        Ok(PluginResult::Value(Value::from(timing.scope_index)))
    }

    fn signature(&self) -> PluginSignature {
        PluginSignature {
            return_type: TypeInfo::UInt,
            arg_types: &[],
            purity: PluginPurity::ContextDependent,
            deterministic: true,
            idempotent: true,
            safe_for_rewrite: true,
            arg_names: &[],
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::plugins::AssertionTiming;

    fn context_with_timing<'a>(
        response: &'a Value,
        timing: &'a AssertionTiming,
    ) -> PluginContext<'a> {
        PluginContext::new(response).with_timing(Some(timing))
    }

    #[test]
    fn test_elapsed_ms_plugin_returns_value() {
        let plugin = ElapsedMsPlugin;
        let response = Value::Null;
        let timing = AssertionTiming {
            elapsed_ms: 15,
            total_elapsed_ms: 23,
            scope_message_count: 2,
            scope_index: 3,
        };
        let context = context_with_timing(&response, &timing);

        let result = plugin.execute(&[], &context).unwrap();
        assert!(matches!(result, PluginResult::Value(Value::Number(_))));
    }

    #[test]
    fn test_total_elapsed_ms_plugin_returns_value() {
        let plugin = TotalElapsedMsPlugin;
        let response = Value::Null;
        let timing = AssertionTiming {
            elapsed_ms: 10,
            total_elapsed_ms: 15,
            scope_message_count: 1,
            scope_index: 2,
        };
        let context = context_with_timing(&response, &timing);

        let result = plugin.execute(&[], &context).unwrap();
        assert!(matches!(result, PluginResult::Value(Value::Number(_))));
    }

    #[test]
    fn test_plugins_fail_without_timing_context() {
        let plugin = ElapsedMsPlugin;
        let context = PluginContext::new(&Value::Null);

        let result = plugin.execute(&[], &context);
        assert!(result.is_err());
    }
}