From 2118f11fea99dcde4ae5d169d006c5244d3b207f Mon Sep 17 00:00:00 2001 From: Alexander Lisachenko Date: Wed, 19 Aug 2026 05:36:49 +0000 Subject: [PATCH 1/2] fix(parallel): call offsetExists() instead of the deprecated contains() PHP 8.5 deprecates SplObjectStorage::contains() in favour of offsetExists(). PreforkTaskDirectory keyed its task-to-address map by an SplObjectStorage and probed it with contains() in both register() and addressOf(), so every prefork test raised the deprecation on 8.5. offsetExists() has the same semantics and has existed since PHP 5.3, so this needs no floor change. The suite never showed it: every .phpt suppresses E_DEPRECATED. Forcing error_reporting=E_ALL over each file's own --INI-- block on PHP 8.5.9 with z-engine 8.5.0 turned 12 of the 121 tests red, every one of them on this deprecation; with the call replaced the same forced-E_ALL run is 121/121 on both PHP 8.4.19 (z-engine 8.4.2) and PHP 8.5.9 (z-engine 8.5.0), and no deprecation of any kind is emitted. Closes #39 Co-Authored-By: Claude Claude-Session: https://claude.ai/code/session_013foRd1XwLwqjUSkSWeWrMe --- src/Parallel/PreforkTaskDirectory.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Parallel/PreforkTaskDirectory.php b/src/Parallel/PreforkTaskDirectory.php index bf4d3f7..141e29b 100644 --- a/src/Parallel/PreforkTaskDirectory.php +++ b/src/Parallel/PreforkTaskDirectory.php @@ -76,7 +76,7 @@ public function __construct() */ public function register(Task $task): int { - if ($this->addresses->contains($task)) { + if ($this->addresses->offsetExists($task)) { return $this->addresses[$task]; } @@ -91,7 +91,7 @@ public function register(Task $task): int public function addressOf(Task $task): int { - if (!$this->addresses->contains($task)) { + if (!$this->addresses->offsetExists($task)) { throw new \LogicException(sprintf( '%s was never published to the workers; without the shared arena (see #7) a task ' . 'can only reach a worker by being registered before the fork', From c72a1c79027ef73ea8a6b1af8ed7ba419af7b730 Mon Sep 17 00:00:00 2001 From: Alexander Lisachenko Date: Wed, 19 Aug 2026 05:40:17 +0000 Subject: [PATCH 2/2] test(tests): stop suppressing deprecations in the .phpt suite MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Every one of the 121 functional tests carried a third --INI-- line, error_reporting=E_ALL & ~E_DEPRECATED. It was added for a dependency: PHPUnit's .phpt runner forces display_errors=1, so a deprecation raised by z-engine — back when it was consumed from a development branch — was prepended to the captured output of every test and failed --EXPECT-- blocks over noise that had nothing to do with this library. The stable releases now required (~8.4.2 || ~8.5.0) raise none, and the line was hiding this package's own deprecations too. That is exactly how the SplObjectStorage::contains() call PHP 8.5 deprecated stayed green through 12 tests until it was fixed in the previous commit. Removing the suppression is only safe in that order, so it goes second. ffi.enable=1 and opcache.jit=off stay: FFI cannot be enabled at runtime and the JIT rewrites the executor internals the engine hooks depend on. The guard test is renamed to match and now checks both halves — that each file declares both required lines, and that no file sets error_reporting at all, so a filter cannot be reintroduced to silence a future deprecation instead of fixing it. tests/Support/childProcess.php spawns children with the same settings and drops the line for the same reason. AGENTS.md now documents two lines and why the third went away; README.md and .github/workflows/ci.yml never named it and are unchanged. Verified with a runner that reproduces PhptTestCase: 121/121 on PHP 8.4.19 (z-engine 8.4.2) and 121/121 on PHP 8.5.9 (z-engine 8.5.0), with no diagnostics of any kind in the output. Closes #38 Co-Authored-By: Claude Claude-Session: https://claude.ai/code/session_013foRd1XwLwqjUSkSWeWrMe --- AGENTS.md | 22 +++-- ...BufferedChannelParksOnlyAtItsCapacity.phpt | 1 - ...FreeLoopDoesNotStarveAnotherCoroutine.phpt | 1 - ...llPreemptedAfterTheRuntimeHasBeenIdle.phpt | 1 - ...extWithdrawsItsRendezvousRegistration.phpt | 1 - ...stsABoundedNumberOfWakeupsOnBothSides.phpt | 1 - ...nelComposesInASelectWithALocalChannel.phpt | 1 - ...tACapacityZeroSharedChannelIsAccepted.phpt | 1 - ...insItsBufferBeforeReportingExhaustion.phpt | 1 - ...edChannelReportsThatASendWouldNotPark.phpt | 1 - ...eRegisteredBeforeTheForkRunsInAWorker.phpt | 1 - ...tedResultSlotIsNotRetainedByItsWaiter.phpt | 1 - ...textIsSelectableThroughItsDoneChannel.phpt | 1 - ...ativeRuntimeHasNoSharedArenaAndSaysSo.phpt | 1 - ...verCooperatesEndsTheRunWithADiagnosis.phpt | 1 - .../testACriticalSectionIsNeverPreempted.phpt | 1 - ...AForkedChildMustRearmItsOwnSliceTimer.phpt | 1 - ...herThanAnsweredWithAnotherTasksResult.phpt | 1 - ...tALocalChannelNeedsNoPollerDescriptor.phpt | 1 - ...etailCannotBeAttachedSaysSoExplicitly.phpt | 1 - ...testAPanickingTaskSurfacesAtItsWaiter.phpt | 1 - ...arkedSenderThrowsWhenTheChannelCloses.phpt | 1 - .../testAPoolOutlivesItsResultSlotSupply.phpt | 1 - ...tAPreemptedCoroutineIsNeverThrownInto.phpt | 1 - ...ptedCoroutineThatParksIsSafeToDiscard.phpt | 1 - ...ecordSplitAcrossTwoReadsIsReassembled.phpt | 1 - ...zvousReceiverParksUntilASenderArrives.phpt | 1 - ...WaitsUntilAnotherProcessTakesTheValue.phpt | 1 - ...tARendezvousWaitIsNeverAFalseDeadlock.phpt | 1 - ...sultSlotIsAwaitableFromAnotherProcess.phpt | 1 - ...NoRendezvousRegistrationForALaterSend.phpt | 1 - ...OnACapacityZeroSharedChannelIsRefused.phpt | 1 - ...elAndALocalChannelBothFireInOneSelect.phpt | 1 - ...sNeverReadWithATagNewerThanItsPayload.phpt | 1 - ...endezvousSendIsStillParkedOnItsTicket.phpt | 1 - ...wnedTaskComesBackWithItsIntegerResult.phpt | 1 - .../testATaskCanBePinnedToOneWorker.phpt | 1 - ...icSurfacesWithItsClassMessageAndTrace.phpt | 1 - ...TickOutsideACoroutineIsNotAPreemption.phpt | 1 - ...AWorkerExitsWhenItsParentDiesAbruptly.phpt | 1 - ...rInAPreemptivePoolIsActuallyPreempted.phpt | 1 - ...illedHoldingAnArenaLockFailsItsWaiter.phpt | 1 - ...riticalSectionReportsTheRecoveredLock.phpt | 1 - ...MutatesASharedObjectOnTheSameIdentity.phpt | 1 - ...testAWorkerTaskReachesItsOwnPreemptor.phpt | 1 - ...dyCompleteSlotIsAwaitedWithoutParking.phpt | 1 - ...eRunCostsNoMoreCpuThanACooperativeOne.phpt | 1 - ...RuntimeDoesNotWakeThePollerEverySlice.phpt | 1 - ...reemptivePoolStopsItsOwnSliceTimerToo.phpt | 1 - .../testAnIoWaitIsNeverAFalseDeadlock.phpt | 1 - ...dingParallelWaitIsNeverAFalseDeadlock.phpt | 1 - ...ndleGivesItsSlotBackWhenItIsCollected.phpt | 1 - ...utineIsNeverLeftForTheEngineToDestroy.phpt | 1 - ...testAwaitReadableWakesWhenDataArrives.phpt | 1 - ...ableWakesWhenTheReaderDrainsTheBuffer.phpt | 1 - ...tCancellingAContextCancelsItsChildren.phpt | 1 - .../testClosingAChannelTwiceThrows.phpt | 1 - .../testClosingWakesEveryParkedReceiver.phpt | 1 - ...llationIsSelectableOnTheRealScheduler.phpt | 1 - ...thTimeoutCancelsWhenItsSleeperReturns.phpt | 1 - ...ntrolRecordCarriesOnlyFixedSizeFields.phpt | 1 - ...rkersAreReapedRatherThanLeftAsZombies.phpt | 1 - ...lockDumpsEveryLocallyBlockedCoroutine.phpt | 1 - ...aringASharedRootAfterTheForkIsRefused.phpt | 1 - .../testEachPanicKeepsItsOwnSharedError.phpt | 1 - ...TagCrossesAWorkerBoundaryAndComesBack.phpt | 1 - ...yTestDeclaresTheThreeRequiredIniLines.phpt | 71 ---------------- ...eryTestDeclaresTheTwoRequiredIniLines.phpt | 83 +++++++++++++++++++ ...EveryWorkerForksAtStartAndIsReachable.phpt | 1 - ...AChannelStopsWhenItIsClosedAndDrained.phpt | 1 - ...tIdleWithAWatchedStreamBlocksInSelect.phpt | 1 - ...stKillingAWorkerMidTaskFailsItsWaiter.phpt | 1 - ...ainReturningDiscardsPendingCoroutines.phpt | 1 - ...MutexReportsAReentrantLockAsADeadlock.phpt | 1 - ...SerializesCriticalSectionsInFifoOrder.phpt | 1 - ...dSourceDumpsOrHandleKeysASharedObject.phpt | 1 - ...cksLaterCallersUntilTheFirstCompletes.phpt | 1 - ...eplaysAFailedInitializerToEveryCaller.phpt | 1 - ...testOnceRunsItsInitializerExactlyOnce.phpt | 1 - ...stParkedReceiversAreServedInFifoOrder.phpt | 1 - ...testParkedSendersAreServedInFifoOrder.phpt | 1 - ...tPollerRetriesAfterSignalInterruption.phpt | 1 - ...ArithmeticMatchesTheUnpreemptedResult.phpt | 1 - ...eemptedCoroutinesAreDrainedAtShutdown.phpt | 1 - .../testPreemptionIsOffByDefault.phpt | 1 - ...sChannelHandsOffBetweenRealCoroutines.phpt | 1 - ...stRendezvousHandsTheValueOverDirectly.phpt | 1 - ...dezvousSendParksUntilAReceiverArrives.phpt | 1 - ...sultsThatNeedTheArenaAreRefusedByName.phpt | 1 - ...testSelectParksAndIsWokenByARealTimer.phpt | 1 - ...tParksOnEveryCaseUntilOneBecomesReady.phpt | 1 - ...SendCaseCompletesWhenAReceiverArrives.phpt | 1 - ...ThrowsWhenItsChannelClosesWhileParked.phpt | 1 - ...preadsItsChoiceAcrossAlwaysReadyCases.phpt | 1 - ...stSelectTakesAReadyCaseWithoutParking.phpt | 1 - ...lectUnlinksTheWaitersOfItsLosingCases.phpt | 1 - .../testSelectWithADefaultNeverParks.phpt | 1 - .../testSendOnAClosedChannelThrows.phpt | 1 - .../testSleepingProgramBurnsNoCpu.phpt | 1 - .../testSleepsWakeInDeadlineOrder.phpt | 1 - .../testSpawnedCoroutinesRunInFifoOrder.phpt | 1 - .../testTaggedRecordRoundTripsEveryTag.phpt | 1 - .../testTasksAreDistributedRoundRobin.phpt | 1 - ...trolSocketCarriesOnlyFixedSizeRecords.phpt | 1 - ...estTheShippedSourceNeverEncodesAValue.phpt | 1 - ...utdownLadderClimbsFromRecordToSigkill.phpt | 1 - ...IsArmedAgainAfterASignalCutAPollShort.phpt | 1 - ...faceCarriesNoConfigurationOrLifecycle.phpt | 1 - ...ontractMatchesTheSubstrateNumerically.phpt | 1 - ...kePipeIsDrainedSoThePollerDoesNotSpin.phpt | 1 - .../testTimerAfterFiresOnItsDeadline.phpt | 1 - ...fOneProcessRendezvousOnASharedChannel.phpt | 1 - ...stTwoSharedRootsOfOneClassAreDistinct.phpt | 1 - .../testTwoStreamsWakeIndependently.phpt | 1 - ...stTwoTasksOfOneClassAreInFlightAtOnce.phpt | 1 - ...testUncaughtThrowableSurfacesOutOfRun.phpt | 1 - ...tUnparkIsIdempotentAndFirstCallerWins.phpt | 1 - ...reableValuesAreRefusedWithTheirRemedy.phpt | 1 - ...tWaitGroupJoinsRealSleepingCoroutines.phpt | 1 - .../testWaitGroupRejectsANegativeCounter.phpt | 1 - .../testWaitGroupWaitsForEveryUnitOfWork.phpt | 1 - ...estWatchReadableWakesAnotherCoroutine.phpt | 1 - .../testYieldGoesToTheTailOfTheRunQueue.phpt | 1 - tests/Support/childProcess.php | 7 +- 124 files changed, 101 insertions(+), 202 deletions(-) delete mode 100644 tests/Functional/testEveryTestDeclaresTheThreeRequiredIniLines.phpt create mode 100644 tests/Functional/testEveryTestDeclaresTheTwoRequiredIniLines.phpt diff --git a/AGENTS.md b/AGENTS.md index d86fddf..e2ff62f 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -329,9 +329,16 @@ as permission to run Layer 2 or parallel code against a mismatched tree. - `ffi.enable=1` — cannot be turned on at runtime. - `opcache.jit=off` — the JIT rewrites the very executor internals the engine hooks depend on. -- `error_reporting=E_ALL & ~E_DEPRECATED` — the z-engine dev lines may report deprecations from - dependency code, and PHPUnit's `.phpt` runner forces `display_errors=1`, so an unsuppressed - deprecation is prepended to a test's captured output and fails an `--EXPECT--` block over noise. + +Those two, and nothing else. A third line, `error_reporting=E_ALL & ~E_DEPRECATED`, used to sit +alongside them: PHPUnit's `.phpt` runner forces `display_errors=1`, so a deprecation raised by the +**dependency** — z-engine, back when it was consumed from a development branch — was prepended to a +test's captured output and failed an `--EXPECT--` block over noise. The stable releases now required +(`~8.4.2 || ~8.5.0`) raise none, and the suppression was hiding **this package's own** deprecations +too: that is how the `SplObjectStorage::contains()` call PHP 8.5 deprecated (#39) sat green through +12 tests. The suite now runs at the runner's default `error_reporting`, so a deprecation from our +code fails a test the day it appears. **Do not re-add a diagnostic filter to a `.phpt`** — the guard +test rejects any `error_reporting` line in an `--INI--` section. ```bash php8.4 -d ffi.enable=1 -d opcache.jit=off vendor/bin/phpunit @@ -360,10 +367,11 @@ version and a minimal reproducer, and report it. The suite is PHPUnit 12 driving `.phpt` files in `tests/Functional/`, one behaviour per file. -- **`--INI--` is mandatory and carries all three lines.** The child processes the runner spawns - inherit nothing by luck. `tests/Functional/testEveryTestDeclaresTheThreeRequiredIniLines.phpt` - scans the whole suite — itself included — and fails naming the file and the missing setting, so a - new test cannot quietly omit one. +- **`--INI--` is mandatory and carries both lines** — `ffi.enable=1` and `opcache.jit=off`, and no + diagnostic filter beside them. The child processes the runner spawns inherit nothing by luck. + `tests/Functional/testEveryTestDeclaresTheTwoRequiredIniLines.phpt` scans the whole suite — itself + included — and fails naming the file and the missing setting, so a new test cannot quietly omit + one; it fails the same way on an `error_reporting` line that would silence a real bug. - **`test.phpt`**, and the `--TEST--` line says the behaviour, not the class. - **Prefer `--EXPECT--`** (exact match). Tests about errors `echo` the caught message rather than `var_dump()`ing it, so no string lengths need maintaining. diff --git a/tests/Functional/testABufferedChannelParksOnlyAtItsCapacity.phpt b/tests/Functional/testABufferedChannelParksOnlyAtItsCapacity.phpt index f5b5d94..d8fb08f 100644 --- a/tests/Functional/testABufferedChannelParksOnlyAtItsCapacity.phpt +++ b/tests/Functional/testABufferedChannelParksOnlyAtItsCapacity.phpt @@ -3,7 +3,6 @@ A buffered send parks only when the buffer is full, and a receive only when it i --INI-- ffi.enable=1 opcache.jit=off -error_reporting=E_ALL & ~E_DEPRECATED --FILE-- ---EXPECT-- -every .phpt declares all three required --INI-- lines diff --git a/tests/Functional/testEveryTestDeclaresTheTwoRequiredIniLines.phpt b/tests/Functional/testEveryTestDeclaresTheTwoRequiredIniLines.phpt new file mode 100644 index 0000000..acf98f9 --- /dev/null +++ b/tests/Functional/testEveryTestDeclaresTheTwoRequiredIniLines.phpt @@ -0,0 +1,83 @@ +--TEST-- +Every .phpt in the suite carries the two mandatory --INI-- lines and suppresses no diagnostics +--INI-- +ffi.enable=1 +opcache.jit=off +--FILE-- + +--EXPECT-- +every .phpt declares both required --INI-- lines and filters no diagnostics diff --git a/tests/Functional/testEveryWorkerForksAtStartAndIsReachable.phpt b/tests/Functional/testEveryWorkerForksAtStartAndIsReachable.phpt index 22bb7f0..ceb6b29 100644 --- a/tests/Functional/testEveryWorkerForksAtStartAndIsReachable.phpt +++ b/tests/Functional/testEveryWorkerForksAtStartAndIsReachable.phpt @@ -3,7 +3,6 @@ Every worker is forked eagerly by start() and every one of them can be reached w --INI-- ffi.enable=1 opcache.jit=off -error_reporting=E_ALL & ~E_DEPRECATED --FILE--