Skip to content

Commit 4e5874b

Browse files
authored
Capture malformed PHPUnit bootstrap payloads (#1933)
* Capture malformed PHPUnit bootstrap payloads * Treat empty PHPUnit suites as successful
1 parent 7befb07 commit 4e5874b

5 files changed

Lines changed: 44 additions & 6 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 {

packages/runtime-playground/src/phpunit-command-handlers.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1236,7 +1236,7 @@ try {
12361236
pg_log('NO_TEST_FILES');
12371237
pg_log('NOTICE:tests directory not found at ' . $test_dir);
12381238
pg_stage_ok('discover_tests');
1239-
exit(1);
1239+
exit(0);
12401240
}
12411241
list($directories, $suffixes, $prefixes, $excludes, $configured_files) = wp_codebox_phpunit_parse_config(${JSON.stringify(options.phpunitXml)}, $test_dir);
12421242
$test_files = wp_codebox_phpunit_discover($directories, $suffixes, $prefixes, $excludes, $configured_files);
@@ -1261,7 +1261,7 @@ try {
12611261
if (empty($test_files)) {
12621262
pg_log('NO_TEST_FILES');
12631263
pg_stage_ok('discover_tests');
1264-
exit(1);
1264+
exit(0);
12651265
}
12661266
pg_stage_ok('discover_tests');
12671267
} catch (Throwable $e) {
@@ -1587,7 +1587,7 @@ try {
15871587
if (empty($test_files)) {
15881588
core_pg_log('NO_TEST_FILES');
15891589
core_pg_stage_ok('discover_tests');
1590-
exit(1);
1590+
exit(0);
15911591
}
15921592
core_pg_stage_ok('discover_tests');
15931593
} catch (Throwable $e) {

tests/phpunit-project-autoload.test.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -358,6 +358,7 @@ assert.ok(projectModeCode.includes("$test_files = wp_codebox_phpunit_discover($d
358358
assert.ok(projectModeCode.includes("' files=' . count($configured_files)"))
359359
assert.equal(projectModeCode.match(/return array\(\$directories, \$suffixes, \$prefixes, \$excludes\);/g)?.length ?? 0, 0)
360360
assert.equal(projectModeCode.match(/return \$return_values\(\);/g)?.length, 3)
361+
assert.match(projectModeCode, /if \(empty\(\$test_files\)\) \{\s+pg_log\('NO_TEST_FILES'\);\s+pg_stage_ok\('discover_tests'\);\s+exit\(0\);/)
361362
assertPhpunitParseConfigFallbacksReturnFiveTuple(projectModeCode, "wp_codebox_phpunit_parse_config", "pg_log")
362363
assertSelectedTestFileResolution(projectModeCode)
363364

@@ -466,6 +467,7 @@ assert.ok(coreModeCode.includes("list($directories, $suffixes, $prefixes, $exclu
466467
assert.ok(coreModeCode.includes("$test_files = core_pg_discover_tests($directories, $suffixes, $prefixes, $excludes, $configured_files);"))
467468
assert.equal(coreModeCode.match(/return array\(\$directories, \$suffixes, \$prefixes, \$excludes\);/g)?.length ?? 0, 0)
468469
assert.equal(coreModeCode.match(/return \$return_values\(\);/g)?.length, 3)
470+
assert.match(coreModeCode, /if \(empty\(\$test_files\)\) \{\s+core_pg_log\('NO_TEST_FILES'\);\s+core_pg_stage_ok\('discover_tests'\);\s+exit\(0\);/)
469471
assertPhpunitParseConfigFallbacksReturnFiveTuple(coreModeCode, "core_pg_parse_phpunit_config", "core_pg_log")
470472
for (const privateConstructor of [true, false]) {
471473
assertDiscoveredTestExecutes(projectModeCode, "pg", privateConstructor)

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)