From 216fcee8f68d86da605797c731b68e4b23a68cfb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Zieli=C5=84ski?= Date: Sat, 22 Nov 2025 00:40:35 +0100 Subject: [PATCH 1/3] [CI] Rebalance unit test groups --- .github/workflows/ci.yml | 54 +++---- packages/php-wasm/node/project.json | 212 ++++++++++------------------ 2 files changed, 95 insertions(+), 171 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3ab62fbb26..accd3812fe 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -31,24 +31,18 @@ jobs: fail-fast: false matrix: include: - - name: test-unit-asyncify (1/9) + - name: test-unit-asyncify (1/6) target: test - - name: test-unit-asyncify (2/9) - target: test-asyncify - - name: test-unit-asyncify (3/9) - target: test-php-file-get-contents-asyncify - - name: test-unit-asyncify (4/9) - target: test-php-fopen-asyncify - - name: test-unit-asyncify (5/9) - target: test-php-fsockopen-asyncify - - name: test-unit-asyncify (6/9) - target: test-php-gethostbyname-asyncify - - name: test-unit-asyncify (7/9) - target: test-php-mysqli-asyncify - - name: test-unit-asyncify (8/9) - target: test-php-sqlite3-asyncify - - name: test-unit-asyncify (9/9) - target: test-php-file-locking-asyncify + - name: test-unit-asyncify (2/6) + target: test-group-1-asyncify + - name: test-unit-asyncify (3/6) + target: test-group-2-asyncify + - name: test-unit-asyncify (4/6) + target: test-group-3-asyncify + - name: test-unit-asyncify (5/6) + target: test-group-4-asyncify + - name: test-unit-asyncify (6/6) + target: test-group-5-asyncify name: ${{ matrix.name }} services: mysql: @@ -83,22 +77,16 @@ jobs: fail-fast: false matrix: include: - - name: test-unit-jspi (1/8) - target: test-jspi - - name: test-unit-jspi (2/8) - target: test-php-file-get-contents-jspi - - name: test-unit-jspi (3/8) - target: test-php-fopen-jspi - - name: test-unit-jspi (4/8) - target: test-php-fsockopen-jspi - - name: test-unit-jspi (5/8) - target: test-php-gethostbyname-jspi - - name: test-unit-jspi (6/8) - target: test-php-mysqli-jspi - - name: test-unit-jspi (7/8) - target: test-php-sqlite3-jspi - - name: test-unit-jspi (8/8) - target: test-php-file-locking-jspi + - name: test-unit-jspi (1/5) + target: test-group-1-jspi + - name: test-unit-jspi (2/5) + target: test-group-2-jspi + - name: test-unit-jspi (3/5) + target: test-group-3-jspi + - name: test-unit-jspi (4/5) + target: test-group-4-jspi + - name: test-unit-jspi (5/5) + target: test-group-5-jspi name: ${{ matrix.name }} services: mysql: diff --git a/packages/php-wasm/node/project.json b/packages/php-wasm/node/project.json index e570139259..e0ba27d5dc 100644 --- a/packages/php-wasm/node/project.json +++ b/packages/php-wasm/node/project.json @@ -146,213 +146,149 @@ "executor": "@wp-playground/nx-extensions:package-for-self-hosting", "dependsOn": ["build"] }, - "test-asyncify": { + "test-group-1-asyncify": { "executor": "@nx/vite:test", "outputs": ["{workspaceRoot}/coverage/packages/php-wasm/node"], "options": { "reportsDirectory": "../../../coverage/packages/php-wasm/node", - "testFiles": [ - "unzip-file.spec.ts", - "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", - "php-worker.spec.ts", - "php-soap.spec.ts", - "php-image-extensions.spec.ts", - "php-imagick.spec.ts" - ] - } - }, - "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": [ - "unzip-file.spec.ts", - "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", - "php-worker.spec.ts", - "php-soap.spec.ts", - "php-image-extensions.spec.ts", - "php-imagick.spec.ts" - ] - } - }, - "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-file-get-contents.spec.ts"] - } - }, - "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-file-get-contents.spec.ts"] - } - }, - "test-php-fopen-asyncify": { - "executor": "@nx/vite:test", - "outputs": ["{workspaceRoot}/coverage/packages/php-wasm/node"], - "options": { - "reportsDirectory": "../../../coverage/packages/php-wasm/node", - "testFiles": ["php-fopen.spec.ts"] - } - }, - "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-fopen.spec.ts"] - } - }, - "test-php-fsockopen-asyncify": { - "executor": "@nx/vite:test", - "outputs": ["{workspaceRoot}/coverage/packages/php-wasm/node"], - "options": { - "reportsDirectory": "../../../coverage/packages/php-wasm/node", - "testFiles": ["php-fsockopen.spec.ts"] - } - }, - "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-fsockopen.spec.ts"] - } - }, - "test-php-gethostbyname-asyncify": { - "executor": "@nx/vite:test", - "outputs": ["{workspaceRoot}/coverage/packages/php-wasm/node"], - "options": { - "reportsDirectory": "../../../coverage/packages/php-wasm/node", - "testFiles": ["php-gethostbyname.spec.ts"] + "testFiles": ["php.spec.ts"] } }, - "test-php-gethostbyname-jspi": { + "test-group-1-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-gethostbyname.spec.ts"] + "testFiles": ["php.spec.ts"] } }, - "test-php-mysqli-asyncify": { + "test-group-2-asyncify": { "executor": "@nx/vite:test", "outputs": ["{workspaceRoot}/coverage/packages/php-wasm/node"], "options": { "reportsDirectory": "../../../coverage/packages/php-wasm/node", - "testFiles": ["php-mysqli.spec.ts"] + "testFiles": [ + "php-file-locking.spec.ts", + "file-lock-manager-for-node.spec.ts" + ] } }, - "test-php-mysqli-jspi": { + "test-group-2-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-mysqli.spec.ts"] + "testFiles": [ + "php-file-locking.spec.ts", + "file-lock-manager-for-node.spec.ts" + ] } }, - "test-php-sqlite3-asyncify": { + "test-group-3-asyncify": { "executor": "@nx/vite:test", "outputs": ["{workspaceRoot}/coverage/packages/php-wasm/node"], "options": { "reportsDirectory": "../../../coverage/packages/php-wasm/node", - "testFiles": ["php-sqlite3.spec.ts"] + "testFiles": [ + "php-request-handler.spec.ts", + "mount.spec.ts", + "rotate-php-runtime.spec.ts" + ] } }, - "test-php-sqlite3-jspi": { + "test-group-3-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-sqlite3.spec.ts"] + "testFiles": [ + "php-request-handler.spec.ts", + "mount.spec.ts", + "rotate-php-runtime.spec.ts" + ] } }, - "test-php-file-locking-asyncify": { + "test-group-4-asyncify": { "executor": "@nx/vite:test", "outputs": ["{workspaceRoot}/coverage/packages/php-wasm/node"], "options": { "reportsDirectory": "../../../coverage/packages/php-wasm/node", "testFiles": [ - "php-file-locking.spec.ts", - "file-lock-manager-for-node.spec.ts" + "php-imagick.spec.ts", + "php-soap.spec.ts", + "php-image-extensions.spec.ts", + "php-fsockopen.spec.ts", + "php-fopen.spec.ts", + "php-file-get-contents.spec.ts", + "php-gethostbyname.spec.ts" ] } }, - "test-php-file-locking-jspi": { + "test-group-4-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-file-locking.spec.ts", - "file-lock-manager-for-node.spec.ts" + "php-imagick.spec.ts", + "php-soap.spec.ts", + "php-image-extensions.spec.ts", + "php-fsockopen.spec.ts", + "php-fopen.spec.ts", + "php-file-get-contents.spec.ts", + "php-gethostbyname.spec.ts" ] } }, - "test-php-image-extensions-asyncify": { + "test-group-5-asyncify": { "executor": "@nx/vite:test", "outputs": ["{workspaceRoot}/coverage/packages/php-wasm/node"], "options": { "reportsDirectory": "../../../coverage/packages/php-wasm/node", - "testFiles": ["php-image-extensions.spec.ts"] + "testFiles": [ + "symlinks.spec.ts", + "php-sqlite3.spec.ts", + "php-networking.spec.ts", + "php-dynamic-loading.spec.ts", + "php-crash.spec.ts", + "php-mysqli.spec.ts", + "php-process-manager.spec.ts", + "php-ini.spec.ts", + "write-files.spec.ts", + "php-worker.spec.ts", + "unzip-file.spec.ts", + "php-vars.spec.ts", + "php-memory.spec.ts" + ] } }, - "test-php-image-extensions-jspi": { + "test-group-5-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-image-extensions.spec.ts"] + "testFiles": [ + "symlinks.spec.ts", + "php-sqlite3.spec.ts", + "php-networking.spec.ts", + "php-dynamic-loading.spec.ts", + "php-crash.spec.ts", + "php-mysqli.spec.ts", + "php-process-manager.spec.ts", + "php-ini.spec.ts", + "write-files.spec.ts", + "php-worker.spec.ts", + "unzip-file.spec.ts", + "php-vars.spec.ts", + "php-memory.spec.ts" + ] } }, - "test-php-asyncify-all": { - "executor": "nx:noop", - "dependsOn": [ - "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", - "test-php-image-extensions-asyncify" - ] - }, "test-opcache": { "executor": "@nx/vite:test", "outputs": ["{workspaceRoot}/coverage/packages/php-wasm/node"], From 39579a4c624574d7d60d536247e89905d518e644 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Zieli=C5=84ski?= Date: Tue, 25 Nov 2025 14:25:54 +0100 Subject: [PATCH 2/3] Split php.spec.ts in two --- .github/workflows/ci.yml | 4 + packages/php-wasm/node/project.json | 84 +- .../test/{php.spec.ts => php-part-1.spec.ts} | 1130 +-------------- .../php-wasm/node/src/test/php-part-2.spec.ts | 1236 +++++++++++++++++ 4 files changed, 1291 insertions(+), 1163 deletions(-) rename packages/php-wasm/node/src/test/{php.spec.ts => php-part-1.spec.ts} (62%) create mode 100644 packages/php-wasm/node/src/test/php-part-2.spec.ts diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index accd3812fe..1705388691 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -43,6 +43,8 @@ jobs: target: test-group-4-asyncify - name: test-unit-asyncify (6/6) target: test-group-5-asyncify + - name: test-unit-asyncify (7/6) + target: test-group-6-asyncify name: ${{ matrix.name }} services: mysql: @@ -87,6 +89,8 @@ jobs: target: test-group-4-jspi - name: test-unit-jspi (5/5) target: test-group-5-jspi + - name: test-unit-jspi (6/5) + target: test-group-6-jspi name: ${{ matrix.name }} services: mysql: diff --git a/packages/php-wasm/node/project.json b/packages/php-wasm/node/project.json index e0ba27d5dc..11e0858019 100644 --- a/packages/php-wasm/node/project.json +++ b/packages/php-wasm/node/project.json @@ -151,7 +151,7 @@ "outputs": ["{workspaceRoot}/coverage/packages/php-wasm/node"], "options": { "reportsDirectory": "../../../coverage/packages/php-wasm/node", - "testFiles": ["php.spec.ts"] + "testFiles": ["php-part-1.spec.ts"] } }, "test-group-1-jspi": { @@ -160,7 +160,7 @@ "options": { "configFile": "packages/php-wasm/node/vite.jspi.config.ts", "reportsDirectory": "../../../coverage/packages/php-wasm/node", - "testFiles": ["php.spec.ts"] + "testFiles": ["php-part-1.spec.ts"] } }, "test-group-2-asyncify": { @@ -170,7 +170,11 @@ "reportsDirectory": "../../../coverage/packages/php-wasm/node", "testFiles": [ "php-file-locking.spec.ts", - "file-lock-manager-for-node.spec.ts" + "file-lock-manager-for-node.spec.ts", + "php-imagick.spec.ts", + "php-soap.spec.ts", + "php-image-extensions.spec.ts", + "php-fsockopen.spec.ts" ] } }, @@ -182,7 +186,11 @@ "reportsDirectory": "../../../coverage/packages/php-wasm/node", "testFiles": [ "php-file-locking.spec.ts", - "file-lock-manager-for-node.spec.ts" + "file-lock-manager-for-node.spec.ts", + "php-imagick.spec.ts", + "php-soap.spec.ts", + "php-image-extensions.spec.ts", + "php-fsockopen.spec.ts" ] } }, @@ -194,7 +202,10 @@ "testFiles": [ "php-request-handler.spec.ts", "mount.spec.ts", - "rotate-php-runtime.spec.ts" + "rotate-php-runtime.spec.ts", + "php-fopen.spec.ts", + "php-file-get-contents.spec.ts", + "php-gethostbyname.spec.ts" ] } }, @@ -207,7 +218,10 @@ "testFiles": [ "php-request-handler.spec.ts", "mount.spec.ts", - "rotate-php-runtime.spec.ts" + "rotate-php-runtime.spec.ts", + "php-fopen.spec.ts", + "php-file-get-contents.spec.ts", + "php-gethostbyname.spec.ts" ] } }, @@ -217,13 +231,13 @@ "options": { "reportsDirectory": "../../../coverage/packages/php-wasm/node", "testFiles": [ - "php-imagick.spec.ts", - "php-soap.spec.ts", - "php-image-extensions.spec.ts", - "php-fsockopen.spec.ts", - "php-fopen.spec.ts", - "php-file-get-contents.spec.ts", - "php-gethostbyname.spec.ts" + "symlinks.spec.ts", + "php-sqlite3.spec.ts", + "php-networking.spec.ts", + "php-dynamic-loading.spec.ts", + "php-crash.spec.ts", + "php-mysqli.spec.ts", + "php-process-manager.spec.ts" ] } }, @@ -234,13 +248,13 @@ "configFile": "packages/php-wasm/node/vite.jspi.config.ts", "reportsDirectory": "../../../coverage/packages/php-wasm/node", "testFiles": [ - "php-imagick.spec.ts", - "php-soap.spec.ts", - "php-image-extensions.spec.ts", - "php-fsockopen.spec.ts", - "php-fopen.spec.ts", - "php-file-get-contents.spec.ts", - "php-gethostbyname.spec.ts" + "symlinks.spec.ts", + "php-sqlite3.spec.ts", + "php-networking.spec.ts", + "php-dynamic-loading.spec.ts", + "php-crash.spec.ts", + "php-mysqli.spec.ts", + "php-process-manager.spec.ts" ] } }, @@ -250,19 +264,13 @@ "options": { "reportsDirectory": "../../../coverage/packages/php-wasm/node", "testFiles": [ - "symlinks.spec.ts", - "php-sqlite3.spec.ts", - "php-networking.spec.ts", - "php-dynamic-loading.spec.ts", - "php-crash.spec.ts", - "php-mysqli.spec.ts", - "php-process-manager.spec.ts", "php-ini.spec.ts", "write-files.spec.ts", "php-worker.spec.ts", "unzip-file.spec.ts", "php-vars.spec.ts", - "php-memory.spec.ts" + "php-memory.spec.ts", + "php-opcache.spec.ts" ] } }, @@ -273,13 +281,6 @@ "configFile": "packages/php-wasm/node/vite.jspi.config.ts", "reportsDirectory": "../../../coverage/packages/php-wasm/node", "testFiles": [ - "symlinks.spec.ts", - "php-sqlite3.spec.ts", - "php-networking.spec.ts", - "php-dynamic-loading.spec.ts", - "php-crash.spec.ts", - "php-mysqli.spec.ts", - "php-process-manager.spec.ts", "php-ini.spec.ts", "write-files.spec.ts", "php-worker.spec.ts", @@ -289,12 +290,21 @@ ] } }, - "test-opcache": { + "test-group-6-asyncify": { "executor": "@nx/vite:test", "outputs": ["{workspaceRoot}/coverage/packages/php-wasm/node"], "options": { "reportsDirectory": "../../../coverage/packages/php-wasm/node", - "testFiles": ["php-opcache.spec.ts"] + "testFiles": ["php-part-2.spec.ts"] + } + }, + "test-group-6-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-part-2.spec.ts"] } }, "lint": { diff --git a/packages/php-wasm/node/src/test/php.spec.ts b/packages/php-wasm/node/src/test/php-part-1.spec.ts similarity index 62% rename from packages/php-wasm/node/src/test/php.spec.ts rename to packages/php-wasm/node/src/test/php-part-1.spec.ts index 3ceac8b140..12befc4af9 100644 --- a/packages/php-wasm/node/src/test/php.spec.ts +++ b/packages/php-wasm/node/src/test/php-part-1.spec.ts @@ -1,7 +1,5 @@ import { - __private__dont__use, getPhpIniEntries, - loadPHPRuntime, PHP, PHPProcessManager, sandboxedSpawnHandlerFactory, @@ -9,22 +7,12 @@ import { type SpawnedPHP, SupportedPHPVersions, } from '@php-wasm/universal'; -import { createSpawnHandler, joinPaths, phpVar } from '@php-wasm/util'; -import { - existsSync, - mkdirSync, - mkdtempSync, - readFileSync, - rmSync, - statfsSync, - writeFileSync, -} from 'fs'; -import { tmpdir } from 'os'; -import { vi } from 'vitest'; -import { getPHPLoaderModule, loadNodeRuntime } from '..'; +import { createSpawnHandler, phpVar } from '@php-wasm/util'; +import { RecommendedPHPVersion } from '@wp-playground/common'; +import { existsSync, mkdirSync, rmSync, writeFileSync } from 'fs'; import type { PHPLoaderOptions } from '..'; +import { loadNodeRuntime } from '..'; import { createNodeFsMountHandler } from '../lib/node-fs-mount'; -import { RecommendedPHPVersion } from '@wp-playground/common'; const testDirPath = '/__test987654321'; const testFilePath = '/__test987654321.txt'; @@ -1835,1116 +1823,6 @@ phpLoaderOptions.forEach((options) => { ]); }); }); - - describe('Exit codes', { skip: options.withXdebug }, () => { - describe('Returns exit code 0', () => { - const testsSnippets = { - 'on empty code': '', - 'on successful run': ' { - const result = await php.run({ - code: testSnippet, - }); - expect(result.exitCode).toEqual(0); - }); - - // Run via request handler - it(testName, async () => { - php.writeFile('/test.php', testSnippet); - const result = await php.run({ - scriptPath: '/test.php', - }); - expect(result.exitCode).toEqual(0); - }); - } - }); - describe('Returns exit code > 0', () => { - const testsSnippets = { - 'syntax error': ' { - const promise = php.run({ - code: testSnippet, - }); - await expect(promise).rejects.toThrow(); - }); - - // Run via the request handler - it(testName, async () => { - php.writeFile('/test.php', testSnippet); - const promise = php.run({ - scriptPath: '/test.php', - }); - await expect(promise).rejects.toThrow(); - }); - } - }); - it('Returns the correct exit code on subsequent runs', async () => { - const promise1 = php.run({ - code: ' { - const promise1 = php.run({ - code: ' { - it('should output strings (1)', async () => { - expect( - await php.run({ code: ' { - expect( - await php.run({ - code: ' { - const results = await php.run({ - code: ' { - expect( - await php.run({ code: ' { - const code = ` { - const code = ` { - const code = ` "world"]); - `; - const response = await php.run({ code }); - expect(response.json).toEqual({ hello: 'world' }); - }); - }); - - describe('Interface', { skip: options.withXdebug }, () => { - it('run() should throw an error when neither `code` nor `scriptFile` is provided', async () => { - await expect(() => php.run({})).rejects.toThrowError( - /The request object must have either a `code` or a `scriptPath` property/ - ); - }); - }); - - describe( - 'Startup sequence – basics', - { skip: options.withXdebug }, - () => { - /** - * This test ensures that the PHP runtime can be loaded twice. - * - * It protects from a regression that happened in the past - * after making the Emscripten module's main function the - * default export. Turns out, the generated Emscripten code - * replaces the default export with an instantiated module upon - * the first call. - */ - it('Should spawn two PHP runtimes', async () => { - const phpLoaderModule1 = await getPHPLoaderModule( - phpVersion as any - ); - const runtimeId1 = await loadPHPRuntime(phpLoaderModule1); - - const phpLoaderModule2 = await getPHPLoaderModule( - phpVersion as any - ); - const runtimeId2 = await loadPHPRuntime(phpLoaderModule2); - - expect(runtimeId1).not.toEqual(runtimeId2); - }); - } - ); - - describe('Startup sequence', { skip: options.withXdebug }, () => { - const testScriptPath = '/test.php'; - afterEach(() => { - if (existsSync(testScriptPath)) { - rmSync(testScriptPath); - } - }); - - /** - * Issue https://github.com/WordPress/wordpress-playground/issues/169 - */ - it('Should work with long POST body', async () => { - php.writeFile(testScriptPath, ''); - const body = new Uint8Array( - readFileSync( - new URL( - './test-data/long-post-body.txt', - import.meta.url - ).pathname - ) - ); - // 0x4000 is SAPI_POST_BLOCK_SIZE - expect(body.length).toBeGreaterThan(0x4000); - await expect( - php.run({ - code: 'echo "A";', - relativeUri: '/test.php?a=b', - body, - method: 'POST', - headers: { - 'Content-Type': 'application/x-www-form-urlencoded', - }, - }) - ).resolves.not.toThrow(); - }); - - it('Should run a script when no code snippet is provided', async () => { - php.writeFile( - testScriptPath, - `\n` - ); - const response = await php.run({ - scriptPath: testScriptPath, - }); - const bodyText = new TextDecoder().decode(response.bytes); - expect(bodyText).toEqual('Hello world!'); - }); - - it('Should run a code snippet when provided, even if scriptPath is set', async () => { - php.writeFile(testScriptPath, ''); - const response = await php.run({ - scriptPath: testScriptPath, - code: ' { - const response = await php.run({ - headers: { 'Content-Type': 'application/json' }, - method: 'POST', - body: new TextEncoder().encode('{"foo": "bar"}'), - code: ` { - php.writeFile('/php/index.php', ` { - php.writeFile('/php/index.php', ` { - const estimateFreeMemory = () => - php[__private__dont__use].HEAPU32.reduce( - (count: number, byte: number) => - byte === 0 ? count + 1 : count, - 0 - ) / 4; - - // The initial request will allocate a lot of memory so let's get that - // out of the way before we start measuring. - php.writeFile('/php/index.php', ` { - const response = await php.run({ - code: ` { - php.writeFile( - '/php/index.php', - ` { - const response = await php.run({ - code: ' { - php.mkdir('/php/subdirectory'); - - php.writeFile( - `/php/subdirectory/test.php`, - ` { - const response = await php.run({ - code: ` { - const response = await php.run({ - code: ` { - const response = await php.run({ - code: ` { - const response = await php.run({ - code: ` $_FILES, - "is_uploaded" => is_uploaded_file($_FILES["myFile"]["tmp_name"]) - ));`, - method: 'POST', - body: new TextEncoder().encode( - `--boundary\r\n` + - `Content-Disposition: form-data; name="myFile"; filename="text.txt"\r\n` + - `Content-Type: text/plain\r\n` + - `\r\n` + - `bar\r\n` + - `--boundary--\r\n` - ), - headers: { - 'Content-Type': - 'multipart/form-data; boundary=boundary', - }, - }); - const bodyText = new TextDecoder().decode(response.bytes); - const expectedResult = { - files: { - myFile: { - name: 'text.txt', - type: 'text/plain', - tmp_name: expect.any(String), - error: 0, - size: 3, - }, - }, - is_uploaded: true, - }; - if (Number(phpVersion) > 8) { - (expectedResult.files.myFile as any).full_path = 'text.txt'; - } - expect(JSON.parse(bodyText)).toEqual(expectedResult); - }); - - it('Should provide the correct $_SERVER information', async () => { - php.writeFile( - testScriptPath, - '' - ); - const response = await php.run({ - scriptPath: testScriptPath, - relativeUri: '/test.php?a=b', - method: 'POST', - body: new TextEncoder().encode(`--boundary - Content-Disposition: form-data; name="myFile1"; filename="from_body.txt" - Content-Type: text/plain - - bar1 - --boundary--`), - headers: { - 'Content-Type': - 'multipart/form-data; boundary=boundary', - Host: 'https://example.com:1235', - 'X-is-ajax': 'true', - }, - }); - const bodyText = new TextDecoder().decode(response.bytes); - const $_SERVER = JSON.parse(bodyText); - expect($_SERVER).toHaveProperty('REQUEST_URI', '/test.php?a=b'); - expect($_SERVER).toHaveProperty('REQUEST_METHOD', 'POST'); - expect($_SERVER).toHaveProperty( - 'CONTENT_TYPE', - 'multipart/form-data; boundary=boundary' - ); - expect($_SERVER).toHaveProperty( - 'HTTP_HOST', - 'https://example.com:1235' - ); - expect($_SERVER).toHaveProperty( - 'SERVER_NAME', - 'https://example.com:1235' - ); - expect($_SERVER).toHaveProperty('HTTP_X_IS_AJAX', 'true'); - expect($_SERVER).toHaveProperty('SERVER_PORT', '1235'); - expect($_SERVER).toHaveProperty('QUERY_STRING', 'a=b'); - }); - - it('Should have an empty QUERY_STRING when the URI has no query string', async () => { - const response = await php.run({ - code: ` { - it('Should emit a request.error event when PHP.run() exits with a non-zero exit code', async () => { - const spyListener = vi.fn(); - php.addEventListener('request.error', spyListener); - try { - await php.run({ - code: ` { - const spyListener = vi.fn(); - php.addEventListener('request.error', spyListener); - try { - const response = await php.runStream({ - code: ` { - it('Should be able to create a database', async () => { - const response = await php.run({ - code: `exec('CREATE TABLE test (id INTEGER PRIMARY KEY, name TEXT)'); - $db->exec('INSERT INTO test (name) VALUES ("This is a test")'); - $result = $db->query('SELECT name FROM test'); - $rows = $result->fetchAll(PDO::FETCH_COLUMN); - echo json_encode($rows); - ?>`, - }); - const bodyText = new TextDecoder().decode(response.bytes); - expect(JSON.parse(bodyText)).toEqual(['This is a test']); - }); - - it('Should support modern libsqlite (ON CONFLICT)', async () => { - const response = await php.run({ - code: `exec('CREATE TABLE test (id INTEGER PRIMARY KEY, name TEXT)'); - $db->exec('CREATE UNIQUE INDEX test_name ON test (name)'); - $db->exec('INSERT INTO test (name) VALUES ("This is a test")'); - $db->exec('INSERT INTO test (name) VALUES ("This is a test") ON CONFLICT DO NOTHING'); - $result = $db->query('SELECT name FROM test'); - $rows = $result->fetchAll(PDO::FETCH_COLUMN); - echo json_encode($rows); - ?>`, - }); - const bodyText = new TextDecoder().decode(response.bytes); - expect(JSON.parse(bodyText)).toEqual(['This is a test']); - }); - }); - - /** - * hash extension needs to be explicitly enabled in Dockerfile - * for PHP < 7.3 – let's make sure it works - */ - describe('Hash extension support', { skip: options.withXdebug }, () => { - it('Should be able to hash a string', async () => { - const response = await php.run({ - code: ` md5('test'), - 'sha1' => sha1('test'), - 'hash' => hash('sha256', 'test'), - ]); - ?>`, - }); - const bodyText = new TextDecoder().decode(response.bytes); - expect(JSON.parse(bodyText)).toEqual({ - md5: '098f6bcd4621d373cade4e832627b4f6', - sha1: 'a94a8fe5ccb19ba61c4c0873d391e987982fbbd3', - hash: '9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08', - }); - }); - }); - - /** - * mbregex support - */ - describe( - 'mbregex extension support', - { skip: options.withXdebug }, - () => { - it('Should be able to use mb_regex_encoding functions', async () => { - const promise = php.run({ - code: ``, - }); - const response = await promise; - expect(response.errors).toBe(''); - }); - } - ); - - describe('64 bit integer support', { skip: options.withXdebug }, () => { - it('Should be able to use 64 bit integers', async () => { - const response = await php.run({ - code: ` { - const response = await php.run({ - code: ` $timestamp, - 'type' => gettype($timestamp), - ]);`, - }); - const result = JSON.parse(response.text); - expect(result.value).toEqual(2210555647); - expect(result.type).toBe('integer'); - }); - - it('Should handle adding 64 bit integers', async () => { - const response = await php.run({ - code: ` $product, - 'type' => gettype($product), - ]); - `, - }); - const result = JSON.parse(response.text); - expect(result.value + '').toEqual('9223372036854774000'); - expect(result.type).toEqual('integer'); - }); - - it('Should handle multiplying 64 bit integers', async () => { - const response = await php.run({ - code: ` $product, - 'type' => gettype($product), - ]); - `, - }); - const result = JSON.parse(response.text); - expect(result.value + '').toEqual('9223372036854774000'); - expect(result.type).toEqual('integer'); - }); - - it('Should handle large integer division', async () => { - const response = await php.run({ - code: ` $division, - 'type' => gettype($division), - ]);`, - }); - const result = JSON.parse(response.text); - expect(result.value + '').toEqual('4611686018427387000'); - expect(result.type).toEqual('integer'); - }); - - it('Should handle PHP_MAX_INT', async () => { - const response = await php.run({ - code: ` $maxInt, - 'type' => gettype($maxInt), - ]); - `, - }); - const result = JSON.parse(response.text); - expect(result.value + '').toEqual('9223372036854776000'); - expect(result.type).toEqual('integer'); - }); - }); - - /** - * fileinfo support - */ - describe( - 'fileinfo extension support', - { skip: options.withXdebug }, - () => { - it('Should be able to use finfo_file', async () => { - await php.writeFile( - '/test.php', - 'file('/test.php'); - ?>`, - }); - expect(response.text).toEqual('text/x-php'); - }); - } - ); - - /** - * exif support - */ - describe('exif extension support', { skip: options.withXdebug }, () => { - beforeEach(async () => { - await php.writeFile( - '/image.jpg', - new Uint8Array( - readFileSync( - joinPaths(__dirname, 'test-data', 'image.jpg') - ) - ) - ); - }); - it('should return correct image type using exif_imagetype', async () => { - const response = await php.run({ - code: ` { - const response = await php.run({ - code: ` { - const response = await php.run({ - code: ` { - const response = await php.run({ - code: ` { - it('should pass messages to JS', async () => { - let messageReceived = ''; - php.onMessage((message) => { - messageReceived = message; - }); - const out = await php.run({ - code: ` { - php.onMessage(async (message) => message + '!'); - const out = await php.run({ - code: ` { - php.onMessage(async (message) => { - // Simulate getting data asynchronously. - return await new Promise((resolve) => - setTimeout(() => resolve(message + '!'), 100) - ); - }); - const out = await php.run({ - code: ` { - php.onMessage(async () => { - // Simulate getting data asynchronously. - return await new Promise((resolve, reject) => - setTimeout(() => reject('Failure!'), 100) - ); - }); - const out = await php.run({ - code: ` { - let consoleLogMock: any; - let consoleErrorMock: any; - beforeEach(() => { - consoleLogMock = vi - .spyOn(console, 'log') - .mockImplementation(() => {}); - consoleErrorMock = vi - .spyOn(console, 'error') - .mockImplementation(() => {}); - }); - - afterAll(() => { - consoleLogMock.mockReset(); - consoleErrorMock.mockReset(); - }); - it('should not log an error message on exit status 0', async () => { - await php.cli(['php', '-r', '$tmp = "Hello";']); - expect(consoleLogMock).not.toHaveBeenCalled(); - expect(consoleErrorMock).not.toHaveBeenCalled(); - }); - - it('should define the PHP_BINARY constant', async () => { - const response = await php.cli([ - 'php', - '-r', - 'echo PHP_BINARY;', - ]); - expect(await response.stdoutText).toBe( - '/internal/shared/bin/php' - ); - }); - - it('should support multiple calls to php.cli() and php.runStream() when runtime rotation is enabled', async () => { - php.enableRuntimeRotation({ - maxRequests: 1, - recreateRuntime: () => - loadNodeRuntime(phpVersion as any, options), - }); - const response = await php.cli(['php', '-r', 'echo "Hello";']); - expect(await response.stdoutText).toBe('Hello'); - const response2 = await php.runStream({ - code: ` { - it('should encode response headers', async () => { - const out = await php.run({ - code: `[\\d]+)');`, - }); - expect(out.headers['location'][0]).toEqual('/(?P[\\d]+)'); - }); - }); - - describe('Disk space', { skip: options.withXdebug }, () => { - it('should return the correct total disk space', async () => { - const response = await php.run({ - code: ` { - const response = await php.run({ - code: ` { - php.writeFile('/test.txt', new Uint8Array(1024)); - const response = await php.run({ - code: ` { - const tempDir = mkdtempSync( - joinPaths(tmpdir(), 'php-wasm-test-') - ); - const filePath = joinPaths(tempDir, 'test.txt'); - writeFileSync(filePath, new Uint8Array(1024)); - php.mount('/tmp', createNodeFsMountHandler(tempDir)); - - const response = await php.run({ - code: ` { + describe.each(phpVersions)('PHP %s', (phpVersion) => { + let php: PHP; + beforeEach(async () => { + php = new PHP(await loadNodeRuntime(phpVersion as any, options)); + php.mkdir('/php'); + await setPhpIniEntries(php, { + disable_functions: '', + html_errors: false, + }); + }); + afterEach(async () => { + php.exit(); + }); + + describe('Exit codes', { skip: options.withXdebug }, () => { + describe('Returns exit code 0', () => { + const testsSnippets = { + 'on empty code': '', + 'on successful run': ' { + const result = await php.run({ + code: testSnippet, + }); + expect(result.exitCode).toEqual(0); + }); + + // Run via request handler + it(testName, async () => { + php.writeFile('/test.php', testSnippet); + const result = await php.run({ + scriptPath: '/test.php', + }); + expect(result.exitCode).toEqual(0); + }); + } + }); + describe('Returns exit code > 0', () => { + const testsSnippets = { + 'syntax error': ' { + const promise = php.run({ + code: testSnippet, + }); + await expect(promise).rejects.toThrow(); + }); + + // Run via the request handler + it(testName, async () => { + php.writeFile('/test.php', testSnippet); + const promise = php.run({ + scriptPath: '/test.php', + }); + await expect(promise).rejects.toThrow(); + }); + } + }); + it('Returns the correct exit code on subsequent runs', async () => { + const promise1 = php.run({ + code: ' { + const promise1 = php.run({ + code: ' { + it('should output strings (1)', async () => { + expect( + await php.run({ code: ' { + expect( + await php.run({ + code: ' { + const results = await php.run({ + code: ' { + expect( + await php.run({ code: ' { + const code = ` { + const code = ` { + const code = ` "world"]); + `; + const response = await php.run({ code }); + expect(response.json).toEqual({ hello: 'world' }); + }); + }); + + describe('Interface', { skip: options.withXdebug }, () => { + it('run() should throw an error when neither `code` nor `scriptFile` is provided', async () => { + await expect(() => php.run({})).rejects.toThrowError( + /The request object must have either a `code` or a `scriptPath` property/ + ); + }); + }); + + describe( + 'Startup sequence – basics', + { skip: options.withXdebug }, + () => { + /** + * This test ensures that the PHP runtime can be loaded twice. + * + * It protects from a regression that happened in the past + * after making the Emscripten module's main function the + * default export. Turns out, the generated Emscripten code + * replaces the default export with an instantiated module upon + * the first call. + */ + it('Should spawn two PHP runtimes', async () => { + const phpLoaderModule1 = await getPHPLoaderModule( + phpVersion as any + ); + const runtimeId1 = await loadPHPRuntime(phpLoaderModule1); + + const phpLoaderModule2 = await getPHPLoaderModule( + phpVersion as any + ); + const runtimeId2 = await loadPHPRuntime(phpLoaderModule2); + + expect(runtimeId1).not.toEqual(runtimeId2); + }); + } + ); + + describe('Startup sequence', { skip: options.withXdebug }, () => { + const testScriptPath = '/test.php'; + afterEach(() => { + if (existsSync(testScriptPath)) { + rmSync(testScriptPath); + } + }); + + /** + * Issue https://github.com/WordPress/wordpress-playground/issues/169 + */ + it('Should work with long POST body', async () => { + php.writeFile(testScriptPath, ''); + const body = new Uint8Array( + readFileSync( + new URL( + './test-data/long-post-body.txt', + import.meta.url + ).pathname + ) + ); + // 0x4000 is SAPI_POST_BLOCK_SIZE + expect(body.length).toBeGreaterThan(0x4000); + await expect( + php.run({ + code: 'echo "A";', + relativeUri: '/test.php?a=b', + body, + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + }, + }) + ).resolves.not.toThrow(); + }); + + it('Should run a script when no code snippet is provided', async () => { + php.writeFile( + testScriptPath, + `\n` + ); + const response = await php.run({ + scriptPath: testScriptPath, + }); + const bodyText = new TextDecoder().decode(response.bytes); + expect(bodyText).toEqual('Hello world!'); + }); + + it('Should run a code snippet when provided, even if scriptPath is set', async () => { + php.writeFile(testScriptPath, ''); + const response = await php.run({ + scriptPath: testScriptPath, + code: ' { + const response = await php.run({ + headers: { 'Content-Type': 'application/json' }, + method: 'POST', + body: new TextEncoder().encode('{"foo": "bar"}'), + code: ` { + php.writeFile('/php/index.php', ` { + php.writeFile('/php/index.php', ` { + const estimateFreeMemory = () => + php[__private__dont__use].HEAPU32.reduce( + (count: number, byte: number) => + byte === 0 ? count + 1 : count, + 0 + ) / 4; + + // The initial request will allocate a lot of memory so let's get that + // out of the way before we start measuring. + php.writeFile('/php/index.php', ` { + const response = await php.run({ + code: ` { + php.writeFile( + '/php/index.php', + ` { + const response = await php.run({ + code: ' { + php.mkdir('/php/subdirectory'); + + php.writeFile( + `/php/subdirectory/test.php`, + ` { + const response = await php.run({ + code: ` { + const response = await php.run({ + code: ` { + const response = await php.run({ + code: ` { + const response = await php.run({ + code: ` $_FILES, + "is_uploaded" => is_uploaded_file($_FILES["myFile"]["tmp_name"]) + ));`, + method: 'POST', + body: new TextEncoder().encode( + `--boundary\r\n` + + `Content-Disposition: form-data; name="myFile"; filename="text.txt"\r\n` + + `Content-Type: text/plain\r\n` + + `\r\n` + + `bar\r\n` + + `--boundary--\r\n` + ), + headers: { + 'Content-Type': + 'multipart/form-data; boundary=boundary', + }, + }); + const bodyText = new TextDecoder().decode(response.bytes); + const expectedResult = { + files: { + myFile: { + name: 'text.txt', + type: 'text/plain', + tmp_name: expect.any(String), + error: 0, + size: 3, + }, + }, + is_uploaded: true, + }; + if (Number(phpVersion) > 8) { + (expectedResult.files.myFile as any).full_path = 'text.txt'; + } + expect(JSON.parse(bodyText)).toEqual(expectedResult); + }); + + it('Should provide the correct $_SERVER information', async () => { + php.writeFile( + testScriptPath, + '' + ); + const response = await php.run({ + scriptPath: testScriptPath, + relativeUri: '/test.php?a=b', + method: 'POST', + body: new TextEncoder().encode(`--boundary + Content-Disposition: form-data; name="myFile1"; filename="from_body.txt" + Content-Type: text/plain + + bar1 + --boundary--`), + headers: { + 'Content-Type': + 'multipart/form-data; boundary=boundary', + Host: 'https://example.com:1235', + 'X-is-ajax': 'true', + }, + }); + const bodyText = new TextDecoder().decode(response.bytes); + const $_SERVER = JSON.parse(bodyText); + expect($_SERVER).toHaveProperty('REQUEST_URI', '/test.php?a=b'); + expect($_SERVER).toHaveProperty('REQUEST_METHOD', 'POST'); + expect($_SERVER).toHaveProperty( + 'CONTENT_TYPE', + 'multipart/form-data; boundary=boundary' + ); + expect($_SERVER).toHaveProperty( + 'HTTP_HOST', + 'https://example.com:1235' + ); + expect($_SERVER).toHaveProperty( + 'SERVER_NAME', + 'https://example.com:1235' + ); + expect($_SERVER).toHaveProperty('HTTP_X_IS_AJAX', 'true'); + expect($_SERVER).toHaveProperty('SERVER_PORT', '1235'); + expect($_SERVER).toHaveProperty('QUERY_STRING', 'a=b'); + }); + + it('Should have an empty QUERY_STRING when the URI has no query string', async () => { + const response = await php.run({ + code: ` { + it('Should emit a request.error event when PHP.run() exits with a non-zero exit code', async () => { + const spyListener = vi.fn(); + php.addEventListener('request.error', spyListener); + try { + await php.run({ + code: ` { + const spyListener = vi.fn(); + php.addEventListener('request.error', spyListener); + try { + const response = await php.runStream({ + code: ` { + it('Should be able to create a database', async () => { + const response = await php.run({ + code: `exec('CREATE TABLE test (id INTEGER PRIMARY KEY, name TEXT)'); + $db->exec('INSERT INTO test (name) VALUES ("This is a test")'); + $result = $db->query('SELECT name FROM test'); + $rows = $result->fetchAll(PDO::FETCH_COLUMN); + echo json_encode($rows); + ?>`, + }); + const bodyText = new TextDecoder().decode(response.bytes); + expect(JSON.parse(bodyText)).toEqual(['This is a test']); + }); + + it('Should support modern libsqlite (ON CONFLICT)', async () => { + const response = await php.run({ + code: `exec('CREATE TABLE test (id INTEGER PRIMARY KEY, name TEXT)'); + $db->exec('CREATE UNIQUE INDEX test_name ON test (name)'); + $db->exec('INSERT INTO test (name) VALUES ("This is a test")'); + $db->exec('INSERT INTO test (name) VALUES ("This is a test") ON CONFLICT DO NOTHING'); + $result = $db->query('SELECT name FROM test'); + $rows = $result->fetchAll(PDO::FETCH_COLUMN); + echo json_encode($rows); + ?>`, + }); + const bodyText = new TextDecoder().decode(response.bytes); + expect(JSON.parse(bodyText)).toEqual(['This is a test']); + }); + }); + + /** + * hash extension needs to be explicitly enabled in Dockerfile + * for PHP < 7.3 – let's make sure it works + */ + describe('Hash extension support', { skip: options.withXdebug }, () => { + it('Should be able to hash a string', async () => { + const response = await php.run({ + code: ` md5('test'), + 'sha1' => sha1('test'), + 'hash' => hash('sha256', 'test'), + ]); + ?>`, + }); + const bodyText = new TextDecoder().decode(response.bytes); + expect(JSON.parse(bodyText)).toEqual({ + md5: '098f6bcd4621d373cade4e832627b4f6', + sha1: 'a94a8fe5ccb19ba61c4c0873d391e987982fbbd3', + hash: '9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08', + }); + }); + }); + + /** + * mbregex support + */ + describe( + 'mbregex extension support', + { skip: options.withXdebug }, + () => { + it('Should be able to use mb_regex_encoding functions', async () => { + const promise = php.run({ + code: ``, + }); + const response = await promise; + expect(response.errors).toBe(''); + }); + } + ); + + describe('64 bit integer support', { skip: options.withXdebug }, () => { + it('Should be able to use 64 bit integers', async () => { + const response = await php.run({ + code: ` { + const response = await php.run({ + code: ` $timestamp, + 'type' => gettype($timestamp), + ]);`, + }); + const result = JSON.parse(response.text); + expect(result.value).toEqual(2210555647); + expect(result.type).toBe('integer'); + }); + + it('Should handle adding 64 bit integers', async () => { + const response = await php.run({ + code: ` $product, + 'type' => gettype($product), + ]); + `, + }); + const result = JSON.parse(response.text); + expect(result.value + '').toEqual('9223372036854774000'); + expect(result.type).toEqual('integer'); + }); + + it('Should handle multiplying 64 bit integers', async () => { + const response = await php.run({ + code: ` $product, + 'type' => gettype($product), + ]); + `, + }); + const result = JSON.parse(response.text); + expect(result.value + '').toEqual('9223372036854774000'); + expect(result.type).toEqual('integer'); + }); + + it('Should handle large integer division', async () => { + const response = await php.run({ + code: ` $division, + 'type' => gettype($division), + ]);`, + }); + const result = JSON.parse(response.text); + expect(result.value + '').toEqual('4611686018427387000'); + expect(result.type).toEqual('integer'); + }); + + it('Should handle PHP_MAX_INT', async () => { + const response = await php.run({ + code: ` $maxInt, + 'type' => gettype($maxInt), + ]); + `, + }); + const result = JSON.parse(response.text); + expect(result.value + '').toEqual('9223372036854776000'); + expect(result.type).toEqual('integer'); + }); + }); + + /** + * fileinfo support + */ + describe( + 'fileinfo extension support', + { skip: options.withXdebug }, + () => { + it('Should be able to use finfo_file', async () => { + await php.writeFile( + '/test.php', + 'file('/test.php'); + ?>`, + }); + expect(response.text).toEqual('text/x-php'); + }); + } + ); + + /** + * exif support + */ + describe('exif extension support', { skip: options.withXdebug }, () => { + beforeEach(async () => { + await php.writeFile( + '/image.jpg', + new Uint8Array( + readFileSync( + joinPaths(__dirname, 'test-data', 'image.jpg') + ) + ) + ); + }); + it('should return correct image type using exif_imagetype', async () => { + const response = await php.run({ + code: ` { + const response = await php.run({ + code: ` { + const response = await php.run({ + code: ` { + const response = await php.run({ + code: ` { + it('should pass messages to JS', async () => { + let messageReceived = ''; + php.onMessage((message) => { + messageReceived = message; + }); + const out = await php.run({ + code: ` { + php.onMessage(async (message) => message + '!'); + const out = await php.run({ + code: ` { + php.onMessage(async (message) => { + // Simulate getting data asynchronously. + return await new Promise((resolve) => + setTimeout(() => resolve(message + '!'), 100) + ); + }); + const out = await php.run({ + code: ` { + php.onMessage(async () => { + // Simulate getting data asynchronously. + return await new Promise((resolve, reject) => + setTimeout(() => reject('Failure!'), 100) + ); + }); + const out = await php.run({ + code: ` { + let consoleLogMock: any; + let consoleErrorMock: any; + beforeEach(() => { + consoleLogMock = vi + .spyOn(console, 'log') + .mockImplementation(() => {}); + consoleErrorMock = vi + .spyOn(console, 'error') + .mockImplementation(() => {}); + }); + + afterAll(() => { + consoleLogMock.mockReset(); + consoleErrorMock.mockReset(); + }); + it('should not log an error message on exit status 0', async () => { + await php.cli(['php', '-r', '$tmp = "Hello";']); + expect(consoleLogMock).not.toHaveBeenCalled(); + expect(consoleErrorMock).not.toHaveBeenCalled(); + }); + + it('should define the PHP_BINARY constant', async () => { + const response = await php.cli([ + 'php', + '-r', + 'echo PHP_BINARY;', + ]); + expect(await response.stdoutText).toBe( + '/internal/shared/bin/php' + ); + }); + + it('should support multiple calls to php.cli() and php.runStream() when runtime rotation is enabled', async () => { + php.enableRuntimeRotation({ + maxRequests: 1, + recreateRuntime: () => + loadNodeRuntime(phpVersion as any, options), + }); + const response = await php.cli(['php', '-r', 'echo "Hello";']); + expect(await response.stdoutText).toBe('Hello'); + const response2 = await php.runStream({ + code: ` { + it('should encode response headers', async () => { + const out = await php.run({ + code: `[\\d]+)');`, + }); + expect(out.headers['location'][0]).toEqual('/(?P[\\d]+)'); + }); + }); + + describe('Disk space', { skip: options.withXdebug }, () => { + it('should return the correct total disk space', async () => { + const response = await php.run({ + code: ` { + const response = await php.run({ + code: ` { + php.writeFile('/test.txt', new Uint8Array(1024)); + const response = await php.run({ + code: ` { + const tempDir = mkdtempSync( + joinPaths(tmpdir(), 'php-wasm-test-') + ); + const filePath = joinPaths(tempDir, 'test.txt'); + writeFileSync(filePath, new Uint8Array(1024)); + php.mount('/tmp', createNodeFsMountHandler(tempDir)); + + const response = await php.run({ + code: ` { + const phpVersion = RecommendedPHPVersion; + let php: PHP; + let spawnedPhp: SpawnedPHP; + let processManager: PHPProcessManager; + beforeEach(async () => { + processManager = new PHPProcessManager({ + phpFactory: async () => { + const php = new PHP( + await loadNodeRuntime(phpVersion as any, {}) + ); + php.mkdir('/tmp/shared-test-directory'); + php.chdir('/tmp/shared-test-directory'); + + php.writeFile( + '/tmp/shared-test-directory/README.md', + 'Hello, world!' + ); + php.mkdir('/tmp/shared-test-directory/code'); + php.writeFile( + '/tmp/shared-test-directory/code/index.php', + 'Hello, world!' + ); + await php.setSpawnHandler( + sandboxedSpawnHandlerFactory(processManager) + ); + return php; + }, + maxPhpInstances: 5, + }); + spawnedPhp = await processManager.acquirePHPInstance(); + php = spawnedPhp.php; + }); + afterEach(async () => { + await processManager[Symbol.asyncDispose](); + spawnedPhp?.reap(); + }); + it.each([ + // Default cwd + { + command: 'ls', + expected: ['README.md', 'code', ''].join('\n'), + }, + // Explicit path + { + command: 'ls /tmp/shared-test-directory', + expected: ['README.md', 'code', ''].join('\n'), + }, + // Subdirectory – we expect a different output + { + command: 'ls /tmp/shared-test-directory/code', + expected: ['index.php', ''].join('\n'), + }, + // pwd + { + command: 'pwd', + expected: '/tmp/shared-test-directory\n', + }, + ])('should be able to run "$command"', async ({ command, expected }) => { + const response = await php.run({ + code: ` { + const response = await php.cli(['ls', '/tmp/shared-test-directory']); + expect(await response.stdoutText).toEqual( + ['README.md', 'code', ''].join('\n') + ); + }); +}); From 0982e3aa7d0554111c3e975a6edd507977e167c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Adam=20Zieli=C5=84ski?= Date: Tue, 25 Nov 2025 15:51:19 +0100 Subject: [PATCH 3/3] tweak the balance --- .github/workflows/ci.yml | 26 +++++++++++++------------- packages/php-wasm/node/project.json | 4 ++-- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1705388691..4f56fa3749 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -31,19 +31,19 @@ jobs: fail-fast: false matrix: include: - - name: test-unit-asyncify (1/6) + - name: test-unit-asyncify (1/7) target: test - - name: test-unit-asyncify (2/6) + - name: test-unit-asyncify (2/7) target: test-group-1-asyncify - - name: test-unit-asyncify (3/6) + - name: test-unit-asyncify (3/7) target: test-group-2-asyncify - - name: test-unit-asyncify (4/6) + - name: test-unit-asyncify (4/7) target: test-group-3-asyncify - - name: test-unit-asyncify (5/6) + - name: test-unit-asyncify (5/7) target: test-group-4-asyncify - - name: test-unit-asyncify (6/6) + - name: test-unit-asyncify (6/7) target: test-group-5-asyncify - - name: test-unit-asyncify (7/6) + - name: test-unit-asyncify (7/7) target: test-group-6-asyncify name: ${{ matrix.name }} services: @@ -79,17 +79,17 @@ jobs: fail-fast: false matrix: include: - - name: test-unit-jspi (1/5) + - name: test-unit-jspi (1/6) target: test-group-1-jspi - - name: test-unit-jspi (2/5) + - name: test-unit-jspi (2/6) target: test-group-2-jspi - - name: test-unit-jspi (3/5) + - name: test-unit-jspi (3/6) target: test-group-3-jspi - - name: test-unit-jspi (4/5) + - name: test-unit-jspi (4/6) target: test-group-4-jspi - - name: test-unit-jspi (5/5) + - name: test-unit-jspi (5/6) target: test-group-5-jspi - - name: test-unit-jspi (6/5) + - name: test-unit-jspi (6/6) target: test-group-6-jspi name: ${{ matrix.name }} services: diff --git a/packages/php-wasm/node/project.json b/packages/php-wasm/node/project.json index 11e0858019..168caf2a4d 100644 --- a/packages/php-wasm/node/project.json +++ b/packages/php-wasm/node/project.json @@ -235,7 +235,6 @@ "php-sqlite3.spec.ts", "php-networking.spec.ts", "php-dynamic-loading.spec.ts", - "php-crash.spec.ts", "php-mysqli.spec.ts", "php-process-manager.spec.ts" ] @@ -252,7 +251,6 @@ "php-sqlite3.spec.ts", "php-networking.spec.ts", "php-dynamic-loading.spec.ts", - "php-crash.spec.ts", "php-mysqli.spec.ts", "php-process-manager.spec.ts" ] @@ -264,6 +262,7 @@ "options": { "reportsDirectory": "../../../coverage/packages/php-wasm/node", "testFiles": [ + "php-crash.spec.ts", "php-ini.spec.ts", "write-files.spec.ts", "php-worker.spec.ts", @@ -281,6 +280,7 @@ "configFile": "packages/php-wasm/node/vite.jspi.config.ts", "reportsDirectory": "../../../coverage/packages/php-wasm/node", "testFiles": [ + "php-crash.spec.ts", "php-ini.spec.ts", "write-files.spec.ts", "php-worker.spec.ts",