Skip to content
Permalink
Browse files

Add support for JUnit report type

Fixes #2485
  • Loading branch information
muglug committed Dec 19, 2019
1 parent 15cd62d commit d7b99148be42711639d1dd5715228dac8a4cc540
@@ -283,6 +283,7 @@ public static function getFileReportOptions(array $report_file_paths, bool $show
'checkstyle.xml' => Report::TYPE_CHECKSTYLE,
'sonarqube.json' => Report::TYPE_SONARQUBE,
'summary.json' => Report::TYPE_JSON_SUMMARY,
'junit.xml' => Report::TYPE_JUNIT,
'.xml' => Report::TYPE_XML,
'.json' => Report::TYPE_JSON,
'.txt' => Report::TYPE_TEXT,
@@ -20,6 +20,7 @@
use Psalm\Report\EmacsReport;
use Psalm\Report\JsonReport;
use Psalm\Report\JsonSummaryReport;
use Psalm\Report\JunitReport;
use Psalm\Report\PylintReport;
use Psalm\Report\SonarqubeReport;
use Psalm\Report\TextReport;
@@ -629,6 +630,10 @@ public static function getOutput(
$output = new XmlReport(self::$issues_data, self::$fixable_issue_counts, $report_options);
break;

case Report::TYPE_JUNIT:
$output = new JUnitReport(self::$issues_data, self::$fixable_issue_counts, $report_options);
break;

case Report::TYPE_CONSOLE:
$output = new ConsoleReport(self::$issues_data, self::$fixable_issue_counts, $report_options);
break;
@@ -13,6 +13,7 @@ abstract class Report
const TYPE_SONARQUBE = 'sonarqube';
const TYPE_EMACS = 'emacs';
const TYPE_XML = 'xml';
const TYPE_JUNIT = 'junit';
const TYPE_CHECKSTYLE = 'checkstyle';
const TYPE_TEXT = 'text';

@@ -25,6 +26,7 @@ abstract class Report
self::TYPE_SONARQUBE,
self::TYPE_EMACS,
self::TYPE_XML,
self::TYPE_JUNIT,
self::TYPE_CHECKSTYLE,
self::TYPE_TEXT,
];
@@ -0,0 +1,251 @@
<?php
namespace Psalm\Report;

use DOMDocument;
use DOMElement;
use Psalm\Config;
use Psalm\Report;
use function count;
use function sprintf;
use function trim;
use Doctrine\Instantiator\Exception\UnexpectedValueException;

/**
* based on https://github.com/m50/psalm-json-to-junit
* Copyright (c) Marisa Clardy marisa@clardy.eu
*
* with a few modifications
*/
class JunitReport extends Report
{
/**
* {@inheritdoc}
*/
public function create(): string
{
$errors = 0;
$warnings = 0;
$tests = 0;

$ndata = [];

foreach ($this->issues_data as $error) {
$is_error = $error['severity'] === Config::REPORT_ERROR;
$is_warning = $error['severity'] === Config::REPORT_INFO;

if ($is_error) {
$errors++;
} elseif ($is_warning) {
$warnings++;
} else {
// currently this never happens
continue;
}

$tests++;

$fname = $error['file_name'];

if (!isset($ndata[$fname])) {
$ndata[$fname] = [
'errors' => $is_error ? 1 : 0,
'warnings' => $is_warning ? 1 : 0,
'failures' => [
$this->createFailure($error),
],
];
} else {
if ($is_error) {
$ndata[$fname]['errors']++;
} else {
$ndata[$fname]['warnings']++;
}

$ndata[$fname]['failures'][] = $this->createFailure($error);
}
}

$dom = new DOMDocument('1.0', 'UTF-8');
$dom->formatOutput = true;

$schema = 'https://raw.githubusercontent.com/junit-team/'.
'junit5/r5.5.1/platform-tests/src/test/resources/jenkins-junit.xsd';

$suites = $dom->createElement('testsuites');
$testsuite = $dom->createElement('testsuite');

if ($testsuite === false) {
throw new \UnexpectedValueException('Bad falsy value');
}

$testsuite->setAttribute('failures', (string) $errors);
$testsuite->setAttribute('warnings', (string) $warnings);
$testsuite->setAttribute('name', 'psalm');
$testsuite->setAttribute('tests', (string) $tests);
$testsuite->setAttribute('xmlns:xsi', 'http://www.w3.org/2001/XMLSchema-instance');
$testsuite->setAttribute('xsi:noNamespaceSchemaLocation', $schema);
$suites->appendChild($testsuite);
$dom->appendChild($suites);

if (!count($ndata)) {
$testcase = $dom->createElement('testcase');
$testcase->setAttribute('name', 'psalm');
$testsuite->appendChild($testcase);
} else {
foreach ($ndata as $file => $report) {
$this->createTestSuite($dom, $testsuite, $file, $report);
}
}



return $dom->saveXML();
}

/**
* @param array{
* errors: int,
* warnings: int,
* failures: list<array{
* data: array{
* column_from: int,
* column_to: int,
* line: int,
* message: string,
* selected_text: string,
* snippet: string,
* type: string},
* type: string
* }>
* } $report
*/
private function createTestSuite(DOMDocument $dom, DOMElement $parent, string $file, array $report): void
{
$totalTests = $report['errors'] + $report['warnings'];
if ($totalTests < 1) {
$totalTests = 1;
}

$testsuite = $dom->createElement('testsuite');
$testsuite->setAttribute('name', $file);
$testsuite->setAttribute('file', $file);
$testsuite->setAttribute('assertions', (string) $totalTests);
$testsuite->setAttribute('failures', (string) $report['errors']);
$testsuite->setAttribute('warnings', (string) $report['warnings']);

$failuresByType = $this->groupByType($report['failures']);
$testsuite->setAttribute('tests', (string) count($failuresByType));

$iterator = 0;
foreach ($failuresByType as $type => $data) {
foreach ($data as $d) {
$testcase = $dom->createElement('testcase');
$testcase->setAttribute('name', "{$file}:{$d['line']}");
$testcase->setAttribute('file', $file);
$testcase->setAttribute('class', $type);
$testcase->setAttribute('classname', $type);
$testcase->setAttribute('line', (string) $d['line']);
$testcase->setAttribute('assertions', (string) count($data));

$failure = $dom->createElement('failure');
$failure->setAttribute('type', $type);
$failure->nodeValue = $this->dataToOutput($d);

$testcase->appendChild($failure);
$testsuite->appendChild($testcase);
}
$iterator++;
}
$parent->appendChild($testsuite);
}

/**
* @param array{
* line_from: int,
* type: string,
* message: string,
* selected_text: string,
* snippet: string,
* column_from: int,
* column_to: int
* } $issue_data
*
* @return array{
* data: array{
* column_from: int,
* column_to: int,
* line: int,
* message: string,
* selected_text: string,
* snippet: string,
* type: string
* },
* type: string
* }
*/
private function createFailure(array $issue_data) : array
{
return [
'type' => $issue_data['type'],
'data' => [
'message' => $issue_data['message'],
'type' => $issue_data['type'],
'snippet' => $issue_data['snippet'],
'selected_text' => $issue_data['selected_text'],
'line' => $issue_data['line_from'],
'column_from' => $issue_data['column_from'],
'column_to' => $issue_data['column_to'],
],
];
}

/**
* @param array<array{
* data: array{
* column_from: int,
* column_to: int,
* line: int,
* message: string,
* selected_text: string,
* snippet: string,
* type: string
* },
* type: string
* }> $failures
*
* @return array<string, non-empty-list<array{
* column_from: int,
* column_to: int,
* line: int,
* message: string,
* selected_text: string,
* snippet: string,
* type: string
* }>>
*/
private function groupByType(array $failures)
{
$nfailures = [];

foreach ($failures as $failure) {
$nfailures[$failure['type']][] = $failure['data'];
}

return $nfailures;
}

/**
* @param array<string, int|string> $data
*/
private function dataToOutput(array $data): string
{
$ret = '';

foreach ($data as $key => $value) {
$value = trim((string) $value);
$ret .= "{$key}: {$value}\n";
}

return $ret;
}
}
@@ -333,7 +333,8 @@ function getPsalmHelpText(): string
Enable monochrome output
--output-format=console
Changes the output format. Available formats: compact, console, emacs, json, pylint, xml, checkstyle, sonarqube
Changes the output format.
Available formats: compact, console, emacs, json, pylint, xml, checkstyle, junit, sonarqube
--no-progress
Disable the progress indicator
@@ -412,6 +412,7 @@ public function testCompactReport()
$this->toUnixLineEndings(IssueBuffer::getOutput($compact_report_options))
);
}

