From 6a641c9cecb1557b98b86a3c083c78490f2ce259 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20Andr=C3=A9?= Date: Tue, 31 Dec 2024 17:13:43 +0100 Subject: [PATCH] [Console] Add a Tree Helper + multiple Styles --- src/Symfony/Component/Console/CHANGELOG.md | 2 + .../Component/Console/Helper/TreeHelper.php | 111 ++++++ .../Component/Console/Helper/TreeNode.php | 105 ++++++ .../Component/Console/Helper/TreeStyle.php | 78 ++++ .../Component/Console/Style/SymfonyStyle.php | 21 ++ .../Console/Tests/Helper/TreeHelperTest.php | 339 ++++++++++++++++++ .../Console/Tests/Helper/TreeNodeTest.php | 68 ++++ .../Console/Tests/Helper/TreeStyleTest.php | 226 ++++++++++++ .../Console/Tests/Style/SymfonyStyleTest.php | 95 +++++ 9 files changed, 1045 insertions(+) create mode 100644 src/Symfony/Component/Console/Helper/TreeHelper.php create mode 100644 src/Symfony/Component/Console/Helper/TreeNode.php create mode 100644 src/Symfony/Component/Console/Helper/TreeStyle.php create mode 100644 src/Symfony/Component/Console/Tests/Helper/TreeHelperTest.php create mode 100644 src/Symfony/Component/Console/Tests/Helper/TreeNodeTest.php create mode 100644 src/Symfony/Component/Console/Tests/Helper/TreeStyleTest.php diff --git a/src/Symfony/Component/Console/CHANGELOG.md b/src/Symfony/Component/Console/CHANGELOG.md index fc2b64bf156bb..d44d6ad458600 100644 --- a/src/Symfony/Component/Console/CHANGELOG.md +++ b/src/Symfony/Component/Console/CHANGELOG.md @@ -4,6 +4,8 @@ CHANGELOG 7.3 --- + * Add `TreeHelper` and `TreeStyle` to display tree-like structures + * Add `SymfonyStyle::createTree()` * Add support for invokable commands and add `#[Argument]` and `#[Option]` attributes to define input arguments and options * Deprecate not declaring the parameter type in callable commands defined through `setCode` method * Add support for help definition via `AsCommand` attribute diff --git a/src/Symfony/Component/Console/Helper/TreeHelper.php b/src/Symfony/Component/Console/Helper/TreeHelper.php new file mode 100644 index 0000000000000..561cd6ccb0320 --- /dev/null +++ b/src/Symfony/Component/Console/Helper/TreeHelper.php @@ -0,0 +1,111 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console\Helper; + +use Symfony\Component\Console\Output\OutputInterface; + +/** + * The TreeHelper class provides methods to display tree-like structures. + * + * @author Simon André + * + * @implements \RecursiveIterator + */ +final class TreeHelper implements \RecursiveIterator +{ + /** + * @var \Iterator + */ + private \Iterator $children; + + private function __construct( + private readonly OutputInterface $output, + private readonly TreeNode $node, + private readonly TreeStyle $style, + ) { + $this->children = new \IteratorIterator($this->node->getChildren()); + $this->children->rewind(); + } + + public static function createTree(OutputInterface $output, string|TreeNode|null $root = null, iterable $values = [], ?TreeStyle $style = null): self + { + $node = $root instanceof TreeNode ? $root : new TreeNode($root ?? ''); + + return new self($output, TreeNode::fromValues($values, $node), $style ?? TreeStyle::default()); + } + + public function current(): TreeNode + { + return $this->children->current(); + } + + public function key(): int + { + return $this->children->key(); + } + + public function next(): void + { + $this->children->next(); + } + + public function rewind(): void + { + $this->children->rewind(); + } + + public function valid(): bool + { + return $this->children->valid(); + } + + public function hasChildren(): bool + { + if (null === $current = $this->current()) { + return false; + } + + foreach ($current->getChildren() as $child) { + return true; + } + + return false; + } + + public function getChildren(): \RecursiveIterator + { + return new self($this->output, $this->current(), $this->style); + } + + /** + * Recursively renders the tree to the output, applying the tree style. + */ + public function render(): void + { + $treeIterator = new \RecursiveTreeIterator($this); + + $this->style->applyPrefixes($treeIterator); + + $this->output->writeln($this->node->getValue()); + + $visited = new \SplObjectStorage(); + foreach ($treeIterator as $node) { + $currentNode = $node instanceof TreeNode ? $node : $treeIterator->getInnerIterator()->current(); + if ($visited->contains($currentNode)) { + throw new \LogicException(\sprintf('Cycle detected at node: "%s".', $currentNode->getValue())); + } + $visited->attach($currentNode); + + $this->output->writeln($node); + } + } +} diff --git a/src/Symfony/Component/Console/Helper/TreeNode.php b/src/Symfony/Component/Console/Helper/TreeNode.php new file mode 100644 index 0000000000000..7f2ed8a4af371 --- /dev/null +++ b/src/Symfony/Component/Console/Helper/TreeNode.php @@ -0,0 +1,105 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console\Helper; + +/** + * @implements \IteratorAggregate + * + * @author Simon André + */ +final class TreeNode implements \Countable, \IteratorAggregate +{ + /** + * @var array + */ + private array $children = []; + + public function __construct( + private readonly string $value = '', + iterable $children = [], + ) { + foreach ($children as $child) { + $this->addChild($child); + } + } + + public static function fromValues(iterable $nodes, ?self $node = null): self + { + $node ??= new self(); + foreach ($nodes as $key => $value) { + if (is_iterable($value)) { + $child = new self($key); + self::fromValues($value, $child); + $node->addChild($child); + } elseif ($value instanceof self) { + $node->addChild($value); + } else { + $node->addChild(new self($value)); + } + } + + return $node; + } + + public function getValue(): string + { + return $this->value; + } + + public function addChild(self|string|callable $node): self + { + if (\is_string($node)) { + $node = new self($node, $this); + } + + $this->children[] = $node; + + return $this; + } + + /** + * @return \Traversable + */ + public function getChildren(): \Traversable + { + foreach ($this->children as $child) { + if (\is_callable($child)) { + yield from $child(); + } elseif ($child instanceof self) { + yield $child; + } + } + } + + /** + * @return \Traversable + */ + public function getIterator(): \Traversable + { + return $this->getChildren(); + } + + public function count(): int + { + $count = 0; + foreach ($this->getChildren() as $child) { + ++$count; + } + + return $count; + } + + public function __toString(): string + { + return $this->value; + } +} diff --git a/src/Symfony/Component/Console/Helper/TreeStyle.php b/src/Symfony/Component/Console/Helper/TreeStyle.php new file mode 100644 index 0000000000000..21cc04b3c05e8 --- /dev/null +++ b/src/Symfony/Component/Console/Helper/TreeStyle.php @@ -0,0 +1,78 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console\Helper; + +/** + * Configures the output of the Tree helper. + * + * @author Simon André + */ +final class TreeStyle +{ + public function __construct( + private readonly string $prefixEndHasNext, + private readonly string $prefixEndLast, + private readonly string $prefixLeft, + private readonly string $prefixMidHasNext, + private readonly string $prefixMidLast, + private readonly string $prefixRight, + ) { + } + + public static function box(): self + { + return new self('┃╸ ', '┗╸ ', '', '┃ ', ' ', ''); + } + + public static function boxDouble(): self + { + return new self('╠═ ', '╚═ ', '', '║ ', ' ', ''); + } + + public static function compact(): self + { + return new self('├ ', '└ ', '', '│ ', ' ', ''); + } + + public static function default(): self + { + return new self('├── ', '└── ', '', '│ ', ' ', ''); + } + + public static function light(): self + { + return new self('|-- ', '`-- ', '', '| ', ' ', ''); + } + + public static function minimal(): self + { + return new self('. ', '. ', '', '. ', ' ', ''); + } + + public static function rounded(): self + { + return new self('├─ ', '╰─ ', '', '│ ', ' ', ''); + } + + /** + * @internal + */ + public function applyPrefixes(\RecursiveTreeIterator $iterator): void + { + $iterator->setPrefixPart(\RecursiveTreeIterator::PREFIX_LEFT, $this->prefixLeft); + $iterator->setPrefixPart(\RecursiveTreeIterator::PREFIX_MID_HAS_NEXT, $this->prefixMidHasNext); + $iterator->setPrefixPart(\RecursiveTreeIterator::PREFIX_MID_LAST, $this->prefixMidLast); + $iterator->setPrefixPart(\RecursiveTreeIterator::PREFIX_END_HAS_NEXT, $this->prefixEndHasNext); + $iterator->setPrefixPart(\RecursiveTreeIterator::PREFIX_END_LAST, $this->prefixEndLast); + $iterator->setPrefixPart(\RecursiveTreeIterator::PREFIX_RIGHT, $this->prefixRight); + } +} diff --git a/src/Symfony/Component/Console/Style/SymfonyStyle.php b/src/Symfony/Component/Console/Style/SymfonyStyle.php index 4cf62cdba2cd3..d0788e88df663 100644 --- a/src/Symfony/Component/Console/Style/SymfonyStyle.php +++ b/src/Symfony/Component/Console/Style/SymfonyStyle.php @@ -21,6 +21,9 @@ use Symfony\Component\Console\Helper\Table; use Symfony\Component\Console\Helper\TableCell; use Symfony\Component\Console\Helper\TableSeparator; +use Symfony\Component\Console\Helper\TreeHelper; +use Symfony\Component\Console\Helper\TreeNode; +use Symfony\Component\Console\Helper\TreeStyle; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\ConsoleOutputInterface; use Symfony\Component\Console\Output\ConsoleSectionOutput; @@ -369,6 +372,24 @@ private function getProgressBar(): ProgressBar ?? throw new RuntimeException('The ProgressBar is not started.'); } + /** + * @param iterable $nodes + */ + public function tree(iterable $nodes, string $root = ''): void + { + $this->createTree($nodes, $root)->render(); + } + + /** + * @param iterable $nodes + */ + public function createTree(iterable $nodes, string $root = ''): TreeHelper + { + $output = $this->output instanceof ConsoleOutputInterface ? $this->output->section() : $this->output; + + return TreeHelper::createTree($output, $root, $nodes, TreeStyle::default()); + } + private function autoPrependBlock(): void { $chars = substr(str_replace(\PHP_EOL, "\n", $this->bufferedOutput->fetch()), -2); diff --git a/src/Symfony/Component/Console/Tests/Helper/TreeHelperTest.php b/src/Symfony/Component/Console/Tests/Helper/TreeHelperTest.php new file mode 100644 index 0000000000000..e7e1b54aef6cd --- /dev/null +++ b/src/Symfony/Component/Console/Tests/Helper/TreeHelperTest.php @@ -0,0 +1,339 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console\Tests\Helper; + +use PHPUnit\Framework\TestCase; +use Symfony\Component\Console\Helper\TreeHelper; +use Symfony\Component\Console\Helper\TreeNode; +use Symfony\Component\Console\Helper\TreeStyle; +use Symfony\Component\Console\Output\BufferedOutput; + +class TreeHelperTest extends TestCase +{ + public function testRenderWithoutNode() + { + $output = new BufferedOutput(); + $tree = TreeHelper::createTree($output); + + $tree->render(); + $this->assertSame("\n", $output->fetch()); + } + + public function testRenderSingleNode() + { + $rootNode = new TreeNode('Root'); + $output = new BufferedOutput(); + $tree = TreeHelper::createTree($output, $rootNode); + + $tree->render(); + $this->assertSame("Root\n", $output->fetch()); + } + + public function testRenderTwoLevelTree() + { + $rootNode = new TreeNode('Root'); + $child1 = new TreeNode('Child 1'); + $child2 = new TreeNode('Child 2'); + + $rootNode->addChild($child1); + $rootNode->addChild($child2); + + $output = new BufferedOutput(); + $tree = TreeHelper::createTree($output, $rootNode); + + $tree->render(); + $this->assertSame(<<fetch())); + } + + public function testRenderThreeLevelTree() + { + $rootNode = new TreeNode('Root'); + $child1 = new TreeNode('Child 1'); + $child2 = new TreeNode('Child 2'); + $subChild1 = new TreeNode('SubChild 1'); + + $child1->addChild($subChild1); + $rootNode->addChild($child1); + $rootNode->addChild($child2); + + $output = new BufferedOutput(); + $tree = TreeHelper::createTree($output, $rootNode); + + $tree->render(); + $this->assertSame(<<fetch())); + } + + public function testRenderMultiLevelTree() + { + $rootNode = new TreeNode('Root'); + $child1 = new TreeNode('Child 1'); + $child2 = new TreeNode('Child 2'); + $subChild1 = new TreeNode('SubChild 1'); + $subChild2 = new TreeNode('SubChild 2'); + $subSubChild1 = new TreeNode('SubSubChild 1'); + + $subChild1->addChild($subSubChild1); + $child1->addChild($subChild1); + $child1->addChild($subChild2); + $rootNode->addChild($child1); + $rootNode->addChild($child2); + + $output = new BufferedOutput(); + $tree = TreeHelper::createTree($output, $rootNode); + + $tree->render(); + $this->assertSame(<<fetch())); + } + + public function testRenderSingleNodeTree() + { + $rootNode = new TreeNode('Root'); + $output = new BufferedOutput(); + $tree = TreeHelper::createTree($output, $rootNode); + + $tree->render(); + $this->assertSame(<<fetch())); + } + + public function testRenderEmptyTree() + { + $rootNode = new TreeNode('Root'); + $output = new BufferedOutput(); + $tree = TreeHelper::createTree($output, $rootNode); + + $tree->render(); + $this->assertSame(<<fetch())); + } + + public function testRenderDeeplyNestedTree() + { + $rootNode = new TreeNode('Root'); + $current = $rootNode; + for ($i = 1; $i <= 10; ++$i) { + $child = new TreeNode("Level $i"); + $current->addChild($child); + $current = $child; + } + + $style = new TreeStyle(...[ + '└── ', + '└── ', + '', + ' ', + ' ', + '', + ]); + + $output = new BufferedOutput(); + $tree = TreeHelper::createTree($output, $rootNode, [], $style); + + $tree->render(); + $this->assertSame(<<fetch())); + } + + public function testRenderNodeWithMultipleChildren() + { + $rootNode = new TreeNode('Root'); + $child1 = new TreeNode('Child 1'); + $child2 = new TreeNode('Child 2'); + $child3 = new TreeNode('Child 3'); + + $rootNode->addChild($child1); + $rootNode->addChild($child2); + $rootNode->addChild($child3); + + $output = new BufferedOutput(); + $tree = TreeHelper::createTree($output, $rootNode); + + $tree->render(); + $this->assertSame(<<fetch())); + } + + public function testRenderTreeWithDuplicateNodeNames() + { + $rootNode = new TreeNode('Root'); + $child1 = new TreeNode('Child'); + $child2 = new TreeNode('Child'); + $subChild1 = new TreeNode('Child'); + + $child1->addChild($subChild1); + $rootNode->addChild($child1); + $rootNode->addChild($child2); + + $output = new BufferedOutput(); + $tree = TreeHelper::createTree($output, $rootNode); + + $tree->render(); + $this->assertSame(<<fetch())); + } + + public function testRenderTreeWithComplexNodeNames() + { + $rootNode = new TreeNode('Root'); + $child1 = new TreeNode('Child 1 (special)'); + $child2 = new TreeNode('Child_2@#$'); + $subChild1 = new TreeNode('Node with spaces'); + + $child1->addChild($subChild1); + $rootNode->addChild($child1); + $rootNode->addChild($child2); + + $output = new BufferedOutput(); + $tree = TreeHelper::createTree($output, $rootNode); + + $tree->render(); + $this->assertSame(<<fetch())); + } + + public function testRenderTreeWithCycle() + { + $rootNode = new TreeNode('Root'); + $child1 = new TreeNode('Child 1'); + $child2 = new TreeNode('Child 2'); + + $child1->addChild($child2); + // Create a cycle voluntarily + $child2->addChild($child1); + + $rootNode->addChild($child1); + + $output = new BufferedOutput(); + $tree = TreeHelper::createTree($output, $rootNode); + + $this->expectException(\LogicException::class); + $tree->render(); + } + + public function testRenderWideTree() + { + $rootNode = new TreeNode('Root'); + for ($i = 1; $i <= 100; ++$i) { + $rootNode->addChild(new TreeNode("Child $i")); + } + + $output = new BufferedOutput(); + + $tree = TreeHelper::createTree($output, $rootNode); + $tree->render(); + + $lines = explode("\n", trim($output->fetch())); + $this->assertCount(101, $lines); + $this->assertSame('Root', $lines[0]); + $this->assertSame('└── Child 100', end($lines)); + } + + public function testCreateWithRoot() + { + $output = new BufferedOutput(); + $array = ['child1', 'child2']; + + $tree = TreeHelper::createTree($output, 'root', $array); + + $tree->render(); + $this->assertSame(<<fetch())); + } + + public function testCreateWithNestedArray() + { + $output = new BufferedOutput(); + $array = ['child1', 'child2' => ['child2.1', 'child2.2' => ['child2.2.1']], 'child3']; + + $tree = TreeHelper::createTree($output, 'root', $array); + + $tree->render(); + $this->assertSame(<<fetch())); + } + + public function testCreateWithoutRoot() + { + $output = new BufferedOutput(); + $array = ['child1', 'child2']; + + $tree = TreeHelper::createTree($output, null, $array); + + $tree->render(); + $this->assertSame(<<fetch())); + } + + public function testCreateWithEmptyArray() + { + $output = new BufferedOutput(); + $array = []; + + $tree = TreeHelper::createTree($output, null, $array); + + $tree->render(); + $this->assertSame('', trim($output->fetch())); + } +} diff --git a/src/Symfony/Component/Console/Tests/Helper/TreeNodeTest.php b/src/Symfony/Component/Console/Tests/Helper/TreeNodeTest.php new file mode 100644 index 0000000000000..981e7ea477bfb --- /dev/null +++ b/src/Symfony/Component/Console/Tests/Helper/TreeNodeTest.php @@ -0,0 +1,68 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console\Tests\Helper; + +use PHPUnit\Framework\TestCase; +use Symfony\Component\Console\Helper\TreeNode; + +class TreeNodeTest extends TestCase +{ + public function testNodeInitialization() + { + $node = new TreeNode('Root'); + $this->assertSame('Root', $node->getValue()); + $this->assertSame(0, iterator_count($node->getChildren())); + } + + public function testAddingChildren() + { + $root = new TreeNode('Root'); + $child = new TreeNode('Child'); + + $root->addChild($child); + + $this->assertSame(1, iterator_count($root->getChildren())); + $this->assertSame($child, iterator_to_array($root->getChildren())[0]); + } + + public function testAddingChildrenWithGenerators() + { + $root = new TreeNode('Root'); + + $root->addChild(function () { + yield new TreeNode('Generated Child 1'); + yield new TreeNode('Generated Child 2'); + }); + + $this->assertSame(2, iterator_count($root->getChildren())); + + $children = iterator_to_array($root->getChildren()); + + $this->assertSame('Generated Child 1', $children[0]->getValue()); + $this->assertSame('Generated Child 2', $children[1]->getValue()); + } + + public function testRecursiveStructure() + { + $root = new TreeNode('Root'); + $child1 = new TreeNode('Child 1'); + $child2 = new TreeNode('Child 2'); + $leaf1 = new TreeNode('Leaf 1'); + + $child1->addChild($leaf1); + $root->addChild($child1); + $root->addChild($child2); + + $this->assertSame(2, iterator_count($root->getChildren())); + $this->assertSame($leaf1, iterator_to_array($child1->getChildren())[0]); + } +} diff --git a/src/Symfony/Component/Console/Tests/Helper/TreeStyleTest.php b/src/Symfony/Component/Console/Tests/Helper/TreeStyleTest.php new file mode 100644 index 0000000000000..216931f9c6b7d --- /dev/null +++ b/src/Symfony/Component/Console/Tests/Helper/TreeStyleTest.php @@ -0,0 +1,226 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Console\Tests\Helper; + +use PHPUnit\Framework\TestCase; +use Symfony\Component\Console\Helper\TreeHelper; +use Symfony\Component\Console\Helper\TreeNode; +use Symfony\Component\Console\Helper\TreeStyle; +use Symfony\Component\Console\Output\BufferedOutput; +use Symfony\Component\Console\Output\OutputInterface; + +class TreeStyleTest extends TestCase +{ + public function testDefaultStyle() + { + $output = new BufferedOutput(); + $tree = self::createTree($output); + + $tree->render(); + + $this->assertSame(<<fetch())); + } + + public function testBoxStyle() + { + $output = new BufferedOutput(); + $this->createTree($output, TreeStyle::box())->render(); + + $this->assertSame(<<fetch())); + } + + public function testBoxDoubleStyle() + { + $output = new BufferedOutput(); + $this->createTree($output, TreeStyle::boxDouble())->render(); + + $this->assertSame(<<fetch())); + } + + public function testCompactStyle() + { + $output = new BufferedOutput(); + $this->createTree($output, TreeStyle::compact())->render(); + + $this->assertSame(<<<'TREE' +root +├ A +│ ├ A1 +│ └ A2 +│ └ A2.1 +│ ├ A2.1.1 +│ └ A2.1.2 +├ B +│ ├ B1 +│ │ ├ B11 +│ │ └ B12 +│ └ B2 +└ C +TREE, trim($output->fetch())); + } + + public function testLightStyle() + { + $output = new BufferedOutput(); + $this->createTree($output, TreeStyle::light())->render(); + + $this->assertSame(<<<'TREE' +root +|-- A +| |-- A1 +| `-- A2 +| `-- A2.1 +| |-- A2.1.1 +| `-- A2.1.2 +|-- B +| |-- B1 +| | |-- B11 +| | `-- B12 +| `-- B2 +`-- C +TREE, trim($output->fetch())); + } + + public function testMinimalStyle() + { + $output = new BufferedOutput(); + $this->createTree($output, TreeStyle::minimal())->render(); + + $this->assertSame(<<<'TREE' +root +. A +. . A1 +. . A2 +. . A2.1 +. . A2.1.1 +. . A2.1.2 +. B +. . B1 +. . . B11 +. . . B12 +. . B2 +. C +TREE, trim($output->fetch())); + } + + public function testRoundedStyle() + { + $output = new BufferedOutput(); + $this->createTree($output, TreeStyle::rounded())->render(); + + $this->assertSame(<<<'TREE' +root +├─ A +│ ├─ A1 +│ ╰─ A2 +│ ╰─ A2.1 +│ ├─ A2.1.1 +│ ╰─ A2.1.2 +├─ B +│ ├─ B1 +│ │ ├─ B11 +│ │ ╰─ B12 +│ ╰─ B2 +╰─ C +TREE, trim($output->fetch())); + } + + public function testCustomPrefix() + { + $style = new TreeStyle('A ', 'B ', 'C ', 'D ', 'E ', 'F '); + $output = new BufferedOutput(); + self::createTree($output, $style)->render(); + + $this->assertSame(<<<'TREE' +root +C A F A +C D A F A1 +C D B F A2 +C D E B F A2.1 +C D E E A F A2.1.1 +C D E E B F A2.1.2 +C A F B +C D A F B1 +C D D A F B11 +C D D B F B12 +C D B F B2 +C B F C +TREE, trim($output->fetch())); + } + + private static function createTree(OutputInterface $output, ?TreeStyle $style = null): TreeHelper + { + $root = new TreeNode('root'); + $root + ->addChild((new TreeNode('A')) + ->addChild(new TreeNode('A1')) + ->addChild((new TreeNode('A2')) + ->addChild((new TreeNode('A2.1')) + ->addChild(new TreeNode('A2.1.1')) + ->addChild(new TreeNode('A2.1.2')) + ) + ) + ) + ->addChild((new TreeNode('B')) + ->addChild((new TreeNode('B1')) + ->addChild(new TreeNode('B11')) + ->addChild(new TreeNode('B12')) + ) + ->addChild(new TreeNode('B2')) + ) + ->addChild(new TreeNode('C')); + + return TreeHelper::createTree($output, $root, [], $style); + } +} diff --git a/src/Symfony/Component/Console/Tests/Style/SymfonyStyleTest.php b/src/Symfony/Component/Console/Tests/Style/SymfonyStyleTest.php index 0b40c7c3f972e..7ad08d4adf353 100644 --- a/src/Symfony/Component/Console/Tests/Style/SymfonyStyleTest.php +++ b/src/Symfony/Component/Console/Tests/Style/SymfonyStyleTest.php @@ -15,9 +15,11 @@ use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Exception\RuntimeException; use Symfony\Component\Console\Formatter\OutputFormatter; +use Symfony\Component\Console\Helper\TreeHelper; use Symfony\Component\Console\Input\ArrayInput; use Symfony\Component\Console\Input\Input; use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\BufferedOutput; use Symfony\Component\Console\Output\ConsoleOutputInterface; use Symfony\Component\Console\Output\ConsoleSectionOutput; use Symfony\Component\Console\Output\NullOutput; @@ -154,6 +156,99 @@ public function testCreateTableWithoutConsoleOutput() $style->createTable()->appendRow(['row']); } + public function testCreateTree() + { + $output = $this->createMock(OutputInterface::class); + $output + ->method('getFormatter') + ->willReturn(new OutputFormatter()); + + $style = new SymfonyStyle($this->createMock(InputInterface::class), $output); + + $tree = $style->createTree([]); + $this->assertInstanceOf(TreeHelper::class, $tree); + } + + public function testTree() + { + $input = $this->createMock(InputInterface::class); + $output = new BufferedOutput(); + $style = new SymfonyStyle($input, $output); + + $tree = $style->createTree(['A', 'B' => ['B1' => ['B11', 'B12'], 'B2'], 'C'], 'root'); + $tree->render(); + + $this->assertSame(<<fetch())); + } + + public function testCreateTreeWithArray() + { + $input = $this->createMock(InputInterface::class); + $output = new BufferedOutput(); + $style = new SymfonyStyle($input, $output); + + $tree = $style->createTree(['A', 'B' => ['B1' => ['B11', 'B12'], 'B2'], 'C'], 'root'); + $tree->render(); + + $this->assertSame($tree = <<fetch())); + } + + public function testCreateTreeWithIterable() + { + $input = $this->createMock(InputInterface::class); + $output = new BufferedOutput(); + $style = new SymfonyStyle($input, $output); + + $tree = $style->createTree(new \ArrayIterator(['A', 'B' => ['B1' => ['B11', 'B12'], 'B2'], 'C']), 'root'); + $tree->render(); + + $this->assertSame(<<fetch())); + } + + public function testCreateTreeWithConsoleOutput() + { + $input = $this->createMock(InputInterface::class); + $output = $this->createMock(ConsoleOutputInterface::class); + $output + ->method('getFormatter') + ->willReturn(new OutputFormatter()); + $output + ->expects($this->once()) + ->method('section') + ->willReturn($this->createMock(ConsoleSectionOutput::class)); + + $style = new SymfonyStyle($input, $output); + + $style->createTree([]); + } + public function testGetErrorStyleUsesTheCurrentOutputIfNoErrorOutputIsAvailable() { $output = $this->createMock(OutputInterface::class);