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:
parent
435a169589
commit
c32a520b91
13 changed files with 1385 additions and 4 deletions
272
src/PHPCensor/Plugin/Option/PhpUnitOptions.php
Normal file
272
src/PHPCensor/Plugin/Option/PhpUnitOptions.php
Normal 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
src/PHPCensor/Plugin/PhpUnitV2.php
Normal file
207
src/PHPCensor/Plugin/PhpUnitV2.php
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
227
src/PHPCensor/Plugin/Util/PhpUnitResult.php
Normal file
227
src/PHPCensor/Plugin/Util/PhpUnitResult.php
Normal 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;
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue