Refactored project structure.

This commit is contained in:
Dmitry Khomutov 2018-03-04 18:04:15 +07:00
commit c015d8c58b
No known key found for this signature in database
GPG key ID: EC19426474B37AAC
308 changed files with 39 additions and 47 deletions

109
src/Plugin/Atoum.php Normal file
View file

@ -0,0 +1,109 @@
<?php
namespace PHPCensor\Plugin;
use PHPCensor\Builder;
use PHPCensor\Model\Build;
use PHPCensor\Plugin;
/**
* Atoum plugin, runs Atoum tests within a project.
*/
class Atoum extends Plugin
{
/**
* @var string
*/
protected $executable;
/**
* @var array
*/
protected $args;
/**
* @var array
*/
protected $config;
/**
* @var string
*/
protected $directory;
/**
* @return string
*/
public static function pluginName()
{
return 'atoum';
}
/**
* {@inheritdoc}
*/
public function __construct(Builder $builder, Build $build, array $options = [])
{
parent::__construct($builder, $build, $options);
if (isset($options['executable'])) {
$this->executable = $this->builder->buildPath . DIRECTORY_SEPARATOR.$options['executable'];
} else {
$this->executable = $this->findBinary('atoum');
}
if (isset($options['args'])) {
$this->args = $options['args'];
}
if (isset($options['config'])) {
$this->config = $options['config'];
}
if (isset($options['directory'])) {
$this->directory = $options['directory'];
}
}
/**
* Run the Atoum plugin.
*
* @return bool
*/
public function execute()
{
$cmd = $this->executable;
if ($this->args !== null) {
$cmd .= " {$this->args}";
}
if ($this->config !== null) {
$cmd .= " -c '{$this->config}'";
}
if ($this->directory !== null) {
$dirPath = $this->builder->buildPath . DIRECTORY_SEPARATOR . $this->directory;
$cmd .= " -d '{$dirPath}'";
}
chdir($this->builder->buildPath);
$output = '';
$status = true;
exec($cmd, $output);
if (count(preg_grep("/Success \(/", $output)) == 0) {
$status = false;
$this->builder->log($output);
}
if (count($output) == 0) {
$status = false;
$this->builder->log('No tests have been performed.');
}
return $status;
}
}

128
src/Plugin/Behat.php Normal file
View file

@ -0,0 +1,128 @@
<?php
namespace PHPCensor\Plugin;
use PHPCensor\Builder;
use PHPCensor\Model\Build;
use PHPCensor\Model\BuildError;
use PHPCensor\Plugin;
/**
* Behat BDD Plugin
*
* @author Dan Cryer <dan@block8.co.uk>
*/
class Behat extends Plugin
{
protected $features;
protected $executable;
/**
* @return string
*/
public static function pluginName()
{
return 'behat';
}
/**
* {@inheritdoc}
*/
public function __construct(Builder $builder, Build $build, array $options = [])
{
parent::__construct($builder, $build, $options);
$this->features = '';
if (isset($options['executable'])) {
$this->executable = $options['executable'];
} else {
$this->executable = $this->findBinary('behat');
}
if (!empty($options['features'])) {
$this->features = $options['features'];
}
}
/**
* Runs Behat tests.
*/
public function execute()
{
$current_dir = getcwd();
chdir($this->builder->buildPath);
$behat = $this->executable;
if (!$behat) {
$this->builder->logFailure(sprintf('Could not find %s', 'behat'));
return false;
}
$success = $this->builder->executeCommand($behat . ' %s', $this->features);
chdir($current_dir);
list($errorCount, $data) = $this->parseBehatOutput();
$this->build->storeMeta('behat-warnings', $errorCount);
$this->build->storeMeta('behat-data', $data);
return $success;
}
/**
* Parse the behat output and return details on failures
*
* @return array
*/
public function parseBehatOutput()
{
$output = $this->builder->getLastOutput();
$parts = explode('---', $output);
if (count($parts) <= 1) {
return [0, []];
}
$lines = explode(PHP_EOL, $parts[1]);
$storeFailures = false;
$data = [];
foreach ($lines as $line) {
$line = trim($line);
if ($line == 'Failed scenarios:') {
$storeFailures = true;
continue;
}
if (strpos($line, ':') === false) {
$storeFailures = false;
}
if ($storeFailures) {
$lineParts = explode(':', $line);
$data[] = [
'file' => $lineParts[0],
'line' => $lineParts[1]
];
$this->build->reportError(
$this->builder,
'behat',
'Behat scenario failed.',
BuildError::SEVERITY_HIGH,
$lineParts[0],
$lineParts[1]
);
}
}
$errorCount = count($data);
return [$errorCount, $data];
}
}

151
src/Plugin/Campfire.php Normal file
View file

@ -0,0 +1,151 @@
<?php
namespace PHPCensor\Plugin;
use PHPCensor\Builder;
use PHPCensor\Model\Build;
use PHPCensor\Plugin;
/**
* Campfire Plugin - Allows Campfire API actions. Strongly based on icecube (http://labs.mimmin.com/icecube)
*
* @author André Cianfarani <acianfa@gmail.com>
*/
class Campfire extends Plugin
{
protected $url;
protected $authToken;
protected $userAgent;
protected $cookie;
protected $verbose;
protected $roomId;
protected $message;
/**
* @return string
*/
public static function pluginName()
{
return 'campfire';
}
/**
* {@inheritdoc}
*/
public function __construct(Builder $builder, Build $build, array $options = [])
{
parent::__construct($builder, $build, $options);
$this->message = $options['message'];
$this->userAgent = "PHP Censor/1.0";
$this->cookie = "php-censor-cookie";
$buildSettings = $this->builder->getConfig('build_settings');
if (isset($buildSettings['campfire'])) {
$campfire = $buildSettings['campfire'];
$this->url = $campfire['url'];
$this->authToken = $campfire['authToken'];
$this->roomId = $campfire['roomId'];
} else {
throw new \Exception('No connection parameters given for Campfire plugin');
}
}
/**
* Run the Campfire plugin.
* @return bool|mixed
*/
public function execute()
{
$url = APP_URL . "build/view/" . $this->build->getId();
$message = str_replace("%buildurl%", $url, $this->message);
$this->joinRoom($this->roomId);
$status = $this->speak($message, $this->roomId);
$this->leaveRoom($this->roomId);
return $status;
}
/**
* Join a Campfire room.
* @param $roomId
*/
public function joinRoom($roomId)
{
$this->getPageByPost('/room/'.$roomId.'/join.json');
}
/**
* Leave a Campfire room.
* @param $roomId
*/
public function leaveRoom($roomId)
{
$this->getPageByPost('/room/'.$roomId.'/leave.json');
}
/**
* Send a message to a campfire room.
* @param $message
* @param $roomId
* @param bool $isPaste
* @return bool|mixed
*/
public function speak($message, $roomId, $isPaste = false)
{
$page = '/room/'.$roomId.'/speak.json';
if ($isPaste) {
$type = 'PasteMessage';
} else {
$type = 'TextMessage';
}
return $this->getPageByPost($page, ['message' => ['type' => $type, 'body' => $message]]);
}
/**
* Make a request to Campfire.
* @param $page
* @param null $data
* @return bool|mixed
*/
private function getPageByPost($page, $data = null)
{
$url = $this->url . $page;
// The new API allows JSON, so we can pass
// PHP data structures instead of old school POST
$json = json_encode($data);
// cURL init & config
$handle = curl_init();
curl_setopt($handle, CURLOPT_URL, $url);
curl_setopt($handle, CURLOPT_RETURNTRANSFER, true);
curl_setopt($handle, CURLOPT_POST, 1);
curl_setopt($handle, CURLOPT_USERAGENT, $this->userAgent);
curl_setopt($handle, CURLOPT_VERBOSE, $this->verbose);
curl_setopt($handle, CURLOPT_FOLLOWLOCATION, 1);
curl_setopt($handle, CURLOPT_USERPWD, $this->authToken . ':x');
curl_setopt($handle, CURLOPT_HTTPHEADER, ["Content-type: application/json"]);
curl_setopt($handle, CURLOPT_COOKIEFILE, $this->cookie);
curl_setopt($handle, CURLOPT_POSTFIELDS, $json);
$output = curl_exec($handle);
curl_close($handle);
// We tend to get one space with an otherwise blank response
$output = trim($output);
if (strlen($output)) {
/* Responses are JSON. Decode it to a data structure */
return json_decode($output);
}
// Simple 200 OK response (such as for joining a room)
return true;
}
}

59
src/Plugin/CleanBuild.php Normal file
View file

@ -0,0 +1,59 @@
<?php
namespace PHPCensor\Plugin;
use PHPCensor\Builder;
use PHPCensor\Model\Build;
use PHPCensor\Plugin;
/**
* Clean build removes Composer related files and allows users to clean up their build directory.
* Useful as a precursor to copy_build.
*
* @author Dan Cryer <dan@block8.co.uk>
*/
class CleanBuild extends Plugin
{
protected $remove;
/**
* @return string
*/
public static function pluginName()
{
return 'clean_build';
}
/**
* {@inheritdoc}
*/
public function __construct(Builder $builder, Build $build, array $options = [])
{
parent::__construct($builder, $build, $options);
$this->remove = isset($options['remove']) && is_array($options['remove']) ? $options['remove'] : [];
}
/**
* Executes Composer and runs a specified command (e.g. install / update)
*/
public function execute()
{
$cmd = 'rm -Rf "%s"';
$this->builder->executeCommand($cmd, $this->builder->buildPath . 'composer.phar');
$this->builder->executeCommand($cmd, $this->builder->buildPath . 'composer.lock');
$success = true;
foreach ($this->remove as $file) {
$ok = $this->builder->executeCommand($cmd, $this->builder->buildPath . $file);
if (!$ok) {
$success = false;
}
}
return $success;
}
}

164
src/Plugin/Codeception.php Normal file
View file

@ -0,0 +1,164 @@
<?php
namespace PHPCensor\Plugin;
use PHPCensor\Builder;
use PHPCensor\Model\Build;
use PHPCensor\Plugin\Util\TestResultParsers\Codeception as Parser;
use PHPCensor\Plugin;
use Symfony\Component\Yaml\Parser as YamlParser;
use PHPCensor\ZeroConfigPluginInterface;
/**
* Codeception Plugin - Enables full acceptance, unit, and functional testing.
*
* @author Don Gilbert <don@dongilbert.net>
* @author Igor Timoshenko <contact@igortimoshenko.com>
* @author Adam Cooper <adam@networkpie.co.uk>
*/
class Codeception extends Plugin implements ZeroConfigPluginInterface
{
/** @var string */
protected $args = '';
/**
* @var string $ymlConfigFile The path of a yml config for Codeception
*/
protected $ymlConfigFile;
/**
* @var array $path The path to the codeception tests folder.
*/
protected $path = [
'tests/_output',
'tests/_log',
];
/**
* @return string
*/
public static function pluginName()
{
return 'codeception';
}
/**
* {@inheritdoc}
*/
public function __construct(Builder $builder, Build $build, array $options = [])
{
parent::__construct($builder, $build, $options);
if (empty($options['config'])) {
$this->ymlConfigFile = self::findConfigFile($this->builder->buildPath);
} else {
$this->ymlConfigFile = $options['config'];
}
if (isset($options['args'])) {
$this->args = (string) $options['args'];
}
if (isset($options['path'])) {
array_unshift($this->path, $options['path']);
}
}
/**
* @param $stage
* @param Builder $builder
* @param Build $build
* @return bool
*/
public static function canExecute($stage, Builder $builder, Build $build)
{
return $stage == Build::STAGE_TEST && !is_null(self::findConfigFile($builder->buildPath));
}
/**
* Try and find the codeception YML config file.
* @param $buildPath
* @return null|string
*/
public static function findConfigFile($buildPath)
{
if (file_exists($buildPath . 'codeception.yml')) {
return 'codeception.yml';
}
if (file_exists($buildPath . 'codeception.dist.yml')) {
return 'codeception.dist.yml';
}
return null;
}
/**
* Runs Codeception tests
*/
public function execute()
{
if (empty($this->ymlConfigFile)) {
throw new \Exception("No configuration file found");
}
// Run any config files first. This can be either a single value or an array.
return $this->runConfigFile($this->ymlConfigFile);
}
/**
* Run tests from a Codeception config file.
* @param $configPath
* @return bool|mixed
* @throws \Exception
*/
protected function runConfigFile($configPath)
{
$codeception = $this->findBinary('codecept');
if (!$codeception) {
$this->builder->logFailure(sprintf('Could not find %s', 'codecept'));
return false;
}
$cmd = 'cd "%s" && ' . $codeception . ' run -c "%s" --xml ' . $this->args;
$configPath = $this->builder->buildPath . $configPath;
$success = $this->builder->executeCommand($cmd, $this->builder->buildPath, $configPath);
$parser = new YamlParser();
$yaml = file_get_contents($configPath);
$config = (array)$parser->parse($yaml);
$outputPath = null;
if ($config && isset($config['paths']['log'])) {
$outputPath = $this->builder->buildPath . $config['paths']['log'] . '/';
}
if (!file_exists($outputPath . 'report.xml')) {
foreach ($this->path as $path) {
$outputPath = $this->builder->buildPath . rtrim($path, '/\\') . '/';
if (file_exists($outputPath . 'report.xml')) {
break;
}
}
}
$xml = file_get_contents($outputPath . 'report.xml', false);
$parser = new Parser($this->builder, $xml);
$output = $parser->parse();
$meta = [
'tests' => $parser->getTotalTests(),
'timetaken' => $parser->getTotalTimeTaken(),
'failures' => $parser->getTotalFailures()
];
$this->build->storeMeta('codeception-meta', $meta);
$this->build->storeMeta('codeception-data', $output);
$this->build->storeMeta('codeception-errors', $parser->getTotalFailures());
return $success;
}
}

124
src/Plugin/Composer.php Normal file
View file

@ -0,0 +1,124 @@
<?php
namespace PHPCensor\Plugin;
use PHPCensor;
use PHPCensor\Builder;
use PHPCensor\Model\Build;
use PHPCensor\Plugin;
use PHPCensor\ZeroConfigPluginInterface;
/**
* Composer Plugin - Provides access to Composer functionality.
*
* @author Dan Cryer <dan@block8.co.uk>
*/
class Composer extends Plugin implements ZeroConfigPluginInterface
{
protected $directory;
protected $action;
protected $preferDist;
protected $noDev;
protected $ignorePlatformReqs;
protected $preferSource;
/**
* @return string
*/
public static function pluginName()
{
return 'composer';
}
/**
* {@inheritdoc}
*/
public function __construct(Builder $builder, Build $build, array $options = [])
{
parent::__construct($builder, $build, $options);
$path = $this->builder->buildPath;
$this->directory = $path;
$this->action = 'install';
$this->preferDist = false;
$this->preferSource = false;
$this->noDev = false;
$this->ignorePlatformReqs = false;
if (array_key_exists('directory', $options)) {
$this->directory = $path . DIRECTORY_SEPARATOR . $options['directory'];
}
if (array_key_exists('action', $options)) {
$this->action = $options['action'];
}
if (array_key_exists('prefer_dist', $options)) {
$this->preferDist = (bool)$options['prefer_dist'];
}
if (array_key_exists('prefer_source', $options)) {
$this->preferDist = false;
$this->preferSource = (bool)$options['prefer_source'];
}
if (array_key_exists('no_dev', $options)) {
$this->noDev = (bool)$options['no_dev'];
}
if (array_key_exists('ignore_platform_reqs', $options)) {
$this->ignorePlatformReqs = (bool)$options['ignore_platform_reqs'];
}
}
/**
* Check if this plugin can be executed.
* @param $stage
* @param Builder $builder
* @param Build $build
* @return bool
*/
public static function canExecute($stage, Builder $builder, Build $build)
{
$path = $builder->buildPath . DIRECTORY_SEPARATOR . 'composer.json';
if (file_exists($path) && $stage == Build::STAGE_SETUP) {
return true;
}
return false;
}
/**
* Executes Composer and runs a specified command (e.g. install / update)
*/
public function execute()
{
$composerLocation = $this->findBinary(['composer', 'composer.phar']);
$cmd = $composerLocation . ' --no-ansi --no-interaction ';
if ($this->preferDist) {
$this->builder->log('Using --prefer-dist flag');
$cmd .= ' --prefer-dist';
}
if ($this->preferSource) {
$this->builder->log('Using --prefer-source flag');
$cmd .= ' --prefer-source';
}
if ($this->noDev) {
$this->builder->log('Using --no-dev flag');
$cmd .= ' --no-dev';
}
if ($this->ignorePlatformReqs) {
$this->builder->log('Using --ignore-platform-reqs flag');
$cmd .= ' --ignore-platform-reqs';
}
$cmd .= ' --working-dir="%s" %s';
return $this->builder->executeCommand($cmd, $this->directory, $this->action);
}
}

91
src/Plugin/CopyBuild.php Normal file
View file

@ -0,0 +1,91 @@
<?php
namespace PHPCensor\Plugin;
use PHPCensor\Builder;
use PHPCensor\Model\Build;
use PHPCensor\Plugin;
/**
* Copy Build Plugin - Copies the entire build to another directory.
*
* @author Dan Cryer <dan@block8.co.uk>
*/
class CopyBuild extends Plugin
{
protected $directory;
protected $ignore;
protected $wipe;
/**
* @return string
*/
public static function pluginName()
{
return 'copy_build';
}
/**
* {@inheritdoc}
*/
public function __construct(Builder $builder, Build $build, array $options = [])
{
parent::__construct($builder, $build, $options);
$path = $this->builder->buildPath;
$this->directory = isset($options['directory']) ? $options['directory'] : $path;
$this->wipe = isset($options['wipe']) ? (bool)$options['wipe'] : false;
$this->ignore = isset($options['respect_ignore']) ? (bool)$options['respect_ignore'] : false;
}
/**
* Copies files from the root of the build directory into the target folder
*/
public function execute()
{
$build = $this->builder->buildPath;
if ($this->directory == $build) {
return false;
}
$this->wipeExistingDirectory();
$cmd = 'mkdir -p "%s" && cp -R "%s" "%s"';
$success = $this->builder->executeCommand($cmd, $this->directory, $build, $this->directory);
$this->deleteIgnoredFiles();
return $success;
}
/**
* Wipe the destination directory if it already exists.
* @throws \Exception
*/
protected function wipeExistingDirectory()
{
if ($this->wipe === true && $this->directory != '/' && is_dir($this->directory)) {
$cmd = 'rm -Rf "%s*"';
$success = $this->builder->executeCommand($cmd, $this->directory);
if (!$success) {
throw new \Exception(sprintf('Failed to wipe existing directory %s before copy', $this->directory));
}
}
}
/**
* Delete any ignored files from the build prior to copying.
*/
protected function deleteIgnoredFiles()
{
if ($this->ignore) {
foreach ($this->builder->ignore as $file) {
$cmd = 'rm -Rf "%s/%s"';
$this->builder->executeCommand($cmd, $this->directory, $file);
}
}
}
}

81
src/Plugin/Deployer.php Normal file
View file

