From 24b19c4a657cd49d221bf27241112f13f76fe3cb Mon Sep 17 00:00:00 2001 From: Edmond <1571649+EdmondDantes@users.noreply.github.com> Date: Mon, 4 May 2026 11:22:04 +0300 Subject: [PATCH 01/10] Add true-async-server framework entry Native PHP HTTP server using TrueAsync coroutine engine with ThreadPool for SO_REUSEPORT multi-worker scaling. Supports HTTP/1.1 and HTTP/2 over TLS via ALPN. Covers baseline, pipelined, json, upload, static, async-db, api-4/16, baseline-h2, static-h2, json-tls test profiles. --- frameworks/true-async-server/Dockerfile | 53 +++++ frameworks/true-async-server/PostgreSQL.php | 92 +++++++++ frameworks/true-async-server/entry.php | 207 ++++++++++++++++++++ frameworks/true-async-server/meta.json | 24 +++ 4 files changed, 376 insertions(+) create mode 100644 frameworks/true-async-server/Dockerfile create mode 100644 frameworks/true-async-server/PostgreSQL.php create mode 100644 frameworks/true-async-server/entry.php create mode 100644 frameworks/true-async-server/meta.json diff --git a/frameworks/true-async-server/Dockerfile b/frameworks/true-async-server/Dockerfile new file mode 100644 index 00000000..93dd066d --- /dev/null +++ b/frameworks/true-async-server/Dockerfile @@ -0,0 +1,53 @@ +FROM ubuntu:24.04 + +# Minimal runtime deps: +# libnghttp2-14 — system nghttp2 (.so linked by true_async_server.so) +# libsqlite3-0 — PDO sqlite (built into PHP) +# ca-certificates — TLS cert store +RUN apt-get update -qq && \ + apt-get install -y --no-install-recommends \ + libnghttp2-14 \ + libsqlite3-0 \ + libpq5 \ + ca-certificates \ + && rm -rf /var/lib/apt/lists/* + +# ── PHP binary (locally built, ZTS + TrueAsync ABI) ───────────────── +COPY php /usr/local/bin/php + +# ── Custom shared libraries (libuv, OpenSSL 3, ngtcp2, nghttp3) ────── +COPY libs/ /usr/local/lib/ +RUN ldconfig + +# ── PHP extension dir + true_async_server.so ───────────────────────── +RUN mkdir -p /usr/local/lib/php/extensions/no-debug-zts-20250926 +COPY true_async_server.so /usr/local/lib/php/extensions/no-debug-zts-20250926/true_async_server.so +COPY pdo_pgsql.so /usr/local/lib/php/extensions/no-debug-zts-20250926/pdo_pgsql.so + +# ── Minimal php.ini ─────────────────────────────────────────────────── +RUN mkdir -p /usr/local/lib && printf '%s\n' \ + 'extension_dir=/usr/local/lib/php/extensions/no-debug-zts-20250926' \ + 'extension=pdo_pgsql' \ + 'extension=true_async_server' \ + 'opcache.enable=1' \ + 'opcache.enable_cli=1' \ + 'opcache.jit=1255' \ + 'opcache.jit_buffer_size=128M' \ + 'opcache.memory_consumption=256' \ + 'opcache.max_accelerated_files=10000' \ + 'opcache.validate_timestamps=0' \ + 'opcache.preload=' \ + 'memory_limit=1024M' \ + 'realpath_cache_size=4M' \ + 'realpath_cache_ttl=600' \ + > /usr/local/lib/php.ini + +# ── Application ─────────────────────────────────────────────────────── +WORKDIR /app +COPY entry.php /app/entry.php +COPY PostgreSQL.php /app/PostgreSQL.php + +EXPOSE 8080 +EXPOSE 8443 + +CMD ["php", "/app/entry.php"] diff --git a/frameworks/true-async-server/PostgreSQL.php b/frameworks/true-async-server/PostgreSQL.php new file mode 100644 index 00000000..da6fcfef --- /dev/null +++ b/frameworks/true-async-server/PostgreSQL.php @@ -0,0 +1,92 @@ + PDO::ERRMODE_EXCEPTION, + PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, + PDO::ATTR_EMULATE_PREPARES => false, + PDO::ATTR_POOL_ENABLED => true, + PDO::ATTR_POOL_MIN => $minConn, + PDO::ATTR_POOL_MAX => $maxConn, + ] + ); + + self::$available = true; + } + + public static function query(float $min, float $max, int $limit = 50): string + { + if (!self::$available) { + self::init(); + if (!self::$available) { + return '{"items":[],"count":0}'; + } + } + + try { + $stmt = self::$pdo->prepare(self::SQL); + $stmt->execute([$min, $max, $limit]); + $rows = []; + while ($row = $stmt->fetch()) { + $rows[] = [ + 'id' => $row['id'], + 'name' => $row['name'], + 'category' => $row['category'], + 'price' => $row['price'], + 'quantity' => $row['quantity'], + 'active' => (bool)$row['active'], + 'tags' => json_decode($row['tags'], true), + 'rating' => [ + 'score' => $row['rating_score'], + 'count' => $row['rating_count'], + ], + ]; + } + return json_encode( + ['items' => $rows, 'count' => count($rows)], + JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES + ); + } catch (\Throwable) { + return '{"items":[],"count":0}'; + } + } +} diff --git a/frameworks/true-async-server/entry.php b/frameworks/true-async-server/entry.php new file mode 100644 index 00000000..4cbd96f4 --- /dev/null +++ b/frameworks/true-async-server/entry.php @@ -0,0 +1,207 @@ +start() on the same + * transferred object; SO_REUSEPORT lets the kernel load-balance + * accept()s across all threads. + * - Each worker has its own PDO connection pool (ext-async PDO::ATTR_POOL_*). + * + * Override worker count with WORKERS env var. + */ + +use TrueAsync\HttpServer; +use TrueAsync\HttpServerConfig; +use TrueAsync\HttpRequest; +use TrueAsync\HttpResponse; +use Async\ThreadPool; +use function Async\spawn; +use function Async\await_all_or_fail; +use function Async\available_parallelism; + +require __DIR__ . '/PostgreSQL.php'; + +// --- Preload at process start (read once, transferred to all workers) --- + +$datasetRaw = json_decode(file_get_contents('/data/dataset.json'), true); +$datasetCount = count($datasetRaw); + +$mimeTypes = [ + 'css' => 'text/css', + 'js' => 'application/javascript', + 'html' => 'text/html', + 'woff2' => 'font/woff2', + 'svg' => 'image/svg+xml', + 'webp' => 'image/webp', + 'json' => 'application/json', +]; + +$staticFiles = []; +$staticDir = '/data/static'; +if (is_dir($staticDir)) { + foreach (scandir($staticDir) as $name) { + if ($name === '.' || $name === '..') continue; + if (str_ends_with($name, '.br') || str_ends_with($name, '.gz')) continue; + $base = $staticDir . '/' . $name; + $ext = pathinfo($name, PATHINFO_EXTENSION); + $staticFiles['/static/' . $name] = [ + 'data' => file_get_contents($base), + 'mime' => $mimeTypes[$ext] ?? 'application/octet-stream', + 'br' => file_exists($base . '.br') ? file_get_contents($base . '.br') : null, + 'gz' => file_exists($base . '.gz') ? file_get_contents($base . '.gz') : null, + ]; + } +} + +// --- Runtime knobs --- + +$port = (int)(getenv('PORT') ?: 8080); +$tlsPort = (int)(getenv('TLS_PORT') ?: 8443); +$workers = (int)(getenv('WORKERS') ?: 0); +if ($workers <= 0) { + $workers = available_parallelism(); +} + +$certPath = '/certs/server.crt'; +$keyPath = '/certs/server.key'; +$tlsAvailable = is_readable($certPath) && is_readable($keyPath); + +// --- Step 1: build the server (one instance, transferred into each thread) --- + +$config = (new HttpServerConfig()) + ->addListener('0.0.0.0', $port) + ->setBacklog(2048) + ->setReadTimeout(15) + ->setWriteTimeout(15) + ->setKeepAliveTimeout(60) + ->setShutdownTimeout(5) + ->setMaxBodySize(32 * 1024 * 1024); + +if ($tlsAvailable) { + $config + ->addListener('0.0.0.0', $tlsPort, true) + ->setCertificate($certPath) + ->setPrivateKey($keyPath); +} + +$server = new HttpServer($config); + +$server->addHttpHandler( + static function (HttpRequest $request, HttpResponse $response) + use ($datasetRaw, $datasetCount, $staticFiles): void + { + $path = $request->getPath(); + + if ($path === '/pipeline') { + $response->setStatusCode(200) + ->setHeader('Content-Type', 'text/plain') + ->setBody('ok'); + return; + } + + if ($path === '/baseline2' || $path === '/baseline11') { + $method = $request->getMethod(); + if ($method !== 'GET' && $method !== 'POST') { + $response->setStatusCode(405) + ->setHeader('Content-Type', 'text/plain') + ->setBody('Method Not Allowed'); + return; + } + $sum = 0; + foreach ($request->getQuery() as $v) { $sum += (int)$v; } + if ($method === 'POST') { + $sum += (int)$request->getBody(); + } + $response->setStatusCode(200) + ->setHeader('Content-Type', 'text/plain') + ->setBody((string)$sum); + return; + } + + if (str_starts_with($path, '/json/')) { + $tail = substr($path, 6); + if ($tail !== '' && ctype_digit($tail)) { + $query = $request->getQuery(); + $count = min((int)$tail, $datasetCount); + $mult = (int)($query['m'] ?? 1); + if ($mult === 0) $mult = 1; + $items = []; + for ($i = 0; $i < $count; $i++) { + $item = $datasetRaw[$i]; + $item['total'] = $item['price'] * $item['quantity'] * $mult; + $items[] = $item; + } + $response->setStatusCode(200) + ->setHeader('Content-Type', 'application/json') + ->setBody(json_encode( + ['items' => $items, 'count' => $count], + JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES + )); + return; + } + } + + if ($path === '/upload') { + $response->setStatusCode(200) + ->setHeader('Content-Type', 'text/plain') + ->setBody((string)strlen($request->getBody())); + return; + } + + if ($path === '/async-db') { + $query = $request->getQuery(); + $min = (float)($query['min'] ?? 10); + $max = (float)($query['max'] ?? 50); + $limit = max(1, min(50, (int)($query['limit'] ?? 50))); + $response->setStatusCode(200) + ->setHeader('Content-Type', 'application/json') + ->setBody(PostgreSQL::query($min, $max, $limit)); + return; + } + + if (str_starts_with($path, '/static/') && isset($staticFiles[$path])) { + $f = $staticFiles[$path]; + $ae = $request->getHeader('Accept-Encoding') ?? ''; + $response->setStatusCode(200) + ->setHeader('Content-Type', $f['mime']); + if ($f['br'] !== null && str_contains($ae, 'br')) { + $response->setHeader('Content-Encoding', 'br')->setBody($f['br']); + } elseif ($f['gz'] !== null && str_contains($ae, 'gzip')) { + $response->setHeader('Content-Encoding', 'gzip')->setBody($f['gz']); + } else { + $response->setBody($f['data']); + } + return; + } + + $response->setStatusCode(404) + ->setHeader('Content-Type', 'text/plain') + ->setBody('404 Not Found'); + } +); + +// --- Step 2: launch pool, run $server->start() in every thread --- + +fprintf( + STDERR, + "[true-async-server] %d workers · :%d%s · pid %d\n", + $workers, + $port, + $tlsAvailable ? " · tls :{$tlsPort}" : '', + getmypid() +); + +$pool = new ThreadPool($workers); +$futures = []; +for ($i = 0; $i < $workers; $i++) { + $futures[] = $pool->submit(static fn() => $server->start()); +} + +// Wait until all workers finish (i.e. until the process is stopped). +spawn(static fn() => await_all_or_fail($futures)); diff --git a/frameworks/true-async-server/meta.json b/frameworks/true-async-server/meta.json new file mode 100644 index 00000000..6c7cded2 --- /dev/null +++ b/frameworks/true-async-server/meta.json @@ -0,0 +1,24 @@ +{ + "display_name": "true-async-server", + "language": "PHP", + "type": "tuned", + "engine": "true-async-server", + "description": "Native PHP HTTP server built on the TrueAsync coroutine engine. Each connection runs as a cooperative coroutine; one OS thread per CPU listens via SO_REUSEPORT inside a single PHP process. PDO connection pool, OPcache JIT, no Caddy / FrankenPHP / FastCGI in front.", + "repo": "https://github.com/true-async/server", + "enabled": true, + "maintainers": ["EdmondDantes"], + "tests": [ + "baseline", + "pipelined", + "limited-conn", + "json", + "upload", + "static", + "async-db", + "api-4", + "api-16", + "baseline-h2", + "static-h2", + "json-tls" + ] +} From 64f184078259221b9c30db5d1e91692103f5438f Mon Sep 17 00:00:00 2001 From: Edmond <1571649+EdmondDantes@users.noreply.github.com> Date: Mon, 4 May 2026 15:03:36 +0300 Subject: [PATCH 02/10] Add true-async-server Docker Compose test suite (34/38 passing) - Add docker-compose.yml: postgres + server + validator services on a shared bridge network; validator exits with the test result code - Add test/Dockerfile + test/validate.sh: full HTTP/1.1, HTTP/2, TLS, JSON, upload, static, async-db and TCP-fragmentation tests - Fix entry.php: require PostgreSQL.php inside each per-thread closure so every worker thread has the class in its PHP environment - Fix Dockerfile: switch to slim base image with INI tuning - Add ISSUES.md: documents 4 remaining alpha.3 failures with root causes and suggested server-level fixes --- frameworks/true-async-server/Dockerfile | 46 +-- frameworks/true-async-server/ISSUES.md | 91 ++++++ .../true-async-server/docker-compose.yml | 54 ++++ frameworks/true-async-server/entry.php | 7 +- frameworks/true-async-server/test/Dockerfile | 11 + frameworks/true-async-server/test/validate.sh | 263 ++++++++++++++++++ 6 files changed, 430 insertions(+), 42 deletions(-) create mode 100644 frameworks/true-async-server/ISSUES.md create mode 100644 frameworks/true-async-server/docker-compose.yml create mode 100644 frameworks/true-async-server/test/Dockerfile create mode 100644 frameworks/true-async-server/test/validate.sh diff --git a/frameworks/true-async-server/Dockerfile b/frameworks/true-async-server/Dockerfile index 93dd066d..9b89130c 100644 --- a/frameworks/true-async-server/Dockerfile +++ b/frameworks/true-async-server/Dockerfile @@ -1,53 +1,17 @@ -FROM ubuntu:24.04 +FROM trueasync/php-true-async:0.7.0-alpha.3-php8.6 -# Minimal runtime deps: -# libnghttp2-14 — system nghttp2 (.so linked by true_async_server.so) -# libsqlite3-0 — PDO sqlite (built into PHP) -# ca-certificates — TLS cert store -RUN apt-get update -qq && \ - apt-get install -y --no-install-recommends \ - libnghttp2-14 \ - libsqlite3-0 \ - libpq5 \ - ca-certificates \ - && rm -rf /var/lib/apt/lists/* - -# ── PHP binary (locally built, ZTS + TrueAsync ABI) ───────────────── -COPY php /usr/local/bin/php - -# ── Custom shared libraries (libuv, OpenSSL 3, ngtcp2, nghttp3) ────── -COPY libs/ /usr/local/lib/ -RUN ldconfig - -# ── PHP extension dir + true_async_server.so ───────────────────────── -RUN mkdir -p /usr/local/lib/php/extensions/no-debug-zts-20250926 -COPY true_async_server.so /usr/local/lib/php/extensions/no-debug-zts-20250926/true_async_server.so -COPY pdo_pgsql.so /usr/local/lib/php/extensions/no-debug-zts-20250926/pdo_pgsql.so - -# ── Minimal php.ini ─────────────────────────────────────────────────── -RUN mkdir -p /usr/local/lib && printf '%s\n' \ - 'extension_dir=/usr/local/lib/php/extensions/no-debug-zts-20250926' \ - 'extension=pdo_pgsql' \ - 'extension=true_async_server' \ - 'opcache.enable=1' \ - 'opcache.enable_cli=1' \ +RUN printf '%s\n' \ 'opcache.jit=1255' \ 'opcache.jit_buffer_size=128M' \ 'opcache.memory_consumption=256' \ 'opcache.max_accelerated_files=10000' \ 'opcache.validate_timestamps=0' \ - 'opcache.preload=' \ 'memory_limit=1024M' \ - 'realpath_cache_size=4M' \ - 'realpath_cache_ttl=600' \ - > /usr/local/lib/php.ini + > /etc/php.d/99-arena.ini -# ── Application ─────────────────────────────────────────────────────── WORKDIR /app -COPY entry.php /app/entry.php -COPY PostgreSQL.php /app/PostgreSQL.php +COPY entry.php PostgreSQL.php /app/ -EXPOSE 8080 -EXPOSE 8443 +EXPOSE 8080 8443 CMD ["php", "/app/entry.php"] diff --git a/frameworks/true-async-server/ISSUES.md b/frameworks/true-async-server/ISSUES.md new file mode 100644 index 00000000..abe16351 --- /dev/null +++ b/frameworks/true-async-server/ISSUES.md @@ -0,0 +1,91 @@ +# Known Issues — true-async-server (alpha.3) + +Tested against `trueasync/php-true-async:0.7.0-alpha.3-php8.6`. +Test suite: `frameworks/true-async-server/test/validate.sh` +Result: **34 / 38 passed**. + +--- + +## Passing test groups + +| Group | Tests | +|---|---| +| baseline HTTP/1.1 (GET, POST, chunked POST) | ✅ | +| baseline TCP fragmentation — GET only | ✅ 2/4 | +| pipelined | ✅ | +| json processing | ✅ | +| upload | ✅ | +| static files | ✅ | +| async-db (PostgreSQL) | ✅ | +| baseline-h2 (HTTPS + HTTP/2) | ✅ | +| static-h2 | ✅ | +| json-tls (HTTPS + HTTP/1.1 TLS) | ✅ | + +--- + +## Failing tests (4) + +### 1. HTTP/1.1 body not fully buffered on fragmented POST + +**Tests:** +- `POST split headers/body` — headers in one TCP segment, body `"20"` in a second +- `POST split body bytes` — headers in one TCP segment, body split into `"2"` + `"0"` + +**Observed behaviour:** +``` +FAIL [POST split headers/body]: expected='75' got='57' +FAIL [POST split body bytes]: expected='75' got='55' +``` + +`57 = 13 + 42 + 2` — the server read only the first byte `"2"` of the two-byte body `"20"`. +`55 = 13 + 42 + 0` — the server read no body bytes at all when they arrived in two separate writes. + +**Root cause:** +`HttpRequest::getBody()` appears to return whatever bytes are present in the receive buffer at the moment of the call instead of waiting until `Content-Length` bytes have accumulated. When the request body arrives in a separate TCP segment after the headers, the handler coroutine is dispatched before the body bytes land in the buffer, and `getBody()` returns a partial (or empty) string. + +**HTTP/2 is not affected** — HTTP/2 frames the body before dispatching, so `getBody()` is always complete. + +**Suggested fix (server-level):** +`getBody()` must block / yield until `Content-Length` bytes have been fully received from the socket before returning control to the handler. + +--- + +### 2. Spорadic empty responses during server startup (race condition) + +**Tests (intermittent):** +- `GET /baseline11 random a=… b=…` +- `POST /baseline11 random body=…` + +**Observed behaviour:** +``` +FAIL [GET /baseline11 random a=372 b=922]: expected='1294' got='' +FAIL [POST /baseline11 random body=346]: expected='401' got='' +``` +The same requests succeed when sent against a server that has been running for several seconds. + +**Server log (concurrent with failures):** +``` +Warning: Attempt to finalize a coroutine that is still in the queue in Unknown on line 0 +``` + +**Root cause:** +When `ThreadPool` starts 16 workers simultaneously, their event-loop initialisation coroutines overlap with early incoming requests. The lifecycle management in `alpha.3` occasionally finalises a request-handler coroutine while it is still enqueued, causing the connection to be closed without a response. + +The issue is transient: workers stabilise after a few seconds and subsequent identical requests succeed. A 20-request warm-up + 1 s sleep was added to `validate.sh` to reduce the window, but under low-latency Docker networks the race can still be triggered. + +**Suggested fix (server-level):** +Ensure that per-worker event-loop initialisation is fully complete (all coroutines flushed) before the first `accept()` call is made, or guard against finalising a coroutine that has not yet been dequeued. + +**Workaround (application-level):** +Set `WORKERS=1` or `WORKERS=2` via environment variable for integration testing; the race disappears with a single worker. + +--- + +## Fixes applied in this integration + +| Issue | Fix | +|---|---| +| `async-db` → HTTP 500 "Class PostgreSQL not found" | `PostgreSQL.php` is now `require`-d inside each per-thread closure; every worker thread has its own PHP class table | +| `check_header` using `curl -I` failing with newer curl | Replaced `-sI` with `-D - -o /dev/null` to dump headers without changing the HTTP method | +| Validator failures immediately after Docker Compose start | Added 20-request warm-up loop + `sleep 1` in `validate.sh` | +| `\r\n` literals not decoded to real CRLF in fragmented-TCP tests | Fixed bash→Python escaping: `'\\\\r'` in the `-c "…"` string becomes `'\\r'` in Python source, correctly replacing literal `\r` with CR | diff --git a/frameworks/true-async-server/docker-compose.yml b/frameworks/true-async-server/docker-compose.yml new file mode 100644 index 00000000..42b1b8b2 --- /dev/null +++ b/frameworks/true-async-server/docker-compose.yml @@ -0,0 +1,54 @@ +services: + postgres: + image: postgres:18 + command: ["-c", "max_connections=256"] + environment: + POSTGRES_USER: bench + POSTGRES_PASSWORD: bench + POSTGRES_DB: benchmark + volumes: + - ${DATA_DIR:-../../data}/pgdb-seed.sql:/docker-entrypoint-initdb.d/seed.sql:ro + healthcheck: + test: ["CMD-SHELL", "pg_isready -U bench -d benchmark && psql -U bench -d benchmark -tAc 'SELECT 1 FROM items LIMIT 1' | grep -q 1"] + interval: 2s + timeout: 10s + retries: 30 + networks: [arena] + + server: + build: + context: . + depends_on: + postgres: + condition: service_healthy + environment: + DATABASE_URL: postgres://bench:bench@postgres:5432/benchmark + DATABASE_MAX_CONN: "256" + volumes: + - ${DATA_DIR:-../../data}/dataset.json:/data/dataset.json:ro + - ${DATA_DIR:-../../data}/static:/data/static:ro + - ${CERTS_DIR:-../../certs}:/certs:ro + healthcheck: + test: ["CMD", "curl", "-sf", "http://127.0.0.1:8080/pipeline"] + interval: 2s + timeout: 5s + retries: 20 + networks: [arena] + + validator: + build: + context: ./test + depends_on: + server: + condition: service_healthy + environment: + SERVER: server + HTTP_PORT: "8080" + HTTPS_PORT: "8443" + volumes: + - ${CERTS_DIR:-../../certs}:/certs:ro + networks: [arena] + +networks: + arena: + driver: bridge diff --git a/frameworks/true-async-server/entry.php b/frameworks/true-async-server/entry.php index 4cbd96f4..0b8dd2d6 100644 --- a/frameworks/true-async-server/entry.php +++ b/frameworks/true-async-server/entry.php @@ -177,6 +177,7 @@ static function (HttpRequest $request, HttpResponse $response) } else { $response->setBody($f['data']); } + return; } @@ -200,7 +201,11 @@ static function (HttpRequest $request, HttpResponse $response) $pool = new ThreadPool($workers); $futures = []; for ($i = 0; $i < $workers; $i++) { - $futures[] = $pool->submit(static fn() => $server->start()); + // Each worker thread has its own PHP environment — re-require class files. + $futures[] = $pool->submit(static function () use ($server): void { + require __DIR__ . '/PostgreSQL.php'; + $server->start(); + }); } // Wait until all workers finish (i.e. until the process is stopped). diff --git a/frameworks/true-async-server/test/Dockerfile b/frameworks/true-async-server/test/Dockerfile new file mode 100644 index 00000000..f92336db --- /dev/null +++ b/frameworks/true-async-server/test/Dockerfile @@ -0,0 +1,11 @@ +FROM python:3.12-slim + +# curl with HTTP/2 support (Debian bookworm libcurl includes nghttp2) +RUN apt-get update && apt-get install -y --no-install-recommends \ + curl \ + && rm -rf /var/lib/apt/lists/* + +COPY validate.sh /validate.sh +RUN chmod +x /validate.sh + +ENTRYPOINT ["/validate.sh"] diff --git a/frameworks/true-async-server/test/validate.sh b/frameworks/true-async-server/test/validate.sh new file mode 100644 index 00000000..a78f21fb --- /dev/null +++ b/frameworks/true-async-server/test/validate.sh @@ -0,0 +1,263 @@ +#!/usr/bin/env bash +# Validation suite for true-async-server — runs inside the 'validator' service. +# +# Environment (set by docker-compose.yml): +# SERVER hostname of the server service (default: server) +# HTTP_PORT plain HTTP port (default: 8080) +# HTTPS_PORT TLS port for HTTP/2 + TLS (default: 8443) + +set -uo pipefail # -e intentionally omitted — arithmetic would kill the script + +SERVER="${SERVER:-server}" +HTTP_PORT="${HTTP_PORT:-8080}" +HTTPS_PORT="${HTTPS_PORT:-8443}" + +H1="http://${SERVER}:${HTTP_PORT}" +TLS="https://${SERVER}:${HTTPS_PORT}" + +PASS=0 +FAIL=0 + +# ── helpers ────────────────────────────────────────────────────────────────── + +ok() { echo " PASS [$1]"; PASS=$((PASS + 1)); } +fail() { echo " FAIL [$1]: $2"; FAIL=$((FAIL + 1)); } + +# check