Skip to content

Commit 7e45126

Browse files
authored
Load PHPUnit dependencies after database install (#1868)
* Load PHPUnit dependencies after install * Replay PHPUnit dependency plugin hooks
1 parent ffa7fd9 commit 7e45126

5 files changed

Lines changed: 56 additions & 4 deletions

File tree

docs/recipe-contract.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -716,6 +716,15 @@ The runtime provides:
716716
- Structured diagnostics in the recipe artifact bundle, including the raw test
717717
result log collected from `/tmp/wp-codebox-phpunit-result.txt`.
718718

719+
`dependencyMounts` is the canonical PHPUnit builder input for plugins that need
720+
the PHPUnit database during activation. Mount each dependency through
721+
`inputs.extra_plugins` with `activate: false`, then provide its sandbox plugin
722+
directory in `dependencyMounts`. In managed bootstrap mode, `wordpress.phpunit`
723+
loads and activates those dependencies after the PHPUnit install stage has
724+
created test tables and before test discovery and execution. Any dependency
725+
`plugins_loaded` callbacks registered during that load are invoked once after
726+
dependency activation.
727+
719728
Use `recipe build phpunit` when generating recipes for plugin CI or offloaded lab
720729
runners:
721730

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -948,7 +948,7 @@ export const commandRegistry = [
948948
{ name: "changed-tests-json", description: "Changed test files for diagnostics.", format: "JSON array" },
949949
{ name: "env-json", description: "PHPUnit environment values.", format: "JSON object" },
950950
{ name: "wp-config-defines-json", description: "wp-config.php constants for the run.", format: "JSON object" },
951-
{ name: "dependency-mounts", description: "Comma-separated mounted dependency paths.", format: "comma-separated sandbox paths" },
951+
{ name: "dependency-mounts", description: "Comma-separated mounted dependency paths loaded and activated after managed PHPUnit installation, before tests execute.", format: "comma-separated sandbox paths" },
952952
{ name: "bootstrap-files-json", description: "Plugin-relative bootstrap file fallbacks loaded in managed mode after wp-phpunit fixtures.", format: "JSON array" },
953953
{ name: "phpunit-args-json", description: "Structured PHPUnit CLI arguments such as [\"--filter\", \"MyTest::test_case\"].", format: "JSON array" },
954954
{ name: "bootstrap-mode", description: "Bootstrap strategy: managed keeps WP Codebox-owned setup; project requires the plugin's native PHPUnit bootstrap.", format: "managed|project" },

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

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1104,15 +1104,17 @@ $loaded_dep_files = array();
11041104
$loaded_component_file = null;
11051105
11061106
require_once $tests_dir . '/includes/functions.php';
1107-
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) {
1108-
$loaded_dep_files = pg_run_load_deps_stage(array('dep_mounts' => $dep_mounts));
1107+
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) {
11091108
$loaded_component_file = pg_run_load_component_stage(array('plugin_slug' => $plugin_slug, 'plugin_path' => $plugin_path, 'activate' => false));
11101109
$deferred_install_plugins_loaded_callbacks = pg_defer_new_wordpress_hook_callbacks('plugins_loaded', $pre_component_plugins_loaded_callbacks);
11111110
$deferred_install_init_callbacks = pg_defer_new_wordpress_hook_callbacks('init', $pre_component_init_callbacks);
11121111
});
11131112
11141113
pg_run_install_stage(array('config_path' => $config_path, 'tests_dir' => $tests_dir, 'multisite' => $multisite));
11151114
pg_remove_new_wordpress_hook_callbacks('shutdown', $pre_component_shutdown_callbacks);
1115+
$pre_dependency_plugins_loaded_callbacks = pg_snapshot_wordpress_hook_callbacks('plugins_loaded');
1116+
$loaded_dep_files = pg_run_load_deps_stage(array('dep_mounts' => $dep_mounts));
1117+
$deferred_dependency_plugins_loaded_callbacks = pg_defer_new_wordpress_hook_callbacks('plugins_loaded', $pre_dependency_plugins_loaded_callbacks);
11161118
$activation_files = $loaded_dep_files;
11171119
if ($loaded_component_file !== null) {
11181120
$activation_files[] = $loaded_component_file;
@@ -1123,6 +1125,7 @@ $pre_replayed_plugins_loaded_init_callbacks = pg_snapshot_wordpress_hook_callbac
11231125
$reopened_ability_categories_init = pg_reopen_wordpress_action('wp_abilities_api_categories_init');
11241126
$reopened_ability_init = pg_reopen_wordpress_action('wp_abilities_api_init');
11251127
pg_run_deferred_wordpress_hook_callbacks($deferred_install_plugins_loaded_callbacks, array(), 'plugins_loaded');
1128+
pg_run_deferred_wordpress_hook_callbacks($deferred_dependency_plugins_loaded_callbacks, array(), 'plugins_loaded');
11261129
$deferred_install_init_callbacks = array_merge($deferred_install_init_callbacks, pg_defer_new_wordpress_hook_callbacks('init', $pre_replayed_plugins_loaded_init_callbacks));
11271130
usort($deferred_install_init_callbacks, static function (array $left, array $right): int {
11281131
return ($left['priority'] ?? 10) <=> ($right['priority'] ?? 10);

tests/phpunit-project-autoload.test.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -410,6 +410,36 @@ const managedModeCode = phpunitRunCode({
410410

411411
assert.ok(managedModeCode.includes("configured PHPUnit harness autoload file is not readable"))
412412
assert.ok(managedModeCode.includes("'cacheResult' => false"))
413+
const installStageIndex = managedModeCode.indexOf("pg_run_install_stage(array(")
414+
const dependencyLoadStageIndex = managedModeCode.indexOf("$loaded_dep_files = pg_run_load_deps_stage", installStageIndex)
415+
const activationStageIndex = managedModeCode.indexOf("pg_run_activation_stage", dependencyLoadStageIndex)
416+
const dependencyPluginsLoadedSnapshotIndex = managedModeCode.indexOf("$pre_dependency_plugins_loaded_callbacks = pg_snapshot_wordpress_hook_callbacks('plugins_loaded');", installStageIndex)
417+
const dependencyPluginsLoadedDeferIndex = managedModeCode.indexOf("$deferred_dependency_plugins_loaded_callbacks = pg_defer_new_wordpress_hook_callbacks('plugins_loaded', $pre_dependency_plugins_loaded_callbacks);", dependencyLoadStageIndex)
418+
const dependencyPluginsLoadedReplayIndex = managedModeCode.indexOf("pg_run_deferred_wordpress_hook_callbacks($deferred_dependency_plugins_loaded_callbacks, array(), 'plugins_loaded');", activationStageIndex)
419+
assert.ok(installStageIndex > 0)
420+
assert.ok(dependencyPluginsLoadedSnapshotIndex > installStageIndex && dependencyPluginsLoadedSnapshotIndex < dependencyLoadStageIndex, "dependency plugins_loaded callbacks must be scoped to dependency loading")
421+
assert.ok(dependencyLoadStageIndex > installStageIndex, "dependency plugins must load after managed PHPUnit installation")
422+
assert.ok(dependencyPluginsLoadedDeferIndex > dependencyLoadStageIndex && dependencyPluginsLoadedDeferIndex < activationStageIndex, "dependency plugins_loaded callbacks must defer until activation completes")
423+
assert.ok(activationStageIndex > dependencyLoadStageIndex, "dependency plugins must activate after loading and before tests execute")
424+
assert.ok(dependencyPluginsLoadedReplayIndex > activationStageIndex, "dependency plugins_loaded callbacks must run once after activation")
425+
426+
const dependencyRecipe = buildWordPressPhpunitRecipe({
427+
pluginSlug: "demo-plugin",
428+
extra_plugins: [{
429+
source: "/workspace/dependency",
430+
slug: "dependency",
431+
pluginFile: "dependency/dependency.php",
432+
activate: false,
433+
}],
434+
dependencyMounts: ["/wordpress/wp-content/plugins/dependency"],
435+
})
436+
assert.deepEqual(dependencyRecipe.inputs.extra_plugins, [{
437+
source: "/workspace/dependency",
438+
slug: "dependency",
439+
pluginFile: "dependency/dependency.php",
440+
activate: false,
441+
}])
442+
assert.ok(dependencyRecipe.workflow.steps[0].args.includes("dependency-mounts=/wordpress/wp-content/plugins/dependency"))
413443

414444
const phpunitCacheAllocator = extractPhpFunction(managedModeCode, "wp_codebox_phpunit_args_private_cache_result_file")
415445
const phpunitArgsFunction = extractPhpFunction(managedModeCode, "wp_codebox_phpunit_args")

tests/playground-phpunit-readonly-cache.integration.test.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { buildWordPressPhpunitRecipe } from "../packages/runtime-core/src/recipe
1111
const execFileAsync = promisify(execFile)
1212
const root = await mkdtemp(join(tmpdir(), "wp-codebox-phpunit-readonly-"))
1313
const plugin = join(root, "plugin")
14+
const dependency = join(root, "dependency")
1415
const harness = join(root, "harness")
1516
const recipePath = join(root, "recipe.json")
1617
const artifactsPath = join(root, "artifacts")
@@ -24,6 +25,13 @@ try {
2425

2526
const recipe = buildWordPressPhpunitRecipe({
2627
pluginSlug: "readonly-phpunit-fixture",
28+
extra_plugins: [{
29+
source: dependency,
30+
slug: "activation-dependency",
31+
pluginFile: "activation-dependency/activation-dependency.php",
32+
activate: false,
33+
}],
34+
dependencyMounts: ["/wordpress/wp-content/plugins/activation-dependency"],
2735
mounts: [
2836
{ source: plugin, target: "/wordpress/wp-content/plugins/readonly-phpunit-fixture", mode: "readonly" },
2937
{ source: join(harness, "vendor"), target: "/wp-codebox-vendor", mode: "readonly" },
@@ -49,9 +57,11 @@ try {
4957

5058
async function writeFixture(): Promise<void> {
5159
await mkdir(join(plugin, "tests"), { recursive: true })
60+
await mkdir(dependency, { recursive: true })
5261
await writeFile(join(plugin, "readonly-phpunit-fixture.php"), "<?php\n/**\n * Plugin Name: Readonly PHPUnit Fixture\n */\n")
5362
await writeFile(join(plugin, "source-sentinel.bin"), sentinel)
54-
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")
63+
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")
64+
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")
5565
}
5666

5767
async function digestTree(directory: string): Promise<string> {

0 commit comments

Comments
 (0)