@ -0,0 +1,81 @@
<?php
namespace PHPCensor\Plugin;
use GuzzleHttp\Client;
use PHPCensor\Builder;
use PHPCensor\Model\Build;
use PHPCensor\Plugin;
/**
* Integration with Deployer: https://github.com/rebelinblue/deployer
*
* @author Dan Cryer <dan@block8.co.uk>
*/
class Deployer extends Plugin
{
protected $webhookUrl;
protected $reason;
protected $updateOnly;
/**
* @return string
*/
public static function pluginName()
{
return 'deployer';
}
/**
* {@inheritdoc}
*/
public function __construct(Builder $builder, Build $build, array $options = [])
{
parent::__construct($builder, $build, $options);
$this->reason = 'PHP Censor Build #%BUILD% - %COMMIT_MESSAGE%';
if (isset($options['webhook_url'])) {
$this->webhookUrl = $options['webhook_url'];
}
if (isset($options['reason'])) {
$this->reason = $options['reason'];
}
$this->updateOnly = isset($options['update_only']) ? (bool) $options['update_only'] : true;
}
/**
* Copies files from the root of the build directory into the target folder
*/
public function execute()
{
if (empty($this->webhookUrl)) {
$this->builder->logFailure('You must specify a webhook URL.');
return false;
}
$client = new Client();
$response = $client->post(
$this->webhookUrl,
[
'form_params' => [
'reason' => $this->builder->interpolate($this->reason),
'source' => 'PHP Censor',
'url' => $this->builder->interpolate('%BUILD_URI%'),
'branch' => $this->builder->interpolate('%BRANCH%'),
'commit' => $this->builder->interpolate('%COMMIT%'),
'update_only' => $this->updateOnly,
]
]
);
$status = (integer)$response->getStatusCode();
return (
($status >= 200 && $status < 300)
? true
: false
);
}
}

214
src/Plugin/Email.php Normal file
View file

@ -0,0 +1,214 @@
<?php
namespace PHPCensor\Plugin;
use PHPCensor\View;
use PHPCensor\Helper\Email as EmailHelper;
use Psr\Log\LogLevel;
use PHPCensor\Plugin;
/**
* Email Plugin - Provides simple email capability.
*
* @author Steve Brazier <meadsteve@gmail.com>
*/
class Email extends Plugin
{
/**
* @return string
*/
public static function pluginName()
{
return 'email';
}
/**
* Send a notification mail.
*
* @return boolean
*/
public function execute()
{
$addresses = $this->getEmailAddresses();
// Without some email addresses in the yml file then we
// can't do anything.
if (count($addresses) == 0) {
return false;
}
$buildStatus = $this->build->isSuccessful() ? "Passing Build" : "Failing Build";
$projectName = $this->build->getProject()->getTitle();
try {
$view = $this->getMailTemplate();
} catch (\RuntimeException $e) {
$this->builder->log(
sprintf('Unknown mail template "%s", falling back to default.', $this->options['template']),
LogLevel::WARNING
);
$view = $this->getDefaultMailTemplate();
}
$view->build = $this->build;
$view->project = $this->build->getProject();
$layout = new View('Email/layout');
$layout->build = $this->build;
$layout->project = $this->build->getProject();
$layout->content = $view->render();
$body = $layout->render();
$sendFailures = $this->sendSeparateEmails(
$addresses,
sprintf("PHP Censor - %s - %s", $projectName, $buildStatus),
$body
);
// This is a success if we've not failed to send anything.
$this->builder->log(sprintf('%d emails sent.', (count($addresses) - $sendFailures)));
$this->builder->log(sprintf('%d emails failed to send.', $sendFailures));
return ($sendFailures === 0);
}
/**
* @param string $toAddress Single address to send to
* @param string[] $ccList
* @param string $subject Email subject
* @param string $body Email body
*
* @return integer
*/
protected function sendEmail($toAddress, $ccList, $subject, $body)
{
$email = new EmailHelper();
$email->setEmailTo($toAddress, $toAddress);
$email->setSubject($subject);
$email->setBody($body);
$email->setHtml(true);
if (is_array($ccList) && count($ccList)) {
foreach ($ccList as $address) {
$email->addCc($address, $address);
}
}
return $email->send($this->builder);
}
/**
* Send an email to a list of specified subjects.
*
* @param array $toAddresses
* List of destination addresses for message.
* @param string $subject
* Mail subject
* @param string $body
* Mail body
*
* @return int number of failed messages
*/
public function sendSeparateEmails(array $toAddresses, $subject, $body)
{
$failures = 0;
$ccList = $this->getCcAddresses();
foreach ($toAddresses as $address) {
if (!$this->sendEmail($address, $ccList, $subject, $body)) {
$failures++;
}
}
return $failures;
}
/**
* Get the list of email addresses to send to.
* @return array
*/
protected function getEmailAddresses()
{
$addresses = [];
$committer = $this->build->getCommitterEmail();
$this->builder->logDebug(sprintf("Committer email: '%s'", $committer));
$this->builder->logDebug(sprintf(
"Committer option: '%s'",
(!empty($this->options['committer']) && $this->options['committer']) ? 'true' : 'false'
));
if (!empty($this->options['committer']) && $this->options['committer']) {
if ($committer) {
$addresses[] = $committer;
}
}
$this->builder->logDebug(sprintf(
"Addresses option: '%s'",
(!empty($this->options['addresses']) && is_array($this->options['addresses'])) ? implode(', ', $this->options['addresses']) : 'false'
));
if (!empty($this->options['addresses']) && is_array($this->options['addresses'])) {
foreach ($this->options['addresses'] as $address) {
$addresses[] = $address;
}
}
$this->builder->logDebug(sprintf(
"Default mailTo option: '%s'",
!empty($this->options['default_mailto_address']) ? $this->options['default_mailto_address'] : 'false'
));
if (empty($addresses) && !empty($this->options['default_mailto_address'])) {
$addresses[] = $this->options['default_mailto_address'];
}
return array_unique($addresses);
}
/**
* Get the list of email addresses to CC.
*
* @return array
*/
protected function getCcAddresses()
{
$ccAddresses = [];
if (isset($this->options['cc'])) {
foreach ($this->options['cc'] as $address) {
$ccAddresses[] = $address;
}
}
return $ccAddresses;
}
/**
* Get the mail template used to sent the mail.
*
* @return View
*/
protected function getMailTemplate()
{
if (isset($this->options['template'])) {
return new View('Email/' . $this->options['template']);
}
return $this->getDefaultMailTemplate();
}
/**
* Get the default mail template.
*
* @return View
*/
protected function getDefaultMailTemplate()
{
$template = $this->build->isSuccessful() ? 'short' : 'long';
return new View('Email/' . $template);
}
}

44
src/Plugin/Env.php Normal file
View file

@ -0,0 +1,44 @@
<?php
namespace PHPCensor\Plugin;
use PHPCensor\Plugin;
/**
* Environment variable plugin
*
* @author Steve Kamerman <stevekamerman@gmail.com>
*/
class Env extends Plugin
{
/**
* @return string
*/
public static function pluginName()
{
return 'env';
}
/**
* Adds the specified environment variables to the builder environment
*/
public function execute()
{
$success = true;
foreach ($this->options as $key => $value) {
if (is_numeric($key)) {
// This allows the developer to specify env vars like " - FOO=bar" or " - FOO: bar"
$envVar = is_array($value)? key($value).'='.current($value): $value;
} else {
// This allows the standard syntax: "FOO: bar"
$envVar = "$key=$value";
}
if (!putenv($this->builder->interpolate($envVar))) {
$success = false;
$this->builder->logFailure('Unable to set environment variable');
}
}
return $success;
}
}

View file

@ -0,0 +1,73 @@
<?php
namespace PHPCensor\Plugin;
use PHPCensor\Builder;
use PHPCensor\Model\Build;
use Mremi\Flowdock\Api\Push\Push;
use Mremi\Flowdock\Api\Push\TeamInboxMessage;
use PHPCensor\Plugin;
/**
* Flowdock Plugin
*
* @author Petr Cervenka <petr@nanosolutions.io>
*/
class FlowdockNotify extends Plugin
{
protected $apiKey;
protected $email;
protected $message;
const MESSAGE_DEFAULT = 'Build %BUILD% has finished for commit <a href="%COMMIT_URI%">%SHORT_COMMIT%</a>
(%COMMIT_EMAIL%)> on branch <a href="%BRANCH_URI%">%BRANCH%</a>';
/**
* @return string
*/
public static function pluginName()
{
return 'flowdock_notify';
}
/**
* {@inheritdoc}
*/
public function __construct(Builder $builder, Build $build, array $options = [])
{
parent::__construct($builder, $build, $options);
if (!is_array($options) || !isset($options['api_key'])) {
throw new \Exception('Please define the api_key for Flowdock Notify plugin!');
}
$this->apiKey = trim($options['api_key']);
$this->message = isset($options['message']) ? $options['message'] : self::MESSAGE_DEFAULT;
$this->email = isset($options['email']) ? $options['email'] : 'PHP Censor';
}
/**
* Run the Flowdock plugin.
* @return bool
* @throws \Exception
*/
public function execute()
{
$message = $this->builder->interpolate($this->message);
$successfulBuild = $this->build->isSuccessful() ? 'Success' : 'Failed';
$push = new Push($this->apiKey);
$flowMessage = TeamInboxMessage::create()
->setSource("PHPCensor")
->setFromAddress($this->email)
->setFromName($this->build->getProject()->getTitle())
->setSubject($successfulBuild)
->setTags(['#ci'])
->setLink($this->build->getBranchLink())
->setContent($message);
if (!$push->sendTeamInboxMessage($flowMessage, ['connect_timeout' => 5000, 'timeout' => 5000])) {
throw new \Exception(sprintf('Flowdock Failed: %s', $flowMessage->getResponseErrors()));
}
return true;
}
}

157
src/Plugin/Git.php Normal file
View file

@ -0,0 +1,157 @@
<?php
namespace PHPCensor\Plugin;
use PHPCensor\Plugin;
/**
* Git plugin.
*
* @author Dan Cryer <dan@block8.co.uk>
*/
class Git extends Plugin
{
protected $actions = [];
/**
* @return string
*/
public static function pluginName()
{
return 'git';
}
/**
* Run the Git plugin.
* @return bool
*/
public function execute()
{
$buildPath = $this->builder->buildPath;
// Check if there are any actions to be run for the branch we're running on:
if (!array_key_exists($this->build->getBranch(), $this->actions)) {
return true;
}
// If there are, run them:
$curdir = getcwd();
chdir($buildPath);
$success = true;
foreach ($this->actions[$this->build->getBranch()] as $action => $options) {
if (!$this->runAction($action, $options)) {
$success = false;
break;
}
}
chdir($curdir);
return $success;
}
/**
* Determine which action to run, and run it.
* @param $action
* @param array $options
* @return bool
*/
protected function runAction($action, array $options = [])
{
switch ($action) {
case 'merge':
return $this->runMergeAction($options);
case 'tag':
return $this->runTagAction($options);
case 'pull':
return $this->runPullAction($options);
case 'push':
return $this->runPushAction($options);
}
return false;
}
/**
* Handle a merge action.
* @param $options
* @return bool
*/
protected function runMergeAction($options)
{
if (array_key_exists('branch', $options)) {
$cmd = 'cd "%s" && git checkout %s && git merge "%s"';
$path = $this->builder->buildPath;
return $this->builder->executeCommand($cmd, $path, $options['branch'], $this->build->getBranch());
}
}
/**
* Handle a tag action.
* @param $options
* @return bool
*/
protected function runTagAction($options)
{
$tagName = date('Ymd-His');
$message = sprintf('Tag created by PHP Censor: %s', date('Y-m-d H:i:s'));
if (array_key_exists('name', $options)) {
$tagName = $this->builder->interpolate($options['name']);
}
if (array_key_exists('message', $options)) {
$message = $this->builder->interpolate($options['message']);
}
$cmd = 'git tag %s -m "%s"';
return $this->builder->executeCommand($cmd, $tagName, $message);
}
/**
* Handle a pull action.
* @param $options
* @return bool
*/
protected function runPullAction($options)
{
$branch = $this->build->getBranch();
$remote = 'origin';
if (array_key_exists('branch', $options)) {
$branch = $this->builder->interpolate($options['branch']);
}
if (array_key_exists('remote', $options)) {
$remote = $this->builder->interpolate($options['remote']);
}
return $this->builder->executeCommand('git pull %s %s', $remote, $branch);
}
/**
* Handle a push action.
* @param $options
* @return bool
*/
protected function runPushAction($options)
{
$branch = $this->build->getBranch();
$remote = 'origin';
if (array_key_exists('branch', $options)) {
$branch = $this->builder->interpolate($options['branch']);
}
if (array_key_exists('remote', $options)) {
$remote = $this->builder->interpolate($options['remote']);
}
return $this->builder->executeCommand('git push %s %s', $remote, $branch);
}
}

81
src/Plugin/Grunt.php Normal file
View file

@ -0,0 +1,81 @@
<?php
namespace PHPCensor\Plugin;
use PHPCensor\Builder;
use PHPCensor\Model\Build;
use PHPCensor\Plugin;
/**
* Grunt Plugin - Provides access to grunt functionality.
*
* @author Tobias Tom <t.tom@succont.de>
*/
class Grunt extends Plugin
{
protected $directory;
protected $task;
protected $preferDist;
protected $grunt;
protected $gruntfile;
/**
* @return string
*/
public static function pluginName()
{
return 'grunt';
}
/**
* {@inheritdoc}
*/
public function __construct(Builder $builder, Build $build, array $options = [])
{
parent::__construct($builder, $build, $options);
$path = $this->builder->buildPath;
$this->directory = $path;
$this->task = null;
$this->grunt = $this->findBinary('grunt');
$this->gruntfile = 'Gruntfile.js';
// Handle options:
if (isset($options['directory'])) {
$this->directory = $path . DIRECTORY_SEPARATOR . $options['directory'];
}
if (isset($options['task'])) {
$this->task = $options['task'];
}
if (isset($options['grunt'])) {
$this->grunt = $options['grunt'];
}
if (isset($options['gruntfile'])) {
$this->gruntfile = $options['gruntfile'];
}
}
/**
* Executes grunt and runs a specified command (e.g. install / update)
*/
public function execute()
{
// if npm does not work, we cannot use grunt, so we return false
$cmd = 'cd %s && npm install';
if (!$this->builder->executeCommand($cmd, $this->directory)) {
return false;
}
// build the grunt command
$cmd = 'cd %s && ' . $this->grunt;
$cmd .= ' --no-color';
$cmd .= ' --gruntfile %s';
$cmd .= ' %s'; // the task that will be executed
// and execute it
return $this->builder->executeCommand($cmd, $this->directory, $this->gruntfile, $this->task);
}
}

81
src/Plugin/Gulp.php Normal file
View file

@ -0,0 +1,81 @@
<?php
namespace PHPCensor\Plugin;
use PHPCensor\Builder;
use PHPCensor\Model\Build;
use PHPCensor\Plugin;
/**
* Gulp Plugin - Provides access to gulp functionality.
*
* @author Dirk Heilig <dirk@heilig-online.com>
*/
class Gulp extends Plugin
{
protected $directory;
protected $task;
protected $preferDist;
protected $gulp;
protected $gulpfile;
/**
* @return string
*/
public static function pluginName()
{
return 'gulp';
}
/**
* {@inheritdoc}
*/
public function __construct(Builder $builder, Build $build, array $options = [])
{
parent::__construct($builder, $build, $options);
$path = $this->builder->buildPath;
$this->directory = $path;
$this->task = null;
$this->gulp = $this->findBinary('gulp');
$this->gulpfile = 'gulpfile.js';
// Handle options:
if (isset($options['directory'])) {
$this->directory = $path . DIRECTORY_SEPARATOR . $options['directory'];
}
if (isset($options['task'])) {
$this->task = $options['task'];
}
if (isset($options['gulp'])) {
$this->gulp = $options['gulp'];
}
if (isset($options['gulpfile'])) {
$this->gulpfile = $options['gulpfile'];
}
}
/**
* Executes gulp and runs a specified command (e.g. install / update)
*/
public function execute()
{
// if npm does not work, we cannot use gulp, so we return false
$cmd = 'cd %s && npm install';
if (!$this->builder->executeCommand($cmd, $this->directory)) {
return false;
}
// build the gulp command
$cmd = 'cd %s && ' . $this->gulp;
$cmd .= ' --no-color';
$cmd .= ' --gulpfile %s';
$cmd .= ' %s'; // the task that will be executed
// and execute it
return $this->builder->executeCommand($cmd, $this->directory, $this->gulpfile, $this->task);
}
}

View file

@ -0,0 +1,93 @@
<?php
namespace PHPCensor\Plugin;
use PHPCensor\Builder;
use PHPCensor\Model\Build;
use PHPCensor\Plugin;
use HipChat\HipChat;
/**
* Hipchat Plugin
*
* @author James Inman <james@jamesinman.co.uk>
*/
class HipchatNotify extends Plugin
{
protected $authToken;
protected $color;
protected $notify;
protected $userAgent;
protected $cookie;
protected $message;
protected $room;
/**
* @return string
*/
public static function pluginName()
{
return 'hipchat_notify';
}
/**
* {@inheritdoc}
*/
public function __construct(Builder $builder, Build $build, array $options = [])
{
parent::__construct($builder, $build, $options);
$this->userAgent = "PHP Censor/1.0";
$this->cookie = "php-censor-cookie";
if (is_array($options) && isset($options['authToken']) && isset($options['room'])) {
$this->authToken = $options['authToken'];
$this->room = $options['room'];
if (isset($options['message'])) {
$this->message = $options['message'];
} else {
$this->message = '%PROJECT_TITLE% built at %BUILD_URI%';
}
if (isset($options['color'])) {
$this->color = $options['color'];
} else {
$this->color = 'yellow';
}
if (isset($options['notify'])) {
$this->notify = $options['notify'];
} else {
$this->notify = false;
}
} else {
throw new \Exception('Please define room and authToken for hipchat_notify plugin.');
}
}
/**
* Run the HipChat plugin.
* @return bool
*/
public function execute()
{
$hipChat = new HipChat($this->authToken);
$message = $this->builder->interpolate($this->message);
$result = true;
if (is_array($this->room)) {
foreach ($this->room as $room) {
if (!$hipChat->message_room($room, 'PHP Censor', $message, $this->notify, $this->color)) {
$result = false;
}
}
} else {
if (!$hipChat->message_room($this->room, 'PHP Censor', $message, $this->notify, $this->color)) {
$result = false;
}
}
return $result;
}
}

119
src/Plugin/Irc.php Normal file
View file

@ -0,0 +1,119 @@
<?php
namespace PHPCensor\Plugin;
use PHPCensor\Builder;
use PHPCensor\Model\Build;
use PHPCensor\Plugin;
/**
* IRC Plugin - Sends a notification to an IRC channel
*
* @author Dan Cryer <dan@block8.co.uk>
*/
class Irc extends Plugin
{
protected $message;
protected $server;
protected $port;
protected $room;
protected $nick;
/**
* @return string
*/
public static function pluginName()
{
return 'irc';
}
/**
* {@inheritdoc}
*/
public function __construct(Builder $builder, Build $build, array $options = [])
{
parent::__construct($builder, $build, $options);
$this->message = $options['message'];
$buildSettings = $this->builder->getConfig('build_settings');
if (isset($buildSettings['irc'])) {
$irc = $buildSettings['irc'];
$this->server = $irc['server'];
$this->port = $irc['port'];
$this->room = $irc['room'];
$this->nick = $irc['nick'];
}
}
/**
* Run IRC plugin.
* @return bool
*/
public function execute()
{
$msg = $this->builder->interpolate($this->message);
if (empty($this->server) || empty($this->room) || empty($this->nick)) {
$this->builder->logFailure('You must configure a server, room and nick.');
}
if (empty($this->port)) {
$this->port = 6667;
}
$sock = fsockopen($this->server, $this->port);
stream_set_timeout($sock, 1);
$connectCommands = [
'USER ' . $this->nick . ' 0 * :' . $this->nick,
'NICK ' . $this->nick,
];
$this->executeIrcCommands($sock, $connectCommands);
$this->executeIrcCommand($sock, 'JOIN ' . $this->room);
$this->executeIrcCommand($sock, 'PRIVMSG ' . $this->room . ' :' . $msg);
fclose($sock);
return true;
}
/**
* @param resource $socket
* @param array $commands
* @return bool
*/
private function executeIrcCommands($socket, array $commands)
{
foreach ($commands as $command) {
fputs($socket, $command . "\n");
}
$pingBack = false;
// almost all servers expect pingback!
while ($response = fgets($socket)) {
$matches = [];
if (preg_match('/^PING \\:([A-Z0-9]+)/', $response, $matches)) {
$pingBack = $matches[1];
}
}
if ($pingBack) {
$command = 'PONG :' . $pingBack . "\n";
fputs($socket, $command);
}
}
/**
*
* @param resource $socket
* @param string $command
* @return bool
*/
private function executeIrcCommand($socket, $command)
{
return $this->executeIrcCommands($socket, [$command]);
}
}

