Merge 8127130bf9
into 64b0f60368
This commit is contained in:
commit
a41f333859
120
PHPCI/Plugin/Paratest.php
Normal file
120
PHPCI/Plugin/Paratest.php
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
134
PHPCI/Plugin/Util/JUnitParser.php
Normal file
134
PHPCI/Plugin/Util/JUnitParser.php
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
|
@ -82,3 +82,10 @@
|
||||||
#phpunit-data .error td { background: none; color: #f56954; }
|
#phpunit-data .error td { background: none; color: #f56954; }
|
||||||
#phpunit-data .skipped td { background: none; color: #e08e0b; }
|
#phpunit-data .skipped td { background: none; color: #e08e0b; }
|
||||||
#phpunit-data .todo td { background: none; color: #00c0ef; }
|
#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; }
|
148
public/assets/js/paratest.js
Normal file
148
public/assets/js/paratest.js
Normal 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' : '') + '/> ' +
|
||||||
|
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>=></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());
|
Loading…
Reference in a new issue