diff --git a/src/Symfony/Bundle/FrameworkBundle/Tests/Command/XliffLintCommandTest.php b/src/Symfony/Bundle/FrameworkBundle/Tests/Command/XliffLintCommandTest.php new file mode 100644 index 0000000000000..9834d826a8265 --- /dev/null +++ b/src/Symfony/Bundle/FrameworkBundle/Tests/Command/XliffLintCommandTest.php @@ -0,0 +1,149 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Bundle\FrameworkBundle\Tests\Command; + +use PHPUnit\Framework\TestCase; +use Symfony\Bundle\FrameworkBundle\Command\XliffLintCommand; +use Symfony\Bundle\FrameworkBundle\Console\Application; +use Symfony\Component\Console\Application as BaseApplication; +use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Console\Tester\CommandTester; +use Symfony\Component\Console\Helper\HelperSet; +use Symfony\Component\Console\Input\InputDefinition; +use Symfony\Component\HttpKernel\KernelInterface; + +/** + * Tests the part of the XliffLintCommand managed by the FrameworkBundle. The + * rest of the features are tested in the Translation component. + * + * @author Javier Eguiluz + */ +class XliffLintCommandTest extends TestCase +{ + private $files; + + public function testGetHelp() + { + $command = new XliffLintCommand(); + $expected = <<%command.name% command lints a XLIFF file and outputs to STDOUT +the first encountered syntax error. + +You can validates XLIFF contents passed from STDIN: + + cat filename | php %command.full_name% + +You can also validate the syntax of a file: + + php %command.full_name% filename + +Or of a whole directory: + + php %command.full_name% dirname + php %command.full_name% dirname --format=json + +Or find all files in a bundle: + + php %command.full_name% @AcmeDemoBundle + +EOF; + + $this->assertEquals($expected, $command->getHelp()); + } + + public function testLintFilesFromBundleDirectory() + { + $tester = $this->createCommandTester($this->getKernelAwareApplicationMock()); + $tester->execute( + array('filename' => '@AppBundle/Resources'), + array('verbosity' => OutputInterface::VERBOSITY_VERBOSE, 'decorated' => false) + ); + + $this->assertEquals(0, $tester->getStatusCode(), 'Returns 0 in case of success'); + $this->assertContains('[OK] All 0 XLIFF files contain valid syntax', trim($tester->getDisplay())); + } + + /** + * @return CommandTester + */ + private function createCommandTester($application = null) + { + if (!$application) { + $application = new BaseApplication(); + $application->add(new XliffLintCommand()); + } + + $command = $application->find('lint:xliff'); + + if ($application) { + $command->setApplication($application); + } + + return new CommandTester($command); + } + + private function getKernelAwareApplicationMock() + { + $kernel = $this->getMockBuilder(KernelInterface::class) + ->disableOriginalConstructor() + ->getMock(); + + $kernel + ->expects($this->once()) + ->method('locateResource') + ->with('@AppBundle/Resources') + ->willReturn(sys_get_temp_dir().'/xliff-lint-test'); + + $application = $this->getMockBuilder(Application::class) + ->disableOriginalConstructor() + ->getMock(); + + $application + ->expects($this->once()) + ->method('getKernel') + ->willReturn($kernel); + + $application + ->expects($this->once()) + ->method('getHelperSet') + ->willReturn(new HelperSet()); + + $application + ->expects($this->any()) + ->method('getDefinition') + ->willReturn(new InputDefinition()); + + $application + ->expects($this->once()) + ->method('find') + ->with('lint:xliff') + ->willReturn(new XliffLintCommand()); + + return $application; + } + + protected function setUp() + { + @mkdir(sys_get_temp_dir().'/xliff-lint-test'); + $this->files = array(); + } + + protected function tearDown() + { + foreach ($this->files as $file) { + if (file_exists($file)) { + unlink($file); + } + } + rmdir(sys_get_temp_dir().'/xliff-lint-test'); + } +} diff --git a/src/Symfony/Component/Translation/Command/XliffLintCommand.php b/src/Symfony/Component/Translation/Command/XliffLintCommand.php index 042ab1eab8b32..035e11b0a9f83 100644 --- a/src/Symfony/Component/Translation/Command/XliffLintCommand.php +++ b/src/Symfony/Component/Translation/Command/XliffLintCommand.php @@ -101,6 +101,8 @@ protected function execute(InputInterface $input, OutputInterface $output) private function validate($content, $file = null) { + $errors = array(); + // Avoid: Warning DOMDocument::loadXML(): Empty string supplied as input if ('' === trim($content)) { return array('file' => $file, 'valid' => true); @@ -110,22 +112,33 @@ private function validate($content, $file = null) $document = new \DOMDocument(); $document->loadXML($content); - if ($document->schemaValidate(__DIR__.'/../Resources/schemas/xliff-core-1.2-strict.xsd')) { - return array('file' => $file, 'valid' => true); + + if (null !== $targetLanguage = $this->getTargetLanguageFromFile($document)) { + $expectedFileExtension = sprintf('%s.xlf', str_replace('-', '_', $targetLanguage)); + $realFileExtension = explode('.', basename($file), 2)[1] ?? ''; + + if ($expectedFileExtension !== $realFileExtension) { + $errors[] = array( + 'line' => -1, + 'column' => -1, + 'message' => sprintf('There is a mismatch between the file extension ("%s") and the "%s" value used in the "target-language" attribute of the file.', $realFileExtension, $targetLanguage), + ); + } } - $errorMessages = array_map(function ($error) { - return array( - 'line' => $error->line, - 'column' => $error->column, - 'message' => trim($error->message), - ); - }, libxml_get_errors()); + $document->schemaValidate(__DIR__.'/../Resources/schemas/xliff-core-1.2-strict.xsd'); + foreach (libxml_get_errors() as $xmlError) { + $errors[] = array( + 'line' => $xmlError->line, + 'column' => $xmlError->column, + 'message' => trim($xmlError->message), + ); + } libxml_clear_errors(); libxml_use_internal_errors(false); - return array('file' => $file, 'valid' => false, 'messages' => $errorMessages); + return array('file' => $file, 'valid' => 0 === count($errors), 'messages' => $errors); } private function display(SymfonyStyle $io, array $files) @@ -242,4 +255,15 @@ private function isReadable($fileOrDirectory) return $default($fileOrDirectory); } + + private function getTargetLanguageFromFile(\DOMDocument $xliffContents): ?string + { + foreach ($xliffContents->getElementsByTagName('file')[0]->attributes ?? array() as $attribute) { + if ('target-language' === $attribute->nodeName) { + return $attribute->nodeValue; + } + } + + return null; + } } diff --git a/src/Symfony/Component/Translation/Tests/Command/XliffLintCommandTest.php b/src/Symfony/Component/Translation/Tests/Command/XliffLintCommandTest.php new file mode 100644 index 0000000000000..fd60356d18039 --- /dev/null +++ b/src/Symfony/Component/Translation/Tests/Command/XliffLintCommandTest.php @@ -0,0 +1,166 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\Translation\Tests\Command; + +use PHPUnit\Framework\TestCase; +use Symfony\Component\Console\Application; +use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Console\Tester\CommandTester; +use Symfony\Component\Console\Helper\HelperSet; +use Symfony\Component\Console\Input\InputDefinition; +use Symfony\Component\HttpKernel\KernelInterface; +use Symfony\Component\Translation\Command\XliffLintCommand; + +/** + * Tests the XliffLintCommand. + * + * @author Javier Eguiluz + */ +class XliffLintCommandTest extends TestCase +{ + private $files; + + public function testLintCorrectFile() + { + $tester = $this->createCommandTester(); + $filename = $this->createFile(); + + $tester->execute( + array('filename' => $filename), + array('verbosity' => OutputInterface::VERBOSITY_VERBOSE, 'decorated' => false) + ); + + $this->assertEquals(0, $tester->getStatusCode(), 'Returns 0 in case of success'); + $this->assertContains('OK', trim($tester->getDisplay())); + } + + public function testLintIncorrectXmlSyntax() + { + $tester = $this->createCommandTester(); + $filename = $this->createFile('note '); + + $tester->execute(array('filename' => $filename), array('decorated' => false)); + + $this->assertEquals(1, $tester->getStatusCode(), 'Returns 1 in case of error'); + $this->assertContains('Opening and ending tag mismatch: target line 6 and source', trim($tester->getDisplay())); + } + + public function testLintIncorrectTargetLanguage() + { + $tester = $this->createCommandTester(); + $filename = $this->createFile('note', 'es'); + + $tester->execute(array('filename' => $filename), array('decorated' => false)); + + $this->assertEquals(1, $tester->getStatusCode(), 'Returns 1 in case of error'); + $this->assertContains('There is a mismatch between the file extension ("en.xlf") and the "es" value used in the "target-language" attribute of the file.', trim($tester->getDisplay())); + } + + /** + * @expectedException \RuntimeException + */ + public function testLintFileNotReadable() + { + $tester = $this->createCommandTester(); + $filename = $this->createFile(); + unlink($filename); + + $tester->execute(array('filename' => $filename), array('decorated' => false)); + } + + public function testGetHelp() + { + $command = new XliffLintCommand(); + $expected = <<%command.name% command lints a XLIFF file and outputs to STDOUT +the first encountered syntax error. + +You can validates XLIFF contents passed from STDIN: + + cat filename | php %command.full_name% + +You can also validate the syntax of a file: + + php %command.full_name% filename + +Or of a whole directory: + + php %command.full_name% dirname + php %command.full_name% dirname --format=json + +EOF; + + $this->assertEquals($expected, $command->getHelp()); + } + + /** + * @return string Path to the new file + */ + private function createFile($sourceContent = 'note', $targetLanguage = 'en') + { + $xliffContent = << + + + + + $sourceContent + NOTE + + + + +XLIFF; + + $filename = sprintf('%s/xliff-lint-test/messages.en.xlf', sys_get_temp_dir()); + file_put_contents($filename, $xliffContent); + + $this->files[] = $filename; + + return $filename; + } + + /** + * @return CommandTester + */ + private function createCommandTester($application = null) + { + if (!$application) { + $application = new Application(); + $application->add(new XliffLintCommand()); + } + + $command = $application->find('lint:xliff'); + + if ($application) { + $command->setApplication($application); + } + + return new CommandTester($command); + } + + protected function setUp() + { + @mkdir(sys_get_temp_dir().'/xliff-lint-test'); + $this->files = array(); + } + + protected function tearDown() + { + foreach ($this->files as $file) { + if (file_exists($file)) { + unlink($file); + } + } + rmdir(sys_get_temp_dir().'/xliff-lint-test'); + } +} diff --git a/src/Symfony/Component/Translation/composer.json b/src/Symfony/Component/Translation/composer.json index 8c80c01c6cede..8cf7a462977d0 100644 --- a/src/Symfony/Component/Translation/composer.json +++ b/src/Symfony/Component/Translation/composer.json @@ -21,6 +21,7 @@ }, "require-dev": { "symfony/config": "~3.4|~4.0", + "symfony/console": "~3.4|~4.0", "symfony/dependency-injection": "~3.4|~4.0", "symfony/intl": "~3.4|~4.0", "symfony/yaml": "~3.4|~4.0",