Implementation of an alternative PHPUnit plugin:

- Reformat the error output.
- Display collapsed trace in the Information tab widget.
- Handle incomplete tests in the results.
- Unit tests for all new classes.
- Display raw phpunit output.
This commit is contained in:
Pablo Tejada 2016-09-25 11:26:40 -04:00
parent 64b0f60368
commit 11f087a239
13 changed files with 1385 additions and 5 deletions

View file

@ -181,6 +181,7 @@ PHPCI',
'phpcs_errors' => 'PHPCS Errors',
'phplint_errors' => 'Lint Errors',
'phpunit_errors' => 'PHPUnit Errors',
'phpunit_fail_init' => 'Neither a configuration file nor a test directory found.',
'phpdoccheck_warnings' => 'Missing Docblocks',
'issues' => 'Issues',

View file

@ -176,6 +176,7 @@ PHPCI',
'phpcs_errors' => 'PHPCS Errors',
'phplint_errors' => 'Lint Errors',
'phpunit_errors' => 'PHPUnit Errors',
'phpunit_fail_init' => 'No se encontro archivo o folder de pruevas.',
'phpdoccheck_warnings' => 'Docblocks faltantes',
'issues' => 'Incidencias',

View file

@ -0,0 +1,272 @@
<?php
/**
* PHPCI - Continuous Integration for PHP
*
* @copyright Copyright 2014, Block 8 Limited.
* @license https://github.com/Block8/PHPCI/blob/master/LICENSE.md
* @link https://www.phptesting.org/
*/
namespace PHPCI\Plugin\Option;
/**
* Class PhpUnitOptions validates and parse the option for the PhpUnitV2 plugin
*
* @author Pablo Tejada <pablo@ptejada.com>
* @package PHPCI
* @subpackage Plugin
*/
class PhpUnitOptions
{
protected $options;
protected $arguments = array();
public function __construct($options)
{
$this->options = $options;
}
/**
* Remove a command argument
*
* @param $argumentName
*
* @return $this
*/
public function removeArgument($argumentName)
{
unset($this->arguments[$argumentName]);
return $this;
}
/**
* Combine all the argument into a string for the phpunit command
*
* @return string
*/
public function buildArgumentString()
{
$argumentString = '';
foreach ($this->getCommandArguments() as $argumentName => $argumentValues) {
$prefix = $argumentName[0] == '-' ? '' : '--';
if (!is_array($argumentValues)) {
$argumentValues = array($argumentValues);
}
foreach ($argumentValues as $argValue) {
$postfix = ' ';
if (!empty($argValue)) {
$postfix = ' "' . $argValue . '" ';
}
$argumentString .= $prefix . $argumentName . $postfix;
}
}
return $argumentString;
}
/**
* Get all the command arguments
*
* @return string[]
*/
public function getCommandArguments()
{
/*
* Return the full list of arguments
*/
return $this->parseArguments()->arguments;
}
/**
* Parse the arguments from the config options
*
* @return $this
*/
private function parseArguments()
{
if (empty($this->arguments)) {
/*
* Parse the arguments from the YML options file
*/
if (isset($this->options['args'])) {
$rawArgs = $this->options['args'];
if (is_array($rawArgs)) {
$this->arguments = $rawArgs;
} else {
/*
* Try to parse old argument in a single string
*/
preg_match_all('/--([a-z\-]+)\s?("?[^-]{2}[^"]*"?)?/', (string)$rawArgs, $argsMatch);
if (!empty($argsMatch) && sizeof($argsMatch) > 2) {
foreach ($argsMatch[1] as $index => $argName) {
$this->addArgument($argName, $argsMatch[2][$index]);
}
}
}
}
/*
* Handles command aliases outside of the args option
*/
if (isset($this->options['coverage'])) {
$this->addArgument('coverage-html', $this->options['coverage']);
}
/*
* Handles command aliases outside of the args option
*/
if (isset($this->options['config'])) {
$this->addArgument('configuration', $this->options['config']);
}
}
return $this;
}
/**
* Add an argument to the collection
* Note: adding argument before parsing the options will prevent the other options from been parsed.
*
* @param string $argumentName
* @param string $argumentValue
*/
public function addArgument($argumentName, $argumentValue)
{
if (isset($this->arguments[$argumentName])) {
if (!is_array($this->arguments[$argumentName])) {
// Convert existing argument values into an array
$this->arguments[$argumentName] = array($this->arguments[$argumentName]);
}
// Appends the new argument to the list
$this->arguments[$argumentName][] = $argumentValue;
} else {
// Adds new argument
$this->arguments[$argumentName] = $argumentValue;
}
}
/**
* Get the list of directory to run phpunit in
*
* @return string[] List of directories
*/
public function getDirectories()
{
$directories = $this->getOption('directory');
if (is_string($directories)) {
$directories = array($directories);
} else {
if (is_null($directories)) {
$directories = array();
}
}
return is_array($directories) ? $directories : array($directories);
}
/**
* Get an option if defined
*
* @param $optionName
*
* @return string[]|string|null
*/
public function getOption($optionName)
{
if (isset($this->options[$optionName])) {
return $this->options[$optionName];
}
return null;
}
/**
* Get the directory to execute the command from
*
* @return mixed|null
*/
public function getRunFrom()
{
return $this->getOption('run_from');
}
/**
* Ge the directory name where tests file reside
*
* @return string|null
*/
public function getTestsPath()
{
return $this->getOption('path');
}
/**
* Get the PHPUnit configuration from the options, or the optional path
*
* @param string $altPath
*
* @return string[] path of files
*/
public function getConfigFiles($altPath = '')
{
$configFiles = $this->getArgument('configuration');
if (empty($configFiles)) {
$configFile = self::findConfigFile($altPath);
if ($configFile) {
$configFiles[] = $configFile;
}
}
return $configFiles;
}
/**
* Get options for a given argument
*
* @param $argumentName
*
* @return string[] All the options for given argument
*/
public function getArgument($argumentName)
{
$this->parseArguments();
if (isset($this->arguments[$argumentName])) {
return is_array(
$this->arguments[$argumentName]
) ? $this->arguments[$argumentName] : array($this->arguments[$argumentName]);
}
return array();
}
/**
* Find a PHPUnit configuration file in a directory
*
* @param string $buildPath The path to configuration file
*
* @return null|string
*/
public static function findConfigFile($buildPath)
{
$files = array(
'phpunit.xml',
'phpunit.xml.dist',
'tests/phpunit.xml',
'tests/phpunit.xml.dist',
);
foreach ($files as $file) {
if (is_file($buildPath . $file)) {
return $file;
}
}
return null;
}
}

