Skip to content

Commit 5ac3898

Browse files
committed
Capture malformed PHPUnit bootstrap payloads
1 parent 6bbfc83 commit 5ac3898

3 files changed

Lines changed: 39 additions & 3 deletions

File tree

packages/runtime-playground/src/php-bootstrap.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,7 @@ export function bootstrapPhpCode(spec: RuntimeCreateSpec, code: string, args: st
2727

2828
const command = splitLeadingStrictTypesDeclare(code)
2929

30-
return `<?php
30+
const bootstrapped = `<?php
3131
${command.strictTypesDeclare ? `${command.strictTypesDeclare}\n` : ""}${phpFatalDiagnosticPhp()}
3232
${failureDiagnosticFile ? phpFailureDiagnosticFilePhp(failureDiagnosticFile) : ""}
3333
${phpCliStreamConstants()}
@@ -43,6 +43,18 @@ ${wpCliBridge ? `putenv(${JSON.stringify(`WP_CODEBOX_TERMINAL_ACTION_URL=${wpCli
4343
putenv(${JSON.stringify(`WP_CODEBOX_TERMINAL_ACTION_TOKEN=${wpCliBridge.token}`)});
4444
` : ""}
4545
${command.body}`
46+
47+
return failureDiagnosticFile ? phpFailureDiagnosticWrapperPhp(bootstrapped, failureDiagnosticFile) : bootstrapped
48+
}
49+
50+
function phpFailureDiagnosticWrapperPhp(code: string, path: string): string {
51+
return `<?php
52+
try {
53+
eval('?>' . base64_decode(${JSON.stringify(Buffer.from(code, "utf8").toString("base64"))}));
54+
} catch (Throwable $wp_codebox_bootstrap_throwable) {
55+
@file_put_contents(${JSON.stringify(path)}, 'STAGE_FAIL:bootstrap:' . get_class($wp_codebox_bootstrap_throwable) . ': ' . $wp_codebox_bootstrap_throwable->getMessage() . ' at ' . $wp_codebox_bootstrap_throwable->getFile() . ':' . $wp_codebox_bootstrap_throwable->getLine() . "\\n", FILE_APPEND);
56+
throw $wp_codebox_bootstrap_throwable;
57+
}`
4658
}
4759

4860
function phpFailureDiagnosticFilePhp(path: string): string {

tests/phpunit-runtime-failure-diagnostics.test.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,8 +42,11 @@ const captured = await readFile(join(artifactRoot, "files", "phpunit", ".pg-test
4242
assert.match(captured, /Bootstrap failed with token: \[redacted\]/)
4343
assert.doesNotMatch(captured, new RegExp(secret))
4444

45-
const preBootstrapRecorder = submittedCode.indexOf("STAGE_FATAL:bootstrap:")
46-
const wordpressBootstrap = submittedCode.indexOf("require_once '/wordpress/wp-load.php';")
45+
const encodedBootstrap = submittedCode.match(/base64_decode\("([A-Za-z0-9+/=]+)"\)/)?.[1]
46+
assert.ok(encodedBootstrap, "PHPUnit payload must execute inside the bootstrap diagnostic wrapper")
47+
const decodedBootstrap = Buffer.from(encodedBootstrap, "base64").toString("utf8")
48+
const preBootstrapRecorder = decodedBootstrap.indexOf("STAGE_FATAL:bootstrap:")
49+
const wordpressBootstrap = decodedBootstrap.indexOf("require_once '/wordpress/wp-load.php';")
4750
assert.ok(preBootstrapRecorder >= 0 && preBootstrapRecorder < wordpressBootstrap, "fatal diagnostics must be recorded before the WordPress bootstrap boundary")
4851

4952
console.log("phpunit runtime failure diagnostics ok")

tests/playground-phpunit-bootstrap-failure.integration.test.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ const fatalMuPlugin = join(root, "fatal-bootstrap.php")
1212
const recipePath = join(root, "recipe.json")
1313
const artifactsPath = join(root, "artifacts")
1414
const exitArtifactsPath = join(root, "exit-artifacts")
15+
const parseArtifactsPath = join(root, "parse-artifacts")
1516

1617
try {
1718
await writeFile(fatalMuPlugin, `<?php\ntrigger_error('PHPUnit bootstrap fixture token: ${secret}', E_USER_ERROR);\n`)
@@ -65,6 +66,26 @@ try {
6566
const exitDiagnostic = await readFile(join(exitArtifactsPath, exitRuntimeDirectory, "files", "phpunit", ".pg-test-result.txt"), "utf8")
6667
assert.match(exitDiagnostic, /STAGE_DIE:bootstrap:PHPUnit bootstrap exit fixture token: \[redacted\]/)
6768
assert.doesNotMatch(exitDiagnostic, new RegExp(secret))
69+
70+
await writeFile(recipePath, `${JSON.stringify({
71+
schema: "wp-codebox/workspace-recipe/v1",
72+
runtime: { backend: "wordpress-playground", wp: "6.5", blueprint: { steps: [] } },
73+
inputs: { mounts: [] },
74+
workflow: {
75+
steps: [{ command: "wordpress.phpunit", args: ["plugin-slug=bootstrap-failure-fixture", "code=<?php function malformed("] }],
76+
},
77+
})}\n`)
78+
const parseOutput = await runFailedRecipe(parseArtifactsPath)
79+
assert.equal(parseOutput.success, false, JSON.stringify(parseOutput))
80+
const parseMessage = parseOutput.error?.message ?? ""
81+
assert.match(parseMessage, /wordpress\.phpunit structured diagnostics/)
82+
assert.match(parseMessage, /ParseError/)
83+
84+
const parseLatest = JSON.parse(await readFile(join(parseArtifactsPath, "latest-runtime.json"), "utf8")) as { paths?: { runtimeDirectory?: string } }
85+
const parseRuntimeDirectory = parseLatest.paths?.runtimeDirectory
86+
assert.ok(parseRuntimeDirectory, "malformed recipe must retain a runtime artifact directory")
87+
const parseDiagnostic = await readFile(join(parseArtifactsPath, parseRuntimeDirectory, "files", "phpunit", ".pg-test-result.txt"), "utf8")
88+
assert.match(parseDiagnostic, /STAGE_FAIL:bootstrap:ParseError/)
6889
} finally {
6990
await rm(root, { recursive: true, force: true })
7091
}

0 commit comments

Comments
 (0)