From 6af43b41cce5d56a77ff5e1bafec8b025aa86c92 Mon Sep 17 00:00:00 2001 From: andimarek Date: Wed, 5 Jul 2017 04:25:37 +0200 Subject: [PATCH] A start to #324 that provides a promise interface to executing querie --- src/main/java/graphql/GraphQL.java | 43 +++---- .../java/graphql/execution/Execution.java | 15 ++- .../graphql/execution/ExecutionStrategy.java | 97 ++++++++------- .../ExecutorServiceExecutionStrategy.java | 7 +- .../execution/SimpleExecutionStrategy.java | 9 +- .../batched/BatchedExecutionStrategy.java | 6 +- src/test/groovy/graphql/GraphQLTest.groovy | 104 ++-------------- .../groovy/graphql/NonNullHandlingTest.groovy | 116 ++++++++++++++++++ ...etchThenCompleteExecutionTestStrategy.java | 8 +- .../BreadthFirstExecutionTestStrategy.java | 7 +- .../graphql/execution/ExecutionIdTest.groovy | 4 +- .../execution/ExecutionStrategyTest.groovy | 26 ++-- .../graphql/execution/ExecutionTest.groovy | 49 +++++--- 13 files changed, 278 insertions(+), 213 deletions(-) create mode 100644 src/test/groovy/graphql/NonNullHandlingTest.groovy diff --git a/src/main/java/graphql/GraphQL.java b/src/main/java/graphql/GraphQL.java index d85e72475f..eb8ae87ee9 100644 --- a/src/main/java/graphql/GraphQL.java +++ b/src/main/java/graphql/GraphQL.java @@ -26,6 +26,7 @@ import java.util.List; import java.util.Map; +import java.util.concurrent.CompletableFuture; import static graphql.Assert.assertNotNull; @@ -49,7 +50,6 @@ public class GraphQL { * A GraphQL object ready to execute queries * * @param graphQLSchema the schema to use - * * @deprecated use the {@link #newGraphQL(GraphQLSchema)} builder instead. This will be removed in a future version. */ @Internal @@ -63,7 +63,6 @@ public GraphQL(GraphQLSchema graphQLSchema) { * * @param graphQLSchema the schema to use * @param queryStrategy the query execution strategy to use - * * @deprecated use the {@link #newGraphQL(GraphQLSchema)} builder instead. This will be removed in a future version. */ @Internal @@ -78,7 +77,6 @@ public GraphQL(GraphQLSchema graphQLSchema, ExecutionStrategy queryStrategy) { * @param graphQLSchema the schema to use * @param queryStrategy the query execution strategy to use * @param mutationStrategy the mutation execution strategy to use - * * @deprecated use the {@link #newGraphQL(GraphQLSchema)} builder instead. This will be removed in a future version. */ @Internal @@ -93,7 +91,6 @@ public GraphQL(GraphQLSchema graphQLSchema, ExecutionStrategy queryStrategy, Exe * @param queryStrategy the query execution strategy to use * @param mutationStrategy the mutation execution strategy to use * @param subscriptionStrategy the subscription execution strategy to use - * * @deprecated use the {@link #newGraphQL(GraphQLSchema)} builder instead. This will be removed in a future version. */ @Internal @@ -115,7 +112,6 @@ private GraphQL(GraphQLSchema graphQLSchema, ExecutionStrategy queryStrategy, Ex * Helps you build a GraphQL object ready to execute queries * * @param graphQLSchema the schema to use - * * @return a builder of GraphQL objects */ public static Builder newGraphQL(GraphQLSchema graphQLSchema) { @@ -183,9 +179,7 @@ public GraphQL build() { /** * @param query the query/mutation/subscription - * * @return result including errors - * * @deprecated Use {@link #execute(ExecutionInput)} */ @Deprecated @@ -201,9 +195,7 @@ public ExecutionResult execute(String query) { * * @param query the query/mutation/subscription * @param context custom object provided to each {@link graphql.schema.DataFetcher} - * * @return result including errors - * * @deprecated Use {@link #execute(ExecutionInput)} */ @Deprecated @@ -222,9 +214,7 @@ public ExecutionResult execute(String query, Object context) { * @param query the query/mutation/subscription * @param operationName the name of the operation to execute * @param context custom object provided to each {@link graphql.schema.DataFetcher} - * * @return result including errors - * * @deprecated Use {@link #execute(ExecutionInput)} */ @Deprecated @@ -244,9 +234,7 @@ public ExecutionResult execute(String query, String operationName, Object contex * @param query the query/mutation/subscription * @param context custom object provided to each {@link graphql.schema.DataFetcher} * @param variables variable values uses as argument - * * @return result including errors - * * @deprecated Use {@link #execute(ExecutionInput)} */ @Deprecated @@ -267,9 +255,7 @@ public ExecutionResult execute(String query, Object context, Map * @param operationName name of the operation to execute * @param context custom object provided to each {@link graphql.schema.DataFetcher} * @param variables variable values uses as argument - * * @return result including errors - * * @deprecated Use {@link #execute(ExecutionInput)} */ @Deprecated @@ -285,25 +271,34 @@ public ExecutionResult execute(String query, String operationName, Object contex } /** - * @param executionInput {@link ExecutionInput} + * This will return a promise (aka {@link CompletableFuture}) to provide a {@link ExecutionResult} + * which is the result of executing the provided query. * - * @return result including errors + * @param executionInput {@link ExecutionInput} + * @return a promise to an execution result */ - public ExecutionResult execute(ExecutionInput executionInput) { + public CompletableFuture executeAsync(ExecutionInput executionInput) { log.debug("Executing request. operation name: {}. query: {}. variables {} ", executionInput.getOperationName(), executionInput.getQuery(), executionInput.getVariables()); InstrumentationContext executionInstrumentation = instrumentation.beginExecution(new InstrumentationExecutionParameters(executionInput)); - final ExecutionResult executionResult = parseValidateAndExecute(executionInput); - executionInstrumentation.onEnd(executionResult); - + final CompletableFuture executionResult = parseValidateAndExecute(executionInput); + executionResult.thenAccept(executionInstrumentation::onEnd); return executionResult; } - private ExecutionResult parseValidateAndExecute(ExecutionInput executionInput) { + /** + * @param executionInput {@link ExecutionInput} + * @return result including errors + */ + public ExecutionResult execute(ExecutionInput executionInput) { + return executeAsync(executionInput).join(); + } + + private CompletableFuture parseValidateAndExecute(ExecutionInput executionInput) { PreparsedDocumentEntry preparsedDoc = preparsedDocumentProvider.get(executionInput.getQuery(), query -> parseAndValidate(executionInput)); if (preparsedDoc.hasErrors()) { - return new ExecutionResultImpl(preparsedDoc.getErrors()); + return CompletableFuture.completedFuture(new ExecutionResultImpl(preparsedDoc.getErrors())); } return execute(executionInput, preparsedDoc.getDocument()); @@ -351,7 +346,7 @@ private List validate(ExecutionInput executionInput, Document d return validationErrors; } - private ExecutionResult execute(ExecutionInput executionInput, Document document) { + private CompletableFuture execute(ExecutionInput executionInput, Document document) { String query = executionInput.getQuery(); String operationName = executionInput.getOperationName(); Object context = executionInput.getContext(); diff --git a/src/main/java/graphql/execution/Execution.java b/src/main/java/graphql/execution/Execution.java index 1eebfe51d2..a4ea7f8606 100644 --- a/src/main/java/graphql/execution/Execution.java +++ b/src/main/java/graphql/execution/Execution.java @@ -18,6 +18,7 @@ import java.util.Collections; import java.util.List; import java.util.Map; +import java.util.concurrent.CompletableFuture; import static graphql.Assert.assertShouldNeverHappen; import static graphql.execution.ExecutionStrategyParameters.newParameters; @@ -25,6 +26,7 @@ import static graphql.language.OperationDefinition.Operation.MUTATION; import static graphql.language.OperationDefinition.Operation.QUERY; import static graphql.language.OperationDefinition.Operation.SUBSCRIPTION; +import static java.util.concurrent.CompletableFuture.completedFuture; @Internal public class Execution { @@ -42,7 +44,7 @@ public Execution(ExecutionStrategy queryStrategy, ExecutionStrategy mutationStra this.instrumentation = instrumentation; } - public ExecutionResult execute(Document document, GraphQLSchema graphQLSchema, ExecutionId executionId, ExecutionInput executionInput) { + public CompletableFuture execute(Document document, GraphQLSchema graphQLSchema, ExecutionId executionId, ExecutionInput executionInput) { ExecutionContext executionContext = new ExecutionContextBuilder() .valuesResolver(new ValuesResolver()) @@ -61,7 +63,7 @@ public ExecutionResult execute(Document document, GraphQLSchema graphQLSchema, E return executeOperation(executionContext, executionInput.getRoot(), executionContext.getOperationDefinition()); } - private ExecutionResult executeOperation(ExecutionContext executionContext, Object root, OperationDefinition operationDefinition) { + private CompletableFuture executeOperation(ExecutionContext executionContext, Object root, OperationDefinition operationDefinition) { InstrumentationContext dataFetchCtx = instrumentation.beginDataFetch(new InstrumentationDataFetchParameters(executionContext)); @@ -74,7 +76,7 @@ private ExecutionResult executeOperation(ExecutionContext executionContext, Obje // for the record earlier code has asserted that we have a query type in the schema since the spec says this is // ALWAYS required if (operation == MUTATION && operationRootType == null) { - return new ExecutionResultImpl(Collections.singletonList(new MutationNotSupportedError())); + return completedFuture(new ExecutionResultImpl(Collections.singletonList(new MutationNotSupportedError()))); } FieldCollectorParameters collectorParameters = FieldCollectorParameters.newParameters() @@ -97,7 +99,7 @@ private ExecutionResult executeOperation(ExecutionContext executionContext, Obje .path(ExecutionPath.rootPath()) .build(); - ExecutionResult result; + CompletableFuture result; try { if (operation == OperationDefinition.Operation.MUTATION) { result = mutationStrategy.execute(executionContext, parameters); @@ -114,10 +116,11 @@ private ExecutionResult executeOperation(ExecutionContext executionContext, Obje // // http://facebook.github.io/graphql/#sec-Errors-and-Non-Nullability // - result = new ExecutionResultImpl(null, executionContext.getErrors()); + result = completedFuture(new ExecutionResultImpl(null, executionContext.getErrors())); } - dataFetchCtx.onEnd(result); + result.thenAccept(dataFetchCtx::onEnd); + return result; } diff --git a/src/main/java/graphql/execution/ExecutionStrategy.java b/src/main/java/graphql/execution/ExecutionStrategy.java index f8b186a324..59ce3f39e6 100644 --- a/src/main/java/graphql/execution/ExecutionStrategy.java +++ b/src/main/java/graphql/execution/ExecutionStrategy.java @@ -35,6 +35,7 @@ import java.util.List; import java.util.Map; import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CompletionException; import static graphql.execution.FieldCollectorParameters.newParameters; import static graphql.execution.TypeInfo.newTypeInfo; @@ -42,10 +43,11 @@ import static graphql.introspection.Introspection.TypeMetaFieldDef; import static graphql.introspection.Introspection.TypeNameMetaFieldDef; import static graphql.schema.DataFetchingEnvironmentBuilder.newDataFetchingEnvironment; +import static java.util.concurrent.CompletableFuture.completedFuture; /** * An execution strategy is give a list of fields from the graphql query to execute and find values for using a recursive strategy. - * + *

