From fe9863fd556f42d6d8c46ddcbf89174d84eea34c Mon Sep 17 00:00:00 2001 From: Amir Sarabadani Date: Mon, 8 Jul 2019 00:23:30 +0200 Subject: [PATCH] Move unit tests FormatJsonTest.php to a dedicated file in unit tests Out of 140 tests of this file, 131 one of them are pure unit test Let's keep the 9 in the original file and move the rest Bug: T87781 Change-Id: I86dfe17f794c615048b3c20487b0e84d38d13b93 --- .../phpunit/includes/json/FormatJsonTest.php | 354 ----------------- .../unit/includes/json/FormatJsonUnitTest.php | 362 ++++++++++++++++++ 2 files changed, 362 insertions(+), 354 deletions(-) create mode 100644 tests/phpunit/unit/includes/json/FormatJsonUnitTest.php diff --git a/tests/phpunit/includes/json/FormatJsonTest.php b/tests/phpunit/includes/json/FormatJsonTest.php index a6adf343d5..1a99775833 100644 --- a/tests/phpunit/includes/json/FormatJsonTest.php +++ b/tests/phpunit/includes/json/FormatJsonTest.php @@ -5,179 +5,6 @@ */ class FormatJsonTest extends MediaWikiTestCase { - public static function provideEncoderPrettyPrinting() { - return [ - // Four spaces - [ true, ' ' ], - [ ' ', ' ' ], - // Two spaces - [ ' ', ' ' ], - // One tab - [ "\t", "\t" ], - ]; - } - - /** - * @dataProvider provideEncoderPrettyPrinting - */ - public function testEncoderPrettyPrinting( $pretty, $expectedIndent ) { - $obj = [ - 'emptyObject' => new stdClass, - 'emptyArray' => [], - 'string' => 'foobar\\', - 'filledArray' => [ - [ - 123, - 456, - ], - // Nested json works without problems - '"7":["8",{"9":"10"}]', - // Whitespace clean up doesn't touch strings that look alike - "{\n\t\"emptyObject\": {\n\t},\n\t\"emptyArray\": [ ]\n}", - ], - ]; - - // No trailing whitespace, no trailing linefeed - $json = '{ - "emptyObject": {}, - "emptyArray": [], - "string": "foobar\\\\", - "filledArray": [ - [ - 123, - 456 - ], - "\"7\":[\"8\",{\"9\":\"10\"}]", - "{\n\t\"emptyObject\": {\n\t},\n\t\"emptyArray\": [ ]\n}" - ] -}'; - - $json = str_replace( "\r", '', $json ); // Windows compat - $json = str_replace( "\t", $expectedIndent, $json ); - $this->assertSame( $json, FormatJson::encode( $obj, $pretty ) ); - } - - public static function provideEncodeDefault() { - return self::getEncodeTestCases( [] ); - } - - /** - * @dataProvider provideEncodeDefault - */ - public function testEncodeDefault( $from, $to ) { - $this->assertSame( $to, FormatJson::encode( $from ) ); - } - - public static function provideEncodeUtf8() { - return self::getEncodeTestCases( [ 'unicode' ] ); - } - - /** - * @dataProvider provideEncodeUtf8 - */ - public function testEncodeUtf8( $from, $to ) { - $this->assertSame( $to, FormatJson::encode( $from, false, FormatJson::UTF8_OK ) ); - } - - public static function provideEncodeXmlMeta() { - return self::getEncodeTestCases( [ 'xmlmeta' ] ); - } - - /** - * @dataProvider provideEncodeXmlMeta - */ - public function testEncodeXmlMeta( $from, $to ) { - $this->assertSame( $to, FormatJson::encode( $from, false, FormatJson::XMLMETA_OK ) ); - } - - public static function provideEncodeAllOk() { - return self::getEncodeTestCases( [ 'unicode', 'xmlmeta' ] ); - } - - /** - * @dataProvider provideEncodeAllOk - */ - public function testEncodeAllOk( $from, $to ) { - $this->assertSame( $to, FormatJson::encode( $from, false, FormatJson::ALL_OK ) ); - } - - public function testEncodePhpBug46944() { - $this->assertNotEquals( - '\ud840\udc00', - strtolower( FormatJson::encode( "\xf0\xa0\x80\x80" ) ), - 'Test encoding an broken json_encode character (U+20000)' - ); - } - - public function testEncodeFail() { - // Set up a recursive object that can't be encoded. - $a = new stdClass; - $b = new stdClass; - $a->b = $b; - $b->a = $a; - $this->assertFalse( FormatJson::encode( $a ) ); - } - - public function testDecodeReturnType() { - $this->assertInternalType( - 'object', - FormatJson::decode( '{"Name": "Cheeso", "Rank": 7}' ), - 'Default to object' - ); - - $this->assertInternalType( - 'array', - FormatJson::decode( '{"Name": "Cheeso", "Rank": 7}', true ), - 'Optional array' - ); - } - - public static function provideParse() { - return [ - [ null ], - [ true ], - [ false ], - [ 0 ], - [ 1 ], - [ 1.2 ], - [ '' ], - [ 'str' ], - [ [ 0, 1, 2 ] ], - [ [ 'a' => 'b' ] ], - [ [ 'a' => 'b' ] ], - [ [ 'a' => 'b', 'x' => [ 'c' => 'd' ] ] ], - ]; - } - - /** - * Recursively convert arrays into stdClass - * @param array|string|bool|int|float|null $value - * @return stdClass|string|bool|int|float|null - */ - public static function toObject( $value ) { - return !is_array( $value ) ? $value : (object)array_map( __METHOD__, $value ); - } - - /** - * @dataProvider provideParse - * @param mixed $value - */ - public function testParse( $value ) { - $expected = self::toObject( $value ); - $json = FormatJson::encode( $expected, false, FormatJson::ALL_OK ); - $this->assertJson( $json ); - - $st = FormatJson::parse( $json ); - $this->assertInstanceOf( Status::class, $st ); - $this->assertTrue( $st->isGood() ); - $this->assertEquals( $expected, $st->getValue() ); - - $st = FormatJson::parse( $json, FormatJson::FORCE_ASSOC ); - $this->assertInstanceOf( Status::class, $st ); - $this->assertTrue( $st->isGood() ); - $this->assertEquals( $value, $st->getValue() ); - } - /** * Test data for testParseTryFixing. * @@ -252,185 +79,4 @@ class FormatJsonTest extends MediaWikiTestCase { } } - public static function provideParseErrors() { - return [ - [ 'aaa' ], - [ '{"j": 1 ] }' ], - ]; - } - - /** - * @dataProvider provideParseErrors - * @param mixed $value - */ - public function testParseErrors( $value ) { - $st = FormatJson::parse( $value ); - $this->assertInstanceOf( Status::class, $st ); - $this->assertFalse( $st->isOK() ); - } - - public function provideStripComments() { - return [ - [ '{"a":"b"}', '{"a":"b"}' ], - [ "{\"a\":\"b\"}\n", "{\"a\":\"b\"}\n" ], - [ '/*c*/{"c":"b"}', '{"c":"b"}' ], - [ '{"a":"c"}/*c*/', '{"a":"c"}' ], - [ '/*c//d*/{"c":"b"}', '{"c":"b"}' ], - [ '{/*c*/"c":"b"}', '{"c":"b"}' ], - [ "/*\nc\r\n*/{\"c\":\"b\"}", '{"c":"b"}' ], - [ "//c\n{\"c\":\"b\"}", '{"c":"b"}' ], - [ "//c\r\n{\"c\":\"b\"}", '{"c":"b"}' ], - [ '{"a":"c"}//c', '{"a":"c"}' ], - [ "{\"a-c\"://c\n\"b\"}", '{"a-c":"b"}' ], - [ '{"/*a":"b"}', '{"/*a":"b"}' ], - [ '{"a":"//b"}', '{"a":"//b"}' ], - [ '{"a":"b/*c*/"}', '{"a":"b/*c*/"}' ], - [ "{\"\\\"/*a\":\"b\"}", "{\"\\\"/*a\":\"b\"}" ], - [ '', '' ], - [ '/*c', '' ], - [ '//c', '' ], - [ '"http://example.com"', '"http://example.com"' ], - [ "\0", "\0" ], - [ '"Blåbærsyltetøy"', '"Blåbærsyltetøy"' ], - ]; - } - - /** - * @covers FormatJson::stripComments - * @dataProvider provideStripComments - * @param string $json - * @param string $expect - */ - public function testStripComments( $json, $expect ) { - $this->assertSame( $expect, FormatJson::stripComments( $json ) ); - } - - public function provideParseStripComments() { - return [ - [ '/* blah */true', true ], - [ "// blah \ntrue", true ], - [ '[ "a" , /* blah */ "b" ]', [ 'a', 'b' ] ], - ]; - } - - /** - * @covers FormatJson::parse - * @covers FormatJson::stripComments - * @dataProvider provideParseStripComments - * @param string $json - * @param mixed $expect - */ - public function testParseStripComments( $json, $expect ) { - $st = FormatJson::parse( $json, FormatJson::STRIP_COMMENTS ); - $this->assertInstanceOf( Status::class, $st ); - $this->assertTrue( $st->isGood() ); - $this->assertEquals( $expect, $st->getValue() ); - } - - /** - * Generate a set of test cases for a particular combination of encoder options. - * - * @param array $unescapedGroups List of character groups to leave unescaped - * @return array Arrays of unencoded strings and corresponding encoded strings - */ - private static function getEncodeTestCases( array $unescapedGroups ) { - $groups = [ - 'always' => [ - // Forward slash (always unescaped) - '/' => '/', - - // Control characters - "\0" => '\u0000', - "\x08" => '\b', - "\t" => '\t', - "\n" => '\n', - "\r" => '\r', - "\f" => '\f', - "\x1f" => '\u001f', // representative example - - // Double quotes - '"' => '\"', - - // Backslashes - '\\' => '\\\\', - '\\\\' => '\\\\\\\\', - '\\u00e9' => '\\\u00e9', // security check for Unicode unescaping - - // Line terminators - "\xe2\x80\xa8" => '\u2028', - "\xe2\x80\xa9" => '\u2029', - ], - 'unicode' => [ - "\xc3\xa9" => '\u00e9', - "\xf0\x9d\x92\x9e" => '\ud835\udc9e', // U+1D49E, outside the BMP - ], - 'xmlmeta' => [ - '<' => '\u003C', // JSON_HEX_TAG uses uppercase hex digits - '>' => '\u003E', - '&' => '\u0026', - ], - ]; - - $cases = []; - foreach ( $groups as $name => $rules ) { - $leaveUnescaped = in_array( $name, $unescapedGroups ); - foreach ( $rules as $from => $to ) { - $cases[] = [ $from, '"' . ( $leaveUnescaped ? $from : $to ) . '"' ]; - } - } - - return $cases; - } - - public function provideEmptyJsonKeyStrings() { - return [ - [ - '{"":"foo"}', - '{"":"foo"}', - '' - ], - [ - '{"_empty_":"foo"}', - '{"_empty_":"foo"}', - '_empty_' ], - [ - '{"\u005F\u0065\u006D\u0070\u0074\u0079\u005F":"foo"}', - '{"_empty_":"foo"}', - '_empty_' - ], - [ - '{"_empty_":"bar","":"foo"}', - '{"_empty_":"bar","":"foo"}', - '' - ], - [ - '{"":"bar","_empty_":"foo"}', - '{"":"bar","_empty_":"foo"}', - '_empty_' - ] - ]; - } - - /** - * @covers FormatJson::encode - * @covers FormatJson::decode - * @dataProvider provideEmptyJsonKeyStrings - * @param string $json - * - * Decoding behavior with empty keys can be surprising. - * See https://phabricator.wikimedia.org/T206411 - */ - public function testEmptyJsonKeyArray( $json, $expect, $php71Name ) { - // Decoding to array is consistent across supported PHP versions - $this->assertSame( $expect, FormatJson::encode( - FormatJson::decode( $json, true ) ) ); - - // Decoding to object differs between supported PHP versions - $obj = FormatJson::decode( $json ); - if ( version_compare( PHP_VERSION, '7.1', '<' ) ) { - $this->assertEquals( 'foo', $obj->_empty_ ); - } else { - $this->assertEquals( 'foo', $obj->{$php71Name} ); - } - } } diff --git a/tests/phpunit/unit/includes/json/FormatJsonUnitTest.php b/tests/phpunit/unit/includes/json/FormatJsonUnitTest.php new file mode 100644 index 0000000000..58ad495518 --- /dev/null +++ b/tests/phpunit/unit/includes/json/FormatJsonUnitTest.php @@ -0,0 +1,362 @@ + new stdClass, + 'emptyArray' => [], + 'string' => 'foobar\\', + 'filledArray' => [ + [ + 123, + 456, + ], + // Nested json works without problems + '"7":["8",{"9":"10"}]', + // Whitespace clean up doesn't touch strings that look alike + "{\n\t\"emptyObject\": {\n\t},\n\t\"emptyArray\": [ ]\n}", + ], + ]; + + // No trailing whitespace, no trailing linefeed + $json = '{ + "emptyObject": {}, + "emptyArray": [], + "string": "foobar\\\\", + "filledArray": [ + [ + 123, + 456 + ], + "\"7\":[\"8\",{\"9\":\"10\"}]", + "{\n\t\"emptyObject\": {\n\t},\n\t\"emptyArray\": [ ]\n}" + ] +}'; + + $json = str_replace( "\r", '', $json ); // Windows compat + $json = str_replace( "\t", $expectedIndent, $json ); + $this->assertSame( $json, FormatJson::encode( $obj, $pretty ) ); + } + + public static function provideEncodeDefault() { + return self::getEncodeTestCases( [] ); + } + + /** + * @dataProvider provideEncodeDefault + */ + public function testEncodeDefault( $from, $to ) { + $this->assertSame( $to, FormatJson::encode( $from ) ); + } + + public static function provideEncodeUtf8() { + return self::getEncodeTestCases( [ 'unicode' ] ); + } + + /** + * @dataProvider provideEncodeUtf8 + */ + public function testEncodeUtf8( $from, $to ) { + $this->assertSame( $to, FormatJson::encode( $from, false, FormatJson::UTF8_OK ) ); + } + + public static function provideEncodeXmlMeta() { + return self::getEncodeTestCases( [ 'xmlmeta' ] ); + } + + /** + * @dataProvider provideEncodeXmlMeta + */ + public function testEncodeXmlMeta( $from, $to ) { + $this->assertSame( $to, FormatJson::encode( $from, false, FormatJson::XMLMETA_OK ) ); + } + + public static function provideEncodeAllOk() { + return self::getEncodeTestCases( [ 'unicode', 'xmlmeta' ] ); + } + + /** + * @dataProvider provideEncodeAllOk + */ + public function testEncodeAllOk( $from, $to ) { + $this->assertSame( $to, FormatJson::encode( $from, false, FormatJson::ALL_OK ) ); + } + + public function testEncodePhpBug46944() { + $this->assertNotEquals( + '\ud840\udc00', + strtolower( FormatJson::encode( "\xf0\xa0\x80\x80" ) ), + 'Test encoding an broken json_encode character (U+20000)' + ); + } + + public function testEncodeFail() { + // Set up a recursive object that can't be encoded. + $a = new stdClass; + $b = new stdClass; + $a->b = $b; + $b->a = $a; + $this->assertFalse( FormatJson::encode( $a ) ); + } + + public function testDecodeReturnType() { + $this->assertInternalType( + 'object', + FormatJson::decode( '{"Name": "Cheeso", "Rank": 7}' ), + 'Default to object' + ); + + $this->assertInternalType( + 'array', + FormatJson::decode( '{"Name": "Cheeso", "Rank": 7}', true ), + 'Optional array' + ); + } + + public static function provideParse() { + return [ + [ null ], + [ true ], + [ false ], + [ 0 ], + [ 1 ], + [ 1.2 ], + [ '' ], + [ 'str' ], + [ [ 0, 1, 2 ] ], + [ [ 'a' => 'b' ] ], + [ [ 'a' => 'b' ] ], + [ [ 'a' => 'b', 'x' => [ 'c' => 'd' ] ] ], + ]; + } + + /** + * Recursively convert arrays into stdClass + * @param array|string|bool|int|float|null $value + * @return stdClass|string|bool|int|float|null + */ + public static function toObject( $value ) { + return !is_array( $value ) ? $value : (object)array_map( __METHOD__, $value ); + } + + /** + * @dataProvider provideParse + * @param mixed $value + */ + public function testParse( $value ) { + $expected = self::toObject( $value ); + $json = FormatJson::encode( $expected, false, FormatJson::ALL_OK ); + $this->assertJson( $json ); + + $st = FormatJson::parse( $json ); + $this->assertInstanceOf( Status::class, $st ); + $this->assertTrue( $st->isGood() ); + $this->assertEquals( $expected, $st->getValue() ); + + $st = FormatJson::parse( $json, FormatJson::FORCE_ASSOC ); + $this->assertInstanceOf( Status::class, $st ); + $this->assertTrue( $st->isGood() ); + $this->assertEquals( $value, $st->getValue() ); + } + + public static function provideParseErrors() { + return [ + [ 'aaa' ], + [ '{"j": 1 ] }' ], + ]; + } + + /** + * @dataProvider provideParseErrors + * @param mixed $value + */ + public function testParseErrors( $value ) { + $st = FormatJson::parse( $value ); + $this->assertInstanceOf( Status::class, $st ); + $this->assertFalse( $st->isOK() ); + } + + public function provideStripComments() { + return [ + [ '{"a":"b"}', '{"a":"b"}' ], + [ "{\"a\":\"b\"}\n", "{\"a\":\"b\"}\n" ], + [ '/*c*/{"c":"b"}', '{"c":"b"}' ], + [ '{"a":"c"}/*c*/', '{"a":"c"}' ], + [ '/*c//d*/{"c":"b"}', '{"c":"b"}' ], + [ '{/*c*/"c":"b"}', '{"c":"b"}' ], + [ "/*\nc\r\n*/{\"c\":\"b\"}", '{"c":"b"}' ], + [ "//c\n{\"c\":\"b\"}", '{"c":"b"}' ], + [ "//c\r\n{\"c\":\"b\"}", '{"c":"b"}' ], + [ '{"a":"c"}//c', '{"a":"c"}' ], + [ "{\"a-c\"://c\n\"b\"}", '{"a-c":"b"}' ], + [ '{"/*a":"b"}', '{"/*a":"b"}' ], + [ '{"a":"//b"}', '{"a":"//b"}' ], + [ '{"a":"b/*c*/"}', '{"a":"b/*c*/"}' ], + [ "{\"\\\"/*a\":\"b\"}", "{\"\\\"/*a\":\"b\"}" ], + [ '', '' ], + [ '/*c', '' ], + [ '//c', '' ], + [ '"http://example.com"', '"http://example.com"' ], + [ "\0", "\0" ], + [ '"Blåbærsyltetøy"', '"Blåbærsyltetøy"' ], + ]; + } + + /** + * @covers FormatJson::stripComments + * @dataProvider provideStripComments + * @param string $json + * @param string $expect + */ + public function testStripComments( $json, $expect ) { + $this->assertSame( $expect, FormatJson::stripComments( $json ) ); + } + + public function provideParseStripComments() { + return [ + [ '/* blah */true', true ], + [ "// blah \ntrue", true ], + [ '[ "a" , /* blah */ "b" ]', [ 'a', 'b' ] ], + ]; + } + + /** + * @covers FormatJson::parse + * @covers FormatJson::stripComments + * @dataProvider provideParseStripComments + * @param string $json + * @param mixed $expect + */ + public function testParseStripComments( $json, $expect ) { + $st = FormatJson::parse( $json, FormatJson::STRIP_COMMENTS ); + $this->assertInstanceOf( Status::class, $st ); + $this->assertTrue( $st->isGood() ); + $this->assertEquals( $expect, $st->getValue() ); + } + + /** + * Generate a set of test cases for a particular combination of encoder options. + * + * @param array $unescapedGroups List of character groups to leave unescaped + * @return array Arrays of unencoded strings and corresponding encoded strings + */ + private static function getEncodeTestCases( array $unescapedGroups ) { + $groups = [ + 'always' => [ + // Forward slash (always unescaped) + '/' => '/', + + // Control characters + "\0" => '\u0000', + "\x08" => '\b', + "\t" => '\t', + "\n" => '\n', + "\r" => '\r', + "\f" => '\f', + "\x1f" => '\u001f', // representative example + + // Double quotes + '"' => '\"', + + // Backslashes + '\\' => '\\\\', + '\\\\' => '\\\\\\\\', + '\\u00e9' => '\\\u00e9', // security check for Unicode unescaping + + // Line terminators + "\xe2\x80\xa8" => '\u2028', + "\xe2\x80\xa9" => '\u2029', + ], + 'unicode' => [ + "\xc3\xa9" => '\u00e9', + "\xf0\x9d\x92\x9e" => '\ud835\udc9e', // U+1D49E, outside the BMP + ], + 'xmlmeta' => [ + '<' => '\u003C', // JSON_HEX_TAG uses uppercase hex digits + '>' => '\u003E', + '&' => '\u0026', + ], + ]; + + $cases = []; + foreach ( $groups as $name => $rules ) { + $leaveUnescaped = in_array( $name, $unescapedGroups ); + foreach ( $rules as $from => $to ) { + $cases[] = [ $from, '"' . ( $leaveUnescaped ? $from : $to ) . '"' ]; + } + } + + return $cases; + } + + public function provideEmptyJsonKeyStrings() { + return [ + [ + '{"":"foo"}', + '{"":"foo"}', + '' + ], + [ + '{"_empty_":"foo"}', + '{"_empty_":"foo"}', + '_empty_' ], + [ + '{"\u005F\u0065\u006D\u0070\u0074\u0079\u005F":"foo"}', + '{"_empty_":"foo"}', + '_empty_' + ], + [ + '{"_empty_":"bar","":"foo"}', + '{"_empty_":"bar","":"foo"}', + '' + ], + [ + '{"":"bar","_empty_":"foo"}', + '{"":"bar","_empty_":"foo"}', + '_empty_' + ] + ]; + } + + /** + * @covers FormatJson::encode + * @covers FormatJson::decode + * @dataProvider provideEmptyJsonKeyStrings + * @param string $json + * + * Decoding behavior with empty keys can be surprising. + * See https://phabricator.wikimedia.org/T206411 + */ + public function testEmptyJsonKeyArray( $json, $expect, $php71Name ) { + // Decoding to array is consistent across supported PHP versions + $this->assertSame( $expect, FormatJson::encode( + FormatJson::decode( $json, true ) ) ); + + // Decoding to object differs between supported PHP versions + $obj = FormatJson::decode( $json ); + if ( version_compare( PHP_VERSION, '7.1', '<' ) ) { + $this->assertEquals( 'foo', $obj->_empty_ ); + } else { + $this->assertEquals( 'foo', $obj->{$php71Name} ); + } + } +} -- 2.20.1