Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
85 changes: 38 additions & 47 deletions src/PHPCensor/Plugin/PhpUnit.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@
use PHPCensor\Model\Build;
use PHPCensor\Model\BuildError;
use PHPCensor\Plugin\Option\PhpUnitOptions;
use PHPCensor\Plugin\Util\PhpUnitResult;
use PHPCensor\Plugin\Util\PhpUnitResultJson;
use PHPCensor\Plugin\Util\PhpUnitResultJunit;
use PHPCensor\Plugin;
use PHPCensor\ZeroConfigPluginInterface;

Expand Down Expand Up @@ -79,18 +80,28 @@ public function execute()
return false;
}

$cmd = $this->builder->findBinary('phpunit');
// run without logging
$ret = null;
$lastLine = exec($cmd.' --log-json . --version');
if (false !== strpos($lastLine, '--log-json')) {
$logFormat = 'junit'; // --log-json is not supported
} else {
$logFormat = 'json';
}

$success = [];

// Run any directories
if (!empty($directories)) {
foreach ($directories as $directory) {
$success[] = $this->runDir($directory);
$success[] = $this->runConfig($directory, null, $logFormat);
}
} else {
// Run any config files
if (!empty($xmlConfigFiles)) {
foreach ($xmlConfigFiles as $configFile) {
$success[] = $this->runConfigFile($configFile);
$success[] = $this->runConfig($this->options->getTestsPath(), $configFile, $logFormat);
}
}
}
Expand All @@ -99,75 +110,55 @@ public function execute()
}

/**
* Run the PHPUnit tests in a specific directory or array of directories.
* Run the tests defined in a PHPUnit config file or in a specific directory.
*
* @param $directory
*
* @return bool|mixed
*/
protected function runDir($directory)
{
$options = clone $this->options;

$buildPath = $this->build->getBuildPath() . DIRECTORY_SEPARATOR;

// Save the results into a json file
$jsonFile = @tempnam($buildPath, 'jLog_');
$options->addArgument('log-json', $jsonFile);

// Removes any current configurations files
$options->removeArgument('configuration');

$arguments = $this->builder->interpolate($options->buildArgumentString());
$cmd = $this->findBinary('phpunit') . ' %s "%s"';
$success = $this->builder->executeCommand($cmd, $arguments, $directory);

$this->processResults($jsonFile);

return $success;
}

/**
* Run the tests defined in a PHPUnit config file.
*
* @param $configFile
* @param string $logFormat
*
* @return bool|mixed
*/
protected function runConfigFile($configFile)
protected function runConfig($directory, $configFile, $logFormat)
{
$options = clone $this->options;
$buildPath = $this->build->getBuildPath() . DIRECTORY_SEPARATOR;

// Save the results into a json file
$jsonFile = @tempnam($buildPath, 'jLog_');
$options->addArgument('log-json', $jsonFile);
// Save the results into a log file
$logFile = @tempnam($buildPath, 'jLog_');
$options->addArgument('log-'.$logFormat, $logFile);

// Removes any current configurations files
$options->removeArgument('configuration');
// Only the add the configuration file been passed
$options->addArgument('configuration', $buildPath . $configFile);
if (null !== $configFile) {
// Only the add the configuration file been passed
$options->addArgument('configuration', $buildPath . $configFile);
}

$arguments = $this->builder->interpolate($options->buildArgumentString());
$cmd = $this->findBinary('phpunit') . ' %s %s';
$success = $this->builder->executeCommand($cmd, $arguments, $options->getTestsPath());
$success = $this->builder->executeCommand($cmd, $arguments, $directory);

$this->processResults($jsonFile);
$this->processResults($logFile, $logFormat);

return $success;
}

