Skip to content

Commit a663b71

Browse files
committed
feat(Framework): Add Session and Worker events
refactor(Teamcity): Render environment info refactor(CLI): Print environment info
1 parent 7cbf6d3 commit a663b71

File tree

15 files changed

+360
-39
lines changed

15 files changed

+360
-39
lines changed

docs/spec/events-naming.md

Lines changed: 58 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -42,23 +42,35 @@ TestRetrying // Event fired when a test is about to be retried
4242
When events form a hierarchy, the entity name should reflect the nesting level:
4343

4444
```
45-
TestPipeline → Top level (run interceptors pipeline)
46-
TestBatch → Mid level (multiple runs of same test)
47-
Test → Individual test execution
45+
Session → Outermost level (entire test run lifecycle)
46+
Worker → Subprocess level (parallel/isolated execution)
47+
TestSuite → Suite level (collection of test cases)
48+
TestCase → Class level (single test class)
49+
TestPipeline → Pipeline level (run interceptors)
50+
TestBatch → Batch level (DataProvider/Retry)
51+
Test → Individual test execution
4852
```
4953

5054
### Complete Event Hierarchy Example
5155

5256
```php
53-
TestPipelineStarting // Before run interceptors start
54-
TestBatchStarting // Before batch of test runs (DataProvider/Retry)
55-
TestStarting // Before individual test execution
56-
TestFinished // After individual test execution
57-
TestRetrying // Before retry attempt (if applicable)
58-
TestStarting // Next attempt
59-
TestFinished
60-
TestBatchFinished // After all test runs in batch
61-
TestPipelineFinished // After run interceptors finish
57+
SessionStarting // Before any tests are discovered or executed
58+
WorkerStarting // Before a subprocess starts (parallel/isolated mode)
59+
TestSuiteStarting // Before a test suite (collection of cases) starts
60+
TestCaseStarting // Before a test class starts
61+
TestPipelineStarting // Before run interceptors start
62+
TestBatchStarting // Before batch of test runs (DataProvider/Retry)
63+
TestStarting // Before individual test execution
64+
TestFinished // After individual test execution
65+
TestRetrying // Before retry attempt (if applicable)
66+
TestStarting // Next attempt
67+
TestFinished
68+
TestBatchFinished // After all test runs in batch
69+
TestPipelineFinished // After run interceptors finish
70+
TestCaseFinished // After a test class finishes
71+
TestSuiteFinished // After a test suite finishes
72+
WorkerFinished // After a subprocess finishes
73+
SessionFinished // After all tests complete, carries RunResult
6274
```
6375

6476
## Event Class Structure
@@ -159,8 +171,42 @@ Each event class must have a PHPDoc comment that includes:
159171
4. **Hierarchy clarity**: Entity names reflect nesting levels
160172
5. **Ecosystem alignment**: Consistent with Laravel, Symfony event naming patterns
161173

174+
## Namespace Organization
175+
176+
Events are grouped into namespaces by entity layer. The namespace name matches the entity
177+
prefix for test-domain events. Framework-level events are the exception — `Session` and
178+
`Worker` share a single `Framework` namespace since they belong to the same infrastructure
179+
layer and are not test-domain entities.
180+
181+
```
182+
Event\Framework\ → Session*, Worker* (infrastructure lifecycle)
183+
Event\TestSuite\ → TestSuite*
184+
Event\TestCase\ → TestCase*
185+
Event\Test\ → TestPipeline*, TestBatch*, TestDataSet*, Test*
186+
```
187+
162188
## Examples from Testo
163189

