test(wasm): add comprehensive WASM testing infrastructure and runtime

- Integrate browser-based WASM test runner using Puppeteer
- Add built-in extensions test for WASM runtime compatibility
- Implement WASI filesystem, clock and random functionality tests
- Create IO and environment variable handling tests for WASI
- Add PHPX numeric containers test for high precision types
- Include basic runtime platform detection tests
- Update run-tests.php with --target and --wasm command options
- Implement multi-target test execution for native and WASM backends
- Add WASM-specific configuration sections and argument parsing
- Integrate dependency management for browser test harness
- Refactor build script references from build-typephp-program.sh to build-program.sh
- Remove legacy numeric smoke test files and update documentation
- Migrate high precision example location from wasm/examples to examples directory
pull/47/head
韩天峰 2 weeks ago
parent 1072e9d1c7
commit a4f1188a7e
  1. 1
      .gitignore
  2. 4
      docs/WASI_BUILD.md
  3. 0
      examples/high-precision.php
  4. 4
      package.php
  5. 263
      run-tests.php
  6. 2
      src/compiler.php
  7. 23
      tests/wasm/extensions/builtin-extensions.phpt
  8. 177
      tests/wasm/harness/browser-runner.mjs
  9. 3807
      tests/wasm/harness/package-lock.json
  10. 9
      tests/wasm/harness/package.json
  11. 33
      tests/wasm/runtime/phpx-numeric-containers.phpt
  12. 15
      tests/wasm/smoke/runtime.phpt
  13. 19
      tests/wasm/wasi/clock-random.phpt
  14. 23
      tests/wasm/wasi/filesystem.phpt
  15. 22
      tests/wasm/wasi/io-environment.phpt
  16. 12
      wasm/README.md
  17. 0
      wasm/build-program.sh
  18. 38
      wasm/link-numeric-smoke-test.sh
  19. 52
      wasm/numeric-smoke-test.cc
  20. 35
      wasm/test-typephp-program.sh

1
.gitignore vendored

