Skip to content
Open
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
26 changes: 14 additions & 12 deletions packages/cli/src/commands/recipe-runtime-setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -127,19 +127,20 @@ export async function applyRecipeRuntimeSetup(args: {
interruption?.throwIfInterrupted()
}

const extraPluginMounts: MountSpec[] = extraPlugins.map((plugin) => ({
type: "directory",
source: plugin.source,
target: plugin.target,
mode: "readonly",
metadata: {
kind: "extra-plugin",
slug: plugin.slug,
source: plugin.provenance,
},
}))
await phaseTracker.run("mount_plugins", phasePluginMountData(extraPlugins), async () => {
for (const plugin of extraPlugins) {
await awaitRecipe(`extra-plugin.mount:${plugin.slug}`, runtime.mount({
type: "directory",
source: plugin.source,
target: plugin.target,
mode: "readonly",
metadata: {
kind: "extra-plugin",
slug: plugin.slug,
source: plugin.provenance,
},
}))
for (const [index, plugin] of extraPlugins.entries()) {
await awaitRecipe(`extra-plugin.mount:${plugin.slug}`, runtime.mount(extraPluginMounts[index]))
interruption?.throwIfInterrupted()
}
})
Expand Down Expand Up @@ -190,6 +191,7 @@ export async function applyRecipeRuntimeSetup(args: {
}

const materializableMounts: MountSpec[] = [
...extraPluginMounts,
...inputMounts,
...stagedFiles.map((stagedFile) => ({
type: stagedFile.type,
Expand Down
180 changes: 138 additions & 42 deletions packages/runtime-playground/src/phpunit-command-handlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ export interface PhpunitRunCodeOptions {
bootstrapMode: string
projectBootstrap: string
multisite: boolean
preinstalledMultisite?: boolean
databaseType: "sqlite" | "mysql"
/**
* Sandbox-internal, writable path for the structured diagnostics log. Defaults
Expand All @@ -30,6 +31,8 @@ export interface PhpunitRunCodeOptions {
resultFile?: string
}

export type PhpunitMultisitePreinstallCodeOptions = Pick<PhpunitRunCodeOptions, "testsDir" | "env" | "wpConfigDefines" | "databaseType" | "resultFile">

export interface CorePhpunitRunCodeOptions {
coreRoot: string
testsDir: string
Expand Down Expand Up @@ -334,6 +337,101 @@ function ${functionName}(array $argv) {
}`
}

function managedPhpunitConfigWriterPhp(): string {
return `function pg_write_managed_test_config(array $extra_defines, string $table_prefix, string $database_type): string {
$config_path = '/tmp/wp-tests-config.php';
$config = "<?php\n";
pg_append_wp_config_defines($config, $extra_defines);
$config .= '$table_prefix = ' . var_export($table_prefix, true) . ";\n";
if ($database_type === 'mysql') {
$db_host = getenv('DB_HOST');
if (!is_string($db_host) || $db_host === '') {
throw new RuntimeException('Managed PHPUnit requires DB_HOST when database-type=mysql; declare a MySQL runtime service with canonical DB_* outputs.');
}
$db_port = getenv('DB_PORT');
if (is_string($db_port) && $db_port !== '') {
$db_host .= ':' . $db_port;
}
foreach (array(
'DB_NAME' => getenv('DB_NAME') ?: 'runtime',
'DB_USER' => getenv('DB_USER') ?: 'runtime',
'DB_PASSWORD' => getenv('DB_PASSWORD') ?: '',
'DB_HOST' => $db_host,
) as $name => $value) {
$config .= 'define(' . var_export($name, true) . ', ' . var_export($value, true) . ");\n";
}
} else {
$config .= <<<'CONFIG'
define('DB_NAME', ':memory:');
define('DB_USER', 'root');
define('DB_PASSWORD', '');
define('DB_HOST', 'localhost');
CONFIG;
}
$config .= <<<'CONFIG'
define('DB_CHARSET', 'utf8');
define('WP_TESTS_DOMAIN', 'example.org');
define('WP_TESTS_EMAIL', 'admin@example.org');
define('WP_TESTS_TITLE', 'Test Blog');
define('WP_PHP_BINARY', 'php');
define('ABSPATH', '/wordpress/');
define('FS_CHMOD_FILE', 0644);
define('FS_CHMOD_DIR', 0755);
define('FS_METHOD', 'direct');
CONFIG;
file_put_contents($config_path, $config);
return $config_path;
}`
}

export function phpunitMultisitePreinstallCode(options: PhpunitMultisitePreinstallCodeOptions): string {
const wpConfigDefines = { ...options.wpConfigDefines }
for (const name of ["MULTISITE", "SUBDOMAIN_INSTALL", "DOMAIN_CURRENT_SITE", "PATH_CURRENT_SITE", "SITE_ID_CURRENT_SITE", "BLOG_ID_CURRENT_SITE"]) {
delete wpConfigDefines[name]
}
return `error_reporting(E_ALL);
ini_set('display_errors', '1');
ini_set('display_startup_errors', '1');

$tests_dir = ${JSON.stringify(options.testsDir)};
$bench_env = json_decode(${JSON.stringify(JSON.stringify(options.env))}, true);
$wp_config_defines = json_decode(${JSON.stringify(JSON.stringify(wpConfigDefines))}, true);
$database_type = ${JSON.stringify(options.databaseType)};
$result_file = ${JSON.stringify(options.resultFile ?? PLUGIN_PHPUNIT_RESULT_FILE)};
$preinstall_complete = false;

@file_put_contents($result_file, '');
register_shutdown_function(static function () use (&$preinstall_complete, $result_file): void {
if ($preinstall_complete) {
return;
}
$failure = error_get_last();
if (is_array($failure) && in_array($failure['type'] ?? 0, array(E_ERROR, E_PARSE, E_CORE_ERROR, E_COMPILE_ERROR, E_USER_ERROR, E_RECOVERABLE_ERROR), true)) {
@file_put_contents($result_file, 'STAGE_FATAL:preinstall:' . (string) ($failure['message'] ?? '') . ' at ' . (string) ($failure['file'] ?? '') . ':' . (int) ($failure['line'] ?? 0) . "\n", FILE_APPEND);
}
});

${phpEnvAssignmentFunction("pg_apply_env", "json_encode", "error_log('Skipping invalid environment key: ' . var_export($name, true));")}
${phpWpConfigDefineAppenderFunction("pg_append_wp_config_defines", "error_log('Skipping invalid wp_config_defines key: ' . var_export($name, true));")}
${managedPhpunitConfigWriterPhp()}

pg_apply_env($bench_env);
$config_path = pg_write_managed_test_config($wp_config_defines, 'wptests_', $database_type);
if (!defined('WP_TESTS_MULTISITE')) {
define('WP_TESTS_MULTISITE', true);
}
$argv = array('install.php', $config_path, 'run_ms_tests', 'no_core_tests');
$_SERVER['argv'] = $argv;
$_SERVER['argc'] = count($argv);
try {
require $tests_dir . '/includes/install.php';
$preinstall_complete = true;
} catch (Throwable $error) {
@file_put_contents($result_file, 'STAGE_FAIL:preinstall:' . get_class($error) . ': ' . $error->getMessage() . ' at ' . $error->getFile() . ':' . $error->getLine() . "\n", FILE_APPEND);
throw $error;
}`
}

export function phpunitRunCode(options: PhpunitRunCodeOptions): string {
return `error_reporting(E_ALL);
ini_set('display_errors', '1');
Expand Down Expand Up @@ -362,6 +460,7 @@ $preload_files = json_decode(${JSON.stringify(JSON.stringify(options.preloadFile
$bootstrap_mode = ${JSON.stringify(options.bootstrapMode || "managed")};
$project_bootstrap = ${JSON.stringify(options.projectBootstrap)};
$multisite = ${JSON.stringify(options.multisite)};
$preinstalled_multisite = ${JSON.stringify(options.preinstalledMultisite ?? false)};
$database_type = ${JSON.stringify(options.databaseType)};

function pg_build_phpunit_argv($raw): array {
Expand Down Expand Up @@ -415,6 +514,8 @@ ${phpEnvAssignmentFunction("pg_apply_env", "json_encode", "pg_log('NOTICE: skipp

${phpWpConfigDefineAppenderFunction("pg_append_wp_config_defines", "pg_log('NOTICE: skipping invalid wp_config_defines key: ' . var_export($name, true));")}

${managedPhpunitConfigWriterPhp()}

function pg_diagnostic_context(): string {
global $current_stage;
$hook = function_exists('current_filter') ? current_filter() : null;
Expand Down Expand Up @@ -661,48 +762,8 @@ function pg_run_boot_stage(array $cfg = []): ?string {
$autoload_required = !empty($cfg['autoload_required']);
$extra_defines = $cfg['extra_defines'] ?? array();
$table_prefix = isset($cfg['table_prefix']) && is_string($cfg['table_prefix']) && $cfg['table_prefix'] !== '' ? $cfg['table_prefix'] : 'wptests_';
$config_path = '/tmp/wp-tests-config.php';
$config = "<?php\n";
pg_append_wp_config_defines($config, $extra_defines);
$config .= '$table_prefix = ' . var_export($table_prefix, true) . ";\n";
$database_type = (string) ($cfg['database_type'] ?? 'sqlite');
if ($database_type === 'mysql') {
$db_host = getenv('DB_HOST');
if (!is_string($db_host) || $db_host === '') {
throw new RuntimeException('Managed PHPUnit requires DB_HOST when database-type=mysql; declare a MySQL runtime service with canonical DB_* outputs.');
}
$db_port = getenv('DB_PORT');
if (is_string($db_port) && $db_port !== '') {
$db_host .= ':' . $db_port;
}
foreach (array(
'DB_NAME' => getenv('DB_NAME') ?: 'runtime',
'DB_USER' => getenv('DB_USER') ?: 'runtime',
'DB_PASSWORD' => getenv('DB_PASSWORD') ?: '',
'DB_HOST' => $db_host,
) as $name => $value) {
$config .= 'define(' . var_export($name, true) . ', ' . var_export($value, true) . ");\n";
}
} else {
$config .= <<<'CONFIG'
define('DB_NAME', ':memory:');
define('DB_USER', 'root');
define('DB_PASSWORD', '');
define('DB_HOST', 'localhost');
CONFIG;
}
$config .= <<<'CONFIG'
define('DB_CHARSET', 'utf8');
define('WP_TESTS_DOMAIN', 'example.org');
define('WP_TESTS_EMAIL', 'admin@example.org');
define('WP_TESTS_TITLE', 'Test Blog');
define('WP_PHP_BINARY', 'php');
define('ABSPATH', '/wordpress/');
define('FS_CHMOD_FILE', 0644);
define('FS_CHMOD_DIR', 0755);
define('FS_METHOD', 'direct');
CONFIG;
file_put_contents($config_path, $config);
$config_path = pg_write_managed_test_config($extra_defines, $table_prefix, $database_type);
if ($harness_autoload !== '' && is_readable($harness_autoload)) {
pg_preload_wp_cli_namespaced_functions($harness_autoload);
require_once $harness_autoload;
Expand All @@ -719,6 +780,37 @@ CONFIG;
}
}

function pg_run_preinstalled_multisite_stage(array $cfg): void {
pg_stage_begin('install');
try {
$config_path = (string) ($cfg['config_path'] ?? '');
$tests_dir = (string) ($cfg['tests_dir'] ?? '');
if ($config_path === '' || !is_readable($config_path)) {
throw new RuntimeException('managed multisite config is not readable: ' . $config_path);
}
if ($tests_dir === '' || !is_readable($tests_dir . '/includes/functions.php')) {
throw new RuntimeException('managed multisite test library is not readable: ' . $tests_dir);
}
if (!defined('WP_INSTALLING')) {
define('WP_INSTALLING', true);
}
if (!defined('DISABLE_WP_CRON')) {
define('DISABLE_WP_CRON', true);
}
require_once $config_path;
require_once $tests_dir . '/includes/functions.php';
tests_reset__SERVER();
$GLOBALS['PHP_SELF'] = '/index.php';
$_SERVER['PHP_SELF'] = '/index.php';
tests_add_filter('wp_die_handler', '_wp_die_handler_filter_exit');
require_once ABSPATH . 'wp-settings.php';
pg_stage_ok('install');
} catch (Throwable $e) {
pg_stage_fail('install', $e);
exit(1);
}
}

function pg_resolve_runtime_cwd(string $cwd, string $plugin_path): string {
$cwd = trim(str_replace('\\\\', '/', $cwd));
if ($cwd === '') {
Expand Down Expand Up @@ -1163,7 +1255,11 @@ tests_add_filter('muplugins_loaded', function () use ($plugin_slug, $plugin_path
$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));
if ($preinstalled_multisite) {
pg_run_preinstalled_multisite_stage(array('config_path' => $config_path, 'tests_dir' => $tests_dir));
} else {
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');
$pre_dependency_init_callbacks = pg_snapshot_wordpress_hook_callbacks('init');
Expand Down
16 changes: 15 additions & 1 deletion packages/runtime-playground/src/wordpress-command-runners.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import {
restPerformanceObservationInputFromArgs,
restPerformanceObservationPhpCode,
phpunitRunCode,
phpunitMultisitePreinstallCode,
pluginStateInputFromArgs,
pluginStatePhpCode,
PLUGIN_PHPUNIT_RESULT_FILE,
Expand Down Expand Up @@ -928,6 +929,7 @@ export async function runPhpunitCommand({
const autoloadFile = argValue(args, "autoload-file")?.trim() || (bootstrapMode === "project" ? "" : "/wp-codebox-vendor/autoload.php")
const autoloadFileRole = argValue(args, "autoload-file-role")?.trim() === "harness" ? "harness" : undefined
const processIdentity = boundedProcessIdentity(spec.processIdentity)
const multisite = booleanArg(args, "multisite")
const resultFile = processIdentity ? `/tmp/wp-codebox-phpunit-result-${processIdentity}.txt` : PLUGIN_PHPUNIT_RESULT_FILE
const diagnosticHostFile = `/wordpress/wp-content/plugins/${pluginSlug}/.pg-test-result${processIdentity ? `-${processIdentity}` : ""}.txt`
const code = explicitCode ? await phpCodeFromArgs(args, "wordpress.phpunit", false) : phpunitRunCode({
Expand All @@ -950,7 +952,8 @@ export async function runPhpunitCommand({
preloadFiles: jsonArrayArg(args, "preload-files-json").filter((value): value is string => typeof value === "string"),
bootstrapMode,
projectBootstrap: argValue(args, "project-bootstrap")?.trim() || "",
multisite: booleanArg(args, "multisite"),
multisite,
preinstalledMultisite: bootstrapMode === "managed" && databaseType === "mysql" && multisite,
databaseType,
resultFile,
})
Expand All @@ -960,6 +963,17 @@ export async function runPhpunitCommand({
let response: PlaygroundRunResponse
try {
const bootstrapArgs = explicitCode ? args : [...args, "bootstrap=runtime-only"]
if (!explicitCode && bootstrapMode === "managed" && databaseType === "mysql" && multisite) {
const preinstallCode = phpunitMultisitePreinstallCode({
testsDir: argValue(args, "tests-dir")?.trim() || "/wp-codebox-vendor/wp-phpunit/wp-phpunit",
env: jsonObjectArg(args, "env-json"),
wpConfigDefines: jsonObjectArg(args, "wp-config-defines-json"),
databaseType,
resultFile,
})
const preinstallResponse = await runPlaygroundCommand("wordpress.phpunit", server, { code: bootstrapPhpCode(runtimeSpec, preinstallCode, bootstrapArgs) })
assertPlaygroundResponseOk("wordpress.phpunit multisite preinstall", preinstallResponse)
}
response = await runPlaygroundCommand("wordpress.phpunit", server, { code: bootstrapPhpCode(runtimeSpec, code, bootstrapArgs, undefined, resultFile) })
} catch (error) {
await persistPluginPhpunitResult(server, resultFile, artifactRoot, processIdentity)
Expand Down
15 changes: 14 additions & 1 deletion tests/disposable-mysql-mysqli.integration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -149,9 +149,12 @@ require_once ABSPATH . 'wp-settings.php';
assert.deepEqual(aggregate.entries.map((entry: { inputIndex: number }) => entry.inputIndex), [0, 1])

const multisitePlugin = join(directory, "managed-multisite-fixture")
const multisiteDependency = join(directory, "managed-multisite-dependency")
const multisiteRecipePath = join(directory, "managed-multisite-recipe.json")
const multisiteArtifacts = join(directory, "managed-multisite-artifacts")
await mkdir(join(multisitePlugin, "tests"), { recursive: true })
await mkdir(multisiteDependency, { recursive: true })
await writeFile(join(multisiteDependency, "managed-multisite-dependency.php"), "<?php\n/** Plugin Name: Managed Multisite Dependency */\n")
await writeFile(join(multisitePlugin, "managed-multisite-fixture.php"), `<?php
/** Plugin Name: Managed Multisite Fixture */
add_action('init', static function (): void {
Expand Down Expand Up @@ -189,6 +192,15 @@ final class ManagedMultisiteTest extends WP_UnitTestCase {
$this->assertInstanceOf(mysqli::class, $wpdb->dbh);
$this->assertSame('1', (string) $wpdb->get_var("SELECT JSON_VALID('{\\"valid\\":true}')"));
}

public function test_network_schema_seed_and_dependency_are_materialized(): void {
global $wpdb;
$this->assertGreaterThanOrEqual(1, (int) $wpdb->get_var("SELECT COUNT(*) FROM {$wpdb->blogs} WHERE blog_id = 1"));
$this->assertSame($wpdb->sitemeta, $wpdb->get_var($wpdb->prepare('SHOW TABLES LIKE %s', $wpdb->sitemeta)));
update_network_option(1, 'managed_multisite_fixture', 'available');
$this->assertSame('available', get_network_option(1, 'managed_multisite_fixture'));
$this->assertFileExists('/wordpress/wp-content/plugins/managed-multisite-dependency/managed-multisite-dependency.php');
}
}
`)
const multisiteRecipe = buildWordPressPhpunitRecipe({
Expand All @@ -197,7 +209,8 @@ final class ManagedMultisiteTest extends WP_UnitTestCase {
databaseType: "mysql",
multisite: true,
pluginSource: multisitePlugin,
dependencyMounts: ["/wordpress/wp-content/plugins/managed-multisite-fixture"],
extra_plugins: [{ source: multisiteDependency, slug: "managed-multisite-dependency", activate: false }],
dependencyMounts: ["/wordpress/wp-content/plugins/managed-multisite-fixture", "/wordpress/wp-content/plugins/managed-multisite-dependency"],
mounts: [{ source: join(harness, "vendor"), target: "/wp-codebox-vendor", mode: "readonly" }],
})
await writeFile(multisiteRecipePath, `${JSON.stringify(multisiteRecipe)}\n`)
Expand Down
Loading
Loading