Skip to content

Commit e82fd89

Browse files
committed
Merge origin/main into recipe evidence fixes
2 parents 758c29d + 6de9042 commit e82fd89

7 files changed

Lines changed: 96 additions & 2 deletions

File tree

docs/recipe-contract.md

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,51 @@ artifact bundle and returns after recipe work finishes. Use
166166
`--preview-hold-blocking` only for operator workflows that need the CLI process
167167
to keep a live preview server open for the hold duration.
168168

169+
## WordPress PHPUnit Runtime
170+
171+
`wordpress.phpunit` is the lightweight WP Codebox equivalent of plugin PHPUnit
172+
commands commonly run through `wp-env run ... vendor/bin/phpunit`. It boots a
173+
disposable WordPress Playground runtime, mounts the tested plugin at
174+
`/wordpress/wp-content/plugins/<plugin-slug>`, prepares the WordPress PHPUnit
175+
contract, and captures command output in the artifact bundle.
176+
177+
The runtime provides:
178+
179+
- `WP_TESTS_DIR` pointing at the configured WordPress tests library.
180+
- `WP_TESTS_CONFIG_FILE_PATH` and `WP_PHPUNIT__TESTS_CONFIG` pointing at the
181+
generated `wp-tests-config.php`.
182+
- An isolated SQLite test database via `DB_NAME=':memory:'`.
183+
- A plugin working directory via `cwd=<sandbox path>`, matching the practical
184+
role of `wp-env run --env-cwd`.
185+
- Structured diagnostics in the recipe artifact bundle, including the raw test
186+
result log collected from `/tmp/wp-codebox-phpunit-result.txt`.
187+
188+
Use `recipe build phpunit` when generating recipes for plugin CI or offloaded lab
189+
runners:
190+
191+
```json
192+
{
193+
"pluginSlug": "woocommerce",
194+
"pluginSource": "../woocommerce/plugins/woocommerce",
195+
"cwd": "/wordpress/wp-content/plugins/woocommerce",
196+
"autoloadFile": "/wp-codebox-vendor/autoload.php",
197+
"testsDir": "/wp-codebox-vendor/wp-phpunit/wp-phpunit",
198+
"bootstrapMode": "project",
199+
"projectBootstrap": "tests/legacy/bootstrap.php",
200+
"phpunitArgs": ["--filter", "WC_Checkout_Test::test_checkout"]
201+
}
202+
```
203+
204+
```bash
205+
npm run wp-codebox -- recipe build phpunit --options ./phpunit-options.json --output ./phpunit.recipe.json
206+
npm run wp-codebox -- recipe-run --recipe ./phpunit.recipe.json --artifacts ./artifacts/phpunit --json
207+
```
208+
209+
For monorepos such as WooCommerce, set `pluginSource` to the directory that
210+
should appear as `wp-content/plugins/<plugin-slug>` and set `cwd` to the same
211+
sandbox directory a `wp-env --env-cwd` command would use. Relative `cwd` values
212+
resolve inside the mounted plugin directory.
213+
169214
## Browser Assertions
170215

171216
`wordpress.browser-probe` accepts repeated `assert=<assertion>` arguments.

