diff --git a/behat.yaml.dist b/behat.yaml.dist index 6344a49..8196474 100644 --- a/behat.yaml.dist +++ b/behat.yaml.dist @@ -3,12 +3,16 @@ default: default: paths: - '%paths.base%/src/bridge/Resources/features' + - '%paths.base%/src/behat/Resources/features' - '%paths.base%/src/phpspec/Resources/features' contexts: - Doyo\Bridge\CodeCoverage\Context\ContainerContext - Doyo\Bridge\CodeCoverage\Context\CoverageContext - Doyo\PhpSpec\CodeCoverage\Context\ApplicationContext - Doyo\PhpSpec\CodeCoverage\Context\FilesystemContext + - Doyo\Bridge\CodeCoverage\Context\BehatContext: + cwd: '%paths.base%/src/bridge/Resources/fixtures' + - Doyo\Bridge\CodeCoverage\Context\ConsoleContext extensions: Doyo\Behat\CodeCoverage\Extension: diff --git a/composer.json b/composer.json index 76f59e2..f5d3adc 100644 --- a/composer.json +++ b/composer.json @@ -45,6 +45,8 @@ "symfony/event-dispatcher": "^3.4 | ^4.0" }, "require-dev": { + "behat/mink-goutte-driver": "^1.2", + "behatch/contexts": "^3.2", "guzzlehttp/guzzle": "^6.3", "phpspec/phpspec": "^4.3 | ^5.0", "phpunit/phpcov": "^4.0 | >=5.0", diff --git a/src/behat/Extension.php b/src/behat/Extension.php index 68910e8..98ad09d 100644 --- a/src/behat/Extension.php +++ b/src/behat/Extension.php @@ -63,8 +63,9 @@ public function load(ContainerBuilder $container, array $config) $container->set('doyo.coverage.container', $coverageContainer); $container->set('doyo.coverage', $coverageContainer->get('coverage')); + /* @var \Symfony\Component\Console\Input\InputInterface $input */ $input = $container->get('cli.input'); - $coverageEnabled = $input->hasParameterOption(['--coverage'], true); + $coverageEnabled = $input->hasParameterOption(['--coverage']); $container->setParameter('doyo.coverage_enabled', $coverageEnabled); $listener = new Definition(CoverageListener::class); diff --git a/src/bridge/.editorconfig b/src/bridge/.editorconfig new file mode 100644 index 0000000..47ba67c --- /dev/null +++ b/src/bridge/.editorconfig @@ -0,0 +1,96 @@ +# EditorConfig helps developers define and maintain consistent +# coding styles between different editors and IDEs +# editorconfig.org + +root = true + +[*] +# Change these settings to your own preference +indent_style = space +indent_size = 4 + +# We recommend you to keep these unchanged +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true + +[*.feature] +indent_style = space +indent_size = 2 + +[*.json] +indent_style = space +indent_size = 2 + +[*.md] +trim_trailing_whitespace = false + +[*.neon] +indent_style = tab +indent_size = 4 + +[*.php] +indent_style = space +indent_size = 4 + +[*.xml] +indent_style = space +indent_size = 4 + +[*.yml] +indent_style = space +indent_size = 4 +trim_trailing_whitespace = false + +[*.yaml] +indent_style = space +indent_size = 4 +trim_trailing_whitespace = false + +[.circleci/config.yml] +indent_style = space +indent_size = 2 + +[.gitmodules] +indent_style = tab + +[.php_cs] +indent_style = space +indent_size = 4 + +[.styleci.yml] +indent_style = space +indent_size = 2 + +[.travis.yml] +indent_style = space +indent_size = 2 + +[appveyor.yml] +indent_style = space +indent_size = 2 + +[behat.yml{.dist}] +indent_style = space +indent_size = 2 + +[coverage.yml{,.dist}] +indent_style = space +indent_size = 2 + +[coverage.yaml{,.dist}] +indent_style = space +indent_size = 2 + +[composer.json] +indent_style = space +indent_size = 4 + +[phpstan.neon{,.dist}] +indent_style = tab +indent_size = 4 + +[phpunit.xml{,.dist}] +indent_style = space +indent_size = 4 diff --git a/src/bridge/Compiler/CoveragePass.php b/src/bridge/Compiler/CoveragePass.php index 9d3bd3e..d31867a 100644 --- a/src/bridge/Compiler/CoveragePass.php +++ b/src/bridge/Compiler/CoveragePass.php @@ -40,15 +40,14 @@ private function processDriver(ContainerBuilder $container) private function processFilter(ContainerBuilder $container) { - $config = $container->getParameter('config.filter'); + $config = $container->getParameter('config'); + $config = $config['filter']; $definition = $container->getDefinition('coverage.filter'); foreach ($config as $options) { - $options['basePath'] = ''; $this->filterWhitelist($definition, $options, 'add'); $exclude = $options['exclude']; foreach ($exclude as $item) { - $item['basePath'] = ''; $this->filterWhitelist($definition, $item, 'remove'); } } @@ -85,11 +84,13 @@ private function filterWhitelist(Definition $definition, $options, $method) if (false !== ($pos=strpos($file, '*'))) { $files = []; foreach (glob($file) as $filename) { + $filename = realpath($filename); $files[] = $filename; } } - - $definition->addMethodCall($method.'File'.$methodSuffix, $files); + foreach($files as $file){ + $definition->addMethodCall($method.'File'.$methodSuffix, [$file]); + } } } } diff --git a/src/bridge/Compiler/SessionPass.php b/src/bridge/Compiler/SessionPass.php new file mode 100644 index 0000000..01b1d06 --- /dev/null +++ b/src/bridge/Compiler/SessionPass.php @@ -0,0 +1,48 @@ +getParameterBag()->get('sessions'); + + foreach($sessions as $name => $config){ + $driver = $config['driver']; + if('local' === $driver){ + $this->createLocalSession($container, $name, $config); + } + } + } + + private function createLocalSession(ContainerBuilder $container, string $name, array $config) + { + $id = 'sessions.'.$name; + $sessionId = $id.'.session'; + $listenerId = $id.'.listener'; + + $session = new Definition(LocalSession::class); + $session->addArgument($name); + $session->addTag('coverage.session'); + $session->addMethodCall('init',[$container->getParameter('config')]); + $container->setDefinition($sessionId, $session); + + $listener = new Definition(LocalListener::class); + $listener->addArgument(new Reference($sessionId)); + $container->setDefinition($listenerId, $listener); + + $dispatcher = $container->findDefinition('coverage'); + $dispatcher->addMethodCall('addSubscriber',[new Reference($listenerId)]); + } +} diff --git a/src/bridge/Configuration.php b/src/bridge/Configuration.php index 1c7cfd2..f884058 100644 --- a/src/bridge/Configuration.php +++ b/src/bridge/Configuration.php @@ -63,6 +63,7 @@ private function configureCoverageSection(ArrayNodeDefinition $node) ->end() ->booleanNode('xdebug_patch')->defaultTrue()->end() ->booleanNode('debug')->defaultFalse()->end() + ->scalarNode('env')->defaultValue('prod')->end() ->arrayNode('coverage') ->addDefaultsIfNotSet() ->children() diff --git a/src/bridge/ContainerFactory.php b/src/bridge/ContainerFactory.php index 9ccf88a..921def0 100644 --- a/src/bridge/ContainerFactory.php +++ b/src/bridge/ContainerFactory.php @@ -15,6 +15,7 @@ use Doyo\Bridge\CodeCoverage\Compiler\CoveragePass; use Doyo\Bridge\CodeCoverage\Compiler\ReportPass; +use Doyo\Bridge\CodeCoverage\Compiler\SessionPass; use Doyo\Bridge\CodeCoverage\Console\Application; use Doyo\Bridge\CodeCoverage\DependencyInjection\CodeCoverageExtension; use Symfony\Component\Config\ConfigCache; @@ -38,6 +39,7 @@ class ContainerFactory public function __construct(array $config = []) { $this->config = $config; + $this->doCreateContainer(); } /** @@ -45,10 +47,6 @@ public function __construct(array $config = []) */ public function getContainer(): ContainerInterface { - if (null === $this->container) { - $this->doCreateContainer(); - } - return $this->container; } @@ -119,6 +117,7 @@ private function doCreateContainer() $builder->addCompilerPass(new CoveragePass()); $builder->addCompilerPass(new ReportPass()); + $builder->addCompilerPass(new SessionPass()); $builder->compile(true); $dumper = new PhpDumper($builder); @@ -143,7 +142,11 @@ private function normalizeConfig($configuration) { $configs = []; foreach ($configuration['imports'] as $file) { - $configs[] = $this->importFile($file); + if(false !== ($file = realpath($file))){ + $configs[] = $this->importFile($file); + }else{ + throw new \RuntimeException('Import file: '.$file.' is not exists or readable.'); + } } return $configs; diff --git a/src/bridge/Context/BehatContext.php b/src/bridge/Context/BehatContext.php new file mode 100644 index 0000000..3024540 --- /dev/null +++ b/src/bridge/Context/BehatContext.php @@ -0,0 +1,92 @@ +cwd = $cwd; + } + + /** + * @beforeScenario + */ + public function beforeScenario(BeforeScenarioScope $scope) + { + $this->consoleContext = $scope->getEnvironment()->getContext(ConsoleContext::class); + + $coverageContext = $scope->getEnvironment()->getContext(CoverageContext::class); + $coverageContext->setWorkingDir($this->cwd); + } + + /** + * @Given I configure behat with: + */ + public function iConfigureBehat(PyStringNode $node) + { + $contents = $node->getRaw(); + $id = md5($contents); + $tmp = sys_get_temp_dir().'/doyo/tests/behat/behat-'.$id.'.yaml'; + if(!is_dir($dir = dirname($tmp))){ + mkdir($dir, 0775, true); + } + file_put_contents($tmp, $contents); + + $this->configFile = $tmp; + } + + /** + * @Given I run behat + * + * @param array $options + * @param string $cwd + */ + public function iRunBehat(array $options = [], $cwd = null) + { + $finder = new ExecutableFinder(); + $phpdbg = $finder->find('phpdbg'); + $cmd = realpath(__DIR__.'/../Resources/fixtures/bin/behat'); + $configFile = $this->configFile; + $cwd = realpath($this->cwd); + + $phpdbg = [$phpdbg, '-qrr']; + if(extension_loaded('xdebug')){ + $phpdbg = []; + } + + + $commands = array_merge($phpdbg,[ + $cmd, + '--config='.$configFile, + '--coverage' + ]); + + $commands = array_merge($commands, $options); + $this->consoleContext->run($commands, $cwd); + } +} diff --git a/src/bridge/Context/ConsoleContext.php b/src/bridge/Context/ConsoleContext.php new file mode 100644 index 0000000..063dc54 --- /dev/null +++ b/src/bridge/Context/ConsoleContext.php @@ -0,0 +1,48 @@ +output = ''; + } + + public function run($commands, $cwd) + { + $process = new Process($commands,$cwd); + $process->setTimeout(0); + $process->run(); + if(0 === $process->getExitCode()){ + $this->output = (string)$process->getOutput(); + }else{ + if(!empty($errorOutput = $process->getErrorOutput())){ + $this->output = $errorOutput; + }else{ + $this->output = $process->getOutput(); + } + + } + } + + /** + * @Then I should see console output :expected + * @param string $expected + */ + public function iShouldSeeConsoleOutput($expected) + { + Assert::contains($this->output, $expected); + } +} diff --git a/src/bridge/Context/ContainerContext.php b/src/bridge/Context/ContainerContext.php index 5a20f42..c2d2332 100644 --- a/src/bridge/Context/ContainerContext.php +++ b/src/bridge/Context/ContainerContext.php @@ -40,16 +40,21 @@ public function setContainer($container) */ public function iConfigureCodeCoverage(PyStringNode $node = null) { - $config = []; + $config = [ + 'env' => 'behat', + 'debug' => true, + ]; if (null !== $node) { $config = Yaml::parse($node->getRaw()); } - $container = (new ContainerFactory($config, true))->getContainer(); - $container->set('console.input', new StringInput('')); - $container->set('console.output', new StreamOutput(fopen('php://memory', '+w'))); + if(is_null($this->container)){ + $container = (new ContainerFactory($config))->getContainer(); + $container->set('console.input', new StringInput('')); + $container->set('console.output', new StreamOutput(fopen('php://memory', '+w'))); - $this->container = $container; + $this->container = $container; + } } /** diff --git a/src/bridge/Context/CoverageContext.php b/src/bridge/Context/CoverageContext.php index 0e637d9..1d3ca84 100644 --- a/src/bridge/Context/CoverageContext.php +++ b/src/bridge/Context/CoverageContext.php @@ -14,6 +14,7 @@ namespace Doyo\Bridge\CodeCoverage\Context; use Behat\Behat\Context\Context; +use Doyo\Bridge\CodeCoverage\Driver\Dummy; use SebastianBergmann\CodeCoverage\CodeCoverage; use Webmozart\Assert\Assert; @@ -24,6 +25,16 @@ class CoverageContext implements Context */ private $coverage; + /** + * @var string + */ + private $workingDir; + + public function setWorkingDir($workingDir) + { + $this->workingDir = $workingDir; + } + /** * @Given I read coverage report :file * @@ -31,16 +42,13 @@ class CoverageContext implements Context */ public function iReadPhpCoverageReport($file) { - $file = getcwd().\DIRECTORY_SEPARATOR.$file; + $file = $this->workingDir.\DIRECTORY_SEPARATOR.$file; Assert::fileExists($file); - /** @var CodeCoverage $coverage */ - include $file; - - $this->coverage = $coverage; + $this->coverage = $this->getCoverage($file); } /** - * @Then file :file line :line should covered + * @Then file :file line :line should be covered * * @param mixed $file * @param mixed|null $line @@ -48,14 +56,37 @@ public function iReadPhpCoverageReport($file) public function fileAtLineShouldCovered($file, $line = null) { $data = $this->coverage->getData(); - $file = getcwd().\DIRECTORY_SEPARATOR.$file; + $expectedFile = $this->workingDir.\DIRECTORY_SEPARATOR.$file; - Assert::true(isset($data[$file])); + Assert::true(isset($data[$expectedFile]), 'File '.$file.' is not covered.'); if (null === $line) { return; } - Assert::true(isset($data[$file][$line])); - Assert::notEmpty($data[$file][$line]); + Assert::true(isset($data[$expectedFile][$line]), 'Line: '.$line. ' is not covered'); + Assert::notEmpty($data[$expectedFile][$line]); + } + + /** + * @return CodeCoverage + */ + private function getCoverage($file) + { + $coverageFile = $file; + Assert::fileExists($coverageFile, 'Code coverage is not generated'); + $patchedFile = $coverageFile.'.php'; + + $contents = file_get_contents($coverageFile); + $pattern = '/^\$coverage\s\=.*/im'; + $contents = preg_replace($pattern, '', $contents); + + file_put_contents($patchedFile, $contents); + + $driver = new Dummy(); + $coverage = new CodeCoverage($driver); + + include $patchedFile; + + return $coverage; } } diff --git a/src/bridge/DependencyInjection/CodeCoverageExtension.php b/src/bridge/DependencyInjection/CodeCoverageExtension.php index 9e03f5c..71f1f5d 100644 --- a/src/bridge/DependencyInjection/CodeCoverageExtension.php +++ b/src/bridge/DependencyInjection/CodeCoverageExtension.php @@ -28,9 +28,11 @@ public function load(array $configs, ContainerBuilder $container) $configuration = $this->processConfiguration(new Configuration(), $configs); + $container->setParameter('config', $configuration); $container->setParameter('reports', $configuration['reports']); $container->setParameter('config.filter', $configuration['filter']); $container->setParameter('coverage.options', $configuration['coverage']); + $container->setParameter('sessions', $configuration['sessions']); $loader->load('code_coverage.xml'); $loader->load('reports.xml'); diff --git a/src/bridge/Resources/coverage.yaml b/src/bridge/Resources/coverage.yaml new file mode 100644 index 0000000..e45c6b4 --- /dev/null +++ b/src/bridge/Resources/coverage.yaml @@ -0,0 +1,10 @@ +filter: + - directory: "" + exclude: + - Spec + - Resources + - Context + - Driver + - vendor + - build + - file: RoboFile.php diff --git a/src/bridge/Resources/features/config.feature b/src/bridge/Resources/features/config.feature index 5e03cc7..7d3afbc 100644 --- a/src/bridge/Resources/features/config.feature +++ b/src/bridge/Resources/features/config.feature @@ -19,12 +19,12 @@ Feature: Configuration Given I have load container with: """ reports: - clover: build/clover.xml - crap4j: build/logs/crap4j.xml - html: build/html - php: build/cov/php.cov + clover: build/behat-test/clover.xml + crap4j: build/behat-test/logs/crap4j.xml + html: build/behat-test/html + php: build/behat-test/cov/php.cov text: console - xml: build/xml + xml: build/behat-test/xml """ Then service "" should loaded diff --git a/src/bridge/Resources/features/local-session.feature b/src/bridge/Resources/features/local-session.feature new file mode 100644 index 0000000..04467ad --- /dev/null +++ b/src/bridge/Resources/features/local-session.feature @@ -0,0 +1,38 @@ +Feature: Local Session + Background: + Given I configure behat with: + """ + default: + suites: + default: + paths: + - 'features' + contexts: + - Behat\MinkExtension\Context\MinkContext + - behatch:context:rest + - behatch:context:json + extensions: + Behat\MinkExtension: + base_url: http://localhost:8000 + sessions: + default: + goutte: ~ + Behatch\Extension: ~ + Doyo\Behat\CodeCoverage\Extension: + env: dev + debug: true + filter: + - src + sessions: + local: ~ + reports: + php: build/local.cov + """ + + Scenario: Run code coverage with local session enabled + When I run behat + Then I should see console output "1 scenario (1 passed)" + And I should see console output "4 steps (4 passed)" + When I read coverage report "build/local.cov" + Then file "src/Foo.php" line 11 should be covered + diff --git a/src/bridge/Resources/fixtures/.gitignore b/src/bridge/Resources/fixtures/.gitignore new file mode 100644 index 0000000..84c048a --- /dev/null +++ b/src/bridge/Resources/fixtures/.gitignore @@ -0,0 +1 @@ +/build/ diff --git a/src/bridge/Resources/fixtures/behat.yaml b/src/bridge/Resources/fixtures/behat.yaml new file mode 100644 index 0000000..f669304 --- /dev/null +++ b/src/bridge/Resources/fixtures/behat.yaml @@ -0,0 +1,21 @@ +default: + suites: + default: + contexts: + - Behat\MinkExtension\Context\MinkContext + - behatch:context:rest + - behatch:context:json + extensions: + Behat\MinkExtension: + base_url: http://localhost:8000 + sessions: + default: + goutte: ~ + Behatch\Extension: ~ + Doyo\Behat\CodeCoverage\Extension: + filter: + - src + sessions: + local: ~ + reports: + php: build/local.cov diff --git a/src/bridge/Resources/fixtures/bin/behat b/src/bridge/Resources/fixtures/bin/behat new file mode 100755 index 0000000..70d3644 --- /dev/null +++ b/src/bridge/Resources/fixtures/bin/behat @@ -0,0 +1,9 @@ +#!/usr/bin/env php +createApplication()->run(); diff --git a/src/bridge/Resources/fixtures/features/local.feature b/src/bridge/Resources/fixtures/features/local.feature new file mode 100644 index 0000000..501773a --- /dev/null +++ b/src/bridge/Resources/fixtures/features/local.feature @@ -0,0 +1,8 @@ +Feature: Local + + Scenario: Say foo bar + Given I send a "GET" request to "/local.php" + Then the response status code should be 200 + And the response should be in JSON + And the JSON node foo should exist + diff --git a/src/bridge/Resources/fixtures/public/local.php b/src/bridge/Resources/fixtures/public/local.php new file mode 100644 index 0000000..adffb82 --- /dev/null +++ b/src/bridge/Resources/fixtures/public/local.php @@ -0,0 +1,18 @@ + (new Foo())->say() +]); + +$response->send(); diff --git a/src/bridge/Resources/fixtures/src/Foo.php b/src/bridge/Resources/fixtures/src/Foo.php new file mode 100644 index 0000000..b5687bb --- /dev/null +++ b/src/bridge/Resources/fixtures/src/Foo.php @@ -0,0 +1,13 @@ +adapter; $cached = $adapter->getItem(static::CACHE_KEY)->get(); - $this->fromCache($cached); - $this->createContainer($this->config); + if(!is_null($cached)){ + $this->fromCache($cached); + } + if(!empty($this->config)){ + $this->createContainer($this->config); + } } private function createContainer($config) @@ -153,11 +161,8 @@ private function toCache() return $data; } - private function fromCache($cache) + private function fromCache(array $cache) { - if (null === $cache) { - return; - } foreach ($cache as $name => $value) { $this->{$name} = $value; } @@ -178,7 +183,9 @@ public function reset() $this->testCase = null; $this->exceptions = []; - $this->processor->clear(); + if($this->processor instanceof ProcessorInterface){ + $this->processor->clear(); + } } public function hasExceptions() @@ -212,29 +219,52 @@ public function setTestCase(TestCase $testCase) public function start() { if (null === $this->testCase) { - throw new SessionException('Can not start coverage without null TestCase'); + throw new SessionException('Can not start coverage with null TestCase'); } try { $container = $this->container; $testCase = $this->testCase; - $processor = $container->get('factory')->createProcessor(); - $processor->setCurrentTestCase($testCase); + $filter = $this->getProcessor()->getCodeCoverage()->filter(); + $class = $container->getParameter('coverage.driver.class'); + $coverage = new CodeCoverage(new $class, $filter); + $processor = new Processor($coverage); + + $processor->start($testCase); $this->currentProcessor = $processor; + $this->started = true; } catch (\Exception $exception) { + $this->started = false; + $message = sprintf( + "Can not start coverage on session %s. Error message:\n%s", + $this->getName(), + $exception->getMessage() + ); + $exception = new SessionException($message); $this->addException($exception); } } public function stop() { - $this->currentProcessor->stop(); - $this->processor->merge($this->currentProcessor); + try{ + $this->currentProcessor->stop(); + $this->processor->merge($this->currentProcessor); + $this->started = false; + }catch (\Exception $exception){ + $message = sprintf( + "Can not stop coverage on session %s. Error message:\n%s", + $this->name, + $exception->getMessage() + ); + $e = new SessionException($message); + $this->addException($e); + } } public function shutdown() { - if (null !== $this->currentProcessor) { + if ($this->started) { $this->stop(); } $this->save(); diff --git a/src/bridge/Session/LocalSession.php b/src/bridge/Session/LocalSession.php index 7532744..b0677a8 100644 --- a/src/bridge/Session/LocalSession.php +++ b/src/bridge/Session/LocalSession.php @@ -20,13 +20,10 @@ public static function startSession($name): bool $self = new static($name); try { $self->start(); - register_shutdown_function([$self, 'shutdown']); - return true; } catch (\Exception $exception) { $self->addException($exception); $self->save(); - return false; } } diff --git a/src/bridge/Session/RemoteSession.php b/src/bridge/Session/RemoteSession.php index ccc48fe..c6c0d23 100644 --- a/src/bridge/Session/RemoteSession.php +++ b/src/bridge/Session/RemoteSession.php @@ -26,31 +26,24 @@ public static function startSession() return false; } - $name = $_SERVER[static::HEADER_SESSION_KEY]; - $session = new static($name); - - if (isset($_SERVER[static::HEADER_TEST_CASE_KEY])) { - $session->doStartSession(); - } else { + if(!isset($_SERVER[static::HEADER_TEST_CASE_KEY])){ return false; } - $session->save(); - - return true; - } - public function doStartSession() - { - $name = $_SERVER[static::HEADER_TEST_CASE_KEY]; - $testCase = new TestCase($name); - $this->setTestCase($testCase); - - try { - $this->start(); - register_shutdown_function([$this, 'shutdown']); - } catch (\Exception $e) { - $this->reset(); - $this->exceptions[] = $e; + $sessionName = $_SERVER[static::HEADER_SESSION_KEY]; + $session = new static($sessionName); + $testCaseName = $_SERVER[static::HEADER_TEST_CASE_KEY]; + $testCase = new TestCase($testCaseName); + + try{ + $session->setTestCase($testCase); + $session->start(); + $session->save(); + return true; + }catch (\Exception $e){ + $session->addException($e); + $session->save(); + return false; } } } diff --git a/src/bridge/Session/SessionFactory.php b/src/bridge/Session/SessionFactory.php deleted file mode 100644 index 44bdf61..0000000 --- a/src/bridge/Session/SessionFactory.php +++ /dev/null @@ -1,38 +0,0 @@ - - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -declare(strict_types=1); - -namespace Doyo\Bridge\CodeCoverage\Session; - -use Doyo\Bridge\CodeCoverage\Driver\Dummy; -use Doyo\Bridge\CodeCoverage\Processor; -use Symfony\Component\DependencyInjection\ContainerInterface; - -class SessionFactory -{ - public function createLocalSession(ContainerInterface $container, $name) - { - $session = new LocalSession($name); - $this->decorateSession($container, $session); - } - - protected function decorateSession(ContainerInterface $container, SessionInterface $session) - { - $filter = $container->get('coverage.filter'); - $dummy = $container->getParameter(new Dummy()); - - $processor = new Processor($dummy, $filter); - $session->setProcessor($processor); - $session->setContainer($container); - $session->save(); - } -} diff --git a/src/bridge/Spec/ContainerFactorySpec.php b/src/bridge/Spec/ContainerFactorySpec.php index 61792cd..e0d490b 100644 --- a/src/bridge/Spec/ContainerFactorySpec.php +++ b/src/bridge/Spec/ContainerFactorySpec.php @@ -13,8 +13,13 @@ namespace Spec\Doyo\Bridge\CodeCoverage; +use Prophecy\Argument; +use SebastianBergmann\CodeCoverage\Filter; +use Doyo\Bridge\CodeCoverage\Console\Application; use Doyo\Bridge\CodeCoverage\ContainerFactory; +use Doyo\Bridge\CodeCoverage\ProcessorInterface; use PhpSpec\ObjectBehavior; +use SebastianBergmann\CodeCoverage\CodeCoverage; use Symfony\Component\DependencyInjection\ContainerInterface; /** @@ -26,7 +31,13 @@ class ContainerFactorySpec extends ObjectBehavior { public function let() { - $this->beConstructedWith([], true); + $this->beConstructedWith([ + 'debug' => true, + 'env' => 'spec', + 'imports' => [ + __DIR__.'/Fixtures/test-coverage.yaml' + ] + ]); } public function it_is_initializable() @@ -34,6 +45,13 @@ public function it_is_initializable() $this->shouldHaveType(ContainerFactory::class); } + function it_should_handle_imports() + { + $filter = $this->getContainer()->get('coverage.filter'); + $filter->shouldReturnAnInstanceOf(Filter::class); + $filter->getWhitelistedFiles()->shouldHaveCount(2); + } + /** * @covers \Doyo\Bridge\CodeCoverage\ContainerFactory */ @@ -41,4 +59,19 @@ public function it_should_create_container() { $this->getContainer()->shouldBeAnInstanceOf(ContainerInterface::class); } + + public function it_should_create_processor() + { + $this->createProcessor(true)->shouldReturnAnInstanceOf(ProcessorInterface::class); + } + + public function it_should_create_code_coverage() + { + $this->createCodeCoverage(true)->shouldReturnAnInstanceOf(CodeCoverage::class); + } + + public function it_should_create_application() + { + $this->createApplication()->shouldReturnAnInstanceOf(Application::class); + } } diff --git a/src/bridge/Spec/Fixtures/src/foo.php b/src/bridge/Spec/Fixtures/src/foo.php new file mode 100644 index 0000000..b3d9bbc --- /dev/null +++ b/src/bridge/Spec/Fixtures/src/foo.php @@ -0,0 +1 @@ +shouldBeCalled() ->willThrow($e); - $consoleIO->coverageInfo('some error')->shouldBeCalledOnce(); + $consoleIO->coverageInfo(Argument::any())->shouldBeCalled(); $this->refresh(); $this->complete($event); diff --git a/src/bridge/Spec/Session/AbstractSessionSpec.php b/src/bridge/Spec/Session/AbstractSessionSpec.php index abbf119..25f23ca 100644 --- a/src/bridge/Spec/Session/AbstractSessionSpec.php +++ b/src/bridge/Spec/Session/AbstractSessionSpec.php @@ -13,13 +13,26 @@ namespace Spec\Doyo\Bridge\CodeCoverage\Session; +use Doyo\Bridge\CodeCoverage\Driver\Dummy; +use Doyo\Bridge\CodeCoverage\Exception\SessionException; +use Doyo\Bridge\CodeCoverage\ProcessorInterface; use Doyo\Bridge\CodeCoverage\Session\AbstractSession; +use Doyo\Bridge\CodeCoverage\TestCase; use PhpSpec\ObjectBehavior; +use Prophecy\Argument; +use SebastianBergmann\CodeCoverage\CodeCoverage; +use SebastianBergmann\CodeCoverage\Filter; class AbstractSessionSpec extends ObjectBehavior { - public function let() + public function let( + ProcessorInterface $processor, + Dummy $driver + ) { + $filter = new Filter(); + $coverage = new CodeCoverage($driver->getWrappedObject(), $filter); + $processor->getCodeCoverage()->willReturn($coverage); $this->beAnInstanceOf(TestSession::class); $this->beConstructedWith('abstract'); } @@ -29,7 +42,9 @@ public function it_is_initializable() $this->shouldHaveType(AbstractSession::class); } - public function it_should_init_session() + public function it_should_init_session( + TestCase $testCase + ) { $config = [ 'filter' => [ @@ -39,12 +54,90 @@ public function it_should_init_session() ], ]; + $this->setTestCase($testCase); $this->init($config); - $this->save(); $this->refresh(); $this->getName()->shouldReturn('abstract'); - $this->getConfig()->shouldReturn($config); + $this->getProcessor()->shouldBeAnInstanceOf(ProcessorInterface::class); + } + + public function its_exceptions_should_be_mutable() + { + $this->hasExceptions()->shouldReturn(false); + $this->getExceptions()->shouldBeEqualTo([]); + + $e = new \Exception('test'); + $this->addException($e); + $this->hasExceptions()->shouldBe(true); + $this->getExceptions()->shouldContain($e); + } + + public function its_start_throw_exception_when_TestCase_is_null() + { + $this->reset(); + $this->save(); + $this->shouldThrow(SessionException::class) + ->during('start'); + } + + public function it_should_start_and_stop_coverage( + ProcessorInterface $processor, + TestCase $testCase + ) + { + $processor->merge(Argument::type(ProcessorInterface::class)) + ->shouldBeCalledOnce(); + $testCase->getName()->willReturn('some-test'); + + $this->setProcessor($processor); + $this->setTestCase($testCase); + $this->start(); + $this->stop(); + + $this->hasExceptions()->shouldBe(false); + } + + public function its_start_should_handle_exception( + ProcessorInterface $processor, + TestCase $testCase + ) + { + $e = new \Exception('some error'); + + $testCase->getName()->willThrow($e); + + $this->setTestCase($testCase); + $this->setProcessor($processor); + $this->start(); + $this->hasExceptions()->shouldBe(true); + } + + public function its_stop_should_handle_exception( + ProcessorInterface $processor, + TestCase $testCase + ) + { + $testCase->getName()->willReturn('test'); + $e = new \Exception('test'); + $processor->merge(Argument::any())->willThrow($e); + $this->setProcessor($processor); + $this->setTestCase($testCase); + + $this->start(); + $this->stop(); + + $this->hasExceptions()->shouldBe(true); + } + + public function it_should_shutdown_properly( + TestCase $testCase + ) + { + $testCase->getName()->willReturn('test'); + $this->setTestCase($testCase); + $this->start(); + $this->shutdown(); } } diff --git a/src/bridge/Spec/Session/LocalSessionSpec.php b/src/bridge/Spec/Session/LocalSessionSpec.php new file mode 100644 index 0000000..7dae627 --- /dev/null +++ b/src/bridge/Spec/Session/LocalSessionSpec.php @@ -0,0 +1,43 @@ +beConstructedWith('spec-local'); + $this->init(['env' => 'spec', 'debug' => true]); + $this->reset(); + } + + function it_is_initializable() + { + $this->shouldHaveType(LocalSession::class); + } + + /** + * Note: Error should throw because of null TestCase + */ + function its_startSession_should_handle_error() + { + /* @todo Recheck if exceptions really added to cache */ + $this::startSession('spec-local')->shouldBe(false); + $this->refresh(); + } + + function it_should_start_session( + TestCase $testCase + ) + { + $this->setTestCase($testCase); + $this->save(); + + $this::startSession('spec-local')->shouldBe(true); + } +} diff --git a/src/bridge/Spec/Session/RemoteSessionSpec.php b/src/bridge/Spec/Session/RemoteSessionSpec.php new file mode 100644 index 0000000..315e1a1 --- /dev/null +++ b/src/bridge/Spec/Session/RemoteSessionSpec.php @@ -0,0 +1,45 @@ +beConstructedWith('spec-remote'); + $this->init([ + 'env' => 'spec', + 'debug' => true + ]); + } + + function it_is_initializable() + { + $this->shouldHaveType(RemoteSession::class); + } + + function its_startSession_returns_false_with_empty_server_session_key() + { + $this::startSession()->shouldBe(false); + } + + function its_startSession_returns_false_with_empty_server_test_case_key() + { + $_SERVER[RemoteSession::HEADER_SESSION_KEY] = 'spec-remote'; + + $this::startSession()->shouldBe(false); + } + + + function it_should_start_session() + { + $_SERVER[RemoteSession::HEADER_SESSION_KEY] = 'spec-remote'; + $_SERVER[RemoteSession::HEADER_TEST_CASE_KEY] = 'test'; + + $this::startSession()->shouldBe(true); + } +} diff --git a/src/bridge/Spec/Session/TestRemoteSession.php b/src/bridge/Spec/Session/TestRemoteSession.php new file mode 100644 index 0000000..a3d8355 --- /dev/null +++ b/src/bridge/Spec/Session/TestRemoteSession.php @@ -0,0 +1,16 @@ +processor = $processor; + } +} diff --git a/src/bridge/Spec/Session/TestSession.php b/src/bridge/Spec/Session/TestSession.php index 7bf7805..40699ae 100644 --- a/src/bridge/Spec/Session/TestSession.php +++ b/src/bridge/Spec/Session/TestSession.php @@ -13,10 +13,18 @@ namespace Spec\Doyo\Bridge\CodeCoverage\Session; +use Doyo\Bridge\CodeCoverage\ProcessorInterface; use Doyo\Bridge\CodeCoverage\Session\AbstractSession; class TestSession extends AbstractSession { + public function setProcessor( + ProcessorInterface $processor + ) + { + $this->processor = $processor; + } + public function getConfig() { return $this->config; diff --git a/src/bridge/behat.yaml.dist b/src/bridge/behat.yaml.dist index 434fc81..4069b21 100644 --- a/src/bridge/behat.yaml.dist +++ b/src/bridge/behat.yaml.dist @@ -5,14 +5,17 @@ default: - Resources/features contexts: - Doyo\Bridge\CodeCoverage\Context\ContainerContext + - Doyo\Bridge\CodeCoverage\Context\CoverageContext + - Doyo\Bridge\CodeCoverage\Context\ConsoleContext + - Doyo\Bridge\CodeCoverage\Context\BehatContext: + cwd: Resources/fixtures + extensions: Doyo\Behat\CodeCoverage\Extension: - filter: - - directory: "" - exclude: - - vendor - - build - - Spec - - Resources - - Context - - file: RoboFile.php + sessions: + console: ~ + imports: + - Resources/coverage.yaml + reports: + php: build/cov/behat.cov + html: build/cov/html diff --git a/src/bridge/composer.json b/src/bridge/composer.json index 3aa1dbb..c87b204 100644 --- a/src/bridge/composer.json +++ b/src/bridge/composer.json @@ -37,13 +37,16 @@ "php": ">=7.0", "psr/http-message": "^1.0", "symfony/cache": "^3.4 | ^4.0", + "symfony/console": "^3.4 | ^4.0", "symfony/http-foundation": "^3.4 | ^4.0" }, "require-dev": { "behat/behat": "^3.5", + "behatch/contexts": "^3.2", "doyo/behat-code-coverage": "^1.0@dev", "doyo/phpspec-code-coverage": "^1.0@dev", "phpspec/phpspec": "^4.3 | ^5.0", - "phpunit/phpcov": "^4.0 | >=5.0" + "phpunit/phpcov": "^4.0 | >=5.0", + "symfony/process": "^3.4 | ^4.0" } } diff --git a/src/bridge/phpspec.yaml.dist b/src/bridge/phpspec.yaml.dist index d3e3fae..9370322 100644 --- a/src/bridge/phpspec.yaml.dist +++ b/src/bridge/phpspec.yaml.dist @@ -8,16 +8,8 @@ suites: extensions: Doyo\PhpSpec\CodeCoverage\Extension: - filter: - - directory: "" - exclude: - - Spec - - Resources - - Context - - Driver - - vendor - - build - - file: RoboFile.php + imports: + - Resources/coverage.yaml reports: php: build/cov/phpspec.cov html: build/phpspec diff --git a/src/phpspec/Context/FilesystemContext.php b/src/phpspec/Context/FilesystemContext.php index 9ff8682..68b0484 100644 --- a/src/phpspec/Context/FilesystemContext.php +++ b/src/phpspec/Context/FilesystemContext.php @@ -14,7 +14,9 @@ namespace Doyo\PhpSpec\CodeCoverage\Context; use Behat\Behat\Context\Context; +use Behat\Behat\Hook\Scope\ScenarioScope; use Behat\Gherkin\Node\PyStringNode; +use Doyo\Bridge\CodeCoverage\Context\CoverageContext; use PHPUnit\Framework\Assert; use Symfony\Component\Filesystem\Exception\IOException; use Symfony\Component\Filesystem\Filesystem; @@ -34,6 +36,11 @@ class FilesystemContext implements Context */ private $filesystem; + /** + * @var CoverageContext + */ + private $coverageContext; + public function __construct() { $this->filesystem = new Filesystem(); @@ -42,9 +49,15 @@ public function __construct() /** * @beforeScenario */ - public function prepWorkingDirectory() + public function prepWorkingDirectory(ScenarioScope $scope) { - $this->workingDirectory = tempnam(sys_get_temp_dir(), 'phpspec-behat'); + $this->coverageContext = $scope->getEnvironment()->getContext(CoverageContext::class); + + $dir = sys_get_temp_dir().'/doyo/tests'; + if(!is_dir($dir)){ + mkdir($dir,0775,true); + } + $this->workingDirectory = tempnam($dir, 'phpspec-behat'); $this->filesystem->remove($this->workingDirectory); $this->filesystem->mkdir($this->workingDirectory); chdir($this->workingDirectory); @@ -65,13 +78,25 @@ public function prepWorkingDirectory() ); } + /** + * @Given I read phpspec coverage report :file + * + * @param string $file + */ + public function iReadPhpspecCoverageReport($file) + { + $context = $this->coverageContext; + $context->setWorkingDir(getcwd()); + $context->iReadPhpCoverageReport($file); + } + /** * @afterScenario */ public function removeWorkingDirectory() { try { - //$this->filesystem->remove($this->workingDirectory); + $this->filesystem->remove($this->workingDirectory); } catch (IOException $e) { //ignoring exception } diff --git a/src/phpspec/Resources/features/coverage.feature b/src/phpspec/Resources/features/coverage.feature index 54371b9..a166a59 100644 --- a/src/phpspec/Resources/features/coverage.feature +++ b/src/phpspec/Resources/features/coverage.feature @@ -55,5 +55,5 @@ Feature: PHP Spec Code Coverage And I should see "generated html" And I should see "generated php" And I should see "\Test::Test\Foo" - When I read coverage report "build/cov/phpspec.cov" - Then file "src/Test/Foo.php" line 9 should covered + When I read phpspec coverage report "build/cov/phpspec.cov" + Then file "src/Test/Foo.php" line 9 should be covered