Repository: apollographql/apollo-rs
Pull request #866 (2024-06-12 10:20:38)

apollo-smith: more varied fields generation
This changes the field generation algorithm in apollo-smith to allow more variety, because the previous implementation was too biased towards the first field specified in a type.
This also adds an example to generate a random query from a schema.

One thing that comes out of these tests is that we need the input data for Arbitrary to be large enough, otherwise once it has no more bytes to consume, it returns 0 every time and the selections get no more variations

lrlna: This does make the snapshots fail, and I think that failure is legit. We should have _some_ fields in the operation's selection set and the fragment's selection set
Geal: you're right, I messed up the lower bound of the range. I set it to at least 1 selection instead of 2 and updated the snapshots. We do not lose much by doing that

Patch:
diff --git a/crates/apollo-smith/Cargo.toml b/crates/apollo-smith/Cargo.toml
index bd72cc9..71f2153 100644
--- a/crates/apollo-smith/Cargo.toml
+++ b/crates/apollo-smith/Cargo.toml
@@ -31,3 +31,4 @@ thiserror = "1.0.37"
 
 [dev-dependencies]
 expect-test = "1.4"
+rand = "0.8.5"
diff --git a/crates/apollo-smith/examples/generate.rs b/crates/apollo-smith/examples/generate.rs
new file mode 100644
index 0000000..b9279c0
--- /dev/null
+++ b/crates/apollo-smith/examples/generate.rs
@@ -0,0 +1,62 @@
+use std::fs;
+
+use apollo_parser::Parser;
+use apollo_smith::Document;
+use apollo_smith::DocumentBuilder;
+use arbitrary::Result;
+use arbitrary::Unstructured;
+use rand::distributions::Alphanumeric;
+use rand::rngs::StdRng;
+use rand::SeedableRng;
+use rand::{thread_rng, Rng};
+
+/// This generate an arbitrary valid GraphQL operation
+pub fn generate_valid_operation(schema_path: &str, seed_arg: Option<String>) -> Result<String> {
+    let contents = fs::read_to_string(schema_path).expect("cannot read file");
+    let parser = Parser::new(&contents);
+
+    let tree = parser.parse();
+    if tree.errors().len() > 0 {
+        let errors = tree
+            .errors()
+            .map(|err| err.message())
+            .collect::<Vec<&str>>()
+            .join("\n");
+        panic!("cannot parse the supergraph:\n{errors}");
+    }
+
+    let seed: u64 = match seed_arg {
+        Some(s) => s.parse().unwrap(),
+        None => thread_rng().gen(),
+    };
+
+    println!("generating from seed: {seed}");
+
+    let rng: StdRng = SeedableRng::seed_from_u64(seed);
+    let data: String = rng
+        .sample_iter(&Alphanumeric)
+        .take(65536)
+        .map(char::from)
+        .collect();
+
+    let mut u = Unstructured::new(data.as_bytes());
+    let mut gql_doc = DocumentBuilder::with_document(
+        &mut u,
+        Document::try_from(tree.document()).expect("tree should not have errors"),
+    )?;
+    let operation_def: String = gql_doc.operation_definition()?.unwrap().into();
+
+    Ok(operation_def)
+}
+
+fn main() -> Result<(), Box<dyn std::error::Error>> {
+    let mut args = std::env::args().skip(1);
+    let Some(schema_path) = args.next() else {
+        return Err("Provide a schema path".into());
+    };
+    let seed = args.next();
+
+    let operation_def = generate_valid_operation(&schema_path, seed)?;
+    println!("operation definition:\n{operation_def}");
+    Ok(())
+}
diff --git a/crates/apollo-smith/examples/schema.graphql b/crates/apollo-smith/examples/schema.graphql
new file mode 100644
index 0000000..893d94d
--- /dev/null
+++ b/crates/apollo-smith/examples/schema.graphql
@@ -0,0 +1,58 @@
+schema {
+  query: Query
+}
+
+
+type Query {
+  dog: Dog
+  cat: Cat
+  alien: Alien
+}
+
+enum DogCommand {
+  SIT
+  DOWN
+  HEEL
+}
+
+type Dog implements Pet {
+  name: String!
+  nickname: String
+  barkVolume: Int
+  doesKnowCommand(dogCommand: DogCommand!): Boolean!
+  isHouseTrained(atOtherHomes: Boolean): Boolean!
+  owner: Human
+}
+
+interface Sentient {
+  name: String!
+}
+
+interface Pet {
+  name: String!
+}
+
+type Alien implements Sentient {
+  name: String!
+  homePlanet: String
+}
+
+type Human implements Sentient {
+  name: String!
+  pets: [Pet!]
+}
+
+enum CatCommand {
+  JUMP
+}
+
+type Cat implements Pet {
+  name: String!
+  nickname: String
+  doesKnowCommand(catCommand: CatCommand!): Boolean!
+  meowVolume: Int
+}
+
+union CatOrDog = Cat | Dog
+union DogOrHuman = Dog | Human
+union HumanOrAlien = Human | Alien
diff --git a/crates/apollo-smith/src/selection_set.rs b/crates/apollo-smith/src/selection_set.rs
index 06d17ee..b41d376 100644
--- a/crates/apollo-smith/src/selection_set.rs
+++ b/crates/apollo-smith/src/selection_set.rs
@@ -87,12 +87,13 @@ impl<'a> DocumentBuilder<'a> {
     /// Create an arbitrary `SelectionSet`
     pub fn selection_set(&mut self) -> ArbitraryResult<SelectionSet> {
         let mut exclude_names = Vec::new();
-        let selection_nb = std::cmp::max(
-            self.stack.last().map(|o| o.fields_def().len()).unwrap_or(7),
-            3,
-        );
-        let selections = (0..self.u.int_in_range(2..=selection_nb)?)
-            .map(|index| self.selection(index, &mut exclude_names)) // TODO do not generate duplication variable name
+        let selection_nb = self.stack.last().map(|o| o.fields_def().len()).unwrap_or(0);
+
+        let selections = (0..self.u.int_in_range(1..=5)?)
+            .map(|_| {
+                let index = self.u.int_in_range(0..=selection_nb)?;
+                self.selection(index, &mut exclude_names)
+            }) // TODO do not generate duplication variable name
             .collect::<ArbitraryResult<Vec<_>>>()?;
         Ok(SelectionSet { selections })
     }


Test patch:
diff --git a/crates/apollo-smith/src/snapshot_tests.rs b/crates/apollo-smith/src/snapshot_tests.rs
index 71e19c6..3f948b6 100644
--- a/crates/apollo-smith/src/snapshot_tests.rs
+++ b/crates/apollo-smith/src/snapshot_tests.rs
@@ -15,12 +15,10 @@ fn snapshot_tests() {
     expect![[r#"
         {
           A0
-          A0
         }
 
         fragment A2 on A1 {
           A0
-          A0
         }
 
         schema {
@@ -59,12 +57,10 @@ fn snapshot_tests() {
     expect![[r#"
         {
           A0
-          A0
         }
 
         fragment A2 on A1 {
           A0
-          A0
         }
 
         schema {
@@ -103,12 +99,10 @@ fn snapshot_tests() {
     expect![[r#"
         {
           A0
-          A0
         }
 
         fragment A21 on A20 {
           A0
-          A0
         }
 
         schema {
@@ -242,12 +236,10 @@ fn snapshot_tests() {
     expect![[r#"
         {
           A0
-          A0
         }
 
         fragment A21 on A20 {
           A0
-          A0
         }
 
         schema {