This commit is contained in:
2020-10-06 14:27:47 +07:00
commit 586be80cf6
16613 changed files with 3274099 additions and 0 deletions

View File

@@ -0,0 +1,175 @@
<?php
namespace Codeception\Coverage;
use Codeception\Configuration;
use Codeception\Exception\ConfigurationException;
use Codeception\Exception\ModuleException;
use Symfony\Component\Finder\Finder;
class Filter
{
/**
* @var \SebastianBergmann\CodeCoverage\CodeCoverage
*/
protected $phpCodeCoverage = null;
/**
* @var Filter
*/
protected static $c3;
/**
* @var \SebastianBergmann\CodeCoverage\Filter
*/
protected $filter = null;
public function __construct(\SebastianBergmann\CodeCoverage\CodeCoverage $phpCoverage)
{
$this->phpCodeCoverage = $phpCoverage
? $phpCoverage
: new \SebastianBergmann\CodeCoverage\CodeCoverage;
$this->filter = $this->phpCodeCoverage->filter();
}
/**
* @param \SebastianBergmann\CodeCoverage\CodeCoverage $phpCoverage
* @return Filter
*/
public static function setup(\SebastianBergmann\CodeCoverage\CodeCoverage $phpCoverage)
{
self::$c3 = new self($phpCoverage);
return self::$c3;
}
/**
* @return null|\SebastianBergmann\CodeCoverage\CodeCoverage
*/
public function getPhpCodeCoverage()
{
return $this->phpCodeCoverage;
}
/**
* @param $config
* @return Filter
*/
public function whiteList($config)
{
$filter = $this->filter;
if (!isset($config['coverage'])) {
return $this;
}
$coverage = $config['coverage'];
if (!isset($coverage['whitelist'])) {
$coverage['whitelist'] = [];
if (isset($coverage['include'])) {
$coverage['whitelist']['include'] = $coverage['include'];
}
if (isset($coverage['exclude'])) {
$coverage['whitelist']['exclude'] = $coverage['exclude'];
}
}
if (isset($coverage['whitelist']['include'])) {
if (!is_array($coverage['whitelist']['include'])) {
throw new ConfigurationException('Error parsing yaml. Config `whitelist: include:` should be an array');
}
foreach ($coverage['whitelist']['include'] as $fileOrDir) {
$finder = strpos($fileOrDir, '*') === false
? [Configuration::projectDir() . DIRECTORY_SEPARATOR . $fileOrDir]
: $this->matchWildcardPattern($fileOrDir);
foreach ($finder as $file) {
$filter->addFileToWhitelist($file);
}
}
}
if (isset($coverage['whitelist']['exclude'])) {
if (!is_array($coverage['whitelist']['exclude'])) {
throw new ConfigurationException('Error parsing yaml. Config `whitelist: exclude:` should be an array');
}
foreach ($coverage['whitelist']['exclude'] as $fileOrDir) {
$finder = strpos($fileOrDir, '*') === false
? [Configuration::projectDir() . DIRECTORY_SEPARATOR . $fileOrDir]
: $this->matchWildcardPattern($fileOrDir);
foreach ($finder as $file) {
$filter->removeFileFromWhitelist($file);
}
}
}
return $this;
}
/**
* @param $config
* @return Filter
*/
public function blackList($config)
{
$filter = $this->filter;
if (!isset($config['coverage'])) {
return $this;
}
$coverage = $config['coverage'];
if (isset($coverage['blacklist'])) {
if (!method_exists($filter, 'addFileToBlacklist')) {
throw new ModuleException($this, 'The blacklist functionality has been removed from PHPUnit 5,'
. ' please remove blacklist section from configuration.');
}
if (isset($coverage['blacklist']['include'])) {
foreach ($coverage['blacklist']['include'] as $fileOrDir) {
$finder = strpos($fileOrDir, '*') === false
? [Configuration::projectDir() . DIRECTORY_SEPARATOR . $fileOrDir]
: $this->matchWildcardPattern($fileOrDir);
foreach ($finder as $file) {
$filter->addFileToBlacklist($file);
}
}
}
if (isset($coverage['blacklist']['exclude'])) {
foreach ($coverage['blacklist']['exclude'] as $fileOrDir) {
$finder = strpos($fileOrDir, '*') === false
? [Configuration::projectDir() . DIRECTORY_SEPARATOR . $fileOrDir]
: $this->matchWildcardPattern($fileOrDir);
foreach ($finder as $file) {
$filter->removeFileFromBlacklist($file);
}
}
}
}
return $this;
}
protected function matchWildcardPattern($pattern)
{
$finder = Finder::create();
$fileOrDir = str_replace('\\', '/', $pattern);
$parts = explode('/', $fileOrDir);
$file = array_pop($parts);
$finder->name($file);
if (count($parts)) {
$last_path = array_pop($parts);
if ($last_path === '*') {
$finder->in(Configuration::projectDir() . implode('/', $parts));
} else {
$finder->in(Configuration::projectDir() . implode('/', $parts) . '/' . $last_path);
}
}
$finder->ignoreVCS(true)->files();
return $finder;
}
/**
* @return \SebastianBergmann\CodeCoverage\Filter
*/
public function getFilter()
{
return $this->filter;
}
}