*

  *     query {
  *          friends {
@@ -67,7 +69,7 @@
  *     }
  *
  * 
- * + *

* Given the graphql query above, an execution strategy will be called for the top level fields 'friends' and 'enemies' and it will be asked to find an object * to describe them. Because they are both complex object types, it needs to descend down that query and start fetching and completing * fields such as 'id','name' and other complex fields such as 'friends' and 'allies', by recursively calling to itself to execute these lower @@ -136,12 +138,10 @@ protected ExecutionStrategy(DataFetcherExceptionHandler dataFetcherExceptionHand * * @param executionContext contains the top level execution parameters * @param parameters contains the parameters holding the fields to be executed and source object - * * @return an {@link ExecutionResult} - * * @throws NonNullableFieldWasNullException if a non null field resolves to a null value */ - public abstract ExecutionResult execute(ExecutionContext executionContext, ExecutionStrategyParameters parameters) throws NonNullableFieldWasNullException; + public abstract CompletableFuture execute(ExecutionContext executionContext, ExecutionStrategyParameters parameters) throws NonNullableFieldWasNullException; /** * Handle exceptions which occur during data fetching. By default, add all exceptions to the execution context's @@ -153,7 +153,6 @@ protected ExecutionStrategy(DataFetcherExceptionHandler dataFetcherExceptionHand * @param argumentValues the map of arguments * @param path the logical path to the field in question * @param e the exception that occurred - * * @deprecated implement the new {@link DataFetcherExceptionHandler} interface and pass it in as a parameter to the strategy */ @SuppressWarnings({"unused", "DeprecatedIsStillUsed"}) @@ -174,21 +173,19 @@ protected void handleDataFetchingException( /** * Called to fetch a value for a field and resolve it further in terms of the graphql query. This will call * #fetchField followed by #completeField and the completed {@link ExecutionResult} is returned. - * + *

* An execution strategy can iterate the fields to be executed and call this method for each one - * + *

* Graphql fragments mean that for any give logical field can have one or more {@link Field} values associated with it * in the query, hence the fieldList. However the first entry is representative of the field for most purposes. * * @param executionContext contains the top level execution parameters * @param parameters contains the parameters holding the fields to be executed and source object * @param fieldList the instances of the AST {@link Field} to be fetched - * * @return an {@link ExecutionResult} - * * @throws NonNullableFieldWasNullException if a non null field resolves to a null value */ - protected ExecutionResult resolveField(ExecutionContext executionContext, ExecutionStrategyParameters parameters, List fieldList) { + protected CompletableFuture resolveField(ExecutionContext executionContext, ExecutionStrategyParameters parameters, List fieldList) { GraphQLFieldDefinition fieldDef = getFieldDef(executionContext, parameters, fieldList.get(0)); Instrumentation instrumentation = executionContext.getInstrumentation(); @@ -196,25 +193,24 @@ protected ExecutionResult resolveField(ExecutionContext executionContext, Execut Object fetchedValue = fetchField(executionContext, parameters, fieldList); - ExecutionResult result = completeField(executionContext, parameters, fieldList, fetchedValue); + CompletableFuture result = completeField(executionContext, parameters, fieldList, fetchedValue); + + result.thenAccept(fieldCtx::onEnd); - fieldCtx.onEnd(result); return result; } /** * Called to fetch a value for a field from the {@link DataFetcher} associated with the field * {@link GraphQLFieldDefinition}. - * + *

* Graphql fragments mean that for any give logical field can have one or more {@link Field} values associated with it * in the query, hence the fieldList. However the first entry is representative of the field for most purposes. * * @param executionContext contains the top level execution parameters * @param parameters contains the parameters holding the fields to be executed and source object * @param fieldList the instances of the AST {@link Field} to be fetched - * * @return an fetched object - * * @throws NonNullableFieldWasNullException if a non null field resolves to a null value */ protected Object fetchField(ExecutionContext executionContext, ExecutionStrategyParameters parameters, List fieldList) { @@ -266,10 +262,10 @@ protected Object fetchField(ExecutionContext executionContext, ExecutionStrategy /** * Called to complete a field based on the type of the field. - * + *

* If the field is a scalar type, then it will be coerced and returned. However if the field type is an complex object type, then * the execution strategy will be called recursively again to execute the fields of that type before returning. - * + *

* Graphql fragments mean that for any give logical field can have one or more {@link Field} values associated with it * in the query, hence the fieldList. However the first entry is representative of the field for most purposes. * @@ -277,12 +273,10 @@ protected Object fetchField(ExecutionContext executionContext, ExecutionStrategy * @param parameters contains the parameters holding the fields to be executed and source object * @param fieldList the instances of the AST {@link Field} to be fetched * @param fetchedValue the fetched raw value - * * @return an {@link ExecutionResult} - * * @throws NonNullableFieldWasNullException if a non null field resolves to a null value */ - protected ExecutionResult completeField(ExecutionContext executionContext, ExecutionStrategyParameters parameters, List fieldList, Object fetchedValue) { + protected CompletableFuture completeField(ExecutionContext executionContext, ExecutionStrategyParameters parameters, List fieldList, Object fetchedValue) { Field field = fieldList.get(0); GraphQLObjectType parentType = parameters.typeInfo().castType(GraphQLObjectType.class); GraphQLFieldDefinition fieldDef = getFieldDef(executionContext.getGraphQLSchema(), parentType, field); @@ -312,28 +306,26 @@ protected ExecutionResult completeField(ExecutionContext executionContext, Execu /** * Called to complete a value for a field based on the type of the field. - * + *

* If the field is a scalar type, then it will be coerced and returned. However if the field type is an complex object type, then * the execution strategy will be called recursively again to execute the fields of that type before returning. - * + *

* Graphql fragments mean that for any give logical field can have one or more {@link Field} values associated with it * in the query, hence the fieldList. However the first entry is representative of the field for most purposes. * * @param executionContext contains the top level execution parameters * @param parameters contains the parameters holding the fields to be executed and source object * @param fieldList the instances of the AST {@link Field} to be fetched - * * @return an {@link ExecutionResult} - * * @throws NonNullableFieldWasNullException if a non null field resolves to a null value */ - protected ExecutionResult completeValue(ExecutionContext executionContext, ExecutionStrategyParameters parameters, List fieldList) throws NonNullableFieldWasNullException { + protected CompletableFuture completeValue(ExecutionContext executionContext, ExecutionStrategyParameters parameters, List fieldList) throws NonNullableFieldWasNullException { TypeInfo typeInfo = parameters.typeInfo(); Object result = parameters.source(); GraphQLType fieldType = parameters.typeInfo().type(); if (result == null) { - return parameters.nonNullFieldValidator().validate(parameters.path(), null); + return completedFuture(parameters.nonNullFieldValidator().validate(parameters.path(), null)); } else if (fieldType instanceof GraphQLList) { return completeValueForList(executionContext, parameters, fieldList, toIterable(result)); } else if (fieldType instanceof GraphQLScalarType) { @@ -404,7 +396,6 @@ private Iterable toIterable(Object result) { * Called to resolve a {@link GraphQLInterfaceType} into a specific {@link GraphQLObjectType} so the object can be executed in terms of that type * * @param params the params needed for type resolution - * * @return a {@link GraphQLObjectType} */ protected GraphQLObjectType resolveTypeForInterface(TypeResolutionParameters params) { @@ -420,7 +411,6 @@ protected GraphQLObjectType resolveTypeForInterface(TypeResolutionParameters par * Called to resolve a {@link GraphQLUnionType} into a specific {@link GraphQLObjectType} so the object can be executed in terms of that type * * @param params the params needed for type resolution - * * @return a {@link GraphQLObjectType} */ protected GraphQLObjectType resolveTypeForUnion(TypeResolutionParameters params) { @@ -439,10 +429,9 @@ protected GraphQLObjectType resolveTypeForUnion(TypeResolutionParameters params) * @param parameters contains the parameters holding the fields to be executed and source object * @param enumType the type of the enum * @param result the result to be coerced - * * @return an {@link ExecutionResult} */ - protected ExecutionResult completeValueForEnum(ExecutionContext executionContext, ExecutionStrategyParameters parameters, GraphQLEnumType enumType, Object result) { + protected CompletableFuture completeValueForEnum(ExecutionContext executionContext, ExecutionStrategyParameters parameters, GraphQLEnumType enumType, Object result) { Object serialized; try { serialized = enumType.getCoercing().serialize(result); @@ -450,7 +439,7 @@ protected ExecutionResult completeValueForEnum(ExecutionContext executionContext serialized = handleCoercionProblem(executionContext, parameters, e); } serialized = parameters.nonNullFieldValidator().validate(parameters.path(), serialized); - return new ExecutionResultImpl(serialized, null); + return completedFuture(new ExecutionResultImpl(serialized, null)); } /** @@ -461,10 +450,9 @@ protected ExecutionResult completeValueForEnum(ExecutionContext executionContext * @param parameters contains the parameters holding the fields to be executed and source object * @param scalarType the type of the scalar * @param result the result to be coerced - * * @return an {@link ExecutionResult} */ - protected ExecutionResult completeValueForScalar(ExecutionContext executionContext, ExecutionStrategyParameters parameters, GraphQLScalarType scalarType, Object result) { + protected CompletableFuture completeValueForScalar(ExecutionContext executionContext, ExecutionStrategyParameters parameters, GraphQLScalarType scalarType, Object result) { Object serialized; try { serialized = scalarType.getCoercing().serialize(result); @@ -478,7 +466,7 @@ protected ExecutionResult completeValueForScalar(ExecutionContext executionConte serialized = null; } serialized = parameters.nonNullFieldValidator().validate(parameters.path(), serialized); - return new ExecutionResultImpl(serialized, null); + return completedFuture(new ExecutionResultImpl(serialized, null)); } private Object handleCoercionProblem(ExecutionContext context, ExecutionStrategyParameters parameters, CoercingSerializeException e) { @@ -496,11 +484,10 @@ private Object handleCoercionProblem(ExecutionContext context, ExecutionStrategy * @param parameters contains the parameters holding the fields to be executed and source object * @param fieldList the instances of the AST {@link Field} to be fetched * @param iterableValues the values to complete - * * @return an {@link ExecutionResult} */ - protected ExecutionResult completeValueForList(ExecutionContext executionContext, ExecutionStrategyParameters parameters, List fieldList, Iterable iterableValues) { - List completedResults = new ArrayList<>(); + protected CompletableFuture completeValueForList(ExecutionContext executionContext, ExecutionStrategyParameters parameters, List fieldList, Iterable iterableValues) { + List> completedValues = new ArrayList<>(); TypeInfo typeInfo = parameters.typeInfo(); GraphQLList fieldType = typeInfo.castType(GraphQLList.class); int idx = 0; @@ -518,11 +505,12 @@ protected ExecutionResult completeValueForList(ExecutionContext executionContext .path(indexedPath) .source(item).build(); - ExecutionResult completedValue = completeValue(executionContext, newParameters, fieldList); - completedResults.add(completedValue != null ? completedValue.getData() : null); + CompletableFuture completedValue = completeValue(executionContext, newParameters, fieldList); + completedValues.add(completedValue); idx++; } - return new ExecutionResultImpl(completedResults, null); + ExecutionResult executionResult = joinAllOf(completedValues); + return completedFuture(executionResult); } /** @@ -531,7 +519,6 @@ protected ExecutionResult completeValueForList(ExecutionContext executionContext * @param executionContext contains the top level execution parameters * @param parameters contains the parameters holding the fields to be executed and source object * @param field the field to find the definition of - * * @return a {@link GraphQLFieldDefinition} */ protected GraphQLFieldDefinition getFieldDef(ExecutionContext executionContext, ExecutionStrategyParameters parameters, Field field) { @@ -545,7 +532,6 @@ protected GraphQLFieldDefinition getFieldDef(ExecutionContext executionContext, * @param schema the schema in play * @param parentType the parent type of the field * @param field the field to find the definition of - * * @return a {@link GraphQLFieldDefinition} */ protected GraphQLFieldDefinition getFieldDef(GraphQLSchema schema, GraphQLObjectType parentType, Field field) { @@ -570,7 +556,7 @@ protected GraphQLFieldDefinition getFieldDef(GraphQLSchema schema, GraphQLObject /** * See (http://facebook.github.io/graphql/#sec-Errors-and-Non-Nullability), - * + *

* If a non nullable child field type actually resolves to a null value and the parent type is nullable * then the parent must in fact become null * so we use exceptions to indicate this special case. However if the parent is in fact a non nullable type @@ -579,7 +565,6 @@ protected GraphQLFieldDefinition getFieldDef(GraphQLSchema schema, GraphQLObject * * @param e this indicates that a null value was returned for a non null field, which needs to cause the parent field * to become null OR continue on as an exception - * * @throws NonNullableFieldWasNullException if a non null field resolves to a null value */ protected void assertNonNullFieldPrecondition(NonNullableFieldWasNullException e) throws NonNullableFieldWasNullException { @@ -588,4 +573,26 @@ protected void assertNonNullFieldPrecondition(NonNullableFieldWasNullException e throw e; } } + + /** + * This will join all of the promises of a result as one and return a execution result that + * is a list of all the promised values + * + * @param completableFutures the list of futures to wait for to complete + * @return a new execution result of all the values in the promises + * @throws CompletionException if anything bad happens while waiting + */ + protected ExecutionResult joinAllOf(List> completableFutures) throws CompletionException { + CompletableFuture[] stages = completableFutures.toArray(new CompletableFuture[completableFutures.size()]); + + CompletableFuture.allOf(stages).join(); + // they are all now complete (or an runtime exception has been thrown) + + List completedResults = new ArrayList<>(); + completableFutures.forEach(future -> { + ExecutionResult completedValue = future.getNow(null); + completedResults.add(completedValue != null ? completedValue.getData() : null); + }); + return new ExecutionResultImpl(completedResults, null); + } } diff --git a/src/main/java/graphql/execution/ExecutorServiceExecutionStrategy.java b/src/main/java/graphql/execution/ExecutorServiceExecutionStrategy.java index b768a1b226..7c1d4dd39a 100644 --- a/src/main/java/graphql/execution/ExecutorServiceExecutionStrategy.java +++ b/src/main/java/graphql/execution/ExecutorServiceExecutionStrategy.java @@ -10,6 +10,7 @@ import java.util.List; import java.util.Map; import java.util.concurrent.Callable; +import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutionException; import java.util.concurrent.ExecutorService; import java.util.concurrent.Future; @@ -44,7 +45,7 @@ public ExecutorServiceExecutionStrategy(ExecutorService executorService, DataFet @Override - public ExecutionResult execute(final ExecutionContext executionContext, final ExecutionStrategyParameters parameters) { + public CompletableFuture execute(final ExecutionContext executionContext, final ExecutionStrategyParameters parameters) { if (executorService == null) return new SimpleExecutionStrategy().execute(executionContext, parameters); @@ -56,7 +57,7 @@ public ExecutionResult execute(final ExecutionContext executionContext, final Ex ExecutionPath fieldPath = parameters.path().segment(fieldName); ExecutionStrategyParameters newParameters = parameters.transform(bldr -> bldr.path(fieldPath)); - Callable resolveField = () -> resolveField(executionContext, newParameters, fieldList); + Callable resolveField = () -> resolveField(executionContext, newParameters, fieldList).join(); futures.put(fieldName, executorService.submit(resolveField)); } try { @@ -66,7 +67,7 @@ public ExecutionResult execute(final ExecutionContext executionContext, final Ex results.put(fieldName, executionResult != null ? executionResult.getData() : null); } - return new ExecutionResultImpl(results, executionContext.getErrors()); + return CompletableFuture.completedFuture(new ExecutionResultImpl(results, executionContext.getErrors())); } catch (InterruptedException | ExecutionException e) { throw new GraphQLException(e); } diff --git a/src/main/java/graphql/execution/SimpleExecutionStrategy.java b/src/main/java/graphql/execution/SimpleExecutionStrategy.java index 7971bce9b7..5049d8069c 100644 --- a/src/main/java/graphql/execution/SimpleExecutionStrategy.java +++ b/src/main/java/graphql/execution/SimpleExecutionStrategy.java @@ -7,6 +7,9 @@ import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import java.util.concurrent.CompletableFuture; + +import static java.util.concurrent.CompletableFuture.completedFuture; /** * The standard graphql execution strategy that runs fields in serial order @@ -30,7 +33,7 @@ public SimpleExecutionStrategy(DataFetcherExceptionHandler exceptionHandler) { } @Override - public ExecutionResult execute(ExecutionContext executionContext, ExecutionStrategyParameters parameters) throws NonNullableFieldWasNullException { + public CompletableFuture execute(ExecutionContext executionContext, ExecutionStrategyParameters parameters) throws NonNullableFieldWasNullException { Map> fields = parameters.fields(); Map results = new LinkedHashMap<>(); for (String fieldName : fields.keySet()) { @@ -40,7 +43,7 @@ public ExecutionResult execute(ExecutionContext executionContext, ExecutionStrat ExecutionStrategyParameters newParameters = parameters.transform(builder -> builder.path(fieldPath)); try { - ExecutionResult resolvedResult = resolveField(executionContext, newParameters, fieldList); + ExecutionResult resolvedResult = resolveField(executionContext, newParameters, fieldList).join(); results.put(fieldName, resolvedResult != null ? resolvedResult.getData() : null); } catch (NonNullableFieldWasNullException e) { @@ -49,6 +52,6 @@ public ExecutionResult execute(ExecutionContext executionContext, ExecutionStrat break; } } - return new ExecutionResultImpl(results, executionContext.getErrors()); + return completedFuture(new ExecutionResultImpl(results, executionContext.getErrors())); } } diff --git a/src/main/java/graphql/execution/batched/BatchedExecutionStrategy.java b/src/main/java/graphql/execution/batched/BatchedExecutionStrategy.java index 7a1cf3ecf1..e02bc415b9 100644 --- a/src/main/java/graphql/execution/batched/BatchedExecutionStrategy.java +++ b/src/main/java/graphql/execution/batched/BatchedExecutionStrategy.java @@ -37,10 +37,12 @@ import java.util.List; import java.util.Map; import java.util.Queue; +import java.util.concurrent.CompletableFuture; import static graphql.execution.FieldCollectorParameters.newParameters; import static graphql.schema.DataFetchingEnvironmentBuilder.newDataFetchingEnvironment; import static java.util.Collections.singletonList; +import static java.util.concurrent.CompletableFuture.completedFuture; /** * Execution Strategy that minimizes calls to the data fetcher when used in conjunction with {@link DataFetcher}s that have @@ -67,11 +69,11 @@ public BatchedExecutionStrategy(DataFetcherExceptionHandler dataFetcherException } @Override - public ExecutionResult execute(ExecutionContext executionContext, ExecutionStrategyParameters parameters) { + public CompletableFuture execute(ExecutionContext executionContext, ExecutionStrategyParameters parameters) { GraphQLExecutionNodeDatum data = new GraphQLExecutionNodeDatum(new LinkedHashMap<>(), parameters.source()); GraphQLObjectType type = parameters.typeInfo().castType(GraphQLObjectType.class); GraphQLExecutionNode root = new GraphQLExecutionNode(type, parameters.fields(), singletonList(data)); - return execute(executionContext, parameters, root); + return completedFuture(execute(executionContext, parameters, root)); } private ExecutionResult execute(ExecutionContext executionContext, ExecutionStrategyParameters parameters, GraphQLExecutionNode root) { diff --git a/src/test/groovy/graphql/GraphQLTest.groovy b/src/test/groovy/graphql/GraphQLTest.groovy index 551019bb98..9ade6df0f2 100644 --- a/src/test/groovy/graphql/GraphQLTest.groovy +++ b/src/test/groovy/graphql/GraphQLTest.groovy @@ -1,7 +1,14 @@ package graphql import graphql.language.SourceLocation -import graphql.schema.* +import graphql.schema.DataFetcher +import graphql.schema.DataFetchingEnvironment +import graphql.schema.GraphQLFieldDefinition +import graphql.schema.GraphQLList +import graphql.schema.GraphQLNonNull +import graphql.schema.GraphQLObjectType +import graphql.schema.GraphQLSchema +import graphql.schema.StaticDataFetcher import graphql.validation.ValidationErrorType import spock.lang.Specification @@ -11,7 +18,6 @@ import static graphql.schema.GraphQLArgument.newArgument import static graphql.schema.GraphQLFieldDefinition.newFieldDefinition import static graphql.schema.GraphQLInputObjectField.newInputObjectField import static graphql.schema.GraphQLInputObjectType.newInputObject -import static graphql.schema.GraphQLNonNull.nonNull import static graphql.schema.GraphQLObjectType.newObject import static graphql.schema.GraphQLSchema.newSchema @@ -239,100 +245,6 @@ class GraphQLTest extends Specification { } - class ParentTypeImplementation { - String nullChild = null - String nonNullChild = "not null" - } - - def "#268 - null child field values are allowed in nullable parent type"() { - - // see https://github.com/graphql-java/graphql-java/issues/268 - - given: - - - GraphQLOutputType parentType = newObject() - .name("currentType") - .field(newFieldDefinition().name("nullChild") - .type(nonNull(GraphQLString))) - .field(newFieldDefinition().name("nonNullChild") - .type(nonNull(GraphQLString))) - .build() - - GraphQLSchema schema = newSchema().query( - newObject() - .name("RootQueryType") - .field(newFieldDefinition() - .name("parent") - .type(parentType) // nullable parent - .dataFetcher({ env -> new ParentTypeImplementation() }) - - )) - .build() - - def query = """ - query { - parent { - nonNullChild - nullChild - } - } - """ - - when: - def result = GraphQL.newGraphQL(schema).build().execute(query) - - then: - - result.errors.size() == 1 - result.data["parent"] == null - } - - def "#268 - null child field values are NOT allowed in non nullable parent types"() { - - // see https://github.com/graphql-java/graphql-java/issues/268 - - given: - - - GraphQLOutputType parentType = newObject() - .name("currentType") - .field(newFieldDefinition().name("nullChild") - .type(nonNull(GraphQLString))) - .field(newFieldDefinition().name("nonNullChild") - .type(nonNull(GraphQLString))) - .build() - - GraphQLSchema schema = newSchema().query( - newObject() - .name("RootQueryType") - .field( - newFieldDefinition() - .name("parent") - .type(nonNull(parentType)) // non nullable parent - .dataFetcher({ env -> new ParentTypeImplementation() }) - - )) - .build() - - def query = """ - query { - parent { - nonNullChild - nullChild - } - } - """ - - when: - def result = GraphQL.newGraphQL(schema).build().execute(query) - - then: - - result.errors.size() == 1 - result.data == null - } - def "query with int literal too large"() { given: diff --git a/src/test/groovy/graphql/NonNullHandlingTest.groovy b/src/test/groovy/graphql/NonNullHandlingTest.groovy new file mode 100644 index 0000000000..841a238e66 --- /dev/null +++ b/src/test/groovy/graphql/NonNullHandlingTest.groovy @@ -0,0 +1,116 @@ +package graphql + +import graphql.schema.GraphQLOutputType +import graphql.schema.GraphQLSchema +import spock.lang.Specification + +import static graphql.Scalars.GraphQLString +import static graphql.schema.GraphQLFieldDefinition.newFieldDefinition +import static graphql.schema.GraphQLNonNull.nonNull +import static graphql.schema.GraphQLObjectType.newObject +import static graphql.schema.GraphQLSchema.newSchema + +/** + * A set of tests to show how non null field handling correctly bubble up or not + */ +class NonNullHandlingTest extends Specification { + + @SuppressWarnings("GroovyUnusedDeclaration") + class ParentTypeImplementation { + String nullChild = null + String nonNullChild = "not null" + } + + def executionInput(String query) { + ExecutionInput.newExecutionInput().query(query).build() + } + + def "#268 - null child field values are allowed in nullable parent type"() { + + // see https://github.com/graphql-java/graphql-java/issues/268 + + given: + + + GraphQLOutputType parentType = newObject() + .name("currentType") + .field(newFieldDefinition().name("nullChild") + .type(nonNull(GraphQLString))) + .field(newFieldDefinition().name("nonNullChild") + .type(nonNull(GraphQLString))) + .build() + + GraphQLSchema schema = newSchema().query( + newObject() + .name("RootQueryType") + .field(newFieldDefinition() + .name("parent") + .type(parentType) // nullable parent + .dataFetcher({ env -> new ParentTypeImplementation() }) + + )) + .build() + + def query = """ + query { + parent { + nonNullChild + nullChild + } + } + """ + + when: + def result = GraphQL.newGraphQL(schema).build().execute(executionInput(query)) + + then: + + result.errors.size() == 1 + result.data["parent"] == null + } + + def "#268 - null child field values are NOT allowed in non nullable parent types"() { + + // see https://github.com/graphql-java/graphql-java/issues/268 + + given: + + + GraphQLOutputType parentType = newObject() + .name("currentType") + .field(newFieldDefinition().name("nullChild") + .type(nonNull(GraphQLString))) + .field(newFieldDefinition().name("nonNullChild") + .type(nonNull(GraphQLString))) + .build() + + GraphQLSchema schema = newSchema().query( + newObject() + .name("RootQueryType") + .field( + newFieldDefinition() + .name("parent") + .type(nonNull(parentType)) // non nullable parent + .dataFetcher({ env -> new ParentTypeImplementation() }) + + )) + .build() + + def query = """ + query { + parent { + nonNullChild + nullChild + } + } + """ + + when: + def result = GraphQL.newGraphQL(schema).build().execute(executionInput(query)) + + then: + + result.errors.size() == 1 + result.data == null + } +} \ No newline at end of file diff --git a/src/test/groovy/graphql/execution/AsyncFetchThenCompleteExecutionTestStrategy.java b/src/test/groovy/graphql/execution/AsyncFetchThenCompleteExecutionTestStrategy.java index e7c4ee4994..89851d3671 100644 --- a/src/test/groovy/graphql/execution/AsyncFetchThenCompleteExecutionTestStrategy.java +++ b/src/test/groovy/graphql/execution/AsyncFetchThenCompleteExecutionTestStrategy.java @@ -32,7 +32,7 @@ public AsyncFetchThenCompleteExecutionTestStrategy() { } @Override - public ExecutionResult execute(ExecutionContext executionContext, ExecutionStrategyParameters parameters) throws NonNullableFieldWasNullException { + public CompletableFuture execute(ExecutionContext executionContext, ExecutionStrategyParameters parameters) throws NonNullableFieldWasNullException { Map fetchedValues = fetchFields(executionContext, parameters); @@ -63,7 +63,7 @@ private Map fetchFields(ExecutionContext executionContext, Execu return fetchedValues; } - private ExecutionResult completeFields(ExecutionContext executionContext, ExecutionStrategyParameters parameters, Map fetchedValues) { + private CompletableFuture completeFields(ExecutionContext executionContext, ExecutionStrategyParameters parameters, Map fetchedValues) { Map> fields = parameters.fields(); // then for every fetched value, complete it, breath first @@ -75,7 +75,7 @@ private ExecutionResult completeFields(ExecutionContext executionContext, Execut Object fetchedValue = fetchedValues.get(fieldName); try { - ExecutionResult resolvedResult = completeField(executionContext, newParameters, fieldList, fetchedValue); + ExecutionResult resolvedResult = completeField(executionContext, newParameters, fieldList, fetchedValue).join(); results.put(fieldName, resolvedResult != null ? resolvedResult.getData() : null); } catch (NonNullableFieldWasNullException e) { assertNonNullFieldPrecondition(e); @@ -83,7 +83,7 @@ private ExecutionResult completeFields(ExecutionContext executionContext, Execut break; } } - return new ExecutionResultImpl(results, executionContext.getErrors()); + return CompletableFuture.completedFuture(new ExecutionResultImpl(results, executionContext.getErrors())); } private ExecutionStrategyParameters newParameters(ExecutionStrategyParameters parameters, String fieldName) { diff --git a/src/test/groovy/graphql/execution/BreadthFirstExecutionTestStrategy.java b/src/test/groovy/graphql/execution/BreadthFirstExecutionTestStrategy.java index c8a1b9abc6..1f1abb33d8 100644 --- a/src/test/groovy/graphql/execution/BreadthFirstExecutionTestStrategy.java +++ b/src/test/groovy/graphql/execution/BreadthFirstExecutionTestStrategy.java @@ -8,6 +8,7 @@ import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import java.util.concurrent.CompletableFuture; /** * To prove we can write other execution strategies this one does a breath first approach @@ -20,7 +21,7 @@ public BreadthFirstExecutionTestStrategy() { } @Override - public ExecutionResult execute(ExecutionContext executionContext, ExecutionStrategyParameters parameters) throws NonNullableFieldWasNullException { + public CompletableFuture execute(ExecutionContext executionContext, ExecutionStrategyParameters parameters) throws NonNullableFieldWasNullException { Map> fields = parameters.fields(); Map fetchedValues = new LinkedHashMap<>(); @@ -48,7 +49,7 @@ public ExecutionResult execute(ExecutionContext executionContext, ExecutionStrat break; } } - return new ExecutionResultImpl(results, executionContext.getErrors()); + return CompletableFuture.completedFuture(new ExecutionResultImpl(results, executionContext.getErrors())); } private Object fetchField(ExecutionContext executionContext, ExecutionStrategyParameters parameters, Map> fields, String fieldName) { @@ -61,7 +62,7 @@ private Object fetchField(ExecutionContext executionContext, ExecutionStrategyPa } private void completeValue(ExecutionContext executionContext, Map results, String fieldName, List fieldList, Object fetchedValue, ExecutionStrategyParameters newParameters) { - ExecutionResult resolvedResult = completeField(executionContext, newParameters, fieldList, fetchedValue); + ExecutionResult resolvedResult = completeField(executionContext, newParameters, fieldList, fetchedValue).join(); results.put(fieldName, resolvedResult != null ? resolvedResult.getData() : null); } diff --git a/src/test/groovy/graphql/execution/ExecutionIdTest.groovy b/src/test/groovy/graphql/execution/ExecutionIdTest.groovy index cf46f38eb9..11b8845c19 100644 --- a/src/test/groovy/graphql/execution/ExecutionIdTest.groovy +++ b/src/test/groovy/graphql/execution/ExecutionIdTest.groovy @@ -5,13 +5,15 @@ import graphql.GraphQL import graphql.StarWarsSchema import spock.lang.Specification +import java.util.concurrent.CompletableFuture + class ExecutionIdTest extends Specification { class CaptureIdStrategy extends SimpleExecutionStrategy { ExecutionId executionId = null @Override - ExecutionResult execute(ExecutionContext executionContext, ExecutionStrategyParameters parameters) { + CompletableFuture execute(ExecutionContext executionContext, ExecutionStrategyParameters parameters) { executionId = executionContext.executionId return super.execute(executionContext, parameters) } diff --git a/src/test/groovy/graphql/execution/ExecutionStrategyTest.groovy b/src/test/groovy/graphql/execution/ExecutionStrategyTest.groovy index fdd2ed20b5..d923e81d8b 100644 --- a/src/test/groovy/graphql/execution/ExecutionStrategyTest.groovy +++ b/src/test/groovy/graphql/execution/ExecutionStrategyTest.groovy @@ -17,6 +17,8 @@ import graphql.schema.GraphQLScalarType import graphql.schema.GraphQLSchema import spock.lang.Specification +import java.util.concurrent.CompletableFuture + import static ExecutionStrategyParameters.newParameters import static graphql.Scalars.GraphQLString import static graphql.schema.GraphQLEnumType.newEnum @@ -35,7 +37,7 @@ class ExecutionStrategyTest extends Specification { executionStrategy = new ExecutionStrategy(dataFetcherExceptionHandler) { @Override - ExecutionResult execute(ExecutionContext executionContext, ExecutionStrategyParameters parameters) { + CompletableFuture execute(ExecutionContext executionContext, ExecutionStrategyParameters parameters) { return null } } @@ -51,7 +53,7 @@ class ExecutionStrategyTest extends Specification { ExecutionContext executionContext = buildContext() Field field = new Field() def fieldType = new GraphQLList(GraphQLString) - def result = Arrays.asList("test") + def result = ["test", "1", "2", "3"] def parameters = newParameters() .typeInfo(TypeInfo.newTypeInfo().type(fieldType)) .source(result) @@ -59,10 +61,10 @@ class ExecutionStrategyTest extends Specification { .build() when: - def executionResult = executionStrategy.completeValue(executionContext, parameters, [field]) + def executionResult = executionStrategy.completeValue(executionContext, parameters, [field]).join() then: - executionResult.data == ["test"] + executionResult.data == result } def "completes value for an array"() { @@ -70,7 +72,7 @@ class ExecutionStrategyTest extends Specification { ExecutionContext executionContext = buildContext() Field field = new Field() def fieldType = new GraphQLList(GraphQLString) - String[] result = ["test"] + def result = ["test", "1", "2", "3"] def parameters = newParameters() .typeInfo(TypeInfo.newTypeInfo().type(fieldType)) .source(result) @@ -78,10 +80,10 @@ class ExecutionStrategyTest extends Specification { .build() when: - def executionResult = executionStrategy.completeValue(executionContext, parameters, [field]) + def executionResult = executionStrategy.completeValue(executionContext, parameters, [field]).join() then: - executionResult.data == ["test"] + executionResult.data == result } def "completing value with serializing throwing exception"() { @@ -100,7 +102,7 @@ class ExecutionStrategyTest extends Specification { .build() when: - def executionResult = executionStrategy.completeValue(executionContext, parameters, [new Field()]) + def executionResult = executionStrategy.completeValue(executionContext, parameters, [new Field()]).join() then: executionResult.data == null @@ -125,7 +127,7 @@ class ExecutionStrategyTest extends Specification { .build() when: - def executionResult = executionStrategy.completeValue(executionContext, parameters, [new Field()]) + def executionResult = executionStrategy.completeValue(executionContext, parameters, [new Field()]).join() then: executionResult.data == null @@ -273,7 +275,7 @@ class ExecutionStrategyTest extends Specification { boolean handleDataFetchingExceptionCalled = false ExecutionStrategy overridingStrategy = new ExecutionStrategy() { @Override - ExecutionResult execute(ExecutionContext ec, ExecutionStrategyParameters p) throws NonNullableFieldWasNullException { + CompletableFuture execute(ExecutionContext ec, ExecutionStrategyParameters p) throws NonNullableFieldWasNullException { null } @@ -319,7 +321,7 @@ class ExecutionStrategyTest extends Specification { } }) { @Override - ExecutionResult execute(ExecutionContext ec, ExecutionStrategyParameters p) throws NonNullableFieldWasNullException { + CompletableFuture execute(ExecutionContext ec, ExecutionStrategyParameters p) throws NonNullableFieldWasNullException { null } @@ -350,7 +352,7 @@ class ExecutionStrategyTest extends Specification { ExecutionStrategy overridingStrategy = new ExecutionStrategy() { @Override - ExecutionResult execute(ExecutionContext ec, ExecutionStrategyParameters p) throws NonNullableFieldWasNullException { + CompletableFuture execute(ExecutionContext ec, ExecutionStrategyParameters p) throws NonNullableFieldWasNullException { null } } diff --git a/src/test/groovy/graphql/execution/ExecutionTest.groovy b/src/test/groovy/graphql/execution/ExecutionTest.groovy index 5981661198..29297c8680 100644 --- a/src/test/groovy/graphql/execution/ExecutionTest.groovy +++ b/src/test/groovy/graphql/execution/ExecutionTest.groovy @@ -1,25 +1,46 @@ package graphql.execution import graphql.ExecutionInput +import graphql.ExecutionResult +import graphql.ExecutionResultImpl import graphql.MutationSchema import graphql.execution.instrumentation.NoOpInstrumentation import graphql.parser.Parser import spock.lang.Specification +import java.util.concurrent.CompletableFuture + +import static java.util.Collections.emptyList + class ExecutionTest extends Specification { + class CountingExecutionStrategy extends ExecutionStrategy { + int execute = 0 + + + @Override + CompletableFuture execute(ExecutionContext executionContext, ExecutionStrategyParameters parameters) throws NonNullableFieldWasNullException { + execute++ + return CompletableFuture.completedFuture(result()) + } + + private ExecutionResultImpl result() { + new ExecutionResultImpl(emptyList()) + } + } + def parser = new Parser() - def subscriptionStrategy = Mock(ExecutionStrategy) - def mutationStrategy = Mock(ExecutionStrategy) - def queryStrategy = Mock(ExecutionStrategy) + def subscriptionStrategy = new CountingExecutionStrategy() + def mutationStrategy = new CountingExecutionStrategy() + def queryStrategy = new CountingExecutionStrategy() def execution = new Execution(queryStrategy, mutationStrategy, subscriptionStrategy, NoOpInstrumentation.INSTANCE) def emptyExecutionInput = ExecutionInput.newExecutionInput().build() def "query strategy is used for query requests"() { given: - def mutationStrategy = Mock(ExecutionStrategy) + def mutationStrategy = new CountingExecutionStrategy() - def queryStrategy = Mock(ExecutionStrategy) + def queryStrategy = new CountingExecutionStrategy() def execution = new Execution(queryStrategy, mutationStrategy, subscriptionStrategy, NoOpInstrumentation.INSTANCE) def query = ''' @@ -35,9 +56,9 @@ class ExecutionTest extends Specification { execution.execute(document, MutationSchema.schema, ExecutionId.generate(), emptyExecutionInput) then: - 1 * queryStrategy.execute(*_) - 0 * mutationStrategy.execute(*_) - 0 * subscriptionStrategy.execute(*_) + queryStrategy.execute == 1 + mutationStrategy.execute == 0 + subscriptionStrategy.execute == 0 } def "mutation strategy is used for mutation requests"() { @@ -55,9 +76,9 @@ class ExecutionTest extends Specification { execution.execute(document, MutationSchema.schema, ExecutionId.generate(), emptyExecutionInput) then: - 0 * queryStrategy.execute(*_) - 1 * mutationStrategy.execute(*_) - 0 * subscriptionStrategy.execute(*_) + queryStrategy.execute == 0 + mutationStrategy.execute == 1 + subscriptionStrategy.execute == 0 } def "subscription strategy is used for subscription requests"() { @@ -75,8 +96,8 @@ class ExecutionTest extends Specification { execution.execute(document, MutationSchema.schema, ExecutionId.generate(), emptyExecutionInput) then: - 0 * queryStrategy.execute(*_) - 0 * mutationStrategy.execute(*_) - 1 * subscriptionStrategy.execute(*_) + queryStrategy.execute == 0 + mutationStrategy.execute == 0 + subscriptionStrategy.execute == 1 } }