ExporterTest.php 13 KB

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