View File

@@ -0,0 +1,47 @@
<?php
namespace Codeception\Coverage\Subscriber;
use Codeception\Coverage\SuiteSubscriber;
use Codeception\Event\SuiteEvent;
use Codeception\Events;
use Codeception\Lib\Interfaces\Remote;
/**
* Collects code coverage from unit and functional tests.
* Results from all suites are merged.
*/
class Local extends SuiteSubscriber
{
public static $events = [
Events::SUITE_BEFORE => 'beforeSuite',
Events::SUITE_AFTER => 'afterSuite',
];
/**
* @var Remote
*/
protected $module;
protected function isEnabled()
{
return $this->module === null and $this->settings['enabled'];
}
public function beforeSuite(SuiteEvent $e)
{
$this->applySettings($e->getSettings());
$this->module = $this->getServerConnectionModule($e->getSuite()->getModules());
if (!$this->isEnabled()) {
return;
}
$this->applyFilter($e->getResult());
}
public function afterSuite(SuiteEvent $e)
{
if (!$this->isEnabled()) {
return;
}
$this->mergeToPrint($e->getResult()->getCodeCoverage());
}
}

View File

@@ -0,0 +1,238 @@
<?php
namespace Codeception\Coverage\Subscriber;
use Codeception\Configuration;
use Codeception\Coverage\SuiteSubscriber;
use Codeception\Event\StepEvent;
use Codeception\Event\SuiteEvent;
use Codeception\Event\TestEvent;
use Codeception\Events;
use Codeception\Exception\ModuleException;
use Codeception\Exception\RemoteException;
/**
* When collecting code coverage data from local server HTTP requests are sent to c3.php file.
* Coverage Collection is started by sending cookies/headers.
* Result is taken from the local file and merged with local code coverage results.
*
* Class LocalServer
* @package Codeception\Coverage\Subscriber
*/
class LocalServer extends SuiteSubscriber
{
// headers
const COVERAGE_HEADER = 'X-Codeception-CodeCoverage';
const COVERAGE_HEADER_ERROR = 'X-Codeception-CodeCoverage-Error';
const COVERAGE_HEADER_CONFIG = 'X-Codeception-CodeCoverage-Config';
const COVERAGE_HEADER_SUITE = 'X-Codeception-CodeCoverage-Suite';
// cookie names
const COVERAGE_COOKIE = 'CODECEPTION_CODECOVERAGE';
const COVERAGE_COOKIE_ERROR = 'CODECEPTION_CODECOVERAGE_ERROR';
protected $suiteName;
protected $c3Access = [
'http' => [
'method' => "GET",
'header' => ''
]
];
/**
* @var \Codeception\Lib\Interfaces\Web
*/
protected $module;
public static $events = [
Events::SUITE_BEFORE => 'beforeSuite',
Events::TEST_BEFORE => 'beforeTest',
Events::STEP_AFTER => 'afterStep',
Events::SUITE_AFTER => 'afterSuite',
];
protected function isEnabled()
{
return $this->module && !$this->settings['remote'] && $this->settings['enabled'];
}
public function beforeSuite(SuiteEvent $e)
{
$this->module = $this->getServerConnectionModule($e->getSuite()->getModules());
$this->applySettings($e->getSettings());
if (!$this->isEnabled()) {
return;
}
$this->suiteName = $e->getSuite()->getBaseName();
if ($this->settings['remote_config']) {
$this->addC3AccessHeader(self::COVERAGE_HEADER_CONFIG, $this->settings['remote_config']);
}
$knock = $this->c3Request('clear');
if ($knock === false) {
throw new RemoteException(
'
CodeCoverage Error.
Check the file "c3.php" is included in your application.
We tried to access "/c3/report/clear" but this URI was not accessible.
You can review actual error messages in c3tmp dir.
'
);
}
}
public function beforeTest(TestEvent $e)
{
if (!$this->isEnabled()) {
return;
}
$this->startCoverageCollection($e->getTest()->getName());
}
public function afterStep(StepEvent $e)
{
if (!$this->isEnabled()) {
return;
}
$this->fetchErrors();
}
public function afterSuite(SuiteEvent $e)
{
if (!$this->isEnabled()) {
return;
}
$coverageFile = Configuration::outputDir() . 'c3tmp/codecoverage.serialized';
$retries = 5;
while (!file_exists($coverageFile) && --$retries >= 0) {
usleep(0.5 * 1000000); // 0.5 sec
}
if (!file_exists($coverageFile)) {
if (file_exists(Configuration::outputDir() . 'c3tmp/error.txt')) {
throw new \RuntimeException(file_get_contents(Configuration::outputDir() . 'c3tmp/error.txt'));
}
return;
}
$contents = file_get_contents($coverageFile);
$coverage = @unserialize($contents);
if ($coverage === false) {
return;
}
$this->mergeToPrint($coverage);
}
protected function c3Request($action)
{
$this->addC3AccessHeader(self::COVERAGE_HEADER, 'remote-access');
$context = stream_context_create($this->c3Access);
$c3Url = $this->settings['c3_url'] ? $this->settings['c3_url'] : $this->module->_getUrl();
$contents = file_get_contents($c3Url . '/c3/report/' . $action, false, $context);
$okHeaders = array_filter(
$http_response_header,
function ($h) {
return preg_match('~^HTTP(.*?)\s200~', $h);
}
);
if (empty($okHeaders)) {
throw new RemoteException("Request was not successful. See response header: " . $http_response_header[0]);
}
if ($contents === false) {
$this->getRemoteError($http_response_header);
}
return $contents;
}
protected function startCoverageCollection($testName)
{
$value = [
'CodeCoverage' => $testName,
'CodeCoverage_Suite' => $this->suiteName,
'CodeCoverage_Config' => $this->settings['remote_config']
];
$value = json_encode($value);
if ($this->module instanceof \Codeception\Module\WebDriver) {
$this->module->amOnPage('/');
}
$c3Url = parse_url($this->settings['c3_url'] ? $this->settings['c3_url'] : $this->module->_getUrl());
// we need to separate coverage cookies by host; we can't separate cookies by port.
$c3Host = isset($c3Url['host']) ? $c3Url['host'] : 'localhost';
$this->module->setCookie(self::COVERAGE_COOKIE, $value, ['domain' => $c3Host]);
// putting in configuration ensures the cookie is used for all sessions of a MultiSession test
$cookies = $this->module->_getConfig('cookies');
if (!$cookies || !is_array($cookies)) {
$cookies = [];
}
$found = false;
foreach ($cookies as &$cookie) {
if (!is_array($cookie) || !array_key_exists('Name', $cookie) || !array_key_exists('Value', $cookie)) {
// \Codeception\Lib\InnerBrowser will complain about this
continue;
}
if ($cookie['Name'] === self::COVERAGE_COOKIE) {
$found = true;
$cookie['Value'] = $value;
break;
}
}
if (!$found) {
$cookies[] = [
'Name' => self::COVERAGE_COOKIE,
'Value' => $value
];
}
$this->module->_setConfig(['cookies' => $cookies]);
}
protected function fetchErrors()
{
try {
$error = $this->module->grabCookie(self::COVERAGE_COOKIE_ERROR);
} catch (ModuleException $e) {
// when a new session is started we can't get cookies because there is no
// current page, but there can be no code coverage error either
$error = null;
}
if (!empty($error)) {
$this->module->resetCookie(self::COVERAGE_COOKIE_ERROR);
throw new RemoteException($error);
}
}
protected function getRemoteError($headers)
{
foreach ($headers as $header) {
if (strpos($header, self::COVERAGE_HEADER_ERROR) === 0) {
throw new RemoteException($header);
}
}
}
protected function addC3AccessHeader($header, $value)
{
$headerString = "$header: $value\r\n";
if (strpos($this->c3Access['http']['header'], $headerString) === false) {
$this->c3Access['http']['header'] .= $headerString;
}
}
protected function applySettings($settings)
{
parent::applySettings($settings);
if (isset($settings['coverage']['remote_context_options'])) {
$this->c3Access = array_replace_recursive($this->c3Access, $settings['coverage']['remote_context_options']);
}
}
}

