').append(content).appendTo(tbody);
counts[severity]++;
@@ -141,6 +153,17 @@ var phpunitPlugin = ActiveBuild.UiPlugin.extend({
'';
}
return '???';
+ },
+
+ buildTrace: function(trace){
+ var list = '';
+
+ trace.forEach(function(line){
+ list += '
' + line + '
';
+ });
+ list += '';
+
+ return list;
}
});
diff --git a/src/PHPCensor/Languages/lang.en.php b/src/PHPCensor/Languages/lang.en.php
index b63abeb5..5b866c68 100644
--- a/src/PHPCensor/Languages/lang.en.php
+++ b/src/PHPCensor/Languages/lang.en.php
@@ -193,6 +193,7 @@ PHP Censor',
'phpcs_errors' => 'PHPCS Errors',
'phplint_errors' => 'Lint Errors',
'phpunit_errors' => 'PHPUnit Errors',
+ 'phpunit_fail_init' => 'Neither a configuration file nor a test directory found.',
'phpcpd_warnings' => 'PHP Copy/Paste Detector Warnings',
'phpdoccheck_warnings' => 'Missing Docblocks',
'issues' => 'Issues',
diff --git a/src/PHPCensor/Languages/lang.es.php b/src/PHPCensor/Languages/lang.es.php
index df0032aa..b8716f78 100644
--- a/src/PHPCensor/Languages/lang.es.php
+++ b/src/PHPCensor/Languages/lang.es.php
@@ -176,6 +176,7 @@ PHP Censor',
'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',
diff --git a/src/PHPCensor/Plugin/Option/PhpUnitOptions.php b/src/PHPCensor/Plugin/Option/PhpUnitOptions.php
new file mode 100644
index 00000000..8bb9420a
--- /dev/null
+++ b/src/PHPCensor/Plugin/Option/PhpUnitOptions.php
@@ -0,0 +1,272 @@
+
+ * @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;
+ }
+}
diff --git a/src/PHPCensor/Plugin/PhpUnitV2.php b/src/PHPCensor/Plugin/PhpUnitV2.php
new file mode 100644
index 00000000..e7ac13b5
--- /dev/null
+++ b/src/PHPCensor/Plugin/PhpUnitV2.php
@@ -0,0 +1,207 @@
+
+ * @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);
+ }
+ }
+}
diff --git a/src/PHPCensor/Plugin/Util/PhpUnitResult.php b/src/PHPCensor/Plugin/Util/PhpUnitResult.php
new file mode 100644
index 00000000..e359708e
--- /dev/null
+++ b/src/PHPCensor/Plugin/Util/PhpUnitResult.php
@@ -0,0 +1,227 @@
+
+ * @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;
+ }
+}
diff --git a/src/PHPCensor/View/Build/errors.phtml b/src/PHPCensor/View/Build/errors.phtml
index 55d1ae7b..8a383c92 100644
--- a/src/PHPCensor/View/Build/errors.phtml
+++ b/src/PHPCensor/View/Build/errors.phtml
@@ -3,7 +3,7 @@ use PHPCensor\Helper\Lang;
$linkTemplate = $build->getFileLinkTemplate();
-/** @var \PHPCensor\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):
?>
-
getMessage(); ?>
+
getMessage(); ?>
-
\ No newline at end of file
+
diff --git a/src/PHPCensor/View/layout.phtml b/src/PHPCensor/View/layout.phtml
index 043af1f4..40b6cba6 100644
--- a/src/PHPCensor/View/layout.phtml
+++ b/src/PHPCensor/View/layout.phtml
@@ -17,6 +17,11 @@
+