140
src/Plugin/Lint.php Normal file
View file

@ -0,0 +1,140 @@
<?php
namespace PHPCensor\Plugin;
use PHPCensor;
use PHPCensor\Builder;
use PHPCensor\Model\Build;
use PHPCensor\Plugin;
/**
* PHP Lint Plugin - Provides access to PHP lint functionality.
*
* @author Dan Cryer <dan@block8.co.uk>
*/
class Lint extends Plugin
{
protected $directories;
protected $recursive = true;
protected $ignore;
/**
* @return string
*/
public static function pluginName()
{
return 'lint';
}
/**
* {@inheritdoc}
*/
public function __construct(Builder $builder, Build $build, array $options = [])
{
parent::__construct($builder, $build, $options);
$this->directories = [''];
$this->ignore = $this->builder->ignore;
if (!empty($options['directory'])) {
$this->directories[] = $options['directory'];
}
if (!empty($options['directories'])) {
$this->directories = $options['directories'];
}
if (array_key_exists('recursive', $options)) {
$this->recursive = $options['recursive'];
}
}
/**
* Executes parallel lint
*/
public function execute()
{
$this->builder->quiet = true;
$success = true;
$php = $this->findBinary('php');
foreach ($this->directories as $dir) {
if (!$this->lintDirectory($php, $dir)) {
$success = false;
}
}
$this->builder->quiet = false;
return $success;
}
/**
* Lint an item (file or directory) by calling the appropriate method.
* @param $php
* @param $item
* @param $itemPath
* @return bool
*/
protected function lintItem($php, $item, $itemPath)
{
$success = true;
if ($item->isFile() && $item->getExtension() == 'php' && !$this->lintFile($php, $itemPath)) {
$success = false;
} elseif ($item->isDir() && $this->recursive && !$this->lintDirectory($php, $itemPath . DIRECTORY_SEPARATOR)) {
$success = false;
}
return $success;
}
/**
* Run php -l against a directory of files.
* @param $php
* @param $path
* @return bool
*/
protected function lintDirectory($php, $path)
{
$success = true;
$directory = new \DirectoryIterator($this->builder->buildPath . $path);
foreach ($directory as $item) {
if ($item->isDot()) {
continue;
}
$itemPath = $path . $item->getFilename();
if (in_array($itemPath, $this->ignore)) {
continue;
}
if (!$this->lintItem($php, $item, $itemPath)) {
$success = false;
}
}
return $success;
}
/**
* Run php -l against a specific file.
* @param $php
* @param $path
* @return bool
*/
protected function lintFile($php, $path)
{
$success = true;
if (!$this->builder->executeCommand($php . ' -l "%s"', $this->builder->buildPath . $path)) {
$this->builder->logFailure($path);
$success = false;
}
return $success;
}
}

117
src/Plugin/Mage.php Normal file
View file

@ -0,0 +1,117 @@
<?php
/**
* PHPCensor - Continuous Integration for PHP
*/
namespace PHPCensor\Plugin;
use PHPCensor\Builder;
use PHPCensor\Model\Build;
use Psr\Log\LogLevel;
use \PHPCensor\Plugin;
/**
* Integrates PHPCensor with Mage: https://github.com/andres-montanez/Magallanes
*
* @package PHPCensor
* @subpackage Plugins
*/
class Mage extends Plugin
{
protected $mageBin = 'mage';
protected $mageEnv;
/**
* {@inheritdoc}
*/
public static function pluginName()
{
return 'mage';
}
/**
* {@inheritdoc}
*/
public function __construct(Builder $builder, Build $build, array $options = [])
{
parent::__construct($builder, $build, $options);
$config = $builder->getSystemConfig('mage');
if (!empty($config['bin'])) {
$this->mageBin = $config['bin'];
}
if (isset($options['env'])) {
$this->mageEnv = $builder->interpolate($options['env']);
}
}
/**
* {@inheritdoc}
*/
public function execute()
{
if (empty($this->mageEnv)) {
$this->builder->logFailure('You must specify environment.');
return false;
}
$result = $this->builder->executeCommand($this->mageBin . ' deploy to:' . $this->mageEnv);
try {
$this->builder->log('########## MAGE LOG BEGIN ##########');
$this->builder->log($this->getMageLog());
$this->builder->log('########## MAGE LOG END ##########');
} catch (\Exception $e) {
$this->builder->log($e->getMessage(), LogLevel::NOTICE);
}
return $result;
}
/**
* Get mage log lines
* @return array
* @throws \Exception
*/
protected function getMageLog()
{
$logsDir = $this->build->getBuildPath() . '/.mage/logs';
if (!is_dir($logsDir)) {
throw new \Exception('Log directory not found');
}
$list = scandir($logsDir);
if ($list === false) {
throw new \Exception('Log dir read fail');
}
$list = array_filter($list, function ($name) {
return preg_match('/^log-\d+-\d+\.log$/', $name);
});
if (empty($list)) {
throw new \Exception('Log dir filter fail');
}
$res = sort($list);
if ($res === false) {
throw new \Exception('Logs sort fail');
}
$lastLogFile = end($list);
if ($lastLogFile === false) {
throw new \Exception('Get last Log name fail');
}
$logContent = file_get_contents($logsDir . '/' . $lastLogFile);
if ($logContent === false) {
throw new \Exception('Get last Log content fail');
}
$lines = explode("\n", $logContent);
$lines = array_map('trim', $lines);
$lines = array_filter($lines);
return $lines;
}
}

122
src/Plugin/Mage3.php Normal file
View file

@ -0,0 +1,122 @@
<?php
/**
* PHPCensor - Continuous Integration for PHP
*/
namespace PHPCensor\Plugin;
use PHPCensor\Builder;
use PHPCensor\Model\Build;
use Psr\Log\LogLevel;
use \PHPCensor\Plugin;
/**
* Integrates PHPCensor with Mage v3: https://github.com/andres-montanez/Magallanes
*
* @package PHPCensor
* @subpackage Plugins
*/
class Mage3 extends Plugin
{
protected $mageBin = 'mage';
protected $mageEnv;
protected $mageLogDir;
/**
* {@inheritdoc}
*/
public static function pluginName()
{
return 'mage3';
}
/**
* {@inheritdoc}
*/
public function __construct(Builder $builder, Build $build, array $options = [])
{
parent::__construct($builder, $build, $options);
$config = $builder->getSystemConfig('mage3');
if (!empty($config['bin'])) {
$this->mageBin = $config['bin'];
}
if (isset($options['env'])) {
$this->mageEnv = $builder->interpolate($options['env']);
}
if (isset($options['log_dir'])) {
$this->mageLogDir = $builder->interpolate($options['log_dir']);
}
}
/**
* {@inheritdoc}
*/
public function execute()
{
if (empty($this->mageEnv)) {
$this->builder->logFailure('You must specify environment.');
return false;
}
$result = $this->builder->executeCommand($this->mageBin . ' -n deploy ' . $this->mageEnv);
try {
$this->builder->log('########## MAGE LOG BEGIN ##########');
$this->builder->log($this->getMageLog());
$this->builder->log('########## MAGE LOG END ##########');
} catch (\Exception $e) {
$this->builder->log($e->getMessage(), LogLevel::NOTICE);
}
return $result;
}
/**
* Get mage log lines
* @return array
* @throws \Exception
*/
protected function getMageLog()
{
$logsDir = $this->build->getBuildPath() . (!empty($this->mageLogDir) ? '/' . $this->mageLogDir : '');
if (!is_dir($logsDir)) {
throw new \Exception('Log directory not found');
}
$list = scandir($logsDir);
if ($list === false) {
throw new \Exception('Log dir read fail');
}
$list = array_filter($list, function ($name) {
return preg_match('/^\d+_\d+\.log$/', $name);
});
if (empty($list)) {
throw new \Exception('Log dir filter fail');
}
$res = sort($list);
if ($res === false) {
throw new \Exception('Logs sort fail');
}
$lastLogFile = end($list);
if ($lastLogFile === false) {
throw new \Exception('Get last Log name fail');
}
$logContent = file_get_contents($logsDir . '/' . $lastLogFile);
if ($logContent === false) {
throw new \Exception('Get last Log content fail');
}
$lines = explode("\n", $logContent);
$lines = array_map('trim', $lines);
$lines = array_filter($lines);
return $lines;
}
}

160
src/Plugin/Mysql.php Normal file
View file

@ -0,0 +1,160 @@
<?php
namespace PHPCensor\Plugin;
use PDO;
use PHPCensor\Builder;
use PHPCensor\Model\Build;
use PHPCensor\Plugin;
use PHPCensor\Database;
/**
* MySQL Plugin - Provides access to a MySQL database.
*
* @author Dan Cryer <dan@block8.co.uk>
* @author Steve Kamerman <stevekamerman@gmail.com>
*/
class Mysql extends Plugin
{
/**
* @var string
*/
protected $host;
/**
* @var string
*/
protected $user;
/**
* @var string
*/
protected $pass;
/**
* @return string
*/
public static function pluginName()
{
return 'mysql';
}
/**
* {@inheritdoc}
*/
public function __construct(Builder $builder, Build $build, array $options = [])
{
parent::__construct($builder, $build, $options);
$config = Database::getConnection('write')->getDetails();
$this->host =(defined('DB_HOST')) ? DB_HOST : null;
$this->user = $config['user'];
$this->pass = $config['pass'];
$buildSettings = $this->builder->getConfig('build_settings');
if (!isset($buildSettings['mysql'])) {
return;
}
if (!empty($buildSettings['mysql']['host'])) {
$this->host = $this->builder->interpolate($buildSettings['mysql']['host']);
}
if (!empty($buildSettings['mysql']['user'])) {
$this->user = $this->builder->interpolate($buildSettings['mysql']['user']);
}
if (array_key_exists('pass', $buildSettings['mysql'])) {
$this->pass = $buildSettings['mysql']['pass'];
}
}
/**
* Connects to MySQL and runs a specified set of queries.
* @return boolean
*/
public function execute()
{
try {
$opts = [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION];
$pdo = new PDO('mysql:host=' . $this->host, $this->user, $this->pass, $opts);
foreach ($this->options as $query) {
if (!is_array($query)) {
// Simple query
$pdo->query($this->builder->interpolate($query));
} elseif (isset($query['import'])) {
// SQL file execution
$this->executeFile($query['import']);
} else {
throw new \Exception('Invalid command.');
}
}
} catch (\Exception $ex) {
$this->builder->logFailure($ex->getMessage());
return false;
}
return true;
}
/**
* @param string $query
* @return boolean
* @throws \Exception
*/
protected function executeFile($query)
{
if (!isset($query['file'])) {
throw new \Exception('Import statement must contain a \'file\' key');
}
$importFile = $this->builder->buildPath . $this->builder->interpolate($query['file']);
if (!is_readable($importFile)) {
throw new \Exception(sprintf('Cannot open SQL import file: %s', $importFile));
}
$database = isset($query['database']) ? $this->builder->interpolate($query['database']) : null;
$importCommand = $this->getImportCommand($importFile, $database);
if (!$this->builder->executeCommand($importCommand)) {
throw new \Exception('Unable to execute SQL file');
}
return true;
}
/**
* Builds the MySQL import command required to import/execute the specified file
*
* @param string $import_file Path to file, relative to the build root
* @param string $database If specified, this database is selected before execution
*
* @return string
*/
protected function getImportCommand($import_file, $database = null)
{
$decompression = [
'bz2' => '| bzip2 --decompress',
'gz' => '| gzip --decompress',
];
$extension = strtolower(pathinfo($import_file, PATHINFO_EXTENSION));
$decompressionCmd = '';
if (array_key_exists($extension, $decompression)) {
$decompressionCmd = $decompression[$extension];
}
$args = [
':import_file' => escapeshellarg($import_file),
':decomp_cmd' => $decompressionCmd,
':host' => escapeshellarg($this->host),
':user' => escapeshellarg($this->user),
':pass' => (!$this->pass) ? '' : '-p' . escapeshellarg($this->pass),
':database' => ($database === null)? '': escapeshellarg($database),
];
return strtr('cat :import_file :decomp_cmd | mysql -h:host -u:user :pass :database', $args);
}
}

View file

@ -0,0 +1,292 @@
<?php
namespace PHPCensor\Plugin\Option;
use b8\Config;
/**
* Class PhpUnitOptions validates and parse the option for the PhpUnitV2 plugin
*
* @author Pablo Tejada <pablo@ptejada.com>
*/
class PhpUnitOptions
{
/**
* @var array
*/
protected $options;
/**
* @var string
*/
protected $location;
/**
* @var array
*/
protected $arguments = [];
/**
* @param array $options
* @param string $location
*/
public function __construct($options, $location)
{
$this->options = $options;
$this->location = $location;
}
/**
* Remove a command argument
*
* @param $argumentName
*
* @return $this
*/
public function removeArgument($argumentName)
{
unset($this->arguments[$argumentName]);
return $this;
}
/**
* Combine all the argument into a string for the phpunit command
*
* @return string
*/
public function buildArgumentString()
{
$argumentString = '';
foreach ($this->getCommandArguments() as $argumentName => $argumentValues) {
$prefix = $argumentName[0] == '-' ? '' : '--';
if (!is_array($argumentValues)) {
$argumentValues = [$argumentValues];
}
foreach ($argumentValues as $argValue) {
$postfix = ' ';
if (!empty($argValue)) {
$postfix = ' "' . $argValue . '" ';
}
$argumentString .= $prefix . $argumentName . $postfix;
}
}
return $argumentString;
}
/**
* Get all the command arguments
*
* @return string[]
*/
public function getCommandArguments()
{
/*
* Return the full list of arguments
*/
return $this->parseArguments()->arguments;
}
/**
* Parse the arguments from the config options
*
* @return $this
*/
private function parseArguments()
{
if (empty($this->arguments)) {
/*
* Parse the arguments from the YML options file
*/
if (isset($this->options['args'])) {
$rawArgs = $this->options['args'];
if (is_array($rawArgs)) {
$this->arguments = $rawArgs;
} else {
/*
* Try to parse old arguments in a single string
*/
preg_match_all('@--([a-z\-]+)([\s=]+)?[\'"]?((?!--)[-\w/.,\\\]+)?[\'"]?@', (string)$rawArgs, $argsMatch);
if (!empty($argsMatch) && sizeof($argsMatch) > 2) {
foreach ($argsMatch[1] as $index => $argName) {
$this->addArgument($argName, $argsMatch[3][$index]);
}
}
}
}
/*
* Handles command aliases outside of the args option
*/
if (isset($this->options['coverage']) && $this->options['coverage']) {
$allowPublicArtifacts = (bool)Config::getInstance()->get(
'php-censor.build.allow_public_artifacts',
true
);
if ($allowPublicArtifacts) {
$this->addArgument('coverage-html', $this->location);
}
$this->addArgument('coverage-text');
}
/*
* Handles command aliases outside of the args option
*/
if (isset($this->options['config'])) {
$this->addArgument('configuration', $this->options['config']);
}
}
return $this;
}
/**
* Add an argument to the collection
* Note: adding argument before parsing the options will prevent the other options from been parsed.
*
* @param string $argumentName
* @param string $argumentValue
*/
public function addArgument($argumentName, $argumentValue = null)
{
if (isset($this->arguments[$argumentName])) {
if (!is_array($this->arguments[$argumentName])) {
// Convert existing argument values into an array
$this->arguments[$argumentName] = [$this->arguments[$argumentName]];
}
// Appends the new argument to the list
$this->arguments[$argumentName][] = $argumentValue;
} else {
// Adds new argument
$this->arguments[$argumentName] = $argumentValue;
}
}
/**
* Get the list of directory to run phpunit in
*
* @return string[] List of directories
*/
public function getDirectories()
{
$directories = $this->getOption('directory');
if (is_string($directories)) {
$directories = [$directories];
} else {
if (is_null($directories)) {
$directories = [];
}
}
return is_array($directories) ? $directories : [$directories];
}
/**
* Get an option if defined
*
* @param $optionName
*
* @return string[]|string|null
*/
public function getOption($optionName)
{
if (isset($this->options[$optionName])) {
return $this->options[$optionName];
}
return null;
}
/**
* Get the directory to execute the command from
*
* @return mixed|null
*/
public function getRunFrom()
{
return $this->getOption('run_from');
}
/**
* Ge the directory name where tests file reside
*
* @return string|null
*/
public function getTestsPath()
{
return $this->getOption('path');
}
/**
* Get the PHPUnit configuration from the options, or the optional path
*
* @param string $altPath
*
* @return string[] path of files
*/
public function getConfigFiles($altPath = null)
{
$configFiles = $this->getArgument('configuration');
if (empty($configFiles) && $altPath) {
$configFile = self::findConfigFile($altPath);
if ($configFile) {
$configFiles[] = $configFile;
}
}
return $configFiles;
}
/**
* Get options for a given argument
*
* @param $argumentName
*
* @return string[] All the options for given argument
*/
public function getArgument($argumentName)
{
$this->parseArguments();
if (isset($this->arguments[$argumentName])) {
return is_array(
$this->arguments[$argumentName]
) ? $this->arguments[$argumentName] : [$this->arguments[$argumentName]];
}
return [];
}
/**
* Find a PHPUnit configuration file in a directory
*
* @param string $buildPath The path to configuration file
*
* @return null|string
*/
public static function findConfigFile($buildPath)
{
$files = [
'phpunit.xml',
'phpunit.mysql.xml',
'phpunit.pgsql.xml',
'phpunit.xml.dist',
'tests/phpunit.xml',
'tests/phpunit.xml.dist',
];
foreach ($files as $file) {
if (file_exists($buildPath . $file)) {
return $file;
}
}
return null;
}
}

View file

@ -0,0 +1,86 @@
<?php
namespace PHPCensor\Plugin;
use PHPCensor\Builder;
use PHPCensor\Model\Build;
use PHPCensor\Plugin;
/**
* Create a ZIP or TAR.GZ archive of the entire build.
*
* @author Dan Cryer <dan@block8.co.uk>
*/
class PackageBuild extends Plugin
{
protected $directory;
protected $filename;
protected $format;
/**
* @return string
*/
public static function pluginName()
{
return 'package_build';
}
/**
* {@inheritdoc}
*/
public function __construct(Builder $builder, Build $build, array $options = [])
{
parent::__construct($builder, $build, $options);
$path = $this->builder->buildPath;
$this->directory = isset($options['directory']) ? $options['directory'] : $path;
$this->filename = isset($options['filename']) ? $options['filename'] : 'build';
$this->format = isset($options['format']) ? $options['format'] : 'zip';
}
/**
* Executes Composer and runs a specified command (e.g. install / update)
*/
public function execute()
{
$path = $this->builder->buildPath;
$build = $this->build;
if ($this->directory == $path) {
return false;
}
$filename = str_replace('%build.commit%', $build->getCommitId(), $this->filename);
$filename = str_replace('%build.id%', $build->getId(), $filename);
$filename = str_replace('%build.branch%', $build->getBranch(), $filename);
$filename = str_replace('%project.title%', $build->getProject()->getTitle(), $filename);
$filename = str_replace('%date%', date('Y-m-d'), $filename);
$filename = str_replace('%time%', date('Hi'), $filename);
$filename = preg_replace('/([^a-zA-Z0-9_-]+)/', '', $filename);
$currentDir = getcwd();
chdir($this->builder->buildPath);
if (!is_array($this->format)) {
$this->format = [$this->format];
}
foreach ($this->format as $format) {
switch ($format) {
case 'tar':
$cmd = 'tar cfz "%s/%s.tar.gz" ./*';
break;
default:
case 'zip':
$cmd = 'zip -rq "%s/%s.zip" ./*';
break;
}
$success = $this->builder->executeCommand($cmd, $this->directory, $filename);
}
chdir($currentDir);
return $success;
}
}