207
PHPCI/Plugin/PhpUnitV2.php Normal file
View file

@ -0,0 +1,207 @@
<?php
/**
* PHPCI - Continuous Integration for PHP
*
* @copyright Copyright 2014, Block 8 Limited.
* @license https://github.com/Block8/PHPCI/blob/master/LICENSE.md
* @link https://www.phptesting.org/
*/
namespace PHPCI\Plugin;
use PHPCI;
use PHPCI\Builder;
use PHPCI\Helper\Lang;
use PHPCI\Model\Build;
use PHPCI\Model\BuildError;
use PHPCI\Plugin\Option\PhpUnitOptions;
use PHPCI\Plugin\Util\PhpUnitResult;
/**
* PHP Unit Plugin V2 - Extends the functionality of the original PHP Unit plugin
*
* @author Pablo Tejada <pablo@ptejada.com>
* @package PHPCI
* @subpackage Plugins
*/
class PhpUnitV2 implements PHPCI\Plugin, PHPCI\ZeroConfigPlugin
{
protected $phpci;
protected $build;
/** @var string[] Raw options from the PHPCI config file */
protected $options = array();
/**
* Standard Constructor
* $options['config'] Path to a PHPUnit XML configuration file.
* $options['run_from'] The directory where the phpunit command will run from when using 'config'.
* $options['coverage'] Value for the --coverage-html command line flag.
* $options['directory'] Optional directory or list of directories to run PHPUnit on.
* $options['args'] Command line args (in string format) to pass to PHP Unit
*
* @param Builder $phpci
* @param Build $build
* @param string[] $options
*/
public function __construct(Builder $phpci, Build $build, array $options = array())
{
$this->phpci = $phpci;
$this->build = $build;
$this->options = new PhpUnitOptions($options);
}
/**
* Check if the plugin can be executed without any configurations
*
* @param $stage
* @param Builder $builder
* @param Build $build
*
* @return bool
*/
public static function canExecute($stage, Builder $builder, Build $build)
{
if ($stage == 'test' && !is_null(PhpUnitOptions::findConfigFile($build->getBuildPath()))) {
return true;
}
return false;
}
/**
* Runs PHP Unit tests in a specified directory, optionally using specified config file(s).
*/
public function execute()
{
$xmlConfigFiles = $this->options->getConfigFiles($this->build->getBuildPath());
$directories = $this->options->getDirectories();
if (empty($xmlConfigFiles) && empty($directories)) {
$this->phpci->logFailure(Lang::get('phpunit_fail_init'));
return false;
}
$success = array();
// Run any directories
if (!empty($directories)) {
foreach ($directories as $directory) {
$success[] = $this->runDir($directory);
}
} else {
// Run any config files
if (!empty($xmlConfigFiles)) {
foreach ($xmlConfigFiles as $configFile) {
$success[] = $this->runConfigFile($configFile);
}
}
}
return !in_array(false, $success);
}
/**
* Run the PHPUnit tests in a specific directory or array of directories.
*
* @param $directory
*
* @return bool|mixed
*/
protected function runDir($directory)
{
$options = clone $this->options;
$buildPath = $this->build->getBuildPath() . DIRECTORY_SEPARATOR;
$currentPath = getcwd();
// Change the directory
chdir($buildPath);
// Save the results into a json file
$jsonFile = tempnam(dirname($buildPath), 'jLog_');
$options->addArgument('log-json', $jsonFile);
// Removes any current configurations files
$options->removeArgument('configuration');
$arguments = $this->phpci->interpolate($options->buildArgumentString());
$cmd = $this->phpci->findBinary('phpunit') . ' %s "%s"';
$success = $this->phpci->executeCommand($cmd, $arguments, $directory);
// Change to che original path
chdir($currentPath);
$this->processResults($jsonFile);
return $success;
}
/**
* Run the tests defined in a PHPUnit config file.
*
* @param $configFile
*
* @return bool|mixed
*/
protected function runConfigFile($configFile)
{
$options = clone $this->options;
$runFrom = $options->getRunFrom();
$buildPath = $this->build->getBuildPath() . DIRECTORY_SEPARATOR;
if ($runFrom) {
$originalPath = getcwd();
// Change the directory
chdir($buildPath . $runFrom);
}
// Save the results into a json file
$jsonFile = tempnam($this->phpci->buildPath, 'jLog_');
$options->addArgument('log-json', $jsonFile);
// Removes any current configurations files
$options->removeArgument('configuration');
// Only the add the configuration file been passed
$options->addArgument('configuration', $buildPath . $configFile);
$arguments = $this->phpci->interpolate($options->buildArgumentString());
$cmd = $this->phpci->findBinary('phpunit') . ' %s %s';
$success = $this->phpci->executeCommand($cmd, $arguments, $options->getTestsPath());
if (!empty($originalPath)) {
// Change to che original path
chdir($originalPath);
}
$this->processResults($jsonFile);
return $success;
}
/**
* Saves the test results
*
* @param string $jsonFile
*
* @throws \Exception If the failed to parse the JSON file
*/
protected function processResults($jsonFile)
{
if (is_file($jsonFile)) {
$parser = new PhpUnitResult($jsonFile, $this->build->getBuildPath());
$this->build->storeMeta('phpunit-data', $parser->parse()->getResults());
$this->build->storeMeta('phpunit-errors', $parser->getFailures());
foreach ($parser->getErrors() as $error) {
$severity = $error['severity'] == $parser::SEVERITY_ERROR ? BuildError::SEVERITY_CRITICAL : BuildError::SEVERITY_HIGH;
$this->build->reportError(
$this->phpci, 'php_unit', $error['message'], $severity, $error['file'], $error['line']
);
}
} else {
throw new \Exception('JSON output file does not exist: ' . $jsonFile);
}
}
}

