Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -372,6 +372,7 @@ long testTime = 0L

tasks.withType(Test) {
useJUnitPlatform()
maxHeapSize = "1g"
testLogging {
events "FAILED", "SKIPPED"
exceptionFormat = "FULL"
Expand Down
4 changes: 2 additions & 2 deletions src/main/java/graphql/validation/OperationValidator.java
Original file line number Diff line number Diff line change
Expand Up @@ -1241,8 +1241,8 @@ private boolean sameArguments(List<Argument> arguments1, @Nullable List<Argument
}
GraphQLType typeA = typeAOriginal;
GraphQLType typeB = fieldAndType.graphQLType;
if (typeB == null) {
return mkNotSameTypeError(path, fields, typeA, typeB);
if (typeA == null || typeB == null) {
continue;
}
while (true) {
if (isNonNull(typeA) || isNonNull(typeB)) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,218 @@
package graphql.validation

import graphql.ExecutionResult
import graphql.GraphQL
import graphql.i18n.I18n
import graphql.language.Document
import graphql.parser.Parser
import graphql.schema.GraphQLSchema
import graphql.schema.idl.SchemaGenerator
import spock.lang.Shared
import spock.lang.Specification

/**
* These tests mirror the JMH benchmarks in OverlappingFieldValidationPerformance
* and OverlappingFieldValidationBenchmark. They exercise the same validation paths
* and assert zero errors, ensuring the overlapping fields rule does not produce
* false positives (e.g. from null type handling).
*/
class OverlappingFieldsCanBeMergedBenchmarkTest extends Specification {

static String schemaSdl = " type Query { viewer: Viewer } interface Abstract { field: Abstract leaf: Int } interface Abstract1 { field: Abstract leaf: Int } interface Abstract2 { field: Abstract leaf: Int }" +
" type Concrete1 implements Abstract1{ field: Abstract leaf: Int} " +
"type Concrete2 implements Abstract2{ field: Abstract leaf: Int} " +
"type Viewer { xingId: XingId } type XingId { firstName: String! lastName: String! }"

@Shared
GraphQLSchema schema

@Shared
GraphQLSchema schema2

@Shared
Document largeSchemaDocument

def setupSpec() {
String schemaString = loadResource("large-schema-4.graphqls")
String query = loadResource("large-schema-4-query.graphql")
schema = SchemaGenerator.createdMockedSchema(schemaString)
largeSchemaDocument = Parser.parse(query)
schema2 = SchemaGenerator.createdMockedSchema(schemaSdl)
}

private static String loadResource(String name) {
URL resource = OverlappingFieldsCanBeMergedBenchmarkTest.class.getClassLoader().getResource(name)
if (resource == null) {
throw new IllegalArgumentException("missing resource: " + name)
}
try (InputStream inputStream = resource.openStream()) {
return new String(inputStream.readAllBytes(), "UTF-8")
}
}

private List<ValidationError> validateQuery(GraphQLSchema schema, Document document) {
ValidationErrorCollector errorCollector = new ValidationErrorCollector()
I18n i18n = I18n.i18n(I18n.BundleType.Validation, Locale.ENGLISH)
ValidationContext validationContext = new ValidationContext(schema, document, i18n)
OperationValidator operationValidator = new OperationValidator(validationContext, errorCollector,
{ r -> r == OperationValidationRule.OVERLAPPING_FIELDS_CAN_BE_MERGED })
LanguageTraversal languageTraversal = new LanguageTraversal()
languageTraversal.traverse(document, operationValidator)
return errorCollector.getErrors()
}

// -- Large schema tests (mirrors OverlappingFieldValidationBenchmark) --

def "large schema query produces no validation errors"() {
when:
def errors = validateQuery(schema, largeSchemaDocument)

then:
errors.size() == 0
}

def "large schema query executes without errors"() {
when:
GraphQL graphQL = GraphQL.newGraphQL(schema).build()
ExecutionResult executionResult = graphQL.execute(loadResource("large-schema-4-query.graphql"))

then:
executionResult.errors.size() == 0
}

// -- Parameterized tests (mirrors OverlappingFieldValidationPerformance) --

def "overlapping fields with fragments produce no errors"() {
given:
Document doc = makeQueryWithFragments(100, true)

when:
def errors = validateQuery(schema2, doc)

then:
errors.size() == 0
}

def "overlapping fields without fragments produce no errors"() {
given:
Document doc = makeQueryWithoutFragments(100, true)

when:
def errors = validateQuery(schema2, doc)

then:
errors.size() == 0
}

def "non-overlapping fields with fragments produce no errors"() {
given:
Document doc = makeQueryWithFragments(100, false)

when:
def errors = validateQuery(schema2, doc)

then:
errors.size() == 0
}

def "non-overlapping fields without fragments produce no errors"() {
given:
Document doc = makeQueryWithoutFragments(100, false)

when:
def errors = validateQuery(schema2, doc)

then:
errors.size() == 0
}

def "repeated fields produce no errors"() {
given:
Document doc = makeRepeatedFieldsQuery(100)

when:
def errors = validateQuery(schema2, doc)

then:
errors.size() == 0
}

def "deep abstract concrete fields produce no errors"() {
given:
Document doc = makeDeepAbstractConcreteQuery(100)

when:
def errors = validateQuery(schema2, doc)

then:
errors.size() == 0
}

// -- Query builders (copied from OverlappingFieldValidationPerformance) --

private static Document makeQueryWithFragments(int size, boolean overlapping) {
StringBuilder b = new StringBuilder()

for (int i = 1; i <= size; i++) {
if (overlapping) {
b.append(" fragment mergeIdenticalFields" + i + " on Query {viewer { xingId { firstName lastName }}}")
} else {
b.append("fragment mergeIdenticalFields" + i + " on Query {viewer" + i + " { xingId" + i + " { firstName" + i + " lastName" + i + " } }}")
}
b.append("\n\n")
}

b.append("query testQuery {")
for (int i = 1; i <= size; i++) {
b.append("...mergeIdenticalFields" + i + "\n")
}
b.append("}")
return Parser.parse(b.toString())
}

private static Document makeQueryWithoutFragments(int size, boolean overlapping) {
StringBuilder b = new StringBuilder()

b.append("query testQuery {")
for (int i = 1; i <= size; i++) {
if (overlapping) {
b.append(" viewer { xingId { firstName } } ")
} else {
b.append(" viewer" + i + " { xingId" + i + " { firstName" + i + " } } ")
}
b.append("\n\n")
}
b.append("}")
return Parser.parse(b.toString())
}

private static Document makeRepeatedFieldsQuery(int size) {
StringBuilder b = new StringBuilder()
b.append(" query testQuery { viewer { xingId {")
b.append("firstName\n".repeat(Math.max(0, size)))
b.append("} } }")
return Parser.parse(b.toString())
}

private static Document makeDeepAbstractConcreteQuery(int depth) {
StringBuilder q = new StringBuilder()

q.append("fragment multiply on Whatever { field { " +
"... on Abstract1 { field { leaf } } " +
"... on Abstract2 { field { leaf } } " +
"... on Concrete1 { field { leaf } } " +
"... on Concrete2 { field { leaf } } } } " +
"query DeepAbstractConcrete { ")

for (int i = 1; i <= depth; i++) {
q.append("field { ...multiply ")
}

for (int i = 1; i <= depth; i++) {
q.append(" }")
}

q.append("\n}")
return Parser.parse(q.toString())
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import graphql.language.SourceLocation
import graphql.parser.Parser
import graphql.schema.GraphQLCodeRegistry
import graphql.schema.GraphQLSchema
import graphql.schema.idl.SchemaGenerator
import graphql.validation.LanguageTraversal
import graphql.validation.OperationValidationRule
import graphql.validation.OperationValidator
Expand Down Expand Up @@ -967,6 +968,45 @@ class OverlappingFieldsCanBeMergedTest extends Specification {
errorCollector.getErrors().size() == 0
}

def "mixed null and non-null field types from unresolvable fragments should not conflict"() {
given:
// Regression test for commit 072165b which changed the null-handling in requireSameOutputTypeShape.
// This reproduces the benchmarkDeepAbstractConcrete JMH benchmark scenario: the fragment spreads
// on "Whatever" (which doesn't exist in the schema), so the outer "field" has null graphQLType.
// After mergeSubSelections, the inner field set contains both null-typed entries (from the
// unresolvable parent) and Abstract-typed entries (from the interface inline fragments).
// A null type means the parent was unresolvable, so we should skip the comparison rather than
// report a spurious conflict.
def schema = SchemaGenerator.createdMockedSchema('''
type Query { viewer: Viewer }
interface Abstract { field: Abstract leaf: Int }
interface Abstract1 { field: Abstract leaf: Int }
interface Abstract2 { field: Abstract leaf: Int }
type Concrete1 implements Abstract1 { field: Abstract leaf: Int }
type Concrete2 implements Abstract2 { field: Abstract leaf: Int }
type Viewer { xingId: XingId }
type XingId { firstName: String! lastName: String! }
''')
def query = '''
fragment multiply on Whatever {
field {
... on Abstract1 { field { leaf } }
... on Abstract2 { field { leaf } }
... on Concrete1 { field { leaf } }
... on Concrete2 { field { leaf } }
}
}
query DeepAbstractConcrete {
field { ...multiply field { ...multiply } }
}
'''
when:
traverse(query, schema)

then:
errorCollector.getErrors().isEmpty()
}

def "overlapping fields on lower level"() {
given:
def schema = schema('''
Expand Down
Loading