Extracted lazy schema test; minor related refactoring

This commit is contained in:
Vladimir Razuvaev 2017-08-14 20:41:08 +07:00
parent f47db61907
commit b9d3a11785
8 changed files with 186 additions and 58 deletions

View File

@ -6,7 +6,7 @@ final class Warning
const NAME_WARNING = 1;
const ASSIGN_WARNING = 2;
const CONFIG_WARNING = 4;
const RESOLVE_TYPE_WARNING = 8;
const FULL_SCHEMA_SCAN_WARNING = 8;
const CONFIG_DEPRECATION_WARNING = 16;
const NOT_A_TYPE = 32;

View File

@ -1062,13 +1062,13 @@ class Executor
$runtimeType = $returnType->resolveType($result, $exeContext->contextValue, $info);
if (null === $runtimeType) {
if ($returnType instanceof InterfaceType) {
if ($returnType instanceof InterfaceType && $info->schema->getConfig()->typeLoader) {
Warning::warnOnce(
"GraphQL Interface Type `{$returnType->name}` returned `null` from it`s `resolveType` function ".
'for value: ' . Utils::printSafe($result) . '. Switching to slow resolution method using `isTypeOf` ' .
'of all possible implementations. It degrades query performance significantly. '.
'of all possible implementations. It requires full schema scan and degrades query performance significantly. '.
' Make sure your `resolveType` always returns valid implementation or throws.',
Warning::RESOLVE_TYPE_WARNING
Warning::FULL_SCHEMA_SCAN_WARNING
);
}
$runtimeType = self::defaultTypeResolver($result, $exeContext->contextValue, $info, $returnType);

View File

@ -123,6 +123,7 @@ class Schema
$this->resolvedTypes[$config->subscription->name] = $config->subscription;
}
if (!$this->config->typeLoader) {
// Perform full scan of the schema
$this->getTypeMap();
}
}
@ -199,40 +200,44 @@ class Schema
*/
private function collectAllTypes()
{
$initialTypes = $this->resolvedTypes;
$typeMap = [];
foreach ($initialTypes as $type) {
foreach ($this->resolvedTypes as $type) {
$typeMap = TypeInfo::extractTypes($type, $typeMap);
}
foreach ($this->resolveAdditionalTypes() as $type) {
$typeMap = TypeInfo::extractTypes($type, $typeMap);
}
return $typeMap + Type::getInternalTypes() + Introspection::getTypes();
}
if ($this->config->types) {
$types = $this->config->types;
/**
* @return \Generator
*/
private function resolveAdditionalTypes()
{
$types = $this->config->types ?: [];
if (is_callable($types)) {
$types = $types();
}
if (!is_array($types) && !$types instanceof \Traversable) {
throw new InvariantViolation(sprintf(
'Schema types callable must return array or instance of Traversable but got: %s',
Utils::getVariableType($types)
));
}
foreach ($types as $index => $type) {
if (!$type instanceof Type) {
throw new InvariantViolation(
'Each entry of schema types must be instance of GraphQL\Type\Definition\Type but entry at %s is %s',
$index,
Utils::printSafe($type)
);
}
$typeMap = TypeInfo::extractTypes($type, $typeMap);
}
if (is_callable($types)) {
$types = $types();
}
return $typeMap + Type::getInternalTypes() + Introspection::getTypes();
if (!is_array($types) && !$types instanceof \Traversable) {
throw new InvariantViolation(sprintf(
'Schema types callable must return array or instance of Traversable but got: %s',
Utils::getVariableType($types)
));
}
foreach ($types as $index => $type) {
if (!$type instanceof Type) {
throw new InvariantViolation(
'Each entry of schema types must be instance of GraphQL\Type\Definition\Type but entry at %s is %s',
$index,
Utils::printSafe($type)
);
}
yield $type;
}
}
/**

View File

@ -87,9 +87,9 @@ class AbstractPromiseTest extends \PHPUnit_Framework_TestCase
}
}';
Warning::suppress(Warning::RESOLVE_TYPE_WARNING);
Warning::suppress(Warning::FULL_SCHEMA_SCAN_WARNING);
$result = GraphQL::execute($schema, $query);
Warning::enable(Warning::RESOLVE_TYPE_WARNING);
Warning::enable(Warning::FULL_SCHEMA_SCAN_WARNING);
$expected = [
'data' => [
@ -174,9 +174,9 @@ class AbstractPromiseTest extends \PHPUnit_Framework_TestCase
}
}';
Warning::suppress(Warning::RESOLVE_TYPE_WARNING);
Warning::suppress(Warning::FULL_SCHEMA_SCAN_WARNING);
$result = GraphQL::execute($schema, $query);
Warning::enable(Warning::RESOLVE_TYPE_WARNING);
Warning::enable(Warning::FULL_SCHEMA_SCAN_WARNING);
$expected = [
'data' => [

View File

@ -3,14 +3,11 @@ namespace GraphQL\Tests\Executor;
require_once __DIR__ . '/TestClasses.php';
use GraphQL\Error\Warning;
use GraphQL\Executor\ExecutionResult;
use GraphQL\Executor\Executor;
use GraphQL\Error\FormattedError;
use GraphQL\GraphQL;
use GraphQL\Language\Parser;
use GraphQL\Language\SourceLocation;
use GraphQL\Schema;
use GraphQL\Type\Schema;
use GraphQL\Type\Definition\InterfaceType;
use GraphQL\Type\Definition\ObjectType;
use GraphQL\Type\Definition\Type;
@ -90,21 +87,8 @@ class AbstractTest extends \PHPUnit_Framework_TestCase
]
]);
Warning::suppress(Warning::RESOLVE_TYPE_WARNING);
$result = Executor::execute($schema, Parser::parse($query));
$this->assertEquals($expected, $result);
Warning::enable(Warning::RESOLVE_TYPE_WARNING);
$result = Executor::execute($schema, Parser::parse($query));
$this->assertEquals(1, count($result->errors));
$this->assertInstanceOf('PHPUnit_Framework_Error_Warning', $result->errors[0]->getPrevious());
$this->assertEquals(
'GraphQL Interface Type `Pet` returned `null` from it`s `resolveType` function for value: '.
'instance of GraphQL\Tests\Executor\Dog. Switching to slow resolution method using `isTypeOf` of '.
'all possible implementations. It degrades query performance significantly. '.
'Make sure your `resolveType` always returns valid implementation or throws.',
$result->errors[0]->getMessage());
}
/**

View File

@ -0,0 +1,116 @@
<?php
namespace GraphQL\Tests\Executor;
require_once __DIR__ . '/TestClasses.php';
use GraphQL\Error\Warning;
use GraphQL\Executor\ExecutionResult;
use GraphQL\Executor\Executor;
use GraphQL\Language\Parser;
use GraphQL\Type\Definition\InterfaceType;
use GraphQL\Type\Definition\ObjectType;
use GraphQL\Type\Definition\Type;
use GraphQL\Type\Schema;
class ExecutorLazySchemaTest extends \PHPUnit_Framework_TestCase
{
public function testWarnsAboutSlowIsTypeOfForLazySchema()
{
// isTypeOf used to resolve runtime type for Interface
$petType = new InterfaceType([
'name' => 'Pet',
'fields' => function() {
return [
'name' => ['type' => Type::string()]
];
}
]);
// Added to interface type when defined
$dogType = new ObjectType([
'name' => 'Dog',
'interfaces' => [$petType],
'isTypeOf' => function($obj) { return $obj instanceof Dog; },
'fields' => function() {
return [
'name' => ['type' => Type::string()],
'woofs' => ['type' => Type::boolean()]
];
}
]);
$catType = new ObjectType([
'name' => 'Cat',
'interfaces' => [$petType],
'isTypeOf' => function ($obj) {
return $obj instanceof Cat;
},
'fields' => function() {
return [
'name' => ['type' => Type::string()],
'meows' => ['type' => Type::boolean()],
];
}
]);
$schema = new Schema([
'query' => new ObjectType([
'name' => 'Query',
'fields' => [
'pets' => [
'type' => Type::listOf($petType),
'resolve' => function () {
return [new Dog('Odie', true), new Cat('Garfield', false)];
}
]
]
]),
'types' => [$catType, $dogType],
'typeLoader' => function($name) use ($dogType, $petType, $catType) {
switch ($name) {
case 'Dog':
return $dogType;
case 'Pet':
return $petType;
case 'Cat':
return $catType;
}
}
]);
$query = '{
pets {
name
... on Dog {
woofs
}
... on Cat {
meows
}
}
}';
$expected = new ExecutionResult([
'pets' => [
['name' => 'Odie', 'woofs' => true],
['name' => 'Garfield', 'meows' => false]
],
]);
Warning::suppress(Warning::FULL_SCHEMA_SCAN_WARNING);
$result = Executor::execute($schema, Parser::parse($query));
$this->assertEquals($expected, $result);
Warning::enable(Warning::FULL_SCHEMA_SCAN_WARNING);
$result = Executor::execute($schema, Parser::parse($query));
$this->assertEquals(1, count($result->errors));
$this->assertInstanceOf('PHPUnit_Framework_Error_Warning', $result->errors[0]->getPrevious());
$this->assertEquals(
'GraphQL Interface Type `Pet` returned `null` from it`s `resolveType` function for value: instance of '.
'GraphQL\Tests\Executor\Dog. Switching to slow resolution method using `isTypeOf` of all possible '.
'implementations. It requires full schema scan and degrades query performance significantly. '.
'Make sure your `resolveType` always returns valid implementation or throws.',
$result->errors[0]->getMessage());
}
}

View File

@ -256,9 +256,9 @@ class UnionInterfaceTest extends \PHPUnit_Framework_TestCase
]
];
Warning::suppress(Warning::RESOLVE_TYPE_WARNING);
Warning::suppress(Warning::FULL_SCHEMA_SCAN_WARNING);
$this->assertEquals($expected, Executor::execute($this->schema, $ast, $this->john)->toArray());
Warning::enable(Warning::RESOLVE_TYPE_WARNING);
Warning::enable(Warning::FULL_SCHEMA_SCAN_WARNING);
}
/**
@ -294,9 +294,9 @@ class UnionInterfaceTest extends \PHPUnit_Framework_TestCase
]
];
Warning::suppress(Warning::RESOLVE_TYPE_WARNING);
Warning::suppress(Warning::FULL_SCHEMA_SCAN_WARNING);
$this->assertEquals($expected, Executor::execute($this->schema, $ast, $this->john)->toArray());
Warning::enable(Warning::RESOLVE_TYPE_WARNING);
Warning::enable(Warning::FULL_SCHEMA_SCAN_WARNING);
}
/**
@ -351,9 +351,9 @@ class UnionInterfaceTest extends \PHPUnit_Framework_TestCase
]
];
Warning::suppress(Warning::RESOLVE_TYPE_WARNING);
Warning::suppress(Warning::FULL_SCHEMA_SCAN_WARNING);
$this->assertEquals($expected, Executor::execute($this->schema, $ast, $this->john)->toArray());
Warning::enable(Warning::RESOLVE_TYPE_WARNING);
Warning::enable(Warning::FULL_SCHEMA_SCAN_WARNING);
}
/**

View File

@ -235,6 +235,10 @@ class TypeLoaderTest extends \PHPUnit_Framework_TestCase
$input = $schema->getType('PostStoryMutationInput');
$this->assertSame($this->postStoryMutationInput, $input);
$this->assertEquals(['Node', 'Content', 'PostStoryMutationInput'], $this->calls);
$result = $schema->isPossibleType($this->node, $this->blogStory);
$this->assertTrue($result);
$this->assertEquals(['Node', 'Content', 'PostStoryMutationInput'], $this->calls);
}
public function testOnlyCallsLoaderOnce()
@ -316,4 +320,23 @@ class TypeLoaderTest extends \PHPUnit_Framework_TestCase
$schema->getType('Node');
}
public function testReturnsIdenticalResults()
{
$withoutLoader = new Schema([
'query' => $this->query,
'mutation' => $this->mutation
]);
$withLoader = new Schema([
'query' => $this->query,
'mutation' => $this->mutation,
'typeLoader' => $this->typeLoader
]);
$this->assertSame($withoutLoader->getQueryType(), $withLoader->getQueryType());
$this->assertSame($withoutLoader->getMutationType(), $withLoader->getMutationType());
$this->assertSame($withoutLoader->getType('BlogStory'), $withLoader->getType('BlogStory'));
$this->assertSame($withoutLoader->getDirectives(), $withLoader->getDirectives());
}
}