ExporterTest.php 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433
  1. <?php declare(strict_types=1);
  2. /*
  3. * This file is part of exporter package.
  4. *
  5. * (c) Sebastian Bergmann <sebastian@phpunit.de>
  6. *
  7. * For the full copyright and license information, please view the LICENSE
  8. * file that was distributed with this source code.
  9. */
  10. namespace SebastianBergmann\Exporter;
  11. use PHPUnit\Framework\TestCase;
  12. use SebastianBergmann\RecursionContext\Context;
  13. /**
  14. * @covers SebastianBergmann\Exporter\Exporter
  15. */
  16. class ExporterTest extends TestCase
  17. {
  18. /**
  19. * @var Exporter
  20. */
  21. private $exporter;
  22. protected function setUp()
  23. {
  24. $this->exporter = new Exporter;
  25. }
  26. public function exportProvider()
  27. {
  28. $obj2 = new \stdClass;
  29. $obj2->foo = 'bar';
  30. $obj3 = (object) [1, 2, "Test\r\n", 4, 5, 6, 7, 8];
  31. $obj = new \stdClass;
  32. //@codingStandardsIgnoreStart
  33. $obj->null = null;
  34. //@codingStandardsIgnoreEnd
  35. $obj->boolean = true;
  36. $obj->integer = 1;
  37. $obj->double = 1.2;
  38. $obj->string = '1';
  39. $obj->text = "this\nis\na\nvery\nvery\nvery\nvery\nvery\nvery\rlong\n\rtext";
  40. $obj->object = $obj2;
  41. $obj->objectagain = $obj2;
  42. $obj->array = ['foo' => 'bar'];
  43. $obj->self = $obj;
  44. $storage = new \SplObjectStorage;
  45. $storage->attach($obj2);
  46. $storage->foo = $obj2;
  47. return [
  48. 'export null' => [null, 'null'],
  49. 'export boolean true' => [true, 'true'],
  50. 'export boolean false' => [false, 'false'],
  51. 'export int 1' => [1, '1'],
  52. 'export float 1.0' => [1.0, '1.0'],
  53. 'export float 1.2' => [1.2, '1.2'],
  54. 'export stream' => [\fopen('php://memory', 'r'), 'resource(%d) of type (stream)'],
  55. 'export numeric string' => ['1', "'1'"],
  56. 'export multidimentional array' => [[[1, 2, 3], [3, 4, 5]],
  57. <<<EOF
  58. Array &0 (
  59. 0 => Array &1 (
  60. 0 => 1
  61. 1 => 2
  62. 2 => 3
  63. )
  64. 1 => Array &2 (
  65. 0 => 3
  66. 1 => 4
  67. 2 => 5
  68. )
  69. )
  70. EOF
  71. ],
  72. // \n\r and \r is converted to \n
  73. 'export multiline text' => ["this\nis\na\nvery\nvery\nvery\nvery\nvery\nvery\rlong\n\rtext",
  74. <<<EOF
  75. 'this\\n
  76. is\\n
  77. a\\n
  78. very\\n
  79. very\\n
  80. very\\n
  81. very\\n
  82. very\\n
  83. very\\r
  84. long\\n\\r
  85. text'
  86. EOF
  87. ],
  88. 'export empty stdclass' => [new \stdClass, 'stdClass Object &%x ()'],
  89. 'export non empty stdclass' => [$obj,
  90. <<<EOF
  91. stdClass Object &%x (
  92. 'null' => null
  93. 'boolean' => true
  94. 'integer' => 1
  95. 'double' => 1.2
  96. 'string' => '1'
  97. 'text' => 'this\\n
  98. is\\n
  99. a\\n
  100. very\\n
  101. very\\n
  102. very\\n
  103. very\\n
  104. very\\n
  105. very\\r
  106. long\\n\\r
  107. text'
  108. 'object' => stdClass Object &%x (
  109. 'foo' => 'bar'
  110. )
  111. 'objectagain' => stdClass Object &%x
  112. 'array' => Array &%d (
  113. 'foo' => 'bar'
  114. )
  115. 'self' => stdClass Object &%x
  116. )
  117. EOF
  118. ],
  119. 'export empty array' => [[], 'Array &%d ()'],
  120. 'export splObjectStorage' => [$storage,
  121. <<<EOF
  122. SplObjectStorage Object &%x (
  123. 'foo' => stdClass Object &%x (
  124. 'foo' => 'bar'
  125. )
  126. '%x' => Array &0 (
  127. 'obj' => stdClass Object &%x
  128. 'inf' => null
  129. )
  130. )
  131. EOF
  132. ],
  133. 'export stdClass with numeric properties' => [$obj3,
  134. <<<EOF
  135. stdClass Object &%x (
  136. 0 => 1
  137. 1 => 2
  138. 2 => 'Test\\r\\n
  139. '
  140. 3 => 4
  141. 4 => 5
  142. 5 => 6
  143. 6 => 7
  144. 7 => 8
  145. )
  146. EOF
  147. ],
  148. [
  149. \chr(0) . \chr(1) . \chr(2) . \chr(3) . \chr(4) . \chr(5),
  150. 'Binary String: 0x000102030405'
  151. ],
  152. [
  153. \implode('', \array_map('chr', \range(0x0e, 0x1f))),
  154. 'Binary String: 0x0e0f101112131415161718191a1b1c1d1e1f'
  155. ],
  156. [
  157. \chr(0x00) . \chr(0x09),
  158. 'Binary String: 0x0009'
  159. ],
  160. [
  161. '',
  162. "''"
  163. ],
  164. 'export Exception without trace' => [
  165. new \Exception('The exception message', 42),
  166. <<<EOF
  167. Exception Object &%x (
  168. 'message' => 'The exception message'
  169. 'string' => ''
  170. 'code' => 42
  171. 'file' => '%s/tests/ExporterTest.php'
  172. 'line' => %d
  173. 'previous' => null
  174. )
  175. EOF
  176. ],
  177. 'export Error without trace' => [
  178. new \Error('The exception message', 42),
  179. <<<EOF
  180. Error Object &%x (
  181. 'message' => 'The exception message'
  182. 'string' => ''
  183. 'code' => 42
  184. 'file' => '%s/tests/ExporterTest.php'
  185. 'line' => %d
  186. 'previous' => null
  187. )
  188. EOF
  189. ],
  190. ];
  191. }
  192. /**
  193. * @dataProvider exportProvider
  194. */
  195. public function testExport($value, $expected)
  196. {
  197. $this->assertStringMatchesFormat(
  198. $expected,
  199. $this->trimNewline($this->exporter->export($value))
  200. );
  201. }
  202. public function testExport2()
  203. {
  204. if (\PHP_VERSION === '5.3.3') {
  205. $this->markTestSkipped('Skipped due to "Nesting level too deep - recursive dependency?" fatal error');
  206. }
  207. $obj = new \stdClass;
  208. $obj->foo = 'bar';
  209. $array = [
  210. 0 => 0,
  211. 'null' => null,
  212. 'boolean' => true,
  213. 'integer' => 1,
  214. 'double' => 1.2,
  215. 'string' => '1',
  216. 'text' => "this\nis\na\nvery\nvery\nvery\nvery\nvery\nvery\rlong\n\rtext",
  217. 'object' => $obj,
  218. 'objectagain' => $obj,
  219. 'array' => ['foo' => 'bar'],
  220. ];
  221. $array['self'] = &$array;
  222. $expected = <<<EOF
  223. Array &%d (
  224. 0 => 0
  225. 'null' => null
  226. 'boolean' => true
  227. 'integer' => 1
  228. 'double' => 1.2
  229. 'string' => '1'
  230. 'text' => 'this\\n
  231. is\\n
  232. a\\n
  233. very\\n
  234. very\\n
  235. very\\n
  236. very\\n
  237. very\\n
  238. very\\r
  239. long\\n\\r
  240. text'
  241. 'object' => stdClass Object &%x (
  242. 'foo' => 'bar'
  243. )
  244. 'objectagain' => stdClass Object &%x
  245. 'array' => Array &%d (
  246. 'foo' => 'bar'
  247. )
  248. 'self' => Array &%d (
  249. 0 => 0
  250. 'null' => null
  251. 'boolean' => true
  252. 'integer' => 1
  253. 'double' => 1.2
  254. 'string' => '1'
  255. 'text' => 'this\\n
  256. is\\n
  257. a\\n
  258. very\\n
  259. very\\n
  260. very\\n
  261. very\\n
  262. very\\n
  263. very\\r
  264. long\\n\\r
  265. text'
  266. 'object' => stdClass Object &%x
  267. 'objectagain' => stdClass Object &%x
  268. 'array' => Array &%d (
  269. 'foo' => 'bar'
  270. )
  271. 'self' => Array &%d
  272. )
  273. )
  274. EOF;
  275. $this->assertStringMatchesFormat(
  276. $expected,
  277. $this->trimNewline($this->exporter->export($array))
  278. );
  279. }
  280. public function shortenedExportProvider()
  281. {
  282. $obj = new \stdClass;
  283. $obj->foo = 'bar';
  284. $array = [
  285. 'foo' => 'bar',
  286. ];
  287. return [
  288. 'shortened export null' => [null, 'null'],
  289. 'shortened export boolean true' => [true, 'true'],
  290. 'shortened export integer 1' => [1, '1'],
  291. 'shortened export float 1.0' => [1.0, '1.0'],
  292. 'shortened export float 1.2' => [1.2, '1.2'],
  293. 'shortened export numeric string' => ['1', "'1'"],
  294. // \n\r and \r is converted to \n
  295. 'shortened export multilinestring' => ["this\nis\na\nvery\nvery\nvery\nvery\nvery\nvery\rlong\n\rtext", "'this\\nis\\na\\nvery\\nvery\\nvery...\\rtext'"],
  296. 'shortened export empty stdClass' => [new \stdClass, 'stdClass Object ()'],
  297. 'shortened export not empty stdClass' => [$obj, 'stdClass Object (...)'],
  298. 'shortened export empty array' => [[], 'Array ()'],
  299. 'shortened export not empty array' => [$array, 'Array (...)'],
  300. ];
  301. }
  302. /**
  303. * @dataProvider shortenedExportProvider
  304. */
  305. public function testShortenedExport($value, $expected)
  306. {
  307. $this->assertSame(
  308. $expected,
  309. $this->trimNewline($this->exporter->shortenedExport($value))
  310. );
  311. }
  312. /**
  313. * @requires extension mbstring
  314. */
  315. public function testShortenedExportForMultibyteCharacters()
  316. {
  317. $oldMbLanguage = \mb_language();
  318. \mb_language('Japanese');
  319. $oldMbInternalEncoding = \mb_internal_encoding();
  320. \mb_internal_encoding('UTF-8');
  321. try {
  322. $this->assertSame(
  323. "'いろはにほへとちりぬるをわかよたれそつねならむうゐのおくや...しゑひもせす'",
  324. $this->trimNewline($this->exporter->shortenedExport('いろはにほへとちりぬるをわかよたれそつねならむうゐのおくやまけふこえてあさきゆめみしゑひもせす'))
  325. );
  326. } catch (\Exception $e) {
  327. \mb_internal_encoding($oldMbInternalEncoding);
  328. \mb_language($oldMbLanguage);
  329. throw $e;
  330. }
  331. \mb_internal_encoding($oldMbInternalEncoding);
  332. \mb_language($oldMbLanguage);
  333. }
  334. public function provideNonBinaryMultibyteStrings()
  335. {
  336. return [
  337. [\implode('', \array_map('chr', \range(0x09, 0x0d))), 9],
  338. [\implode('', \array_map('chr', \range(0x20, 0x7f))), 96],
  339. [\implode('', \array_map('chr', \range(0x80, 0xff))), 128],
  340. ];
  341. }
  342. /**
  343. * @dataProvider provideNonBinaryMultibyteStrings
  344. */
  345. public function testNonBinaryStringExport($value, $expectedLength)
  346. {
  347. $this->assertRegExp(
  348. "~'.{{$expectedLength}}'\$~s",
  349. $this->exporter->export($value)
  350. );
  351. }
  352. public function testNonObjectCanBeReturnedAsArray()
  353. {
  354. $this->assertEquals([true], $this->exporter->toArray(true));
  355. }
  356. public function testIgnoreKeysInValue()
  357. {
  358. // Find out what the actual use case was with the PHP bug
  359. $array = [];
  360. $array["\0gcdata"] = '';
  361. $this->assertEquals([], $this->exporter->toArray((object) $array));
  362. }
  363. private function trimNewline($string)
  364. {
  365. return \preg_replace('/[ ]*\n/', "\n", $string);
  366. }
  367. /**
  368. * @dataProvider shortenedRecursiveExportProvider
  369. */
  370. public function testShortenedRecursiveExport(array $value, string $expected)
  371. {
  372. $this->assertEquals($expected, $this->exporter->shortenedRecursiveExport($value));
  373. }
  374. public function shortenedRecursiveExportProvider()
  375. {
  376. return [
  377. 'export null' => [[null], 'null'],
  378. 'export boolean true' => [[true], 'true'],
  379. 'export boolean false' => [[false], 'false'],
  380. 'export int 1' => [[1], '1'],
  381. 'export float 1.0' => [[1.0], '1.0'],
  382. 'export float 1.2' => [[1.2], '1.2'],
  383. 'export numeric string' => [['1'], "'1'"],
  384. 'export with numeric array key' => [[2 => 1], '1'],
  385. 'export with assoc array key' => [['foo' => 'bar'], '\'bar\''],
  386. 'export multidimentional array' => [[[1, 2, 3], [3, 4, 5]], 'array(1, 2, 3), array(3, 4, 5)'],
  387. 'export object' => [[new \stdClass], 'stdClass Object ()'],
  388. ];
  389. }
  390. public function testShortenedRecursiveOccurredRecursion()
  391. {
  392. $recursiveValue = [1];
  393. $context = new Context();
  394. $context->add($recursiveValue);
  395. $value = [$recursiveValue];
  396. $this->assertEquals('*RECURSION*', $this->exporter->shortenedRecursiveExport($value, $context));
  397. }
  398. }