Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(teamcity): Add basic teamcity output format #141

Merged
merged 6 commits into from
Sep 11, 2020
Merged
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
9 changes: 9 additions & 0 deletions phpstan.neon
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,12 @@ parameters:
- "#has parameter \\$closure with default value.#"
- "#has parameter \\$description with default value.#"
- "#Method Pest\\\\Support\\\\Reflection::getParameterClassName\\(\\) has a nullable return type declaration.#"
-
message: '#Call to an undefined method PHPUnit\\Framework\\Test::getName\(\)#'
path: src/TeamCity.php
-
message: '#invalid typehint type Pest\\Concerns\\TestCase#'
path: src/TeamCity.php
-
message: '#is not subtype of native type PHPUnit\\Framework\\Test#'
path: src/TeamCity.php
6 changes: 5 additions & 1 deletion rector.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
declare(strict_types=1);

use Rector\Core\Configuration\Option;
use Rector\Php70\Rector\StaticCall\StaticCallOnNonStaticToInstanceCallRector;
use Rector\Set\ValueObject\SetList;
use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;

Expand All @@ -27,5 +28,8 @@
SetList::SOLID,
]);

$parameters->set(Option::PATHS, [__DIR__.'/src', __DIR__.'/tests']);
$parameters->set(Option::PATHS, [__DIR__ . '/src', __DIR__ . '/tests']);
$parameters->set(Option::EXCLUDE_RECTORS, [
StaticCallOnNonStaticToInstanceCallRector::class,
]);
};
12 changes: 10 additions & 2 deletions src/Actions/AddsDefaults.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,16 @@
namespace Pest\Actions;

use NunoMaduro\Collision\Adapters\Phpunit\Printer;
use Pest\TeamCity;
use PHPUnit\TextUI\DefaultResultPrinter;

