Merge "SpecialSearch: Use CSS instead of cellpadding and cellspacing"
[lhc/web/wiklou.git] / tests / phpunit / includes / json / FormatJsonTest.php
1 <?php
2
3 /**
4 * @covers FormatJson
5 */
6 class FormatJsonTest extends MediaWikiTestCase {
7
8 public static function provideEncoderPrettyPrinting() {
9 return array(
10 // Four spaces
11 array( true, ' ' ),
12 array( ' ', ' ' ),
13 // Two spaces
14 array( ' ', ' ' ),
15 // One tab
16 array( "\t", "\t" ),
17 );
18 }
19
20 /**
21 * @dataProvider provideEncoderPrettyPrinting
22 */
23 public function testEncoderPrettyPrinting( $pretty, $expectedIndent ) {
24 $obj = array(
25 'emptyObject' => new stdClass,
26 'emptyArray' => array(),
27 'string' => 'foobar\\',
28 'filledArray' => array(
29 array(
30 123,
31 456,
32 ),
33 // Nested json works without problems
34 '"7":["8",{"9":"10"}]',
35 // Whitespace clean up doesn't touch strings that look alike
36 "{\n\t\"emptyObject\": {\n\t},\n\t\"emptyArray\": [ ]\n}",
37 ),
38 );
39
40 // No trailing whitespace, no trailing linefeed
41 $json = '{
42 "emptyObject": {},
43 "emptyArray": [],
44 "string": "foobar\\\\",
45 "filledArray": [
46 [
47 123,
48 456
49 ],
50 "\"7\":[\"8\",{\"9\":\"10\"}]",
51 "{\n\t\"emptyObject\": {\n\t},\n\t\"emptyArray\": [ ]\n}"
52 ]
53 }';
54
55 $json = str_replace( "\r", '', $json ); // Windows compat
56 $json = str_replace( "\t", $expectedIndent, $json );
57 $this->assertSame( $json, FormatJson::encode( $obj, $pretty ) );
58 }
59
60 public static function provideEncodeDefault() {
61 return self::getEncodeTestCases( array() );
62 }
63
64 /**
65 * @dataProvider provideEncodeDefault
66 */
67 public function testEncodeDefault( $from, $to ) {
68 $this->assertSame( $to, FormatJson::encode( $from ) );
69 }
70
71 public static function provideEncodeUtf8() {
72 return self::getEncodeTestCases( array( 'unicode' ) );
73 }
74
75 /**
76 * @dataProvider provideEncodeUtf8
77 */
78 public function testEncodeUtf8( $from, $to ) {
79 $this->assertSame( $to, FormatJson::encode( $from, false, FormatJson::UTF8_OK ) );
80 }
81
82 public static function provideEncodeXmlMeta() {
83 return self::getEncodeTestCases( array( 'xmlmeta' ) );
84 }
85
86 /**
87 * @dataProvider provideEncodeXmlMeta
88 */
89 public function testEncodeXmlMeta( $from, $to ) {
90 $this->assertSame( $to, FormatJson::encode( $from, false, FormatJson::XMLMETA_OK ) );
91 }
92
93 public static function provideEncodeAllOk() {
94 return self::getEncodeTestCases( array( 'unicode', 'xmlmeta' ) );
95 }
96
97 /**
98 * @dataProvider provideEncodeAllOk
99 */
100 public function testEncodeAllOk( $from, $to ) {
101 $this->assertSame( $to, FormatJson::encode( $from, false, FormatJson::ALL_OK ) );
102 }
103
104 public function testEncodePhpBug46944() {
105 $this->assertNotEquals(
106 '\ud840\udc00',
107 strtolower( FormatJson::encode( "\xf0\xa0\x80\x80" ) ),
108 'Test encoding an broken json_encode character (U+20000)'
109 );
110 }
111
112 public function testDecodeReturnType() {
113 $this->assertInternalType(
114 'object',
115 FormatJson::decode( '{"Name": "Cheeso", "Rank": 7}' ),
116 'Default to object'
117 );
118
119 $this->assertInternalType(
120 'array',
121 FormatJson::decode( '{"Name": "Cheeso", "Rank": 7}', true ),
122 'Optional array'
123 );
124 }
125
126 public static function provideParse() {
127 return array(
128 array( null ),
129 array( true ),
130 array( false ),
131 array( 0 ),
132 array( 1 ),
133 array( 1.2 ),
134 array( '' ),
135 array( 'str' ),
136 array( array( 0, 1, 2 ) ),
137 array( array( 'a' => 'b' ) ),
138 array( array( 'a' => 'b' ) ),
139 array( array( 'a' => 'b', 'x' => array( 'c' => 'd' ) ) ),
140 );
141 }
142
143 /**
144 * Recursively convert arrays into stdClass
145 * @param array|string|bool|int|float|null $value
146 * @return stdClass|string|bool|int|float|null
147 */
148 public static function toObject( $value ) {
149 return !is_array( $value ) ? $value : (object) array_map( __METHOD__, $value );
150 }
151
152 /**
153 * @dataProvider provideParse
154 * @param mixed $value
155 */
156 public function testParse( $value ) {
157 $expected = self::toObject( $value );
158 $json = FormatJson::encode( $expected, false, FormatJson::ALL_OK );
159 $this->assertJson( $json );
160
161 $st = FormatJson::parse( $json );
162 $this->assertType( 'Status', $st );
163 $this->assertTrue( $st->isGood() );
164 $this->assertEquals( $expected, $st->getValue() );
165
166 $st = FormatJson::parse( $json, FormatJson::FORCE_ASSOC );
167 $this->assertType( 'Status', $st );
168 $this->assertTrue( $st->isGood() );
169 $this->assertEquals( $value, $st->getValue() );
170 }
171
172 /**
173 * Test data for testParseTryFixing.
174 *
175 * HHVM has a lenient json parser (yeah great idea right?) which allows
176 * trailing commas for array and object delarations among other things, so
177 * our JSON_ERROR_SYNTAX rescue block is not always triggered. It however
178 * isn't lenient in exactly the same ways as our TRY_FIXING mode, so the
179 * assertions in this test are a bit more complicated than they ideally
180 * would be:
181 *
182 * Optional third argument: true if hhvm parses the value without
183 * intervention, false otherwise. Defaults to true.
184 *
185 * Optional fourth argument: expected cannonical JSON serialization of HHVM
186 * parsed result. Defaults to the second argument's value.
187 */
188 public static function provideParseTryFixing() {
189 return array(
190 array( "[,]", '[]', false ),
191 array( "[ , ]", '[]', false ),
192 array( "[ , }", false ),
193 array( '[1],', false, true, '[1]' ),
194 array( "[1,]", '[1]' ),
195 array( "[1\n,]", '[1]' ),
196 array( "[1,\n]", '[1]' ),
197 array( "[1,]\n", '[1]' ),
198 array( "[1\n,\n]\n", '[1]' ),
199 array( '["a,",]', '["a,"]' ),
200 array( "[[1,]\n,[2,\n],[3\n,]]", '[[1],[2],[3]]' ),
201 // I wish we could parse this, but would need quote parsing
202 array( '[[1,],[2,],[3,]]', false, true, '[[1],[2],[3]]' ),
203 array( '[1,,]', false, false, '[1]' ),
204 );
205 }
206
207 /**
208 * @dataProvider provideParseTryFixing
209 * @param string $value
210 * @param string|bool $expected Expected result with Zend PHP
211 * @param bool $hhvmParses Will HHVM parse this value without TRY_FIXING?
212 * @param string|bool $expectedHHVM Expected result with HHVM if different
213 * from the PHP5 expectation
214 */
215 public function testParseTryFixing(
216 $value, $expected,
217 $hhvmParses = true, $expectedHHVM = null
218 ) {
219 // PHP5 results are always expected to have isGood() === false
220 $expectedGoodStatus = false;
221
222 if ( wfIsHHVM() ) {
223 // Use HHVM specific expected result if provided
224 $expected = ( $expectedHHVM === null ) ? $expected : $expectedHHVM;
225 // If HHVM parses the value natively, expect isGood() === true
226 $expectedGoodStatus = $hhvmParses;
227 }
228
229 $st = FormatJson::parse( $value, FormatJson::TRY_FIXING );
230 $this->assertType( 'Status', $st );
231 if ( $expected === false ) {
232 $this->assertFalse( $st->isOK(), 'Expected isOK() == false' );
233 } else {
234 $this->assertSame( $expectedGoodStatus, $st->isGood(),
235 'Expected isGood() == ' . ( $expectedGoodStatus ? 'true' : 'false' )
236 );
237 $this->assertTrue( $st->isOK(), 'Expected isOK == true' );
238 $val = FormatJson::encode( $st->getValue(), false, FormatJson::ALL_OK );
239 $this->assertEquals( $expected, $val );
240 }
241 }
242
243 public static function provideParseErrors() {
244 return array(
245 array( 'aaa' ),
246 array( '{"j": 1 ] }' ),
247 );
248 }
249
250 /**
251 * @dataProvider provideParseErrors
252 * @param mixed $value
253 */
254 public function testParseErrors( $value ) {
255 $st = FormatJson::parse( $value );
256 $this->assertType( 'Status', $st );
257 $this->assertFalse( $st->isOK() );
258 }
259
260 public function provideStripComments() {
261 return array(
262 array( '{"a":"b"}', '{"a":"b"}' ),
263 array( "{\"a\":\"b\"}\n", "{\"a\":\"b\"}\n" ),
264 array( '/*c*/{"c":"b"}', '{"c":"b"}' ),
265 array( '{"a":"c"}/*c*/', '{"a":"c"}' ),
266 array( '/*c//d*/{"c":"b"}', '{"c":"b"}' ),
267 array( '{/*c*/"c":"b"}', '{"c":"b"}' ),
268 array( "/*\nc\r\n*/{\"c\":\"b\"}", '{"c":"b"}' ),
269 array( "//c\n{\"c\":\"b\"}", '{"c":"b"}' ),
270 array( "//c\r\n{\"c\":\"b\"}", '{"c":"b"}' ),
271 array( '{"a":"c"}//c', '{"a":"c"}' ),
272 array( "{\"a-c\"://c\n\"b\"}", '{"a-c":"b"}' ),
273 array( '{"/*a":"b"}', '{"/*a":"b"}' ),
274 array( '{"a":"//b"}', '{"a":"//b"}' ),
275 array( '{"a":"b/*c*/"}', '{"a":"b/*c*/"}' ),
276 array( "{\"\\\"/*a\":\"b\"}", "{\"\\\"/*a\":\"b\"}" ),
277 array( '', '' ),
278 array( '/*c', '' ),
279 array( '//c', '' ),
280 array( '"http://example.com"', '"http://example.com"' ),
281 array( "\0", "\0" ),
282 array( '"Blåbærsyltetøy"', '"Blåbærsyltetøy"' ),
283 );
284 }
285
286 /**
287 * @covers FormatJson::stripComments
288 * @dataProvider provideStripComments
289 * @param string $json
290 * @param string $expect
291 */
292 public function testStripComments( $json, $expect ) {
293 $this->assertSame( $expect, FormatJson::stripComments( $json ) );
294 }
295
296 public function provideParseStripComments() {
297 return array(
298 array( '/* blah */true', true ),
299 array( "// blah \ntrue", true ),
300 array( '[ "a" , /* blah */ "b" ]', array( 'a', 'b' ) ),
301 );
302 }
303
304 /**
305 * @covers FormatJson::parse
306 * @covers FormatJson::stripComments
307 * @dataProvider provideParseStripComments
308 * @param string $json
309 * @param mixed $expect
310 */
311 public function testParseStripComments( $json, $expect ) {
312 $st = FormatJson::parse( $json, FormatJson::STRIP_COMMENTS );
313 $this->assertType( 'Status', $st );
314 $this->assertTrue( $st->isGood() );
315 $this->assertEquals( $expect, $st->getValue() );
316 }
317
318 /**
319 * Generate a set of test cases for a particular combination of encoder options.
320 *
321 * @param array $unescapedGroups List of character groups to leave unescaped
322 * @return array Arrays of unencoded strings and corresponding encoded strings
323 */
324 private static function getEncodeTestCases( array $unescapedGroups ) {
325 $groups = array(
326 'always' => array(
327 // Forward slash (always unescaped)
328 '/' => '/',
329
330 // Control characters
331 "\0" => '\u0000',
332 "\x08" => '\b',
333 "\t" => '\t',
334 "\n" => '\n',
335 "\r" => '\r',
336 "\f" => '\f',
337 "\x1f" => '\u001f', // representative example
338
339 // Double quotes
340 '"' => '\"',
341
342 // Backslashes
343 '\\' => '\\\\',
344 '\\\\' => '\\\\\\\\',
345 '\\u00e9' => '\\\u00e9', // security check for Unicode unescaping
346
347 // Line terminators
348 "\xe2\x80\xa8" => '\u2028',
349 "\xe2\x80\xa9" => '\u2029',
350 ),
351 'unicode' => array(
352 "\xc3\xa9" => '\u00e9',
353 "\xf0\x9d\x92\x9e" => '\ud835\udc9e', // U+1D49E, outside the BMP
354 ),
355 'xmlmeta' => array(
356 '<' => '\u003C', // JSON_HEX_TAG uses uppercase hex digits
357 '>' => '\u003E',
358 '&' => '\u0026',
359 ),
360 );
361
362 $cases = array();
363 foreach ( $groups as $name => $rules ) {
364 $leaveUnescaped = in_array( $name, $unescapedGroups );
365 foreach ( $rules as $from => $to ) {
366 $cases[] = array( $from, '"' . ( $leaveUnescaped ? $from : $to ) . '"' );
367 }
368 }
369
370 return $cases;
371 }
372 }