Skip to content
4 changes: 2 additions & 2 deletions src/Application.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ private function __construct(

public static function create(
ApplicationConfig $config,
) {
): self {
$container = Bootstrap::init()
->withConfig($config->services)
->finish();
Expand All @@ -40,7 +40,7 @@ public function run($filter = new Filter()): RunResult

# Iterate Test Suites
foreach ($suiteProvider->withFilter($filter)->getSuites() as $suite) {
$suiteResults[] = $suiteRunner->run($suite, $filter);
$suiteResults[] = $suiteRunner->runSuite($suite, $filter);
}

# Run suites
Expand Down
4 changes: 2 additions & 2 deletions src/Assert/Interceptor/AssertCollectorInterceptor.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

use Testo\Assert\StaticState;
use Testo\Assert\TestState;
use Testo\Interceptor\TestCallInterceptor;
use Testo\Interceptor\TestRunInterceptor;
use Testo\Test\Dto\TestInfo;
use Testo\Test\Dto\TestResult;

Expand All @@ -18,7 +18,7 @@
*
* Supports both synchronous and asynchronous (Fiber-based) environments.
*/
final class AssertCollectorInterceptor implements TestCallInterceptor
final class AssertCollectorInterceptor implements TestRunInterceptor
{
#[\Override]
public function runTest(TestInfo $info, callable $next): TestResult
Expand Down
4 changes: 2 additions & 2 deletions src/Assert/Interceptor/ExpectExceptionConfigurator.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,11 @@
use Testo\Assert\State\ExpectedException;
use Testo\Assert\StaticState;
use Testo\Attribute\ExpectException;
use Testo\Interceptor\TestCallInterceptor;
use Testo\Interceptor\TestRunInterceptor;
use Testo\Test\Dto\TestInfo;
use Testo\Test\Dto\TestResult;

final class ExpectExceptionConfigurator implements TestCallInterceptor
final class ExpectExceptionConfigurator implements TestRunInterceptor
{
public function __construct(
private readonly ExpectException $options,
Expand Down
4 changes: 2 additions & 2 deletions src/Assert/Interceptor/ExpectExceptionInterceptor.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
use Testo\Assert\State\Record;
use Testo\Assert\State\Success;
use Testo\Assert\StaticState;
use Testo\Interceptor\TestCallInterceptor;
use Testo\Interceptor\TestRunInterceptor;
use Testo\Test\Dto\Status;
use Testo\Test\Dto\TestInfo;
use Testo\Test\Dto\TestResult;
Expand All @@ -19,7 +19,7 @@
*
* @note Must be placed right before the test execution.
*/
final class ExpectExceptionInterceptor implements TestCallInterceptor
final class ExpectExceptionInterceptor implements TestRunInterceptor
{
/**
* @throws AssertException When the expected exception is not thrown.
Expand Down
6 changes: 3 additions & 3 deletions src/Attribute/RetryPolicy.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,14 @@

namespace Testo\Attribute;

use Testo\Interceptor\TestCallInterceptor\RetryPolicyCallInterceptor;
use Testo\Interceptor\TestCallInterceptor\RetryPolicyRunInterceptor;
use Testo\Module\Interceptor\FallbackInterceptor;

/**
* Retry test on failure.
*/
#[\Attribute(\Attribute::TARGET_METHOD | \Attribute::TARGET_FUNCTION)]
#[FallbackInterceptor(RetryPolicyCallInterceptor::class)]
#[\Attribute(\Attribute::TARGET_METHOD | \Attribute::TARGET_FUNCTION | \Attribute::TARGET_CLASS )]
#[FallbackInterceptor(RetryPolicyRunInterceptor::class)]
final class RetryPolicy implements Interceptable
{
public function __construct(
Expand Down
5 changes: 3 additions & 2 deletions src/Common/Command/Run.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Testo\Render\StdoutRenderer;
use Testo\Render\Teamcity\TeamcityInterceptor;
use Testo\Render\TeamcityInterceptor;

#[AsCommand(
name: 'run',
Expand All @@ -23,7 +23,8 @@ public function __invoke(
$this->container->bind(StdoutRenderer::class, TeamcityInterceptor::class);

$result = $this->application->run();
tr($result);
// tr($result);

return Command::SUCCESS;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
use Testo\Attribute\Test;
use Testo\Interceptor\CaseLocatorInterceptor;
use Testo\Interceptor\FileLocatorInterceptor;
use Testo\Module\Tokenizer\Reflection;
use Testo\Interceptor\Reflection\Reflection;
use Testo\Module\Tokenizer\Reflection\FileDefinitions;
use Testo\Module\Tokenizer\Reflection\TokenizedFile;
use Testo\Test\Definition\CaseDefinitions;
Expand Down
89 changes: 89 additions & 0 deletions src/Interceptor/Reflection/AttributesInterceptor.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
<?php

declare(strict_types=1);

namespace Testo\Interceptor\Reflection;

use Testo\Attribute\Interceptable;
use Testo\Interceptor\TestCaseRunInterceptor;
use Testo\Interceptor\TestRunInterceptor;
use Testo\Module\Interceptor\InterceptorProvider;
use Testo\Module\Interceptor\Internal\Pipeline;
use Testo\Test\Dto\CaseInfo;
use Testo\Test\Dto\CaseResult;
use Testo\Test\Dto\TestInfo;
use Testo\Test\Dto\TestResult;

/**
* Reads {@see Interceptable} attributes and integrates them into the pipeline.
*/
final class AttributesInterceptor implements TestRunInterceptor, TestCaseRunInterceptor
{
public function __construct(
private readonly InterceptorProvider $interceptorProvider,
) {}

#[\Override]
public function runTest(TestInfo $info, callable $next): TestResult
{
$classAttributes = $info->caseInfo->definition->reflection === null
? []
: Reflection::fetchClassAttributes(
class: $info->caseInfo->definition->reflection,
attributeClass: Interceptable::class,
flags: \ReflectionAttribute::IS_INSTANCEOF,
);

$methodAttributes = Reflection::fetchFunctionAttributes(
function: $info->testDefinition->reflection,
attributeClass: Interceptable::class,
flags: \ReflectionAttribute::IS_INSTANCEOF,
);

$attrs = \array_merge($classAttributes, $methodAttributes);
if ($attrs === []) {
# No attributes, continue to next interceptor
return $next($info);
}

# Merge and instantiate attributes
$interceptors = $this->interceptorProvider->fromAttributes(TestRunInterceptor::class, ...\array_map(
static fn(\ReflectionAttribute $a): Interceptable => $a->newInstance(),
$attrs,
));

return Pipeline::prepare(...$interceptors)->with(
$next,
/** @see TestRunInterceptor::runTest() */
'runTest',
)($info);
}

public function runTestCase(CaseInfo $info, callable $next): CaseResult
{
$attrs = $info->definition->reflection === null
? []
: Reflection::fetchClassAttributes(
class: $info->definition->reflection,
attributeClass: Interceptable::class,
flags: \ReflectionAttribute::IS_INSTANCEOF,
);

if ($attrs === []) {
# No attributes, continue to next interceptor
return $next($info);
}

# Merge and instantiate attributes
$interceptors = $this->interceptorProvider->fromAttributes(TestCaseRunInterceptor::class, ...\array_map(
static fn(\ReflectionAttribute $a): Interceptable => $a->newInstance(),
$attrs,
));

return Pipeline::prepare(...$interceptors)->with(
$next,
/** @see TestCaseRunInterceptor::runTestCase() */
'runTestCase',
)($info);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

declare(strict_types=1);

namespace Testo\Module\Tokenizer;
namespace Testo\Interceptor\Reflection;

/**
* Reflection utilities.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
namespace Testo\Interceptor\TestCallInterceptor;

use Testo\Attribute\RetryPolicy;
use Testo\Interceptor\TestCallInterceptor;
use Testo\Interceptor\TestRunInterceptor;
use Testo\Test\Dto\Status;
use Testo\Test\Dto\TestInfo;
use Testo\Test\Dto\TestResult;
Expand All @@ -15,7 +15,7 @@
*
* @see RetryPolicy
*/
final class RetryPolicyCallInterceptor implements TestCallInterceptor
final class RetryPolicyRunInterceptor implements TestRunInterceptor
{
public function __construct(
private readonly RetryPolicy $options,
Expand All @@ -32,13 +32,17 @@ public function runTest(TestInfo $info, callable $next): TestResult
/** @var TestResult $result */
$result = $next($info);

if ($result->status->isFailure() && $attempts > 0) {
if ($result->status->isFailure()) {
# Test failed, check if we can retry
$isFlaky = true;
goto run;
if ($attempts > 0) {
$isFlaky = true;
goto run;
}

return $result;
}

return $isFlaky && $this->options->markFlaky
return $isFlaky && $this->options->markFlaky && $result->status->isSuccessful()
? $result->with(status: Status::Flaky)
: $result;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,14 @@
namespace Testo\Interceptor\TestCaseCallInterceptor;

use Testo\Interceptor\Exception\TestCaseInstantiationException;
use Testo\Interceptor\TestCaseCallInterceptor;
use Testo\Interceptor\TestCaseRunInterceptor;
use Testo\Test\Dto\CaseInfo;
use Testo\Test\Dto\CaseResult;

/**
* Instantiate the test case class if not already instantiated.
*/
final class InstantiateTestCase implements TestCaseCallInterceptor
final class InstantiateTestCase implements TestCaseRunInterceptor
{
public function runTestCase(CaseInfo $info, callable $next): CaseResult
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
*
* @extends InterceptorMarker<CaseInfo, CaseResult>
*/
interface TestCaseCallInterceptor extends InterceptorMarker
interface TestCaseRunInterceptor extends InterceptorMarker
{
/**
* @param CaseInfo $info Test case to run.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
*
* @extends InterceptorMarker<TestInfo, TestResult>
*/
interface TestCallInterceptor extends InterceptorMarker
interface TestRunInterceptor extends InterceptorMarker
{
/**
* @param TestInfo $info Information about the test to be run.
Expand Down
25 changes: 25 additions & 0 deletions src/Interceptor/TestSuiteRunInterceptor.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
<?php

declare(strict_types=1);

namespace Testo\Interceptor;

use Testo\Module\Interceptor\Internal\InterceptorMarker;
use Testo\Test\Dto\CaseInfo;
use Testo\Test\Dto\CaseResult;
use Testo\Test\Dto\SuiteInfo;
use Testo\Test\Dto\SuiteResult;

/**
* Intercept running a test suite.
*
* @extends InterceptorMarker<CaseInfo, CaseResult>
*/
interface TestSuiteRunInterceptor extends InterceptorMarker
{
/**
* @param SuiteInfo $info Test suite to run.
* @param callable(SuiteInfo): SuiteResult $next Next interceptor or core logic to run the test suite.
*/
public function runTestSuite(SuiteInfo $info, callable $next): SuiteResult;
}
27 changes: 26 additions & 1 deletion src/Module/Interceptor/InterceptorProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,15 @@

namespace Testo\Module\Interceptor;

use Testo\Assert\Interceptor\AssertCollectorInterceptor;
use Testo\Assert\Interceptor\ExpectExceptionInterceptor;
use Testo\Attribute\Interceptable;
use Testo\Common\Container;
use Testo\Interceptor\Reflection\AttributesInterceptor;
use Testo\Interceptor\Reflection\Reflection;
use Testo\Interceptor\TestCaseCallInterceptor\InstantiateTestCase;
use Testo\Module\Interceptor\Internal\InterceptorMarker;
use Testo\Module\Tokenizer\Reflection;
use Testo\Render\StdoutRenderer;
use Yiisoft\Injector\Injector;

final class InterceptorProvider
Expand All @@ -33,6 +38,26 @@ public static function createDefault(Container $container): self
return $self;
}

/**
* Get interceptors for the given configuration filtered by the given class.
*
* @template-covariant T of InterceptorMarker
*
* @param class-string<T> $class The target interceptor class.
*
* @return InterceptorMarker Interceptor instances of the given class.
*/
public function fromConfig(string $class): array
{
return $this->fromClasses($class, ...[
StdoutRenderer::class,
new InstantiateTestCase(),
new AssertCollectorInterceptor(),
AttributesInterceptor::class,
new ExpectExceptionInterceptor(),
]);
}

/**
* Get interceptors for
*
Expand Down
5 changes: 3 additions & 2 deletions src/Module/Interceptor/Internal/Pipeline.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,8 @@ final class Pipeline
/** @var non-empty-string */
private string $method;

private \Closure $last;
/** @var callable(TInput): TOutput */
private mixed $last;

/** @var TInterceptor */
private array $interceptors = [];
Expand Down Expand Up @@ -59,7 +60,7 @@ public static function prepare(TInterceptor ...$interceptors): self
*
* @return callable(object): TOutput
*/
public function with(\Closure $last, string $method): callable
public function with(callable $last, string $method): callable
{
$new = clone $this;

Expand Down
Loading