/**
* @internal
*/
final class AddsDefaults
{
private const PRINTER = 'printer';

/**
* Adds default arguments to the given `arguments` array.
*
Expand All @@ -20,8 +24,12 @@ final class AddsDefaults
*/
public static function to(array $arguments): array
{
if (!array_key_exists('printer', $arguments)) {
$arguments['printer'] = new Printer(null, $arguments['verbose'] ?? false, $arguments['colors'] ?? 'always');
if (!array_key_exists(self::PRINTER, $arguments)) {
$arguments[self::PRINTER] = new Printer(null, $arguments['verbose'] ?? false, $arguments['colors'] ?? DefaultResultPrinter::COLOR_ALWAYS);
}

if ($arguments[self::PRINTER] === \PHPUnit\Util\Log\TeamCity::class) {
$arguments[self::PRINTER] = new TeamCity($arguments['verbose'] ?? false, $arguments['colors'] ?? DefaultResultPrinter::COLOR_ALWAYS);
}

return $arguments;
Expand Down
5 changes: 5 additions & 0 deletions src/Concerns/TestCase.php
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,11 @@ public function getName(bool $withDataSet = true): string
return $this->__description;
}

public static function __getFileName(): string
{
return self::$__filename;
}

/**
* This method is called before the first test of this test class is run.
*/
Expand Down
235 changes: 235 additions & 0 deletions src/TeamCity.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,235 @@
<?php

declare(strict_types=1);

namespace Pest;

use function getmypid;
use Pest\Concerns\TestCase;
use PHPUnit\Framework\AssertionFailedError;
use PHPUnit\Framework\Test;
use PHPUnit\Framework\TestResult;
use PHPUnit\Framework\TestSuite;
use PHPUnit\Framework\Warning;
use PHPUnit\TextUI\DefaultResultPrinter;
use function round;
use function str_replace;
use Throwable;

final class TeamCity extends DefaultResultPrinter
{
private const PROTOCOL = 'pest_qn://';
private const NAME = 'name';
private const LOCATION_HINT = 'locationHint';
private const DURATION = 'duration';
private const TEST_SUITE_STARTED = 'testSuiteStarted';
private const TEST_SUITE_FINISHED = 'testSuiteFinished';
private const TEST_FAILED = 'testFailed';

/** @var int */
private $flowId;

/** @var bool */
private $isSummaryTestCountPrinted = false;

/** @var \PHPUnit\Util\Log\TeamCity */
private $phpunitTeamCity;

public function __construct(bool $verbose, string $colors)
{
parent::__construct(null, $verbose, $colors, false, 80, false);
$this->phpunitTeamCity = new \PHPUnit\Util\Log\TeamCity(
null,
$verbose,
$colors,
false,
80,
false
);
}

public function printResult(TestResult $result): void
{
$this->printHeader($result);
$this->printFooter($result);
}

/** @phpstan-ignore-next-line */
public function startTestSuite(TestSuite $suite): void
{
$this->flowId = getmypid();

if (!$this->isSummaryTestCountPrinted) {
$this->printEvent(
'testCount',
['count' => $suite->count()]
);
$this->isSummaryTestCountPrinted = true;
}

$suiteName = $suite->getName();

if (file_exists($suiteName) || !method_exists($suiteName, '__getFileName')) {
$this->printEvent(
self::TEST_SUITE_STARTED, [
self::NAME => $suiteName,
self::LOCATION_HINT => self::PROTOCOL . $suiteName,
]);

return;
}

$fileName = $suiteName::__getFileName();

$this->printEvent(
self::TEST_SUITE_STARTED, [
self::NAME => substr($suiteName, 2),
self::LOCATION_HINT => self::PROTOCOL . $fileName,
]);
}

/** @phpstan-ignore-next-line */
public function endTestSuite(TestSuite $suite): void
{
$suiteName = $suite->getName();

if (file_exists($suiteName) || !method_exists($suiteName, '__getFileName')) {
$this->printEvent(
self::TEST_SUITE_FINISHED, [
self::NAME => $suiteName,
self::LOCATION_HINT => self::PROTOCOL . $suiteName,
]);

return;
}

$this->printEvent(
self::TEST_SUITE_FINISHED, [
self::NAME => substr($suiteName, 2),
]);
}

/**
* @param Test|TestCase $test
*/
public function startTest(Test $test): void
{
if (!TeamCity::isPestTest($test)) {
$this->phpunitTeamCity->startTest($test);

return;
}

$this->printEvent('testStarted', [
self::NAME => $test->getName(),
/* @phpstan-ignore-next-line */
self::LOCATION_HINT => self::PROTOCOL . $test->toString(),
]);
}

/**
* @param Test|TestCase $test
*/
public function endTest(Test $test, float $time): void
{
if (!TeamCity::isPestTest($test)) {
$this->phpunitTeamCity->endTest($test, $time);

return;
}

$this->printEvent('testFinished', [
self::NAME => $test->getName(),
self::DURATION => self::toMilliseconds($time),
]);
}

/**
* @param Test|TestCase $test
*/
public function addError(Test $test, Throwable $t, float $time): void
{
if (!TeamCity::isPestTest($test)) {
$this->phpunitTeamCity->addError($test, $t, $time);

return;
}

$this->printEvent(
self::TEST_FAILED, [
self::NAME => $test->getName(),
'message' => $t->getMessage(),
'details' => $t->getTraceAsString(),
self::DURATION => self::toMilliseconds($time),
]);
}

/**
* @phpstan-ignore-next-line
*
* @param Test|TestCase $test
*/
public function addWarning(Test $test, Warning $e, float $time): void
{
if (!TeamCity::isPestTest($test)) {
$this->phpunitTeamCity->addWarning($test, $e, $time);

return;
}

$this->printEvent(
self::TEST_FAILED, [
self::NAME => $test->getName(),
'message' => $e->getMessage(),
'details' => $e->getTraceAsString(),
self::DURATION => self::toMilliseconds($time),
]);
}

public function addFailure(Test $test, AssertionFailedError $e, float $time): void
{
$this->phpunitTeamCity->addFailure($test, $e, $time);
}

protected function writeProgress(string $progress): void
{
}

/**
* @param array<string, string|int> $params
*/
private function printEvent(string $eventName, array $params = []): void
{
$this->write("\n##teamcity[{$eventName}");

if ($this->flowId !== 0) {
$params['flowId'] = $this->flowId;
}

foreach ($params as $key => $value) {
$escapedValue = self::escapeValue((string) $value);
$this->write(" {$key}='{$escapedValue}'");
}

$this->write("]\n");
}

private static function escapeValue(string $text): string
{
return str_replace(
['|', "'", "\n", "\r", ']', '['],
['||', "|'", '|n', '|r', '|]', '|['],
$text
);
}

private static function toMilliseconds(float $time): int
{
return (int) round($time * 1000);
}

private static function isPestTest(Test $test): bool
{
return in_array(TestCase::class, class_uses($test), true);
}
}