1: <?php
2:
3: /*
4: * This file is part of the Symfony package.
5: *
6: * (c) Fabien Potencier <fabien@symfony.com>
7: *
8: * For the full copyright and license information, please view the LICENSE
9: * file that was distributed with this source code.
10: */
11:
12: namespace Symfony\Component\Yaml\Tests;
13:
14: use PHPUnit\Framework\TestCase;
15: use Symfony\Component\Yaml\Inline;
16:
17: class InlineTest extends TestCase
18: {
19: /**
20: * @dataProvider getTestsForParse
21: */
22: public function testParse($yaml, $value)
23: {
24: $this->assertSame($value, Inline::parse($yaml), sprintf('::parse() converts an inline YAML to a PHP structure (%s)', $yaml));
25: }
26:
27: /**
28: * @dataProvider getTestsForParseWithMapObjects
29: */
30: public function testParseWithMapObjects($yaml, $value)
31: {
32: $actual = Inline::parse($yaml, false, false, true);
33:
34: $this->assertSame(serialize($value), serialize($actual));
35: }
36:
37: /**
38: * @dataProvider getTestsForDump
39: */
40: public function testDump($yaml, $value)
41: {
42: $this->assertEquals($yaml, Inline::dump($value), sprintf('::dump() converts a PHP structure to an inline YAML (%s)', $yaml));
43:
44: $this->assertSame($value, Inline::parse(Inline::dump($value)), 'check consistency');
45: }
46:
47: public function testDumpNumericValueWithLocale()
48: {
49: $locale = setlocale(LC_NUMERIC, 0);
50: if (false === $locale) {
51: $this->markTestSkipped('Your platform does not support locales.');
52: }
53:
54: try {
55: $requiredLocales = array('fr_FR.UTF-8', 'fr_FR.UTF8', 'fr_FR.utf-8', 'fr_FR.utf8', 'French_France.1252');
56: if (false === setlocale(LC_NUMERIC, $requiredLocales)) {
57: $this->markTestSkipped('Could not set any of required locales: '.implode(', ', $requiredLocales));
58: }
59:
60: $this->assertEquals('1.2', Inline::dump(1.2));
61: $this->assertContains('fr', strtolower(setlocale(LC_NUMERIC, 0)));
62: setlocale(LC_NUMERIC, $locale);
63: } catch (\Exception $e) {
64: setlocale(LC_NUMERIC, $locale);
65: throw $e;
66: }
67: }
68:
69: public function testHashStringsResemblingExponentialNumericsShouldNotBeChangedToINF()
70: {
71: $value = '686e444';
72:
73: $this->assertSame($value, Inline::parse(Inline::dump($value)));
74: }
75:
76: /**
77: * @group legacy
78: * throws \Symfony\Component\Yaml\Exception\ParseException in 3.0
79: */
80: public function testParseScalarWithNonEscapedBlackslashShouldThrowException()
81: {
82: $this->assertSame('Foo\Var', Inline::parse('"Foo\Var"'));
83: }
84:
85: /**
86: * @expectedException \Symfony\Component\Yaml\Exception\ParseException
87: */
88: public function testParseScalarWithNonEscapedBlackslashAtTheEndShouldThrowException()
89: {
90: Inline::parse('"Foo\\"');
91: }
92:
93: /**
94: * @expectedException \Symfony\Component\Yaml\Exception\ParseException
95: */
96: public function testParseScalarWithIncorrectlyQuotedStringShouldThrowException()
97: {
98: $value = "'don't do somthin' like that'";
99: Inline::parse($value);
100: }
101:
102: /**
103: * @expectedException \Symfony\Component\Yaml\Exception\ParseException
104: */
105: public function testParseScalarWithIncorrectlyDoubleQuotedStringShouldThrowException()
106: {
107: $value = '"don"t do somthin" like that"';
108: Inline::parse($value);
109: }
110:
111: /**
112: * @expectedException \Symfony\Component\Yaml\Exception\ParseException
113: */
114: public function testParseInvalidMappingKeyShouldThrowException()
115: {
116: $value = '{ "foo " bar": "bar" }';
117: Inline::parse($value);
118: }
119:
120: /**
121: * @expectedException \Symfony\Component\Yaml\Exception\ParseException
122: */
123: public function testParseInvalidMappingShouldThrowException()
124: {
125: Inline::parse('[foo] bar');
126: }
127:
128: /**
129: * @expectedException \Symfony\Component\Yaml\Exception\ParseException
130: */
131: public function testParseInvalidSequenceShouldThrowException()
132: {
133: Inline::parse('{ foo: bar } bar');
134: }
135:
136: public function testParseScalarWithCorrectlyQuotedStringShouldReturnString()
137: {
138: $value = "'don''t do somthin'' like that'";
139: $expect = "don't do somthin' like that";
140:
141: $this->assertSame($expect, Inline::parseScalar($value));
142: }
143:
144: /**
145: * @dataProvider getDataForParseReferences
146: */
147: public function testParseReferences($yaml, $expected)
148: {
149: $this->assertSame($expected, Inline::parse($yaml, false, false, false, array('var' => 'var-value')));
150: }
151:
152: public function getDataForParseReferences()
153: {
154: return array(
155: 'scalar' => array('*var', 'var-value'),
156: 'list' => array('[ *var ]', array('var-value')),
157: 'list-in-list' => array('[[ *var ]]', array(array('var-value'))),
158: 'map-in-list' => array('[ { key: *var } ]', array(array('key' => 'var-value'))),
159: 'embedded-mapping-in-list' => array('[ key: *var ]', array(array('key' => 'var-value'))),
160: 'map' => array('{ key: *var }', array('key' => 'var-value')),
161: 'list-in-map' => array('{ key: [*var] }', array('key' => array('var-value'))),
162: 'map-in-map' => array('{ foo: { bar: *var } }', array('foo' => array('bar' => 'var-value'))),
163: );
164: }
165:
166: public function testParseMapReferenceInSequence()
167: {
168: $foo = array(
169: 'a' => 'Steve',
170: 'b' => 'Clark',
171: 'c' => 'Brian',
172: );
173: $this->assertSame(array($foo), Inline::parse('[*foo]', false, false, false, array('foo' => $foo)));
174: }
175:
176: /**
177: * @expectedException \Symfony\Component\Yaml\Exception\ParseException
178: * @expectedExceptionMessage A reference must contain at least one character.
179: */
180: public function testParseUnquotedAsterisk()
181: {
182: Inline::parse('{ foo: * }');
183: }
184:
185: /**
186: * @expectedException \Symfony\Component\Yaml\Exception\ParseException
187: * @expectedExceptionMessage A reference must contain at least one character.
188: */
189: public function testParseUnquotedAsteriskFollowedByAComment()
190: {
191: Inline::parse('{ foo: * #foo }');
192: }
193:
194: /**
195: * @group legacy
196: * @expectedDeprecation Not quoting the scalar "@foo " starting with "@" is deprecated since Symfony 2.8 and will throw a ParseException in 3.0.
197: * throws \Symfony\Component\Yaml\Exception\ParseException in 3.0
198: */
199: public function testParseUnquotedScalarStartingWithReservedAtIndicator()
200: {
201: Inline::parse('{ foo: @foo }');
202: }
203:
204: /**
205: * @group legacy
206: * @expectedDeprecation Not quoting the scalar "`foo " starting with "`" is deprecated since Symfony 2.8 and will throw a ParseException in 3.0.
207: * throws \Symfony\Component\Yaml\Exception\ParseException in 3.0
208: */
209: public function testParseUnquotedScalarStartingWithReservedBacktickIndicator()
210: {
211: Inline::parse('{ foo: `foo }');
212: }
213:
214: /**
215: * @group legacy
216: * @expectedDeprecation Not quoting the scalar "|foo " starting with "|" is deprecated since Symfony 2.8 and will throw a ParseException in 3.0.
217: * throws \Symfony\Component\Yaml\Exception\ParseException in 3.0
218: */
219: public function testParseUnquotedScalarStartingWithLiteralStyleIndicator()
220: {
221: Inline::parse('{ foo: |foo }');
222: }
223:
224: /**
225: * @group legacy
226: * @expectedDeprecation Not quoting the scalar ">foo " starting with ">" is deprecated since Symfony 2.8 and will throw a ParseException in 3.0.
227: * throws \Symfony\Component\Yaml\Exception\ParseException in 3.0
228: */
229: public function testParseUnquotedScalarStartingWithFoldedStyleIndicator()
230: {
231: Inline::parse('{ foo: >foo }');
232: }
233:
234: public function getScalarIndicators()
235: {
236: return array(array('|'), array('>'));
237: }
238:
239: /**
240: * @dataProvider getDataForIsHash
241: */
242: public function testIsHash($array, $expected)
243: {
244: $this->assertSame($expected, Inline::isHash($array));
245: }
246:
247: public function getDataForIsHash()
248: {
249: return array(
250: array(array(), false),
251: array(array(1, 2, 3), false),
252: array(array(2 => 1, 1 => 2, 0 => 3), true),
253: array(array('foo' => 1, 'bar' => 2), true),
254: );
255: }
256:
257: public function getTestsForParse()
258: {
259: return array(
260: array('', ''),
261: array('null', null),
262: array('false', false),
263: array('true', true),
264: array('12', 12),
265: array('-12', -12),
266: array('"quoted string"', 'quoted string'),
267: array("'quoted string'", 'quoted string'),
268: array('12.30e+02', 12.30e+02),
269: array('0x4D2', 0x4D2),
270: array('02333', 02333),
271: array('.Inf', -log(0)),
272: array('-.Inf', log(0)),
273: array("'686e444'", '686e444'),
274: array('686e444', 646e444),
275: array('123456789123456789123456789123456789', '123456789123456789123456789123456789'),
276: array('"foo\r\nbar"', "foo\r\nbar"),
277: array("'foo#bar'", 'foo#bar'),
278: array("'foo # bar'", 'foo # bar'),
279: array("'#cfcfcf'", '#cfcfcf'),
280: array('::form_base.html.twig', '::form_base.html.twig'),
281:
282: // Pre-YAML-1.2 booleans
283: array("'y'", 'y'),
284: array("'n'", 'n'),
285: array("'yes'", 'yes'),
286: array("'no'", 'no'),
287: array("'on'", 'on'),
288: array("'off'", 'off'),
289:
290: array('2007-10-30', gmmktime(0, 0, 0, 10, 30, 2007)),
291: array('2007-10-30T02:59:43Z', gmmktime(2, 59, 43, 10, 30, 2007)),
292: array('2007-10-30 02:59:43 Z', gmmktime(2, 59, 43, 10, 30, 2007)),
293: array('1960-10-30 02:59:43 Z', gmmktime(2, 59, 43, 10, 30, 1960)),
294: array('1730-10-30T02:59:43Z', gmmktime(2, 59, 43, 10, 30, 1730)),
295:
296: array('"a \\"string\\" with \'quoted strings inside\'"', 'a "string" with \'quoted strings inside\''),
297: array("'a \"string\" with ''quoted strings inside'''", 'a "string" with \'quoted strings inside\''),
298:
299: // sequences
300: // urls are no key value mapping. see #3609. Valid yaml "key: value" mappings require a space after the colon
301: array('[foo, http://urls.are/no/mappings, false, null, 12]', array('foo', 'http://urls.are/no/mappings', false, null, 12)),
302: array('[ foo , bar , false , null , 12 ]', array('foo', 'bar', false, null, 12)),
303: array('[\'foo,bar\', \'foo bar\']', array('foo,bar', 'foo bar')),
304:
305: // mappings
306: array('{foo:bar,bar:foo,false:false,null:null,integer:12}', array('foo' => 'bar', 'bar' => 'foo', 'false' => false, 'null' => null, 'integer' => 12)),
307: array('{ foo : bar, bar : foo, false : false, null : null, integer : 12 }', array('foo' => 'bar', 'bar' => 'foo', 'false' => false, 'null' => null, 'integer' => 12)),
308: array('{foo: \'bar\', bar: \'foo: bar\'}', array('foo' => 'bar', 'bar' => 'foo: bar')),
309: array('{\'foo\': \'bar\', "bar": \'foo: bar\'}', array('foo' => 'bar', 'bar' => 'foo: bar')),
310: array('{\'foo\'\'\': \'bar\', "bar\"": \'foo: bar\'}', array('foo\'' => 'bar', 'bar"' => 'foo: bar')),
311: array('{\'foo: \': \'bar\', "bar: ": \'foo: bar\'}', array('foo: ' => 'bar', 'bar: ' => 'foo: bar')),
312:
313: // nested sequences and mappings
314: array('[foo, [bar, foo]]', array('foo', array('bar', 'foo'))),
315: array('[foo, {bar: foo}]', array('foo', array('bar' => 'foo'))),
316: array('{ foo: {bar: foo} }', array('foo' => array('bar' => 'foo'))),
317: array('{ foo: [bar, foo] }', array('foo' => array('bar', 'foo'))),
318:
319: array('[ foo, [ bar, foo ] ]', array('foo', array('bar', 'foo'))),
320:
321: array('[{ foo: {bar: foo} }]', array(array('foo' => array('bar' => 'foo')))),
322:
323: array('[foo, [bar, [foo, [bar, foo]], foo]]', array('foo', array('bar', array('foo', array('bar', 'foo')), 'foo'))),
324:
325: array('[foo, {bar: foo, foo: [foo, {bar: foo}]}, [foo, {bar: foo}]]', array('foo', array('bar' => 'foo', 'foo' => array('foo', array('bar' => 'foo'))), array('foo', array('bar' => 'foo')))),
326:
327: array('[foo, bar: { foo: bar }]', array('foo', '1' => array('bar' => array('foo' => 'bar')))),
328: array('[foo, \'@foo.baz\', { \'%foo%\': \'foo is %foo%\', bar: \'%foo%\' }, true, \'@service_container\']', array('foo', '@foo.baz', array('%foo%' => 'foo is %foo%', 'bar' => '%foo%'), true, '@service_container')),
329: );
330: }
331:
332: public function getTestsForParseWithMapObjects()
333: {
334: return array(
335: array('', ''),
336: array('null', null),
337: array('false', false),
338: array('true', true),
339: array('12', 12),
340: array('-12', -12),
341: array('"quoted string"', 'quoted string'),
342: array("'quoted string'", 'quoted string'),
343: array('12.30e+02', 12.30e+02),
344: array('0x4D2', 0x4D2),
345: array('02333', 02333),
346: array('.Inf', -log(0)),
347: array('-.Inf', log(0)),
348: array("'686e444'", '686e444'),
349: array('686e444', 646e444),
350: array('123456789123456789123456789123456789', '123456789123456789123456789123456789'),
351: array('"foo\r\nbar"', "foo\r\nbar"),
352: array("'foo#bar'", 'foo#bar'),
353: array("'foo # bar'", 'foo # bar'),
354: array("'#cfcfcf'", '#cfcfcf'),
355: array('::form_base.html.twig', '::form_base.html.twig'),
356:
357: array('2007-10-30', gmmktime(0, 0, 0, 10, 30, 2007)),
358: array('2007-10-30T02:59:43Z', gmmktime(2, 59, 43, 10, 30, 2007)),
359: array('2007-10-30 02:59:43 Z', gmmktime(2, 59, 43, 10, 30, 2007)),
360: array('1960-10-30 02:59:43 Z', gmmktime(2, 59, 43, 10, 30, 1960)),
361: array('1730-10-30T02:59:43Z', gmmktime(2, 59, 43, 10, 30, 1730)),
362:
363: array('"a \\"string\\" with \'quoted strings inside\'"', 'a "string" with \'quoted strings inside\''),
364: array("'a \"string\" with ''quoted strings inside'''", 'a "string" with \'quoted strings inside\''),
365:
366: // sequences
367: // urls are no key value mapping. see #3609. Valid yaml "key: value" mappings require a space after the colon
368: array('[foo, http://urls.are/no/mappings, false, null, 12]', array('foo', 'http://urls.are/no/mappings', false, null, 12)),
369: array('[ foo , bar , false , null , 12 ]', array('foo', 'bar', false, null, 12)),
370: array('[\'foo,bar\', \'foo bar\']', array('foo,bar', 'foo bar')),
371:
372: // mappings
373: array('{foo:bar,bar:foo,false:false,null:null,integer:12}', (object) array('foo' => 'bar', 'bar' => 'foo', 'false' => false, 'null' => null, 'integer' => 12)),
374: array('{ foo : bar, bar : foo, false : false, null : null, integer : 12 }', (object) array('foo' => 'bar', 'bar' => 'foo', 'false' => false, 'null' => null, 'integer' => 12)),
375: array('{foo: \'bar\', bar: \'foo: bar\'}', (object) array('foo' => 'bar', 'bar' => 'foo: bar')),
376: array('{\'foo\': \'bar\', "bar": \'foo: bar\'}', (object) array('foo' => 'bar', 'bar' => 'foo: bar')),
377: array('{\'foo\'\'\': \'bar\', "bar\"": \'foo: bar\'}', (object) array('foo\'' => 'bar', 'bar"' => 'foo: bar')),
378: array('{\'foo: \': \'bar\', "bar: ": \'foo: bar\'}', (object) array('foo: ' => 'bar', 'bar: ' => 'foo: bar')),
379:
380: // nested sequences and mappings
381: array('[foo, [bar, foo]]', array('foo', array('bar', 'foo'))),
382: array('[foo, {bar: foo}]', array('foo', (object) array('bar' => 'foo'))),
383: array('{ foo: {bar: foo} }', (object) array('foo' => (object) array('bar' => 'foo'))),
384: array('{ foo: [bar, foo] }', (object) array('foo' => array('bar', 'foo'))),
385:
386: array('[ foo, [ bar, foo ] ]', array('foo', array('bar', 'foo'))),
387:
388: array('[{ foo: {bar: foo} }]', array((object) array('foo' => (object) array('bar' => 'foo')))),
389:
390: array('[foo, [bar, [foo, [bar, foo]], foo]]', array('foo', array('bar', array('foo', array('bar', 'foo')), 'foo'))),
391:
392: array('[foo, {bar: foo, foo: [foo, {bar: foo}]}, [foo, {bar: foo}]]', array('foo', (object) array('bar' => 'foo', 'foo' => array('foo', (object) array('bar' => 'foo'))), array('foo', (object) array('bar' => 'foo')))),
393:
394: array('[foo, bar: { foo: bar }]', array('foo', '1' => (object) array('bar' => (object) array('foo' => 'bar')))),
395: array('[foo, \'@foo.baz\', { \'%foo%\': \'foo is %foo%\', bar: \'%foo%\' }, true, \'@service_container\']', array('foo', '@foo.baz', (object) array('%foo%' => 'foo is %foo%', 'bar' => '%foo%'), true, '@service_container')),
396:
397: array('{}', new \stdClass()),
398: array('{ foo : bar, bar : {} }', (object) array('foo' => 'bar', 'bar' => new \stdClass())),
399: array('{ foo : [], bar : {} }', (object) array('foo' => array(), 'bar' => new \stdClass())),
400: array('{foo: \'bar\', bar: {} }', (object) array('foo' => 'bar', 'bar' => new \stdClass())),
401: array('{\'foo\': \'bar\', "bar": {}}', (object) array('foo' => 'bar', 'bar' => new \stdClass())),
402: array('{\'foo\': \'bar\', "bar": \'{}\'}', (object) array('foo' => 'bar', 'bar' => '{}')),
403:
404: array('[foo, [{}, {}]]', array('foo', array(new \stdClass(), new \stdClass()))),
405: array('[foo, [[], {}]]', array('foo', array(array(), new \stdClass()))),
406: array('[foo, [[{}, {}], {}]]', array('foo', array(array(new \stdClass(), new \stdClass()), new \stdClass()))),
407: array('[foo, {bar: {}}]', array('foo', '1' => (object) array('bar' => new \stdClass()))),
408: );
409: }
410:
411: public function getTestsForDump()
412: {
413: return array(
414: array('null', null),
415: array('false', false),
416: array('true', true),
417: array('12', 12),
418: array("'quoted string'", 'quoted string'),
419: array('!!float 1230', 12.30e+02),
420: array('1234', 0x4D2),
421: array('1243', 02333),
422: array('.Inf', -log(0)),
423: array('-.Inf', log(0)),
424: array("'686e444'", '686e444'),
425: array('"foo\r\nbar"', "foo\r\nbar"),
426: array("'foo#bar'", 'foo#bar'),
427: array("'foo # bar'", 'foo # bar'),
428: array("'#cfcfcf'", '#cfcfcf'),
429:
430: array("'a \"string\" with ''quoted strings inside'''", 'a "string" with \'quoted strings inside\''),
431:
432: array("'-dash'", '-dash'),
433: array("'-'", '-'),
434:
435: // Pre-YAML-1.2 booleans
436: array("'y'", 'y'),
437: array("'n'", 'n'),
438: array("'yes'", 'yes'),
439: array("'no'", 'no'),
440: array("'on'", 'on'),
441: array("'off'", 'off'),
442:
443: // sequences
444: array('[foo, bar, false, null, 12]', array('foo', 'bar', false, null, 12)),
445: array('[\'foo,bar\', \'foo bar\']', array('foo,bar', 'foo bar')),
446:
447: // mappings
448: array('{ foo: bar, bar: foo, \'false\': false, \'null\': null, integer: 12 }', array('foo' => 'bar', 'bar' => 'foo', 'false' => false, 'null' => null, 'integer' => 12)),
449: array('{ foo: bar, bar: \'foo: bar\' }', array('foo' => 'bar', 'bar' => 'foo: bar')),
450:
451: // nested sequences and mappings
452: array('[foo, [bar, foo]]', array('foo', array('bar', 'foo'))),
453:
454: array('[foo, [bar, [foo, [bar, foo]], foo]]', array('foo', array('bar', array('foo', array('bar', 'foo')), 'foo'))),
455:
456: array('{ foo: { bar: foo } }', array('foo' => array('bar' => 'foo'))),
457:
458: array('[foo, { bar: foo }]', array('foo', array('bar' => 'foo'))),
459:
460: array('[foo, { bar: foo, foo: [foo, { bar: foo }] }, [foo, { bar: foo }]]', array('foo', array('bar' => 'foo', 'foo' => array('foo', array('bar' => 'foo'))), array('foo', array('bar' => 'foo')))),
461:
462: array('[foo, \'@foo.baz\', { \'%foo%\': \'foo is %foo%\', bar: \'%foo%\' }, true, \'@service_container\']', array('foo', '@foo.baz', array('%foo%' => 'foo is %foo%', 'bar' => '%foo%'), true, '@service_container')),
463:
464: array('{ foo: { bar: { 1: 2, baz: 3 } } }', array('foo' => array('bar' => array(1 => 2, 'baz' => 3)))),
465: );
466: }
467:
468: /**
469: * @expectedException \Symfony\Component\Yaml\Exception\ParseException
470: * @expectedExceptionMessage Malformed inline YAML string: {this, is not, supported}.
471: */
472: public function testNotSupportedMissingValue()
473: {
474: Inline::parse('{this, is not, supported}');
475: }
476:
477: public function testVeryLongQuotedStrings()
478: {
479: $longStringWithQuotes = str_repeat("x\r\n\\\"x\"x", 1000);
480:
481: $yamlString = Inline::dump(array('longStringWithQuotes' => $longStringWithQuotes));
482: $arrayFromYaml = Inline::parse($yamlString);
483:
484: $this->assertEquals($longStringWithQuotes, $arrayFromYaml['longStringWithQuotes']);
485: }
486:
487: public function testBooleanMappingKeysAreConvertedToStrings()
488: {
489: $this->assertSame(array('false' => 'foo'), Inline::parse('{false: foo}'));
490: $this->assertSame(array('true' => 'foo'), Inline::parse('{true: foo}'));
491: }
492:
493: public function testTheEmptyStringIsAValidMappingKey()
494: {
495: $this->assertSame(array('' => 'foo'), Inline::parse('{ "": foo }'));
496: }
497:
498: /**
499: * @expectedException \Symfony\Component\Yaml\Exception\ParseException
500: * @expectedExceptionMessage Unexpected end of line, expected one of ",}".
501: */
502: public function testUnfinishedInlineMap()
503: {
504: Inline::parse("{abc: 'def'");
505: }
506: }
507: