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
55 changes: 24 additions & 31 deletions packages/runtime-playground/src/phpunit-command-handlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,7 @@ function phpunitConfigDiscoveryPhp(options: PhpunitConfigDiscoveryPhpOptions): s
$base = ${options.basePathExpression};
$testsuites = $xml->xpath('//testsuite') ?: array();
if (!empty($selected_testsuites)) {
$directories = array();
$selected = array_fill_keys($selected_testsuites, true);
$available = array();
$testsuites = array_values(array_filter($testsuites, static function($testsuite) use ($selected, &$available): bool {
Expand Down Expand Up @@ -326,6 +327,7 @@ function phpunitArgsPhp(functionName: string, logFunction: string): string {

function ${functionName}(array $argv) {
$arguments = array('colors' => 'never', 'testdox' => true, 'verbose' => false, 'cacheResult' => false, 'cacheResultFile' => ${functionName}_private_cache_result_file(), 'extensions' => array());
$selected_testsuites = array();
$args = array_slice($argv, 1);
for ($i = 0; $i < count($args); $i++) {
$arg = $args[$i];
Expand All @@ -339,6 +341,21 @@ function ${functionName}(array $argv) {
${logFunction}('NOTICE:phpunit filter applied: ' . $arguments['filter']);
continue;
}
$testsuite = null;
if ($arg === '--testsuite' && isset($args[$i + 1])) {
$testsuite = $args[++$i];
} elseif (strpos($arg, '--testsuite=') === 0) {
$testsuite = substr($arg, strlen('--testsuite='));
}
if ($testsuite !== null) {
foreach (explode(',', (string) $testsuite) as $name) {
$name = trim($name);
if ($name !== '') {
$selected_testsuites[$name] = true;
}
}
continue;
}
if ($arg === '--list-tests') {
$arguments['listTests'] = true;
continue;
Expand All @@ -352,35 +369,11 @@ function ${functionName}(array $argv) {
continue;
}
}
$arguments['wpCodeboxTestsuites'] = array_keys($selected_testsuites);
return $arguments;
}`
}

function phpunitSelectedTestsuitesPhp(functionName: string): string {
return `function ${functionName}(array $argv): array {
$selected = array();
$args = array_slice($argv, 1);
for ($i = 0; $i < count($args); $i++) {
$value = null;
if ($args[$i] === '--testsuite' && isset($args[$i + 1])) {
$value = $args[++$i];
} elseif (strpos($args[$i], '--testsuite=') === 0) {
$value = substr($args[$i], strlen('--testsuite='));
}
if ($value === null) {
continue;
}
foreach (explode(',', (string) $value) as $name) {
$name = trim($name);
if ($name !== '') {
$selected[$name] = true;
}
}
}
return array_keys($selected);
}`
}

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';
Expand Down Expand Up @@ -524,8 +517,6 @@ function pg_build_phpunit_argv($raw): array {
return $phpunit_argv;
}

${phpunitSelectedTestsuitesPhp("pg_selected_phpunit_testsuites")}

@file_put_contents($result_file, '');

function pg_log($msg) {
Expand Down Expand Up @@ -1263,6 +1254,8 @@ ${phpunitChangedTestFilterPhp({
testsPathFallback: true,
})}

${phpunitArgsPhp("wp_codebox_phpunit_args", "pg_log")}

pg_install_diagnostics_handlers();

pg_stage_begin('cwd');
Expand All @@ -1283,6 +1276,9 @@ $phpunit_argv = pg_build_phpunit_argv($phpunit_args_raw);
$argv = $phpunit_argv;
$_SERVER['argv'] = $phpunit_argv;
$_SERVER['argc'] = count($phpunit_argv);
$phpunit_args = wp_codebox_phpunit_args($phpunit_argv);
$selected_testsuites = $phpunit_args['wpCodeboxTestsuites'];
unset($phpunit_args['wpCodeboxTestsuites']);

if (!is_array($wp_config_defines)) {
$wp_config_defines = array();
Expand Down Expand Up @@ -1468,7 +1464,7 @@ try {
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, pg_selected_phpunit_testsuites($phpunit_argv));
list($directories, $suffixes, $prefixes, $excludes, $configured_files) = wp_codebox_phpunit_parse_config(${JSON.stringify(options.phpunitXml)}, $test_dir, $selected_testsuites);
$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 Expand Up @@ -1529,8 +1525,6 @@ foreach (array_diff($after_classes, $before_classes) as $class_name) {
}
pg_stage_ok('load_tests');

${phpunitArgsPhp("wp_codebox_phpunit_args", "pg_log")}

function wp_codebox_phpunit_print_test_list($test) {
if ($test instanceof PHPUnit\\Framework\\TestSuite) {
foreach ($test->tests() as $child) {
Expand All @@ -1546,7 +1540,6 @@ function wp_codebox_phpunit_print_test_list($test) {
pg_stage_begin('run_tests');
pg_log('RUNNING ' . count($test_files) . ' TEST FILES');
try {
$phpunit_args = wp_codebox_phpunit_args($phpunit_argv);
if (!empty($phpunit_args['listTests'])) {
wp_codebox_phpunit_print_test_list($suite);
pg_log('ALL TESTS PASSED');
Expand Down
21 changes: 14 additions & 7 deletions tests/phpunit-project-autoload.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -160,8 +160,10 @@ function assertPhpunitConfigurationAndDiscoveryFailures(source: string, function
writeFileSync(malformedAdjacentXml, "<phpunit><testsuites><testsuite><directory>must-not-mask-malformed</directory></testsuite></testsuites></phpunit>")
writeFileSync(explicitAdjacentXml, "<phpunit><testsuites><testsuite><directory>must-not-fallback</directory></testsuite></testsuites></phpunit>")
mkdirSync(join(tempDir, "default"), { recursive: true })
mkdirSync(join(tempDir, "single"), { recursive: true })
writeFileSync(adjacentXml, "<phpunit><testsuites><testsuite><directory>fallback-tests</directory></testsuite></testsuites></phpunit>")
writeFileSync(selectedXml, '<phpunit><testsuites><testsuite name="first"><directory suffix="FirstTest.php">first-tests</directory></testsuite><testsuite name="second"><directory suffix="SecondTest.php">second-tests</directory></testsuite></testsuites></phpunit>')
writeFileSync(join(tempDir, "single", "FileOnlyTest.php"), "<?php")
writeFileSync(selectedXml, '<phpunit><testsuites><testsuite name="first"><directory suffix="FirstTest.php">first-tests</directory></testsuite><testsuite name="second"><directory suffix="SecondTest.php">second-tests</directory></testsuite><testsuite name="file-only"><file>single/FileOnlyTest.php</file></testsuite></testsuites></phpunit>')

const parseConfigFunction = extractPhpFunction(source, functionName)
const discoveryFunction = extractPhpFunction(source, discoveryFunctionName)
Expand Down Expand Up @@ -193,6 +195,14 @@ $multiple = ${functionName}(${phpString(selectedXml)}, ${phpString(join(tempDir,
if ($multiple[0] !== array(${phpString(join(tempDir, "first-tests"))}, ${phpString(join(tempDir, "second-tests"))})) {
throw new RuntimeException('multiple testsuite discovery mismatch: ' . json_encode($multiple));
}
$file_only = ${functionName}(${phpString(selectedXml)}, ${phpString(join(tempDir, "tests"))}, array('file-only'));
if ($file_only[0] !== array() || $file_only[4] !== array(${phpString(join(tempDir, "single", "FileOnlyTest.php"))})) {
throw new RuntimeException('file-only testsuite discovery mismatch: ' . json_encode($file_only));
}
$file_only_discovered = ${discoveryFunctionName}($file_only[0], $file_only[1], $file_only[2], $file_only[3], $file_only[4]);
if ($file_only_discovered !== array(${phpString(join(tempDir, "single", "FileOnlyTest.php"))})) {
throw new RuntimeException('file-only testsuite loaded unrelated files: ' . json_encode($file_only_discovered));
}
assert_phpunit_error(function() { ${functionName}(${phpString(selectedXml)}, ${phpString(join(tempDir, "tests"))}, array('missing')); }, 'Requested PHPUnit testsuite not found in config: missing', 'unknown selected testsuite');
${supportsImplicitFallback ? `
$fallback = ${functionName}(${phpString(defaultXml)}, ${phpString(join(tempDir, "tests"))});
Expand Down Expand Up @@ -433,6 +443,7 @@ final class TestRunner {
namespace {
$test_files = array(${phpString(testFile)});
$phpunit_argv = array('phpunit');
$phpunit_args = array();
$argv = array('phpunit');
function ${stagePrefix}_stage_begin($stage) { file_put_contents(getenv('STAGE_LOG'), 'STAGE_BEGIN:' . $stage . "\\n", FILE_APPEND); }
function ${stagePrefix}_stage_ok($stage) { file_put_contents(getenv('STAGE_LOG'), 'STAGE_OK:' . $stage . "\\n", FILE_APPEND); }
Expand Down Expand Up @@ -907,32 +918,28 @@ assert.ok(externalMysqlMultisiteRecipe.workflow.steps[0].args.includes("multisit

const phpunitCacheAllocator = extractPhpFunction(managedModeCode, "wp_codebox_phpunit_args_private_cache_result_file")
const phpunitArgsFunction = extractPhpFunction(managedModeCode, "wp_codebox_phpunit_args")
const selectedTestsuitesFunction = extractPhpFunction(managedModeCode, "pg_selected_phpunit_testsuites")
const phpunitArgsProbe = join(mkdtempSync(join(tmpdir(), "wp-codebox-phpunit-cache-args-")), "probe.php")
writeFileSync(phpunitArgsProbe, `<?php
function pg_log($message) {}
${phpunitCacheAllocator}
${phpunitArgsFunction}
${selectedTestsuitesFunction}
echo json_encode(array(
'first' => wp_codebox_phpunit_args(array('phpunit', '--filter', 'OnlyTest', '--cache-result-file=/wordpress/ignored.cache')),
'first' => wp_codebox_phpunit_args(array('phpunit', '--filter', 'OnlyTest', '--testsuite', 'first, second', '--testsuite=third', '--testsuite=first', '--cache-result-file=/wordpress/ignored.cache')),
'second' => wp_codebox_phpunit_args(array('phpunit', '--cache-result-file', 'ignored.cache')),
'firstMode' => fileperms(wp_codebox_phpunit_args(array('phpunit'))['cacheResultFile']) & 0777,
'selectedSuites' => pg_selected_phpunit_testsuites(array('phpunit', '--testsuite', 'first, second', '--testsuite=third', '--testsuite=first')),
));
`)
const phpunitArgs = JSON.parse(execFileSync("php", [phpunitArgsProbe], { encoding: "utf8" })) as {
first: Record<string, unknown>
second: Record<string, unknown>
firstMode: number
selectedSuites: string[]
}
for (const argumentSet of [phpunitArgs.first, phpunitArgs.second]) {
assert.equal(argumentSet.cacheResult, false, "PHPUnit result caching must start disabled")
assert.match(String(argumentSet.cacheResultFile), /^\/tmp\/wp-codebox-phpunit-[a-f0-9]{48}\.cache$/, "cache file must be privately allocated under /tmp")
}
assert.equal(phpunitArgs.first.filter, "OnlyTest", "unrecognized caller cache options must not affect supported PHPUnit options")
assert.deepEqual(phpunitArgs.selectedSuites, ["first", "second", "third"], "PHPUnit testsuite selectors support both CLI forms, comma lists, and stable deduplication")
assert.deepEqual(phpunitArgs.first.wpCodeboxTestsuites, ["first", "second", "third"], "PHPUnit testsuite selectors support both CLI forms, comma lists, and stable deduplication")
assert.notEqual(phpunitArgs.first.cacheResultFile, phpunitArgs.second.cacheResultFile, "each PHPUnit invocation must receive an unpredictable cache path")
assert.equal(phpunitArgs.firstMode, 0o600, "the internal cache file must be private to the sandbox process")
assert.equal(existsSync(String(phpunitArgs.first.cacheResultFile)), false, "the internal cache must be removed at PHP shutdown")
Expand Down
12 changes: 11 additions & 1 deletion tests/playground-readonly-mounts-integration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ const root = await mkdtemp(join(tmpdir(), "wp-codebox-readonly-mounts-integratio
const projectSource = join(root, "project")
const projectConfigSource = join(projectSource, "config.php")
const overlaySource = join(root, "config-overlay.php")
const readonlyProjectSource = join(root, "readonly-project")
const readonlyProjectConfigSource = join(readonlyProjectSource, "config.php")
const readonlyProjectOverlaySource = join(root, "readonly-config-overlay.php")
const readonlySource = join(root, "readonly.bin")
const readwriteSource = join(root, "readwrite.bin")
const stagedBinarySource = join(root, "staged-binary.bin")
Expand All @@ -25,8 +28,11 @@ const stagingDirectoriesBefore = await readonlyStagingDirectories()

try {
await mkdir(projectSource)
await mkdir(readonlyProjectSource)
await writeFile(projectConfigSource, originalConfig)
await writeFile(overlaySource, overlayConfig)
await writeFile(readonlyProjectConfigSource, originalConfig)
await writeFile(readonlyProjectOverlaySource, overlayConfig)
await writeFile(readonlySource, readonlyBytes)
await writeFile(readwriteSource, readonlyBytes)
await writeFile(stagedBinarySource, stagedBinaryBytes)
Expand All @@ -37,6 +43,8 @@ try {
mounts: [
{ source: projectSource, target: "/home/project", mode: "readwrite" },
{ source: overlaySource, target: "/home/project/config.php", mode: "readonly" },
{ source: readonlyProjectSource, target: "/home/readonly-project", mode: "readonly" },
{ source: readonlyProjectOverlaySource, target: "/home/readonly-project/config.php", mode: "readonly" },
{ source: readonlySource, target: "/wordpress/readonly.bin", mode: "readonly" },
{ source: readwriteSource, target: "/wordpress/readwrite.bin", mode: "readwrite" },
],
Expand All @@ -47,7 +55,7 @@ try {
workflow: {
steps: [{
command: "wordpress.run-php",
args: [`code=$config = file_get_contents('/home/project/config.php'); if ($config !== "<?php return 'overlay';\\n") { fwrite(STDERR, $config); exit(1); } $staged = file_get_contents('/wordpress/staged-binary.bin'); if ($staged !== base64_decode('${stagedBinaryBytes.toString("base64")}')) { fwrite(STDERR, 'staged binary bytes changed'); exit(1); } $contents = base64_decode('${overwrittenBytes.toString("base64")}'); file_put_contents('/home/project/config.php', "overwritten"); file_put_contents('/home/project/mutated.txt', 'parent mutation'); file_put_contents('/wordpress/readonly.bin', $contents); file_put_contents('/wordpress/readwrite.bin', $contents);`],
args: [`code=$config = file_get_contents('/home/project/config.php'); if ($config !== "<?php return 'overlay';\\n") { fwrite(STDERR, $config); exit(1); } $readonly_config = file_get_contents('/home/readonly-project/config.php'); if ($readonly_config !== "<?php return 'overlay';\\n") { fwrite(STDERR, $readonly_config); exit(1); } $staged = file_get_contents('/wordpress/staged-binary.bin'); if ($staged !== base64_decode('${stagedBinaryBytes.toString("base64")}')) { fwrite(STDERR, 'staged binary bytes changed'); exit(1); } $contents = base64_decode('${overwrittenBytes.toString("base64")}'); file_put_contents('/home/project/config.php', "overwritten"); file_put_contents('/home/project/mutated.txt', 'parent mutation'); file_put_contents('/home/readonly-project/config.php', "overwritten"); file_put_contents('/wordpress/readonly.bin', $contents); file_put_contents('/wordpress/readwrite.bin', $contents);`],
}],
},
})}\n`)
Expand All @@ -59,6 +67,8 @@ try {
assert.deepEqual(await readFile(readwriteSource), overwrittenBytes, "readwrite host bytes must reflect an actual Playground PHP overwrite")
assert.equal(await readFile(overlaySource, "utf8"), overlayConfig, "readonly overlay source must survive an actual Playground PHP overwrite")
assert.equal(await readFile(projectConfigSource, "utf8"), originalConfig, "the parent writeback must exclude the nested overlay path")
assert.equal(await readFile(readonlyProjectOverlaySource, "utf8"), overlayConfig, "an overlay nested beneath a readonly parent must remain isolated")
assert.equal(await readFile(readonlyProjectConfigSource, "utf8"), originalConfig, "a readonly parent source must not receive nested overlay writes")
assert.equal(await readFile(join(projectSource, "mutated.txt"), "utf8"), "parent mutation", "parent readwrite mutations must still materialize")
assert.deepEqual(await readonlyStagingDirectories(), stagingDirectoriesBefore, "recipe-run cleanup must remove readonly mount staging")
}
Expand Down
Loading