mirror of
https://we.phorge.it/source/arcanist.git
synced 2025-01-11 15:21:03 +01:00
Decoupling phpunit test parsing from running and gathering. Should enable third party test engines to reuse common business logic around phpunit without requiring common logic for the typically application-specific running and gathering.
Test Plan: Ran PhpunitTestEngine unit test. Also used refactored PhpunitTestEngine to run phpunit tests. Reviewers: epriestley Reviewed By: epriestley CC: aran, Korvin, aurelijus Differential Revision: https://secure.phabricator.com/D4651
This commit is contained in:
parent
c8efb41811
commit
9946e23f9e
3 changed files with 205 additions and 151 deletions
|
@ -145,6 +145,7 @@ phutil_register_library_map(array(
|
||||||
'ExampleLintEngine' => 'lint/engine/ExampleLintEngine.php',
|
'ExampleLintEngine' => 'lint/engine/ExampleLintEngine.php',
|
||||||
'NoseTestEngine' => 'unit/engine/NoseTestEngine.php',
|
'NoseTestEngine' => 'unit/engine/NoseTestEngine.php',
|
||||||
'PHPUnitTestEngineTestCase' => 'unit/engine/__tests__/PHPUnitTestEngineTestCase.php',
|
'PHPUnitTestEngineTestCase' => 'unit/engine/__tests__/PHPUnitTestEngineTestCase.php',
|
||||||
|
'PhpunitResultParser' => 'unit/engine/PhpunitResultParser.php',
|
||||||
'PhpunitTestEngine' => 'unit/engine/PhpunitTestEngine.php',
|
'PhpunitTestEngine' => 'unit/engine/PhpunitTestEngine.php',
|
||||||
'PhutilLintEngine' => 'lint/engine/PhutilLintEngine.php',
|
'PhutilLintEngine' => 'lint/engine/PhutilLintEngine.php',
|
||||||
'PhutilUnitTestEngine' => 'unit/engine/PhutilUnitTestEngine.php',
|
'PhutilUnitTestEngine' => 'unit/engine/PhutilUnitTestEngine.php',
|
||||||
|
|
200
src/unit/engine/PhpunitResultParser.php
Normal file
200
src/unit/engine/PhpunitResultParser.php
Normal file
|
@ -0,0 +1,200 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
/**
|
||||||
|
* PHPUnit Result Parsing utility
|
||||||
|
*
|
||||||
|
* Intended to enable custom unit engines derived
|
||||||
|
* from phpunit to reuse common business logic related
|
||||||
|
* to parsing phpunit test results and reports
|
||||||
|
*
|
||||||
|
* For an example on how to integrate with your test
|
||||||
|
* engine, see PhpunitTestEngine.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
final class PhpunitResultParser {
|
||||||
|
|
||||||
|
private $enableCoverage;
|
||||||
|
private $projectRoot;
|
||||||
|
|
||||||
|
public function setEnableCoverage($enable_coverage) {
|
||||||
|
$this->enableCoverage = $enable_coverage;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function setProjectRoot($project_root) {
|
||||||
|
$this->projectRoot = $project_root;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse test results from phpunit json report
|
||||||
|
*
|
||||||
|
* @param string $path Path to test
|
||||||
|
* @param string $json_path Path to phpunit json report
|
||||||
|
* @param string $clover_tmp Path to phpunit clover report
|
||||||
|
* @param array $affected_tests Array of the tests affected by this run
|
||||||
|
* @param bool $enable_coverage Option to enable coverage in results
|
||||||
|
*
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public function parseTestResults(
|
||||||
|
$path,
|
||||||
|
$json_tmp,
|
||||||
|
$clover_tmp,
|
||||||
|
$affected_tests) {
|
||||||
|
|
||||||
|
$test_results = Filesystem::readFile($json_tmp);
|
||||||
|
|
||||||
|
$report = $this->getJsonReport($json_tmp);
|
||||||
|
|
||||||
|
// coverage is for all testcases in the executed $path
|
||||||
|
$coverage = array();
|
||||||
|
if ($this->enableCoverage !== false) {
|
||||||
|
$coverage = $this->readCoverage($clover_tmp, $affected_tests);
|
||||||
|
}
|
||||||
|
|
||||||
|
$results = array();
|
||||||
|
foreach ($report as $event) {
|
||||||
|
if ('test' != $event->event) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$status = ArcanistUnitTestResult::RESULT_PASS;
|
||||||
|
$user_data = '';
|
||||||
|
|
||||||
|
if ('fail' == $event->status) {
|
||||||
|
$status = ArcanistUnitTestResult::RESULT_FAIL;
|
||||||
|
$user_data .= $event->message . "\n";
|
||||||
|
foreach ($event->trace as $trace) {
|
||||||
|
$user_data .= sprintf("\n%s:%s", $trace->file, $trace->line);
|
||||||
|
}
|
||||||
|
} else if ('error' == $event->status) {
|
||||||
|
if ('Skipped Test' == $event->message) {
|
||||||
|
$status = ArcanistUnitTestResult::RESULT_SKIP;
|
||||||
|
$user_data .= $event->message;
|
||||||
|
} else if ('Incomplete Test' == $event->message) {
|
||||||
|
$status = ArcanistUnitTestResult::RESULT_SKIP;
|
||||||
|
$user_data .= $event->message;
|
||||||
|
} else {
|
||||||
|
$status = ArcanistUnitTestResult::RESULT_BROKEN;
|
||||||
|
$user_data .= $event->message;
|
||||||
|
foreach ($event->trace as $trace) {
|
||||||
|
$user_data .= sprintf("\n%s:%s", $trace->file, $trace->line);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$name = preg_replace('/ \(.*\)/', '', $event->test);
|
||||||
|
|
||||||
|
$result = new ArcanistUnitTestResult();
|
||||||
|
$result->setName($name);
|
||||||
|
$result->setResult($status);
|
||||||
|
$result->setDuration($event->time);
|
||||||
|
$result->setCoverage($coverage);
|
||||||
|
$result->setUserData($user_data);
|
||||||
|
|
||||||
|
$results[] = $result;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $results;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Read the coverage from phpunit generated clover report
|
||||||
|
*
|
||||||
|
* @param string $path Path to report
|
||||||
|
*
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
private function readCoverage($path, $affected_tests) {
|
||||||
|
$test_results = Filesystem::readFile($path);
|
||||||
|
if (empty($test_results)) {
|
||||||
|
throw new Exception('Clover coverage XML report file is empty, '
|
||||||
|
. 'it probably means that phpunit failed to run tests. '
|
||||||
|
. 'Try running arc unit with --trace option and then run '
|
||||||
|
. 'generated phpunit command yourself, you might get the '
|
||||||
|
. 'answer.'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
$coverage_dom = new DOMDocument();
|
||||||
|
$coverage_dom->loadXML($test_results);
|
||||||
|
|
||||||
|
$reports = array();
|
||||||
|
$files = $coverage_dom->getElementsByTagName('file');
|
||||||
|
|
||||||
|
foreach ($files as $file) {
|
||||||
|
$class_path = $file->getAttribute('name');
|
||||||
|
if (empty($affected_tests[$class_path])) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
$test_path = $affected_tests[$file->getAttribute('name')];
|
||||||
|
// get total line count in file
|
||||||
|
$line_count = count(file($class_path));
|
||||||
|
|
||||||
|
$coverage = '';
|
||||||
|
$start_line = 1;
|
||||||
|
$lines = $file->getElementsByTagName('line');
|
||||||
|
for ($ii = 0; $ii < $lines->length; $ii++) {
|
||||||
|
$line = $lines->item($ii);
|
||||||
|
for (; $start_line < $line->getAttribute('num'); $start_line++) {
|
||||||
|
$coverage .= 'N';
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($line->getAttribute('type') != 'stmt') {
|
||||||
|
$coverage .= 'N';
|
||||||
|
} else {
|
||||||
|
if ((int) $line->getAttribute('count') == 0) {
|
||||||
|
$coverage .= 'U';
|
||||||
|
} else if ((int) $line->getAttribute('count') > 0) {
|
||||||
|
$coverage .= 'C';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$start_line++;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (; $start_line <= $line_count; $start_line++) {
|
||||||
|
$coverage .= 'N';
|
||||||
|
}
|
||||||
|
|
||||||
|
$len = strlen($this->projectRoot . DIRECTORY_SEPARATOR);
|
||||||
|
$class_path = substr($class_path, $len);
|
||||||
|
$reports[$class_path] = $coverage;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $reports;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* We need this non-sense to make json generated by phpunit
|
||||||
|
* valid.
|
||||||
|
*
|
||||||
|
* @param string $json_tmp Path to JSON report
|
||||||
|
*
|
||||||
|
* @return array JSON decoded array
|
||||||
|
*/
|
||||||
|
private function getJsonReport($json_tmp) {
|
||||||
|
$json = Filesystem::readFile($json_tmp);
|
||||||
|
|
||||||
|
if (empty($json)) {
|
||||||
|
throw new Exception('JSON report file is empty, '
|
||||||
|
. 'it probably means that phpunit failed to run tests. '
|
||||||
|
. 'Try running arc unit with --trace option and then run '
|
||||||
|
. 'generated phpunit command yourself, you might get the '
|
||||||
|
. 'answer.'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
$json = preg_replace('/}{\s*"/', '},{"', $json);
|
||||||
|
$json = '[' . $json . ']';
|
||||||
|
$json = json_decode($json);
|
||||||
|
if (!is_array($json)) {
|
||||||
|
throw new Exception('JSON could not be decoded');
|
||||||
|
}
|
||||||
|
|
||||||
|
return $json;
|
||||||
|
}
|
||||||
|
}
|
|
@ -90,36 +90,6 @@ final class PhpunitTestEngine extends ArcanistBaseUnitTestEngine {
|
||||||
return array_mergev($results);
|
return array_mergev($results);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* We need this non-sense to make json generated by phpunit
|
|
||||||
* valid.
|
|
||||||
*
|
|
||||||
* @param string $json_tmp Path to JSON report
|
|
||||||
*
|
|
||||||
* @return array JSON decoded array
|
|
||||||
*/
|
|
||||||
private function getJsonReport($json_tmp) {
|
|
||||||
$json = Filesystem::readFile($json_tmp);
|
|
||||||
|
|
||||||
if (empty($json)) {
|
|
||||||
throw new Exception('JSON report file is empty, '
|
|
||||||
. 'it probably means that phpunit failed to run tests. '
|
|
||||||
. 'Try running arc unit with --trace option and then run '
|
|
||||||
. 'generated phpunit command yourself, you might get the '
|
|
||||||
. 'answer.'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
$json = preg_replace('/}{\s*"/', '},{"', $json);
|
|
||||||
$json = '[' . $json . ']';
|
|
||||||
$json = json_decode($json);
|
|
||||||
if (!is_array($json)) {
|
|
||||||
throw new Exception('JSON could not be decoded');
|
|
||||||
}
|
|
||||||
|
|
||||||
return $json;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Parse test results from phpunit json report
|
* Parse test results from phpunit json report
|
||||||
*
|
*
|
||||||
|
@ -130,127 +100,10 @@ final class PhpunitTestEngine extends ArcanistBaseUnitTestEngine {
|
||||||
* @return array
|
* @return array
|
||||||
*/
|
*/
|
||||||
private function parseTestResults($path, $json_tmp, $clover_tmp) {
|
private function parseTestResults($path, $json_tmp, $clover_tmp) {
|
||||||
$test_results = Filesystem::readFile($json_tmp);
|
return id(new PhpunitResultParser())
|
||||||
|
->setEnableCoverage($this->getEnableCoverage())
|
||||||
$report = $this->getJsonReport($json_tmp);
|
->setProjectRoot($this->projectRoot)
|
||||||
|
->parseTestResults($path, $json_tmp, $clover_tmp, $this->affectedTests);
|
||||||
// coverage is for all testcases in the executed $path
|
|
||||||
$coverage = array();
|
|
||||||
if ($this->getEnableCoverage() !== false) {
|
|
||||||
$coverage = $this->readCoverage($clover_tmp);
|
|
||||||
}
|
|
||||||
|
|
||||||
$results = array();
|
|
||||||
foreach ($report as $event) {
|
|
||||||
if ('test' != $event->event) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
$status = ArcanistUnitTestResult::RESULT_PASS;
|
|
||||||
$user_data = '';
|
|
||||||
|
|
||||||
if ('fail' == $event->status) {
|
|
||||||
$status = ArcanistUnitTestResult::RESULT_FAIL;
|
|
||||||
$user_data .= $event->message . "\n";
|
|
||||||
foreach ($event->trace as $trace) {
|
|
||||||
$user_data .= sprintf("\n%s:%s", $trace->file, $trace->line);
|
|
||||||
}
|
|
||||||
} else if ('error' == $event->status) {
|
|
||||||
if ('Skipped Test' == $event->message) {
|
|
||||||
$status = ArcanistUnitTestResult::RESULT_SKIP;
|
|
||||||
$user_data .= $event->message;
|
|
||||||
} else if ('Incomplete Test' == $event->message) {
|
|
||||||
$status = ArcanistUnitTestResult::RESULT_SKIP;
|
|
||||||
$user_data .= $event->message;
|
|
||||||
} else {
|
|
||||||
$status = ArcanistUnitTestResult::RESULT_BROKEN;
|
|
||||||
$user_data .= $event->message;
|
|
||||||
foreach ($event->trace as $trace) {
|
|
||||||
$user_data .= sprintf("\n%s:%s", $trace->file, $trace->line);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$name = preg_replace('/ \(.*\)/', '', $event->test);
|
|
||||||
|
|
||||||
$result = new ArcanistUnitTestResult();
|
|
||||||
$result->setName($name);
|
|
||||||
$result->setResult($status);
|
|
||||||
$result->setDuration($event->time);
|
|
||||||
$result->setCoverage($coverage);
|
|
||||||
$result->setUserData($user_data);
|
|
||||||
|
|
||||||
$results[] = $result;
|
|
||||||
}
|
|
||||||
|
|
||||||
return $results;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Red the coverage from phpunit generated clover report
|
|
||||||
*
|
|
||||||
* @param string $path Path to report
|
|
||||||
*
|
|
||||||
* @return array
|
|
||||||
*/
|
|
||||||
private function readCoverage($path) {
|
|
||||||
$test_results = Filesystem::readFile($path);
|
|
||||||
if (empty($test_results)) {
|
|
||||||
throw new Exception('Clover coverage XML report file is empty, '
|
|
||||||
. 'it probably means that phpunit failed to run tests. '
|
|
||||||
. 'Try running arc unit with --trace option and then run '
|
|
||||||
. 'generated phpunit command yourself, you might get the '
|
|
||||||
. 'answer.'
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
$coverage_dom = new DOMDocument();
|
|
||||||
$coverage_dom->loadXML($test_results);
|
|
||||||
|
|
||||||
$reports = array();
|
|
||||||
$files = $coverage_dom->getElementsByTagName('file');
|
|
||||||
|
|
||||||
foreach ($files as $file) {
|
|
||||||
$class_path = $file->getAttribute('name');
|
|
||||||
if (empty($this->affectedTests[$class_path])) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
$test_path = $this->affectedTests[$file->getAttribute('name')];
|
|
||||||
// get total line count in file
|
|
||||||
$line_count = count(file($class_path));
|
|
||||||
|
|
||||||
$coverage = '';
|
|
||||||
$start_line = 1;
|
|
||||||
$lines = $file->getElementsByTagName('line');
|
|
||||||
for ($ii = 0; $ii < $lines->length; $ii++) {
|
|
||||||
$line = $lines->item($ii);
|
|
||||||
for (; $start_line < $line->getAttribute('num'); $start_line++) {
|
|
||||||
$coverage .= 'N';
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($line->getAttribute('type') != 'stmt') {
|
|
||||||
$coverage .= 'N';
|
|
||||||
} else {
|
|
||||||
if ((int) $line->getAttribute('count') == 0) {
|
|
||||||
$coverage .= 'U';
|
|
||||||
} else if ((int) $line->getAttribute('count') > 0) {
|
|
||||||
$coverage .= 'C';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$start_line++;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (; $start_line <= $line_count; $start_line++) {
|
|
||||||
$coverage .= 'N';
|
|
||||||
}
|
|
||||||
|
|
||||||
$len = strlen($this->projectRoot . DIRECTORY_SEPARATOR);
|
|
||||||
$class_path = substr($class_path, $len);
|
|
||||||
$reports[$class_path] = $coverage;
|
|
||||||
}
|
|
||||||
|
|
||||||
return $reports;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue