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
4 changes: 3 additions & 1 deletion packages/cli/src/commands/recipe-build.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { readFile, writeFile } from "node:fs/promises"
import { buildGenericAbilityRuntimeRunRecipe, buildRuntimePackageRunRecipe, buildWordPressBenchRecipe, buildWordPressPhpunitRecipe, compileRecipeTemplate, type GenericAbilityRuntimeRunOptions, type RecipeTemplateInput, type RuntimePackageRunRecipeOptions, type RuntimeWordPressInstallMode, type WorkspaceRecipe, type WorkspaceRecipeExtraPlugin, type WorkspaceRecipeMount, type WorkspaceRecipePHPWasmExtensionManifest, type WorkspaceRecipeRuntimeBackendPackage, type WorkspaceRecipeRuntimeService, type WorkspaceRecipeStep } from "@automattic/wp-codebox-core"
import { buildGenericAbilityRuntimeRunRecipe, buildRuntimePackageRunRecipe, buildWordPressBenchRecipe, buildWordPressPhpunitRecipe, compileRecipeTemplate, type GenericAbilityRuntimeRunOptions, type RecipeTemplateInput, type RuntimePackageRunRecipeOptions, type RuntimeWordPressInstallMode, type WorkspaceRecipe, type WorkspaceRecipeExtraPlugin, type WorkspaceRecipeMount, type WorkspaceRecipePHPWasmExtensionManifest, type WorkspaceRecipePluginRuntime, type WorkspaceRecipeRuntimeBackendPackage, type WorkspaceRecipeRuntimeService, type WorkspaceRecipeStep } from "@automattic/wp-codebox-core"

