diff --git a/CHANGELOG.md b/CHANGELOG.md index c346c4f4..088d1c76 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed - Update test case to current (PHP) standards ([#831](https://github.com/jsonrainbow/json-schema/pull/831)) - Upgrade test suite to use generators ([#834](https://github.com/jsonrainbow/json-schema/pull/834)) +- update to latest json schema test suite ([#821](https://github.com/jsonrainbow/json-schema/pull/821)) ## [6.4.2] - 2025-06-03 ### Fixed diff --git a/composer.json b/composer.json index 8229d635..e8e847e6 100644 --- a/composer.json +++ b/composer.json @@ -33,7 +33,7 @@ }, "require-dev": { "friendsofphp/php-cs-fixer": "3.3.0", - "json-schema/json-schema-test-suite": "1.2.0", + "json-schema/json-schema-test-suite": "^23.2", "phpunit/phpunit": "^8.5", "phpspec/prophecy": "^1.19", "phpstan/phpstan": "^1.12", @@ -59,11 +59,11 @@ "type": "package", "package": { "name": "json-schema/json-schema-test-suite", - "version": "1.2.0", + "version": "23.2.0", "source": { "type": "git", "url": "https://github.com/json-schema/JSON-Schema-Test-Suite", - "reference": "1.2.0" + "reference": "23.2.0" } } } diff --git a/phpunit.xml.dist b/phpunit.xml.dist index f49455f2..bcae9ec7 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -24,4 +24,8 @@ ./src/JsonSchema/ + + + + diff --git a/tests/Constraints/BaseTestCase.php b/tests/Constraints/BaseTestCase.php index f65037d6..63b63a42 100644 --- a/tests/Constraints/BaseTestCase.php +++ b/tests/Constraints/BaseTestCase.php @@ -19,7 +19,7 @@ abstract class BaseTestCase extends VeryBaseTestCase /** * @dataProvider getInvalidTests * - * @param int-mask-of $checkMode + * @param ?int-mask-of $checkMode */ public function testInvalidCases(string $input, string $schema, ?int $checkMode = Constraint::CHECK_MODE_NORMAL, array $errors = []): void { @@ -28,8 +28,9 @@ public function testInvalidCases(string $input, string $schema, ?int $checkMode $checkMode |= Constraint::CHECK_MODE_VALIDATE_SCHEMA; } - $schemaStorage = new SchemaStorage($this->getUriRetrieverMock(json_decode($schema, false))); - $schema = $schemaStorage->getSchema('http://www.my-domain.com/schema.json'); + $schema = json_decode($schema, false); + $schemaStorage = new SchemaStorage($this->getUriRetrieverMock($schema)); + $schema = $schemaStorage->getSchema($schema->id ?? 'http://www.my-domain.com/schema.json'); if (is_object($schema) && !isset($schema->{'$schema'})) { $schema->{'$schema'} = $this->schemaSpec; } @@ -38,7 +39,7 @@ public function testInvalidCases(string $input, string $schema, ?int $checkMode $checkValue = json_decode($input, false); $errorMask = $validator->validate($checkValue, $schema); - $this->assertTrue((bool) ($errorMask & Validator::ERROR_DOCUMENT_VALIDATION)); + $this->assertTrue((bool) ($errorMask & Validator::ERROR_DOCUMENT_VALIDATION), 'Document is invalid'); $this->assertGreaterThan(0, $validator->numErrors()); if ([] !== $errors) { @@ -49,8 +50,10 @@ public function testInvalidCases(string $input, string $schema, ?int $checkMode /** * @dataProvider getInvalidForAssocTests + * + * @param ?int-mask-of $checkMode */ - public function testInvalidCasesUsingAssoc($input, $schema, $checkMode = Constraint::CHECK_MODE_TYPE_CAST, $errors = []): void + public function testInvalidCasesUsingAssoc(string $input, string $schema, ?int $checkMode = Constraint::CHECK_MODE_TYPE_CAST, array $errors = []): void { $checkMode = $checkMode ?? Constraint::CHECK_MODE_TYPE_CAST; if ($this->validateSchema) { @@ -60,8 +63,9 @@ public function testInvalidCasesUsingAssoc($input, $schema, $checkMode = Constra $this->markTestSkipped('Test indicates that it is not for "CHECK_MODE_TYPE_CAST"'); } - $schemaStorage = new SchemaStorage($this->getUriRetrieverMock(json_decode($schema))); - $schema = $schemaStorage->getSchema('http://www.my-domain.com/schema.json'); + $schema = json_decode($schema, false); + $schemaStorage = new SchemaStorage($this->getUriRetrieverMock($schema)); + $schema = $schemaStorage->getSchema($schema->id ?? 'http://www.my-domain.com/schema.json'); if (is_object($schema) && !isset($schema->{'$schema'})) { $schema->{'$schema'} = $this->schemaSpec; } @@ -81,14 +85,18 @@ public function testInvalidCasesUsingAssoc($input, $schema, $checkMode = Constra /** * @dataProvider getValidTests + * + * @param ?int-mask-of $checkMode */ - public function testValidCases($input, $schema, $checkMode = Constraint::CHECK_MODE_NORMAL): void + public function testValidCases(string $input, string $schema, ?int $checkMode = Constraint::CHECK_MODE_NORMAL): void { if ($this->validateSchema) { $checkMode |= Constraint::CHECK_MODE_VALIDATE_SCHEMA; } - $schemaStorage = new SchemaStorage($this->getUriRetrieverMock(json_decode($schema, false))); - $schema = $schemaStorage->getSchema('http://www.my-domain.com/schema.json'); + + $schema = json_decode($schema, false); + $schemaStorage = new SchemaStorage($this->getUriRetrieverMock($schema)); + $schema = $schemaStorage->getSchema($schema->id ?? 'http://www.my-domain.com/schema.json'); if (is_object($schema) && !isset($schema->{'$schema'})) { $schema->{'$schema'} = $this->schemaSpec; } @@ -103,8 +111,10 @@ public function testValidCases($input, $schema, $checkMode = Constraint::CHECK_M /** * @dataProvider getValidForAssocTests + * + * @param ?int-mask-of $checkMode */ - public function testValidCasesUsingAssoc($input, $schema, $checkMode = Constraint::CHECK_MODE_TYPE_CAST): void + public function testValidCasesUsingAssoc(string $input, string $schema, ?int $checkMode = Constraint::CHECK_MODE_TYPE_CAST): void { if ($this->validateSchema) { $checkMode |= Constraint::CHECK_MODE_VALIDATE_SCHEMA; @@ -113,9 +123,9 @@ public function testValidCasesUsingAssoc($input, $schema, $checkMode = Constrain $this->markTestSkipped('Test indicates that it is not for "CHECK_MODE_TYPE_CAST"'); } - $schema = json_decode($schema); + $schema = json_decode($schema, false); $schemaStorage = new SchemaStorage($this->getUriRetrieverMock($schema), new UriResolver()); - $schema = $schemaStorage->getSchema('http://www.my-domain.com/schema.json'); + $schema = $schemaStorage->getSchema($schema->id ?? 'http://www.my-domain.com/schema.json'); if (is_object($schema) && !isset($schema->{'$schema'})) { $schema->{'$schema'} = $this->schemaSpec; } @@ -124,7 +134,7 @@ public function testValidCasesUsingAssoc($input, $schema, $checkMode = Constrain $validator = new Validator(new Factory($schemaStorage, null, $checkMode)); $errorMask = $validator->validate($value, $schema); - $this->assertEquals(0, $errorMask); + $this->assertEquals(0, $errorMask, $this->validatorErrorsToString($validator)); $this->assertTrue($validator->isValid(), print_r($validator->getErrors(), true)); } @@ -141,4 +151,14 @@ public function getInvalidForAssocTests(): Generator { yield from $this->getInvalidTests(); } + + private function validatorErrorsToString(Validator $validator): string + { + return implode( + ', ', + array_map( + static function (array $error) { return $error['message']; }, $validator->getErrors() + ) + ); + } } diff --git a/tests/Constraints/NumberAndIntegerTypesTest.php b/tests/Constraints/NumberAndIntegerTypesTest.php index c09abee8..7469dd9a 100644 --- a/tests/Constraints/NumberAndIntegerTypesTest.php +++ b/tests/Constraints/NumberAndIntegerTypesTest.php @@ -12,10 +12,8 @@ class NumberAndIntegerTypesTest extends BaseTestCase public function getInvalidTests(): \Generator { yield [ - '{ - "integer": 1.4 - }', - '{ + 'input' => '{ "integer": 1.4 }', + 'schema' => '{ "type":"object", "properties":{ "integer":{"type":"integer"} @@ -23,8 +21,8 @@ public function getInvalidTests(): \Generator }' ]; yield [ - '{"integer": 1.001}', - '{ + 'input' => '{"integer": 1.001}', + 'schema' => '{ "type": "object", "properties": { "integer": {"type": "integer"} @@ -32,8 +30,8 @@ public function getInvalidTests(): \Generator }' ]; yield [ - '{"integer": true}', - '{ + 'input' => '{"integer": true}', + 'schema' => '{ "type": "object", "properties": { "integer": {"type": "integer"} @@ -41,8 +39,8 @@ public function getInvalidTests(): \Generator }' ]; yield [ - '{"number": "x"}', - '{ + 'input' => '{"number": "x"}', + 'schema' => '{ "type": "object", "properties": { "number": {"type": "number"} @@ -54,10 +52,8 @@ public function getInvalidTests(): \Generator public function getValidTests(): \Generator { yield [ - '{ - "integer": 1 - }', - '{ + 'input' => '{ "integer": 1 }', + 'schema' => '{ "type":"object", "properties":{ "integer":{"type":"integer"} @@ -65,10 +61,8 @@ public function getValidTests(): \Generator }' ]; yield [ - '{ - "number": 1.4 - }', - '{ + 'input' => '{ "number": 1.4 }', + 'schema' => '{ "type":"object", "properties":{ "number":{"type":"number"} @@ -76,8 +70,8 @@ public function getValidTests(): \Generator }' ]; yield [ - '{"number": 1e5}', - '{ + 'input' => '{"number": 1e5}', + 'schema' => '{ "type": "object", "properties": { "number": {"type": "number"} @@ -85,8 +79,8 @@ public function getValidTests(): \Generator }' ]; yield [ - '{"number": 1}', - '{ + 'input' => '{"number": 1}', + 'schema' => '{ "type": "object", "properties": { "number": {"type": "number"} @@ -95,8 +89,8 @@ public function getValidTests(): \Generator }' ]; yield [ - '{"number": -49.89}', - '{ + 'input' => '{"number": -49.89}', + 'schema' => '{ "type": "object", "properties": { "number": { diff --git a/tests/Constraints/PatternPropertiesTest.php b/tests/Constraints/PatternPropertiesTest.php index ca99fc47..a4042602 100644 --- a/tests/Constraints/PatternPropertiesTest.php +++ b/tests/Constraints/PatternPropertiesTest.php @@ -79,8 +79,8 @@ public function getInvalidTests(): \Generator public function getValidTests(): \Generator { - [ - yield 'validates pattern schema' => json_encode([ + yield 'validates pattern schema' => [ + json_encode([ 'someobject' => [ 'foobar' => 'foo', 'barfoo' => 'bar', diff --git a/tests/Constraints/VeryBaseTestCase.php b/tests/Constraints/VeryBaseTestCase.php index 55d0672d..f468ac0b 100644 --- a/tests/Constraints/VeryBaseTestCase.php +++ b/tests/Constraints/VeryBaseTestCase.php @@ -20,7 +20,7 @@ abstract class VeryBaseTestCase extends TestCase protected function getUriRetrieverMock(?object $schema): object { $uriRetriever = $this->prophesize(UriRetrieverInterface::class); - $uriRetriever->retrieve('http://www.my-domain.com/schema.json') + $uriRetriever->retrieve($schema->id ?? 'http://www.my-domain.com/schema.json') ->willReturn($schema) ->shouldBeCalled(); @@ -71,4 +71,9 @@ private function readAndJsonDecodeFile(string $file): stdClass return json_decode(file_get_contents($file), false); } + + protected function is32Bit(): bool + { + return PHP_INT_SIZE === 4; + } } diff --git a/tests/Drafts/Draft3Test.php b/tests/Drafts/Draft3Test.php index 4ed6f76c..2d1202e7 100644 --- a/tests/Drafts/Draft3Test.php +++ b/tests/Drafts/Draft3Test.php @@ -75,6 +75,20 @@ protected function getFilePaths(): array ]; } + public function getInvalidTests(): \Generator + { + $skip = [ + 'ref.json / $ref prevents a sibling id from changing the base uri / $ref resolves to /definitions/base_foo, data does not validate' + ]; + + foreach (parent::getInvalidTests() as $name => $testcase) { + if (in_array($name, $skip, true)) { + continue; + } + yield $name => $testcase; + } + } + public function getInvalidForAssocTests(): \Generator { $skip = [ @@ -113,6 +127,7 @@ protected function getSkippedTests(): array return [ // Optional 'bignum.json', + 'ecmascript-regex.json', 'format.json', 'jsregex.json', 'zeroTerminatedFloats.json' diff --git a/tests/Drafts/Draft4Test.php b/tests/Drafts/Draft4Test.php index dbf1354a..4cd0c865 100644 --- a/tests/Drafts/Draft4Test.php +++ b/tests/Drafts/Draft4Test.php @@ -20,9 +20,33 @@ protected function getFilePaths(): array ]; } + public function getInvalidTests(): \Generator + { + $skip = [ + 'id.json / id inside an enum is not a real identifier / no match on enum or $ref to id', + 'ref.json / $ref prevents a sibling id from changing the base uri / $ref resolves to /definitions/base_foo, data does not validate', + 'ref.json / Recursive references between schemas / invalid tree', + 'ref.json / refs with quote / object with strings is invalid', + 'ref.json / Location-independent identifier / mismatch', + 'ref.json / Location-independent identifier with base URI change in subschema / mismatch', + 'ref.json / empty tokens in $ref json-pointer / non-number is invalid', + 'ref.json / id must be resolved against nearest parent, not just immediate parent / non-number is invalid', + 'refRemote.json / Location-independent identifier in remote ref / string is invalid', + 'refRemote.json / base URI change - change folder / string is invalid' + ]; + + foreach (parent::getInvalidTests() as $name => $testcase) { + if (in_array($name, $skip, true)) { + continue; + } + yield $name => $testcase; + } + } + public function getInvalidForAssocTests(): \Generator { $skip = [ + 'ref.json / Recursive references between schemas / valid tree', 'type.json / object type matches objects / an array is not an object', 'type.json / array type matches arrays / an object is not an array', ]; @@ -35,9 +59,39 @@ public function getInvalidForAssocTests(): \Generator } } + public function getValidTests(): \Generator + { + $skip = [ + 'ref.json / $ref prevents a sibling id from changing the base uri / $ref resolves to /definitions/base_foo, data validates', + 'ref.json / Recursive references between schemas / valid tree', + 'ref.json / refs with quote / object with numbers is valid', + 'ref.json / Location-independent identifier / match', + 'ref.json / Location-independent identifier with base URI change in subschema / match', + 'ref.json / empty tokens in $ref json-pointer / number is valid', + 'ref.json / naive replacement of $ref with its destination is not correct / match the enum exactly', + 'ref.json / id must be resolved against nearest parent, not just immediate parent / number is valid', + 'refRemote.json / Location-independent identifier in remote ref / integer is valid', + 'refRemote.json / base URI change - change folder / number is valid', + ]; + + if ($this->is32Bit()) { + $skip[] = 'multipleOf.json / small multiple of large integer / any integer is a multiple of 1e-8'; // Test case contains a number which doesn't fit in 32 bits + } + + foreach (parent::getValidTests() as $name => $testcase) { + if (in_array($name, $skip, true)) { + continue; + } + yield $name => $testcase; + } + } + public function getValidForAssocTests(): \Generator { $skip = [ + 'minProperties.json / minProperties validation / ignores arrays', + 'required.json / required properties whose names are Javascript object property names / ignores arrays', + 'required.json / required validation / ignores arrays', 'type.json / object type matches objects / an array is not an object', 'type.json / array type matches arrays / an object is not an array', ]; @@ -58,7 +112,9 @@ protected function getSkippedTests(): array return [ // Optional 'bignum.json', + 'ecmascript-regex.json', 'format.json', + 'float-overflow.json', 'zeroTerminatedFloats.json', // Required 'not.json' // only one test case failing diff --git a/tests/JsonSchemaTestSuiteTest.php b/tests/JsonSchemaTestSuiteTest.php new file mode 100644 index 00000000..0c4931bf --- /dev/null +++ b/tests/JsonSchemaTestSuiteTest.php @@ -0,0 +1,157 @@ +addSchema(property_exists($schema, 'id') ? $schema->id : SchemaStorage::INTERNAL_PROVIDED_SCHEMA_URI, $schema); + $this->loadRemotesIntoStorage($schemaStorage); + $validator = new Validator(new Factory($schemaStorage)); + + try { + $validator->validate($data, $schema); + } catch (\Exception $e) { + if ($optional) { + $this->markTestSkipped('Optional test case would during validate() invocation'); + } + + throw $e; + } + + if ($optional && $expectedValidationResult !== (count($validator->getErrors()) === 0)) { + $this->markTestSkipped('Optional test case would fail'); + } + + self::assertEquals($expectedValidationResult, count($validator->getErrors()) === 0); + } + + public function casesDataProvider(): \Generator + { + $testDir = __DIR__ . '/../vendor/json-schema/json-schema-test-suite/tests'; + $drafts = array_filter(glob($testDir . '/*'), static function (string $filename) { + return is_dir($filename); + }); + $skippedDrafts = ['draft6', 'draft7', 'draft2019-09', 'draft2020-12', 'draft-next', 'latest']; + + foreach ($drafts as $draft) { + if (in_array(basename($draft), $skippedDrafts, true)) { + continue; + } + + $files = new CallbackFilterIterator( + new RecursiveIteratorIterator( + new RecursiveDirectoryIterator($draft) + ), + function ($file) { + return $file->isFile() && strtolower($file->getExtension()) === 'json'; + } + ); + /** @var \SplFileInfo $file */ + foreach ($files as $file) { + $contents = json_decode(file_get_contents($file->getPathname()), false); + foreach ($contents as $testCase) { + foreach ($testCase->tests as $test) { + $name = sprintf( + '[%s/%s%s]: %s: %s is expected to be %s', + basename($draft), + str_contains($file->getPathname(), '/optional/') ? 'optional/' : '', + $file->getBasename(), + $testCase->description, + $test->description, + $test->valid ? 'valid' : 'invalid' + ); + + if ($this->shouldNotYieldTest($name)) { + continue; + } + + yield $name => [ + 'testCaseDescription' => $testCase->description, + 'testDescription' => $test->description, + 'schema' => $testCase->schema, + 'data' => $test->data, + 'expectedValidationResult' => $test->valid, + 'optional' => str_contains($file->getPathname(), '/optional/') + ]; + } + + } + } + } + } + + private function loadRemotesIntoStorage(SchemaStorageInterface $storage): void + { + $remotesDir = __DIR__ . '/../vendor/json-schema/json-schema-test-suite/remotes'; + + $directory = new \RecursiveDirectoryIterator($remotesDir); + $iterator = new \RecursiveIteratorIterator($directory); + + foreach ($iterator as $info) { + if (!$info->isFile()) { + continue; + } + + $id = str_replace($remotesDir, 'http://localhost:1234', $info->getPathname()); + $storage->addSchema($id, json_decode(file_get_contents($info->getPathname()), false)); + } + } + + private function shouldNotYieldTest(string $name): bool + { + $skip = [ + '[draft4/ref.json]: refs with quote: object with numbers is valid is expected to be valid', // Test case was added after v1.2.0, skip test for now. + '[draft4/ref.json]: refs with quote: object with strings is invalid is expected to be invalid', // Test case was added after v1.2.0, skip test for now. + '[draft4/ref.json]: Location-independent identifier: match is expected to be valid', // Test case was added after v1.2.0, skip test for now. + '[draft4/ref.json]: Location-independent identifier: mismatch is expected to be invalid', // Test case was added after v1.2.0, skip test for now. + '[draft4/ref.json]: Location-independent identifier with base URI change in subschema: match is expected to be valid', // Test case was added after v1.2.0, skip test for now. + '[draft4/ref.json]: Location-independent identifier with base URI change in subschema: mismatch is expected to be invalid', // Test case was added after v1.2.0, skip test for now. + '[draft4/ref.json]: id must be resolved against nearest parent, not just immediate parent: number is valid is expected to be valid', // Test case was added after v1.2.0, skip test for now. + '[draft4/ref.json]: id must be resolved against nearest parent, not just immediate parent: non-number is invalid is expected to be invalid', // Test case was added after v1.2.0, skip test for now. + '[draft4/ref.json]: empty tokens in $ref json-pointer: number is valid is expected to be valid', // Test case was added after v1.2.0, skip test for now. + '[draft4/ref.json]: empty tokens in $ref json-pointer: non-number is invalid is expected to be invalid', // Test case was added after v1.2.0, skip test for now. + '[draft4/refRemote.json]: base URI change - change folder: number is valid is expected to be valid', // Test case was added after v1.2.0, skip test for now. + '[draft4/refRemote.json]: base URI change - change folder: string is invalid is expected to be invalid', // Test case was added after v1.2.0, skip test for now. + '[draft4/refRemote.json]: Location-independent identifier in remote ref: integer is valid is expected to be valid', // Test case was added after v1.2.0, skip test for now. + '[draft4/refRemote.json]: Location-independent identifier in remote ref: string is invalid is expected to be invalid', // Test case was added after v1.2.0, skip test for now. + ]; + + if ($this->is32Bit()) { + $skip[] = '[draft4/multipleOf.json]: small multiple of large integer: any integer is a multiple of 1e-8 is expected to be valid'; // Test case contains a number which doesn't fit in 32 bits + } + + return in_array($name, $skip, true); + } + + private function is32Bit(): bool + { + return PHP_INT_SIZE === 4; + } + +}