packages/cli/src/commands/recipe-build.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,9 @@ interface WordPressPhpunitBuilderOptions {
1111
blueprint?: unknown
1212
wordpressVersion?: string
1313
mounts?: WorkspaceRecipeMount[]
14+
pluginSource?: string
1415
pluginSlug: string
16+
cwd?: string
1517
selectedTestFile?: string
1618
changedTestFiles?: string[]
1719
env?: Record<string, unknown>
@@ -64,7 +66,9 @@ function buildRecipe(recipeType: RecipeBuildOptions["recipeType"], options: Word
6466
blueprint: options.blueprint,
6567
wordpressVersion: stringOrUndefined(options.wordpressVersion),
6668
mounts: Array.isArray(options.mounts) ? options.mounts : [],
69+
pluginSource: stringOrUndefined((options as WordPressPhpunitBuilderOptions).pluginSource),
6770
pluginSlug: requiredString(options.pluginSlug, "pluginSlug"),
71+
cwd: stringOrUndefined((options as WordPressPhpunitBuilderOptions).cwd),
6872
selectedTestFile: stringOrUndefined((options as WordPressPhpunitBuilderOptions).selectedTestFile),
6973
changedTestFiles: Array.isArray((options as WordPressPhpunitBuilderOptions).changedTestFiles) ? (options as WordPressPhpunitBuilderOptions).changedTestFiles : [],
7074
env: plainObject(options.env),

packages/runtime-core/src/command-registry.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,7 @@ export const commandRegistry = [
105105
description: "Run plugin PHPUnit tests with normalized diagnostics and test-result artifact capture.",
106106
acceptedArgs: [
107107
{ name: "plugin-slug", description: "Plugin slug under wp-content/plugins.", format: "slug" },
108+
{ name: "cwd", description: "Sandbox working directory for the PHPUnit process. Relative values resolve inside the mounted plugin directory; defaults to wp-content/plugins/<plugin-slug>.", format: "sandbox path" },
108109
{ name: "code", description: "Inline override PHP runner code.", format: "PHP string" },
109110
{ name: "code-file", description: "Path to override PHP runner code.", format: "path" },
110111
{ name: "autoload-file", description: "PHPUnit/vendor autoload path inside the sandbox.", format: "sandbox path" },

packages/runtime-core/src/recipe-builders.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,9 @@ export interface WordPressPhpunitRecipeOptions {
1111
wordpressVersion?: string
1212
blueprint?: unknown
1313
mounts?: WorkspaceRecipeMount[]
14+
pluginSource?: string
1415
pluginSlug: string
16+
cwd?: string
1517
selectedTestFile?: string
1618
changedTestFiles?: string[]
1719
env?: JsonObject
@@ -72,6 +74,7 @@ export function normalizeRecipeMounts(mounts: readonly WorkspaceRecipeMount[] =
7274

7375
export function buildWordPressPhpunitRecipe(options: WordPressPhpunitRecipeOptions): WorkspaceRecipe {
7476
const pluginSlug = requiredPluginSlug(options.pluginSlug, "buildWordPressPhpunitRecipe")
77+
const pluginTarget = `/wordpress/wp-content/plugins/${pluginSlug}`
7578

7679
return {
7780
schema: "wp-codebox/workspace-recipe/v1",
@@ -80,13 +83,17 @@ export function buildWordPressPhpunitRecipe(options: WordPressPhpunitRecipeOptio
8083
blueprint: options.blueprint ?? { steps: [] },
8184
},
8285
inputs: {
83-
mounts: normalizeRecipeMounts(options.mounts),
86+
mounts: normalizeRecipeMounts([
87+
...(options.pluginSource ? [{ source: options.pluginSource, target: pluginTarget } satisfies WorkspaceRecipeMount] : []),
88+
...(options.mounts ?? []),
89+
]),
8490
},
8591
workflow: {
8692
steps: [{
8793
command: "wordpress.phpunit",
8894
args: [
8995
`plugin-slug=${pluginSlug}`,
96+
`cwd=${options.cwd ?? pluginTarget}`,
9097
`test-file=${options.selectedTestFile ?? ""}`,
9198
`changed-tests-json=${JSON.stringify(options.changedTestFiles ?? [])}`,
9299
`env-json=${JSON.stringify(options.env ?? {})}`,

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

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
export interface PhpunitRunCodeOptions {
22
pluginSlug: string
3+
cwd: string
34
autoloadFile: string
45
testsDir: string
56
phpunitXml: string
@@ -281,6 +282,7 @@ ini_set('display_startup_errors', '1');
281282
282283
$plugin_slug = ${JSON.stringify(options.pluginSlug)};
283284
$plugin_path = '/wordpress/wp-content/plugins/' . $plugin_slug;
285+
$runtime_cwd = ${JSON.stringify(options.cwd || `/wordpress/wp-content/plugins/${options.pluginSlug}`)};
284286
$result_file = ${JSON.stringify(options.resultFile ?? PLUGIN_PHPUNIT_RESULT_FILE)};
285287
$current_stage = 'preboot';
286288
$pg_stage_output_buffering = false;
@@ -547,6 +549,25 @@ CONFIG;
547549
}
548550
}
549551
552+
function pg_resolve_runtime_cwd(string $cwd, string $plugin_path): string {
553+
$cwd = trim(str_replace('\\', '/', $cwd));
554+
if ($cwd === '') {
555+
return $plugin_path;
556+
}
557+
if ($cwd[0] !== '/') {
558+
$cwd = rtrim($plugin_path, '/') . '/' . ltrim($cwd, '/');
559+
}
560+
$real = realpath($cwd);
561+
$plugin_real = realpath($plugin_path);
562+
if ($real === false || $plugin_real === false || !is_dir($real)) {
563+
throw new RuntimeException('cwd is not a readable sandbox directory: ' . $cwd);
564+
}
565+
if ($real !== $plugin_real && strpos($real, rtrim($plugin_real, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR) !== 0) {
566+
throw new RuntimeException('cwd must resolve inside the mounted plugin directory: ' . $cwd);
567+
}
568+
return $real;
569+
}
570+
550571
function pg_plugin_real_path(string $relative_path, string $kind): ?string {
551572
global $plugin_path;
552573
$relative_path = trim(str_replace('\\\\', '/', $relative_path));
@@ -779,6 +800,17 @@ ${phpunitChangedTestFilterPhp({
779800
780801
pg_install_diagnostics_handlers();
781802
803+
pg_stage_begin('cwd');
804+
try {
805+
$runtime_cwd = pg_resolve_runtime_cwd($runtime_cwd, $plugin_path);
806+
chdir($runtime_cwd);
807+
pg_log('CWD:' . $runtime_cwd);
808+
pg_stage_ok('cwd');
809+
} catch (Throwable $e) {
810+
pg_stage_fail('cwd', $e);
811+
exit(1);
812+
}
813+
782814
if (is_array($bench_env)) {
783815
foreach ($bench_env as $name => $value) {
784816
if (is_string($name) && preg_match('/^[A-Za-z_][A-Za-z0-9_]*$/', $name)) {

packages/runtime-playground/src/wordpress-command-runners.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -392,6 +392,7 @@ export async function runPhpunitCommand({
392392
const resultFile = PLUGIN_PHPUNIT_RESULT_FILE
393393
const code = explicitCode ? await phpCodeFromArgs(args, "wordpress.phpunit") : normalizePhpCode(phpunitRunCode({
394394
pluginSlug,
395+
cwd: argValue(args, "cwd")?.trim() || `/wordpress/wp-content/plugins/${pluginSlug}`,
395396
autoloadFile: argValue(args, "autoload-file")?.trim() || "/wp-codebox-vendor/autoload.php",
396397
testsDir: argValue(args, "tests-dir")?.trim() || "/wp-codebox-vendor/wp-phpunit/wp-phpunit",
397398
phpunitXml: argValue(args, "phpunit-xml")?.trim() || `/wordpress/wp-content/plugins/${pluginSlug}/phpunit.xml.dist`,

scripts/wordpress-recipe-builders-smoke.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ const validateBenchResults = ajv.compile(createBenchResultsJsonSchema())
1111
const phpunitRecipe = buildWordPressPhpunitRecipe({
1212
wordpressVersion: "6.9",
1313
pluginSlug: "demo-plugin",
14+
pluginSource: "/repo/demo-plugin-source",
15+
cwd: "tests/phpunit",
1416
selectedTestFile: "tests/unit/DemoTest.php",
1517
changedTestFiles: ["tests/unit/DemoTest.php"],
1618
env: { DEMO_ENV: "yes" },
@@ -22,7 +24,6 @@ const phpunitRecipe = buildWordPressPhpunitRecipe({
2224
projectBootstrap: "tests/bootstrap.php",
2325
multisite: true,
2426
mounts: [
25-
{ source: "/repo/demo-plugin", target: "/wordpress/wp-content/plugins/demo-plugin" },
2627
{ source: "/repo/vendor", target: "/wp-codebox-vendor", mode: "readonly" },
2728
],
2829
})
@@ -31,8 +32,11 @@ assert.equal(buildWordPressPhpunitRecipe({ pluginSlug: "demo-plugin" }).runtime?
3132
assert.equal(buildWordPressBenchRecipe({ pluginSlug: "demo-plugin" }).runtime?.wp, DEFAULT_WORDPRESS_VERSION)
3233

3334
assert.equal(phpunitRecipe.inputs?.mounts?.[0]?.mode, "readwrite")
35+
assert.deepEqual(phpunitRecipe.inputs?.mounts?.[0], { source: "/repo/demo-plugin-source", target: "/wordpress/wp-content/plugins/demo-plugin", mode: "readwrite" })
36+
assert.deepEqual(phpunitRecipe.inputs?.mounts?.[1], { source: "/repo/vendor", target: "/wp-codebox-vendor", mode: "readonly" })
3437
assert.deepEqual(phpunitRecipe.workflow.steps[0]?.args, [
3538
"plugin-slug=demo-plugin",
39+
"cwd=tests/phpunit",
3640
"test-file=tests/unit/DemoTest.php",
3741
'changed-tests-json=["tests/unit/DemoTest.php"]',
3842
'env-json={"DEMO_ENV":"yes"}',

0 commit comments

Comments
 (0)