Browse documentation

Configuration

Configure Greenlight with one greenlight.php file at the project root.

The file returns a Greenlight\Config\GreenlightConfig builder. The CLI loads the builder and applies command-line overrides. It then creates an immutable configuration object.

Precedence

Greenlight applies configuration in this order:

  1. Built-in defaults
  2. greenlight.php
  3. CLI flags

Later layers override earlier ones.

For example, use this configuration:

->workers('auto')

combined with this CLI flag:

greenlight run --workers=1

runs with one worker.

GreenlightConfig

Create the builder with:

GreenlightConfig::create()

Every builder method returns $this. Thus, you can chain method calls.

paths(array $tests): self

Default: ['tests'].

Sets the directories that Greenlight scans when you do not select a suite.

Paths must be non-empty strings. The list itself must not be empty.

suite(string $name, callable $configurator): self

Default: no suites.

Declares a named suite. Greenlight gives a SuiteBuilder to the configurator. The configurator must add at least one path. Greenlight ignores its return value. Thus, you can use an arrow function.

A second declaration with the same suite name causes an error.

->suite('unit', fn ($s) => $s->in('tests/Unit'))
->suite('integration', fn ($s) => $s->in('tests/Integration')->tag('io'))

SuiteBuilder has two methods:

workers(int|string $count = 'auto', ?int $recycleAfterTests = null, string $recycleAboveMemory = '256M'): self

Default: 'auto' workers, a 256M memory recycle limit, and no test-count recycle limit.

A worker requests one class at a time. When the worker finishes that class, it requests the next class. Thus, a long class does not make another worker idle. The orchestrator gives first priority to classes that failed in the previous run. It orders the other classes by previous duration, longest first.

Worker placement is load-dependent. The stable parts are:

The seed reproduces failures related to order. It does not reproduce exact worker placement.

$count accepts a positive integer or 'auto'. With 'auto', Greenlight uses one worker per CPU core. Install the suggested fidry/cpu-core-counter package for CPU detection that respects cgroup limits in containers.

Greenlight recycles a worker when one of these conditions is true:

$recycleAboveMemory is a size string such as '256M' or '1G'.

Test-count recycle is optional because each recycle requires a complete worker boot. The worker channel is idle during the boot. Use this limit for suites that collect per-process state that memory checks cannot detect. Examples include connections and file handles.

resourceLimit(string $name, int $limit = 1): self

Default: 1 for a resource used by #[RequiresResource].

Limits how many assignments in one Greenlight run can use the named resource at once.

return GreenlightConfig::create()
    ->resourceLimit('postgres', 3)
    ->resourceLimit('payments-sandbox');

Limits must be positive. Names must match [a-z0-9][a-z0-9._-]*. Configure each name only one time.

An assignment that requires several resources waits until all of them have capacity. Greenlight claims the slots together and sends the assignment only after all slots are available.

The limit is an in-memory scheduler gate, not a distributed lock. Separate Greenlight processes, worktrees, and CI shards do not share capacity.

The scheduler does not choose a concrete resource instance or expose a lease identifier. If a test needs one of several distinct instances, the application must allocate it. Use a channel instead when every worker can have its own instance.

coverage(callable $configurator): self

Default: coverage off.

The coverage() method enables coverage collection. Greenlight gives a CoverageBuilder to the configurator.

Multiple calls to coverage() use the same builder. Thus, its settings accumulate.

CoverageBuilder methods:

->coverage(fn ($c) => $c
    ->include('src')
    ->driver('pcov')
    ->export('lcov', 'coverage/lcov.info')
    ->export('html', 'coverage/html'))

When you configure coverage, the run prints the total percentage and writes each coverage export.

If no worker can collect coverage because neither pcov nor Xdebug coverage mode is available, Greenlight warns on stderr. That warning does not fail the run by itself.

