diff --git a/PHPCI/Plugin/Composer.php b/PHPCI/Plugin/Composer.php index 70462eba..6be4d281 100644 --- a/PHPCI/Plugin/Composer.php +++ b/PHPCI/Plugin/Composer.php @@ -42,7 +42,7 @@ class Composer implements PHPCI\Plugin, PHPCI\ZeroConfigPlugin $path = $phpci->buildPath; $this->phpci = $phpci; $this->directory = isset($options['directory']) ? $path . '/' . $options['directory'] : $path; - $this->action = isset($options['action']) ? $options['action'] : 'update'; + $this->action = isset($options['action']) ? $options['action'] : 'install'; $this->preferDist = isset($options['prefer_dist']) ? $options['prefer_dist'] : true; } diff --git a/PHPCI/Plugin/PhpUnit.php b/PHPCI/Plugin/PhpUnit.php index ab0cfc58..16de7aa0 100755 --- a/PHPCI/Plugin/PhpUnit.php +++ b/PHPCI/Plugin/PhpUnit.php @@ -12,6 +12,7 @@ namespace PHPCI\Plugin; use PHPCI; use PHPCI\Builder; use PHPCI\Model\Build; +use PHPCI\Plugin\Util\TapParser; /** * PHP Unit Plugin - Allows PHP Unit testing. @@ -23,6 +24,7 @@ class PhpUnit implements PHPCI\Plugin, PHPCI\ZeroConfigPlugin { protected $args; protected $phpci; + protected $build; /** * @var string|string[] $directory The directory (or array of dirs) to run PHPUnit on @@ -58,20 +60,20 @@ class PhpUnit implements PHPCI\Plugin, PHPCI\ZeroConfigPlugin public static function findConfigFile($buildPath) { - if (file_exists($buildPath . '/phpunit.xml')) { - return $buildPath . '/phpunit.xml'; + if (file_exists($buildPath . 'phpunit.xml')) { + return 'phpunit.xml'; } - if (file_exists($buildPath . '/tests/phpunit.xml')) { - return $buildPath . '/tests/phpunit.xml'; + if (file_exists($buildPath . 'tests/phpunit.xml')) { + return 'tests/phpunit.xml'; } - if (file_exists($buildPath . '/phpunit.xml.dist')) { - return $buildPath . '/phpunit.xml.dist'; + if (file_exists($buildPath . 'phpunit.xml.dist')) { + return 'phpunit.xml.dist'; } - if (file_exists($buildPath . '/tests/phpunit.xml.dist')) { - return $buildPath . '/tests/phpunit.xml.dist'; + if (file_exists($buildPath . 'tests/phpunit.xml.dist')) { + return 'tests/phpunit.xml.dist'; } return null; @@ -80,9 +82,9 @@ class PhpUnit implements PHPCI\Plugin, PHPCI\ZeroConfigPlugin public function __construct(Builder $phpci, Build $build, array $options = array()) { $this->phpci = $phpci; + $this->build = $build; - if (!count($options)) { - $this->runFrom = $phpci->buildPath; + if (empty($options['config']) && empty($options['directory'])) { $this->xmlConfigFile = self::findConfigFile($phpci->buildPath); } @@ -118,6 +120,8 @@ class PhpUnit implements PHPCI\Plugin, PHPCI\ZeroConfigPlugin { $success = true; + $this->phpci->logExecOutput(false); + // Run any config files first. This can be either a single value or an array. if ($this->xmlConfigFile !== null) { $success &= $this->runConfigFile($this->xmlConfigFile); @@ -128,6 +132,16 @@ class PhpUnit implements PHPCI\Plugin, PHPCI\ZeroConfigPlugin $success &= $this->runDir($this->directory); } + $output = $this->phpci->getLastOutput(); + $tapParser = new TapParser($output); + $output = $tapParser->parse(); + $failures = $tapParser->getTotalFailures(); + + $this->build->storeMeta('phpunit-errors', $failures); + $this->build->storeMeta('phpunit-data', $output); + + $this->phpci->logExecOutput(true); + return $success; } @@ -150,7 +164,7 @@ class PhpUnit implements PHPCI\Plugin, PHPCI\ZeroConfigPlugin } - $cmd = $phpunit . ' %s -c "%s" ' . $this->coverage . $this->path; + $cmd = $phpunit . ' --tap %s -c "%s" ' . $this->coverage . $this->path; $success = $this->phpci->executeCommand($cmd, $this->args, $this->phpci->buildPath . $configPath); if ($this->runFrom) { @@ -176,7 +190,7 @@ class PhpUnit implements PHPCI\Plugin, PHPCI\ZeroConfigPlugin return false; } - $cmd = $phpunit . ' %s "%s"'; + $cmd = $phpunit . ' --tap %s "%s"'; $success = $this->phpci->executeCommand($cmd, $this->args, $this->phpci->buildPath . $dirPath); chdir($curdir); return $success; diff --git a/PHPCI/Plugin/Util/TapParser.php b/PHPCI/Plugin/Util/TapParser.php new file mode 100644 index 00000000..610e1696 --- /dev/null +++ b/PHPCI/Plugin/Util/TapParser.php @@ -0,0 +1,92 @@ +tapString = trim($tapString); + } + + /** + * Parse a given TAP format string and return an array of tests and their status. + */ + public function parse() + { + // Split up the TAP string into an array of lines, then + // trim all of the lines so there's no leading or trailing whitespace. + $lines = explode("\n", $this->tapString); + $lines = array_map(function ($line) { + return trim($line); + }, $lines); + + // Check TAP version: + $versionLine = array_shift($lines); + + if ($versionLine != 'TAP version 13') { + throw new \Exception('TapParser only supports TAP version 13'); + } + + $matches = array(); + $totalTests = 0; + if (preg_match(self::TEST_COUNTS_PATTERN, $lines[0], $matches)) { + array_shift($lines); + $totalTests = (int)$matches[2]; + } + + if (preg_match(self::TEST_COUNTS_PATTERN, $lines[count($lines) - 1], $matches)) { + array_pop($lines); + $totalTests = (int)$matches[2]; + } + + $rtn = array(); + foreach ($lines as $line) { + $matches = array(); + + if (preg_match(self::TEST_LINE_PATTERN, $line, $matches)) { + $ok = ($matches[1] == 'ok' ? true : false); + + if (!$ok) { + $this->failures++; + } + + $item = array( + 'pass' => $ok, + 'suite' => $matches[2], + 'test' => $matches[3], + ); + + $rtn[] = $item; + } elseif (preg_match(self::TEST_MESSAGE_PATTERN, $line, $matches)) { + $rtn[count($rtn) - 1]['message'] = $matches[1]; + } + } + + if ($totalTests != count($rtn)) { + throw new \Exception('Invalid TAP string, number of tests does not match specified test count.'); + } + + return $rtn; + } + + public function getTotalFailures() + { + return $this->failures; + } +} diff --git a/public/assets/js/build-plugins/phpunit.js b/public/assets/js/build-plugins/phpunit.js new file mode 100644 index 00000000..c09855a9 --- /dev/null +++ b/public/assets/js/build-plugins/phpunit.js @@ -0,0 +1,65 @@ +var phpunitPlugin = PHPCI.UiPlugin.extend({ + id: 'build-phpunit-errors', + css: 'col-lg-12 col-md-12 col-sm-12 col-xs-12', + title: 'PHPUnit', + lastData: null, + displayOnUpdate: false, + box: true, + + register: function() { + var self = this; + var query = PHPCI.registerQuery('phpunit-data', -1, {key: 'phpunit-data'}) + + $(window).on('phpunit-data', function(data) { + self.onUpdate(data); + }); + + $(window).on('build-updated', function(data) { + if (data.queryData.status > 1) { + self.displayOnUpdate = true; + query(); + } + }); + }, + + render: function() { + + return $('' + + '' + + '' + + ' ' + + '' + + '
Test
'); + }, + + onUpdate: function(e) { + if (this.lastData && this.lastData[0]) { + return; + } + + this.lastData = e.queryData; + + var tests = this.lastData[0].meta_value; + var tbody = $('#phpunit-data tbody'); + tbody.empty(); + + for (var i in tests) { + + var row = $('' + + ''+tests[i].suite+'' + + '::'+tests[i].test+'
' + + ''+(tests[i].message || '')+'' + + ''); + + if (!tests[i].pass) { + row.addClass('danger'); + } else { + row.addClass('success'); + } + + tbody.append(row); + } + } +}); + +PHPCI.registerPlugin(new phpunitPlugin()); diff --git a/public/assets/js/build-plugins/warnings.js b/public/assets/js/build-plugins/warnings.js index 8b86a1e7..cc2db78f 100644 --- a/public/assets/js/build-plugins/warnings.js +++ b/public/assets/js/build-plugins/warnings.js @@ -3,10 +3,11 @@ var warningsPlugin = PHPCI.UiPlugin.extend({ css: 'col-lg-6 col-md-6 col-sm-12 col-xs-12', title: 'Quality Trend', keys: { - 'phpmd-warnings': 'PHPMD Warnings', - 'phpcs-warnings': 'PHPCS Warnings', - 'phpcs-errors': 'PHPCS Errors', - 'phplint-errors': 'PHPLint Errors' + 'phpmd-warnings': 'PHPMD Warnings', + 'phpcs-warnings': 'PHPCS Warnings', + 'phpcs-errors': 'PHPCS Errors', + 'phplint-errors': 'PHPLint Errors', + 'phpunit-errors': 'PHPUnit Errors' }, data: {}, displayOnUpdate: false, @@ -19,7 +20,7 @@ var warningsPlugin = PHPCI.UiPlugin.extend({ queries.push(PHPCI.registerQuery(key, -1, {num_builds: 10, key: key})); } - $(window).on('phpmd-warnings phpcs-warnings phpcs-errors phplint-errors', function(data) { + $(window).on('phpmd-warnings phpcs-warnings phpcs-errors phplint-errors phpunit-errors', function(data) { self.onUpdate(data); });