/**
* @return void
*/
@@ -448,6 +449,74 @@ public function testCheckstyleReport()
//);
}

/**
* @return void
*/
public function testJunitReport()
{
$this->analyzeFileForReport();

$checkstyle_report_options = ProjectAnalyzer::getFileReportOptions([__DIR__ . '/test-report.junit.xml'])[0];

$this->assertSame(
'<?xml version="1.0" encoding="UTF-8"?>
<testsuites>
<testsuite failures="3" warnings="1" name="psalm" tests="4" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="https://raw.githubusercontent.com/junit-team/junit5/r5.5.1/platform-tests/src/test/resources/jenkins-junit.xsd">
<testsuite name="somefile.php" file="somefile.php" assertions="4" failures="3" warnings="1" tests="4">
<testcase name="somefile.php:3" file="somefile.php" class="UndefinedVariable" classname="UndefinedVariable" line="3" assertions="1">
<failure type="UndefinedVariable">message: Cannot find referenced variable $as_you
type: UndefinedVariable
snippet: return $as_you . "type";
selected_text: $as_you
line: 3
column_from: 10
column_to: 17
</failure>
</testcase>
<testcase name="somefile.php:2" file="somefile.php" class="MixedInferredReturnType" classname="MixedInferredReturnType" line="2" assertions="1">
<failure type="MixedInferredReturnType">message: Could not verify return type \'null|string\' for psalmCanVerify
type: MixedInferredReturnType
snippet: function psalmCanVerify(int $your_code): ?string {
selected_text: ?string
line: 2
column_from: 42
column_to: 49
</failure>
</testcase>
<testcase name="somefile.php:7" file="somefile.php" class="UndefinedConstant" classname="UndefinedConstant" line="7" assertions="1">
<failure type="UndefinedConstant">message: Const CHANGE_ME is not defined
type: UndefinedConstant
snippet: echo CHANGE_ME;
selected_text: CHANGE_ME
line: 7
column_from: 6
column_to: 15
</failure>
</testcase>
<testcase name="somefile.php:15" file="somefile.php" class="PossiblyUndefinedGlobalVariable" classname="PossiblyUndefinedGlobalVariable" line="15" assertions="1">
<failure type="PossiblyUndefinedGlobalVariable">message: Possibly undefined global variable $a, first seen on line 10
type: PossiblyUndefinedGlobalVariable
snippet: echo $a
selected_text: $a
line: 15
column_from: 6
column_to: 8
</failure>
</testcase>
</testsuite>
</testsuite>
</testsuites>
',
IssueBuffer::getOutput($checkstyle_report_options)
);

// FIXME: The XML parser only return strings, all int value are casted, so the assertSame failed
//$this->assertSame(
// ['report' => ['item' => $issue_data]],
// XML2Array::createArray(IssueBuffer::getOutput(ProjectAnalyzer::TYPE_XML, false), LIBXML_NOCDATA)
//);
}

/**
* @return void
*/

0 comments on commit d7b9914

Please sign in to comment.
You can’t perform that action at this time.