Skip to content

Commit d804f12

Browse files
authored
Retain non-fatal PHPUnit bootstrap exits (#1932)
1 parent c43759a commit d804f12

2 files changed

Lines changed: 54 additions & 6 deletions

File tree

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

Lines changed: 36 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ ${runtimeEnvPhp(spec, args)}
3737
${secretEnvPhp(spec)}
3838
${componentManifestPhp(spec)}
3939
require_once '/wordpress/wp-load.php';
40+
${failureDiagnosticFile ? phpFailureDiagnosticCompletionPhp() : ""}
4041
${recipeActivePluginBootstrapPhp(spec, args)}
4142
${wpCliBridge ? `putenv(${JSON.stringify(`WP_CODEBOX_TERMINAL_ACTION_URL=${wpCliBridge.url}`)});
4243
putenv(${JSON.stringify(`WP_CODEBOX_TERMINAL_ACTION_TOKEN=${wpCliBridge.token}`)});
@@ -45,15 +46,46 @@ ${command.body}`
4546
}
4647

4748
function phpFailureDiagnosticFilePhp(path: string): string {
48-
return `register_shutdown_function(static function (): void {
49-
$wp_codebox_failure = error_get_last();
50-
if (!is_array($wp_codebox_failure) || !in_array($wp_codebox_failure['type'] ?? 0, array(E_ERROR, E_PARSE, E_CORE_ERROR, E_COMPILE_ERROR, E_USER_ERROR), true)) {
49+
return `$wp_codebox_bootstrap_complete = false;
50+
$wp_codebox_bootstrap_buffer_level = ob_get_level();
51+
ob_start();
52+
register_shutdown_function(static function () use (&$wp_codebox_bootstrap_complete, $wp_codebox_bootstrap_buffer_level): void {
53+
if ($wp_codebox_bootstrap_complete) {
5154
return;
5255
}
53-
@file_put_contents(${JSON.stringify(path)}, 'STAGE_FATAL:bootstrap:' . (string) ($wp_codebox_failure['message'] ?? '') . ' at ' . (string) ($wp_codebox_failure['file'] ?? '') . ':' . (int) ($wp_codebox_failure['line'] ?? 0) . "\\n", FILE_APPEND);
56+
$wp_codebox_bootstrap_output = '';
57+
while (ob_get_level() > $wp_codebox_bootstrap_buffer_level) {
58+
$wp_codebox_bootstrap_chunk = ob_get_clean();
59+
if ($wp_codebox_bootstrap_chunk !== false) {
60+
$wp_codebox_bootstrap_output = $wp_codebox_bootstrap_chunk . $wp_codebox_bootstrap_output;
61+
}
62+
}
63+
$wp_codebox_failure = error_get_last();
64+
if (is_array($wp_codebox_failure) && in_array($wp_codebox_failure['type'] ?? 0, array(E_ERROR, E_PARSE, E_CORE_ERROR, E_COMPILE_ERROR, E_USER_ERROR, E_RECOVERABLE_ERROR), true)) {
65+
$wp_codebox_bootstrap_diagnostic = 'STAGE_FATAL:bootstrap:' . (string) ($wp_codebox_failure['message'] ?? '') . ' at ' . (string) ($wp_codebox_failure['file'] ?? '') . ':' . (int) ($wp_codebox_failure['line'] ?? 0);
66+
} else {
67+
$wp_codebox_bootstrap_detail = trim((string) preg_replace('/\\s+/', ' ', strip_tags($wp_codebox_bootstrap_output)));
68+
if ($wp_codebox_bootstrap_detail === '') {
69+
$wp_codebox_bootstrap_detail = 'WordPress bootstrap terminated before completion without emitting output';
70+
}
71+
$wp_codebox_bootstrap_diagnostic = 'STAGE_DIE:bootstrap:' . substr($wp_codebox_bootstrap_detail, 0, 16384);
72+
}
73+
@file_put_contents(${JSON.stringify(path)}, $wp_codebox_bootstrap_diagnostic . "\\n", FILE_APPEND);
5474
});`
5575
}
5676

77+
function phpFailureDiagnosticCompletionPhp(): string {
78+
return `$wp_codebox_bootstrap_complete = true;
79+
$wp_codebox_bootstrap_output = '';
80+
while (ob_get_level() > $wp_codebox_bootstrap_buffer_level) {
81+
$wp_codebox_bootstrap_chunk = ob_get_clean();
82+
if ($wp_codebox_bootstrap_chunk !== false) {
83+
$wp_codebox_bootstrap_output = $wp_codebox_bootstrap_chunk . $wp_codebox_bootstrap_output;
84+
}
85+
}
86+
echo $wp_codebox_bootstrap_output;`
87+
}
88+
5789
export function splitLeadingStrictTypesDeclare(code: string): { strictTypesDeclare: string; body: string } {
5890
const normalized = normalizePhpCode(code)
5991
const match = normalized.match(/^<\?php\s*(declare\s*\(\s*strict_types\s*=\s*1\s*\)\s*;)\s*/i)

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

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ const root = await mkdtemp(join(tmpdir(), "wp-codebox-phpunit-bootstrap-failure-
1111
const fatalMuPlugin = join(root, "fatal-bootstrap.php")
1212
const recipePath = join(root, "recipe.json")
1313
const artifactsPath = join(root, "artifacts")
14+
const exitArtifactsPath = join(root, "exit-artifacts")
1415

1516
try {
1617
await writeFile(fatalMuPlugin, `<?php\ntrigger_error('PHPUnit bootstrap fixture token: ${secret}', E_USER_ERROR);\n`)
@@ -49,13 +50,28 @@ try {
4950
assert.match(commandLog, /wordpress\.phpunit structured diagnostics/)
5051
assert.match(commandLog, /PHPUnit bootstrap fixture token: \[redacted\]/)
5152
assert.doesNotMatch(commandLog, new RegExp(secret))
53+
54+
await writeFile(fatalMuPlugin, `<?php\necho 'PHPUnit bootstrap exit fixture token: ${secret}';\nexit(1);\n`)
55+
const exitOutput = await runFailedRecipe(exitArtifactsPath)
56+
assert.equal(exitOutput.success, false, JSON.stringify(exitOutput))
57+
const exitMessage = exitOutput.error?.message ?? ""
58+
assert.match(exitMessage, /wordpress\.phpunit structured diagnostics/)
59+
assert.match(exitMessage, /PHPUnit bootstrap exit fixture token: \[redacted\]/)
60+
assert.doesNotMatch(exitMessage, new RegExp(secret))
61+
62+
const exitLatest = JSON.parse(await readFile(join(exitArtifactsPath, "latest-runtime.json"), "utf8")) as { paths?: { runtimeDirectory?: string } }
63+
const exitRuntimeDirectory = exitLatest.paths?.runtimeDirectory
64+
assert.ok(exitRuntimeDirectory, "terminated recipe must retain a runtime artifact directory")
65+
const exitDiagnostic = await readFile(join(exitArtifactsPath, exitRuntimeDirectory, "files", "phpunit", ".pg-test-result.txt"), "utf8")
66+
assert.match(exitDiagnostic, /STAGE_DIE:bootstrap:PHPUnit bootstrap exit fixture token: \[redacted\]/)
67+
assert.doesNotMatch(exitDiagnostic, new RegExp(secret))
5268
} finally {
5369
await rm(root, { recursive: true, force: true })
5470
}
5571

56-
async function runFailedRecipe(): Promise<RecipeRunOutput> {
72+
async function runFailedRecipe(outputPath = artifactsPath): Promise<RecipeRunOutput> {
5773
try {
58-
const result = await execFileAsync(process.execPath, ["packages/cli/dist/index.js", "recipe-run", "--recipe", recipePath, "--artifacts", artifactsPath, "--json"], {
74+
const result = await execFileAsync(process.execPath, ["packages/cli/dist/index.js", "recipe-run", "--recipe", recipePath, "--artifacts", outputPath, "--json"], {
5975
cwd: process.cwd(),
6076
timeout: 300_000,
6177
maxBuffer: 2 * 1024 * 1024,

0 commit comments

Comments
 (0)