From 4bfb28f6b654ad9eb5c3760c527101fe0ce8e085 Mon Sep 17 00:00:00 2001 From: "sascha.heilmeier" Date: Tue, 4 Apr 2023 09:37:03 +0200 Subject: [PATCH 01/10] add ProgressOutputNodeProcessor --- composer.json | 3 +- .../ProgressOutputNodeProcessor.php | 67 +++++++++++++ .../ProgressOutputNodeProcessorTest.php | 96 +++++++++++++++++++ 3 files changed, 165 insertions(+), 1 deletion(-) create mode 100644 src/NodeProcessor/ProgressOutputNodeProcessor.php create mode 100644 tests/Unit/NodeProcessor/ProgressOutputNodeProcessorTest.php diff --git a/composer.json b/composer.json index c4f3b86..0176068 100644 --- a/composer.json +++ b/composer.json @@ -8,7 +8,8 @@ }, "require-dev": { "behat/behat": "^3.12.0", - "phpunit/phpunit": "^9.6.6" + "phpunit/phpunit": "^9.6.6", + "symfony/console": "^5.0 || ^6.0" }, "autoload": { "psr-4": { diff --git a/src/NodeProcessor/ProgressOutputNodeProcessor.php b/src/NodeProcessor/ProgressOutputNodeProcessor.php new file mode 100644 index 0000000..32042b3 --- /dev/null +++ b/src/NodeProcessor/ProgressOutputNodeProcessor.php @@ -0,0 +1,67 @@ +progressBarFactory = $progressBarFactory ?? new NodeProcessorProgressBarFactory(); + } + + public function SetOutput(?ConsoleOutputInterface $output = NULL): void + { + $this->output = $output; + $this->progressBar = NULL; + } + + function getSubscribedEvents(string $nodePath, XmlProcessorContext $context): \Iterator + { + if ($this->progressBar === NULL) { + yield XmlProcessor::EVENT_OPEN_FILE => [$this, 'openFile']; + } else { + yield 'NodeType_' . \XMLReader::ELEMENT => [$this, 'openElement']; + yield XmlProcessor::EVENT_END_OF_FILE => [$this, 'endOfFile']; + } + } + + function openFile(): void + { + if ($this->progressBar !== NULL) { + $this->progressBar->finish(); + } + if($this->output === NULL) { + $this->progressBar = NULL; + return; + } + $this->progressBar = $this->progressBarFactory->createProgressBar($this->output); + } + + function openElement(OpenContext $context): void + { + $this->progressBar->advance(); + $this->progressBar->setMessage($context->getNodePath(), 'node'); + } + + function endOfFile(): void + { + $this->progressBar->finish(); + $this->progressBar = NULL; + } +} diff --git a/tests/Unit/NodeProcessor/ProgressOutputNodeProcessorTest.php b/tests/Unit/NodeProcessor/ProgressOutputNodeProcessorTest.php new file mode 100644 index 0000000..c7f475d --- /dev/null +++ b/tests/Unit/NodeProcessor/ProgressOutputNodeProcessorTest.php @@ -0,0 +1,96 @@ +getMockBuilder(XmlProcessorContext::class) + ->disableOriginalConstructor() + ->getMock(); + + self::assertEquals( + [ + XmlProcessor::EVENT_OPEN_FILE => [$nodeProcessor, 'openFile'] + ], + iterator_to_array($nodeProcessor->getSubscribedEvents('test', $context)) + ); + + $output = $this::getMockForAbstractClass(ConsoleOutputInterface::class); + $nodeProcessor->setOutput($output); + $nodeProcessor->openFile(); + + self::assertEquals( + [ + 'NodeType_' . \XMLReader::ELEMENT => [$nodeProcessor, 'openElement'], + XmlProcessor::EVENT_END_OF_FILE => [$nodeProcessor, 'endOfFile'] + ], + iterator_to_array($nodeProcessor->getSubscribedEvents('test', $context)) + ); + } + + public function testOpenFile(): void + { + $reflectionClass = new \ReflectionClass(ProgressOutputNodeProcessor::class); + + $nodeProcessor = new ProgressOutputNodeProcessor(); + $nodeProcessor->openFile(); + self::assertNull($reflectionClass->getProperty('progressBar')->getValue($nodeProcessor)); + + $output = $this::getMockForAbstractClass(ConsoleOutputInterface::class); + $nodeProcessor->setOutput($output); + $nodeProcessor->openFile(); + self::assertInstanceOf(ProgressBar::class, $reflectionClass->getProperty('progressBar')->getValue($nodeProcessor)); + + self::markTestIncomplete('ToDo: $this->progressBar->finish()'); + } + + public function testOpenElement(): void + { + $nodeProcessor = new ProgressOutputNodeProcessor(); + $output = $this::getMockForAbstractClass(ConsoleOutputInterface::class); + $nodeProcessor->setOutput($output); + $nodeProcessor->openFile(); + + $openContext = $this->createMock(OpenContext::class); + $openContext->method('getNodePath')->willReturn('foo/bar'); + + $nodeProcessor->openElement($openContext); + $reflectionClass = new \ReflectionClass($nodeProcessor); + + /** @var ProgressBar $progressBar */ + $progressBar = $reflectionClass->getProperty('progressBar')->getValue($nodeProcessor); + self::assertEquals(1, $progressBar->getProgress()); + self::assertEquals('foo/bar', $progressBar->getMessage('node')); + } + + function testEndOfFile(): void + { + $nodeProcessor = new ProgressOutputNodeProcessor(); + $output = $this::getMockForAbstractClass(ConsoleOutputInterface::class); + $nodeProcessor->setOutput($output); + $nodeProcessor->openFile(); + + $nodeProcessor->endOfFile(); + $reflectionClass = new \ReflectionClass($nodeProcessor); + $progressBar = $reflectionClass->getProperty('progressBar'); + self::assertNull($progressBar->getValue($nodeProcessor)); + } +} From c4a68a713013ad5a22b4c0fed3c4afe24e297758 Mon Sep 17 00:00:00 2001 From: "sascha.heilmeier" Date: Tue, 4 Apr 2023 09:44:08 +0200 Subject: [PATCH 02/10] add missing NodeProcessorProgressBarFactory --- .../NodeProcessorProgressBarFactory.php | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 src/Factory/NodeProcessorProgressBarFactory.php diff --git a/src/Factory/NodeProcessorProgressBarFactory.php b/src/Factory/NodeProcessorProgressBarFactory.php new file mode 100644 index 0000000..983167c --- /dev/null +++ b/src/Factory/NodeProcessorProgressBarFactory.php @@ -0,0 +1,27 @@ +setBarCharacter('⚬'); + $progressBar->setEmptyBarCharacter("⚬"); + $progressBar->setProgressCharacter("➤"); + $progressBar->setFormat( + "%node%\n%current% [%bar%]\n %memory:6s%\n" + ); + + return $progressBar; + } +} \ No newline at end of file From eece4bd2a44ba5b05c5127e69b007aee95aaaac5 Mon Sep 17 00:00:00 2001 From: Sascha Date: Wed, 5 Apr 2023 10:00:14 +0200 Subject: [PATCH 03/10] add skipNodes to XmlProcessor (#6) --- features/SkipNode.feature | 61 +++++++++++++ features/bootstrap/FeatureContext.php | 10 ++- src/NodeProcessor/AbstractNodeProcessor.php | 13 +-- src/XmlProcessor.php | 73 ++++++++++++++-- src/XmlProcessorContext.php | 13 ++- .../TestNodeProcessor.php | 20 ++++- tests/Fixtures/XmlProcessorTest/test.xml | 6 +- .../NodeProcessor/ArrayNodeProcessorTest.php | 11 +++ .../NodeProcessor/TextNodeProcessorTest.php | 1 + .../AbstractNodeProcessorTest.php | 23 ++++- .../Context/CloseContextTest.php | 1 + .../NodeProcessor/Context/OpenContextTest.php | 1 - .../NodeProcessor/Context/TextContextTest.php | 1 - tests/Unit/XmlProcessorContextTest.php | 31 +++++-- tests/Unit/XmlProcessorTest.php | 85 ++++++++++++++++--- 15 files changed, 307 insertions(+), 43 deletions(-) create mode 100644 features/SkipNode.feature diff --git a/features/SkipNode.feature b/features/SkipNode.feature new file mode 100644 index 0000000..ac7cb11 --- /dev/null +++ b/features/SkipNode.feature @@ -0,0 +1,61 @@ +Feature: run XMLProcessor with TextNodeProcessor + + Scenario: run XMLProcessor + Given initialize XMLProcessor with "Netlogix\XmlProcessor\Behat\NodeProcessor\ArrayNodeProcessor" + When set skipNode to: + """ + category + """ + When process xml with current XMLProcessor instance: + """ + + foo + + + baz + + + + bar + + """ + Then NodeProcessor "Netlogix\XmlProcessor\Behat\NodeProcessor\ArrayNodeProcessor" should return: + """ +[ + { + "node": "root", + "level": 1, + "attributes": { + "name": "main" + }, + "children": [ + { + "node": "product", + "level": 2, + "attributes": { + "id": "1" + }, + "children": [], + "text": "foo" + }, + { + "node": "category", + "level": 2, + "attributes": { + "id": "1" + }, + "children": [] + }, + { + "node": "product", + "level": 2, + "attributes": { + "id": "2" + }, + "children": [], + "text": "bar" + } + ] + } +] + """ \ No newline at end of file diff --git a/features/bootstrap/FeatureContext.php b/features/bootstrap/FeatureContext.php index 53883d5..637f33d 100644 --- a/features/bootstrap/FeatureContext.php +++ b/features/bootstrap/FeatureContext.php @@ -40,6 +40,14 @@ public function iRunXmlProcessor(PyStringNode $content): void $this->xmlProcessor->processFile($fileName); } + /** + * @When set skipNode to: + */ + public function iSetSkipNode(PyStringNode $skipNode): void + { + $this->xmlProcessor->setSkipNodes($skipNode->getStrings()); + } + /** * @Then NodeProcessor :nodeProcessorClass should return: */ @@ -52,7 +60,7 @@ function nodeProcessorShouldReturn(string $nodeProcessorClass, PyStringNode $con throw new \Exception(sprintf('Class %s does not extend %s', $nodeProcessorClass, NodeProcessorInterface::class)); } /** @var InvokeNodeProcessorInterface $nodeProcessor */ - $nodeProcessor = $this->xmlProcessor->getProcessorContext()->getProcessor($nodeProcessorClass); + $nodeProcessor = $this->xmlProcessor->getProcessor($nodeProcessorClass); $expected = json_decode($content->getRaw(), true); if (json_last_error() !== JSON_ERROR_NONE) { throw new \Exception(sprintf('Could not decode expected result: %s', json_last_error_msg())); diff --git a/src/NodeProcessor/AbstractNodeProcessor.php b/src/NodeProcessor/AbstractNodeProcessor.php index c0ac117..1544709 100644 --- a/src/NodeProcessor/AbstractNodeProcessor.php +++ b/src/NodeProcessor/AbstractNodeProcessor.php @@ -3,6 +3,7 @@ namespace Netlogix\XmlProcessor\NodeProcessor; +use Netlogix\XmlProcessor\XmlProcessor; use Netlogix\XmlProcessor\XmlProcessorContext; class AbstractNodeProcessor implements NodeProcessorInterface @@ -34,16 +35,6 @@ public function getSubscribedEvents(string $nodePath, XmlProcessorContext $conte public function isNode(string $nodePath): bool { - $expected = $this->getNodePath(); - if ($expected === '/' . $nodePath) { - return true; - } - - return $nodePath === $this->getNodePath() - || ( - function_exists('str_end_with') - ? str_end_with($nodePath, $expected) : - substr_compare($nodePath, $expected, -strlen($expected)) === 0 - ); + return XmlProcessor::checkNodePath($nodePath, $this->getNodePath()); } } diff --git a/src/XmlProcessor.php b/src/XmlProcessor.php index b15d098..885c686 100644 --- a/src/XmlProcessor.php +++ b/src/XmlProcessor.php @@ -17,37 +17,57 @@ class XmlProcessor private array $nodePath = []; private string $currentValue = ''; + private ?array $skipNodes = NULL; + private \XMLReader $xml; private XmlProcessorContext $context; /** @var iterable */ private iterable $processors; + /** @var iterable */ + private iterable $parserProperties; + /** * @param iterable $processors - * @param iterable $options + * @param iterable $parserProperties */ public function __construct( iterable $processors, - iterable $options = [] + iterable $parserProperties = [] ) { $this->xml = new \XMLReader(); - foreach ($options as $option => $value) { - $this->xml->setParserProperty($option, $value); - } $this->processors = $processors; - $this->context = new XmlProcessorContext($this->xml, $this->processors); + $this->parserProperties = $parserProperties; + $this->context = new XmlProcessorContext( + $this->xml, + $this->processors, + fn() => $this->skipNode() + ); } - function getProcessorContext(): XmlProcessorContext + function setSkipNodes(?array $skipNodes = NULL): void { - return $this->context; + $this->skipNodes = $skipNodes; + } + + function getSkipNodes(): ?array + { + return $this->skipNodes; + } + + function getProcessor(string $processorName): ?NodeProcessorInterface + { + return $this->context->getProcessor($processorName); } public function processFile(string $filename): void { $this->xml->open($filename); + foreach ($this->parserProperties as $parserProperty => $value) { + $this->xml->setParserProperty($parserProperty, $value); + } $this->getProcessorEvents(self::EVENT_OPEN_FILE); while ($this->xml->read()) { switch ($this->xml->nodeType) { @@ -57,6 +77,10 @@ public function processFile(string $filename): void case \XMLReader::ELEMENT: $selfClosing = $this->xml->isEmptyElement; $this->eventOpenElement(); + if ($this->shouldSkipNode()) { + $this->skipNode(); + break; + } if ($selfClosing) { $this->eventCloseElement(); } @@ -73,6 +97,28 @@ public function processFile(string $filename): void $this->xml->close(); } + private function skipNode(): bool + { + $result = $this->xml->next(); + $this->eventCloseElement(); + return $result; + } + + private function shouldSkipNode(): bool + { + if ($this->skipNodes === NULL) { + return false; + } + $nodePath = implode('/', $this->nodePath); + foreach ($this->skipNodes as $skipNode) { + if (self::checkNodePath($nodePath, $skipNode)) { + return true; + } + } + + return false; + } + private function eventOpenElement(): void { $this->pushNodePath(); @@ -148,4 +194,15 @@ private function createContext(string $contextClass): NodeProcessorContext } return $context; } + + static function checkNodePath(string $nodePath, string $expected): bool + { + return + $expected === '/' . $nodePath || + $nodePath === $expected || ( + function_exists('str_end_with') + ? str_end_with($nodePath, $expected) : + substr_compare($nodePath, $expected, -strlen($expected)) === 0 + ); + } } diff --git a/src/XmlProcessorContext.php b/src/XmlProcessorContext.php index 842a9b1..0e91d01 100644 --- a/src/XmlProcessorContext.php +++ b/src/XmlProcessorContext.php @@ -3,6 +3,7 @@ namespace Netlogix\XmlProcessor; +use Netlogix\XmlProcessor\NodeProcessor\NamedNodeProcessorInterface; use Netlogix\XmlProcessor\NodeProcessor\NodeProcessorInterface; class XmlProcessorContext @@ -13,16 +14,24 @@ class XmlProcessorContext */ private iterable $processors; - public function __construct(\XMLReader $xml, iterable $processors) + private \Closure $skipNode; + + public function __construct(\XMLReader $xml, iterable $processors, \Closure $skipNode) { $this->xml = $xml; $this->processors = $processors; + $this->skipNode = $skipNode; + } + + public function skipCurrentNode(): bool + { + return ($this->skipNode)(); } public function getProcessor(string $class): ?NodeProcessorInterface { foreach ($this->processors as $processor) { - if ($processor instanceof $class) { + if (class_exists($class) && $processor instanceof $class) { return $processor; } } diff --git a/tests/Fixtures/AbstractNodeProcessorTest/TestNodeProcessor.php b/tests/Fixtures/AbstractNodeProcessorTest/TestNodeProcessor.php index 4fea789..bccc615 100644 --- a/tests/Fixtures/AbstractNodeProcessorTest/TestNodeProcessor.php +++ b/tests/Fixtures/AbstractNodeProcessorTest/TestNodeProcessor.php @@ -3,8 +3,26 @@ namespace Netlogix\XmlProcessor\Tests\Fixtures\AbstractNodeProcessorTest; use Netlogix\XmlProcessor\NodeProcessor\AbstractNodeProcessor; +use Netlogix\XmlProcessor\NodeProcessor\CloseNodeProcessorInterface; +use Netlogix\XmlProcessor\NodeProcessor\Context\CloseContext; +use Netlogix\XmlProcessor\NodeProcessor\Context\OpenContext; +use Netlogix\XmlProcessor\NodeProcessor\Context\TextContext; +use Netlogix\XmlProcessor\NodeProcessor\OpenNodeProcessorInterface; +use Netlogix\XmlProcessor\NodeProcessor\TextNodeProcessorInterface; -class TestNodeProcessor extends AbstractNodeProcessor +class TestNodeProcessor extends AbstractNodeProcessor implements OpenNodeProcessorInterface, TextNodeProcessorInterface, CloseNodeProcessorInterface { const NODE_PATH = 'test'; + + function openElement(OpenContext $context): void + { + } + + function textElement(TextContext $context): void + { + } + + function closeElement(CloseContext $context): void + { + } } \ No newline at end of file diff --git a/tests/Fixtures/XmlProcessorTest/test.xml b/tests/Fixtures/XmlProcessorTest/test.xml index d458015..ead37af 100644 --- a/tests/Fixtures/XmlProcessorTest/test.xml +++ b/tests/Fixtures/XmlProcessorTest/test.xml @@ -1,4 +1,8 @@ - hallo + + text + + hallo +
\ No newline at end of file diff --git a/tests/Unit/Behat/NodeProcessor/ArrayNodeProcessorTest.php b/tests/Unit/Behat/NodeProcessor/ArrayNodeProcessorTest.php index ada431e..e8f85f1 100644 --- a/tests/Unit/Behat/NodeProcessor/ArrayNodeProcessorTest.php +++ b/tests/Unit/Behat/NodeProcessor/ArrayNodeProcessorTest.php @@ -1,9 +1,11 @@ ['foo', 'bar'], 'attributes' => ['name' => 'me'], + 'text' => 'test' ], [ 'nodePath' => ['foo', 'bar'], 'attributes' => ['name' => 'you'], + 'text' => 'test2' ], [ 'nodePath' => ['foo'], @@ -57,6 +61,11 @@ function testOpenElement(): void $context = new OpenContext($xmlProcessorContext, $item['nodePath']); $context->setAttributes($item['attributes']); $nodeProcessor->openElement($context); + if (isset($item['text'])) { + $textContext = new TextContext($xmlProcessorContext, $item['nodePath']); + $textContext->setText($item['text']); + $nodeProcessor->textElement($textContext); + } } self::assertEquals([ @@ -70,12 +79,14 @@ function testOpenElement(): void 'level' => 2, 'attributes' => ['name' => 'me'], 'children' => [], + 'text' => 'test' ], [ 'node' => 'bar', 'level' => 2, 'attributes' => ['name' => 'you'], 'children' => [], + 'text' => 'test2' ], ], diff --git a/tests/Unit/Behat/NodeProcessor/TextNodeProcessorTest.php b/tests/Unit/Behat/NodeProcessor/TextNodeProcessorTest.php index 112f5bd..888018a 100644 --- a/tests/Unit/Behat/NodeProcessor/TextNodeProcessorTest.php +++ b/tests/Unit/Behat/NodeProcessor/TextNodeProcessorTest.php @@ -1,4 +1,5 @@ assertEquals($expectedResult, $nodeProcessor->isNode($nodePath)); + $nodeProcessor = $this->getMockForAbstractClass( + TestNodeProcessor::class + ); + $context = $this->createMock(XmlProcessorContext::class); + $events = []; + foreach ($nodeProcessor->getSubscribedEvents('test', $context) as $event => $action) { + $events[] = $event; + self::assertIsCallable($action); + } + + self::assertEquals([ + 'NodeType_' . \XMLReader::ELEMENT, + 'NodeType_' . \XMLReader::END_ELEMENT, + 'NodeType_' . \XMLReader::TEXT, + ], $events); } public static function isNodeDataProvider(): \Generator diff --git a/tests/Unit/NodeProcessor/Context/CloseContextTest.php b/tests/Unit/NodeProcessor/Context/CloseContextTest.php index aaa2b7b..be439a2 100644 --- a/tests/Unit/NodeProcessor/Context/CloseContextTest.php +++ b/tests/Unit/NodeProcessor/Context/CloseContextTest.php @@ -1,4 +1,5 @@ getXMLReaderMock(), []); + $context = new XmlProcessorContext($this->getXMLReaderMock(), [], fn() => true); $this->assertInstanceOf(XmlProcessorContext::class, $context); } function testGetXMLReader(): void { $xmlReader = $this->getXMLReaderMock(); - $context = new XmlProcessorContext($xmlReader, []); + $context = new XmlProcessorContext($xmlReader, [], fn() => true); $this->assertSame($xmlReader, $context->getXMLReader()); } @@ -35,7 +39,7 @@ function testGetXMLReader(): void */ function testGetProcessor($processor, $expected): void { - $context = new XmlProcessorContext($this->getXMLReaderMock(), [$processor]); + $context = new XmlProcessorContext($this->getXMLReaderMock(), [$processor], fn() => true); $this->assertSame($expected, $context->getProcessor(NodeProcessorInterface::class)); } @@ -45,6 +49,23 @@ public static function getProcessorDataProvider(): \Generator yield [[], NULL]; } + /** + * @dataProvider skipCurrentNodeDataProvider + */ + public function testSkipCurrentNode(bool $return): void + { + $skipNodeMock = $this->getMockBuilder(\stdClass::class)->addMethods(['skipNode'])->getMock(); + $skipNodeMock->expects($this->atLeastOnce())->method('skipNode')->willReturn($return); + $context = new XmlProcessorContext($this->getXMLReaderMock(), [], fn() => $skipNodeMock->skipNode()); + self::assertEquals($context->skipCurrentNode(), $return); + } + + function skipCurrentNodeDataProvider(): iterable + { + yield [true]; + yield [false]; + } + private function getXMLReaderMock(): \XMLReader { return $this->getMockBuilder(\XMLReader::class)->getMock(); diff --git a/tests/Unit/XmlProcessorTest.php b/tests/Unit/XmlProcessorTest.php index 83369b6..73a8ee6 100644 --- a/tests/Unit/XmlProcessorTest.php +++ b/tests/Unit/XmlProcessorTest.php @@ -1,4 +1,5 @@ getMockForAbstractClass(NodeProcessorInterface::class) - ]); + $xmlProcessor = new XmlProcessor( + [ + $this->getMockForAbstractClass(NodeProcessorInterface::class) + ], + [ + \XMLReader::SUBST_ENTITIES => true + ] + ); self::assertInstanceOf(XmlProcessor::class, $xmlProcessor); } - public function testGetProcessorContext(): void + public function testGetProcessor(): void { - $xmlProcessor = new XmlProcessor([ - $this->getMockForAbstractClass(NodeProcessorInterface::class) - ]); - $context = $xmlProcessor->getProcessorContext(); - self::assertInstanceOf(XmlProcessorContext::class, $context); + $nodeProcessor = $this->getMockForAbstractClass(TestNodeProcessor::class); + $xmlProcessor = new XmlProcessor([$nodeProcessor]); + + self::assertInstanceOf(TestNodeProcessor::class, $xmlProcessor->getProcessor(TestNodeProcessor::class)); + self::assertInstanceOf(get_class($nodeProcessor), $xmlProcessor->getProcessor(TestNodeProcessor::class)); + self::assertNull($xmlProcessor->getProcessor(OpenNodeProcessorInterface::class)); } public function testProcessFile() @@ -58,5 +64,62 @@ public function testProcessFile() ]); $xmlProcessor->processFile(__DIR__ . '/../Fixtures/XmlProcessorTest/test.xml'); + + $xmlProcessor->setSkipNodes(['foo']); + $xmlProcessor->processFile(__DIR__ . '/../Fixtures/XmlProcessorTest/test.xml'); + + $xmlProcessor = new XmlProcessor( + [$nodeProcessor], + [\XMLReader::SUBST_ENTITIES => true] + ); + $xmlProcessor->processFile(__DIR__ . '/../Fixtures/XmlProcessorTest/test.xml'); + + if(!function_exists('str_end_with')){ + function str_end_with(string $nodePath, string $expected){ + return substr_compare($nodePath, $expected, -strlen($expected)) === 0; + } + } + $xmlProcessor->processFile(__DIR__ . '/../Fixtures/XmlProcessorTest/test.xml'); + } + + /** + * @dataProvider checkNodePathDataProvider + */ + function testCheckNodePath(string $nodePath, string $expected, bool $result): void + { + self::assertSame(XmlProcessor::checkNodePath($nodePath, $expected), $result); } + + public static function checkNodePathDataProvider(): iterable + { + yield ['/foo/bar', 'foo/bar', true]; + yield ['/foo', 'foo/bar', false]; + yield ['foo', 'foo/bar', false]; + yield ['bar', 'foo/bar', false]; + yield ['foo/bar', 'bar', true]; + yield ['foo/bar', 'foo/bar', true]; + yield ['foo/bar/baz', 'foo/bar', false]; + } + + function testSetSkipNodes(): void + { + $xmlProcessor = new XmlProcessor([ + $this->getMockForAbstractClass(NodeProcessorInterface::class) + ]); + $xmlProcessor->setSkipNodes(['foo']); + self::assertSame(['foo'], $xmlProcessor->getSkipNodes()); + } + + function testGetSkipNodes(): void + { + $xmlProcessor = new XmlProcessor([ + $this->getMockForAbstractClass(NodeProcessorInterface::class) + ]); + self::assertNull($xmlProcessor->getSkipNodes()); + $xmlProcessor->setSkipNodes([]); + self::assertSame([], $xmlProcessor->getSkipNodes()); + $xmlProcessor->setSkipNodes(['foo']); + self::assertSame(['foo'], $xmlProcessor->getSkipNodes()); + } + } From e3a1a43ff5a6d270365c8ed8c7b20446f8d86819 Mon Sep 17 00:00:00 2001 From: "sascha.heilmeier" Date: Wed, 5 Apr 2023 12:04:41 +0200 Subject: [PATCH 04/10] install dg/bypass-finals add PHPUnit extension BypassFinalHook to mock final classes --- composer.json | 3 ++- phpunit.xml | 3 +++ tests/Hooks/BypassFinalHook.php | 18 ++++++++++++++++++ 3 files changed, 23 insertions(+), 1 deletion(-) create mode 100644 tests/Hooks/BypassFinalHook.php diff --git a/composer.json b/composer.json index 0176068..400330d 100644 --- a/composer.json +++ b/composer.json @@ -9,7 +9,8 @@ "require-dev": { "behat/behat": "^3.12.0", "phpunit/phpunit": "^9.6.6", - "symfony/console": "^5.0 || ^6.0" + "symfony/console": "^5.0 || ^6.0", + "dg/bypass-finals": "^1.4" }, "autoload": { "psr-4": { diff --git a/phpunit.xml b/phpunit.xml index ad0eeee..05d225e 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -4,6 +4,9 @@ bootstrap="vendor/autoload.php" colors="true" > + + + src/ diff --git a/tests/Hooks/BypassFinalHook.php b/tests/Hooks/BypassFinalHook.php new file mode 100644 index 0000000..bb51820 --- /dev/null +++ b/tests/Hooks/BypassFinalHook.php @@ -0,0 +1,18 @@ + Date: Wed, 5 Apr 2023 12:05:20 +0200 Subject: [PATCH 05/10] fix ProgressOutputNodeProcessor tests --- .../NodeProcessorProgressBarFactory.php | 2 +- .../ProgressOutputNodeProcessor.php | 4 - .../ProgressOutputNodeProcessorTest.php | 83 ++++++++++++++----- 3 files changed, 61 insertions(+), 28 deletions(-) diff --git a/src/Factory/NodeProcessorProgressBarFactory.php b/src/Factory/NodeProcessorProgressBarFactory.php index 983167c..3629d68 100644 --- a/src/Factory/NodeProcessorProgressBarFactory.php +++ b/src/Factory/NodeProcessorProgressBarFactory.php @@ -9,7 +9,7 @@ class NodeProcessorProgressBarFactory { - public function createProgressBar(OutputInterface $output): ?ProgressBar + public function createProgressBar(?OutputInterface $output = NULL): ?ProgressBar { if (!($output instanceof ConsoleOutputInterface)) { return NULL; diff --git a/src/NodeProcessor/ProgressOutputNodeProcessor.php b/src/NodeProcessor/ProgressOutputNodeProcessor.php index 32042b3..af24ebd 100644 --- a/src/NodeProcessor/ProgressOutputNodeProcessor.php +++ b/src/NodeProcessor/ProgressOutputNodeProcessor.php @@ -46,10 +46,6 @@ function openFile(): void if ($this->progressBar !== NULL) { $this->progressBar->finish(); } - if($this->output === NULL) { - $this->progressBar = NULL; - return; - } $this->progressBar = $this->progressBarFactory->createProgressBar($this->output); } diff --git a/tests/Unit/NodeProcessor/ProgressOutputNodeProcessorTest.php b/tests/Unit/NodeProcessor/ProgressOutputNodeProcessorTest.php index c7f475d..bac1c90 100644 --- a/tests/Unit/NodeProcessor/ProgressOutputNodeProcessorTest.php +++ b/tests/Unit/NodeProcessor/ProgressOutputNodeProcessorTest.php @@ -3,6 +3,7 @@ namespace Netlogix\XmlProcessor\Tests\Unit\NodeProcessor; +use Netlogix\XmlProcessor\Factory\NodeProcessorProgressBarFactory; use Netlogix\XmlProcessor\NodeProcessor\Context\OpenContext; use Netlogix\XmlProcessor\NodeProcessor\ProgressOutputNodeProcessor; use Netlogix\XmlProcessor\XmlProcessor; @@ -13,6 +14,15 @@ class ProgressOutputNodeProcessorTest extends TestCase { + protected function setUp(): void + { + parent::setUp(); + + $reflectionClass = new \ReflectionClass(ProgressOutputNodeProcessor::class); + $this->progressBarProperty = $reflectionClass->getProperty('progressBar'); + $this->progressBarProperty->setAccessible(true); + } + public function test__construct(): void { $nodeProcessor = new ProgressOutputNodeProcessor(); @@ -21,7 +31,11 @@ public function test__construct(): void public function testGetSubscribedEvents(): void { - $nodeProcessor = new ProgressOutputNodeProcessor(); + $progressBarFactory = $this::getMockBuilder(NodeProcessorProgressBarFactory::class) + ->disableOriginalConstructor() + ->getMock(); + $progressBarFactory->method('createProgressBar')->willReturn($this->createMock(ProgressBar::class)); + $nodeProcessor = new ProgressOutputNodeProcessor($progressBarFactory); $context = $this->getMockBuilder(XmlProcessorContext::class) ->disableOriginalConstructor() ->getMock(); @@ -33,8 +47,7 @@ public function testGetSubscribedEvents(): void iterator_to_array($nodeProcessor->getSubscribedEvents('test', $context)) ); - $output = $this::getMockForAbstractClass(ConsoleOutputInterface::class); - $nodeProcessor->setOutput($output); + $nodeProcessor->setOutput($this->getOutputMock()); $nodeProcessor->openFile(); self::assertEquals( @@ -48,49 +61,73 @@ public function testGetSubscribedEvents(): void public function testOpenFile(): void { - $reflectionClass = new \ReflectionClass(ProgressOutputNodeProcessor::class); - $nodeProcessor = new ProgressOutputNodeProcessor(); $nodeProcessor->openFile(); - self::assertNull($reflectionClass->getProperty('progressBar')->getValue($nodeProcessor)); + self::assertNull($this->progressBarProperty->getValue($nodeProcessor)); - $output = $this::getMockForAbstractClass(ConsoleOutputInterface::class); - $nodeProcessor->setOutput($output); + $progressBar = $this->getMockBuilder(ProgressBar::class)->disableOriginalConstructor() + ->getMock(); + $progressBar->expects($this->once())->method('finish'); + $progressBarFactory = $this::getMockBuilder(NodeProcessorProgressBarFactory::class) + ->disableOriginalConstructor() + ->getMock(); + $progressBarFactory->method('createProgressBar')->willReturn($progressBar); + + $nodeProcessor = new ProgressOutputNodeProcessor($progressBarFactory); + $nodeProcessor->setOutput($this->getOutputMock()); $nodeProcessor->openFile(); - self::assertInstanceOf(ProgressBar::class, $reflectionClass->getProperty('progressBar')->getValue($nodeProcessor)); + self::assertInstanceOf(ProgressBar::class, $this->progressBarProperty->getValue($nodeProcessor)); self::markTestIncomplete('ToDo: $this->progressBar->finish()'); } public function testOpenElement(): void { - $nodeProcessor = new ProgressOutputNodeProcessor(); - $output = $this::getMockForAbstractClass(ConsoleOutputInterface::class); - $nodeProcessor->setOutput($output); + + $progressBar = $this->getMockBuilder(ProgressBar::class)->disableOriginalConstructor() + ->getMock(); + + $progressBar->expects($this->once())->method('advance')->with(1); + $progressBar->expects($this->once())->method('setMessage')->with('foo/bar', 'node'); + $progressBarFactory = $this::getMockBuilder(NodeProcessorProgressBarFactory::class) + ->disableOriginalConstructor() + ->getMock(); + $progressBarFactory->method('createProgressBar')->willReturn($progressBar); + + $nodeProcessor = new ProgressOutputNodeProcessor($progressBarFactory); + $nodeProcessor->setOutput($this->getOutputMock()); $nodeProcessor->openFile(); $openContext = $this->createMock(OpenContext::class); $openContext->method('getNodePath')->willReturn('foo/bar'); $nodeProcessor->openElement($openContext); - $reflectionClass = new \ReflectionClass($nodeProcessor); - - /** @var ProgressBar $progressBar */ - $progressBar = $reflectionClass->getProperty('progressBar')->getValue($nodeProcessor); - self::assertEquals(1, $progressBar->getProgress()); - self::assertEquals('foo/bar', $progressBar->getMessage('node')); } function testEndOfFile(): void { - $nodeProcessor = new ProgressOutputNodeProcessor(); - $output = $this::getMockForAbstractClass(ConsoleOutputInterface::class); - $nodeProcessor->setOutput($output); + $progressBar = $this->getMockBuilder(ProgressBar::class)->disableOriginalConstructor() + ->getMock(); + $progressBar->expects($this->once())->method('finish'); + $progressBarFactory = $this::getMockBuilder(NodeProcessorProgressBarFactory::class) + ->disableOriginalConstructor() + ->getMock(); + $progressBarFactory->method('createProgressBar')->willReturn($progressBar); + + $nodeProcessor = new ProgressOutputNodeProcessor($progressBarFactory); + $nodeProcessor->setOutput($this->getOutputMock()); $nodeProcessor->openFile(); $nodeProcessor->endOfFile(); - $reflectionClass = new \ReflectionClass($nodeProcessor); - $progressBar = $reflectionClass->getProperty('progressBar'); + $progressBar = $this->progressBarProperty; self::assertNull($progressBar->getValue($nodeProcessor)); } + + private function getOutputMock(): ConsoleOutputInterface + { + $output = $this::getMockForAbstractClass(ConsoleOutputInterface::class); + $output->method('getErrorOutput')->willReturn($output); + $output->method('isDecorated')->willReturn(true); + return $output; + } } From 65c539c67f7448ab4555e59b28ebc78b6d2217ab Mon Sep 17 00:00:00 2001 From: "sascha.heilmeier" Date: Thu, 6 Apr 2023 08:56:24 +0200 Subject: [PATCH 06/10] [test] add test for NodeProcessorProgressBarFactory --- .../NodeProcessorProgressBarFactoryTest.php | 36 +++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 tests/Unit/Factory/NodeProcessorProgressBarFactoryTest.php diff --git a/tests/Unit/Factory/NodeProcessorProgressBarFactoryTest.php b/tests/Unit/Factory/NodeProcessorProgressBarFactoryTest.php new file mode 100644 index 0000000..b7355a6 --- /dev/null +++ b/tests/Unit/Factory/NodeProcessorProgressBarFactoryTest.php @@ -0,0 +1,36 @@ +createProgressBar(); + self::assertNull($progressBar); + + $progressBar = $nodeProcessorProgressBarFactory->createProgressBar($this->getMockBuilder(OutputInterface::class)->getMock()); + self::assertNull($progressBar); + + $output = $this::getMockForAbstractClass(ConsoleOutputInterface::class); + $output->method('getErrorOutput')->willReturn($output); + $output->method('isDecorated')->willReturn(true); + + $progressBar = $nodeProcessorProgressBarFactory->createProgressBar($output); + self::assertInstanceOf(ProgressBar::class, $progressBar); + } +} \ No newline at end of file From 23fc57171d96d5f4ea756fa5aa92949e73417872 Mon Sep 17 00:00:00 2001 From: "sascha.heilmeier" Date: Thu, 6 Apr 2023 08:56:56 +0200 Subject: [PATCH 07/10] [test] update ProgressOutputNodeProcessorTest for better coverage --- tests/Unit/NodeProcessor/ProgressOutputNodeProcessorTest.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/Unit/NodeProcessor/ProgressOutputNodeProcessorTest.php b/tests/Unit/NodeProcessor/ProgressOutputNodeProcessorTest.php index bac1c90..7029f90 100644 --- a/tests/Unit/NodeProcessor/ProgressOutputNodeProcessorTest.php +++ b/tests/Unit/NodeProcessor/ProgressOutputNodeProcessorTest.php @@ -89,10 +89,11 @@ public function testOpenElement(): void $progressBar->expects($this->once())->method('advance')->with(1); $progressBar->expects($this->once())->method('setMessage')->with('foo/bar', 'node'); + $progressBar->expects($this->once())->method('finish'); $progressBarFactory = $this::getMockBuilder(NodeProcessorProgressBarFactory::class) ->disableOriginalConstructor() ->getMock(); - $progressBarFactory->method('createProgressBar')->willReturn($progressBar); + $progressBarFactory->expects($this->exactly(2))->method('createProgressBar')->willReturn($progressBar); $nodeProcessor = new ProgressOutputNodeProcessor($progressBarFactory); $nodeProcessor->setOutput($this->getOutputMock()); @@ -102,6 +103,7 @@ public function testOpenElement(): void $openContext->method('getNodePath')->willReturn('foo/bar'); $nodeProcessor->openElement($openContext); + $nodeProcessor->openFile(); } function testEndOfFile(): void From 82920efd2085b700580b62a2020855b0e2c7e773 Mon Sep 17 00:00:00 2001 From: "sascha.heilmeier" Date: Thu, 6 Apr 2023 08:58:26 +0200 Subject: [PATCH 08/10] [fix] use better return type --- src/NodeProcessor/ProgressOutputNodeProcessor.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/NodeProcessor/ProgressOutputNodeProcessor.php b/src/NodeProcessor/ProgressOutputNodeProcessor.php index af24ebd..008b8a7 100644 --- a/src/NodeProcessor/ProgressOutputNodeProcessor.php +++ b/src/NodeProcessor/ProgressOutputNodeProcessor.php @@ -31,7 +31,7 @@ public function SetOutput(?ConsoleOutputInterface $output = NULL): void $this->progressBar = NULL; } - function getSubscribedEvents(string $nodePath, XmlProcessorContext $context): \Iterator + function getSubscribedEvents(string $nodePath, XmlProcessorContext $context): \Generator { if ($this->progressBar === NULL) { yield XmlProcessor::EVENT_OPEN_FILE => [$this, 'openFile']; From 2b00a4aa2c9aac65f02acf1e4aa16414c7143108 Mon Sep 17 00:00:00 2001 From: Sascha Date: Thu, 6 Apr 2023 10:17:48 +0200 Subject: [PATCH 09/10] add skipNodes to XmlProcessor (#7) * [bugfix] skipNode * [feature] add selfClosing to OpenContext and CloseContext --- .../Context/AbstractElementContext.php | 18 +++++ src/NodeProcessor/Context/CloseContext.php | 2 +- src/NodeProcessor/Context/OpenContext.php | 2 +- src/XmlProcessor.php | 21 ++++-- .../Context/AbstractElementContextTest.php | 66 +++++++++++++++++++ tests/Unit/XmlProcessorTest.php | 33 +++++++++- 6 files changed, 133 insertions(+), 9 deletions(-) create mode 100644 src/NodeProcessor/Context/AbstractElementContext.php create mode 100644 tests/Unit/NodeProcessor/Context/AbstractElementContextTest.php diff --git a/src/NodeProcessor/Context/AbstractElementContext.php b/src/NodeProcessor/Context/AbstractElementContext.php new file mode 100644 index 0000000..0071998 --- /dev/null +++ b/src/NodeProcessor/Context/AbstractElementContext.php @@ -0,0 +1,18 @@ +selfClosing = $selfClosing; + } + + function getSelfClosing(): bool + { + return $this->selfClosing; + } +} diff --git a/src/NodeProcessor/Context/CloseContext.php b/src/NodeProcessor/Context/CloseContext.php index 6e4db29..d8516b1 100644 --- a/src/NodeProcessor/Context/CloseContext.php +++ b/src/NodeProcessor/Context/CloseContext.php @@ -3,6 +3,6 @@ namespace Netlogix\XmlProcessor\NodeProcessor\Context; -class CloseContext extends NodeProcessorContext +class CloseContext extends AbstractElementContext { } diff --git a/src/NodeProcessor/Context/OpenContext.php b/src/NodeProcessor/Context/OpenContext.php index 369410f..ee012a4 100644 --- a/src/NodeProcessor/Context/OpenContext.php +++ b/src/NodeProcessor/Context/OpenContext.php @@ -3,7 +3,7 @@ namespace Netlogix\XmlProcessor\NodeProcessor\Context; -class OpenContext extends NodeProcessorContext +class OpenContext extends AbstractElementContext { /** * @var array diff --git a/src/XmlProcessor.php b/src/XmlProcessor.php index 885c686..703cc3b 100644 --- a/src/XmlProcessor.php +++ b/src/XmlProcessor.php @@ -28,6 +28,9 @@ class XmlProcessor /** @var iterable */ private iterable $parserProperties; + private bool $skipCurrentNode = false; + private bool $selfClosing = false; + /** * @param iterable $processors * @param iterable $parserProperties @@ -43,7 +46,7 @@ public function __construct( $this->context = new XmlProcessorContext( $this->xml, $this->processors, - fn() => $this->skipNode() + fn() => $this->skipCurrentNode = true ); } @@ -75,13 +78,12 @@ public function processFile(string $filename): void $this->eventCloseElement(); break; case \XMLReader::ELEMENT: - $selfClosing = $this->xml->isEmptyElement; + $this->selfClosing = $this->xml->isEmptyElement; $this->eventOpenElement(); - if ($this->shouldSkipNode()) { - $this->skipNode(); - break; + if ($skip = $this->shouldSkipNode()) { + $this->xml->next(); } - if ($selfClosing) { + if ($skip || $this->selfClosing) { $this->eventCloseElement(); } break; @@ -106,6 +108,10 @@ private function skipNode(): bool private function shouldSkipNode(): bool { + if ($this->skipCurrentNode) { + $this->skipCurrentNode = false; + return true; + } if ($this->skipNodes === NULL) { return false; } @@ -186,6 +192,9 @@ private function popNodePath(): void private function createContext(string $contextClass): NodeProcessorContext { $context = new $contextClass($this->context, $this->nodePath); + if (method_exists($context, 'setSelfClosing')) { + $context->setSelfClosing($this->selfClosing); + } if (method_exists($context, 'setAttributes')) { $context->setAttributes($this->getAttributes()); } diff --git a/tests/Unit/NodeProcessor/Context/AbstractElementContextTest.php b/tests/Unit/NodeProcessor/Context/AbstractElementContextTest.php new file mode 100644 index 0000000..f6d3c5b --- /dev/null +++ b/tests/Unit/NodeProcessor/Context/AbstractElementContextTest.php @@ -0,0 +1,66 @@ +getMockBuilder(XmlProcessorContext::class) + ->disableOriginalConstructor() + ->getMock(), + $nodePath + ); + } + + public function test__construct(): void + { + $nodeProcessorContext = $this->getCloseContext(); + self::assertInstanceOf(AbstractElementContext::class, $nodeProcessorContext); + } + + /** + * @dataProvider setSelfClosingDataProvider + */ + function testSetSelfClosing($set, $expect): void + { + $nodeProcessorContext = $this->getCloseContext(); + $nodeProcessorContext->setSelfClosing($set); + self::assertEquals($expect, $nodeProcessorContext->getSelfClosing()); + } + + function setSelfClosingDataProvider(): \Generator + { + yield [true, true]; + yield [false, false]; + } + + /** + * @dataProvider getSelfClosingDataProvider + */ + function testGetSelfClosing($set, $expect): void + { + $nodeProcessorContext = $this->getCloseContext(); + if ($set !== NULL) { + $nodeProcessorContext->setSelfClosing($set); + } + self::assertEquals($expect, $nodeProcessorContext->getSelfClosing()); + } + + function getSelfClosingDataProvider(): \Generator + { + yield [NULL, false]; + yield [true, true]; + yield [false, false]; + } + +} diff --git a/tests/Unit/XmlProcessorTest.php b/tests/Unit/XmlProcessorTest.php index 73a8ee6..ec6ed6e 100644 --- a/tests/Unit/XmlProcessorTest.php +++ b/tests/Unit/XmlProcessorTest.php @@ -73,6 +73,36 @@ public function testProcessFile() [\XMLReader::SUBST_ENTITIES => true] ); $xmlProcessor->processFile(__DIR__ . '/../Fixtures/XmlProcessorTest/test.xml'); + } + + public function testProcessFile_skipCurrentNode() + { + $nodeProcessor = $this->getMockForAbstractClass(NodeProcessorInterface::class); + + $nodeProcessor->method('getSubscribedEvents') + ->will( + $this->returnCallback(fn() => yield from [ + 'NodeType_' . \XMLReader::ELEMENT => function (OpenContext $context) { + $context->getXmlProcessorContext()->skipCurrentNode(); + self::assertNotEquals('bar', $context->getCurrentNodeName()); + }, + ]) + ); + + $xmlProcessor = new XmlProcessor([ + $nodeProcessor + ]); + + $xmlProcessor->processFile(__DIR__ . '/../Fixtures/XmlProcessorTest/test.xml'); + + $xmlProcessor->setSkipNodes(['foo']); + $xmlProcessor->processFile(__DIR__ . '/../Fixtures/XmlProcessorTest/test.xml'); + + $xmlProcessor = new XmlProcessor( + [$nodeProcessor], + [\XMLReader::SUBST_ENTITIES => true] + ); + $xmlProcessor->processFile(__DIR__ . '/../Fixtures/XmlProcessorTest/test.xml'); if(!function_exists('str_end_with')){ function str_end_with(string $nodePath, string $expected){ @@ -90,8 +120,9 @@ function testCheckNodePath(string $nodePath, string $expected, bool $result): vo self::assertSame(XmlProcessor::checkNodePath($nodePath, $expected), $result); } - public static function checkNodePathDataProvider(): iterable + public static function checkNodePathDataProvider(): \Generator { + yield ['', 'foo/bar', false]; yield ['/foo/bar', 'foo/bar', true]; yield ['/foo', 'foo/bar', false]; yield ['foo', 'foo/bar', false]; From 12be0e84254f7baeb6376ef1cc2ae19b7994be3f Mon Sep 17 00:00:00 2001 From: Sascha Date: Tue, 25 Jul 2023 05:39:17 +0000 Subject: [PATCH 10/10] [test] ProgressOutputNodeProcessorTest --- tests/Unit/NodeProcessor/ProgressOutputNodeProcessorTest.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/Unit/NodeProcessor/ProgressOutputNodeProcessorTest.php b/tests/Unit/NodeProcessor/ProgressOutputNodeProcessorTest.php index 7029f90..4255631 100644 --- a/tests/Unit/NodeProcessor/ProgressOutputNodeProcessorTest.php +++ b/tests/Unit/NodeProcessor/ProgressOutputNodeProcessorTest.php @@ -77,8 +77,8 @@ public function testOpenFile(): void $nodeProcessor->setOutput($this->getOutputMock()); $nodeProcessor->openFile(); self::assertInstanceOf(ProgressBar::class, $this->progressBarProperty->getValue($nodeProcessor)); - - self::markTestIncomplete('ToDo: $this->progressBar->finish()'); + $nodeProcessor->openFile(); + self::assertInstanceOf(ProgressBar::class, $this->progressBarProperty->getValue($nodeProcessor)); } public function testOpenElement(): void