OutputFormatterTest.php 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397
  1. <?php
  2. /*
  3. * This file is part of the Symfony package.
  4. *
  5. * (c) Fabien Potencier <fabien@symfony.com>
  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 Symfony\Component\Console\Tests\Formatter;
  11. use PHPUnit\Framework\TestCase;
  12. use Symfony\Component\Console\Formatter\OutputFormatter;
  13. use Symfony\Component\Console\Formatter\OutputFormatterStyle;
  14. class OutputFormatterTest extends TestCase
  15. {
  16. public function testEmptyTag()
  17. {
  18. $formatter = new OutputFormatter(true);
  19. $this->assertEquals('foo<>bar', $formatter->format('foo<>bar'));
  20. }
  21. public function testLGCharEscaping()
  22. {
  23. $formatter = new OutputFormatter(true);
  24. $this->assertEquals('foo<bar', $formatter->format('foo\\<bar'));
  25. $this->assertEquals('foo << bar', $formatter->format('foo << bar'));
  26. $this->assertEquals('foo << bar \\', $formatter->format('foo << bar \\'));
  27. $this->assertEquals("foo << \033[32mbar \\ baz\033[39m \\", $formatter->format('foo << <info>bar \\ baz</info> \\'));
  28. $this->assertEquals('<info>some info</info>', $formatter->format('\\<info>some info\\</info>'));
  29. $this->assertEquals('\\<info\\>some info\\</info\\>', OutputFormatter::escape('<info>some info</info>'));
  30. // every < and > gets escaped if not already escaped, but already escaped ones do not get escaped again
  31. // and escaped backslashes remain as such, same with backslashes escaping non-special characters
  32. $this->assertEquals('foo \\< bar \\< baz \\\\< foo \\> bar \\> baz \\\\> \\x', OutputFormatter::escape('foo < bar \\< baz \\\\< foo > bar \\> baz \\\\> \\x'));
  33. $this->assertEquals(
  34. "\033[33mSymfony\\Component\\Console does work very well!\033[39m",
  35. $formatter->format('<comment>Symfony\Component\Console does work very well!</comment>')
  36. );
  37. }
  38. public function testBundledStyles()
  39. {
  40. $formatter = new OutputFormatter(true);
  41. $this->assertTrue($formatter->hasStyle('error'));
  42. $this->assertTrue($formatter->hasStyle('info'));
  43. $this->assertTrue($formatter->hasStyle('comment'));
  44. $this->assertTrue($formatter->hasStyle('question'));
  45. $this->assertEquals(
  46. "\033[37;41msome error\033[39;49m",
  47. $formatter->format('<error>some error</error>')
  48. );
  49. $this->assertEquals(
  50. "\033[32msome info\033[39m",
  51. $formatter->format('<info>some info</info>')
  52. );
  53. $this->assertEquals(
  54. "\033[33msome comment\033[39m",
  55. $formatter->format('<comment>some comment</comment>')
  56. );
  57. $this->assertEquals(
  58. "\033[30;46msome question\033[39;49m",
  59. $formatter->format('<question>some question</question>')
  60. );
  61. }
  62. public function testNestedStyles()
  63. {
  64. $formatter = new OutputFormatter(true);
  65. $this->assertEquals(
  66. "\033[37;41msome \033[39;49m\033[32msome info\033[39m\033[37;41m error\033[39;49m",
  67. $formatter->format('<error>some <info>some info</info> error</error>')
  68. );
  69. }
  70. public function testAdjacentStyles()
  71. {
  72. $formatter = new OutputFormatter(true);
  73. $this->assertEquals(
  74. "\033[37;41msome error\033[39;49m\033[32msome info\033[39m",
  75. $formatter->format('<error>some error</error><info>some info</info>')
  76. );
  77. }
  78. public function testStyleMatchingNotGreedy()
  79. {
  80. $formatter = new OutputFormatter(true);
  81. $this->assertEquals(
  82. "(\033[32m>=2.0,<2.3\033[39m)",
  83. $formatter->format('(<info>>=2.0,<2.3</info>)')
  84. );
  85. }
  86. public function testStyleEscaping()
  87. {
  88. $formatter = new OutputFormatter(true);
  89. $this->assertEquals(
  90. "(\033[32mz>=2.0,<<<a2.3\\\033[39m)",
  91. $formatter->format('(<info>'.$formatter->escape('z>=2.0,<\\<<a2.3\\').'</info>)')
  92. );
  93. $this->assertEquals(
  94. "\033[32m<error>some error</error>\033[39m",
  95. $formatter->format('<info>'.$formatter->escape('<error>some error</error>').'</info>')
  96. );
  97. }
  98. public function testDeepNestedStyles()
  99. {
  100. $formatter = new OutputFormatter(true);
  101. $this->assertEquals(
  102. "\033[37;41merror\033[39;49m\033[32minfo\033[39m\033[33mcomment\033[39m\033[37;41merror\033[39;49m",
  103. $formatter->format('<error>error<info>info<comment>comment</info>error</error>')
  104. );
  105. }
  106. public function testNewStyle()
  107. {
  108. $formatter = new OutputFormatter(true);
  109. $style = new OutputFormatterStyle('blue', 'white');
  110. $formatter->setStyle('test', $style);
  111. $this->assertEquals($style, $formatter->getStyle('test'));
  112. $this->assertNotEquals($style, $formatter->getStyle('info'));
  113. $style = new OutputFormatterStyle('blue', 'white');
  114. $formatter->setStyle('b', $style);
  115. $this->assertEquals("\033[34;47msome \033[39;49m\033[34;47mcustom\033[39;49m\033[34;47m msg\033[39;49m", $formatter->format('<test>some <b>custom</b> msg</test>'));
  116. }
  117. public function testRedefineStyle()
  118. {
  119. $formatter = new OutputFormatter(true);
  120. $style = new OutputFormatterStyle('blue', 'white');
  121. $formatter->setStyle('info', $style);
  122. $this->assertEquals("\033[34;47msome custom msg\033[39;49m", $formatter->format('<info>some custom msg</info>'));
  123. }
  124. public function testInlineStyle()
  125. {
  126. $formatter = new OutputFormatter(true);
  127. $this->assertEquals("\033[34;41msome text\033[39;49m", $formatter->format('<fg=blue;bg=red>some text</>'));
  128. $this->assertEquals("\033[34;41msome text\033[39;49m", $formatter->format('<fg=blue;bg=red>some text</fg=blue;bg=red>'));
  129. }
  130. /**
  131. * @dataProvider provideInlineStyleOptionsCases
  132. */
  133. public function testInlineStyleOptions(string $tag, string $expected = null, string $input = null, bool $truecolor = false)
  134. {
  135. if ($truecolor && 'truecolor' !== getenv('COLORTERM')) {
  136. $this->markTestSkipped('The terminal does not support true colors.');
  137. }
  138. $styleString = substr($tag, 1, -1);
  139. $formatter = new OutputFormatter(true);
  140. $method = new \ReflectionMethod($formatter, 'createStyleFromString');
  141. $method->setAccessible(true);
  142. $result = $method->invoke($formatter, $styleString);
  143. if (null === $expected) {
  144. $this->assertNull($result);
  145. $expected = $tag.$input.'</'.$styleString.'>';
  146. $this->assertSame($expected, $formatter->format($expected));
  147. } else {
  148. /* @var OutputFormatterStyle $result */
  149. $this->assertInstanceOf(OutputFormatterStyle::class, $result);
  150. $this->assertSame($expected, $formatter->format($tag.$input.'</>'));
  151. $this->assertSame($expected, $formatter->format($tag.$input.'</'.$styleString.'>'));
  152. }
  153. }
  154. public static function provideInlineStyleOptionsCases()
  155. {
  156. return [
  157. ['<unknown=_unknown_>'],
  158. ['<unknown=_unknown_;a=1;b>'],
  159. ['<fg=green;>', "\033[32m[test]\033[39m", '[test]'],
  160. ['<fg=green;bg=blue;>', "\033[32;44ma\033[39;49m", 'a'],
  161. ['<fg=green;options=bold>', "\033[32;1mb\033[39;22m", 'b'],
  162. ['<fg=green;options=reverse;>', "\033[32;7m<a>\033[39;27m", '<a>'],
  163. ['<fg=green;options=bold,underscore>', "\033[32;1;4mz\033[39;22;24m", 'z'],
  164. ['<fg=green;options=bold,underscore,reverse;>', "\033[32;1;4;7md\033[39;22;24;27m", 'd'],
  165. ['<fg=#00ff00;bg=#00f>', "\033[38;2;0;255;0;48;2;0;0;255m[test]\033[39;49m", '[test]', true],
  166. ];
  167. }
  168. public function provideInlineStyleTagsWithUnknownOptions()
  169. {
  170. return [
  171. ['<options=abc;>', 'abc'],
  172. ['<options=abc,def;>', 'abc'],
  173. ['<fg=green;options=xyz;>', 'xyz'],
  174. ['<fg=green;options=efg,abc>', 'efg'],
  175. ];
  176. }
  177. public function testNonStyleTag()
  178. {
  179. $formatter = new OutputFormatter(true);
  180. $this->assertEquals("\033[32msome \033[39m\033[32m<tag>\033[39m\033[32m \033[39m\033[32m<setting=value>\033[39m\033[32m styled \033[39m\033[32m<p>\033[39m\033[32msingle-char tag\033[39m\033[32m</p>\033[39m", $formatter->format('<info>some <tag> <setting=value> styled <p>single-char tag</p></info>'));
  181. }
  182. public function testFormatLongString()
  183. {
  184. $formatter = new OutputFormatter(true);
  185. $long = str_repeat('\\', 14000);
  186. $this->assertEquals("\033[37;41msome error\033[39;49m".$long, $formatter->format('<error>some error</error>'.$long));
  187. }
  188. public function testFormatToStringObject()
  189. {
  190. $formatter = new OutputFormatter(false);
  191. $this->assertEquals(
  192. 'some info', $formatter->format(new TableCell())
  193. );
  194. }
  195. public function testFormatterHasStyles()
  196. {
  197. $formatter = new OutputFormatter(false);
  198. $this->assertTrue($formatter->hasStyle('error'));
  199. $this->assertTrue($formatter->hasStyle('info'));
  200. $this->assertTrue($formatter->hasStyle('comment'));
  201. $this->assertTrue($formatter->hasStyle('question'));
  202. }
  203. /**
  204. * @dataProvider provideDecoratedAndNonDecoratedOutput
  205. */
  206. public function testNotDecoratedFormatterOnJediTermEmulator(
  207. string $input,
  208. string $expectedNonDecoratedOutput,
  209. string $expectedDecoratedOutput,
  210. bool $shouldBeJediTerm = false
  211. ) {
  212. $terminalEmulator = $shouldBeJediTerm ? 'JetBrains-JediTerm' : 'Unknown';
  213. $prevTerminalEmulator = getenv('TERMINAL_EMULATOR');
  214. putenv('TERMINAL_EMULATOR='.$terminalEmulator);
  215. try {
  216. $this->assertEquals($expectedDecoratedOutput, (new OutputFormatter(true))->format($input));
  217. $this->assertEquals($expectedNonDecoratedOutput, (new OutputFormatter(false))->format($input));
  218. } finally {
  219. putenv('TERMINAL_EMULATOR'.($prevTerminalEmulator ? "=$prevTerminalEmulator" : ''));
  220. }
  221. }
  222. /**
  223. * @dataProvider provideDecoratedAndNonDecoratedOutput
  224. */
  225. public function testNotDecoratedFormatterOnIDEALikeEnvironment(
  226. string $input,
  227. string $expectedNonDecoratedOutput,
  228. string $expectedDecoratedOutput,
  229. bool $expectsIDEALikeTerminal = false
  230. ) {
  231. // Backup previous env variable
  232. $previousValue = $_SERVER['IDEA_INITIAL_DIRECTORY'] ?? null;
  233. $hasPreviousValue = \array_key_exists('IDEA_INITIAL_DIRECTORY', $_SERVER);
  234. if ($expectsIDEALikeTerminal) {
  235. $_SERVER['IDEA_INITIAL_DIRECTORY'] = __DIR__;
  236. } elseif ($hasPreviousValue) {
  237. // Forcibly remove the variable because the test runner may contain it
  238. unset($_SERVER['IDEA_INITIAL_DIRECTORY']);
  239. }
  240. try {
  241. $this->assertEquals($expectedDecoratedOutput, (new OutputFormatter(true))->format($input));
  242. $this->assertEquals($expectedNonDecoratedOutput, (new OutputFormatter(false))->format($input));
  243. } finally {
  244. // Rollback previous env state
  245. if ($hasPreviousValue) {
  246. $_SERVER['IDEA_INITIAL_DIRECTORY'] = $previousValue;
  247. } else {
  248. unset($_SERVER['IDEA_INITIAL_DIRECTORY']);
  249. }
  250. }
  251. }
  252. public static function provideDecoratedAndNonDecoratedOutput()
  253. {
  254. return [
  255. ['<error>some error</error>', 'some error', "\033[37;41msome error\033[39;49m"],
  256. ['<info>some info</info>', 'some info', "\033[32msome info\033[39m"],
  257. ['<comment>some comment</comment>', 'some comment', "\033[33msome comment\033[39m"],
  258. ['<question>some question</question>', 'some question', "\033[30;46msome question\033[39;49m"],
  259. ['<fg=red>some text with inline style</>', 'some text with inline style', "\033[31msome text with inline style\033[39m"],
  260. ['<href=idea://open/?file=/path/SomeFile.php&line=12>some URL</>', 'some URL', "\033]8;;idea://open/?file=/path/SomeFile.php&line=12\033\\some URL\033]8;;\033\\"],
  261. ['<href=https://example.com/\<woohoo\>>some URL with \<woohoo\></>', 'some URL with <woohoo>', "\033]8;;https://example.com/<woohoo>\033\\some URL with <woohoo>\033]8;;\033\\"],
  262. ['<href=idea://open/?file=/path/SomeFile.php&line=12>some URL</>', 'some URL', 'some URL', true],
  263. ];
  264. }
  265. public function testContentWithLineBreaks()
  266. {
  267. $formatter = new OutputFormatter(true);
  268. $this->assertEquals(<<<EOF
  269. \033[32m
  270. some text\033[39m
  271. EOF
  272. , $formatter->format(<<<'EOF'
  273. <info>
  274. some text</info>
  275. EOF
  276. ));
  277. $this->assertEquals(<<<EOF
  278. \033[32msome text
  279. \033[39m
  280. EOF
  281. , $formatter->format(<<<'EOF'
  282. <info>some text
  283. </info>
  284. EOF
  285. ));
  286. $this->assertEquals(<<<EOF
  287. \033[32m
  288. some text
  289. \033[39m
  290. EOF
  291. , $formatter->format(<<<'EOF'
  292. <info>
  293. some text
  294. </info>
  295. EOF
  296. ));
  297. $this->assertEquals(<<<EOF
  298. \033[32m
  299. some text
  300. more text
  301. \033[39m
  302. EOF
  303. , $formatter->format(<<<'EOF'
  304. <info>
  305. some text
  306. more text
  307. </info>
  308. EOF
  309. ));
  310. }
  311. public function testFormatAndWrap()
  312. {
  313. $formatter = new OutputFormatter(true);
  314. $this->assertSame("fo\no\e[37;41mb\e[39;49m\n\e[37;41mar\e[39;49m\nba\nz", $formatter->formatAndWrap('foo<error>bar</error> baz', 2));
  315. $this->assertSame("pr\ne \e[37;41m\e[39;49m\n\e[37;41mfo\e[39;49m\n\e[37;41mo\e[39;49m\n\e[37;41mba\e[39;49m\n\e[37;41mr\e[39;49m\n\e[37;41mba\e[39;49m\n\e[37;41mz\e[39;49m \npo\nst", $formatter->formatAndWrap('pre <error>foo bar baz</error> post', 2));
  316. $this->assertSame("pre\e[37;41m\e[39;49m\n\e[37;41mfoo\e[39;49m\n\e[37;41mbar\e[39;49m\n\e[37;41mbaz\e[39;49m\npos\nt", $formatter->formatAndWrap('pre <error>foo bar baz</error> post', 3));
  317. $this->assertSame("pre \e[37;41m\e[39;49m\n\e[37;41mfoo\e[39;49m\n\e[37;41mbar\e[39;49m\n\e[37;41mbaz\e[39;49m \npost", $formatter->formatAndWrap('pre <error>foo bar baz</error> post', 4));
  318. $this->assertSame("pre \e[37;41mf\e[39;49m\n\e[37;41moo\e[39;49m\n\e[37;41mbar\e[39;49m\n\e[37;41mbaz\e[39;49m p\nost", $formatter->formatAndWrap('pre <error>foo bar baz</error> post', 5));
  319. $this->assertSame("Lore\nm \e[37;41mip\e[39;49m\n\e[37;41msum\e[39;49m \ndolo\nr \e[32msi\e[39m\n\e[32mt\e[39m am\net", $formatter->formatAndWrap('Lorem <error>ipsum</error> dolor <info>sit</info> amet', 4));
  320. $this->assertSame("Lorem \e[37;41mip\e[39;49m\n\e[37;41msum\e[39;49m dolo\nr \e[32msit\e[39m am\net", $formatter->formatAndWrap('Lorem <error>ipsum</error> dolor <info>sit</info> amet', 8));
  321. $this->assertSame("Lorem \e[37;41mipsum\e[39;49m dolor \e[32m\e[39m\n\e[32msit\e[39m, \e[37;41mamet\e[39;49m et \e[32mlauda\e[39m\n\e[32mntium\e[39m architecto", $formatter->formatAndWrap('Lorem <error>ipsum</error> dolor <info>sit</info>, <error>amet</error> et <info>laudantium</info> architecto', 18));
  322. $formatter = new OutputFormatter();
  323. $this->assertSame("fo\nob\nar\nba\nz", $formatter->formatAndWrap('foo<error>bar</error> baz', 2));
  324. $this->assertSame("pr\ne \nfo\no\nba\nr\nba\nz \npo\nst", $formatter->formatAndWrap('pre <error>foo bar baz</error> post', 2));
  325. $this->assertSame("pre\nfoo\nbar\nbaz\npos\nt", $formatter->formatAndWrap('pre <error>foo bar baz</error> post', 3));
  326. $this->assertSame("pre \nfoo\nbar\nbaz \npost", $formatter->formatAndWrap('pre <error>foo bar baz</error> post', 4));
  327. $this->assertSame("pre f\noo\nbar\nbaz p\nost", $formatter->formatAndWrap('pre <error>foo bar baz</error> post', 5));
  328. $this->assertSame("Â rèälly\nlöng tîtlè\nthät cöüld\nnèêd\nmúltîplê\nlínès", $formatter->formatAndWrap('Â rèälly löng tîtlè thät cöüld nèêd múltîplê línès', 10));
  329. $this->assertSame("Â rèälly\nlöng tîtlè\nthät cöüld\nnèêd\nmúltîplê\n línès", $formatter->formatAndWrap("Â rèälly löng tîtlè thät cöüld nèêd múltîplê\n línès", 10));
  330. $this->assertSame('', $formatter->formatAndWrap(null, 5));
  331. }
  332. }
  333. class TableCell
  334. {
  335. public function __toString(): string
  336. {
  337. return '<info>some info</info>';
  338. }
  339. }