Testing Strategy
ePHPm uses a layered testing approach: fast unit tests for inner logic, a dedicated Rust E2E crate (ephpm-e2e) for integration assertions, and Tilt + Kind for orchestrating real infrastructure.
Test Layers
| Layer | Tool | What it tests | Speed |
|---|---|---|---|
| Unit | cargo nextest | Config parsing, routing logic, SAPI mapping, response building | Seconds |
| Integration | cargo nextest (ignored by default) | PHP execution, WordPress lifecycle — requires libphp | Seconds (with SDK) |
| E2E | ephpm-e2e crate + Tilt + Kind | Full stack against real K8s infrastructure | Minutes |
| Benchmarks | Criterion | Throughput, latency p99 — requires libphp | Minutes |
Unit & Integration Tests
Run locally, no infrastructure needed (stub mode):
cargo nextest run --workspace # all unit tests
cargo nextest run -p ephpm-server # single crate
cargo nextest run -p ephpm-server test_routing # single testIntegration tests that require PHP are #[ignore] by default. Run them after building with cargo xtask release:
cargo nextest run --workspace --run-ignored allE2E Testing: ephpm-e2e Crate
E2E tests live in a dedicated Rust crate (crates/ephpm-e2e/) that runs inside a Kind cluster. The crate is excluded from the workspace — it has different dependencies and is only built inside the E2E test runner container.
Current Tests
All tests read EPHPM_URL from the environment. phpinfo.rs additionally reads EXPECTED_PHP_VERSION.
tests/basic.rs — core request lifecycle:
missing_file_returns_404php_renders_correctlystatic_file_serving
tests/phpinfo.rs — PHP version and SAPI identity:
php_version_matches— GETs/index.php, checksPHP Version: X.Y, confirmsServer API: ephpmhealth_check— GETs/, asserts success
tests/http.rs — HTTP protocol correctness:
head_request_has_no_body— HEAD returns same headers as GET but empty bodypost_body_reaches_php— form POST reaches$_POSTcontent_type_for_static_files—.css→text/css,.js→application/javascriptetag_304_not_modified— ETag round-trip returns 304 with empty bodygzip_response_is_compressed—Accept-Encoding: gziptriggersContent-Encoding: gziprequest_body_too_large_returns_413— body >max_body_sizereturns 413cache_control_present_on_static_files—Cache-Controlheader present on static filesx_forwarded_for_header_reaches_php—X-Forwarded-Forappears asHTTP_X_FORWARDED_FORin$_SERVERfallback_chain_serves_index_php— GET/resolves via fallback chain toindex.php
tests/php.rs — PHP execution correctness:
query_string_available—$_GETpopulated from query stringserver_vars_populated—REQUEST_METHOD,REQUEST_URI,DOCUMENT_ROOT,REMOTE_ADDRsetphp_exit_returns_output— output beforeexit(0)delivered to clientphp_sets_custom_status—http_response_code(201)propagates to HTTP status linecookie_header_populates_cookie_superglobal—Cookie:header reaches$_COOKIEphp_input_stream_readable—php://inputcontains raw body for non-form POSTcustom_response_header_reaches_client— PHPheader()appears in HTTP response
tests/errors.rs — PHP error recovery (zend_try/zend_catch correctness):
php_fatal_error_returns_500— undefined function call → 500, server continuesphp_memory_limit_exceeded_returns_500— OOM → 500, server continuesphp_syntax_error_returns_500— parse error → 500, server continues
tests/kv.rs — KV store PHP native functions:
kv_set_get_roundtrip,kv_ttl_expiry,kv_incr_atomic,kv_del_and_existskv_pttl_returns_minus_two_for_missing,kv_pttl_positive_for_live_keykv_incr_by_delta,kv_expire_extends_ttlkv_setnx_does_not_overwrite,kv_mset_mget_roundtrip
tests/concurrency.rs — correctness under concurrent load:
concurrent_php_requests_all_succeed— 20 parallel GETs all return correct outputconcurrent_kv_increments_are_consistent— 20 concurrent increments yield unique values 1–20
tests/security.rs — path and access controls:
dotfile_returns_403—/.envreturns 403php_source_not_exposed—.phpresponse never contains<?phpblocked_path_pattern_returns_403—vendor/*glob returns 403path_traversal_is_blocked— URL-encoded%2e%2esequences don’t escape docroot
PHP Version Flow
The PHP version flows through the entire pipeline:
GHA matrix (php: "8.4")
→ cargo xtask e2e --php-version 8.4
→ podman build --build-arg PHP_VERSION=8.4 (Dockerfile)
→ EXPECTED_PHP_VERSION=8.4 tilt ci
→ Tiltfile replaces __EXPECTED_PHP_VERSION__ in e2e-job.yaml
→ E2E Job container env: EXPECTED_PHP_VERSION=8.4
→ Rust test asserts body contains "PHP Version: 8.4"Crate Structure
crates/ephpm-e2e/
├── Cargo.toml # reqwest + tokio (no TLS needed in-cluster)
├── src/
│ └── lib.rs # Shared helpers (required_env)
└── tests/
└── phpinfo.rs # PHP version + SAPI validationTilt + Kind Orchestration
Prerequisites
Podman or Docker is required — Kind needs a container runtime.
For kind, tilt, and kubectl, you have two options:
Option A: Local install via xtask (recommended)
cargo xtask e2e-installDownloads kind, tilt, and kubectl to ./bin/. No global install, no sudo. All e2e* commands check ./bin/ first, then fall back to PATH.
Option B: Install globally yourself
- Kind: https://kind.sigs.k8s.io/user/quick-start/#installation
- Tilt: https://docs.tilt.dev/install.html
- kubectl: https://kubernetes.io/tasks/tools/
What Gets Deployed
┌────────────────────────────────────────────────┐
│ Kind cluster: ephpm-dev │
│ │
│ ┌──────────────┐ │
│ │ ephpm │ Deployment (1 replica) │
│ │ :8080 │ Serves test docroot │
│ └──────────────┘ │
│ ▲ │
│ │ http://ephpm:8080 │
│ │ │
│ ┌──────────────┐ │
│ │ ephpm-e2e │ Job — runs Rust test binary │
│ │ (test runner)│ Exits 0=pass, 1=fail │
│ └──────────────┘ │
└────────────────────────────────────────────────┘Directory Structure
k8s/
├── kind-config.yaml # Kind cluster config (single control-plane node)
├── Tiltfile # Tilt orchestration — builds, deploys, runs tests
├── base/
│ └── ephpm-single.yaml # Deployment + Service for ephpm
└── tests/
└── e2e-job.yaml # Job that runs ephpm-e2e test binary
docker/
├── Dockerfile # Multi-stage: build ephpm with PHP → minimal runtime
└── Dockerfile.e2e # Multi-stage: build test binary → minimal runnerTiltfile
The Tiltfile (k8s/Tiltfile) handles:
- Building
ephpm:devimage fromdocker/Dockerfile - Building
ephpm-e2e:devimage fromdocker/Dockerfile.e2e - Deploying ephpm Deployment + Service
- Deploying the E2E test Job with
EXPECTED_PHP_VERSIONinjected via string replacement - In
tilt cimode: waits for Job completion, exits with Job’s exit code
Running Tests via xtask
Run E2E tests (headless)
cargo xtask e2e --php-version 8.5This does everything in one shot:
- Creates the Kind cluster
ephpm-dev(skips if it exists) - Builds
ephpm:devwith--build-arg PHP_VERSION=8.5 - Builds
ephpm-e2e:devtest runner image - Loads both images into Kind
- Runs
tilt ciwithEXPECTED_PHP_VERSION=8.5 - On failure, dumps pod logs for debugging
Start dev environment (interactive)
cargo xtask e2e-up --php-version 8.5Same setup, but runs tilt up --stream:
- Streams logs to your terminal
- Tilt web dashboard at http://localhost:10350
- Watches for source changes and auto-rebuilds
- Ctrl+C to stop
Tear down
cargo xtask e2e-downRemoves Tilt resources and deletes the Kind cluster.
Container engine
Defaults to podman if available, otherwise docker:
CONTAINER_ENGINE=docker cargo xtask e2e --php-version 8.4GitHub Actions
The E2E workflow (.github/workflows/e2e.yml) runs a matrix of PHP 8.4 and 8.5:
strategy:
matrix:
php: ["8.4", "8.5"]
steps:
- cargo xtask e2e-install
- cargo xtask e2e --php-version ${{ matrix.php }}Each job builds ephpm with the specified PHP version, deploys it to a Kind cluster, and validates that /index.php reports the correct PHP version and embedded SAPI.
Development Workflow
| Task | Command | Infrastructure needed |
|---|---|---|
| HTTP routing, config, CLI | cargo build + cargo nextest | None (stub mode) |
| PHP execution | cargo xtask release + cargo nextest --run-ignored all | PHP SDK |
| E2E tests (headless) | cargo xtask e2e --php-version 8.5 | Kind + Podman/Docker |
| E2E dev environment | cargo xtask e2e-up --php-version 8.5 | Kind + Podman/Docker |
| Tear down E2E | cargo xtask e2e-down | — |
Future E2E Tests (Planned)
These will be added as the corresponding features are implemented:
- Cluster tests — 3-node StatefulSet, KV gossip replication, node failure recovery
- DB proxy tests — MySQL/Postgres connection pooling, query digest, slow query detection
- WordPress lifecycle — Install wizard, post creation, plugin activation
- External PHP mode — Validate worker process management