158
src/Plugin/Pdepend.php Normal file
View file

@ -0,0 +1,158 @@
<?php
namespace PHPCensor\Plugin;
use b8\Config;
use PHPCensor\Builder;
use PHPCensor\Model\Build;
use PHPCensor\Plugin;
use Symfony\Component\Filesystem\Filesystem;
/**
* Pdepend Plugin - Allows Pdepend report
*
* @author Johan van der Heide <info@japaveh.nl>
*/
class Pdepend extends Plugin
{
protected $args;
/**
* @var string
*/
protected $buildDirectory;
/**
* @var string
*/
protected $buildBranchDirectory;
/**
* @var string Directory which needs to be scanned
*/
protected $directory;
/**
* @var string File where the summary.xml is stored
*/
protected $summary;
/**
* @var string File where the chart.svg is stored
*/
protected $chart;
/**
* @var string File where the pyramid.svg is stored
*/
protected $pyramid;
/**
* @var string
*/
protected $buildLocation;
/**
* @var string
*/
protected $buildBranchLocation;
/**
* @return string
*/
public static function pluginName()
{
return 'pdepend';
}
/**
* {@inheritdoc}
*/
public function __construct(Builder $builder, Build $build, array $options = [])
{
parent::__construct($builder, $build, $options);
$this->directory = isset($options['directory'])
? $options['directory']
: $this->builder->buildPath;
$this->summary = 'summary.xml';
$this->pyramid = 'pyramid.svg';
$this->chart = 'chart.svg';
$this->buildDirectory = $build->getBuildDirectory();
$this->buildBranchDirectory = $build->getBuildBranchDirectory();
$this->buildLocation = PUBLIC_DIR . 'artifacts/pdepend/' . $this->buildDirectory;
$this->buildBranchLocation = PUBLIC_DIR . 'artifacts/pdepend/' . $this->buildBranchDirectory;
}
/**
* Runs Pdepend with the given criteria as arguments
*/
public function execute()
{
$allowPublicArtifacts = (bool)Config::getInstance()->get(
'php-censor.build.allow_public_artifacts',
true
);
$fileSystem = new Filesystem();
if (!$fileSystem->exists($this->buildLocation)) {
$fileSystem->mkdir($this->buildLocation, (0777 & ~umask()));
}
if (!is_writable($this->buildLocation)) {
throw new \Exception(sprintf(
'The location %s is not writable or does not exist.',
$this->buildLocation
));
}
$pdepend = $this->findBinary('pdepend');
$cmd = $pdepend . ' --summary-xml="%s" --jdepend-chart="%s" --overview-pyramid="%s" %s "%s"';
// If we need to ignore directories
if (count($this->builder->ignore)) {
$ignore = ' --ignore=' . implode(',', $this->builder->ignore);
} else {
$ignore = '';
}
$success = $this->builder->executeCommand(
$cmd,
$this->buildLocation . DIRECTORY_SEPARATOR . $this->summary,
$this->buildLocation . DIRECTORY_SEPARATOR . $this->chart,
$this->buildLocation . DIRECTORY_SEPARATOR . $this->pyramid,
$ignore,
$this->directory
);
if (!$allowPublicArtifacts) {
$fileSystem->remove($this->buildLocation);
}
if ($allowPublicArtifacts && file_exists($this->buildLocation)) {
$fileSystem->remove($this->buildBranchLocation);
$fileSystem->mirror($this->buildLocation, $this->buildBranchLocation);
}
$config = $this->builder->getSystemConfig('php-censor');
if ($allowPublicArtifacts && $success) {
$this->builder->logSuccess(
sprintf(
"\nPdepend successful build report.\nYou can use report for this build for inclusion in the readme.md file:\n%s,\n![Chart](%s \"Pdepend Chart\") and\n![Pyramid](%s \"Pdepend Pyramid\")\n\nOr report for last build in the branch:\n%s,\n![Chart](%s \"Pdepend Chart\") and\n![Pyramid](%s \"Pdepend Pyramid\")\n",
$config['url'] . '/artifacts/pdepend/' . $this->buildDirectory . '/' . $this->summary,
$config['url'] . '/artifacts/pdepend/' . $this->buildDirectory . '/' . $this->chart,
$config['url'] . '/artifacts/pdepend/' . $this->buildDirectory . '/' . $this->pyramid,
$config['url'] . '/artifacts/pdepend/' . $this->buildBranchDirectory . '/' . $this->summary,
$config['url'] . '/artifacts/pdepend/' . $this->buildBranchDirectory . '/' . $this->chart,
$config['url'] . '/artifacts/pdepend/' . $this->buildBranchDirectory . '/' . $this->pyramid
)
);
}
return $success;
}
}

76
src/Plugin/Pgsql.php Normal file
View file

@ -0,0 +1,76 @@
<?php
namespace PHPCensor\Plugin;
use PDO;
use PHPCensor\Builder;
use PHPCensor\Model\Build;
use PHPCensor\Plugin;
/**
* PgSQL Plugin - Provides access to a PgSQL database.
*
* @author Dan Cryer <dan@block8.co.uk>
*/
class Pgsql extends Plugin
{
/**
* @var string
*/
protected $host;
/**
* @var string
*/
protected $user;
/**
* @var string
*/
protected $pass;
/**
* @return string
*/
public static function pluginName()
{
return 'pgsql';
}
/**
* {@inheritdoc}
*/
public function __construct(Builder $builder, Build $build, array $options = [])
{
parent::__construct($builder, $build, $options);
$buildSettings = $this->builder->getConfig('build_settings');
if (isset($buildSettings['pgsql'])) {
$sql = $buildSettings['pgsql'];
$this->host = $sql['host'];
$this->user = $sql['user'];
$this->pass = $sql['pass'];
}
}
/**
* Connects to PgSQL and runs a specified set of queries.
* @return boolean
*/
public function execute()
{
try {
$opts = [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION];
$pdo = new PDO('pgsql:host=' . $this->host, $this->user, $this->pass, $opts);
foreach ($this->options as $query) {
$pdo->query($this->builder->interpolate($query));
}
} catch (\Exception $ex) {
$this->builder->logFailure($ex->getMessage());
return false;
}
return true;
}
}

212
src/Plugin/Phar.php Normal file
View file

@ -0,0 +1,212 @@
<?php
namespace PHPCensor\Plugin;
use PHPCensor\Builder;
use PHPCensor\Model\Build;
use Phar as PHPPhar;
use PHPCensor\Plugin;
/**
* Phar Plugin
*/
class Phar extends Plugin
{
/**
* Output Directory
* @var string
*/
protected $directory;
/**
* Phar Filename
* @var string
*/
protected $filename;
/**
* Regular Expression Filename Capture
* @var string
*/
protected $regexp;
/**
* Stub Filename
* @var string
*/
protected $stub;
/**
* @return string
*/
public static function pluginName()
{
return 'phar';
}
/**
* {@inheritdoc}
*/
public function __construct(Builder $builder, Build $build, array $options = [])
{
parent::__construct($builder, $build, $options);
// Directory?
if (isset($options['directory'])) {
$this->setDirectory($options['directory']);
}
// Filename?
if (isset($options['filename'])) {
$this->setFilename($options['filename']);
}
// RegExp?
if (isset($options['regexp'])) {
$this->setRegExp($options['regexp']);
}
// Stub?
if (isset($options['stub'])) {
$this->setStub($options['stub']);
}
}
/**
* Directory Setter
*
* @param string $directory Configuration Value
* @return Phar Fluent Interface
*/
public function setDirectory($directory)
{
$this->directory = $directory;
return $this;
}
/**
* Directory Getter
*
* @return string Configurated or Default Value
*/
public function getDirectory()
{
if (!isset($this->directory)) {
$this->setDirectory($this->builder->buildPath);
}
return $this->directory;
}
/**
* Filename Setter
*
* @param string $filename Configuration Value
* @return Phar Fluent Interface
*/
public function setFilename($filename)
{
$this->filename = $filename;
return $this;
}
/**
* Filename Getter
*
* @return string Configurated or Default Value
*/
public function getFilename()
{
if (!isset($this->filename)) {
$this->setFilename('build.phar');
}
return $this->filename;
}
/**
* Regular Expression Setter
*
* @param string $regexp Configuration Value
* @return Phar Fluent Interface
*/
public function setRegExp($regexp)
{
$this->regexp = $regexp;
return $this;
}
/**
* Regular Expression Getter
*
* @return string Configurated or Default Value
*/
public function getRegExp()
{
if (!isset($this->regexp)) {
$this->setRegExp('/\.php$/');
}
return $this->regexp;
}
/**
* Stub Filename Setter
*
* @param string $stub Configuration Value
* @return Phar Fluent Interface
*/
public function setStub($stub)
{
$this->stub = $stub;
return $this;
}
/**
* Stub Filename Getter
*
* @return string Configurated Value
*/
public function getStub()
{
return $this->stub;
}
/**
* Get stub content for the Phar file.
* @return string
*/
public function getStubContent()
{
$content = '';
$filename = $this->getStub();
if ($filename) {
$content = file_get_contents($this->builder->buildPath . DIRECTORY_SEPARATOR . $this->getStub());
}
return $content;
}
/**
* Run the phar plugin.
* @return bool
*/
public function execute()
{
$success = false;
try {
$file = $this->getDirectory() . DIRECTORY_SEPARATOR . $this->getFilename();
$phar = new PHPPhar($file, 0, $this->getFilename());
$phar->buildFromDirectory($this->builder->buildPath, $this->getRegExp());
$stub = $this->getStubContent();
if ($stub) {
$phar->setStub($stub);
}
$success = true;
} catch (\Exception $e) {
$this->builder->log('Phar Plugin Internal Error');
$this->builder->log($e->getMessage());
}
return $success;
}
}

237
src/Plugin/Phing.php Normal file
View file

@ -0,0 +1,237 @@
<?php
namespace PHPCensor\Plugin;
use PHPCensor\Builder;
use PHPCensor\Model\Build;
use PHPCensor\Plugin;
/**
* Phing Plugin - Provides access to Phing functionality.
*
* @author Pavel Pavlov <ppavlov@alera.ru>
*/
class Phing extends Plugin
{
protected $directory;
protected $buildFile = 'build.xml';
protected $targets = ['build'];
protected $properties = [];
protected $propertyFile;
/**
* @return string
*/
public static function pluginName()
{
return 'phing';
}
/**
* {@inheritdoc}
*/
public function __construct(Builder $builder, Build $build, array $options = [])
{
parent::__construct($builder, $build, $options);
/*
* Set working directory
*/
if (isset($options['directory'])) {
$directory = $this->builder->buildPath . DIRECTORY_SEPARATOR . $options['directory'];
} else {
$directory = $this->builder->buildPath;
}
$this->setDirectory($directory);
/*
* Sen name of a non default build file
*/
if (isset($options['build_file'])) {
$this->setBuildFile($options['build_file']);
}
if (isset($options['targets'])) {
$this->setTargets($options['targets']);
}
if (isset($options['properties'])) {
$this->setProperties($options['properties']);
}
if (isset($options['property_file'])) {
$this->setPropertyFile($options['property_file']);
}
}
/**
* Executes Phing and runs a specified targets
*/
public function execute()
{
$phingExecutable = $this->findBinary('phing');
$cmd[] = $phingExecutable . ' -f ' . $this->getBuildFilePath();
if ($this->getPropertyFile()) {
$cmd[] = '-propertyfile ' . $this->getPropertyFile();
}
$cmd[] = $this->propertiesToString();
$cmd[] = '-logger phing.listener.DefaultLogger';
$cmd[] = $this->targetsToString();
$cmd[] = '2>&1';
return $this->builder->executeCommand(implode(' ', $cmd), $this->directory, $this->targets);
}
/**
* @return string
*/
public function getDirectory()
{
return $this->directory;
}
/**
* @param string $directory
*
* @return $this
*/
public function setDirectory($directory)
{
$this->directory = $directory;
}
/**
* @return array
*/
public function getTargets()
{
return $this->targets;
}
/**
* Converts an array of targets into a string.
* @return string
*/
private function targetsToString()
{
return implode(' ', $this->targets);
}
/**
* @param array|string $targets
*
* @return $this
*/
public function setTargets($targets)
{
if (is_string($targets)) {
$targets = [$targets];
}
$this->targets = $targets;
}
/**
* @return string
*/
public function getBuildFile()
{
return $this->buildFile;
}
/**
* @param mixed $buildFile
*
* @return $this
* @throws \Exception
*/
public function setBuildFile($buildFile)
{
if (!file_exists($this->getDirectory() . $buildFile)) {
throw new \Exception('Specified build file does not exist.');
}
$this->buildFile = $buildFile;
}
/**
* Get phing build file path.
* @return string
*/
public function getBuildFilePath()
{
return $this->getDirectory() . $this->buildFile;
}
/**
* @return mixed
*/
public function getProperties()
{
return $this->properties;
}
/**
* @return string
*/
public function propertiesToString()
{
/**
* fix the problem when execute phing out of the build dir
* @ticket 748
*/
if (!isset($this->properties['project.basedir'])) {
$this->properties['project.basedir'] = $this->getDirectory();
}
$propertiesString = [];
foreach ($this->properties as $name => $value) {
$propertiesString[] = '-D' . $name . '="' . $value . '"';
}
return implode(' ', $propertiesString);
}
/**
* @param array|string $properties
*
* @return $this
*/
public function setProperties($properties)
{
if (is_string($properties)) {
$properties = [$properties];
}
$this->properties = $properties;
}
/**
* @return string
*/
public function getPropertyFile()
{
return $this->propertyFile;
}
/**
* @param string $propertyFile
*
* @return $this
* @throws \Exception
*/
public function setPropertyFile($propertyFile)
{
if (!file_exists($this->getDirectory() . DIRECTORY_SEPARATOR . $propertyFile)) {
throw new \Exception('Specified property file does not exist.');
}
$this->propertyFile = $propertyFile;
}
}

View file

@ -0,0 +1,291 @@
<?php
namespace PHPCensor\Plugin;
use PHPCensor;
use PHPCensor\Builder;
use PHPCensor\Model\Build;
use PHPCensor\Model\BuildError;
use PHPCensor\Plugin;
use PHPCensor\ZeroConfigPluginInterface;
/**
* PHP Code Sniffer Plugin - Allows PHP Code Sniffer testing.
*
* @author Dan Cryer <dan@block8.co.uk>
*/
class PhpCodeSniffer extends Plugin implements ZeroConfigPluginInterface
{
/**
* @var array
*/
protected $suffixes;
/**
* @var string
*/
protected $directory;
/**
* @var string
*/
protected $standard;
/**
* @var string
*/
protected $tabWidth;
/**
* @var string
*/
protected $encoding;
/**
* @var int
*/
protected $allowedErrors;
/**
* @var int
*/
protected $allowedWarnings;
/**
* @var string, based on the assumption the root may not hold the code to be tested, extends the base path
*/
protected $path;
/**
* @var array - paths to ignore
*/
protected $ignore;
/**
* @var int
*/
protected $severity = null;
/**
* @var null|int
*/
protected $errorSeverity = null;
/**
* @var null|int
*/
protected $warningSeverity = null;
/**
* @return string
*/
public static function pluginName()
{
return 'php_code_sniffer';
}
/**
* {@inheritdoc}
*/
public function __construct(Builder $builder, Build $build, array $options = [])
{
parent::__construct($builder, $build, $options);
$this->suffixes = ['php'];
$this->directory = $this->builder->buildPath;
$this->standard = 'PSR2';
$this->tabWidth = '';
$this->encoding = '';
$this->path = '';
$this->ignore = $this->builder->ignore;
$this->allowedWarnings = 0;
$this->allowedErrors = 0;
if (isset($options['zero_config']) && $options['zero_config']) {
$this->allowedWarnings = -1;
$this->allowedErrors = -1;
}
if (!empty($options['allowed_errors']) && is_int($options['allowed_errors'])) {
$this->allowedErrors = $options['allowed_errors'];
}
if (!empty($options['allowed_warnings']) && is_int($options['allowed_warnings'])) {
$this->allowedWarnings = $options['allowed_warnings'];
}
if (isset($options['suffixes'])) {
$this->suffixes = (array)$options['suffixes'];
}
if (!empty($options['tab_width'])) {
$this->tabWidth = ' --tab-width='.$options['tab_width'];
}
if (!empty($options['encoding'])) {
$this->encoding = ' --encoding=' . $options['encoding'];
}
if (!empty($options['ignore'])) {
$this->ignore = (array)$options['ignore'];
}
if (!empty($options['standard'])) {
$this->standard = $options['standard'];
}
if (!empty($options['path'])) {
$this->path = $options['path'];
}
if (isset($options['severity']) && is_int($options['severity'])) {
$this->severity = $options['severity'];
}
if (isset($options['error_severity']) && is_int($options['error_severity'])) {
$this->errorSeverity = $options['error_severity'];
}
if (isset($options['warning_severity']) && is_int($options['warning_severity'])) {
$this->warningSeverity = $options['warning_severity'];
}
}
/**
* Check if this plugin can be executed.
*
* @param $stage
* @param Builder $builder
* @param Build $build
*
* @return bool
*/
public static function canExecute($stage, Builder $builder, Build $build)
{
if ($stage == Build::STAGE_TEST) {
return true;
}
return false;
}
/**
* Runs PHP Code Sniffer in a specified directory, to a specified standard.
*/
public function execute()
{
list($ignore, $standard, $suffixes, $severity, $errorSeverity, $warningSeverity) = $this->getFlags();
$phpcs = $this->findBinary('phpcs');
$this->builder->logExecOutput(false);
$cmd = $phpcs . ' --report=json %s %s %s %s %s "%s" %s %s %s';
$this->builder->executeCommand(
$cmd,
$standard,
$suffixes,
$ignore,
$this->tabWidth,
$this->encoding,
$this->builder->buildPath . $this->path,
$severity,
$errorSeverity,
$warningSeverity
);
$output = $this->builder->getLastOutput();
list($errors, $warnings) = $this->processReport($output);
$this->builder->logExecOutput(true);
$success = true;
$this->build->storeMeta('phpcs-warnings', $warnings);
$this->build->storeMeta('phpcs-errors', $errors);
if ($this->allowedWarnings != -1 && $warnings > $this->allowedWarnings) {
$success = false;
}
if ($this->allowedErrors != -1 && $errors > $this->allowedErrors) {
$success = false;
}
return $success;
}
/**
* Process options and produce an arguments string for PHPCS.
* @return array
*/
protected function getFlags()
{
$ignore = '';
if (count($this->ignore)) {
$ignore = ' --ignore=' . implode(',', $this->ignore);
}
if (strpos($this->standard, '/') !== false) {
$standard = ' --standard=' . $this->directory.$this->standard;
} else {
$standard = ' --standard=' . $this->standard;
}
$suffixes = '';
if (count($this->suffixes)) {
$suffixes = ' --extensions=' . implode(',', $this->suffixes);
}
$severity = '';
if ($this->severity !== null) {
$severity = ' --severity=' . $this->severity;
}
$errorSeverity = '';
if ($this->errorSeverity !== null) {
$errorSeverity = ' --error-severity=' . $this->errorSeverity;
}
$warningSeverity = '';
if ($this->warningSeverity !== null) {
$warningSeverity = ' --warning-severity=' . $this->warningSeverity;
}
return [$ignore, $standard, $suffixes, $severity, $errorSeverity, $warningSeverity];
}
/**
* Process the PHPCS output report.
* @param $output
* @return array
* @throws \Exception
*/
protected function processReport($output)
{
$data = json_decode(trim($output), true);
if (!is_array($data)) {
$this->builder->log($output);
throw new \Exception('Could not process the report generated by PHP Code Sniffer.');
}
$errors = $data['totals']['errors'];
$warnings = $data['totals']['warnings'];
foreach ($data['files'] as $fileName => $file) {
$fileName = str_replace($this->builder->buildPath, '', $fileName);
foreach ($file['messages'] as $message) {
$this->build->reportError(
$this->builder,
'php_code_sniffer',
'PHPCS: ' . $message['message'],
$message['type'] == 'ERROR' ? BuildError::SEVERITY_HIGH : BuildError::SEVERITY_LOW,
$fileName,
$message['line']
);
}
}
return [$errors, $warnings];
}
}

