Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -102,13 +102,17 @@ miniwebapp-gates:
./script/miniwebapp-gates.sh

# HTTP smoke: phpc serve + curl for 001-SimpleWeb and 002-StaticWeb (issue #298)
.PHONY: examples-web-smoke examples-web-smoke-prebuild
.PHONY: examples-web-smoke examples-web-smoke-prebuild examples-aot-smoke
examples-web-smoke:
./script/examples-web-smoke.sh

examples-web-smoke-prebuild:
./script/examples-web-smoke-prebuild.sh

# AOT build + CLI execute for 000-004 (issue #667); skips when LLVM missing
examples-aot-smoke:
./script/examples-aot-smoke.sh

# Local HTTP dev server (see bin/serve.php)
SERVE_ADDR ?= 127.0.0.1:8080
SERVE_ROOT ?= examples/001-SimpleWeb
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ mkdir my-app && ./phpc init my-app # phpc.json + public/index.php scaffold
./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-aot-smoke # phpc build + CLI execute for 000–004 when LLVM ready (#667)
./phpc serve examples/001-SimpleWeb # http://127.0.0.1:8080/ (or: make serve)
```

Expand Down
1 change: 1 addition & 0 deletions examples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,7 @@ 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-aot-smoke # phpc build + CLI execute (000–004; skips when LLVM missing; 003 skipped #568)
```

Full suite on the host (after `composer install`):
Expand Down
9 changes: 9 additions & 0 deletions script/ci-common.sh
Original file line number Diff line number Diff line change
Expand Up @@ -168,3 +168,12 @@ ci_run_examples_web_smoke_aot() {
echo "examples-web-smoke (AOT): HTTP harness via phpc serve --aot..."
"$_CI_SCRIPT_DIR/examples-web-smoke.sh" --aot
}

# CLI AOT build + execute smoke (issue #667); opt-in via EXAMPLES_AOT_SMOKE_GATE=1.
ci_run_examples_aot_smoke() {
if [[ "${EXAMPLES_AOT_SMOKE_GATE:-}" != "1" ]]; then
return 0
fi
echo "examples-aot-smoke: CLI build + execute (EXAMPLES_AOT_SMOKE_GATE=1, #667)..."
"$_CI_SCRIPT_DIR/examples-aot-smoke.sh"
}
1 change: 1 addition & 0 deletions script/ci-local.sh
Original file line number Diff line number Diff line change
Expand Up @@ -68,4 +68,5 @@ if ci_llvm_ready; then
fi

ci_run_examples_web_smoke_aot
ci_run_examples_aot_smoke
fi
158 changes: 158 additions & 0 deletions script/examples-aot-smoke.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
#!/usr/bin/env bash
# AOT build + CLI execute smoke for shipped examples (issue #667).
#
# Builds each example to .phpc/smoke/<name>/app, runs the native binary once,
# and checks stdout needles (no HTTP). Skips with exit 0 when LLVM 9 is missing.
#
# Usage:
# ./script/examples-aot-smoke.sh
#
# Docker:
# docker run --rm -v "$(pwd):/compiler" -w /compiler php-compiler:22.04-dev make examples-aot-smoke
#
# 003-MiniWebApp stays skipped until #568 / #485 (native user-class AOT link).
set -euo pipefail

cd "$(dirname "$0")/.."
ROOT="$PWD"
PHPC="${ROOT}/phpc"
SMOKE_ROOT="${ROOT}/.phpc/smoke"

resolve_llvm_dir() {
if [[ -n "${PHP_COMPILER_LLVM_PATH:-}" ]]; then
if [[ -f "${PHP_COMPILER_LLVM_PATH}/libLLVM-9.so.1" ]]; then
echo "${PHP_COMPILER_LLVM_PATH}"
return 0
fi
return 1
fi
if [[ -f "${ROOT}/.llvm/libLLVM-9.so.1" ]]; then
echo "${ROOT}/.llvm"
return 0
fi
if [[ -f /opt/llvm9/libLLVM-9.so.1 ]]; then
echo /opt/llvm9
return 0
fi
return 1
}

LLVM_DIR=""
if ! LLVM_DIR="$(resolve_llvm_dir)"; then
hint="${PHP_COMPILER_LLVM_PATH:-${ROOT}/.llvm}"
echo "examples-aot-smoke: skipped (LLVM 9 not available at ${hint})"
exit 0
fi
export PHP_COMPILER_LLVM_PATH="$LLVM_DIR"

# shellcheck source=php-env.sh
source "${ROOT}/script/php-env.sh"
export PHP_COMPILER_LLVM_PATH="$LLVM_DIR"

