Browse documentation

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.