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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions packages/cli/src/commands/recipe-build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ interface WordPressPhpunitBuilderOptions {
autoloadFile?: string
projectAutoloadFile?: string
testsDir?: string
testRoot?: string
phpunitXml?: string
dependencyMounts?: string[]
bootstrapFiles?: string[]
preloadFiles?: string[]
Expand Down Expand Up @@ -87,6 +89,8 @@ function buildRecipe(recipeType: RecipeBuildOptions["recipeType"], options: Word
autoloadFile: stringOrUndefined(phpunitOptions.autoloadFile),
projectAutoloadFile: stringOrUndefined(phpunitOptions.projectAutoloadFile),
testsDir: stringOrUndefined(phpunitOptions.testsDir),
testRoot: stringOrUndefined(phpunitOptions.testRoot),
phpunitXml: stringOrUndefined(phpunitOptions.phpunitXml),
dependencyMounts: Array.isArray(phpunitOptions.dependencyMounts) ? phpunitOptions.dependencyMounts : [],
bootstrapFiles: Array.isArray(phpunitOptions.bootstrapFiles) ? phpunitOptions.bootstrapFiles : [],
preloadFiles: Array.isArray(phpunitOptions.preloadFiles) ? phpunitOptions.preloadFiles : [],
Expand Down
1 change: 1 addition & 0 deletions packages/runtime-core/src/recipe-builders.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,7 @@ export function buildWordPressPhpunitRecipe(options: WordPressPhpunitRecipeOptio
commandJsonArg("env-json", options.env ?? {}),
commandJsonArg("wp-config-defines-json", options.wpConfigDefines ?? {}),
commandArg("autoload-file", options.autoloadFile ?? "/wp-codebox-vendor/autoload.php"),
commandArg("autoload-file-role", "harness"),
commandArg("project-autoload-file", options.projectAutoloadFile ?? ""),
commandArg("tests-dir", options.testsDir ?? "/wp-codebox-vendor/wp-phpunit/wp-phpunit"),
commandArg("test-root", options.testRoot ?? `${pluginTarget}/tests`),
Expand Down
4 changes: 3 additions & 1 deletion packages/runtime-playground/src/phpunit-command-handlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ export interface PhpunitRunCodeOptions {
pluginSlug: string
cwd: string
autoloadFile: string
autoloadFileRole?: "harness"
projectAutoloadFile?: string
testsDir: string
testRoot?: string
Expand Down Expand Up @@ -353,6 +354,7 @@ $result_file = ${JSON.stringify(options.resultFile ?? PLUGIN_PHPUNIT_RESULT_FILE
$current_stage = 'preboot';
$pg_stage_output_buffering = false;
$autoload_file = ${JSON.stringify(options.autoloadFile)};
$autoload_file_role = ${JSON.stringify(options.autoloadFileRole ?? "")};
$project_autoload_file = ${JSON.stringify(options.projectAutoloadFile ?? "")};
$tests_dir = ${JSON.stringify(options.testsDir)};
$test_root = ${JSON.stringify(options.testRoot || `/wordpress/wp-content/plugins/${options.pluginSlug}/tests`)};
Expand Down Expand Up @@ -1062,7 +1064,7 @@ if ($multisite) {
}

$legacy_project_autoload_file = '';
if ($bootstrap_mode === 'project' && $project_autoload_file === '' && $autoload_file !== '' && $autoload_file !== '/wp-codebox-vendor/autoload.php') {
if ($autoload_file_role === '' && $bootstrap_mode === 'project' && $project_autoload_file === '' && $autoload_file !== '' && $autoload_file !== '/wp-codebox-vendor/autoload.php') {
$legacy_project_autoload_file = $autoload_file;
}
$harness_autoload_file = $legacy_project_autoload_file !== '' ? '/wp-codebox-vendor/autoload.php' : $autoload_file;
Expand Down
2 changes: 2 additions & 0 deletions packages/runtime-playground/src/wordpress-command-runners.ts
Original file line number Diff line number Diff line change
Expand Up @@ -909,11 +909,13 @@ export async function runPhpunitCommand({
const pluginSlug = argValue(args, "plugin-slug")?.trim() || ""
const bootstrapMode = argValue(args, "bootstrap-mode")?.trim() || "managed"
const autoloadFile = argValue(args, "autoload-file")?.trim() || (bootstrapMode === "project" ? "" : "/wp-codebox-vendor/autoload.php")
const autoloadFileRole = argValue(args, "autoload-file-role")?.trim() === "harness" ? "harness" : undefined
const resultFile = PLUGIN_PHPUNIT_RESULT_FILE
const code = explicitCode ? await phpCodeFromArgs(args, "wordpress.phpunit") : normalizePhpCode(phpunitRunCode({
pluginSlug,
cwd: argValue(args, "cwd")?.trim() || `/wordpress/wp-content/plugins/${pluginSlug}`,
autoloadFile,
autoloadFileRole,
projectAutoloadFile: argValue(args, "project-autoload-file")?.trim() || "",
testsDir: argValue(args, "tests-dir")?.trim() || "/wp-codebox-vendor/wp-phpunit/wp-phpunit",
testRoot: argValue(args, "test-root")?.trim() || `/wordpress/wp-content/plugins/${pluginSlug}/tests`,
Expand Down
49 changes: 45 additions & 4 deletions tests/phpunit-project-autoload.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -201,10 +201,12 @@ assert.deepEqual(recipe.workflow.steps[0].args.filter((arg) => arg.includes("aut
"autoload-file=/wp-codebox-vendor/autoload.php",
`project-autoload-file=${woocommerceAutoload}`,
])
assert.ok(recipe.workflow.steps[0].args.includes("autoload-file-role=harness"), "modern PHPUnit recipes explicitly preserve harness autoload intent")
assert.deepEqual(rewriteInputMountPathArgs(recipe.workflow.steps[0].args, recipeInputMountPathMap(recipe)).filter((arg) => arg.includes("autoload-file=")), [
"autoload-file=/tmp/wp-codebox-inputs/0-wp-codebox-vendor-73845ca47d2f/autoload.php",
`project-autoload-file=${woocommerceAutoload}`,
])
assert.ok(rewriteInputMountPathArgs(recipe.workflow.steps[0].args, recipeInputMountPathMap(recipe)).includes("autoload-file-role=harness"), "CLI path canonicalization preserves autoload intent")
assert.ok(recipe.workflow.steps[0].args.includes("cwd=/home/example/public_html"))
assert.ok(recipe.workflow.steps[0].args.includes("test-root=/home/example/public_html/bin/tests/phpunit"))
assert.ok(recipe.workflow.steps[0].args.includes("phpunit-xml=/home/example/public_html/bin/tests/phpunit/phpunit.xml.dist"))
Expand Down Expand Up @@ -236,6 +238,8 @@ assert.ok(projectBootstrapIndex > bootIndex)
assert.ok(projectAutoloadIndex > projectBootstrapIndex)
assert.ok(projectModeCode.includes("'autoload_required' => $bootstrap_mode !== 'project' || $harness_autoload_file !== ''"))
assert.ok(projectModeCode.includes("$legacy_project_autoload_file = $autoload_file"))
assert.ok(projectModeCode.includes('$autoload_file_role = "";'), "direct callers without an explicit role retain the legacy compatibility path")
assert.ok(projectModeCode.includes("if ($autoload_file_role === '' && $bootstrap_mode === 'project'"))
assert.ok(projectModeCode.includes("configured PHPUnit harness autoload file is not readable"))
assert.ok(projectModeCode.includes("NOTICE:project bootstrap mode continuing without configured PHPUnit harness autoload"))
assert.ok(projectModeCode.includes("$test_root = \"/home/example/public_html/bin/tests/phpunit\";"))
Expand All @@ -262,12 +266,47 @@ assert.ok(verifyHarnessIndex > 0, "verify_harness stage must be present")
assert.ok(projectModeTestsuiteIndex > verifyHarnessIndex, "harness verification must precede TestSuite construction")
assertProjectBootstrapHarnessGuard(projectModeCode)

let capturedDefaultProjectCode = ""
const canonicalHarnessProjectModeCode = phpunitRunCode({
pluginSlug: "woocommerce",
cwd: "/wordpress/wp-content/plugins/woocommerce",
autoloadFile: "/tmp/wp-codebox-inputs/0-wp-codebox-vendor-73845ca47d2f/autoload.php",
autoloadFileRole: "harness",
projectAutoloadFile: woocommerceAutoload,
testsDir: "/tmp/wp-codebox-inputs/0-wp-codebox-vendor-73845ca47d2f/wp-phpunit/wp-phpunit",
testRoot: "/home/example/public_html/bin/tests/phpunit",
phpunitXml: "/wordpress/wp-content/plugins/woocommerce/phpunit.xml.dist",
selectedTestFile: "",
changedTestFiles: [],
phpunitArgs: [],
env: {},
wpConfigDefines: {},
dependencyMounts: [],
bootstrapFiles: [],
bootstrapMode: "project",
projectBootstrap: "tests/legacy/bootstrap.php",
multisite: false,
})
assert.ok(canonicalHarnessProjectModeCode.includes('$autoload_file_role = "harness";'))
assert.ok(canonicalHarnessProjectModeCode.includes('$harness_autoload_file = $legacy_project_autoload_file !== \'\' ? \'/wp-codebox-vendor/autoload.php\' : $autoload_file;'))
const canonicalHarnessResolution = canonicalHarnessProjectModeCode.match(/\$legacy_project_autoload_file = '';[\s\S]*?\$harness_autoload_file = [^;]+;/)?.[0]
assert.ok(canonicalHarnessResolution, "generated project-mode code must resolve harness autoload intent")
const canonicalHarnessProbe = join(mkdtempSync(join(tmpdir(), "wp-codebox-canonical-harness-")), "probe.php")
writeFileSync(canonicalHarnessProbe, `<?php
$bootstrap_mode = 'project';
$autoload_file = '/tmp/wp-codebox-inputs/0-wp-codebox-vendor-73845ca47d2f/autoload.php';
$autoload_file_role = 'harness';
$project_autoload_file = ${phpString(woocommerceAutoload)};
${canonicalHarnessResolution}
echo json_encode(array($legacy_project_autoload_file, $harness_autoload_file));
`)
assert.deepEqual(JSON.parse(execFileSync("php", [canonicalHarnessProbe], { encoding: "utf8" })), ["", "/tmp/wp-codebox-inputs/0-wp-codebox-vendor-73845ca47d2f/autoload.php"], "a canonical staged harness path remains the harness in project mode")

let capturedCanonicalHarnessCode = ""
await runPhpunitCommand({
artifactRoot: mkdtempSync(join(tmpdir(), "wp-codebox-phpunit-artifacts-")),
mounts: [],
runPlaygroundCommand: async (_command, _server, input) => {
capturedDefaultProjectCode = input.code
capturedCanonicalHarnessCode = input.code
return { text: "ok", exitCode: 0 }
},
server: { playground: {} } as never,
Expand All @@ -276,13 +315,15 @@ await runPhpunitCommand({
args: [
"plugin-slug=ai-provider-for-openai",
"bootstrap-mode=project",
"autoload-file=/tmp/wp-codebox-inputs/0-wp-codebox-vendor-73845ca47d2f/autoload.php",
"autoload-file-role=harness",
"phpunit-xml=phpunit.xml.dist",
"test-file=tests/unit/Models/OpenAiEmbeddingGenerationModelTest.php",
],
},
})
assert.ok(capturedDefaultProjectCode.includes('$autoload_file = "";'))
assert.ok(capturedDefaultProjectCode.includes("NOTICE:project bootstrap mode continuing without configured PHPUnit harness autoload"))
assert.ok(capturedCanonicalHarnessCode.includes('$autoload_file = "/tmp/wp-codebox-inputs/0-wp-codebox-vendor-73845ca47d2f/autoload.php";'))
assert.ok(capturedCanonicalHarnessCode.includes('$autoload_file_role = "harness";'))

const coreModeCode = corePhpunitRunCode({
coreRoot: "/wordpress",
Expand Down
20 changes: 20 additions & 0 deletions tests/runtime-services.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import assert from "node:assert/strict"
import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises"
import { createServer } from "node:net"
import { tmpdir } from "node:os"
import { join } from "node:path"
import { runRecipeBuildCommand } from "../packages/cli/src/commands/recipe-build.ts"
import { parseLoopbackPort, provisionRuntimeServices, provisionRuntimeServicesForRecipe, RuntimeServiceProvisionError, runtimeServiceEvidenceFromError, runtimeServicePlan, waitForMysqlProtocol, type RuntimeServiceDependencies } from "../packages/cli/src/runtime-services.ts"
import { planWorkspaceRecipe } from "../packages/cli/src/recipe-dry-run.ts"
import { validateWorkspaceRecipeSemantics } from "../packages/cli/src/recipe-validation.ts"
Expand All @@ -19,6 +23,22 @@ assert.equal(unsafe.valid, false)
const emptyRootService = { ...service, configuration: { rootAuthentication: "empty-password" as const } }
assert.equal(validateWorkspaceRecipeJsonSchema({ schema: "wp-codebox/workspace-recipe/v1", inputs: { services: [emptyRootService] }, workflow: { steps: [{ command: "wordpress.run-php" }] } }).valid, true)
assert.deepEqual(buildWordPressPhpunitRecipe({ pluginSlug: "example", services: [emptyRootService] }).inputs?.services, [emptyRootService])
const builderDirectory = await mkdtemp(join(tmpdir(), "wp-codebox-phpunit-builder-"))
try {
const optionsPath = join(builderDirectory, "options.json")
const recipePath = join(builderDirectory, "recipe.json")
await writeFile(optionsPath, JSON.stringify({
pluginSlug: "example",
testRoot: "/home/example/bin/tests/core",
phpunitXml: "/home/example/bin/tests/core/phpunit.xml",
}))
assert.equal(await runRecipeBuildCommand(["phpunit", "--options", optionsPath, "--output", recipePath]), 0)
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"))
} finally {
await rm(builderDirectory, { recursive: true, force: true })
}
const recipe: WorkspaceRecipe = { schema: "wp-codebox/workspace-recipe/v1", inputs: { services: [service] }, workflow: { steps: [{ command: "wordpress.run-php", args: ["code=echo 'ok';"] }] } }
assert.deepEqual(await validateWorkspaceRecipeSemantics(recipe, "recipe.json"), [])
const dryRun = await planWorkspaceRecipe(recipe, process.cwd(), { recipePath: "recipe.json" }, {
Expand Down
Loading