diff --git a/composer.json b/composer.json index d0dd2bd..85fe63d 100644 --- a/composer.json +++ b/composer.json @@ -38,9 +38,8 @@ "amphp/http-client": "To use the web test case trait" }, "require-dev": { - "amphp/http-client": "^5.0.1", "friendsofphp/php-cs-fixer": "^3.53.0", - "phpstan/phpstan": "^1.10" + "phpstan/phpstan": "^1.10.67" }, "conflict": { "amphp/http-client": "<5.0.1", diff --git a/phpstan.neon b/phpstan.neon new file mode 100644 index 0000000..dff4656 --- /dev/null +++ b/phpstan.neon @@ -0,0 +1,6 @@ +parameters: + level: max + paths: + - src/ + excludePaths: + - src/Assert/Assertion.php \ No newline at end of file diff --git a/src/Assert/Assertion.php b/src/Assert/Assertion.php index 93937af..df136c8 100644 --- a/src/Assert/Assertion.php +++ b/src/Assert/Assertion.php @@ -5,7 +5,6 @@ namespace Asynit\Assert; use Asynit\Runner\TestStorage; -use Asynit\Test; use bovigo\assert\Assertion as BaseAssertion; use bovigo\assert\AssertionFailure; use bovigo\assert\predicate\Predicate; diff --git a/src/Command/AsynitCommand.php b/src/Command/AsynitCommand.php index 301615e..b5e1e23 100644 --- a/src/Command/AsynitCommand.php +++ b/src/Command/AsynitCommand.php @@ -14,7 +14,10 @@ use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; -class AsynitCommand extends Command +/** + * @internal + */ +final class AsynitCommand extends Command { private string $defaultBootstrapFilename = ''; @@ -45,22 +48,40 @@ protected function execute(InputInterface $input, OutputInterface $output): int throw new \InvalidArgumentException("The bootstrap file '$bootstrapFilename' does not exist."); } + /** @var string $target */ + $target = $input->getArgument('target'); + $testsFinder = new TestsFinder(); - $testMethods = $testsFinder->findTests($input->getArgument('target')); + $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'); + + list($chainOutput, $countOutput) = (new OutputFactory($useOrder))->buildOutput($testsCount); - list($chainOutput, $countOutput) = (new OutputFactory($input->getOption('order')))->buildOutput(\count($testMethods)); + /** @phpstan-ignore-next-line */ + $timeout = (float) $input->getOption('timeout'); + /** @phpstan-ignore-next-line */ + $retry = (int) $input->getOption('retry'); + /** @phpstan-ignore-next-line */ + $concurrency = (int) $input->getOption('concurrency'); + + if ($concurrency < 1) { + throw new \InvalidArgumentException('Concurrency must be greater than 0'); + } $defaultHttpConfiguration = new HttpClientConfiguration( - timeout: $input->getOption('timeout'), - retry: $input->getOption('retry'), + timeout: $timeout, + retry: $retry, allowSelfSignedCertificate: $input->hasOption('allow-self-signed-certificate'), ); $builder = new TestPoolBuilder(); - $runner = new PoolRunner($defaultHttpConfiguration, new TestWorkflow($chainOutput), $input->getOption('concurrency')); + $runner = new PoolRunner($defaultHttpConfiguration, new TestWorkflow($chainOutput), $concurrency); // Build a list of tests from the directory - $pool = $builder->build($testMethods); + $pool = $builder->build($testsSuites); $runner->loop($pool); // Return the number of failed tests diff --git a/src/HttpClient/ApiResponse.php b/src/HttpClient/ApiResponse.php index 2d5b0e8..04b77d0 100644 --- a/src/HttpClient/ApiResponse.php +++ b/src/HttpClient/ApiResponse.php @@ -14,7 +14,7 @@ class ApiResponse extends HttpResponse implements \ArrayAccess /** * @var array|null */ - private ?array $data = null; + private mixed $data = null; public function __construct(private readonly Response $response) { @@ -24,7 +24,13 @@ public function __construct(private readonly Response $response) private function ensureBodyIsRead(bool $associative = true): void { if (null === $this->data) { - $this->data = json_decode($this->response->getBody()->read(), $associative, flags: JSON_THROW_ON_ERROR); + $data = json_decode((string) $this->response->getBody(), $associative, flags: JSON_THROW_ON_ERROR); + + if (!is_array($data)) { + throw new \InvalidArgumentException('The response body is not a valid JSON object.'); + } + + $this->data = $data; } } diff --git a/src/Output/Chain.php b/src/Output/Chain.php index 05d4e84..b33c27a 100644 --- a/src/Output/Chain.php +++ b/src/Output/Chain.php @@ -9,38 +9,38 @@ class Chain implements OutputInterface { /** @var OutputInterface[] */ - private $outputs = []; + private array $outputs = []; /** * Add output to the chain. */ - public function addOutput(OutputInterface $output) + public function addOutput(OutputInterface $output): void { $this->outputs[] = $output; } - public function outputStep(Test $test, $debugOutput) + public function outputStep(Test $test, string $debugOutput): void { foreach ($this->outputs as $output) { $output->outputStep($test, $debugOutput); } } - public function outputFailure(Test $test, $debugOutput, $failure) + public function outputFailure(Test $test, string $debugOutput, \Throwable $failure): void { foreach ($this->outputs as $output) { $output->outputFailure($test, $debugOutput, $failure); } } - public function outputSuccess(Test $test, $debugOutput) + public function outputSuccess(Test $test, string $debugOutput): void { foreach ($this->outputs as $output) { $output->outputSuccess($test, $debugOutput); } } - public function outputSkipped(Test $test, $debugOutput) + public function outputSkipped(Test $test, string $debugOutput): void { foreach ($this->outputs as $output) { $output->outputSkipped($test, $debugOutput); diff --git a/src/Output/Count.php b/src/Output/Count.php index 74b39e4..303c9ef 100644 --- a/src/Output/Count.php +++ b/src/Output/Count.php @@ -8,41 +8,35 @@ class Count implements OutputInterface { - private $succeed = 0; - private $failed = 0; - private $skipped = 0; + private int $succeed = 0; + private int $failed = 0; + private int $skipped = 0; - public function outputStep(Test $test, $debugOutput) + public function outputStep(Test $test, string $debugOutput): void { } - public function outputFailure(Test $test, $debugOutput, $failure) + public function outputFailure(Test $test, string $debugOutput, \Throwable $failure): void { ++$this->failed; } - public function outputSuccess(Test $test, $debugOutput) + public function outputSuccess(Test $test, string $debugOutput): void { ++$this->succeed; } - public function outputSkipped(Test $test, $debugOutput) + public function outputSkipped(Test $test, string $debugOutput): void { ++$this->skipped; } - /** - * @return int - */ - public function getSucceed() + public function getSucceed(): int { return $this->succeed; } - /** - * @return int - */ - public function getFailed() + public function getFailed(): int { return $this->failed; } diff --git a/src/Output/OutputFactory.php b/src/Output/OutputFactory.php index 0fb8966..198084a 100644 --- a/src/Output/OutputFactory.php +++ b/src/Output/OutputFactory.php @@ -6,16 +6,18 @@ /** * Allow to detect current environment and choose the best output. + * + * @internal */ -class OutputFactory +final class OutputFactory { - private $order = false; - - public function __construct(bool $order = false) + public function __construct(public readonly bool $order = false) { - $this->order = $order; } + /** + * @return array{Chain, Count} + */ public function buildOutput(int $testCount): array { $countOutput = new Count(); diff --git a/src/Output/OutputInterface.php b/src/Output/OutputInterface.php index 589a569..ef6c230 100644 --- a/src/Output/OutputInterface.php +++ b/src/Output/OutputInterface.php @@ -6,18 +6,16 @@ /** * Interface for displaying tests. + * + * @internal */ interface OutputInterface { - public function outputStep(Test $test, $debugOutput); + public function outputStep(Test $test, string $debugOutput): void; - /** - * @param string $debugOutput - * @param \Throwable|\Exception $failure - */ - public function outputFailure(Test $test, $debugOutput, $failure); + public function outputFailure(Test $test, string $debugOutput, \Throwable $failure): void; - public function outputSuccess(Test $test, $debugOutput); + public function outputSuccess(Test $test, string $debugOutput): void; - public function outputSkipped(Test $test, $debugOutput); + public function outputSkipped(Test $test, string $debugOutput): void; } diff --git a/src/Output/OutputOrder.php b/src/Output/OutputOrder.php index 55317d3..7f3135b 100644 --- a/src/Output/OutputOrder.php +++ b/src/Output/OutputOrder.php @@ -7,23 +7,23 @@ class OutputOrder implements OutputInterface { /** @var Test[] */ - private $tests = []; + private array $tests = []; - public function outputStep(Test $test, $debugOutput) + public function outputStep(Test $test, string $debugOutput): void { } - public function outputFailure(Test $test, $debugOutput, $failure) + public function outputFailure(Test $test, string $debugOutput, \Throwable $failure): void { $this->tests[] = $test; } - public function outputSuccess(Test $test, $debugOutput) + public function outputSuccess(Test $test, string $debugOutput): void { $this->tests[] = $test; } - public function outputSkipped(Test $test, $debugOutput) + public function outputSkipped(Test $test, string $debugOutput): void { } @@ -48,6 +48,11 @@ public function __destruct() } } + /** + * @param array $orders + * + * @return int[] + */ public function createDepends(Test $test, array $orders = []): array { $depends = []; diff --git a/src/Output/PhpUnitAlike.php b/src/Output/PhpUnitAlike.php index f80ed86..3f4905f 100644 --- a/src/Output/PhpUnitAlike.php +++ b/src/Output/PhpUnitAlike.php @@ -10,14 +10,15 @@ class PhpUnitAlike implements OutputInterface public const SPLIT_AT = 60; public const MAX_TRACE = 10; - private $outputFormatFail; - private $outputFormatSuccess; - private $outputFormatSkipped; - private $testOutputed; - private $failures; - private $assertionCount; - private $start; - private $testCount; + private OutputFormatterStyle $outputFormatFail; + private OutputFormatterStyle $outputFormatSuccess; + private OutputFormatterStyle $outputFormatSkipped; + private int $testOutputed; + /** @var array */ + private array $failures; + private int $assertionCount; + private float $start; + private int $testCount; public function __construct(int $testCount) { @@ -34,11 +35,11 @@ public function __construct(int $testCount) $this->failures = []; } - public function outputStep(Test $test, $debugOutput) + public function outputStep(Test $test, string $debugOutput): void { } - public function outputFailure(Test $test, $debugOutput, $failure) + public function outputFailure(Test $test, string $debugOutput, \Throwable $failure): void { $text = 'F'; @@ -57,7 +58,7 @@ public function outputFailure(Test $test, $debugOutput, $failure) ]; } - public function outputSuccess(Test $test, $debugOutput) + public function outputSuccess(Test $test, string $debugOutput): void { $this->writeTest($test, $this->outputFormatSuccess->apply('.')); fwrite(STDOUT, $debugOutput); @@ -65,7 +66,7 @@ public function outputSuccess(Test $test, $debugOutput) $this->assertionCount += \count($test->getAssertions()); } - public function outputSkipped(Test $test, $debugOutput) + public function outputSkipped(Test $test, string $debugOutput): void { $this->writeTest($test, $this->outputFormatSkipped->apply('S')); fwrite(STDOUT, $debugOutput); @@ -73,7 +74,7 @@ public function outputSkipped(Test $test, $debugOutput) $this->assertionCount += \count($test->getAssertions()); } - private function writeTest(Test $test, $text) + private function writeTest(Test $test, string $text): void { if (!$test->isRealTest) { return; @@ -89,7 +90,7 @@ private function writeTest(Test $test, $text) ++$this->testOutputed; } - private function writeFailure($step, Test $test, $failure) + private function writeFailure(int $step, Test $test, \Throwable $failure): void { fwrite(STDOUT, $step + 1 .') '.$test->getDisplayName()." failed\n"); @@ -143,7 +144,7 @@ public function __destruct() fwrite(STDOUT, $outputFormatSuccess->apply("OK, Tests: $this->testOutputed, Assertions: $this->assertionCount.\n")); } - private function getDisplayableMemory($memory) + private function getDisplayableMemory(int $memory): string { $unit = ['B', 'KB', 'MB', 'GB', 'TB', 'PB']; diff --git a/src/Parser/TestPoolBuilder.php b/src/Parser/TestPoolBuilder.php index 8f82bb8..c9a41eb 100644 --- a/src/Parser/TestPoolBuilder.php +++ b/src/Parser/TestPoolBuilder.php @@ -6,6 +6,7 @@ use Asynit\Attribute\DisplayName; use Asynit\Pool; use Asynit\Test; +use Asynit\TestSuite; /** * Build test. @@ -15,19 +16,26 @@ final class TestPoolBuilder /** * Build the initial test pool. * - * @param Test[] $tests + * @param TestSuite[] $testSuites * * @throws \RuntimeException */ - public function build(array $tests): Pool + public function build(array $testSuites): Pool { $pool = new Pool(); - $tests = new \ArrayObject($tests); + /** @var \ArrayObject $tests */ + $tests = new \ArrayObject(); + + foreach ($testSuites as $testSuite) { + foreach ($testSuite->tests as $test) { + $tests[$test->getIdentifier()] = $test; + } + } foreach ($tests as $test) { $this->processTestAnnotations($tests, $test); - $pool->addTest($test); + $pool->tests[] = $test; } return $pool; diff --git a/src/Parser/TestsFinder.php b/src/Parser/TestsFinder.php index a9b7992..b46b09b 100644 --- a/src/Parser/TestsFinder.php +++ b/src/Parser/TestsFinder.php @@ -5,11 +5,15 @@ use Asynit\Attribute\Test as TestAnnotation; use Asynit\Attribute\TestCase; use Asynit\Test; +use Asynit\TestSuite; use Symfony\Component\Finder\Finder; -class TestsFinder +/** + * @internal + */ +final class TestsFinder { - /** @return Test[] */ + /** @return TestSuite[] */ public function findTests(string $path): array { if (\is_file($path)) { @@ -28,13 +32,11 @@ public function findTests(string $path): array /** * @param iterable $files * - * @return Test[] - * - * @throws \ReflectionException + * @return TestSuite[] */ private function doFindTests(iterable $files): array { - $methods = []; + $suites = []; foreach ($files as $file) { $existingClasses = get_declared_classes(); @@ -56,6 +58,9 @@ private function doFindTests(iterable $files): array continue; } + $testSuite = new TestSuite($reflectionClass); + $suites[] = $testSuite; + foreach ($reflectionClass->getMethods(\ReflectionMethod::IS_PUBLIC) as $reflectionMethod) { $tests = $reflectionMethod->getAttributes(TestAnnotation::class); $test = null; @@ -67,12 +72,12 @@ private function doFindTests(iterable $files): array } if (null !== $test) { - $methods[$test->getIdentifier()] = $test; + $testSuite->tests[$test->getIdentifier()] = $test; } } } } - return $methods; + return $suites; } } diff --git a/src/Pool.php b/src/Pool.php index fc31207..d11806e 100644 --- a/src/Pool.php +++ b/src/Pool.php @@ -4,24 +4,13 @@ /** * Pool containing test, running tests and running http calls. + * + * @internal */ -class Pool +final class Pool { /** @var Test[] */ - private $tests; - - public function __construct() - { - $this->tests = []; - } - - /** - * Queue a test. - */ - public function addTest(Test $test): void - { - $this->tests[] = $test; - } + public array $tests = []; public function isEmpty(): bool { @@ -32,7 +21,7 @@ public function isEmpty(): bool return 0 === count($notCompletedTests); } - public function getTestToRun(): ?Test + public function getNextTestToRun(): ?Test { foreach ($this->tests as $test) { if ($test->canBeRun()) { diff --git a/src/Runner/PoolRunner.php b/src/Runner/PoolRunner.php index fbbacd8..ab72e72 100644 --- a/src/Runner/PoolRunner.php +++ b/src/Runner/PoolRunner.php @@ -38,7 +38,7 @@ public function loop(Pool $pool): void $futures = []; while (!$pool->isEmpty()) { - $test = $pool->getTestToRun(); + $test = $pool->getNextTestToRun(); if (null === $test) { Future\awaitAny($futures); diff --git a/src/Runner/TestStorage.php b/src/Runner/TestStorage.php index 2ad275a..12d6a4a 100644 --- a/src/Runner/TestStorage.php +++ b/src/Runner/TestStorage.php @@ -7,6 +7,7 @@ /** @internal */ final class TestStorage { + /** @var \WeakMap<\Fiber, Test|null>|null */ private static ?\WeakMap $localStorage = null; public static function set(Test $test): void @@ -17,7 +18,9 @@ public static function set(Test $test): void return; } + /* @phpstan-ignore-next-line */ self::$localStorage ??= new \WeakMap(); + /* @phpstan-ignore-next-line */ self::$localStorage[$fiber] = $test; } @@ -29,8 +32,10 @@ public static function get(): ?Test return null; } + /* @phpstan-ignore-next-line */ self::$localStorage ??= new \WeakMap(); + /* @phpstan-ignore-next-line */ return self::$localStorage[$fiber] ?? null; } } diff --git a/src/Test.php b/src/Test.php index 136a64b..8475406 100644 --- a/src/Test.php +++ b/src/Test.php @@ -2,7 +2,10 @@ namespace Asynit; -class Test +/** + * @internal + */ +final class Test { public const STATE_PENDING = 'pending'; public const STATE_RUNNING = 'running'; @@ -13,7 +16,7 @@ class Test /** @var Test[] */ private array $parents = []; - /** @var Test[] */ + /** @var array */ private array $children = []; /** @var mixed[] */ diff --git a/src/TestSuite.php b/src/TestSuite.php new file mode 100644 index 0000000..c24449e --- /dev/null +++ b/src/TestSuite.php @@ -0,0 +1,22 @@ + */ + public array $tests = []; + + /** + * @param \ReflectionClass $reflectionClass + */ + public function __construct( + public readonly \ReflectionClass $reflectionClass + ) { + } +} diff --git a/src/TestWorkflow.php b/src/TestWorkflow.php index df7e11d..01f2790 100644 --- a/src/TestWorkflow.php +++ b/src/TestWorkflow.php @@ -4,7 +4,10 @@ use Asynit\Output\OutputInterface; -class TestWorkflow +/** + * @internal + */ +final class TestWorkflow { public function __construct(private OutputInterface $output) { @@ -21,7 +24,7 @@ public function markTestAsRunning(Test $test): void $debugOutput = ob_get_contents(); ob_clean(); - $this->output->outputStep($test, $debugOutput); + $this->output->outputStep($test, false === $debugOutput ? '' : $debugOutput); } public function markTestAsSuccess(Test $test): void @@ -34,7 +37,7 @@ public function markTestAsSuccess(Test $test): void $debugOutput = ob_get_contents(); ob_clean(); - $this->output->outputSuccess($test, $debugOutput); + $this->output->outputSuccess($test, false === $debugOutput ? '' : $debugOutput); } public function markTestAsFailed(Test $test, \Throwable $error): void @@ -71,6 +74,6 @@ public function markTestAsSkipped(Test $test): void $debugOutput = ob_get_contents(); ob_clean(); - $this->output->outputSkipped($test, $debugOutput); + $this->output->outputSkipped($test, false === $debugOutput ? '' : $debugOutput); } }