interface RecipeBuildOptions {
recipeType: "phpunit" | "bench" | "template" | "generic-ability-runtime-run" | "runtime-package-run"
Expand All @@ -16,6 +16,7 @@ interface WordPressPhpunitBuilderOptions {
backendPackage?: WorkspaceRecipeRuntimeBackendPackage
mounts?: WorkspaceRecipeMount[]
services?: WorkspaceRecipeRuntimeService[]
pluginRuntime?: WorkspaceRecipePluginRuntime
extra_plugins?: WorkspaceRecipeExtraPlugin[]
pluginSource?: string
pluginSlug: string
Expand Down Expand Up @@ -86,6 +87,7 @@ function buildRecipe(recipeType: RecipeBuildOptions["recipeType"], options: Word
backendPackage: phpunitOptions.backendPackage,
mounts: Array.isArray(phpunitOptions.mounts) ? phpunitOptions.mounts : [],
services: Array.isArray(phpunitOptions.services) ? phpunitOptions.services : [],
pluginRuntime: phpunitOptions.pluginRuntime ? plainObject(phpunitOptions.pluginRuntime) as WorkspaceRecipePluginRuntime : undefined,
extra_plugins: Array.isArray(phpunitOptions.extra_plugins) ? phpunitOptions.extra_plugins : [],
pluginSource: stringOrUndefined(phpunitOptions.pluginSource),
pluginSlug: requiredString(phpunitOptions.pluginSlug, "pluginSlug"),
Expand Down
10 changes: 6 additions & 4 deletions packages/cli/src/recipe-validation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -743,11 +743,13 @@ function validateRecipeRuntimeServices(recipe: WorkspaceRecipe, addIssue: (code:
if (ids.has(service.id)) addIssue("duplicate-runtime-service-id", `${path}.id`, `Runtime service ids must be unique: ${service.id}`)
ids.add(service.id)
if (service.kind !== "mysql") addIssue("unsupported-runtime-service-kind", `${path}.kind`, `Unsupported managed runtime service kind: ${service.kind}`)
for (const [output, name] of Object.entries(service.outputs)) {
for (const [output, names] of Object.entries(service.outputs)) {
if (!/^(host|port|username|password|database)$/.test(output)) addIssue("unknown-runtime-service-output", `${path}.outputs.${output}`, `Unsupported ${service.kind} service output: ${output}`)
if (!/^[A-Z_][A-Z0-9_]*$/.test(name)) addIssue("invalid-runtime-service-env", `${path}.outputs.${output}`, "Runtime service environment variable names must match /^[A-Z_][A-Z0-9_]*$/.")
if (environment.has(name)) addIssue("duplicate-runtime-service-env", `${path}.outputs.${output}`, `Runtime service output environment variable is already declared: ${name}`)
environment.add(name)
for (const name of Array.isArray(names) ? names : [names]) {
if (!/^[A-Z_][A-Z0-9_]*$/.test(name)) addIssue("invalid-runtime-service-env", `${path}.outputs.${output}`, "Runtime service environment variable names must match /^[A-Z_][A-Z0-9_]*$/.")
if (environment.has(name)) addIssue("duplicate-runtime-service-env", `${path}.outputs.${output}`, `Runtime service output environment variable is already declared: ${name}`)
environment.add(name)
}
}
}
}
Expand Down
4 changes: 2 additions & 2 deletions packages/cli/src/runtime-services.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ const defaultDependencies: RuntimeServiceDependencies = {
randomBytes,
}

export function runtimeServicePlan(services: WorkspaceRecipeRuntimeService[]): Array<{ id: string; kind: string; provider: string; version: string; bind: "loopback"; port: "ephemeral"; persistentVolume: false; configuration?: WorkspaceRecipeRuntimeService["configuration"]; outputs: Record<string, string> }> {
export function runtimeServicePlan(services: WorkspaceRecipeRuntimeService[]): Array<{ id: string; kind: string; provider: string; version: string; bind: "loopback"; port: "ephemeral"; persistentVolume: false; configuration?: WorkspaceRecipeRuntimeService["configuration"]; outputs: Record<string, string | string[]> }> {
return services.map((service) => {
const provider = runtimeServiceProvider(service.kind)
return { id: service.id, kind: service.kind, provider: provider.name, version: provider.version(service), bind: "loopback", port: "ephemeral", persistentVolume: false, ...(service.configuration ? { configuration: service.configuration } : {}), outputs: service.outputs }
Expand Down Expand Up @@ -177,7 +177,7 @@ async function provisionMysqlDockerService(service: WorkspaceRecipeRuntimeServic
evidence.readiness = "ready"
evidence.lifecycle = "provisioned"
const values: Record<string, string> = { host: "127.0.0.1", port: String(port), username: "runtime", password, database: "runtime" }
return { env: Object.fromEntries(Object.entries(service.outputs).map(([output, name]) => [name, values[output] ?? ""])), evidence, async release() { await releaseService(container, evidence, dependencies) } }
return { env: Object.fromEntries(Object.entries(service.outputs).flatMap(([output, names]) => (Array.isArray(names) ? names : [names]).map((name) => [name, values[output] ?? ""]))), evidence, async release() { await releaseService(container, evidence, dependencies) } }
} catch (error) {
evidence.readiness = "failed"
evidence.lifecycle = "failed"
Expand Down
4 changes: 3 additions & 1 deletion packages/runtime-core/src/recipe-builders.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { RuntimePreviewSpec, RuntimeWordPressInstallMode, WorkspaceRecipe, WorkspaceRecipeExtraPlugin, WorkspaceRecipeMount, WorkspaceRecipePHPWasmExtensionManifest, WorkspaceRecipeRuntimeBackendPackage, WorkspaceRecipeRuntimeService, WorkspaceRecipeStep } from "./runtime-contracts.js"
import type { RuntimePreviewSpec, RuntimeWordPressInstallMode, WorkspaceRecipe, WorkspaceRecipeExtraPlugin, WorkspaceRecipeMount, WorkspaceRecipePHPWasmExtensionManifest, WorkspaceRecipePluginRuntime, WorkspaceRecipeRuntimeBackendPackage, WorkspaceRecipeRuntimeService, WorkspaceRecipeStep } from "./runtime-contracts.js"
import { commandArg, commandJsonArg, commandStringListArg } from "./command-codecs.js"
export { buildRuntimePackageRunRecipe, CODEBOX_RUN_RUNTIME_PACKAGE_ABILITY, RUNTIME_PACKAGE_ARTIFACT_DECLARATION_SCHEMA, RUNTIME_PACKAGE_EXECUTION_INPUT_SCHEMA, RUNTIME_PACKAGE_EXECUTION_RESULT_SCHEMA, RUNTIME_PACKAGE_OUTPUT_PROJECTION_SCHEMA, runtimePackageExecutionInput, type RuntimePackageArtifactDeclaration, type RuntimePackageExecutionInput, type RuntimePackageOutputProjection, type RuntimePackageRunRecipeOptions } from "./runtime-package-execution.js"
export { RUNTIME_PACKAGE_DIAGNOSTIC_SCHEMA, RUNTIME_PACKAGE_RESULT_SCHEMA, RUNTIME_PACKAGE_TASK_SCHEMA, normalizeRuntimePackageResult, normalizeRuntimePackageTask, validateRuntimePackageTask, type RuntimePackageDiagnostic, type RuntimePackageResult, type RuntimePackageTask } from "./runtime-package-contracts.js"
Expand All @@ -21,6 +21,7 @@ export interface WordPressPhpunitRecipeOptions {
preview?: RuntimePreviewSpec
mounts?: WorkspaceRecipeMount[]
services?: WorkspaceRecipeRuntimeService[]
pluginRuntime?: WorkspaceRecipePluginRuntime
extra_plugins?: WorkspaceRecipeExtraPlugin[]
pluginSource?: string
pluginSlug: string
Expand Down Expand Up @@ -88,6 +89,7 @@ export function buildWordPressPhpunitRecipe(options: WordPressPhpunitRecipeOptio
inputs: {
extra_plugins: normalizeExtraPlugins(options.extra_plugins),
...(options.services && options.services.length > 0 ? { services: options.services } : {}),
...(options.pluginRuntime ? { pluginRuntime: options.pluginRuntime } : {}),
mounts: normalizeRecipeMounts([
...(options.pluginSource ? [{ source: options.pluginSource, target: pluginTarget } satisfies WorkspaceRecipeMount] : []),
...(options.mounts ?? []),
Expand Down
7 changes: 6 additions & 1 deletion packages/runtime-core/src/recipe-schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -930,7 +930,12 @@ export function createWorkspaceRecipeJsonSchema(options: WorkspaceRecipeJsonSche
outputs: {
type: "object",
minProperties: 1,
additionalProperties: { type: "string", pattern: "^[A-Z_][A-Z0-9_]*$" },
additionalProperties: {
anyOf: [
{ type: "string", pattern: "^[A-Z_][A-Z0-9_]*$" },
{ type: "array", minItems: 1, uniqueItems: true, items: { type: "string", pattern: "^[A-Z_][A-Z0-9_]*$" } },
],
},
},
},
},
Expand Down
4 changes: 2 additions & 2 deletions packages/runtime-core/src/runtime-contracts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -142,8 +142,8 @@ export interface WorkspaceRecipeRuntimeService {
rootAuthentication?: "generated-password" | "empty-password"
foreignKeyTargetPolicy?: "unique-only" | "indexed"
}
/** Explicit map from a provider output (for example `port`) to a runtime env name. */
outputs: Record<string, string>
/** Explicit map from a provider output (for example `port`) to one or more runtime env names. */
outputs: Record<string, string | string[]>
}

export interface WorkspaceRecipeRuntimeStack {
Expand Down
10 changes: 7 additions & 3 deletions packages/runtime-playground/src/php-bootstrap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,15 @@ ${phpBody(code)}`
}

export function bootstrapPhpCode(spec: RuntimeCreateSpec, code: string, args: string[], wpCliBridge?: PhpBootstrapBridge, failureDiagnosticFile?: string): string {
const command = splitLeadingStrictTypesDeclare(code)
if (argValue(args, "bootstrap") === "none") {
return code
return `<?php
${command.strictTypesDeclare ? `${command.strictTypesDeclare}\n` : ""}${runtimeEnvPhp(spec, args)}
${secretEnvPhp(spec)}
${command.body}`
}

const command = splitLeadingStrictTypesDeclare(code)
const wordpressBootstrap = argValue(args, "bootstrap-mode") === "project" ? "" : "require_once '/wordpress/wp-load.php';"

const bootstrapped = `<?php
${command.strictTypesDeclare ? `${command.strictTypesDeclare}\n` : ""}${phpFatalDiagnosticPhp()}
Expand All @@ -36,7 +40,7 @@ ${saveQueriesBootstrapPhp(args)}
${runtimeEnvPhp(spec, args)}
${secretEnvPhp(spec)}
${componentManifestPhp(spec)}
require_once '/wordpress/wp-load.php';
${wordpressBootstrap}
${failureDiagnosticFile ? phpFailureDiagnosticCompletionPhp() : ""}
${recipeActivePluginBootstrapPhp(spec, args)}
${wpCliBridge ? `putenv(${JSON.stringify(`WP_CODEBOX_TERMINAL_ACTION_URL=${wpCliBridge.url}`)});
Expand Down
50 changes: 45 additions & 5 deletions packages/runtime-playground/src/phpunit-command-handlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ function phpunitConfigDiscoveryPhp(options: PhpunitConfigDiscoveryPhpOptions): s
const suffixAssignment = options.replaceDefaultMatchers ? "$suffixes = $config_suffixes;" : "$suffixes = array_merge($suffixes, $config_suffixes);"
const prefixAssignment = options.replaceDefaultMatchers ? "$prefixes = $config_prefixes;" : "$prefixes = array_merge($prefixes, $config_prefixes);"

return `function ${options.functionName}($xml_path, $test_dir_default) {
return `function ${options.functionName}($xml_path, $test_dir_default, $requested_suites = array()) {
$directories = array($test_dir_default);
$suffixes = array('Test.php');
$prefixes = array('test-');
Expand All @@ -118,11 +118,23 @@ function phpunitConfigDiscoveryPhp(options: PhpunitConfigDiscoveryPhpOptions): s
$first = $errors ? trim($errors[0]->message) : 'unknown';
throw new RuntimeException('PHPUnit config could not be parsed at ' . $xml_path . ': ' . $first);
}
$suite_nodes = $xml->xpath('//testsuite') ?: array();
if (!empty($requested_suites)) {
$requested_lookup = array_fill_keys(array_map('strval', $requested_suites), true);
$suite_nodes = array_values(array_filter($suite_nodes, static function($suite) use ($requested_lookup) {
return isset($requested_lookup[(string) ($suite['name'] ?? '')]);
}));
if (empty($suite_nodes)) {
throw new RuntimeException('Requested PHPUnit testsuite was not found in config: ' . implode(',', array_keys($requested_lookup)));
}
}
$directories = array();
$base = ${options.basePathExpression};
$config_dirs = array();
$config_suffixes = array();
$config_prefixes = array();
foreach ($xml->xpath('//testsuite/directory') ?: array() as $dir) {
foreach ($suite_nodes as $suite) {
foreach ($suite->xpath('./directory') ?: array() as $dir) {
$raw = trim((string) $dir);${directoryRestriction}
$config_dirs[] = $raw[0] === '/' ? rtrim($raw, '/') : rtrim($base . '/' . $raw, '/');
foreach (explode(',', (string) ($dir['suffix'] ?? '')) as $suffix) {
Expand All @@ -137,18 +149,23 @@ function phpunitConfigDiscoveryPhp(options: PhpunitConfigDiscoveryPhpOptions): s
$config_prefixes[] = $prefix;
}
}
}
}
foreach ($xml->xpath('//testsuite/exclude') ?: array() as $exclude) {
foreach ($suite_nodes as $suite) {
foreach ($suite->xpath('./exclude') ?: array() as $exclude) {
$raw = trim((string) $exclude);
if ($raw !== '') {
$excludes[] = $raw[0] === '/' ? rtrim($raw, '/') : rtrim($base . '/' . $raw, '/');
}
}
}
foreach ($xml->xpath('//testsuite/file') ?: array() as $file) {
foreach ($suite_nodes as $suite) {
foreach ($suite->xpath('./file') ?: array() as $file) {
$raw = trim((string) $file);
if ($raw !== '') {
$files[] = $raw[0] === '/' ? $raw : $base . '/' . $raw;
}
}
}
if (!empty($config_dirs)) {
$directories = $config_dirs;
Expand Down Expand Up @@ -836,6 +853,14 @@ function pg_run_project_bootstrap_stage(array $cfg): void {
$bootstrap = pg_project_bootstrap_from_config($phpunit_xml, $phpunit_xml_is_default);
$from_config = $bootstrap !== '';
}
if ($bootstrap === '') {
if (!$phpunit_xml_is_default && !is_readable($phpunit_xml)) {
throw new RuntimeException('explicit PHPUnit config is not readable: ' . $phpunit_xml);
}
pg_log('NOTICE:project bootstrap not declared; continuing without one');
pg_stage_ok('project_bootstrap');
return;
}
$bootstrap_real = pg_project_bootstrap_real_path($bootstrap, $phpunit_xml, $from_config);
if ($bootstrap_real === null) {
throw new RuntimeException('project bootstrap not found; pass project-bootstrap=<relative path> or declare phpunit bootstrap');
Expand Down Expand Up @@ -1219,13 +1244,28 @@ ${phpunitConfigDiscoveryPhp({

${phpunitDiscoveryPhp("wp_codebox_phpunit_discover", "pg_log")}

function wp_codebox_requested_phpunit_suites($args): array {
$suites = array();
if (!is_array($args)) {
return $suites;
}
foreach ($args as $index => $arg) {
if ($arg === '--testsuite' && isset($args[$index + 1])) {
$suites = array_merge($suites, explode(',', (string) $args[$index + 1]));
} elseif (is_string($arg) && strpos($arg, '--testsuite=') === 0) {
$suites = array_merge($suites, explode(',', substr($arg, strlen('--testsuite='))));
}
}
return array_values(array_filter(array_map('trim', $suites), static function($suite) { return $suite !== ''; }));
}

pg_stage_begin('discover_tests');
try {
$test_dir = $test_root;
if (!is_dir($test_dir)) {
throw new RuntimeException('configured PHPUnit test root is not a readable directory: ' . $test_dir);
}
list($directories, $suffixes, $prefixes, $excludes, $configured_files) = wp_codebox_phpunit_parse_config(${JSON.stringify(options.phpunitXml)}, $test_dir);
list($directories, $suffixes, $prefixes, $excludes, $configured_files) = wp_codebox_phpunit_parse_config(${JSON.stringify(options.phpunitXml)}, $test_dir, wp_codebox_requested_phpunit_suites($phpunit_args_raw));
$test_files = wp_codebox_phpunit_discover($directories, $suffixes, $prefixes, $excludes, $configured_files);
$test_files = pg_filter_changed_test_files($test_files, $changed_test_files_raw, $test_dir);
if ($selected_test_file !== '') {
Expand Down
Loading
Loading