diff --git a/PHPCI/Plugin/Paratest.php b/PHPCI/Plugin/Paratest.php new file mode 100644 index 00000000..7901da13 --- /dev/null +++ b/PHPCI/Plugin/Paratest.php @@ -0,0 +1,112 @@ +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; + } + } +} diff --git a/PHPCI/Plugin/Util/JUnitParser.php b/PHPCI/Plugin/Util/JUnitParser.php new file mode 100644 index 00000000..7a6dd67e --- /dev/null +++ b/PHPCI/Plugin/Util/JUnitParser.php @@ -0,0 +1,134 @@ +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; + } +} diff --git a/public/assets/css/AdminLTE-custom.css b/public/assets/css/AdminLTE-custom.css index ff75c31a..277a8709 100644 --- a/public/assets/css/AdminLTE-custom.css +++ b/public/assets/css/AdminLTE-custom.css @@ -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; } \ No newline at end of file diff --git a/public/assets/js/paratest.js b/public/assets/js/paratest.js new file mode 100644 index 00000000..3b3de00e --- /dev/null +++ b/public/assets/js/paratest.js @@ -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 $('
' + + '' + + '' + + ' ' + + '' + + '
'+Lang.get('test_message')+'
'); + }, + + 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(''+Lang.get('test_message')+''); + 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 = $(''), + message = $('
').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('' + Lang.get('test_no_message') + ''); + } + + if (tests[i].data) { + content.append('
' + this.repr(tests[i].data) + '
'); + } + + $('').append(content).appendTo(tbody); + + counts[severity]++; + total++; + } + + var checkboxes = $(''); + thead.append(checkboxes).append('' + Lang.get('test_total', total) + ''); + + for (var key in counts) { + var count = counts[key]; + if(count > 0) { + checkboxes.append( + '
 ' + + Lang.get('test_'+key, count)+ '
' + ); + } + } + + tbody.find('.success').hide(); + + $('#build-paratest-errors').show(); + }, + + repr: function(data) + { + switch(typeof(data)) { + case 'boolean': + return '' + (data ? 'true' : 'false') + ''; + case 'string': + return '"' + data + '"'; + case 'undefined': case null: + return 'null'; + case 'object': + var rows = []; + if(data instanceof Array) { + for(var i in data) { + rows.push('' + this.repr(data[i]) + ','); + } + } else { + for(var key in data) { + rows.push( + '' + + '' + this.repr(key) + '' + + '=>' + + '' + this.repr(data[key]) + ',' + + ''); + } + } + return '' + + '' + + rows.join('') + + '' + + '
array(
)
'; + } + return '???'; + } +}); + +ActiveBuild.registerPlugin(new paratestPlugin()); \ No newline at end of file