This commit is contained in:
Brian Danchilla 2016-09-27 17:11:05 +00:00 committed by GitHub
commit a41f333859
4 changed files with 409 additions and 0 deletions

120
PHPCI/Plugin/Paratest.php Normal file
View file

@ -0,0 +1,120 @@
<?php
namespace PHPCI\Plugin;
use PHPCI;
use PHPCI\Plugin\Util\JUnitParser;
/**
* Paratest (parallel PHP Unit) Plugin - Allows parallel PHP Unit testing.
* @author Brian Danchilla
* @package PHPCI
* @subpackage Plugins
*/
class Paratest extends PhpUnit
{
//TODO make these configurable
protected $log_file = "junit.log";
protected $phpunit_config_file = "phpunit-parallel.xml";
protected $number_processes = 4;
/**
* Runs PHP Unit tests in a specified directory, optionally using specified config file(s).
*/
public function execute()
{
if (empty($this->xmlConfigFile) && empty($this->directory)) {
$this->phpci->logFailure('Neither configuration file nor test directory found.');
return false;
}
$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);
}
// Run any dirs next. Again this can be either a single value or an array.
if ($this->directory !== null) {
$success &= $this->runDir($this->directory);
}
//check output from JUnit log file
$xml_string = file_get_contents($this->phpci->buildPath . DIRECTORY_SEPARATOR . $this->log_file);
try {
$junit_parser = new JUnitParser($xml_string);
$output = $junit_parser->parse();
} catch (\Exception $ex) {
$this->phpci->logFailure($xml_string);
throw $ex;
}
$failures = $junit_parser->getTotalFailures();
$this->build->storeMeta('paratest-errors', $failures);
$this->build->storeMeta('paratest-data', $output);
$this->phpci->logExecOutput(true);
return $success;
}
/**
* Run the tests defined in a PHPUnit config file.
* @param $configPath
* @return bool|mixed
*/
protected function runConfigFile($configPath)
{
if (is_array($configPath)) {
return $this->recurseArg($configPath, array($this, "runConfigFile"));
} else {
if ($this->runFrom) {
$curdir = getcwd();
chdir($this->phpci->buildPath . DIRECTORY_SEPARATOR . $this->runFrom);
}
$cmd = $this->getParatestCommand();
$success = $this->phpci->executeCommand($cmd, $this->args, $this->phpci->buildPath . $configPath);
if ($this->runFrom) {
chdir($curdir);
}
return $success;
}
}
/**
* Run the PHPUnit tests in a specific directory or array of directories.
* @param $directory
* @return bool|mixed
*/
protected function runDir($directory)
{
if (is_array($directory)) {
return $this->recurseArg($directory, array($this, "runDir"));
} else {
$curdir = getcwd();
chdir($this->phpci->buildPath);
$cmd = $this->getParatestCommand();
$success = $this->phpci->executeCommand($cmd, $this->args, $this->phpci->buildPath . $directory);
chdir($curdir);
return $success;
}
}
protected function getParatestCommand(){
$paratest = $this->phpci->findBinary('paratest');
$cmd = $paratest . ' -c ' . $this->phpunit_config_file . ' -p ' . $this->number_processes . ' --stop-on-failure --log-junit ' . $this->log_file;
return $cmd;
}
}

View file

@ -0,0 +1,134 @@
<?php
namespace PHPCI\Plugin\Util;
use PHPCI\Plugin\Util\TestResultParsers\ParserInterface;
use SimpleXMLElement;
/**
* Processes JUnit XML log file into usable test result data.
* @package PHPCI\Plugin\Util
*/
class JUnitParser implements ParserInterface
{
protected $xml = null;
protected $failures = 0;
protected $number_tests = 0;
protected $duration_time = 0;
public function __construct($xml_string)
{
$this->xml = new SimpleXMLElement($xml_string);
}
/**
* @return array An array of key/value pairs for storage in the plugins result metadata
*/
public function parse()
{
$attr = $this->xml->testsuite->attributes();
$this->duration = floatval($attr['time']);
$this->number_tests = intval($attr['tests']);
$this->failures = intval($attr['failures']);
$raw_data = $this->parseTestSuites($this->xml);
//we want to log individual test cases in JSON format for meta data and flatten results
$data = [];
foreach ($raw_data as $suites) {
foreach ($suites as $suite) {
$suite_name = $suite['name'];
foreach ($suite['cases'] as $test_case) {
$result = [
'pass' => true,
'message' => $suite_name . '::' . $test_case['name'],
'severity' => 'success',
];
if (isset($test_case['error'])) {
$result = [
'pass' => false,
'message' => $test_case['error']['message'],
'severity' => strpos($test_case['error']['type'], 'Error') !== false ? 'error' : 'fail'
];
}
$data[] = $result;
}
}
}
return $data;
}
public function getTotalTests()
{
return $this->number_tests;
}
public function getTotalTimeTaken()
{
return $this->duration;
}
public function getTotalFailures()
{
return $this->failures;
}
//NOTE: this is modified from https://github.com/Block8/PHPCI/blob/2ddda7711e1272cf6591f274e09d45b9865f4a60/PHPCI/Plugin/PhpSpec.php
protected function parseTestSuites(SimpleXMLElement $xml)
{
$data = [];
/**
* @var \SimpleXMLElement $group
*/
foreach ($xml->testsuite->testsuite as $group) {
$attr = $group->attributes();
$suite = array(
'name' => (String)$attr['name'],
'time' => (float)$attr['time'],
'tests' => (int)$attr['tests'],
'failures' => (int)$attr['failures'],
'errors' => (int)$attr['errors'],
// now the cases
'cases' => array()
);
/**
* @var \SimpleXMLElement $child
*/
foreach ($group->testcase as $child) {
$attr = $child->attributes();
$case = array(
'name' => (String)$attr['name'],
'classname' => (String)$attr['class'],
'filename' => (String)$attr['file'],
'line' => (String)$attr['line'],
'time' => (float)$attr['time'],
);
$error = [];
foreach ($child->failure as $f) {
$error['type'] = $f->attributes()['type'];
$error['message'] = (String)$f;
}
foreach ($child->{'system-err'} as $system_err) {
$error['raw'] = (String)$system_err;
}
if (!empty($error)) {
$case['error'] = $error;
}
$suite['cases'][] = $case;
}
$data['suites'][] = $suite;
}
return $data;
}
}