The workers are not the only measured processes. In a parallel run, the orchestrator collects its own coverage. Thus, the export includes code that executes only in the orchestrator. A coverage run also exports GREENLIGHT_COVERAGE_DIR and GREENLIGHT_COVERAGE_INCLUDE to each process that it starts. A bin/greenlight process writes coverage to the shared directory if it inherits these variables. For example, an acceptance test can start this process to operate the real CLI.

Before export, the run adds these coverage files to the result. This collection operation does not fail the run. The configured include paths filter coverage from all processes.

watch(callable $configurator): self

Default: 200 ms debounce.

The configurator receives a WatchBuilder.

WatchBuilder has one method:

A rerun starts after the configured period has no file changes. Thus, a group of save operations starts only one run.

->watch(fn ($w) => $w->debounceMilliseconds(500))

failOnDeprecation(bool $enabled = true): self

Default: off.

Fails a test that otherwise passed if its captured diagnostics contain a deprecation.

The worker records the change as a result transformation. Thus, the exit code, --bail, JUnit output, and plugins use the same final result.

The worker applies this policy after retries and afterTest() subscribers.

Also available as --fail-on-deprecation.

failOnNotice(bool $enabled = true): self

Default: off.

Fails a test that otherwise passed if its captured diagnostics contain a notice.

As with failOnDeprecation(), the worker records the change as a result transformation. It applies the change after retries and afterTest() subscribers.

Also available as --fail-on-notice.

ignoreDeprecationsMatching(string ...$patterns): self

Default: none.

Exempts deprecations that match a pattern from failOnDeprecation().

Greenlight compares patterns without regard to case. A plain pattern matches a part of the message. A pattern that contains * or ? must match the complete message.

The method is repeatable and patterns accumulate.

Use this for dependency deprecations you cannot fix yet.

failOnRisky(bool $enabled = true): self

Default: off.

A successful test is risky if it does not verify an expectation. Such a test has no Expect calls and no mock expectations that Greenlight verifies at teardown.

The tty and plain reporters always list risky tests after the summary. The failOnRisky() method changes risky tests to failures.

Use #[NoExpectations] for a test that intentionally verifies no expectations.

Each eventually() or consistently() matcher counts once.

Also available as --fail-on-risky.

plugins(Plugin ...$plugins): self

Default: none.

Registers Greenlight plugin instances. Each plugin implements one or more capability interfaces.

The method is repeatable and instances accumulate.

artifacts(callable $configurator): self

Default: output below build/greenlight-artifacts, with failure-only retention.

Greenlight gives an ArtifactBuilder to the configurator. Repeated calls use the same builder.

->artifacts(fn ($artifacts) => $artifacts
    ->directory('build/test-evidence')
    ->maxAttachmentsPerTest(20)
    ->maxAttachmentSize('10M')
    ->maxTestSize('50M')
    ->maxRunAttachments(2_000)
    ->maxRunSize('500M'))

Defaults are 32 attachments and 100 MiB per test. Each attachment has a 25 MiB limit. Each run has limits of 10,000 attachments and 1 GiB. Per-test limits include all retry attempts, even if retention policy discards attachments later. Greenlight releases run quota when it discards an attachment.

See test attachments for the runtime API and security model.

failFast(bool $enabled = true): self

Default: off.

Stops the run after the first failure.

randomizeOrder(?int $seed = null): self

Default: declared order, no seed.

Randomizes class order.

If $seed is null, Greenlight selects and prints a seed at run time. Use --seed with that value to reproduce the same order.

build(): Configuration

The loader calls this method. User configuration does not call it.

build() validates the builder and returns the immutable configuration object.

Return the builder from greenlight.php. Do not call build().

Channels and resource limits

Every worker process runs in a channel: a stable slot numbered from 1 to the worker count.

Use the channel to derive external resources that parallel tests must not share. Examples include database names, ports, virtual hosts, and temporary directories.

Use a channel when each worker can have a separate resource. Use #[RequiresResource] when workers share a dependency with lower safe concurrency. A resource limit controls the number of assignments that can run. It does not assign a resource instance to an assignment.

