-
Notifications
You must be signed in to change notification settings - Fork 59
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
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
Showing
8 changed files
with
333 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
193
test/unit/Configuration/ParseXmlConfigurationFileTest.php
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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.*#', | ||
), | ||
]; | ||
} | ||
} |