View file

@ -0,0 +1,227 @@
<?php
/**
* PHPCI - Continuous Integration for PHP
*
* @copyright Copyright 2014, Block 8 Limited.
* @license https://github.com/Block8/PHPCI/blob/master/LICENSE.md
* @link https://www.phptesting.org/
*/
namespace PHPCI\Plugin\Util;
/**
* Class PhpUnitResult parses the results for the PhpUnitV2 plugin
*
* @author Pablo Tejada <pablo@ptejada.com>
* @package PHPCI
* @subpackage Plugin
*/
class PhpUnitResult
{
const EVENT_TEST = 'test';
const EVENT_TEST_START = 'testStart';
const EVENT_SUITE_START = 'suiteStart';
const SEVERITY_PASS = 'success';
const SEVERITY_FAIL = 'fail';
const SEVERITY_ERROR = 'error';
const SEVERITY_SKIPPED = 'skipped';
protected $options;
protected $arguments = array();
protected $results;
protected $failures = 0;
protected $errors = array();
public function __construct($outputFile, $buildPath = '')
{
$this->outputFile = $outputFile;
$this->buildPath = $buildPath;
}
/**
* Parse the results
*
* @return $this
* @throws \Exception If fails to parse the output
*/
public function parse()
{
$rawResults = file_get_contents($this->outputFile);
if ($rawResults[0] == '{') {
$fixedJson = '[' . str_replace('}{', '},{', $rawResults) . ']';
$events = json_decode($fixedJson, true);
} else {
$events = json_decode($rawResults, true);
}
// Reset the parsing variables
$this->results = array();
$this->errors = array();
$this->failures = 0;
if (is_array($events)) {
foreach ($events as $event) {
if ($event['event'] == self::EVENT_TEST) {
$this->results[] = $this->parseEvent($event);
}
}
} else {
throw new \Exception('Failed to parse the JSON output.');
}
return $this;
}
/**
* Parse a test event
*
* @param array $event
*
* @return string[]
*/
protected function parseEvent($event)
{
list($pass, $severity) = $this->getStatus($event);
$data = array(
'pass' => $pass,
'severity' => $severity,
'message' => $this->buildMessage($event),
'trace' => $pass ? array() : $this->buildTrace($event),
'output' => $event['output'],
);
if (!$pass) {
$this->failures++;
$this->addError($data, $event);
}
return $data;
}
/**
* Build the status of the event
*
* @param $event
*
* @return mixed[bool,string] - The pass and severity flags
* @throws \Exception
*/
protected function getStatus($event)
{
$status = $event['status'];
switch ($status) {
case 'fail':
$pass = false;
$severity = self::SEVERITY_FAIL;
break;
case 'error':
if (strpos($event['message'], 'Skipped') === 0 || strpos($event['message'], 'Incomplete') === 0) {
$pass = true;
$severity = self::SEVERITY_SKIPPED;
} else {
$pass = false;
$severity = self::SEVERITY_ERROR;
}
break;
case 'pass':
$pass = true;
$severity = self::SEVERITY_PASS;
break;
default:
throw new \Exception("Unexpected PHPUnit test status: {$status}");
break;
}
return array($pass, $severity);
}
/**
* Build the message string for an event
*
* @param array $event
*
* @return string
*/
protected function buildMessage($event)
{
$message = $event['test'];
if ($event['message']) {
$message .= PHP_EOL . $event ['message'];
}
return $message;
}
/**
* Build a string base trace of the failure
*
* @param array $event
*
* @return string[]
*/
protected function buildTrace($event)
{
$formattedTrace = array();
if (!empty($event['trace'])) {
foreach ($event['trace'] as $step){
$line = str_replace($this->buildPath, '', $step['file']) . ':' . $step['line'];
$formattedTrace[] = $line;
}
}
return $formattedTrace;
}
/**
* Saves additional info for a failing test
*
* @param array $data
* @param array $event
*/
protected function addError($data, $event)
{
$firstTrace = end($event['trace']);
reset($event['trace']);
$this->errors[] = array(
'message' => $data['message'],
'severity' => $data['severity'],
'file' => str_replace($this->buildPath, '', $firstTrace['file']),
'line' => $firstTrace['line'],
);
}
/**
* Get the parse results
*
* @return string[]
*/
public function getResults()
{
return $this->results;
}
/**
* Get the total number of failing tests
*
* @return int
*/
public function getFailures()
{
return $this->failures;
}
/**
* Get the tests with failing status
*
* @return string[]
*/
public function getErrors()
{
return $this->errors;
}
}

