Plugins
Plugins implement one or more capability interfaces. Pass each plugin to
GreenlightConfig::plugins() in greenlight.php. Greenlight identifies the
plugin capabilities from the interfaces. One plugin can implement more than one
interface.
Worker-side plugins get the live test instance, its metadata, and access to harness services.
return GreenlightConfig::create()
->paths(['tests'])
->plugins(new FlakyQuarantine(), new SlackNotifier());
How plugins reach workers
Tests run in worker processes. A process cannot send live PHP objects across a
process boundary. Each worker loads greenlight.php and creates its own plugin
instances.
Plugin constructors run once per worker. Plugins cannot share in-memory state between workers. If a plugin needs shared state, keep it outside the process. For example, use a file, socket, or external service.
Capability interfaces
TestLifecycleSubscriber
Worker-side.
use Greenlight\Plugin\TestContext;
use Greenlight\Plugin\TestLifecycleSubscriber;
final class FlakyQuarantine implements TestLifecycleSubscriber
{
public function beforeTest(TestContext $context): void {}
public function afterTest(TestContext $context, TestResult $result): TestResult
{
if ($result->outcome->isSuccessful() || !\in_array('quarantined', $context->metadata->groups, true)) {
return $result;
}
return $result->withOutcome(Outcome::Skipped, self::class);
}
}
beforeTest() runs after Greenlight constructs the test instance. It runs once
before all #[Before] hooks.
Call $context->skip('reason') to stop the attempt and report the test as
skipped. The method has the type never, so code after the call does not run.
It throws Greenlight\Core\Test\SkipTest. The interface declares this
exception. A direct throw of this exception has the same effect. A different
throwable causes an error result that names the plugin.
afterTest() receives the finished result and must return a result, either the
same one or a replacement.
Use TestResult::withOutcome() for each outcome change. This method records the
plugin that changed the result. If a plugin changes the outcome without a
transformation-log entry, Greenlight reports an error and names the plugin.
TestContext contains the live test instance, the TestId, the
TestMetadata, and attachments. Its service(SomeType::class) method
resolves services from the active harness scopes.
$context->attachments is the same attempt-owned
Greenlight\Core\Artifact\Attachments object a test can receive through
constructor injection. Plugins can add attachments in either hook. This
includes an attachment after a failure inspection in afterTest(). The usual
retention and size limits apply. See attachments.
The service() method is available during beforeTest() and the test. The
per-test scope closes before afterTest(), so service() throws in that hook.
RetryDecider
Worker-side.
public function shouldRetry(TestMetadata $metadata, TestResult $result, int $attempt, ?\Throwable $cause): bool;
After an unsuccessful attempt, Greenlight asks retry deciders until one returns
true. Greenlight then starts a new attempt with a new test instance and
scope.
The result contains metadata for the attachments from that attempt. A decider can inspect names, kinds, sizes, and media types. It cannot read the attachment content.
The built-in #[Retry] attribute uses this interface.
RunLifecycleSubscriber
Orchestrator-side.
public function onRunEvent(Event $event): void;
Run subscribers receive the event stream in the orchestrator process. The stream contains run, worker, class, and test events.
This side is observation-only. Run subscribers cannot change results across the process boundary. If a run subscriber throws, the run fails.
HarnessProvider
use Greenlight\Harness\Scope;
use Greenlight\Harness\ServiceDefinition;
final class DatabaseProvider implements HarnessProvider
{
public function services(): array
{
return [
new ServiceDefinition(TestDatabase::class, Scope::PerSuite, static fn() => TestDatabase::migrate()),
];
}
}
Harness providers supply services to test constructors.
Choose PerTest, PerClass, PerSuite, or PerRun as the service scope.
PerRun means the worker lifetime. Greenlight constructs a service only when
the test uses it.
If a service implements Greenlight\Harness\Disposable, Greenlight calls its
disposal method when the scope closes. Greenlight uses reverse creation order.
If disposal throws ExpectationFailed, the test fails with diffs. This
mechanism gives services automatic verification. The built-in Greenlight
doubles use this mechanism.
ServiceResolver
In Greenlight\Harness.
public function resolve(string $type, array $attributes): ?object;
A service resolver is a fallback source for a constructor parameter type. Use it when no harness service provides that type.
Registered harness services always take precedence. If no service matches, Greenlight calls resolvers in registration order. Each call receives the declared parameter type and the attribute instances. Greenlight injects the first non-null object.
If the resolver cannot supply the requested type, return null. If it returns
an object of a different type, the test has an error and names the resolver.
The resolver owns each object that it returns. Harness scopes do not track or call disposal methods on these objects.
The Symfony bridge uses this interface to inject container services. You can bridge other dependency containers in the same way. See Symfony applications.
ExpectationExtension
In Greenlight\Expect.
final class UuidMatchers implements ExpectationExtension
{
public function matchers(): array
{
return [
'toBeValidUuid' => static fn(mixed $subject): bool => \is_string($subject)
&& \preg_match('/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/', $subject) === 1,
];
}
}
Call extension matchers through the expectation chain:
Expect::that($id)->toBeValidUuid();
Extension matchers support not() and cannot replace native matchers.
Extension matchers also work with eventually() and consistently().
Greenlight calls the predicate for each value from the probe. The matcher counts
as one expectation. A predicate exception stops the poll operation.
Declare matcher parameters with normal native PHP types. PHP enforces these types at run time. Greenlight’s PHPStan extension reads them for static analysis.
PHPStan support for extension matchers
The expectation chain sends matcher calls through __call. PHPStan cannot
check these calls without an extension.
Greenlight includes a PHPStan extension for matcher calls. It loads your
Greenlight configuration files in the same way as workers. It reflects each
matcher closure and exposes each matcher as a real expectation-chain method.
This support includes eventually() and consistently() chains.
Typos, incorrect argument counts, and incorrect argument types then cause a
normal phpstan analyse error.
includes:
- vendor/greenlight/greenlight/extension.neon
parameters:
greenlight:
configFiles:
- greenlight.php
PHPStan provides analysis. IDE completion needs a separate file because IDE indexers do not run PHPStan plugins.
Run vendor/bin/greenlight ide-helper to generate
_greenlight_ide_helper.php. No process executes this file. It declares a
duplicate expectation chain with @method annotations for each configured
matcher. PhpStorm and Intelephense merge the duplicate declaration. Thus,
configured matchers have their real signatures in IDE completion.
Add the helper file to .gitignore. Regenerate it after a matcher change.
PHPStan and the IDE helper use the same matcher map. Thus, analysis and completion remain consistent.
PHPStan resolves relative configuration paths from its current directory. Multiple configuration files supply the union of their matchers. Analysis fails if the same matcher name has different signatures. One analysis run can use only one signature for a matcher name.
When PHPStan first loads the matcher map, it runs plugin constructors in its process. Each worker also runs the plugin constructors.
Reporter
In Greenlight\Reporting.
Implement onEvent(Event $event): void and finish(): void. These methods
render the event stream in another format.
The built-in Greenlight reporters use the same interface.
Plugin order and error policy
Subscribers run in registration order.
A plugin can also implement Greenlight\Plugin\Prioritized:
public function priority(): int;
Lower numbers run earlier. The default priority is 0. The stable sort keeps
the registration order of plugins that have the same priority.
Greenlight reports all plugin failures. A worker-side failure causes an error for the affected test and names the plugin. An orchestrator-side failure causes the run to fail.