diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 46121bb732..e3f62ed675 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -30,38 +30,22 @@ jobs: fail-fast: false matrix: include: - - name: test-unit-asyncify (1/16) + - name: test-unit-asyncify (1/8) target: test - - name: test-unit-asyncify (2/16) - target: test-php - - name: test-unit-asyncify (3/16) - target: test-php-networking - - name: test-unit-asyncify (4/16) - target: test-php-request-handler-files - - name: test-unit-asyncify (5/16) - target: test-php-request-handler-requests - - name: test-unit-asyncify (6/16) - target: test-php-asyncify-file-get-contents-http - - name: test-unit-asyncify (7/16) - target: test-php-asyncify-file-get-contents-https - - name: test-unit-asyncify (8/16) - target: test-php-asyncify-fopen-http - - name: test-unit-asyncify (9/16) - target: test-php-asyncify-fopen-https - - name: test-unit-asyncify (10/16) - target: test-php-asyncify-fsockopen-http - - name: test-unit-asyncify (11/16) - target: test-php-asyncify-fsockopen-https - - name: test-unit-asyncify (12/16) - target: test-php-asyncify-gethostbyname-http - - name: test-unit-asyncify (13/16) - target: test-php-asyncify-gethostbyname-https - - name: test-unit-asyncify (14/16) - target: test-php-asyncify-mysqli-http - - name: test-unit-asyncify (15/16) - target: test-php-asyncify-mysqli-https - - name: test-unit-asyncify (16/16) - target: test-php-asyncify-sqlite3 + - name: test-unit-asyncify (2/8) + target: test-asyncify + - name: test-unit-asyncify (3/8) + target: test-php-file-get-contents-asyncify + - name: test-unit-asyncify (4/8) + target: test-php-fopen-asyncify + - name: test-unit-asyncify (5/8) + target: test-php-fsockopen-asyncify + - name: test-unit-asyncify (6/8) + target: test-php-gethostbyname-asyncify + - name: test-unit-asyncify (7/8) + target: test-php-mysqli-asyncify + - name: test-unit-asyncify (8/8) + target: test-php-sqlite3-asyncify name: ${{ matrix.name }} services: mysql: @@ -97,9 +81,36 @@ jobs: fail-fast: false matrix: include: - - name: test-unit-jspi (1/1) - target: test-php-dynamic-loading-jspi + - name: test-unit-jspi (1/7) + target: test-jspi + - name: test-unit-jspi (2/7) + target: test-php-file-get-contents-jspi + - name: test-unit-jspi (3/7) + target: test-php-fopen-jspi + - name: test-unit-jspi (4/7) + target: test-php-fsockopen-jspi + - name: test-unit-jspi (5/7) + target: test-php-gethostbyname-jspi + - name: test-unit-jspi (6/7) + target: test-php-mysqli-jspi + - name: test-unit-jspi (7/7) + target: test-php-sqlite3-jspi name: ${{ matrix.name }} + services: + mysql: + image: mysql:5.7 + env: + MYSQL_DATABASE: test_db + MYSQL_USER: user + MYSQL_PASSWORD: password + MYSQL_ROOT_PASSWORD: rootpassword + ports: + - 3306:3306 + options: >- + --health-cmd="mysqladmin ping --silent" + --health-interval=10s + --health-timeout=5s + --health-retries=3 steps: - uses: actions/checkout@v4 with: @@ -108,21 +119,10 @@ jobs: with: node-version: 23 - run: node --expose-gc node_modules/nx/bin/nx affected --target=${{ matrix.target }} - # Most of these tests pass locally but the process is crashing - # on the CI runner. - # - # test-unit-jspi: - # runs-on: ubuntu-latest - # needs: [lint-and-typecheck] - # steps: - # - uses: actions/checkout@v4 - # with: - # submodules: true - # - uses: ./.github/actions/prepare-playground - # with: - # # @TODO: Switch to the production version once it's released - # node-version: 23.0.0-nightly2024100909d10b50dc - # - run: node --experimental-wasm-jspi --experimental-wasm-stack-switching --expose-gc node_modules/nx/bin/nx affected --target=test --configuration=ci + env: + MYSQL_DATABASE: test_db + MYSQL_USER: user + MYSQL_PASSWORD: password test-e2e: runs-on: ubuntu-latest needs: [lint-and-typecheck] diff --git a/packages/php-wasm/node/asyncify/7_2_34/php_7_2.wasm b/packages/php-wasm/node/asyncify/7_2_34/php_7_2.wasm index e58c55773f..d3521238c9 100755 Binary files a/packages/php-wasm/node/asyncify/7_2_34/php_7_2.wasm and b/packages/php-wasm/node/asyncify/7_2_34/php_7_2.wasm differ diff --git a/packages/php-wasm/node/asyncify/7_3_33/php_7_3.wasm b/packages/php-wasm/node/asyncify/7_3_33/php_7_3.wasm index e2734bbe7c..701ccc569f 100755 Binary files a/packages/php-wasm/node/asyncify/7_3_33/php_7_3.wasm and b/packages/php-wasm/node/asyncify/7_3_33/php_7_3.wasm differ diff --git a/packages/php-wasm/node/asyncify/7_4_33/php_7_4.wasm b/packages/php-wasm/node/asyncify/7_4_33/php_7_4.wasm index 6fd00865b6..371e791859 100755 Binary files a/packages/php-wasm/node/asyncify/7_4_33/php_7_4.wasm and b/packages/php-wasm/node/asyncify/7_4_33/php_7_4.wasm differ diff --git a/packages/php-wasm/node/asyncify/8_0_30/php_8_0.wasm b/packages/php-wasm/node/asyncify/8_0_30/php_8_0.wasm index e3fdf62cd6..e45ae07bcc 100755 Binary files a/packages/php-wasm/node/asyncify/8_0_30/php_8_0.wasm and b/packages/php-wasm/node/asyncify/8_0_30/php_8_0.wasm differ diff --git a/packages/php-wasm/node/asyncify/8_1_23/php_8_1.wasm b/packages/php-wasm/node/asyncify/8_1_23/php_8_1.wasm index f257348cf2..2db7f28a65 100755 Binary files a/packages/php-wasm/node/asyncify/8_1_23/php_8_1.wasm and b/packages/php-wasm/node/asyncify/8_1_23/php_8_1.wasm differ diff --git a/packages/php-wasm/node/asyncify/8_2_10/php_8_2.wasm b/packages/php-wasm/node/asyncify/8_2_10/php_8_2.wasm index 4c71732daf..f608ad718f 100755 Binary files a/packages/php-wasm/node/asyncify/8_2_10/php_8_2.wasm and b/packages/php-wasm/node/asyncify/8_2_10/php_8_2.wasm differ diff --git a/packages/php-wasm/node/asyncify/8_3_0/php_8_3.wasm b/packages/php-wasm/node/asyncify/8_3_0/php_8_3.wasm index 92b4fb65fe..e0929e1472 100755 Binary files a/packages/php-wasm/node/asyncify/8_3_0/php_8_3.wasm and b/packages/php-wasm/node/asyncify/8_3_0/php_8_3.wasm differ diff --git a/packages/php-wasm/node/asyncify/8_4_0/php_8_4.wasm b/packages/php-wasm/node/asyncify/8_4_0/php_8_4.wasm index 1b3a82db59..341010aff5 100755 Binary files a/packages/php-wasm/node/asyncify/8_4_0/php_8_4.wasm and b/packages/php-wasm/node/asyncify/8_4_0/php_8_4.wasm differ diff --git a/packages/php-wasm/node/asyncify/php_7_3.js b/packages/php-wasm/node/asyncify/php_7_3.js index 4c6b37a924..089f82310f 100644 --- a/packages/php-wasm/node/asyncify/php_7_3.js +++ b/packages/php-wasm/node/asyncify/php_7_3.js @@ -8,7 +8,7 @@ import path from 'path'; const dependencyFilename = path.join(__dirname, '7_3_33', 'php_7_3.wasm'); export { dependencyFilename }; -export const dependenciesTotalSize = 17697844; +export const dependenciesTotalSize = 17697847; export function init(RuntimeName, PHPLoader) { // The rest of the code comes from the built php.js file and esm-suffix.js // include: shell.js diff --git a/packages/php-wasm/node/asyncify/php_8_0.js b/packages/php-wasm/node/asyncify/php_8_0.js index a33072a432..a9eb1dd49d 100644 --- a/packages/php-wasm/node/asyncify/php_8_0.js +++ b/packages/php-wasm/node/asyncify/php_8_0.js @@ -8,7 +8,7 @@ import path from 'path'; const dependencyFilename = path.join(__dirname, '8_0_30', 'php_8_0.wasm'); export { dependencyFilename }; -export const dependenciesTotalSize = 18445624; +export const dependenciesTotalSize = 18445623; export function init(RuntimeName, PHPLoader) { // The rest of the code comes from the built php.js file and esm-suffix.js // include: shell.js diff --git a/packages/php-wasm/node/asyncify/php_8_3.js b/packages/php-wasm/node/asyncify/php_8_3.js index e2db65a9ad..38f74cffae 100644 --- a/packages/php-wasm/node/asyncify/php_8_3.js +++ b/packages/php-wasm/node/asyncify/php_8_3.js @@ -8,7 +8,7 @@ import path from 'path'; const dependencyFilename = path.join(__dirname, '8_3_0', 'php_8_3.wasm'); export { dependencyFilename }; -export const dependenciesTotalSize = 19201581; +export const dependenciesTotalSize = 19201580; export function init(RuntimeName, PHPLoader) { // The rest of the code comes from the built php.js file and esm-suffix.js // include: shell.js diff --git a/packages/php-wasm/node/project.json b/packages/php-wasm/node/project.json index e0bece583f..c16cb17f35 100644 --- a/packages/php-wasm/node/project.json +++ b/packages/php-wasm/node/project.json @@ -146,7 +146,7 @@ "executor": "@wp-playground/nx-extensions:package-for-self-hosting", "dependsOn": ["build"] }, - "test": { + "test-asyncify": { "executor": "@nx/vite:test", "outputs": ["{workspaceRoot}/coverage/packages/php-wasm/node"], "options": { @@ -156,162 +156,149 @@ "php-ini.spec.ts", "php-memory.spec.ts", "php-process-manager.spec.ts", - "php-request-handler.spec.ts", "php-vars.spec.ts", "rotate-php-runtime.spec.ts", "symlinks.spec.ts", - "write-files.spec.ts" + "write-files.spec.ts", + "php-networking.spec.ts", + "php-request-handler.spec.ts", + "php.spec.ts" ] } }, - "test-php": { - "executor": "@nx/vite:test", - "outputs": ["{workspaceRoot}/coverage/packages/php-wasm/node"], - "options": { - "reportsDirectory": "../../../coverage/packages/php-wasm/node", - "testFiles": ["php.spec.ts"] - } - }, - "test-php-networking": { - "executor": "@nx/vite:test", - "outputs": ["{workspaceRoot}/coverage/packages/php-wasm/node"], - "options": { - "reportsDirectory": "../../../coverage/packages/php-wasm/node", - "testFiles": ["php-networking.spec.ts"] - } - }, - "test-php-dynamic-loading-jspi": { + "test-jspi": { "executor": "@nx/vite:test", "outputs": ["{workspaceRoot}/coverage/packages/php-wasm/node"], "options": { "configFile": "packages/php-wasm/node/vite.jspi.config.ts", "reportsDirectory": "../../../coverage/packages/php-wasm/node", - "testFiles": ["php-dynamic-loading.spec.ts"] - } - }, - "test-php-request-handler-files": { - "executor": "@nx/vite:test", - "outputs": ["{workspaceRoot}/coverage/packages/php-wasm/node"], - "options": { - "reportsDirectory": "../../../coverage/packages/php-wasm/node", - "testFiles": ["php-request-handler-files.spec.ts"] + "testFiles": [ + "php-crash.spec.ts", + "php-ini.spec.ts", + "php-memory.spec.ts", + "php-process-manager.spec.ts", + "php-vars.spec.ts", + "rotate-php-runtime.spec.ts", + "symlinks.spec.ts", + "write-files.spec.ts", + "php-networking.spec.ts", + "php-dynamic-loading.spec.ts", + "php-request-handler.spec.ts", + "php.spec.ts" + ] } }, - "test-php-request-handler-requests": { + "test-php-file-get-contents-asyncify": { "executor": "@nx/vite:test", "outputs": ["{workspaceRoot}/coverage/packages/php-wasm/node"], "options": { "reportsDirectory": "../../../coverage/packages/php-wasm/node", - "testFiles": ["php-request-handler-requests.spec.ts"] + "testFiles": ["php-file-get-contents.spec.ts"] } }, - "test-php-asyncify-file-get-contents-http": { + "test-php-file-get-contents-jspi": { "executor": "@nx/vite:test", "outputs": ["{workspaceRoot}/coverage/packages/php-wasm/node"], "options": { + "configFile": "packages/php-wasm/node/vite.jspi.config.ts", "reportsDirectory": "../../../coverage/packages/php-wasm/node", - "testFiles": ["php-asyncify-file-get-contents.spec.ts"] + "testFiles": ["php-file-get-contents.spec.ts"] } }, - "test-php-asyncify-file-get-contents-https": { + "test-php-fopen-asyncify": { "executor": "@nx/vite:test", "outputs": ["{workspaceRoot}/coverage/packages/php-wasm/node"], "options": { - "configFile": "packages/php-wasm/node/vite.https.config.ts", "reportsDirectory": "../../../coverage/packages/php-wasm/node", - "testFiles": ["php-asyncify-file-get-contents.spec.ts"] + "testFiles": ["php-fopen.spec.ts"] } }, - "test-php-asyncify-fopen-http": { + "test-php-fopen-jspi": { "executor": "@nx/vite:test", "outputs": ["{workspaceRoot}/coverage/packages/php-wasm/node"], "options": { + "configFile": "packages/php-wasm/node/vite.jspi.config.ts", "reportsDirectory": "../../../coverage/packages/php-wasm/node", - "testFiles": ["php-asyncify-fopen.spec.ts"] + "testFiles": ["php-fopen.spec.ts"] } }, - "test-php-asyncify-fopen-https": { + "test-php-fsockopen-asyncify": { "executor": "@nx/vite:test", "outputs": ["{workspaceRoot}/coverage/packages/php-wasm/node"], "options": { - "configFile": "packages/php-wasm/node/vite.https.config.ts", "reportsDirectory": "../../../coverage/packages/php-wasm/node", - "testFiles": ["php-asyncify-fopen.spec.ts"] + "testFiles": ["php-fsockopen.spec.ts"] } }, - "test-php-asyncify-fsockopen-http": { + "test-php-fsockopen-jspi": { "executor": "@nx/vite:test", "outputs": ["{workspaceRoot}/coverage/packages/php-wasm/node"], "options": { + "configFile": "packages/php-wasm/node/vite.jspi.config.ts", "reportsDirectory": "../../../coverage/packages/php-wasm/node", - "testFiles": ["php-asyncify-fsockopen.spec.ts"] + "testFiles": ["php-fsockopen.spec.ts"] } }, - "test-php-asyncify-fsockopen-https": { + "test-php-gethostbyname-asyncify": { "executor": "@nx/vite:test", "outputs": ["{workspaceRoot}/coverage/packages/php-wasm/node"], "options": { - "configFile": "packages/php-wasm/node/vite.https.config.ts", "reportsDirectory": "../../../coverage/packages/php-wasm/node", - "testFiles": ["php-asyncify-fsockopen.spec.ts"] + "testFiles": ["php-gethostbyname.spec.ts"] } }, - "test-php-asyncify-gethostbyname-http": { + "test-php-gethostbyname-jspi": { "executor": "@nx/vite:test", "outputs": ["{workspaceRoot}/coverage/packages/php-wasm/node"], "options": { + "configFile": "packages/php-wasm/node/vite.jspi.config.ts", "reportsDirectory": "../../../coverage/packages/php-wasm/node", - "testFiles": ["php-asyncify-gethostbyname.spec.ts"] + "testFiles": ["php-gethostbyname.spec.ts"] } }, - "test-php-asyncify-gethostbyname-https": { + "test-php-mysqli-asyncify": { "executor": "@nx/vite:test", "outputs": ["{workspaceRoot}/coverage/packages/php-wasm/node"], "options": { - "configFile": "packages/php-wasm/node/vite.https.config.ts", "reportsDirectory": "../../../coverage/packages/php-wasm/node", - "testFiles": ["php-asyncify-gethostbyname.spec.ts"] + "testFiles": ["php-mysqli.spec.ts"] } }, - "test-php-asyncify-mysqli-http": { + "test-php-mysqli-jspi": { "executor": "@nx/vite:test", "outputs": ["{workspaceRoot}/coverage/packages/php-wasm/node"], "options": { + "configFile": "packages/php-wasm/node/vite.jspi.config.ts", "reportsDirectory": "../../../coverage/packages/php-wasm/node", - "testFiles": ["php-asyncify-mysqli.spec.ts"] + "testFiles": ["php-mysqli.spec.ts"] } }, - "test-php-asyncify-mysqli-https": { + "test-php-sqlite3-asyncify": { "executor": "@nx/vite:test", "outputs": ["{workspaceRoot}/coverage/packages/php-wasm/node"], "options": { - "configFile": "packages/php-wasm/node/vite.https.config.ts", "reportsDirectory": "../../../coverage/packages/php-wasm/node", - "testFiles": ["php-asyncify-mysqli.spec.ts"] + "testFiles": ["php-sqlite3.spec.ts"] } }, - "test-php-asyncify-sqlite3": { + "test-php-sqlite3-jspi": { "executor": "@nx/vite:test", "outputs": ["{workspaceRoot}/coverage/packages/php-wasm/node"], "options": { + "configFile": "packages/php-wasm/node/vite.jspi.config.ts", "reportsDirectory": "../../../coverage/packages/php-wasm/node", - "testFiles": ["php-asyncify-sqlite3.spec.ts"] + "testFiles": ["php-sqlite3.spec.ts"] } }, "test-php-asyncify-all": { "executor": "nx:noop", "dependsOn": [ - "test-php-asyncify-file-get-contents-http", - "test-php-asyncify-file-get-contents-https", - "test-php-asyncify-fopen-http", - "test-php-asyncify-fopen-https", - "test-php-asyncify-fsockopen-http", - "test-php-asyncify-fsockopen-https", - "test-php-asyncify-gethostbyname-http", - "test-php-asyncify-gethostbyname-https", - "test-php-asyncify-mysqli-http", - "test-php-asyncify-mysqli-https", - "test-php-asyncify-sqlite3" + "test-php-file-get-contents-asyncify", + "test-php-fopen-asyncify", + "test-php-fsockopen-asyncify", + "test-php-gethostbyname-asyncify", + "test-php-mysqli-asyncify", + "test-php-sqlite3-asyncify" ] }, "lint": { diff --git a/packages/php-wasm/node/src/test/php-asyncify-file-get-contents.spec.ts b/packages/php-wasm/node/src/test/php-asyncify-file-get-contents.spec.ts deleted file mode 100644 index b19857ba06..0000000000 --- a/packages/php-wasm/node/src/test/php-asyncify-file-get-contents.spec.ts +++ /dev/null @@ -1,351 +0,0 @@ -import http from 'http'; -import https from 'https'; -import fs from 'fs'; -import path from 'path'; -import { - PHP, - SupportedPHPVersions, - setPhpIniEntries, -} from '@php-wasm/universal'; -import { phpVars } from '@php-wasm/util'; -// eslint-disable-next-line @nx/enforce-module-boundaries -import InitialDockerfile from '../../../compile/php/Dockerfile?raw'; -import { loadNodeRuntime } from '../lib'; - -const requestHandler = ( - req: http.IncomingMessage, - res: http.ServerResponse -) => { - if (req.url === '/image.jpg') { - const image = fs.readFileSync( - path.join(__dirname, 'test-data', 'image.jpg') - ); - res.writeHead(200, { 'Content-Type': 'image/jpeg' }); - res.write(image); - res.end(); - } else { - res.writeHead(200, { 'Content-Type': 'text/plain' }); - res.end('Hello World\n'); - } -}; - -const httpServer = http.createServer(requestHandler); -const selfSignedCert = { - key: fs.readFileSync(path.join(__dirname, 'test-data', 'key.pem')), - cert: fs.readFileSync(path.join(__dirname, 'test-data', 'cert.pem')), -}; -const httpsServer = https.createServer( - { - key: selfSignedCert.key, - cert: selfSignedCert.cert, - }, - requestHandler -); - -type Mode = 'http' | 'https'; - -const protocols = { - http: { - protocol: 'http', - port: new Promise((resolve) => { - httpServer.listen(0, function () { - resolve((httpServer.address() as any).port); - }); - }), - }, - https: { - protocol: 'https', - port: new Promise((resolve) => { - httpsServer.listen(0, function () { - resolve((httpsServer.address() as any).port); - }); - }), - }, -}; - -const { protocol, port } = protocols[import.meta.env['PROTOCOL'] as Mode]; - -const host = '127.0.0.1'; - -const httpUrl = `${protocol}://${host}:${port}`; - -describe(`${protocol} protocol – asyncify`, () => { - const js = phpVars({ - host, - port, - httpUrl, - }); - - const phpVersions = - 'PHP' in process.env ? [process.env['PHP']] : SupportedPHPVersions; - - const topOfTheStack: Record = { - file_get_contents: `file_get_contents(${js['httpUrl']});`, - }; - - describe.each(phpVersions)('PHP %s – asyncify', (phpVersion) => { - let php: PHP; - beforeEach(async () => { - php = new PHP(await loadNodeRuntime(phpVersion as any)); - await setPhpIniEntries(php, { allow_url_fopen: 1 }); - }); - - afterEach(async () => { - php?.[Symbol.dispose]?.(); - }); - - describe.each(Object.keys(topOfTheStack))('%s', (networkCallKey) => { - const networkCall = topOfTheStack[networkCallKey]; - test('Direct call', () => assertNoCrash(networkCall)); - describe('Function calls', () => { - test('Simple call', () => assertNoCrash(`${networkCall};`)); - test('Simple call', () => - assertNoCrash(`function top() { ${networkCall} } top();`)); - test('Via call_user_func', () => - assertNoCrash( - `function top() { ${networkCall} } call_user_func('top'); ` - )); - test('Via call_user_func_array', () => - assertNoCrash( - `function top() { ${networkCall} } call_user_func_array('top', array());` - )); - }); - - describe('Array functions', () => { - test('array_filter', () => - assertNoCrash(` - function top() { ${networkCall} } - array_filter(array('top'), 'top'); - `)); - - test('array_map', () => - assertNoCrash(` - function top() { ${networkCall} } - array_map(array('top'), 'top'); - `)); - - // Network calls in sort() would be silly so let's skip those for now. - }); - - describe('Class method calls', () => { - test('Regular method', () => - assertNoCrash(` - class Top { - function my_method() { ${networkCall} } - } - $x = new Top(); - $x->my_method(); - `)); - test('Via ReflectionMethod->invoke()', () => - assertNoCrash(` - class Top { - function my_method() { ${networkCall} } - } - $reflectionMethod = new ReflectionMethod('Top', 'my_method'); - $reflectionMethod->invoke(new Top()); - `)); - test('Via ReflectionMethod->invokeArgs()', () => - assertNoCrash(` - class Top { - function my_method() { ${networkCall} } - } - $reflectionMethod = new ReflectionMethod('Top', 'my_method'); - $reflectionMethod->invokeArgs(new Top(), array()); - `)); - test('Via call_user_func', () => - assertNoCrash(` - class Top { - function my_method() { ${networkCall} } - } - call_user_func([new Top(), 'my_method']); - `)); - test('Via call_user_func_array', () => - assertNoCrash(` - class Top { - function my_method() { ${networkCall} } - } - call_user_func_array([new Top(), 'my_method'], []); - `)); - test('Constructor', () => - assertNoCrash(` - class Top { - function __construct() { ${networkCall} } - } - new Top(); - `)); - test('Destructor', () => - assertNoCrash(` - class Top { - function __destruct() { ${networkCall} } - } - $x = new Top(); - unset($x); - `)); - test('__call', () => - assertNoCrash(` - class Top { - function __call($method, $args) { ${networkCall} } - } - $x = new Top(); - $x->test(); - `)); - test('__get', () => - assertNoCrash(` - class Top { - function __get($prop) { ${networkCall} } - } - $x = new Top(); - $x->test; - `)); - test('__set', () => - assertNoCrash(` - class Top { - function __set($prop, $value) { ${networkCall} } - } - $x = new Top(); - $x->test = 1; - `)); - test('__isset', () => - assertNoCrash(` - class Top { - function __isset($prop) { ${networkCall} } - } - $x = new Top(); - isset($x->test); - `)); - test('ArrayAccess', () => { - assertNoCrash(` - class Top implements ArrayAccess { - function offsetExists($offset) { ${networkCall} } - function offsetGet($offset) { ${networkCall} } - function offsetSet($offset, $value) { ${networkCall} } - function offsetUnset($offset) { ${networkCall} } - } - $x = new Top(); - isset($x['test']); - $a = $x['test']; - $x['test'] = 123; - unset($x['test']); - `); - }); - test('Iterator', () => - assertNoCrash(` - $data = new class() implements IteratorAggregate { - public function getIterator(): Traversable { - ${networkCall}; - return new ArrayIterator( [] ); - } - }; - echo json_encode( [ - ...$data - ] ); - `)); - - test('Countable', () => - assertNoCrash(` - $data = new class() implements Countable { - public function count() { - ${networkCall} - return 0; - } - }; - count($data); - `)); - - test('yield', () => - assertNoCrash(` - function countTo2() { - ${networkCall}; - yield '1'; - ${networkCall}; - yield '2'; - } - foreach(countTo2() as $number) { - echo $number; - } - `)); - }); - - describe('exif extension support', () => { - it('exif_read_data', async () => { - assertNoCrash( - `var_dump(exif_read_data('${httpUrl}/image.jpg'));` - ); - }); - it('exif_imagetype', async () => { - assertNoCrash( - `var_dump(exif_imagetype('${httpUrl}/image.jpg'));` - ); - }); - it('exif_thumbnail', async () => { - assertNoCrash( - `var_dump(exif_thumbnail('${httpUrl}/image.jpg'));` - ); - }); - }); - }); - - async function assertNoCrash(code: string) { - try { - const result = await php.run({ - code: ` - candidate.replace('byn$fpcast-emu$', '') - ) - .filter( - (candidate) => - !Dockerfile.includes(`"${candidate}"`) - ); - if (missingCandidates.length) { - addAsyncifyFunctionsToDockerfile(missingCandidates); - throw new Error( - `Asyncify crash! The following missing functions were just auto-added to the ASYNCIFY_ONLY list in the Dockerfile: \n ` + - missingCandidates.join(', ') + - `\nYou now need to rebuild PHP and re-run this test: \n` + - ` npm run recompile:php:node:asyncify:8.0\n` + - ` node --stack-trace-limit=100 ./node_modules/.bin/nx test php-wasm-node --test-name-pattern='asyncify'\n` - ); - } - - const err = new Error( - `Asyncify crash! No C functions present in the stack trace were missing ` + - `from the Dockerfile. This could mean the stack trace is too short – try increasing the stack trace limit ` + - `with --stack-trace-limit=100. If you already did that, fixing this problem will likely take more digging.` - ); - err.cause = e; - throw err; - } - } - } - }); -}); - -let Dockerfile = InitialDockerfile; -const DockerfilePath = path.resolve( - __dirname, - '../../../compile/php/Dockerfile' -); -function addAsyncifyFunctionsToDockerfile(functions: string[]) { - const currentDockerfile = fs.readFileSync(DockerfilePath, 'utf8') + ''; - const lookup = `export ASYNCIFY_ONLY=$'`; - const idx = currentDockerfile.indexOf(lookup) + lookup.length; - const updatedDockerfile = - currentDockerfile.substring(0, idx) + - functions.map((f) => `"${f}",\\\n`).join('') + - currentDockerfile.substring(idx); - fs.writeFileSync(DockerfilePath, updatedDockerfile); - Dockerfile = updatedDockerfile; -} diff --git a/packages/php-wasm/node/src/test/php-asyncify-fopen.spec.ts b/packages/php-wasm/node/src/test/php-asyncify-fopen.spec.ts deleted file mode 100644 index d358c669eb..0000000000 --- a/packages/php-wasm/node/src/test/php-asyncify-fopen.spec.ts +++ /dev/null @@ -1,354 +0,0 @@ -import http from 'http'; -import https from 'https'; -import fs from 'fs'; -import path from 'path'; -import { - PHP, - SupportedPHPVersions, - setPhpIniEntries, -} from '@php-wasm/universal'; -import { phpVars } from '@php-wasm/util'; -// eslint-disable-next-line @nx/enforce-module-boundaries -import InitialDockerfile from '../../../compile/php/Dockerfile?raw'; -import { loadNodeRuntime } from '../lib'; - -const requestHandler = ( - req: http.IncomingMessage, - res: http.ServerResponse -) => { - if (req.url === '/image.jpg') { - const image = fs.readFileSync( - path.join(__dirname, 'test-data', 'image.jpg') - ); - res.writeHead(200, { 'Content-Type': 'image/jpeg' }); - res.write(image); - res.end(); - } else { - res.writeHead(200, { 'Content-Type': 'text/plain' }); - res.end('Hello World\n'); - } -}; - -const httpServer = http.createServer(requestHandler); -const selfSignedCert = { - key: fs.readFileSync(path.join(__dirname, 'test-data', 'key.pem')), - cert: fs.readFileSync(path.join(__dirname, 'test-data', 'cert.pem')), -}; -const httpsServer = https.createServer( - { - key: selfSignedCert.key, - cert: selfSignedCert.cert, - }, - requestHandler -); - -type Mode = 'http' | 'https'; - -const protocols = { - http: { - protocol: 'http', - port: new Promise((resolve) => { - httpServer.listen(0, function () { - resolve((httpServer.address() as any).port); - }); - }), - }, - https: { - protocol: 'https', - port: new Promise((resolve) => { - httpsServer.listen(0, function () { - resolve((httpsServer.address() as any).port); - }); - }), - }, -}; - -const { protocol, port } = protocols[import.meta.env['PROTOCOL'] as Mode]; - -const host = '127.0.0.1'; - -const httpUrl = `${protocol}://${host}:${port}`; - -describe(`${protocol} protocol – asyncify`, () => { - const js = phpVars({ - host, - port, - httpUrl, - }); - - const phpVersions = - 'PHP' in process.env ? [process.env['PHP']] : SupportedPHPVersions; - - const topOfTheStack: Record = { - fopen: ` - $fp = fopen(${js['httpUrl']}, "r"); - fread($fp, 1024); - fclose($fp);`, - }; - - describe.each(phpVersions)('PHP %s – asyncify', (phpVersion) => { - let php: PHP; - beforeEach(async () => { - php = new PHP(await loadNodeRuntime(phpVersion as any)); - await setPhpIniEntries(php, { allow_url_fopen: 1 }); - }); - - afterEach(async () => { - php?.[Symbol.dispose]?.(); - }); - - describe.each(Object.keys(topOfTheStack))('%s', (networkCallKey) => { - const networkCall = topOfTheStack[networkCallKey]; - test('Direct call', () => assertNoCrash(networkCall)); - describe('Function calls', () => { - test('Simple call', () => assertNoCrash(`${networkCall};`)); - test('Simple call', () => - assertNoCrash(`function top() { ${networkCall} } top();`)); - test('Via call_user_func', () => - assertNoCrash( - `function top() { ${networkCall} } call_user_func('top'); ` - )); - test('Via call_user_func_array', () => - assertNoCrash( - `function top() { ${networkCall} } call_user_func_array('top', array());` - )); - }); - - describe('Array functions', () => { - test('array_filter', () => - assertNoCrash(` - function top() { ${networkCall} } - array_filter(array('top'), 'top'); - `)); - - test('array_map', () => - assertNoCrash(` - function top() { ${networkCall} } - array_map(array('top'), 'top'); - `)); - - // Network calls in sort() would be silly so let's skip those for now. - }); - - describe('Class method calls', () => { - test('Regular method', () => - assertNoCrash(` - class Top { - function my_method() { ${networkCall} } - } - $x = new Top(); - $x->my_method(); - `)); - test('Via ReflectionMethod->invoke()', () => - assertNoCrash(` - class Top { - function my_method() { ${networkCall} } - } - $reflectionMethod = new ReflectionMethod('Top', 'my_method'); - $reflectionMethod->invoke(new Top()); - `)); - test('Via ReflectionMethod->invokeArgs()', () => - assertNoCrash(` - class Top { - function my_method() { ${networkCall} } - } - $reflectionMethod = new ReflectionMethod('Top', 'my_method'); - $reflectionMethod->invokeArgs(new Top(), array()); - `)); - test('Via call_user_func', () => - assertNoCrash(` - class Top { - function my_method() { ${networkCall} } - } - call_user_func([new Top(), 'my_method']); - `)); - test('Via call_user_func_array', () => - assertNoCrash(` - class Top { - function my_method() { ${networkCall} } - } - call_user_func_array([new Top(), 'my_method'], []); - `)); - test('Constructor', () => - assertNoCrash(` - class Top { - function __construct() { ${networkCall} } - } - new Top(); - `)); - test('Destructor', () => - assertNoCrash(` - class Top { - function __destruct() { ${networkCall} } - } - $x = new Top(); - unset($x); - `)); - test('__call', () => - assertNoCrash(` - class Top { - function __call($method, $args) { ${networkCall} } - } - $x = new Top(); - $x->test(); - `)); - test('__get', () => - assertNoCrash(` - class Top { - function __get($prop) { ${networkCall} } - } - $x = new Top(); - $x->test; - `)); - test('__set', () => - assertNoCrash(` - class Top { - function __set($prop, $value) { ${networkCall} } - } - $x = new Top(); - $x->test = 1; - `)); - test('__isset', () => - assertNoCrash(` - class Top { - function __isset($prop) { ${networkCall} } - } - $x = new Top(); - isset($x->test); - `)); - test('ArrayAccess', () => { - assertNoCrash(` - class Top implements ArrayAccess { - function offsetExists($offset) { ${networkCall} } - function offsetGet($offset) { ${networkCall} } - function offsetSet($offset, $value) { ${networkCall} } - function offsetUnset($offset) { ${networkCall} } - } - $x = new Top(); - isset($x['test']); - $a = $x['test']; - $x['test'] = 123; - unset($x['test']); - `); - }); - test('Iterator', () => - assertNoCrash(` - $data = new class() implements IteratorAggregate { - public function getIterator(): Traversable { - ${networkCall}; - return new ArrayIterator( [] ); - } - }; - echo json_encode( [ - ...$data - ] ); - `)); - - test('Countable', () => - assertNoCrash(` - $data = new class() implements Countable { - public function count() { - ${networkCall} - return 0; - } - }; - count($data); - `)); - - test('yield', () => - assertNoCrash(` - function countTo2() { - ${networkCall}; - yield '1'; - ${networkCall}; - yield '2'; - } - foreach(countTo2() as $number) { - echo $number; - } - `)); - }); - - describe('exif extension support', () => { - it('exif_read_data', async () => { - assertNoCrash( - `var_dump(exif_read_data('${httpUrl}/image.jpg'));` - ); - }); - it('exif_imagetype', async () => { - assertNoCrash( - `var_dump(exif_imagetype('${httpUrl}/image.jpg'));` - ); - }); - it('exif_thumbnail', async () => { - assertNoCrash( - `var_dump(exif_thumbnail('${httpUrl}/image.jpg'));` - ); - }); - }); - }); - - async function assertNoCrash(code: string) { - try { - const result = await php.run({ - code: ` - candidate.replace('byn$fpcast-emu$', '') - ) - .filter( - (candidate) => - !Dockerfile.includes(`"${candidate}"`) - ); - if (missingCandidates.length) { - addAsyncifyFunctionsToDockerfile(missingCandidates); - throw new Error( - `Asyncify crash! The following missing functions were just auto-added to the ASYNCIFY_ONLY list in the Dockerfile: \n ` + - missingCandidates.join(', ') + - `\nYou now need to rebuild PHP and re-run this test: \n` + - ` npm run recompile:php:node:asyncify:8.0\n` + - ` node --stack-trace-limit=100 ./node_modules/.bin/nx test php-wasm-node --test-name-pattern='asyncify'\n` - ); - } - - const err = new Error( - `Asyncify crash! No C functions present in the stack trace were missing ` + - `from the Dockerfile. This could mean the stack trace is too short – try increasing the stack trace limit ` + - `with --stack-trace-limit=100. If you already did that, fixing this problem will likely take more digging.` - ); - err.cause = e; - throw err; - } - } - } - }); -}); - -let Dockerfile = InitialDockerfile; -const DockerfilePath = path.resolve( - __dirname, - '../../../compile/php/Dockerfile' -); -function addAsyncifyFunctionsToDockerfile(functions: string[]) { - const currentDockerfile = fs.readFileSync(DockerfilePath, 'utf8') + ''; - const lookup = `export ASYNCIFY_ONLY=$'`; - const idx = currentDockerfile.indexOf(lookup) + lookup.length; - const updatedDockerfile = - currentDockerfile.substring(0, idx) + - functions.map((f) => `"${f}",\\\n`).join('') + - currentDockerfile.substring(idx); - fs.writeFileSync(DockerfilePath, updatedDockerfile); - Dockerfile = updatedDockerfile; -} diff --git a/packages/php-wasm/node/src/test/php-asyncify-fsockopen.spec.ts b/packages/php-wasm/node/src/test/php-asyncify-fsockopen.spec.ts deleted file mode 100644 index eef049074b..0000000000 --- a/packages/php-wasm/node/src/test/php-asyncify-fsockopen.spec.ts +++ /dev/null @@ -1,356 +0,0 @@ -import http from 'http'; -import https from 'https'; -import fs from 'fs'; -import path from 'path'; -import { - PHP, - SupportedPHPVersions, - setPhpIniEntries, -} from '@php-wasm/universal'; -import { phpVars } from '@php-wasm/util'; -// eslint-disable-next-line @nx/enforce-module-boundaries -import InitialDockerfile from '../../../compile/php/Dockerfile?raw'; -import { loadNodeRuntime } from '../lib'; - -const requestHandler = ( - req: http.IncomingMessage, - res: http.ServerResponse -) => { - if (req.url === '/image.jpg') { - const image = fs.readFileSync( - path.join(__dirname, 'test-data', 'image.jpg') - ); - res.writeHead(200, { 'Content-Type': 'image/jpeg' }); - res.write(image); - res.end(); - } else { - res.writeHead(200, { 'Content-Type': 'text/plain' }); - res.end('Hello World\n'); - } -}; - -const httpServer = http.createServer(requestHandler); -const selfSignedCert = { - key: fs.readFileSync(path.join(__dirname, 'test-data', 'key.pem')), - cert: fs.readFileSync(path.join(__dirname, 'test-data', 'cert.pem')), -}; -const httpsServer = https.createServer( - { - key: selfSignedCert.key, - cert: selfSignedCert.cert, - }, - requestHandler -); - -type Mode = 'http' | 'https'; - -const protocols = { - http: { - protocol: 'http', - port: new Promise((resolve) => { - httpServer.listen(0, function () { - resolve((httpServer.address() as any).port); - }); - }), - }, - https: { - protocol: 'https', - port: new Promise((resolve) => { - httpsServer.listen(0, function () { - resolve((httpsServer.address() as any).port); - }); - }), - }, -}; - -const { protocol, port } = protocols[import.meta.env['PROTOCOL'] as Mode]; - -const host = '127.0.0.1'; - -const httpUrl = `${protocol}://${host}:${port}`; - -describe(`${protocol} protocol – asyncify`, () => { - const js = phpVars({ - host, - port, - httpUrl, - }); - - const phpVersions = - 'PHP' in process.env ? [process.env['PHP']] : SupportedPHPVersions; - - const topOfTheStack: Record = { - // Network functions from https://www.php.net/manual/en/book.network.php - fsockopen: ` - $fp = fsockopen(${js['host']}, ${js['port']}); - fwrite($fp, "GET / HTTP/1.1\\r\\n\\r\\n"); - fread($fp, 10); - fclose($fp);`, - }; - - describe.each(phpVersions)('PHP %s – asyncify', (phpVersion) => { - let php: PHP; - beforeEach(async () => { - php = new PHP(await loadNodeRuntime(phpVersion as any)); - await setPhpIniEntries(php, { allow_url_fopen: 1 }); - }); - - afterEach(async () => { - php?.[Symbol.dispose]?.(); - }); - - describe.each(Object.keys(topOfTheStack))('%s', (networkCallKey) => { - const networkCall = topOfTheStack[networkCallKey]; - test('Direct call', () => assertNoCrash(networkCall)); - describe('Function calls', () => { - test('Simple call', () => assertNoCrash(`${networkCall};`)); - test('Simple call', () => - assertNoCrash(`function top() { ${networkCall} } top();`)); - test('Via call_user_func', () => - assertNoCrash( - `function top() { ${networkCall} } call_user_func('top'); ` - )); - test('Via call_user_func_array', () => - assertNoCrash( - `function top() { ${networkCall} } call_user_func_array('top', array());` - )); - }); - - describe('Array functions', () => { - test('array_filter', () => - assertNoCrash(` - function top() { ${networkCall} } - array_filter(array('top'), 'top'); - `)); - - test('array_map', () => - assertNoCrash(` - function top() { ${networkCall} } - array_map(array('top'), 'top'); - `)); - - // Network calls in sort() would be silly so let's skip those for now. - }); - - describe('Class method calls', () => { - test('Regular method', () => - assertNoCrash(` - class Top { - function my_method() { ${networkCall} } - } - $x = new Top(); - $x->my_method(); - `)); - test('Via ReflectionMethod->invoke()', () => - assertNoCrash(` - class Top { - function my_method() { ${networkCall} } - } - $reflectionMethod = new ReflectionMethod('Top', 'my_method'); - $reflectionMethod->invoke(new Top()); - `)); - test('Via ReflectionMethod->invokeArgs()', () => - assertNoCrash(` - class Top { - function my_method() { ${networkCall} } - } - $reflectionMethod = new ReflectionMethod('Top', 'my_method'); - $reflectionMethod->invokeArgs(new Top(), array()); - `)); - test('Via call_user_func', () => - assertNoCrash(` - class Top { - function my_method() { ${networkCall} } - } - call_user_func([new Top(), 'my_method']); - `)); - test('Via call_user_func_array', () => - assertNoCrash(` - class Top { - function my_method() { ${networkCall} } - } - call_user_func_array([new Top(), 'my_method'], []); - `)); - test('Constructor', () => - assertNoCrash(` - class Top { - function __construct() { ${networkCall} } - } - new Top(); - `)); - test('Destructor', () => - assertNoCrash(` - class Top { - function __destruct() { ${networkCall} } - } - $x = new Top(); - unset($x); - `)); - test('__call', () => - assertNoCrash(` - class Top { - function __call($method, $args) { ${networkCall} } - } - $x = new Top(); - $x->test(); - `)); - test('__get', () => - assertNoCrash(` - class Top { - function __get($prop) { ${networkCall} } - } - $x = new Top(); - $x->test; - `)); - test('__set', () => - assertNoCrash(` - class Top { - function __set($prop, $value) { ${networkCall} } - } - $x = new Top(); - $x->test = 1; - `)); - test('__isset', () => - assertNoCrash(` - class Top { - function __isset($prop) { ${networkCall} } - } - $x = new Top(); - isset($x->test); - `)); - test('ArrayAccess', () => { - assertNoCrash(` - class Top implements ArrayAccess { - function offsetExists($offset) { ${networkCall} } - function offsetGet($offset) { ${networkCall} } - function offsetSet($offset, $value) { ${networkCall} } - function offsetUnset($offset) { ${networkCall} } - } - $x = new Top(); - isset($x['test']); - $a = $x['test']; - $x['test'] = 123; - unset($x['test']); - `); - }); - test('Iterator', () => - assertNoCrash(` - $data = new class() implements IteratorAggregate { - public function getIterator(): Traversable { - ${networkCall}; - return new ArrayIterator( [] ); - } - }; - echo json_encode( [ - ...$data - ] ); - `)); - - test('Countable', () => - assertNoCrash(` - $data = new class() implements Countable { - public function count() { - ${networkCall} - return 0; - } - }; - count($data); - `)); - - test('yield', () => - assertNoCrash(` - function countTo2() { - ${networkCall}; - yield '1'; - ${networkCall}; - yield '2'; - } - foreach(countTo2() as $number) { - echo $number; - } - `)); - }); - - describe('exif extension support', () => { - it('exif_read_data', async () => { - assertNoCrash( - `var_dump(exif_read_data('${httpUrl}/image.jpg'));` - ); - }); - it('exif_imagetype', async () => { - assertNoCrash( - `var_dump(exif_imagetype('${httpUrl}/image.jpg'));` - ); - }); - it('exif_thumbnail', async () => { - assertNoCrash( - `var_dump(exif_thumbnail('${httpUrl}/image.jpg'));` - ); - }); - }); - }); - - async function assertNoCrash(code: string) { - try { - const result = await php.run({ - code: ` - candidate.replace('byn$fpcast-emu$', '') - ) - .filter( - (candidate) => - !Dockerfile.includes(`"${candidate}"`) - ); - if (missingCandidates.length) { - addAsyncifyFunctionsToDockerfile(missingCandidates); - throw new Error( - `Asyncify crash! The following missing functions were just auto-added to the ASYNCIFY_ONLY list in the Dockerfile: \n ` + - missingCandidates.join(', ') + - `\nYou now need to rebuild PHP and re-run this test: \n` + - ` npm run recompile:php:node:asyncify:8.0\n` + - ` node --stack-trace-limit=100 ./node_modules/.bin/nx test php-wasm-node --test-name-pattern='asyncify'\n` - ); - } - - const err = new Error( - `Asyncify crash! No C functions present in the stack trace were missing ` + - `from the Dockerfile. This could mean the stack trace is too short – try increasing the stack trace limit ` + - `with --stack-trace-limit=100. If you already did that, fixing this problem will likely take more digging.` - ); - err.cause = e; - throw err; - } - } - } - }); -}); - -let Dockerfile = InitialDockerfile; -const DockerfilePath = path.resolve( - __dirname, - '../../../compile/php/Dockerfile' -); -function addAsyncifyFunctionsToDockerfile(functions: string[]) { - const currentDockerfile = fs.readFileSync(DockerfilePath, 'utf8') + ''; - const lookup = `export ASYNCIFY_ONLY=$'`; - const idx = currentDockerfile.indexOf(lookup) + lookup.length; - const updatedDockerfile = - currentDockerfile.substring(0, idx) + - functions.map((f) => `"${f}",\\\n`).join('') + - currentDockerfile.substring(idx); - fs.writeFileSync(DockerfilePath, updatedDockerfile); - Dockerfile = updatedDockerfile; -} diff --git a/packages/php-wasm/node/src/test/php-asyncify-gethostbyname.spec.ts b/packages/php-wasm/node/src/test/php-asyncify-gethostbyname.spec.ts deleted file mode 100644 index 8d1edb29ef..0000000000 --- a/packages/php-wasm/node/src/test/php-asyncify-gethostbyname.spec.ts +++ /dev/null @@ -1,355 +0,0 @@ -import http from 'http'; -import https from 'https'; -import fs from 'fs'; -import path from 'path'; -import { - PHP, - SupportedPHPVersions, - setPhpIniEntries, -} from '@php-wasm/universal'; -import { phpVars } from '@php-wasm/util'; -// eslint-disable-next-line @nx/enforce-module-boundaries -import InitialDockerfile from '../../../compile/php/Dockerfile?raw'; -import { loadNodeRuntime } from '../lib'; - -const requestHandler = ( - req: http.IncomingMessage, - res: http.ServerResponse -) => { - if (req.url === '/image.jpg') { - const image = fs.readFileSync( - path.join(__dirname, 'test-data', 'image.jpg') - ); - res.writeHead(200, { 'Content-Type': 'image/jpeg' }); - res.write(image); - res.end(); - } else { - res.writeHead(200, { 'Content-Type': 'text/plain' }); - res.end('Hello World\n'); - } -}; - -const httpServer = http.createServer(requestHandler); -const selfSignedCert = { - key: fs.readFileSync(path.join(__dirname, 'test-data', 'key.pem')), - cert: fs.readFileSync(path.join(__dirname, 'test-data', 'cert.pem')), -}; -const httpsServer = https.createServer( - { - key: selfSignedCert.key, - cert: selfSignedCert.cert, - }, - requestHandler -); - -type Mode = 'http' | 'https'; - -const protocols = { - http: { - protocol: 'http', - port: new Promise((resolve) => { - httpServer.listen(0, function () { - resolve((httpServer.address() as any).port); - }); - }), - }, - https: { - protocol: 'https', - port: new Promise((resolve) => { - httpsServer.listen(0, function () { - resolve((httpsServer.address() as any).port); - }); - }), - }, -}; - -const { protocol, port } = protocols[import.meta.env['PROTOCOL'] as Mode]; - -const host = '127.0.0.1'; - -const httpUrl = `${protocol}://${host}:${port}`; - -describe(`${protocol} protocol – asyncify`, () => { - const js = phpVars({ - host, - port, - httpUrl, - }); - - const phpVersions = - 'PHP' in process.env ? [process.env['PHP']] : SupportedPHPVersions; - - const topOfTheStack: Record = { - gethostbyname: `gethostbyname(${js['httpUrl']});`, - - // @TODO: - // PDO functions from https://www.php.net/manual/en/book.pdo.php - // Sockets functions from https://www.php.net/manual/en/book.sockets.php - }; - - describe.each(phpVersions)('PHP %s – asyncify', (phpVersion) => { - let php: PHP; - beforeEach(async () => { - php = new PHP(await loadNodeRuntime(phpVersion as any)); - await setPhpIniEntries(php, { allow_url_fopen: 1 }); - }); - - afterEach(async () => { - php?.[Symbol.dispose]?.(); - }); - - describe.each(Object.keys(topOfTheStack))('%s', (networkCallKey) => { - const networkCall = topOfTheStack[networkCallKey]; - test('Direct call', () => assertNoCrash(networkCall)); - describe('Function calls', () => { - test('Simple call', () => assertNoCrash(`${networkCall};`)); - test('Simple call', () => - assertNoCrash(`function top() { ${networkCall} } top();`)); - test('Via call_user_func', () => - assertNoCrash( - `function top() { ${networkCall} } call_user_func('top'); ` - )); - test('Via call_user_func_array', () => - assertNoCrash( - `function top() { ${networkCall} } call_user_func_array('top', array());` - )); - }); - - describe('Array functions', () => { - test('array_filter', () => - assertNoCrash(` - function top() { ${networkCall} } - array_filter(array('top'), 'top'); - `)); - - test('array_map', () => - assertNoCrash(` - function top() { ${networkCall} } - array_map(array('top'), 'top'); - `)); - - // Network calls in sort() would be silly so let's skip those for now. - }); - - describe('Class method calls', () => { - test('Regular method', () => - assertNoCrash(` - class Top { - function my_method() { ${networkCall} } - } - $x = new Top(); - $x->my_method(); - `)); - test('Via ReflectionMethod->invoke()', () => - assertNoCrash(` - class Top { - function my_method() { ${networkCall} } - } - $reflectionMethod = new ReflectionMethod('Top', 'my_method'); - $reflectionMethod->invoke(new Top()); - `)); - test('Via ReflectionMethod->invokeArgs()', () => - assertNoCrash(` - class Top { - function my_method() { ${networkCall} } - } - $reflectionMethod = new ReflectionMethod('Top', 'my_method'); - $reflectionMethod->invokeArgs(new Top(), array()); - `)); - test('Via call_user_func', () => - assertNoCrash(` - class Top { - function my_method() { ${networkCall} } - } - call_user_func([new Top(), 'my_method']); - `)); - test('Via call_user_func_array', () => - assertNoCrash(` - class Top { - function my_method() { ${networkCall} } - } - call_user_func_array([new Top(), 'my_method'], []); - `)); - test('Constructor', () => - assertNoCrash(` - class Top { - function __construct() { ${networkCall} } - } - new Top(); - `)); - test('Destructor', () => - assertNoCrash(` - class Top { - function __destruct() { ${networkCall} } - } - $x = new Top(); - unset($x); - `)); - test('__call', () => - assertNoCrash(` - class Top { - function __call($method, $args) { ${networkCall} } - } - $x = new Top(); - $x->test(); - `)); - test('__get', () => - assertNoCrash(` - class Top { - function __get($prop) { ${networkCall} } - } - $x = new Top(); - $x->test; - `)); - test('__set', () => - assertNoCrash(` - class Top { - function __set($prop, $value) { ${networkCall} } - } - $x = new Top(); - $x->test = 1; - `)); - test('__isset', () => - assertNoCrash(` - class Top { - function __isset($prop) { ${networkCall} } - } - $x = new Top(); - isset($x->test); - `)); - test('ArrayAccess', () => { - assertNoCrash(` - class Top implements ArrayAccess { - function offsetExists($offset) { ${networkCall} } - function offsetGet($offset) { ${networkCall} } - function offsetSet($offset, $value) { ${networkCall} } - function offsetUnset($offset) { ${networkCall} } - } - $x = new Top(); - isset($x['test']); - $a = $x['test']; - $x['test'] = 123; - unset($x['test']); - `); - }); - test('Iterator', () => - assertNoCrash(` - $data = new class() implements IteratorAggregate { - public function getIterator(): Traversable { - ${networkCall}; - return new ArrayIterator( [] ); - } - }; - echo json_encode( [ - ...$data - ] ); - `)); - - test('Countable', () => - assertNoCrash(` - $data = new class() implements Countable { - public function count() { - ${networkCall} - return 0; - } - }; - count($data); - `)); - - test('yield', () => - assertNoCrash(` - function countTo2() { - ${networkCall}; - yield '1'; - ${networkCall}; - yield '2'; - } - foreach(countTo2() as $number) { - echo $number; - } - `)); - }); - - describe('exif extension support', () => { - it('exif_read_data', async () => { - assertNoCrash( - `var_dump(exif_read_data('${httpUrl}/image.jpg'));` - ); - }); - it('exif_imagetype', async () => { - assertNoCrash( - `var_dump(exif_imagetype('${httpUrl}/image.jpg'));` - ); - }); - it('exif_thumbnail', async () => { - assertNoCrash( - `var_dump(exif_thumbnail('${httpUrl}/image.jpg'));` - ); - }); - }); - }); - - async function assertNoCrash(code: string) { - try { - const result = await php.run({ - code: ` - candidate.replace('byn$fpcast-emu$', '') - ) - .filter( - (candidate) => - !Dockerfile.includes(`"${candidate}"`) - ); - if (missingCandidates.length) { - addAsyncifyFunctionsToDockerfile(missingCandidates); - throw new Error( - `Asyncify crash! The following missing functions were just auto-added to the ASYNCIFY_ONLY list in the Dockerfile: \n ` + - missingCandidates.join(', ') + - `\nYou now need to rebuild PHP and re-run this test: \n` + - ` npm run recompile:php:node:asyncify:8.0\n` + - ` node --stack-trace-limit=100 ./node_modules/.bin/nx test php-wasm-node --test-name-pattern='asyncify'\n` - ); - } - - const err = new Error( - `Asyncify crash! No C functions present in the stack trace were missing ` + - `from the Dockerfile. This could mean the stack trace is too short – try increasing the stack trace limit ` + - `with --stack-trace-limit=100. If you already did that, fixing this problem will likely take more digging.` - ); - err.cause = e; - throw err; - } - } - } - }); -}); - -let Dockerfile = InitialDockerfile; -const DockerfilePath = path.resolve( - __dirname, - '../../../compile/php/Dockerfile' -); -function addAsyncifyFunctionsToDockerfile(functions: string[]) { - const currentDockerfile = fs.readFileSync(DockerfilePath, 'utf8') + ''; - const lookup = `export ASYNCIFY_ONLY=$'`; - const idx = currentDockerfile.indexOf(lookup) + lookup.length; - const updatedDockerfile = - currentDockerfile.substring(0, idx) + - functions.map((f) => `"${f}",\\\n`).join('') + - currentDockerfile.substring(idx); - fs.writeFileSync(DockerfilePath, updatedDockerfile); - Dockerfile = updatedDockerfile; -} diff --git a/packages/php-wasm/node/src/test/php-asyncify-mysqli.spec.ts b/packages/php-wasm/node/src/test/php-asyncify-mysqli.spec.ts deleted file mode 100644 index e5655bfd7c..0000000000 --- a/packages/php-wasm/node/src/test/php-asyncify-mysqli.spec.ts +++ /dev/null @@ -1,391 +0,0 @@ -import http from 'http'; -import https from 'https'; -import fs from 'fs'; -import path from 'path'; -import { - PHP, - SupportedPHPVersions, - setPhpIniEntries, -} from '@php-wasm/universal'; -// eslint-disable-next-line @nx/enforce-module-boundaries -import InitialDockerfile from '../../../compile/php/Dockerfile?raw'; -import { loadNodeRuntime } from '../lib'; - -const requestHandler = ( - req: http.IncomingMessage, - res: http.ServerResponse -) => { - if (req.url === '/image.jpg') { - const image = fs.readFileSync( - path.join(__dirname, 'test-data', 'image.jpg') - ); - res.writeHead(200, { 'Content-Type': 'image/jpeg' }); - res.write(image); - res.end(); - } else { - res.writeHead(200, { 'Content-Type': 'text/plain' }); - res.end('Hello World\n'); - } -}; - -const httpServer = http.createServer(requestHandler); -const selfSignedCert = { - key: fs.readFileSync(path.join(__dirname, 'test-data', 'key.pem')), - cert: fs.readFileSync(path.join(__dirname, 'test-data', 'cert.pem')), -}; -const httpsServer = https.createServer( - { - key: selfSignedCert.key, - cert: selfSignedCert.cert, - }, - requestHandler -); - -type Mode = 'http' | 'https'; - -const protocols = { - http: { - protocol: 'http', - port: new Promise((resolve) => { - httpServer.listen(0, function () { - resolve((httpServer.address() as any).port); - }); - }), - }, - https: { - protocol: 'https', - port: new Promise((resolve) => { - httpsServer.listen(0, function () { - resolve((httpsServer.address() as any).port); - }); - }), - }, -}; - -const { protocol, port } = protocols[import.meta.env['PROTOCOL'] as Mode]; - -const host = '127.0.0.1'; - -const httpUrl = `${protocol}://${host}:${port}`; - -describe(`${protocol} protocol – asyncify`, () => { - const phpVersions = - 'PHP' in process.env ? [process.env['PHP']] : SupportedPHPVersions; - - const topOfTheStack: Record = {}; - - // Run MySQL functions if credentials are provided - const mysqlCredentials = { - host: process.env['MYSQL_HOST'] ?? '127.0.0.1', - user: process.env['MYSQL_USER'], - password: process.env['MYSQL_PASSWORD'], - database: process.env['MYSQL_DATABASE'], - port: process.env['MYSQL_PORT'] ?? '3306', - }; - - if ( - mysqlCredentials.host && - mysqlCredentials.user && - mysqlCredentials.password && - mysqlCredentials.database - ) { - topOfTheStack['mysqli'] = ` - $mysqli = new mysqli( - "${mysqlCredentials.host}", - "${mysqlCredentials.user}", - "${mysqlCredentials.password}", - "${mysqlCredentials.database}", - ${mysqlCredentials.port} - ); - if (mysqli_connect_errno()) { - // This should crash the process I hope - klfhjkljfkdjfd(); - } - mysqli_ping($mysqli); - mysqli_query($mysqli, "SELECT 1"); - mysqli_multi_query($mysqli, "SELECT 1; SELECT 2;"); - mysqli_get_server_info($mysqli); - mysqli_get_server_version($mysqli); - mysqli_get_proto_info($mysqli); - mysqli_close($mysqli);`; - } else { - console.log(` - Skipping MySQL network functions because no credentials were provided. - - To run MySQL network function tests, set the following environment variables: - - MYSQL_HOST - - MYSQL_USER - - MYSQL_PASSWORD - - MYSQL_DATABASE - - Use 127.0.0.1 instead of localhost to ensure MySQL uses - TCP instead of socket, because MySQL in Playground - still doesn't support sockets. - `); - } - describe.each(phpVersions)('PHP %s – asyncify', (phpVersion) => { - let php: PHP; - beforeEach(async () => { - php = new PHP(await loadNodeRuntime(phpVersion as any)); - await setPhpIniEntries(php, { allow_url_fopen: 1 }); - }); - - afterEach(async () => { - php?.[Symbol.dispose]?.(); - }); - - describe.each(Object.keys(topOfTheStack))('%s', (networkCallKey) => { - const networkCall = topOfTheStack[networkCallKey]; - test('Direct call', () => assertNoCrash(networkCall)); - describe('Function calls', () => { - test('Simple call', () => assertNoCrash(`${networkCall};`)); - test('Simple call', () => - assertNoCrash(`function top() { ${networkCall} } top();`)); - test('Via call_user_func', () => - assertNoCrash( - `function top() { ${networkCall} } call_user_func('top'); ` - )); - test('Via call_user_func_array', () => - assertNoCrash( - `function top() { ${networkCall} } call_user_func_array('top', array());` - )); - }); - - describe('Array functions', () => { - test('array_filter', () => - assertNoCrash(` - function top() { ${networkCall} } - array_filter(array('top'), 'top'); - `)); - - test('array_map', () => - assertNoCrash(` - function top() { ${networkCall} } - array_map(array('top'), 'top'); - `)); - - // Network calls in sort() would be silly so let's skip those for now. - }); - - describe('Class method calls', () => { - test('Regular method', () => - assertNoCrash(` - class Top { - function my_method() { ${networkCall} } - } - $x = new Top(); - $x->my_method(); - `)); - test('Via ReflectionMethod->invoke()', () => - assertNoCrash(` - class Top { - function my_method() { ${networkCall} } - } - $reflectionMethod = new ReflectionMethod('Top', 'my_method'); - $reflectionMethod->invoke(new Top()); - `)); - test('Via ReflectionMethod->invokeArgs()', () => - assertNoCrash(` - class Top { - function my_method() { ${networkCall} } - } - $reflectionMethod = new ReflectionMethod('Top', 'my_method'); - $reflectionMethod->invokeArgs(new Top(), array()); - `)); - test('Via call_user_func', () => - assertNoCrash(` - class Top { - function my_method() { ${networkCall} } - } - call_user_func([new Top(), 'my_method']); - `)); - test('Via call_user_func_array', () => - assertNoCrash(` - class Top { - function my_method() { ${networkCall} } - } - call_user_func_array([new Top(), 'my_method'], []); - `)); - test('Constructor', () => - assertNoCrash(` - class Top { - function __construct() { ${networkCall} } - } - new Top(); - `)); - test('Destructor', () => - assertNoCrash(` - class Top { - function __destruct() { ${networkCall} } - } - $x = new Top(); - unset($x); - `)); - test('__call', () => - assertNoCrash(` - class Top { - function __call($method, $args) { ${networkCall} } - } - $x = new Top(); - $x->test(); - `)); - test('__get', () => - assertNoCrash(` - class Top { - function __get($prop) { ${networkCall} } - } - $x = new Top(); - $x->test; - `)); - test('__set', () => - assertNoCrash(` - class Top { - function __set($prop, $value) { ${networkCall} } - } - $x = new Top(); - $x->test = 1; - `)); - test('__isset', () => - assertNoCrash(` - class Top { - function __isset($prop) { ${networkCall} } - } - $x = new Top(); - isset($x->test); - `)); - test('ArrayAccess', () => { - assertNoCrash(` - class Top implements ArrayAccess { - function offsetExists($offset) { ${networkCall} } - function offsetGet($offset) { ${networkCall} } - function offsetSet($offset, $value) { ${networkCall} } - function offsetUnset($offset) { ${networkCall} } - } - $x = new Top(); - isset($x['test']); - $a = $x['test']; - $x['test'] = 123; - unset($x['test']); - `); - }); - test('Iterator', () => - assertNoCrash(` - $data = new class() implements IteratorAggregate { - public function getIterator(): Traversable { - ${networkCall}; - return new ArrayIterator( [] ); - } - }; - echo json_encode( [ - ...$data - ] ); - `)); - - test('Countable', () => - assertNoCrash(` - $data = new class() implements Countable { - public function count() { - ${networkCall} - return 0; - } - }; - count($data); - `)); - - test('yield', () => - assertNoCrash(` - function countTo2() { - ${networkCall}; - yield '1'; - ${networkCall}; - yield '2'; - } - foreach(countTo2() as $number) { - echo $number; - } - `)); - }); - - describe('exif extension support', () => { - it('exif_read_data', async () => { - assertNoCrash( - `var_dump(exif_read_data('${httpUrl}/image.jpg'));` - ); - }); - it('exif_imagetype', async () => { - assertNoCrash( - `var_dump(exif_imagetype('${httpUrl}/image.jpg'));` - ); - }); - it('exif_thumbnail', async () => { - assertNoCrash( - `var_dump(exif_thumbnail('${httpUrl}/image.jpg'));` - ); - }); - }); - }); - - async function assertNoCrash(code: string) { - try { - const result = await php.run({ - code: ` - candidate.replace('byn$fpcast-emu$', '') - ) - .filter( - (candidate) => - !Dockerfile.includes(`"${candidate}"`) - ); - if (missingCandidates.length) { - addAsyncifyFunctionsToDockerfile(missingCandidates); - throw new Error( - `Asyncify crash! The following missing functions were just auto-added to the ASYNCIFY_ONLY list in the Dockerfile: \n ` + - missingCandidates.join(', ') + - `\nYou now need to rebuild PHP and re-run this test: \n` + - ` npm run recompile:php:node:asyncify:8.0\n` + - ` node --stack-trace-limit=100 ./node_modules/.bin/nx test php-wasm-node --test-name-pattern='asyncify'\n` - ); - } - - const err = new Error( - `Asyncify crash! No C functions present in the stack trace were missing ` + - `from the Dockerfile. This could mean the stack trace is too short – try increasing the stack trace limit ` + - `with --stack-trace-limit=100. If you already did that, fixing this problem will likely take more digging.` - ); - err.cause = e; - throw err; - } - } - } - }); -}); - -let Dockerfile = InitialDockerfile; -const DockerfilePath = path.resolve( - __dirname, - '../../../compile/php/Dockerfile' -); -function addAsyncifyFunctionsToDockerfile(functions: string[]) { - const currentDockerfile = fs.readFileSync(DockerfilePath, 'utf8') + ''; - const lookup = `export ASYNCIFY_ONLY=$'`; - const idx = currentDockerfile.indexOf(lookup) + lookup.length; - const updatedDockerfile = - currentDockerfile.substring(0, idx) + - functions.map((f) => `"${f}",\\\n`).join('') + - currentDockerfile.substring(idx); - fs.writeFileSync(DockerfilePath, updatedDockerfile); - Dockerfile = updatedDockerfile; -} diff --git a/packages/php-wasm/node/src/test/php-crash.spec.ts b/packages/php-wasm/node/src/test/php-crash.spec.ts index 2214a99f6b..1b09009ce5 100644 --- a/packages/php-wasm/node/src/test/php-crash.spec.ts +++ b/packages/php-wasm/node/src/test/php-crash.spec.ts @@ -6,11 +6,12 @@ import { PHP, } from '@php-wasm/universal'; import { loadNodeRuntime } from '../lib'; +import { jspi } from 'wasm-feature-detect'; // @TODO Prevent crash on PHP versions 5.6, 7.2, 8.2 describe.each(['7.3', '7.4', '8.0', '8.1'])( 'PHP %s – process crash', - (phpVersion) => { + async (phpVersion) => { let php: PHP; let unhandledRejection: any; beforeEach(async () => { @@ -23,7 +24,7 @@ describe.each(['7.3', '7.4', '8.0', '8.1'])( }); afterEach(async () => { - php?.[Symbol.dispose]?.(); + php.exit(); }); function unhandledRejectionHandler(error: any) { @@ -37,50 +38,52 @@ describe.each(['7.3', '7.4', '8.0', '8.1'])( process.off('unhandledRejection', unhandledRejectionHandler); }); - it('Does not crash due to an unhandled Asyncify error ', async () => { - let caughtError; - - try { - /** - * PHP is intentionally built without network support for __clone() - * because it's an extremely unlikely place for any network activity - * and not supporting it allows us to test the error handling here. - * - * `clone $x` will throw an asynchronous error out when attempting - * to do a network call ("unreachable" WASM instruction executed). - * This test should gracefully catch and handle that error. - * - * A failure to do so will crash the entire process - */ - await php.run({ - code: ` { + let caughtError; + + try { + /** + * PHP is intentionally built without network support for __clone() + * because it's an extremely unlikely place for any network activity + * and not supporting it allows us to test the error handling here. + * + * `clone $x` will throw an asynchronous error out when attempting + * to do a network call ("unreachable" WASM instruction executed). + * This test should gracefully catch and handle that error. + * + * A failure to do so will crash the entire process + */ + await php.run({ + code: ` { // Tolerate an unhandled rejections diff --git a/packages/php-wasm/node/src/test/php-dynamic-loading.spec.ts b/packages/php-wasm/node/src/test/php-dynamic-loading.spec.ts index 4b76bfd3cc..453dbd5537 100644 --- a/packages/php-wasm/node/src/test/php-dynamic-loading.spec.ts +++ b/packages/php-wasm/node/src/test/php-dynamic-loading.spec.ts @@ -12,6 +12,10 @@ describe.each(SupportedPHPVersions)('PHP %s', (phpVersion) => { ); }); + afterEach(async () => { + php.exit(); + }); + it('does not load dynamically by default', async () => { php = new PHP(await loadNodeRuntime(phpVersion as any)); diff --git a/packages/php-wasm/node/src/test/php-file-get-contents.spec.ts b/packages/php-wasm/node/src/test/php-file-get-contents.spec.ts new file mode 100644 index 0000000000..f8f3deec52 --- /dev/null +++ b/packages/php-wasm/node/src/test/php-file-get-contents.spec.ts @@ -0,0 +1,357 @@ +import http from 'http'; +import https from 'https'; +import fs from 'fs'; +import path from 'path'; +import { + PHP, + SupportedPHPVersions, + setPhpIniEntries, +} from '@php-wasm/universal'; +import { phpVars } from '@php-wasm/util'; +// eslint-disable-next-line @nx/enforce-module-boundaries +import InitialDockerfile from '../../../compile/php/Dockerfile?raw'; +import { loadNodeRuntime } from '../lib'; +import { jspi } from 'wasm-feature-detect'; + +const runtimeMode = (await jspi()) ? 'jspi' : 'asyncify'; + +const requestHandler = ( + req: http.IncomingMessage, + res: http.ServerResponse +) => { + if (req.url === '/image.jpg') { + const image = fs.readFileSync( + path.join(__dirname, 'test-data', 'image.jpg') + ); + res.writeHead(200, { 'Content-Type': 'image/jpeg' }); + res.write(image); + res.end(); + } else { + res.writeHead(200, { 'Content-Type': 'text/plain' }); + res.end('Hello World\n'); + } +}; + +const httpServer = http.createServer(requestHandler); +const selfSignedCert = { + key: fs.readFileSync(path.join(__dirname, 'test-data', 'key.pem')), + cert: fs.readFileSync(path.join(__dirname, 'test-data', 'cert.pem')), +}; +const httpsServer = https.createServer( + { + key: selfSignedCert.key, + cert: selfSignedCert.cert, + }, + requestHandler +); + +[ + { + protocol: 'http', + port: new Promise((resolve) => { + httpServer.listen(0, function () { + resolve((httpServer.address() as any).port); + }); + }), + }, + { + protocol: 'https', + port: new Promise((resolve) => { + httpsServer.listen(0, function () { + resolve((httpsServer.address() as any).port); + }); + }), + }, +].forEach(({ protocol, port }) => { + const host = '127.0.0.1'; + + const httpUrl = `${protocol}://${host}:${port}`; + + describe(`${protocol} protocol – ${runtimeMode}`, () => { + const js = phpVars({ + host, + port, + httpUrl, + }); + + const phpVersions = + 'PHP' in process.env ? [process.env['PHP']] : SupportedPHPVersions; + + const topOfTheStack: Record = { + file_get_contents: `file_get_contents(${js['httpUrl']});`, + }; + + describe.each(phpVersions)(`PHP %s – ${runtimeMode}`, (phpVersion) => { + let php: PHP; + beforeEach(async () => { + php = new PHP(await loadNodeRuntime(phpVersion as any)); + await setPhpIniEntries(php, { allow_url_fopen: 1 }); + }); + + afterEach(async () => { + php.exit(); + }); + + describe.each(Object.keys(topOfTheStack))( + '%s', + (networkCallKey) => { + const networkCall = topOfTheStack[networkCallKey]; + test('Direct call', () => assertNoCrash(networkCall)); + describe('Function calls', () => { + test('Simple call', () => + assertNoCrash(`${networkCall};`)); + test('Simple call', () => + assertNoCrash( + `function top() { ${networkCall} } top();` + )); + test('Via call_user_func', () => + assertNoCrash( + `function top() { ${networkCall} } call_user_func('top'); ` + )); + test('Via call_user_func_array', () => + assertNoCrash( + `function top() { ${networkCall} } call_user_func_array('top', array());` + )); + }); + + describe('Array functions', () => { + test('array_filter', () => + assertNoCrash(` + function top() { ${networkCall} } + array_filter(array('top'), 'top'); + `)); + + test('array_map', () => + assertNoCrash(` + function top() { ${networkCall} } + array_map(array('top'), 'top'); + `)); + + // Network calls in sort() would be silly so let's skip those for now. + }); + + describe('Class method calls', () => { + test('Regular method', () => + assertNoCrash(` + class Top { + function my_method() { ${networkCall} } + } + $x = new Top(); + $x->my_method(); + `)); + test('Via ReflectionMethod->invoke()', () => + assertNoCrash(` + class Top { + function my_method() { ${networkCall} } + } + $reflectionMethod = new ReflectionMethod('Top', 'my_method'); + $reflectionMethod->invoke(new Top()); + `)); + test('Via ReflectionMethod->invokeArgs()', () => + assertNoCrash(` + class Top { + function my_method() { ${networkCall} } + } + $reflectionMethod = new ReflectionMethod('Top', 'my_method'); + $reflectionMethod->invokeArgs(new Top(), array()); + `)); + test('Via call_user_func', () => + assertNoCrash(` + class Top { + function my_method() { ${networkCall} } + } + call_user_func([new Top(), 'my_method']); + `)); + test('Via call_user_func_array', () => + assertNoCrash(` + class Top { + function my_method() { ${networkCall} } + } + call_user_func_array([new Top(), 'my_method'], []); + `)); + test('Constructor', () => + assertNoCrash(` + class Top { + function __construct() { ${networkCall} } + } + new Top(); + `)); + test('Destructor', () => + assertNoCrash(` + class Top { + function __destruct() { ${networkCall} } + } + $x = new Top(); + unset($x); + `)); + test('__call', () => + assertNoCrash(` + class Top { + function __call($method, $args) { ${networkCall} } + } + $x = new Top(); + $x->test(); + `)); + test('__get', () => + assertNoCrash(` + class Top { + function __get($prop) { ${networkCall} } + } + $x = new Top(); + $x->test; + `)); + test('__set', () => + assertNoCrash(` + class Top { + function __set($prop, $value) { ${networkCall} } + } + $x = new Top(); + $x->test = 1; + `)); + test('__isset', () => + assertNoCrash(` + class Top { + function __isset($prop) { ${networkCall} } + } + $x = new Top(); + isset($x->test); + `)); + test('ArrayAccess', () => { + assertNoCrash(` + class Top implements ArrayAccess { + function offsetExists($offset) { ${networkCall} } + function offsetGet($offset) { ${networkCall} } + function offsetSet($offset, $value) { ${networkCall} } + function offsetUnset($offset) { ${networkCall} } + } + $x = new Top(); + isset($x['test']); + $a = $x['test']; + $x['test'] = 123; + unset($x['test']); + `); + }); + test('Iterator', () => + assertNoCrash(` + $data = new class() implements IteratorAggregate { + public function getIterator(): Traversable { + ${networkCall}; + return new ArrayIterator( [] ); + } + }; + echo json_encode( [ + ...$data + ] ); + `)); + + test('Countable', () => + assertNoCrash(` + $data = new class() implements Countable { + public function count() { + ${networkCall} + return 0; + } + }; + count($data); + `)); + + test('yield', () => + assertNoCrash(` + function countTo2() { + ${networkCall}; + yield '1'; + ${networkCall}; + yield '2'; + } + foreach(countTo2() as $number) { + echo $number; + } + `)); + }); + + describe('exif extension support', () => { + it('exif_read_data', async () => { + assertNoCrash( + `var_dump(exif_read_data('${httpUrl}/image.jpg'));` + ); + }); + it('exif_imagetype', async () => { + assertNoCrash( + `var_dump(exif_imagetype('${httpUrl}/image.jpg'));` + ); + }); + it('exif_thumbnail', async () => { + assertNoCrash( + `var_dump(exif_thumbnail('${httpUrl}/image.jpg'));` + ); + }); + }); + } + ); + + async function assertNoCrash(code: string) { + try { + const result = await php.run({ + code: ` + candidate.replace('byn$fpcast-emu$', '') + ) + .filter( + (candidate) => + !Dockerfile.includes(`"${candidate}"`) + ); + if (missingCandidates.length) { + addAsyncifyFunctionsToDockerfile(missingCandidates); + throw new Error( + `Asyncify crash! The following missing functions were just auto-added to the ASYNCIFY_ONLY list in the Dockerfile: \n ` + + missingCandidates.join(', ') + + `\nYou now need to rebuild PHP and re-run this test: \n` + + ` npm run recompile:php:node:asyncify:8.0\n` + + ` node --stack-trace-limit=100 ./node_modules/.bin/nx test php-wasm-node --test-name-pattern='asyncify'\n` + ); + } + + const err = new Error( + `Asyncify crash! No C functions present in the stack trace were missing ` + + `from the Dockerfile. This could mean the stack trace is too short – try increasing the stack trace limit ` + + `with --stack-trace-limit=100. If you already did that, fixing this problem will likely take more digging.` + ); + err.cause = e; + throw err; + } + } + } + }); + }); +}); + +let Dockerfile = InitialDockerfile; +const DockerfilePath = path.resolve( + __dirname, + '../../../compile/php/Dockerfile' +); +function addAsyncifyFunctionsToDockerfile(functions: string[]) { + const currentDockerfile = fs.readFileSync(DockerfilePath, 'utf8') + ''; + const lookup = `export ASYNCIFY_ONLY=$'`; + const idx = currentDockerfile.indexOf(lookup) + lookup.length; + const updatedDockerfile = + currentDockerfile.substring(0, idx) + + functions.map((f) => `"${f}",\\\n`).join('') + + currentDockerfile.substring(idx); + fs.writeFileSync(DockerfilePath, updatedDockerfile); + Dockerfile = updatedDockerfile; +} diff --git a/packages/php-wasm/node/src/test/php-fopen.spec.ts b/packages/php-wasm/node/src/test/php-fopen.spec.ts new file mode 100644 index 0000000000..2e71985659 --- /dev/null +++ b/packages/php-wasm/node/src/test/php-fopen.spec.ts @@ -0,0 +1,360 @@ +import http from 'http'; +import https from 'https'; +import fs from 'fs'; +import path from 'path'; +import { + PHP, + SupportedPHPVersions, + setPhpIniEntries, +} from '@php-wasm/universal'; +import { phpVars } from '@php-wasm/util'; +// eslint-disable-next-line @nx/enforce-module-boundaries +import InitialDockerfile from '../../../compile/php/Dockerfile?raw'; +import { loadNodeRuntime } from '../lib'; +import { jspi } from 'wasm-feature-detect'; + +const runtimeMode = (await jspi()) ? 'jspi' : 'asyncify'; + +const requestHandler = ( + req: http.IncomingMessage, + res: http.ServerResponse +) => { + if (req.url === '/image.jpg') { + const image = fs.readFileSync( + path.join(__dirname, 'test-data', 'image.jpg') + ); + res.writeHead(200, { 'Content-Type': 'image/jpeg' }); + res.write(image); + res.end(); + } else { + res.writeHead(200, { 'Content-Type': 'text/plain' }); + res.end('Hello World\n'); + } +}; + +const httpServer = http.createServer(requestHandler); +const selfSignedCert = { + key: fs.readFileSync(path.join(__dirname, 'test-data', 'key.pem')), + cert: fs.readFileSync(path.join(__dirname, 'test-data', 'cert.pem')), +}; +const httpsServer = https.createServer( + { + key: selfSignedCert.key, + cert: selfSignedCert.cert, + }, + requestHandler +); + +[ + { + protocol: 'http', + port: new Promise((resolve) => { + httpServer.listen(0, function () { + resolve((httpServer.address() as any).port); + }); + }), + }, + { + protocol: 'https', + port: new Promise((resolve) => { + httpsServer.listen(0, function () { + resolve((httpsServer.address() as any).port); + }); + }), + }, +].forEach(({ protocol, port }) => { + const host = '127.0.0.1'; + + const httpUrl = `${protocol}://${host}:${port}`; + + describe(`${protocol} protocol – ${runtimeMode}`, () => { + const js = phpVars({ + host, + port, + httpUrl, + }); + + const phpVersions = + 'PHP' in process.env ? [process.env['PHP']] : SupportedPHPVersions; + + const topOfTheStack: Record = { + fopen: ` + $fp = fopen(${js['httpUrl']}, "r"); + fread($fp, 1024); + fclose($fp);`, + }; + + describe.each(phpVersions)(`PHP %s – ${runtimeMode}`, (phpVersion) => { + let php: PHP; + beforeEach(async () => { + php = new PHP(await loadNodeRuntime(phpVersion as any)); + await setPhpIniEntries(php, { allow_url_fopen: 1 }); + }); + + afterEach(async () => { + php.exit(); + }); + + describe.each(Object.keys(topOfTheStack))( + '%s', + (networkCallKey) => { + const networkCall = topOfTheStack[networkCallKey]; + test('Direct call', () => assertNoCrash(networkCall)); + describe('Function calls', () => { + test('Simple call', () => + assertNoCrash(`${networkCall};`)); + test('Simple call', () => + assertNoCrash( + `function top() { ${networkCall} } top();` + )); + test('Via call_user_func', () => + assertNoCrash( + `function top() { ${networkCall} } call_user_func('top'); ` + )); + test('Via call_user_func_array', () => + assertNoCrash( + `function top() { ${networkCall} } call_user_func_array('top', array());` + )); + }); + + describe('Array functions', () => { + test('array_filter', () => + assertNoCrash(` + function top() { ${networkCall} } + array_filter(array('top'), 'top'); + `)); + + test('array_map', () => + assertNoCrash(` + function top() { ${networkCall} } + array_map(array('top'), 'top'); + `)); + + // Network calls in sort() would be silly so let's skip those for now. + }); + + describe('Class method calls', () => { + test('Regular method', () => + assertNoCrash(` + class Top { + function my_method() { ${networkCall} } + } + $x = new Top(); + $x->my_method(); + `)); + test('Via ReflectionMethod->invoke()', () => + assertNoCrash(` + class Top { + function my_method() { ${networkCall} } + } + $reflectionMethod = new ReflectionMethod('Top', 'my_method'); + $reflectionMethod->invoke(new Top()); + `)); + test('Via ReflectionMethod->invokeArgs()', () => + assertNoCrash(` + class Top { + function my_method() { ${networkCall} } + } + $reflectionMethod = new ReflectionMethod('Top', 'my_method'); + $reflectionMethod->invokeArgs(new Top(), array()); + `)); + test('Via call_user_func', () => + assertNoCrash(` + class Top { + function my_method() { ${networkCall} } + } + call_user_func([new Top(), 'my_method']); + `)); + test('Via call_user_func_array', () => + assertNoCrash(` + class Top { + function my_method() { ${networkCall} } + } + call_user_func_array([new Top(), 'my_method'], []); + `)); + test('Constructor', () => + assertNoCrash(` + class Top { + function __construct() { ${networkCall} } + } + new Top(); + `)); + test('Destructor', () => + assertNoCrash(` + class Top { + function __destruct() { ${networkCall} } + } + $x = new Top(); + unset($x); + `)); + test('__call', () => + assertNoCrash(` + class Top { + function __call($method, $args) { ${networkCall} } + } + $x = new Top(); + $x->test(); + `)); + test('__get', () => + assertNoCrash(` + class Top { + function __get($prop) { ${networkCall} } + } + $x = new Top(); + $x->test; + `)); + test('__set', () => + assertNoCrash(` + class Top { + function __set($prop, $value) { ${networkCall} } + } + $x = new Top(); + $x->test = 1; + `)); + test('__isset', () => + assertNoCrash(` + class Top { + function __isset($prop) { ${networkCall} } + } + $x = new Top(); + isset($x->test); + `)); + test('ArrayAccess', () => { + assertNoCrash(` + class Top implements ArrayAccess { + function offsetExists($offset) { ${networkCall} } + function offsetGet($offset) { ${networkCall} } + function offsetSet($offset, $value) { ${networkCall} } + function offsetUnset($offset) { ${networkCall} } + } + $x = new Top(); + isset($x['test']); + $a = $x['test']; + $x['test'] = 123; + unset($x['test']); + `); + }); + test('Iterator', () => + assertNoCrash(` + $data = new class() implements IteratorAggregate { + public function getIterator(): Traversable { + ${networkCall}; + return new ArrayIterator( [] ); + } + }; + echo json_encode( [ + ...$data + ] ); + `)); + + test('Countable', () => + assertNoCrash(` + $data = new class() implements Countable { + public function count() { + ${networkCall} + return 0; + } + }; + count($data); + `)); + + test('yield', () => + assertNoCrash(` + function countTo2() { + ${networkCall}; + yield '1'; + ${networkCall}; + yield '2'; + } + foreach(countTo2() as $number) { + echo $number; + } + `)); + }); + + describe('exif extension support', () => { + it('exif_read_data', async () => { + assertNoCrash( + `var_dump(exif_read_data('${httpUrl}/image.jpg'));` + ); + }); + it('exif_imagetype', async () => { + assertNoCrash( + `var_dump(exif_imagetype('${httpUrl}/image.jpg'));` + ); + }); + it('exif_thumbnail', async () => { + assertNoCrash( + `var_dump(exif_thumbnail('${httpUrl}/image.jpg'));` + ); + }); + }); + } + ); + + async function assertNoCrash(code: string) { + try { + const result = await php.run({ + code: ` + candidate.replace('byn$fpcast-emu$', '') + ) + .filter( + (candidate) => + !Dockerfile.includes(`"${candidate}"`) + ); + if (missingCandidates.length) { + addAsyncifyFunctionsToDockerfile(missingCandidates); + throw new Error( + `Asyncify crash! The following missing functions were just auto-added to the ASYNCIFY_ONLY list in the Dockerfile: \n ` + + missingCandidates.join(', ') + + `\nYou now need to rebuild PHP and re-run this test: \n` + + ` npm run recompile:php:node:asyncify:8.0\n` + + ` node --stack-trace-limit=100 ./node_modules/.bin/nx test php-wasm-node --test-name-pattern='asyncify'\n` + ); + } + + const err = new Error( + `Asyncify crash! No C functions present in the stack trace were missing ` + + `from the Dockerfile. This could mean the stack trace is too short – try increasing the stack trace limit ` + + `with --stack-trace-limit=100. If you already did that, fixing this problem will likely take more digging.` + ); + err.cause = e; + throw err; + } + } + } + }); + }); +}); + +let Dockerfile = InitialDockerfile; +const DockerfilePath = path.resolve( + __dirname, + '../../../compile/php/Dockerfile' +); +function addAsyncifyFunctionsToDockerfile(functions: string[]) { + const currentDockerfile = fs.readFileSync(DockerfilePath, 'utf8') + ''; + const lookup = `export ASYNCIFY_ONLY=$'`; + const idx = currentDockerfile.indexOf(lookup) + lookup.length; + const updatedDockerfile = + currentDockerfile.substring(0, idx) + + functions.map((f) => `"${f}",\\\n`).join('') + + currentDockerfile.substring(idx); + fs.writeFileSync(DockerfilePath, updatedDockerfile); + Dockerfile = updatedDockerfile; +} diff --git a/packages/php-wasm/node/src/test/php-fsockopen.spec.ts b/packages/php-wasm/node/src/test/php-fsockopen.spec.ts new file mode 100644 index 0000000000..d809cfcede --- /dev/null +++ b/packages/php-wasm/node/src/test/php-fsockopen.spec.ts @@ -0,0 +1,362 @@ +import http from 'http'; +import https from 'https'; +import fs from 'fs'; +import path from 'path'; +import { + PHP, + SupportedPHPVersions, + setPhpIniEntries, +} from '@php-wasm/universal'; +import { phpVars } from '@php-wasm/util'; +// eslint-disable-next-line @nx/enforce-module-boundaries +import InitialDockerfile from '../../../compile/php/Dockerfile?raw'; +import { loadNodeRuntime } from '../lib'; +import { jspi } from 'wasm-feature-detect'; + +const runtimeMode = (await jspi()) ? 'jspi' : 'asyncify'; + +const requestHandler = ( + req: http.IncomingMessage, + res: http.ServerResponse +) => { + if (req.url === '/image.jpg') { + const image = fs.readFileSync( + path.join(__dirname, 'test-data', 'image.jpg') + ); + res.writeHead(200, { 'Content-Type': 'image/jpeg' }); + res.write(image); + res.end(); + } else { + res.writeHead(200, { 'Content-Type': 'text/plain' }); + res.end('Hello World\n'); + } +}; + +const httpServer = http.createServer(requestHandler); +const selfSignedCert = { + key: fs.readFileSync(path.join(__dirname, 'test-data', 'key.pem')), + cert: fs.readFileSync(path.join(__dirname, 'test-data', 'cert.pem')), +}; +const httpsServer = https.createServer( + { + key: selfSignedCert.key, + cert: selfSignedCert.cert, + }, + requestHandler +); + +[ + { + protocol: 'http', + port: new Promise((resolve) => { + httpServer.listen(0, function () { + resolve((httpServer.address() as any).port); + }); + }), + }, + { + protocol: 'https', + port: new Promise((resolve) => { + httpsServer.listen(0, function () { + resolve((httpsServer.address() as any).port); + }); + }), + }, +].forEach(({ protocol, port }) => { + const host = '127.0.0.1'; + + const httpUrl = `${protocol}://${host}:${port}`; + + describe(`${protocol} protocol – ${runtimeMode}`, () => { + const js = phpVars({ + host, + port, + httpUrl, + }); + + const phpVersions = + 'PHP' in process.env ? [process.env['PHP']] : SupportedPHPVersions; + + const topOfTheStack: Record = { + // Network functions from https://www.php.net/manual/en/book.network.php + fsockopen: ` + $fp = fsockopen(${js['host']}, ${js['port']}); + fwrite($fp, "GET / HTTP/1.1\\r\\n\\r\\n"); + fread($fp, 10); + fclose($fp);`, + }; + + describe.each(phpVersions)(`PHP %s – ${runtimeMode}`, (phpVersion) => { + let php: PHP; + beforeEach(async () => { + php = new PHP(await loadNodeRuntime(phpVersion as any)); + await setPhpIniEntries(php, { allow_url_fopen: 1 }); + }); + + afterEach(async () => { + php.exit(); + }); + + describe.each(Object.keys(topOfTheStack))( + '%s', + (networkCallKey) => { + const networkCall = topOfTheStack[networkCallKey]; + test('Direct call', () => assertNoCrash(networkCall)); + describe('Function calls', () => { + test('Simple call', () => + assertNoCrash(`${networkCall};`)); + test('Simple call', () => + assertNoCrash( + `function top() { ${networkCall} } top();` + )); + test('Via call_user_func', () => + assertNoCrash( + `function top() { ${networkCall} } call_user_func('top'); ` + )); + test('Via call_user_func_array', () => + assertNoCrash( + `function top() { ${networkCall} } call_user_func_array('top', array());` + )); + }); + + describe('Array functions', () => { + test('array_filter', () => + assertNoCrash(` + function top() { ${networkCall} } + array_filter(array('top'), 'top'); + `)); + + test('array_map', () => + assertNoCrash(` + function top() { ${networkCall} } + array_map(array('top'), 'top'); + `)); + + // Network calls in sort() would be silly so let's skip those for now. + }); + + describe('Class method calls', () => { + test('Regular method', () => + assertNoCrash(` + class Top { + function my_method() { ${networkCall} } + } + $x = new Top(); + $x->my_method(); + `)); + test('Via ReflectionMethod->invoke()', () => + assertNoCrash(` + class Top { + function my_method() { ${networkCall} } + } + $reflectionMethod = new ReflectionMethod('Top', 'my_method'); + $reflectionMethod->invoke(new Top()); + `)); + test('Via ReflectionMethod->invokeArgs()', () => + assertNoCrash(` + class Top { + function my_method() { ${networkCall} } + } + $reflectionMethod = new ReflectionMethod('Top', 'my_method'); + $reflectionMethod->invokeArgs(new Top(), array()); + `)); + test('Via call_user_func', () => + assertNoCrash(` + class Top { + function my_method() { ${networkCall} } + } + call_user_func([new Top(), 'my_method']); + `)); + test('Via call_user_func_array', () => + assertNoCrash(` + class Top { + function my_method() { ${networkCall} } + } + call_user_func_array([new Top(), 'my_method'], []); + `)); + test('Constructor', () => + assertNoCrash(` + class Top { + function __construct() { ${networkCall} } + } + new Top(); + `)); + test('Destructor', () => + assertNoCrash(` + class Top { + function __destruct() { ${networkCall} } + } + $x = new Top(); + unset($x); + `)); + test('__call', () => + assertNoCrash(` + class Top { + function __call($method, $args) { ${networkCall} } + } + $x = new Top(); + $x->test(); + `)); + test('__get', () => + assertNoCrash(` + class Top { + function __get($prop) { ${networkCall} } + } + $x = new Top(); + $x->test; + `)); + test('__set', () => + assertNoCrash(` + class Top { + function __set($prop, $value) { ${networkCall} } + } + $x = new Top(); + $x->test = 1; + `)); + test('__isset', () => + assertNoCrash(` + class Top { + function __isset($prop) { ${networkCall} } + } + $x = new Top(); + isset($x->test); + `)); + test('ArrayAccess', () => { + assertNoCrash(` + class Top implements ArrayAccess { + function offsetExists($offset) { ${networkCall} } + function offsetGet($offset) { ${networkCall} } + function offsetSet($offset, $value) { ${networkCall} } + function offsetUnset($offset) { ${networkCall} } + } + $x = new Top(); + isset($x['test']); + $a = $x['test']; + $x['test'] = 123; + unset($x['test']); + `); + }); + test('Iterator', () => + assertNoCrash(` + $data = new class() implements IteratorAggregate { + public function getIterator(): Traversable { + ${networkCall}; + return new ArrayIterator( [] ); + } + }; + echo json_encode( [ + ...$data + ] ); + `)); + + test('Countable', () => + assertNoCrash(` + $data = new class() implements Countable { + public function count() { + ${networkCall} + return 0; + } + }; + count($data); + `)); + + test('yield', () => + assertNoCrash(` + function countTo2() { + ${networkCall}; + yield '1'; + ${networkCall}; + yield '2'; + } + foreach(countTo2() as $number) { + echo $number; + } + `)); + }); + + describe('exif extension support', () => { + it('exif_read_data', async () => { + assertNoCrash( + `var_dump(exif_read_data('${httpUrl}/image.jpg'));` + ); + }); + it('exif_imagetype', async () => { + assertNoCrash( + `var_dump(exif_imagetype('${httpUrl}/image.jpg'));` + ); + }); + it('exif_thumbnail', async () => { + assertNoCrash( + `var_dump(exif_thumbnail('${httpUrl}/image.jpg'));` + ); + }); + }); + } + ); + + async function assertNoCrash(code: string) { + try { + const result = await php.run({ + code: ` + candidate.replace('byn$fpcast-emu$', '') + ) + .filter( + (candidate) => + !Dockerfile.includes(`"${candidate}"`) + ); + if (missingCandidates.length) { + addAsyncifyFunctionsToDockerfile(missingCandidates); + throw new Error( + `Asyncify crash! The following missing functions were just auto-added to the ASYNCIFY_ONLY list in the Dockerfile: \n ` + + missingCandidates.join(', ') + + `\nYou now need to rebuild PHP and re-run this test: \n` + + ` npm run recompile:php:node:asyncify:8.0\n` + + ` node --stack-trace-limit=100 ./node_modules/.bin/nx test php-wasm-node --test-name-pattern='asyncify'\n` + ); + } + + const err = new Error( + `Asyncify crash! No C functions present in the stack trace were missing ` + + `from the Dockerfile. This could mean the stack trace is too short – try increasing the stack trace limit ` + + `with --stack-trace-limit=100. If you already did that, fixing this problem will likely take more digging.` + ); + err.cause = e; + throw err; + } + } + } + }); + }); +}); + +let Dockerfile = InitialDockerfile; +const DockerfilePath = path.resolve( + __dirname, + '../../../compile/php/Dockerfile' +); +function addAsyncifyFunctionsToDockerfile(functions: string[]) { + const currentDockerfile = fs.readFileSync(DockerfilePath, 'utf8') + ''; + const lookup = `export ASYNCIFY_ONLY=$'`; + const idx = currentDockerfile.indexOf(lookup) + lookup.length; + const updatedDockerfile = + currentDockerfile.substring(0, idx) + + functions.map((f) => `"${f}",\\\n`).join('') + + currentDockerfile.substring(idx); + fs.writeFileSync(DockerfilePath, updatedDockerfile); + Dockerfile = updatedDockerfile; +} diff --git a/packages/php-wasm/node/src/test/php-gethostbyname.spec.ts b/packages/php-wasm/node/src/test/php-gethostbyname.spec.ts new file mode 100644 index 0000000000..7ae8293fa8 --- /dev/null +++ b/packages/php-wasm/node/src/test/php-gethostbyname.spec.ts @@ -0,0 +1,361 @@ +import http from 'http'; +import https from 'https'; +import fs from 'fs'; +import path from 'path'; +import { + PHP, + SupportedPHPVersions, + setPhpIniEntries, +} from '@php-wasm/universal'; +import { phpVars } from '@php-wasm/util'; +// eslint-disable-next-line @nx/enforce-module-boundaries +import InitialDockerfile from '../../../compile/php/Dockerfile?raw'; +import { loadNodeRuntime } from '../lib'; +import { jspi } from 'wasm-feature-detect'; + +const runtimeMode = (await jspi()) ? 'jspi' : 'asyncify'; + +const requestHandler = ( + req: http.IncomingMessage, + res: http.ServerResponse +) => { + if (req.url === '/image.jpg') { + const image = fs.readFileSync( + path.join(__dirname, 'test-data', 'image.jpg') + ); + res.writeHead(200, { 'Content-Type': 'image/jpeg' }); + res.write(image); + res.end(); + } else { + res.writeHead(200, { 'Content-Type': 'text/plain' }); + res.end('Hello World\n'); + } +}; + +const httpServer = http.createServer(requestHandler); +const selfSignedCert = { + key: fs.readFileSync(path.join(__dirname, 'test-data', 'key.pem')), + cert: fs.readFileSync(path.join(__dirname, 'test-data', 'cert.pem')), +}; +const httpsServer = https.createServer( + { + key: selfSignedCert.key, + cert: selfSignedCert.cert, + }, + requestHandler +); + +[ + { + protocol: 'http', + port: new Promise((resolve) => { + httpServer.listen(0, function () { + resolve((httpServer.address() as any).port); + }); + }), + }, + { + protocol: 'https', + port: new Promise((resolve) => { + httpsServer.listen(0, function () { + resolve((httpsServer.address() as any).port); + }); + }), + }, +].forEach(({ protocol, port }) => { + const host = '127.0.0.1'; + + const httpUrl = `${protocol}://${host}:${port}`; + + describe(`${protocol} protocol – ${runtimeMode}`, () => { + const js = phpVars({ + host, + port, + httpUrl, + }); + + const phpVersions = + 'PHP' in process.env ? [process.env['PHP']] : SupportedPHPVersions; + + const topOfTheStack: Record = { + gethostbyname: `gethostbyname(${js['httpUrl']});`, + + // @TODO: + // PDO functions from https://www.php.net/manual/en/book.pdo.php + // Sockets functions from https://www.php.net/manual/en/book.sockets.php + }; + + describe.each(phpVersions)(`PHP %s – ${runtimeMode}`, (phpVersion) => { + let php: PHP; + beforeEach(async () => { + php = new PHP(await loadNodeRuntime(phpVersion as any)); + await setPhpIniEntries(php, { allow_url_fopen: 1 }); + }); + + afterEach(async () => { + php.exit(); + }); + + describe.each(Object.keys(topOfTheStack))( + '%s', + (networkCallKey) => { + const networkCall = topOfTheStack[networkCallKey]; + test('Direct call', () => assertNoCrash(networkCall)); + describe('Function calls', () => { + test('Simple call', () => + assertNoCrash(`${networkCall};`)); + test('Simple call', () => + assertNoCrash( + `function top() { ${networkCall} } top();` + )); + test('Via call_user_func', () => + assertNoCrash( + `function top() { ${networkCall} } call_user_func('top'); ` + )); + test('Via call_user_func_array', () => + assertNoCrash( + `function top() { ${networkCall} } call_user_func_array('top', array());` + )); + }); + + describe('Array functions', () => { + test('array_filter', () => + assertNoCrash(` + function top() { ${networkCall} } + array_filter(array('top'), 'top'); + `)); + + test('array_map', () => + assertNoCrash(` + function top() { ${networkCall} } + array_map(array('top'), 'top'); + `)); + + // Network calls in sort() would be silly so let's skip those for now. + }); + + describe('Class method calls', () => { + test('Regular method', () => + assertNoCrash(` + class Top { + function my_method() { ${networkCall} } + } + $x = new Top(); + $x->my_method(); + `)); + test('Via ReflectionMethod->invoke()', () => + assertNoCrash(` + class Top { + function my_method() { ${networkCall} } + } + $reflectionMethod = new ReflectionMethod('Top', 'my_method'); + $reflectionMethod->invoke(new Top()); + `)); + test('Via ReflectionMethod->invokeArgs()', () => + assertNoCrash(` + class Top { + function my_method() { ${networkCall} } + } + $reflectionMethod = new ReflectionMethod('Top', 'my_method'); + $reflectionMethod->invokeArgs(new Top(), array()); + `)); + test('Via call_user_func', () => + assertNoCrash(` + class Top { + function my_method() { ${networkCall} } + } + call_user_func([new Top(), 'my_method']); + `)); + test('Via call_user_func_array', () => + assertNoCrash(` + class Top { + function my_method() { ${networkCall} } + } + call_user_func_array([new Top(), 'my_method'], []); + `)); + test('Constructor', () => + assertNoCrash(` + class Top { + function __construct() { ${networkCall} } + } + new Top(); + `)); + test('Destructor', () => + assertNoCrash(` + class Top { + function __destruct() { ${networkCall} } + } + $x = new Top(); + unset($x); + `)); + test('__call', () => + assertNoCrash(` + class Top { + function __call($method, $args) { ${networkCall} } + } + $x = new Top(); + $x->test(); + `)); + test('__get', () => + assertNoCrash(` + class Top { + function __get($prop) { ${networkCall} } + } + $x = new Top(); + $x->test; + `)); + test('__set', () => + assertNoCrash(` + class Top { + function __set($prop, $value) { ${networkCall} } + } + $x = new Top(); + $x->test = 1; + `)); + test('__isset', () => + assertNoCrash(` + class Top { + function __isset($prop) { ${networkCall} } + } + $x = new Top(); + isset($x->test); + `)); + test('ArrayAccess', () => { + assertNoCrash(` + class Top implements ArrayAccess { + function offsetExists($offset) { ${networkCall} } + function offsetGet($offset) { ${networkCall} } + function offsetSet($offset, $value) { ${networkCall} } + function offsetUnset($offset) { ${networkCall} } + } + $x = new Top(); + isset($x['test']); + $a = $x['test']; + $x['test'] = 123; + unset($x['test']); + `); + }); + test('Iterator', () => + assertNoCrash(` + $data = new class() implements IteratorAggregate { + public function getIterator(): Traversable { + ${networkCall}; + return new ArrayIterator( [] ); + } + }; + echo json_encode( [ + ...$data + ] ); + `)); + + test('Countable', () => + assertNoCrash(` + $data = new class() implements Countable { + public function count() { + ${networkCall} + return 0; + } + }; + count($data); + `)); + + test('yield', () => + assertNoCrash(` + function countTo2() { + ${networkCall}; + yield '1'; + ${networkCall}; + yield '2'; + } + foreach(countTo2() as $number) { + echo $number; + } + `)); + }); + + describe('exif extension support', () => { + it('exif_read_data', async () => { + assertNoCrash( + `var_dump(exif_read_data('${httpUrl}/image.jpg'));` + ); + }); + it('exif_imagetype', async () => { + assertNoCrash( + `var_dump(exif_imagetype('${httpUrl}/image.jpg'));` + ); + }); + it('exif_thumbnail', async () => { + assertNoCrash( + `var_dump(exif_thumbnail('${httpUrl}/image.jpg'));` + ); + }); + }); + } + ); + + async function assertNoCrash(code: string) { + try { + const result = await php.run({ + code: ` + candidate.replace('byn$fpcast-emu$', '') + ) + .filter( + (candidate) => + !Dockerfile.includes(`"${candidate}"`) + ); + if (missingCandidates.length) { + addAsyncifyFunctionsToDockerfile(missingCandidates); + throw new Error( + `Asyncify crash! The following missing functions were just auto-added to the ASYNCIFY_ONLY list in the Dockerfile: \n ` + + missingCandidates.join(', ') + + `\nYou now need to rebuild PHP and re-run this test: \n` + + ` npm run recompile:php:node:asyncify:8.0\n` + + ` node --stack-trace-limit=100 ./node_modules/.bin/nx test php-wasm-node --test-name-pattern='asyncify'\n` + ); + } + + const err = new Error( + `Asyncify crash! No C functions present in the stack trace were missing ` + + `from the Dockerfile. This could mean the stack trace is too short – try increasing the stack trace limit ` + + `with --stack-trace-limit=100. If you already did that, fixing this problem will likely take more digging.` + ); + err.cause = e; + throw err; + } + } + } + }); + }); +}); + +let Dockerfile = InitialDockerfile; +const DockerfilePath = path.resolve( + __dirname, + '../../../compile/php/Dockerfile' +); +function addAsyncifyFunctionsToDockerfile(functions: string[]) { + const currentDockerfile = fs.readFileSync(DockerfilePath, 'utf8') + ''; + const lookup = `export ASYNCIFY_ONLY=$'`; + const idx = currentDockerfile.indexOf(lookup) + lookup.length; + const updatedDockerfile = + currentDockerfile.substring(0, idx) + + functions.map((f) => `"${f}",\\\n`).join('') + + currentDockerfile.substring(idx); + fs.writeFileSync(DockerfilePath, updatedDockerfile); + Dockerfile = updatedDockerfile; +} diff --git a/packages/php-wasm/node/src/test/php-ini.spec.ts b/packages/php-wasm/node/src/test/php-ini.spec.ts index de03c6238b..251d4f9bca 100644 --- a/packages/php-wasm/node/src/test/php-ini.spec.ts +++ b/packages/php-wasm/node/src/test/php-ini.spec.ts @@ -23,7 +23,7 @@ custom_setting = true }); afterEach(async () => { - php?.[Symbol.dispose]?.(); + php.exit(); }); describe('getPhpIniEntries', () => { diff --git a/packages/php-wasm/node/src/test/php-memory.spec.ts b/packages/php-wasm/node/src/test/php-memory.spec.ts index e436ab7c1b..fd68ffc07e 100644 --- a/packages/php-wasm/node/src/test/php-memory.spec.ts +++ b/packages/php-wasm/node/src/test/php-memory.spec.ts @@ -16,7 +16,7 @@ describe.each(phpVersions)('PHP %s – memory allocation', (phpVersion) => { }); afterEach(async () => { - php?.[Symbol.dispose]?.(); + php.exit(); }); it('can concat large string out of many small strings without reaching Out-of-memory condition', async () => { diff --git a/packages/php-wasm/node/src/test/php-mysqli.spec.ts b/packages/php-wasm/node/src/test/php-mysqli.spec.ts new file mode 100644 index 0000000000..fd8659d5ad --- /dev/null +++ b/packages/php-wasm/node/src/test/php-mysqli.spec.ts @@ -0,0 +1,397 @@ +import http from 'http'; +import https from 'https'; +import fs from 'fs'; +import path from 'path'; +import { + PHP, + SupportedPHPVersions, + setPhpIniEntries, +} from '@php-wasm/universal'; +// eslint-disable-next-line @nx/enforce-module-boundaries +import InitialDockerfile from '../../../compile/php/Dockerfile?raw'; +import { loadNodeRuntime } from '../lib'; +import { jspi } from 'wasm-feature-detect'; + +const runtimeMode = (await jspi()) ? 'jspi' : 'asyncify'; + +const requestHandler = ( + req: http.IncomingMessage, + res: http.ServerResponse +) => { + if (req.url === '/image.jpg') { + const image = fs.readFileSync( + path.join(__dirname, 'test-data', 'image.jpg') + ); + res.writeHead(200, { 'Content-Type': 'image/jpeg' }); + res.write(image); + res.end(); + } else { + res.writeHead(200, { 'Content-Type': 'text/plain' }); + res.end('Hello World\n'); + } +}; + +const httpServer = http.createServer(requestHandler); +const selfSignedCert = { + key: fs.readFileSync(path.join(__dirname, 'test-data', 'key.pem')), + cert: fs.readFileSync(path.join(__dirname, 'test-data', 'cert.pem')), +}; +const httpsServer = https.createServer( + { + key: selfSignedCert.key, + cert: selfSignedCert.cert, + }, + requestHandler +); + +[ + { + protocol: 'http', + port: new Promise((resolve) => { + httpServer.listen(0, function () { + resolve((httpServer.address() as any).port); + }); + }), + }, + { + protocol: 'https', + port: new Promise((resolve) => { + httpsServer.listen(0, function () { + resolve((httpsServer.address() as any).port); + }); + }), + }, +].forEach(({ protocol, port }) => { + const host = '127.0.0.1'; + + const httpUrl = `${protocol}://${host}:${port}`; + + describe(`${protocol} protocol – ${runtimeMode}`, () => { + const phpVersions = + 'PHP' in process.env ? [process.env['PHP']] : SupportedPHPVersions; + + const topOfTheStack: Record = {}; + + // Run MySQL functions if credentials are provided + const mysqlCredentials = { + host: process.env['MYSQL_HOST'] ?? '127.0.0.1', + user: process.env['MYSQL_USER'], + password: process.env['MYSQL_PASSWORD'], + database: process.env['MYSQL_DATABASE'], + port: process.env['MYSQL_PORT'] ?? '3306', + }; + + if ( + mysqlCredentials.host && + mysqlCredentials.user && + mysqlCredentials.password && + mysqlCredentials.database + ) { + topOfTheStack['mysqli'] = ` + $mysqli = new mysqli( + "${mysqlCredentials.host}", + "${mysqlCredentials.user}", + "${mysqlCredentials.password}", + "${mysqlCredentials.database}", + ${mysqlCredentials.port} + ); + if (mysqli_connect_errno()) { + // This should crash the process I hope + klfhjkljfkdjfd(); + } + mysqli_ping($mysqli); + mysqli_query($mysqli, "SELECT 1"); + mysqli_multi_query($mysqli, "SELECT 1; SELECT 2;"); + mysqli_get_server_info($mysqli); + mysqli_get_server_version($mysqli); + mysqli_get_proto_info($mysqli); + mysqli_close($mysqli);`; + } else { + console.log(` + Skipping MySQL network functions because no credentials were provided. + + To run MySQL network function tests, set the following environment variables: + - MYSQL_HOST + - MYSQL_USER + - MYSQL_PASSWORD + - MYSQL_DATABASE + + Use 127.0.0.1 instead of localhost to ensure MySQL uses + TCP instead of socket, because MySQL in Playground + still doesn't support sockets. + `); + } + describe.each(phpVersions)(`PHP %s – ${runtimeMode}`, (phpVersion) => { + let php: PHP; + beforeEach(async () => { + php = new PHP(await loadNodeRuntime(phpVersion as any)); + await setPhpIniEntries(php, { allow_url_fopen: 1 }); + }); + + afterEach(async () => { + php.exit(); + }); + + describe.each(Object.keys(topOfTheStack))( + '%s', + (networkCallKey) => { + const networkCall = topOfTheStack[networkCallKey]; + test('Direct call', () => assertNoCrash(networkCall)); + describe('Function calls', () => { + test('Simple call', () => + assertNoCrash(`${networkCall};`)); + test('Simple call', () => + assertNoCrash( + `function top() { ${networkCall} } top();` + )); + test('Via call_user_func', () => + assertNoCrash( + `function top() { ${networkCall} } call_user_func('top'); ` + )); + test('Via call_user_func_array', () => + assertNoCrash( + `function top() { ${networkCall} } call_user_func_array('top', array());` + )); + }); + + describe('Array functions', () => { + test('array_filter', () => + assertNoCrash(` + function top() { ${networkCall} } + array_filter(array('top'), 'top'); + `)); + + test('array_map', () => + assertNoCrash(` + function top() { ${networkCall} } + array_map(array('top'), 'top'); + `)); + + // Network calls in sort() would be silly so let's skip those for now. + }); + + describe('Class method calls', () => { + test('Regular method', () => + assertNoCrash(` + class Top { + function my_method() { ${networkCall} } + } + $x = new Top(); + $x->my_method(); + `)); + test('Via ReflectionMethod->invoke()', () => + assertNoCrash(` + class Top { + function my_method() { ${networkCall} } + } + $reflectionMethod = new ReflectionMethod('Top', 'my_method'); + $reflectionMethod->invoke(new Top()); + `)); + test('Via ReflectionMethod->invokeArgs()', () => + assertNoCrash(` + class Top { + function my_method() { ${networkCall} } + } + $reflectionMethod = new ReflectionMethod('Top', 'my_method'); + $reflectionMethod->invokeArgs(new Top(), array()); + `)); + test('Via call_user_func', () => + assertNoCrash(` + class Top { + function my_method() { ${networkCall} } + } + call_user_func([new Top(), 'my_method']); + `)); + test('Via call_user_func_array', () => + assertNoCrash(` + class Top { + function my_method() { ${networkCall} } + } + call_user_func_array([new Top(), 'my_method'], []); + `)); + test('Constructor', () => + assertNoCrash(` + class Top { + function __construct() { ${networkCall} } + } + new Top(); + `)); + test('Destructor', () => + assertNoCrash(` + class Top { + function __destruct() { ${networkCall} } + } + $x = new Top(); + unset($x); + `)); + test('__call', () => + assertNoCrash(` + class Top { + function __call($method, $args) { ${networkCall} } + } + $x = new Top(); + $x->test(); + `)); + test('__get', () => + assertNoCrash(` + class Top { + function __get($prop) { ${networkCall} } + } + $x = new Top(); + $x->test; + `)); + test('__set', () => + assertNoCrash(` + class Top { + function __set($prop, $value) { ${networkCall} } + } + $x = new Top(); + $x->test = 1; + `)); + test('__isset', () => + assertNoCrash(` + class Top { + function __isset($prop) { ${networkCall} } + } + $x = new Top(); + isset($x->test); + `)); + test('ArrayAccess', () => { + assertNoCrash(` + class Top implements ArrayAccess { + function offsetExists($offset) { ${networkCall} } + function offsetGet($offset) { ${networkCall} } + function offsetSet($offset, $value) { ${networkCall} } + function offsetUnset($offset) { ${networkCall} } + } + $x = new Top(); + isset($x['test']); + $a = $x['test']; + $x['test'] = 123; + unset($x['test']); + `); + }); + test('Iterator', () => + assertNoCrash(` + $data = new class() implements IteratorAggregate { + public function getIterator(): Traversable { + ${networkCall}; + return new ArrayIterator( [] ); + } + }; + echo json_encode( [ + ...$data + ] ); + `)); + + test('Countable', () => + assertNoCrash(` + $data = new class() implements Countable { + public function count() { + ${networkCall} + return 0; + } + }; + count($data); + `)); + + test('yield', () => + assertNoCrash(` + function countTo2() { + ${networkCall}; + yield '1'; + ${networkCall}; + yield '2'; + } + foreach(countTo2() as $number) { + echo $number; + } + `)); + }); + + describe('exif extension support', () => { + it('exif_read_data', async () => { + assertNoCrash( + `var_dump(exif_read_data('${httpUrl}/image.jpg'));` + ); + }); + it('exif_imagetype', async () => { + assertNoCrash( + `var_dump(exif_imagetype('${httpUrl}/image.jpg'));` + ); + }); + it('exif_thumbnail', async () => { + assertNoCrash( + `var_dump(exif_thumbnail('${httpUrl}/image.jpg'));` + ); + }); + }); + } + ); + + async function assertNoCrash(code: string) { + try { + const result = await php.run({ + code: ` + candidate.replace('byn$fpcast-emu$', '') + ) + .filter( + (candidate) => + !Dockerfile.includes(`"${candidate}"`) + ); + if (missingCandidates.length) { + addAsyncifyFunctionsToDockerfile(missingCandidates); + throw new Error( + `Asyncify crash! The following missing functions were just auto-added to the ASYNCIFY_ONLY list in the Dockerfile: \n ` + + missingCandidates.join(', ') + + `\nYou now need to rebuild PHP and re-run this test: \n` + + ` npm run recompile:php:node:asyncify:8.0\n` + + ` node --stack-trace-limit=100 ./node_modules/.bin/nx test php-wasm-node --test-name-pattern='asyncify'\n` + ); + } + + const err = new Error( + `Asyncify crash! No C functions present in the stack trace were missing ` + + `from the Dockerfile. This could mean the stack trace is too short – try increasing the stack trace limit ` + + `with --stack-trace-limit=100. If you already did that, fixing this problem will likely take more digging.` + ); + err.cause = e; + throw err; + } + } + } + }); + }); +}); + +let Dockerfile = InitialDockerfile; +const DockerfilePath = path.resolve( + __dirname, + '../../../compile/php/Dockerfile' +); +function addAsyncifyFunctionsToDockerfile(functions: string[]) { + const currentDockerfile = fs.readFileSync(DockerfilePath, 'utf8') + ''; + const lookup = `export ASYNCIFY_ONLY=$'`; + const idx = currentDockerfile.indexOf(lookup) + lookup.length; + const updatedDockerfile = + currentDockerfile.substring(0, idx) + + functions.map((f) => `"${f}",\\\n`).join('') + + currentDockerfile.substring(idx); + fs.writeFileSync(DockerfilePath, updatedDockerfile); + Dockerfile = updatedDockerfile; +} diff --git a/packages/php-wasm/node/src/test/php-networking.spec.ts b/packages/php-wasm/node/src/test/php-networking.spec.ts index fdb273541c..2ec4afd1a8 100644 --- a/packages/php-wasm/node/src/test/php-networking.spec.ts +++ b/packages/php-wasm/node/src/test/php-networking.spec.ts @@ -239,21 +239,16 @@ describe.each(SupportedPHPVersions)('PHP %s', (phpVersion) => { } }); - it( - 'should support HTTPS requests', - async () => { - const php = new PHP(await loadNodeRuntime(phpVersion)); - await setPhpIniEntries(php, { - 'openssl.cafile': '/tmp/ca-bundle.crt', - allow_url_fopen: 1, - disable_functions: '', - }); - php.writeFile( - '/tmp/ca-bundle.crt', - rootCertificates.join('\n') - ); - const { text } = await php.run({ - code: ` { + const php = new PHP(await loadNodeRuntime(phpVersion)); + await setPhpIniEntries(php, { + 'openssl.cafile': '/tmp/ca-bundle.crt', + allow_url_fopen: 1, + disable_functions: '', + }); + php.writeFile('/tmp/ca-bundle.crt', rootCertificates.join('\n')); + const { text } = await php.run({ + code: ` { $json = json_decode($result, true); var_dump(array_key_exists('8.3', $json)); `, - }); - expect(text).toContain('bool(true)'); - }, - { timeout: 4000 } - ); + }); + expect(text).toContain('bool(true)'); + }, 8000); - it( - 'should support HTTPS requests when certificate verification is disabled', - async () => { - const php = new PHP(await loadNodeRuntime(phpVersion)); - await setPhpIniEntries(php, { - allow_url_fopen: 1, - disable_functions: '', - }); - const { text } = await php.run({ - code: ` { + const php = new PHP(await loadNodeRuntime(phpVersion)); + await setPhpIniEntries(php, { + allow_url_fopen: 1, + disable_functions: '', + }); + const { text } = await php.run({ + code: ` { $json = json_decode($result, true); var_dump(array_key_exists('8.3', $json)); `, - }); - expect(text).toContain('bool(true)'); - }, - { timeout: 4000 } - ); + }); + expect(text).toContain('bool(true)'); + }, 8000); it('should close server when runtime is exited', async () => { const id = await loadNodeRuntime(phpVersion); diff --git a/packages/php-wasm/node/src/test/php-request-handler-files.spec.ts b/packages/php-wasm/node/src/test/php-request-handler-files.spec.ts deleted file mode 100644 index 8fd6ef45b1..0000000000 --- a/packages/php-wasm/node/src/test/php-request-handler-files.spec.ts +++ /dev/null @@ -1,347 +0,0 @@ -// import { getFileNotFoundActionForWordPress } from '@wp-playground/wordpress'; -import { loadNodeRuntime } from '..'; -import type { FileNotFoundGetActionCallback } from '@php-wasm/universal'; -import { - PHP, - PHPRequestHandler, - SupportedPHPVersions, -} from '@php-wasm/universal'; -import { joinPaths } from '@php-wasm/util'; - -interface ConfigForRequestTests { - phpVersion: (typeof SupportedPHPVersions)[number]; - docRoot: string; - absoluteUrl: string | undefined; -} - -const configsForRequestTests: ConfigForRequestTests[] = - SupportedPHPVersions.map((phpVersion) => { - const documentRoots = [ - '/', - // TODO: Re-enable when we can avoid GH workflow cancelation. - // Disable for now because the GH CI unit test workflow is getting - // auto-canceled when this is enabled - //'/wordpress', - ]; - return documentRoots.map((docRoot) => { - const absoluteUrls = [ - undefined, - // TODO: Re-enable when we can avoid GH workflow cancelation. - // Disable for now because the GH CI unit test workflow is - // getting auto-canceled when this is enabled. - //'http://localhost:4321/nested/playground/', - ]; - return absoluteUrls.map((absoluteUrl) => ({ - phpVersion, - docRoot, - absoluteUrl, - })); - }); - }).flat(2); - -describe.each(configsForRequestTests)( - '[PHP $phpVersion, DocRoot $docRoot, AbsUrl $absoluteUrl] PHPRequestHandler – request', - ({ phpVersion, docRoot, absoluteUrl }) => { - let php: PHP; - let handler: PHPRequestHandler; - let getFileNotFoundActionForTest: FileNotFoundGetActionCallback = - () => ({ - type: '404', - }); - beforeEach(async () => { - handler = new PHPRequestHandler({ - documentRoot: docRoot, - absoluteUrl, - phpFactory: async () => - new PHP(await loadNodeRuntime(phpVersion)), - maxPhpInstances: 1, - getFileNotFoundAction: (relativePath: string) => { - return getFileNotFoundActionForTest(relativePath); - }, - }); - php = await handler.getPrimaryPhp(); - php.mkdir(docRoot); - }); - - afterEach(async () => { - php?.[Symbol.dispose]?.(); - await handler?.[Symbol.asyncDispose]?.(); - }); - - it('should execute a PHP file', async () => { - php.writeFile( - joinPaths(docRoot, 'index.php'), - ` { - php.mkdirTree(joinPaths(docRoot, 'folder')); - php.writeFile( - joinPaths(docRoot, 'folder/some.php'), - ` { - php.writeFile(joinPaths(docRoot, 'index.html'), `Hello World`); - const response = await handler.request({ - url: '/index.html', - }); - expect(response).toEqual({ - httpStatusCode: 200, - headers: { - 'content-type': ['text/html'], - - 'accept-ranges': ['bytes'], - 'cache-control': ['public, max-age=0'], - 'content-length': ['11'], - }, - bytes: new TextEncoder().encode('Hello World'), - errors: '', - exitCode: 0, - }); - }); - - it('should serve a static file with urlencoded entities in the path', async () => { - php.writeFile( - joinPaths(docRoot, 'Screenshot 2024-04-05 at 7.13.36 AM.html'), - `Hello World` - ); - const response = await handler.request({ - url: '/Screenshot 2024-04-05 at 7.13.36%E2%80%AFAM.html', - }); - expect(response).toEqual({ - httpStatusCode: 200, - headers: { - 'content-type': ['text/html'], - - 'accept-ranges': ['bytes'], - 'cache-control': ['public, max-age=0'], - 'content-length': ['11'], - }, - bytes: new TextEncoder().encode('Hello World'), - errors: '', - exitCode: 0, - }); - }); - - it('should serve a PHP file with urlencoded entities in the path', async () => { - php.writeFile( - joinPaths(docRoot, 'Screenshot 2024-04-05 at 7.13.36 AM.php'), - `Hello World` - ); - const response = await handler.request({ - url: '/Screenshot 2024-04-05 at 7.13.36%E2%80%AFAM.php', - }); - expect(response).toEqual({ - httpStatusCode: 200, - headers: { - 'content-type': ['text/html; charset=UTF-8'], - 'x-powered-by': [expect.any(String)], - }, - bytes: new TextEncoder().encode('Hello World'), - errors: '', - exitCode: 0, - }); - }); - - const fileNotFoundFallbackTestUris = [ - '/index.php', - '/other.php', - '/index.html', - '/testing.html', - '/', - '/subdir', - ]; - fileNotFoundFallbackTestUris.forEach((nonExistentFileUri) => { - it(`should support internal redirection to a PHP file as a fallback for non-existent file: '${nonExistentFileUri}'`, async () => { - const primaryPhp = await handler.getPrimaryPhp(); - const scriptPath = joinPaths(docRoot, 'fallback.php'); - primaryPhp.writeFile( - scriptPath, - ` { - if (uri === nonExistentFileUri) { - return { - type: 'internal-redirect', - uri: '/fallback.php', - }; - } else { - return { type: '404' }; - } - }; - const response = await handler.request({ - url: nonExistentFileUri, - }); - - const expectedRequestUri = - absoluteUrl === undefined - ? nonExistentFileUri - : joinPaths( - new URL(absoluteUrl as string).pathname, - nonExistentFileUri - ); - expect(response).toEqual({ - httpStatusCode: 200, - headers: expect.any(Object), - bytes: new TextEncoder().encode( - 'expected fallback to PHP content:' + - `${expectedRequestUri}:` + - `${scriptPath}` - ), - errors: '', - exitCode: 0, - }); - }); - it(`should support internal redirection to a static file as a fallback for non-existent file: '${nonExistentFileUri}'`, async () => { - const primaryPhp = await handler.getPrimaryPhp(); - primaryPhp.writeFile( - joinPaths(docRoot, 'fallback.txt'), - 'expected fallback to static content' - ); - - getFileNotFoundActionForTest = (uri: string) => { - if (uri === nonExistentFileUri) { - return { - type: 'internal-redirect', - uri: '/fallback.txt', - }; - } else { - return { type: '404' }; - } - }; - const response = await handler.request({ - url: nonExistentFileUri, - }); - expect(response).toEqual({ - httpStatusCode: 200, - headers: expect.any(Object), - bytes: new TextEncoder().encode( - 'expected fallback to static content' - ), - errors: '', - exitCode: 0, - }); - }); - }); - - it('should redirect to add trailing slash to existing dir', async () => { - php.mkdirTree(joinPaths(docRoot, 'folder')); - const response = await handler.request({ - url: '/folder', - }); - expect(response).toEqual({ - httpStatusCode: 301, - headers: { - Location: ['/folder/'], - }, - bytes: expect.any(Uint8Array), - errors: '', - exitCode: 0, - }); - }); - - it('should serve a symlinked file', async () => { - php.writeFile( - joinPaths(docRoot, 'target.php'), - ` { - php.mkdir(joinPaths(docRoot, 'target')); - php.writeFile( - joinPaths(docRoot, 'target', 'index.php'), - ` { - php.mkdir(joinPaths(docRoot, 'target')); - php.writeFile( - joinPaths(docRoot, 'target', 'index.php'), - ` { - php.writeFile( - joinPaths(docRoot, 'target.php'), - ` { - const documentRoots = [ - '/', - // TODO: Re-enable when we can avoid GH workflow cancelation. - // Disable for now because the GH CI unit test workflow is getting - // auto-canceled when this is enabled - //'/wordpress', - ]; - return documentRoots.map((docRoot) => { - const absoluteUrls = [ - undefined, - // TODO: Re-enable when we can avoid GH workflow cancelation. - // Disable for now because the GH CI unit test workflow is - // getting auto-canceled when this is enabled. - //'http://localhost:4321/nested/playground/', - ]; - return absoluteUrls.map((absoluteUrl) => ({ - phpVersion, - docRoot, - absoluteUrl, - })); - }); - }).flat(2); - -describe.each(configsForRequestTests)( - '[PHP $phpVersion, DocRoot $docRoot, AbsUrl $absoluteUrl] PHPRequestHandler – request', - ({ phpVersion, docRoot, absoluteUrl }) => { - let php: PHP; - let handler: PHPRequestHandler; - let getFileNotFoundActionForTest: FileNotFoundGetActionCallback = - () => ({ - type: '404', - }); - beforeEach(async () => { - handler = new PHPRequestHandler({ - documentRoot: docRoot, - absoluteUrl, - phpFactory: async () => - new PHP(await loadNodeRuntime(phpVersion)), - maxPhpInstances: 1, - getFileNotFoundAction: (relativePath: string) => { - return getFileNotFoundActionForTest(relativePath); - }, - }); - php = await handler.getPrimaryPhp(); - php.mkdir(docRoot); - }); - - afterEach(async () => { - php?.[Symbol.dispose]?.(); - await handler?.[Symbol.asyncDispose]?.(); - }); - - const fileNotFoundFallbackTestUris = [ - '/index.php', - '/other.php', - '/index.html', - '/testing.html', - '/', - '/subdir', - ]; - fileNotFoundFallbackTestUris.forEach((nonExistentFileUri) => { - it(`should relay a fallback response for non-existent file: '${nonExistentFileUri}'`, async () => { - getFileNotFoundActionForTest = (uri: string) => { - if (uri === nonExistentFileUri) { - return { - type: 'response', - response: new PHPResponse( - 404, - { 'x-backfill-from': ['remote-host'] }, - new TextEncoder().encode('404 File not found') - ), - }; - } else { - return { type: '404' }; - } - }; - const response = await handler.request({ - url: nonExistentFileUri, - }); - expect(response).toEqual({ - httpStatusCode: 404, - headers: { - 'x-backfill-from': ['remote-host'], - }, - bytes: expect.any(Uint8Array), - errors: '', - exitCode: 0, - }); - }); - it(`should support responding with a plain 404 for non-existent file: '${nonExistentFileUri}'`, async () => { - getFileNotFoundActionForTest = () => ({ type: '404' }); - const response = await handler.request({ - url: nonExistentFileUri, - }); - expect(response).toEqual({ - httpStatusCode: 404, - headers: expect.any(Object), - bytes: expect.any(Uint8Array), - errors: '', - exitCode: 0, - }); - }); - }); - - it('should default a folder request to index.php when when both index.php and index.html exist', async () => { - php.mkdirTree(joinPaths(docRoot, 'folder')); - php.writeFile( - joinPaths(docRoot, 'folder/index.php'), - `INDEX DOT PHP` - ); - php.writeFile( - joinPaths(docRoot, 'folder/index.html'), - `INDEX DOT HTML` - ); - const response = await handler.request({ - url: '/folder/?key=value', - }); - expect(response.httpStatusCode).toEqual(200); - expect(response.text).toEqual('INDEX DOT PHP'); - }); - - it('should return httpStatus 500 if exit code is not 0', async () => { - php.writeFile( - joinPaths(docRoot, 'index.php'), - ` { - /** - * Tests against calling phpwasm_init_uploaded_files_hash() when - * the Content-type header is set to multipart/form-data. See the - * phpwasm_init_uploaded_files_hash() docstring for more info. - */ - php.writeFile( - joinPaths(docRoot, 'index.php'), - ` { - /** - * Tests against calling phpwasm_init_uploaded_files_hash() when - * the Content-type header is set to multipart/form-data. See the - * phpwasm_init_uploaded_files_hash() docstring for more info. - */ - php.writeFile( - joinPaths(docRoot, 'index.php'), - ` { - php.writeFile( - joinPaths(docRoot, 'index.php'), - ` { - php.writeFile( - joinPaths(docRoot, 'index.php'), - ` file_exists('/tmp/moved.txt')) - ));` - ); - const response = await handler.request({ - url: '/index.php', - method: 'POST', - body: { - key: 'value', - myFile: new File(['bar'], 'bar.txt'), - }, - }); - expect(response.text).toEqual( - JSON.stringify({ key: 'value', file_exists: true }) - ); - }); - - it('Should handle an empty file object and post data', async () => { - await php.writeFile( - joinPaths(docRoot, 'index.php'), - ` { - php.writeFile( - joinPaths(docRoot, 'test.php'), - ` { - beforeEach(() => { - getFileNotFoundActionForTest = - getFileNotFoundActionForWordPress; - }); - - it('should delegate request for non-existent PHP file to /index.php with query args', async () => { - php.writeFile( - joinPaths(docRoot, 'index.php'), - ` { - php.writeFile( - joinPaths(docRoot, 'index.php'), - ` { - php.writeFile( - joinPaths(docRoot, 'index.php'), - ` { + const documentRoots = [ + '/', + // TODO: Re-enable when we can avoid GH workflow cancelation. + // Disable for now because the GH CI unit test workflow is getting + // auto-canceled when this is enabled + //'/wordpress', + ]; + return documentRoots.map((docRoot) => { + const absoluteUrls = [ + undefined, + // TODO: Re-enable when we can avoid GH workflow cancelation. + // Disable for now because the GH CI unit test workflow is + // getting auto-canceled when this is enabled. + //'http://localhost:4321/nested/playground/', + ]; + return absoluteUrls.map((absoluteUrl) => ({ + phpVersion, + docRoot, + absoluteUrl, + })); + }); + }).flat(2); + +describe.each(configsForRequestTests)( + '[PHP $phpVersion, DocRoot $docRoot, AbsUrl $absoluteUrl] PHPRequestHandler – request', + ({ phpVersion, docRoot, absoluteUrl }) => { + let php: PHP; + let handler: PHPRequestHandler; + let getFileNotFoundActionForTest: FileNotFoundGetActionCallback = + () => ({ + type: '404', + }); + beforeEach(async () => { + handler = new PHPRequestHandler({ + documentRoot: docRoot, + absoluteUrl, + phpFactory: async () => + new PHP(await loadNodeRuntime(phpVersion)), + maxPhpInstances: 1, + getFileNotFoundAction: (relativePath: string) => { + return getFileNotFoundActionForTest(relativePath); + }, + }); + php = await handler.getPrimaryPhp(); + php.mkdir(docRoot); + }); + + afterEach(async () => { + php.exit(); + }); + + it('should execute a PHP file', async () => { + php.writeFile( + joinPaths(docRoot, 'index.php'), + ` { + php.mkdirTree(joinPaths(docRoot, 'folder')); + php.writeFile( + joinPaths(docRoot, 'folder/some.php'), + ` { + php.writeFile(joinPaths(docRoot, 'index.html'), `Hello World`); + const response = await handler.request({ + url: '/index.html', + }); + expect(response).toEqual({ + httpStatusCode: 200, + headers: { + 'content-type': ['text/html'], + + 'accept-ranges': ['bytes'], + 'cache-control': ['public, max-age=0'], + 'content-length': ['11'], + }, + bytes: new TextEncoder().encode('Hello World'), + errors: '', + exitCode: 0, + }); + }); + + it('should serve a static file with urlencoded entities in the path', async () => { + php.writeFile( + joinPaths(docRoot, 'Screenshot 2024-04-05 at 7.13.36 AM.html'), + `Hello World` + ); + const response = await handler.request({ + url: '/Screenshot 2024-04-05 at 7.13.36%E2%80%AFAM.html', + }); + expect(response).toEqual({ + httpStatusCode: 200, + headers: { + 'content-type': ['text/html'], + + 'accept-ranges': ['bytes'], + 'cache-control': ['public, max-age=0'], + 'content-length': ['11'], + }, + bytes: new TextEncoder().encode('Hello World'), + errors: '', + exitCode: 0, + }); + }); + + it('should serve a PHP file with urlencoded entities in the path', async () => { + php.writeFile( + joinPaths(docRoot, 'Screenshot 2024-04-05 at 7.13.36 AM.php'), + `Hello World` + ); + const response = await handler.request({ + url: '/Screenshot 2024-04-05 at 7.13.36%E2%80%AFAM.php', + }); + expect(response).toEqual({ + httpStatusCode: 200, + headers: { + 'content-type': ['text/html; charset=UTF-8'], + 'x-powered-by': [expect.any(String)], + }, + bytes: new TextEncoder().encode('Hello World'), + errors: '', + exitCode: 0, + }); + }); + + const fileNotFoundFallbackTestUris = [ + '/index.php', + '/other.php', + '/index.html', + '/testing.html', + '/', + '/subdir', + ]; + fileNotFoundFallbackTestUris.forEach((nonExistentFileUri) => { + it(`should relay a fallback response for non-existent file: '${nonExistentFileUri}'`, async () => { + getFileNotFoundActionForTest = (uri: string) => { + if (uri === nonExistentFileUri) { + return { + type: 'response', + response: new PHPResponse( + 404, + { 'x-backfill-from': ['remote-host'] }, + new TextEncoder().encode('404 File not found') + ), + }; + } else { + return { type: '404' }; + } + }; + const response = await handler.request({ + url: nonExistentFileUri, + }); + expect(response).toEqual({ + httpStatusCode: 404, + headers: { + 'x-backfill-from': ['remote-host'], + }, + bytes: expect.any(Uint8Array), + errors: '', + exitCode: 0, + }); + }); + it(`should support internal redirection to a PHP file as a fallback for non-existent file: '${nonExistentFileUri}'`, async () => { + const primaryPhp = await handler.getPrimaryPhp(); + const scriptPath = joinPaths(docRoot, 'fallback.php'); + primaryPhp.writeFile( + scriptPath, + ` { + if (uri === nonExistentFileUri) { + return { + type: 'internal-redirect', + uri: '/fallback.php', + }; + } else { + return { type: '404' }; + } + }; + const response = await handler.request({ + url: nonExistentFileUri, + }); + + const expectedRequestUri = + absoluteUrl === undefined + ? nonExistentFileUri + : joinPaths( + new URL(absoluteUrl as string).pathname, + nonExistentFileUri + ); + expect(response).toEqual({ + httpStatusCode: 200, + headers: expect.any(Object), + bytes: new TextEncoder().encode( + 'expected fallback to PHP content:' + + `${expectedRequestUri}:` + + `${scriptPath}` + ), + errors: '', + exitCode: 0, + }); + }); + it(`should support internal redirection to a static file as a fallback for non-existent file: '${nonExistentFileUri}'`, async () => { + const primaryPhp = await handler.getPrimaryPhp(); + primaryPhp.writeFile( + joinPaths(docRoot, 'fallback.txt'), + 'expected fallback to static content' + ); + + getFileNotFoundActionForTest = (uri: string) => { + if (uri === nonExistentFileUri) { + return { + type: 'internal-redirect', + uri: '/fallback.txt', + }; + } else { + return { type: '404' }; + } + }; + const response = await handler.request({ + url: nonExistentFileUri, + }); + expect(response).toEqual({ + httpStatusCode: 200, + headers: expect.any(Object), + bytes: new TextEncoder().encode( + 'expected fallback to static content' + ), + errors: '', + exitCode: 0, + }); + }); + it(`should support responding with a plain 404 for non-existent file: '${nonExistentFileUri}'`, async () => { + getFileNotFoundActionForTest = () => ({ type: '404' }); + const response = await handler.request({ + url: nonExistentFileUri, + }); + expect(response).toEqual({ + httpStatusCode: 404, + headers: expect.any(Object), + bytes: expect.any(Uint8Array), + errors: '', + exitCode: 0, + }); + }); + }); + + it('should redirect to add trailing slash to existing dir', async () => { + php.mkdirTree(joinPaths(docRoot, 'folder')); + const response = await handler.request({ + url: '/folder', + }); + expect(response).toEqual({ + httpStatusCode: 301, + headers: { + Location: ['/folder/'], + }, + bytes: expect.any(Uint8Array), + errors: '', + exitCode: 0, + }); + }); + + it('should return 200 and pass query strings when a valid request is made to a folder', async () => { + php.mkdirTree(joinPaths(docRoot, 'folder')); + php.writeFile( + joinPaths(docRoot, 'folder/index.php'), + ` { + php.mkdirTree(joinPaths(docRoot, 'folder')); + php.writeFile( + joinPaths(docRoot, 'folder/index.html'), + `INDEX DOT HTML` + ); + const response = await handler.request({ + url: '/folder/?key=value', + }); + expect(response.httpStatusCode).toEqual(200); + expect(response.text).toEqual('INDEX DOT HTML'); + }); + + it('should default a folder request to index.php when when both index.php and index.html exist', async () => { + php.mkdirTree(joinPaths(docRoot, 'folder')); + php.writeFile( + joinPaths(docRoot, 'folder/index.php'), + `INDEX DOT PHP` + ); + php.writeFile( + joinPaths(docRoot, 'folder/index.html'), + `INDEX DOT HTML` + ); + const response = await handler.request({ + url: '/folder/?key=value', + }); + expect(response.httpStatusCode).toEqual(200); + expect(response.text).toEqual('INDEX DOT PHP'); + }); + + it('should return httpStatus 500 if exit code is not 0', async () => { + php.writeFile( + joinPaths(docRoot, 'index.php'), + ` { + /** + * Tests against calling phpwasm_init_uploaded_files_hash() when + * the Content-type header is set to multipart/form-data. See the + * phpwasm_init_uploaded_files_hash() docstring for more info. + */ + php.writeFile( + joinPaths(docRoot, 'index.php'), + ` { + /** + * Tests against calling phpwasm_init_uploaded_files_hash() when + * the Content-type header is set to multipart/form-data. See the + * phpwasm_init_uploaded_files_hash() docstring for more info. + */ + php.writeFile( + joinPaths(docRoot, 'index.php'), + ` { + php.writeFile( + joinPaths(docRoot, 'index.php'), + ` { + php.writeFile( + joinPaths(docRoot, 'index.php'), + ` file_exists('/tmp/moved.txt')) + ));` + ); + const response = await handler.request({ + url: '/index.php', + method: 'POST', + body: { + key: 'value', + myFile: new File(['bar'], 'bar.txt'), + }, + }); + expect(response.text).toEqual( + JSON.stringify({ key: 'value', file_exists: true }) + ); + }); + + it('Should handle an empty file object and post data', async () => { + await php.writeFile( + joinPaths(docRoot, 'index.php'), + ` { + php.writeFile( + joinPaths(docRoot, 'test.php'), + ` { + php.writeFile( + joinPaths(docRoot, 'target.php'), + ` { + php.mkdir(joinPaths(docRoot, 'target')); + php.writeFile( + joinPaths(docRoot, 'target', 'index.php'), + ` { + php.mkdir(joinPaths(docRoot, 'target')); + php.writeFile( + joinPaths(docRoot, 'target', 'index.php'), + ` { + php.writeFile( + joinPaths(docRoot, 'target.php'), + ` { + beforeEach(() => { + getFileNotFoundActionForTest = + getFileNotFoundActionForWordPress; + }); + + it('should delegate request for non-existent PHP file to /index.php with query args', async () => { + php.writeFile( + joinPaths(docRoot, 'index.php'), + ` { + php.writeFile( + joinPaths(docRoot, 'index.php'), + ` { + php.writeFile( + joinPaths(docRoot, 'index.php'), + ` { - await handler?.[Symbol.asyncDispose]?.(); + (await handler.getPrimaryPhp()).exit(); }); it.each([ @@ -62,9 +725,11 @@ describe.each(SupportedPHPVersions)( it('should assign the correct cwd', async () => { const php = await handler.getPrimaryPhp(); php.writeFile('/var/www/index.php', ` { scriptPath: args[1], env: options.env, }); + // @ts-ignore processApi.stdout(result.bytes); processApi.stderr(result.errors); processApi.exit(result.exitCode); @@ -121,6 +787,7 @@ describe('PHPRequestHandler – Loopback call', () => { const result = await handler.request({ url: '/second.php', }); + // @ts-ignore processApi.stdout(result.bytes); processApi.stderr(result.errors); processApi.exit(result.exitCode); @@ -144,3 +811,98 @@ describe('PHPRequestHandler – Loopback call', () => { expect(response.text).toEqual('Starting: Ran second.php! Done'); }); }); + +describe('PHPRequestHandler – Cookie store', () => { + const prepareHandler = async (cookieStore?: CookieStore | false) => { + const handler = new PHPRequestHandler({ + documentRoot: '/', + phpFactory: async () => + new PHP(await loadNodeRuntime(RecommendedPHPVersion)), + maxPhpInstances: 1, + cookieStore, + }); + const php = await handler.getPrimaryPhp(); + php.writeFile( + '/set-cookie.php', + ` { + const handler = await prepareHandler(); + + // Cookies return in the response + let response = await handler.request({ + url: '/set-cookie.php', + }); + const cookies = response.headers['set-cookie']; + expect(cookies).toHaveLength(1); + expect(cookies[0]).toMatch( + /my-cookie=where-is-my-cookie; expires=.*; Max-Age=3600; path=\// + ); + + // Cookies are persisted internally in the request handler. + // Note that we are not passing cookies in the header of the response. + response = await handler.request({ + url: '/get-cookie.php', + }); + expect(response.text).toEqual( + JSON.stringify({ 'my-cookie': 'where-is-my-cookie' }) + ); + }); + + it('should persist cookies internally with the HttpCookieStore', async () => { + const handler = await prepareHandler(new HttpCookieStore()); + + // Cookies return in the response + let response = await handler.request({ + url: '/set-cookie.php', + }); + const cookies = response.headers['set-cookie']; + expect(cookies).toHaveLength(1); + expect(cookies[0]).toMatch( + /my-cookie=where-is-my-cookie; expires=.*; Max-Age=3600; path=\// + ); + + // Cookies are persisted internally in the request handler. + // Note that we are not passing cookies in the header of the response. + response = await handler.request({ + url: '/get-cookie.php', + }); + expect(response.text).toEqual( + JSON.stringify({ 'my-cookie': 'where-is-my-cookie' }) + ); + }); + + it('should not persist cookies internally when the cookie store is false', async () => { + const handler = await prepareHandler(false); + + // Cookies return in the response + let response = await handler.request({ + url: '/set-cookie.php', + }); + const cookies = response.headers['set-cookie']; + expect(cookies).toHaveLength(1); + expect(cookies[0]).toMatch( + /my-cookie=where-is-my-cookie; expires=.*; Max-Age=3600; path=\// + ); + + // No cookies are persisted internally. + // Note that we are not passing cookies in the header of the response. + response = await handler.request({ + url: '/get-cookie.php', + }); + expect(response.text).toEqual(JSON.stringify([])); + + // Cookies are available in the PHP environment when passed in the + // request. + response = await handler.request({ + url: '/get-cookie.php', + headers: { Cookie: 'my-cookie=where-is-my-cookie' }, + }); + expect(response.text).toEqual( + JSON.stringify({ 'my-cookie': 'where-is-my-cookie' }) + ); + }); +}); diff --git a/packages/php-wasm/node/src/test/php-asyncify-sqlite3.spec.ts b/packages/php-wasm/node/src/test/php-sqlite3.spec.ts similarity index 97% rename from packages/php-wasm/node/src/test/php-asyncify-sqlite3.spec.ts rename to packages/php-wasm/node/src/test/php-sqlite3.spec.ts index 9b7a850ba4..9ac47ae8de 100644 --- a/packages/php-wasm/node/src/test/php-asyncify-sqlite3.spec.ts +++ b/packages/php-wasm/node/src/test/php-sqlite3.spec.ts @@ -8,9 +8,11 @@ import { // eslint-disable-next-line @nx/enforce-module-boundaries import InitialDockerfile from '../../../compile/php/Dockerfile?raw'; import { loadNodeRuntime } from '../lib'; +import { jspi } from 'wasm-feature-detect'; -// TODO: Re-enable this after troubleshooting all the GH Actions unexpected terminations. -describe.skip(`SQLite3 – asyncify`, () => { +const runtimeMode = (await jspi()) ? 'jspi' : 'asyncify'; + +describe(`SQLite3 – ${runtimeMode}`, () => { const phpVersions = 'PHP' in process.env ? [process.env['PHP']] : SupportedPHPVersions; @@ -267,7 +269,7 @@ describe.skip(`SQLite3 – asyncify`, () => { $drivers = PDO::getAvailableDrivers(); `; - describe.each(phpVersions)('PHP %s – asyncify', (phpVersion) => { + describe.each(phpVersions)(`PHP %s – ${runtimeMode}`, (phpVersion) => { let php: PHP; beforeEach(async () => { php = new PHP(await loadNodeRuntime(phpVersion as any)); @@ -275,7 +277,7 @@ describe.skip(`SQLite3 – asyncify`, () => { }); afterEach(async () => { - php?.[Symbol.dispose]?.(); + php.exit(); }); describe.each(Object.keys(topOfTheStack))('%s', (networkCallKey) => { @@ -296,6 +298,7 @@ describe.skip(`SQLite3 – asyncify`, () => { if ( 'FIX_DOCKERFILE' in process.env && process.env['FIX_DOCKERFILE'] === 'true' && + runtimeMode == 'asyncify' && 'functionsMaybeMissingFromAsyncify' in php ) { const missingCandidates = ( diff --git a/packages/php-wasm/node/src/test/php-vars.spec.ts b/packages/php-wasm/node/src/test/php-vars.spec.ts index afea10f698..f4b8e951b9 100644 --- a/packages/php-wasm/node/src/test/php-vars.spec.ts +++ b/packages/php-wasm/node/src/test/php-vars.spec.ts @@ -16,7 +16,7 @@ describe('phpVar', () => { }); afterEach(async () => { - php?.[Symbol.dispose]?.(); + php.exit(); }); const data = [ diff --git a/packages/php-wasm/node/src/test/php.spec.ts b/packages/php-wasm/node/src/test/php.spec.ts index c7fe4dcc7b..94a54d02ab 100644 --- a/packages/php-wasm/node/src/test/php.spec.ts +++ b/packages/php-wasm/node/src/test/php.spec.ts @@ -88,12 +88,7 @@ describe.each(SupportedPHPVersions)('PHP %s', (phpVersion) => { await setPhpIniEntries(php, { disable_functions: '' }); }); afterEach(async () => { - // Clean up - try { - php.exit(0); - } catch { - // ignore exit-related exceptions - } + php.exit(); }); describe('php.runStream()', () => { @@ -1853,6 +1848,7 @@ describe.each(SupportedPHPVersions)('PHP %s', (phpVersion) => { it('Should have access to raw request data via the php://input stream', async () => { const response = await php.run({ + headers: { 'Content-Type': 'application/json' }, method: 'POST', body: new TextEncoder().encode('{"foo": "bar"}'), code: ` { } }); afterEach(async () => { - // Clean up - try { - if (fs.existsSync(symlinkPath)) { - fs.unlinkSync(symlinkPath); - } - php.exit(0); - } catch { - // ignore exit-related exceptions + if (fs.existsSync(symlinkPath)) { + fs.unlinkSync(symlinkPath); } + php.exit(); }); describe('Test symlinks', () => { diff --git a/packages/php-wasm/node/src/test/write-files.spec.ts b/packages/php-wasm/node/src/test/write-files.spec.ts index a7091e7c02..3f3f2c8466 100644 --- a/packages/php-wasm/node/src/test/write-files.spec.ts +++ b/packages/php-wasm/node/src/test/write-files.spec.ts @@ -13,7 +13,7 @@ describe('writeFiles', () => { }); afterEach(async () => { - php?.[Symbol.dispose]?.(); + php.exit(); }); it('removes the previous directory contents', async () => { diff --git a/packages/php-wasm/node/vite.config.ts b/packages/php-wasm/node/vite.config.ts index f7ece71e12..0a57056821 100644 --- a/packages/php-wasm/node/vite.config.ts +++ b/packages/php-wasm/node/vite.config.ts @@ -96,7 +96,6 @@ export default defineConfig(function () { define: { TEST: JSON.stringify(true), - 'process.env.PROTOCOL': JSON.stringify('http'), }, }; }); diff --git a/packages/php-wasm/node/vite.https.config.ts b/packages/php-wasm/node/vite.https.config.ts deleted file mode 100644 index 2f7c471a27..0000000000 --- a/packages/php-wasm/node/vite.https.config.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { defineConfig, mergeConfig } from 'vite'; -import config from './vite.config'; - -export default defineConfig((env) => - mergeConfig( - config(env), - defineConfig({ - define: { - 'process.env.PROTOCOL': JSON.stringify('https'), - }, - }) - ) -);