View file

@ -3,7 +3,7 @@ use PHPCI\Helper\Lang;
$linkTemplate = $build->getFileLinkTemplate();
/** @var \PHPCI\Model\BuildError $error */
/** @var \PHPCI\Model\BuildError[] $errors */
foreach ($errors as $error):
$link = str_replace('{FILE}', $error->getFile(), $linkTemplate);
@ -30,8 +30,8 @@ foreach ($errors as $error):
?>
</a>
</td>
<td><?php print $error->getMessage(); ?></td>
<td class="visible-line-breaks"><?php print $error->getMessage(); ?></td>
</tr>
<?php endforeach; ?>
<?php endforeach; ?>

View file

@ -18,7 +18,11 @@
<link href="<?php print PHPCI_URL; ?>assets/css/AdminLTE.min.css" rel="stylesheet" type="text/css" />
<link href="<?php print PHPCI_URL; ?>assets/css/AdminLTE-skins.min.css" rel="stylesheet" type="text/css" />
<link href="<?php print PHPCI_URL; ?>assets/css/AdminLTE-custom.css" rel="stylesheet" type="text/css" />
<style type="text/css">
.visible-line-breaks {
white-space: pre-wrap;
}
</style>
<script>
var PHPCI_URL = '<?php print PHPCI_URL; ?>';
var PHPCI_LANGUAGE = <?php print json_encode(Lang::getLanguage()); ?>;

View file

@ -0,0 +1,98 @@
<?php
/**
* PHPCI - Continuous Integration for PHP
*
* @copyright Copyright 2013, Block 8 Limited.
* @license https://github.com/Block8/PHPCI/blob/master/LICENSE.md
* @link https://www.phptesting.org/
*/
namespace Tests\PHPCI\Plugin;
use PHPCI\Plugin\Option\PhpUnitOptions;
/**
* Unit test for the PHPUnitOptions parser
*
* @author Pablo Tejada <pablo@ptejada.com>
*/
class PhpUnitOptionsTest extends \PHPUnit_Framework_TestCase
{
public function validOptionsProvider()
{
return array(
array(
array(
'config' => 'tests/phpunit.xml',
'args' => '--stop-on-error --log-junit /path/to/log/',
),
array(
'stop-on-error' => '',
'log-junit' => '/path/to/log/',
'configuration' => 'tests/phpunit.xml',
),
),
array(
array(
'coverage' => '/path/to/coverage2/',
'args' => array(
'coverage-html' => '/path/to/coverage1/',
),
),
array(
'coverage-html' => array(
'/path/to/coverage1/',
'/path/to/coverage2/',
),
),
),
array(
array(
'directory' => array(
'/path/to/test1/',
'/path/to/test2/',
),
'args' => array(
'coverage-html' => '/path/to/coverage1/',
),
),
array(
'coverage-html' => '/path/to/coverage1/',
),
),
);
}
/**
* @param $rawOptions
* @param $parsedArguments
*
* @dataProvider validOptionsProvider
*/
public function testCommandArguments($rawOptions, $parsedArguments)
{
$options = new PhpUnitOptions($rawOptions);
$this->assertSame($parsedArguments, $options->getCommandArguments());
}
public function testGetters()
{
$options = new PhpUnitOptions(
array(
'run_from' => '/path/to/run/from',
'path' => 'subTest',
)
);
$this->assertEquals('/path/to/run/from', $options->getRunFrom());
$this->assertEquals('subTest', $options->getTestsPath());
$this->assertNull($options->getOption('random'));
$this->assertEmpty($options->getDirectories());
$this->assertEmpty($options->getConfigFiles());
$files = $options->getConfigFiles(PHPCI_DIR);
$this->assertFileExists(PHPCI_DIR . $files[0]);
}
}