Two concurrent tests do not share a channel. Channel numbers are from 1 through the worker count. The number of worker processes during the run does not change this range. After a worker recycle or crash, its replacement reuses the freed slot.

A --workers=1 run executes in-process on channel 1.

Greenlight makes the channel available in two ways:

TestChannel->number is the numeric slot.

TestChannel->label() returns gl-<number> for resource names.

Because Greenlight uses channel slots again, channel resources can persist after a worker recycle. A replacement worker on channel 2 can access resources from the previous channel-2 worker.

This design reduces the cost of one-resource-per-channel setups. For example, you can create one database schema per channel and use it for the complete run.

A test can use both. Its channel can select a private database while #[RequiresResource('payments-sandbox')] limits access to a shared external service.

CLI reference

greenlight [command] [options]

Commands

run

Discovers and executes tests.

This is the default command if you do not give a command.

list-tests

Prints each discovered test ID on a separate line. It then prints the count.

coverage:diff

Compares two coverage JSON exports.

Requires:

--baseline=<path>
--current=<path>

Exits with code 1 when coverage regressed against the baseline.

profile:report

Renders a run profile from a saved JSONL event stream.

Requires:

--input=<path>

ide-helper

Writes the IDE autocomplete helper for extension matchers.

Default output:

_greenlight_ide_helper.php

Override it with:

--output=<path>

Add the generated file to .gitignore. Regenerate the file after a matcher change.

completion

Prints a shell completion script to stdout.

Example setup:

source <(greenlight completion bash)
source <(greenlight completion zsh)
greenlight completion fish > ~/.config/fish/completions/greenlight.fish

For zsh, run compinit before you source the completion script.

Options

--config=<path>

Uses this configuration file instead of ./greenlight.php.

--workers=<n|auto>

Overrides the worker process count.

--resource-limit=<name>=<n>

Sets a resource limit for this run and overrides greenlight.php.

vendor/bin/greenlight run \
    --resource-limit=postgres=2 \
    --resource-limit=payments-sandbox=1

Repeat the option to set limits for different resources. Use each name only one time.

--bail[=<n>]

Stops after <n> failures.

Bare --bail means --bail=1.

--group=<name>

Runs only tests in the given group.

Repeatable.

--filter=<pattern>

Runs only tests with a test ID that matches the pattern.

A test ID is Class::method with an optional data-set label.

By default, Greenlight matches substrings without regard to case. A pattern that contains * or ? must match the complete test ID.

Repeatable. Multiple filters form a union.

--test-id=<id>

Runs only the exact test ID. Unlike --filter, this option does not compare substrings or wildcard patterns.

Use the test IDs printed by list-tests or --list-tests. Include the data-set label when present:

greenlight run \
    '--test-id=App\Tests\OrderTest::placesOrder[card]'

Repeatable. Multiple exact test IDs and --filter patterns form a union. Exclusions have precedence.

--exclude-group=<name>

Excludes tests in the given group.

Repeatable. Exclusions take precedence over --group and --filter.

--exclude-class=<pattern>

Excludes classes with names that match the pattern.

By default, Greenlight matches substrings without regard to case. A pattern that contains * or ? must match the complete class name.

Repeatable.

--exclude-method=<pattern>

Excludes methods with names that match the pattern.

By default, Greenlight matches substrings without regard to case. A pattern that contains * or ? must match the complete method name.

Repeatable.

--exclude-path=<prefix>

Excludes tests whose source file is below the path prefix.

Greenlight resolves relative prefixes from the current directory. Repeatable.

--list-tests

Prints the selected test IDs. It does not run them.

--list-groups

Prints each selected group and its test count. It does not run tests.

--list-suites

Prints the configured named suites. It does not discover or run tests.

--repeat=<n>

Runs the selected tests in <n> fresh iterations.

The command exits with a nonzero code if an iteration fails. --repeat=1 is equivalent to an ordinary run.

--repeat-until-failure

Repeats fresh runs until an iteration fails.

