diff --git a/.gitignore b/.gitignore index 04b69a41..e2e8af29 100644 --- a/.gitignore +++ b/.gitignore @@ -4,6 +4,7 @@ !/bin/update-changelog.sh !/bin/prepare-release.sh !/bin/extract-release-notes.sh +!/bin/run-test-case coverage .buildpath .project diff --git a/bin/run-test-case b/bin/run-test-case new file mode 100755 index 00000000..ff2e867b --- /dev/null +++ b/bin/run-test-case @@ -0,0 +1,227 @@ +#!/usr/bin/env php + "/" + +Arguments: + test-file Path to a test JSON file. Resolved in order: + 1. As given (absolute or relative to CWD) + 2. Relative to the bundled test suite root + description "/" — split on the first "/" only. + Exact match is tried first, then case-insensitive substring. + +Options: + -v --verbose Also print the schema + -h --help Show this help + +Exit codes: + 0 Test found and result matches expected + 1 Test found but result does not match expected + 2 Usage error, file not found, or test not found + +HLP; + exit(0); +} + +$suiteRoot = $projectDirectory . '/vendor/json-schema/json-schema-test-suite/tests'; +$remotesDir = $projectDirectory . '/vendor/json-schema/json-schema-test-suite/remotes'; + +// Resolve test file path: as-is first, then relative to suite root +$filePath = $arArgs[0]; +if (!file_exists($filePath)) { + $filePath = $suiteRoot . '/' . $arArgs[0]; + if (!file_exists($filePath)) { + echo "Error: test file not found: {$arArgs[0]}\n"; + exit(2); + } +} +$filePath = (string) realpath($filePath); + +// Detect draft name from directory component (e.g. draft4, draft7, draft2019-09) +$draft = null; +if (preg_match('/(draft[^\/]+)/i', str_replace('\\', '/', $filePath), $m)) { + $draft = strtolower($m[1]); +} + +// Parse "/" — split on first "/" only so slashes in test descriptions are preserved +$descParts = explode('/', $arArgs[1], 2); +$groupDesc = $descParts[0]; +$testDesc = $descParts[1] ?? null; + +// Load test file +$contents = json_decode((string) file_get_contents($filePath), false); +if (!is_array($contents)) { + echo "Error: invalid test file format\n"; + exit(2); +} + +// Search: exact match first, then case-insensitive substring +// $matchedGroup tracks a group that matched even when no test inside it matched +$foundGroup = null; +$foundTest = null; +$matchedGroup = null; + +foreach ([true, false] as $exact) { + foreach ($contents as $group) { + $groupMatch = $exact + ? $group->description === $groupDesc + : stripos($group->description, $groupDesc) !== false; + + if (!$groupMatch) { + continue; + } + + if ($testDesc === null) { + $foundGroup = $group; + break 2; + } + + $matchedGroup = $group; + + foreach ($group->tests as $test) { + $testMatch = $exact + ? $test->description === $testDesc + : stripos($test->description, $testDesc) !== false; + + if ($testMatch) { + $foundGroup = $group; + $foundTest = $test; + break 3; + } + } + } +} + +if ($matchedGroup === null && $foundGroup === null) { + echo "Error: no matching group found for: {$groupDesc}\n"; + echo "Available groups:\n"; + foreach ($contents as $group) { + echo " - {$group->description}\n"; + } + exit(2); +} + +if ($testDesc !== null && $foundTest === null) { + echo "Error: no matching test found for: {$testDesc}\n"; + $listGroup = $matchedGroup ?? $foundGroup; + echo "Available tests in \"{$listGroup->description}\":\n"; + foreach ($listGroup->tests as $test) { + $expect = $test->valid ? 'valid' : 'invalid'; + echo " [{$expect}] {$test->description}\n"; + } + exit(2); +} + +// No test description given — list tests in the matched group +if ($foundTest === null) { + echo "Group: {$foundGroup->description}\n"; + echo "Tests:\n"; + foreach ($foundGroup->tests as $test) { + $expect = $test->valid ? 'valid' : 'invalid'; + echo " [{$expect}] {$test->description}\n"; + } + exit(0); +} + +// Determine check mode (mirrors JsonSchemaTestSuiteTest::getCheckModeForDraft) +$checkMode = in_array($draft, ['draft6', 'draft7'], true) + ? Constraint::CHECK_MODE_NORMAL | Constraint::CHECK_MODE_STRICT + : Constraint::CHECK_MODE_NORMAL; + +try { + $draftIdentifier = DraftIdentifiers::fromConstraintName($draft ?? 'draft4'); +} catch (\InvalidArgumentException $e) { + $draftIdentifier = DraftIdentifiers::DRAFT_4(); +} + +// Set up validator (mirrors JsonSchemaTestSuiteTest::testTestCaseValidatesCorrectly) +$schema = $foundGroup->schema; +$schemaId = is_object($schema) && property_exists($schema, 'id') + ? $schema->id + : SchemaStorage::INTERNAL_PROVIDED_SCHEMA_URI; + +$schemaStorage = new SchemaStorage(); +$schemaStorage->addSchema($schemaId, $schema); + +if (is_dir($remotesDir)) { + $iterator = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($remotesDir)); + foreach ($iterator as $info) { + if (!$info->isFile()) { + continue; + } + $remoteId = str_replace($remotesDir, 'http://localhost:1234', $info->getPathname()); + $schemaStorage->addSchema($remoteId, json_decode((string) file_get_contents($info->getPathname()), false)); + } +} + +$factory = new Factory($schemaStorage); +$factory->setDefaultDialect($draftIdentifier->getValue()); +$validator = new Validator($factory); + +try { + $validator->validate($foundTest->data, $schema, $checkMode); +} catch (\Exception $e) { + echo "Error during validation: " . $e->getMessage() . "\n"; + exit(2); +} + +$isValid = count($validator->getErrors()) === 0; +$passed = $isValid === $foundTest->valid; + +echo "Group : {$foundGroup->description}\n"; +echo "Test : {$foundTest->description}\n"; +if (isset($arOptions['--verbose']) || isset($arOptions['-v'])) { + echo "Schema: " . json_encode($schema, JSON_PRETTY_PRINT) . "\n"; +} +echo "Data : " . json_encode($foundTest->data) . "\n"; +echo "Expect: " . ($foundTest->valid ? 'valid' : 'invalid') . "\n"; +echo "Result: " . ($passed ? 'PASS' : 'FAIL') . "\n"; + +if (!$passed) { + if ($isValid) { + echo "\nValidator returned valid but the test case expects invalid.\n"; + } else { + echo "\nValidation errors:\n"; + foreach ($validator->getErrors() as $error) { + echo sprintf(" [%s] %s\n", $error['property'], $error['message']); + } + } +} + +exit($passed ? 0 : 1);