Visitar URL original
Test runner sometimes just stops, sometimes gives "Allowed memory size exhausted" error · Issue #6840 · Codeception/Codeception · GitHub
Skip to content

Test runner sometimes just stops, sometimes gives "Allowed memory size exhausted" error #6840

Description

@ThomasLandauer

Since my latest run (~2 weeks ago), the Codeception runner is suddenly hanging strangely:

  • Sometimes, in the middle of the suite, it just stops with message "COMMAND DID NOT FINISH PROPERLY."

  • Sometimes (at another test), it crashes with:

    PHP Fatal error: Allowed memory size of 1073741824 bytes exhausted (tried to allocate 20480 bytes) in ...vendor/symfony/var-dumper/Caster/ClassStub.php on line 52

    However, the memory_limit in my php.inis (for CLI and FPM) is 3000M. So where is the 1G limit coming from?

This is not caused by the tests themselves, cause when I run just the affected files alone, everything's fine.

Any ideas? Is somebody else experiencing this? Were there recent changes regarding memory management (couldn't find anything)?

Activity

  1. s1lver commented on Apr 14, 2025

    @s1lver

    PHP 8.3.19

    I have the same problem after upgrading to version 5.2.1

    FATAL ERROR. TESTS NOT FINISHED.
    Allowed memory size of 1073741824 bytes exhausted (tried to allocate 1003520 bytes)
    An Error occurred while handling another error:
    Headers already sent in /var/www/vendor/codeception/codeception/src/Codeception/Subscriber/ErrorHandler.php on line 175

  2. s1lver commented on Apr 14, 2025

    @s1lver

    Memory consumed(same tests):

    Codeception v5.1.2 - 770.34 MB
    Codeception v5.2.1 - 1.30 GB
    Codeception v5.3.0 - 1.27 GB

  3. TavoNiievez commented on Jun 23, 2025

    @TavoNiievez
    Member

    If you provide a sample application where this behavior can be replicated I could go commit by commit backwards and find out what change causes it.

    In 5.2.0 and 5.2.1 a lot of changes were integrated.

  4. ThomasLandauer commented on Dec 19, 2025

    @ThomasLandauer
    MemberAuthor

    This issue was now gone for a while, however it just re-appeared (probably introduced by Codeception 5.3.3).

    My main question is still:
    Where is the 1G limit coming from??
    (I have memory_limit = 3000M in php.ini)

  5. ThomasLandauer commented on Dec 19, 2025

    @ThomasLandauer
    MemberAuthor

    Answer: Codeception has a hard memory_limit of 1024M. I'm removing it in #6916

    Meanwhile, you can override it by adding this to your codeception.yml:

    settings:
        memory_limit: 3G
  6. W0rma commented on Dec 19, 2025

    @W0rma
    Contributor

    Codeception has a hard memory_limit of 1024M.

    The memory limit is not really "hard" but configurable. The "memory_limit" setting (and its default value of 1024M) is documented in https://codeception.com/docs/reference/Configuration#settings.

  7. Apache02 commented on Jul 1, 2026

    @Apache02

    Root cause: PHPUnit's EventFacade is never sealed under Codeception

    I profiled a large suite that OOMs (individual files run fine, full run dies around
    ~3000 tests) and tracked it down to unbounded accumulation of PHPUnit event objects,
    not test/app state.

    What accumulates

    Comparing memory snapshots across the run, the object growth is dominated by PHPUnit's
    per-event telemetry:

    Δ objects (between two snapshots) Class
    +382k PHPUnit\Event\Telemetry\MemoryUsage
    +191k PHPUnit\Event\Telemetry\Duration
    +95k (each) Telemetry\Snapshot / HRTime / Info / GarbageCollectorStatus
    +81k PHPUnit\Event\Test\AssertionSucceeded

    AssertionSucceeded is emitted for every successful assertion, and each event carries a
    telemetry Snapshot, so growth is linear in the number of assertions → 2 GB on a big suite.

    Who retains them

    Building a reverse-reachability graph from these objects to a GC root, every instance is held by:

    PHPUnit\Event\Facade::instance (static singleton)
    └─ DeferringDispatcher
    └─ EventCollection → array ← every event ever emitted, for the whole run

    PHPUnit\Event\DeferringDispatcher starts with recording = true; in that state dispatch()
    just appends each event to its EventCollection and returns. It only stops recording (and clears
    the buffer) when EventFacade::seal() runs. seal() is called in exactly one place —
    PHPUnit\TextUI\Application — which Codeception does not use (it wires PHPUnit's components
    directly via TextUI\Configuration). So the facade is never sealed, recording stays true
    forever, and every event is retained. Nothing ever consumes them, since Codeception runs its own
    (Symfony) event bus.

    This still reproduces on main — Suite.php never calls seal() — so it's not version-specific.

    Fix / workaround

    Sealing the facade once, early, makes PHPUnit dispatch events directly (to no subscribers) instead
    of buffering them. A minimal Codeception extension does the job:

    use Codeception\Event\SuiteEvent;
    use Codeception\Events;
    use Codeception\Extension;
    use PHPUnit\Event\Facade as EventFacade;
    
    final class SealPhpUnitEvents extends Extension
    {
        public static array $events = [Events::SUITE_BEFORE => 'seal'];
    
        private static bool $done = false;
    
        public function seal(SuiteEvent $e): void
        {
            if (!self::$done && class_exists(EventFacade::class)) {
                EventFacade::instance()->seal();
                self::$done = true;
            }
        }
    }

    This is safe because Codeception registers no subscribers on PHPUnit's EventFacade, so
    EventFacadeIsSealedException can't be triggered and no behavior depends on the buffered events.

    A/B on the same large suite:

      ┌──────────────┬─────────────────────────────────┬────────────────┐
      │              │             Result              │      Peak      │
      ├──────────────┼─────────────────────────────────┼────────────────┤
      │ without seal │ OOM mid-run (~2272 tests)       │ ~2022 MB       │
      ├──────────────┼─────────────────────────────────┼────────────────┤
      │ with seal    │ full suite passes (~3300 tests) │ ~1050 MB, flat │
      └──────────────┴─────────────────────────────────┴────────────────┘
    

    Memory becomes flat (final ≈ peak) instead of climbing linearly.

    Verification methodology (reproducible)

    1. Install php-meminfo.
    2. In a Codeception extension, on each TEST_AFTER, log memory_get_peak_usage(true) per test
      and dump the live heap with meminfo_dump() whenever peak grows by a threshold.
    3. Run the full suite; peak grows monotonically → accumulation, not a one-off spike.
    4. Diff object counts per class between an early and a late dump → PHPUnit Event\Telemetry*
      and AssertionSucceeded dominate.
    5. Reverse-walk the children graph from those objects to the first is_root node → it's
      PHPUnit\Event\Facade::instance → DeferringDispatcher → EventCollection.
    6. Add EventFacade::instance()->seal() on SUITE_BEFORE, re-run → memory stays flat.

    Investigated with Claude Code — php-meminfo heap diffing + reverse-reachability analysis.
    This write-up was generated by Claude.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions