Greenlight

Documentation

Start

On this page

Start with Greenlight

Greenlight is an attribute-based test framework for PHP 8.4 and later. It runs tests in parallel by default.

This guide starts with an empty project and creates a successful test run.

Requirements and installation

Greenlight requires PHP 8.4 or later. It does not require a PHP extension.

The parallel runner uses core stream sockets and proc_open. Coverage requires ext-pcov or Xdebug in coverage mode.

If PHP disables a process or stream function that the parallel runner requires, Greenlight uses an in-process sequential run.

An in-process run cannot give #[Isolated] tests a dedicated process.

Install Greenlight as a development dependency:

composer require --dev greenlight/greenlight

Configure the Composer autoloader

Add these keys to the project composer.json file:

{
    "autoload": {
        "psr-4": {
            "App\\": "src/"
        }
    },
    "autoload-dev": {
        "psr-4": {
            "App\\Tests\\": "tests/"
        }
    }
}

Create the source and test directories. Then update the Composer autoloader:

mkdir -p src tests
composer dump-autoload

Create the configuration file

Greenlight reads greenlight.php from the project root. This file returns a typed builder.

Create this minimum configuration:

<?php

declare(strict_types=1);

use Greenlight\Config\GreenlightConfig;

return GreenlightConfig::create()
    ->paths(['tests'])
    ->workers(count: 'auto');

paths() specifies the directories that Greenlight scans for tests. workers('auto') calculates the worker count from the CPU count.

These values are defaults. Thus, this configuration has the same result:

return GreenlightConfig::create();

The longer form can make a new project easier to understand. See the configuration reference for the complete builder API.

Create the first test

Tests are PHP classes. Add #[Test] to each test method.

Greenlight scans the configured paths for files whose names end in Test.php. The class short name must match the file name. Composer must be able to autoload the class from its namespace.

For example, tests/GreeterTest.php must declare the class App\Tests\GreeterTest with the mappings in this guide.

Greenlight does not require a TestCase base class or a test method name pattern. Start each expectation with Expect::value().

Constructor injection supplies stateful test services when a test requests them.

Create this small class:

<?php

declare(strict_types=1);

namespace App;

final class Greeter
{
    public function greet(string $name): string
    {
        $name = \trim($name);

        if ($name === '') {
            throw new \InvalidArgumentException('Name cannot be empty.');
        }

        return \sprintf('Hello, %s!', $name);
    }
}

Save the file as src/Greeter.php.

Create a test for both results of the public method:

<?php

declare(strict_types=1);

namespace App\Tests;

use App\Greeter;
use Greenlight\Attribute\Test;
use Greenlight\Expect\Expect;

final class GreeterTest
{
    #[Test]
    public function greetsByName(): void
    {
        $greeter = new Greeter();

        Expect::value($greeter->greet('Ada'))->toBe('Hello, Ada!');
    }

    #[Test]
    public function rejectsEmptyNames(): void
    {
        $greeter = new Greeter();

        Expect::calling(
            static fn (): string => $greeter->greet(''),
        )->toThrow(\InvalidArgumentException::class, matching: '/empty/');
    }
}

Save the file as tests/GreeterTest.php.

Expect::value() starts a matcher chain for a value. A failed matcher throws immediately and includes a clear difference when applicable.

The expectations reference describes each matcher, negation, polls, exception checks, and explicit failures.

Run tests

Run the suite:

vendor/bin/greenlight run

run is the default command. This command has the same result:

vendor/bin/greenlight

Use these commands for common tasks:

With --bail, active assignments can finish after the limit. The final failure count can therefore exceed one.

The --exclude-class, --exclude-method, and --exclude-path flags also exclude tests. Exclusion rules take priority over inclusion rules.

The --list-groups and --list-suites flags print the discovered groups and configured suites.

Repeat one plan to find an intermittent failure:

vendor/bin/greenlight run --filter=CheckoutTest --repeat=20
vendor/bin/greenlight run --filter=CheckoutTest --repeat-until-failure

Each iteration reports its number. The summary identifies each failed iteration.

The command returns a nonzero exit code if an iteration fails. --repeat-until-failure stops after the first failed iteration.

Without --repeat=N, this mode stops after 100 iterations. Add --repeat=N to specify a different limit.

Repeat modes do not support JUnit output or enabled coverage. Run a separate command for each required report.

Read the output

Greenlight uses the tty reporter on an interactive terminal. This reporter shows live progress with ANSI color. At the end of the run, it prints problem details and differences before the summary.

Greenlight uses the plain reporter when standard output is not a TTY. This reporter prints the run start, one line for each completed test, problem details, and the final summary. It does not print escape codes.

Select a reporter with --reporter:

vendor/bin/greenlight run --reporter=plain
vendor/bin/greenlight run --reporter=junit

Repeat the flag to select more than one reporter:

vendor/bin/greenlight run --reporter=tty --reporter=junit

Add a file to keep a reporter separate from standard output:

vendor/bin/greenlight run --reporter=tty --reporter=junit=reports/junit.xml

Greenlight creates missing parent directories. It replaces an existing file.

Plugins can register custom reporter factories. See ReporterProvider.

Tests can retain diagnostic data without output to standard output. Inject Greenlight\Artifact\Attachments.

Call value(), text(), bytes(), or file() to add an attachment. Greenlight prints retained paths and stores files below build/greenlight-artifacts by default.