164
src/Plugin/PhpCpd.php Normal file
View file

@ -0,0 +1,164 @@
<?php
namespace PHPCensor\Plugin;
use PHPCensor\Builder;
use PHPCensor\Model\Build;
use PHPCensor\Model\BuildError;
use PHPCensor\Plugin;
use PHPCensor\ZeroConfigPluginInterface;
/**
* PHP Copy / Paste Detector - Allows PHP Copy / Paste Detector testing.
*
* @author Dan Cryer <dan@block8.co.uk>
*/
class PhpCpd extends Plugin implements ZeroConfigPluginInterface
{
protected $directory;
protected $args;
/**
* @var string, based on the assumption the root may not hold the code to be
* tested, extends the base path
*/
protected $path;
/**
* @var array - paths to ignore
*/
protected $ignore;
/**
* @return string
*/
public static function pluginName()
{
return 'php_cpd';
}
/**
* {@inheritdoc}
*/
public function __construct(Builder $builder, Build $build, array $options = [])
{
parent::__construct($builder, $build, $options);
$this->path = $this->builder->buildPath;
$this->ignore = $this->builder->ignore;
if (!empty($options['path'])) {
$this->path = $this->builder->buildPath . $options['path'];
}
if (!empty($options['ignore'])) {
$this->ignore = $options['ignore'];
}
}
/**
* Check if this plugin can be executed.
*
* @param $stage
* @param Builder $builder
* @param Build $build
*
* @return bool
*/
public static function canExecute($stage, Builder $builder, Build $build)
{
if ($stage == Build::STAGE_TEST) {
return true;
}
return false;
}
/**
* Runs PHP Copy/Paste Detector in a specified directory.
*/
public function execute()
{
$ignore = '';
$namesExclude = ' --names-exclude ';
foreach ($this->ignore as $item) {
$item = rtrim($item, DIRECTORY_SEPARATOR);
if (is_file(rtrim($this->path, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR . $item)) {
$ignoredFile = explode('/', $item);
$filesToIgnore[] = array_pop($ignoredFile);
} else {
$ignore .= ' --exclude ' . $item;
}
}
if (isset($filesToIgnore)) {
$filesToIgnore = $namesExclude . implode(',', $filesToIgnore);
$ignore = $ignore . $filesToIgnore;
}
$phpcpd = $this->findBinary('phpcpd');
$tmpFileName = tempnam('/tmp', 'phpcpd');
$cmd = $phpcpd . ' --log-pmd "%s" %s "%s"';
$success = $this->builder->executeCommand($cmd, $tmpFileName, $ignore, $this->path);
$errorCount = $this->processReport(file_get_contents($tmpFileName));
$this->build->storeMeta('phpcpd-warnings', $errorCount);
unlink($tmpFileName);
return $success;
}
/**
* Process the PHPCPD XML report.
*
* @param $xmlString
*
* @return integer
*
* @throws \Exception
*/
protected function processReport($xmlString)
{
$xml = simplexml_load_string($xmlString);
if ($xml === false) {
$this->builder->log($xmlString);
throw new \Exception('Could not process the report generated by PHPCpd.');
}
$warnings = 0;
foreach ($xml->duplication as $duplication) {
foreach ($duplication->file as $file) {
$fileName = (string)$file['path'];
$fileName = str_replace($this->builder->buildPath, '', $fileName);
$message = <<<CPD
Copy and paste detected:
```
{$duplication->codefragment}
```
CPD;
$this->build->reportError(
$this->builder,
'php_cpd',
$message,
BuildError::SEVERITY_NORMAL,
$fileName,
(int)$file['line'],
(int)$file['line'] + (int)$duplication['lines']
);
}
$warnings++;
}
return $warnings;
}
}

98
src/Plugin/PhpCsFixer.php Normal file
View file

@ -0,0 +1,98 @@
<?php
namespace PHPCensor\Plugin;
use PHPCensor\Builder;
use PHPCensor\Model\Build;
use PHPCensor\Plugin;
/**
* PHP CS Fixer - Works with the PHP Coding Standards Fixer for testing coding standards.
*
* @author Gabriel Baker <gabriel@autonomicpilot.co.uk>
*/
class PhpCsFixer extends Plugin
{
protected $directory = null;
protected $args = '';
protected $config = false;
protected $configs = [
'.php_cs',
'.php_cs.dist',
];
/**
* @return string
*/
public static function pluginName()
{
return 'php_cs_fixer';
}
/**
* {@inheritdoc}
*/
public function __construct(Builder $builder, Build $build, array $options = [])
{
parent::__construct($builder, $build, $options);
if (!empty($options['args'])) {
$this->args = $options['args'];
}
if (isset($options['verbose']) && $options['verbose']) {
$this->args .= ' --verbose';
}
if (isset($options['diff']) && $options['diff']) {
$this->args .= ' --diff';
}
if (isset($options['rules']) && $options['rules']) {
$this->args .= ' --rules=' . $options['rules'];
}
if (isset($options['config']) && $options['config']) {
$this->config = true;
$this->args .= ' --config=' . $builder->interpolate($options['config']);
}
if (isset($options['directory']) && $options['directory']) {
$this->directory = $builder->interpolate($options['directory']);
}
}
/**
* Run PHP CS Fixer.
*
* @return boolean
*/
public function execute()
{
$directory = '';
if (!empty($this->directory)) {
$directory = $this->directory;
}
if (!$this->config) {
foreach ($this->configs as $config) {
if (file_exists($this->builder->buildPath . '/' . $config)) {
$this->config = true;
$this->args .= ' --config=./' . $config;
break;
}
}
}
if (!$this->config && !$directory) {
$directory = '.';
}
$phpCsFixer = $this->findBinary('php-cs-fixer');
$cmd = $phpCsFixer . ' fix ' . $directory . ' %s';
$success = $this->builder->executeCommand($cmd, $this->args);
return $success;
}
}

View file

@ -0,0 +1,206 @@
<?php
namespace PHPCensor\Plugin;
use PHPCensor;
use PHPCensor\Builder;
use PHPCensor\Model\Build;
use PHPCensor\Model\BuildError;
use PHPCensor\Plugin;
use PHPCensor\ZeroConfigPluginInterface;
/**
* PHP Docblock Checker Plugin - Checks your PHP files for appropriate uses of Docblocks
*
* @author Dan Cryer <dan@block8.co.uk>
*/
class PhpDocblockChecker extends Plugin implements ZeroConfigPluginInterface
{
/**
* @var string Based on the assumption the root may not hold the code to be
* tested, extends the build path.
*/
protected $path;
/**
* @var array - paths to ignore
*/
protected $ignore;
protected $skipClasses = false;
protected $skipMethods = false;
/**
* @var integer
*/
protected $allowedWarnings;
/**
* @return string
*/
public static function pluginName()
{
return 'php_docblock_checker';
}
/**
* {@inheritdoc}
*/
public function __construct(Builder $builder, Build $build, array $options = [])
{
parent::__construct($builder, $build, $options);
$this->ignore = $this->builder->ignore;
$this->path = '';
$this->allowedWarnings = 0;
if (isset($options['zero_config']) && $options['zero_config']) {
$this->allowedWarnings = -1;
}
if (array_key_exists('skip_classes', $options)) {
$this->skipClasses = true;
}
if (array_key_exists('skip_methods', $options)) {
$this->skipMethods = true;
}
if (!empty($options['path'])) {
$this->path = $options['path'];
}
if (array_key_exists('allowed_warnings', $options)) {
$this->allowedWarnings = (int)$options['allowed_warnings'];
}
}
/**
* Check if this plugin can be executed.
* @param $stage
* @param Builder $builder
* @param Build $build
* @return bool
*/
public static function canExecute($stage, Builder $builder, Build $build)
{
if ($stage == Build::STAGE_TEST) {
return true;
}
return false;
}
/**
* Runs PHP Mess Detector in a specified directory.
*/
public function execute()
{
// Check that the binary exists:
$checker = $this->findBinary('phpdoccheck');
// Build ignore string:
$ignore = '';
if (count($this->ignore)) {
$ignore = ' --exclude="' . implode(',', $this->ignore) . '"';
}
// Are we skipping any checks?
$add = '';
if ($this->skipClasses) {
$add .= ' --skip-classes';
}
if ($this->skipMethods) {
$add .= ' --skip-methods';
}
// Build command string:
$path = $this->builder->buildPath . $this->path;
$cmd = $checker . ' --json --directory="%s"%s%s';
// Disable exec output logging, as we don't want the XML report in the log:
$this->builder->logExecOutput(false);
// Run checker:
$this->builder->executeCommand(
$cmd,
$path,
$ignore,
$add
);
// Re-enable exec output logging:
$this->builder->logExecOutput(true);
$output = json_decode($this->builder->getLastOutput(), true);
$errors = count($output);
$success = true;
$this->build->storeMeta('phpdoccheck-warnings', $errors);
$this->reportErrors($output);
if ($this->allowedWarnings != -1 && $errors > $this->allowedWarnings) {
$success = false;
}
return $success;
}
/**
* Report all of the errors we've encountered line-by-line.
* @param array $output
*/
protected function reportErrors($output)
{
foreach ($output as $error) {
switch ($error['type']) {
case 'class':
$message = 'Class ' . $error['class'] . ' is missing a docblock.';
$severity = BuildError::SEVERITY_NORMAL;
break;
case 'method':
$message = $error['class'] . '::' . $error['method'] . ' is missing a docblock.';
$severity = BuildError::SEVERITY_NORMAL;
break;
case 'param-missing':
$message = $error['class'] . '::' . $error['method'] . ' @param ' . $error['param'] . ' missing.';
$severity = BuildError::SEVERITY_LOW;
break;
case 'param-mismatch':
$message = $error['class'] . '::' . $error['method'] . ' @param ' . $error['param'] .
'(' . $error['doc-type'] . ') does not match method signature (' . $error['param-type'] . ')';
$severity = BuildError::SEVERITY_LOW;
break;
case 'return-missing':
$message = $error['class'] . '::' . $error['method'] . ' @return missing.';
$severity = BuildError::SEVERITY_LOW;
break;
case 'return-mismatch':
$message = $error['class'] . '::' . $error['method'] . ' @return ' . $error['doc-type'] .
' does not match method signature (' . $error['return-type'] . ')';
$severity = BuildError::SEVERITY_LOW;
break;
default:
$message = 'Class ' . $error['class'] . ' invalid/missing a docblock.';
$severity = BuildError::SEVERITY_LOW;
break;
}
$this->build->reportError(
$this->builder,
'php_docblock_checker',
$message,
$severity,
$error['file'],
$error['line']
);
}
}
}

93
src/Plugin/PhpLoc.php Normal file
View file

@ -0,0 +1,93 @@
<?php
namespace PHPCensor\Plugin;
use PHPCensor;
use PHPCensor\Builder;
use PHPCensor\Model\Build;
use PHPCensor\Plugin;
use PHPCensor\ZeroConfigPluginInterface;
/**
* PHP Loc - Allows PHP Copy / Lines of Code testing.
*
* @author Johan van der Heide <info@japaveh.nl>
*/
class PhpLoc extends Plugin implements ZeroConfigPluginInterface
{
/**
* @var string
*/
protected $directory;
/**
* @return string
*/
public static function pluginName()
{
return 'php_loc';
}
/**
* Check if this plugin can be executed.
* @param $stage
* @param Builder $builder
* @param Build $build
* @return bool
*/
public static function canExecute($stage, Builder $builder, Build $build)
{
if ($stage == Build::STAGE_TEST) {
return true;
}
return false;
}
/**
* {@inheritdoc}
*/
public function __construct(Builder $builder, Build $build, array $options = [])
{
parent::__construct($builder, $build, $options);
$this->directory = $this->builder->buildPath;
if (isset($options['directory'])) {
$this->directory .= $options['directory'];
}
}
/**
* Runs PHP Copy/Paste Detector in a specified directory.
*/
public function execute()
{
$ignore = '';
if (count($this->builder->ignore)) {
$map = function ($item) {
return ' --exclude ' . rtrim($item, DIRECTORY_SEPARATOR);
};
$ignore = array_map($map, $this->builder->ignore);
$ignore = implode('', $ignore);
}
$phploc = $this->findBinary('phploc');
$success = $this->builder->executeCommand($phploc . ' %s "%s"', $ignore, $this->directory);
$output = $this->builder->getLastOutput();
if (preg_match_all('/\((LOC|CLOC|NCLOC|LLOC)\)\s+([0-9]+)/', $output, $matches)) {
$data = [];
foreach ($matches[1] as $k => $v) {
$data[$v] = (int)$matches[2][$k];
}
$this->build->storeMeta('phploc', $data);
}
return $success;
}
}

View file

@ -0,0 +1,256 @@
<?php
namespace PHPCensor\Plugin;
use PHPCensor;
use PHPCensor\Builder;
use PHPCensor\Model\Build;
use PHPCensor\Plugin;
use PHPCensor\ZeroConfigPluginInterface;
/**
* PHP Mess Detector Plugin - Allows PHP Mess Detector testing.
*
* @author Dan Cryer <dan@block8.co.uk>
*/
class PhpMessDetector extends Plugin implements ZeroConfigPluginInterface
{
/**
* @var array
*/
protected $suffixes;
/**
* @var string, based on the assumption the root may not hold the code to be
* tested, extends the base path only if the provided path is relative. Absolute
* paths are used verbatim
*/
protected $path;
/**
* @var array - paths to ignore
*/
protected $ignore;
/**
* Array of PHPMD rules. Can be one of the builtins (codesize, unusedcode, naming, design, controversial)
* or a filename (detected by checking for a / in it), either absolute or relative to the project root.
* @var array
*/
protected $rules;
protected $allowedWarnings;
/**
* @return string
*/
public static function pluginName()
{
return 'php_mess_detector';
}
/**
* {@inheritdoc}
*/
public function __construct(Builder $builder, Build $build, array $options = [])
{
parent::__construct($builder, $build, $options);
$this->suffixes = ['php'];
$this->ignore = $this->builder->ignore;
$this->path = '';
$this->rules = ['codesize', 'unusedcode', 'naming'];
$this->allowedWarnings = 0;
if (isset($options['zero_config']) && $options['zero_config']) {
$this->allowedWarnings = -1;
}
if (!empty($options['path'])) {
$this->path = $options['path'];
}
if (array_key_exists('allowed_warnings', $options)) {
$this->allowedWarnings = (int)$options['allowed_warnings'];
}
foreach (['rules', 'ignore', 'suffixes'] as $key) {
$this->overrideSetting($options, $key);
}
}
/**
* Check if this plugin can be executed.
* @param $stage
* @param Builder $builder
* @param Build $build
* @return bool
*/
public static function canExecute($stage, Builder $builder, Build $build)
{
if ($stage == Build::STAGE_TEST) {
return true;
}
return false;
}
/**
* Runs PHP Mess Detector in a specified directory.
*/
public function execute()
{
if (!$this->tryAndProcessRules()) {
return false;
}
$phpmdBinaryPath = $this->findBinary('phpmd');
$this->executePhpMd($phpmdBinaryPath);
$errorCount = $this->processReport(trim($this->builder->getLastOutput()));
$this->build->storeMeta('phpmd-warnings', $errorCount);
return $this->wasLastExecSuccessful($errorCount);
}
/**
* Override a default setting.
* @param $options
* @param $key
*/
protected function overrideSetting($options, $key)
{
if (isset($options[$key]) && is_array($options[$key])) {
$this->{$key} = $options[$key];
}
}
/**
* Process PHPMD's XML output report.
*
* @param $xmlString
*
* @return integer
*
* @throws \Exception
*/
protected function processReport($xmlString)
{
$xml = simplexml_load_string($xmlString);
if ($xml === false) {
$this->builder->log($xmlString);
throw new \Exception('Could not process PHPMD report XML.');
}
$warnings = 0;
foreach ($xml->file as $file) {
$fileName = (string)$file['name'];
$fileName = str_replace($this->builder->buildPath, '', $fileName);
foreach ($file->violation as $violation) {
$warnings++;
$this->build->reportError(
$this->builder,
'php_mess_detector',
(string)$violation,
PHPCensor\Model\BuildError::SEVERITY_HIGH,
$fileName,
(int)$violation['beginline'],
(int)$violation['endline']
);
}
}
return $warnings;
}
/**
* Try and process the rules parameter from .php-censor.yml.
* @return bool
*/
protected function tryAndProcessRules()
{
if (!empty($this->rules) && !is_array($this->rules)) {
$this->builder->logFailure('The "rules" option must be an array.');
return false;
}
foreach ($this->rules as &$rule) {
if (strpos($rule, '/') !== false) {
$rule = $this->builder->buildPath . $rule;
}
}
return true;
}
/**
* Execute PHP Mess Detector.
* @param $binaryPath
*/
protected function executePhpMd($binaryPath)
{
$cmd = $binaryPath . ' "%s" xml %s %s %s';
$path = $this->getTargetPath();
$ignore = '';
if (count($this->ignore)) {
$ignore = ' --exclude ' . implode(',', $this->ignore);
}
$suffixes = '';
if (count($this->suffixes)) {
$suffixes = ' --suffixes ' . implode(',', $this->suffixes);
}
// Disable exec output logging, as we don't want the XML report in the log:
$this->builder->logExecOutput(false);
// Run PHPMD:
$this->builder->executeCommand(
$cmd,
$path,
implode(',', $this->rules),
$ignore,
$suffixes
);
// Re-enable exec output logging:
$this->builder->logExecOutput(true);
}
/**
* Get the path PHPMD should be run against.
* @return string
*/
protected function getTargetPath()
{
$path = $this->builder->buildPath . $this->path;
if (!empty($this->path) && $this->path{0} == '/') {
$path = $this->path;
return $path;
}
return $path;
}
/**
* Returns a boolean indicating if the error count can be considered a success.
*
* @param int $errorCount
* @return bool
*/
protected function wasLastExecSuccessful($errorCount)
{
$success = true;
if ($this->allowedWarnings != -1 && $errorCount > $this->allowedWarnings) {
$success = false;
return $success;
}
return $success;
}
}

View file

@ -0,0 +1,143 @@
<?php
namespace PHPCensor\Plugin;
use PHPCensor\Builder;
use PHPCensor\Model\Build;
use PHPCensor\Plugin;
use PHPCensor\ZeroConfigPluginInterface;
/**
* Php Parallel Lint Plugin - Provides access to PHP lint functionality.
*
* @author Vaclav Makes <vaclav@makes.cz>
*/
class PhpParallelLint extends Plugin implements ZeroConfigPluginInterface
{
/**
* @var string
*/
protected $directory;
/**
* @var array - paths to ignore
*/
protected $ignore;
/**
* @var string - comma separated list of file extensions
*/
protected $extensions;
/**
* @var bool - enable short tags
*/
protected $shortTag;
/**
* @return string
*/
public static function pluginName()
{
return 'php_parallel_lint';
}
/**
* $options['directory'] Output Directory. Default: %BUILDPATH%
* $options['filename'] Phar Filename. Default: build.phar
* $options['extensions'] Filename extensions. Default: php
* $options['shorttags'] Enable short tags. Default: false
* $options['stub'] Stub Content. No Default Value
*/
public function __construct(Builder $builder, Build $build, array $options = [])
{
parent::__construct($builder, $build, $options);
$this->directory = $this->builder->buildPath;
$this->ignore = $this->builder->ignore;
$this->extensions = 'php';
$this->shortTag = false;
if (isset($options['directory'])) {
$this->directory = $this->builder->buildPath.$options['directory'];
}
if (isset($options['ignore'])) {
$this->ignore = $options['ignore'];
}
if (isset($options['shorttags'])) {
$this->shortTag = $options['shorttags'];
}
if (isset($options['extensions'])) {
// Only use if this is a comma delimited list
$pattern = '/^([a-z]+)(,\ *[a-z]*)*$/';
if (preg_match($pattern, $options['extensions'])) {
$this->extensions = str_replace(' ', '', $options['extensions']);
}
}
}
/**
* Check if this plugin can be executed.
*
* @param $stage
* @param Builder $builder
* @param Build $build
*
* @return bool
*/
public static function canExecute($stage, Builder $builder, Build $build)
{
if ($stage == Build::STAGE_TEST) {
return true;
}
return false;
}
/**
* Executes parallel lint
*/
public function execute()
{
list($ignore) = $this->getFlags();
$phplint = $this->findBinary('parallel-lint');
$cmd = $phplint . ' -e %s' . '%s %s "%s"';
$success = $this->builder->executeCommand(
$cmd,
$this->extensions,
($this->shortTag ? ' -s' : ''),
$ignore,
$this->directory
);
$output = $this->builder->getLastOutput();
$matches = [];
if (preg_match_all('/Parse error\:/', $output, $matches)) {
$this->build->storeMeta('phplint-errors', count($matches[0]));
}
return $success;
}
/**
* Produce an argument string for PHP Parallel Lint.
* @return array
*/
protected function getFlags()
{
$ignoreFlags = [];
foreach ($this->ignore as $ignoreDir) {
$ignoreFlags[] = '--exclude "' . $this->builder->buildPath . $ignoreDir . '"';
}
$ignore = implode(' ', $ignoreFlags);
return [$ignore];
}
}

118
src/Plugin/PhpSpec.php Normal file
View file

@ -0,0 +1,118 @@
<?php
namespace PHPCensor\Plugin;
use PHPCensor;
use PHPCensor\Plugin;
/**
* PHP Spec Plugin - Allows PHP Spec testing.
*
* @author Dan Cryer <dan@block8.co.uk>
*/
class PhpSpec extends Plugin
{
/**
* @return string
*/
public static function pluginName()
{
return 'php_spec';
}
/**
* Runs PHP Spec tests.
*/
public function execute()
{
$currentDir = getcwd();
chdir($this->builder->buildPath);
$phpspec = $this->findBinary(['phpspec', 'phpspec.php']);
$success = $this->builder->executeCommand($phpspec . ' --format=junit --no-code-generation run');
$output = $this->builder->getLastOutput();
chdir($currentDir);
/*
* process xml output
*
* <testsuites time=FLOAT tests=INT failures=INT errors=INT>
* <testsuite name=STRING time=FLOAT tests=INT failures=INT errors=INT skipped=INT>
* <testcase name=STRING time=FLOAT classname=STRING status=STRING/>
* </testsuite>
* </testsuites
*/
$xml = new \SimpleXMLElement($output);
$attr = $xml->attributes();
$data = [
'time' => (float)$attr['time'],
'tests' => (int)$attr['tests'],
'failures' => (int)$attr['failures'],
'errors' => (int)$attr['errors'],
// now all the tests
'suites' => []
];
/**
* @var \SimpleXMLElement $group
*/
foreach ($xml->xpath('testsuite') as $group) {
$attr = $group->attributes();
$suite = [
'name' => (String)$attr['name'],
'time' => (float)$attr['time'],
'tests' => (int)$attr['tests'],
'failures' => (int)$attr['failures'],
'errors' => (int)$attr['errors'],
'skipped' => (int)$attr['skipped'],
// now the cases
'cases' => []
];
/**
* @var \SimpleXMLElement $child
*/
foreach ($group->xpath('testcase') as $child) {
$attr = $child->attributes();
$case = [
'name' => (String)$attr['name'],
'classname' => (String)$attr['classname'],
'time' => (float)$attr['time'],
'status' => (String)$attr['status'],
];
if ($case['status']=='failed') {
$error = [];
/*
* ok, sad, we had an error
*
* there should be one - foreach makes this easier
*/
foreach ($child->xpath('failure') as $failure) {
$attr = $failure->attributes();
$error['type'] = (String)$attr['type'];
$error['message'] = (String)$attr['message'];
}
foreach ($child->xpath('system-err') as $system_err) {
$error['raw'] = (String)$system_err;
}
$case['error'] = $error;
}
$suite['cases'][] = $case;
}
$data['suites'][] = $suite;
}
$this->build->storeMeta('phpspec', $data);
return $success;
}
}

237
src/Plugin/PhpTalLint.php Normal file
View file

@ -0,0 +1,237 @@
<?php
namespace PHPCensor\Plugin;
use PHPCensor;
use PHPCensor\Builder;
use PHPCensor\Model\Build;
use PHPCensor\Plugin;
/**
* PHPTAL Lint Plugin - Provides access to PHPTAL lint functionality.
*
* @author Stephen Ball <phpci@stephen.rebelinblue.com>
*/
class PhpTalLint extends Plugin
{
protected $directories;
protected $recursive = true;
protected $suffixes;
protected $ignore;
/**
* @return string
*/
public static function pluginName()
{
return 'php_tal_lint';
}
/**
* @var string The path to a file contain custom phptal_tales_ functions
*/
protected $tales;
/**
* @var int
*/
protected $allowed_warnings;
/**
* @var int
*/
protected $allowed_errors;
/**
* @var array The results of the lint scan
*/
protected $failedPaths = [];
/**
* {@inheritdoc}
*/
public function __construct(Builder $builder, Build $build, array $options = [])
{
parent::__construct($builder, $build, $options);
$this->directories = [''];
$this->suffixes = ['zpt'];
$this->ignore = $this->builder->ignore;
$this->allowed_warnings = 0;
$this->allowed_errors = 0;
if (!empty($options['directory'])) {
$this->directories = [$options['directory']];
}
if (isset($options['suffixes'])) {
$this->suffixes = (array)$options['suffixes'];
}
}
/**
* Executes phptal lint
*/
public function execute()
{
$this->builder->quiet = true;
$this->builder->logExecOutput(false);
foreach ($this->directories as $dir) {
$this->lintDirectory($dir);
}
$this->builder->quiet = false;
$this->builder->logExecOutput(true);
$errors = 0;
$warnings = 0;
foreach ($this->failedPaths as $path) {
if ($path['type'] == 'error') {
$errors++;
} else {
$warnings++;
}
}
$this->build->storeMeta('phptallint-warnings', $warnings);
$this->build->storeMeta('phptallint-errors', $errors);
$this->build->storeMeta('phptallint-data', $this->failedPaths);
$success = true;
if ($this->allowed_warnings != -1 && $warnings > $this->allowed_warnings) {
$success = false;
}
if ($this->allowed_errors != -1 && $errors > $this->allowed_errors) {
$success = false;
}
return $success;
}
/**
* Lint an item (file or directory) by calling the appropriate method.
* @param $item
* @param $itemPath
* @return bool
*/
protected function lintItem($item, $itemPath)
{
$success = true;
if ($item->isFile() && in_array(strtolower($item->getExtension()), $this->suffixes)) {
if (!$this->lintFile($itemPath)) {
$success = false;
}
} elseif ($item->isDir() && $this->recursive && !$this->lintDirectory($itemPath . DIRECTORY_SEPARATOR)) {
$success = false;
}
return $success;
}
/**
* Run phptal lint against a directory of files.
* @param $path
* @return bool
*/
protected function lintDirectory($path)
{
$success = true;
$directory = new \DirectoryIterator($this->builder->buildPath . $path);
foreach ($directory as $item) {
if ($item->isDot()) {
continue;
}
$itemPath = $path . $item->getFilename();
if (in_array($itemPath, $this->ignore)) {
continue;
}
if (!$this->lintItem($item, $itemPath)) {
$success = false;
}
}
return $success;
}
/**
* Run phptal lint against a specific file.
* @param $path
* @return bool
*/
protected function lintFile($path)
{
$success = true;
list($suffixes, $tales) = $this->getFlags();
$lint = __DIR__ . DIRECTORY_SEPARATOR . DIRECTORY_SEPARATOR;
$lint .= 'vendor' . DIRECTORY_SEPARATOR . 'phptal' . DIRECTORY_SEPARATOR . 'phptal' . DIRECTORY_SEPARATOR;
$lint .= 'tools' . DIRECTORY_SEPARATOR . 'phptal_lint.php';
$cmd = '/usr/bin/env php ' . $lint . ' %s %s "%s"';
$this->builder->executeCommand($cmd, $suffixes, $tales, $this->builder->buildPath . $path);
$output = $this->builder->getLastOutput();
if (preg_match('/Found (.+?) (error|warning)/i', $output, $matches)) {
$rows = explode(PHP_EOL, $output);
unset($rows[0]);
unset($rows[1]);
unset($rows[2]);
unset($rows[3]);
foreach ($rows as $row) {
$name = basename($path);
$row = str_replace('(use -i to include your custom modifier functions)', '', $row);
$message = str_replace($name . ': ', '', $row);
$parts = explode(' (line ', $message);
$message = trim($parts[0]);
$line = str_replace(')', '', $parts[1]);
$this->failedPaths[] = [
'file' => $path,
'line' => $line,
'type' => $matches[2],
'message' => $message
];
}
$success = false;
}
return $success;
}
/**
* Process options and produce an arguments string for PHPTAL Lint.
* @return array
*/
protected function getFlags()
{
$tales = '';
if (!empty($this->tales)) {
$tales = ' -i ' . $this->builder->buildPath . $this->tales;
}
$suffixes = '';
if (count($this->suffixes)) {
$suffixes = ' -e ' . implode(',', $this->suffixes);
}
return [$suffixes, $tales];
}
}

266
src/Plugin/PhpUnit.php Normal file
View file

@ -0,0 +1,266 @@
<?php
namespace PHPCensor\Plugin;
use b8\Config;
use PHPCensor;
use PHPCensor\Builder;
use PHPCensor\Model\Build;
use PHPCensor\Model\BuildError;
use PHPCensor\Plugin\Option\PhpUnitOptions;
use PHPCensor\Plugin\Util\PhpUnitResultJson;
use PHPCensor\Plugin\Util\PhpUnitResultJunit;
use PHPCensor\Plugin;
use PHPCensor\ZeroConfigPluginInterface;
use Symfony\Component\Filesystem\Filesystem;
/**
* PHP Unit Plugin - A rewrite of the original PHP Unit plugin
*
* @author Dan Cryer <dan@block8.co.uk>
* @author Pablo Tejada <pablo@ptejada.com>
*/
class PhpUnit extends Plugin implements ZeroConfigPluginInterface
{
/**
* @var string
*/
protected $buildDirectory;
/**
* @var string
*/
protected $buildBranchDirectory;
/**
* @var string
*/
protected $buildLocation;
/**
* @var string
*/
protected $buildBranchLocation;
/**
* @var string[] Raw options from the config file
*/
protected $options = [];
/**
* @return string
*/
public static function pluginName()
{
return 'php_unit';
}
/**
* Standard Constructor
* $options['config'] Path to a PHPUnit XML configuration file.
* $options['run_from'] The directory where the phpunit command will run from when using 'config'.
* $options['coverage'] Value for the --coverage-html command line flag.
* $options['directory'] Optional directory or list of directories to run PHPUnit on.
* $options['args'] Command line args (in string format) to pass to PHP Unit
*
* @param Builder $builder
* @param Build $build
* @param string[] $options
*/
public function __construct(Builder $builder, Build $build, array $options = [])
{
parent::__construct($builder, $build, $options);
$this->buildDirectory = $build->getBuildDirectory();
$this->buildBranchDirectory = $build->getBuildBranchDirectory();
$this->buildLocation = PUBLIC_DIR . 'artifacts/phpunit/' . $this->buildDirectory;
$this->buildBranchLocation = PUBLIC_DIR . 'artifacts/phpunit/' . $this->buildBranchDirectory;
$this->options = new PhpUnitOptions($options, $this->buildLocation);
}
/**
* Check if the plugin can be executed without any configurations
*
* @param $stage
* @param Builder $builder
* @param Build $build
*
* @return bool
*/
public static function canExecute($stage, Builder $builder, Build $build)
{
if ($stage == Build::STAGE_TEST && !is_null(PhpUnitOptions::findConfigFile($build->getBuildPath()))) {
return true;
}
return false;
}
/**
* Runs PHP Unit tests in a specified directory, optionally using specified config file(s).
*/
public function execute()
{
$xmlConfigFiles = $this->options->getConfigFiles($this->build->getBuildPath());
$directories = $this->options->getDirectories();
if (empty($xmlConfigFiles) && empty($directories)) {
$this->builder->logFailure('Neither a configuration file nor a test directory found.');
return false;
}
$cmd = $this->findBinary('phpunit');
$lastLine = exec($cmd.' --log-json . --version');
if (false !== strpos($lastLine, '--log-json')) {
$logFormat = 'junit'; // --log-json is not supported
} else {
$logFormat = 'json';
}
$success = [];
// Run any directories
if (!empty($directories)) {
foreach ($directories as $directory) {
$success[] = $this->runConfig($directory, null, $logFormat);
}
} else {
// Run any config files
if (!empty($xmlConfigFiles)) {
foreach ($xmlConfigFiles as $configFile) {
$success[] = $this->runConfig($this->options->getTestsPath(), $configFile, $logFormat);
}
}
}
return !in_array(false, $success);
}
/**
* Run the tests defined in a PHPUnit config file or in a specific directory.
*
* @param $directory
* @param $configFile
* @param string $logFormat
*
* @return bool|mixed
*
* @throws \Exception
*/
protected function runConfig($directory, $configFile, $logFormat)
{
$allowPublicArtifacts = (bool)Config::getInstance()->get(
'php-censor.build.allow_public_artifacts',
true
);
$fileSystem = new Filesystem();
$options = clone $this->options;
$buildPath = $this->build->getBuildPath();
// Save the results into a log file
$logFile = @tempnam($buildPath, 'jLog_');
$options->addArgument('log-' . $logFormat, $logFile);
// Removes any current configurations files
$options->removeArgument('configuration');
if (null !== $configFile) {
// Only the add the configuration file been passed
$options->addArgument('configuration', $buildPath . $configFile);
}
if ($options->getOption('coverage') && $allowPublicArtifacts) {
if (!$fileSystem->exists($this->buildLocation)) {
$fileSystem->mkdir($this->buildLocation, (0777 & ~umask()));
}
if (!is_writable($this->buildLocation)) {
throw new \Exception(sprintf(
'The location %s is not writable or does not exist.',
$this->buildLocation
));
}
}
$arguments = $this->builder->interpolate($options->buildArgumentString());
$cmd = $this->findBinary('phpunit') . ' %s %s';
$success = $this->builder->executeCommand($cmd, $arguments, $directory);
$output = $this->builder->getLastOutput();
if (
$fileSystem->exists($this->buildLocation) &&
$options->getOption('coverage') &&
$allowPublicArtifacts
) {
$fileSystem->remove($this->buildBranchLocation);
$fileSystem->mirror($this->buildLocation, $this->buildBranchLocation);
}
$this->processResults($logFile, $logFormat);
$config = $this->builder->getSystemConfig('php-censor');
if ($options->getOption('coverage')) {
preg_match(
'#Classes:[\s]*(.*?)%[^M]*?Methods:[\s]*(.*?)%[^L]*?Lines:[\s]*(.*?)\%#s',
$output,
$matches
);
$this->build->storeMeta('phpunit-coverage', [
'classes' => !empty($matches[1]) ? $matches[1] : '0.00',
'methods' => !empty($matches[2]) ? $matches[2] : '0.00',
'lines' => !empty($matches[3]) ? $matches[3] : '0.00',
]);
if ($allowPublicArtifacts) {
$this->builder->logSuccess(
sprintf(
"\nPHPUnit successful build coverage report.\nYou can use coverage report for this build: %s\nOr coverage report for last build in the branch: %s",
$config['url'] . '/artifacts/phpunit/' . $this->buildDirectory . '/index.html',
$config['url'] . '/artifacts/phpunit/' . $this->buildBranchDirectory . '/index.html'
)
);
}
}
return $success;
}
/**
* Saves the test results
*
* @param string $logFile
* @param string $logFormat
*
* @throws \Exception If failed to parse the log file
*/
protected function processResults($logFile, $logFormat)
{
if (file_exists($logFile)) {
if ('json' === $logFormat) {
$parser = new PhpUnitResultJson($logFile, $this->build->getBuildPath());
} else {
$parser = new PhpUnitResultJunit($logFile, $this->build->getBuildPath());
}
$this->build->storeMeta('phpunit-data', $parser->parse()->getResults());
$this->build->storeMeta('phpunit-errors', $parser->getFailures());
foreach ($parser->getErrors() as $error) {
$severity = $error['severity'] ==
$parser::SEVERITY_ERROR ?
BuildError::SEVERITY_CRITICAL :
BuildError::SEVERITY_HIGH;
$this->build->reportError(
$this->builder, 'php_unit', $error['message'], $severity, $error['file'], $error['line']
);
}
@unlink($logFile);
} else {
throw new \Exception('log output file does not exist: ' . $logFile);
}
}
}

View file

@ -0,0 +1,98 @@
<?php
namespace PHPCensor\Plugin;
use PHPCensor;
use PHPCensor\Builder;
use PHPCensor\Model\Build;
use PHPCensor\Plugin;
use PHPCensor\Model\BuildError;
use PHPCensor\ZeroConfigPluginInterface;
use SensioLabs\Security\SecurityChecker as BaseSecurityChecker;
/**
* SensioLabs Security Checker Plugin
*
* @author Dmitry Khomutov <poisoncorpsee@gmail.com>
*/
class SecurityChecker extends Plugin implements ZeroConfigPluginInterface
{
/**
* @var integer
*/
protected $allowedWarnings;
/**
* @return string
*/
public static function pluginName()
{
return 'security_checker';
}
/**
* {@inheritdoc}
*/
public function __construct(Builder $builder, Build $build, array $options = [])
{
parent::__construct($builder, $build, $options);
$this->allowedWarnings = 0;
if (isset($options['zero_config']) && $options['zero_config']) {
$this->allowedWarnings = -1;
}
if (array_key_exists('allowed_warnings', $options)) {
$this->allowedWarnings = (int)$options['allowed_warnings'];
}
}
/**
* Check if this plugin can be executed.
*
* @param $stage
* @param Builder $builder
* @param Build $build
*
* @return bool
*/
public static function canExecute($stage, Builder $builder, Build $build)
{
$path = $builder->buildPath . DIRECTORY_SEPARATOR . 'composer.lock';
if (file_exists($path) && $stage == Build::STAGE_TEST) {
return true;
}
return false;
}
public function execute()
{
$success = true;
$checker = new BaseSecurityChecker();
$warnings = $checker->check($this->builder->buildPath . DIRECTORY_SEPARATOR . 'composer.lock');
if ($warnings) {
foreach ($warnings as $library => $warning) {
foreach ($warning['advisories'] as $data) {
$this->build->reportError(
$this->builder,
'security_checker',
$library . ' (' . $warning['version'] . ")\n" . $data['cve'] . ': ' . $data['title'] . "\n" . $data['link'],
BuildError::SEVERITY_CRITICAL,
'-',
'-'
);
}
}
if ($this->allowedWarnings != -1 && ((int)$checker->getLastVulnerabilityCount() > $this->allowedWarnings)) {
$success = false;
}
}
return $success;
}
}

77
src/Plugin/Shell.php Normal file
View file

@ -0,0 +1,77 @@
<?php
namespace PHPCensor\Plugin;
use PHPCensor\Builder;
use PHPCensor\Model\Build;
use PHPCensor\Plugin;
/**
* Shell Plugin - Allows execute shell commands.
*
* @author Kinn Coelho Julião <kinncj@gmail.com>
*/
class Shell extends Plugin
{
/**
* @var array
*/
protected $args;
/**
* @var string[] $commands The commands to be executed
*/
protected $commands = [];
/**
* @return string
*/
public static function pluginName()
{
return 'shell';
}
/**
* {@inheritdoc}
*/
public function __construct(Builder $builder, Build $build, array $options = [])
{
parent::__construct($builder, $build, $options);
if (isset($options['command'])) {
// Keeping this for backwards compatibility, new projects should use interpolation vars.
$options['command'] = str_replace("%buildpath%", $this->builder->buildPath, $options['command']);
$this->commands = [$options['command']];
return;
}
/*
* Support the new syntax:
*
* shell:
* - "cd /www"
* - "rm -f file.txt"
*/
if (is_array($options)) {
$this->commands = $options;
}
}
/**
* Runs the shell command.
*
* @return bool
*/
public function execute()
{
foreach ($this->commands as $command) {
$command = $this->builder->interpolate($command);
if (!$this->builder->executeCommand($command)) {
return false;
}
}
return true;
}
}

137
src/Plugin/SlackNotify.php Normal file
View file

@ -0,0 +1,137 @@
<?php
namespace PHPCensor\Plugin;
use PHPCensor\Builder;
use PHPCensor\Model\Build;
use PHPCensor\Plugin;
use Maknz\Slack\Client;
use Maknz\Slack\Attachment;
use Maknz\Slack\AttachmentField;
/**
* Slack Plugin
*
* @author Stephen Ball <phpci@stephen.rebelinblue.com>
*/
class SlackNotify extends Plugin
{
private $webHook;
private $room;
private $username;
private $message;
private $icon;
private $showStatus;
/**
* @return string
*/
public static function pluginName()
{
return 'slack_notify';
}
/**
* {@inheritdoc}
*/
public function __construct(Builder $builder, Build $build, array $options = [])
{
parent::__construct($builder, $build, $options);
if (is_array($options) && isset($options['webhook_url'])) {
$this->webHook = trim($options['webhook_url']);
if (isset($options['message'])) {
$this->message = $options['message'];
} else {
$this->message = '<%PROJECT_URI%|%PROJECT_TITLE%> - <%BUILD_URI%|Build #%BUILD%> has finished ';
$this->message .= 'for commit <%COMMIT_URI%|%SHORT_COMMIT% (%COMMIT_EMAIL%)> ';
$this->message .= 'on branch <%BRANCH_URI%|%BRANCH%>';
}
if (isset($options['room'])) {
$this->room = $options['room'];
} else {
$this->room = '#php-censor';
}
if (isset($options['username'])) {
$this->username = $options['username'];
} else {
$this->username = 'PHP Censor';
}
if (isset($options['show_status'])) {
$this->showStatus = (bool) $options['show_status'];
} else {
$this->showStatus = true;
}
if (isset($options['icon'])) {
$this->icon = $options['icon'];
}
} else {
throw new \Exception('Please define the webhook_url for slack_notify plugin!');
}
}
/**
* Run the Slack plugin.
* @return bool
*/
public function execute()
{
$body = $this->builder->interpolate($this->message);
$client = new Client($this->webHook);
$message = $client->createMessage();
if (!empty($this->room)) {
$message->setChannel($this->room);
}
if (!empty($this->username)) {
$message->setUsername($this->username);
}
if (!empty($this->icon)) {
$message->setIcon($this->icon);
}
// Include an attachment which shows the status and hide the message
if ($this->showStatus) {
$successfulBuild = $this->build->isSuccessful();
if ($successfulBuild) {
$status = 'Success';
$color = 'good';
} else {
$status = 'Failed';
$color = 'danger';
}
// Build up the attachment data
$attachment = new Attachment([
'fallback' => $body,
'pretext' => $body,
'color' => $color,
'fields' => [
new AttachmentField([
'title' => 'Status',
'value' => $status,
'short' => false
])
]
]);
$message->attach($attachment);
$body = '';
}
$message->send($body);
return true;
}
}

69
src/Plugin/Sqlite.php Normal file
View file

@ -0,0 +1,69 @@
<?php
namespace PHPCensor\Plugin;
use PDO;
use PHPCensor\Builder;
use PHPCensor\Model\Build;
use PHPCensor\Plugin;
/**
* SQLite Plugin Provides access to a SQLite database.
*
* @author Dmitry Khomutov <poisoncorpsee@gmail.com>
*/
class Sqlite extends Plugin
{
/**
* @var array
*/
protected $queries = [];
/**
* @var string
*/
protected $path;
/**
* @return string
*/
public static function pluginName()
{
return 'sqlite';
}
/**
* {@inheritdoc}
*/
public function __construct(Builder $builder, Build $build, array $options = [])
{
parent::__construct($builder, $build, $options);
$buildSettings = $this->builder->getConfig('build_settings');
if (isset($buildSettings['sqlite'])) {
$sql = $buildSettings['sqlite'];
$this->path = $sql['path'];
}
}
/**
* Connects to SQLite and runs a specified set of queries.
* @return boolean
*/
public function execute()
{
try {
$opts = [PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION];
$pdo = new PDO('sqlite:' . $this->path, $opts);
foreach ($this->queries as $query) {
$pdo->query($this->builder->interpolate($query));
}
} catch (\Exception $ex) {
$this->builder->logFailure($ex->getMessage());
return false;
}
return true;
}
}

View file

@ -0,0 +1,277 @@
<?php
namespace PHPCensor\Plugin;
use PHPCensor;
use PHPCensor\Builder;
use PHPCensor\Model\Build;
use PHPCensor\Plugin;
use PHPCensor\ZeroConfigPluginInterface;
/**
* Technical Debt Plugin - Checks for existence of "TODO", "FIXME", etc.
*
* @author James Inman <james@jamesinman.co.uk>
*/
class TechnicalDebt extends Plugin implements ZeroConfigPluginInterface
{
/**
* @var array
*/
protected $suffixes;
/**
* @var string
*/
protected $directory;
/**
* @var int
*/
protected $allowedErrors;
/**
* @var array - paths to ignore
*/
protected $ignore;
/**
* @var array - terms to search for
*/
protected $searches;
/**
* @var array - lines of . and X to visualize errors
*/
protected $errorPerFile = [];
/**
* @var int
*/
protected $currentLineSize = 0;
/**
* @var int
*/
protected $lineNumber = 0;
/**
* @var int
*/
protected $numberOfAnalysedFile = 0;
/**
* @return string
*/
public static function pluginName()
{
return 'technical_debt';
}
/**
* Store the status of the file :
* . : checked no errors
* X : checked with one or more errors
*
* @param string $char
*/
protected function buildLogString($char)
{
if (isset($this->errorPerFile[$this->lineNumber])) {
$this->errorPerFile[$this->lineNumber] .= $char;
} else {
$this->errorPerFile[$this->lineNumber] = $char;
}
$this->currentLineSize++;
$this->numberOfAnalysedFile++;
if ($this->currentLineSize > 59) {
$this->currentLineSize = 0;
$this->lineNumber++;
}
}
/**
* Create a visual representation of file with Todo
* ...XX... 10/300 (10 %)
*
* @return string The visual representation
*/
protected function returnResult()
{
$string = '';
$fileNumber = 0;
foreach ($this->errorPerFile as $oneLine) {
$fileNumber += strlen($oneLine);
$string .= str_pad($oneLine, 60, ' ', STR_PAD_RIGHT);
$string .= str_pad($fileNumber, 4, ' ', STR_PAD_LEFT);
$string .= "/" . $this->numberOfAnalysedFile . " (" . floor($fileNumber * 100 / $this->numberOfAnalysedFile) . " %)\n";
}
$string .= "Checked {$fileNumber} files\n";
return $string;
}
/**
* {@inheritdoc}
*/
public function __construct(Builder $builder, Build $build, array $options = [])
{
parent::__construct($builder, $build, $options);
$this->suffixes = ['php'];
$this->directory = $this->builder->buildPath;
$this->ignore = $this->builder->ignore;
$this->allowedErrors = 0;
$this->searches = ['TODO', 'FIXME', 'TO DO', 'FIX ME'];
if (!empty($options['suffixes']) && is_array($options['suffixes'])) {
$this->suffixes = $options['suffixes'];
}
if (!empty($options['searches']) && is_array($options['searches'])) {
$this->searches = $options['searches'];
}
if (isset($options['zero_config']) && $options['zero_config']) {
$this->allowedErrors = -1;
}
if (array_key_exists('allowed_errors', $options) && $options['allowed_errors']) {
$this->allowedErrors = (int) $options['allowed_errors'];
}
$this->setOptions($options);
}
/**
* Handle this plugin's options.
*
* @param $options
*/
protected function setOptions($options)
{
foreach (['directory', 'ignore'] as $key) {
if (array_key_exists($key, $options)) {
$this->{$key} = $options[$key];
}
}
}
/**
* Check if this plugin can be executed.
*
* @param string $stage
* @param Builder $builder
* @param Build $build
*
* @return boolean
*/
public static function canExecute($stage, Builder $builder, Build $build)
{
if ($stage == Build::STAGE_TEST) {
return true;
}
return false;
}
/**
* Runs the plugin
*/
public function execute()
{
$success = true;
$errorCount = $this->getErrorList();
$this->builder->log($this->returnResult() . "Found $errorCount instances of " . implode(', ', $this->searches));
$this->build->storeMeta('technical_debt-warnings', $errorCount);
if ($this->allowedErrors !== -1 && $errorCount > $this->allowedErrors) {
$success = false;
}
return $success;
}
/**
* Gets the number and list of errors returned from the search
*
* @return integer
*/
protected function getErrorList()
{
$iterator = new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($this->directory));
$this->builder->logDebug("Ignored path: ".json_encode($this->ignore, true));
$errorCount = 0;
/** @var \SplFileInfo $file */
foreach ($iterator as $file) {
$filePath = $file->getRealPath();
$extension = $file->getExtension();
$ignored = false;
foreach ($this->suffixes as $suffix) {
if ($suffix !== $extension) {
$ignored = true;
break;
}
}
foreach ($this->ignore as $ignore) {
if ('/' === $ignore{0}) {
if (0 === strpos($filePath, $ignore)) {
$ignored = true;
break;
}
} else {
$ignoreReal = $this->directory . $ignore;
if (0 === strpos($filePath, $ignoreReal)) {
$ignored = true;
break;
}
}
}
if (!$ignored) {
$handle = fopen($filePath, "r");
$lineNumber = 1;
$errorInFile = false;
while (false === feof($handle)) {
$line = fgets($handle);
foreach ($this->searches as $search) {
if ($technicalDeptLine = trim(strstr($line, $search))) {
$fileName = str_replace($this->directory, '', $filePath);
$this->build->reportError(
$this->builder,
'technical_debt',
$technicalDeptLine,
PHPCensor\Model\BuildError::SEVERITY_LOW,
$fileName,
$lineNumber
);
$errorInFile = true;
$errorCount++;
}
}
$lineNumber++;
}
fclose ($handle);
if ($errorInFile === true) {
$this->buildLogString('X');
} else {
$this->buildLogString('.');
}
}
}
return $errorCount;
}
}