View file

@ -0,0 +1,127 @@
<?php
/**
* PHPCI - Continuous Integration for PHP
*
* @copyright Copyright 2013, Block 8 Limited.
* @license https://github.com/Block8/PHPCI/blob/master/LICENSE.md
* @link https://www.phptesting.org/
*/
namespace Tests\PHPCI\Plugin;
/**
* Unit test for the PHPUnit V2 plugin.
*
* @author Pablo Tejada <pablo@ptejada.com>
*/
class PhpUnitV2Test extends \PHPUnit_Framework_TestCase
{
public function testSingleConfigFile()
{
$options = array(
'config' => PHPCI_DIR . 'phpunit.xml'
);
$mockPlugin = $this->getPluginBuilder($options)->setMethods(array('runConfigFile'))->getMock();
$mockPlugin->expects($this->once())->method('runConfigFile')->with(PHPCI_DIR . 'phpunit.xml');
$mockPlugin->execute();
}
public function testMultiConfigFile()
{
$options = array(
'config' => array(
PHPCI_DIR . 'phpunit1.xml',
PHPCI_DIR . 'phpunit2.xml',
)
);
$mockPlugin = $this->getPluginBuilder($options)->setMethods(array('runConfigFile'))->getMock();
$mockPlugin->expects($this->exactly(2))->method('runConfigFile')->withConsecutive(
array(PHPCI_DIR . 'phpunit1.xml'), array(PHPCI_DIR . 'phpunit2.xml')
);
$mockPlugin->execute();
}
/**
* @param array $options
*
* @return \PHPUnit_Framework_MockObject_MockBuilder
*/
protected function getPluginBuilder($options = array())
{
$loggerMock = $this->getMockBuilder('\Monolog\Logger')
->setConstructorArgs(array('Test'))
->setMethods(array('addRecord'))
->getMock();
$mockBuild = $this->getMockBuilder('\PHPCI\Model\Build')->getMock();
$mockBuilder = $this->getMockBuilder('\PHPCI\Builder')
->setConstructorArgs(array($mockBuild, $loggerMock))
->setMethods(array('executeCommand'))->getMock();
return $this->getMockBuilder('PHPCI\Plugin\PhpUnitV2')->setConstructorArgs(
array($mockBuilder, $mockBuild, $options)
);
}
public function testSingleDir()
{
$options = array(
'directory' => '/test/directory/one'
);
$mockPlugin = $this->getPluginBuilder($options)->setMethods(array('runDir'))->getMock();
$mockPlugin->expects($this->once())->method('runDir')->with('/test/directory/one');
$mockPlugin->execute();
}
public function testMultiDir()
{
$options = array(
'directory' => array(
'/test/directory/one',
'/test/directory/two',
)
);
$mockPlugin = $this->getPluginBuilder($options)->setMethods(array('runDir'))->getMock();
$mockPlugin->expects($this->exactly(2))->method('runDir')->withConsecutive(
array('/test/directory/one'), array('/test/directory/two')
);
$mockPlugin->execute();
}
public function testProcessResultsFromConfig()
{
$options = array(
'config' => PHPCI_DIR . 'phpunit.xml'
);
$mockPlugin = $this->getPluginBuilder($options)->setMethods(array('processResults'))->getMock();
$mockPlugin->expects($this->once())->method('processResults')->with($this->isType('string'));
$mockPlugin->execute();
}
public function testProcessResultsFromDir()
{
$options = array(
'directory' => PHPCI_DIR . 'Tests'
);
$mockPlugin = $this->getPluginBuilder($options)->setMethods(array('processResults'))->getMock();
$mockPlugin->expects($this->once())->method('processResults')->with($this->isType('string'));
$mockPlugin->execute();
}
}

View file

@ -0,0 +1,10 @@
{
"event": "suiteStart",
"suite": "Money Test Suite",
"tests": 61
}
{
"event": "suiteStart",
"suite": "Tests\\Money\\MoneyTest",
"tests": 15
}

View file