See test attachments for more information.

Use watch mode

Start watch mode:

vendor/bin/greenlight run --watch

Watch mode runs all selected tests at startup and after each file change. It watches effective test paths and coverage include paths.

Classes that failed in the previous watch iteration run first.

Press Enter to rerun all selected tests. Press q to stop watch mode with exit code 0, regardless of the last iteration result.

Watch mode does not print coverage totals or write coverage exports.

Watch mode combines rapid save events. The default delay is 200 ms.

Use the watch() configuration builder to change this delay.

The builder can also watch non-PHP inputs:

->watch(fn ($watch) => $watch
    ->paths('templates', 'config')
    ->include('**/*.twig', '**/*.yaml')
    ->exclude('build/**', 'coverage/**'))

Relative paths and patterns use the command working directory. Exclusions have precedence and can prevent runs that generated artifacts would cause.

Configure workers

Tests run in parallel worker processes by default.

--workers=auto uses one worker for each detected CPU core. This value is the default. If CPU detection fails, Greenlight uses four workers.

--workers=4 specifies four workers. --workers=1 uses one in-process runner. The last mode is usually the simplest choice for debug work.

Do not use --workers=1 when a test depends on #[Isolated] for process-global state cleanup.

A worker remains active until it has no more tests or the worker fails. This behavior makes memory growth and state leaks visible in the suite.

Parallel suites need separate resources for workers or a limit for one shared dependency. Greenlight supports both methods.

Use a channel when each worker has a separate external resource. The channel number is from 1 through the worker count.

Each worker receives its channel through Greenlight\Test\TestChannel and the GREENLIGHT_CHANNEL environment variable.

A plugin can also provision real infrastructure in the orchestrator and expose typed connection data through Greenlight\IntegrationFixture\IntegrationResources. That keeps setup and teardown active when a worker crashes. See Writing plugins.

final class OrderRepositoryTest
{
    public function __construct(
        private readonly TestChannel $channel,
    ) {}

    #[Test]
    public function persistsAnOrder(): void
    {
        $pdo = new \PDO('mysql:host=127.0.0.1;dbname=app_test_' . $this->channel->number, 'app', 'secret');
        // ...
    }
}

Concurrent tests within one run never share a channel. Thus, databases such as app_test_1 and app_test_2 do not conflict within that run.

Separate runs and CI shards reuse channel numbers. Add a separate resource prefix for each concurrent run that uses the same database server.

Use #[RequiresResource] when several workers use one dependency with limited capacity:

#[RequiresResource('payments-sandbox')]
final class PaymentGatewayTest { ... }
return GreenlightConfig::create()
    ->workers(8)
    ->resourceLimit('payments-sandbox', 2);

Other workers can run tests that do not require payments-sandbox. Without a configured limit, only one assignment can use the resource at a time.

A resource limit controls capacity. It does not select a sandbox, database, or account for a test.

Use a channel when each worker has one resource instance. Use an application-owned allocator for fewer resource instances.

A Greenlight resource limit can restrict the tests that enter that allocator.

A test can use both methods. Its channel can select a database while #[RequiresResource] controls access to a shared service.

Resource limits apply to one Greenlight run. Other worktrees and CI shards have separate counts.

Use external coordination when different processes share the dependency. See the configuration reference for all resource limit rules.

Use built-in sandboxes

The harness supplies per-test sandboxes from Greenlight\Sandbox. Constructor injection supplies each sandbox.

Greenlight disposes each sandbox after its test. Thus, temporary state does not leak to other tests or workers.

use Greenlight\Sandbox\EnvironmentVariables;
use Greenlight\Sandbox\TemporaryDirectory;

final class ExporterTest
{
    public function __construct(
        private readonly TemporaryDirectory $tmp,
        private readonly EnvironmentVariables $env,
    ) {}

    #[Test]
    public function writesTheExportFile(): void
    {
        $this->env->set('EXPORT_DIR', $this->tmp->path());

        new Exporter()->run();

        Expect::value(\file_exists($this->tmp->path() . '/export.csv'))->toBeTrue();
    }
}

Each test receives separate sandbox instances. Each TemporaryDirectory instance has a unique directory.

Defer test cleanup

Ask for Greenlight\Test\Cleanup through constructor injection. Call defer() immediately after the test acquires a resource.

use Greenlight\Attribute\Test;
use Greenlight\Test\Cleanup;

final readonly class ServerTest
{
    public function __construct(private Cleanup $cleanup) {}

    #[Test]
    public function acceptsAConnection(): void
    {
        $server = TestServer::start();
        $this->cleanup->defer(static fn() => $server->stop());

        // Test the server.
    }
}

Greenlight runs cleanup callbacks once in reverse registration order. It runs them after After hooks and before per-test sandbox disposal.

A callback failure does not prevent the remaining callbacks. For a passed or skipped test, the first cleanup failure determines the new outcome. An ExpectationFailed changes the outcome to failed. Another throwable changes it to errored. An earlier test failure or error remains primary.

Exit codes

Greenlight uses these exit codes:

For example, SIGINT returns 130 and SIGTERM returns 143. See interruption for the graceful shutdown rules.

Exit code 1 includes test failures, test errors, invalid configuration, discovery errors, coverage gate failures, coverage export errors, and detected leaks.

Greenlight treats a run without tests as a configuration problem.