diff --git a/packages/cli/src/commands/recipe-build.ts b/packages/cli/src/commands/recipe-build.ts index 6a55384d..184a9f7d 100644 --- a/packages/cli/src/commands/recipe-build.ts +++ b/packages/cli/src/commands/recipe-build.ts @@ -1,5 +1,5 @@ import { readFile, writeFile } from "node:fs/promises" -import { buildGenericAbilityRuntimeRunRecipe, buildRuntimePackageRunRecipe, buildWordPressBenchRecipe, buildWordPressPhpunitRecipe, compileRecipeTemplate, type GenericAbilityRuntimeRunOptions, type RecipeTemplateInput, type RuntimePackageRunRecipeOptions, type WorkspaceRecipe, type WorkspaceRecipeExtraPlugin, type WorkspaceRecipeMount, type WorkspaceRecipeRuntimeService, type WorkspaceRecipeStep } from "@automattic/wp-codebox-core" +import { buildGenericAbilityRuntimeRunRecipe, buildRuntimePackageRunRecipe, buildWordPressBenchRecipe, buildWordPressPhpunitRecipe, compileRecipeTemplate, type GenericAbilityRuntimeRunOptions, type RecipeTemplateInput, type RuntimePackageRunRecipeOptions, type WorkspaceRecipe, type WorkspaceRecipeExtraPlugin, type WorkspaceRecipeMount, type WorkspaceRecipePHPWasmExtensionManifest, type WorkspaceRecipeRuntimeBackendPackage, type WorkspaceRecipeRuntimeService, type WorkspaceRecipeStep } from "@automattic/wp-codebox-core" interface RecipeBuildOptions { recipeType: "phpunit" | "bench" | "template" | "generic-ability-runtime-run" | "runtime-package-run" @@ -10,6 +10,9 @@ interface RecipeBuildOptions { interface WordPressPhpunitBuilderOptions { blueprint?: unknown wordpressVersion?: string + phpVersion?: string + extensions?: WorkspaceRecipePHPWasmExtensionManifest[] + backendPackage?: WorkspaceRecipeRuntimeBackendPackage mounts?: WorkspaceRecipeMount[] services?: WorkspaceRecipeRuntimeService[] extra_plugins?: WorkspaceRecipeExtraPlugin[] @@ -76,6 +79,9 @@ function buildRecipe(recipeType: RecipeBuildOptions["recipeType"], options: Word return buildWordPressPhpunitRecipe({ blueprint: phpunitOptions.blueprint, wordpressVersion: stringOrUndefined(phpunitOptions.wordpressVersion), + phpVersion: stringOrUndefined(phpunitOptions.phpVersion), + extensions: Array.isArray(phpunitOptions.extensions) ? phpunitOptions.extensions : [], + backendPackage: phpunitOptions.backendPackage, mounts: Array.isArray(phpunitOptions.mounts) ? phpunitOptions.mounts : [], services: Array.isArray(phpunitOptions.services) ? phpunitOptions.services : [], extra_plugins: Array.isArray(phpunitOptions.extra_plugins) ? phpunitOptions.extra_plugins : [], diff --git a/packages/runtime-core/src/recipe-builders.ts b/packages/runtime-core/src/recipe-builders.ts index 50f8ba5a..48581b4d 100644 --- a/packages/runtime-core/src/recipe-builders.ts +++ b/packages/runtime-core/src/recipe-builders.ts @@ -1,4 +1,4 @@ -import type { WorkspaceRecipe, WorkspaceRecipeExtraPlugin, WorkspaceRecipeMount, WorkspaceRecipeRuntimeService, WorkspaceRecipeStep } from "./runtime-contracts.js" +import type { WorkspaceRecipe, WorkspaceRecipeExtraPlugin, WorkspaceRecipeMount, WorkspaceRecipePHPWasmExtensionManifest, WorkspaceRecipeRuntimeBackendPackage, WorkspaceRecipeRuntimeService, WorkspaceRecipeStep } from "./runtime-contracts.js" import { commandArg, commandJsonArg, commandStringListArg } from "./command-codecs.js" export { buildRuntimePackageRunRecipe, CODEBOX_RUN_RUNTIME_PACKAGE_ABILITY, RUNTIME_PACKAGE_ARTIFACT_DECLARATION_SCHEMA, RUNTIME_PACKAGE_EXECUTION_INPUT_SCHEMA, RUNTIME_PACKAGE_EXECUTION_RESULT_SCHEMA, RUNTIME_PACKAGE_OUTPUT_PROJECTION_SCHEMA, runtimePackageExecutionInput, type RuntimePackageArtifactDeclaration, type RuntimePackageExecutionInput, type RuntimePackageOutputProjection, type RuntimePackageRunRecipeOptions } from "./runtime-package-execution.js" export { RUNTIME_PACKAGE_DIAGNOSTIC_SCHEMA, RUNTIME_PACKAGE_RESULT_SCHEMA, RUNTIME_PACKAGE_TASK_SCHEMA, normalizeRuntimePackageResult, normalizeRuntimePackageTask, validateRuntimePackageTask, type RuntimePackageDiagnostic, type RuntimePackageResult, type RuntimePackageTask } from "./runtime-package-contracts.js" @@ -13,7 +13,10 @@ export interface NormalizeRecipeMountsOptions { export interface WordPressPhpunitRecipeOptions { wordpressVersion?: string + phpVersion?: string blueprint?: unknown + extensions?: WorkspaceRecipePHPWasmExtensionManifest[] + backendPackage?: WorkspaceRecipeRuntimeBackendPackage mounts?: WorkspaceRecipeMount[] services?: WorkspaceRecipeRuntimeService[] extra_plugins?: WorkspaceRecipeExtraPlugin[] @@ -73,7 +76,10 @@ export function buildWordPressPhpunitRecipe(options: WordPressPhpunitRecipeOptio schema: "wp-codebox/workspace-recipe/v1", runtime: { wp: options.wordpressVersion ?? DEFAULT_WORDPRESS_VERSION, + ...(options.phpVersion ? { phpVersion: options.phpVersion } : {}), blueprint: options.blueprint ?? { steps: [] }, + ...(options.extensions?.length ? { extensions: options.extensions } : {}), + ...(options.backendPackage ? { backendPackage: options.backendPackage } : {}), }, inputs: { extra_plugins: normalizeExtraPlugins(options.extra_plugins), diff --git a/packages/runtime-playground/src/phpunit-command-handlers.ts b/packages/runtime-playground/src/phpunit-command-handlers.ts index 7d998266..ee472671 100644 --- a/packages/runtime-playground/src/phpunit-command-handlers.ts +++ b/packages/runtime-playground/src/phpunit-command-handlers.ts @@ -370,6 +370,18 @@ $bootstrap_mode = ${JSON.stringify(options.bootstrapMode || "managed")}; $project_bootstrap = ${JSON.stringify(options.projectBootstrap)}; $multisite = ${JSON.stringify(options.multisite)}; +function pg_build_phpunit_argv($raw): array { + $phpunit_argv = array('phpunit'); + if (is_array($raw)) { + foreach ($raw as $arg) { + if (is_scalar($arg)) { + $phpunit_argv[] = (string) $arg; + } + } + } + return $phpunit_argv; +} + @file_put_contents($result_file, ''); function pg_log($msg) { @@ -1045,6 +1057,10 @@ try { } pg_apply_env($bench_env); +$phpunit_argv = pg_build_phpunit_argv($phpunit_args_raw); +$argv = $phpunit_argv; +$_SERVER['argv'] = $phpunit_argv; +$_SERVER['argc'] = count($phpunit_argv); if (!is_array($wp_config_defines)) { $wp_config_defines = array(); @@ -1291,14 +1307,6 @@ function wp_codebox_phpunit_print_test_list($test) { pg_stage_begin('run_tests'); pg_log('RUNNING ' . count($test_files) . ' TEST FILES'); try { - $phpunit_argv = array('phpunit'); - if (is_array($phpunit_args_raw)) { - foreach ($phpunit_args_raw as $arg) { - if (is_scalar($arg)) { - $phpunit_argv[] = (string) $arg; - } - } - } $phpunit_args = wp_codebox_phpunit_args($phpunit_argv); if (!empty($phpunit_args['listTests'])) { wp_codebox_phpunit_print_test_list($suite); diff --git a/packages/runtime-playground/src/playground-cli-runner.ts b/packages/runtime-playground/src/playground-cli-runner.ts index 749f1d67..976c167a 100644 --- a/packages/runtime-playground/src/playground-cli-runner.ts +++ b/packages/runtime-playground/src/playground-cli-runner.ts @@ -33,6 +33,7 @@ export interface PlaygroundCliModule { wordpressInstallMode?: "install-from-existing-files" | "install-from-existing-files-if-needed" | "do-not-attempt-installing" "site-url"?: string phpIniEntries?: Record + phpExtension?: string[] }): Promise } @@ -137,6 +138,7 @@ export async function startPlaygroundCliServer(spec: RuntimeCreateSpec, mounts: } : {}), wp: localAssetServer?.url ?? wordpressStartupAsset?.wp, php: spec.environment.phpVersion, + ...(spec.environment.extensions?.length ? { phpExtension: spec.environment.extensions.map((extension) => extension.manifest) } : {}), phpIniEntries: pluginRuntimePhpIniEntries(spec), "site-url": spec.preview?.siteUrl, blueprint: playgroundCliBlueprint(spec), diff --git a/packages/runtime-playground/src/wordpress-command-runners.ts b/packages/runtime-playground/src/wordpress-command-runners.ts index c717d6a2..cb967b6a 100644 --- a/packages/runtime-playground/src/wordpress-command-runners.ts +++ b/packages/runtime-playground/src/wordpress-command-runners.ts @@ -952,6 +952,10 @@ export async function runPhpunitCommand({ await persistPluginPhpunitResult(server, resultFile, artifactRoot) await persistVfsDiagnosticFileToHost(server, resultFile, `/wordpress/wp-content/plugins/${pluginSlug}/.pg-test-result.txt`, mounts) assertPlaygroundResponseOk("wordpress.phpunit", response) + const structured = await readPluginPhpunitDiagnostic(server, resultFile) + if (structured) { + throw new Error(`wordpress.phpunit could not run: ${structured}`) + } return response.text } diff --git a/tests/phpunit-project-autoload.test.ts b/tests/phpunit-project-autoload.test.ts index 18dde629..28d371de 100644 --- a/tests/phpunit-project-autoload.test.ts +++ b/tests/phpunit-project-autoload.test.ts @@ -254,7 +254,9 @@ const projectModeCode = phpunitRunCode({ const bootIndex = projectModeCode.indexOf("$config_path = pg_run_boot_stage") const projectBootstrapIndex = projectModeCode.indexOf("pg_run_project_bootstrap_stage", bootIndex) const projectAutoloadIndex = projectModeCode.indexOf("pg_run_project_autoload_stage", projectBootstrapIndex) +const phpunitArgvIndex = projectModeCode.indexOf("$_SERVER['argv'] = $phpunit_argv;") assert.ok(bootIndex > 0) +assert.ok(phpunitArgvIndex > 0 && phpunitArgvIndex < projectBootstrapIndex, "project bootstrap must receive forwarded PHPUnit arguments") assert.ok(projectBootstrapIndex > bootIndex) assert.ok(projectAutoloadIndex > projectBootstrapIndex) assert.ok(projectModeCode.includes("'autoload_required' => $bootstrap_mode !== 'project' || $harness_autoload_file !== ''")) diff --git a/tests/playground-cli-runner-bootstrap-ini.test.ts b/tests/playground-cli-runner-bootstrap-ini.test.ts index 80825dcc..0f5257bb 100644 --- a/tests/playground-cli-runner-bootstrap-ini.test.ts +++ b/tests/playground-cli-runner-bootstrap-ini.test.ts @@ -33,6 +33,7 @@ try { phpVersion: "8.4", wordpressInstallMode: "do-not-attempt-installing", assets: { wordpressDirectory }, + extensions: [{ manifest: "/tmp/sodium/manifest.json" }], blueprint: {}, }, policy: { @@ -68,6 +69,7 @@ try { assert.equal(calls[0].workers, 6) assert.equal(calls[0].wordpressInstallMode, "do-not-attempt-installing") assert.deepEqual(calls[0].phpIniEntries, { memory_limit: "512M" }) + assert.deepEqual(calls[0].phpExtension, ["/tmp/sodium/manifest.json"]) const sharedMount = calls[0]["mount-before-install"]?.[0]?.hostPath assert.equal(typeof sharedMount, "string") const sharedPhpIni = await readFile(join(sharedMount as string, "php.ini"), "utf8") diff --git a/tests/runtime-services.test.ts b/tests/runtime-services.test.ts index 84d55f46..94ce0550 100644 --- a/tests/runtime-services.test.ts +++ b/tests/runtime-services.test.ts @@ -29,6 +29,9 @@ try { const recipePath = join(builderDirectory, "recipe.json") await writeFile(optionsPath, JSON.stringify({ pluginSlug: "example", + phpVersion: "8.3", + extensions: [{ manifest: "./sodium/manifest.json" }], + backendPackage: { kind: "playground", source: "./playground-cli", package: "@wp-playground/cli" }, testRoot: "/home/example/bin/tests/core", phpunitXml: "/home/example/bin/tests/core/phpunit.xml", })) @@ -36,6 +39,9 @@ try { const builtRecipe = JSON.parse(await readFile(recipePath, "utf8")) as WorkspaceRecipe assert.ok(builtRecipe.workflow.steps[0].args?.includes("test-root=/home/example/bin/tests/core")) assert.ok(builtRecipe.workflow.steps[0].args?.includes("phpunit-xml=/home/example/bin/tests/core/phpunit.xml")) + assert.equal(builtRecipe.runtime?.phpVersion, "8.3") + assert.deepEqual(builtRecipe.runtime?.extensions, [{ manifest: "./sodium/manifest.json" }]) + assert.deepEqual(builtRecipe.runtime?.backendPackage, { kind: "playground", source: "./playground-cli", package: "@wp-playground/cli" }) await writeFile(optionsPath, JSON.stringify({ pluginSlug: "example" })) assert.equal(await runRecipeBuildCommand(["phpunit", "--options", optionsPath, "--output", recipePath]), 0)