From 8ba7f1f9dc4492a6d258d894cac710e53b01b771 Mon Sep 17 00:00:00 2001 From: Adirelle Date: Fri, 24 Apr 2015 19:33:13 +0200 Subject: [PATCH] Track and display the build progression, for each stages and plugins. Translations for the build summary. Closed #944 --- PHPCI/Languages/lang.en.php | 11 +++ PHPCI/Languages/lang.fr.php | 11 +++ PHPCI/Plugin/Util/Executor.php | 109 +++++++++++++++++----- PHPCI/Plugin/Util/Factory.php | 8 +- PHPCI/Store/BuildStore.php | 4 +- Tests/PHPCI/Plugin/Util/ExecutorTest.php | 15 ++- public/assets/js/build-plugins/summary.js | 67 +++++++++++++ 7 files changed, 192 insertions(+), 33 deletions(-) create mode 100644 public/assets/js/build-plugins/summary.js diff --git a/PHPCI/Languages/lang.en.php b/PHPCI/Languages/lang.en.php index d6c48754..9729d746 100644 --- a/PHPCI/Languages/lang.en.php +++ b/PHPCI/Languages/lang.en.php @@ -295,6 +295,17 @@ PHPCI', 'search_packagist_for_more' => 'Search Packagist for more packages', 'search' => 'Search »', + // Summary plugin + 'build-summary' => 'Summary', + 'stage' => 'Stage', + 'duration' => 'Duration', + 'plugin' => 'Plugin', + 'stage_setup' => 'Setup', + 'stage_test' => 'Test', + 'stage_complete' => 'Complete', + 'stage_success' => 'Success', + 'stage_failure' => 'Failure', + // Installer 'installation_url' => 'PHPCI Installation URL', 'db_host' => 'Database Host', diff --git a/PHPCI/Languages/lang.fr.php b/PHPCI/Languages/lang.fr.php index f1ad7129..dcde8424 100644 --- a/PHPCI/Languages/lang.fr.php +++ b/PHPCI/Languages/lang.fr.php @@ -286,6 +286,17 @@ PHPCI', 'search_packagist_for_more' => 'Rechercher sur Packagist pour trouver plus de paquets', 'search' => 'Rechercher »', + // Summary plugin + 'build-summary' => 'Résumé', + 'stage' => 'Étape', + 'duration' => 'Durée', + 'plugin' => 'Plugin', + 'stage_setup' => 'Préparation', + 'stage_test' => 'Test', + 'stage_complete' => 'Terminé', + 'stage_success' => 'Succes', + 'stage_failure' => 'Échec', + // Installer 'installation_url' => 'URL d\'installation de PHPCI', 'db_host' => 'Hôte de la BDD', diff --git a/PHPCI/Plugin/Util/Executor.php b/PHPCI/Plugin/Util/Executor.php index 8c74707e..3ca244af 100644 --- a/PHPCI/Plugin/Util/Executor.php +++ b/PHPCI/Plugin/Util/Executor.php @@ -2,8 +2,12 @@ namespace PHPCI\Plugin\Util; +use b8\Store\Factory as StoreFactory; +use Exception; use PHPCI\Helper\Lang; -use \PHPCI\Logging\BuildLogger; +use PHPCI\Logging\BuildLogger; +use PHPCI\Model\Build; +use PHPCI\Store\BuildStore; /** * Plugin Executor - Runs the configured plugins for a given build stage. @@ -21,14 +25,20 @@ class Executor */ protected $pluginFactory; + /** + * @var BuildStore + */ + protected $store; + /** * @param Factory $pluginFactory * @param BuildLogger $logger */ - public function __construct(Factory $pluginFactory, BuildLogger $logger) + public function __construct(Factory $pluginFactory, BuildLogger $logger, BuildStore $store = null) { $this->pluginFactory = $pluginFactory; $this->logger = $logger; + $this->store = $store ?: StoreFactory::getStore('Build'); } /** @@ -48,22 +58,29 @@ class Executor foreach ($config[$stage] as $plugin => $options) { $this->logger->log(Lang::get('running_plugin', $plugin)); - // Try and execute it: - if ($this->executePlugin($plugin, $options)) { - // Execution was successful: - $this->logger->logSuccess(Lang::get('plugin_success')); - } elseif ($stage == 'setup') { - // If we're in the "setup" stage, execution should not continue after - // a plugin has failed: - throw new \Exception('Plugin failed: ' . $plugin); - } else { - // If we're in the "test" stage and the plugin is not allowed to fail, - // then mark the build as failed: - if ($stage == 'test' && (!isset($options['allow_failures']) || !$options['allow_failures'])) { - $success = false; - } + $this->setPluginStatus($stage, $plugin, Build::STATUS_RUNNING); + // Try and execute it + if ($this->executePlugin($plugin, $options)) { + // Execution was successful + $this->logger->logSuccess(Lang::get('plugin_success')); + $this->setPluginStatus($stage, $plugin, Build::STATUS_SUCCESS); + } else { + // Execution failed $this->logger->logFailure(Lang::get('plugin_failed')); + $this->setPluginStatus($stage, $plugin, Build::STATUS_FAILED); + + if ($stage === 'setup') { + // If we're in the "setup" stage, execution should not continue after + // a plugin has failed: + throw new Exception('Plugin failed: ' . $plugin); + } elseif ($stage === 'test') { + // 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'])) { + $success = false; + } + } } } @@ -91,20 +108,62 @@ class Executor return false; } - $rtn = true; - - // Try running it: try { + // Build and run it $obj = $this->pluginFactory->buildPlugin($class, $options); - - if (!$obj->execute()) { - $rtn = false; - } + return $obj->execute(); } catch (\Exception $ex) { $this->logger->logFailure(Lang::get('exception') . $ex->getMessage(), $ex); - $rtn = false; + 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] = array(); } - return $rtn; + $summary[$stage][$plugin]['status'] = $status; + + if ($status === Build::STATUS_RUNNING) { + $summary[$stage][$plugin]['started'] = time(); + } elseif ($status >= Build::STATUS_SUCCESS) { + $summary[$stage][$plugin]['ended'] = time(); + } + + $this->setBuildSummary($summary); + } + + /** + * Fetch the summary data of the current build. + * + * @return array + */ + private function getBuildSummary() + { + $build = $this->pluginFactory->getResourceFor('PHPCI\Model\Build'); + $metas = $this->store->getMeta('plugin-summary', $build->getProjectId(), $build->getId()); + return isset($metas[0]['meta_value']) ? $metas[0]['meta_value'] : array(); + } + + /** + * Sets the summary data of the current build. + * + * @param array summary + */ + private function setBuildSummary($summary) + { + $build = $this->pluginFactory->getResourceFor('PHPCI\Model\Build'); + $this->store->setMeta($build->getProjectId(), $build->getId(), 'plugin-summary', json_encode($summary)); } } diff --git a/PHPCI/Plugin/Util/Factory.php b/PHPCI/Plugin/Util/Factory.php index 76de2b7d..30c68340 100644 --- a/PHPCI/Plugin/Util/Factory.php +++ b/PHPCI/Plugin/Util/Factory.php @@ -150,11 +150,11 @@ class Factory } /** - * @param null $type - * @param null $name - * @return null + * @param string $type + * @param string $name + * @return mixed */ - private function getResourceFor($type = null, $name = null) + public function getResourceFor($type = null, $name = null) { $fullId = $this->getInternalID($type, $name); if (isset($this->container[$fullId])) { diff --git a/PHPCI/Store/BuildStore.php b/PHPCI/Store/BuildStore.php index 7519a12f..9974c7b3 100644 --- a/PHPCI/Store/BuildStore.php +++ b/PHPCI/Store/BuildStore.php @@ -165,7 +165,9 @@ class BuildStore extends BuildStoreBase $stmt->bindValue(':projectId', (int)$projectId, \PDO::PARAM_INT); $stmt->bindValue(':buildId', (int)$buildId, \PDO::PARAM_INT); $stmt->bindValue(':numResults', (int)$numResults, \PDO::PARAM_INT); - $stmt->bindValue(':branch', $branch, \PDO::PARAM_STR); + if (!is_null($branch)) { + $stmt->bindValue(':branch', $branch, \PDO::PARAM_STR); + } if ($stmt->execute()) { $rtn = $stmt->fetchAll(\PDO::FETCH_ASSOC); diff --git a/Tests/PHPCI/Plugin/Util/ExecutorTest.php b/Tests/PHPCI/Plugin/Util/ExecutorTest.php index 8501f209..e9083cec 100644 --- a/Tests/PHPCI/Plugin/Util/ExecutorTest.php +++ b/Tests/PHPCI/Plugin/Util/ExecutorTest.php @@ -24,12 +24,19 @@ class ExecutorTest extends \PHPUnit_Framework_TestCase protected $mockFactory; + protected $mockStore; + protected function setUp() { parent::setUp(); $this->mockBuildLogger = $this->prophesize('\PHPCI\Logging\BuildLogger'); $this->mockFactory = $this->prophesize('\PHPCI\Plugin\Util\Factory'); - $this->testedExecutor = new Executor($this->mockFactory->reveal(), $this->mockBuildLogger->reveal()); + $this->mockStore = $this->prophesize('\PHPCI\Store\BuildStore'); + $this->testedExecutor = new Executor( + $this->mockFactory->reveal(), + $this->mockBuildLogger->reveal(), + $this->mockStore->reveal() + ); } public function testExecutePlugin_AssumesPHPCINamespaceIfNoneGiven() @@ -61,11 +68,13 @@ class ExecutorTest extends \PHPUnit_Framework_TestCase { $options = array(); $pluginName = 'PhpUnit'; + $build = new \PHPCI\Model\Build(); $mockPlugin = $this->prophesize('PHPCI\Plugin'); $mockPlugin->execute()->shouldBeCalledTimes(1); $this->mockFactory->buildPlugin(Argument::any(), Argument::any())->willReturn($mockPlugin->reveal()); + $this->mockFactory->getResourceFor('PHPCI\Model\Build')->willReturn($build); $this->testedExecutor->executePlugin($pluginName, $options); } @@ -119,6 +128,7 @@ class ExecutorTest extends \PHPUnit_Framework_TestCase { $phpUnitPluginOptions = array(); $behatPluginOptions = array(); + $build = new \PHPCI\Model\Build(); $config = array( 'stageOne' => array( @@ -134,7 +144,7 @@ class ExecutorTest extends \PHPUnit_Framework_TestCase $this->mockFactory->buildPlugin($pluginNamespace . 'PhpUnit', $phpUnitPluginOptions) ->willReturn($mockPhpUnitPlugin->reveal()); - + $this->mockFactory->getResourceFor('PHPCI\Model\Build')->willReturn($build); $mockBehatPlugin = $this->prophesize('PHPCI\Plugin'); $mockBehatPlugin->execute()->shouldBeCalledTimes(1)->willReturn(true); @@ -142,7 +152,6 @@ class ExecutorTest extends \PHPUnit_Framework_TestCase $this->mockFactory->buildPlugin($pluginNamespace . 'Behat', $behatPluginOptions) ->willReturn($mockBehatPlugin->reveal()); - $this->testedExecutor->executePlugins($config, 'stageOne'); } diff --git a/public/assets/js/build-plugins/summary.js b/public/assets/js/build-plugins/summary.js new file mode 100644 index 00000000..27b4ba2d --- /dev/null +++ b/public/assets/js/build-plugins/summary.js @@ -0,0 +1,67 @@ +var SummaryPlugin = ActiveBuild.UiPlugin.extend({ + id: 'build-summary', + css: 'col-lg-6 col-md-12 col-sm-12 col-xs-12', + title: Lang.get('build-summary'), + box: true, + statusIcons: [ 'fa-clock-o', 'fa-cogs', 'fa-check', 'fa-remove' ], + statusLabels: [ Lang.get('pending'), Lang.get('running'), Lang.get('successful'), Lang.get('failed') ], + statusClasses: ['text-blue', 'text-yellow', 'text-green', 'text-red'], + + register: function() { + var self = this; + var query = ActiveBuild.registerQuery('plugin-summary', 5, {key: 'plugin-summary'}) + + $(window).on('plugin-summary', function(data) { + self.onUpdate(data); + }); + + $(window).on('build-updated', function() { + query(); + }); + }, + + render: function() { + return $( + '
' + + '' + + '' + + '' + + '' + + '' + + '
'+Lang.get('stage')+''+Lang.get('plugin')+''+Lang.get('status')+''+Lang.get('duration')+' (s)
' + ); + }, + + onUpdate: function(e) { + if (!e.queryData) { + $('#build-summary').hide(); + return; + } + + var tbody = $('#plugin-summary tbody'), + summary = e.queryData[0].meta_value; + tbody.empty(); + + for(var stage in summary) { + for(var plugin in summary[stage]) { + var data = summary[stage][plugin], + duration = data.started ? ((data.ended || Math.floor(Date.now()/1000)) - data.started) : '-'; + tbody.append( + '' + + '' + Lang.get('stage_'+stage) + '' + + '' + plugin + '' + + '' + + ' ' + + this.statusLabels[data.status] + + '' + + '' + duration + '' + + '' + ); + } + } + + $('#build-summary').show(); + } +}); + +ActiveBuild.registerPlugin(new SummaryPlugin());