Skip to content

Commit

Permalink
feat: Implement applying generators to mutate the Protobuf messages #69
Browse files Browse the repository at this point in the history
  • Loading branch information
rholshausen committed Aug 7, 2024
1 parent ef237ec commit 7e85d34
Show file tree
Hide file tree
Showing 3 changed files with 193 additions and 38 deletions.
160 changes: 157 additions & 3 deletions src/dynamic_message.rs
Original file line number Diff line number Diff line change
@@ -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};
Expand Down Expand Up @@ -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<ProtobufField> {
let path_tokens = path.tokens().clone();
let mut iter = path_tokens.iter().peekable();
Expand All @@ -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();
Expand Down Expand Up @@ -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)
}
Expand All @@ -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<DocPath, Generator>>,
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> {
Expand Down Expand Up @@ -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};

Expand Down Expand Up @@ -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));
}
}
3 changes: 2 additions & 1 deletion src/message_decoder/generators.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<ProtobufFieldData> for Generator {
#[instrument(ret)]
fn generate_value(&self,
value: &ProtobufFieldData,
context: &HashMap<&str, Value>,
Expand Down
68 changes: 34 additions & 34 deletions src/server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -387,6 +394,13 @@ impl ProtobufPactPlugin {
None => Ok(self.manifest.plugin_config.clone())
}
}

fn verification_preparation_error_response<E: Into<String>>(err: E) -> Response<VerificationPreparationResponse> {
Response::new(proto::VerificationPreparationResponse {
response: Some(proto::verification_preparation_response::Response::Error(err.into())),
..proto::VerificationPreparationResponse::default()
})
}
}

#[instrument(level = "trace")]
Expand Down Expand Up @@ -699,40 +713,26 @@ 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();
let interaction_by_id = lookup_interaction_by_id(key, &pact);
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()))
}
};

Expand All @@ -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<String, proto::MetadataValue> = interaction.request.metadata.iter()
Expand Down Expand Up @@ -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 {
Expand Down

0 comments on commit 7e85d34

Please sign in to comment.