View file

@ -0,0 +1,288 @@
<?php
namespace PHPCensor\Plugin\Util;
use PHPCensor\Store\Factory as StoreFactory;
use Exception;
use PHPCensor\Helper\Lang;
use PHPCensor\Logging\BuildLogger;
use PHPCensor\Model\Build;
use PHPCensor\Plugin;
use PHPCensor\Store\BuildStore;
/**
* Plugin Executor - Runs the configured plugins for a given build stage.
*/
class Executor
{
/**
* @var BuildLogger
*/
protected $logger;
/**
* @var Factory
*/
protected $pluginFactory;
/**
* @var BuildStore
*/
protected $store;
/**
* @param Factory $pluginFactory
* @param BuildLogger $logger
*/
public function __construct(Factory $pluginFactory, BuildLogger $logger, BuildStore $store = null)
{
$this->pluginFactory = $pluginFactory;
$this->logger = $logger;
$this->store = $store ?: StoreFactory::getStore('Build');
}
/**
* Execute a the appropriate set of plugins for a given build stage.
*
* @param array $config Configuration
* @param string $stage
*
* @return bool
*/
public function executePlugins($config, $stage)
{
$success = true;
$pluginsToExecute = [];
// If we have global plugins to execute for this stage, add them to the list to be executed:
if (array_key_exists($stage, $config) && is_array($config[$stage])) {
$pluginsToExecute[] = $config[$stage];
}
$pluginsToExecute = $this->getBranchSpecificPlugins($config, $stage, $pluginsToExecute);
foreach ($pluginsToExecute as $pluginSet) {
if (!$this->doExecutePlugins($pluginSet, $stage)) {
$success = false;
}
}
return $success;
}
/**
* @param array $config
* @param string $branch
*
* @return bool|array
*/
public function getBranchSpecificConfig($config, $branch)
{
$configSections = array_keys($config);
foreach ($configSections as $configSection) {
if (0 === strpos($configSection, 'branch-')) {
if ($configSection === ('branch-' . $branch)) {
return $config[$configSection];
}
if (0 === strpos($configSection, 'branch-regex:')) {
$pattern = '#' . substr($configSection, 13) . '#u';
preg_match($pattern, $branch, $matches);
if (!empty($matches[0])) {
return $config[$configSection];
}
}
}
}
return [];
}
/**
* Check the config for any plugins specific to the branch we're currently building.
*
* @param array $config
* @param string $stage
* @param array $pluginsToExecute
*
* @return array
*/
protected function getBranchSpecificPlugins($config, $stage, $pluginsToExecute)
{
/** @var \PHPCensor\Model\Build $build */
$build = $this->pluginFactory->getResourceFor('PHPCensor\Model\Build');
$branch = $build->getBranch();
$branchConfig = $this->getBranchSpecificConfig($config, $branch);
if (!$branchConfig) {
return $pluginsToExecute;
}
$plugins = !empty($branchConfig[$stage]) ? $branchConfig[$stage] : [];
$runOption = 'after';
if (!empty($branchConfig['run-option'])) {
$runOption = $branchConfig['run-option'];
}
switch ($runOption) {
// Replace standard plugin set for this stage with just the branch-specific ones:
case 'replace':
$pluginsToExecute = [];
$pluginsToExecute[] = $plugins;
break;
// Run branch-specific plugins before standard plugins:
case 'before':
array_unshift($pluginsToExecute, $plugins);
break;
// Run branch-specific plugins after standard plugins:
case 'after':
array_push($pluginsToExecute, $plugins);
break;
default:
array_push($pluginsToExecute, $plugins);
break;
}
return $pluginsToExecute;
}
/**
* Execute the list of plugins found for a given testing stage.
* @param $plugins
* @param $stage
* @return bool
* @throws \Exception
*/
protected function doExecutePlugins(&$plugins, $stage)
{
$success = true;
foreach ($plugins as $plugin => $options) {
$this->logger->log("\n" .
sprintf('RUNNING PLUGIN: %s', Lang::get($plugin)) . ' (' .
'Stage' . ': ' . ucfirst($stage) . ')'
);
$this->setPluginStatus($stage, $plugin, Plugin::STATUS_RUNNING);
// Try and execute it
if ($this->executePlugin($plugin, $options)) {
// Execution was successful
$this->logger->logSuccess('PLUGIN: SUCCESS');
$this->setPluginStatus($stage, $plugin, Plugin::STATUS_SUCCESS);
} else {
$status = Plugin::STATUS_FAILED;
if ($stage === Build::STAGE_SETUP) {
$this->logger->logFailure('PLUGIN: FAILED');
// If we're in the "setup" stage, execution should not continue after
// a plugin has failed:
throw new Exception('Plugin failed: ' . $plugin);
} elseif ($stage === Build::STAGE_DEPLOY) {
$this->logger->logFailure('PLUGIN: FAILED');
$success = false;
} else {
// If we're in the "test" stage and the plugin is not allowed to fail,
// then mark the build as failed:
if (empty($options['allow_failures']) && $stage === Build::STAGE_TEST) {
$this->logger->logFailure('PLUGIN: FAILED');
$success = false;
} else {
$status = Plugin::STATUS_FAILED_ALLOWED;
$this->logger->logFailure('PLUGIN: FAILED (ALLOWED)');
}
}
$this->setPluginStatus($stage, $plugin, $status);
}
}
return $success;
}
/**
* Executes a given plugin, with options and returns the result.
*/
public function executePlugin($plugin, $options)
{
$class = $plugin;
if (!class_exists($class)) {
$class = str_replace('_', ' ', $plugin);
$class = ucwords($class);
$class = 'PHPCensor\\Plugin\\' . str_replace(' ', '', $class);
if (!class_exists($class)) {
$this->logger->logFailure(sprintf('Plugin does not exist: %s', $plugin));
return false;
}
}
try {
// Build and run it
$obj = $this->pluginFactory->buildPlugin($class, (is_null($options) ? [] : $options));
return $obj->execute();
} catch (\Exception $ex) {
$this->logger->logFailure('Exception: ' . $ex->getMessage(), $ex);
return false;
}
}
/**
* Change the status of a plugin for a given stage.
*
* @param string $stage The builder stage.
* @param string $plugin The plugin name.
* @param int $status The new status.
*/
protected function setPluginStatus($stage, $plugin, $status)
{
$summary = $this->getBuildSummary();
if (!isset($summary[$stage][$plugin])) {
$summary[$stage][$plugin] = [];
}
$summary[$stage][$plugin]['status'] = $status;
if ($status === Plugin::STATUS_RUNNING) {
$summary[$stage][$plugin]['started'] = time();
} elseif ($status >= Plugin::STATUS_SUCCESS) {
$summary[$stage][$plugin]['ended'] = time();
}
$this->setBuildSummary($summary);
}
/**
* Fetch the summary data of the current build.
*
* @return array
*/
private function getBuildSummary()
{
/** @var Build $build */
$build = $this->pluginFactory->getResourceFor('PHPCensor\Model\Build');
$metas = $this->store->getMeta('plugin-summary', $build->getProjectId(), $build->getId());
return isset($metas[0]['meta_value']) ? $metas[0]['meta_value'] : [];
}
/**
* Sets the summary data of the current build.
*
* @param array $summary
*/
private function setBuildSummary($summary)
{
/** @var Build $build */
$build = $this->pluginFactory->getResourceFor('PHPCensor\Model\Build');
$this->store->setMeta($build->getId(), 'plugin-summary', json_encode($summary));
}
}

