paratest (parallel PHPUnit) plugin
This commit is contained in:
parent
64b0f60368
commit
1446974db9
112
PHPCI/Plugin/Paratest.php
Normal file
112
PHPCI/Plugin/Paratest.php
Normal file
|
@ -0,0 +1,112 @@
|
|||
<?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
|
||||
{
|
||||
protected $log_file = "junit.log";
|
||||
|
||||
/**
|
||||
* 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);
|
||||
}
|
||||
|
||||
$paratest = $this->phpci->findBinary('paratest');
|
||||
|
||||
$cmd = $paratest . ' -c phpunit-parallel.xml -p 4 --stop-on-failure --log-junit ' . $this->log_file;
|
||||
$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);
|
||||
|
||||
$paratest = $this->phpci->findBinary('paratest');
|
||||
|
||||
$cmd = $paratest . ' -c phpunit-parallel.xml -p 4 --stop-on-failure --log-junit ' . $this->log_file;
|
||||
$success = $this->phpci->executeCommand($cmd, $this->args, $this->phpci->buildPath . $directory);
|
||||
chdir($curdir);
|
||||
return $success;
|
||||
}
|
||||
}
|
||||
}
|
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 .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; }
|
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