[JsonStreamer] Use composer package for RFC 8259 compliance tests

This commit is contained in:
Mathias Arlaud
2026-03-23 10:17:54 +01:00
parent a7a89a11b4
commit 046e795c0b
3 changed files with 68 additions and 318 deletions

View File

@@ -141,6 +141,7 @@
"guzzlehttp/guzzle": "^7.10",
"jolicode/jolinotif": "^2.7.2|^3.0",
"jsonpath-standard/jsonpath-compliance-test-suite": "*",
"nst/json-test-suite": "*",
"league/html-to-markdown": "^5.0",
"league/uri": "^6.5|^7.0",
"masterminds/html5": "^2.7.2",
@@ -245,6 +246,18 @@
}
}
},
{
"type": "package",
"package": {
"name": "nst/json-test-suite",
"version": "2024.12.14",
"source": {
"type": "git",
"url": "https://github.com/nst/JSONTestSuite.git",
"reference": "1ef36fa01286573e846ac449e8683f8833c5b26a"
}
}
},
{
"type": "package",
"package": {

View File

@@ -18,6 +18,8 @@ use Symfony\Component\JsonStreamer\Read\Lexer;
class LexerTest extends TestCase
{
private const RFC8259_TEST_SUITE_PATH = '/vendor/nst/json-test-suite';
public function testTokens()
{
$this->assertTokens([['1', 0]], '1');
@@ -43,11 +45,8 @@ class LexerTest extends TestCase
$this->assertTokens([[$veryLongString, 0]], $veryLongString);
}
/**
* Ensures that the lexer is compliant with RFC 8259.
*/
#[DataProvider('jsonDataProvider')]
public function testValidJson(string $name, string $json, bool $valid)
#[DataProvider('rfc8259ComplianceProvider')]
public function testRfc8259Compliance(string $name, string $json, bool $valid)
{
$resource = fopen('php://temp', 'w');
fwrite($resource, $json);
@@ -74,329 +73,52 @@ class LexerTest extends TestCase
}
/**
* Pulled from https://github.com/nst/JSONTestSuite.
*
* @return iterable<array{0: string, 1: string, 2: bool}>
*/
public static function jsonDataProvider(): iterable
public static function rfc8259ComplianceProvider(): iterable
{
yield ['array_1_true_without_comma', '[1 true]', false];
yield ['array_a_invalid_utf8', '[aå]', false];
yield ['array_colon_instead_of_comma', '["": 1]', false];
yield ['array_comma_after_close', '[""],', false];
yield ['array_comma_and_number', '[,1]', false];
yield ['array_double_comma', '[1,,2]', false];
yield ['array_double_extra_comma', '["x",,]', false];
yield ['array_extra_close', '["x"]]', false];
yield ['array_extra_comma', '["",]', false];
yield ['array_incomplete', '["x"', false];
yield ['array_incomplete_invalid_value', '[x', false];
yield ['array_inner_array_no_comma', '[3[4]]', false];
yield ['array_invalid_utf8', '[ÿ]', false];
yield ['array_items_separated_by_semicolon', '[1:2]', false];
yield ['array_just_comma', '[,]', false];
yield ['array_just_minus', '[-]', false];
yield ['array_missing_value', '[ , ""]', false];
yield ['array_newlines_unclosed', <<<JSON
["a",
4
,1,
JSON,
false,
];
yield ['array_number_and_comma', '[1,]', false];
yield ['array_number_and_several_commas', '[1,,]', false];
yield ['array_spaces_vertical_tab_formfeed', '["
a"\f]', false];
yield ['array_star_inside', '[*]', false];
yield ['array_unclosed', '[""', false];
yield ['array_unclosed_trailing_comma', '[1,', false];
yield ['array_unclosed_with_new_lines', <<<JSON
[1,
1
,1
JSON,
false,
];
yield ['array_unclosed_with_object_inside', '[{}', false];
yield ['incomplete_false', '[fals]', false];
yield ['incomplete_null', '[nul]', false];
yield ['incomplete_true', '[tru]', false];
yield ['multidigit_number_then_00', '123\\u0000', false];
yield ['number_++', '[++1234]', false];
yield ['number_+1', '[+1]', false];
yield ['number_+Inf', '[+Inf]', false];
yield ['number_-01', '[-01]', false];
yield ['number_-1.0.', '[-1.0.]', false];
yield ['number_-2.', '[-2.]', false];
yield ['number_-NaN', '[-NaN]', false];
yield ['number_.-1', '[.-1]', false];
yield ['number_.2e-3', '[.2e-3]', false];
yield ['number_0.1.2', '[0.1.2]', false];
yield ['number_0.3e+', '[0.3e+]', false];
yield ['number_0.3e', '[0.3e]', false];
yield ['number_0.e1', '[0.e1]', false];
yield ['number_0_capital_E+', '[0E+]', false];
yield ['number_0_capital_E', '[0E]', false];
yield ['number_0e+', '[0e+]', false];
yield ['number_0e', '[0e]', false];
yield ['number_1.0e+', '[1.0e+]', false];
yield ['number_1.0e-', '[1.0e-]', false];
yield ['number_1.0e', '[1.0e]', false];
yield ['number_1_000', '[1 000.0]', false];
yield ['number_1eE2', '[1eE2]', false];
yield ['number_2.e+3', '[2.e+3]', false];
yield ['number_2.e-3', '[2.e-3]', false];
yield ['number_2.e3', '[2.e3]', false];
yield ['number_9.e+', '[9.e+]', false];
yield ['number_expression', '[1+2]', false];
yield ['number_hex_1_digit', '[0x1]', false];
yield ['number_hex_2_digits', '[0x42]', false];
yield ['number_Inf', '[Inf]', false];
yield ['number_infinity', '[Infinity]', false];
yield ['number_invalid+-', '[0e+-1]', false];
yield ['number_invalid-negative-real', '[-123.123foo]', false];
yield ['number_invalid-utf-8-in-bigger-int', '[123å]', false];
yield ['number_invalid-utf-8-in-exponent', '[1e1å]', false];
yield ['number_invalid-utf-8-in-int', '[0å]', false];
yield ['number_minus_infinity', '[-Infinity]', false];
yield ['number_minus_sign_with_trailing_garbage', '[-foo]', false];
yield ['number_minus_space_1', '[- 1]', false];
yield ['number_NaN', '[NaN]', false];
yield ['number_neg_int_starting_with_zero', '[-012]', false];
yield ['number_neg_real_without_int_part', '[-.123]', false];
yield ['number_neg_with_garbage_at_end', '[-1x]', false];
yield ['number_real_garbage_after_e', '[1ea]', false];
yield ['number_real_with_invalid_utf8_after_e', '[1eå]', false];
yield ['number_real_without_fractional_part', '[1.]', false];
yield ['number_starting_with_dot', '[.123]', false];
yield ['number_U+FF11_fullwidth_digit_one', '[]', false];
yield ['number_with_alpha', '[1.2a-3]', false];
yield ['number_with_alpha_char', '[1.8011670033376514H-308]', false];
yield ['number_with_leading_zero', '[012]', false];
yield ['object_bad_value', '["x", truth]', false];
yield ['object_bracket_key', '{[: "x"}', false];
yield ['object_comma_instead_of_colon', '{"x", null}', false];
yield ['object_double_colon', '{"x"::"b"}', false];
yield ['object_emoji', '{🇨🇭}', false];
yield ['object_garbage_at_end', '{"a":"a" 123}', false];
yield ['object_key_with_single_quotes', '{key: \'value\'}', false];
yield ['object_lone_continuation_byte_in_key_and_trailing_comma', '{"¹":"0",}', false];
yield ['object_missing_colon', '{"a" b}', false];
yield ['object_missing_key', '{:"b"}', false];
yield ['object_missing_semicolon', '{"a" "b"}', false];
yield ['object_missing_value', '{"a":', false];
yield ['object_no-colon', '{"a"', false];
yield ['object_non_string_key', '{1:1}', false];
yield ['object_non_string_key_but_huge_number_instead', '{9999E9999:1}', false];
yield ['object_repeated_null_null', '{null:null,null:null}', false];
yield ['object_several_trailing_commas', '{"id":0,,,,,}', false];
yield ['object_single_quote', '{\'a\':0}', false];
yield ['object_trailing_comma', '{"id":0,}', false];
yield ['object_trailing_comment', '{"a":"b"}/**/', false];
yield ['object_trailing_comment_open', '{"a":"b"}/**//', false];
yield ['object_trailing_comment_slash_open', '{"a":"b"}//', false];
yield ['object_trailing_comment_slash_open_incomplete', '{"a":"b"}/', false];
yield ['object_two_commas_in_a_row', '{"a":"b",,"c":"d"}', false];
yield ['object_unquoted_key', '{a: "b"}', false];
yield ['object_unterminated-value', '{"a":"a', false];
yield ['object_with_single_string', '{ "foo" : "bar", "a" }', false];
yield ['object_with_trailing_garbage', '{"a":"b"}#', false];
yield ['single_space', ' ', false];
yield ['string_1_surrogate_then_escape', '["\\uD800\\"]', false];
yield ['string_1_surrogate_then_escape_u', '["\\uD800\\u"]', false];
yield ['string_1_surrogate_then_escape_u1', '["\\uD800\\u1"]', false];
yield ['string_1_surrogate_then_escape_u1x', '["\\uD800\\u1x"]', false];
yield ['string_accentuated_char_no_quotes', '[é]', false];
yield ['string_backslash_00', '["\\\\u0000]', false];
yield ['string_escape_x', '["\\x00"]', false];
yield ['string_escaped_backslash_bad', '["\\\\\\"]', false];
yield ['string_escaped_ctrl_char_tab', '["\\ "]', false];
yield ['string_escaped_emoji', '["\\🌀"]', false];
yield ['string_incomplete_escape', '["\\"]', false];
yield ['string_incomplete_escaped_character', '["\\u00A"]', false];
yield ['string_incomplete_surrogate', '["\\uD834\\uDd"]', false];
yield ['string_incomplete_surrogate_escape_invalid', '["\\uD800\\uD800\\x"]', false];
yield ['string_invalid-utf-8-in-escape', '["\\uå"]', false];
yield ['string_invalid_backslash_esc', '["\\a"]', false];
yield ['string_invalid_unicode_escape', '["\\uqqqq"]', false];
yield ['string_invalid_utf8_after_escape', '["\å"]', false];
yield ['string_leading_uescaped_thinspace', '[\u0020"asd"]', false];
yield ['string_no_quotes_with_bad_escape', '[\n]', false];
yield ['string_single_doublequote', '"', false];
yield ['string_single_quote', '[\'single quote\']', false];
yield ['string_single_string_no_double_quotes', 'abc', false];
yield ['string_start_escape_unclosed', '["\\', false];
yield ['string_unescaped_newline', <<<JSON
["new
line"]
JSON,
false,
];
yield ['string_unescaped_tab', '[" "]', false];
yield ['string_unicode_CapitalU', '"\\UA66D"', false];
yield ['string_with_trailing_garbage', '""x', false];
yield ['structure_angle_bracket_.', '<.>', false];
yield ['structure_angle_bracket_null', '[<null>]', false];
yield ['structure_array_trailing_garbage', '[1]x', false];
yield ['structure_array_with_extra_array_close', '[1]]', false];
yield ['structure_array_with_unclosed_string', '["asd]', false];
yield ['structure_ascii-unicode-identifier', 'aå', false];
yield ['structure_capitalized_True', '[True]', false];
yield ['structure_close_unopened_array', '1]', false];
yield ['structure_comma_instead_of_closing_brace', '{"x": true,', false];
yield ['structure_double_array', '[][]', false];
yield ['structure_end_array', ']', false];
yield ['structure_incomplete_UTF8_BOM', 'ï»{}', false];
yield ['structure_lone-invalid-utf-8', 'å', false];
yield ['structure_lone-open-bracket', '[', false];
yield ['structure_no_data', '', false];
yield ['structure_null-byte-outside-string', '[\\u0000]', false];
yield ['structure_number_with_trailing_garbage', '2@', false];
yield ['structure_object_followed_by_closing_object', '{}}', false];
yield ['structure_object_unclosed_no_value', '{"":', false];
yield ['structure_object_with_comment', '{"a":/*comment*/"b"}', false];
yield ['structure_object_with_trailing_garbage', '{"a": true} "x"', false];
yield ['structure_open_array_apostrophe', '[\'', false];
yield ['structure_open_array_comma', '[,', false];
yield ['structure_open_array_object', '[{', false];
yield ['structure_open_array_open_object', '[{"":[{"":', false];
yield ['structure_open_array_open_string', '["a', false];
yield ['structure_open_array_string', '["a"', false];
yield ['structure_open_object', '{', false];
yield ['structure_open_object_close_array', '{]', false];
yield ['structure_open_object_comma', '{,', false];
yield ['structure_open_object_open_array', '{[', false];
yield ['structure_open_object_open_string', '{"a', false];
yield ['structure_open_object_string_with_apostrophes', '{\'a\'', false];
yield ['structure_open_open', '["\\{["\\{["\\{["\\{', false];
yield ['structure_single_eacute', 'é', false];
yield ['structure_single_star', '*', false];
yield ['structure_trailing_#', '{"a":"b"}#{}', false];
yield ['structure_U+2060_word_joined', '[\\u2060]', false];
yield ['structure_uescaped_LF_before_string', '[\\u000A""]', false];
yield ['structure_unclosed_array', '[1', false];
yield ['structure_unclosed_array_partial_null', '[ false, nul', false];
yield ['structure_unclosed_array_unfinished_false', '[ true, fals', false];
yield ['structure_unclosed_array_unfinished_true', '[ false, tru', false];
yield ['structure_unclosed_object', '{"asd":"asd"', false];
yield ['structure_whitespace_formfeed', '[\\u000c]', false];
$testSuitePath = self::getRfc8259TestSuitePath();
if (null === $testSuitePath) {
yield 'json-test-suite-missing' => ['JSON Test Suite Missing', '{}', true];
yield ['array_arraysWithSpaces', '[[] ]', true];
yield ['array_empty-string', '[""]', true];
yield ['array_empty', '[]', true];
yield ['array_ending_with_newline', '["a"]', true];
yield ['array_false', '[false]', true];
yield ['array_heterogeneous', '[null, 1, "1", {}]', true];
yield ['array_null', '[null]', true];
yield ['array_with_1_and_newline', <<<JSON
[1
]
JSON,
true,
];
yield ['array_with_leading_space', '[1]', true];
yield ['array_with_several_null', '[1,null,null,null,2]', true];
yield ['array_with_trailing_space', '[2] ', true];
yield ['number', '[123e65]', true];
yield ['number_0e+1', '[0e+1]', true];
yield ['number_0e1', '[0e1]', true];
yield ['number_after_space', '[ 4]', true];
yield ['number_double_close_to_zero', '[-0.000000000000000000000000000000000000000000000000000000000000000000000000000001]', true];
yield ['number_int_with_exp', '[20e1]', true];
yield ['number_negative_int', '[-123]', true];
yield ['number_negative_one', '[-1]', true];
yield ['number_negative_zero', '[-0]', true];
yield ['number_real_capital_e', '[1E22]', true];
yield ['number_real_capital_e_neg_exp', '[1E-2]', true];
yield ['number_real_capital_e_pos_exp', '[1E+2]', true];
yield ['number_real_exponent', '[123e45]', true];
yield ['number_real_fraction_exponent', '[123.456e78]', true];
yield ['number_real_neg_exp', '[1e-2]', true];
yield ['number_real_pos_exponent', '[1e+2]', true];
yield ['number_simple_int', '[123]', true];
yield ['number_simple_real', '[123.456789]', true];
yield ['object', '{"asd":"sdf", "dfg":"fgh"}', true];
yield ['object_basic', '{"asd":"sdf"}', true];
yield ['object_empty', '{}', true];
yield ['object_empty_key', '{"":0}', true];
yield ['object_escaped_null_in_key', '{"foo\\u0000bar": 42}', true];
yield ['object_extreme_numbers', '{ "min": -1.0e+28, "max": 1.0e+28 }', true];
yield ['object_long_strings', '{"x":[{"id": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"}], "id": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"}', true];
yield ['object_simple', '{"a":[]}', true];
yield ['object_string_unicode', '{"title":"\\u041f\\u043e\\u043b\\u0442\\u043e\\u0440\\u0430 \\u0417\\u0435\\u043c\\u043b\\u0435\\u043a\\u043e\\u043f\\u0430" }', true];
yield ['object_with_newlines', <<<JSON
{
"a": "b"
}
JSON,
true,
];
yield ['string_1_2_3_bytes_UTF-8_sequences', '["\\u0060\\u012a\\u12AB"]', true];
yield ['string_accepted_surrogate_pair', '["\\uD801\\udc37"]', true];
yield ['string_accepted_surrogate_pairs', '["\\ud83d\\ude39\\ud83d\\udc8d"]', true];
yield ['string_allowed_escapes', '["\\"\\\\\\/\\b\\f\\n\\r\t"]', true];
yield ['string_backslash_and_u_escaped_zero', '["\\\\u0000"]', true];
yield ['string_backslash_doublequotes', '["\\""]', true];
yield ['string_comments', '["a/*b*/c/*d//e"]', true];
yield ['string_double_escape_a', '["\\\\a"]', true];
yield ['string_double_escape_n', '["\\\\n"]', true];
yield ['string_escaped_control_character', '["\\u0012"]', true];
yield ['string_escaped_noncharacter', '["\\uFFFF"]', true];
yield ['string_in_array', '["asd"]', true];
yield ['string_in_array_with_leading_space', '[ "asd"]', true];
yield ['string_last_surrogates_1_and_2', '["\uDBFF\uDFFF"]', true];
yield ['string_nbsp_uescaped', '["new\u00A0line"]', true];
yield ['string_nonCharacterInUTF-8_U+10FFFF', '["\\u10fff"]', true];
yield ['string_nonCharacterInUTF-8_U+FFFF', '["\\uffff"]', true];
yield ['string_null_escape', '["\\u0000"]', true];
yield ['string_one-byte-utf-8', '["\\u002c"]', true];
yield ['string_pi', '["π"]', true];
yield ['string_reservedCharacterInUTF-8_U+1BFFF', '["\\u1bfff𛿿"]', true];
yield ['string_simple_ascii', '["asd "]', true];
yield ['string_space', '" "', true];
yield ['string_surrogates_U+1D11E_MUSICAL_SYMBOL_G_CLEF', '["\\uD834\\uDd1e"]', true];
yield ['string_three-byte-utf-8', '["\\u0821"]', true];
yield ['string_two-byte-utf-8', '["\\u0123"]', true];
yield ['string_u+2028_line_sep', '["\\u2028"]', true];
yield ['string_u+2029_par_sep', '["\\u2029"]', true];
yield ['string_uEscape', '["\\u0061\\u30af\\u30EA\\u30b9"]', true];
yield ['string_uescaped_newline', '["new\\u000Aline"]', true];
yield ['string_unescaped_char_delete', '[""]', true];
yield ['string_unicode', '["\\uA66D"]', true];
yield ['string_unicode_2', '["⍂㈴⍂"]', true];
yield ['string_unicode_escaped_double_quote', '["\\u0022"]', true];
yield ['string_unicode_U+10FFFE_nonchar', '["\\uDBFF\\uDFFE"]', true];
yield ['string_unicode_U+1FFFE_nonchar', '["\\uD83F\\uDFFE"]', true];
yield ['string_unicode_U+200B_ZERO_WIDTH_SPACE', '["\\u200B"]', true];
yield ['string_unicode_U+2064_invisible_plus', '["\\u2064"]', true];
yield ['string_unicode_U+FDD0_nonchar', '["\\uFDD0"]', true];
yield ['string_unicode_U+FFFE_nonchar', '["\\uFFFE"]', true];
yield ['string_unicodeEscapedBackslash', '["\\u005C"]', true];
yield ['string_utf8', '["€𝄞"]', true];
yield ['string_with_del_character', '["aa"]', true];
yield ['structure_lonely_false', 'false', true];
yield ['structure_lonely_int', '69004', true];
yield ['structure_lonely_negative_real', '-0.1', true];
yield ['structure_lonely_null', 'null', true];
yield ['structure_lonely_string', '"asd"', true];
yield ['structure_lonely_true', 'true', true];
yield ['structure_string_empty', '""', true];
yield ['structure_trailing_newline', <<<JSON
["a"]
return;
}
JSON,
true,
];
yield ['structure_true_in_array', '[true]', true];
yield ['structure_whitespace_array', '[] ', true];
$skip = [];
// Contrary to what https://datatracker.ietf.org/doc/html/rfc8259 says,
// duplicate keys must result in error, see https://github.com/golang/go/discussions/63397.
// Therefore "object_duplicated_key" and "object_duplicated_key_and_value" are considered
// as invalid.
yield ['object_duplicated_key', '{"a":"b","a":"c"}', false];
yield ['object_duplicated_key_and_value', '{"a":"b","a":"b"}', false];
yield 'object_duplicated_key' => ['object_duplicated_key', '{"a":"b","a":"c"}', false];
$skip['y_object_duplicated_key.json'] = true;
yield 'object_duplicated_key_and_value' => ['object_duplicated_key_and_value', '{"a":"b","a":"b"}', false];
$skip['y_object_duplicated_key_and_value.json'] = true;
foreach (glob($testSuitePath.'/test_parsing/*.json') as $file) {
$filename = basename($file);
if (isset($skip[$filename]) || str_starts_with($filename, 'i_')) {
continue;
}
$name = substr($filename, 2, -5);
$valid = str_starts_with($filename, 'y_');
yield $name => [$name, file_get_contents($file), $valid];
}
}
private static function getRfc8259TestSuitePath(): ?string
{
$monorepoPath = \dirname(__DIR__, 6).self::RFC8259_TEST_SUITE_PATH;
$standalonePath = \dirname(__DIR__, 2).self::RFC8259_TEST_SUITE_PATH;
if (file_exists($monorepoPath)) {
return $monorepoPath;
}
return file_exists($standalonePath) ? $standalonePath : null;
}
private function assertTokens(array $tokens, string $content, int $offset = 0, ?int $length = null): void

View File

@@ -15,6 +15,20 @@
"homepage": "https://symfony.com/contributors"
}
],
"repositories": [
{
"type": "package",
"package": {
"name": "nst/json-test-suite",
"version": "2024.12.14",
"source": {
"type": "git",
"url": "https://github.com/nst/JSONTestSuite.git",
"reference": "1ef36fa01286573e846ac449e8683f8833c5b26a"
}
}
}
],
"require": {
"php": ">=8.4",
"psr/container": "^1.1|^2.0",
@@ -25,6 +39,7 @@
"symfony/var-exporter": "^7.4|^8.0"
},
"require-dev": {
"nst/json-test-suite": "*",
"phpstan/phpdoc-parser": "^1.0",
"symfony/config": "^7.4|^8.0",
"symfony/dependency-injection": "^7.4|^8.0",