214
src/Plugin/Util/Factory.php Normal file
View file

@ -0,0 +1,214 @@
<?php
namespace PHPCensor\Plugin\Util;
use PHPCensor\Plugin;
use Pimple\Container;
/**
* Plugin Factory - Loads Plugins and passes required dependencies.
*/
class Factory
{
const TYPE_ARRAY = "array";
const TYPE_CALLABLE = "callable";
const INTERFACE_PLUGIN = '\PHPCensor\Plugin';
private $currentPluginOptions;
/**
* @var Container
*/
private $container;
/**
* @param Container $container
*/
public function __construct(Container $container = null)
{
if ($container) {
$this->container = $container;
} else {
$this->container = new Container();
}
}
/**
* Trys to get a function from the file path specified. If the
* file returns a function then $this will be passed to it.
* This enables the config file to call any public methods.
*
* @param $configPath
* @return bool - true if the function exists else false.
*/
public function addConfigFromFile($configPath)
{
// The file is expected to return a function which can
// act on the pluginFactory to register any resources needed.
if (file_exists($configPath)) {
$configFunction = require($configPath);
if (is_callable($configFunction)) {
$configFunction($this);
return true;
}
}
return false;
}
/**
* Get most recently used factory options.
* @return mixed
*/
public function getLastOptions()
{
return $this->currentPluginOptions;
}
/**
* Builds an instance of plugin of class $className. $options will
* be passed along with any resources registered with the factory.
*
* @param $className
* @param array|null $options
*
* @throws \InvalidArgumentException if $className doesn't represent a valid plugin
*
* @return \PHPCensor\Plugin
*/
public function buildPlugin($className, $options = [])
{
$this->currentPluginOptions = $options;
$reflectedPlugin = new \ReflectionClass($className);
$constructor = $reflectedPlugin->getConstructor();
if ($constructor) {
$argsToUse = [];
foreach ($constructor->getParameters() as $param) {
if ('options' === $param->getName()) {
$argsToUse[] = $options;
} else {
$argsToUse = $this->addArgFromParam($argsToUse, $param);
}
}
/** @var Plugin $plugin */
$plugin = $reflectedPlugin->newInstanceArgs($argsToUse);
} else {
/** @var Plugin $plugin */
$plugin = $reflectedPlugin->newInstance();
}
return $plugin;
}
/**
* @param callable $loader
* @param string|null $name
* @param string|null $type
* @throws \InvalidArgumentException
* @internal param mixed $resource
*/
public function registerResource(
$loader,
$name = null,
$type = null
) {
if ($name === null && $type === null) {
throw new \InvalidArgumentException(
"Type or Name must be specified"
);
}
if (!($loader instanceof \Closure)) {
throw new \InvalidArgumentException(
'$loader is expected to be a function'
);
}
$resourceID = $this->getInternalID($type, $name);
$this->container[$resourceID] = $loader;
}
/**
* Get an internal resource ID.
* @param null $type
* @param null $name
* @return string
*/
private function getInternalID($type = null, $name = null)
{
$type = $type ? : "";
$name = $name ? : "";
return $type . "-" . $name;
}
/**
* @param string $type
* @param string $name
* @return mixed
*/
public function getResourceFor($type = null, $name = null)
{
$fullId = $this->getInternalID($type, $name);
if (isset($this->container[$fullId])) {
return $this->container[$fullId];
}
$typeOnlyID = $this->getInternalID($type, null);
if (isset($this->container[$typeOnlyID])) {
return $this->container[$typeOnlyID];
}
$nameOnlyID = $this->getInternalID(null, $name);
if (isset($this->container[$nameOnlyID])) {
return $this->container[$nameOnlyID];
}
return null;
}
/**
* @param \ReflectionParameter $param
* @return null|string
*/
private function getParamType(\ReflectionParameter $param)
{
$class = $param->getClass();
if ($class) {
return $class->getName();
} elseif ($param->isArray()) {
return self::TYPE_ARRAY;
} elseif (is_callable($param)) {
return self::TYPE_CALLABLE;
} else {
return null;
}
}
/**
* @param $existingArgs
* @param \ReflectionParameter $param
* @return array
* @throws \DomainException
*/
private function addArgFromParam($existingArgs, \ReflectionParameter $param)
{
$name = $param->getName();
$type = $this->getParamType($param);
$arg = $this->getResourceFor($type, $name);
if ($arg !== null) {
$existingArgs[] = $arg;
} elseif ($arg === null && $param->isOptional()) {
$existingArgs[] = $param->getDefaultValue();
} else {
throw new \DomainException(
"Unsatisfied dependency: " . $param->getName()
);
}
return $existingArgs;
}
}

