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
9 changes: 9 additions & 0 deletions docs/recipe-contract.md
Original file line number Diff line number Diff line change
Expand Up @@ -716,6 +716,15 @@ The runtime provides:
- Structured diagnostics in the recipe artifact bundle, including the raw test
result log collected from `/tmp/wp-codebox-phpunit-result.txt`.

`dependencyMounts` is the canonical PHPUnit builder input for plugins that need
the PHPUnit database during activation. Mount each dependency through
`inputs.extra_plugins` with `activate: false`, then provide its sandbox plugin
directory in `dependencyMounts`. In managed bootstrap mode, `wordpress.phpunit`
loads and activates those dependencies after the PHPUnit install stage has
created test tables and before test discovery and execution. Any dependency
`plugins_loaded` callbacks registered during that load are invoked once after
dependency activation.

Use `recipe build phpunit` when generating recipes for plugin CI or offloaded lab
runners:

Expand Down
2 changes: 1 addition & 1 deletion packages/runtime-core/src/command-registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -948,7 +948,7 @@ export const commandRegistry = [
{ name: "changed-tests-json", description: "Changed test files for diagnostics.", format: "JSON array" },
{ name: "env-json", description: "PHPUnit environment values.", format: "JSON object" },
{ name: "wp-config-defines-json", description: "wp-config.php constants for the run.", format: "JSON object" },
{ name: "dependency-mounts", description: "Comma-separated mounted dependency paths.", format: "comma-separated sandbox paths" },
{ name: "dependency-mounts", description: "Comma-separated mounted dependency paths loaded and activated after managed PHPUnit installation, before tests execute.", format: "comma-separated sandbox paths" },
{ name: "bootstrap-files-json", description: "Plugin-relative bootstrap file fallbacks loaded in managed mode after wp-phpunit fixtures.", format: "JSON array" },
{ name: "phpunit-args-json", description: "Structured PHPUnit CLI arguments such as [\"--filter\", \"MyTest::test_case\"].", format: "JSON array" },
{ name: "bootstrap-mode", description: "Bootstrap strategy: managed keeps WP Codebox-owned setup; project requires the plugin's native PHPUnit bootstrap.", format: "managed|project" },
Expand Down
7 changes: 5 additions & 2 deletions packages/runtime-playground/src/phpunit-command-handlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1104,15 +1104,17 @@ $loaded_dep_files = array();
$loaded_component_file = null;

require_once $tests_dir . '/includes/functions.php';
tests_add_filter('muplugins_loaded', function () use ($plugin_slug, $plugin_path, $dep_mounts, $pre_component_plugins_loaded_callbacks, $pre_component_init_callbacks, &$deferred_install_plugins_loaded_callbacks, &$deferred_install_init_callbacks, &$loaded_dep_files, &$loaded_component_file) {
$loaded_dep_files = pg_run_load_deps_stage(array('dep_mounts' => $dep_mounts));
tests_add_filter('muplugins_loaded', function () use ($plugin_slug, $plugin_path, $pre_component_plugins_loaded_callbacks, $pre_component_init_callbacks, &$deferred_install_plugins_loaded_callbacks, &$deferred_install_init_callbacks, &$loaded_component_file) {
$loaded_component_file = pg_run_load_component_stage(array('plugin_slug' => $plugin_slug, 'plugin_path' => $plugin_path, 'activate' => false));
$deferred_install_plugins_loaded_callbacks = pg_defer_new_wordpress_hook_callbacks('plugins_loaded', $pre_component_plugins_loaded_callbacks);
$deferred_install_init_callbacks = pg_defer_new_wordpress_hook_callbacks('init', $pre_component_init_callbacks);
});

pg_run_install_stage(array('config_path' => $config_path, 'tests_dir' => $tests_dir, 'multisite' => $multisite));
pg_remove_new_wordpress_hook_callbacks('shutdown', $pre_component_shutdown_callbacks);
$pre_dependency_plugins_loaded_callbacks = pg_snapshot_wordpress_hook_callbacks('plugins_loaded');
$loaded_dep_files = pg_run_load_deps_stage(array('dep_mounts' => $dep_mounts));
$deferred_dependency_plugins_loaded_callbacks = pg_defer_new_wordpress_hook_callbacks('plugins_loaded', $pre_dependency_plugins_loaded_callbacks);
$activation_files = $loaded_dep_files;
if ($loaded_component_file !== null) {
$activation_files[] = $loaded_component_file;
Expand All @@ -1123,6 +1125,7 @@ $pre_replayed_plugins_loaded_init_callbacks = pg_snapshot_wordpress_hook_callbac
$reopened_ability_categories_init = pg_reopen_wordpress_action('wp_abilities_api_categories_init');
$reopened_ability_init = pg_reopen_wordpress_action('wp_abilities_api_init');
pg_run_deferred_wordpress_hook_callbacks($deferred_install_plugins_loaded_callbacks, array(), 'plugins_loaded');
pg_run_deferred_wordpress_hook_callbacks($deferred_dependency_plugins_loaded_callbacks, array(), 'plugins_loaded');
$deferred_install_init_callbacks = array_merge($deferred_install_init_callbacks, pg_defer_new_wordpress_hook_callbacks('init', $pre_replayed_plugins_loaded_init_callbacks));
usort($deferred_install_init_callbacks, static function (array $left, array $right): int {
return ($left['priority'] ?? 10) <=> ($right['priority'] ?? 10);
Expand Down
30 changes: 30 additions & 0 deletions tests/phpunit-project-autoload.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -410,6 +410,36 @@ const managedModeCode = phpunitRunCode({

assert.ok(managedModeCode.includes("configured PHPUnit harness autoload file is not readable"))
assert.ok(managedModeCode.includes("'cacheResult' => false"))
const installStageIndex = managedModeCode.indexOf("pg_run_install_stage(array(")
const dependencyLoadStageIndex = managedModeCode.indexOf("$loaded_dep_files = pg_run_load_deps_stage", installStageIndex)
const activationStageIndex = managedModeCode.indexOf("pg_run_activation_stage", dependencyLoadStageIndex)
const dependencyPluginsLoadedSnapshotIndex = managedModeCode.indexOf("$pre_dependency_plugins_loaded_callbacks = pg_snapshot_wordpress_hook_callbacks('plugins_loaded');", installStageIndex)
const dependencyPluginsLoadedDeferIndex = managedModeCode.indexOf("$deferred_dependency_plugins_loaded_callbacks = pg_defer_new_wordpress_hook_callbacks('plugins_loaded', $pre_dependency_plugins_loaded_callbacks);", dependencyLoadStageIndex)
const dependencyPluginsLoadedReplayIndex = managedModeCode.indexOf("pg_run_deferred_wordpress_hook_callbacks($deferred_dependency_plugins_loaded_callbacks, array(), 'plugins_loaded');", activationStageIndex)
assert.ok(installStageIndex > 0)
assert.ok(dependencyPluginsLoadedSnapshotIndex > installStageIndex && dependencyPluginsLoadedSnapshotIndex < dependencyLoadStageIndex, "dependency plugins_loaded callbacks must be scoped to dependency loading")
assert.ok(dependencyLoadStageIndex > installStageIndex, "dependency plugins must load after managed PHPUnit installation")
assert.ok(dependencyPluginsLoadedDeferIndex > dependencyLoadStageIndex && dependencyPluginsLoadedDeferIndex < activationStageIndex, "dependency plugins_loaded callbacks must defer until activation completes")
assert.ok(activationStageIndex > dependencyLoadStageIndex, "dependency plugins must activate after loading and before tests execute")
assert.ok(dependencyPluginsLoadedReplayIndex > activationStageIndex, "dependency plugins_loaded callbacks must run once after activation")

const dependencyRecipe = buildWordPressPhpunitRecipe({
pluginSlug: "demo-plugin",
extra_plugins: [{
source: "/workspace/dependency",
slug: "dependency",
pluginFile: "dependency/dependency.php",
activate: false,
}],
dependencyMounts: ["/wordpress/wp-content/plugins/dependency"],
})
assert.deepEqual(dependencyRecipe.inputs.extra_plugins, [{
source: "/workspace/dependency",
slug: "dependency",
pluginFile: "dependency/dependency.php",
activate: false,
}])
assert.ok(dependencyRecipe.workflow.steps[0].args.includes("dependency-mounts=/wordpress/wp-content/plugins/dependency"))

const phpunitCacheAllocator = extractPhpFunction(managedModeCode, "wp_codebox_phpunit_args_private_cache_result_file")
const phpunitArgsFunction = extractPhpFunction(managedModeCode, "wp_codebox_phpunit_args")
Expand Down
12 changes: 11 additions & 1 deletion tests/playground-phpunit-readonly-cache.integration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { buildWordPressPhpunitRecipe } from "../packages/runtime-core/src/recipe
const execFileAsync = promisify(execFile)
const root = await mkdtemp(join(tmpdir(), "wp-codebox-phpunit-readonly-"))
const plugin = join(root, "plugin")
const dependency = join(root, "dependency")
const harness = join(root, "harness")
const recipePath = join(root, "recipe.json")
const artifactsPath = join(root, "artifacts")
Expand All @@ -24,6 +25,13 @@ try {

const recipe = buildWordPressPhpunitRecipe({
pluginSlug: "readonly-phpunit-fixture",
extra_plugins: [{
source: dependency,
slug: "activation-dependency",
pluginFile: "activation-dependency/activation-dependency.php",
activate: false,
}],
dependencyMounts: ["/wordpress/wp-content/plugins/activation-dependency"],
mounts: [
{ source: plugin, target: "/wordpress/wp-content/plugins/readonly-phpunit-fixture", mode: "readonly" },
{ source: join(harness, "vendor"), target: "/wp-codebox-vendor", mode: "readonly" },
Expand All @@ -49,9 +57,11 @@ try {

async function writeFixture(): Promise<void> {
await mkdir(join(plugin, "tests"), { recursive: true })
await mkdir(dependency, { recursive: true })
await writeFile(join(plugin, "readonly-phpunit-fixture.php"), "<?php\n/**\n * Plugin Name: Readonly PHPUnit Fixture\n */\n")
await writeFile(join(plugin, "source-sentinel.bin"), sentinel)
await writeFile(join(plugin, "tests", "ReadonlyCacheTest.php"), "<?php\nclass ReadonlyCacheTest extends WP_UnitTestCase { public function test_sentinel_is_available(): void { $this->assertGreaterThan(0, filesize(dirname(__DIR__) . \'/source-sentinel.bin\')); } }\n")
await writeFile(join(plugin, "tests", "ReadonlyCacheTest.php"), "<?php\nclass ReadonlyCacheTest extends WP_UnitTestCase { public function test_sentinel_is_available(): void { $this->assertGreaterThan(0, filesize(dirname(__DIR__) . \'/source-sentinel.bin\')); } public function test_dependency_activation_runs_after_install(): void { $this->assertGreaterThanOrEqual(1, get_option(\'wp_codebox_dependency_activation_users\')); } public function test_dependency_plugins_loaded_runs_once(): void { $this->assertSame(1, (int) get_option(\'wp_codebox_dependency_plugins_loaded_count\')); } }\n")
await writeFile(join(dependency, "activation-dependency.php"), "<?php\n/**\n * Plugin Name: Activation Dependency\n */\nadd_action('plugins_loaded', static function (): void { update_option('wp_codebox_dependency_plugins_loaded_count', (int) get_option('wp_codebox_dependency_plugins_loaded_count', 0) + 1); });\nregister_activation_hook(__FILE__, static function (): void { update_option('wp_codebox_dependency_activation_users', count(get_users(array('number' => 1)))); });\n")
}

async function digestTree(directory: string): Promise<string> {
Expand Down
Loading