@ -30,3 +30,4 @@ tests/**/*.out
tests/**/*.php
tests/**/*.sh
/*.browser/
/tests/wasm/harness/node_modules/

@ -174,10 +174,10 @@ WASI 产物包含 TypePHP 的三种语言级高精度类型:
- `BigFloat`:MPFR 4.2.2
- `Decimal`:mpdecimal 4.0.1
完整示例位于 [high-precision.php](../wasm/examples/high-precision.php)。构建并运行:
完整示例位于 [high-precision.php](../examples/high-precision.php)。构建并运行:
```bash
php bin/tpc.php --wasm wasm/examples/high-precision.php
php bin/tpc.php --wasm examples/high-precision.php
wasmtime -S http high-precision.wasm
```

@ -608,7 +608,7 @@ function packageUnixLike(): void
'README.md',
'LICENSE.md',
'examples/hello.php',
'wasm/build-typephp-program.sh',
'wasm/build-program.sh',
"{$wasiSdkSourceRoot}/.typephp-wasi-sdk-abi",
"{$wasiSdkSourceRoot}/include/php/main/php.h",
"{$wasiSdkSourceRoot}/include/php/main/php_config.h",
@ -740,7 +740,7 @@ function packageUnixLike(): void
"{$topLevelDir}/README.md",
"{$topLevelDir}/LICENSE.md",
"{$topLevelDir}/examples/hello.php",
"{$topLevelDir}/wasm/build-typephp-program.sh",
"{$topLevelDir}/wasm/build-program.sh",
"{$topLevelDir}/{$wasiSdkPackageRoot}/.typephp-wasi-sdk-abi",
"{$topLevelDir}/{$wasiSdkPackageRoot}/include/php/main/php.h",
"{$topLevelDir}/{$wasiSdkPackageRoot}/include/phpx/phpx.h",

@ -134,6 +134,12 @@ Options:
Use specified compiler binary (default: ./bin/tpc.php).
For bootstrap testing, use: --compiler ./swoole_compiler
--target <target>
Select the execution backend: native (default), wasm-component,
wasm-browser, or wasm-all.
--wasm Shorthand for --target wasm-component.
--bless Bless failed tests using scripts/dev/bless_tests.php.
HELP;
@ -161,7 +167,7 @@ function main(): void
$temp_source, $temp_target, $test_cnt,
$test_files, $test_idx, $test_results, $testfile,
$valgrind, $sum_results, $shuffle, $file_cache, $num_repeats,
$show_progress, $aot_parallel_root;
$show_progress, $aot_parallel_root, $test_target;
// Parallel testing
global $workers, $workerID;
global $context_line_count;
@ -358,6 +364,7 @@ function main(): void
$bless = false;
$workers = null;
$aot_parallel_root = null;
$test_target = 'native';
$context_line_count = 3;
$num_repeats = 1;
$show_progress = true;
@ -503,6 +510,15 @@ function main(): void
case '--compiler':
$compiler_path = $argv[++$i];
break;
case '--wasm':
$test_target = 'wasm-component';
break;
case '--target':
$test_target = strtolower($argv[++$i] ?? '');
if (!in_array($test_target, ['native', 'wasm-component', 'wasm-browser', 'wasm-all'], true)) {
error("Unsupported test target '$test_target'; expected native, wasm-component, wasm-browser, or wasm-all");
}
break;
case '--color':
$colorize = true;
break;
@ -681,6 +697,16 @@ function main(): void
if (!$compiler_path) {
$compiler_path = './bin/tpc.php';
}
if ($test_target !== 'native' && $no_aot) {
error('--no-aot cannot be combined with a WASM test target');
}
if (in_array($test_target, ['wasm-browser', 'wasm-all'], true)) {
$harnessBin = __DIR__ . '/tests/wasm/harness/node_modules/.bin';
if (is_dir($harnessBin)) {
$environment['PATH'] = $harnessBin . PATH_SEPARATOR . ($environment['PATH'] ?? getenv('PATH') ?: '');
putenv('PATH=' . $environment['PATH']);
}
}
$php_cgi = getenv('TEST_PHP_CGI_EXECUTABLE') ?: get_binary($php, 'php-cgi', 'sapi/cgi/php-cgi');
$phpdbg = getenv('TEST_PHPDBG_EXECUTABLE') ?: get_binary($php, 'phpdbg', 'sapi/phpdbg/phpdbg');
@ -1832,7 +1858,7 @@ function run_test(string $php, $file, array $env): string
global $slow_min_ms;
global $preload, $file_cache;
global $num_repeats;
global $compiler_path;
global $compiler_path, $test_target;
// Parallel testing
global $workerID;
global $show_progress;
@ -1854,6 +1880,7 @@ function run_test(string $php, $file, array $env): string
retry:
$org_file = $file;
$wasmCommands = [];
$php_cgi = $env['TEST_PHP_CGI_EXECUTABLE'] ?? null;
$phpdbg = $env['TEST_PHPDBG_EXECUTABLE'] ?? null;
@ -1890,6 +1917,16 @@ TEST $file
$tested = $test->getName();
if ($test_target !== 'native' && $test->hasSection('WASM_TARGETS')) {
$targets = preg_split('/[\s,]+/', trim($test->getSection('WASM_TARGETS')), -1, PREG_SPLIT_NO_EMPTY);
$selectedTargets = $test_target === 'wasm-all'
? ['wasm-component', 'wasm-browser']
: [$test_target];
if (array_intersect($selectedTargets, $targets) === []) {
return skip_test($tested, $tested_file, $shortname, "not enabled for $test_target");
}
}
if ($test->hasSection('FILE_EXTERNAL')) {
if ($num_repeats > 1) {
return skip_test($tested, $tested_file, $shortname, 'Test with FILE_EXTERNAL might not be repeatable');
@ -2455,7 +2492,19 @@ TEST $file
if (!$no_aot) {
try {
$aot_args = $test->hasSection('AOT_ARGS') ? trim($test->getSection('AOT_ARGS')) : '';
if ($test_target === 'native') {
$bin_file = compile_php_file($test_file, $aot_args);
} else {
$guestArgs = parse_wasm_test_args($test->hasSection('ARGS') ? $test->getSection('ARGS') : '');
$guestEnv = parse_wasm_test_env($test, $file);
foreach (get_wasm_test_profiles($test_target, $test) as $profile) {
$artifact = compile_wasm_php_file($test_file, $profile, $aot_args);
$wasmCommands[] = $profile === 'component'
? create_wasmtime_test_command($artifact, $guestArgs, $guestEnv)
: create_wasm_browser_test_command($artifact, $guestArgs, $guestEnv);
}
$bin_file = $artifact['wasm'];
}
} catch (Throwable $e) {
$compileOutput = trim($e instanceof CompilationFailureException ? $e->getCompilerOutput() : '');
$message = $e->getMessage();
@ -2488,11 +2537,15 @@ $message
$junit->markTestAs('FAIL', $shortname, $tested, null, $message, $compileOutput);
return 'FAILED';
}
if ($test_target === 'native') {
$args = substr($args, strlen(' -- '));
$executable = str_contains($bin_file, DIRECTORY_SEPARATOR)
? $bin_file
: (IS_WINDOWS ? '.\\' : './') . $bin_file;
$cmd = escapeshellarg($executable) . ' ' . $args . $cmdRedirect;
} else {
$cmd = $wasmCommands[0] . $cmdRedirect;
}
} else {
$content = file_get_contents($test_file);
if (preg_match('/function main\(\)/', $content)) {
@ -2536,7 +2589,30 @@ COMMAND $cmd
$startTime = $hrtime[0] * 1000000000 + $hrtime[1];
$stdin = $test->hasSection('STDIN') ? $test->getSection('STDIN') : null;
if (count($wasmCommands) > 1) {
$wasmOutputs = [];
foreach ($wasmCommands as $wasmCommand) {
$wasmOutputs[] = system_with_timeout(
$wasmCommand . $cmdRedirect,
$env,
$stdin,
$captureStdIn,
$captureStdOut,
$captureStdErr
);
}
$out = array_shift($wasmOutputs);
foreach ($wasmOutputs as $wasmOutput) {
if (normalize_wasm_test_output($wasmOutput) !== normalize_wasm_test_output($out)) {
$out = "WASM backend output mismatch\n"
. "--- wasm-component ---\n" . $out
. "--- wasm-browser ---\n" . $wasmOutput;
break;
}
}
} else {
$out = system_with_timeout($cmd, $env, $stdin, $captureStdIn, $captureStdOut, $captureStdErr);
}
$junit->stopTimer($shortname);
$hrtime = hrtime();
@ -3771,7 +3847,7 @@ class TestFile
private const ALLOWED_SECTIONS = [
'EXPECT', 'EXPECTF', 'EXPECTREGEX', 'EXPECTREGEX_EXTERNAL', 'EXPECT_EXTERNAL', 'EXPECTF_EXTERNAL', 'EXPECTHEADERS',
'POST', 'POST_RAW', 'GZIP_POST', 'DEFLATE_POST', 'PUT', 'GET', 'COOKIE', 'ARGS', 'AOT_ARGS',
'POST', 'POST_RAW', 'GZIP_POST', 'DEFLATE_POST', 'PUT', 'GET', 'COOKIE', 'ARGS', 'AOT_ARGS', 'WASM_TARGETS',
'FILE', 'FILEEOF', 'FILE_EXTERNAL', 'REDIRECTTEST',
'CAPTURE_STDIO', 'STDIN', 'CGI', 'PHPDBG',
'INI', 'ENV', 'EXTENSIONS',
@ -4271,6 +4347,187 @@ function debug()
exit;
}
/** @return string[] */
function get_wasm_test_profiles(string $target, TestFile $test): array
{
$profiles = match ($target) {
'wasm-component' => ['component'],
'wasm-browser' => ['browser'],
'wasm-all' => ['component', 'browser'],
default => [],
};
if (!$test->hasSection('WASM_TARGETS')) {
return $profiles;
}
$enabled = preg_split('/[\s,]+/', trim($test->getSection('WASM_TARGETS')), -1, PREG_SPLIT_NO_EMPTY);
return array_values(array_filter(
$profiles,
static fn(string $profile): bool => in_array('wasm-' . $profile, $enabled, true)
));
}
/** @return string[] */
function parse_wasm_test_args(string $args): array
{
if (trim($args) === '') {
return [];
}
preg_match_all('/"((?:\\\\.|[^"\\\\])*)"|\'((?:\\\\.|[^\'\\\\])*)\'|([^\s]+)/', trim($args), $matches, PREG_SET_ORDER);
$result = [];
foreach ($matches as $match) {
$value = $match[1] !== '' ? $match[1] : ($match[2] !== '' ? $match[2] : $match[3]);
$result[] = preg_replace('/\\\\([\\\\"\'])/', '$1', $value);
}
return $result;
}
/** @return array<string, string> */
function parse_wasm_test_env(TestFile $test, string $file): array
{
if (!$test->sectionNotEmpty('ENV')) {
return [];
}
$result = [];
$env = str_replace('{PWD}', dirname($file), $test->getSection('ENV'));
foreach (preg_split('/\r?\n/', trim($env)) as $line) {
$parts = explode('=', trim($line), 2);
if ($parts[0] !== '' && isset($parts[1])) {
$result[$parts[0]] = $parts[1];
}
}
return $result;
}
/** @return array{wasm:string,browser:string,root:string,sandbox:string} */
function compile_wasm_php_file(string $file, string $profile, string $compilerArgs = ''): array
{
global $compiler_path, $workerID, $aot_parallel_root;
if ($aot_parallel_root === null) {
$aot_parallel_root = create_aot_parallel_root();
register_shutdown_function(static function () use (&$aot_parallel_root): void {
if ($aot_parallel_root !== null) {
remove_directory($aot_parallel_root);
$aot_parallel_root = null;
}
});
}
$case = preg_replace('/[^A-Za-z0-9_.-]+/', '-', basename($file, '.php'))
. '-' . substr(sha1((string) realpath($file)), 0, 12);
$root = $aot_parallel_root . DIRECTORY_SEPARATOR . 'worker-' . $workerID
. DIRECTORY_SEPARATOR . $case . DIRECTORY_SEPARATOR . $profile;
$build = $root . DIRECTORY_SEPARATOR . 'build';
$output = $root . DIRECTORY_SEPARATOR . 'output';
$sandbox = $root . DIRECTORY_SEPARATOR . 'sandbox';
ensure_directory_exists($build);
ensure_directory_exists($output);
ensure_directory_exists($sandbox);
$compiler = $compiler_path;
if (!str_starts_with($compiler, DIRECTORY_SEPARATOR)) {
$resolved = realpath(INIT_DIR . DIRECTORY_SEPARATOR . $compiler);
if ($resolved !== false) {
$compiler = $resolved;
}
}
$command = str_ends_with($compiler, '.php')
? [PHP_BINARY, $compiler]
: [$compiler];
array_push($command, $file, '--wasm=' . $profile, '--build-dir', $build);
if ($compilerArgs !== '') {
array_push($command, ...parse_wasm_test_args($compilerArgs));
}
$log = $root . DIRECTORY_SEPARATOR . 'compile.log';
$process = proc_open(
$command,
[STDIN, ['file', $log, 'w'], ['file', $log, 'a']],
$pipes,
$output,
null,
);
if (!is_resource($process)) {
throw new CompilationFailureException('Unable to start the WASM compiler');
}
$exitCode = proc_close($process);
$compilerOutput = is_file($log) ? trim((string) file_get_contents($log)) : '';
$stem = preg_replace('/[^A-Za-z0-9_-]+/', '_', basename($file, '.php'));
$wasm = $output . DIRECTORY_SEPARATOR . $stem . '.wasm';
$browser = $output . DIRECTORY_SEPARATOR . $stem . '.browser';
if ($exitCode !== 0 || !is_file($wasm) || ($profile === 'browser' && !is_file($browser . '/program.js'))) {
throw new CompilationFailureException('WASM compilation failed', $compilerOutput);
}
return ['wasm' => $wasm, 'browser' => $browser, 'root' => $root, 'sandbox' => $sandbox];
}
/** @param string[] $args @param array<string, string> $env */
function create_wasmtime_test_command(array $artifact, array $args, array $env): string
{
$wasmtime = find_test_executable('TYPEPHP_WASMTIME', ['wasmtime']);
if ($wasmtime === null) {
throw new CompilationFailureException("Required WASI runtime 'wasmtime' was not found in PATH");
}
$command = escapeshellarg($wasmtime) . ' run -S http --dir '
. escapeshellarg($artifact['sandbox'] . '::/sandbox');
foreach ($env as $name => $value) {
$command .= ' --env ' . escapeshellarg($name . '=' . $value);
}
$command .= ' ' . escapeshellarg($artifact['wasm']);
foreach ($args as $arg) {
$command .= ' ' . escapeshellarg($arg);
}
return $command;
}
/** @param string[] $args @param array<string, string> $env */
function create_wasm_browser_test_command(array $artifact, array $args, array $env): string
{
$node = find_test_executable('TYPEPHP_NODE', ['node']);
$chrome = find_test_executable('TYPEPHP_CHROME', ['google-chrome', 'chromium', 'chromium-browser']);
$runner = __DIR__ . '/tests/wasm/harness/browser-runner.mjs';
if ($node === null || $chrome === null || !is_file($runner)) {
throw new CompilationFailureException('WASM browser test requires Node.js, Chrome, and the browser harness');
}
$optionsFile = $artifact['root'] . DIRECTORY_SEPARATOR . 'browser-options.json';
file_put_contents($optionsFile, json_encode([
'args' => $args,
'env' => $env,
'argv0' => basename($artifact['wasm']),
], JSON_THROW_ON_ERROR));
return escapeshellarg($node) . ' ' . escapeshellarg($runner)
. ' ' . escapeshellarg($artifact['browser'])
. ' ' . escapeshellarg($chrome)
. ' ' . escapeshellarg($optionsFile);
}
function find_test_executable(string $environmentName, array $names): ?string
{
$configured = getenv($environmentName);
if (is_string($configured) && $configured !== '' && is_executable($configured)) {
return realpath($configured) ?: $configured;
}
$path = getenv('PATH') ?: '';
foreach (explode(PATH_SEPARATOR, $path) as $directory) {
foreach ($names as $name) {
$candidate = rtrim($directory, '/\\') . DIRECTORY_SEPARATOR . $name . (IS_WINDOWS ? '.exe' : '');
if (is_file($candidate) && is_executable($candidate)) {
return realpath($candidate) ?: $candidate;
}
}
}
return null;
}
function normalize_wasm_test_output(string $output): string
{
return str_replace("\r\n", "\n", trim($output));
}
function compile_php_file(string $file, string $compiler_args = ''): string
{
global $compiler_path, $workerID, $aot_parallel_root;

@ -163,7 +163,7 @@ function compileWasmProgram(array $argv): void
exit(1);
}
$builder = dirname(__DIR__) . '/wasm/build-typephp-program.sh';
$builder = dirname(__DIR__) . '/wasm/build-program.sh';
if (!is_executable($builder)) {
fwrite(STDERR, "TypePHP WASI builder is not executable: {$builder}\n");
exit(1);

@ -0,0 +1,23 @@
--TEST--
WASM runtime contains the portable PHP extension set
--FILE--
<?php
function main(): void
{
$loaded = array_fill_keys(get_loaded_extensions(), true);
foreach (['Core', 'date', 'ctype', 'calendar', 'bcmath', 'filter', 'tokenizer', 'mbstring', 'zlib', 'fileinfo'] as $extension) {
echo $extension, '=', isset($loaded[$extension]) ? 'yes' : 'no', "\n";
}
}
?>
--EXPECT--
Core=yes
date=yes
ctype=yes
calendar=yes
bcmath=yes
filter=yes
tokenizer=yes
mbstring=yes
zlib=yes
fileinfo=yes

@ -0,0 +1,177 @@
import fs from 'node:fs/promises';
import http from 'node:http';
import path from 'node:path';
import process from 'node:process';
import { fileURLToPath } from 'node:url';
import puppeteer from 'puppeteer-core';
const [, , artifactDir, chromePath, optionsFile] = process.argv;
if (!artifactDir || !chromePath || !optionsFile) {
throw new Error('Usage: browser-runner.mjs <artifact-dir> <chrome> <options.json>');
}
const harnessDir = path.dirname(fileURLToPath(import.meta.url));
const shimDir = path.join(harnessDir, 'node_modules', '@bytecodealliance', 'preview2-shim');
const options = JSON.parse(await fs.readFile(optionsFile, 'utf8'));
const stdin = await new Promise((resolve, reject) => {
let value = '';
process.stdin.setEncoding('utf8');
process.stdin.on('data', (chunk) => value += chunk);
process.stdin.on('end', () => resolve(value));
process.stdin.on('error', reject);
});
const contentTypes = new Map([
['.html', 'text/html; charset=utf-8'],
['.js', 'text/javascript; charset=utf-8'],
['.json', 'application/json; charset=utf-8'],
['.wasm', 'application/wasm'],
]);
function safePath(root, requestPath) {
const relative = decodeURIComponent(requestPath).replace(/^\/+/, '');
const resolved = path.resolve(root, relative);
const base = path.resolve(root) + path.sep;
return resolved.startsWith(base) ? resolved : null;
}
const pageOptions = JSON.stringify({ ...options, stdin }).replace(/</g, '\\u003c');
const html = `<!doctype html>
<meta charset="utf-8">
<script type="importmap">
{"imports":{"@bytecodealliance/preview2-shim":"/deps/lib/browser/index.js","@bytecodealliance/preview2-shim/":"/deps/lib/browser/"}}
</script>
<script>globalThis.TYPEPHP_WASM_TEST_OPTIONS = ${pageOptions};</script>
<script type="module" src="/harness.js"></script>`;
const harnessSource = `
import { _setStderr, _setStdin, _setStdout } from '/deps/lib/browser/cli.js';
import { _setFileData } from '/deps/lib/browser/filesystem.js';
import { WASIShim } from '/deps/lib/common/instantiation.js';
import { instantiate } from '/artifact/program.js';
const decoder = new TextDecoder();
const encoder = new TextEncoder();
const options = globalThis.TYPEPHP_WASM_TEST_OPTIONS;
let output = '';
function outputHandler() {
return {
write(bytes) {
output += decoder.decode(bytes, { stream: true });
return BigInt(bytes.byteLength);
},
blockingFlush() {},
};
}
function inputHandler(text) {
const bytes = encoder.encode(text);
let offset = 0;
return {
blockingRead(length) {
if (offset >= bytes.byteLength) throw { tag: 'closed' };
const end = Math.min(offset + Number(length), bytes.byteLength);
const value = bytes.slice(offset, end);
offset = end;
return value;
},
};
}
try {
if (typeof WebAssembly.Suspending !== 'function' || typeof WebAssembly.promising !== 'function') {
throw new Error('Chrome does not provide WebAssembly JSPI');
}
_setFileData({ dir: { sandbox: { dir: {} } } });
_setStdin(inputHandler(String(options.stdin || '')));
_setStdout(outputHandler());
_setStderr(outputHandler());
const wasi = new WASIShim({ sandbox: {
args: [String(options.argv0 || 'test.wasm'), ...(options.args || []).map(String)],
env: { ...(options.env || {}) },
enableNetwork: true,
}});
const component = await instantiate(null, wasi.getImportObject());
const result = await component.run.run();
globalThis.TYPEPHP_WASM_TEST_RESULT = { output, result };
} catch (error) {
if (error?.exitError && error.code === 0) {
globalThis.TYPEPHP_WASM_TEST_RESULT = { output };
} else {
globalThis.TYPEPHP_WASM_TEST_RESULT = { output, error: error?.stack || String(error) };
}
}
`;
const server = http.createServer(async (request, response) => {
try {
const url = new URL(request.url, 'http://127.0.0.1');
if (url.pathname === '/') {
response.writeHead(200, { 'content-type': 'text/html; charset=utf-8' });
response.end(html);
return;
}
if (url.pathname === '/harness.js') {
response.writeHead(200, { 'content-type': 'text/javascript; charset=utf-8' });
response.end(harnessSource);
return;
}
const mapping = url.pathname.startsWith('/artifact/')
? [artifactDir, url.pathname.slice('/artifact/'.length)]
: url.pathname.startsWith('/deps/')
? [shimDir, url.pathname.slice('/deps/'.length)]
: null;
if (!mapping) {
response.writeHead(404).end();
return;
}
const filename = safePath(mapping[0], mapping[1]);
if (!filename) {
response.writeHead(403).end();
return;
}
const data = await fs.readFile(filename);
response.writeHead(200, { 'content-type': contentTypes.get(path.extname(filename)) || 'application/octet-stream' });
response.end(data);
} catch (error) {
response.writeHead(error?.code === 'ENOENT' ? 404 : 500).end(String(error));
}
});
await new Promise((resolve, reject) => {
server.once('error', reject);
server.listen(0, '127.0.0.1', resolve);
});
let browser;
try {
const address = server.address();
browser = await puppeteer.launch({
executablePath: chromePath,
headless: true,
args: ['--no-sandbox', '--disable-dev-shm-usage', '--enable-features=WebAssemblyJSPI'],
});
const page = await browser.newPage();
const diagnostics = [];
page.on('console', (message) => diagnostics.push(`console: ${message.text()}`));
page.on('pageerror', (error) => diagnostics.push(`pageerror: ${error.stack || error}`));
page.on('requestfailed', (request) => diagnostics.push(
`requestfailed: ${request.url()} (${request.failure()?.errorText || 'unknown error'})`
));
await page.goto(`http://127.0.0.1:${address.port}/`, { waitUntil: 'load' });
try {
await page.waitForFunction(() => globalThis.TYPEPHP_WASM_TEST_RESULT !== undefined, { timeout: 120000 });
} catch (error) {
throw new Error(`${error.message}\n${diagnostics.join('\n')}`);
}
const result = await page.evaluate(() => globalThis.TYPEPHP_WASM_TEST_RESULT);
process.stdout.write(result.output || '');
if (result.error) {
process.stderr.write(result.error + '\n');
process.exitCode = 1;
}
} finally {
await browser?.close();
await new Promise((resolve) => server.close(resolve));
}

File diff suppressed because it is too large Load Diff

@ -0,0 +1,9 @@
{
"name": "typephp-wasm-test-harness",
"private": true,
"type": "module",
"dependencies": {
"@bytecodealliance/jco": "^1.27.0",
"puppeteer-core": "^24.16.0"
}
}

@ -0,0 +1,33 @@
--TEST--
WASM PHPX archive links high precision types and std containers
--FILE--
<?php
use native_types;
function main(): void
{
$integer = std::bigInt('12345678901234567890');
$decimal = std::decimal('10.25');
$float = std::bigFloat('100000000000000000000');
echo ($integer + 10)->toString(), "\n";
echo ($decimal * 4)->toString(), "\n";
echo ($float + std::bigFloat('1'))->toString(), "\n";
$array = std::array(Type::Int, 2);
$array[0] = 7;
$vector = std::vector(Type::String);
$vector[] = 'wasm';
$map = std::map(Type::String, Type::Int);
$map['answer'] = 42;
$ordered = std::ordered_map(Type::String, Type::Int);
$ordered['first'] = 1;
echo $array[0], '|', $vector[0], '|', $map['answer'], '|', $ordered['first'], "\n";
}
?>
--EXPECT--
12345678901234567900
41.00
100000000000000000001
7|wasm|42|1

@ -0,0 +1,15 @@
--TEST--
WASM runtime exposes the WASI platform
--FILE--
<?php
function main(): void
{
var_dump(PHP_SAPI === 'cli');
var_dump(PHP_VERSION_ID >= 80400);
var_dump(str_contains(php_uname(), 'wasm32'));
}
?>
--EXPECT--
bool(true)
bool(true)
bool(true)

@ -0,0 +1,19 @@
--TEST--
WASI provides wall clock and secure random sources
--FILE--
<?php
function main(): void
{
$timestamp = time();
$random = random_int(100, 200);
var_dump($timestamp > 1700000000);
var_dump(strtotime(date(DATE_ATOM, $timestamp)) === $timestamp);
var_dump($random >= 100 && $random <= 200);
var_dump(strlen(random_bytes(16)) === 16);
}
?>
--EXPECT--
bool(true)
bool(true)
bool(true)
bool(true)

@ -0,0 +1,23 @@
--TEST--
WASI filesystem supports PHP stream read, write and directory operations
--FILE--
<?php
function main(): void
{
$directory = '/sandbox/typephp';
$file = $directory . '/message.txt';
if (!is_dir($directory)) {
mkdir($directory);
}
file_put_contents($file, "hello filesystem\n");
echo file_get_contents($file);
var_dump(in_array('message.txt', scandir($directory), true));
unlink($file);
rmdir($directory);
var_dump(file_exists($file));
}
?>
--EXPECT--
hello filesystem
bool(true)
bool(false)

@ -0,0 +1,22 @@
--TEST--
WASI provides arguments, environment variables and standard input
--ARGS--
alpha "two words"
--ENV--
TYPEPHP_WASM_GREETING=hello-wasi
--STDIN--
input from host
--FILE--
<?php
function main(): void
{
global $argv;
echo implode('|', array_slice($argv, 1)), "\n";
echo getenv('TYPEPHP_WASM_GREETING'), "\n";
echo trim(stream_get_contents(STDIN)), "\n";
}
?>
--EXPECT--
alpha|two words
hello-wasi
input from host

@ -70,15 +70,9 @@ application build. PHPX release packages include the pinned host-side
`phpx-wit-bindgen` needed for application-specific exports below
`<phpx>/wasm/bin/<host-os>-<host-arch>/`.
SDK producer and TypePHP integration checks are kept with the TypePHP WASM
backend rather than php-src:
```text
wasm/link-numeric-smoke-test.sh GMP, MPFR, and mpdecimal link check
wasm/test-typephp-program.sh TypePHP high-precision integration check
wasm/numeric-smoke-test.cc Native numeric test program
wasm/examples/high-precision.php TypePHP integration example
```
WASM integration checks live in `tests/wasm` and run through `run-tests.php`.
The target-independent high-precision TypePHP example lives at
`examples/high-precision.php`.
## Language-level component exports

@ -1,38 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
script_dir=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
compiler_dir=$(cd "${script_dir}/.." && pwd)
phpx_home=${PHPX_HOME:-${compiler_dir}/vendor/swoole/phpx}
prefix=${TYPEPHP_WASI_SDK_DIR:-${phpx_home}/wasm/wasm32-wasip2}
output=${TYPEPHP_WASM_NUMERIC_OUTPUT:-/tmp/typephp-wasm-numeric.wasm}
wasi_cxx=${TYPEPHP_WASI_CXX:-$(command -v wasm32-wasip2-clang++ || true)}
if [[ -z "${wasi_cxx}" ]]; then
echo "Required WASI tool 'wasm32-wasip2-clang++' was not found in PATH" >&2
exit 1
fi
for library in libgmp.a libgmpxx.a libmpfr.a libmpdec.a libmpdec++.a; do
if [[ ! -f "${prefix}/lib/${library}" ]]; then
echo "WASI numeric library not found: ${prefix}/lib/${library}" >&2
exit 1
fi
done
"${wasi_cxx}" \
-O0 \
-std=c++17 \
-fwasm-exceptions \
-mllvm -wasm-enable-sjlj \
-mllvm -wasm-use-legacy-eh=false \
-I"${prefix}/include" \
"${script_dir}/numeric-smoke-test.cc" \
-L"${prefix}/lib" \
-lmpdec++ -lmpdec -lmpfr -lgmpxx -lgmp \
-lwasi-emulated-signal \
-lsetjmp -lunwind -lm \
-o "${output}"
echo "Linked numeric WASI smoke test: ${output}"

@ -1,52 +0,0 @@
#include <gmpxx.h>
#include <mpfr.h>
#include <decimal.hh>
#include <cstdio>
#include <string>
int main()
{
mpz_class integer("18446744073709551616");
integer = integer * integer + 7;
if (integer.get_str() != "340282366920938463463374607431768211463") {
return 1;
}
mpfr_t value;
mpfr_init2(value, 256);
if (mpfr_set_str(value, "2", 10, MPFR_RNDN) != 0) {
mpfr_clear(value);
return 2;
}
mpfr_sqrt(value, value, MPFR_RNDN);
char float_buffer[96];
mpfr_snprintf(float_buffer, sizeof(float_buffer), "%.40RNf", value);
mpfr_clear(value);
if (std::string(float_buffer) != "1.4142135623730950488016887242096980785697") {
return 3;
}
decimal::Decimal small_decimal("1.25");
if (small_decimal.to_sci() != "1.25") {
std::fprintf(stderr, "unexpected parsed Decimal: %s\n", small_decimal.to_sci().c_str());
return 4;
}
small_decimal *= decimal::Decimal("8");
if (small_decimal.to_sci() != "10.00") {
std::fprintf(stderr, "unexpected small Decimal result: %s\n", small_decimal.to_sci().c_str());
return 5;
}
decimal::Context decimal_context(32);
decimal::Decimal decimal_value("12345678901234567890.125");
decimal_value = decimal_value.mul(decimal::Decimal("8"), decimal_context);
const std::string decimal_string = decimal_value.to_sci();
if (decimal_string != "98765431209876543121.000") {
std::fprintf(stderr, "unexpected Decimal result: %s\n", decimal_string.c_str());
return 6;
}
std::puts("TYPEPHP_WASM_NUMERIC_OK");
return 0;
}

@ -1,35 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
script_dir=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
compiler_dir=$(cd "${script_dir}/.." && pwd)
output=${TYPEPHP_WASM_TEST_OUTPUT:-/tmp/typephp-wasm-high-precision.wasm}
wasmtime_bin=${TYPEPHP_WASMTIME:-$(command -v wasmtime || true)}
if [[ -z "${wasmtime_bin}" ]]; then
echo "Required WASI tool 'wasmtime' was not found in PATH" >&2
exit 1
fi
output_dir=$(dirname "${output}")
output_name=$(basename "${output}")
(
cd "${output_dir}"
php "${compiler_dir}/bin/tpc.php" --wasm=component "${script_dir}/examples/high-precision.php"
if [[ high-precision.wasm != "${output_name}" ]]; then
mv high-precision.wasm "${output_name}"
fi
)
actual=$(XDG_CACHE_HOME=${XDG_CACHE_HOME:-/tmp/typephp-wasmtime-cache} \
"${wasmtime_bin}" -S http "${output}")
expected=$'1111111101111111110111111111010\n1000000000000000000000000000001\n12348.14159265358979324'
if [[ "${actual}" != "${expected}" ]]; then
echo "Unexpected TypePHP/WASI output:" >&2
printf '%s\n' "${actual}" >&2
exit 1
fi
printf '%s\n' "${actual}"
echo "TypePHP/WASI integration test passed"
Loading…
Cancel
Save