diff --git a/Player/Console/PlayerCommand.php b/Player/Console/PlayerCommand.php index 58c4643..12baa2b 100644 --- a/Player/Console/PlayerCommand.php +++ b/Player/Console/PlayerCommand.php @@ -16,6 +16,7 @@ use Blackfire\Player\Extension\BlackfireExtension; use Blackfire\Player\Extension\CliFeedbackExtension; use Blackfire\Player\Extension\DisableInternalNetworkExtension; +use Blackfire\Player\Extension\JUnitExtension; use Blackfire\Player\Extension\SecurityExtension; use Blackfire\Player\Extension\TracerExtension; use Blackfire\Player\Guzzle\CurlFactory; @@ -60,6 +61,8 @@ protected function configure() new InputOption('sandbox', '', InputOption::VALUE_NONE, 'Enable the sandbox mode', null), new InputOption('ssl-no-verify', '', InputOption::VALUE_NONE, 'Disable SSL certificate verification', null), new InputOption('blackfire-env', '', InputOption::VALUE_REQUIRED, 'The blackfire environment to use'), + new InputOption('junit', '', InputOption::VALUE_REQUIRED, 'Save output execution report as JUnit to file'), + ]) ->setDescription('Runs scenario files') ->setHelp('Read https://blackfire.io/docs/builds-cookbooks/player to learn about all supported options.') @@ -80,6 +83,7 @@ protected function execute(InputInterface $input, OutputInterface $output) } $json = $input->getOption('json'); + $junit = $input->getOption('junit'); $sslNoVerify = $input->getOption('ssl-no-verify'); $clients = [$this->createClient($sslNoVerify)]; @@ -114,6 +118,10 @@ protected function execute(InputInterface $input, OutputInterface $output) if ($input->getOption('disable-internal-network')) { $player->addExtension(new DisableInternalNetworkExtension()); } + if ($junit) { + $junitExtension = new JUnitExtension(); + $player->addExtension($junitExtension); + } $scenarios = (new ScenarioHydrator())->hydrate($input); @@ -160,6 +168,10 @@ protected function execute(InputInterface $input, OutputInterface $output) ])); } + if ($junit) { + \file_put_contents($junit, $junitExtension->getXml()); + } + return $exitCode; } diff --git a/Player/Extension/JUnitExtension.php b/Player/Extension/JUnitExtension.php new file mode 100644 index 0000000..843b50e --- /dev/null +++ b/Player/Extension/JUnitExtension.php @@ -0,0 +1,172 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Blackfire\Player\Extension; + +use Blackfire\Player\Context; +use Blackfire\Player\Exception\ExpectationFailureException; +use Blackfire\Player\Result; +use Blackfire\Player\Results; +use Blackfire\Player\Scenario; +use Blackfire\Player\ScenarioSet; +use Blackfire\Player\Step\AbstractStep; +use Blackfire\Player\Step\Step; +use Psr\Http\Message\RequestInterface; +use Psr\Http\Message\ResponseInterface; + +/** + * @author Marcin Czarnecki + */ +class JUnitExtension extends AbstractExtension +{ + /** + * @var \DOMDocument + */ + private $document; + + /** + * @var \DOMElement + */ + private $currentScenarioSet; + + private $scenarioSetErrorCount = 0; + + private $scenarioSetFailureCount = 0; + + private $scenarioSetTestsCount = 0; + + /** + * @var \DOMElement + */ + private $currentScenario; + + private $scenarioErrorCount = 0; + + private $scenarioFailureCount = 0; + + private $scenarioTestsCount = 0; + + /** + * @var \DOMElement + */ + private $currentStep; + + public function __construct() + { + $this->document = new \DOMDocument('1.0', 'UTF-8'); + $this->document->formatOutput = true; + } + + public function enterScenarioSet(ScenarioSet $scenarios, $concurrency) + { + $this->currentScenarioSet = $this->document->createElement('testsuites'); + $this->currentScenarioSet->setAttribute('name', $scenarios->getName()); + $this->document->appendChild($this->currentScenarioSet); + } + + public function enterScenario(Scenario $scenario, Context $context) + { + $this->currentScenario = $this->document->createElement('testsuite'); + $this->currentScenario->setAttribute('name', $scenario->getName()); + $this->currentScenarioSet->appendChild($this->currentScenario); + } + + public function enterStep(AbstractStep $step, RequestInterface $request, Context $context) + { + $this->currentStep = $this->document->createElement('testcase'); + $this->currentStep->setAttribute('name', $step->getName()); + $this->currentScenario->appendChild($this->currentStep); + + if ($step instanceof Step) { + $assertionsCount = \count($step->getExpectations()) + \count($step->getAssertions()); + } else { + $assertionsCount = 0; + } + + $this->currentStep->setAttribute('assertions', $assertionsCount); + + $this->scenarioTestsCount++; + $this->scenarioSetTestsCount++; + + return $request; + } + + public function leaveStep( + AbstractStep $step, + RequestInterface $request, + ResponseInterface $response, + Context $context + ) { + foreach ($step->getErrors() as $failedAssertion) { + $failure = $this->document->createElement('failure'); + $failure->setAttribute('message', $failedAssertion); + $failure->setAttribute('type', 'performance assertion'); + $this->currentStep->appendChild($failure); + + $this->scenarioFailureCount++; + $this->scenarioSetFailureCount++; + } + + return $response; + } + + public function abortStep(AbstractStep $step, RequestInterface $request, \Exception $exception, Context $context) + { + if ($exception instanceof ExpectationFailureException) { + $failure = $this->document->createElement('failure'); + $failure->setAttribute('message', $exception->getMessage()); + $failure->setAttribute('type', 'expectation'); + $this->currentStep->appendChild($failure); + + $this->scenarioFailureCount++; + $this->scenarioSetFailureCount++; + + return; + } + + $this->scenarioErrorCount++; + $this->scenarioSetErrorCount++; + + $error = $this->document->createElement('error'); + $error->setAttribute('message', $exception->getMessage()); + $error->setAttribute('type', \get_class($exception)); + $this->currentStep->appendChild($error); + } + + public function leaveScenario(Scenario $scenario, Result $result, Context $context) + { + $this->currentScenario->setAttribute('errors', $this->scenarioErrorCount); + $this->scenarioErrorCount = 0; + + $this->currentScenario->setAttribute('failures', $this->scenarioFailureCount); + $this->scenarioFailureCount = 0; + + $this->currentScenario->setAttribute('tests', $this->scenarioTestsCount); + $this->scenarioTestsCount = 0; + } + + public function leaveScenarioSet(ScenarioSet $scenarios, Results $results) + { + $this->currentScenarioSet->setAttribute('errors', $this->scenarioSetErrorCount); + $this->scenarioSetErrorCount = 0; + + $this->currentScenarioSet->setAttribute('failures', $this->scenarioSetFailureCount); + $this->scenarioSetFailureCount = 0; + + $this->currentScenarioSet->setAttribute('tests', $this->scenarioSetTestsCount); + $this->scenarioSetTestsCount = 0; + } + + public function getXml() + { + return $this->document->saveXML(); + } +} diff --git a/Player/Tests/Extension/JUnitExtensionTest.php b/Player/Tests/Extension/JUnitExtensionTest.php new file mode 100644 index 0000000..bae0c85 --- /dev/null +++ b/Player/Tests/Extension/JUnitExtensionTest.php @@ -0,0 +1,109 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Blackfire\Player\Tests\Extension; + +use Blackfire\Build\Build; +use Blackfire\Client; +use Blackfire\ClientConfiguration; +use Blackfire\Player\Context; +use Blackfire\Player\Exception\ExpectationFailureException; +use Blackfire\Player\Exception\LogicException; +use Blackfire\Player\ExpressionLanguage\ExpressionLanguage; +use Blackfire\Player\Extension\BlackfireExtension; +use Blackfire\Player\Extension\JUnitExtension; +use Blackfire\Player\Result; +use Blackfire\Player\Results; +use Blackfire\Player\Scenario; +use Blackfire\Player\ScenarioSet; +use Blackfire\Player\Step\ConfigurableStep; +use Blackfire\Player\Step\ReloadStep; +use Blackfire\Player\Step\Step; +use Blackfire\Player\Step\StepContext; +use Blackfire\Player\ValueBag; +use Blackfire\Profile; +use Blackfire\Profile\Request as ProfileRequest; +use GuzzleHttp\Psr7\Request; +use GuzzleHttp\Psr7\Response; +use PHPUnit\Framework\TestCase; +use Psr\Http\Message\RequestInterface; +use Psr\Http\Message\ResponseInterface; +use Symfony\Component\Console\Output\NullOutput; + +class JUnitExtensionTest extends TestCase +{ + public function testCreatingReport() + { + $extension = new JUnitExtension(); + $set = new ScenarioSet(); + + $scenario = new Scenario('scenario 1'); + $scenario->name('Example scenario'); + $stepSucceed = new Step(); + $stepSucceed->name('Successful step'); + $stepSucceed->assert('main.wall_time < 50ms'); + $stepSucceed->expect('status_code() == 200'); + + $stepFailedAssertion = new Step(); + $stepFailedAssertion->name('Assertion failed'); + $stepFailedAssertion->assert('main.wall_time < 5ms'); + $stepFailedAssertion->addError('Example assertion error'); + + $stepFailedExpectation = new Step(); + $stepFailedExpectation->name('Expectation failed'); + $stepFailedExpectation->expect('status_code() == 400'); + $expectationException = new ExpectationFailureException('Expectation failed'); + + $stepFailedException = new Step(); + $stepFailedException->name('Other exception'); + $exception = new \Exception('Some exception'); + + + $request = $this->createMock(RequestInterface::class); + $response = $this->createMock(ResponseInterface::class); + $context = $this->createMock(Context::class); + $result = $this->createMock(Result::class); + $results = $this->createMock(Results::class); + + $extension->enterScenarioSet($set, 1); + $extension->enterScenario($scenario, $context); + $extension->enterStep($stepSucceed, $request, $context); + $extension->leaveStep($stepSucceed, $request, $response, $context); + $extension->enterStep($stepFailedAssertion, $request, $context); + $extension->leaveStep($stepFailedAssertion, $request, $response, $context); + $extension->enterStep($stepFailedExpectation, $request, $context); + $extension->abortStep($stepFailedExpectation, $request, $expectationException, $context); + $extension->enterStep($stepFailedException, $request, $context); + $extension->abortStep($stepFailedException, $request, $exception, $context); + $extension->leaveScenario($scenario, $result, $context); + $extension->leaveScenarioSet($set, $results); + + self::assertSame(<<<'XML' + + + + + + + + + + + + + + + + +XML + , $extension->getXml()); + } +}