├── TODO.md ├── gen_stub.cmd ├── true-async-logo.png ├── internal └── tests │ ├── README.txt │ └── circular_buffer_test.h ├── tests ├── edge_cases │ ├── 011-gc_include_symtable_double_delref_included.inc │ ├── 001-deadlock-basic-test.phpt │ └── 011-gc_include_symtable_double_delref.phpt ├── spawn │ ├── 001-spawn_basic.phpt │ ├── 009-spawn_error.phpt │ ├── 008-spawn_cancellation_exception.phpt │ ├── 002-spawn_multiple_coroutines.phpt │ ├── 003-spawn_with_arguments.phpt │ ├── 007-spawn_exception.phpt │ ├── 004-spawn_return_value.phpt │ ├── 014-spawn-1000.phpt │ ├── 006-spawn_closure_vs_function.phpt │ ├── 010-spawn_fatal_error.phpt │ ├── 015-spawn-1000-with-delay.phpt │ ├── 011-spawn_large_number_coroutines.phpt │ ├── 005-spawn_nested_spawn.phpt │ ├── 012-spawn_error-from-await.phpt │ ├── 013-spawn_in_shutdown.phpt │ ├── 019-spawn_scope_provider_null.phpt │ ├── 018-spawn_scope_provider_exception.phpt │ └── 016-spawn_invalid_scope_provider.phpt ├── protect │ ├── 002-protect_return_value.phpt │ ├── 001-protect_basic.phpt │ ├── 003-protect_nested.phpt │ ├── 008-protect_with_exception.phpt │ ├── 007-protect_exception_in_closure.phpt │ ├── 012-protect_closure_required.phpt │ ├── 004-protect_cancellation_deferred.phpt │ ├── 011-protect_invalid_parameter.phpt │ ├── 009-protect_with_spawn.phpt │ ├── 006-protect_multiple_cancellation.phpt │ ├── 005-protect_cancellation_immediate.phpt │ └── 010-protect_with_await.phpt ├── scope │ ├── 001-scope_construct_basic.phpt │ ├── 005-scope_as_not_safely.phpt │ ├── 003-scope_provide_scope.phpt │ ├── 007-scope_spawn_with_args.phpt │ ├── 012-scope_dispose_after_timeout.phpt │ ├── 004-scope_status_methods.phpt │ ├── 009-scope_dispose_basic.phpt │ ├── 011-scope_on_finally.phpt │ ├── 006-scope_spawn_basic.phpt │ ├── 002-scope_inherit_basic.phpt │ ├── 021-scope_with_wrong_constructor.phpt │ ├── 008-scope_child_scopes.phpt │ ├── 010-scope_exception_handlers.phpt │ ├── 016-scope_onFinally_parameter.phpt │ ├── 014-scope_onFinally_completed.phpt │ ├── 017-scope_onFinally_error.phpt │ ├── 015-scope_onFinally_multiple.phpt │ ├── 013-scope_onFinally_execution.phpt │ ├── 019-scope_gc_basic.phpt │ ├── 020-scope_gc_with_finally.phpt │ └── 035-scope_self_cancellation.phpt ├── coroutine │ ├── 010-coroutine_getAwaitingInfo.phpt │ ├── 012-coroutine_getContext.phpt │ ├── 011-coroutine_asHiPriority.phpt │ ├── 004-coroutine_getException_running.phpt │ ├── 002-coroutine_getResult_basic.phpt │ ├── 015-coroutine_onFinally_finished.phpt │ ├── 001-coroutine_getId_basic.phpt │ ├── 013-coroutine_running_detection.phpt │ ├── 023-coroutine_gc_with_exception.phpt │ ├── 017-coroutine_onFinally_single_exception.phpt │ ├── 006-coroutine_cancel_basic.phpt │ ├── 019-coroutine_gc_basic.phpt │ ├── 008-coroutine_suspend_location.phpt │ ├── 005-coroutine_status_methods.phpt │ ├── 014-coroutine_onFinally_basic.phpt │ ├── 003-coroutine_getException_basic.phpt │ ├── 016-coroutine_onFinally_multiple.phpt │ ├── 007-coroutine_spawn_location.phpt │ ├── 022-coroutine_gc_suspended.phpt │ ├── 020-coroutine_gc_with_finally.phpt │ ├── 025-coroutine_gc_waker_scope.phpt │ ├── 035-coroutine_deep_recursion.phpt │ ├── 024-coroutine_gc_multiple_zvals.phpt │ ├── 021-coroutine_gc_with_context.phpt │ ├── 036-coroutine_gc_circular_finally.phpt │ └── 009-coroutine_getTrace.phpt ├── await │ ├── 006-awaitAnyOrFail_empty.phpt │ ├── 004-await_null_result.phpt │ ├── 001-await_basic.phpt │ ├── 015-awaitAnyOfOrFail_count_zero.phpt │ ├── 003-await_exception.phpt │ ├── 071-awaitAll_with_cancellation_simultaneously.phpt │ ├── 002-await_timeout.phpt │ ├── 010-awaitAllOrFail_basic.phpt │ ├── 005-awaitAnyOrFail_basic.phpt │ ├── 008-awaitFirstSuccess_basic.phpt │ ├── 007-awaitAnyOrFail_exception.phpt │ ├── 027-awaitFirstSuccess_generator.phpt │ ├── 026-awaitAnyOrFail_generator.phpt │ ├── 011-awaitAllOrFail_exception.phpt │ ├── 013-awaitAll_all_success.phpt │ ├── 040-await_cancellation_timeout.phpt │ ├── 014-awaitAnyOfOrFail_basic.phpt │ ├── 072-awaitAll_with_simultaneously.phpt │ ├── 025-awaitAllOrFail_generator.phpt │ ├── 017-awaitAnyOf_all_success.phpt │ ├── 050-awaitFirstSuccess_concurrent_generator.phpt │ ├── 029-awaitAnyOfOrFail_generator.phpt │ ├── 046-awaitFirstSuccess_all_errors.phpt │ ├── 028-awaitAll_generator.phpt │ ├── 038-awaitFirstSuccess_cancellation_timeout.phpt │ ├── 016-awaitAnyOf_basic.phpt │ ├── 037-awaitAnyOrFail_cancellation_timeout.phpt │ ├── 041-awaitAllOrFail_associative_array.phpt │ ├── 036-awaitAllOrFail_cancellation_timeout.phpt │ ├── 054-awaitAllOrFail_generator_exception.phpt │ ├── 030-awaitAnyOf_generator.phpt │ ├── 039-awaitAnyOfOrFail_cancellation_timeout.phpt │ ├── 034-awaitAllOrFail_preserve_key_order.phpt │ ├── 035-awaitAll_fillNull.phpt │ ├── 045-awaitAnyOfOrFail_edge_cases.phpt │ ├── 055-awaitAnyOrFail_generator_exception.phpt │ ├── 018-awaitAllOrFail_double_free.phpt │ ├── 044-awaitAllOrFail_empty_iterable.phpt │ ├── 048-awaitAllOrFail_concurrent_generator.phpt │ ├── 056-awaitFirstSuccess_generator_exception.phpt │ ├── 012-awaitAll_basic.phpt │ ├── 043-awaitAnyOfOrFail_associative_array.phpt │ ├── 042-awaitAll_associative_array.phpt │ └── 060-await_empty_iterable_edge_cases.phpt ├── suspend │ ├── 003-suspend_without_coroutines.phpt │ ├── 001-suspend_basic.phpt │ ├── 004-suspend_multiple_calls.phpt │ ├── 007-suspend_with_exception.phpt │ ├── 002-suspend_execution_order.phpt │ ├── 006-suspend_with_nested_functions.phpt │ └── 005-suspend_with_spawn.phpt ├── spawnWith │ ├── 002-spawnWith_with_arguments.phpt │ ├── 003-spawnWith_return_coroutine.phpt │ ├── 001-spawnWith_basic.phpt │ ├── 006-spawnWith_inherited_scope.phpt │ ├── 011-spawnWith_missing_parameters.phpt │ ├── 005-spawnWith_null_scope_provider.phpt │ ├── 010-spawnWith_invalid_provider.phpt │ ├── 012-spawnWith_invalid_callable.phpt │ └── 004-spawnWith_custom_scope_provider.phpt ├── dns │ ├── 014-dns_check_record.phpt │ ├── 003-gethostbynamel_basic.phpt │ ├── 001-gethostbyname_basic.phpt │ ├── 015-dns_ipv6.phpt │ ├── 002-gethostbyaddr_basic.phpt │ └── 013-dns_unix_specific.phpt ├── fiber │ ├── 013-fiber_getReturn.phpt │ ├── 002-fiber_simple_return.phpt │ ├── 008-fiber_exception_natural.phpt │ ├── 014-fiber_gc_suspended.phpt │ ├── 019-fiber_getCoroutine.phpt │ ├── 026-fiber_getCoroutine_after_termination.phpt │ ├── 017-fiber_suspend_resume_multiple.phpt │ ├── 003-fiber_one_suspend_resume.phpt │ ├── 028-fiber_exception_vs_cancel.phpt │ ├── 010-fiber_exception_propagation.phpt │ ├── 001-fiber_with_coroutine_basic.phpt │ ├── 009-fiber_throw_method.phpt │ ├── 024-fiber_cancel_during_suspend.phpt │ ├── 015-fiber_null_values.phpt │ ├── 004-fiber_multiple_suspends.phpt │ ├── 025-fiber_double_cancel.phpt │ ├── 005-fiber_nested_spawn.phpt │ ├── 011-fiber_exception_nested_spawn.phpt │ ├── 023-fiber_cancel_via_coroutine.phpt │ ├── 006-fiber_nested_fibers.phpt │ ├── 027-fiber_cancel_in_nested_spawn.phpt │ ├── 018-fiber_coroutine_gc_cycles.phpt │ ├── 007-fiber_concurrent_fibers.phpt │ └── 016-fiber_nested_in_separate_coroutines.phpt ├── stream │ ├── 018-stream_select_empty_arrays.phpt │ ├── 017-stream_select_invalid_streams.phpt │ ├── 016-tcp_stream_socket_accept_timeout.phpt │ ├── 001-fread_fwrite_simple.phpt │ ├── ssl_test_cert.pem │ ├── 002-fwrite_simple.phpt │ ├── 008-socket_stream_basic.phpt │ ├── 003-file_get_contents_http.phpt │ ├── tcp_client_disconnect.php │ ├── 019-stream_select_closed_streams.phpt │ ├── 022-stream_select_write_ready.phpt │ ├── 021-stream_select_microsecond_timeout.phpt │ └── 020-stream_select_zero_timeout.phpt ├── output_buffer │ ├── 003-nested_ob_start.phpt │ ├── 001-ob_start_basic_isolation.phpt │ ├── 006-exception_handling.phpt │ ├── 004-ob_flush_isolation.phpt │ ├── 005-mixed_buffering.phpt │ └── 002-multiple_coroutines_isolation.phpt ├── gc │ ├── 006-gc_destructor_simple.phpt │ ├── 013-gc-fiber-destructors.phpt │ └── 014-gc-after-excaption-in-main.phpt ├── sleep │ ├── 001-sleep_basic.phpt │ ├── 002-usleep_basic.phpt │ ├── 004-time_sleep_until_basic.phpt │ └── 003-time_nanosleep_basic.phpt ├── bailout │ ├── 001-memory-exhaustion-simple.phpt │ ├── 003-stack-overflow-simple.phpt │ ├── 009-memory-exhaustion-during-await.phpt │ ├── 002-memory-exhaustion-nested.phpt │ ├── 005-memory-exhaustion-during-suspend.phpt │ ├── 008-memory-exhaustion-in-shutdown.phpt │ ├── 011-stack-overflow-during-await.phpt │ ├── 004-stack-overflow-nested.phpt │ ├── 014-stack-overflow-with-finally.phpt │ ├── 007-stack-overflow-with-suspend.phpt │ └── 013-memory-exhaustion-with-finally.phpt ├── mysqli │ └── inc │ │ └── config.inc ├── pdo_mysql │ ├── inc │ │ └── config.inc │ └── 001-pdo_connection_basic.phpt ├── exec │ ├── 005-shell_exec_basic.phpt │ ├── 009-passthru_basic.phpt │ ├── 004-exec_basic.phpt │ └── 008-system_basic.phpt └── context │ └── README.md ├── .github ├── scripts │ └── windows │ │ ├── find-target-branch.bat │ │ ├── publish.bat │ │ ├── test.bat │ │ └── find-vs-toolset.bat └── actions │ └── setup-windows │ └── action.yml ├── run-tests.cmd ├── benchmarks └── amphp │ └── composer.json ├── run-tests.sh ├── .gitignore └── context.stub.php /TODO.md: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /gen_stub.cmd: -------------------------------------------------------------------------------- 1 | php ..\..\build\gen_stub.php -------------------------------------------------------------------------------- /true-async-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/true-async/php-async/HEAD/true-async-logo.png -------------------------------------------------------------------------------- /internal/tests/README.txt: -------------------------------------------------------------------------------- 1 | # Build 2 | 3 | ```bash 4 | mkdir build 5 | cmake -S . -B build 6 | cmake --build build 7 | ``` 8 | 9 | -------------------------------------------------------------------------------- /tests/edge_cases/011-gc_include_symtable_double_delref_included.inc: -------------------------------------------------------------------------------- 1 | 16 | --EXPECT-- 17 | start 18 | end 19 | coroutine -------------------------------------------------------------------------------- /tests/protect/002-protect_return_value.phpt: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | Async\protect: should return a value 3 | --FILE-- 4 | 15 | --EXPECT-- 16 | string(10) "test value" -------------------------------------------------------------------------------- /tests/scope/001-scope_construct_basic.phpt: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | Scope: __construct() - basic usage 3 | --FILE-- 4 | 13 | --EXPECT-- 14 | bool(true) 15 | bool(true) -------------------------------------------------------------------------------- /tests/protect/001-protect_basic.phpt: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | Async\protect: basic usage 3 | --FILE-- 4 | 17 | --EXPECT-- 18 | start 19 | protected block 20 | end -------------------------------------------------------------------------------- /.github/scripts/windows/find-target-branch.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | 3 | for /f "usebackq tokens=3" %%i in (`findstr PHP_MAJOR_VERSION main\php_version.h`) do set BRANCH=%%i 4 | for /f "usebackq tokens=3" %%i in (`findstr PHP_MINOR_VERSION main\php_version.h`) do set BRANCH=%BRANCH%.%%i 5 | 6 | if /i "%BRANCH%" equ "8.5" ( 7 | set BRANCH=master 8 | ) 9 | -------------------------------------------------------------------------------- /tests/scope/005-scope_as_not_safely.phpt: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | Scope: asNotSafely() - basic usage 3 | --FILE-- 4 | asNotSafely(); 10 | 11 | var_dump($notSafeScope === $scope); 12 | var_dump($notSafeScope instanceof Scope); 13 | 14 | ?> 15 | --EXPECT-- 16 | bool(true) 17 | bool(true) -------------------------------------------------------------------------------- /tests/coroutine/010-coroutine_getAwaitingInfo.phpt: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | Coroutine: getAwaitingInfo() - basic usage 3 | --FILE-- 4 | getAwaitingInfo(); 13 | 14 | var_dump(is_array($info)); 15 | 16 | ?> 17 | --EXPECT-- 18 | bool(true) -------------------------------------------------------------------------------- /tests/scope/003-scope_provide_scope.phpt: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | Scope: provideScope() - basic usage 3 | --FILE-- 4 | provideScope(); 10 | 11 | var_dump($providedScope === $scope); 12 | var_dump($providedScope instanceof Scope); 13 | 14 | ?> 15 | --EXPECT-- 16 | bool(true) 17 | bool(true) -------------------------------------------------------------------------------- /tests/scope/007-scope_spawn_with_args.phpt: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | Scope: spawn() - with arguments 3 | --FILE-- 4 | spawn(function($a, $b) { 11 | return $a + $b; 12 | }, 10, 20); 13 | 14 | var_dump($coroutine instanceof Async\Coroutine); 15 | 16 | ?> 17 | --EXPECT-- 18 | bool(true) -------------------------------------------------------------------------------- /tests/scope/012-scope_dispose_after_timeout.phpt: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | Scope: disposeAfterTimeout() - basic usage 3 | --FILE-- 4 | disposeAfterTimeout(1000); 11 | 12 | echo "Dispose after timeout executed successfully\n"; 13 | 14 | ?> 15 | --EXPECT-- 16 | Dispose after timeout executed successfully -------------------------------------------------------------------------------- /tests/coroutine/012-coroutine_getContext.phpt: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | Coroutine: getContext() - returns the context of the coroutine 3 | --FILE-- 4 | getContext(); 13 | 14 | var_dump($context); 15 | 16 | ?> 17 | --EXPECTF-- 18 | object(Async\Context)%a -------------------------------------------------------------------------------- /run-tests.cmd: -------------------------------------------------------------------------------- 1 | @echo off 2 | setlocal 3 | 4 | set BASE_PATH=%~dp0tests 5 | set RUN_TESTS_PATH=%~dp0..\..\run-tests.php 6 | set PHP_EXECUTABLE=%~dp0..\..\x64\Debug_TS\php.exe 7 | 8 | if "%~1"=="" ( 9 | set TEST_PATH=%BASE_PATH% 10 | ) else ( 11 | set TEST_PATH=%BASE_PATH%\%~1 12 | ) 13 | 14 | php.exe "%RUN_TESTS_PATH%" --show-diff -p "%PHP_EXECUTABLE%" "%TEST_PATH%" 15 | 16 | endlocal -------------------------------------------------------------------------------- /tests/scope/004-scope_status_methods.phpt: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | Scope: isFinished() and isClosed() - basic usage 3 | --FILE-- 4 | isFinished()); 11 | var_dump($scope->isClosed()); 12 | 13 | $scope->cancel(); 14 | 15 | var_dump($scope->isClosed()); 16 | 17 | ?> 18 | --EXPECT-- 19 | bool(true) 20 | bool(false) 21 | bool(true) -------------------------------------------------------------------------------- /tests/scope/009-scope_dispose_basic.phpt: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | Scope: dispose() and disposeSafely() - basic usage 3 | --FILE-- 4 | dispose(); 10 | 11 | $scope2 = new Scope(); 12 | $scope2->disposeSafely(); 13 | 14 | echo "Dispose methods executed without errors\n"; 15 | 16 | ?> 17 | --EXPECT-- 18 | Dispose methods executed without errors -------------------------------------------------------------------------------- /tests/scope/011-scope_on_finally.phpt: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | Scope: onFinally() - basic usage 3 | --FILE-- 4 | onFinally(function() { 11 | echo "Finally callback executed\n"; 12 | }); 13 | 14 | echo "Finally callback set successfully\n"; 15 | 16 | ?> 17 | --EXPECT-- 18 | Finally callback set successfully 19 | Finally callback executed -------------------------------------------------------------------------------- /tests/coroutine/011-coroutine_asHiPriority.phpt: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | Coroutine: asHiPriority() - returns same coroutine (TODO implementation) 3 | --FILE-- 4 | asHiPriority(); 13 | 14 | var_dump($coroutine === $hiPriorityCoroutine); 15 | 16 | ?> 17 | --EXPECT-- 18 | bool(true) -------------------------------------------------------------------------------- /tests/scope/006-scope_spawn_basic.phpt: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | Scope: spawn() - basic usage 3 | --FILE-- 4 | spawn(function() { 12 | return "test result"; 13 | }); 14 | 15 | var_dump($coroutine instanceof Coroutine); 16 | var_dump(is_int($coroutine->getId())); 17 | 18 | ?> 19 | --EXPECT-- 20 | bool(true) 21 | bool(true) -------------------------------------------------------------------------------- /tests/spawn/009-spawn_error.phpt: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | Future: spawn() - error handling in coroutine 3 | --FILE-- 4 | 17 | --EXPECTF-- 18 | start 19 | end 20 | coroutine start 21 | 22 | Warning: Test error in %s on line %d -------------------------------------------------------------------------------- /tests/await/006-awaitAnyOrFail_empty.phpt: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | await_any_or_fail() - empty iterable 3 | --FILE-- 4 | 17 | --EXPECT-- 18 | start 19 | Result is null: OK 20 | end -------------------------------------------------------------------------------- /tests/await/004-await_null_result.phpt: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | await() - coroutine returns null 3 | --FILE-- 4 | 21 | --EXPECT-- 22 | start 23 | coroutine running 24 | NULL 25 | end -------------------------------------------------------------------------------- /tests/scope/002-scope_inherit_basic.phpt: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | Scope: inherit() - basic usage 3 | --FILE-- 4 | 18 | --EXPECT-- 19 | bool(true) 20 | bool(true) 21 | bool(true) -------------------------------------------------------------------------------- /tests/coroutine/004-coroutine_getException_running.phpt: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | Coroutine: getException() - throws Async\AsyncException if running 3 | --FILE-- 4 | getException() === null) { 13 | echo "No exception\n"; 14 | } else { 15 | echo "Exception: " . get_class($coroutine->getException()) . "\n"; 16 | } 17 | 18 | ?> 19 | --EXPECT-- 20 | No exception -------------------------------------------------------------------------------- /tests/spawn/008-spawn_cancellation_exception.phpt: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | Future: spawn() - CancellationError handling (special case) 3 | --FILE-- 4 | 19 | --EXPECT-- 20 | start 21 | end 22 | coroutine start -------------------------------------------------------------------------------- /tests/await/001-await_basic.phpt: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | await() - basic usage with coroutine 3 | --FILE-- 4 | 21 | --EXPECT-- 22 | start 23 | coroutine running 24 | awaited result: result 25 | end -------------------------------------------------------------------------------- /tests/coroutine/002-coroutine_getResult_basic.phpt: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | Coroutine: getResult() - basic usage 3 | --FILE-- 4 | getResult()); 15 | 16 | // After completion 17 | await($coroutine); 18 | var_dump($coroutine->getResult()); 19 | 20 | ?> 21 | --EXPECT-- 22 | NULL 23 | string(11) "test result" -------------------------------------------------------------------------------- /benchmarks/amphp/composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "async-vs-amphp/benchmark", 3 | "description": "Performance comparison between async extension and AmphpPHP", 4 | "type": "project", 5 | "require": { 6 | "php": "^8.1", 7 | "amphp/http-server": "^3.0", 8 | "amphp/socket": "^2.0" 9 | }, 10 | "autoload": { 11 | "psr-4": { 12 | "AsyncBench\\": "src/" 13 | } 14 | }, 15 | "config": { 16 | "optimize-autoloader": true 17 | } 18 | } -------------------------------------------------------------------------------- /tests/suspend/003-suspend_without_coroutines.phpt: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | Suspend without coroutines - test optimization path when no coroutines exist 3 | --FILE-- 4 | 17 | --EXPECT-- 18 | Before suspend call 19 | After suspend call -------------------------------------------------------------------------------- /tests/coroutine/015-coroutine_onFinally_finished.phpt: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | Coroutine: onFinally() - call when coroutine is already finished 3 | --FILE-- 4 | onFinally(function() { 16 | echo "Finally called\n"; 17 | }); 18 | 19 | ?> 20 | --EXPECT-- 21 | Coroutine returned: test 22 | Finally called -------------------------------------------------------------------------------- /tests/spawn/002-spawn_multiple_coroutines.phpt: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | Future: spawn() - multiple coroutines execution order 3 | --FILE-- 4 | 24 | --EXPECT-- 25 | start 26 | end 27 | coroutine 1 28 | coroutine 2 29 | coroutine 3 -------------------------------------------------------------------------------- /tests/protect/003-protect_nested.phpt: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | Async\protect: nested protect calls 3 | --FILE-- 4 | 23 | --EXPECT-- 24 | start 25 | outer protect start 26 | inner protect 27 | outer protect end 28 | end -------------------------------------------------------------------------------- /tests/scope/021-scope_with_wrong_constructor.phpt: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | Scope: GC handler with context data 3 | --FILE-- 4 | 17 | --EXPECTF-- 18 | Fatal error: Uncaught ArgumentCountError: %s 19 | %a -------------------------------------------------------------------------------- /.github/scripts/windows/publish.bat: -------------------------------------------------------------------------------- 1 | REM Clean release folder if exists 2 | IF EXIST C:\php-release rmdir /S /Q C:\php-release 3 | 4 | REM Create release folder 5 | mkdir C:\php-release 6 | 7 | REM Copy php.exe 8 | copy C:\obj\Release_TS\php.exe C:\php-release\ 9 | 10 | REM Copy ini files 11 | copy C:\obj\Release_TS\php.ini-* C:\php-release\ 12 | 13 | REM Copy all DLLs 14 | xcopy /E /I /H /Y C:\obj\Release_TS\*.dll C:\php-release\ 15 | 16 | REM Copy ext directory 17 | xcopy /E /I /H /Y C:\obj\Release_TS\ext C:\php-release\ext\ -------------------------------------------------------------------------------- /tests/coroutine/001-coroutine_getId_basic.phpt: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | Coroutine: getId() - basic usage 3 | --FILE-- 4 | getId(); 17 | $id2 = $coroutine2->getId(); 18 | 19 | var_dump(is_int($id1)); 20 | var_dump(is_int($id2)); 21 | var_dump($id1 !== $id2); 22 | 23 | ?> 24 | --EXPECT-- 25 | bool(true) 26 | bool(true) 27 | bool(true) -------------------------------------------------------------------------------- /tests/spawn/003-spawn_with_arguments.phpt: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | Future: spawn() - coroutine with arguments 3 | --FILE-- 4 | 20 | --EXPECT-- 21 | start 22 | end 23 | coroutine: hello, 42, 1 24 | variadic: arg1, arg2, arg3 -------------------------------------------------------------------------------- /tests/spawn/007-spawn_exception.phpt: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | Future: spawn() - regular exception handling in coroutine 3 | --FILE-- 4 | 18 | --EXPECTF-- 19 | start 20 | end 21 | coroutine start 22 | 23 | Fatal error: Uncaught Exception: Test exception in %s:%d 24 | Stack trace: 25 | %a -------------------------------------------------------------------------------- /tests/spawn/004-spawn_return_value.phpt: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | Future: spawn() - returns Coroutine object 3 | --FILE-- 4 | 21 | --EXPECT-- 22 | start 23 | bool(true) 24 | string(15) "Async\Coroutine" 25 | end 26 | coroutine executed -------------------------------------------------------------------------------- /tests/spawn/014-spawn-1000.phpt: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | Spawn 1000 coroutines return values 3 | --FILE-- 4 | 23 | --EXPECT-- 24 | start 25 | end -------------------------------------------------------------------------------- /tests/spawn/006-spawn_closure_vs_function.phpt: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | Future: spawn() - closure vs function name 3 | --FILE-- 4 | 24 | --EXPECTF-- 25 | start 26 | end 27 | closure executed 28 | named function executed -------------------------------------------------------------------------------- /tests/protect/008-protect_with_exception.phpt: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | Async\protect: exception handling in protected closure 3 | --FILE-- 4 | getMessage() . "\n"; 17 | } 18 | 19 | echo "end\n"; 20 | 21 | ?> 22 | --EXPECT-- 23 | start 24 | protected closure 25 | caught exception: Exception 26 | end -------------------------------------------------------------------------------- /.github/scripts/windows/test.bat: -------------------------------------------------------------------------------- 1 | if /i "%GITHUB_ACTIONS%" neq "True" ( 2 | echo for CI only 3 | exit /b 3 4 | ) 5 | 6 | set SDK_RUNNER=%PHP_BUILD_CACHE_SDK_DIR%\phpsdk-%PHP_BUILD_CRT%-%PLATFORM%.bat 7 | if not exist "%SDK_RUNNER%" ( 8 | echo "%SDK_RUNNER%" doesn't exist 9 | exit /b 3 10 | ) 11 | 12 | for /f "delims=" %%T in ('call .github\scripts\windows\find-vs-toolset.bat %PHP_BUILD_CRT%') do set "VS_TOOLSET=%%T" 13 | cmd /c %SDK_RUNNER% -s %VS_TOOLSET% -t .github\scripts\windows\test_task.bat 14 | if %errorlevel% neq 0 exit /b 3 15 | 16 | exit /b 0 17 | -------------------------------------------------------------------------------- /tests/await/015-awaitAnyOfOrFail_count_zero.phpt: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | await_any_of_or_fail() - count is zero 3 | --FILE-- 4 | 25 | --EXPECT-- 26 | start 27 | array(0) { 28 | } 29 | end -------------------------------------------------------------------------------- /tests/scope/008-scope_child_scopes.phpt: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | Scope: getChildScopes() - basic usage 3 | --FILE-- 4 | getChildScopes(); 13 | 14 | var_dump(is_array($children)); 15 | var_dump(count($children)); 16 | 17 | foreach ($children as $child) { 18 | var_dump($child instanceof Scope); 19 | } 20 | 21 | ?> 22 | --EXPECT-- 23 | bool(true) 24 | int(2) 25 | bool(true) 26 | bool(true) -------------------------------------------------------------------------------- /tests/spawn/010-spawn_fatal_error.phpt: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | Future: spawn() - fatal error handling in coroutine 3 | --FILE-- 4 | undefinedMethod(); 14 | echo "coroutine end (should not print)\n"; 15 | }); 16 | 17 | echo "end\n"; 18 | ?> 19 | --EXPECTF-- 20 | start 21 | end 22 | coroutine start 23 | 24 | Fatal error: Uncaught Error: Call to undefined method stdClass::undefinedMethod() in %s:%d 25 | Stack trace: 26 | %a -------------------------------------------------------------------------------- /tests/spawn/015-spawn-1000-with-delay.phpt: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | Spawn 1000 coroutines with delayed return values 3 | --FILE-- 4 | 24 | --EXPECT-- 25 | start 26 | end -------------------------------------------------------------------------------- /tests/spawn/011-spawn_large_number_coroutines.phpt: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | Future: spawn() - large number of coroutines 3 | --FILE-- 4 | 22 | --EXPECT-- 23 | start 24 | end 25 | coroutine 0 26 | coroutine 20 27 | coroutine 40 28 | coroutine 60 29 | coroutine 80 -------------------------------------------------------------------------------- /tests/spawnWith/002-spawnWith_with_arguments.phpt: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | Async\spawnWith: with arguments 3 | --FILE-- 4 | 25 | --EXPECT-- 26 | start 27 | arguments: 10, 20, 30 28 | result: 60 29 | end -------------------------------------------------------------------------------- /tests/spawnWith/003-spawnWith_return_coroutine.phpt: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | Async\spawnWith: returns Coroutine instance 3 | --FILE-- 4 | getId())); 19 | 20 | $result = await($coroutine); 21 | var_dump($result); 22 | 23 | ?> 24 | --EXPECT-- 25 | bool(true) 26 | bool(true) 27 | string(4) "test" -------------------------------------------------------------------------------- /tests/scope/010-scope_exception_handlers.phpt: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | Scope: setExceptionHandler() and setChildScopeExceptionHandler() - basic usage 3 | --FILE-- 4 | setExceptionHandler(function($exception) { 11 | echo "Exception handled: " . $exception->getMessage() . "\n"; 12 | }); 13 | 14 | $scope->setChildScopeExceptionHandler(function($exception) { 15 | echo "Child scope exception handled: " . $exception->getMessage() . "\n"; 16 | }); 17 | 18 | echo "Exception handlers set successfully\n"; 19 | 20 | ?> 21 | --EXPECT-- 22 | Exception handlers set successfully -------------------------------------------------------------------------------- /tests/scope/016-scope_onFinally_parameter.phpt: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | Scope: onFinally() - finally handler receives scope parameter 3 | --FILE-- 4 | spawn(function() { 11 | return "test"; 12 | }); 13 | 14 | $scope->onFinally(function($receivedScope) use ($scope) { 15 | echo "Finally handler received scope: " . 16 | ($receivedScope === $scope ? "correct" : "incorrect") . "\n"; 17 | }); 18 | 19 | await($coroutine); 20 | $scope->dispose(); 21 | 22 | ?> 23 | --EXPECT-- 24 | Finally handler received scope: correct -------------------------------------------------------------------------------- /tests/spawn/005-spawn_nested_spawn.phpt: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | Future: spawn() - nested spawns 3 | --FILE-- 4 | 26 | --EXPECT-- 27 | start 28 | end 29 | outer coroutine start 30 | outer coroutine end 31 | inner coroutine 1 32 | inner coroutine 2 -------------------------------------------------------------------------------- /tests/spawnWith/001-spawnWith_basic.phpt: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | Async\spawnWith: basic usage with Scope 3 | --FILE-- 4 | 27 | --EXPECT-- 28 | start 29 | spawned coroutine 30 | coroutine executed 31 | result: test result 32 | end -------------------------------------------------------------------------------- /tests/protect/007-protect_exception_in_closure.phpt: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | Async\protect: exception thrown inside protected closure 3 | --FILE-- 4 | getMessage() . "\n"; 18 | } 19 | 20 | echo "end\n"; 21 | 22 | ?> 23 | --EXPECT-- 24 | start 25 | before exception 26 | caught exception: test exception 27 | end -------------------------------------------------------------------------------- /tests/await/003-await_exception.phpt: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | await() - coroutine throws exception 3 | --FILE-- 4 | getMessage() . "\n"; 21 | } 22 | 23 | echo "end\n"; 24 | ?> 25 | --EXPECT-- 26 | start 27 | coroutine running 28 | caught exception: test exception 29 | end -------------------------------------------------------------------------------- /tests/await/071-awaitAll_with_cancellation_simultaneously.phpt: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | await_all() - The object used to cancel the wait is simultaneously the object being awaited. 3 | --FILE-- 4 | 27 | --EXPECTF-- 28 | start 29 | end -------------------------------------------------------------------------------- /tests/scope/014-scope_onFinally_completed.phpt: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | Scope: onFinally() - call when scope is already completed 3 | --FILE-- 4 | spawn(function() { 11 | return "test"; 12 | }); 13 | 14 | await($coroutine); 15 | $scope->dispose(); 16 | 17 | echo "Coroutine completed\n"; 18 | 19 | // Add finally handler to completed scope - should execute immediately 20 | $scope->onFinally(function() { 21 | echo "Finally called on completed scope\n"; 22 | }); 23 | 24 | ?> 25 | --EXPECT-- 26 | Coroutine completed 27 | Finally called on completed scope -------------------------------------------------------------------------------- /tests/spawn/012-spawn_error-from-await.phpt: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | Future: spawn() - exception handling in coroutine with await 3 | --FILE-- 4 | throw new \Exception('error')); 14 | }); 15 | 16 | echo "end\n"; 17 | ?> 18 | --EXPECTF-- 19 | start 20 | end 21 | coroutine start 22 | 23 | Fatal error: Uncaught Exception: error in %s012-spawn_error-from-await.php:%d 24 | Stack trace: 25 | #0 [internal function]: {closure:%s:%d}() 26 | #1 {main} 27 | thrown in %s012-spawn_error-from-await.php on line %d -------------------------------------------------------------------------------- /run-tests.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | BASE_PATH="$(cd "$(dirname "$0")/tests" && pwd)" 4 | RUN_TESTS_PATH="$(cd "$(dirname "$0")/../../" && pwd)/run-tests.php" 5 | PHP_EXECUTABLE="$(cd "$(dirname "$0")/../../" && pwd)/sapi/cli/php" 6 | export VALGRIND_OPTS="--leak-check=full --track-origins=yes" 7 | export MYSQL_TEST_HOST="127.0.0.1" 8 | export MYSQL_TEST_PORT="3306" 9 | export MYSQL_TEST_USER="root" 10 | export MYSQL_TEST_PASSWD="root" 11 | export MYSQL_TEST_DB="php_test" 12 | 13 | if [ -z "$1" ]; then 14 | TEST_PATH="$BASE_PATH" 15 | else 16 | TEST_PATH="$BASE_PATH/$1" 17 | fi 18 | 19 | "$PHP_EXECUTABLE" "$RUN_TESTS_PATH" --show-diff -p "$PHP_EXECUTABLE" "$TEST_PATH" 20 | -------------------------------------------------------------------------------- /tests/suspend/001-suspend_basic.phpt: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | Basic suspend functionality - verify suspend yields control properly 3 | --FILE-- 4 | 25 | --EXPECT-- 26 | Before spawn 27 | After spawn 28 | Coroutine 1 start 29 | Coroutine 2 start 30 | Coroutine 2 end 31 | Coroutine 1 after suspend -------------------------------------------------------------------------------- /tests/scope/017-scope_onFinally_error.phpt: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | Scope onFinally single exception handling 3 | --FILE-- 4 | setExceptionHandler(function($scope, $coroutine, $exception) { 11 | echo "Caught single exception: " . $exception->getMessage() . "\n"; 12 | }); 13 | 14 | $coro = $scope->spawn(function() { 15 | return "result"; 16 | }); 17 | 18 | // Add single finally handler that will throw exception 19 | $scope->onFinally(function() { 20 | throw new Exception("Single exception"); 21 | }); 22 | 23 | $scope->dispose(); 24 | 25 | ?> 26 | --EXPECT-- 27 | Caught single exception: Single exception -------------------------------------------------------------------------------- /tests/spawnWith/006-spawnWith_inherited_scope.phpt: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | Async\spawnWith: with inherited scope 3 | --FILE-- 4 | 26 | --EXPECT-- 27 | start 28 | coroutine in child scope 29 | result: inherited scope result 30 | end -------------------------------------------------------------------------------- /tests/await/002-await_timeout.phpt: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | await() - with timeout cancellation 3 | --FILE-- 4 | 29 | --EXPECT-- 30 | start 31 | caught timeout exception 32 | end -------------------------------------------------------------------------------- /tests/dns/014-dns_check_record.phpt: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | DNS check record functionality in async context 3 | --SKIPIF-- 4 | 9 | --FILE-- 10 | 25 | --EXPECTF-- 26 | Testing DNS check record 27 | localhost A record exists: %s -------------------------------------------------------------------------------- /tests/coroutine/013-coroutine_running_detection.phpt: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | Coroutine: isRunning() - detects currently executing coroutine 3 | --FILE-- 4 | isRunning(); 14 | return "test"; 15 | }); 16 | 17 | await($coroutine); 18 | 19 | echo "Was running during execution: " . ($isRunning ? "yes" : "no") . "\n"; 20 | echo "Is running after completion: " . ($coroutine->isRunning() ? "yes" : "no") . "\n"; 21 | 22 | ?> 23 | --EXPECT-- 24 | Was running during execution: yes 25 | Is running after completion: no -------------------------------------------------------------------------------- /tests/scope/015-scope_onFinally_multiple.phpt: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | Scope: onFinally() - multiple handlers execution 3 | --FILE-- 4 | spawn(function() { 13 | echo "Spawned coroutine\n"; 14 | }); 15 | 16 | $scope->onFinally(function() { 17 | echo "First finally handler\n"; 18 | }); 19 | 20 | $scope->onFinally(function() { 21 | echo "Second finally handler\n"; 22 | }); 23 | 24 | await($coroutine); 25 | 26 | echo "End of main coroutine\n"; 27 | 28 | ?> 29 | --EXPECT-- 30 | Spawned coroutine 31 | End of main coroutine 32 | First finally handler 33 | Second finally handler -------------------------------------------------------------------------------- /tests/fiber/013-fiber_getReturn.phpt: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | Fiber.getReturn() method 3 | --FILE-- 4 | start(); 18 | $fiber->resume(); 19 | 20 | $result = $fiber->getReturn(); 21 | echo "getReturn: " . $result . "\n"; 22 | 23 | return "done"; 24 | }); 25 | 26 | await($coroutine); 27 | echo "Test completed\n"; 28 | ?> 29 | --EXPECT-- 30 | Test: Fiber getReturn 31 | getReturn: final result 32 | Test completed 33 | -------------------------------------------------------------------------------- /tests/scope/013-scope_onFinally_execution.phpt: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | Scope: onFinally() - basic execution of finally handler 3 | --FILE-- 4 | spawn(function() { 13 | echo "Coroutine started\n"; 14 | }); 15 | 16 | // You should understand that this handler will be invoked in a different coroutine, 17 | // so you cannot rely on the exact timing of when it will happen. 18 | $scope->onFinally(function() { 19 | echo "Finally handler executed\n"; 20 | }); 21 | 22 | await($coroutine); 23 | $scope->dispose(); 24 | 25 | ?> 26 | --EXPECT-- 27 | Coroutine started 28 | Finally handler executed -------------------------------------------------------------------------------- /tests/stream/018-stream_select_empty_arrays.phpt: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | stream_select with empty arrays 3 | --FILE-- 4 | 24 | --EXPECT-- 25 | Testing stream_select with empty arrays 26 | Result: 0 27 | Result: empty arrays test completed -------------------------------------------------------------------------------- /tests/coroutine/023-coroutine_gc_with_exception.phpt: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | Coroutine: GC handler with exception objects 3 | --FILE-- 4 | getMessage()); 23 | } 24 | 25 | // Verify GC was called 26 | var_dump($collected >= 0); 27 | 28 | ?> 29 | --EXPECT-- 30 | string(14) "test_exception" 31 | bool(true) -------------------------------------------------------------------------------- /tests/dns/003-gethostbynamel_basic.phpt: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | DNS gethostbynamel() basic functionality in async context 3 | --FILE-- 4 | 25 | --EXPECTF-- 26 | localhost resolved to %d addresses: 27 | 127.0.0.1%A 28 | bool(false) -------------------------------------------------------------------------------- /tests/protect/012-protect_closure_required.phpt: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | Async\protect: closure parameter is required 3 | --FILE-- 4 | getMessage() . "\n"; 15 | } 16 | 17 | // Test with too many parameters 18 | try { 19 | protect(function() {}, "extra param"); 20 | } catch (ArgumentCountError $e) { 21 | echo "caught ArgumentCountError for too many params\n"; 22 | } 23 | 24 | echo "end\n"; 25 | 26 | ?> 27 | --EXPECTF-- 28 | start 29 | caught ArgumentCountError: %s 30 | caught ArgumentCountError for too many params 31 | end -------------------------------------------------------------------------------- /tests/suspend/004-suspend_multiple_calls.phpt: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | Multiple suspend calls - repeated suspends in same coroutine 3 | --FILE-- 4 | 28 | --EXPECT-- 29 | Start 30 | End 31 | Coroutine: Step 1 32 | Other coroutine: Task 33 | Coroutine: Step 2 34 | Coroutine: Step 3 35 | Coroutine: Step 4 -------------------------------------------------------------------------------- /tests/fiber/002-fiber_simple_return.phpt: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | Fiber with simple return without suspend 3 | --FILE-- 4 | start(); 18 | echo "Got: " . $result . "\n"; 19 | 20 | return "coroutine done"; 21 | }); 22 | 23 | await($coroutine); 24 | echo "Test completed\n"; 25 | ?> 26 | --EXPECT-- 27 | Test: Fiber simple return without suspend 28 | Fiber executing 29 | Got: result from fiber 30 | Test completed 31 | -------------------------------------------------------------------------------- /tests/coroutine/017-coroutine_onFinally_single_exception.phpt: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | Coroutine onFinally single exception handling 3 | --FILE-- 4 | setExceptionHandler(function($scope, $coroutine, $exception) { 13 | echo "Caught single exception: " . $exception->getMessage() . "\n"; 14 | }); 15 | 16 | $coro = $scope->spawn(function() { 17 | return "result"; 18 | }); 19 | 20 | // Add single finally handler that will throw exception 21 | $coro->onFinally(function($coroutine) { 22 | throw new Exception("Single exception"); 23 | }); 24 | 25 | ?> 26 | --EXPECT-- 27 | Caught single exception: Single exception -------------------------------------------------------------------------------- /tests/await/010-awaitAllOrFail_basic.phpt: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | await_all_or_fail() - basic usage with multiple coroutines 3 | --FILE-- 4 | 28 | --EXPECT-- 29 | start 30 | array(3) { 31 | [0]=> 32 | string(5) "first" 33 | [1]=> 34 | string(6) "second" 35 | [2]=> 36 | string(5) "third" 37 | } 38 | end -------------------------------------------------------------------------------- /tests/coroutine/006-coroutine_cancel_basic.phpt: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | Coroutine: cancel() - basic usage with CancellationError 3 | --FILE-- 4 | cancel($cancellation); 16 | 17 | var_dump($coroutine->isCancellationRequested()); 18 | var_dump($coroutine->isCancelled()); 19 | 20 | try { 21 | await($coroutine); 22 | } catch (CancellationError $e) { 23 | echo "Caught: " . $e->getMessage() . "\n"; 24 | } 25 | 26 | ?> 27 | --EXPECT-- 28 | bool(true) 29 | bool(false) 30 | Caught: test cancellation -------------------------------------------------------------------------------- /tests/spawnWith/011-spawnWith_missing_parameters.phpt: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | Async\spawnWith: missing required parameters 3 | --FILE-- 4 | getMessage() . "\n"; 15 | } 16 | 17 | // Test with only provider 18 | try { 19 | spawn_with(new Async\Scope()); 20 | } catch (ArgumentCountError $e) { 21 | echo "caught ArgumentCountError for missing callable\n"; 22 | } 23 | 24 | echo "end\n"; 25 | 26 | ?> 27 | --EXPECTF-- 28 | start 29 | caught ArgumentCountError for no params: %s 30 | caught ArgumentCountError for missing callable 31 | end -------------------------------------------------------------------------------- /tests/await/005-awaitAnyOrFail_basic.phpt: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | await_any_or_fail() - basic usage with multiple coroutines 3 | --FILE-- 4 | 32 | --EXPECT-- 33 | start 34 | first completed: second 35 | end -------------------------------------------------------------------------------- /tests/coroutine/019-coroutine_gc_basic.phpt: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | Coroutine: GC handler basic functionality 3 | --FILE-- 4 | getResult(); 21 | var_dump($result); 22 | 23 | // Verify GC was called (should return >= 0) 24 | var_dump($collected >= 0); 25 | 26 | ?> 27 | --EXPECT-- 28 | string(10) "test_value" 29 | bool(true) -------------------------------------------------------------------------------- /tests/await/008-awaitFirstSuccess_basic.phpt: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | await_first_success() - basic usage with mixed success and error 3 | --FILE-- 4 | 30 | --EXPECTF-- 31 | start 32 | Result: success 33 | end -------------------------------------------------------------------------------- /tests/await/007-awaitAnyOrFail_exception.phpt: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | await_any_or_fail() - coroutine throws exception 3 | --FILE-- 4 | getMessage() . "\n"; 27 | } 28 | 29 | echo "end\n"; 30 | ?> 31 | --EXPECT-- 32 | start 33 | caught exception: test exception 34 | end -------------------------------------------------------------------------------- /tests/coroutine/008-coroutine_suspend_location.phpt: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | Coroutine: getSuspendFileAndLine() and getSuspendLocation() - basic usage 3 | --FILE-- 4 | getSuspendFileAndLine(); 18 | $location = $coroutine->getSuspendLocation(); 19 | 20 | var_dump(is_array($fileAndLine)); 21 | var_dump(count($fileAndLine) === 2); 22 | var_dump(is_string($location)); 23 | 24 | ?> 25 | --EXPECT-- 26 | bool(true) 27 | bool(true) 28 | bool(true) -------------------------------------------------------------------------------- /tests/dns/001-gethostbyname_basic.phpt: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | DNS gethostbyname() basic functionality in async context 3 | --FILE-- 4 | 24 | --EXPECTF-- 25 | localhost resolved to: 127.0.0.1 26 | 127.0.0.1 resolved to: 127.0.0.1 27 | invalid hostname returned: invalid.nonexistent.domain.example -------------------------------------------------------------------------------- /tests/dns/015-dns_ipv6.phpt: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | DNS IPv6 resolution in async context 3 | --SKIPIF-- 4 | 12 | --FILE-- 13 | $ipv6_hostname\n"; 24 | } else { 25 | echo "::1 -> resolution failed\n"; 26 | } 27 | }); 28 | 29 | await($coroutine); 30 | 31 | ?> 32 | --EXPECTF-- 33 | Testing IPv6 DNS resolution 34 | ::1 -> %s -------------------------------------------------------------------------------- /tests/await/027-awaitFirstSuccess_generator.phpt: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | await_first_success() - with Generator 3 | --FILE-- 4 | 33 | --EXPECT-- 34 | start 35 | Result: success 36 | end -------------------------------------------------------------------------------- /tests/await/026-awaitAnyOrFail_generator.phpt: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | await_any_or_fail() - with Generator 3 | --FILE-- 4 | 35 | --EXPECT-- 36 | start 37 | Result: fast 38 | end -------------------------------------------------------------------------------- /tests/coroutine/005-coroutine_status_methods.phpt: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | Coroutine: status methods - isStarted, isFinished, isCancelled, isSuspended 3 | --FILE-- 4 | isStarted()); 15 | var_dump($coroutine->isFinished()); 16 | var_dump($coroutine->isCancelled()); 17 | 18 | await($coroutine); 19 | 20 | echo "After completion:\n"; 21 | var_dump($coroutine->isStarted()); 22 | var_dump($coroutine->isFinished()); 23 | var_dump($coroutine->isCancelled()); 24 | 25 | ?> 26 | --EXPECT-- 27 | After spawn: 28 | bool(false) 29 | bool(false) 30 | bool(false) 31 | After completion: 32 | bool(true) 33 | bool(true) 34 | bool(false) -------------------------------------------------------------------------------- /tests/await/011-awaitAllOrFail_exception.phpt: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | await_all_or_fail() - one coroutine throws exception 3 | --FILE-- 4 | getMessage() . "\n"; 28 | } 29 | 30 | echo "end\n"; 31 | ?> 32 | --EXPECT-- 33 | start 34 | caught exception: test exception 35 | end -------------------------------------------------------------------------------- /tests/await/013-awaitAll_all_success.phpt: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | await_all() - all coroutines succeed 3 | --FILE-- 4 | 28 | --EXPECT-- 29 | start 30 | array(2) { 31 | [0]=> 32 | array(3) { 33 | [0]=> 34 | string(5) "first" 35 | [1]=> 36 | string(6) "second" 37 | [2]=> 38 | string(5) "third" 39 | } 40 | [1]=> 41 | array(0) { 42 | } 43 | } 44 | end -------------------------------------------------------------------------------- /tests/await/040-await_cancellation_timeout.phpt: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | await() - with cancellation timeout 3 | --FILE-- 4 | getMessage() . "\n"; 25 | } 26 | 27 | echo "end\n"; 28 | 29 | ?> 30 | --EXPECT-- 31 | start 32 | Timeout caught as expected 33 | end -------------------------------------------------------------------------------- /tests/coroutine/014-coroutine_onFinally_basic.phpt: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | Coroutine: onFinally() - basic usage with callback execution 3 | --FILE-- 4 | onFinally(function() use (&$called) { 17 | $called = true; 18 | echo "Finally callback executed\n"; 19 | }); 20 | 21 | await($coroutine); 22 | 23 | // Give finally handler time to execute 24 | suspend(); 25 | 26 | echo "Result: " . $coroutine->getResult() . "\n"; 27 | echo "Finally called: " . ($called ? "yes" : "no") . "\n"; 28 | 29 | ?> 30 | --EXPECT-- 31 | Finally callback executed 32 | Result: test result 33 | Finally called: yes -------------------------------------------------------------------------------- /tests/output_buffer/003-nested_ob_start.phpt: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | Output Buffer: Nested ob_start in coroutines 3 | --FILE-- 4 | 34 | --EXPECT-- 35 | Start 36 | End 37 | Got level1: 'Level 1: Got level2: 'Level 2: content' ' -------------------------------------------------------------------------------- /tests/protect/004-protect_cancellation_deferred.phpt: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | Async\protect: cancellation is deferred during protected block 3 | --FILE-- 4 | cancel(); 27 | 28 | // Wait for completion 29 | await($coroutine); 30 | 31 | ?> 32 | --EXPECTF-- 33 | coroutine start 34 | protected block start 35 | protected block end -------------------------------------------------------------------------------- /tests/fiber/008-fiber_exception_natural.phpt: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | Natural exception inside Fiber 3 | --FILE-- 4 | start(); 19 | echo "Should not reach here\n"; 20 | } catch (Exception $e) { 21 | echo "Caught: " . $e->getMessage() . "\n"; 22 | } 23 | 24 | return "done"; 25 | }); 26 | 27 | await($coroutine); 28 | echo "Test completed\n"; 29 | ?> 30 | --EXPECT-- 31 | Test: Natural exception in Fiber 32 | Before exception 33 | Caught: Fiber error 34 | Test completed 35 | -------------------------------------------------------------------------------- /tests/await/014-awaitAnyOfOrFail_basic.phpt: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | await_any_of_or_fail() - basic usage with count parameter 3 | --FILE-- 4 | = 2 ? "OK" : "FALSE: ".count($results); 29 | echo "Count of results: $countOfResults\n"; 30 | 31 | echo "end\n"; 32 | ?> 33 | --EXPECT-- 34 | start 35 | Count of results: OK 36 | end -------------------------------------------------------------------------------- /tests/await/072-awaitAll_with_simultaneously.phpt: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | await_all() - Attempt to wait for two identical objects. 3 | --FILE-- 4 | 24 | --EXPECTF-- 25 | start 26 | array(2) { 27 | [0]=> 28 | array(4) { 29 | [0]=> 30 | string(5) "first" 31 | [1]=> 32 | string(6) "second" 33 | [2]=> 34 | string(5) "first" 35 | [3]=> 36 | string(6) "second" 37 | } 38 | [1]=> 39 | array(0) { 40 | } 41 | } 42 | end -------------------------------------------------------------------------------- /tests/protect/011-protect_invalid_parameter.phpt: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | Async\protect: invalid parameter types 3 | --FILE-- 4 | getMessage() . "\n"; 15 | } 16 | 17 | // Test with array 18 | try { 19 | protect([]); 20 | } catch (TypeError $e) { 21 | echo "caught TypeError for array\n"; 22 | } 23 | 24 | // Test with object 25 | try { 26 | protect(new stdClass()); 27 | } catch (TypeError $e) { 28 | echo "caught TypeError for object\n"; 29 | } 30 | 31 | echo "end\n"; 32 | 33 | ?> 34 | --EXPECTF-- 35 | start 36 | caught TypeError: %s 37 | caught TypeError for array 38 | caught TypeError for object 39 | end -------------------------------------------------------------------------------- /tests/coroutine/003-coroutine_getException_basic.phpt: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | Coroutine: getException() - basic usage 3 | --FILE-- 4 | getException()); 16 | 17 | // Test with exception 18 | $coroutine2 = spawn(function() { 19 | throw new RuntimeException("test error"); 20 | }); 21 | 22 | try { 23 | await($coroutine2); 24 | } catch (Exception $e) { 25 | // Ignore 26 | } 27 | 28 | $exception = $coroutine2->getException(); 29 | var_dump($exception instanceof RuntimeException); 30 | var_dump($exception->getMessage()); 31 | 32 | ?> 33 | --EXPECT-- 34 | NULL 35 | bool(true) 36 | string(10) "test error" -------------------------------------------------------------------------------- /tests/coroutine/016-coroutine_onFinally_multiple.phpt: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | Coroutine: onFinally() - multiple handlers execution 3 | --FILE-- 4 | onFinally(function() use (&$calls) { 17 | $calls[] = "first"; 18 | echo "First finally handler\n"; 19 | }); 20 | 21 | $coroutine->onFinally(function() use (&$calls) { 22 | $calls[] = "second"; 23 | echo "Second finally handler\n"; 24 | }); 25 | 26 | await($coroutine); 27 | suspend(); 28 | 29 | echo "Handlers called: " . implode(", ", $calls) . "\n"; 30 | 31 | ?> 32 | --EXPECT-- 33 | First finally handler 34 | Second finally handler 35 | Handlers called: first, second -------------------------------------------------------------------------------- /tests/coroutine/007-coroutine_spawn_location.phpt: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | Coroutine: getSpawnFileAndLine() and getSpawnLocation() - basic usage 3 | --FILE-- 4 | getSpawnFileAndLine(); 13 | $location = $coroutine->getSpawnLocation(); 14 | 15 | var_dump(is_array($fileAndLine)); 16 | var_dump(count($fileAndLine) === 2); 17 | var_dump(is_string($fileAndLine[0]) || is_null($fileAndLine[0])); 18 | var_dump(is_int($fileAndLine[1])); 19 | 20 | var_dump(is_string($location)); 21 | echo "Location contains file info: " . (strpos($location, ':') !== false ? "yes" : "no") . "\n"; 22 | 23 | ?> 24 | --EXPECT-- 25 | bool(true) 26 | bool(true) 27 | bool(true) 28 | bool(true) 29 | bool(true) 30 | Location contains file info: yes -------------------------------------------------------------------------------- /tests/coroutine/022-coroutine_gc_suspended.phpt: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | Coroutine: GC handler for suspended coroutines 3 | --FILE-- 4 | getResult(); 21 | suspend(); // Ensure coroutine is resumed 22 | $result = $coroutine->getResult(); 23 | 24 | var_dump($result); 25 | var_dump($collected >= 0); 26 | 27 | ?> 28 | --EXPECT-- 29 | string(16) "suspended_result" 30 | bool(true) -------------------------------------------------------------------------------- /tests/protect/009-protect_with_spawn.phpt: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | Async\protect: protect inside spawn coroutine 3 | --FILE-- 4 | 33 | --EXPECT-- 34 | start 35 | spawn start 36 | protected in spawn 37 | result: 3 38 | spawn end 39 | final result: spawn result 40 | end -------------------------------------------------------------------------------- /tests/scope/019-scope_gc_basic.phpt: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | Scope: GC handler basic functionality 3 | --FILE-- 4 | spawn(function() { 14 | return "scope_test_value"; 15 | }); 16 | 17 | // Force garbage collection to ensure our GC handler is called 18 | $collected = gc_collect_cycles(); 19 | 20 | suspend(); // Suspend to simulate coroutine lifecycle 21 | 22 | // Check that coroutine completed successfully 23 | $result = $coroutine->getResult(); 24 | var_dump($result); 25 | 26 | // Verify GC was called (should return >= 0) 27 | var_dump($collected >= 0); 28 | 29 | ?> 30 | --EXPECT-- 31 | string(16) "scope_test_value" 32 | bool(true) -------------------------------------------------------------------------------- /tests/await/025-awaitAllOrFail_generator.phpt: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | await_all_or_fail() - with Generator 3 | --FILE-- 4 | 35 | --EXPECT-- 36 | start 37 | Count of results: OK 38 | end -------------------------------------------------------------------------------- /tests/spawn/013-spawn_in_shutdown.phpt: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | Future: spawn() - spawn in shutdown handler should fail 3 | --FILE-- 4 | getMessage() . "\n"; 18 | } 19 | 20 | echo "shutdown handler end\n"; 21 | }); 22 | 23 | echo "start\n"; 24 | 25 | spawn(function() { 26 | echo "normal spawn works\n"; 27 | }); 28 | 29 | echo "end\n"; 30 | ?> 31 | --EXPECT-- 32 | start 33 | end 34 | normal spawn works 35 | shutdown handler start 36 | spawn succeeded 37 | shutdown handler end 38 | should execute -------------------------------------------------------------------------------- /tests/coroutine/020-coroutine_gc_with_finally.phpt: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | Coroutine: GC handler with finally handlers 3 | --FILE-- 4 | onFinally(function() { 16 | echo "Finally executed\n"; 17 | }); 18 | 19 | // Force garbage collection 20 | $collected = gc_collect_cycles(); 21 | 22 | suspend(); // Suspend to simulate coroutine lifecycle 23 | 24 | // Wait for completion 25 | $result = $coroutine->getResult(); 26 | var_dump($result); 27 | 28 | // Verify GC was called 29 | var_dump($collected >= 0); 30 | 31 | ?> 32 | --EXPECT-- 33 | Finally executed 34 | string(10) "test_value" 35 | bool(true) -------------------------------------------------------------------------------- /tests/fiber/014-fiber_gc_suspended.phpt: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | Fiber garbage collection when suspended 3 | --FILE-- 4 | start(); 19 | echo "Fiber suspended\n"; 20 | 21 | // Release reference 22 | unset($fiber); 23 | gc_collect_cycles(); 24 | 25 | echo "Fiber GC'd\n"; 26 | 27 | return "done"; 28 | }); 29 | 30 | await($coroutine); 31 | echo "Test completed\n"; 32 | ?> 33 | --EXPECT-- 34 | Test: Fiber GC when suspended 35 | Before suspend 36 | Fiber suspended 37 | Fiber GC'd 38 | Test completed 39 | -------------------------------------------------------------------------------- /tests/fiber/019-fiber_getCoroutine.phpt: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | Fiber::getCoroutine() method 3 | --FILE-- 4 | start(); 16 | 17 | $coro = $f->getCoroutine(); 18 | echo "Has coroutine: " . ($coro !== null ? "yes" : "no") . "\n"; 19 | echo "Has ID: " . ($coro->getId() > 0 ? "yes" : "no") . "\n"; 20 | echo "Is started: " . ($coro->isStarted() ? "yes" : "no") . "\n"; 21 | echo "Is suspended: " . ($coro->isSuspended() ? "yes" : "no") . "\n"; 22 | 23 | $f->resume(); 24 | }); 25 | 26 | await($c); 27 | 28 | echo "OK\n"; 29 | ?> 30 | --EXPECT-- 31 | Has coroutine: yes 32 | Has ID: yes 33 | Is started: yes 34 | Is suspended: yes 35 | OK 36 | -------------------------------------------------------------------------------- /tests/suspend/007-suspend_with_exception.phpt: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | Suspend with exception handling - verify suspend doesn't break exception flow 3 | --FILE-- 4 | getMessage() . "\n"; 19 | } 20 | }); 21 | 22 | spawn(function() { 23 | echo "Other coroutine: Task\n"; 24 | }); 25 | 26 | echo "End\n"; 27 | 28 | ?> 29 | --EXPECT-- 30 | Start 31 | End 32 | Coroutine: Before suspend 33 | Other coroutine: Task 34 | Coroutine: After suspend 35 | Coroutine: Caught exception: Test exception -------------------------------------------------------------------------------- /tests/spawn/019-spawn_scope_provider_null.phpt: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | spawn_with() - scope provider returns null (valid case) 3 | --FILE-- 4 | getMessage() . "\n"; 27 | } 28 | 29 | echo "end\n"; 30 | 31 | ?> 32 | --EXPECT-- 33 | start 34 | Null provider result: success 35 | end -------------------------------------------------------------------------------- /.github/actions/setup-windows/action.yml: -------------------------------------------------------------------------------- 1 | name: Setup 2 | runs: 3 | using: composite 4 | steps: 5 | - name: Setup MySQL 6 | shell: cmd 7 | run: | 8 | mysqld --initialize-insecure 9 | mysqld --install 10 | net start MySQL 11 | mysql --port=3306 --user=root --password="" -e "ALTER USER 'root'@'localhost' IDENTIFIED BY 'Password12!'; FLUSH PRIVILEGES;" 12 | - name: Setup MSSQL 13 | shell: pwsh 14 | run: | 15 | choco install sql-server-express -y --no-progress --install-arguments="/SECURITYMODE=SQL /SAPWD=Password12!" 16 | - name: Setup PostgreSQL 17 | shell: pwsh 18 | run: | 19 | Set-Service -Name "postgresql-x64-14" -StartupType manual -Status Running 20 | pwsh -Command { $env:PGPASSWORD="root"; & "$env:PGBIN\psql" -U postgres -c "ALTER USER postgres WITH PASSWORD 'Password12!';" } 21 | -------------------------------------------------------------------------------- /tests/await/017-awaitAnyOf_all_success.phpt: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | await_any_of() - all coroutines succeed 3 | --FILE-- 4 | = 2 ? "OK" : "FALSE: ".count($result[0]); 26 | $countOfErrors = count($result[1]) == 0 ? "OK" : "FALSE: ".count($result[1]); 27 | 28 | echo "Count of results: $countOfResults\n"; 29 | echo "Count of errors: $countOfErrors\n"; 30 | 31 | echo "end\n"; 32 | ?> 33 | --EXPECTF-- 34 | start 35 | Count of results: OK 36 | Count of errors: OK 37 | end -------------------------------------------------------------------------------- /tests/fiber/026-fiber_getCoroutine_after_termination.phpt: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | Get fiber's coroutine after fiber termination 3 | --FILE-- 4 | start(); 15 | echo "Fiber completed: {$result}\n"; 16 | 17 | // Get coroutine after fiber is terminated 18 | $coro = $fiber->getCoroutine(); 19 | 20 | if ($coro !== null) { 21 | echo "Has coroutine: yes\n"; 22 | echo "Is finished: " . ($coro->isFinished() ? "yes" : "no") . "\n"; 23 | } else { 24 | echo "Has coroutine: no\n"; 25 | } 26 | 27 | return "ok"; 28 | }); 29 | 30 | await($c); 31 | echo "OK\n"; 32 | ?> 33 | --EXPECTF-- 34 | Fiber completed: done 35 | Has coroutine: yes 36 | Is finished: yes 37 | OK 38 | -------------------------------------------------------------------------------- /tests/spawnWith/005-spawnWith_null_scope_provider.phpt: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | Async\spawnWith: ScopeProvider returning null scope 3 | --FILE-- 4 | 35 | --EXPECT-- 36 | start 37 | returning null scope 38 | coroutine executed 39 | result: null scope result 40 | end -------------------------------------------------------------------------------- /tests/stream/017-stream_select_invalid_streams.phpt: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | stream_select with invalid stream types 3 | --FILE-- 4 | 30 | --EXPECTF-- 31 | Testing stream_select with invalid streams 32 | %a 33 | Result: invalid streams test completed -------------------------------------------------------------------------------- /tests/gc/006-gc_destructor_simple.phpt: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | GC 006: Simple destructor test without async operations 3 | --FILE-- 4 | value = $value; 11 | echo "Created: {$this->value}\n"; 12 | } 13 | 14 | public function __destruct() { 15 | echo "Destructor: {$this->value}\n"; 16 | } 17 | } 18 | 19 | echo "Starting test\n"; 20 | 21 | // Create object that will be garbage collected 22 | $obj = new TestObject("test-object"); 23 | 24 | // Remove reference so object becomes eligible for GC 25 | unset($obj); 26 | 27 | echo "After unset\n"; 28 | 29 | // Force garbage collection 30 | gc_collect_cycles(); 31 | 32 | echo "Test complete\n"; 33 | 34 | ?> 35 | --EXPECT-- 36 | Starting test 37 | Created: test-object 38 | Destructor: test-object 39 | After unset 40 | Test complete -------------------------------------------------------------------------------- /tests/spawnWith/010-spawnWith_invalid_provider.phpt: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | Async\spawnWith: invalid provider parameter 3 | --FILE-- 4 | getMessage() . "\n"; 15 | } 16 | 17 | // Test with array 18 | try { 19 | spawn_with([], function() {}); 20 | } catch (TypeError $e) { 21 | echo "caught TypeError for array\n"; 22 | } 23 | 24 | // Test with stdClass 25 | try { 26 | spawn_with(new stdClass(), function() {}); 27 | } catch (TypeError $e) { 28 | echo "caught TypeError for stdClass\n"; 29 | } 30 | 31 | echo "end\n"; 32 | 33 | ?> 34 | --EXPECTF-- 35 | start 36 | caught TypeError for string: %s 37 | caught TypeError for array 38 | caught TypeError for stdClass 39 | end -------------------------------------------------------------------------------- /tests/protect/006-protect_multiple_cancellation.phpt: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | Async\protect: multiple cancellation attempts during protected block 3 | --FILE-- 4 | cancel(); 29 | suspend(); 30 | $coroutine->cancel(); 31 | suspend(); 32 | $coroutine->cancel(); 33 | 34 | await($coroutine); 35 | 36 | ?> 37 | --EXPECTF-- 38 | coroutine start 39 | protected block 40 | work: 1 41 | work: 2 -------------------------------------------------------------------------------- /tests/coroutine/025-coroutine_gc_waker_scope.phpt: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | Coroutine: GC handler with waker and scope structures 3 | --FILE-- 4 | spawn(function() { 14 | // This creates waker and scope structures with ZVALs 15 | $data = ["test" => "value"]; 16 | return $data; 17 | }); 18 | 19 | // Force garbage collection to test waker/scope ZVAL tracking 20 | $collected = gc_collect_cycles(); 21 | 22 | suspend(); // Suspend to simulate coroutine lifecycle 23 | 24 | // Get result 25 | $result = $coroutine->getResult(); 26 | var_dump($result); 27 | 28 | // Verify GC was called 29 | var_dump($collected >= 0); 30 | 31 | ?> 32 | --EXPECT-- 33 | array(1) { 34 | ["test"]=> 35 | string(5) "value" 36 | } 37 | bool(true) -------------------------------------------------------------------------------- /tests/spawn/018-spawn_scope_provider_exception.phpt: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | spawn_with() - scope provider throws exception 3 | --FILE-- 4 | getMessage() . "\n"; 25 | } catch (Throwable $e) { 26 | echo "Caught exception: " . get_class($e) . ": " . $e->getMessage() . "\n"; 27 | } 28 | 29 | echo "end\n"; 30 | 31 | ?> 32 | --EXPECT-- 33 | start 34 | Caught provider exception: Provider error 35 | end -------------------------------------------------------------------------------- /tests/suspend/002-suspend_execution_order.phpt: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | Suspend execution order - verify scheduler processes other coroutines during suspend 3 | --FILE-- 4 | 32 | --EXPECT-- 33 | Start 34 | End 35 | Coroutine A: Before suspend 36 | Coroutine B: Task 1 37 | Coroutine C: Quick task 38 | Coroutine A: After suspend 39 | Coroutine B: Task 2 40 | Coroutine A: After second suspend -------------------------------------------------------------------------------- /tests/spawnWith/012-spawnWith_invalid_callable.phpt: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | Async\spawnWith: invalid callable parameter 3 | --FILE-- 4 | getMessage() . "\n"; 18 | } 19 | 20 | // Test with array (not callable) 21 | try { 22 | spawn_with($scope, []); 23 | } catch (TypeError $e) { 24 | echo "caught TypeError for array\n"; 25 | } 26 | 27 | // Test with null 28 | try { 29 | spawn_with($scope, null); 30 | } catch (TypeError $e) { 31 | echo "caught TypeError for null\n"; 32 | } 33 | 34 | echo "end\n"; 35 | 36 | ?> 37 | --EXPECTF-- 38 | start 39 | caught TypeError for string: %s 40 | caught TypeError for array 41 | caught TypeError for null 42 | end -------------------------------------------------------------------------------- /tests/await/050-awaitFirstSuccess_concurrent_generator.phpt: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | await_first_success() - With concurrent generator using suspend() in body 3 | --FILE-- 4 | 34 | --EXPECT-- 35 | start 36 | Result: success 37 | end -------------------------------------------------------------------------------- /tests/await/029-awaitAnyOfOrFail_generator.phpt: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | await_any_of_or_fail() - with Generator 3 | --FILE-- 4 | = 2 ? "OK" : "FALSE: ".count($results); 34 | echo "Count of results: $countOfResults\n"; 35 | 36 | echo "end\n"; 37 | 38 | ?> 39 | --EXPECT-- 40 | start 41 | Count of results: OK 42 | end -------------------------------------------------------------------------------- /tests/await/046-awaitFirstSuccess_all_errors.phpt: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | await_first_success() - when all coroutines throw errors 3 | --FILE-- 4 | 33 | --EXPECT-- 34 | start 35 | Result: NULL 36 | Errors count: 3 37 | end -------------------------------------------------------------------------------- /tests/fiber/017-fiber_suspend_resume_multiple.phpt: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | Multiple fiber suspend/resume in different coroutines 3 | --FILE-- 4 | start(); 19 | $f->resume(); 20 | $f->resume(); 21 | }); 22 | 23 | $c2 = spawn(function() { 24 | $f = new Fiber(function() { 25 | echo "C2-1\n"; 26 | Fiber::suspend(); 27 | echo "C2-2\n"; 28 | Fiber::suspend(); 29 | echo "C2-3\n"; 30 | }); 31 | 32 | $f->start(); 33 | $f->resume(); 34 | $f->resume(); 35 | }); 36 | 37 | await($c1); 38 | await($c2); 39 | 40 | echo "OK\n"; 41 | ?> 42 | --EXPECT-- 43 | C1-1 44 | C2-1 45 | C1-2 46 | C2-2 47 | C1-3 48 | C2-3 49 | OK 50 | -------------------------------------------------------------------------------- /tests/output_buffer/001-ob_start_basic_isolation.phpt: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | Output Buffer: Basic ob_start isolation with suspend 3 | --FILE-- 4 | 38 | --EXPECT-- 39 | Main starts 40 | Coroutine starts 41 | Main got: 'Main buffer before Main buffer after' 42 | Got: 'Before suspend After suspend' -------------------------------------------------------------------------------- /tests/gc/013-gc-fiber-destructors.phpt: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | Fibers in destructors 006: multiple GC runs 3 | --FILE-- 4 | self = $this; 15 | } 16 | public function __destruct() { 17 | $id = self::$counter++; 18 | printf("%d: Start destruct\n", $id); 19 | printf("%d: End destruct\n", $id); 20 | } 21 | } 22 | 23 | $f = new Fiber(function () { 24 | new Cycle(); 25 | new Cycle(); 26 | gc_collect_cycles(); 27 | }); 28 | 29 | $f->start(); 30 | 31 | new Cycle(); 32 | new Cycle(); 33 | gc_collect_cycles(); 34 | 35 | ?> 36 | --EXPECT-- 37 | 0: Start destruct 38 | 0: End destruct 39 | 1: Start destruct 40 | 1: End destruct 41 | 2: Start destruct 42 | 2: End destruct 43 | 3: Start destruct 44 | 3: End destruct 45 | Shutdown 46 | -------------------------------------------------------------------------------- /tests/fiber/003-fiber_one_suspend_resume.phpt: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | One suspend/resume cycle 3 | --FILE-- 4 | start(); 20 | echo "Fiber suspended with: " . $suspended . "\n"; 21 | 22 | $result = $fiber->resume("resume value"); 23 | echo "Fiber returned: " . $result . "\n"; 24 | 25 | return "complete"; 26 | }); 27 | 28 | await($coroutine); 29 | echo "Test completed\n"; 30 | ?> 31 | --EXPECT-- 32 | Test: One suspend/resume cycle 33 | Before suspend 34 | Fiber suspended with: suspended 35 | After resume, got: resume value 36 | Fiber returned: done 37 | Test completed 38 | -------------------------------------------------------------------------------- /tests/protect/005-protect_cancellation_immediate.phpt: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | Async\protect: cancellation applied immediately after protected block 3 | --FILE-- 4 | getMessage() . "\n"; 23 | } 24 | }); 25 | 26 | suspend(); 27 | 28 | // Cancel the coroutine 29 | $coroutine->cancel(); 30 | 31 | await($coroutine); 32 | 33 | ?> 34 | --EXPECTF-- 35 | before protect 36 | in protect 37 | finished protect 38 | caught exception: %s -------------------------------------------------------------------------------- /tests/sleep/001-sleep_basic.phpt: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | sleep() async basic functionality 3 | --SKIPIF-- 4 | 7 | --FILE-- 8 | = 1s: " . ($elapsed >= 1.0 ? "yes" : "no") . "\n"; 24 | echo "Sleep test completed\n"; 25 | }); 26 | 27 | spawn(function() { 28 | echo "Other async task executing\n"; 29 | }); 30 | 31 | echo "Main thread end\n"; 32 | ?> 33 | --EXPECT-- 34 | Main thread start 35 | Main thread end 36 | Starting async sleep test 37 | Other async task executing 38 | Sleep returned: 0 39 | Elapsed time >= 1s: yes 40 | Sleep test completed -------------------------------------------------------------------------------- /tests/dns/002-gethostbyaddr_basic.phpt: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | DNS gethostbyaddr() basic functionality in async context 3 | --FILE-- 4 | 34 | --EXPECTF-- 35 | 127.0.0.1 resolved to: %s 36 | %a 37 | bool(false) -------------------------------------------------------------------------------- /tests/await/028-awaitAll_generator.phpt: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | await_all() - with Generator 3 | --FILE-- 4 | 37 | --EXPECT-- 38 | start 39 | Count of results: OK 40 | Count of errors: OK 41 | end -------------------------------------------------------------------------------- /tests/bailout/001-memory-exhaustion-simple.phpt: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | Memory exhaustion bailout in simple async operation 3 | --SKIPIF-- 4 | 10 | --INI-- 11 | memory_limit=2M 12 | --FILE-- 13 | 32 | --EXPECTF-- 33 | Before spawn 34 | After spawn 35 | Before memory exhaustion 36 | 37 | Fatal error: Allowed memory size of %d bytes exhausted%s(tried to allocate %d bytes) in %s on line %d 38 | 39 | Warning: Graceful shutdown mode was started in %s on line %d 40 | Shutdown function called -------------------------------------------------------------------------------- /tests/fiber/028-fiber_exception_vs_cancel.phpt: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | Fiber throws exception while coroutine is being cancelled 3 | --FILE-- 4 | start(); 17 | $coro = $fiber->getCoroutine(); 18 | 19 | // Cancel coroutine 20 | $coro->cancel(new CancellationError("cancel")); 21 | 22 | // Resume - what happens? Exception or CancellationError? 23 | try { 24 | $fiber->resume(); 25 | echo "No exception\n"; 26 | } catch (CancellationError $e) { 27 | echo "CancellationError: " . $e->getMessage() . "\n"; 28 | } catch (Exception $e) { 29 | echo "Exception: " . $e->getMessage() . "\n"; 30 | } 31 | }); 32 | 33 | await($c); 34 | echo "OK\n"; 35 | ?> 36 | --EXPECTF-- 37 | %a 38 | OK 39 | -------------------------------------------------------------------------------- /tests/scope/020-scope_gc_with_finally.phpt: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | Scope: GC handler with finally handlers 3 | --FILE-- 4 | onFinally(function() { 15 | echo "Scope finally executed\n"; 16 | }); 17 | 18 | $coroutine = $scope->spawn(function() { 19 | return "scope_finally_test"; 20 | }); 21 | 22 | // Force garbage collection 23 | $collected = gc_collect_cycles(); 24 | 25 | suspend(); // Suspend to simulate coroutine lifecycle 26 | 27 | // Wait for completion 28 | $result = $coroutine->getResult(); 29 | var_dump($result); 30 | 31 | // Dispose scope to trigger finally handlers 32 | $scope->dispose(); 33 | 34 | // Verify GC was called 35 | var_dump($collected >= 0); 36 | 37 | ?> 38 | --EXPECT-- 39 | string(18) "scope_finally_test" 40 | bool(true) 41 | Scope finally executed -------------------------------------------------------------------------------- /tests/sleep/002-usleep_basic.phpt: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | usleep() async basic functionality 3 | --SKIPIF-- 4 | 7 | --FILE-- 8 | = 0.5 || $elapsed >= 0.3999999999999; 23 | echo "Elapsed time ~ 0.5s: " . ($is_equal ? "yes" : "no") . "\n"; 24 | echo "Usleep test completed\n"; 25 | }); 26 | 27 | spawn(function() { 28 | echo "Other async task executing\n"; 29 | }); 30 | 31 | echo "Main thread end\n"; 32 | ?> 33 | --EXPECT-- 34 | Main thread start 35 | Main thread end 36 | Starting async usleep test 37 | Other async task executing 38 | Elapsed time ~ 0.5s: yes 39 | Usleep test completed -------------------------------------------------------------------------------- /tests/await/038-awaitFirstSuccess_cancellation_timeout.phpt: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | await_first_success() - with cancellation timeout 3 | --FILE-- 4 | getMessage() . "\n"; 32 | } 33 | 34 | echo "end\n"; 35 | 36 | ?> 37 | --EXPECT-- 38 | start 39 | Timeout caught as expected 40 | end -------------------------------------------------------------------------------- /tests/await/016-awaitAnyOf_basic.phpt: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | await_any_of() - basic usage with mixed success and error 3 | --FILE-- 4 | = 2 ? "OK" : "FALSE: ".count($result[0]); 30 | $countOfErrors = count($result[1]) == 1 ? "OK" : "FALSE: ".count($result[1]); 31 | 32 | echo "Count of results: $countOfResults\n"; 33 | echo "Count of errors: $countOfErrors\n"; 34 | 35 | echo "end\n"; 36 | ?> 37 | --EXPECTF-- 38 | start 39 | Count of results: OK 40 | Count of errors: OK 41 | end -------------------------------------------------------------------------------- /tests/await/037-awaitAnyOrFail_cancellation_timeout.phpt: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | await_any_or_fail() - with cancellation timeout 3 | --FILE-- 4 | getMessage() . "\n"; 33 | } 34 | 35 | echo "end\n"; 36 | 37 | ?> 38 | --EXPECT-- 39 | start 40 | Timeout caught as expected 41 | end -------------------------------------------------------------------------------- /tests/mysqli/inc/config.inc: -------------------------------------------------------------------------------- 1 | false !== getenv('MYSQL_TEST_HOST') ? getenv('MYSQL_TEST_HOST') : 'localhost', 5 | 'MYSQL_TEST_PORT' => false !== getenv('MYSQL_TEST_PORT') ? getenv('MYSQL_TEST_PORT') : '3306', 6 | 'MYSQL_TEST_USER' => false !== getenv('MYSQL_TEST_USER') ? getenv('MYSQL_TEST_USER') : 'root', 7 | 'MYSQL_TEST_PASSWD' => false !== getenv('MYSQL_TEST_PASSWD') ? getenv('MYSQL_TEST_PASSWD') : '', 8 | 'MYSQL_TEST_DB' => false !== getenv('MYSQL_TEST_DB') ? getenv('MYSQL_TEST_DB') : 'test', 9 | 'MYSQL_TEST_SOCKET' => false !== getenv('MYSQL_TEST_SOCKET') ? getenv('MYSQL_TEST_SOCKET') : null, 10 | 'MYSQL_TEST_CHARSET' => false !== getenv('MYSQL_TEST_CHARSET') ? getenv('MYSQL_TEST_CHARSET') : 'utf8', 11 | 'MYSQL_TEST_ENGINE' => false !== getenv('MYSQL_TEST_ENGINE') ? getenv('MYSQL_TEST_ENGINE') : 'InnoDB', 12 | ]; 13 | 14 | // Define constants for tests 15 | foreach ($env as $k => $v) { 16 | define($k, $v); 17 | } 18 | ?> -------------------------------------------------------------------------------- /tests/spawn/016-spawn_invalid_scope_provider.phpt: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | spawn_with() - invalid scope provider type error 3 | --FILE-- 4 | getMessage() . "\n"; 25 | } catch (Throwable $e) { 26 | echo "Caught exception: " . get_class($e) . ": " . $e->getMessage() . "\n"; 27 | } 28 | 29 | echo "end\n"; 30 | 31 | ?> 32 | --EXPECTF-- 33 | start 34 | Caught exception: TypeError: InvalidTypeScopeProvider::provideScope(): Return value must be of type ?Async\Scope, string returned 35 | end -------------------------------------------------------------------------------- /tests/pdo_mysql/inc/config.inc: -------------------------------------------------------------------------------- 1 | false !== getenv('MYSQL_TEST_HOST') ? getenv('MYSQL_TEST_HOST') : 'localhost', 5 | 'MYSQL_TEST_PORT' => false !== getenv('MYSQL_TEST_PORT') ? getenv('MYSQL_TEST_PORT') : '3306', 6 | 'MYSQL_TEST_USER' => false !== getenv('MYSQL_TEST_USER') ? getenv('MYSQL_TEST_USER') : 'root', 7 | 'MYSQL_TEST_PASSWD' => false !== getenv('MYSQL_TEST_PASSWD') ? getenv('MYSQL_TEST_PASSWD') : '', 8 | 'MYSQL_TEST_DB' => false !== getenv('MYSQL_TEST_DB') ? getenv('MYSQL_TEST_DB') : 'test', 9 | 'MYSQL_TEST_SOCKET' => false !== getenv('MYSQL_TEST_SOCKET') ? getenv('MYSQL_TEST_SOCKET') : null, 10 | 'MYSQL_TEST_CHARSET' => false !== getenv('MYSQL_TEST_CHARSET') ? getenv('MYSQL_TEST_CHARSET') : 'utf8', 11 | 'MYSQL_TEST_ENGINE' => false !== getenv('MYSQL_TEST_ENGINE') ? getenv('MYSQL_TEST_ENGINE') : 'InnoDB', 12 | ]; 13 | 14 | // Define constants for tests 15 | foreach ($env as $k => $v) { 16 | define($k, $v); 17 | } 18 | ?> -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # from svn:ignore 2 | .deps 3 | .libs 4 | .php-version 5 | .rbenv-version 6 | acinclude.m4 7 | aclocal.m4 8 | autom4te.cache 9 | build 10 | config.cache 11 | config.guess 12 | config.h 13 | config.h.in 14 | config.log 15 | config.nice 16 | config.status 17 | config.sub 18 | configure 19 | configure.ac 20 | configure.in 21 | extras 22 | include 23 | install-sh 24 | libtool 25 | ltmain.sh 26 | Makefile 27 | Makefile.fragments 28 | Makefile.global 29 | Makefile.objects 30 | missing 31 | mkinstalldirs 32 | modules 33 | run-tests.php 34 | tmp-php.ini 35 | 36 | # General Ignores 37 | *~ 38 | .#* 39 | *.dep 40 | *. 41 | *.slo 42 | *.mk 43 | *.mem 44 | *.gcda 45 | *.gcno 46 | *.la 47 | *.lo 48 | *.loT 49 | *.o 50 | *.a 51 | *.ncb 52 | *.opt 53 | *.plg 54 | *swp 55 | *.patch 56 | *.tgz 57 | *.tar.gz 58 | *.tar.bz2 59 | .FBCIndex 60 | .FBCLockFolder 61 | core 62 | 63 | # Test specific Ignores 64 | tests/*/*.diff 65 | tests/*/*.exp 66 | tests/*/*.log 67 | tests/*/*.out 68 | tests/*/*.php 69 | tests/*/*.sh 70 | 71 | # coverage 72 | /coverage.info 73 | /reports 74 | -------------------------------------------------------------------------------- /tests/output_buffer/006-exception_handling.phpt: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | Output Buffer: Exception handling with ob_start 3 | --FILE-- 4 | getMessage(); 21 | } 22 | 23 | $content = ob_get_contents(); 24 | ob_end_clean(); 25 | echo "Got: '$content'\n"; 26 | }); 27 | 28 | spawn(function() { 29 | ob_start(); 30 | echo "Normal coroutine"; 31 | 32 | suspend(); // Context switch 33 | 34 | echo " continues"; 35 | $content = ob_get_contents(); 36 | ob_end_clean(); 37 | echo "Got: '$content'\n"; 38 | }); 39 | 40 | echo "End\n"; 41 | 42 | ?> 43 | --EXPECT-- 44 | Start 45 | End 46 | Got: 'Before exception Caught: Test exception' 47 | Got: 'Normal coroutine continues' -------------------------------------------------------------------------------- /tests/edge_cases/001-deadlock-basic-test.phpt: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | Deadlock basic test 3 | --FILE-- 4 | 31 | --EXPECTF-- 32 | start 33 | end 34 | coroutine1 running 35 | coroutine2 running 36 | 37 | Fatal error: Uncaught Async\DeadlockError: Deadlock detected: no active coroutines, 2 coroutines in waiting in [no active file]:0 38 | Stack trace: 39 | #0 {main} 40 | thrown in [no active file] on line 0 41 | -------------------------------------------------------------------------------- /tests/exec/005-shell_exec_basic.phpt: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | shell_exec() async basic functionality 3 | --SKIPIF-- 4 | 7 | --FILE-- 8 | 34 | --EXPECT-- 35 | Main thread start 36 | Main thread end 37 | Starting async shell_exec test 38 | Other async task executing 39 | Output: Hello from async shell_exec 40 | Shell_exec test completed successfully -------------------------------------------------------------------------------- /tests/fiber/010-fiber_exception_propagation.phpt: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | Exception propagation from Fiber to coroutine 3 | --FILE-- 4 | start(); 21 | echo "Got: " . $val . "\n"; 22 | 23 | $fiber->resume("resume"); 24 | echo "Should not print\n"; 25 | } catch (RuntimeException $e) { 26 | echo "Caught in coroutine: " . $e->getMessage() . "\n"; 27 | } 28 | 29 | return "done"; 30 | }); 31 | 32 | await($coroutine); 33 | echo "Test completed\n"; 34 | ?> 35 | --EXPECT-- 36 | Test: Exception propagation 37 | Fiber: suspending 38 | Got: suspend 39 | Fiber: throwing 40 | Caught in coroutine: fiber exception 41 | Test completed 42 | -------------------------------------------------------------------------------- /tests/await/041-awaitAllOrFail_associative_array.phpt: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | await_all_or_fail() - with associative array 3 | --FILE-- 4 | spawn(function() { 12 | return "first"; 13 | }), 14 | 15 | 'task2' => spawn(function() { 16 | return "second"; 17 | }), 18 | 19 | 'task3' => spawn(function() { 20 | return "third"; 21 | }) 22 | ]; 23 | 24 | echo "start\n"; 25 | 26 | $results = await_all_or_fail($coroutines); 27 | 28 | echo "Count: " . count($results) . "\n"; 29 | echo "Keys preserved: " . (array_keys($results) === ['task1', 'task2', 'task3'] ? "YES" : "NO") . "\n"; 30 | echo "Result task1: {$results['task1']}\n"; 31 | echo "Result task2: {$results['task2']}\n"; 32 | echo "Result task3: {$results['task3']}\n"; 33 | echo "end\n"; 34 | 35 | ?> 36 | --EXPECT-- 37 | start 38 | Count: 3 39 | Keys preserved: YES 40 | Result task1: first 41 | Result task2: second 42 | Result task3: third 43 | end -------------------------------------------------------------------------------- /tests/suspend/006-suspend_with_nested_functions.phpt: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | Suspend with nested functions - test suspend called from nested function calls 3 | --FILE-- 4 | 36 | --EXPECT-- 37 | Start 38 | End 39 | Coroutine: Before nested call 40 | Deeply nested: Start 41 | Nested function: Before suspend 42 | Other coroutine: Task 43 | Nested function: After suspend 44 | Deeply nested: End 45 | Coroutine: After nested call -------------------------------------------------------------------------------- /tests/bailout/003-stack-overflow-simple.phpt: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | Stack overflow bailout in simple async operation 3 | --SKIPIF-- 4 | 10 | --INI-- 11 | opcache.jit_hot_func=0 12 | --FILE-- 13 | 36 | --EXPECTF-- 37 | Before spawn 38 | After spawn 39 | Before stack overflow 40 | 41 | Fatal error: Allowed memory size of %d bytes exhausted%s(tried to allocate %d bytes) in %s on line %d 42 | 43 | Warning: Graceful shutdown mode was started in %s on line %d 44 | Shutdown function called 45 | -------------------------------------------------------------------------------- /tests/fiber/001-fiber_with_coroutine_basic.phpt: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | Fiber with coroutine: Basic fiber creation and execution when async is active 3 | --FILE-- 4 | start(); 22 | echo "Fiber returned: " . $result . "\n"; 23 | 24 | return "coroutine result"; 25 | }); 26 | 27 | $result = await($coroutine); 28 | echo "Coroutine completed with: " . $result . "\n"; 29 | 30 | echo "Test completed\n"; 31 | ?> 32 | --EXPECT-- 33 | Test: Fiber creation with active async scheduler 34 | Coroutine started 35 | Starting fiber 36 | Fiber executing 37 | Fiber returned: fiber result 38 | Coroutine completed with: coroutine result 39 | Test completed -------------------------------------------------------------------------------- /tests/exec/009-passthru_basic.phpt: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | passthru() async basic functionality 3 | --SKIPIF-- 4 | 7 | --FILE-- 8 | 36 | --EXPECT-- 37 | Main thread start 38 | Main thread end 39 | Starting async passthru test 40 | Other async task executing 41 | Hello from async passthruReturn code: 0 42 | Passthru test completed successfully -------------------------------------------------------------------------------- /tests/await/036-awaitAllOrFail_cancellation_timeout.phpt: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | await_all_or_fail() - with cancellation timeout 3 | --FILE-- 4 | getMessage() . "\n"; 37 | } 38 | 39 | echo "end\n"; 40 | 41 | ?> 42 | --EXPECT-- 43 | start 44 | Timeout caught as expected 45 | end -------------------------------------------------------------------------------- /tests/fiber/009-fiber_throw_method.phpt: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | Fiber.throw() with exception handling 3 | --FILE-- 4 | getMessage() . "\n"; 20 | return "recovered"; 21 | } 22 | 23 | return "normal"; 24 | }); 25 | 26 | $val = $fiber->start(); 27 | echo "Suspended: " . $val . "\n"; 28 | 29 | $result = $fiber->throw(new Exception("thrown error")); 30 | echo "Result: " . $result . "\n"; 31 | 32 | return "done"; 33 | }); 34 | 35 | await($coroutine); 36 | echo "Test completed\n"; 37 | ?> 38 | --EXPECT-- 39 | Test: Fiber throw method 40 | Before suspend 41 | Suspended: waiting 42 | Caught in fiber: thrown error 43 | Result: recovered 44 | Test completed 45 | -------------------------------------------------------------------------------- /tests/output_buffer/004-ob_flush_isolation.phpt: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | Output Buffer: ob_flush and ob_clean isolation 3 | --FILE-- 4 | 44 | --EXPECT-- 45 | Start 46 | End 47 | Before flush 48 | Got: 'After clean' 49 | Remaining: ' After flush' -------------------------------------------------------------------------------- /tests/protect/010-protect_with_await.phpt: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | Async\protect: protect with await operations 3 | --FILE-- 4 | 40 | --EXPECT-- 41 | start 42 | child coroutine 43 | main start 44 | protected block start 45 | await result: child result 46 | protected block end 47 | main end 48 | final result: main result 49 | end -------------------------------------------------------------------------------- /tests/suspend/005-suspend_with_spawn.phpt: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | Suspend with spawn integration - test suspend in combination with spawned coroutines 3 | --FILE-- 4 | 36 | --EXPECT-- 37 | Start 38 | End 39 | Parent coroutine: Before spawn 40 | Parent coroutine: After spawn 41 | Independent coroutine 42 | Child coroutine: Before suspend 43 | Parent coroutine: After suspend 44 | Child coroutine: After suspend 45 | Another child coroutine -------------------------------------------------------------------------------- /tests/await/054-awaitAllOrFail_generator_exception.phpt: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | await_all_or_fail() - Exception in generator body should stop process immediately 3 | --FILE-- 4 | $value); 19 | $count++; 20 | } 21 | } 22 | 23 | echo "start\n"; 24 | 25 | $values = ["first", "second", "third"]; 26 | $generator = exceptionGenerator($values); 27 | 28 | try { 29 | $results = await_all_or_fail($generator); 30 | echo "This should not be reached\n"; 31 | } catch (RuntimeException $e) { 32 | echo "Caught exception: " . $e->getMessage() . "\n"; 33 | } 34 | 35 | echo "end\n"; 36 | 37 | ?> 38 | --EXPECT-- 39 | start 40 | Caught exception: Generator exception during iteration 41 | end -------------------------------------------------------------------------------- /tests/output_buffer/005-mixed_buffering.phpt: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | Output Buffer: Mixed buffered and non-buffered output 3 | --FILE-- 4 | 42 | --EXPECT-- 43 | Main without buffer 44 | Main still without buffer 45 | Coroutine 1 without buffer 46 | Buffered was: 'Now with buffer continued' 47 | Got: 'Coroutine 2 always buffered until end' -------------------------------------------------------------------------------- /tests/sleep/004-time_sleep_until_basic.phpt: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | time_sleep_until() async basic functionality 3 | --SKIPIF-- 4 | 7 | --FILE-- 8 | 33 | --EXPECT-- 34 | Main thread start 35 | Main thread end 36 | Starting async time_sleep_until test 37 | Other async task executing 38 | time_sleep_until returned: true 39 | time_sleep_until test completed -------------------------------------------------------------------------------- /tests/await/030-awaitAnyOf_generator.phpt: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | await_any_of() - with Generator 3 | --FILE-- 4 | = 2 ? "OK" : "FALSE: ".count($result[0]); 35 | $countOfErrors = count($result[1]) == 1 ? "OK" : "FALSE: ".count($result[1]); 36 | 37 | echo "Count of results: $countOfResults\n"; 38 | echo "Count of errors: $countOfErrors\n"; 39 | 40 | echo "end\n"; 41 | 42 | ?> 43 | --EXPECT-- 44 | start 45 | Count of results: OK 46 | Count of errors: OK 47 | end -------------------------------------------------------------------------------- /tests/fiber/024-fiber_cancel_during_suspend.phpt: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | Cancel fiber's coroutine while fiber is suspended 3 | --FILE-- 4 | start(); 20 | 21 | // Fiber is suspended, cancel its coroutine 22 | $coro = $fiber->getCoroutine(); 23 | echo "Cancelling coroutine\n"; 24 | $coro->cancel(new CancellationError("test")); 25 | 26 | // Give scheduler a chance 27 | suspend(); 28 | 29 | // Try resume 30 | try { 31 | $fiber->resume(); 32 | } catch (Throwable $e) { 33 | echo "Caught: " . $e->getMessage() . "\n"; 34 | } 35 | }); 36 | 37 | await($c); 38 | echo "OK\n"; 39 | ?> 40 | --EXPECTF-- 41 | Fiber: before suspend 42 | Cancelling coroutine 43 | Caught: Cannot resume a fiber that is not suspended 44 | OK 45 | -------------------------------------------------------------------------------- /tests/spawnWith/004-spawnWith_custom_scope_provider.phpt: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | Async\spawnWith: custom ScopeProvider implementation 3 | --FILE-- 4 | scope = new Scope(); 18 | echo "CustomScopeProvider created\n"; 19 | } 20 | 21 | public function provideScope(): ?Scope 22 | { 23 | echo "provideScope called\n"; 24 | return $this->scope; 25 | } 26 | } 27 | 28 | echo "start\n"; 29 | 30 | $provider = new CustomScopeProvider(); 31 | 32 | $coroutine = spawn_with($provider, function() { 33 | echo "coroutine executed\n"; 34 | return "custom provider result"; 35 | }); 36 | 37 | $result = await($coroutine); 38 | echo "result: $result\n"; 39 | 40 | echo "end\n"; 41 | 42 | ?> 43 | --EXPECT-- 44 | start 45 | CustomScopeProvider created 46 | provideScope called 47 | coroutine executed 48 | result: custom provider result 49 | end -------------------------------------------------------------------------------- /tests/fiber/015-fiber_null_values.phpt: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | Fiber with NULL values in suspend/resume 3 | --FILE-- 4 | start(); 22 | echo "Suspend value is null: " . ($s1 === null ? "Y" : "N") . "\n"; 23 | 24 | $s2 = $fiber->resume(); 25 | echo "Suspend value is null: " . ($s2 === null ? "Y" : "N") . "\n"; 26 | 27 | $r = $fiber->resume(null); 28 | echo "Return value is null: " . ($r === null ? "Y" : "N") . "\n"; 29 | 30 | return "done"; 31 | }); 32 | 33 | await($coroutine); 34 | echo "Test completed\n"; 35 | ?> 36 | --EXPECT-- 37 | Test: NULL values in suspend/resume 38 | Suspend value is null: Y 39 | Resume value is null: Y 40 | Suspend value is null: Y 41 | Return value is null: Y 42 | Test completed 43 | -------------------------------------------------------------------------------- /tests/bailout/009-memory-exhaustion-during-await.phpt: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | Memory exhaustion bailout during await operation 3 | --SKIPIF-- 4 | 10 | --INI-- 11 | memory_limit=2M 12 | --FILE-- 13 | 36 | --EXPECTF-- 37 | Before spawn 38 | Before await 39 | Coroutine started 40 | 41 | Fatal error: Allowed memory size of %d bytes exhausted%s(tried to allocate %d bytes) in %s on line %d 42 | 43 | Warning: Graceful shutdown mode was started in %s on line %d 44 | Shutdown function called -------------------------------------------------------------------------------- /tests/coroutine/035-coroutine_deep_recursion.phpt: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | Coroutine with deep recursion and stack limits 3 | --FILE-- 4 | = $maxDepth) { 17 | echo "reached max depth: $depth\n"; 18 | return $depth; 19 | } 20 | 21 | if ($depth % 20 === 0) { 22 | suspend(); // Suspend periodically 23 | } 24 | 25 | return deepRecursionTest($depth + 1, $maxDepth); 26 | } 27 | 28 | $result = deepRecursionTest(0); 29 | return "recursion_result_$result"; 30 | }); 31 | 32 | $result = await($deep_recursion_coroutine); 33 | echo "deep recursion result: $result\n"; 34 | 35 | echo "end\n"; 36 | 37 | ?> 38 | --EXPECTF-- 39 | start 40 | deep recursion coroutine started 41 | reached max depth: 1000 42 | deep recursion result: recursion_result_1000 43 | end -------------------------------------------------------------------------------- /tests/coroutine/024-coroutine_gc_multiple_zvals.phpt: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | Coroutine: GC handler with multiple ZVALs 3 | --FILE-- 4 | "value1", "key2" => "value2"]; 11 | 12 | $coroutine = spawn(function() use ($test_data) { 13 | // Use all data to ensure they're tracked by GC 14 | $result = []; 15 | foreach ($test_data as $key => $value) { 16 | $result[$key] = $value; 17 | } 18 | return $result; 19 | }); 20 | 21 | // Add finally handler 22 | $coroutine->onFinally(function() { 23 | echo "Finally with data\n"; 24 | }); 25 | 26 | // Force garbage collection 27 | $collected = gc_collect_cycles(); 28 | 29 | suspend(); // Suspend to simulate coroutine lifecycle 30 | 31 | // Get result 32 | $result = $coroutine->getResult(); 33 | var_dump($result); 34 | 35 | // Verify GC was called 36 | var_dump($collected >= 0); 37 | 38 | ?> 39 | --EXPECT-- 40 | Finally with data 41 | array(2) { 42 | ["key1"]=> 43 | string(6) "value1" 44 | ["key2"]=> 45 | string(6) "value2" 46 | } 47 | bool(true) -------------------------------------------------------------------------------- /tests/exec/004-exec_basic.phpt: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | exec() async basic functionality 3 | --SKIPIF-- 4 | 7 | --FILE-- 8 | 38 | --EXPECT-- 39 | Main thread start 40 | Main thread end 41 | Starting async exec test 42 | Other async task executing 43 | Output: Hello from async exec 44 | Return code: 0 45 | Exec test completed successfully -------------------------------------------------------------------------------- /tests/fiber/004-fiber_multiple_suspends.phpt: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | Multiple suspend/resume cycles 3 | --FILE-- 4 | start() . "\n"; 26 | echo "R2: " . $fiber->resume("resume-1") . "\n"; 27 | echo "R3: " . $fiber->resume("resume-2") . "\n"; 28 | echo "R4: " . $fiber->resume("resume-3") . "\n"; 29 | 30 | return "done"; 31 | }); 32 | 33 | await($coroutine); 34 | echo "Test completed\n"; 35 | ?> 36 | --EXPECT-- 37 | Test: Multiple suspend/resume cycles 38 | R1: suspend-1 39 | Got: resume-1 40 | R2: suspend-2 41 | Got: resume-2 42 | R3: suspend-3 43 | Got: resume-3 44 | R4: final 45 | Test completed 46 | -------------------------------------------------------------------------------- /tests/fiber/025-fiber_double_cancel.phpt: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | Double cancel - parent coroutine and fiber's coroutine 3 | --FILE-- 4 | start(); 17 | 18 | // Cancel fiber's coroutine 19 | $fiberCoro = $fiber->getCoroutine(); 20 | $fiberCoro->cancel(new CancellationError("fiber cancel")); 21 | echo "Fiber coroutine cancelled\n"; 22 | 23 | // Try resume 24 | try { 25 | $fiber->resume(); 26 | } catch (Throwable $e) { 27 | echo "Fiber caught: " . $e->getMessage() . "\n"; 28 | } 29 | 30 | return "parent done"; 31 | }); 32 | 33 | // Also cancel parent 34 | $parent->cancel(new CancellationError("parent cancel")); 35 | echo "Parent cancelled\n"; 36 | 37 | try { 38 | await($parent); 39 | } catch (CancellationError $e) { 40 | echo "Parent caught: " . $e->getMessage() . "\n"; 41 | } 42 | 43 | echo "OK\n"; 44 | ?> 45 | --EXPECTF-- 46 | %a 47 | OK 48 | -------------------------------------------------------------------------------- /tests/await/039-awaitAnyOfOrFail_cancellation_timeout.phpt: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | await_any_of_or_fail() - with cancellation timeout 3 | --FILE-- 4 | getMessage() . "\n"; 38 | } 39 | 40 | echo "end\n"; 41 | 42 | ?> 43 | --EXPECT-- 44 | start 45 | Timeout caught as expected 46 | end -------------------------------------------------------------------------------- /tests/exec/008-system_basic.phpt: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | system() async basic functionality 3 | --SKIPIF-- 4 | 7 | --FILE-- 8 | 37 | --EXPECT-- 38 | Main thread start 39 | Main thread end 40 | Starting async system test 41 | Other async task executing 42 | Hello from async system 43 | Output: Hello from async system 44 | Return code: 0 45 | System test completed successfully -------------------------------------------------------------------------------- /tests/sleep/003-time_nanosleep_basic.phpt: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | time_nanosleep() async basic functionality 3 | --SKIPIF-- 4 | 7 | --FILE-- 8 | = 0.2s: " . ($elapsed >= 0.2 ? "yes" : "no") . "\n"; 24 | echo "time_nanosleep test completed\n"; 25 | }); 26 | 27 | spawn(function() { 28 | echo "Other async task executing\n"; 29 | }); 30 | 31 | echo "Main thread end\n"; 32 | ?> 33 | --EXPECT-- 34 | Main thread start 35 | Main thread end 36 | Starting async time_nanosleep test 37 | Other async task executing 38 | time_nanosleep returned: true 39 | Elapsed time >= 0.2s: yes 40 | time_nanosleep test completed -------------------------------------------------------------------------------- /tests/fiber/005-fiber_nested_spawn.phpt: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | Nested spawn inside Fiber 3 | --FILE-- 4 | start(); 30 | echo "Fiber completed: " . $result . "\n"; 31 | 32 | return "outer done"; 33 | }); 34 | 35 | await($coroutine); 36 | echo "Test completed\n"; 37 | ?> 38 | --EXPECT-- 39 | Test: Nested spawn inside Fiber 40 | Fiber: spawning inner coroutine 41 | Inner: started 42 | Inner: resumed 43 | Fiber: inner returned: inner result 44 | Fiber completed: fiber done 45 | Test completed 46 | -------------------------------------------------------------------------------- /tests/fiber/011-fiber_exception_nested_spawn.phpt: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | Exception in nested spawn inside Fiber 3 | --FILE-- 4 | getMessage() . "\n"; 25 | } 26 | 27 | return "fiber recovered"; 28 | }); 29 | 30 | $result = $fiber->start(); 31 | echo "Result: " . $result . "\n"; 32 | 33 | return "done"; 34 | }); 35 | 36 | await($coroutine); 37 | echo "Test completed\n"; 38 | ?> 39 | --EXPECT-- 40 | Test: Exception in nested spawn 41 | Fiber: spawning 42 | Inner: throwing 43 | Caught in fiber: inner exception 44 | Result: fiber recovered 45 | Test completed 46 | -------------------------------------------------------------------------------- /tests/gc/014-gc-after-excaption-in-main.phpt: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | Correct GC behavior when the main coroutine is destroyed due to an exception. 3 | --FILE-- 4 | self = $this; 15 | } 16 | public function __destruct() { 17 | $id = self::$counter++; 18 | printf("%d: Start destruct\n", $id); 19 | printf("%d: End destruct\n", $id); 20 | } 21 | } 22 | 23 | $f = new Fiber(function () { 24 | new Cycle(); 25 | new Cycle(); 26 | gc_collect_cycles(); 27 | }); 28 | 29 | $f->start(); 30 | 31 | new Cycle(); 32 | new Cycle(); 33 | gc_collect_cycles(); 34 | 35 | throw new Exception("Trigger GC"); 36 | 37 | ?> 38 | --EXPECTF-- 39 | 0: Start destruct 40 | 0: End destruct 41 | 1: Start destruct 42 | 1: End destruct 43 | 2: Start destruct 44 | 2: End destruct 45 | 3: Start destruct 46 | 3: End destruct 47 | 48 | Fatal error: Uncaught Exception: Trigger GC in %s:%d 49 | Stack trace: 50 | #0 {main} 51 | thrown in %s on line %d 52 | Shutdown -------------------------------------------------------------------------------- /tests/await/034-awaitAllOrFail_preserve_key_order.phpt: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | await_all_or_fail() - with fillNull parameter 3 | --FILE-- 4 | 47 | --EXPECT-- 48 | start 49 | All expected results found 50 | end -------------------------------------------------------------------------------- /tests/await/035-awaitAll_fillNull.phpt: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | await_all() - with fillNull parameter 3 | --FILE-- 4 | getMessage() . "\n"; 35 | 36 | echo "end\n"; 37 | 38 | ?> 39 | --EXPECT-- 40 | start 41 | Count of results: 3 42 | Count of errors: 1 43 | Result 0: success 44 | Result 1: null 45 | Result 2: another success 46 | Error message: error 47 | end -------------------------------------------------------------------------------- /tests/await/045-awaitAnyOfOrFail_edge_cases.phpt: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | await_any_of_or_fail() - edge cases with count parameter 3 | --FILE-- 4 | 41 | --EXPECT-- 42 | start 43 | Count when requesting more than available: 2 44 | Count when requesting zero: 0 45 | end -------------------------------------------------------------------------------- /tests/stream/016-tcp_stream_socket_accept_timeout.phpt: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | Stream: stream_socket_accept() + timeout 3 | --FILE-- 4 | 38 | --EXPECTF-- 39 | Start 40 | End 41 | Server: starting 42 | Server: listening on port %d 43 | Server: accepting connections 44 | 45 | Warning: stream_socket_accept(): Accept failed: %s 46 | Server end 47 | -------------------------------------------------------------------------------- /tests/await/055-awaitAnyOrFail_generator_exception.phpt: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | await_any_or_fail() - Exception in generator body should stop process immediately 3 | --FILE-- 4 | getMessage() . "\n"; 37 | } 38 | 39 | echo "end\n"; 40 | 41 | ?> 42 | --EXPECT-- 43 | start 44 | Caught exception: Generator exception during iteration 45 | end -------------------------------------------------------------------------------- /tests/fiber/023-fiber_cancel_via_coroutine.phpt: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | Cancel fiber's coroutine via getCoroutine()->cancel() 3 | --FILE-- 4 | start(); 19 | echo "Fiber suspended\n"; 20 | 21 | // Get fiber's coroutine and cancel it 22 | $coro = $fiber->getCoroutine(); 23 | $coro->cancel(new CancellationError("cancelled")); 24 | echo "Coroutine cancelled\n"; 25 | 26 | // Try to resume fiber 27 | try { 28 | $fiber->resume(); 29 | echo "Fiber completed\n"; 30 | } catch (CancellationError $e) { 31 | echo "CancellationError: " . $e->getMessage() . "\n"; 32 | } catch (FiberError $e) { 33 | echo "FiberError: " . $e->getMessage() . "\n"; 34 | } 35 | }); 36 | 37 | await($c); 38 | echo "OK\n"; 39 | ?> 40 | --EXPECTF-- 41 | Fiber started 42 | Fiber suspended 43 | Coroutine cancelled 44 | %a 45 | OK 46 | -------------------------------------------------------------------------------- /tests/coroutine/021-coroutine_gc_with_context.phpt: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | Coroutine: GC handler with context data 3 | --FILE-- 4 | set("string_key", "string_value"); 13 | 14 | // Test object key as well 15 | $obj_key = new stdClass(); 16 | $context->set($obj_key, "object_value"); 17 | 18 | $coroutine = spawn(function() use ($context, $obj_key) { 19 | // Access context to ensure it's tracked by GC 20 | $string_val = $context->get("string_key"); 21 | $obj_val = $context->get($obj_key); 22 | return [$string_val, $obj_val]; 23 | }); 24 | 25 | // Force garbage collection to test context ZVAL tracking 26 | $collected = gc_collect_cycles(); 27 | 28 | suspend(); // Suspend to simulate coroutine lifecycle 29 | 30 | // Get result 31 | $result = $coroutine->getResult(); 32 | var_dump($result); 33 | 34 | // Verify GC was called 35 | var_dump($collected >= 0); 36 | 37 | ?> 38 | --EXPECT-- 39 | array(2) { 40 | [0]=> 41 | string(12) "string_value" 42 | [1]=> 43 | string(12) "object_value" 44 | } 45 | bool(true) -------------------------------------------------------------------------------- /tests/await/018-awaitAllOrFail_double_free.phpt: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | await_all_or_fail() - test for double free issue with many coroutines 3 | --FILE-- 4 | 40 | --EXPECT-- 41 | start 42 | Count of results: OK 43 | Non-null results: OK 44 | end -------------------------------------------------------------------------------- /tests/bailout/002-memory-exhaustion-nested.phpt: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | Memory exhaustion bailout in nested async operations 3 | --SKIPIF-- 4 | 10 | --INI-- 11 | memory_limit=2M 12 | --FILE-- 13 | 38 | --EXPECTF-- 39 | Before spawn 40 | After spawn 41 | Outer async started 42 | Outer async continues 43 | Inner async started 44 | 45 | Fatal error: Allowed memory size of %d bytes exhausted%s(tried to allocate %d bytes) in %s on line %d 46 | 47 | Warning: Graceful shutdown mode was started in %s on line %d 48 | Shutdown function called -------------------------------------------------------------------------------- /tests/stream/001-fread_fwrite_simple.phpt: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | Simple fread/fwrite test with two coroutines 3 | --SKIPIF-- 4 | 11 | --FILE-- 12 | 42 | --EXPECT-- 43 | Start 44 | Writer: about to write 45 | Reader: about to read 46 | Writer: wrote data 47 | Reader: read 'hello' 48 | End -------------------------------------------------------------------------------- /tests/stream/ssl_test_cert.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN CERTIFICATE----- 2 | MIIDEzCCAfugAwIBAgIUM6QzqVbtcWFuFC3tUezaYP0p/WkwDQYJKoZIhvcNAQEL 3 | BQAwGTEXMBUGA1UEAwwOYXN5bmMtdGVzdC5kZXYwHhcNMjUwOTEwMjAxNjQzWhcN 4 | MjYwOTEwMjAxNjQzWjAZMRcwFQYDVQQDDA5hc3luYy10ZXN0LmRldjCCASIwDQYJ 5 | KoZIhvcNAQEBBQADggEPADCCAQoCggEBALS+/rtv9RfG2G+JJrX3IVoTKWcHMrld 6 | yn7qP0oFDW8epymmKjmmnCH7Y4t9i/3U6e+wPlpiO6xGcLOyRgqyv0X/FvDACtM1 7 | ymFXI2kIjySIcaa5yyESMFuR13xLXRqaYIiz68uK1ttjR4XFZADSIUC0QJ3S6caY 8 | GwXBcUTOoPFxDPA5luB7gOSRavniGw/EU/ZC4FgV7qxo64CHbDZZBMWWlganPSh8 9 | DBO4CHQO5ZtoFlHMPktzHFZFDyZaZNhtuibqg8DNNW21YkfpGQWmgk3J2/3bGdoh 10 | TQ9nGWQELndRi+0npGkVb5DXrRyz/ChlzhPlNjB2wPr2m6Xvz8y8Om0CAwEAAaNT 11 | MFEwHQYDVR0OBBYEFN++t8je1cBxmZ72HtaSsQbAB4sJMB8GA1UdIwQYMBaAFN++ 12 | t8je1cBxmZ72HtaSsQbAB4sJMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZIhvcNAQEL 13 | BQADggEBAG1+/NXvBiADSFigpYbGNPZfNsBLEOsIr7FyeIDVJm9wgf6HHUdRcif5 14 | O7iROqMzxoaxDkeNvma39VWcAVEaNqG+HVD+grRdWEvTqJT55hcTlCn/RSaTpPMB 15 | QcgS2h/el+VlHMBo1MozD5+5XeNfyk1zGsU/YH4I1ffWc+uP8l68Vr8Li71e2Ldv 16 | ZL8FITD5e3oKj5p2G9qb1bqadZqvGaPfHRgElk8MPDCGzHmJynN6d+W0gMltM9CP 17 | KLueRgg/K677uCvGPJP3jjBqPr4FgpmnZXsLArzl9PiLrJJ/M6IDmKFLIv0Cu9Nf 18 | uLR0cglXQ2Tq5SvmfIj03jS7R16Gy1U= 19 | -----END CERTIFICATE----- -------------------------------------------------------------------------------- /internal/tests/circular_buffer_test.h: -------------------------------------------------------------------------------- 1 | /* 2 | +----------------------------------------------------------------------+ 3 | | Copyright (c) The PHP Group | 4 | +----------------------------------------------------------------------+ 5 | | This source file is subject to version 3.01 of the PHP license, | 6 | | that is bundled with this package in the file LICENSE, and is | 7 | | available through the world-wide-web at the following url: | 8 | | https://www.php.net/license/3_01.txt | 9 | | If you did not receive a copy of the PHP license and are unable to | 10 | | obtain it through the world-wide-web, please send a note to | 11 | | license@php.net so we can mail you a copy immediately. | 12 | +----------------------------------------------------------------------+ 13 | | Author: Edmond | 14 | +----------------------------------------------------------------------+ 15 | */ 16 | #ifndef CIRCULAR_BUFFER_TEST_H 17 | #define CIRCULAR_BUFFER_TEST_H 18 | 19 | void circular_buffer_run(void); 20 | 21 | #endif // CIRCULAR_BUFFER_TEST_H 22 | -------------------------------------------------------------------------------- /tests/bailout/005-memory-exhaustion-during-suspend.phpt: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | Memory exhaustion bailout during suspend operation 3 | --SKIPIF-- 4 | 10 | --INI-- 11 | memory_limit=2M 12 | --FILE-- 13 | 39 | --EXPECTF-- 40 | Before spawn 41 | After spawn 42 | Before suspend 43 | Other coroutine running 44 | After suspend 45 | 46 | Fatal error: Allowed memory size of %d bytes exhausted%s(tried to allocate %d bytes) in %s on line %d 47 | 48 | Warning: Graceful shutdown mode was started in %s on line %d 49 | Shutdown function called 50 | -------------------------------------------------------------------------------- /tests/dns/013-dns_unix_specific.phpt: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | DNS basic hostname resolution in async context 3 | --SKIPIF-- 4 | 9 | --FILE-- 10 | $ip\n"; 21 | 22 | // Test case sensitivity 23 | $variations = ['localhost', 'LOCALHOST', 'LocalHost']; 24 | $case_sensitive = false; 25 | 26 | foreach ($variations as $var) { 27 | $ip = gethostbyname($var); 28 | if ($ip !== $var && $ip === '127.0.0.1') { 29 | continue; 30 | } elseif ($ip === $var) { 31 | $case_sensitive = true; 32 | break; 33 | } 34 | } 35 | 36 | echo "Case sensitivity detected: " . ($case_sensitive ? 'yes' : 'no') . "\n"; 37 | }); 38 | 39 | await($coroutine); 40 | 41 | ?> 42 | --EXPECTF-- 43 | Testing basic DNS hostname resolution 44 | localhost -> 127.0.0.1 45 | Case sensitivity detected: %s -------------------------------------------------------------------------------- /tests/bailout/008-memory-exhaustion-in-shutdown.phpt: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | Memory exhaustion bailout in shutdown function with async 3 | --SKIPIF-- 4 | 10 | --INI-- 11 | memory_limit=2M 12 | --FILE-- 13 | 36 | --EXPECTF-- 37 | Before spawn 38 | Script ending 39 | Regular async operation 40 | Shutdown function started 41 | Shutdown function continues 42 | Async in shutdown started 43 | 44 | Fatal error: Allowed memory size of %d bytes exhausted%s(tried to allocate %d bytes) in %s on line %d 45 | 46 | Warning: Graceful shutdown mode was started in %s on line %d -------------------------------------------------------------------------------- /tests/bailout/011-stack-overflow-during-await.phpt: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | Stack overflow bailout during await operation 3 | --INI-- 4 | opcache.jit_hot_func=0 5 | --SKIPIF-- 6 | 12 | --FILE-- 13 | 40 | --EXPECTF-- 41 | Before spawn 42 | Before await 43 | Coroutine started 44 | 45 | Fatal error: Allowed memory size of %d bytes exhausted%s(tried to allocate %d bytes) in %s on line %d 46 | 47 | Warning: Graceful shutdown mode was started in %s on line %d 48 | Shutdown function called 49 | -------------------------------------------------------------------------------- /tests/output_buffer/002-multiple_coroutines_isolation.phpt: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | Output Buffer: Multiple coroutines with independent buffers 3 | --FILE-- 4 | 50 | --EXPECT-- 51 | Start 52 | End 53 | A got: 'Coroutine A: part1 part2' 54 | B got: 'Coroutine B: part1 part2' 55 | C got: 'Coroutine C: part1 part2' -------------------------------------------------------------------------------- /.github/scripts/windows/find-vs-toolset.bat: -------------------------------------------------------------------------------- 1 | @echo off 2 | 3 | setlocal enabledelayedexpansion 4 | 5 | if "%~1"=="" ( 6 | echo ERROR: Usage: %~nx0 [vc14^|vc15^|vs16^|vs17] 7 | exit /b 1 8 | ) 9 | 10 | set "toolsets_vc14=14.0" 11 | set "toolsets_vc15=" 12 | set "toolsets_vs16=" 13 | set "toolsets_vs17=" 14 | 15 | 16 | for /f "usebackq tokens=*" %%I in (`vswhere.exe -latest -find "VC\Tools\MSVC"`) do set "MSVCDIR=%%I" 17 | 18 | if not defined MSVCDIR ( 19 | echo ERROR: could not locate VC\Tools\MSVC 20 | exit /b 1 21 | ) 22 | 23 | for /f "delims=" %%D in ('dir /b /ad "%MSVCDIR%"') do ( 24 | for /f "tokens=1,2 delims=." %%A in ("%%D") do ( 25 | set "maj=%%A" & set "min=%%B" 26 | if "!maj!"=="14" ( 27 | if !min! LEQ 9 ( 28 | set "toolsets_vc14=%%D" 29 | ) else if !min! LEQ 19 ( 30 | set "toolsets_vc15=%%D" 31 | ) else if !min! LEQ 29 ( 32 | set "toolsets_vs16=%%D" 33 | ) else ( 34 | set "toolsets_vs17=%%D" 35 | ) 36 | ) 37 | ) 38 | ) 39 | 40 | set "KEY=%~1" 41 | set "VAR=toolsets_%KEY%" 42 | call set "RESULT=%%%VAR%%%" 43 | if defined RESULT ( 44 | echo %RESULT% 45 | exit /b 0 46 | ) else ( 47 | echo ERROR: no toolset found for %KEY% 48 | exit /b 1 49 | ) 50 | -------------------------------------------------------------------------------- /tests/fiber/006-fiber_nested_fibers.phpt: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | Nested Fibers (Fiber inside Fiber) 3 | --FILE-- 4 | start(); 23 | echo "Inner suspended: " . $val . "\n"; 24 | 25 | $result = $inner->resume(); 26 | echo "Inner result: " . $result . "\n"; 27 | 28 | return "outer done"; 29 | }); 30 | 31 | $result = $outer->start(); 32 | echo "Outer result: " . $result . "\n"; 33 | 34 | return "complete"; 35 | }); 36 | 37 | await($coroutine); 38 | echo "Test completed\n"; 39 | ?> 40 | --EXPECT-- 41 | Test: Nested Fibers 42 | Outer fiber started 43 | Inner fiber started 44 | Inner suspended: inner suspend 45 | Inner fiber resumed 46 | Inner result: inner done 47 | Outer result: outer done 48 | Test completed 49 | -------------------------------------------------------------------------------- /tests/stream/002-fwrite_simple.phpt: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | Simple fwrite test with coroutine switching 3 | --SKIPIF-- 4 | 11 | --FILE-- 12 | 45 | --EXPECT-- 46 | Start 47 | Writer: writing data 48 | Worker: doing work 49 | Worker: finished work 50 | Writer: done writing 51 | Read: 'test message' 52 | End -------------------------------------------------------------------------------- /tests/stream/008-socket_stream_basic.phpt: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | Poll2 async: Basic socket stream operations in coroutine context 3 | --FILE-- 4 | 43 | --EXPECT-- 44 | Before spawn 45 | Creating socket pair 46 | Writing to socket 47 | Written: 12 bytes 48 | Reading from socket 49 | Read: 'test message' 50 | Result: socket test completed 51 | After spawn -------------------------------------------------------------------------------- /tests/fiber/027-fiber_cancel_in_nested_spawn.phpt: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | Cancel fiber's coroutine from nested spawn 3 | --FILE-- 4 | start(); 20 | $fiberCoro = $fiber->getCoroutine(); 21 | 22 | // Nested coroutine cancels fiber's coroutine 23 | $inner = spawn(function() use ($fiberCoro) { 24 | echo "Inner: cancelling fiber coroutine\n"; 25 | $fiberCoro->cancel(new CancellationError("nested cancel")); 26 | }); 27 | 28 | await($inner); 29 | suspend(); 30 | 31 | // Try to use fiber 32 | try { 33 | $fiber->resume(); 34 | } catch (Throwable $e) { 35 | echo "Caught: " . $e->getMessage() . "\n"; 36 | } 37 | 38 | return "outer done"; 39 | }); 40 | 41 | $result = await($outer); 42 | echo "Result: {$result}\n"; 43 | echo "OK\n"; 44 | ?> 45 | --EXPECTF-- 46 | Fiber running 47 | Inner: cancelling fiber coroutine 48 | %a 49 | OK 50 | -------------------------------------------------------------------------------- /tests/await/044-awaitAllOrFail_empty_iterable.phpt: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | await_all_or_fail() - with empty iterable 3 | --FILE-- 4 | 39 | --EXPECT-- 40 | start 41 | Empty array count: 0 42 | Empty array type: array 43 | Empty ArrayObject count: 0 44 | Empty ArrayObject type: array 45 | Empty generator count: 0 46 | Empty generator type: array 47 | end -------------------------------------------------------------------------------- /tests/bailout/004-stack-overflow-nested.phpt: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | Stack overflow bailout in nested async operations 3 | --INI-- 4 | opcache.jit_hot_func=0 5 | --SKIPIF-- 6 | 12 | --FILE-- 13 | 42 | --EXPECTF-- 43 | Before spawn 44 | After spawn 45 | Outer async started 46 | Outer async continues 47 | Inner async started 48 | 49 | Fatal error: Allowed memory size of %d bytes exhausted%s(tried to allocate %d bytes) in %s on line %d 50 | 51 | Warning: Graceful shutdown mode was started in %s on line %d 52 | Shutdown function called 53 | -------------------------------------------------------------------------------- /tests/stream/003-file_get_contents_http.phpt: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | file_get_contents with HTTP stream and coroutine switching 3 | --INI-- 4 | allow_url_fopen=1 5 | --SKIPIF-- 6 | 11 | --FILE-- 12 | 41 | --EXPECT-- 42 | Start 43 | HTTP: starting request 44 | Worker: working while HTTP request is made 45 | Worker: still working 46 | Worker: finished 47 | HTTP: got response: 'Hello world' 48 | End -------------------------------------------------------------------------------- /tests/stream/tcp_client_disconnect.php: -------------------------------------------------------------------------------- 1 | 5 | */ 6 | 7 | if ($argc < 2) { 8 | echo "Usage: php tcp_client_disconnect.php \n"; 9 | exit(1); 10 | } 11 | 12 | $port = (int)$argv[1]; 13 | 14 | echo "Client process: connecting to port $port\n"; 15 | 16 | // Set appropriate timeout for different platforms 17 | $timeout = (PHP_OS_FAMILY === 'Windows') ? 5 : 2; 18 | 19 | $client = stream_socket_client("tcp://127.0.0.1:$port", $errno, $errstr, $timeout); 20 | if (!$client) { 21 | echo "Client process: failed to connect: $errstr ($errno)\n"; 22 | exit(1); 23 | } 24 | 25 | echo "Client process: connected, sending data\n"; 26 | fwrite($client, "Hello from external process\n"); 27 | fflush($client); 28 | 29 | // Pause to simulate processing - longer on Windows for stability 30 | $pause_time = (PHP_OS_FAMILY === 'Windows') ? 150000 : 100000; 31 | usleep($pause_time); 32 | 33 | echo "Client process: closing connection abruptly\n"; 34 | fclose($client); 35 | 36 | // On Windows, give extra time for cleanup 37 | if (PHP_OS_FAMILY === 'Windows') { 38 | usleep(50000); // 50ms additional cleanup time 39 | } 40 | 41 | echo "Client process: exited\n"; 42 | exit(0); -------------------------------------------------------------------------------- /tests/await/048-awaitAllOrFail_concurrent_generator.phpt: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | await_all_or_fail() - With concurrent generator using suspend() in body 3 | --FILE-- 4 | $value); 16 | } 17 | } 18 | 19 | echo "start\n"; 20 | 21 | $values = ["first", "second", "third"]; 22 | $generator = concurrentGenerator($values); 23 | 24 | spawn(function() { 25 | // Simulate some processing 26 | for ($i = 1; $i <= 5; $i++) { 27 | echo "Processing item $i\n"; 28 | suspend(); 29 | } 30 | }); 31 | 32 | $results = await_all_or_fail($generator); 33 | 34 | echo "Results: " . implode(", ", $results) . "\n"; 35 | echo "Count: " . count($results) . "\n"; 36 | echo "end\n"; 37 | 38 | ?> 39 | --EXPECT-- 40 | start 41 | Processing item 1 42 | Processing item 2 43 | Yielding item: first 44 | Processing item 3 45 | Yielding item: second 46 | Processing item 4 47 | Yielding item: third 48 | Processing item 5 49 | Results: first, second, third 50 | Count: 3 51 | end -------------------------------------------------------------------------------- /tests/stream/019-stream_select_closed_streams.phpt: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | stream_select with closed streams 3 | --FILE-- 4 | 44 | --EXPECTF-- 45 | Testing stream_select with closed streams 46 | Result: %d 47 | Read array count: %d 48 | Result: closed streams test completed -------------------------------------------------------------------------------- /tests/stream/022-stream_select_write_ready.phpt: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | stream_select with write-ready streams 3 | --FILE-- 4 | $stream) { 30 | echo "Stream $i is write-ready\n"; 31 | } 32 | 33 | fclose($sock1); 34 | fclose($sock2); 35 | 36 | return "write ready test completed"; 37 | }); 38 | 39 | $result = await($coroutine); 40 | echo "Result: $result\n"; 41 | 42 | ?> 43 | --EXPECTF-- 44 | Testing stream_select write-ready streams 45 | Write-ready streams: %d 46 | Write array count: %d 47 | %a 48 | Result: write ready test completed -------------------------------------------------------------------------------- /tests/await/056-awaitFirstSuccess_generator_exception.phpt: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | await_first_success() - Exception in generator body should stop process immediately 3 | --FILE-- 4 | getMessage() . "\n"; 38 | } 39 | 40 | echo "end\n"; 41 | 42 | ?> 43 | --EXPECT-- 44 | start 45 | Caught exception: Generator exception during iteration 46 | end -------------------------------------------------------------------------------- /tests/await/012-awaitAll_basic.phpt: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | await_all() - basic usage with mixed success and error 3 | --FILE-- 4 | 28 | --EXPECTF-- 29 | start 30 | array(2) { 31 | [0]=> 32 | array(2) { 33 | [0]=> 34 | string(5) "first" 35 | [2]=> 36 | string(5) "third" 37 | } 38 | [1]=> 39 | array(1) { 40 | [1]=> 41 | object(RuntimeException)#%d (7) { 42 | ["message":protected]=> 43 | string(14) "test exception" 44 | ["string":"Exception":private]=> 45 | string(0) "" 46 | ["code":protected]=> 47 | int(0) 48 | ["file":protected]=> 49 | string(%d) "%s" 50 | ["line":protected]=> 51 | int(%d) 52 | ["trace":"Exception":private]=> 53 | array(%d) { 54 | %a 55 | } 56 | ["previous":"Exception":private]=> 57 | NULL 58 | } 59 | } 60 | } 61 | end -------------------------------------------------------------------------------- /tests/bailout/014-stack-overflow-with-finally.phpt: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | Stack overflow bailout with onFinally handlers 3 | --INI-- 4 | opcache.jit_hot_func=0 5 | --SKIPIF-- 6 | 12 | --FILE-- 13 | onFinally(function() { 31 | echo "Finally handler executed\n"; 32 | }); 33 | 34 | $coroutine = $scope->spawn(function() { 35 | echo "Before stack overflow\n"; 36 | deepRecursion(); 37 | echo "After stack overflow (should not reach)\n"; 38 | return "result"; 39 | }); 40 | 41 | echo "After spawn\n"; 42 | 43 | ?> 44 | --EXPECTF-- 45 | Before scope 46 | After spawn 47 | Before stack overflow 48 | 49 | Fatal error: Allowed memory size of %d bytes exhausted%s(tried to allocate %d bytes) in %s on line %d 50 | 51 | Warning: Graceful shutdown mode was started in %s on line %d 52 | Shutdown function called 53 | Finally handler executed 54 | -------------------------------------------------------------------------------- /tests/await/043-awaitAnyOfOrFail_associative_array.phpt: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | await_any_of_or_fail() - with associative array 3 | --FILE-- 4 | spawn(function() { 13 | suspend(); 14 | return "slow task"; 15 | }), 16 | 17 | 'fast' => spawn(function() { 18 | return "fast task"; 19 | }), 20 | 21 | 'medium' => spawn(function() { 22 | return "medium task"; 23 | }), 24 | 25 | 'very_slow' => spawn(function() { 26 | suspend(); 27 | return "very slow task"; 28 | }) 29 | ]; 30 | 31 | echo "start\n"; 32 | 33 | $results = await_any_of_or_fail(3, $coroutines); 34 | 35 | echo "Keys preserved: " . (count(array_intersect(array_keys($results), ['slow', 'fast', 'medium', 'very_slow'])) == count($results) ? "YES" : "NO") . "\n"; 36 | 37 | // The fastest should complete first 38 | $keys = array_keys($results); 39 | echo "First completed key: {$keys[0]}\n"; 40 | echo "First completed value: {$results[$keys[0]]}\n"; 41 | 42 | echo "end\n"; 43 | 44 | ?> 45 | --EXPECT-- 46 | start 47 | Keys preserved: YES 48 | First completed key: slow 49 | First completed value: slow task 50 | end -------------------------------------------------------------------------------- /tests/coroutine/036-coroutine_gc_circular_finally.phpt: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | Coroutine: Circular references between coroutines and finally handlers 3 | --FILE-- 4 | coroutine = $coroutine; 19 | 20 | $coroutine->onFinally(function() use ($data) { 21 | echo "circular finally executed\n"; 22 | $data->cleanup = "done"; 23 | // $data holds reference to coroutine, creating cycle 24 | }); 25 | 26 | suspend(); 27 | return "circular_result"; 28 | }); 29 | 30 | await($circular_finally_coroutine); 31 | $result = $circular_finally_coroutine->getResult(); 32 | echo "circular result: $result\n"; 33 | 34 | // Force garbage collection 35 | gc_collect_cycles(); 36 | echo "gc after circular references\n"; 37 | 38 | echo "end\n"; 39 | 40 | ?> 41 | --EXPECTF-- 42 | start 43 | circular finally coroutine started 44 | circular finally executed 45 | circular result: circular_result 46 | gc after circular references 47 | end -------------------------------------------------------------------------------- /tests/pdo_mysql/001-pdo_connection_basic.phpt: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | PDO MySQL: Basic async connection test 3 | --EXTENSIONS-- 4 | pdo_mysql 5 | --SKIPIF-- 6 | 12 | --FILE-- 13 | query("SELECT 1 as test"); 30 | $result = $stmt->fetch(PDO::FETCH_ASSOC); 31 | echo "query result: " . $result['test'] . "\n"; 32 | 33 | return "success"; 34 | } catch (Exception $e) { 35 | echo "error: " . $e->getMessage() . "\n"; 36 | return "failed"; 37 | } 38 | }); 39 | 40 | $result = await($coroutine); 41 | echo "awaited: " . $result . "\n"; 42 | echo "end\n"; 43 | 44 | ?> 45 | --EXPECT-- 46 | start 47 | connected 48 | query result: 1 49 | awaited: success 50 | end -------------------------------------------------------------------------------- /tests/stream/021-stream_select_microsecond_timeout.phpt: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | stream_select with microsecond timeout precision 3 | --FILE-- 4 | 42 | --EXPECTF-- 43 | Testing stream_select with microsecond timeout 44 | Result: 0 45 | Elapsed time: %sms 46 | Result: microsecond timeout test completed -------------------------------------------------------------------------------- /tests/coroutine/009-coroutine_getTrace.phpt: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | Coroutine: getTrace() - returns null for non-suspended coroutine 3 | --FILE-- 4 | getTrace(); 15 | echo "Trace before start is null: " . ($traceBeforeStart === null ? "yes" : "no") . "\n"; 16 | 17 | // Wait for coroutine to complete 18 | await($coroutine); 19 | 20 | // After completion, trace should be null 21 | $trace = $coroutine->getTrace(); 22 | echo "Trace after completion is null: " . ($trace === null ? "yes" : "no") . "\n"; 23 | 24 | // Test with options parameter - should still return null 25 | $trace2 = $coroutine->getTrace(DEBUG_BACKTRACE_IGNORE_ARGS); 26 | echo "Trace with IGNORE_ARGS is null: " . ($trace2 === null ? "yes" : "no") . "\n"; 27 | 28 | // Test with limit parameter - should still return null 29 | $trace3 = $coroutine->getTrace(DEBUG_BACKTRACE_PROVIDE_OBJECT, 5); 30 | echo "Trace with limit is null: " . ($trace3 === null ? "yes" : "no") . "\n"; 31 | 32 | ?> 33 | --EXPECT-- 34 | Trace before start is null: yes 35 | Trace after completion is null: yes 36 | Trace with IGNORE_ARGS is null: yes 37 | Trace with limit is null: yes -------------------------------------------------------------------------------- /tests/fiber/018-fiber_coroutine_gc_cycles.phpt: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | Fiber and Coroutine GC with circular references 3 | --FILE-- 4 | data = str_repeat("x", 1000); 16 | } 17 | } 18 | 19 | $c = spawn(function() { 20 | $node1 = new Node(); 21 | $node2 = new Node(); 22 | 23 | // Create circular reference 24 | $node1->ref = $node2; 25 | $node2->ref = $node1; 26 | 27 | // Store in fiber 28 | $node1->fiber = new Fiber(function() use ($node1, $node2) { 29 | echo "F-start\n"; 30 | Fiber::suspend($node2); 31 | echo "F-resume\n"; 32 | return $node1; 33 | }); 34 | 35 | $result = $node1->fiber->start(); 36 | echo "Got: " . ($result === $node2 ? "node2" : "other") . "\n"; 37 | 38 | $result = $node1->fiber->resume(); 39 | echo "Got: " . ($result === $node1 ? "node1" : "other") . "\n"; 40 | 41 | // Break references and trigger GC 42 | $node1 = null; 43 | $node2 = null; 44 | }); 45 | 46 | await($c); 47 | gc_collect_cycles(); 48 | 49 | echo "OK\n"; 50 | ?> 51 | --EXPECT-- 52 | F-start 53 | Got: node2 54 | F-resume 55 | Got: node1 56 | OK 57 | -------------------------------------------------------------------------------- /tests/await/042-awaitAll_associative_array.phpt: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | await_all() - with associative array 3 | --FILE-- 4 | spawn(function() { 12 | return "first success"; 13 | }), 14 | 15 | 'error1' => spawn(function() { 16 | throw new RuntimeException("first error"); 17 | }), 18 | 19 | 'success2' => spawn(function() { 20 | return "second success"; 21 | }) 22 | ]; 23 | 24 | echo "start\n"; 25 | 26 | $result = await_all($coroutines); 27 | 28 | echo "Count of results: " . count($result[0]) . "\n"; 29 | echo "Count of errors: " . count($result[1]) . "\n"; 30 | echo "Result keys: " . implode(', ', array_keys($result[0])) . "\n"; 31 | echo "Error keys: " . implode(', ', array_keys($result[1])) . "\n"; 32 | echo "Result success1: {$result[0]['success1']}\n"; 33 | echo "Result success2: {$result[0]['success2']}\n"; 34 | echo "Error error1: {$result[1]['error1']->getMessage()}\n"; 35 | echo "end\n"; 36 | 37 | ?> 38 | --EXPECT-- 39 | start 40 | Count of results: 2 41 | Count of errors: 1 42 | Result keys: success1, success2 43 | Error keys: error1 44 | Result success1: first success 45 | Result success2: second success 46 | Error error1: first error 47 | end -------------------------------------------------------------------------------- /tests/await/060-await_empty_iterable_edge_cases.phpt: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | await_all() - empty iterators basic functionality 3 | --FILE-- 4 | 37 | --EXPECT-- 38 | start 39 | EmptyIterator count: 0 40 | EmptyIterator type: array 41 | Empty SplFixedArray count: 0 42 | CustomEmptyIterator count: 0 43 | end -------------------------------------------------------------------------------- /tests/scope/035-scope_self_cancellation.phpt: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | Scope: Coroutine cancelling its own scope 3 | --FILE-- 4 | asNotSafely(); 13 | 14 | $self_cancelling = $scope->spawn(function() use ($scope) { 15 | echo "coroutine started\n"; 16 | suspend(); // Let it start properly 17 | 18 | echo "coroutine cancelling its own scope\n"; 19 | $scope->cancel(new \Async\CancellationError("Self-cancellation")); 20 | echo "coroutine end\n"; 21 | }); 22 | 23 | echo "spawned coroutine\n"; 24 | 25 | // Let coroutine start and cancel scope 26 | try { 27 | await($self_cancelling); 28 | } catch (\Async\CancellationError $e) { 29 | echo "caught cancellation in main\n"; 30 | } 31 | 32 | echo "checking final state\n"; 33 | echo "scope cancelled: " . ($scope->isCancelled() ? "true" : "false") . "\n"; 34 | echo "coroutine cancelled: " . ($self_cancelling->isCancelled() ? "true" : "false") . "\n"; 35 | 36 | echo "end\n"; 37 | 38 | ?> 39 | --EXPECT-- 40 | start 41 | spawned coroutine 42 | coroutine started 43 | coroutine cancelling its own scope 44 | coroutine end 45 | caught cancellation in main 46 | checking final state 47 | scope cancelled: true 48 | coroutine cancelled: true 49 | end -------------------------------------------------------------------------------- /tests/stream/020-stream_select_zero_timeout.phpt: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | stream_select with zero timeout (immediate return) 3 | --FILE-- 4 | 43 | --EXPECTF-- 44 | Testing stream_select with zero timeout 45 | Result: 0 46 | Elapsed time: %sms 47 | Read array count: 0 48 | Result: zero timeout test completed -------------------------------------------------------------------------------- /tests/bailout/007-stack-overflow-with-suspend.phpt: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | Stack overflow bailout with suspend in recursion 3 | --INI-- 4 | opcache.jit_hot_func=0 5 | --SKIPIF-- 6 | 12 | --FILE-- 13 | 44 | --EXPECTF-- 45 | Before spawn 46 | After spawn 47 | Before stack overflow with suspend 48 | Other coroutine running 49 | 50 | Fatal error: Allowed memory size of %d bytes exhausted%s(tried to allocate %d bytes) in %s on line %d 51 | 52 | Warning: Graceful shutdown mode was started in %s on line %d 53 | Shutdown function called 54 | -------------------------------------------------------------------------------- /tests/bailout/013-memory-exhaustion-with-finally.phpt: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | Memory exhaustion bailout with onFinally handlers 3 | --SKIPIF-- 4 | 10 | --INI-- 11 | memory_limit=2M 12 | --FILE-- 13 | onFinally(function() { 27 | echo "Finally handler 1 executed\n"; 28 | }); 29 | 30 | $scope->onFinally(function() { 31 | echo "Finally handler 2 executed\n"; 32 | }); 33 | 34 | $coroutine = $scope->spawn(function() { 35 | echo "Before memory exhaustion\n"; 36 | str_repeat('x', 10000000); 37 | echo "After memory exhaustion (should not reach)\n"; 38 | return "result"; 39 | }); 40 | 41 | echo "After spawn\n"; 42 | 43 | ?> 44 | --EXPECTF-- 45 | Before scope 46 | After spawn 47 | Before memory exhaustion 48 | 49 | Fatal error: Allowed memory size of %d bytes exhausted%s(tried to allocate %d bytes) in %s on line %d 50 | 51 | Warning: Graceful shutdown mode was started in %s on line %d 52 | Shutdown function called 53 | Finally handler 1 executed 54 | Finally handler 2 executed -------------------------------------------------------------------------------- /tests/context/README.md: -------------------------------------------------------------------------------- 1 | # Context Tests 2 | 3 | This directory contains tests for the Context API functionality. 4 | 5 | ## Test Coverage 6 | 7 | ### 001-context_basic.phpt 8 | - Basic Context creation and operations 9 | - String key storage and retrieval 10 | - Object key storage and retrieval 11 | - has/hasLocal methods 12 | - unset method 13 | - Replace parameter in set method 14 | 15 | ### 002-context_inheritance.phpt 16 | - Context inheritance through coroutines 17 | - Parent-child context relationship 18 | - Local vs inherited values 19 | - Nested coroutine context propagation 20 | 21 | ### 003-coroutine_getContext.phpt 22 | - Coroutine::getContext() method 23 | - Context retrieval from coroutines 24 | - Null context handling 25 | 26 | ### 004-context_error_handling.phpt 27 | - Invalid key type error handling 28 | - Error recovery after exceptions 29 | - Type validation for all methods 30 | 31 | ### 005-context_object_keys.phpt 32 | - Object keys functionality 33 | - Different object types as keys 34 | - Object identity vs equality 35 | - Object key unset operations 36 | 37 | ## Expected Behavior 38 | 39 | Context should provide: 40 | - Thread-local storage for coroutines 41 | - Key-value storage with string and object keys 42 | - Parent-child inheritance through coroutine hierarchy 43 | - Type safety and error handling 44 | - Object lifecycle management for object keys -------------------------------------------------------------------------------- /tests/edge_cases/011-gc_include_symtable_double_delref.phpt: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | GC with include in suspended coroutine - symTable double DELREF 3 | --FILE-- 4 | self = $obj; 22 | } 23 | 24 | // Suspend - coroutine is now in suspended state 25 | suspend(); 26 | 27 | // Include inherits parent symTable 28 | // Bug: GC may add same variables twice -> double DELREF 29 | include __DIR__ . '/011-gc_include_symtable_double_delref_included.inc'; 30 | 31 | echo "parent1: {$parent1}\n"; 32 | echo "parent2: {$parent2}\n"; 33 | echo "included: {$included}\n"; 34 | 35 | return "done"; 36 | }); 37 | 38 | $result = await($coroutine); 39 | echo "result: {$result}\n"; 40 | 41 | } catch (Error $e) { 42 | echo "Error: " . $e->getMessage() . "\n"; 43 | } 44 | 45 | echo "OK\n"; 46 | gc_collect_cycles(); 47 | ?> 48 | --EXPECTF-- 49 | parent1: value1 50 | parent2: value2 51 | included: included_value 52 | result: done 53 | OK 54 | -------------------------------------------------------------------------------- /tests/fiber/007-fiber_concurrent_fibers.phpt: -------------------------------------------------------------------------------- 1 | --TEST-- 2 | Multiple concurrent Fibers 3 | --FILE-- 4 | start(); 29 | $fiber2->start(); 30 | 31 | echo "Resume F2\n"; 32 | $r2 = $fiber2->resume(); 33 | echo "F2 result: " . $r2 . "\n"; 34 | 35 | echo "Resume F1 (1)\n"; 36 | $fiber1->resume(); 37 | 38 | echo "Resume F1 (2)\n"; 39 | $r1 = $fiber1->resume(); 40 | echo "F1 result: " . $r1 . "\n"; 41 | 42 | return "done"; 43 | }); 44 | 45 | await($coroutine); 46 | echo "Test completed\n"; 47 | ?> 48 | --EXPECT-- 49 | Test: Concurrent Fibers 50 | F1: step 1 51 | F2: step 1 52 | Resume F2 53 | F2: step 2 54 | F2 result: fiber2 done 55 | Resume F1 (1) 56 | F1: step 2 57 | Resume F1 (2) 58 | F1: step 3 59 | F1 result: fiber1 done 60 | Test completed 61 | -------------------------------------------------------------------------------- /context.stub.php: -------------------------------------------------------------------------------- 1 | start(); 20 | echo "C1-O-middle\n"; 21 | $inner->resume(); 22 | echo "C1-O-end\n"; 23 | }); 24 | 25 | $outer->start(); 26 | }); 27 | 28 | $c2 = spawn(function() { 29 | $outer = new Fiber(function() { 30 | echo "C2-O-start\n"; 31 | 32 | $inner = new Fiber(function() { 33 | echo "C2-I-start\n"; 34 | Fiber::suspend(); 35 | echo "C2-I-resume\n"; 36 | }); 37 | 38 | $inner->start(); 39 | echo "C2-O-middle\n"; 40 | $inner->resume(); 41 | echo "C2-O-end\n"; 42 | }); 43 | 44 | $outer->start(); 45 | }); 46 | 47 | await($c1); 48 | await($c2); 49 | 50 | echo "OK\n"; 51 | ?> 52 | --EXPECT-- 53 | C1-O-start 54 | C2-O-start 55 | C1-I-start 56 | C2-I-start 57 | C1-O-middle 58 | C2-O-middle 59 | C1-I-resume 60 | C2-I-resume 61 | C1-O-end 62 | C2-O-end 63 | OK 64 | --------------------------------------------------------------------------------