Skip to content

Commit

Permalink
feat(junit): add junit report
Browse files Browse the repository at this point in the history
  • Loading branch information
joelwurtz committed Apr 19, 2024
1 parent ac6e415 commit 5ed4d51
Show file tree
Hide file tree
Showing 9 changed files with 316 additions and 28 deletions.
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

0 comments on commit 5ed4d51

Please sign in to comment.