Skip to content

Commit

Permalink
Implement XML configuration parser
Browse files Browse the repository at this point in the history
This ships a parser and the expected schema to ease the validation of
XML documents.

Signed-off-by: Luís Cobucci <[email protected]>
  • Loading branch information
lcobucci committed Mar 1, 2023
1 parent 5d34621 commit 91fcd78
Show file tree
Hide file tree
Showing 8 changed files with 333 additions and 2 deletions.
1 change: 1 addition & 0 deletions box.json.dist
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
"main": "bin/roave-backward-compatibility-check.php",
"output": "dist/roave-backward-compatibility-check.phar",
"files-bin": [
"resources/schema.xsd",
"LICENSE",
"vendor/composer/composer/LICENSE"
],
Expand Down
3 changes: 3 additions & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,10 @@
"description": "Tool to compare two revisions of a public API to check for BC breaks",
"require": {
"php": "~8.1.0 || ~8.2.0",
"ext-dom": "*",
"ext-json": "*",
"ext-libxml": "*",
"ext-simplexml": "*",
"azjezz/psl": "^2.3.1",
"composer/composer": "^2.5.1",
"nikic/php-parser": "^4.15.3",
Expand Down
7 changes: 5 additions & 2 deletions composer.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

24 changes: 24 additions & 0 deletions resources/schema.xsd
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<?xml version="1.0" encoding="UTF-8"?>
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
<xs:annotation>
<xs:appinfo source="https://github.com/Roave/BackwardCompatibilityCheck"/>

<xs:documentation source="https://github.com/Roave/BackwardCompatibilityCheck">
This schema file defines the structure for the XML configuration file of roave/backward-compatibility-check.
</xs:documentation>
</xs:annotation>

<xs:element name="roave-bc-check" type="bcCheckType" />

<xs:complexType name="bcCheckType">
<xs:sequence>
<xs:element name="baseline" type="baselineType" minOccurs="0" />
</xs:sequence>
</xs:complexType>

<xs:complexType name="baselineType">
<xs:sequence>
<xs:element name="ignored-regex" minOccurs="0" maxOccurs="unbounded" type="xs:string" />
</xs:sequence>
</xs:complexType>
</xs:schema>
33 changes: 33 additions & 0 deletions src/Configuration/InvalidConfigurationStructure.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<?php

declare(strict_types=1);

namespace Roave\BackwardCompatibility\Configuration;

use LibXMLError;
use RuntimeException;

use function sprintf;
use function trim;

use const PHP_EOL;

/** @internal */
final class InvalidConfigurationStructure extends RuntimeException
{
/** @param list<LibXMLError> $errors */
public static function fromLibxmlErrors(array $errors): self
{
$message = 'The provided configuration is invalid, errors:' . PHP_EOL;

foreach ($errors as $error) {
$message .= sprintf(
' - [Line %d] %s' . PHP_EOL,
$error->line,
trim($error->message),
);
}

return new self($message);
}
}
2 changes: 2 additions & 0 deletions src/Configuration/ParseConfigurationFile.php
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
<?php

declare(strict_types=1);

namespace Roave\BackwardCompatibility\Configuration;

interface ParseConfigurationFile
{
/** @throws InvalidConfigurationStructure When an incorrect file was found on the directory. */
public function parse(string $currentDirectory): Configuration;
}
72 changes: 72 additions & 0 deletions src/Configuration/ParseXmlConfigurationFile.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
<?php

declare(strict_types=1);

namespace Roave\BackwardCompatibility\Configuration;

use DOMDocument;
use Psl\File;
use Roave\BackwardCompatibility\Baseline;
use SimpleXMLElement;

use function array_values;
use function libxml_get_errors;
use function libxml_use_internal_errors;

/** @internal */
final class ParseXmlConfigurationFile implements ParseConfigurationFile
{
private const CONFIGURATION_FILENAME = '.roave-backward-compatibility-check.xml';

private const SCHEMA = __DIR__ . '/../../resources/schema.xsd';

public function parse(string $currentDirectory): Configuration
{
$filename = $currentDirectory . '/' . self::CONFIGURATION_FILENAME;

try {
$xmlContents = File\read($filename);

$this->validateStructure($xmlContents);
} catch (File\Exception\InvalidArgumentException) {
return Configuration::default();
}

$configuration = new SimpleXMLElement($xmlContents);

return Configuration::fromFile(
$this->parseBaseline($configuration),
$filename,
);
}

private function validateStructure(string $xmlContents): void
{
$previousConfiguration = libxml_use_internal_errors(true);

$xmlDocument = new DOMDocument();
$xmlDocument->loadXML($xmlContents);

$configurationIsValid = $xmlDocument->schemaValidate(self::SCHEMA);

$parsingErrors = array_values(libxml_get_errors());
libxml_use_internal_errors($previousConfiguration);

if ($configurationIsValid) {
return;
}

throw InvalidConfigurationStructure::fromLibxmlErrors($parsingErrors);
}

private function parseBaseline(SimpleXMLElement $element): Baseline
{
$ignoredItems = [];

foreach ($element->xpath('baseline/ignored-regex') ?? [] as $node) {
$ignoredItems[] = (string) $node;
}

return Baseline::fromList(...$ignoredItems);
}
}
193 changes: 193 additions & 0 deletions test/unit/Configuration/ParseXmlConfigurationFileTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
<?php

declare(strict_types=1);

namespace RoaveTest\BackwardCompatibility\Configuration;

use PHPUnit\Framework\TestCase;
use Psl\Env;
use Psl\File;
use Psl\Filesystem;
use Psl\Shell;
use Roave\BackwardCompatibility\Baseline;
use Roave\BackwardCompatibility\Configuration\Configuration;
use Roave\BackwardCompatibility\Configuration\InvalidConfigurationStructure;
use Roave\BackwardCompatibility\Configuration\ParseXmlConfigurationFile;

final class ParseXmlConfigurationFileTest extends TestCase
{
private string $temporaryDirectory;

/** @before */
public function prepareFilesystem(): void
{
$this->temporaryDirectory = Filesystem\create_temporary_file(
Env\temp_dir(),
'roave-backward-compatibility-xml-config-test',
);

self::assertNotEmpty($this->temporaryDirectory);
self::assertFileExists($this->temporaryDirectory);

Filesystem\delete_file($this->temporaryDirectory);
Filesystem\create_directory($this->temporaryDirectory);
}

/** @after */
public function cleanUpFilesystem(): void
{
Shell\execute('rm', ['-rf', $this->temporaryDirectory]);
}

/** @test */
public function defaultConfigurationShouldBeUsedWhenFileDoesNotExist(): void
{
$config = (new ParseXmlConfigurationFile())->parse($this->temporaryDirectory);

self::assertEquals(Configuration::default(), $config);
}

/**
* @test
* @dataProvider invalidConfiguration
*/
public function exceptionShouldBeRaisedWhenStructureIsInvalid(
string $xmlContents,
string $expectedError,
): void {
File\write($this->temporaryDirectory . '/.roave-backward-compatibility-check.xml', $xmlContents);

$this->expectException(InvalidConfigurationStructure::class);
$this->expectExceptionMessage($expectedError);

(new ParseXmlConfigurationFile())->parse($this->temporaryDirectory);
}

/** @return iterable<string, array{string, string}> */
public static function invalidConfiguration(): iterable
{
yield 'invalid root element' => [
<<<'XML'
<?xml version="1.0" encoding="UTF-8" ?>
<anything />
XML,
'[Line 2] Element \'anything\': No matching global declaration available for the validation root',
];

yield 'invalid root child' => [
<<<'XML'
<?xml version="1.0" encoding="UTF-8" ?>
<roave-bc-check>
<something />
</roave-bc-check>
XML,
'[Line 3] Element \'something\': This element is not expected. Expected is ( baseline )',
];

yield 'multiple baseline tags' => [
<<<'XML'
<?xml version="1.0" encoding="UTF-8" ?>
<roave-bc-check>
<baseline />
<baseline />
</roave-bc-check>
XML,
'[Line 4] Element \'baseline\': This element is not expected',
];

yield 'invalid baseline child' => [
<<<'XML'
<?xml version="1.0" encoding="UTF-8" ?>
<roave-bc-check>
<baseline>
<nothing />
</baseline>
</roave-bc-check>
XML,
'[Line 4] Element \'nothing\': This element is not expected. Expected is ( ignored-regex )',
];

yield 'invalid ignored item type' => [
<<<'XML'
<?xml version="1.0" encoding="UTF-8" ?>
<roave-bc-check>
<baseline>
<ignored-regex>
<something-else />
</ignored-regex>
</baseline>
</roave-bc-check>
XML,
'[Line 4] Element \'ignored-regex\': Element content is not allowed, because the type definition is simple',
];
}

/**
* @test
* @dataProvider validConfiguration
*/
public function baselineShouldBeParsed(
string $xmlContents,
Baseline $expectedBaseline,
): void {
File\write($this->temporaryDirectory . '/.roave-backward-compatibility-check.xml', $xmlContents);

self::assertEquals(
Configuration::fromFile(
$expectedBaseline,
$this->temporaryDirectory . '/.roave-backward-compatibility-check.xml',
),
(new ParseXmlConfigurationFile())->parse($this->temporaryDirectory),
);
}

/** @return iterable<string, array{string, Baseline}> */
public static function validConfiguration(): iterable
{
yield 'no baseline' => [
<<<'XML'
<?xml version="1.0" encoding="UTF-8" ?>
<roave-bc-check />
XML,
Baseline::empty(),
];

yield 'empty baseline' => [
<<<'XML'
<?xml version="1.0" encoding="UTF-8" ?>
<roave-bc-check>
<baseline />
</roave-bc-check>
XML,
Baseline::empty(),
];

yield 'baseline with single element' => [
<<<'XML'
<?xml version="1.0" encoding="UTF-8" ?>
<roave-bc-check>
<baseline>
<ignored-regex>#\[BC\] CHANGED: The parameter \$a of TestArtifact\\TheClass\#method.*#</ignored-regex>
</baseline>
</roave-bc-check>
XML,
Baseline::fromList('#\[BC\] CHANGED: The parameter \$a of TestArtifact\\\\TheClass\#method.*#'),
];
yield 'baseline with multiple elements' => [
<<<'XML'
<?xml version="1.0" encoding="UTF-8" ?>
<roave-bc-check>
<baseline>
<ignored-regex>#\[BC\] CHANGED: The parameter \$a of TestArtifact\\TheClass\#method.*#</ignored-regex>
<ignored-regex>#\[BC\] ADDED: Method .*\(\) was added to interface TestArtifact\\TheInterface.*#</ignored-regex>
</baseline>
</roave-bc-check>
XML,
Baseline::fromList(
'#\[BC\] CHANGED: The parameter \$a of TestArtifact\\\\TheClass\#method.*#',
'#\[BC\] ADDED: Method .*\(\) was added to interface TestArtifact\\\\TheInterface.*#',
),
];
}
}

0 comments on commit 91fcd78

Please sign in to comment.