diff --git a/PHPCI/BuildFactory.php b/PHPCI/BuildFactory.php index e4628434..0dd53058 100644 --- a/PHPCI/BuildFactory.php +++ b/PHPCI/BuildFactory.php @@ -22,7 +22,7 @@ class BuildFactory { /** * Takes a generic build and returns a type-specific build model. - * @return PHPCI\Model\Build\LocalBuild|PHPCI\Model\Build\GithubBuild|PHPCI\Model\Build\BitbucketBuild + * @return \PHPCI\Model\Build\LocalBuild|\PHPCI\Model\Build\GithubBuild|\PHPCI\Model\Build\BitbucketBuild */ public static function getBuild(Build $base) { diff --git a/PHPCI/Builder.php b/PHPCI/Builder.php index 6fa20be0..11e46082 100644 --- a/PHPCI/Builder.php +++ b/PHPCI/Builder.php @@ -1,72 +1,70 @@ -*/ -class Builder + * PHPCI Build Runner + * @author Dan Cryer + */ +class Builder implements LoggerAwareInterface { /** - * @var string - */ + * @var string + */ public $buildPath; /** - * @var string[] - */ - public $ignore = array(); + * @var string[] + */ + public $ignore = array(); /** - * @var string - */ + * @var string + */ protected $ciDir; /** - * @var string - */ + * @var string + */ protected $directory; /** - * @var bool - */ - protected $success = true; + * @var bool + */ + protected $success = true; /** - * @var string - */ - protected $log = ''; + * @var bool + */ + protected $verbose = true; /** - * @var bool - */ - protected $verbose = true; - - /** - * @var \PHPCI\Model\Build - */ + * @var \PHPCI\Model\Build + */ protected $build; /** - * @var callable - */ - protected $logCallback; + * @var LoggerInterface + */ + protected $logger; /** - * @var array - */ + * @var array + */ protected $config; /** @@ -75,7 +73,7 @@ class Builder protected $lastOutput; /** - * An array of key => value pairs that will be used for + * An array of key => value pairs that will be used for * interpolation and environment variables * @var array * @see setInterpolationVars() @@ -93,30 +91,32 @@ class Builder public $quiet = false; /** - * Set up the builder. - * @param \PHPCI\Model\Build - * @param callable - */ - public function __construct(Build $build, \Closure $logCallback) + * Set up the builder. + * @param \PHPCI\Model\Build $build + * @param LoggerInterface $logger + */ + public function __construct(Build $build, $logger = null) { + if ($logger) { + $this->setLogger($logger); + } $this->build = $build; $this->store = Store\Factory::getStore('Build'); - $this->logCallback = $logCallback; } /** - * Set the config array, as read from phpci.yml - * @param array - */ + * Set the config array, as read from phpci.yml + * @param array + */ public function setConfigArray(array $config) { $this->config = $config; } /** - * Access a variable from the phpci.yml file. - * @param string - */ + * Access a variable from the phpci.yml file. + * @param string + */ public function getConfig($key) { $rtn = null; @@ -147,8 +147,8 @@ class Builder } /** - * Run the active build. - */ + * Run the active build. + */ public function execute() { // Update the build in the database, ping any external services. @@ -164,7 +164,6 @@ class Builder // Run the core plugin stages: foreach (array('setup', 'test', 'complete') as $stage) { $this->executePlugins($stage); - $this->log(''); } // Failed build? Execute failure plugins and then mark the build as failed. @@ -181,10 +180,10 @@ class Builder } } catch (\Exception $ex) { - $this->logFailure($ex->getMessage()); + $this->logFailure($ex->getMessage(), $ex); $this->build->setStatus(3); } - + // Clean up: $this->log('Removing build.'); shell_exec(sprintf('rm -Rf "%s"', $this->buildPath)); @@ -192,26 +191,25 @@ class Builder // Update the build in the database, ping any external services, etc. $this->build->sendStatusPostback(); $this->build->setFinished(new \DateTime()); - $this->build->setLog($this->log); $this->store->save($this->build); } /** - * Used by this class, and plugins, to execute shell commands. - */ + * Used by this class, and plugins, to execute shell commands. + */ public function executeCommand() { $command = call_user_func_array('sprintf', func_get_args()); if (!$this->quiet) { - $this->log('Executing: ' . $command, ' '); + $this->log('Executing: ' . $command); } $status = 0; exec($command, $this->lastOutput, $status); if (!empty($this->lastOutput) && ($this->verbose || $status != 0)) { - $this->log($this->lastOutput, ' '); + $this->log($this->lastOutput); } @@ -233,43 +231,62 @@ class Builder } /** - * Add an entry to the build log. - * @param string|string[] - * @param string - */ - public function log($message, $prefix = '') + * Add an entry to the build log. + * @param string|string[] $message + * @param string $level + * @param mixed[] $context + */ + public function log($message, $level = LogLevel::INFO, $context = array()) { + // Skip if no logger has been loaded. + if (!$this->logger) { + return; + } + if (!is_array($message)) { $message = array($message); } - foreach ($message as $item) { - call_user_func_array($this->logCallback, array($prefix . $item)); - $this->log .= $prefix . $item . PHP_EOL; - } + // The build is added to the context so the logger can use + // details from it if required. + $context['build'] = $this; - $this->build->setLog($this->log); - $this->store->save($this->build); + foreach ($message as $item) { + $this->logger->log($level, $item, $context); + } } /** - * Add a success-coloured message to the log. - * @param string - */ + * Add a success-coloured message to the log. + * @param string + */ public function logSuccess($message) { $this->log("\033[0;32m" . $message . "\033[0m"); } /** - * Add a failure-coloured message to the log. - * @param string - */ - public function logFailure($message) + * Add a failure-coloured message to the log. + * @param string $message + * @param \Exception $exception The exception that caused the error. + */ + public function logFailure($message, \Exception $exception = null) { - $this->log("\033[0;31m" . $message . "\033[0m"); + $context = array(); + + // The psr3 log interface stipulates that exceptions should be passed + // as the exception key in the context array. + if ($exception) { + $context['exception'] = $exception; + } + + $this->log( + "\033[0;31m" . $message . "\033[0m", + LogLevel::ERROR, + $context + ); } - + /** * Replace every occurance of the interpolation vars in the given string * Example: "This is build %PHPCI_BUILD%" => "This is build 182" @@ -284,7 +301,7 @@ class Builder } /** - * Sets the variables that will be used for interpolation. This must be run + * Sets the variables that will be used for interpolation. This must be run * from setupBuild() because prior to that, we don't know the buildPath */ protected function setInterpolationVars() @@ -294,9 +311,11 @@ class Builder $this->interpolation_vars['%COMMIT%'] = $this->build->getCommitId(); $this->interpolation_vars['%PROJECT%'] = $this->build->getProjectId(); $this->interpolation_vars['%BUILD%'] = $this->build->getId(); - $this->interpolation_vars['%PROJECT_TITLE%'] = $this->getBuildProjectTitle(); + $this->interpolation_vars['%PROJECT_TITLE%'] = $this->getBuildProjectTitle( + ); $this->interpolation_vars['%BUILD_PATH%'] = $this->buildPath; - $this->interpolation_vars['%BUILD_URI%'] = PHPCI_URL . "build/view/" . $this->build->getId(); + $this->interpolation_vars['%BUILD_URI%'] = PHPCI_URL . "build/view/" . $this->build->getId( + ); $this->interpolation_vars['%PHPCI_COMMIT%'] = $this->interpolation_vars['%COMMIT%']; $this->interpolation_vars['%PHPCI_PROJECT%'] = $this->interpolation_vars['%PROJECT%']; $this->interpolation_vars['%PHPCI_BUILD%'] = $this->interpolation_vars['%BUILD%']; @@ -305,25 +324,28 @@ class Builder $this->interpolation_vars['%PHPCI_BUILD_URI%'] = $this->interpolation_vars['%BUILD_URI%']; putenv('PHPCI=1'); - putenv('PHPCI_COMMIT='.$this->interpolation_vars['%COMMIT%']); - putenv('PHPCI_PROJECT='.$this->interpolation_vars['%PROJECT%']); - putenv('PHPCI_BUILD='.$this->interpolation_vars['%BUILD%']); - putenv('PHPCI_PROJECT_TITLE='.$this->interpolation_vars['%PROJECT_TITLE%']); - putenv('PHPCI_BUILD_PATH='.$this->interpolation_vars['%BUILD_PATH%']); - putenv('PHPCI_BUILD_URI='.$this->interpolation_vars['%BUILD_URI%']); + putenv('PHPCI_COMMIT=' . $this->interpolation_vars['%COMMIT%']); + putenv('PHPCI_PROJECT=' . $this->interpolation_vars['%PROJECT%']); + putenv('PHPCI_BUILD=' . $this->interpolation_vars['%BUILD%']); + putenv( + 'PHPCI_PROJECT_TITLE=' . $this->interpolation_vars['%PROJECT_TITLE%'] + ); + putenv('PHPCI_BUILD_PATH=' . $this->interpolation_vars['%BUILD_PATH%']); + putenv('PHPCI_BUILD_URI=' . $this->interpolation_vars['%BUILD_URI%']); } - + /** - * Set up a working copy of the project for building. - */ + * Set up a working copy of the project for building. + */ protected function setupBuild() { - $buildId = 'project' . $this->build->getProject()->getId() . '-build' . $this->build->getId(); - $this->ciDir = dirname(__FILE__) . '/../'; - $this->buildPath = $this->ciDir . 'build/' . $buildId . '/'; - + $buildId = 'project' . $this->build->getProject()->getId( + ) . '-build' . $this->build->getId(); + $this->ciDir = dirname(__FILE__) . '/../'; + $this->buildPath = $this->ciDir . 'build/' . $buildId . '/'; + $this->setInterpolationVars(); - + // Create a working copy of the project: if (!$this->build->createWorkingCopy($this, $this->buildPath)) { throw new \Exception('Could not create a working copy.'); @@ -344,17 +366,20 @@ class Builder } /** - * Execute a the appropriate set of plugins for a given build stage. - */ + * Execute a the appropriate set of plugins for a given build stage. + */ protected function executePlugins($stage) { // Ignore any stages for which we don't have plugins set: - if (!array_key_exists($stage, $this->config) || !is_array($this->config[$stage])) { + if (!array_key_exists( + $stage, + $this->config + ) || !is_array($this->config[$stage]) + ) { return; } foreach ($this->config[$stage] as $plugin => $options) { - $this->log(''); $this->log('RUNNING PLUGIN: ' . $plugin); // Is this plugin allowed to fail? @@ -392,7 +417,7 @@ class Builder $class = 'PHPCI\\Plugin\\' . str_replace(' ', '', $class); if (!class_exists($class)) { - $this->logFailure('Plugin does not exist: ' . $plugin); + $this->logFailure('Plugin does not exist: ' . $plugin, $ex); return false; } @@ -406,7 +431,7 @@ class Builder $rtn = false; } } catch (\Exception $ex) { - $this->logFailure('EXCEPTION: ' . $ex->getMessage()); + $this->logFailure('EXCEPTION: ' . $ex->getMessage(), $ex); $rtn = false; } @@ -445,4 +470,25 @@ class Builder return null; } + + /** + * Sets a logger instance on the object + * + * @param LoggerInterface $logger + * @return null + */ + public function setLogger(LoggerInterface $logger) + { + $this->logger = $logger; + } + + /** + * returns the logger attached to this builder. + * + * @return LoggerInterface + */ + public function getLogger() + { + return $this->logger; + } } diff --git a/PHPCI/Command/RunCommand.php b/PHPCI/Command/RunCommand.php index dc8e21d2..2acf83dc 100644 --- a/PHPCI/Command/RunCommand.php +++ b/PHPCI/Command/RunCommand.php @@ -9,6 +9,11 @@ namespace PHPCI\Command; +use Monolog\Logger; +use PHPCI\Helper\BuildDBLogHandler; +use PHPCI\Helper\LoggedBuildContextTidier; +use PHPCI\Helper\OutputLogHandler; +use Psr\Log\LoggerAwareInterface; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; @@ -26,6 +31,11 @@ use PHPCI\BuildFactory; */ class RunCommand extends Command { + /** + * @var OutputInterface + */ + protected $output; + protected function configure() { $this @@ -34,32 +44,44 @@ class RunCommand extends Command } /** - * Pulls all pending builds from the database and runs them. - */ + * Pulls all pending builds from the database and runs them. + */ protected function execute(InputInterface $input, OutputInterface $output) { $this->output = $output; - $store = Factory::getStore('Build'); + $logger = new Logger("BuildLog"); + + $store = Factory::getStore('Build'); $result = $store->getByStatus(0); $builds = 0; + // For verbose mode we want to output all informational and above + // messages to the symphony output interface. + if ($input->getOption('verbose')) { + $logger->pushHandler( + new OutputLogHandler($this->output, Logger::INFO) + ); + } + + $logger->pushProcessor(new LoggedBuildContextTidier()); + foreach ($result['items'] as $build) { $builds++; $build = BuildFactory::getBuild($build); - if ($input->getOption('verbose')) { - $builder = new Builder($build, function ($log) { - $this->output->writeln($log); - }); - } else { - $builder = new Builder($build, function () { - // Empty stub function. - }); - } + // Logging relevant to this build should be stored + // against the build itself. + $buildDbLog = new BuildDBLogHandler($build, Logger::INFO); + $logger->pushHandler($buildDbLog); + $builder = new Builder($build, $logger); $builder->execute(); + + // After execution we no longer want to record the information + // back to this specific build so the handler should be removed. + $logger->popHandler($buildDbLog); } return $builds; diff --git a/PHPCI/Helper/BuildDBLogHandler.php b/PHPCI/Helper/BuildDBLogHandler.php new file mode 100644 index 00000000..6ae34379 --- /dev/null +++ b/PHPCI/Helper/BuildDBLogHandler.php @@ -0,0 +1,35 @@ +build = $build; + // We want to add to any existing saved log information. + $this->logValue = $build->getLog(); + } + + protected function write(array $record) + { + $this->logValue .= (string)$record['formatted']; + $this->build->setLog($this->logValue); + } +} \ No newline at end of file diff --git a/PHPCI/Helper/LoggedBuildContextTidier.php b/PHPCI/Helper/LoggedBuildContextTidier.php new file mode 100644 index 00000000..2a8a5e72 --- /dev/null +++ b/PHPCI/Helper/LoggedBuildContextTidier.php @@ -0,0 +1,31 @@ +tidyLoggedBuildContext(func_get_arg(0)); + } + + /** + * Removes the build object from the logged record and adds the ID as + * this is more useful to display. + * + * @param array $logRecord + * @return array + */ + protected function tidyLoggedBuildContext(array $logRecord) + { + if (isset($logRecord['context']['build'])) { + $build = $logRecord['context']['build']; + if ($build instanceof Build) { + $logRecord['context']['buildID'] = $build->getId(); + unset($logRecord['context']['build']); + } + } + return $logRecord; + } +} \ No newline at end of file diff --git a/PHPCI/Helper/OutputLogHandler.php b/PHPCI/Helper/OutputLogHandler.php new file mode 100644 index 00000000..993f1c33 --- /dev/null +++ b/PHPCI/Helper/OutputLogHandler.php @@ -0,0 +1,33 @@ +output = $output; + } + + + protected function write(array $record) + { + $this->output->writeln((string)$record['formatted']); + } + + +} \ No newline at end of file diff --git a/PHPCI/Plugin/Atoum.php b/PHPCI/Plugin/Atoum.php index 8d767b41..c5a28eb3 100644 --- a/PHPCI/Plugin/Atoum.php +++ b/PHPCI/Plugin/Atoum.php @@ -56,11 +56,11 @@ class Atoum implements \PHPCI\Plugin if (count(preg_grep("/Success \(/", $output)) == 0) { $status = false; - $this->phpci->log($output, ' '); + $this->phpci->log($output); } if (count($output) == 0) { $status = false; - $this->phpci->log("No test have been performed!", ' '); + $this->phpci->log("No test have been performed!"); } return $status; diff --git a/composer.json b/composer.json index 9e3ff52a..e6fac6cb 100644 --- a/composer.json +++ b/composer.json @@ -27,7 +27,9 @@ "ircmaxell/password-compat": "1.*", "swiftmailer/swiftmailer" : "5.0.*", "symfony/yaml" : "2.*", - "symfony/console" : "2.*" + "symfony/console" : "2.*", + "psr/log": "1.0.0", + "monolog/monolog": "1.6.0" }, "suggest": { diff --git a/composer.lock b/composer.lock index 4921df46..43452ea3 100644 --- a/composer.lock +++ b/composer.lock @@ -3,7 +3,7 @@ "This file locks the dependencies of your project to a known state", "Read more about it at http://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file" ], - "hash": "04cca0ac809838a65555d04534cc95ae", + "hash": "534baabecc11275d5cc7f375eecf738d", "packages": [ { "name": "block8/b8framework", @@ -91,6 +91,106 @@ ], "time": "2013-04-30 19:58:08" }, + { + "name": "monolog/monolog", + "version": "1.6.0", + "source": { + "type": "git", + "url": "https://github.com/Seldaek/monolog.git", + "reference": "f72392d0e6eb855118f5a84e89ac2d257c704abd" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Seldaek/monolog/zipball/f72392d0e6eb855118f5a84e89ac2d257c704abd", + "reference": "f72392d0e6eb855118f5a84e89ac2d257c704abd", + "shasum": "" + }, + "require": { + "php": ">=5.3.0", + "psr/log": "~1.0" + }, + "require-dev": { + "doctrine/couchdb": "dev-master", + "mlehner/gelf-php": "1.0.*", + "raven/raven": "0.5.*" + }, + "suggest": { + "doctrine/couchdb": "Allow sending log messages to a CouchDB server", + "ext-amqp": "Allow sending log messages to an AMQP server (1.0+ required)", + "ext-mongo": "Allow sending log messages to a MongoDB server", + "mlehner/gelf-php": "Allow sending log messages to a GrayLog2 server", + "raven/raven": "Allow sending log messages to a Sentry server" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.6.x-dev" + } + }, + "autoload": { + "psr-0": { + "Monolog": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jordi Boggiano", + "email": "j.boggiano@seld.be", + "homepage": "http://seld.be", + "role": "Developer" + } + ], + "description": "Sends your logs to files, sockets, inboxes, databases and various web services", + "homepage": "http://github.com/Seldaek/monolog", + "keywords": [ + "log", + "logging", + "psr-3" + ], + "time": "2013-07-28 22:38:30" + }, + { + "name": "psr/log", + "version": "1.0.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/log.git", + "reference": "fe0936ee26643249e916849d48e3a51d5f5e278b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/log/zipball/fe0936ee26643249e916849d48e3a51d5f5e278b", + "reference": "fe0936ee26643249e916849d48e3a51d5f5e278b", + "shasum": "" + }, + "type": "library", + "autoload": { + "psr-0": { + "Psr\\Log\\": "" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Common interface for logging libraries", + "keywords": [ + "log", + "psr", + "psr-3" + ], + "time": "2012-12-21 11:40:51" + }, { "name": "swiftmailer/swiftmailer", "version": "v5.0.2", @@ -142,7 +242,7 @@ }, { "name": "symfony/console", - "version": "v2.3.5", + "version": "v2.3.6", "target-dir": "Symfony/Component/Console", "source": { "type": "git", @@ -195,7 +295,7 @@ }, { "name": "symfony/yaml", - "version": "v2.3.5", + "version": "v2.3.6", "target-dir": "Symfony/Component/Yaml", "source": { "type": "git",