@ -0,0 +1,355 @@
{
"event": "suiteStart",
"suite": "Money Test Suite",
"tests": 61
}{
"event": "suiteStart",
"suite": "Tests\\Money\\MoneyTest",
"tests": 15
}{
"event": "testStart",
"suite": "Tests\\Money\\MoneyTest",
"test": "Tests\\Money\\MoneyTest::testFactoryMethods"
}{
"event": "test",
"suite": "Tests\\Money\\MoneyTest",
"test": "Tests\\Money\\MoneyTest::testFactoryMethods",
"status": "pass",
"time": 0.051446914672852,
"trace": [],
"message": "",
"output": ""
}{
"event": "testStart",
"suite": "Tests\\Money\\MoneyTest",
"test": "Tests\\Money\\MoneyTest::testJsonEncoding"
}{
"event": "test",
"suite": "Tests\\Money\\MoneyTest",
"test": "Tests\\Money\\MoneyTest::testJsonEncoding",
"status": "pass",
"time": 0.00051498413085938,
"trace": [],
"message": "",
"output": ""
}{
"event": "testStart",
"suite": "Tests\\Money\\MoneyTest",
"test": "Tests\\Money\\MoneyTest::testMaxInit"
}{
"event": "test",
"suite": "Tests\\Money\\MoneyTest",
"test": "Tests\\Money\\MoneyTest::testMaxInit",
"status": "pass",
"time": 0.0023708343505859,
"trace": [],
"message": "",
"output": ""
}{
"event": "testStart",
"suite": "Tests\\Money\\MoneyTest",
"test": "Tests\\Money\\MoneyTest::testFailure"
}{
"event": "test",
"suite": "Tests\\Money\\MoneyTest",
"test": "Tests\\Money\\MoneyTest::testFailure",
"status": "error",
"time": 0.0025370121002197,
"trace": [
{
"file": "\/path\/to\/build\/src\/Money.php",
"line": 335
},
{
"file": "\/path\/to\/build\/tests\/MoneyTest.php",
"line": 43,
"function": "divide",
"class": "Money\\Money",
"type": "->"
}
],
"message": "Division by zero",
"output": ""
}{
"event": "testStart",
"suite": "Tests\\Money\\MoneyTest",
"test": "Tests\\Money\\MoneyTest::testFailure2"
}{
"event": "test",
"suite": "Tests\\Money\\MoneyTest",
"test": "Tests\\Money\\MoneyTest::testFailure2",
"status": "fail",
"time": 0.008944034576416,
"trace": [
{
"file": "\/path\/to\/build\/tests\/MoneyTest.php",
"line": 61,
"function": "assertEquals",
"class": "PHPUnit_Framework_Assert",
"type": "::"
}
],
"message": "Failed asserting that two objects are equal.",
"output": ""
}{
"event": "testStart",
"suite": "Tests\\Money\\MoneyTest",
"test": "Tests\\Money\\MoneyTest::testFailure3"
}{
"event": "testStart",
"suite": "Tests\\Money\\MoneyTest",
"test": "Tests\\Money\\MoneyTest::testMaxInit"
}{
"event": "test",
"suite": "Tests\\Money\\MoneyTest",
"test": "Tests\\Money\\MoneyTest::testMaxInit",
"status": "error",
"time": 0.0074319839477539,
"trace": [
{
"file": "\/path\/to\/build\/tests\/MoneyTest.php",
"line": 36,
"function": "markTestIncomplete",
"class": "PHPUnit_Framework_Assert",
"type": "::"
}
],
"message": "Incomplete Test: No yet finished...",
"output": ""
}{
"event": "test",
"suite": "Tests\\Money\\MoneyTest",
"test": "Tests\\Money\\MoneyTest::testFailure3",
"status": "fail",
"time": 0.0014960765838623,
"trace": [
{
"file": "\/path\/to\/build\/tests\/MoneyTest.php",
"line": 66,
"function": "assertEquals",
"class": "PHPUnit_Framework_Assert",
"type": "::"
}
],
"message": "Failed asserting that two arrays are equal.",
"output": ""
}{
"event": "testStart",
"suite": "Tests\\Money\\MoneyTest",
"test": "Tests\\Money\\MoneyTest::testFailure4"
}{
"event": "test",
"suite": "Tests\\Money\\MoneyTest",
"test": "Tests\\Money\\MoneyTest::testFailure4",
"status": "fail",
"time": 0.0023319721221924,
"trace": [
{
"file": "\/path\/to\/build\/tests\/MoneyTest.php",
"line": 71,
"function": "assertTrue",
"class": "PHPUnit_Framework_Assert",
"type": "::"
}
],
"message": "Really? It should have been true\nFailed asserting that false is true.",
"output": ""
}{
"event": "suiteStart",
"suite": "Tests\\Money\\MoneyTest::testFailure5",
"tests": 3
}{
"event": "testStart",
"suite": "Tests\\Money\\MoneyTest::testFailure5",
"test": "Tests\\Money\\MoneyTest::testFailure5 with data set #0 (1, 2, 3, 4, 5, 6)"
}{
"event": "test",
"suite": "Tests\\Money\\MoneyTest::testFailure5",
"test": "Tests\\Money\\MoneyTest::testFailure5 with data set #0 (1, 2, 3, 4, 5, 6)",
"status": "fail",
"time": 0.0025498867034912,
"trace": [
{
"file": "\/path\/to\/build\/tests\/MoneyTest.php",
"line": 82,
"function": "assertSame",
"class": "PHPUnit_Framework_Assert",
"type": "::"
}
],
"message": "Failed asserting that 3 is identical to 1.",
"output": ""
}{
"event": "testStart",
"suite": "Tests\\Money\\MoneyTest::testFailure5",
"test": "Tests\\Money\\MoneyTest::testFailure5 with data set #1 ('one', 'two', 'three', 'four', 'five', 'six')"
}{
"event": "test",
"suite": "Tests\\Money\\MoneyTest::testFailure5",
"test": "Tests\\Money\\MoneyTest::testFailure5 with data set #1 ('one', 'two', 'three', 'four', 'five', 'six')",
"status": "fail",
"time": 0.00087904930114746,
"trace": [
{
"file": "\/path\/to\/build\/tests\/MoneyTest.php",
"line": 82,
"function": "assertSame",
"class": "PHPUnit_Framework_Assert",
"type": "::"
}
],
"message": "Failed asserting that two strings are identical.",
"output": ""
}{
"event": "testStart",
"suite": "Tests\\Money\\MoneyTest::testFailure5",
"test": "Tests\\Money\\MoneyTest::testFailure5 with data set #2 (array(1, 'one'), array(2, 'two'), array(3, 'three'), array(4, 'four'))"
}{
"event": "test",
"suite": "Tests\\Money\\MoneyTest::testFailure5",
"test": "Tests\\Money\\MoneyTest::testFailure5 with data set #2 (array(1, 'one'), array(2, 'two'), array(3, 'three'), array(4, 'four'))",
"status": "fail",
"time": 0.0010340213775635,
"trace": [
{
"file": "\/path\/to\/build\/tests\/MoneyTest.php",
"line": 82,
"function": "assertSame",
"class": "PHPUnit_Framework_Assert",
"type": "::"
}
],
"message": "Failed asserting that Array &0 (\n 0 => 3\n 1 => 'three'\n) is identical to Array &0 (\n 0 => 1\n 1 => 'one'\n).",
"output": ""
}{
"event": "suiteStart",
"suite": "Tests\\Money\\MoneyTest::testSkipped",
"tests": 3
}{
"event": "testStart",
"suite": "Tests\\Money\\MoneyTest::testSkipped",
"test": "Tests\\Money\\MoneyTest::testSkipped with data set #0 (1, 2, 3, 4, 5, 6)"
}{
"event": "test",
"suite": "Tests\\Money\\MoneyTest::testSkipped",
"test": "Tests\\Money\\MoneyTest::testSkipped with data set #0 (1, 2, 3, 4, 5, 6)",
"status": "error",
"time": 0.0041179656982422,
"trace": [
{
"file": "\/path\/to\/build\/tests\/MoneyTest.php",
"line": 99,
"function": "markTestSkipped",
"class": "PHPUnit_Framework_Assert",
"type": "::"
}
],
"message": "Skipped Test: This test is currently failing",
"output": ""
}{
"event": "testStart",
"suite": "Tests\\Money\\MoneyTest::testSkipped",
"test": "Tests\\Money\\MoneyTest::testSkipped with data set #1 ('one', 'two', 'three', 'four', 'five', 'six')"
}{
"event": "test",
"suite": "Tests\\Money\\MoneyTest::testSkipped",
"test": "Tests\\Money\\MoneyTest::testSkipped with data set #1 ('one', 'two', 'three', 'four', 'five', 'six')",
"status": "error",
"time": 0.00082302093505859,
"trace": [
{
"file": "\/path\/to\/build\/tests\/MoneyTest.php",
"line": 99,
"function": "markTestSkipped",
"class": "PHPUnit_Framework_Assert",
"type": "::"
}
],
"message": "Skipped Test: This test is currently failing",
"output": ""
}{
"event": "testStart",
"suite": "Tests\\Money\\MoneyTest::testSkipped",
"test": "Tests\\Money\\MoneyTest::testSkipped with data set #2 (array(1, 'one'), array(2, 'two'), array(3, 'three'), array(4, 'four'))"
}{
"event": "test",
"suite": "Tests\\Money\\MoneyTest::testSkipped",
"test": "Tests\\Money\\MoneyTest::testSkipped with data set #2 (array(1, 'one'), array(2, 'two'), array(3, 'three'), array(4, 'four'))",
"status": "error",
"time": 0.00070905685424805,
"trace": [
{
"file": "\/path\/to\/build\/tests\/MoneyTest.php",
"line": 99,
"function": "markTestSkipped",
"class": "PHPUnit_Framework_Assert",
"type": "::"
}
],
"message": "Skipped Test: This test is currently failing",
"output": ""
}{
"event": "testStart",
"suite": "",
"test": "Tests\\Money\\MoneyTest::testDepends1"
}{
"event": "test",
"suite": "",
"test": "Tests\\Money\\MoneyTest::testDepends1",
"status": "fail",
"time": 0.00078010559082031,
"trace": [
{
"file": "\/path\/to\/build\/tests\/MoneyTest.php",
"line": 105,
"function": "assertTrue",
"class": "PHPUnit_Framework_Assert",
"type": "::"
}
],
"message": "Failed asserting that false is true.",
"output": ""
}{
"event": "test",
"suite": "",
"test": "Tests\\Money\\MoneyTest::testDepends1",
"status": "error",
"time": 0,
"trace": [],
"message": "Skipped Test: This test depends on \"Tests\\Money\\MoneyTest::testDepends1\" to pass.",
"output": ""
}{
"event": "suiteStart",
"suite": "Tests\\Money\\Parser\\IntlMoneyParserTest",
"tests": 23
}{
"event": "suiteStart",
"suite": "Tests\\Money\\Parser\\IntlMoneyParserTest::testIntlParser",
"tests": 18
}{
"event": "testStart",
"suite": "Tests\\Money\\Parser\\IntlMoneyParserTest::testIntlParser",
"test": "Tests\\Money\\Parser\\IntlMoneyParserTest::testIntlParser with data set #0 ('$1000.50', 100050)"
}{
"event": "test",
"suite": "Tests\\Money\\Parser\\IntlMoneyParserTest::testIntlParser",
"test": "Tests\\Money\\Parser\\IntlMoneyParserTest::testIntlParser with data set #0 ('$1000.50', 100050)",
"status": "pass",
"time": 0.0069050788879395,
"trace": [],
"message": "",
"output": ""
}{
"event": "testStart",
"suite": "Tests\\Money\\Parser\\IntlMoneyParserTest::testIntlParser",
"test": "Tests\\Money\\Parser\\IntlMoneyParserTest::testIntlParser with data set #1 ('$1000.00', 100000)"
}{
"event": "test",
"suite": "Tests\\Money\\Parser\\IntlMoneyParserTest::testIntlParser",
"test": "Tests\\Money\\Parser\\IntlMoneyParserTest::testIntlParser with data set #1 ('$1000.00', 100000)",
"status": "pass",
"time": 0.00067996978759766,
"trace": [],
"message": "",
"output": ""
}