190+
### Framework Events (`Event\Framework`)
191+
192+
Top-level lifecycle events not tied to any specific test entity. Grouped in their own
193+
namespace since `Session` and `Worker` belong to the same infrastructure layer.
194+
195+
```php
196+
SessionStarting // Before the entire test run begins (fired once)
197+
SessionFinished // After all tests complete, carries RunResult with duration
198+
WorkerStarting // Before a subprocess starts (parallel/isolated mode)
199+
WorkerFinished // After a subprocess finishes
200+
```
201+
202+
### Suite and Case Events
203+
```php
204+
TestSuiteStarting // Before a suite (collection of test cases) starts
205+
TestSuiteFinished // After a suite finishes
206+
TestCaseStarting // Before a test class starts
207+
TestCaseFinished // After a test class finishes
208+
```
209+
164210
### Pipeline Events
165211
```php
166212
TestPipelineStarting // Before run interceptors

src/Application/Application.php

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
namespace Testo\Application;
66

77
use Internal\Path;
8+
use Psr\EventDispatcher\EventDispatcherInterface;
89
use Testo\Application\Config\ApplicationConfig;
910
use Testo\Application\Config\Internal\ConfigInflector;
1011
use Testo\Application\Config\PluginConfigurator;
@@ -15,6 +16,11 @@
1516
use Testo\Application\Value\RunResult;
1617
use Testo\Common\Container;
1718
use Testo\Core\Value\Status;
19+
use Testo\Event\Framework\FrameworkStarting;
20+
use Testo\Event\Framework\SessionFinished;
21+
use Testo\Event\Framework\SessionStarting;
22+
use Testo\Event\Framework\WorkerFinished;
23+
use Testo\Event\Framework\WorkerStarting;
1824

1925
final class Application
2026
{
@@ -85,19 +91,29 @@ public function run(?Filter $filter = null): RunResult
8591
$filter === null or $this->container->set($filter);
8692
$filter ??= $this->container->get(Filter::class);
8793

94+
$dispatcher = $this->container->get(EventDispatcherInterface::class);
95+
$dispatcher->dispatch(new SessionStarting());
96+
$dispatcher->dispatch(new WorkerStarting());
97+
98+
8899
$suiteProvider = $this->container->get(SuiteProvider::class);
89100
$suiteRunner = $this->container->get(SuiteRunner::class);
90101
$status = Status::Passed;
102+
$duration = microtime(true);
91103

92-
# Iterate Test Suites
104+
# Iterate and run Test Suites
93105
$suiteResults = [];
94106
foreach ($suiteProvider->withFilter($filter)->getSuites() as $suite) {
95107
$suiteResults[] = $suiteResult = $suiteRunner->runSuite($suite, $filter);
96108
$suiteResult->status->isFailure() and $status = Status::Failed;
97109
}
98110

99-
# Run suites
100-
return new RunResult($suiteResults, status: $status);
111+
$duration = microtime(true) - $duration;
112+
$result = new RunResult($suiteResults, status: $status, duration: $duration);
113+
114+
$dispatcher->dispatch(new WorkerFinished());
115+
$dispatcher->dispatch(new SessionFinished($result));
116+
return $result;
101117
}
102118

103119
public function getContainer(): Container

src/Application/Value/RunResult.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ public function __construct(
2222
*/
2323
public readonly iterable $results,
2424
public readonly Status $status,
25+
/** Duration of the session in seconds. */
26+
public readonly float $duration,
2527
) {}
2628

2729
/**

src/Bridge/Symfony/Command/Base.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,4 +110,5 @@ protected function getConfigFile(InputInterface $input): ?Path
110110

111111
return null;
112112
}
113+
113114
}

src/Bridge/Symfony/Renderer/Formatter.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ public static function runHeader(): string
5757
{
5858
$name = Info::NAME;
5959
$version = Info::version();
60-
return Style::bold('# Running Tests') . Style::dim(" ({$name} v{$version})") . "\n\n";
60+
return Style::bold("{$name}") . Style::dim(" v{$version}") . "\n\n";
6161
}
6262

6363
/**

src/Bridge/Symfony/Renderer/TerminalLogger.php

Lines changed: 27 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
namespace Testo\Bridge\Symfony\Renderer;
66

77
use Testo\Assert\TestState;
8+
use Testo\Common\Environment;
89
use Testo\Core\Context\CaseInfo;
910
use Testo\Core\Context\CaseResult;
1011
use Testo\Core\Context\SuiteInfo;
@@ -39,9 +40,6 @@ final class TerminalLogger
3940
/** @var list<array{result: TestResult, duration: int<0, max>|null}> */
4041
private array $failures = [];
4142

42-
private float $startTime;
43-
private bool $headerPrinted = false;
44-
4543
/**
4644
* Current indentation level for nested tests (e.g., DataProvider datasets).
4745
*
@@ -56,16 +54,13 @@ final class TerminalLogger
5654

5755
public function __construct(
5856
private readonly OutputFormat $format = OutputFormat::Compact,
59-
) {
60-
$this->startTime = \microtime(true);
61-
}
57+
) {}
6258

6359
/**
6460
* Publishes test suite started message.
6561
*/
6662
public function suiteStartedFromInfo(SuiteInfo $info): void
6763
{
68-
$this->ensureHeader();
6964
echo Formatter::suiteHeader($info->name, $this->format);
7065
}
7166

@@ -82,7 +77,6 @@ public function handleSuiteResult(SuiteInfo $info, SuiteResult $result): void
8277
*/
8378
public function caseStartedFromInfo(CaseInfo $info): void
8479
{
85-
$this->ensureHeader();
8680
echo Formatter::caseHeader($info->name, $this->format);
8781
}
8882

@@ -100,7 +94,6 @@ public function handleCaseResult(CaseInfo $info, CaseResult $result): void
10094
*/
10195
public function batchStartedFromInfo(TestInfo $info): void
10296
{
103-
$this->ensureHeader();
10497
$this->currentIndentLevel = 1;
10598

10699
if ($this->format === OutputFormat::Dots) {
@@ -129,7 +122,6 @@ public function batchFinishedFromInfo(TestInfo $info): void
129122
*/
130123
public function testStartedFromInfo(TestInfo $info, ?string $overrideName = null): void
131124
{
132-
$this->ensureHeader();
133125
$this->currentTestName = $overrideName;
134126
// No output on test start for compact/dots mode
135127
}
@@ -154,23 +146,39 @@ public function handleTestResult(TestResult $result, ?int $duration): void
154146
/**
155147
* Prints final summary with all failures and statistics.
156148
*/
157-
public function printSummary(): void
149+
public function printSummary(float $duration): void
158150
{
159151
$this->printFailures();
160-
$this->printStatistics();
152+
$this->printStatistics($duration);
161153
}
162154

163155
/**
164156
* Ensures run header is printed once.
165157
*/
166-
private function ensureHeader(): void
158+
public function ensureHeader(): void
167159
{
168-
if ($this->headerPrinted) {
169-
return;
170-
}
171-
172160
echo Formatter::runHeader();
173-
$this->headerPrinted = true;
161+
}
162+
163+
public function printEnvironment(): void
164+
{
165+
echo \sprintf(' %s %s (%s)', Style::info('OS:'), Environment::getOs(), Environment::getCpu()) . "\n";
166+
echo \sprintf(' %s %s (%s, memory: %s)', Style::info('PHP:'), Environment::getPhpVersion(), \PHP_SAPI, \ini_get('memory_limit') ?: 'unlimited') . "\n";
167+
168+
$modes = Environment::getXDebugMode();
169+
$xdebug = match (true) {
170+
!Environment::hasXDebug() => 'off',
171+
$modes !== [] => Environment::getXDebugVersion() . Style::dim(' (' . \implode(', ', $modes) . ')'),
172+
default => Environment::getXDebugVersion() . Style::dim(' (off)'),
173+
};
174+
echo \sprintf(' %s %s', Style::info('XDebug:'), $xdebug) . "\n";
175+
176+
$opcache = match (true) {
177+
!Environment::isOpCacheEnabled() => 'off',
178+
Environment::isJitEnabled() => 'enabled with JIT',
179+
default => 'enabled',
180+
};
181+
echo \sprintf(' %s %s', Style::info('OPcache:'), $opcache) . "\n\n";
174182
}
175183

176184
/**
@@ -348,9 +356,8 @@ function: $result->info->testDefinition->reflection,
348356
/**
349357
* Prints final statistics.
350358
*/
351-
private function printStatistics(): void
359+
private function printStatistics(float $duration): void
352360
{
353-
$duration = \microtime(true) - $this->startTime;
354361
$success = $this->failedTests === 0;
355362

356363
echo Formatter::summary(

src/Bridge/Symfony/TerminalRenderer.php

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@
1111
use Testo\Bridge\Symfony\Renderer\TerminalLogger;
1212
use Testo\Common\Container;
1313
use Testo\Core\Context\TestInfo;
14+
use Testo\Event\Framework\SessionFinished;
15+
use Testo\Event\Framework\SessionStarting;
1416
use Testo\Event\Test\TestBatchFinished;
1517
use Testo\Event\Test\TestBatchStarting;
1618
use Testo\Event\Test\TestDataSetFinished;
@@ -48,6 +50,10 @@ public function configure(Container $container): void
4850
{
4951
$listeners = $container->get(EventListenerCollector::class);
5052

53+
// Framework events
54+
$listeners->addListener(SessionStarting::class, $this->onSessionStarting(...));
55+
$listeners->addListener(SessionFinished::class, $this->onSessionFinished(...));
56+
5157
// Test Pipeline events (lifecycle of entire test through all interceptors)
5258
$listeners->addListener(TestPipelineFinished::class, $this->onTestPipelineFinished(...));
5359

@@ -68,6 +74,17 @@ public function configure(Container $container): void
6874
$listeners->addListener(TestSuiteFinished::class, $this->onTestSuiteFinished(...));
6975
}
7076

77+
private function onSessionStarting(SessionStarting $event): void
78+
{
79+
$this->logger->ensureHeader();
80+
$this->logger->printEnvironment();
81+
}
82+
83+
private function onSessionFinished(SessionFinished $event): void
84+
{
85+
$this->logger->printSummary($event->result->duration);
86+
}
87+
7188
private static function getId(TestInfo $testInfo): string
7289
{
7390
return \spl_object_hash($testInfo->testDefinition);
@@ -138,8 +155,5 @@ private function onTestSuiteStarting(TestSuiteStarting $event): void
138155
private function onTestSuiteFinished(TestSuiteFinished $event): void
139156
{
140157
$this->logger->handleSuiteResult($event->suiteInfo, $event->suiteResult);
141-
142-
// Print final summary after all tests in suite complete
143-
$this->logger->printSummary();
144158
}
145159
}

0 commit comments

Comments
 (0)