View file

@ -82,3 +82,10 @@
#phpunit-data .error td { background: none; color: #f56954; }
#phpunit-data .skipped td { background: none; color: #e08e0b; }
#phpunit-data .todo td { background: none; color: #00c0ef; }
#paratest-data th div { margin: 0 0.5em; }
#paratest-data .success td { background: none; color: #00a65a; }
#paratest-data .fail td { background: none; color: #f56954; }
#paratest-data .error td { background: none; color: #f56954; }
#paratest-data .skipped td { background: none; color: #e08e0b; }
#paratest-data .todo td { background: none; color: #00c0ef; }

View file

@ -0,0 +1,148 @@
//NOTE: modified from phpunitPlugin
var paratestPlugin = ActiveBuild.UiPlugin.extend({
id: 'build-paratest-errors',
css: 'col-lg-6 col-md-12 col-sm-12 col-xs-12',
title: 'Paratest',
lastData: null,
displayOnUpdate: false,
box: true,
rendered: false,
statusMap: {
success : 'ok',
fail: 'remove',
error: 'warning-sign',
todo: 'info-sign',
skipped: 'exclamation-sign'
},
register: function() {
var self = this;
var query = ActiveBuild.registerQuery('paratest-data', -1, {key: 'paratest-data'})
$(window).on('paratest-data', function(data) {
self.onUpdate(data);
});
$(window).on('build-updated', function() {
if (!self.rendered) {
self.displayOnUpdate = true;
query();
}
});
$(document).on('click', '#paratest-data .test-toggle', function(ev) {
var input = $(ev.target);
$('#paratest-data tbody ' + input.data('target')).toggle(input.prop('checked'));
});
},
render: function() {
return $('<div class="table-responsive"><table class="table" id="paratest-data">' +
'<thead>' +
'<tr>' +
' <th>'+Lang.get('test_message')+'</th>' +
'</tr>' +
'</thead><tbody></tbody></table></div>');
},
onUpdate: function(e) {
if (!e.queryData) {
$('#build-paratest-errors').hide();
return;
}
this.rendered = true;
this.lastData = e.queryData;
var tests = this.lastData[0].meta_value;
var thead = $('#paratest-data thead tr');
var tbody = $('#paratest-data tbody');
thead.empty().append('<th>'+Lang.get('test_message')+'</th>');
tbody.empty();
if (tests.length == 0) {
$('#build-paratest-errors').hide();
return;
}
var counts = { success: 0, fail: 0, error: 0, skipped: 0, todo: 0 }, total = 0;
for (var i in tests) {
var content = $('<td colspan="3"></td>'),
message = $('<div></div>').appendTo(content),
severity = tests[i].severity || (tests[i].pass ? 'success' : 'failed');
if (tests[i].message) {
message.text(tests[i].message);
} else if (tests[i].test && tests[i].suite) {
message.text(tests[i].suite + '::' + tests[i].test);
} else {
message.html('<i>' + Lang.get('test_no_message') + '</i>');
}
if (tests[i].data) {
content.append('<div>' + this.repr(tests[i].data) + '</div>');
}
$('<tr class="'+ severity + '"></tr>').append(content).appendTo(tbody);
counts[severity]++;
total++;
}
var checkboxes = $('<th/>');
thead.append(checkboxes).append('<th>' + Lang.get('test_total', total) + '</th>');
for (var key in counts) {
var count = counts[key];
if(count > 0) {
checkboxes.append(
'<div style="float:left" class="' + key + '"><input type="checkbox" class="test-toggle" data-target=".' + key + '" ' +
(key !== 'success' ? ' checked' : '') + '/>&nbsp;' +
Lang.get('test_'+key, count)+ '</div> '
);
}
}
tbody.find('.success').hide();
$('#build-paratest-errors').show();
},
repr: function(data)
{
switch(typeof(data)) {
case 'boolean':
return '<span class="boolean">' + (data ? 'true' : 'false') + '</span>';
case 'string':
return '<span class="string">"' + data + '"</span>';
case 'undefined': case null:
return '<span class="null">null</span>';
case 'object':
var rows = [];
if(data instanceof Array) {
for(var i in data) {
rows.push('<tr><td colspan="3">' + this.repr(data[i]) + ',</td></tr>');
}
} else {
for(var key in data) {
rows.push(
'<tr>' +
'<td>' + this.repr(key) + '</td>' +
'<td>=&gt;</td>' +
'<td>' + this.repr(data[key]) + ',</td>' +
'</tr>');
}
}
return '<table>' +
'<tr><th colspan="3">array(</th></tr>' +
rows.join('') +
'<tr><th colspan="3">)</th></tr>' +
'</table>';
}
return '???';
}
});
ActiveBuild.registerPlugin(new paratestPlugin());