From 9931cde6d40787a1015a740c865ed68ba245161c Mon Sep 17 00:00:00 2001 From: Vladimir Razuvaev Date: Tue, 15 Aug 2017 01:49:56 +0700 Subject: [PATCH] Tests for lazy type loading during query execution + related changed --- src/Type/Schema.php | 10 +- tests/Executor/ExecutorLazySchemaTest.php | 235 ++++++++++++++ tests/StarWarsIntrospectionTest.php | 16 +- tests/Type/IntrospectionTest.php | 356 +++++++++++----------- tests/Type/TypeLoaderTest.php | 13 + 5 files changed, 442 insertions(+), 188 deletions(-) diff --git a/src/Type/Schema.php b/src/Type/Schema.php index 74a961b..3a88f33 100644 --- a/src/Type/Schema.php +++ b/src/Type/Schema.php @@ -113,7 +113,7 @@ class Schema ); $this->config = $config; - + $this->resolvedTypes = Type::getInternalTypes() + Introspection::getTypes(); $this->resolvedTypes[$config->query->name] = $config->query; if ($config->mutation) { @@ -207,7 +207,7 @@ class Schema foreach ($this->resolveAdditionalTypes() as $type) { $typeMap = TypeInfo::extractTypes($type, $typeMap); } - return $typeMap + Type::getInternalTypes() + Introspection::getTypes(); + return $typeMap; } /** @@ -373,7 +373,13 @@ class Schema ); } + $internalTypes = Type::getInternalTypes() + Introspection::getTypes(); + foreach ($this->getTypeMap() as $name => $type) { + if (isset($internalTypes[$name])) { + continue ; + } + $type->assertValid(); if ($type instanceof AbstractType) { diff --git a/tests/Executor/ExecutorLazySchemaTest.php b/tests/Executor/ExecutorLazySchemaTest.php index c3c0704..92c0394 100644 --- a/tests/Executor/ExecutorLazySchemaTest.php +++ b/tests/Executor/ExecutorLazySchemaTest.php @@ -8,13 +8,37 @@ use GraphQL\Error\Warning; use GraphQL\Executor\ExecutionResult; use GraphQL\Executor\Executor; use GraphQL\Language\Parser; +use GraphQL\Type\Definition\CustomScalarType; use GraphQL\Type\Definition\InterfaceType; use GraphQL\Type\Definition\ObjectType; use GraphQL\Type\Definition\Type; +use GraphQL\Type\Definition\UnionType; use GraphQL\Type\Schema; class ExecutorLazySchemaTest extends \PHPUnit_Framework_TestCase { + public $SomeScalarType; + + public $SomeObjectType; + + public $OtherObjectType; + + public $DeeperObjectType; + + public $SomeUnionType; + + public $SomeInterfaceType; + + public $SomeEnumType; + + public $SomeInputObjectType; + + public $QueryType; + + public $calls = []; + + public $loadedTypes = []; + public function testWarnsAboutSlowIsTypeOfForLazySchema() { // isTypeOf used to resolve runtime type for Interface @@ -169,4 +193,215 @@ class ExecutorLazySchemaTest extends \PHPUnit_Framework_TestCase $result->errors[0]->getPrevious() ); } + + public function testSimpleQuery() + { + $schema = new Schema([ + 'query' => $this->loadType('Query'), + 'typeLoader' => function($name) { + return $this->loadType($name, true); + } + ]); + + $query = '{ object { string } }'; + $result = Executor::execute( + $schema, + Parser::parse($query), + ['object' => ['string' => 'test']] + ); + + $expected = [ + 'data' => ['object' => ['string' => 'test']], + ]; + $expectedExecutorCalls = [ + 'Query.fields', + 'SomeObject', + 'SomeObject.fields' + ]; + $this->assertEquals($expected, $result->toArray(true)); + $this->assertEquals($expectedExecutorCalls, $this->calls); + } + + public function testDeepQuery() + { + $schema = new Schema([ + 'query' => $this->loadType('Query'), + 'typeLoader' => function($name) { + return $this->loadType($name, true); + } + ]); + + $query = '{ object { object { object { string } } } }'; + $result = Executor::execute( + $schema, + Parser::parse($query), + ['object' => ['object' => ['object' => ['string' => 'test']]]] + ); + + $expected = [ + 'data' => ['object' => ['object' => ['object' => ['string' => 'test']]]] + ]; + $expectedLoadedTypes = [ + 'Query' => true, + 'SomeObject' => true, + 'OtherObject' => true + ]; + + $this->assertEquals($expected, $result->toArray(true)); + $this->assertEquals($expectedLoadedTypes, $this->loadedTypes); + + $expectedExecutorCalls = [ + 'Query.fields', + 'SomeObject', + 'SomeObject.fields' + ]; + $this->assertEquals($expectedExecutorCalls, $this->calls); + } + + public function testResolveUnion() + { + $schema = new Schema([ + 'query' => $this->loadType('Query'), + 'typeLoader' => function($name) { + return $this->loadType($name, true); + } + ]); + + $query = ' + { + other { + union { + scalar + } + } + } + '; + $result = Executor::execute( + $schema, + Parser::parse($query), + ['other' => ['union' => ['scalar' => 'test']]] + ); + + $expected = [ + 'data' => ['other' => ['union' => ['scalar' => 'test']]], + ]; + $expectedLoadedTypes = [ + 'Query' => true, + 'SomeObject' => true, + 'OtherObject' => true, + 'SomeUnion' => true, + 'SomeInterface' => true, + 'DeeperObject' => true, + 'SomeScalar' => true, + ]; + + $this->assertEquals($expected, $result->toArray(true)); + $this->assertEquals($expectedLoadedTypes, $this->loadedTypes); + + $expectedCalls = [ + 'Query.fields', + 'OtherObject', + 'OtherObject.fields', + 'SomeUnion', + 'SomeUnion.resolveType', + 'SomeUnion.types', + 'DeeperObject', + 'SomeScalar', + ]; + $this->assertEquals($expectedCalls, $this->calls); + } + + public function loadType($name, $isExecutorCall = false) + { + if ($isExecutorCall) { + $this->calls[] = $name; + } + $this->loadedTypes[$name] = true; + + switch ($name) { + case 'Query': + return $this->QueryType ?: $this->QueryType = new ObjectType([ + 'name' => 'Query', + 'fields' => function() { + $this->calls[] = 'Query.fields'; + return [ + 'object' => ['type' => $this->loadType('SomeObject')], + 'other' => ['type' => $this->loadType('OtherObject')], + ]; + } + ]); + case 'SomeObject': + return $this->SomeObjectType ?: $this->SomeObjectType = new ObjectType([ + 'name' => 'SomeObject', + 'fields' => function() { + $this->calls[] = 'SomeObject.fields'; + return [ + 'string' => ['type' => Type::string()], + 'object' => ['type' => $this->SomeObjectType] + ]; + }, + 'interfaces' => function() { + $this->calls[] = 'SomeObject.interfaces'; + return [ + $this->loadType('SomeInterface') + ]; + } + ]); + case 'OtherObject': + return $this->OtherObjectType ?: $this->OtherObjectType = new ObjectType([ + 'name' => 'OtherObject', + 'fields' => function() { + $this->calls[] = 'OtherObject.fields'; + return [ + 'union' => ['type' => $this->loadType('SomeUnion')], + 'iface' => ['type' => Type::nonNull($this->loadType('SomeInterface'))], + ]; + } + ]); + case 'DeeperObject': + return $this->DeeperObjectType ?: $this->DeeperObjectType = new ObjectType([ + 'name' => 'DeeperObject', + 'fields' => function() { + return [ + 'scalar' => ['type' => $this->loadType('SomeScalar')], + ]; + } + ]); + case 'SomeScalar'; + return $this->SomeScalarType ?: $this->SomeScalarType = new CustomScalarType([ + 'name' => 'SomeScalar', + 'serialize' => function($value) {return $value;}, + 'parseValue' => function($value) {return $value;}, + 'parseLiteral' => function() {} + ]); + case 'SomeUnion': + return $this->SomeUnionType ?: $this->SomeUnionType = new UnionType([ + 'name' => 'SomeUnion', + 'resolveType' => function() { + $this->calls[] = 'SomeUnion.resolveType'; + return $this->loadType('DeeperObject'); + }, + 'types' => function() { + $this->calls[] = 'SomeUnion.types'; + return [ $this->loadType('DeeperObject') ]; + } + ]); + case 'SomeInterface': + return $this->SomeInterfaceType ?: $this->SomeInterfaceType = new InterfaceType([ + 'name' => 'SomeInterface', + 'resolveType' => function() { + $this->calls[] = 'SomeInterface.resolveType'; + return $this->loadType('SomeObject'); + }, + 'fields' => function() { + $this->calls[] = 'SomeInterface.fields'; + return [ + 'string' => ['type' => Type::string() ] + ]; + } + ]); + default: + return null; + } + } } diff --git a/tests/StarWarsIntrospectionTest.php b/tests/StarWarsIntrospectionTest.php index 9ee434e..530c760 100644 --- a/tests/StarWarsIntrospectionTest.php +++ b/tests/StarWarsIntrospectionTest.php @@ -26,24 +26,24 @@ class StarWarsIntrospectionTest extends \PHPUnit_Framework_TestCase $expected = [ '__schema' => [ 'types' => [ - ['name' => 'Query'], - ['name' => 'Episode'], - ['name' => 'Character'], - ['name' => 'String'], - ['name' => 'Human'], - ['name' => 'Droid'], ['name' => 'ID'], + ['name' => 'String'], ['name' => 'Float'], ['name' => 'Int'], ['name' => 'Boolean'], ['name' => '__Schema'], ['name' => '__Type'], - ['name' => '__Directive'], + ['name' => '__TypeKind'], ['name' => '__Field'], ['name' => '__InputValue'], ['name' => '__EnumValue'], - ['name' => '__TypeKind'], + ['name' => '__Directive'], ['name' => '__DirectiveLocation'], + ['name' => 'Query'], + ['name' => 'Episode'], + ['name' => 'Character'], + ['name' => 'Human'], + ['name' => 'Droid'], ] ] ]; diff --git a/tests/Type/IntrospectionTest.php b/tests/Type/IntrospectionTest.php index 82e18ba..54a00d7 100644 --- a/tests/Type/IntrospectionTest.php +++ b/tests/Type/IntrospectionTest.php @@ -42,32 +42,9 @@ class IntrospectionTest extends \PHPUnit_Framework_TestCase ), 'types' => array ( - array ( - 'kind' => 'OBJECT', - 'name' => 'QueryRoot', - 'inputFields' => NULL, - 'interfaces' => - array ( - ), - 'enumValues' => NULL, - 'possibleTypes' => NULL, - 'fields' => array ( - array ( - 'name' => 'a', - 'args' => array(), - 'type' => array( - 'kind' => 'SCALAR', - 'name' => 'String', - 'ofType' => null - ), - 'isDeprecated' => false, - 'deprecationReason' => null, - ) - ) - ), array ( 'kind' => 'SCALAR', - 'name' => 'String', + 'name' => 'ID', 'fields' => NULL, 'inputFields' => NULL, 'interfaces' => NULL, @@ -76,7 +53,7 @@ class IntrospectionTest extends \PHPUnit_Framework_TestCase ), array ( 'kind' => 'SCALAR', - 'name' => 'ID', + 'name' => 'String', 'fields' => NULL, 'inputFields' => NULL, 'interfaces' => NULL, @@ -440,165 +417,62 @@ class IntrospectionTest extends \PHPUnit_Framework_TestCase 'possibleTypes' => NULL, ), array ( - 'kind' => 'OBJECT', - 'name' => '__Directive', - 'fields' => + 'kind' => 'ENUM', + 'name' => '__TypeKind', + 'fields' => NULL, + 'inputFields' => NULL, + 'interfaces' => NULL, + 'enumValues' => array ( 0 => array ( - 'name' => 'name', - 'args' => - array ( - ), - 'type' => - array ( - 'kind' => 'NON_NULL', - 'name' => NULL, - 'ofType' => - array ( - 'kind' => 'SCALAR', - 'name' => 'String', - ), - ), + 'name' => 'SCALAR', 'isDeprecated' => false, 'deprecationReason' => NULL, ), 1 => array ( - 'name' => 'description', - 'args' => - array ( - ), - 'type' => - array ( - 'kind' => 'SCALAR', - 'name' => 'String', - ), + 'name' => 'OBJECT', 'isDeprecated' => false, 'deprecationReason' => NULL, ), 2 => array ( - 'name' => 'locations', - 'args' => - array ( - ), - 'type' => - array ( - 'kind' => 'NON_NULL', - 'name' => NULL, - 'ofType' => - array ( - 'kind' => 'LIST', - 'name' => NULL, - 'ofType' => - array ( - 'kind' => 'NON_NULL', - 'name' => NULL, - 'ofType' => - array ( - 'kind' => 'ENUM', - 'name' => '__DirectiveLocation', - ), - ), - ), - ), + 'name' => 'INTERFACE', 'isDeprecated' => false, 'deprecationReason' => NULL, ), 3 => array ( - 'name' => 'args', - 'args' => - array ( - ), - 'type' => - array ( - 'kind' => 'NON_NULL', - 'name' => NULL, - 'ofType' => - array ( - 'kind' => 'LIST', - 'name' => NULL, - 'ofType' => - array ( - 'kind' => 'NON_NULL', - 'name' => NULL, - 'ofType' => - array ( - 'kind' => 'OBJECT', - 'name' => '__InputValue', - ), - ), - ), - ), + 'name' => 'UNION', 'isDeprecated' => false, 'deprecationReason' => NULL, ), 4 => array ( - 'name' => 'onOperation', - 'args' => - array ( - ), - 'type' => - array ( - 'kind' => 'NON_NULL', - 'name' => NULL, - 'ofType' => - array ( - 'kind' => 'SCALAR', - 'name' => 'Boolean', - ), - ), - 'isDeprecated' => true, - 'deprecationReason' => 'Use `locations`.', + 'name' => 'ENUM', + 'isDeprecated' => false, + 'deprecationReason' => NULL, ), 5 => array ( - 'name' => 'onFragment', - 'args' => - array ( - ), - 'type' => - array ( - 'kind' => 'NON_NULL', - 'name' => NULL, - 'ofType' => - array ( - 'kind' => 'SCALAR', - 'name' => 'Boolean', - ), - ), - 'isDeprecated' => true, - 'deprecationReason' => 'Use `locations`.', + 'name' => 'INPUT_OBJECT', + 'isDeprecated' => false, + 'deprecationReason' => NULL, ), 6 => array ( - 'name' => 'onField', - 'args' => - array ( - ), - 'type' => - array ( - 'kind' => 'NON_NULL', - 'name' => NULL, - 'ofType' => - array ( - 'kind' => 'SCALAR', - 'name' => 'Boolean', - - ), - ), - 'isDeprecated' => true, - 'deprecationReason' => 'Use `locations`.', + 'name' => 'LIST', + 'isDeprecated' => false, + 'deprecationReason' => NULL, + ), + 7 => + array ( + 'name' => 'NON_NULL', + 'isDeprecated' => false, + 'deprecationReason' => NULL, ), ), - 'inputFields' => NULL, - 'interfaces' => - array ( - ), - 'enumValues' => NULL, 'possibleTypes' => NULL, ), array ( @@ -887,62 +761,165 @@ class IntrospectionTest extends \PHPUnit_Framework_TestCase 'possibleTypes' => NULL, ), array ( - 'kind' => 'ENUM', - 'name' => '__TypeKind', - 'fields' => NULL, - 'inputFields' => NULL, - 'interfaces' => NULL, - 'enumValues' => + 'kind' => 'OBJECT', + 'name' => '__Directive', + 'fields' => array ( 0 => array ( - 'name' => 'SCALAR', + 'name' => 'name', + 'args' => + array ( + ), + 'type' => + array ( + 'kind' => 'NON_NULL', + 'name' => NULL, + 'ofType' => + array ( + 'kind' => 'SCALAR', + 'name' => 'String', + ), + ), 'isDeprecated' => false, 'deprecationReason' => NULL, ), 1 => array ( - 'name' => 'OBJECT', + 'name' => 'description', + 'args' => + array ( + ), + 'type' => + array ( + 'kind' => 'SCALAR', + 'name' => 'String', + ), 'isDeprecated' => false, 'deprecationReason' => NULL, ), 2 => array ( - 'name' => 'INTERFACE', + 'name' => 'locations', + 'args' => + array ( + ), + 'type' => + array ( + 'kind' => 'NON_NULL', + 'name' => NULL, + 'ofType' => + array ( + 'kind' => 'LIST', + 'name' => NULL, + 'ofType' => + array ( + 'kind' => 'NON_NULL', + 'name' => NULL, + 'ofType' => + array ( + 'kind' => 'ENUM', + 'name' => '__DirectiveLocation', + ), + ), + ), + ), 'isDeprecated' => false, 'deprecationReason' => NULL, ), 3 => array ( - 'name' => 'UNION', + 'name' => 'args', + 'args' => + array ( + ), + 'type' => + array ( + 'kind' => 'NON_NULL', + 'name' => NULL, + 'ofType' => + array ( + 'kind' => 'LIST', + 'name' => NULL, + 'ofType' => + array ( + 'kind' => 'NON_NULL', + 'name' => NULL, + 'ofType' => + array ( + 'kind' => 'OBJECT', + 'name' => '__InputValue', + ), + ), + ), + ), 'isDeprecated' => false, 'deprecationReason' => NULL, ), 4 => array ( - 'name' => 'ENUM', - 'isDeprecated' => false, - 'deprecationReason' => NULL, + 'name' => 'onOperation', + 'args' => + array ( + ), + 'type' => + array ( + 'kind' => 'NON_NULL', + 'name' => NULL, + 'ofType' => + array ( + 'kind' => 'SCALAR', + 'name' => 'Boolean', + ), + ), + 'isDeprecated' => true, + 'deprecationReason' => 'Use `locations`.', ), 5 => array ( - 'name' => 'INPUT_OBJECT', - 'isDeprecated' => false, - 'deprecationReason' => NULL, + 'name' => 'onFragment', + 'args' => + array ( + ), + 'type' => + array ( + 'kind' => 'NON_NULL', + 'name' => NULL, + 'ofType' => + array ( + 'kind' => 'SCALAR', + 'name' => 'Boolean', + ), + ), + 'isDeprecated' => true, + 'deprecationReason' => 'Use `locations`.', ), 6 => array ( - 'name' => 'LIST', - 'isDeprecated' => false, - 'deprecationReason' => NULL, - ), - 7 => - array ( - 'name' => 'NON_NULL', - 'isDeprecated' => false, - 'deprecationReason' => NULL, + 'name' => 'onField', + 'args' => + array ( + ), + 'type' => + array ( + 'kind' => 'NON_NULL', + 'name' => NULL, + 'ofType' => + array ( + 'kind' => 'SCALAR', + 'name' => 'Boolean', + + ), + ), + 'isDeprecated' => true, + 'deprecationReason' => 'Use `locations`.', ), ), + 'inputFields' => NULL, + 'interfaces' => + array ( + ), + 'enumValues' => NULL, 'possibleTypes' => NULL, ), array ( @@ -998,6 +975,29 @@ class IntrospectionTest extends \PHPUnit_Framework_TestCase ), 'possibleTypes' => NULL, ), + array ( + 'kind' => 'OBJECT', + 'name' => 'QueryRoot', + 'inputFields' => NULL, + 'interfaces' => + array ( + ), + 'enumValues' => NULL, + 'possibleTypes' => NULL, + 'fields' => array ( + array ( + 'name' => 'a', + 'args' => array(), + 'type' => array( + 'kind' => 'SCALAR', + 'name' => 'String', + 'ofType' => null + ), + 'isDeprecated' => false, + 'deprecationReason' => null, + ) + ) + ), ), 'directives' => array ( diff --git a/tests/Type/TypeLoaderTest.php b/tests/Type/TypeLoaderTest.php index 5521123..1848d7e 100644 --- a/tests/Type/TypeLoaderTest.php +++ b/tests/Type/TypeLoaderTest.php @@ -339,4 +339,17 @@ class TypeLoaderTest extends \PHPUnit_Framework_TestCase $this->assertSame($withoutLoader->getType('BlogStory'), $withLoader->getType('BlogStory')); $this->assertSame($withoutLoader->getDirectives(), $withLoader->getDirectives()); } + + public function testSkipsLoaderForInternalTypes() + { + $schema = new Schema([ + 'query' => $this->query, + 'mutation' => $this->mutation, + 'typeLoader' => $this->typeLoader + ]); + + $type = $schema->getType('ID'); + $this->assertSame(Type::id(), $type); + $this->assertEquals([], $this->calls); + } }