Skip to content

Commit 1038bc8

Browse files
committed
feat(inline): Implement inline test functionality with TestInline attribute
1 parent 381bff9 commit 1038bc8

File tree

10 files changed

+280
-3
lines changed

10 files changed

+280
-3
lines changed

src/Module/Interceptor/InterceptorProvider.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
use Testo\Interceptor\Reflection\Reflection;
1616
use Testo\Interceptor\TestCaseCallInterceptor\InstantiateTestCase;
1717
use Testo\Module\Interceptor\Internal\InterceptorMarker;
18+
use Testo\Sample\Internal\TestInlineFinder;
1819
use Yiisoft\Injector\Injector;
1920

2021
final class InterceptorProvider
@@ -57,6 +58,7 @@ public function fromConfig(string $class): array
5758
new TestoAttributesLocatorInterceptor(),
5859
new InstantiateTestCase(),
5960
new AssertCollectorInterceptor(),
61+
TestInlineFinder::class,
6062
AttributesInterceptor::class,
6163
new ExpectationsInterceptor(),
6264
]);

src/Sample/Internal/DataProviderInterceptor.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717
use Testo\Test\Event\Test\TestDataSetStarting;
1818

1919
/**
20-
* Interceptor that retries a test execution based on the provided retry policy.
20+
* Interceptor that handles data providers for tests.
2121
*/
2222
final class DataProviderInterceptor implements TestRunInterceptor
2323
{
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Testo\Sample\Internal;
6+
7+
use Testo\Assert;use Testo\Sample\TestInline;use Testo\Test\Dto\TestInfo;class InlineTestInvoker
8+
{
9+
public function __construct() {}
10+
11+
public function __invoke(TestInfo $info): mixed
12+
{
13+
$attr = $info->getAttribute(TestInline::class);
14+
\assert($attr instanceof TestInline);
15+
16+
# Execute the method
17+
$result = $info->caseInfo->instance === null
18+
? $info->testDefinition->reflection->invoke(...$info->arguments)
19+
: $info->testDefinition->reflection->invoke($info->caseInfo->instance, ...$info->arguments);
20+
21+
# Verify the expected result
22+
if ($attr->result instanceof \Closure) {
23+
($attr->result)($result);
24+
return $result;
25+
}
26+
27+
Assert::same($attr->result, $result);
28+
return $result;
29+
}
30+
}
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Testo\Sample\Internal;
6+
7+
use Testo\Attribute\Test;
8+
use Testo\Interceptor\CaseLocatorInterceptor;
9+
use Testo\Interceptor\FileLocatorInterceptor;
10+
use Testo\Interceptor\Reflection\Reflection;
11+
use Testo\Module\Tokenizer\Reflection\FileDefinitions;
12+
use Testo\Module\Tokenizer\Reflection\TokenizedFile;
13+
use Testo\Sample\TestInline;
14+
use Testo\Test\Dto\CaseDefinitions;
15+
use Testo\Test\Dto\TestInfo;
16+
17+
/**
18+
* Finds inline tests defined with the {@see TestInline} attribute.
19+
*/
20+
final class TestInlineFinder implements FileLocatorInterceptor, CaseLocatorInterceptor
21+
{
22+
/** @var callable(TestInfo): mixed Invoker for the test method. */
23+
private readonly \CLosure $invoker;
24+
25+
public function __construct()
26+
{
27+
$this->invoker = (new InlineTestInvoker())(...);
28+
}
29+
30+
#[\Override]
31+
public function locateFile(TokenizedFile $file, callable $next): ?bool
32+
{
33+
return $file->getClasses() !== [] || $file->getFunctions() !== [] ? true : $next($file);
34+
}
35+
36+
#[\Override]
37+
public function locateTestCases(FileDefinitions $file, callable $next): CaseDefinitions
38+
{
39+
// Define cases for classes
40+
foreach ($file->classes as $class) {
41+
if ($class->isAbstract()) {
42+
continue;
43+
}
44+
45+
$case = null;
46+
foreach ($class->getMethods() as $method) {
47+
if (Reflection::fetchFunctionAttributes($method, attributeClass: TestInline::class)) {
48+
if ($case === null) {
49+
$case = $file->cases->define($class, $file);
50+
$case->invoker = $this->invoker;
51+
}
52+
53+
$case->tests->define($method);
54+
}
55+
}
56+
}
57+
58+
if ($file->functions === []) {
59+
return $next($file);
60+
}
61+
62+
// Define a case for functions
63+
// Implement a lazy case definition
64+
$case = null;
65+
foreach ($file->functions as $function) {
66+
if (Reflection::fetchFunctionAttributes($function, attributeClass: Test::class)) {
67+
if ($case === null) {
68+
$case = $file->cases->define(null, $file);
69+
$case->invoker = $this->invoker;
70+
}
71+
72+
$case->tests->define($function);
73+
}
74+
}
75+
76+
return $next($file);
77+
}
78+
}
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Testo\Sample\Internal;
6+
7+
use Psr\EventDispatcher\EventDispatcherInterface;
8+
use Testo\Attribute\Test;
9+
use Testo\Interceptor\CaseLocatorInterceptor;
10+
use Testo\Interceptor\FileLocatorInterceptor;
11+
use Testo\Interceptor\Reflection\Reflection;
12+
use Testo\Interceptor\TestRunInterceptor;
13+
use Testo\Module\Tokenizer\Reflection\FileDefinitions;
14+
use Testo\Module\Tokenizer\Reflection\TokenizedFile;
15+
use Testo\Sample\MultipleResult;
16+
use Testo\Sample\TestInline;
17+
use Testo\Test\Dto\CaseDefinitions;
18+
use Testo\Test\Dto\Status;
19+
use Testo\Test\Dto\TestInfo;
20+
use Testo\Test\Dto\TestResult;
21+
use Testo\Test\Event\Test\TestBatchFinished;
22+
use Testo\Test\Event\Test\TestBatchStarting;
23+
use Testo\Test\Event\Test\TestDataSetFinished;
24+
use Testo\Test\Event\Test\TestDataSetStarting;
25+
26+
/**
27+
* Interceptor that runs the target method as a pure function with provided arguments and expected result.
28+
*/
29+
final class TestInlineInterceptor implements TestRunInterceptor
30+
{
31+
/** @var callable(TestInfo): mixed Invoker for the test method. */
32+
private readonly \CLosure $invoker;
33+
34+
public function __construct(
35+
private readonly EventDispatcherInterface $eventDispatcher,
36+
) {
37+
$this->invoker = (new InlineTestInvoker())(...);
38+
}
39+
40+
#[\Override]
41+
public function runTest(TestInfo $info, callable $next): TestResult
42+
{
43+
/** @var TestInline[] $attributes */
44+
$attributes = $info->getAttribute(TestInline::class);
45+
if ($attributes === []) {
46+
return $next($info);
47+
}
48+
49+
if (\count($attributes) === 1) {
50+
$inline = \reset($attributes);
51+
return $next($info->with(arguments: $inline->arguments)->withAttribute(TestInline::class, $inline));
52+
}
53+
54+
# Dispatch batch starting event
55+
$this->eventDispatcher->dispatch(new TestBatchStarting($info));
56+
57+
// Run the test for each data set
58+
$results = [];
59+
$status = Status::Passed;
60+
foreach ($attributes as $index => $inline) {
61+
$newInfo = $info->with(arguments: $inline->arguments);
62+
$label = "$index";
63+
64+
# Dispatch dataset starting event
65+
$this->eventDispatcher->dispatch(
66+
new TestDataSetStarting($newInfo, $label, $index)
67+
);
68+
69+
try {
70+
$result = $next($newInfo);
71+
} catch (\Throwable $throwable) {
72+
$result = new TestResult(info: $newInfo, status: Status::Error, failure: $throwable);
73+
}
74+
75+
# Dispatch dataset finished event
76+
$this->eventDispatcher->dispatch(
77+
new TestDataSetFinished($newInfo, $result, $label, $index)
78+
);
79+
80+
unset($inline, $newInfo);
81+
$result->status->isFailure() and ($status = Status::Failed);
82+
$results[] = $result;
83+
}
84+
85+
$results = new MultipleResult($results);
86+
87+
$finalResult = new TestResult(info: $info, status: $status, result: $results, attributes: [
88+
MultipleResult::class => $results,
89+
]);
90+
91+
# Dispatch batch finished event
92+
$this->eventDispatcher->dispatch(new TestBatchFinished($info, $finalResult));
93+
94+
return $finalResult;
95+
}
96+
}

src/Sample/TestInline.php

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Testo\Sample;
6+
7+
use Testo\Attribute\Interceptable;
8+
use Testo\Module\Interceptor\FallbackInterceptor;
9+
use Testo\Sample\Internal\TestInlineInterceptor;
10+
11+
/**
12+
* Test that a method or function returns a specified result when called with given arguments.
13+
*
14+
* @api
15+
*/
16+
#[\Attribute(\Attribute::TARGET_METHOD | \Attribute::TARGET_FUNCTION | \Attribute::IS_REPEATABLE)]
17+
#[FallbackInterceptor(TestInlineInterceptor::class)]
18+
final class TestInline implements Interceptable
19+
{
20+
public function __construct(
21+
public readonly array $arguments,
22+
/**
23+
* @var mixed|\Closure(mixed): mixed $result The expected result of the test method or function.
24+
*/
25+
public readonly mixed $result = null,
26+
) {}
27+
}

src/Test/Definition/CaseDefinition.php

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,18 @@
55
namespace Testo\Test\Definition;
66

77
use Testo\Test\Dto\TestDefinitions;
8+
use Testo\Test\Dto\TestInfo;
89

910
final class CaseDefinition
1011
{
12+
/**
13+
* @param null|\Closure(TestInfo): mixed $invoker Invoker for the test method.
14+
*/
1115
public function __construct(
1216
public readonly ?string $name,
1317
public readonly ?\ReflectionClass $reflection = null,
1418
public readonly TestDefinitions $tests = new TestDefinitions(),
15-
// public ?string $runner = null,
19+
public ?\Closure $invoker = null,
1620
) {}
1721

1822
public function with(

src/Test/Runner/SuiteRunner.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ public function run(SuiteInfo $suite, Filter $filter): SuiteResult
7373
try {
7474
$caseInfo = new CaseInfo(
7575
definition: $caseDefinition,
76-
invoker: $invoker,
76+
invoker: $caseDefinition->invoker ?? $invoker,
7777
);
7878
$result = $runner->runCase($caseInfo, $filter);
7979
$result->status->isFailure() and $status = Status::Failed;

tests/Sample/Self/TestInline.php

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Tests\Sample\Self;
6+
7+
use Testo\Assert;
8+
use Testo\Attribute\ExpectException;
9+
use Testo\Attribute\Test;
10+
use Testo\Expect;
11+
12+
/**
13+
* @see Expect::leaks()
14+
*/
15+
final class TestInline
16+
{
17+
#[\Testo\Sample\TestInline(arguments: [40, 2], result: 42)]
18+
public function sum(int $a, int $b): int
19+
{
20+
return $a + $b;
21+
}
22+
}

tests/Sample/suites.php

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
use Testo\Config\FinderConfig;
6+
use Testo\Config\SuiteConfig;
7+
8+
/**
9+
* Test suites for Sample component.
10+
*/
11+
return [
12+
new SuiteConfig(
13+
name: 'Sample: Self Testing',
14+
location: new FinderConfig(
15+
include: [__DIR__ . '/Self'],
16+
),
17+
),
18+
];

0 commit comments

Comments
 (0)