View File

@@ -0,0 +1,151 @@
<?php
namespace Codeception\Coverage\Subscriber;
use Codeception\Configuration;
use Codeception\Coverage\Filter;
use Codeception\Event\PrintResultEvent;
use Codeception\Events;
use Codeception\Subscriber\Shared\StaticEvents;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
class Printer implements EventSubscriberInterface
{
use StaticEvents;
public static $events = [
Events::RESULT_PRINT_AFTER => 'printResult'
];
protected $settings = [
'enabled' => true,
'low_limit' => '35',
'high_limit' => '70',
'show_uncovered' => false
];
public static $coverage;
protected $options;
protected $logDir;
protected $destination = [];
public function __construct($options)
{
$this->options = $options;
$this->logDir = Configuration::outputDir();
$this->settings = array_merge($this->settings, Configuration::config()['coverage']);
self::$coverage = new \SebastianBergmann\CodeCoverage\CodeCoverage();
// Apply filter
$filter = new Filter(self::$coverage);
$filter
->whiteList(Configuration::config())
->blackList(Configuration::config());
}
protected function absolutePath($path)
{
if ((strpos($path, '/') === 0) || (strpos($path, ':') === 1)) { // absolute path
return $path;
}
return $this->logDir . $path;
}
public function printResult(PrintResultEvent $e)
{
$printer = $e->getPrinter();
if (!$this->settings['enabled']) {
$printer->write("\nCodeCoverage is disabled in `codeception.yml` config\n");
return;
}
if (!$this->options['quiet']) {
$this->printConsole($printer);
}
$printer->write("Remote CodeCoverage reports are not printed to console\n");
$this->printPHP();
$printer->write("\n");
if ($this->options['coverage-html']) {
$this->printHtml();
$printer->write("HTML report generated in {$this->options['coverage-html']}\n");
}
if ($this->options['coverage-xml']) {
$this->printXml();
$printer->write("XML report generated in {$this->options['coverage-xml']}\n");
}
if ($this->options['coverage-text']) {
$this->printText();
$printer->write("Text report generated in {$this->options['coverage-text']}\n");
}
if ($this->options['coverage-crap4j']) {
$this->printCrap4j();
$printer->write("Crap4j report generated in {$this->options['coverage-crap4j']}\n");
}
if ($this->options['coverage-phpunit']) {
$this->printPHPUnit();
$printer->write("PHPUnit report generated in {$this->options['coverage-phpunit']}\n");
}
}
protected function printConsole(\PHPUnit\Util\Printer $printer)
{
$writer = new \SebastianBergmann\CodeCoverage\Report\Text(
$this->settings['low_limit'],
$this->settings['high_limit'],
$this->settings['show_uncovered'],
false
);
$printer->write($writer->process(self::$coverage, $this->options['colors']));
}
protected function printHtml()
{
$writer = new \SebastianBergmann\CodeCoverage\Report\Html\Facade(
$this->settings['low_limit'],
$this->settings['high_limit'],
sprintf(
', <a href="http://codeception.com">Codeception</a> and <a href="http://phpunit.de/">PHPUnit %s</a>',
\PHPUnit\Runner\Version::id()
)
);
$writer->process(self::$coverage, $this->absolutePath($this->options['coverage-html']));
}
protected function printXml()
{
$writer = new \SebastianBergmann\CodeCoverage\Report\Clover();
$writer->process(self::$coverage, $this->absolutePath($this->options['coverage-xml']));
}
protected function printPHP()
{
$writer = new \SebastianBergmann\CodeCoverage\Report\PHP;
$writer->process(self::$coverage, $this->absolutePath($this->options['coverage']));
}
protected function printText()
{
$writer = new \SebastianBergmann\CodeCoverage\Report\Text(
$this->settings['low_limit'],
$this->settings['high_limit'],
$this->settings['show_uncovered'],
false
);
file_put_contents(
$this->absolutePath($this->options['coverage-text']),
$writer->process(self::$coverage, false)
);
}
protected function printCrap4j()
{
$writer = new \SebastianBergmann\CodeCoverage\Report\Crap4j;
$writer->process(self::$coverage, $this->absolutePath($this->options['coverage-crap4j']));
}
protected function printPHPUnit()
{
$writer = new \SebastianBergmann\CodeCoverage\Report\Xml\Facade(\PHPUnit\Runner\Version::id());
$writer->process(self::$coverage, $this->absolutePath($this->options['coverage-phpunit']));
}
}