if [[ ! -x "$PHPC" ]]; then
echo "examples-aot-smoke: phpc wrapper missing or not executable: ${PHPC}" >&2
exit 1
fi

mkdir -p "$SMOKE_ROOT"

build_binary() {
local label="$1"
local source="$2"
local outfile="$3"
local bindir
bindir="$(dirname "$outfile")"
mkdir -p "$bindir"
if [[ -x "$outfile" ]]; then
rm -f "$outfile"
fi
echo "examples-aot-smoke: ${label}: phpc build -> ${outfile}"
"$PHPC" build -o "$outfile" "$source"
if [[ ! -x "$outfile" ]]; then
echo "examples-aot-smoke: ${label}: expected executable ${outfile}" >&2
exit 1
fi
}

assert_needles() {
local label="$1"
local output="$2"
shift 2
local needle
for needle in "$@"; do
if [[ "$output" != *"$needle"* ]]; then
echo "examples-aot-smoke: ${label}: stdout missing needle: ${needle}" >&2
echo "--- stdout ---" >&2
echo "$output" >&2
echo "--- end ---" >&2
exit 1
fi
done
}

run_binary() {
local label="$1"
local binary="$2"
shift 2
local stderr_file stdout stderr
stderr_file="$(mktemp "${SMOKE_ROOT}/run.XXXXXX")"
if (("$#" > 0)); then
stdout="$(env "$@" "$binary" 2>"$stderr_file")"
else
stdout="$("$binary" 2>"$stderr_file")"
fi
local exit_code=$?
stderr="$(cat "$stderr_file" 2>/dev/null || true)"
rm -f "$stderr_file"
if [[ "$exit_code" -ne 0 ]]; then
echo "examples-aot-smoke: ${label}: binary exited ${exit_code}" >&2
[[ -n "$stderr" ]] && echo "$stderr" >&2
exit 1
fi
if [[ -n "$stderr" ]]; then
echo "examples-aot-smoke: ${label}: stderr: ${stderr}" >&2
exit 1
fi
printf '%s' "$stdout"
}

echo "examples-aot-smoke: starting (LLVM at ${LLVM_DIR})"

# 000-HelloWorld — single script, no phpc.json
build_binary '000-HelloWorld' \
"${ROOT}/examples/000-HelloWorld/example.php" \
"${SMOKE_ROOT}/000-HelloWorld/app"
out="$(run_binary '000-HelloWorld' "${SMOKE_ROOT}/000-HelloWorld/app")"
assert_needles '000-HelloWorld' "$out" 'Hello World'
echo "examples-aot-smoke: 000-HelloWorld: ok"

# 001-SimpleWeb — runtime QUERY_STRING refresh (issue #309)
build_binary '001-SimpleWeb' \
"${ROOT}/examples/001-SimpleWeb/example.php" \
"${SMOKE_ROOT}/001-SimpleWeb/app"
out="$(run_binary '001-SimpleWeb' "${SMOKE_ROOT}/001-SimpleWeb/app" \
'QUERY_STRING=name=Smoke' \
'SCRIPT_NAME=/example.php' \
'REQUEST_URI=/example.php?name=Smoke')"
assert_needles '001-SimpleWeb' "$out" '<h1>Hello Smoke</h1>'
echo "examples-aot-smoke: 001-SimpleWeb: ok"

# 002-StaticWeb
build_binary '002-StaticWeb' \
"${ROOT}/examples/002-StaticWeb/example.php" \
"${SMOKE_ROOT}/002-StaticWeb/app"
out="$(run_binary '002-StaticWeb' "${SMOKE_ROOT}/002-StaticWeb/app")"
assert_needles '002-StaticWeb' "$out" 'Hello World'
echo "examples-aot-smoke: 002-StaticWeb: ok"

# 004-ApiJson
build_binary '004-ApiJson' \
"${ROOT}/examples/004-ApiJson/example.php" \
"${SMOKE_ROOT}/004-ApiJson/app"
out="$(run_binary '004-ApiJson' "${SMOKE_ROOT}/004-ApiJson/app")"
assert_needles '004-ApiJson' "$out" 'Content-Type: application/json' 'Status: 200' '"ok":true' 'php-compiler'
echo "examples-aot-smoke: 004-ApiJson: ok"

echo "examples-aot-smoke: 003-MiniWebApp: skip (AOT link blocked #568; see #485)" >&2

echo "examples-aot-smoke: ok"
21 changes: 21 additions & 0 deletions test/unit/CiScriptsTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,27 @@ public function testExamplesWebSmokePrebuildScriptExists(): void
$this->assertStringContainsString('phpc build --project', $body);
}