View file

@ -0,0 +1,55 @@
<?php
/**
* PHPCI - Continuous Integration for PHP
*
* @copyright Copyright 2014, Block 8 Limited.
* @license https://github.com/Block8/PHPCI/blob/master/LICENSE.md
* @link https://www.phptesting.org/
*/
namespace Tests\PHPCI\Plugin\Util;
use PHPCI\Plugin\Util\PhpUnitResult;
/**
* Class PhpUnitResultTest parses the results for the PhpUnitV2 plugin
* @author Pablo Tejada <pablo@ptejada.com>
* @package PHPCI
* @subpackage Plugin
*/
class PhpUnitResultTest extends \PHPUnit_Framework_TestCase
{
public function testInitParse()
{
$buildPath = '/path/to/build';
$parser = new PhpUnitResult(PHPCI_DIR . 'Tests/PHPCI/Plugin/SampleFiles/phpunit_money.txt', $buildPath);
$output = $parser->parse()->getResults();
$errors = $parser->getErrors();
$this->assertEquals(8, $parser->getFailures());
$this->assertInternalType('array', $output);
$this->assertInternalType('array', $errors);
$this->assertNotEmpty($output);
$this->assertNotEmpty($errors);
// The trace elements should not include the build path
$this->assertStringStartsNotWith($buildPath, $output[3]['trace'][0]);
$this->assertStringStartsNotWith($buildPath, $output[3]['trace'][1]);
$this->assertEquals(PhpUnitResult::SEVERITY_SKIPPED, $output[5]['severity']);
$this->assertContains('Incomplete Test:', $output[5]['message']);
$this->assertEquals(PhpUnitResult::SEVERITY_SKIPPED, $output[11]['severity']);
$this->assertContains('Skipped Test:', $output[11]['message']);
}
public function testParseFailure()
{
$this->setExpectedException('\Exception', 'Failed to parse the JSON output');
$buildPath = '/path/to/build';
$parser = new PhpUnitResult(PHPCI_DIR . 'Tests/PHPCI/Plugin/SampleFiles/invalid_format.txt', $buildPath);
$parser->parse();
}
}

