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(junit): add junit report #66

Merged
merged 1 commit into from
Apr 19, 2024
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
17 changes: 12 additions & 5 deletions .github/workflows/ci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,14 @@ on:
push:
branches:
- master
pull_request: null
schedule:
- cron: "0 0 * * MON"
pull_request: ~
jobs:
tests:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
php-version: ["8.2", "8.3"]
php-version: ["8.3"]
composer-flags: [""]
name: [""]
include:
Expand All @@ -34,4 +32,13 @@ jobs:
- name: Run docker
run: docker run -d --rm -p 8081:80 httpbin
- name: tests
run: composer test
run: bin/asynit tests --report report/asynit.xml

- name: Test Report
uses: dorny/test-reporter@v1
if: success() || failure() # run this step even if previous step failed
with:
name: Asynit Tests ${{ matrix.php-version }}
path: report/asynit.xml
reporter: java-junit

1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ composer.lock
.php_cs.cache
.php-cs-fixer.cache
/vendor/
report

# Commit your application's lock file http://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file
# You may choose to ignore a library lock file http://getcomposer.org/doc/02-libraries.md#lock-file
Expand Down
16 changes: 14 additions & 2 deletions src/Command/AsynitCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
use Asynit\Output\OutputFactory;
use Asynit\Parser\TestPoolBuilder;
use Asynit\Parser\TestsFinder;
use Asynit\Report\JUnitReport;
use Asynit\Runner\PoolRunner;
use Asynit\TestWorkflow;
use Symfony\Component\Console\Command\Command;
Expand Down Expand Up @@ -35,6 +36,7 @@ protected function configure(): void
->addOption('retry', null, InputOption::VALUE_REQUIRED, 'Default retry number for http request', 0)
->addOption('bootstrap', null, InputOption::VALUE_REQUIRED, 'A PHP file to include before anything else', $this->defaultBootstrapFilename)
->addOption('order', null, InputOption::VALUE_NONE, 'Output tests execution order')
->addOption('report', null, InputOption::VALUE_REQUIRED, 'JUnit report directory')
;
}

Expand All @@ -55,8 +57,7 @@ protected function execute(InputInterface $input, OutputInterface $output): int
$testsSuites = $testsFinder->findTests($target);
$testsCount = array_reduce($testsSuites, fn (int $carry, $suite) => $carry + \count($suite->tests), 0);

/** @phpstan-ignore-next-line */
$useOrder = (boolean) $input->getOption('order');
$useOrder = (bool) $input->getOption('order');

list($chainOutput, $countOutput) = (new OutputFactory($useOrder))->buildOutput($testsCount);

Expand All @@ -82,7 +83,18 @@ protected function execute(InputInterface $input, OutputInterface $output): int

// Build a list of tests from the directory
$pool = $builder->build($testsSuites);
$start = microtime(true);
$runner->loop($pool);
$end = microtime(true);

/** @var string|null $reportDir */
$reportDir = $input->getOption('report');

if (null !== $reportDir) {
$report = new JUnitReport($reportDir);
$time = $end - $start;
$report->generate($time, $testsSuites);
}

// Return the number of failed tests
return $countOutput->getFailed();
Expand Down
2 changes: 1 addition & 1 deletion src/Parser/TestPoolBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ private function processTestAnnotations(\ArrayObject $tests, Test $test): void
throw new \RuntimeException(sprintf('Failed to build test pool "%s" dependency is not resolvable for "%s::%s".', $dependency->dependency, $test->getMethod()->getDeclaringClass()->getName(), $test->getMethod()->getName()));
}

$dependentTest = new Test(new \ReflectionMethod($class, $method), null, false);
$dependentTest = new Test(null, new \ReflectionMethod($class, $method), null, false);