View File

@@ -0,0 +1,90 @@
<?php
namespace Codeception\Coverage\Subscriber;
use Codeception\Configuration;
use Codeception\Event\SuiteEvent;
use Codeception\Util\FileSystem;
/**
* When collecting code coverage on remote server
* data is retrieved over HTTP and not merged with the local code coverage results.
*
* Class RemoteServer
* @package Codeception\Coverage\Subscriber
*/
class RemoteServer extends LocalServer
{
public function isEnabled()
{
return $this->module and $this->settings['remote'] and $this->settings['enabled'];
}
public function afterSuite(SuiteEvent $e)
{
if (!$this->isEnabled()) {
return;
}
$suite = strtr($e->getSuite()->getName(), ['\\' => '.']);
if ($this->options['coverage-xml']) {
$this->retrieveAndPrintXml($suite);
}
if ($this->options['coverage-html']) {
$this->retrieveAndPrintHtml($suite);
}
if ($this->options['coverage-crap4j']) {
$this->retrieveAndPrintCrap4j($suite);
}
if ($this->options['coverage-phpunit']) {
$this->retrieveAndPrintPHPUnit($suite);
}
}
protected function retrieveAndPrintHtml($suite)
{
$tempFile = tempnam(sys_get_temp_dir(), 'C3') . '.tar';
file_put_contents($tempFile, $this->c3Request('html'));
$destDir = Configuration::outputDir() . $suite . '.remote.coverage';
if (is_dir($destDir)) {
FileSystem::doEmptyDir($destDir);
} else {
mkdir($destDir, 0777, true);
}
$phar = new \PharData($tempFile);
$phar->extractTo($destDir);
unlink($tempFile);
}
protected function retrieveAndPrintXml($suite)
{
$destFile = Configuration::outputDir() . $suite . '.remote.coverage.xml';
file_put_contents($destFile, $this->c3Request('clover'));
}
protected function retrieveAndPrintCrap4j($suite)
{
$destFile = Configuration::outputDir() . $suite . '.remote.crap4j.xml';
file_put_contents($destFile, $this->c3Request('crap4j'));
}
protected function retrieveAndPrintPHPUnit($suite)
{
$tempFile = tempnam(sys_get_temp_dir(), 'C3') . '.tar';
file_put_contents($tempFile, $this->c3Request('phpunit'));
$destDir = Configuration::outputDir() . $suite . '.remote.coverage-phpunit';
if (is_dir($destDir)) {
FileSystem::doEmptyDir($destDir);
} else {
mkdir($destDir, 0777, true);
}
$phar = new \PharData($tempFile);
$phar->extractTo($destDir);
unlink($tempFile);
}
}