View file

@ -33,6 +33,11 @@ var phpunitPlugin = ActiveBuild.UiPlugin.extend({
var input = $(ev.target);
$('#phpunit-data tbody ' + input.data('target')).toggle(input.prop('checked'));
});
$(document).on('click', '#phpunit-data button.trace', function() {
var $btn = $(this);
$($btn).replaceWith(self.buildTrace($btn.data('trace')));
});
},
render: function() {
@ -69,7 +74,7 @@ var phpunitPlugin = ActiveBuild.UiPlugin.extend({
for (var i in tests) {
var content = $('<td colspan="3"></td>'),
message = $('<div></div>').appendTo(content),
message = $('<div class="visible-line-breaks"></div>').appendTo(content),
severity = tests[i].severity || (tests[i].pass ? 'success' : 'failed');
if (tests[i].message) {
@ -84,6 +89,13 @@ var phpunitPlugin = ActiveBuild.UiPlugin.extend({
content.append('<div>' + this.repr(tests[i].data) + '</div>');
}
if (tests[i].trace && tests[i].trace.length) {
var $traceBtn = $('<button class="btn btn-default btn-xs trace" type="button" title="Expand Trace">...</button>');
$traceBtn.data('trace', tests[i].trace);
content.append('Trace: ');
content.append($traceBtn);
}
$('<tr class="'+ severity + '"></tr>').append(content).appendTo(tbody);
counts[severity]++;
@ -141,6 +153,17 @@ var phpunitPlugin = ActiveBuild.UiPlugin.extend({
'</table>';
}
return '???';
},
buildTrace: function(trace){
var list = '<ol reversed>';
trace.forEach(function(line){
list += '<li>' + line + '</li>';
});
list += '</ol>';
return list;
}
});