if (isset($tests[$dependentTest->getIdentifier()])) {
$dependentTest = $tests[$dependentTest->getIdentifier()];
Expand Down
4 changes: 2 additions & 2 deletions src/Parser/TestsFinder.php
Original file line number Diff line number Diff line change
Expand Up @@ -66,9 +66,9 @@ private function doFindTests(iterable $files): array
$test = null;

if (count($tests) > 0) {
$test = new Test($reflectionMethod);
$test = new Test($testSuite, $reflectionMethod);
} elseif (preg_match('/^test(.+)$/', $reflectionMethod->getName())) {
$test = new Test($reflectionMethod);
$test = new Test($testSuite, $reflectionMethod);
}

if (null !== $test) {
Expand Down
136 changes: 136 additions & 0 deletions src/Report/JUnitReport.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
<?php

namespace Asynit\Report;

use Asynit\Test;
use Asynit\TestSuite;
use bovigo\assert\AssertionFailure;

final class JUnitReport
{
public function __construct(
private readonly string $filename
) {
}

/**
* @param TestSuite<object>[] $testSuites
*/
public function generate(float $time, array $testSuites): void
{
$xml = new \DOMDocument('1.0', 'UTF-8');
$xml->formatOutput = true;

$root = $xml->createElement('testsuites');
$root->setAttribute('name', 'asynit');

$totalTests = 0;
$totalFailures = 0;
$totalErrors = 0;
$totalSuccess = 0;
$totalSKipped = 0;
$totalAssertions = 0;

/** @var TestSuite<object> $testSuite */
foreach ($testSuites as $testSuite) {
$testsCount = count($testSuite->tests);

if (0 === $testsCount) {
continue;
}

$failures = $testSuite->getFailure();
$errors = $testSuite->getErrors();
$success = $testSuite->getSuccess();
$skipped = $testSuite->getSkipped();
$assertions = $testSuite->getAssertions();

$totalTests += $testsCount;
$totalFailures += $failures;
$totalErrors += $errors;
$totalSuccess += $success;
$totalSKipped += $skipped;
$totalAssertions += $assertions;

$testsuites = $xml->createElement('testsuite');
$testsuites->setAttribute('name', $testSuite->reflectionClass->getName());
$testsuites->setAttribute('tests', (string) $testsCount);
$testsuites->setAttribute('failures', (string) $failures);
$testsuites->setAttribute('errors', (string) $errors);
$testsuites->setAttribute('skipped', (string) $skipped);
$testsuites->setAttribute('assertions', (string) $assertions);
$testsuites->setAttribute('time', (string) $testSuite->getTime());
// timestamp in ISO 8601 format
$date = \DateTime::createFromFormat('U.u', (string) $testSuite->startTime);

if ($date) {
$testsuites->setAttribute('timestamp', $date->format(\DateTimeInterface::ISO8601_EXPANDED));
}

$testsuites->setAttribute('file', (string) $testSuite->reflectionClass->getFileName());
$root->appendChild($testsuites);

/** @var Test $test */
foreach ($testSuite->tests as $test) {
$testcase = $xml->createElement('testcase');
$testcase->setAttribute('name', $test->getDisplayName());
$testcase->setAttribute('classname', $testSuite->reflectionClass->getName());
$testcase->setAttribute('assertions', (string) $test->getAssertionsCount());
$testcase->setAttribute('time', (string) $test->getTime());
$testcase->setAttribute('file', (string) $testSuite->reflectionClass->getFileName());
$testcase->setAttribute('line', (string) $test->method->getStartLine());
$date = \DateTime::createFromFormat('U.u', (string) $test->startTime);

if ($date) {
$testcase->setAttribute('timestamp', $date->format(\DateTimeInterface::ISO8601_EXPANDED));
}

$testsuites->appendChild($testcase);

if ('' !== $test->output) {
$systemOut = $xml->createElement('system-out');
$systemOut->appendChild($xml->createCDATASection($test->output));
$testcase->appendChild($systemOut);
}

if (Test::STATE_FAILURE === $test->state) {
if ($test->failure instanceof AssertionFailure) {
$failure = $xml->createElement('failure');
$failure->setAttribute('message', $test->failure->getMessage());
$failure->setAttribute('type', get_class($test->failure));

$testcase->appendChild($failure);
} else {
$failure = $xml->createElement('error');
$failure->setAttribute('message', $test->failure->getMessage());
$failure->setAttribute('type', get_class($test->failure));

$testcase->appendChild($failure);
}
}

if (Test::STATE_SKIPPED === $test->state) {
$skipped = $xml->createElement('skipped');
$testcase->appendChild($skipped);
}
}
}

$directory = dirname($this->filename);

if (!is_dir($directory)) {
@mkdir($directory, 0755, true);
}

$root->setAttribute('tests', (string) $totalTests);
$root->setAttribute('failures', (string) $totalFailures);
$root->setAttribute('errors', (string) $totalErrors);
$root->setAttribute('skipped', (string) $totalSKipped);
$root->setAttribute('assertions', (string) $totalAssertions);
$root->setAttribute('time', (string) $time);

$xml->appendChild($root);

$xml->save($this->filename);
}
}
59 changes: 52 additions & 7 deletions src/Test.php
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,24 @@ final class Test

private string $identifier;

private string $state;

private string $displayName;

public string $state;

public float $startTime;

public float $endTime;

public string $output;

public \Throwable $failure;

/**
* @param TestSuite<object>|null $suite
*/
public function __construct(
private readonly \ReflectionMethod $method,
public readonly ?TestSuite $suite,
public readonly \ReflectionMethod $method,
?string $identifier = null,
public readonly bool $isRealTest = true,
) {
Expand Down Expand Up @@ -75,14 +87,37 @@ public function canBeRun(): bool
return true;
}

public function getState(): string
public function start(): void
{
$this->suite?->start();
$this->startTime = microtime(true);
$this->state = self::STATE_RUNNING;
}

public function success(string $output): void
{
return $this->state;
$this->endTime = microtime(true);
$this->output = $output;
$this->state = self::STATE_SUCCESS;
$this->suite?->tryEnd();
}

public function setState(string $state): void
public function failure(string $output, \Throwable $error): void
{
$this->state = $state;
$this->endTime = microtime(true);
$this->output = $output;
$this->state = self::STATE_FAILURE;
$this->failure = $error;
$this->suite?->tryEnd();
}

public function skipped(): void
{
$this->startTime = microtime(true);
$this->endTime = microtime(true);
$this->output = '';
$this->state = self::STATE_SKIPPED;
$this->suite?->tryEnd();
}

public function getIdentifier(): string
Expand Down Expand Up @@ -124,6 +159,11 @@ public function getAssertions(): array
return $this->assertions;
}

public function getAssertionsCount(): int
{
return \count($this->assertions);
}

/**
* @return Test[]
*/
Expand Down Expand Up @@ -171,4 +211,9 @@ public function setDisplayName(string $displayName): void
{
$this->displayName = $displayName;
}

public function getTime(): float
{
return $this->endTime - $this->startTime;
}
}
Loading