On its own, the command stops after 100 successful iterations. Use --repeat=<n> to set a different limit. Do not use it with --watch.

--shard=n/m

Runs the nth of m disjoint slices of the plan.

Greenlight selects shards by a stable class hash. Thus, CI machines can split a suite without coordination. Together, all shards contain the complete suite.

Only whole classes move between shards. Individual methods are not split.

Combines with --group and --filter. Greenlight divides the filtered plan into shards.

Each shard enforces its own resource limits. If four shards each use postgres=2, up to eight tests can use PostgreSQL across the CI job.

--failed

Reruns only tests that did not pass in the previous run.

Greenlight records failure state for each run in the system temporary directory.

If no previous failure state exists, this is a usage error.

If the previous run passed completely, Greenlight reports that there is nothing to rerun and exits 0.

When failure state exists, normal runs put previously failed classes first. --seed disables this order.

--seed=<n>

Randomizes class order with this seed.

Seeded runs do not use the timing-cache order. The seed determines the complete order.

--reporter=<name>

Selects the output format.

Supported reporters:

Repeatable. Multiple reporters write concurrently.

Default: tty on an interactive terminal, otherwise plain.

The tty reporter supports parallel work. It keeps one live line for each active class. Each line has a spinner and a current count. The reporter completes the line in place when the class finishes. Multi-worker output does not interleave randomly.

The teamcity reporter includes IDE navigation metadata: php_qn:// location hints for click-to-source, plus a per-class flowId to keep parallel output separated in JetBrains tools.

--artifacts-dir=<path>

Overrides the configured artifact parent directory for this run. Greenlight creates a unique run directory below it and reports that path in human and machine-readable output.

--baseline=<path>

Sets the baseline coverage JSON file for coverage:diff.

--current=<path>

Sets the current coverage JSON file for coverage:diff.

--watch

Reruns on file changes.

In watch mode:

--detect-leaks

Verifies that garbage collection removes each test instance after its test.

A detected leak fails the run.

--fail-on-deprecation

Enables the deprecation policy for this run.

--fail-on-notice

Enables the notice policy for this run.

--fail-on-risky

Enables the risky-test policy for this run.

--profile

Adds a run profile after the summary.

The profile includes:

The event stream supplies all profile data. You can render a saved JSONL artifact later with:

greenlight profile:report --input=<file>

--input=<path>

Sets the JSONL input file for profile:report.

--output=<path>

Changes the file written by ide-helper.

Default: _greenlight_ide_helper.php.

--dry-run

Prints the resolved configuration and does not run tests.

--verbose

In interactive output, prints a permanent line for every completed class.

--no-ansi

Disables colors and the live progress window. Output becomes plain and append-only.

A truthy CI environment variable has the same effect.

NO_COLOR disables colors only.

-h, —help

Shows help.

-V, —version

Shows the version.

Exit codes

Greenlight uses these exit codes:

Greenlight treats a run with zero tests as a configuration problem. It does not treat the run as a success.

Interruption

The first Ctrl+C, SIGINT, or SIGTERM starts a graceful shutdown.

During graceful shutdown, Greenlight:

The run then exits with:

A second signal during shutdown terminates immediately.

Graceful shutdown requires ext-pcntl. Without it, PHP uses its default signal behavior and exits immediately.

Discovery cache

Greenlight caches discovery results per file under the system temp directory.

The cache key includes the file path, mtime, and size. Greenlight can reuse discovery data for an unchanged file in the next run. If the cache data is uncertain, Greenlight parses the file again.

Watch mode benefits most because every iteration rediscovers the suite.

Interactive output

In an interactive terminal, the tty reporter shows a bounded live window:

The reporter prints failures and skips permanently when their class finishes.

Classes that pass only advance the counter unless you enable --verbose.

Both human reporters start with a one-line header that contains:

They end with a “Slowest tests” block when a test took at least 500 ms. The block lists the five slowest tests.

Fast suites do not print the block.

--profile extends the slowest-test list to 25 entries.