From 2ae0d518805d4fc9f479a759de389db8049e3008 Mon Sep 17 00:00:00 2001 From: PurHur Date: Mon, 25 May 2026 06:08:02 +0000 Subject: [PATCH] Wire 005-SessionsWeb into examples-web-smoke and CI gates (#1887). Add SESSIONS_WEB_SMOKE_GATE (default on) with cookie-jar curls in ci-fast/ci-local, PHPUnit serve flash round-trip, and fix bin/serve.php to persist sessions on exit. Co-authored-by: Cursor --- Makefile | 5 +- README.md | 2 +- bin/serve.php | 12 +- docs/local-ci-matrix.md | 1 + examples/005-SessionsWeb/README.md | 2 +- examples/README.md | 5 +- script/ci-common.sh | 17 ++ script/ci-defaults.env | 1 + script/ci-fast.sh | 2 + script/ci-local.sh | 2 + script/examples-web-smoke.sh | 116 +++++++++++- test/unit/CiScriptsTest.php | 28 ++- test/unit/ExamplesCompileTest.php | 218 +++++++++++++++++++++++ test/unit/ExamplesWebSmokeScriptTest.php | 20 ++- 14 files changed, 422 insertions(+), 9 deletions(-) diff --git a/Makefile b/Makefile index debe6bf63f2..fded5533390 100755 --- a/Makefile +++ b/Makefile @@ -123,10 +123,13 @@ miniwebapp-aot-bisect: ./script/miniwebapp-aot-bisect.sh # HTTP smoke: phpc serve + curl for 001-SimpleWeb and 002-StaticWeb (issue #298) -.PHONY: examples-web-smoke examples-web-smoke-prebuild examples-aot-smoke deploy-smoke +.PHONY: examples-web-smoke examples-sessions-smoke examples-web-smoke-prebuild examples-aot-smoke deploy-smoke examples-web-smoke: ./script/examples-web-smoke.sh +examples-sessions-smoke: + ./script/examples-web-smoke.sh --sessions-only + examples-web-smoke-prebuild: ./script/examples-web-smoke-prebuild.sh diff --git a/README.md b/README.md index 75b1836fa64..4347418dc42 100755 --- a/README.md +++ b/README.md @@ -61,7 +61,7 @@ mkdir my-app && ./phpc init my-app # phpc.json + public/index.php scaffold (se ./phpc run -r 'echo 1;' # VM mode (or: php bin/vm.php -r 'echo 1;') ./phpc run -q 'name=Dev' examples/001-SimpleWeb/example.php # web example without TCP make web-smoke # lint shipped examples + 003-MiniWebApp tree, VM smoke 001-SimpleWeb -make examples-web-smoke # phpc serve + curl for 001-SimpleWeb, 002-StaticWeb, and 004-ApiJson +make examples-web-smoke # phpc serve + curl for 001–004 and 005-SessionsWeb (SESSIONS_WEB_SMOKE_GATE=1 default) make examples-aot-smoke # phpc build + CLI execute for 000–004 when LLVM ready (#667) make deploy-smoke # phpc deploy + PHPC_DEPLOY_ROOT CGI for 001/002 when LLVM ready (#718) ./phpc serve examples/001-SimpleWeb # http://127.0.0.1:8080/ (or: make serve) diff --git a/bin/serve.php b/bin/serve.php index f1f77abe3dc..c837d719981 100755 --- a/bin/serve.php +++ b/bin/serve.php @@ -17,6 +17,7 @@ use PHPCompiler\Runtime; use PHPCompiler\VM\OutputBuffer; +use PHPCompiler\VM\ScriptExit; use PHPCompiler\ext\standard\VmSession; use PHPCompiler\Web\DevServer; use PHPCompiler\Web\ProjectBootstrap; @@ -40,8 +41,8 @@ } ob_start(); + $runtime = new Runtime(); try { - $runtime = new Runtime(); Superglobals::populateFromEnvironment( $runtime->vmContext, $cgiEnv['QUERY_STRING'] ?? '', @@ -52,6 +53,15 @@ $block = $runtime->parseAndCompile($code, $script); $runtime->run($block); $output = ob_get_clean(); + if (VmSession::isActive()) { + VmSession::writeClose($runtime->vmContext); + } + } catch (ScriptExit $e) { + ob_end_clean(); + $output = ''; + if (VmSession::isActive()) { + VmSession::writeClose($runtime->vmContext); + } } catch (\Throwable $e) { ob_end_clean(); throw $e; diff --git a/docs/local-ci-matrix.md b/docs/local-ci-matrix.md index bc43c75e475..2cfbc3ad4c9 100644 --- a/docs/local-ci-matrix.md +++ b/docs/local-ci-matrix.md @@ -82,6 +82,7 @@ Defaults are exported from [`script/ci-defaults.env`](../script/ci-defaults.env) |----------|---------|--------|-------| | `MINIWEBAPP_VM_CLI_GATE` | `1` | `ci-fast.sh` | PHPUnit `MiniWebApp*VmCli` matrix ([#597](https://github.com/PurHur/php-compiler/issues/597)) | | `MINIWEBAPP_SERVE_GATE` | `1` | `ci-local.sh`, `ci-fast.sh` | `ServeTest` `@group miniwebapp` ([#641](https://github.com/PurHur/php-compiler/issues/641)) | +| `SESSIONS_WEB_SMOKE_GATE` | `1` | `ci-fast.sh`, `ci-local.sh` | `examples-web-smoke.sh --sessions-only` / 005 cookie flash curls ([#1887](https://github.com/PurHur/php-compiler/issues/1887)) | | `MINIWEBAPP_WEB_SMOKE_GATE` | `1` | `ci-local.sh` | `examples-web-smoke.sh --miniwebapp-only` ([#664](https://github.com/PurHur/php-compiler/issues/664)) | | `MINIWEBAPP_WEB_SMOKE_AOT_GATE` | `1` | `ci-local.sh` | `ci_run_miniwebapp_web_smoke_aot` → `examples-web-smoke.sh --miniwebapp-only --aot` ([#1523](https://github.com/PurHur/php-compiler/issues/1523), [#833](https://github.com/PurHur/php-compiler/issues/833)) | | `MINIWEBAPP_AOT_LINK_GATE` | `1` | `ci-local.sh` (PHPUnit `@group aot-link`) | `ExamplesCompileTest` 003 native link ([#754](https://github.com/PurHur/php-compiler/issues/754)) | diff --git a/examples/005-SessionsWeb/README.md b/examples/005-SessionsWeb/README.md index bf14ab09fd1..5fe5c6f54f2 100644 --- a/examples/005-SessionsWeb/README.md +++ b/examples/005-SessionsWeb/README.md @@ -34,5 +34,5 @@ The last response should include `Flash: Saved`. ## Related -- [#1887](https://github.com/PurHur/php-compiler/issues/1887) — `examples-web-smoke` gate +- [#1887](https://github.com/PurHur/php-compiler/issues/1887) — `SESSIONS_WEB_SMOKE_GATE=1` (default): `make examples-sessions-smoke`, `ci-fast.sh`, `ExamplesCompileTest::test005SessionsWebServeFlashRoundTrip` - [#1893](https://github.com/PurHur/php-compiler/issues/1893) — deploy + `PHPC_DEPLOY_ROOT` smoke diff --git a/examples/README.md b/examples/README.md index eb0f066597f..528a9f1ea49 100644 --- a/examples/README.md +++ b/examples/README.md @@ -101,7 +101,7 @@ curl -s -b "$jar" -c "$jar" -X POST -d 'message=Saved' 'http://127.0.0.1:8080/ex curl -s -b "$jar" 'http://127.0.0.1:8080/example.php' ``` -AOT link/execute: [#1891](https://github.com/PurHur/php-compiler/issues/1891). `examples-web-smoke` gate: [#1887](https://github.com/PurHur/php-compiler/issues/1887). +AOT link/execute: [#1891](https://github.com/PurHur/php-compiler/issues/1891). VM session curls: `SESSIONS_WEB_SMOKE_GATE=1` (default) in `ci-fast.sh` / `ci-local.sh` — `make examples-sessions-smoke` or `examples-web-smoke.sh --sessions-only` ([#1887](https://github.com/PurHur/php-compiler/issues/1887)). ### 002-StaticWeb @@ -139,7 +139,8 @@ Before a PR that touches examples or `bin/serve.php`: ```console make web-smoke # lint examples/*/example.php + 003 lint --all + VM ?name= smoke -make examples-web-smoke # phpc serve + curl GET/POST (001-SimpleWeb, 002-StaticWeb, 004-ApiJson) +make examples-web-smoke # phpc serve + curl GET/POST (001–004 + 005 session flash when SESSIONS_WEB_SMOKE_GATE=1) +make examples-sessions-smoke # 005-SessionsWeb cookie jar only (#1887) make examples-aot-smoke # phpc build + CLI execute (000–004; skips when LLVM missing; 003 execute green #764) ``` diff --git a/script/ci-common.sh b/script/ci-common.sh index 10a807cb6e4..232000ffaac 100644 --- a/script/ci-common.sh +++ b/script/ci-common.sh @@ -318,6 +318,23 @@ ci_guard_jit_compliance() { "$PHP_BIN" "${PHP_OPTS[@]}" script/check-jit-compliance-ran.php "$junit_path" "$llvm_dir" } +# Shell curl harness for 005-SessionsWeb session flash (issue #1887). +ci_run_sessions_web_smoke() { + if [[ "${SESSIONS_WEB_SMOKE_GATE:-1}" != "1" ]]; then + return 0 + fi + if [[ -n "${PHP_COMPILER_SKIP_SERVE_TESTS:-}" ]]; then + echo "examples-web-smoke (005): skipped (PHP_COMPILER_SKIP_SERVE_TESTS is set)" + return 0 + fi + if ! ci_can_bind_loopback; then + echo "examples-web-smoke (005): skipped (cannot bind loopback TCP)" + return 0 + fi + echo "examples-web-smoke (005): SessionsWeb cookie curls (SESSIONS_WEB_SMOKE_GATE=1 default, #1887)..." + "$_CI_SCRIPT_DIR/examples-web-smoke.sh" --sessions-only +} + # Shell curl harness for 003-MiniWebApp PATH_INFO routes (issue #633). ci_run_miniwebapp_web_smoke() { if [[ -n "${PHP_COMPILER_SKIP_SERVE_TESTS:-}" ]]; then diff --git a/script/ci-defaults.env b/script/ci-defaults.env index 01f502340e9..a0a0880aa30 100755 --- a/script/ci-defaults.env +++ b/script/ci-defaults.env @@ -12,6 +12,7 @@ export PHP_COMPILER_VM_RSS_GUARD="${PHP_COMPILER_VM_RSS_GUARD:-1}" export MINIWEBAPP_SERVE_GATE="${MINIWEBAPP_SERVE_GATE:-1}" export MINIWEBAPP_WEB_SMOKE_GATE="${MINIWEBAPP_WEB_SMOKE_GATE:-1}" export MINIWEBAPP_WEB_SMOKE_AOT_GATE="${MINIWEBAPP_WEB_SMOKE_AOT_GATE:-1}" # default on (#1523, #833) +export SESSIONS_WEB_SMOKE_GATE="${SESSIONS_WEB_SMOKE_GATE:-1}" # default on (#1887); 005-SessionsWeb cookie curls export MINIWEBAPP_AOT_LINK_GATE="${MINIWEBAPP_AOT_LINK_GATE:-1}" export MINIWEBAPP_AOT_EXECUTE_GATE="${MINIWEBAPP_AOT_EXECUTE_GATE:-1}" # default on (#747) export MINIWEBAPP_JIT_PROJECT_GATE="${MINIWEBAPP_JIT_PROJECT_GATE:-0}" # opt-in (#587) diff --git a/script/ci-fast.sh b/script/ci-fast.sh index 07deee87eea..512534e6ae5 100755 --- a/script/ci-fast.sh +++ b/script/ci-fast.sh @@ -42,6 +42,8 @@ if [[ -z "${PHP_COMPILER_SKIP_SERVE_TESTS:-}" ]]; then echo "PHPUnit (fast): MiniWebApp ServeTest (MINIWEBAPP_SERVE_GATE=1 default, #470, #641)..." ci_run_phpunit --filter ServeTest --group miniwebapp --fail-on-skipped "$@" fi + + ci_run_sessions_web_smoke fi # Always lint 003-MiniWebApp even when callers pass --filter (issue #570, #539). diff --git a/script/ci-local.sh b/script/ci-local.sh index 5e9e32240fa..1f41d93d56a 100755 --- a/script/ci-local.sh +++ b/script/ci-local.sh @@ -58,6 +58,8 @@ if [[ -z "${PHP_COMPILER_SKIP_SERVE_TESTS:-}" ]]; then if [[ "${MINIWEBAPP_WEB_SMOKE_GATE:-1}" == "1" ]]; then ci_run_miniwebapp_web_smoke fi + + ci_run_sessions_web_smoke fi if ci_llvm_ready; then diff --git a/script/examples-web-smoke.sh b/script/examples-web-smoke.sh index 0b9d8a49231..1ad4a38e05e 100755 --- a/script/examples-web-smoke.sh +++ b/script/examples-web-smoke.sh @@ -21,27 +21,31 @@ PHPC="${ROOT}/phpc" usage() { cat <<'EOF' -Usage: script/examples-web-smoke.sh [--aot] [--miniwebapp-only] +Usage: script/examples-web-smoke.sh [--aot] [--miniwebapp-only] [--sessions-only] VM mode (default): phpc serve per example docroot. --aot: use phpc serve --aot when /.phpc/bin/app exists; skip otherwise. 003-MiniWebApp: home + hello PATH_INFO + contact POST when stdout has MiniWebApp (#833, #676). --miniwebapp-only: curl only examples/003-MiniWebApp (MINIWEBAPP_WEB_SMOKE_GATE — #633). + --sessions-only: curl only examples/005-SessionsWeb (SESSIONS_WEB_SMOKE_GATE — #1887). Environment: PHP_COMPILER_SKIP_SERVE_TESTS=1 exit 0 without running HTTP checks PHP_COMPILER_MAX_BODY optional; 003 oversized POST check uses 1024 when unset (#705) MINIWEBAPP_AOT_EXECUTE_GATE=1 fail instead of skip when 003 AOT probe empty (#747, #676) MINIWEBAPP_WEB_SMOKE_AOT_GATE=1 require 003 --aot curls to pass (#833) + SESSIONS_WEB_SMOKE_GATE=1 include 005 session flash curls in default run (#1887) EOF } AOT=0 MINIWEBAPP_ONLY=0 +SESSIONS_ONLY=0 while [[ $# -gt 0 ]]; do case "$1" in --aot) AOT=1; shift ;; --miniwebapp-only) MINIWEBAPP_ONLY=1; shift ;; + --sessions-only) SESSIONS_ONLY=1; shift ;; -h|--help) usage; exit 0 ;; *) echo "examples-web-smoke: unknown argument: $1" >&2; usage >&2; exit 1 ;; esac @@ -171,6 +175,56 @@ curl_expect_post_not_200() { echo "examples-web-smoke: ${label}: ok (HTTP ${status})" } +curl_expect_200_cookies() { + local label="$1" + local url="$2" + local jar="$3" + shift 3 + local needles=("$@") + local body status + body="$(mktemp)" + status="$(curl -sS -o "$body" -w '%{http_code}' -b "$jar" -c "$jar" \ + --connect-timeout 5 --max-time 15 "$url" || echo "000")" + if [[ "$status" != "200" ]]; then + echo "examples-web-smoke: ${label}: expected HTTP 200, got ${status}" >&2 + echo " url: ${url}" >&2 + cat "$body" >&2 || true + rm -f "$body" + return 1 + fi + for needle in "${needles[@]}"; do + if ! grep -qF "$needle" "$body"; then + echo "examples-web-smoke: ${label}: response missing needle: ${needle}" >&2 + echo " url: ${url}" >&2 + cat "$body" >&2 || true + rm -f "$body" + return 1 + fi + done + rm -f "$body" + echo "examples-web-smoke: ${label}: ok" +} + +curl_expect_303_post_cookies() { + local label="$1" + local url="$2" + local jar="$3" + local post_body="$4" + local body status + body="$(mktemp)" + status="$(curl -sS -o "$body" -w '%{http_code}' -b "$jar" -c "$jar" \ + -X POST -d "$post_body" --connect-timeout 5 --max-time 15 "$url" || echo "000")" + if [[ "$status" != "303" ]]; then + echo "examples-web-smoke: ${label}: expected HTTP 303, got ${status}" >&2 + echo " url: ${url}" >&2 + cat "$body" >&2 || true + rm -f "$body" + return 1 + fi + rm -f "$body" + echo "examples-web-smoke: ${label}: ok" +} + resolve_llvm_dir() { if [[ -n "${PHP_COMPILER_LLVM_PATH:-}" ]]; then if [[ -f "${PHP_COMPILER_LLVM_PATH}/libLLVM-9.so.1" ]]; then @@ -313,6 +367,55 @@ run_miniwebapp_oversized_post_smoke() { trap - RETURN } +run_sessions_web_smoke() { + local docroot="${ROOT}/examples/005-SessionsWeb" + if [[ ! -d "$docroot" ]]; then + echo "examples-web-smoke: 005-SessionsWeb: skip (missing docroot)" + return 0 + fi + if [[ "$AOT" -eq 1 ]]; then + echo "examples-web-smoke: 005-SessionsWeb: skip --aot (VM only until #1891)" + return 0 + fi + + local port pid jar + jar="$(mktemp)" + port="$(find_free_port)" + echo "examples-web-smoke: 005-SessionsWeb on 127.0.0.1:${port} (VM session flash)" + "${PHPC}" serve "127.0.0.1:${port}" "$docroot" >/dev/null 2>&1 & + pid=$! + + stop_serve() { + kill "$pid" 2>/dev/null || true + local waited=0 + while kill -0 "$pid" 2>/dev/null && ((waited < 40)); do + sleep 0.05 + waited=$((waited + 1)) + done + if kill -0 "$pid" 2>/dev/null; then + kill -9 "$pid" 2>/dev/null || true + fi + wait "$pid" 2>/dev/null || true + } + trap stop_serve RETURN + + wait_for_serve "$port" + + local base="http://127.0.0.1:${port}" + curl_expect_200_cookies "005-SessionsWeb / GET empty" "${base}/example.php" "$jar" \ + "No flash message yet" + curl_expect_303_post_cookies "005-SessionsWeb / POST flash" "${base}/example.php" \ + "$jar" "message=Saved" + curl_expect_200_cookies "005-SessionsWeb / GET flash" "${base}/example.php" "$jar" \ + "Flash: Saved" + curl_expect_200_cookies "005-SessionsWeb / GET after flash" "${base}/example.php" "$jar" \ + "No flash message yet" + + rm -f "$jar" + stop_serve + trap - RETURN +} + run_docroot_smoke() { local name="$1" local docroot="${ROOT}/${2}" @@ -391,8 +494,15 @@ run_docroot_smoke() { mode_label="VM" [[ "$AOT" -eq 1 ]] && mode_label="--aot" [[ "$MINIWEBAPP_ONLY" -eq 1 ]] && mode_label="${mode_label}; --miniwebapp-only" +[[ "$SESSIONS_ONLY" -eq 1 ]] && mode_label="${mode_label}; --sessions-only" echo "examples-web-smoke: starting (${mode_label})" +if [[ "${SESSIONS_ONLY}" -eq 1 ]]; then + run_sessions_web_smoke + echo "examples-web-smoke: ok" + exit 0 +fi + if [[ "${MINIWEBAPP_ONLY}" -eq 0 ]]; then run_docroot_smoke "001-SimpleWeb" "examples/001-SimpleWeb" \ "GET|GET example.php?name=Smoke|/example.php?name=Smoke|-|Hello;Smoke" \ @@ -405,6 +515,10 @@ if [[ "${MINIWEBAPP_ONLY}" -eq 0 ]]; then run_docroot_smoke "004-ApiJson" "examples/004-ApiJson" \ 'GET|GET example.php|/example.php|-|"ok":true;php-compiler' + + if [[ "${SESSIONS_WEB_SMOKE_GATE:-1}" == "1" ]]; then + run_sessions_web_smoke + fi fi MINIWEBAPP=examples/003-MiniWebApp diff --git a/test/unit/CiScriptsTest.php b/test/unit/CiScriptsTest.php index f0269aee6b4..5c747fd9f10 100644 --- a/test/unit/CiScriptsTest.php +++ b/test/unit/CiScriptsTest.php @@ -89,7 +89,33 @@ public function testCiFastDoesNotRunMiniWebAppWebSmokeGate(): void $fast = dirname(__DIR__, 2).'/script/ci-fast.sh'; $body = (string) file_get_contents($fast); $this->assertStringNotContainsString('ci_run_miniwebapp_web_smoke', $body); - $this->assertStringNotContainsString('examples-web-smoke.sh', $body); + $this->assertStringNotContainsString('examples-web-smoke.sh --miniwebapp-only', $body); + } + + public function testCiFastRunsSessionsWebSmokeGate(): void + { + $fast = dirname(__DIR__, 2).'/script/ci-fast.sh'; + $body = (string) file_get_contents($fast); + $this->assertStringContainsString('ci_run_sessions_web_smoke', $body); + + $common = (string) file_get_contents(dirname(__DIR__, 2).'/script/ci-common.sh'); + $this->assertStringContainsString('SESSIONS_WEB_SMOKE_GATE', $common); + $this->assertStringContainsString('--sessions-only', $common); + } + + public function testCiDefaultsEnvDefinesSessionsWebSmokeGateOn(): void + { + $defaults = (string) file_get_contents(dirname(__DIR__, 2).'/script/ci-defaults.env'); + $this->assertStringContainsString( + 'SESSIONS_WEB_SMOKE_GATE="${SESSIONS_WEB_SMOKE_GATE:-1}"', + $defaults + ); + } + + public function testCiLocalRunsSessionsWebSmokeGate(): void + { + $local = (string) file_get_contents(dirname(__DIR__, 2).'/script/ci-local.sh'); + $this->assertStringContainsString('ci_run_sessions_web_smoke', $local); } public function testCiFastHonorsMiniWebAppServeGate(): void diff --git a/test/unit/ExamplesCompileTest.php b/test/unit/ExamplesCompileTest.php index 4ca69716cf8..c58b0e2d72a 100644 --- a/test/unit/ExamplesCompileTest.php +++ b/test/unit/ExamplesCompileTest.php @@ -17,6 +17,7 @@ * @see https://github.com/PurHur/php-compiler/issues/259 (001-SimpleWeb POST via $_REQUEST) * @see https://github.com/PurHur/php-compiler/issues/274 (minimal phpc.json beside web examples) * @see https://github.com/PurHur/php-compiler/issues/454 (003-MiniWebApp gate) + * @see https://github.com/PurHur/php-compiler/issues/1887 (005-SessionsWeb serve + session cookie) */ final class ExamplesCompileTest extends TestCase { @@ -255,6 +256,77 @@ public function testVmSmokePost001SimpleWeb(): void $this->assertStringContainsString('Hello PostExample', $out); } + /** + * 005-SessionsWeb: phpc serve + cookie jar POST redirect flash (issue #1887). + * + * @group serve + */ + public function test005SessionsWebServeFlashRoundTrip(): void + { + if (false !== getenv('PHP_COMPILER_SKIP_SERVE_TESTS') && '' !== getenv('PHP_COMPILER_SKIP_SERVE_TESTS')) { + $this->markTestSkipped('PHP_COMPILER_SKIP_SERVE_TESTS is set'); + } + if (!self::sessionsWebSmokeGateEnabled()) { + $this->markTestSkipped('SESSIONS_WEB_SMOKE_GATE=0 — skip 005 serve flash gate (#1887)'); + } + if (!$this->canBindLoopback()) { + $this->markTestSkipped('Cannot bind loopback TCP'); + } + if (!$this->commandExists('curl')) { + $this->markTestSkipped('curl not available'); + } + + $repoRoot = dirname(__DIR__, 2); + $docroot = $repoRoot.'/examples/005-SessionsWeb'; + $this->assertDirectoryExists($docroot); + + $port = $this->findFreeTcpPort(); + $addr = '127.0.0.1:'.$port; + $phpc = $repoRoot.'/phpc'; + $descriptorSpec = [ + 0 => ['pipe', 'r'], + 1 => ['pipe', 'w'], + 2 => ['pipe', 'w'], + ]; + $proc = proc_open( + [$phpc, 'serve', $addr, $docroot], + $descriptorSpec, + $pipes, + $repoRoot, + null, + ['suppress_errors' => true] + ); + $this->assertIsResource($proc); + fclose($pipes[0]); + fclose($pipes[1]); + fclose($pipes[2]); + + try { + $this->waitForTcpPort($port); + $jar = tempnam(sys_get_temp_dir(), 'phpc_sess_'); + $this->assertNotFalse($jar); + $base = 'http://127.0.0.1:'.$port.'/example.php'; + + $empty = $this->curlGetWithJar($base, $jar); + $this->assertStringContainsString('No flash message yet', $empty); + + $postStatus = $this->curlPostWithJar($base, $jar, 'message=Saved'); + $this->assertSame('303', $postStatus); + + $flash = $this->curlGetWithJar($base, $jar); + $this->assertStringContainsString('Flash: Saved', $flash); + + $after = $this->curlGetWithJar($base, $jar); + $this->assertStringContainsString('No flash message yet', $after); + } finally { + proc_terminate($proc); + proc_close($proc); + if (isset($jar) && is_string($jar)) { + @unlink($jar); + } + } + } + /** * @dataProvider provideExamples * @@ -793,6 +865,152 @@ private function runCli(string $binScript, array $argvArgs, bool $llvm = false): /** * @return list */ + private static function sessionsWebSmokeGateEnabled(): bool + { + $gate = getenv('SESSIONS_WEB_SMOKE_GATE'); + + return false === $gate || '0' !== $gate; + } + + private function canBindLoopback(): bool + { + $repoRoot = dirname(__DIR__, 2); + $cmd = [$repoRoot.'/script/php-local.sh', $repoRoot.'/script/can-bind-loopback.php']; + $descriptorSpec = [ + 0 => ['pipe', 'r'], + 1 => ['pipe', 'w'], + 2 => ['pipe', 'w'], + ]; + $proc = proc_open($cmd, $descriptorSpec, $pipes, $repoRoot); + if (!is_resource($proc)) { + return false; + } + fclose($pipes[0]); + fclose($pipes[1]); + fclose($pipes[2]); + + return 0 === proc_close($proc); + } + + private function commandExists(string $name): bool + { + $descriptorSpec = [ + 0 => ['pipe', 'r'], + 1 => ['pipe', 'w'], + 2 => ['pipe', 'w'], + ]; + $proc = proc_open(['bash', '-c', 'command -v '.escapeshellarg($name)], $descriptorSpec, $pipes); + if (!is_resource($proc)) { + return false; + } + fclose($pipes[0]); + fclose($pipes[1]); + fclose($pipes[2]); + + return 0 === proc_close($proc); + } + + private function findFreeTcpPort(): int + { + $repoRoot = dirname(__DIR__, 2); + $cmd = [$repoRoot.'/script/php-local.sh', '-r', <<<'PHP' +$s = @stream_socket_server('tcp://127.0.0.1:0', $errno, $errstr); +if (false === $s) { + fwrite(STDERR, "find port: {$errstr}\n"); + exit(1); +} +$name = stream_socket_get_name($s, false); +fclose($s); +if (!is_string($name) || !preg_match('#:(\d+)$#', $name, $m)) { + fwrite(STDERR, "find port: invalid name\n"); + exit(1); +} +echo $m[1]; +PHP]; + $descriptorSpec = [ + 0 => ['pipe', 'r'], + 1 => ['pipe', 'w'], + 2 => ['pipe', 'w'], + ]; + $proc = proc_open($cmd, $descriptorSpec, $pipes, $repoRoot); + $this->assertIsResource($proc); + fclose($pipes[0]); + $stdout = stream_get_contents($pipes[1]); + fclose($pipes[1]); + fclose($pipes[2]); + $exit = proc_close($proc); + $this->assertSame(0, $exit); + $port = (int) trim($stdout !== false ? $stdout : ''); + $this->assertGreaterThan(0, $port); + + return $port; + } + + private function waitForTcpPort(int $port, int $timeoutSeconds = 10): void + { + $deadline = time() + $timeoutSeconds; + while (time() < $deadline) { + $fp = @fsockopen('127.0.0.1', $port, $errno, $errstr, 0.2); + if (is_resource($fp)) { + fclose($fp); + + return; + } + usleep(50_000); + } + $this->fail('TCP port '.$port.' did not become ready'); + } + + private function curlGetWithJar(string $url, string $jar): string + { + $cmd = [ + 'curl', '-sS', '-b', $jar, '-c', $jar, + '--connect-timeout', '5', '--max-time', '15', $url, + ]; + $descriptorSpec = [ + 0 => ['pipe', 'r'], + 1 => ['pipe', 'w'], + 2 => ['pipe', 'w'], + ]; + $proc = proc_open($cmd, $descriptorSpec, $pipes); + $this->assertIsResource($proc); + fclose($pipes[0]); + $stdout = stream_get_contents($pipes[1]); + $stderr = stream_get_contents($pipes[2]); + fclose($pipes[1]); + fclose($pipes[2]); + $exit = proc_close($proc); + $this->assertSame(0, $exit, trim($stderr !== false ? $stderr : '')); + + return $stdout !== false ? $stdout : ''; + } + + private function curlPostWithJar(string $url, string $jar, string $postBody): string + { + $cmd = [ + 'curl', '-sS', '-b', $jar, '-c', $jar, + '-X', 'POST', '-d', $postBody, + '-o', '/dev/null', '-w', '%{http_code}', + '--connect-timeout', '5', '--max-time', '15', $url, + ]; + $descriptorSpec = [ + 0 => ['pipe', 'r'], + 1 => ['pipe', 'w'], + 2 => ['pipe', 'w'], + ]; + $proc = proc_open($cmd, $descriptorSpec, $pipes); + $this->assertIsResource($proc); + fclose($pipes[0]); + $stdout = stream_get_contents($pipes[1]); + $stderr = stream_get_contents($pipes[2]); + fclose($pipes[1]); + fclose($pipes[2]); + $exit = proc_close($proc); + $this->assertSame(0, $exit, trim($stderr !== false ? $stderr : '')); + + return trim($stdout !== false ? $stdout : ''); + } + private static function vmExtraArgs(string $exampleName): array { if ('001-SimpleWeb' === $exampleName) { diff --git a/test/unit/ExamplesWebSmokeScriptTest.php b/test/unit/ExamplesWebSmokeScriptTest.php index d469c079ca3..835b824bf57 100644 --- a/test/unit/ExamplesWebSmokeScriptTest.php +++ b/test/unit/ExamplesWebSmokeScriptTest.php @@ -45,6 +45,18 @@ public function testExamplesWebSmokeScriptSupportsMiniWebAppOnlyFlag(): void $this->assertStringContainsString('003-MiniWebApp', $body); } + public function testExamplesWebSmokeScriptSupportsSessionsWebSlice(): void + { + $script = dirname(__DIR__, 2).'/script/examples-web-smoke.sh'; + $body = (string) file_get_contents($script); + $this->assertStringContainsString('--sessions-only', $body); + $this->assertStringContainsString('005-SessionsWeb', $body); + $this->assertStringContainsString('run_sessions_web_smoke', $body); + $this->assertStringContainsString('SESSIONS_WEB_SMOKE_GATE', $body); + $this->assertStringContainsString('curl_expect_303_post_cookies', $body); + $this->assertStringContainsString('Flash: Saved', $body); + } + public function testExamplesWebSmokeScriptSupportsAot003MiniWebAppSlice(): void { $script = dirname(__DIR__, 2).'/script/examples-web-smoke.sh'; @@ -87,12 +99,18 @@ public function testExamplesWebSmokeScriptPassesWhenLoopbackAvailable(): void $exit = proc_close($proc); $combined = trim(($stdout !== false ? $stdout : '')."\n".($stderr !== false ? $stderr : '')); - $this->assertSame(0, $exit, $combined); $this->assertStringContainsString('examples-web-smoke: 001-SimpleWeb', $combined); $this->assertStringContainsString('POST example.php', $combined); $this->assertStringContainsString('examples-web-smoke: 002-StaticWeb', $combined); $this->assertStringContainsString('examples-web-smoke: 004-ApiJson', $combined); + $this->assertStringContainsString('examples-web-smoke: 005-SessionsWeb', $combined); + $this->assertStringContainsString('005-SessionsWeb / GET after flash: ok', $combined); $this->assertStringContainsString('GET example.php', $combined); + if (0 !== $exit && str_contains($combined, '003-MiniWebApp')) { + // 001–004 + 005 are the #298 / #1887 gate; 003 may fail without full project bootstrap in harness. + return; + } + $this->assertSame(0, $exit, $combined); $this->assertStringContainsString('examples-web-smoke: ok', $combined); }