sebastianbergmann/phpunit · error · FrameworkException

Format description cannot be matched: %s

Error message

Format description cannot be matched: %s

What it means

assertStringMatchesFormat() translates its format description (%s, %d, %a, %A, %e, %f, %x, %c, %r...) into a single anchored PCRE regex ('/^...$/s') and runs preg_match(). If PCRE fails on that generated regex (returns false), PHPUnit throws this exception carrying preg_last_error_msg(). The culprit is almost always the generated pattern over-running pcre.backtrack_limit/pcre.recursion_limit on the actual string, not the format syntax itself.

Source

Thrown at src/Framework/Constraint/String/StringMatchesFormatDescription.php:86

     * @throws FrameworkException
     */
    protected function matches(mixed $other): bool
    {
        if (!is_string($other)) {
            return false;
        }

        $other = $this->convertNewlines($other);

        $matches = @preg_match(
            $this->regularExpressionForFormatDescription(
                $this->convertNewlines($this->formatDescription),
            ),
            $other,
        );

        if ($matches === false) {
            throw new FrameworkException(
                sprintf(
                    'Format description cannot be matched: %s',
                    preg_last_error_msg(),
                ),
            );
        }

        return $matches > 0;
    }

    protected function failureDescription(mixed $other): string
    {
        return 'string matches format description';
    }

    protected function failureDescriptionInContext(Operator $operator, mixed $role, mixed $other): string
    {
        // @codeCoverageIgnoreStart

View on GitHub (pinned to f123cdb2a2)

Solutions

  1. Reduce placeholder density: anchor with more literal lines between %a/%A placeholders so the regex has less ambiguity
  2. Split the assertion: assert the stable parts with assertStringContainsString() and only variable tokens with assertStringMatchesFormat()
  3. Raise the limit in phpunit.xml or bootstrap: <ini name="pcre.backtrack_limit" value="10000000"/>
  4. For huge outputs, prefer assertStringEqualsStringIgnoringLineEndings or a normalized diff comparison instead of one giant format regex

Example fix

// before
$this->assertStringMatchesFormat(
    "Header: %s\n%A\nBody: %a\n%A\nFooter: %s",
    $hugeOutput // PCRE backtrack limit exhausted
);

// after
$this->assertStringContainsString('Header: ', $hugeOutput);
$this->assertStringContainsString('Footer: ', $hugeOutput);
// or raise the limit:
// <ini name="pcre.backtrack_limit" value="10000000"/> in phpunit.xml
Defensive patterns

Strategy: validation

Validate before calling

// Keep formats small: assert stable literals with contains, placeholders only where needed:
foreach (['Header: ', 'Footer: '] as $literal) {
    if (!str_contains($actual, $literal)) {
        $this->fail("Missing literal: $literal");
    }
}
$this->assertStringMatchesFormat("Body: %d lines", $bodyLine);

Try / catch

use PHPUnit\Framework\Exception as FrameworkException;

try {
    $this->assertStringMatchesFormat($format, $actual);
} catch (FrameworkException $e) {
    if (str_contains($e->getMessage(), 'Format description cannot be matched')) {
        $this->markTestIncomplete('Format regex hit PCRE limit: ' . $e->getMessage());
    }
}

Prevention

When it happens

Trigger: assertStringMatchesFormat($format, $actual) where the format contains multiple greedy or lazy multiline placeholders (%a, %A, %s chains) applied to a long multi-line string, driving backtracking past pcre.backtrack_limit; or the actual string is invalid UTF-8 relative to an implied unicode context.

Common situations: Snapshot-ish tests asserting CLI/artisan command output, API responses or templated emails with '%a ... %a ... %a' between concrete lines; large payloads in CI where the PHP pcre.backtrack_limit default (1M) is exceeded; formats copied from sprintf() templates with dozens of placeholders.

Related errors


AI-assisted analysis of sebastianbergmann/phpunit@f123cdb2a2 (2026-08-23). Data as JSON: /api/errors/49bc27957c6bec5e. Report an issue: GitHub.