/**
* Saves the test results
*
* @param string $jsonFile
* @param string $logFile
* @param string $logFormat
*
* @throws \Exception If the failed to parse the JSON file
* @throws \Exception If failed to parse the log file
*/
protected function processResults($jsonFile)
protected function processResults($logFile, $logFormat)
{
if (file_exists($jsonFile)) {
$parser = new PhpUnitResult($jsonFile, $this->build->getBuildPath());
if (file_exists($logFile)) {
if ('json' === $logFormat) {
$parser = new PhpUnitResultJson($logFile, $this->build->getBuildPath());
} else {
$parser = new PhpUnitResultJunit($logFile, $this->build->getBuildPath());
}

$this->build->storeMeta('phpunit-data', $parser->parse()->getResults());
$this->build->storeMeta('phpunit-errors', $parser->getFailures());
Expand All @@ -178,9 +169,9 @@ protected function processResults($jsonFile)
$this->builder, 'php_unit', $error['message'], $severity, $error['file'], $error['line']
);
}
@unlink($jsonFile);
@unlink($logFile);
} else {
throw new \Exception('JSON output file does not exist: ' . $jsonFile);
throw new \Exception('log output file does not exist: ' . $logFile);
}
}
}
176 changes: 32 additions & 144 deletions src/PHPCensor/Plugin/Util/PhpUnitResult.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,19 +7,17 @@
*
* @author Pablo Tejada <pablo@ptejada.com>
*/
class PhpUnitResult
abstract 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';
const SEVERITY_WARN = self::SEVERITY_PASS;
const SEVERITY_RISKY = self::SEVERITY_PASS;

protected $options;
protected $arguments = [];
protected $outputFile;
protected $buildPath;
protected $results;
protected $failures = 0;
protected $errors = [];
Expand All @@ -36,158 +34,48 @@ public function __construct($outputFile, $buildPath = '')
* @return $this
* @throws \Exception If fails to parse the output
*/
public function parse()
{
$rawResults = file_get_contents($this->outputFile);

$events = [];
if ($rawResults && $rawResults[0] == '{') {
$fixedJson = '[' . str_replace('}{', '},{', $rawResults) . ']';
$events = json_decode($fixedJson, true);
} elseif ($rawResults) {
$events = json_decode($rawResults, true);
}
abstract public function parse();

// Reset the parsing variables
$this->results = [];
$this->errors = [];
$this->failures = 0;

if ($events) {
foreach ($events as $event) {
if (isset($event['event']) && $event['event'] == self::EVENT_TEST) {
$this->results[] = $this->parseEvent($event);
}
}
}
abstract protected function getSeverity($testcase);

return $this;
abstract protected function buildMessage($testcase);

abstract protected function buildTrace($testcase);

protected function getFileAndLine($testcase)
{
return $testcase;
}

/**
* Parse a test event
*
* @param array $event
*
* @return string[]
*/
protected function parseEvent($event)
protected function getOutput($testcase)
{
list($pass, $severity) = $this->getStatus($event);
return $testcase['output'];
}

protected function parseTestcase($testcase)
{
$severity = $this->getSeverity($testcase);
$pass = isset(array_fill_keys([self::SEVERITY_PASS, self::SEVERITY_SKIPPED], true)[$severity]);
$data = [
'pass' => $pass,
'severity' => $severity,
'message' => $this->buildMessage($event),
'trace' => $pass ? [] : $this->buildTrace($event),
'output' => $event['output'],
'message' => $this->buildMessage($testcase),
'trace' => $pass ? [] : $this->buildTrace($testcase),
'output' => $this->getOutput($testcase),
];

if (!$pass) {
$this->failures++;
$this->addError($data, $event);
$info = $this->getFileAndLine($testcase);
$this->errors[] = [
'message' => $data['message'],
'severity' => $severity,
'file' => $info['file'],
'line' => $info['line'],
];
}

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;
case 'warning':
$pass = true;
$severity = self::SEVERITY_PASS;
break;
default:
throw new \Exception("Unexpected PHPUnit test status: {$status}");
break;
}

return [$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 = [];

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[] = [
'message' => $data['message'],
'severity' => $data['severity'],
'file' => str_replace($this->buildPath, '', $firstTrace['file']),
'line' => $firstTrace['line'],
];
$this->results[] = $data;
}

/**
Expand Down
Loading