diff --git a/crates/apollo-smith/Cargo.toml b/crates/apollo-smith/Cargo.toml index 13f8d60bf..01588c47a 100644 --- a/crates/apollo-smith/Cargo.toml +++ b/crates/apollo-smith/Cargo.toml @@ -28,6 +28,8 @@ arbitrary = { version = "1.3.0", features = ["derive"] } indexmap = "2.0.0" once_cell = "1.9.0" thiserror = "1.0.37" +paste = "1.0.0" +uuid = { version = "1.7.0", features = ["v4"] } [dev-dependencies] expect-test = "1.4" diff --git a/crates/apollo-smith/src/lib.rs b/crates/apollo-smith/src/lib.rs index c72fa5874..ee1573639 100644 --- a/crates/apollo-smith/src/lib.rs +++ b/crates/apollo-smith/src/lib.rs @@ -11,6 +11,7 @@ pub(crate) mod input_object; pub(crate) mod input_value; pub(crate) mod interface; pub(crate) mod name; +pub mod next; pub(crate) mod object; pub(crate) mod operation; pub(crate) mod scalar; diff --git a/crates/apollo-smith/src/next/README.md b/crates/apollo-smith/src/next/README.md new file mode 100644 index 000000000..59853b1c0 --- /dev/null +++ b/crates/apollo-smith/src/next/README.md @@ -0,0 +1,2 @@ +# Apollo-Smith Next +The next folder **** diff --git a/crates/apollo-smith/src/next/ast/definition.rs b/crates/apollo-smith/src/next/ast/definition.rs new file mode 100644 index 000000000..a8d36c1e1 --- /dev/null +++ b/crates/apollo-smith/src/next/ast/definition.rs @@ -0,0 +1,83 @@ +use arbitrary::Unstructured; + +use apollo_compiler::ast::{Definition, InputObjectTypeDefinition, Name, Type}; + +pub(crate) trait DefinitionExt { + fn ty(&self, u: &mut Unstructured) -> arbitrary::Result; +} + +impl DefinitionExt for Definition { + fn ty(&self, u: &mut Unstructured) -> arbitrary::Result { + let name = self.name().expect("definition must have a name").clone(); + Ok(ty(u, name)?) + } +} + +impl DefinitionExt for InputObjectTypeDefinition { + fn ty(&self, u: &mut Unstructured) -> arbitrary::Result { + Ok(ty(u, self.name.clone())?) + } +} + +fn ty(u: &mut Unstructured, name: Name) -> arbitrary::Result { + let mut ty = if u.arbitrary()? { + Type::Named(name) + } else { + Type::NonNullNamed(name) + }; + + for _ in 0..u.int_in_range(0..=5)? { + if u.arbitrary()? { + ty = Type::List(Box::new(ty)) + } else { + ty = Type::NonNullList(Box::new(ty)) + }; + } + Ok(ty) +} + +#[derive(Debug)] +pub(crate) enum DefinitionKind { + OperationDefinition, + FragmentDefinition, + DirectiveDefinition, + SchemaDefinition, + ScalarTypeDefinition, + ObjectTypeDefinition, + InterfaceTypeDefinition, + UnionTypeDefinition, + EnumTypeDefinition, + InputObjectTypeDefinition, + SchemaExtension, + ScalarTypeExtension, + ObjectTypeExtension, + InterfaceTypeExtension, + UnionTypeExtension, + EnumTypeExtension, + InputObjectTypeExtension, +} + +impl DefinitionKind { + pub(crate) fn matches(&self, definition: &Definition) -> bool { + match (self, definition) { + (Self::OperationDefinition, Definition::OperationDefinition(_)) => true, + (Self::FragmentDefinition, Definition::FragmentDefinition(_)) => true, + (Self::DirectiveDefinition, Definition::DirectiveDefinition(_)) => true, + (Self::SchemaDefinition, Definition::SchemaDefinition(_)) => true, + (Self::ScalarTypeDefinition, Definition::ScalarTypeDefinition(_)) => true, + (Self::ObjectTypeDefinition, Definition::ObjectTypeDefinition(_)) => true, + (Self::InterfaceTypeDefinition, Definition::InterfaceTypeDefinition(_)) => true, + (Self::UnionTypeDefinition, Definition::UnionTypeDefinition(_)) => true, + (Self::EnumTypeDefinition, Definition::EnumTypeDefinition(_)) => true, + (Self::InputObjectTypeDefinition, Definition::InputObjectTypeDefinition(_)) => true, + (Self::SchemaExtension, Definition::SchemaExtension(_)) => true, + (Self::ScalarTypeExtension, Definition::ScalarTypeExtension(_)) => true, + (Self::ObjectTypeExtension, Definition::ObjectTypeExtension(_)) => true, + (Self::InterfaceTypeExtension, Definition::InterfaceTypeExtension(_)) => true, + (Self::UnionTypeExtension, Definition::UnionTypeExtension(_)) => true, + (Self::EnumTypeExtension, Definition::EnumTypeExtension(_)) => true, + (Self::InputObjectTypeExtension, Definition::InputObjectTypeExtension(_)) => true, + _ => false, + } + } +} diff --git a/crates/apollo-smith/src/next/ast/directive_definition.rs b/crates/apollo-smith/src/next/ast/directive_definition.rs new file mode 100644 index 000000000..8f8515601 --- /dev/null +++ b/crates/apollo-smith/src/next/ast/directive_definition.rs @@ -0,0 +1,73 @@ +use std::ops::Deref; + +use apollo_compiler::ast::{ + Argument, Directive, DirectiveDefinition, DirectiveList, DirectiveLocation, +}; +use apollo_compiler::{Node, Schema}; + +use crate::next::unstructured::Unstructured; + +pub(crate) struct LocationFilter(I, DirectiveLocation); + +impl<'a, T> Iterator for LocationFilter +where + T: Iterator>, +{ + type Item = &'a Node; + + fn next(&mut self) -> Option { + self.0.find(|d| d.locations.contains(&self.1)) + } +} + +pub(crate) trait DirectiveDefinitionIterExt { + fn with_location<'a>(self, location: DirectiveLocation) -> LocationFilter + where + Self: Iterator> + Sized; + + fn try_collect<'a>( + self, + u: &mut Unstructured, + schema: &Schema, + ) -> arbitrary::Result + where + Self: Iterator> + Sized; +} + +impl DirectiveDefinitionIterExt for I { + fn with_location<'a>(self, location: DirectiveLocation) -> LocationFilter + where + I: Iterator>, + Self: Sized, + { + LocationFilter(self, location) + } + + fn try_collect<'a>( + mut self, + u: &mut Unstructured, + schema: &Schema, + ) -> arbitrary::Result + where + Self: Iterator> + Sized, + { + let mut directives = DirectiveList::new(); + while let Some(d) = self.next() { + let mut arguments = Vec::new(); + for arg in &d.arguments { + if arg.is_required() || u.arbitrary()? { + arguments.push(Node::new(Argument { + name: arg.name.clone(), + value: Node::new(u.arbitrary_value(schema, arg.ty.deref())?), + })) + } + } + + directives.push(Node::new(Directive { + name: d.name.clone(), + arguments, + })) + } + Ok(directives) + } +} diff --git a/crates/apollo-smith/src/next/ast/document.rs b/crates/apollo-smith/src/next/ast/document.rs new file mode 100644 index 000000000..3d0f80de7 --- /dev/null +++ b/crates/apollo-smith/src/next/ast/document.rs @@ -0,0 +1,157 @@ +use paste::paste; + +use crate::next::ast::definition::DefinitionKind; +use apollo_compiler::ast::{ + Definition, DirectiveDefinition, Document, EnumTypeDefinition, EnumTypeExtension, + FragmentDefinition, InputObjectTypeDefinition, InputObjectTypeExtension, + InterfaceTypeDefinition, InterfaceTypeExtension, ObjectTypeDefinition, ObjectTypeExtension, + OperationDefinition, ScalarTypeDefinition, ScalarTypeExtension, SchemaDefinition, + SchemaExtension, UnionTypeDefinition, UnionTypeExtension, +}; +use apollo_compiler::Node; + +use crate::next::unstructured::Unstructured; + +/// Macro to create accessors for definitions +macro_rules! access { + ($ty: ty) => { + paste! { + fn []( + &self, + u: &mut Unstructured, + ) -> arbitrary::Result>> { + let mut existing = self + .target() + .definitions + .iter() + .filter_map(|d| { + if let Definition::$ty(definition) = d { + Some(definition) + } else { + None + } + }) + .collect::>(); + match u.choose_index(existing.len()) { + Ok(idx)=> Ok(Some(existing.remove(idx))), + Err(arbitrary::Error::EmptyChoose)=> Ok(None), + Err(e)=> Err(e) + } + + } + + fn []( + &mut self, + u: &mut Unstructured, + ) -> arbitrary::Result>> { + let mut existing = self + .target_mut() + .definitions + .iter_mut() + .filter_map(|d| { + if let Definition::$ty(definition) = d { + Some(definition) + } else { + None + } + }) + .collect::>(); + + match u.choose_index(existing.len()) { + Ok(idx)=> Ok(Some(existing.remove(idx))), + Err(arbitrary::Error::EmptyChoose)=> Ok(None), + Err(e)=> Err(e) + } + } + + fn []( + &self, + u: &mut Unstructured, + ) -> arbitrary::Result>> { + let existing = self + .target() + .definitions + .iter() + .filter_map(|d| { + if let Definition::$ty(definition) = d { + Some(definition) + } else { + None + } + }) + .filter(|_| u.arbitrary().unwrap_or(false)) + .collect::>(); + + Ok(existing) + } + } + }; +} + +pub(crate) trait DocumentExt { + access!(OperationDefinition); + access!(FragmentDefinition); + access!(DirectiveDefinition); + access!(SchemaDefinition); + access!(ScalarTypeDefinition); + access!(ObjectTypeDefinition); + access!(InterfaceTypeDefinition); + access!(UnionTypeDefinition); + access!(EnumTypeDefinition); + access!(InputObjectTypeDefinition); + access!(SchemaExtension); + access!(ScalarTypeExtension); + access!(ObjectTypeExtension); + access!(InterfaceTypeExtension); + access!(UnionTypeExtension); + access!(EnumTypeExtension); + access!(InputObjectTypeExtension); + + fn random_definition( + &self, + u: &mut Unstructured, + definitions: Vec, + ) -> arbitrary::Result> { + let mut existing = self + .target() + .definitions + .iter() + .filter(|d| definitions.iter().any(|t| t.matches(*d))) + .collect::>(); + match u.choose_index(existing.len()) { + Ok(idx) => Ok(Some(existing.remove(idx))), + Err(arbitrary::Error::EmptyChoose) => Ok(None), + Err(e) => Err(e), + } + } + + fn random_definition_mut( + &mut self, + u: &mut Unstructured, + definitions: Vec, + ) -> arbitrary::Result> { + let mut existing = self + .target_mut() + .definitions + .iter_mut() + .filter(|d| definitions.iter().any(|t| t.matches(*d))) + .collect::>(); + match u.choose_index(existing.len()) { + Ok(idx) => Ok(Some(existing.remove(idx))), + Err(arbitrary::Error::EmptyChoose) => Ok(None), + Err(e) => Err(e), + } + } + + fn target(&self) -> &Document; + fn target_mut(&mut self) -> &mut Document; +} + +impl DocumentExt for Document { + fn target(&self) -> &Document { + self + } + fn target_mut(&mut self) -> &mut Document { + self + } +} diff --git a/crates/apollo-smith/src/next/ast/mod.rs b/crates/apollo-smith/src/next/ast/mod.rs new file mode 100644 index 000000000..3e1426546 --- /dev/null +++ b/crates/apollo-smith/src/next/ast/mod.rs @@ -0,0 +1,118 @@ +use apollo_compiler::ast::{ + Definition, FieldDefinition, InterfaceTypeDefinition, ObjectTypeDefinition, +}; +use apollo_compiler::Node; + +pub(crate) mod definition; +pub(crate) mod directive_definition; +pub(crate) mod document; + +/// macro for accessing fields on ast elements +macro_rules! field_access { + ($ty:ty) => { + paste::paste! { + pub(crate) trait [<$ty Ext>] { + fn random_field( + &self, + u: &mut crate::next::Unstructured, + ) -> arbitrary::Result<&apollo_compiler::Node> { + Ok(u.choose(&self.target().fields).map_err(|e| { + if let arbitrary::Error::EmptyChoose = e { + panic!("no existing fields") + } else { + e + } + })?) + } + + fn random_field_mut( + &mut self, + u: &mut crate::next::Unstructured, + ) -> arbitrary::Result<&mut apollo_compiler::Node> { + let idx = u.choose_index(self.target().fields.len()).map_err(|e| { + if let arbitrary::Error::EmptyChoose = e { + panic!("no existing fields") + } else { + e + } + })?; + Ok(&mut self.target_mut().fields[idx]) + } + + fn sample_fields( + &self, + u: &mut crate::next::Unstructured, + ) -> arbitrary::Result>> { + let existing = self + .target() + .fields + .iter() + .filter(|_| u.arbitrary().unwrap_or(false)) + .collect::>(); + + Ok(existing) + } + fn target(&self) -> &$ty; + fn target_mut(&mut self) -> &mut $ty; + } + + impl [<$ty Ext>] for $ty { + fn target(&self) -> &$ty { + self + } + fn target_mut(&mut self) -> &mut $ty { + self + } + } + } + }; +} + +field_access!(ObjectTypeDefinition); +field_access!(InterfaceTypeDefinition); + +pub(crate) trait DefinitionHasFields { + fn fields(&self) -> &Vec>; + fn fields_mut(&mut self) -> &mut Vec>; +} + +impl DefinitionHasFields for ObjectTypeDefinition { + fn fields(&self) -> &Vec> { + &self.fields + } + + fn fields_mut(&mut self) -> &mut Vec> { + &mut self.fields + } +} + +impl DefinitionHasFields for InterfaceTypeDefinition { + fn fields(&self) -> &Vec> { + &self.fields + } + + fn fields_mut(&mut self) -> &mut Vec> { + &mut self.fields + } +} + +impl DefinitionHasFields for Definition { + fn fields(&self) -> &Vec> { + static EMPTY: Vec> = Vec::new(); + match self { + Definition::ObjectTypeDefinition(d) => &d.fields, + Definition::InterfaceTypeDefinition(d) => &d.fields, + _ => &EMPTY, + } + } + + fn fields_mut<'a>(&mut self) -> &mut Vec> { + match self { + Definition::ObjectTypeDefinition(d) => d.make_mut().fields_mut(), + Definition::InterfaceTypeDefinition(d) => d.make_mut().fields_mut(), + _ => panic!("fields_mut cannot be called on a definition that has no fields"), + } + } +} + + diff --git a/crates/apollo-smith/src/next/executable/executable_document.rs b/crates/apollo-smith/src/next/executable/executable_document.rs new file mode 100644 index 000000000..87d914a36 --- /dev/null +++ b/crates/apollo-smith/src/next/executable/executable_document.rs @@ -0,0 +1,30 @@ +use crate::next::Unstructured; +use apollo_compiler::executable::Fragment; +use apollo_compiler::{ExecutableDocument, Node}; +use crate::next::schema::Selectable; + +pub(crate) trait ExecutableDocumentExt { + fn random_fragment(&self, u: &mut Unstructured) -> arbitrary::Result<&Node> { + let fragments = self.target().fragments.values().collect::>(); + Ok(fragments[u.choose_index(fragments.len())?]) + } + + fn random_fragment_of_type( + &self, + u: &mut Unstructured, + selectable: &impl Selectable + ) -> arbitrary::Result>> { + let fragments = self.target().fragments.values().filter(|f|&f.selection_set.ty == selectable.name()).collect::>(); + if fragments.is_empty() { + return Ok(None) + } + Ok(Some(fragments[u.choose_index(fragments.len())?])) + } + fn target(&self) -> &ExecutableDocument; +} + +impl ExecutableDocumentExt for ExecutableDocument { + fn target(&self) -> &ExecutableDocument { + &self + } +} diff --git a/crates/apollo-smith/src/next/executable/mod.rs b/crates/apollo-smith/src/next/executable/mod.rs new file mode 100644 index 000000000..318745448 --- /dev/null +++ b/crates/apollo-smith/src/next/executable/mod.rs @@ -0,0 +1 @@ +pub(crate) mod executable_document; diff --git a/crates/apollo-smith/src/next/mod.rs b/crates/apollo-smith/src/next/mod.rs new file mode 100644 index 000000000..2bbcfe4d8 --- /dev/null +++ b/crates/apollo-smith/src/next/mod.rs @@ -0,0 +1,208 @@ +use std::path::PathBuf; + +use apollo_compiler::ast::Document; +use apollo_compiler::validation::{Valid, WithErrors}; +use apollo_compiler::{ExecutableDocument, Schema}; + +pub use crate::next::unstructured::Unstructured; + +mod ast; +mod executable; +mod mutations; +mod schema; +mod unstructured; + +#[derive(thiserror::Error, Debug)] +pub enum Error { + #[error("arbitrary error")] + Arbitrary(#[from] arbitrary::Error), + + #[error("schema document validation failed")] + SchemaDocumentValidation { + mutation: String, + doc: Document, + errors: WithErrors, + }, + + #[error("executable document validation failed")] + ExecutableDocumentValidation { + mutation: String, + doc: Document, + schema: Valid, + errors: WithErrors, + }, + + #[error("validation passed, but should have failed")] + SchemaExpectedValidationFail { mutation: String, doc: Document }, + + #[error("the serialized AST did not round trip to an identical AST")] + SerializationInconsistency { original: Document, new: Document }, + + #[error("parse error")] + Parse(WithErrors), + + #[error("validation passed, but should have failed")] + ExecutableExpectedValidationFail { + schema: Valid, + doc: Document, + mutation: String, + }, + + #[error("reparse error")] + SchemaReparse { + mutation: String, + doc: Document, + errors: WithErrors, + }, + + #[error("reparse error")] + ExecutableReparse { + mutation: String, + schema: Valid, + doc: Document, + errors: WithErrors, + }, +} + +pub fn generate_schema_document(u: &mut Unstructured) -> Result { + let mut doc = Document::parse( + "type Query { me: String }".to_string(), + PathBuf::from("synthetic"), + ) + .map_err(Error::Parse)?; // Start with a minimal schema + let mutations = mutations::schema_mutations(); + let mut schema = doc.to_schema().expect("initial schema must be valid"); + for _ in 0..1000 { + if u.len() == 0 { + // We ran out of data abort. This is not an error + return Err(Error::Arbitrary(arbitrary::Error::NotEnoughData))?; + } + + let mutation = u.choose(&mutations)?; + let mut new_doc = doc.clone(); + // First let's modify the document. We use the schema because it has all the built-in definitions in it. + if !mutation.apply(u, &mut new_doc, &schema)? { + // The mutation didn't apply, let's try another one + continue; + } + + // Now let's validate that the schema says it's OK + match (mutation.is_valid(), new_doc.to_schema_validate()) { + (true, Ok(new_schema)) => { + // Let's reparse the document to check that it can be parsed + let reparsed = Document::parse(new_doc.to_string(), PathBuf::from("synthetic")) + .map_err(|e| Error::SchemaReparse { + mutation: mutation.type_name().to_string(), + doc: new_doc.clone(), + errors: e, + })?; + + // The reparsed document should be the same as the original document + if reparsed != new_doc { + return Err(Error::SerializationInconsistency { + original: new_doc, + new: reparsed, + }); + } + + // Let's try and create an executable document from the schema + generate_executable_document(u, &new_schema)?; + schema = new_schema.into_inner(); + doc = new_doc; + continue; + } + (true, Err(e)) => { + return Err(Error::SchemaDocumentValidation { + mutation: mutation.type_name().to_string(), + doc: new_doc, + errors: e, + }); + } + (false, Ok(_)) => { + return Err(Error::SchemaExpectedValidationFail { + doc: new_doc, + mutation: mutation.type_name().to_string(), + }); + } + (false, Err(_)) => { + // Validation was expected to fail, we can continue using the old doc and schema + continue; + } + } + } + Ok(doc) +} + +pub(crate) fn generate_executable_document( + u: &mut Unstructured, + schema: &Valid, +) -> Result { + let mut doc = Document::new(); + let mut executable_document = doc + .to_executable(schema) + .expect("initial document must be valid"); + let mutations = mutations::executable_document_mutations(); + for _ in 0..10 { + if u.len() == 0 { + // We ran out of data abort. This is not an error + return Err(Error::Arbitrary(arbitrary::Error::NotEnoughData))?; + } + let mutation = u.choose(&mutations)?; + let mut new_doc = doc.clone(); + // First let's modify the document. + if !mutation.apply(u, &mut new_doc, &schema, &executable_document)? { + // The mutation didn't apply, let's try another one + continue; + } + + // Now let's validate that the schema says it's OK + match (mutation.is_valid(), new_doc.to_executable_validate(schema)) { + (true, Ok(new_executable_document)) => { + // Let's reparse the document to check that it can be parsed + let reparsed = Document::parse(new_doc.to_string(), PathBuf::from("synthetic")) + .map_err(|e| Error::ExecutableReparse { + mutation: mutation.type_name().to_string(), + schema: schema.clone(), + doc: new_doc.clone(), + errors: e, + })?; + + // The reparsed document should be the same as the original document + if reparsed != new_doc { + return Err(Error::SerializationInconsistency { + original: new_doc, + new: reparsed, + }); + } + + doc = new_doc; + executable_document = new_executable_document.into_inner(); + continue; + } + (true, Err(e)) => { + return Err(Error::ExecutableDocumentValidation { + mutation: mutation.type_name().to_string(), + doc: new_doc, + schema: schema.clone(), + errors: e, + }); + } + (false, Ok(_)) => { + return Err(Error::SchemaExpectedValidationFail { + mutation: mutation.type_name().to_string(), + doc: new_doc, + }); + } + (false, Err(_)) => { + // Validation was expected to fail, we can continue using the old doc and schema + continue; + } + } + } + println!( + "Generated executable document: {}\n for schema {}", + doc, schema + ); + + Ok(doc) +} diff --git a/crates/apollo-smith/src/next/mutations/add_anonymous_operation_definition.rs b/crates/apollo-smith/src/next/mutations/add_anonymous_operation_definition.rs new file mode 100644 index 000000000..846f6aec0 --- /dev/null +++ b/crates/apollo-smith/src/next/mutations/add_anonymous_operation_definition.rs @@ -0,0 +1,30 @@ +use crate::next::mutations::{ExecutableDocumentMutation}; + +use crate::next::unstructured::Unstructured; +use apollo_compiler::ast::{Definition, Document}; +use apollo_compiler::{ExecutableDocument, Node, Schema}; + +pub(crate) struct AddAnonymousOperationDefinition; + +impl ExecutableDocumentMutation for AddAnonymousOperationDefinition { + fn apply( + &self, + u: &mut Unstructured, + doc: &mut Document, + schema: &Schema, + executable_document: &ExecutableDocument, + ) -> arbitrary::Result { + if !executable_document.named_operations.is_empty() || executable_document.anonymous_operation.is_some() { + // We already have an operation, so we can't add an anonymous one + return Ok(false); + } + doc.definitions + .push(Definition::OperationDefinition(Node::new( + u.arbitrary_operation_definition(schema, executable_document, None)?, + ))); + Ok(true) + } + fn is_valid(&self) -> bool { + true + } +} diff --git a/crates/apollo-smith/src/next/mutations/add_directive_definition.rs b/crates/apollo-smith/src/next/mutations/add_directive_definition.rs new file mode 100644 index 000000000..5464e26f2 --- /dev/null +++ b/crates/apollo-smith/src/next/mutations/add_directive_definition.rs @@ -0,0 +1,25 @@ +use apollo_compiler::ast::{Definition, Document}; +use apollo_compiler::{Node, Schema}; + +use crate::next::mutations::SchemaMutation; +use crate::next::unstructured::Unstructured; + +pub(crate) struct AddDirectiveDefinition; +impl SchemaMutation for AddDirectiveDefinition { + fn apply( + &self, + u: &mut Unstructured, + doc: &mut Document, + schema: &Schema, + ) -> arbitrary::Result { + doc.definitions + .push(Definition::DirectiveDefinition(Node::new( + u.arbitrary_directive_definition(schema)?, + ))); + + Ok(true) + } + fn is_valid(&self) -> bool { + true + } +} diff --git a/crates/apollo-smith/src/next/mutations/add_enum_type_definition.rs b/crates/apollo-smith/src/next/mutations/add_enum_type_definition.rs new file mode 100644 index 000000000..359ff5cd8 --- /dev/null +++ b/crates/apollo-smith/src/next/mutations/add_enum_type_definition.rs @@ -0,0 +1,25 @@ +use apollo_compiler::ast::{Definition, Document}; +use apollo_compiler::{Node, Schema}; + +use crate::next::mutations::SchemaMutation; +use crate::next::unstructured::Unstructured; + +pub(crate) struct AddEnumTypeDefinition; +impl SchemaMutation for AddEnumTypeDefinition { + fn apply( + &self, + u: &mut Unstructured, + doc: &mut Document, + schema: &Schema, + ) -> arbitrary::Result { + doc.definitions + .push(Definition::EnumTypeDefinition(Node::new( + u.arbitrary_enum_type_definition(schema)?, + ))); + + Ok(true) + } + fn is_valid(&self) -> bool { + true + } +} diff --git a/crates/apollo-smith/src/next/mutations/add_fragment_definiton.rs b/crates/apollo-smith/src/next/mutations/add_fragment_definiton.rs new file mode 100644 index 000000000..13b340475 --- /dev/null +++ b/crates/apollo-smith/src/next/mutations/add_fragment_definiton.rs @@ -0,0 +1,26 @@ +use crate::next::mutations::{ExecutableDocumentMutation}; + +use crate::next::unstructured::Unstructured; +use apollo_compiler::ast::{Definition, Document}; +use apollo_compiler::{ExecutableDocument, Node, Schema}; + +pub(crate) struct AddFragmentDefiniton; + +impl ExecutableDocumentMutation for AddFragmentDefiniton { + fn apply( + &self, + u: &mut Unstructured, + doc: &mut Document, + schema: &Schema, + executable_document: &ExecutableDocument, + ) -> arbitrary::Result { + doc.definitions + .push(Definition::FragmentDefinition(Node::new( + u.arbitrary_fragment_definition(schema, executable_document)?, + ))); + Ok(true) + } + fn is_valid(&self) -> bool { + true + } +} diff --git a/crates/apollo-smith/src/next/mutations/add_input_object_type_definition.rs b/crates/apollo-smith/src/next/mutations/add_input_object_type_definition.rs new file mode 100644 index 000000000..8892cb92f --- /dev/null +++ b/crates/apollo-smith/src/next/mutations/add_input_object_type_definition.rs @@ -0,0 +1,25 @@ +use apollo_compiler::ast::{Definition, Document}; +use apollo_compiler::{Node, Schema}; + +use crate::next::mutations::SchemaMutation; +use crate::next::unstructured::Unstructured; + +pub(crate) struct AddInputObjectTypeDefinition; +impl SchemaMutation for AddInputObjectTypeDefinition { + fn apply( + &self, + u: &mut Unstructured, + doc: &mut Document, + schema: &Schema, + ) -> arbitrary::Result { + doc.definitions + .push(Definition::InputObjectTypeDefinition(Node::new( + u.arbitrary_input_object_type_definition(schema)?, + ))); + Ok(true) + } + + fn is_valid(&self) -> bool { + true + } +} diff --git a/crates/apollo-smith/src/next/mutations/add_interface_type_definition.rs b/crates/apollo-smith/src/next/mutations/add_interface_type_definition.rs new file mode 100644 index 000000000..aaecb06fb --- /dev/null +++ b/crates/apollo-smith/src/next/mutations/add_interface_type_definition.rs @@ -0,0 +1,25 @@ +use apollo_compiler::ast::{Definition, Document}; +use apollo_compiler::{Node, Schema}; + +use crate::next::mutations::SchemaMutation; +use crate::next::unstructured::Unstructured; + +pub(crate) struct AddInterfaceTypeDefinition; +impl SchemaMutation for AddInterfaceTypeDefinition { + fn apply( + &self, + u: &mut Unstructured, + doc: &mut Document, + schema: &Schema, + ) -> arbitrary::Result { + doc.definitions + .push(Definition::InterfaceTypeDefinition(Node::new( + u.arbitrary_interface_type_definition(schema)?, + ))); + + Ok(true) + } + fn is_valid(&self) -> bool { + true + } +} diff --git a/crates/apollo-smith/src/next/mutations/add_named_operation_definition.rs b/crates/apollo-smith/src/next/mutations/add_named_operation_definition.rs new file mode 100644 index 000000000..6dc76b107 --- /dev/null +++ b/crates/apollo-smith/src/next/mutations/add_named_operation_definition.rs @@ -0,0 +1,31 @@ +use crate::next::mutations::{ExecutableDocumentMutation}; + +use crate::next::unstructured::Unstructured; +use apollo_compiler::ast::{Definition, Document}; +use apollo_compiler::{ExecutableDocument, Node, Schema}; + +pub(crate) struct AddNamedOperationDefinition; + +impl ExecutableDocumentMutation for AddNamedOperationDefinition { + fn apply( + &self, + u: &mut Unstructured, + doc: &mut Document, + schema: &Schema, + executable_document: &ExecutableDocument, + ) -> arbitrary::Result { + if executable_document.anonymous_operation.is_some() { + // We already have an anonymous operation, so we can't add a named one + return Ok(false); + } + let name = u.unique_name(); + doc.definitions + .push(Definition::OperationDefinition(Node::new( + u.arbitrary_operation_definition(schema, executable_document, Some(name))?, + ))); + Ok(true) + } + fn is_valid(&self) -> bool { + true + } +} diff --git a/crates/apollo-smith/src/next/mutations/add_object_type_definition.rs b/crates/apollo-smith/src/next/mutations/add_object_type_definition.rs new file mode 100644 index 000000000..63e893be1 --- /dev/null +++ b/crates/apollo-smith/src/next/mutations/add_object_type_definition.rs @@ -0,0 +1,25 @@ +use apollo_compiler::ast::{Definition, Document}; +use apollo_compiler::{Node, Schema}; + +use crate::next::mutations::SchemaMutation; +use crate::next::unstructured::Unstructured; + +pub(crate) struct AddObjectTypeDefinition; +impl SchemaMutation for AddObjectTypeDefinition { + fn apply( + &self, + u: &mut Unstructured, + doc: &mut Document, + schema: &Schema, + ) -> arbitrary::Result { + doc.definitions + .push(Definition::ObjectTypeDefinition(Node::new( + u.arbitrary_object_type_definition(schema)?, + ))); + Ok(true) + } + + fn is_valid(&self) -> bool { + true + } +} diff --git a/crates/apollo-smith/src/next/mutations/add_scalar_type_definition.rs b/crates/apollo-smith/src/next/mutations/add_scalar_type_definition.rs new file mode 100644 index 000000000..c1f8713f1 --- /dev/null +++ b/crates/apollo-smith/src/next/mutations/add_scalar_type_definition.rs @@ -0,0 +1,25 @@ +use apollo_compiler::ast::{Definition, Document}; +use apollo_compiler::{Node, Schema}; + +use crate::next::mutations::SchemaMutation; +use crate::next::unstructured::Unstructured; + +pub(crate) struct AddScalarTypeDefinition; +impl SchemaMutation for AddScalarTypeDefinition { + fn apply( + &self, + u: &mut Unstructured, + doc: &mut Document, + schema: &Schema, + ) -> arbitrary::Result { + doc.definitions + .push(Definition::ScalarTypeDefinition(Node::new( + u.arbitrary_scalar_type_definition(schema)?, + ))); + + Ok(true) + } + fn is_valid(&self) -> bool { + true + } +} diff --git a/crates/apollo-smith/src/next/mutations/add_schema_definition.rs b/crates/apollo-smith/src/next/mutations/add_schema_definition.rs new file mode 100644 index 000000000..64be9826d --- /dev/null +++ b/crates/apollo-smith/src/next/mutations/add_schema_definition.rs @@ -0,0 +1,25 @@ +use apollo_compiler::ast::{Definition, Document}; +use apollo_compiler::{Node, Schema}; + +use crate::next::mutations::SchemaMutation; +use crate::next::unstructured::Unstructured; + +pub(crate) struct AddSchemaDefiniton; +impl SchemaMutation for AddSchemaDefiniton { + fn apply( + &self, + u: &mut Unstructured, + doc: &mut Document, + schema: &Schema, + ) -> arbitrary::Result { + // If the document already has a schema definition, we don't need to add another one + doc.definitions.push(Definition::SchemaDefinition(Node::new( + u.arbitrary_schema_definition(schema)?, + ))); + + Ok(true) + } + fn is_valid(&self) -> bool { + true + } +} diff --git a/crates/apollo-smith/src/next/mutations/add_union_type_definition.rs b/crates/apollo-smith/src/next/mutations/add_union_type_definition.rs new file mode 100644 index 000000000..91befd0fd --- /dev/null +++ b/crates/apollo-smith/src/next/mutations/add_union_type_definition.rs @@ -0,0 +1,25 @@ +use apollo_compiler::ast::{Definition, Document}; +use apollo_compiler::{Node, Schema}; + +use crate::next::mutations::SchemaMutation; +use crate::next::unstructured::Unstructured; + +pub(crate) struct AddUnionTypeDefinition; +impl SchemaMutation for AddUnionTypeDefinition { + fn apply( + &self, + u: &mut Unstructured, + doc: &mut Document, + schema: &Schema, + ) -> arbitrary::Result { + doc.definitions + .push(Definition::UnionTypeDefinition(Node::new( + u.arbitrary_union_type_definition(schema)?, + ))); + + Ok(true) + } + fn is_valid(&self) -> bool { + true + } +} diff --git a/crates/apollo-smith/src/next/mutations/mod.rs b/crates/apollo-smith/src/next/mutations/mod.rs new file mode 100644 index 000000000..30bb2fc4b --- /dev/null +++ b/crates/apollo-smith/src/next/mutations/mod.rs @@ -0,0 +1,85 @@ +use std::any::type_name; + +use apollo_compiler::ast::Document; +use apollo_compiler::{ExecutableDocument, Schema}; + +use crate::next::mutations::add_anonymous_operation_definition::AddAnonymousOperationDefinition; +use crate::next::mutations::add_directive_definition::AddDirectiveDefinition; +use crate::next::mutations::add_enum_type_definition::AddEnumTypeDefinition; +use crate::next::mutations::add_input_object_type_definition::AddInputObjectTypeDefinition; +use crate::next::mutations::add_interface_type_definition::AddInterfaceTypeDefinition; +use crate::next::mutations::add_named_operation_definition::AddNamedOperationDefinition; +use crate::next::mutations::add_object_type_definition::AddObjectTypeDefinition; +use crate::next::mutations::add_scalar_type_definition::AddScalarTypeDefinition; +use crate::next::mutations::add_union_type_definition::AddUnionTypeDefinition; +use crate::next::mutations::remove_all_fields::RemoveAllFields; +use crate::next::mutations::remove_required_field::RemoveRequiredField; +use crate::next::unstructured::Unstructured; + +mod add_anonymous_operation_definition; +mod add_directive_definition; +mod add_enum_type_definition; +mod add_fragment_definiton; +mod add_input_object_type_definition; +mod add_interface_type_definition; +mod add_named_operation_definition; +mod add_object_type_definition; +mod add_scalar_type_definition; +mod add_schema_definition; +mod add_union_type_definition; +mod remove_all_fields; +mod remove_required_field; + +pub(crate) trait SchemaMutation { + /// Apply the mutation to the document + /// Returns false if the mutation did not apply + fn apply( + &self, + u: &mut Unstructured, + doc: &mut Document, + schema: &Schema, + ) -> arbitrary::Result; + fn is_valid(&self) -> bool; + + fn type_name(&self) -> &str { + type_name::() + } +} + +pub(crate) trait ExecutableDocumentMutation { + /// Apply the mutation to the document + /// Returns false if the mutation did not apply + fn apply( + &self, + u: &mut Unstructured, + doc: &mut Document, + schema: &Schema, + executable: &ExecutableDocument, + ) -> arbitrary::Result; + fn is_valid(&self) -> bool; + + fn type_name(&self) -> &str { + type_name::() + } +} + +pub(crate) fn schema_mutations() -> Vec> { + vec![ + Box::new(AddObjectTypeDefinition), + Box::new(AddInterfaceTypeDefinition), + Box::new(AddDirectiveDefinition), + Box::new(AddInputObjectTypeDefinition), + Box::new(AddUnionTypeDefinition), + Box::new(AddEnumTypeDefinition), + Box::new(AddScalarTypeDefinition), + //Box::new(RemoveAllFields), + Box::new(RemoveRequiredField), + ] +} + +pub(crate) fn executable_document_mutations() -> Vec> { + vec![ + Box::new(AddNamedOperationDefinition), + Box::new(AddAnonymousOperationDefinition), + ] +} diff --git a/crates/apollo-smith/src/next/mutations/remove_all_fields.rs b/crates/apollo-smith/src/next/mutations/remove_all_fields.rs new file mode 100644 index 000000000..da492876e --- /dev/null +++ b/crates/apollo-smith/src/next/mutations/remove_all_fields.rs @@ -0,0 +1,39 @@ +use crate::next::mutations::SchemaMutation; + +use crate::next::ast::definition::DefinitionKind; +use crate::next::ast::document::DocumentExt; +use crate::next::unstructured::Unstructured; +use apollo_compiler::ast::{Definition, Document}; +use apollo_compiler::Schema; + +pub(crate) struct RemoveAllFields; +impl SchemaMutation for RemoveAllFields { + fn apply( + &self, + u: &mut Unstructured, + doc: &mut Document, + _schema: &Schema, + ) -> arbitrary::Result { + match doc.random_definition_mut( + u, + vec![ + DefinitionKind::ObjectTypeDefinition, + DefinitionKind::InterfaceTypeDefinition, + ], + )? { + Some(Definition::ObjectTypeDefinition(definition)) => { + definition.make_mut().fields.clear(); + Ok(true) + } + Some(Definition::InterfaceTypeDefinition(definition)) => { + definition.make_mut().fields.clear(); + Ok(true) + } + + _ => Ok(false), + } + } + fn is_valid(&self) -> bool { + false + } +} diff --git a/crates/apollo-smith/src/next/mutations/remove_required_field.rs b/crates/apollo-smith/src/next/mutations/remove_required_field.rs new file mode 100644 index 000000000..2d31cecf5 --- /dev/null +++ b/crates/apollo-smith/src/next/mutations/remove_required_field.rs @@ -0,0 +1,63 @@ +use crate::next::mutations::SchemaMutation; + +use crate::next::ast::definition::DefinitionKind; +use crate::next::ast::document::DocumentExt; +use crate::next::schema::InterfaceTypeExt; +use crate::next::unstructured::Unstructured; +use apollo_compiler::ast::{Definition, Document}; +use apollo_compiler::Schema; + +pub(crate) struct RemoveRequiredField; + +impl SchemaMutation for RemoveRequiredField { + fn apply( + &self, + u: &mut Unstructured, + doc: &mut Document, + schema: &Schema, + ) -> arbitrary::Result { + match doc.random_definition_mut( + u, + vec![ + DefinitionKind::ObjectTypeDefinition, + DefinitionKind::InterfaceTypeDefinition, + ], + )? { + Some(Definition::ObjectTypeDefinition(definition)) => { + if !definition.implements_interfaces.is_empty() { + let interface = schema + .get_interface(u.choose(&definition.implements_interfaces)?) + .expect("interface not found"); + if let Some(field) = interface.random_field(u)? { + definition + .make_mut() + .fields + .retain(|f| f.name != field.name); + return Ok(true); + } + } + Ok(false) + } + Some(Definition::InterfaceTypeDefinition(definition)) => { + if !definition.implements_interfaces.is_empty() { + let interface = schema + .get_interface(u.choose(&definition.implements_interfaces)?) + .expect("interface not found"); + if let Some(field) = interface.random_field(u)? { + definition + .make_mut() + .fields + .retain(|f| f.name != field.name); + return Ok(true); + } + } + Ok(false) + } + + _ => Ok(false), + } + } + fn is_valid(&self) -> bool { + false + } +} diff --git a/crates/apollo-smith/src/next/schema/extended_type.rs b/crates/apollo-smith/src/next/schema/extended_type.rs new file mode 100644 index 000000000..b1d2e456c --- /dev/null +++ b/crates/apollo-smith/src/next/schema/extended_type.rs @@ -0,0 +1,55 @@ +use arbitrary::Unstructured; + +use apollo_compiler::ast::{Name, Type}; +use apollo_compiler::schema::ExtendedType; + +pub(crate) trait ExtendedTypeExt { + fn ty(&self, u: &mut Unstructured) -> arbitrary::Result; +} + +impl ExtendedTypeExt for ExtendedType { + fn ty(&self, u: &mut Unstructured) -> arbitrary::Result { + Ok(ty(u, self.name().clone())?) + } +} + +fn ty(u: &mut Unstructured, name: Name) -> arbitrary::Result { + let mut ty = if u.arbitrary()? { + Type::Named(name) + } else { + Type::NonNullNamed(name) + }; + + for _ in 0..u.int_in_range(0..=5)? { + if u.arbitrary()? { + ty = Type::List(Box::new(ty)) + } else { + ty = Type::NonNullList(Box::new(ty)) + }; + } + Ok(ty) +} + +#[derive(Debug)] +pub(crate) enum ExtendedTypeKind { + Scalar, + Object, + Interface, + Union, + Enum, + InputObjectTypeDefinition, +} + +impl ExtendedTypeKind { + pub(crate) fn matches(&self, definition: &ExtendedType) -> bool { + match (self, definition) { + (Self::Scalar, ExtendedType::Scalar(_)) => true, + (Self::Object, ExtendedType::Object(_)) => true, + (Self::Interface, ExtendedType::Interface(_)) => true, + (Self::Union, ExtendedType::Union(_)) => true, + (Self::Enum, ExtendedType::Enum(_)) => true, + (Self::InputObjectTypeDefinition, ExtendedType::InputObject(_)) => true, + _ => false, + } + } +} diff --git a/crates/apollo-smith/src/next/schema/mod.rs b/crates/apollo-smith/src/next/schema/mod.rs new file mode 100644 index 000000000..4010a781a --- /dev/null +++ b/crates/apollo-smith/src/next/schema/mod.rs @@ -0,0 +1,192 @@ +use std::ops::Deref; +use std::sync::OnceLock; +use apollo_compiler::ast::{FieldDefinition, Name}; +use apollo_compiler::schema::{Component, ExtendedType, InterfaceType, ObjectType, UnionType}; +use indexmap::IndexMap; +use apollo_compiler::Schema; +use crate::next::Unstructured; + +pub(crate) mod extended_type; + +pub(crate) mod object_type; +pub(crate) mod schema; + +/// macro for accessing fields on schema elements +macro_rules! field_access { + ($ty:ty) => { + paste::paste! { + pub(crate) trait [<$ty Ext>] { + fn random_field( + &self, + u: &mut crate::next::Unstructured, + ) -> arbitrary::Result>> { + let mut fields = self.target().fields.values().collect::>(); + match u.choose_index(fields.len()) { + Ok(idx)=> Ok(Some(fields.remove(idx))), + Err(arbitrary::Error::EmptyChoose)=> Ok(None), + Err(e)=> Err(e) + } + } + + fn random_field_mut( + &mut self, + u: &mut crate::next::Unstructured, + ) -> arbitrary::Result>> { + let mut fields = self.target_mut().fields.values_mut().collect::>(); + match u.choose_index(fields.len()) { + Ok(idx)=> Ok(Some(fields.remove(idx))), + Err(arbitrary::Error::EmptyChoose)=> Ok(None), + Err(e)=> Err(e) + } + + } + + fn sample_fields( + &self, + u: &mut crate::next::Unstructured, + ) -> arbitrary::Result>> { + let existing = self + .target() + .fields + .values() + .filter(|_| u.arbitrary().unwrap_or(false)) + .collect::>(); + + Ok(existing) + } + fn target(&self) -> &$ty; + fn target_mut(&mut self) -> &mut $ty; + } + + impl [<$ty Ext>] for $ty { + fn target(&self) -> &$ty { + self + } + fn target_mut(&mut self) -> &mut $ty { + self + } + } + } + }; +} + +field_access!(ObjectType); +field_access!(InterfaceType); + +pub(crate) trait Selectable { + + fn name(&self) -> &Name; + fn fields(&self) -> &IndexMap>; + + fn random_specialization<'a>(&self, u: &mut Unstructured, schema: &'a Schema) -> arbitrary::Result>; + fn random_field(&self, u: &mut Unstructured) -> arbitrary::Result<&Component> { + // Types always have at least one field + let fields = self.fields().values().collect::>(); + Ok(fields[u.choose_index(fields.len())?]) + } + +} + +impl Selectable for ObjectType { + fn name(&self) -> &Name { + &self.name + } + + + + fn fields(&self) -> &IndexMap> { + &self.fields + } + + fn random_specialization<'a>(&self, _u: &mut Unstructured, _schema: &'a Schema) -> arbitrary::Result> { + Ok(None) + } +} + +impl Selectable for &UnionType { + fn name(&self) -> &Name { + &self.name + } + + fn fields(&self) -> &IndexMap> { + static EMPTY: OnceLock>> = OnceLock::new(); + &EMPTY.get_or_init(||Default::default()) + } + + fn random_specialization<'a>(&self, u: &mut Unstructured, schema: &'a Schema) -> arbitrary::Result> { + let members = self.members.iter().map(|name| schema.types.get(&name.name)).collect::>(); + if members.is_empty() { + Ok(None) + } + else { + Ok(members[u.choose_index(members.len())?]) + } + } +} + +impl Selectable for InterfaceType { + fn name(&self) -> &Name { + &self.name + } + + fn fields(&self) -> &IndexMap> { + &self.fields + } + + fn random_specialization<'a>(&self, u: &mut Unstructured, schema: &'a Schema) -> arbitrary::Result> { + // An interface specialization is either an object or another interface that implements this interface + let implements = schema + .types + .values() + .filter(|ty| { + match ty { + ExtendedType::Object(o) => o.implements_interfaces.contains(&self.name), + ExtendedType::Interface(i) => i.implements_interfaces.contains(&self.name), + _=> return false, + } + }) + .collect::>(); + if implements.is_empty() { + Ok(None) + } + else { + Ok(Some(implements[u.choose_index(implements.len())?])) + } + + + } +} + + +impl Selectable for ExtendedType { + fn name(&self) -> &Name { + match self { + ExtendedType::Scalar(scalar) => {&scalar.name} + ExtendedType::Object(object_type) => {&object_type.name} + ExtendedType::Interface(interface_type) => {&interface_type.name} + ExtendedType::Union(union_type) => {&union_type.name} + ExtendedType::Enum(enum_type) => {&enum_type.name} + ExtendedType::InputObject(input_object) => {&input_object.name} + } + } + + + + fn fields(&self) -> &IndexMap> { + static EMPTY: OnceLock>> = OnceLock::new(); + match self { + ExtendedType::Object(t) => &t.fields, + ExtendedType::Interface(t) => &t.fields, + _ => &EMPTY.get_or_init(||Default::default()), + } + } + + fn random_specialization<'a>(&self, u: &mut Unstructured, schema: &'a Schema) -> arbitrary::Result> { + match self { + ExtendedType::Object(object_type) => {object_type.deref().random_specialization(u, schema)} + ExtendedType::Interface(interface_type) => { interface_type.deref().random_specialization(u, schema)} + ExtendedType::Union(union_type) => { union_type.deref().random_specialization(u, schema)} + _ => Ok(None) + } + } +} diff --git a/crates/apollo-smith/src/next/schema/object_type.rs b/crates/apollo-smith/src/next/schema/object_type.rs new file mode 100644 index 000000000..16412f74f --- /dev/null +++ b/crates/apollo-smith/src/next/schema/object_type.rs @@ -0,0 +1,17 @@ +use apollo_compiler::ast::OperationType; +use apollo_compiler::schema::ObjectType; + +pub(crate) trait ObjectTypeExt { + fn operation_type(&self) -> Option; +} + +impl ObjectTypeExt for ObjectType { + fn operation_type(&self) -> Option { + match self.name.as_str() { + "Query" => Some(OperationType::Query), + "Mutation" => Some(OperationType::Mutation), + "Subscription" => Some(OperationType::Subscription), + _ => None, + } + } +} diff --git a/crates/apollo-smith/src/next/schema/schema.rs b/crates/apollo-smith/src/next/schema/schema.rs new file mode 100644 index 000000000..a0e2dbb92 --- /dev/null +++ b/crates/apollo-smith/src/next/schema/schema.rs @@ -0,0 +1,149 @@ +use arbitrary::Unstructured; +use paste::paste; + +use apollo_compiler::schema::{DirectiveDefinition, ExtendedType}; +use apollo_compiler::schema::{ + EnumType, InputObjectType, InterfaceType, ObjectType, ScalarType, UnionType, +}; +use apollo_compiler::Node; +use apollo_compiler::Schema; + +use crate::next::schema::extended_type::ExtendedTypeKind; + +macro_rules! access { + ($variant: ident, $ty: ty) => { + paste! { + fn []( + &self, + u: &mut Unstructured, + ) -> arbitrary::Result<&Node<$ty>> { + let mut existing = self + .target() + .types.values() + .filter_map(|d| { + if let ExtendedType::$variant(definition) = d { + Some(definition) + } else { + None + } + }) + .collect::>(); + let idx = u.choose_index(existing.len()).map_err(|e|{ + if let arbitrary::Error::EmptyChoose = e { + panic!("no existing definitions of type {}", stringify!($ty)) + } else { + e + } + })?; + Ok(existing.remove(idx)) + } + + fn []( + &self, + u: &mut Unstructured, + ) -> arbitrary::Result>> { + let existing = self + .target() + .types.values() + .filter_map(|d| { + if let ExtendedType::$variant(definition) = d { + Some(definition) + } else { + None + } + }) + .filter(|_| u.arbitrary().unwrap_or(false)) + .collect::>(); + + Ok(existing) + } + } + }; +} + +pub(crate) trait SchemaExt { + access!(Scalar, ScalarType); + access!(Object, ObjectType); + access!(Interface, InterfaceType); + access!(Union, UnionType); + access!(Enum, EnumType); + access!(InputObject, InputObjectType); + + fn random_type( + &self, + u: &mut Unstructured, + types: Vec, + ) -> arbitrary::Result<&ExtendedType> { + let definitions = self + .target() + .types + .values() + .filter(|d| types.iter().any(|t| t.matches(*d))) + .filter(|d| d.is_scalar() || !d.is_built_in()) + .collect::>(); + Ok(u.choose(definitions.as_slice()).map_err(|e| { + if let arbitrary::Error::EmptyChoose = e { + panic!("no existing definitions of types {:?}", types) + } else { + e + } + })?) + } + + fn random_directive( + &self, + u: &mut Unstructured, + ) -> arbitrary::Result<&Node> { + let mut existing = self + .target() + .directive_definitions + .values() + .collect::>(); + let idx = u.choose_index(existing.len()).map_err(|e| { + if let arbitrary::Error::EmptyChoose = e { + panic!("no existing directive definitions") + } else { + e + } + })?; + Ok(existing.remove(idx)) + } + + fn sample_directives( + &self, + u: &mut Unstructured, + ) -> arbitrary::Result>> { + let existing = self + .target() + .directive_definitions + .values() + .filter(|_| u.arbitrary().unwrap_or(false)) + .collect::>(); + + Ok(existing) + } + + fn random_query_mutation_subscription( + &self, + u: &mut Unstructured, + ) -> arbitrary::Result<&Node> { + Ok(*u.choose( + &vec![ + self.target().get_object("Query"), + self.target().get_object("Mutation"), + self.target().get_object("Subscription"), + ] + .into_iter() + .filter_map(|o| o) + .collect::>(), + )?) + } + + fn target(&self) -> &Schema; +} + +impl SchemaExt for Schema { + fn target(&self) -> &Schema { + &self + } +} diff --git a/crates/apollo-smith/src/next/unstructured.rs b/crates/apollo-smith/src/next/unstructured.rs new file mode 100644 index 000000000..794277c02 --- /dev/null +++ b/crates/apollo-smith/src/next/unstructured.rs @@ -0,0 +1,711 @@ +use std::collections::{HashMap, HashSet}; +use std::ops::{Deref, DerefMut}; + +use arbitrary::Result; + +use apollo_compiler::ast::{ + Argument, DirectiveDefinition, DirectiveLocation, EnumTypeDefinition, EnumValueDefinition, + Field, FieldDefinition, FragmentDefinition, FragmentSpread, InlineFragment, + InputObjectTypeDefinition, InputValueDefinition, InterfaceTypeDefinition, Name, + ObjectTypeDefinition, OperationDefinition, OperationType, ScalarTypeDefinition, + SchemaDefinition, Selection, Type, UnionTypeDefinition, Value, VariableDefinition, +}; +use apollo_compiler::schema::{ExtendedType, InterfaceType}; +use apollo_compiler::{ExecutableDocument, Node, NodeStr, Schema}; + +use crate::next::ast::directive_definition::DirectiveDefinitionIterExt; +use crate::next::executable::executable_document::ExecutableDocumentExt; +use crate::next::schema::extended_type::{ExtendedTypeExt, ExtendedTypeKind}; +use crate::next::schema::object_type::ObjectTypeExt; +use crate::next::schema::schema::SchemaExt; +use crate::next::schema::Selectable; + +pub struct Unstructured<'a> { + u: arbitrary::Unstructured<'a>, + counter: usize, +} + +impl Unstructured<'_> { + pub fn new<'a>(data: &'a [u8]) -> Unstructured<'a> { + Unstructured { + u: arbitrary::Unstructured::new(data), + counter: 0, + } + } + + pub(crate) fn arbitrary_vec Result>( + &mut self, + min: usize, + max: usize, + callback: C, + ) -> Result> { + let count = self.int_in_range(min..=max)?; + let mut results = Vec::with_capacity(count); + for _ in 0..count { + results.push(callback(self)?); + } + Ok(results) + } + pub(crate) fn arbitrary_optional Result>( + &mut self, + callback: C, + ) -> Result> { + if self.arbitrary()? { + Ok(Some(callback(self)?)) + } else { + Ok(None) + } + } + + pub(crate) fn unique_name(&mut self) -> Name { + self.counter = self.counter + 1; + Name::new(NodeStr::new(&format!("f{}", self.counter))).expect("valid name") + } + + pub(crate) fn arbitrary_node_str(&mut self) -> Result { + let s: String = self.arbitrary()?; + let idx = s + .char_indices() + .nth(10) + .map(|(s, _c)| s) + .unwrap_or_else(|| s.len()); + Ok(NodeStr::new(&s[..idx])) + } + + pub(crate) fn arbitrary_directive_locations( + &mut self, + ) -> arbitrary::Result> { + let mut locations = Vec::new(); + for _ in 0..self.int_in_range(1..=5)? { + locations.push( + self.choose(&[ + DirectiveLocation::Query, + DirectiveLocation::Mutation, + DirectiveLocation::Subscription, + DirectiveLocation::Field, + DirectiveLocation::FragmentDefinition, + DirectiveLocation::FragmentSpread, + DirectiveLocation::InlineFragment, + DirectiveLocation::Schema, + DirectiveLocation::Scalar, + DirectiveLocation::Object, + DirectiveLocation::FieldDefinition, + DirectiveLocation::ArgumentDefinition, + DirectiveLocation::Interface, + DirectiveLocation::Union, + DirectiveLocation::Enum, + DirectiveLocation::EnumValue, + DirectiveLocation::InputObject, + DirectiveLocation::InputFieldDefinition, + ])? + .clone(), + ); + } + Ok(locations) + } + + pub(crate) fn arbitrary_schema_definition( + &mut self, + schema: &Schema, + ) -> Result { + Ok(SchemaDefinition { + description: self.arbitrary_optional(|u| u.arbitrary_node_str())?, + directives: schema + .sample_directives(self)? + .into_iter() + .with_location(DirectiveLocation::Schema) + .try_collect(self, schema)?, + root_operations: vec![ + Some(Node::new(( + OperationType::Query, + schema.random_object_type(self)?.name.clone(), + ))), + self.arbitrary_optional(|u| { + Ok(Node::new(( + OperationType::Mutation, + schema.random_object_type(u)?.name.clone(), + ))) + })?, + self.arbitrary_optional(|u| { + Ok(Node::new(( + OperationType::Subscription, + schema.random_object_type(u)?.name.clone(), + ))) + })?, + ] + .into_iter() + .filter_map(|op| op) + .collect(), + }) + } + + pub(crate) fn arbitrary_object_type_definition( + &mut self, + schema: &Schema, + ) -> Result { + let implements = + Self::all_transitive_interfaces(schema, schema.sample_interface_types(self)?); + let implements_fields = Self::all_unique_fields_from_interfaces(&implements); + let new_fields = self.arbitrary_vec(1, 5, |u| { + Ok(Node::new(u.arbitrary_field_definition( + schema, + DirectiveLocation::FieldDefinition, + )?)) + })?; + Ok(ObjectTypeDefinition { + description: self.arbitrary_optional(|u| u.arbitrary_node_str())?, + name: self.unique_name(), + implements_interfaces: implements.iter().map(|i| i.name.clone()).collect(), + directives: schema + .sample_directives(self)? + .into_iter() + .with_location(DirectiveLocation::Object) + .try_collect(self, schema)?, + fields: new_fields + .into_iter() + .chain(implements_fields.into_iter()) + .collect(), + }) + } + + pub(crate) fn arbitrary_directive_definition( + &mut self, + schema: &Schema, + ) -> Result { + Ok(DirectiveDefinition { + description: self.arbitrary_optional(|u| u.arbitrary_node_str())?, + name: self.unique_name(), + arguments: self.arbitrary_vec(0, 5, |u| { + Ok(Node::new(u.arbitrary_input_value_definition( + schema, + DirectiveLocation::ArgumentDefinition, + )?)) + })?, + repeatable: self.arbitrary()?, + locations: self.arbitrary_directive_locations()?, + }) + } + + pub(crate) fn arbitrary_input_object_type_definition( + &mut self, + schema: &Schema, + ) -> Result { + Ok(InputObjectTypeDefinition { + description: self.arbitrary_optional(|u| u.arbitrary_node_str())?, + name: self.unique_name(), + directives: schema + .sample_directives(self)? + .into_iter() + .with_location(DirectiveLocation::InputObject) + .try_collect(self, schema)?, + fields: self.arbitrary_vec(0, 5, |u| { + Ok(Node::new(u.arbitrary_input_value_definition( + schema, + DirectiveLocation::InputFieldDefinition, + )?)) + })?, + }) + } + + pub(crate) fn arbitrary_input_value_definition( + &mut self, + schema: &Schema, + location: DirectiveLocation, + ) -> Result { + let ty = schema + .random_type( + self, + vec![ + ExtendedTypeKind::InputObjectTypeDefinition, + ExtendedTypeKind::Scalar, + ], + )? + .ty(self)?; + let default_value = + self.arbitrary_optional(|u| Ok(Node::new(u.arbitrary_value(schema, &ty)?)))?; + + Ok(InputValueDefinition { + description: self.arbitrary_optional(|u| u.arbitrary_node_str())?, + name: self.unique_name(), + ty: Node::new(ty), + default_value, + directives: schema + .sample_directives(self)? + .into_iter() + .with_location(location) + .try_collect(self, schema)?, + }) + } + + pub(crate) fn arbitrary_scalar_type_definition( + &mut self, + schema: &Schema, + ) -> Result { + Ok(ScalarTypeDefinition { + description: self.arbitrary_optional(|u| u.arbitrary_node_str())?, + name: self.unique_name(), + directives: schema + .sample_directives(self)? + .into_iter() + .with_location(DirectiveLocation::Scalar) + .try_collect(self, schema)?, + }) + } + + pub(crate) fn arbitrary_enum_type_definition( + &mut self, + schema: &Schema, + ) -> Result { + Ok(EnumTypeDefinition { + description: self.arbitrary_optional(|u| u.arbitrary_node_str())?, + name: self.unique_name(), + directives: schema + .sample_directives(self)? + .into_iter() + .with_location(DirectiveLocation::Enum) + .try_collect(self, schema)?, + values: self.arbitrary_vec(0, 5, |u| { + Ok(Node::new(EnumValueDefinition { + description: u.arbitrary_optional(|u| u.arbitrary_node_str())?, + value: u.unique_name(), + directives: schema + .sample_directives(u)? + .into_iter() + .with_location(DirectiveLocation::EnumValue) + .try_collect(u, schema)?, + })) + })?, + }) + } + + pub(crate) fn arbitrary_union_type_definition( + &mut self, + schema: &Schema, + ) -> Result { + Ok(UnionTypeDefinition { + description: self.arbitrary_optional(|u| u.arbitrary_node_str())?, + name: self.unique_name(), + directives: schema + .sample_directives(self)? + .into_iter() + .with_location(DirectiveLocation::Union) + .try_collect(self, schema)?, + members: schema + .sample_object_types(self)? + .iter() + .map(|i| i.name.clone()) + .collect(), + }) + } + + pub(crate) fn arbitrary_interface_type_definition( + &mut self, + schema: &Schema, + ) -> Result { + // All interfaces need to have all the fields from the interfaces they implement. + let implements = + Self::all_transitive_interfaces(schema, schema.sample_interface_types(self)?); + + // Interfaces cannot have duplicate fields so stash them in a map + let mut implements_fields = Self::all_unique_fields_from_interfaces(&implements); + + let new_fields = self.arbitrary_vec(1, 5, |u| { + Ok(Node::new(u.arbitrary_field_definition( + schema, + DirectiveLocation::FieldDefinition, + )?)) + })?; + + Ok(InterfaceTypeDefinition { + description: self.arbitrary_optional(|u| u.arbitrary_node_str())?, + name: self.unique_name(), + implements_interfaces: implements + .iter() + .map(|interface| interface.name.clone()) + .collect(), + directives: schema + .sample_directives(self)? + .into_iter() + .with_location(DirectiveLocation::Interface) + .try_collect(self, schema)?, + fields: new_fields + .into_iter() + .chain(implements_fields.into_iter()) + .collect(), + }) + } + + fn all_unique_fields_from_interfaces( + interfaces: &Vec<&Node>, + ) -> Vec> { + let all_fields = interfaces + .iter() + .flat_map(|interface| interface.fields.values()) + .map(|field| (field.name.clone(), field.deref().clone())) + .collect::>(); + all_fields.values().cloned().collect() + } + + pub(crate) fn arbitrary_field_definition( + &mut self, + schema: &Schema, + directive_location: DirectiveLocation, + ) -> Result { + Ok(FieldDefinition { + description: self.arbitrary_optional(|u| u.arbitrary_node_str())?, + name: self.unique_name(), + arguments: self.arbitrary_vec(0, 5, |u| { + Ok(Node::new(u.arbitrary_input_value_definition( + schema, + DirectiveLocation::ArgumentDefinition, + )?)) + })?, + ty: schema + .random_type( + self, + vec![ + ExtendedTypeKind::Scalar, + ExtendedTypeKind::Object, + ExtendedTypeKind::Enum, + ExtendedTypeKind::Union, + ExtendedTypeKind::Interface, + ], + )? + .ty(self)?, + directives: schema + .sample_directives(self)? + .into_iter() + .with_location(directive_location) + .try_collect(self, schema)?, + }) + } + + pub(crate) fn arbitrary_value_untyped(&mut self) -> Result { + enum ValueKind { + Int, + Float, + Boolean, + String, + Object, + List, + } + let kind = self.choose(&[ + ValueKind::Int, + ValueKind::Float, + ValueKind::Boolean, + ValueKind::String, + ValueKind::Object, + ValueKind::List, + ])?; + + Ok(match *kind { + ValueKind::Int => Value::from(self.arbitrary::()?), + ValueKind::Float => loop { + let val = self.arbitrary::()?; + if val.is_finite() { + return Ok(Value::from(val)); + } + }, + ValueKind::Boolean => Value::Boolean(self.arbitrary()?), + ValueKind::String => Value::String(self.arbitrary_node_str()?), + ValueKind::Object => Value::Object(self.arbitrary_vec(0, 20, |u| { + Ok((u.unique_name(), Node::new(u.arbitrary_value_untyped()?))) + })?), + ValueKind::List => Value::List( + self.arbitrary_vec(0, 20, |u| Ok(Node::new(u.arbitrary_value_untyped()?)))?, + ), + }) + } + + pub(crate) fn arbitrary_value(&mut self, schema: &Schema, ty: &Type) -> Result { + match ty { + Type::Named(ty) => { + if self.arbitrary()? { + self.arbitrary_value(schema, &Type::NonNullNamed(ty.clone())) + } else { + Ok(Value::Null) + } + } + Type::List(ty) => { + if self.arbitrary()? { + Ok(Value::List(self.arbitrary_vec(0, 5, |u| { + Ok(Node::new(u.arbitrary_value(schema, ty)?)) + })?)) + } else { + Ok(Value::Null) + } + } + Type::NonNullNamed(ty) => match schema.types.get(ty).expect("type must exist") { + ExtendedType::Scalar(ty) => { + if ty.name == "Int" { + Ok(Value::from(self.arbitrary::()?)) + } else if ty.name == "Float" { + loop { + // not ideal, but graphql requires finite values is not a valid value. + let val = self.arbitrary::()?; + if val.is_finite() { + return Ok(Value::from(val)); + } + } + } else if ty.name == "Boolean" { + Ok(Value::Boolean(self.arbitrary()?)) + } else if ty.name == "String" { + Ok(Value::String(self.arbitrary_node_str()?)) + } else { + self.arbitrary_value_untyped() + } + } + ExtendedType::Object(ty) => { + let mut values = Vec::new(); + for (name, definition) in &ty.fields { + values.push(( + name.clone(), + Node::new(self.arbitrary_value(schema, &definition.ty)?), + )); + } + Ok(Value::Object(values)) + } + ExtendedType::Enum(ty) => { + let values = ty + .values + .iter() + .map(|(_name, v)| &v.value) + .collect::>(); + if values.is_empty() { + panic!("enum must have at least one value") + } else { + Ok(Value::Enum((*self.choose(&values)?).clone())) + } + } + ExtendedType::InputObject(ty) => { + let mut values = Vec::new(); + for (name, definition) in &ty.fields { + values.push(( + name.clone(), + Node::new(self.arbitrary_value(schema, &definition.ty)?), + )); + } + Ok(Value::Object(values)) + } + _ => { + panic!("type must be a scalar, object, enum or input object") + } + }, + Type::NonNullList(ty) => { + Ok(Value::List(self.arbitrary_vec(0, 5, |u| { + Ok(Node::new(u.arbitrary_value(schema, ty)?)) + })?)) + } + } + } + + pub(crate) fn arbitrary_operation_definition( + &mut self, + schema: &Schema, + executable_document: &ExecutableDocument, + name: Option, + ) -> Result { + let object = schema.random_query_mutation_subscription(self)?; + let directive_location = match object.name.as_ref() { + "Query" => DirectiveLocation::Query, + "Mutation" => DirectiveLocation::Mutation, + "Subscription" => DirectiveLocation::Subscription, + _ => panic!("invalid object name"), + }; + Ok(OperationDefinition { + operation_type: object + .deref() + .operation_type() + .expect("top level operation must have type"), + name, + variables: vec![], + directives: schema + .sample_directives(self)? + .into_iter() + .with_location(directive_location) + .try_collect(self, schema)?, + selection_set: self.arbitrary_vec(1, 20, |u| { + Ok(u.arbitrary_selection(schema, object.deref(), executable_document)?) + })?, + }) + } + + fn arbitrary_variable_definition(&mut self, schema: &Schema) -> Result { + let ty = schema + .random_type(self, vec![ExtendedTypeKind::InputObjectTypeDefinition])? + .ty(self)?; + Ok(VariableDefinition { + name: self.unique_name(), + default_value: self + .arbitrary_optional(|u| Ok(Node::new(u.arbitrary_value(schema, &ty)?)))?, + ty: Node::new(ty), + directives: schema + .sample_directives(self)? + .into_iter() + .with_location(DirectiveLocation::ArgumentDefinition) + .try_collect(self, schema)?, + }) + } + + pub(crate) fn arbitrary_fragment_definition( + &mut self, + schema: &Schema, + executable_document: &ExecutableDocument, + ) -> Result { + let type_condition = schema.random_type( + self, + vec![ + ExtendedTypeKind::Object, + ExtendedTypeKind::Interface, + ExtendedTypeKind::Union, + ], + )?; + Ok(FragmentDefinition { + name: self.unique_name(), + type_condition: type_condition.name().clone(), + directives: schema + .sample_directives(self)? + .into_iter() + .with_location(DirectiveLocation::FragmentDefinition) + .try_collect(self, schema)?, + selection_set: self.arbitrary_vec(1, 5, |u| { + Ok(u.arbitrary_selection(schema, type_condition, executable_document)?) + })?, + }) + } + + pub(crate) fn arbitrary_inline_fragment<'a>( + &mut self, + schema: &Schema, + selectable: &impl Selectable, + executable_document: &ExecutableDocument, + ) -> Result { + Ok(InlineFragment { + type_condition: self.arbitrary_optional(|_| Ok(selectable.name().clone()))?, + directives: schema + .sample_directives(self)? + .into_iter() + .with_location(DirectiveLocation::InlineFragment) + .try_collect(self, schema)?, + selection_set: self.arbitrary_vec(1, 5, |u| { + Ok(u.arbitrary_selection(schema, selectable, executable_document)?) + })?, + }) + } + + fn arbitrary_selection<'a>( + &mut self, + schema: &Schema, + selectable: &impl Selectable, + executable_document: &ExecutableDocument, + ) -> Result { + // The selection must contain at least one field, fragment spread or inline fragment + // If the type is a union then it must contain at least one inline fragment + + let selection_type = *self.choose(&vec![ + SelectionType::Field, + SelectionType::InlineFragment, + SelectionType::FragmentSpread, + ])?; + + if selection_type == SelectionType::FragmentSpread { + if let Some(fragment) = executable_document.random_fragment_of_type(self, selectable)? { + return Ok(Selection::FragmentSpread(Node::new(FragmentSpread { + fragment_name: fragment.name.clone(), + directives: schema + .sample_directives(self)? + .into_iter() + .with_location(DirectiveLocation::Field) + .try_collect(self, schema)?, + }))); + } + } + if selection_type == SelectionType::InlineFragment { + if let Some(specialization) = selectable.random_specialization(self, schema)? { + return Ok(Selection::InlineFragment(Node::new( + self.arbitrary_inline_fragment(schema, specialization, executable_document)?, + ))); + } + } + + let field = selectable.random_field(self)?; + let field_ty = schema + .types + .get(field.ty.inner_named_type()) + .expect("type must exist"); + let selection_set = if field_ty.is_scalar() { + vec![] + } else { + self.arbitrary_vec(1, 5, |u| { + Ok(u.arbitrary_selection(schema, field_ty, executable_document)?) + })? + }; + Ok(Selection::Field(Node::new(Field { + alias: self.arbitrary_optional(|u| Ok(u.unique_name()))?, + name: field.name.clone(), + arguments: self.arbitrary_arguments(schema, field)?, + directives: schema + .sample_directives(self)? + .into_iter() + .with_location(DirectiveLocation::Field) + .try_collect(self, schema)?, + selection_set, + }))) + } + fn arbitrary_arguments( + &mut self, + schema: &Schema, + field_definition: &FieldDefinition, + ) -> Result>> { + let mut args = Vec::new(); + for arg in &field_definition.arguments { + args.push(Node::new(Argument { + name: arg.name.clone(), + value: Node::new(self.arbitrary_value(schema, &field_definition.ty)?), + })); + } + Ok(args) + } + + fn all_transitive_interfaces<'a>( + schema: &'a Schema, + interfaces: Vec<&'a Node>, + ) -> Vec<&'a Node> { + // In graphql interfaces can extend other interfaces, but when using them you need to specify every single one in the entire type hierarchy. + interfaces + .into_iter() + .flat_map(|interface| { + std::iter::once(&interface.name).chain( + interface + .implements_interfaces + .iter() + .map(|component| &component.name), + ) + }) + .collect::>() + .into_iter() + .filter_map(|interface| schema.get_interface(interface)) + .collect() + } +} + +#[derive(Copy, Clone, Eq, PartialEq)] +enum SelectionType { + Field, + FragmentSpread, + InlineFragment, +} + +impl<'a> Deref for Unstructured<'a> { + type Target = arbitrary::Unstructured<'a>; + + fn deref(&self) -> &Self::Target { + &self.u + } +} + +impl<'a> DerefMut for Unstructured<'a> { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.u + } +} diff --git a/fuzz/Cargo.toml b/fuzz/Cargo.toml index 945004450..9c9b49a4a 100644 --- a/fuzz/Cargo.toml +++ b/fuzz/Cargo.toml @@ -58,3 +58,9 @@ name = "coordinate" path = "fuzz_targets/coordinate.rs" test = false doc = false + +[[bin]] +name = "parser_next" +path = "fuzz_targets/parser_next.rs" +test = false +doc = false diff --git a/fuzz/fuzz_targets/parser_next.rs b/fuzz/fuzz_targets/parser_next.rs new file mode 100644 index 000000000..33795668e --- /dev/null +++ b/fuzz/fuzz_targets/parser_next.rs @@ -0,0 +1,89 @@ +#![no_main] + +use std::panic; + +use libfuzzer_sys::{arbitrary, fuzz_target}; + +use apollo_smith::next::Error; + +fuzz_target!(|data: &[u8]| { + let _ = env_logger::try_init(); + + if let Err(e) = apollo_rs_fuzz::generate_schema_document(data) { + match &e { + Error::Arbitrary(arbitrary::Error::NotEnoughData) => { + return; + } + Error::Arbitrary(e) => { + println!("arbitrary error: {}", e); + } + Error::Parse(doc) => { + println!("{}\ndoc:\n{}\nerrors:\n{}", e, doc.to_string(), doc.errors); + } + Error::SchemaExpectedValidationFail { doc, mutation } => { + println!("{}\nmutation:\n{}\ndoc:\n{}", e, mutation, doc.to_string()); + } + Error::SerializationInconsistency { original, new } => { + println!( + "{}\noriginal:\n{}\nnew:\n{}", + e, + original.to_string(), + new.to_string() + ); + } + Error::SchemaDocumentValidation {mutation, doc, errors } => { + println!( + "{}\nmutation:\n{}\ndoc:\n{}\nerrors:\n{}", + e, + mutation, + doc.to_string(), + errors.errors + ); + } + Error::SchemaReparse { mutation, doc, errors } => { + println!( + "{}\nmutation:\n{}\ndoc:\n{}\nerrors:\n{}", + e, + mutation, + doc.to_string(), + errors.errors + ); + } + + Error::ExecutableReparse { + mutation, + schema, + doc, + errors, + } => { + println!( + "{}\nmutation:\n{}\ndoc:\n{}\nschema:\n{}\nerrors:\n{}", + e, + mutation, + doc.to_string(), + schema.to_string(), + errors.errors + ); + } + Error::ExecutableDocumentValidation { + mutation, + doc, + schema, + errors, + } => { + println!( + "{}\nmutation:\n{}\ndoc\n{}\nschema:\n{}\nerrors:\n{}", + e, + mutation, + doc.to_string(), + schema.to_string(), + errors.errors + ); + } + Error::ExecutableExpectedValidationFail { schema, doc, mutation } => { + println!("{}\nmutation:\n{}\ndoc:\n{}\nschema\n{}", e, mutation, doc.to_string(), schema.to_string()); + } + } + panic!("error detected: {}", e); + } +}); diff --git a/fuzz/src/lib.rs b/fuzz/src/lib.rs index 547643ed8..32e20db5f 100644 --- a/fuzz/src/lib.rs +++ b/fuzz/src/lib.rs @@ -1,7 +1,8 @@ use apollo_smith::DocumentBuilder; + use libfuzzer_sys::arbitrary::{Result, Unstructured}; -/// This generate an arbitrary valid GraphQL document +/// This generates an arbitrary valid GraphQL document pub fn generate_valid_document(input: &[u8]) -> Result { drop(env_logger::try_init()); @@ -12,6 +13,17 @@ pub fn generate_valid_document(input: &[u8]) -> Result { Ok(document.into()) } +pub fn generate_schema_document( + input: &[u8], +) -> std::result::Result<(), apollo_smith::next::Error> { + drop(env_logger::try_init()); + + apollo_smith::next::generate_schema_document(&mut apollo_smith::next::Unstructured::new( + input, + ))?; + Ok(()) +} + /// Log the error and the document generated for these errors /// Save it into files pub fn log_gql_doc(gql_doc: &str, errors: &str) {