From 7e85d34152ae7c2c503f9bb31ae6121d55735a25 Mon Sep 17 00:00:00 2001 From: Ronald Holshausen Date: Wed, 7 Aug 2024 10:40:13 +1000 Subject: [PATCH] feat: Implement applying generators to mutate the Protobuf messages #69 --- src/dynamic_message.rs | 160 +++++++++++++++++++++++++++++- src/message_decoder/generators.rs | 3 +- src/server.rs | 68 ++++++------- 3 files changed, 193 insertions(+), 38 deletions(-) diff --git a/src/dynamic_message.rs b/src/dynamic_message.rs index 0881076..7953161 100644 --- a/src/dynamic_message.rs +++ b/src/dynamic_message.rs @@ -1,11 +1,20 @@ //! gRPC codec that used a Pact interaction +use std::collections::HashMap; use std::iter::Peekable; use std::slice::Iter; use anyhow::anyhow; use bytes::{BufMut, Bytes}; use itertools::Itertools; +use maplit::hashmap; +use pact_models::generators::{ + GenerateValue, + Generator, + GeneratorTestMode, + NoopVariantMatcher, + VariantMatcher +}; use pact_models::path_exp::{DocPath, PathToken}; use pact_models::v4::sync_message::SynchronousMessage; use prost::encoding::{encode_key, encode_varint, WireType}; @@ -143,6 +152,7 @@ impl DynamicMessage { } /// Retrieve the value using the given path + #[instrument(ret, skip(self))] pub fn fetch_value(&mut self, path: &DocPath) -> Option { let path_tokens = path.tokens().clone(); let mut iter = path_tokens.iter().peekable(); @@ -159,7 +169,7 @@ impl DynamicMessage { } /// Update the value using the given path - #[instrument] + #[instrument(ret, skip(self))] pub fn set_value(&mut self, path: &DocPath, value: ProtobufFieldData) -> anyhow::Result<()> { let path_tokens = path.tokens().clone(); let mut iter = path_tokens.iter().peekable(); @@ -190,14 +200,18 @@ impl DynamicMessage { if path_tokens.peek().is_none() { callback(field); } else { - match &field.data { - ProtobufFieldData::Enum(_, _) => todo!(), + match &mut field.data { + ProtobufFieldData::Enum(_, _) => todo!("Support for dynamically fetching enum values is not supported yet"), ProtobufFieldData::Message(data, descriptor) => { let mut buffer = Bytes::copy_from_slice(data); match decode_message(&mut buffer, descriptor, &descriptors) { Ok(fields) => { let mut message = DynamicMessage::new(fields.as_slice(), &descriptors); message.match_path(path_tokens, callback); + data.clear(); + if let Err(err) = message.write_to(data) { + error!("Failed to rewrite child message: {}", err); + } } Err(err) => error!("Failed to decode child message: {}", err) } @@ -213,6 +227,32 @@ impl DynamicMessage { } } } + + /// Mutates the message by applying the generators to any matching message fields + #[instrument(ret, skip(self))] + pub fn apply_generators( + &mut self, + generators: Option<&HashMap>, + mode: &GeneratorTestMode + ) -> anyhow::Result<()> { + if let Some(generators) = generators { + let variant_matcher = NoopVariantMatcher {}; + let vm_boxed = variant_matcher.boxed(); + let context = hashmap!{}; + + for (path, generator) in generators { + let value = self.fetch_value(&path); + if let Some(value) = value { + if generator.corresponds_to_mode(mode) { + let generated_value = generator.generate_value(&value.data, &context, &vm_boxed)?; + self.set_value(&path, generated_value)?; + } + } + } + } + + Ok(()) + } } fn find_field<'a>(fields: &'a mut [ProtobufField], field_name: &str) -> Option<&'a mut ProtobufField> { @@ -277,7 +317,10 @@ impl Decoder for DynamicMessageDecoder { mod tests { use bytes::BytesMut; use expectest::prelude::*; + use maplit::hashmap; + use pact_models::generators::GeneratorTestMode; use pact_models::path_exp::DocPath; + use pact_models::prelude::Generator::RandomInt; use prost::encoding::WireType; use prost_types::{DescriptorProto, FieldDescriptorProto, FileDescriptorSet}; @@ -399,4 +442,115 @@ mod tests { let path = DocPath::new("$.one.two").unwrap(); expect!(message.fetch_value(&path)).to(be_some().value(child_field)); } + + #[test] + fn dynamic_message_generate_value_with_no_fields() { + let fields = vec![]; + let descriptors = FileDescriptorSet { + file: vec![] + }; + let mut message = DynamicMessage::new(fields.as_slice(), &descriptors); + let path = DocPath::new_unwrap("$.one.two.three"); + let generators = hashmap!{ + path.clone() => RandomInt(1, 10) + }; + + expect!(message.apply_generators(Some(&generators), &GeneratorTestMode::Provider)).to(be_ok()); + } + + #[test] + fn dynamic_message_generate_value_with_no_matching_field() { + let field = ProtobufField { + field_num: 1, + field_name: "one".to_string(), + wire_type: WireType::Varint, + data: ProtobufFieldData::Integer64(100) + }; + let descriptors = FileDescriptorSet { + file: vec![] + }; + let fields = vec![ field.clone() ]; + let mut message = DynamicMessage::new(fields.as_slice(), &descriptors); + let generators = hashmap!{ + DocPath::new_unwrap("$.two") => RandomInt(1, 10) + }; + + expect!(message.apply_generators(Some(&generators), &GeneratorTestMode::Provider)).to(be_ok()); + } + + #[test] + fn dynamic_message_generate_value_with_matching_field() { + let field = ProtobufField { + field_num: 1, + field_name: "one".to_string(), + wire_type: WireType::Varint, + data: ProtobufFieldData::Integer64(100) + }; + let descriptors = FileDescriptorSet { + file: vec![] + }; + let fields = vec![ field.clone() ]; + let mut message = DynamicMessage::new(fields.as_slice(), &descriptors); + let generators = hashmap!{ + DocPath::new_unwrap("$.one") => RandomInt(1, 10) + }; + + expect!(message.apply_generators(Some(&generators), &GeneratorTestMode::Provider)).to(be_ok()); + expect!(message.fields[0].data.as_i64().unwrap()).to_not(be_equal_to(100)); + } + + #[test] + fn dynamic_message_generate_value_with_matching_child_field() { + let child_descriptor = DescriptorProto { + name: Some("child".to_string()), + field: vec![ + FieldDescriptorProto { + name: Some("two".to_string()), + number: Some(1), + r#type: Some(3), + .. FieldDescriptorProto::default() + }, + FieldDescriptorProto { + name: Some("three".to_string()), + number: Some(2), + r#type: Some(3), + .. FieldDescriptorProto::default() + } + ], + .. DescriptorProto::default() + }; + let child_field = ProtobufField { + field_num: 1, + field_name: "two".to_string(), + wire_type: WireType::Varint, + data: ProtobufFieldData::Integer64(100) + }; + let child_field2 = ProtobufField { + field_num: 2, + field_name: "three".to_string(), + wire_type: WireType::Varint, + data: ProtobufFieldData::Integer64(200) + }; + let descriptors = FileDescriptorSet { + file: vec![] + }; + let child_message = DynamicMessage::new(&[child_field.clone(), child_field2], &descriptors); + let mut buffer = BytesMut::new(); + child_message.write_to(&mut buffer).unwrap(); + let field = ProtobufField { + field_num: 1, + field_name: "one".to_string(), + wire_type: WireType::LengthDelimited, + data: ProtobufFieldData::Message(buffer.to_vec(), child_descriptor) + }; + let fields = vec![ field.clone() ]; + let mut message = DynamicMessage::new(fields.as_slice(), &descriptors); + let path = DocPath::new_unwrap("$.one.two"); + let generators = hashmap!{ + path.clone() => RandomInt(1, 10) + }; + + expect!(message.apply_generators(Some(&generators), &GeneratorTestMode::Provider)).to(be_ok()); + expect!(message.fetch_value(&path).unwrap().data.as_i64().unwrap()).to_not(be_equal_to(100)); + } } diff --git a/src/message_decoder/generators.rs b/src/message_decoder/generators.rs index 0d63412..078cd56 100644 --- a/src/message_decoder/generators.rs +++ b/src/message_decoder/generators.rs @@ -22,12 +22,13 @@ use pact_models::time_utils::{parse_pattern, to_chrono_pattern}; use rand::prelude::*; use regex::{Captures, Regex}; use serde_json::Value; -use tracing::{debug, warn}; +use tracing::{debug, instrument, warn}; use uuid::Uuid; use crate::message_decoder::ProtobufFieldData; impl GenerateValue for Generator { + #[instrument(ret)] fn generate_value(&self, value: &ProtobufFieldData, context: &HashMap<&str, Value>, diff --git a/src/server.rs b/src/server.rs index 93ddc54..3fadac1 100644 --- a/src/server.rs +++ b/src/server.rs @@ -9,14 +9,21 @@ use anyhow::{anyhow, bail}; use bytes::{Bytes, BytesMut}; use maplit::hashmap; use pact_matching::{BodyMatchResult, Mismatch}; -use pact_models::generators::{GenerateValue, Generator, GeneratorTestMode, NoopVariantMatcher, VariantMatcher}; +use pact_models::generators::{ + GenerateValue, + Generator, + GeneratorCategory, + GeneratorTestMode, + NoopVariantMatcher, + VariantMatcher +}; use pact_models::json_utils::json_to_string; use pact_models::matchingrules::MatchingRule; use pact_models::path_exp::DocPath; use pact_models::prelude::{ContentType, MatchingRuleCategory, OptionalBody, RuleLogic}; use pact_plugin_driver::plugin_models::PactPluginManifest; use pact_plugin_driver::proto; -use pact_plugin_driver::proto::{Body, body, CompareContentsRequest, CompareContentsResponse, GenerateContentRequest, GenerateContentResponse, MockServerResult, PluginConfiguration}; +use pact_plugin_driver::proto::{Body, body, CompareContentsRequest, CompareContentsResponse, GenerateContentRequest, GenerateContentResponse, MockServerResult, PluginConfiguration, VerificationPreparationResponse}; use pact_plugin_driver::proto::body::ContentTypeHint; use pact_plugin_driver::proto::catalogue_entry::EntryType; use pact_plugin_driver::proto::generate_content_request::TestMode; @@ -387,6 +394,13 @@ impl ProtobufPactPlugin { None => Ok(self.manifest.plugin_config.clone()) } } + + fn verification_preparation_error_response>(err: E) -> Response { + Response::new(proto::VerificationPreparationResponse { + response: Some(proto::verification_preparation_response::Response::Error(err.into())), + ..proto::VerificationPreparationResponse::default() + }) + } } #[instrument(level = "trace")] @@ -699,10 +713,7 @@ impl PactPlugin for ProtobufPactPlugin { let pact = match parse_pact_from_request_json(request.pact.as_str(), "grpc:prepare_interaction_for_verification") { Ok(pact) => pact, - Err(err) => return Ok(Response::new(proto::VerificationPreparationResponse { - response: Some(proto::verification_preparation_response::Response::Error(format!("Failed to parse Pact JSON: {}", err))), - .. proto::VerificationPreparationResponse::default() - })) + Err(err) => return Ok(Self::verification_preparation_error_response(format!("Failed to parse Pact JSON: {}", err))) }; let key = request.interaction_key.as_str(); @@ -710,29 +721,18 @@ impl PactPlugin for ProtobufPactPlugin { let interaction = match interaction_by_id { Some(interaction) => match interaction.as_v4_sync_message() { Some(interaction) => interaction, - None => return Ok(Response::new(proto::VerificationPreparationResponse { - response: Some(proto::verification_preparation_response::Response::Error(format!("gRPC interactions must be of type V4 synchronous message, got {}", interaction.type_of()))), - ..proto::VerificationPreparationResponse::default() - })) + None => return Ok(Self::verification_preparation_error_response(format!("gRPC interactions must be of type V4 synchronous message, got {}", interaction.type_of()))) } None => { error!(?key, "Did not find an interaction that matches the given key"); - return Ok(Response::new(proto::VerificationPreparationResponse { - response: Some(proto::verification_preparation_response::Response::Error( - format!("Did not find an interaction that matches the given key '{}'", key) - )), - ..proto::VerificationPreparationResponse::default() - })) + return Ok(Self::verification_preparation_error_response(format!("Did not find an interaction that matches the given key '{}'", key))); } }; let (file_desc, service_desc, method_desc, package) = match lookup_service_descriptors_for_interaction(&interaction, &pact) { Ok(values) => values, Err(err) => { - return Ok(Response::new(proto::VerificationPreparationResponse { - response: Some(proto::verification_preparation_response::Response::Error(err.to_string())), - ..proto::VerificationPreparationResponse::default() - })) + return Ok(Self::verification_preparation_error_response(err.to_string())) } }; @@ -741,23 +741,26 @@ impl PactPlugin for ProtobufPactPlugin { let input_message = match find_message_type_by_name(last_name(input_message_name.as_str()), &file_desc) { Ok(message) => message.0, Err(err) => { - return Ok(Response::new(proto::VerificationPreparationResponse { - response: Some(proto::verification_preparation_response::Response::Error(err.to_string())), - ..proto::VerificationPreparationResponse::default() - })) + return Ok(Self::verification_preparation_error_response(err.to_string())) } }; - // TODO: use any generators here let decoded_body = match decode_message(&mut raw_request_body, &input_message, &file_desc) { - Ok(message) => DynamicMessage::new(&message, &file_desc), + Ok(message) => { + let mut message = DynamicMessage::new(&message, &file_desc); + if let Err(err) = message.apply_generators( + interaction.request.generators.categories.get(&GeneratorCategory::BODY), + &GeneratorTestMode::Provider + ) { + return Ok(Self::verification_preparation_error_response(err.to_string())); + } + message + }, Err(err) => { - return Ok(Response::new(proto::VerificationPreparationResponse { - response: Some(proto::verification_preparation_response::Response::Error(err.to_string())), - ..proto::VerificationPreparationResponse::default() - })) + return Ok(Self::verification_preparation_error_response(err.to_string())); } }; + let request = Request::new(decoded_body.clone()); let mut request_metadata: HashMap = interaction.request.metadata.iter() @@ -792,10 +795,7 @@ impl PactPlugin for ProtobufPactPlugin { let mut buffer = BytesMut::new(); if let Err(err) = decoded_body.write_to(&mut buffer) { - return Ok(Response::new(proto::VerificationPreparationResponse { - response: Some(proto::verification_preparation_response::Response::Error(err.to_string())), - ..proto::VerificationPreparationResponse::default() - })) + return Ok(Self::verification_preparation_error_response(err.to_string())); } let integration_data = proto::InteractionData { body: Some(Body {