View file

@ -0,0 +1,110 @@
<?php
namespace PHPCensor\Plugin\Util;
/**
* Class PhpUnitResult parses the results for the PhpUnitV2 plugin
*
* @author Pablo Tejada <pablo@ptejada.com>
*/
abstract class PhpUnitResult
{
const SEVERITY_PASS = 'success';
const SEVERITY_FAIL = 'fail';
const SEVERITY_ERROR = 'error';
const SEVERITY_SKIPPED = 'skipped';
const SEVERITY_WARN = self::SEVERITY_PASS;
const SEVERITY_RISKY = self::SEVERITY_PASS;
protected $outputFile;
protected $buildPath;
protected $results;
protected $failures = 0;
protected $errors = [];
public function __construct($outputFile, $buildPath = '')
{
$this->outputFile = $outputFile;
$this->buildPath = $buildPath;
}
/**
* Parse the results
*
* @return $this
* @throws \Exception If fails to parse the output
*/
abstract public function parse();
abstract protected function getSeverity($testcase);
abstract protected function buildMessage($testcase);
abstract protected function buildTrace($testcase);
protected function getFileAndLine($testcase)
{
return $testcase;
}
protected function getOutput($testcase)
{
return $testcase['output'];
}
protected function parseTestcase($testcase)
{
$severity = $this->getSeverity($testcase);
$pass = isset(array_fill_keys([self::SEVERITY_PASS, self::SEVERITY_SKIPPED], true)[$severity]);
$data = [
'pass' => $pass,
'severity' => $severity,
'message' => $this->buildMessage($testcase),
'trace' => $pass ? [] : $this->buildTrace($testcase),
'output' => $this->getOutput($testcase),
];
if (!$pass) {
$this->failures++;
$info = $this->getFileAndLine($testcase);
$this->errors[] = [
'message' => $data['message'],
'severity' => $severity,
'file' => $info['file'],
'line' => $info['line'],
];
}
$this->results[] = $data;
}
/**
* Get the parse results
*
* @return string[]
*/
public function getResults()
{
return $this->results;
}
/**
* Get the total number of failing tests
*
* @return int
*/
public function getFailures()
{
return $this->failures;
}
/**
* Get the tests with failing status
*
* @return string[]
*/
public function getErrors()
{
return $this->errors;
}
}

View file

@ -0,0 +1,163 @@
<?php
namespace PHPCensor\Plugin\Util;
/**
* Class PhpUnitResult parses the results for the PhpUnitV2 plugin
*
* @author Pablo Tejada <pablo@ptejada.com>
*/
class PhpUnitResultJson extends PhpUnitResult
{
const EVENT_TEST = 'test';
const EVENT_TEST_START = 'testStart';
const EVENT_SUITE_START = 'suiteStart';
protected $options;
protected $arguments = [];
/**
* Parse the results
*
* @return $this
* @throws \Exception If fails to parse the output
*/
public function parse()
{
$rawResults = file_get_contents($this->outputFile);
$events = [];
if ($rawResults && $rawResults[0] == '{') {
$fixedJson = '[' . str_replace('}{', '},{', $rawResults) . ']';
$events = json_decode($fixedJson, true);
} elseif ($rawResults) {
$events = json_decode($rawResults, true);
}
// Reset the parsing variables
$this->results = [];
$this->errors = [];
$this->failures = 0;
if ($events) {
$started = null;
foreach ($events as $event) {
if (isset($event['event']) && $event['event'] == self::EVENT_TEST) {
$this->parseTestcase($event);
$started = null;
} elseif (isset($event['event']) && $event['event'] == self::EVENT_TEST_START) {
$started = $event;
}
}
if ($started) {
$event = $started;
$event['status'] = 'error';
$event['message'] = 'Test is not finished';
$event['output'] = '';
$this->parseTestcase($event);
}
}
return $this;
}
/**
* Build the severity of the event
*
* @param $event
*
* @return string The severity flags
* @throws \Exception
*/
protected function getSeverity($event)
{
$status = $event['status'];
switch ($status) {
case 'fail':
$severity = self::SEVERITY_FAIL;
break;
case 'error':
if (strpos($event['message'], 'Skipped') === 0 || strpos($event['message'], 'Incomplete') === 0) {
$severity = self::SEVERITY_SKIPPED;
} else {
$severity = self::SEVERITY_ERROR;
}
break;
case 'pass':
$severity = self::SEVERITY_PASS;
break;
case 'warning':
$severity = self::SEVERITY_PASS;
break;
default:
throw new \Exception("Unexpected PHPUnit test status: {$status}");
break;
}
return $severity;
}
/**
* Build the message string for an event
*
* @param array $event
*
* @return string
*/
protected function buildMessage($event)
{
$message = $event['test'];
if ($event['message']) {
$message .= PHP_EOL . $event ['message'];
}
return $message;
}
/**
* Build a string base trace of the failure
*
* @param array $event
*
* @return string[]
*/
protected function buildTrace($event)
{
$formattedTrace = [];
if (!empty($event['trace'])) {
foreach ($event['trace'] as $step){
$line = str_replace($this->buildPath, '', $step['file']) . ':' . $step['line'];
$formattedTrace[] = $line;
}
}
return $formattedTrace;
}
/**
* Saves additional info for a failing test
*
* @param array $event
*
* @return array
*/
protected function getFileAndLine($event)
{
if (empty($event['trace'])) {
return [
'file' => '',
'line' => '',
];
}
$firstTrace = end($event['trace']);
reset($event['trace']);
return [
'file' => str_replace($this->buildPath, '', $firstTrace['file']),
'line' => $firstTrace['line']
];
}
}

View file

@ -0,0 +1,130 @@
<?php
namespace PHPCensor\Plugin\Util;
/**
* Class PhpUnitResultJunit parses the results for the PhpUnitV2 plugin
*
* @author Simon Heimberg <simon.heimberg@heimberg-ea.ch>
*/
class PhpUnitResultJunit extends PhpUnitResult
{
/**
* Parse the results
*
* @return $this
* @throws \Exception If fails to parse the output
*/
public function parse()
{
$suites = simplexml_load_file($this->outputFile);
// Reset the parsing variables
$this->results = [];
$this->errors = [];
$this->failures = 0;
foreach ($suites->xpath('//testcase') as $testCase) {
$this->parseTestcase($testCase);
}
$suites['failures'];
$suites['errors'];
return $this;
}
protected function getSeverity($testCase)
{
$severity = self::SEVERITY_PASS;
foreach($testCase as $child) {
switch ($child->getName()) {
case 'failure':
$severity = self::SEVERITY_FAIL;
break 2;
case 'error':
if ('PHPUnit\Framework\RiskyTestError' == $child['type']) { // == because convertion to string is desired
$severity = self::SEVERITY_RISKY;
} else {
$severity = self::SEVERITY_ERROR;
}
break 2;
case 'skipped':
// skipped and ignored, can not distinguish
$severity = self::SEVERITY_SKIPPED;
break 2;
case 'warning':
$severity = self::SEVERITY_WARN;
break 2;
case 'system-out':
case 'system-err':
// not results
continue;
default:
$severity = 'UNKNOWN RESULT TYPE: '.$child->getName();
break 2;
}
}
return $severity;
}
protected function buildMessage($testCase)
{
$tracePos = -1;
$msg = $this->getMessageTrace($testCase);
if ('' !== $msg) {
//strip trace
$trPos = strrpos($msg, "\n\n");
if (false !== $trPos) {
$tracePos = $trPos;
$msg = substr($msg, 0, $trPos);
}
}
if ('' === $msg) {
$msg = $testCase['class'].'::'.$testCase['name'];
};
$testCase['_tracePos'] = $tracePos; // will be converted to string
return $msg;
}
protected function getOutput($testCase) {
return (string)$testCase->{'system-out'};
}
protected function buildTrace($testCase)
{
if (!is_int($testCase['_tracePos'])) {
$this->buildMessage($testCase);
}
if ($testCase['_tracePos'] >= 0) {
$stackStr = substr($this->getMessageTrace($testCase), (int)$testCase['_tracePos'] + 2, -1);
$trace = explode("\n", str_replace($this->buildPath, '.', $stackStr));
} else {
$trace = array();
}
return $trace;
}
private function getMessageTrace($testCase) {
$msg = '';
foreach($testCase as $child) {
switch ($child->getName()) {
case 'system-out':
case 'system-err':
// not results
continue;
default:
$msg = (string)$child['message']; // according to xsd
if ('' === $msg) {
$msg = (string)$child;
}
break 2;
}
}
return $msg;
}
}

View file

@ -0,0 +1,110 @@
<?php
namespace PHPCensor\Plugin\Util\TestResultParsers;
use PHPCensor\Builder;
/**
* Class Codeception
*
* @author Adam Cooper <adam@networkpie.co.uk>
*/
class Codeception implements ParserInterface
{
protected $builder;
protected $resultsXml;
protected $results;
protected $totalTests;
protected $totalTimeTaken;
protected $totalFailures;
protected $totalErrors;
/**
* @param Builder $builder
* @param $resultsXml
*/
public function __construct(Builder $builder, $resultsXml)
{
$this->builder = $builder;
$this->resultsXml = $resultsXml;
$this->totalTests = 0;
}
/**
* @return array An array of key/value pairs for storage in the plugins result metadata
*/
public function parse()
{
$rtn = [];
$this->results = new \SimpleXMLElement($this->resultsXml);
// calculate total results
foreach ($this->results->testsuite as $test_suite) {
$this->totalTests += (int)$test_suite['tests'];
$this->totalTimeTaken += (float)$test_suite['time'];
$this->totalFailures += (int)$test_suite['failures'];
$this->totalErrors += (int)$test_suite['errors'];
foreach ($test_suite->testcase as $test_case) {
$test_result = [
'suite' => (string)$test_suite['name'],
'file' => str_replace($this->builder->buildPath, '/', (string) $test_case['file']),
'name' => (string)$test_case['name'],
'feature' => (string)$test_case['feature'],
'assertions' => (int)$test_case['assertions'],
'time' => (float)$test_case['time']
];
if (isset($test_case['class'])) {
$test_result['class'] = (string) $test_case['class'];
}
// PHPUnit testcases does not have feature field. Use class::method instead
if (!$test_result['feature']) {
$test_result['feature'] = sprintf('%s::%s', $test_result['class'], $test_result['name']);
}
if (isset($test_case->failure) || isset($test_case->error)) {
$test_result['pass'] = false;
$test_result['message'] = isset($test_case->failure) ? (string)$test_case->failure : (string)$test_case->error;
} else {
$test_result['pass'] = true;
}
$rtn[] = $test_result;
}
}
return $rtn;
}
/**
* Get the total number of tests performed.
*
* @return int
*/
public function getTotalTests()
{
return $this->totalTests;
}
/**
* The time take to complete all tests
*
* @return mixed
*/
public function getTotalTimeTaken()
{
return $this->totalTimeTaken;
}
/**
* A count of the test failures
*
* @return mixed
*/
public function getTotalFailures()
{
return $this->totalFailures + $this->totalErrors;
}
}

View file

@ -0,0 +1,15 @@
<?php
namespace PHPCensor\Plugin\Util\TestResultParsers;
interface ParserInterface
{
/**
* @return array An array of key/value pairs for storage in the plugins result metadata
*/
public function parse();
public function getTotalTests();
public function getTotalTimeTaken();
public function getTotalFailures();
}

55
src/Plugin/Wipe.php Normal file
View file

@ -0,0 +1,55 @@
<?php
namespace PHPCensor\Plugin;
use PHPCensor\Builder;
use PHPCensor\Model\Build;
use PHPCensor\Plugin;
/**
* Wipe Plugin - Wipes a folder
*
* @author Claus Due <claus@namelesscoder.net>
*/
class Wipe extends Plugin
{
protected $directory;
/**
* @return string
*/
public static function pluginName()
{
return 'wipe';
}
/**
* {@inheritdoc}
*/
public function __construct(Builder $builder, Build $build, array $options = [])
{
parent::__construct($builder, $build, $options);
$path = $this->builder->buildPath;
$this->directory = isset($options['directory']) ? $this->builder->interpolate($options['directory']) : $path;
}
/**
* Wipes a directory's contents
*/
public function execute()
{
$build = $this->builder->buildPath;
if ($this->directory == $build || empty($this->directory)) {
return true;
}
if (is_dir($this->directory)) {
$cmd = 'rm -Rf "%s"';
return $this->builder->executeCommand($cmd, $this->directory);
}
return true;
}
}

196
src/Plugin/Xmpp.php Normal file
View file

@ -0,0 +1,196 @@
<?php
namespace PHPCensor\Plugin;
use PHPCensor\Builder;
use PHPCensor\Model\Build;
use PHPCensor\Plugin;
/**
* XMPP Notification - Send notification for successful or failure build
*
* @author Alexandre Russo <dev.github@ange7.com>
*/
class XMPP extends Plugin
{
protected $directory;
/**
* @var string, username of sender account xmpp
*/
protected $username;
/**
* @var string, alias server of sender account xmpp
*/
protected $server;
/**
* @var string, password of sender account xmpp
*/
protected $password;
/**
* @var string, alias for sender
*/
protected $alias;
/**
* @var string, use tls
*/
protected $tls;
/**
* @var array, list of recipients xmpp accounts
*/
protected $recipients;
/**
* @var string, mask to format date
*/
protected $dateFormat;
/**
* @return string
*/
public static function pluginName()
{
return 'xmpp';
}
/**
* {@inheritdoc}
*/
public function __construct(Builder $builder, Build $build, array $options = [])
{
parent::__construct($builder, $build, $options);
$this->username = '';
$this->password = '';
$this->server = '';
$this->alias = '';
$this->recipients = [];
$this->tls = false;
$this->dateFormat = '%c';
/*
* Set recipients list
*/
if (!empty($options['recipients'])) {
if (is_string($options['recipients'])) {
$this->recipients = [$options['recipients']];
} elseif (is_array($options['recipients'])) {
$this->recipients = $options['recipients'];
}
}
}
/**
* Get config format for sendxmpp config file
*
* @return string
*/
protected function getConfigFormat()
{
$conf = $this->username;
if (!empty($this->server)) {
$conf .= ';'.$this->server;
}
$conf .= ' '.$this->password;
if (!empty($this->alias)) {
$conf .= ' '.$this->alias;
}
return $conf;
}
/**
* Find config file for sendxmpp binary (default is .sendxmpprc)
*/
public function findConfigFile()
{
if (file_exists($this->builder->buildPath . DIRECTORY_SEPARATOR . '.sendxmpprc')) {
if (md5(file_get_contents($this->builder->buildPath . DIRECTORY_SEPARATOR . '.sendxmpprc'))
!== md5($this->getConfigFormat())) {
return null;
}
return true;
}
return null;
}
/**
* Send notification message.
*/
public function execute()
{
$sendxmpp = $this->findBinary('sendxmpp');
/*
* Without recipients we can't send notification
*/
if (count($this->recipients) == 0) {
return false;
}
/*
* Try to build conf file
*/
$config_file = $this->builder->buildPath . DIRECTORY_SEPARATOR . '.sendxmpprc';
if (is_null($this->findConfigFile())) {
file_put_contents($config_file, $this->getConfigFormat());
chmod($config_file, 0600);
}
/*
* Enabled ssl for connection
*/
$tls = '';
if ($this->tls) {
$tls = ' -t';
}
$message_file = $this->builder->buildPath . DIRECTORY_SEPARATOR . uniqid('xmppmessage');
if ($this->buildMessage($message_file) === false) {
return false;
}
/*
* Send XMPP notification for all recipients
*/
$cmd = $sendxmpp . "%s -f %s -m %s %s";
$recipients = implode(' ', $this->recipients);
$success = $this->builder->executeCommand($cmd, $tls, $config_file, $message_file, $recipients);
print $this->builder->getLastOutput();
/*
* Remove temp message file
*/
$this->builder->executeCommand("rm -rf ".$message_file);
return $success;
}
/**
* @param $message_file
* @return int
*/
protected function buildMessage($message_file)
{
if ($this->build->isSuccessful()) {
$message = "✔ [".$this->build->getProjectTitle()."] Build #" . $this->build->getId()." successful";
} else {
$message = "✘ [".$this->build->getProjectTitle()."] Build #" . $this->build->getId()." failure";
}
$message .= ' ('.strftime($this->dateFormat).')';
return file_put_contents($message_file, $message);
}
}