View File

@@ -0,0 +1,95 @@
<?php
namespace Codeception\Coverage;
use Codeception\Configuration;
use Codeception\Coverage\Subscriber\Printer;
use Codeception\Lib\Interfaces\Remote;
use Codeception\Stub;
use Codeception\Subscriber\Shared\StaticEvents;
use PHPUnit\Framework\CodeCoverageException;
use SebastianBergmann\CodeCoverage\CodeCoverage;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
abstract class SuiteSubscriber implements EventSubscriberInterface
{
use StaticEvents;
protected $defaultSettings = [
'enabled' => false,
'remote' => false,
'local' => false,
'xdebug_session' => 'codeception',
'remote_config' => null,
'show_uncovered' => false,
'c3_url' => null
];
protected $settings = [];
protected $filters = [];
protected $modules = [];
protected $coverage;
protected $logDir;
protected $options;
public static $events = [];
abstract protected function isEnabled();
public function __construct($options = [])
{
$this->options = $options;
$this->logDir = Configuration::outputDir();
}
protected function applySettings($settings)
{
try {
$this->coverage = new \SebastianBergmann\CodeCoverage\CodeCoverage();
} catch (CodeCoverageException $e) {
throw new \Exception(
'XDebug is required to collect CodeCoverage. Please install xdebug extension and enable it in php.ini'
);
}
$this->filters = $settings;
$this->settings = $this->defaultSettings;
$keys = array_keys($this->defaultSettings);
foreach ($keys as $key) {
if (isset($settings['coverage'][$key])) {
$this->settings[$key] = $settings['coverage'][$key];
}
}
$this->coverage->setProcessUncoveredFilesFromWhitelist($this->settings['show_uncovered']);
}
/**
* @param array $modules
* @return \Codeception\Lib\Interfaces\Remote|null
*/
protected function getServerConnectionModule(array $modules)
{
foreach ($modules as $module) {
if ($module instanceof Remote) {
return $module;
}
}
return null;
}
public function applyFilter(\PHPUnit\Framework\TestResult $result)
{
$driver = Stub::makeEmpty('SebastianBergmann\CodeCoverage\Driver\Driver');
$result->setCodeCoverage(new CodeCoverage($driver));
Filter::setup($this->coverage)
->whiteList($this->filters)
->blackList($this->filters);
$result->setCodeCoverage($this->coverage);
}
protected function mergeToPrint($coverage)
{
Printer::$coverage->merge($coverage);
}
}