public function testExamplesAotSmokeScriptExists(): void
{
$script = dirname(__DIR__, 2).'/script/examples-aot-smoke.sh';
$this->assertFileExists($script);
$this->assertTrue(is_executable($script));
$body = (string) file_get_contents($script);
$this->assertStringContainsString('000-HelloWorld', $body);
$this->assertStringContainsString('QUERY_STRING=name=Smoke', $body);
$this->assertStringContainsString('.phpc/smoke', $body);
}

public function testCiLocalHonorsExamplesAotSmokeGate(): void
{
$local = (string) file_get_contents(dirname(__DIR__, 2).'/script/ci-local.sh');
$this->assertStringContainsString('ci_run_examples_aot_smoke', $local);

$common = (string) file_get_contents(dirname(__DIR__, 2).'/script/ci-common.sh');
$this->assertStringContainsString('EXAMPLES_AOT_SMOKE_GATE', $common);
$this->assertStringContainsString('examples-aot-smoke.sh', $common);
}

public function testCiResourceLimitsSourcesDefaults(): void
{
$limits = dirname(__DIR__, 2).'/script/ci-resource-limits.sh';
Expand Down
103 changes: 103 additions & 0 deletions test/unit/ExamplesAotSmokeScriptTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
<?php

declare(strict_types=1);

namespace PHPCompiler;

use PHPUnit\Framework\TestCase;

/**
* script/examples-aot-smoke.sh CLI AOT harness (issue #667).
*/
final class ExamplesAotSmokeScriptTest extends TestCase
{
public function testExamplesAotSmokeScriptSkipsWhenLlvmMissing(): void
{
$repoRoot = dirname(__DIR__, 2);
$script = $repoRoot.'/script/examples-aot-smoke.sh';
$this->assertFileIsReadable($script);

$env = $this->baseEnv();
unset($env['PHP_COMPILER_LLVM_PATH']);
$env['PHP_COMPILER_LLVM_PATH'] = $repoRoot.'/.llvm-missing-probe-'.bin2hex(random_bytes(4));

$descriptorSpec = [
0 => ['pipe', 'r'],
1 => ['pipe', 'w'],
2 => ['pipe', 'w'],
];
$proc = proc_open(['bash', $script], $descriptorSpec, $pipes, $repoRoot, $env);
$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);

$combined = trim(($stdout !== false ? $stdout : '')."\n".($stderr !== false ? $stderr : ''));
$this->assertSame(0, $exit, $combined);
$this->assertStringContainsString('skipped (LLVM 9 not available', $combined);
}

public function testExamplesAotSmokeScriptDocumentsMiniWebAppSkip(): void
{
$body = (string) file_get_contents(dirname(__DIR__, 2).'/script/examples-aot-smoke.sh');
$this->assertStringContainsString('003-MiniWebApp', $body);
$this->assertStringContainsString('#568', $body);
$this->assertStringContainsString('.phpc/smoke', $body);
}

public function testExamplesAotSmokeScriptPassesWhenLlvmReady(): void
{
if (!LlvmToolchain::isReady(dirname(__DIR__, 2))) {
$this->markTestSkipped(
'LLVM 9 toolchain not available. Run script/install-llvm9.sh from the repository root.'
);
}

$repoRoot = dirname(__DIR__, 2);
$phpc = $repoRoot.'/phpc';
if (!is_executable($phpc)) {
$this->markTestSkipped('phpc wrapper not executable');
}

$script = $repoRoot.'/script/examples-aot-smoke.sh';
$descriptorSpec = [
0 => ['pipe', 'r'],
1 => ['pipe', 'w'],
2 => ['pipe', 'w'],
];
$proc = proc_open(['bash', $script], $descriptorSpec, $pipes, $repoRoot, $this->baseEnv());
$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);

$combined = trim(($stdout !== false ? $stdout : '')."\n".($stderr !== false ? $stderr : ''));
$this->assertSame(0, $exit, $combined);
$this->assertStringContainsString('examples-aot-smoke: 000-HelloWorld: ok', $combined);
$this->assertStringContainsString('examples-aot-smoke: 001-SimpleWeb: ok', $combined);
$this->assertStringContainsString('examples-aot-smoke: 002-StaticWeb: ok', $combined);
$this->assertStringContainsString('examples-aot-smoke: 004-ApiJson: ok', $combined);
$this->assertStringContainsString('examples-aot-smoke: ok', $combined);
}

/**
* @return array<string, string>
*/
private function baseEnv(): array
{
$env = [];
foreach (array_merge($_ENV, $_SERVER) as $key => $value) {
if (is_string($key) && is_string($value)) {
$env[$key] = $value;
}
}

return $env;
}
}