Initial Drupal 11 with DDEV setup

This commit is contained in:
gluebox
2025-10-08 11:39:17 -04:00
commit 89ef74b305
25344 changed files with 2599172 additions and 0 deletions

View File

@ -0,0 +1,70 @@
<?php
namespace Consolidation\SiteProcess;
use PHPUnit\Framework\TestCase;
use Consolidation\SiteProcess\Util\ArgumentProcessor;
use Consolidation\SiteAlias\SiteAlias;
class ArgumentProcessorTest extends TestCase
{
/**
* Data provider for testArgumentProcessor.
*/
public function argumentProcessorTestValues()
{
return [
[
'["ls", "-al"]',
[],
['ls', '-al'],
[],
[],
],
[
'["drush", "status", "-vvv", "--fields=root,uri"]',
[],
['drush', 'status'],
['vvv' => TRUE, 'fields' => 'root,uri'],
[],
],
[
'["drush", "rsync", "a", "b", "--", "--exclude=vendor"]',
[],
['drush', 'rsync', 'a', 'b',],
[],
['exclude' => 'vendor'],
],
[
'["drush", "rsync", "a", "b", "--", "--exclude=vendor", "--include=vendor/autoload.php"]',
[],
['drush', 'rsync', 'a', 'b', '--', '--include=vendor/autoload.php'],
[],
['exclude' => 'vendor'],
],
];
}
/**
* Test the SiteProcess class.
*
* @dataProvider argumentProcessorTestValues
*/
public function testArgumentProcessor(
$expected,
$siteAliasData,
$args,
$options,
$optionsPassedAsArgs)
{
$siteAlias = new SiteAlias($siteAliasData, '@alias.dev');
$processor = new ArgumentProcessor();
$actual = $processor->selectArgs($siteAlias, $args, $options, $optionsPassedAsArgs);
$actual = '["' . implode('", "', $actual) . '"]';
$this->assertEquals($expected, $actual);
}
}

View File

@ -0,0 +1,173 @@
<?php
namespace Consolidation\SiteProcess;
use PHPUnit\Framework\TestCase;
use Consolidation\SiteProcess\Util\ArgumentProcessor;
use Consolidation\SiteAlias\SiteAlias;
use Consolidation\SiteProcess\Util\Escape;
class EscapeTest extends TestCase
{
const DEFAULT_SITE_ALIAS = ['host' => 'example.com', ];
const LINUX_SITE_ALIAS = ['host' => 'example.com', 'os' => 'Linux'];
const WINDOWS_SITE_ALIAS = ['host' => 'example.com', 'os' => 'WIN'];
/**
* Data provider for testIsWindows.
*/
public function isWindowsTestValues()
{
return [
[
'Linux',
false,
static::DEFAULT_SITE_ALIAS,
],
[
'Linux',
false,
static::LINUX_SITE_ALIAS,
],
[
'WIN',
true,
static::WINDOWS_SITE_ALIAS,
],
];
}
/**
* Test the isWindows method.
*
* @dataProvider isWindowsTestValues
*/
public function testIsWindows(
$expected,
$expectToBeWindows,
$siteAliasData)
{
$siteAlias = new SiteAlias($siteAliasData, '@alias.dev');
$actual = $siteAlias->os();
$this->assertEquals($expected, $actual);
$actuallyIsWindows = Escape::isWindows($siteAlias->os());
$this->assertEquals($expectToBeWindows, $actuallyIsWindows);
}
/**
* Data provider for testEscapeForSite.
*/
public function escapeForSiteTestValues()
{
return [
[
'foo',
'foo',
static::DEFAULT_SITE_ALIAS,
],
[
'foo',
'foo',
static::LINUX_SITE_ALIAS,
],
[
'foo',
'foo',
static::WINDOWS_SITE_ALIAS,
],
[
"'foo bar'",
'foo bar',
static::DEFAULT_SITE_ALIAS,
],
[
"'foo bar'",
'foo bar',
static::LINUX_SITE_ALIAS,
],
[
'"foo bar"',
'foo bar',
static::WINDOWS_SITE_ALIAS,
],
[
"'don'\\''t forget'",
"don't forget",
static::DEFAULT_SITE_ALIAS,
],
[
"'don'\\''t forget'",
"don't forget",
static::LINUX_SITE_ALIAS,
],
[
'"don\'t forget"',
"don't forget",
static::WINDOWS_SITE_ALIAS,
],
[
"'I'\''ll try the \"easy\" fix.'",
"I'll try the \"easy\" fix.",
static::DEFAULT_SITE_ALIAS,
],
[
"'I'\''ll try the \"easy\" fix.'",
"I'll try the \"easy\" fix.",
static::LINUX_SITE_ALIAS,
],
[
'"I\'ll try the ""easy"" fix."',
"I'll try the \"easy\" fix.",
static::WINDOWS_SITE_ALIAS,
],
[
"'a b'",
"a\tb",
static::DEFAULT_SITE_ALIAS,
],
[
"'a b'",
"a\tb",
static::LINUX_SITE_ALIAS,
],
[
'"a b"',
"a\tb",
static::WINDOWS_SITE_ALIAS,
],
];
}
/**
* Test the forSite method.
*
* @dataProvider escapeForSiteTestValues
*/
public function testEscapeForSite(
$expected,
$arg,
$siteAliasData)
{
$siteAlias = new SiteAlias($siteAliasData, '@alias.dev');
$actual = Escape::forSite($siteAlias, $arg);
$this->assertEquals($expected, $actual);
}
}

View File

@ -0,0 +1,78 @@
<?php
namespace Consolidation\SiteProcess;
use Consolidation\SiteProcess\Util\Escape;
use PHPUnit\Framework\TestCase;
use Consolidation\SiteProcess\Util\ArgumentProcessor;
use Consolidation\SiteAlias\SiteAlias;
use Symfony\Component\Console\Output\BufferedOutput;
use Symfony\Component\Console\Style\SymfonyStyle;
use Symfony\Component\Console\Input\ArrayInput;
class RealtimeOutputHandlerTest extends TestCase
{
/**
* Data provider for testRealtimeOutputHandler.
*/
public function realtimeOutputHandlerTestValues()
{
return [
[
'hello, world',
'',
['echo', 'hello, world'],
'LINUX',
],
[
'"hello, world"',
'',
['echo', 'hello, world'],
'WIN'
],
[
'README.md',
'',
['ls', 'README.md'],
'LINUX',
],
[
'',
'No such file or directory',
['ls', 'no/such/file'],
'LINUX',
],
];
}
/**
* Test the RealtimeOutputHandler class.
*
* @dataProvider realtimeOutputHandlerTestValues
*/
public function testRealtimeOutputHandler($expectedStdout, $expectedStderr, $args, $os)
{
if (Escape::isWindows() != Escape::isWindows($os)) {
$this->markTestSkipped("OS isn't supported");
}
$stdin = new ArrayInput([]);
$stdout = new BufferedOutput();
$stderr = new BufferedOutput();
$symfonyStyle = new SymfonyStyle($stdin, $stdout);
$process = new ProcessBase($args);
$process->setRealtimeOutput($symfonyStyle, $stderr);
$process->run($process->showRealtime());
$this->assertEquals($expectedStdout, trim($stdout->fetch()));
if (empty($expectedStderr)) {
$this->assertEquals('', trim($stderr->fetch()));
}
else {
$this->assertStringContainsString($expectedStderr, trim($stderr->fetch()));
}
}
}

View File

@ -0,0 +1,314 @@
<?php
namespace Consolidation\SiteProcess;
use PHPUnit\Framework\TestCase;
use Consolidation\SiteProcess\Util\ArgumentProcessor;
use Consolidation\SiteProcess\Util\Escape;
use Consolidation\SiteAlias\SiteAlias;
class SiteProcessTest extends TestCase
{
/**
* Data provider for testSiteProcess.
*/
public function siteProcessTestValues()
{
return [
[
"ls -al",
false,
false,
[],
['ls', '-al'],
[],
[],
NULL,
],
[
"ls -al",
'src',
false,
[],
['ls', '-al'],
[],
[],
NULL,
],
[
"ls -al /path1 /path2",
false,
false,
[],
['ls', '-al', '/path1', '/path2'],
[],
[],
NULL,
],
[
"ssh -o PasswordAuthentication=no www-admin@server.net 'ls -al'",
false,
false,
['host' => 'server.net', 'user' => 'www-admin'],
['ls', '-al'],
[],
[],
NULL,
],
[
"ssh -o PasswordAuthentication=no www-admin@server.net 'cd /srv/www/docroot && ls -al'",
false,
false,
['host' => 'server.net', 'user' => 'www-admin', 'root' => '/srv/www/docroot'],
['ls', '-al'],
[],
[],
NULL,
],
[
"ssh -o PasswordAuthentication=no www-admin@server.net 'cd src && ls -al'",
'src',
false,
['host' => 'server.net', 'user' => 'www-admin'],
['ls', '-al'],
[],
[],
NULL,
],
[
"ssh -t -o PasswordAuthentication=no www-admin@server.net 'ls -al'",
false,
true,
['host' => 'server.net', 'user' => 'www-admin'],
['ls', '-al'],
[],
[],
NULL,
],
[
"ssh -t -o PasswordAuthentication=no www-admin@server.net 'cd src && ls -al /path1 /path2'",
'src',
true,
['host' => 'server.net', 'user' => 'www-admin'],
['ls', '-al', '/path1', '/path2'],
[],
[],
NULL,
],
[
"ssh -t -o PasswordAuthentication=no www-admin@server.net 'cd src && ls -al /path1 /path2'",
'src',
true,
['host' => 'server.net', 'user' => 'www-admin'],
['ls', '-al', '/path1', '/path2'],
[],
[],
NULL,
],
[
"docker-compose exec --workdir src --user root drupal ls -al /path1 /path2",
'src',
true,
['docker' => ['service' => 'drupal', 'exec' => ['options' => '--user root']]],
['ls', '-al', '/path1', '/path2'],
[],
[],
NULL,
],
[
"docker-compose -p project exec --workdir src --user root drupal ls -al /path1 /path2",
'src',
true,
['docker' => ['service' => 'drupal', 'project' => 'project', 'exec' => ['options' => '--user root']]],
['ls', '-al', '/path1', '/path2'],
[],
[],
NULL,
],
[
"drush status '--fields=root,uri'",
false,
false,
[],
['drush', 'status'],
['fields' => 'root,uri'],
[],
'LINUX',
],
[
'drush status --fields=root,uri',
false,
false,
[],
['drush', 'status'],
['fields' => 'root,uri'],
[],
'WIN',
],
[
"drush rsync a b -- --exclude=vendor",
false,
false,
[],
['drush', 'rsync', 'a', 'b',],
[],
['exclude' => 'vendor'],
NULL,
],
[
"drush rsync a b -- --exclude=vendor --include=vendor/autoload.php",
false,
false,
[],
['drush', 'rsync', 'a', 'b', '--', '--include=vendor/autoload.php'],
[],
['exclude' => 'vendor'],
NULL,
],
[
"env foo=bar baz=zong drush status",
false,
false,
['env-vars' => ['foo' => 'bar', 'baz' => 'zong']],
['drush', 'status'],
[],
[],
NULL,
],
];
}
/**
* Test the SiteProcess class.
*
* @dataProvider siteProcessTestValues
*/
public function testSiteProcess(
$expected,
$cd,
$useTty,
$siteAliasData,
$args,
$options,
$optionsPassedAsArgs,
$os)
{
if (Escape::isWindows() != Escape::isWindows($os)) {
$this->markTestSkipped("OS isn't supported");
}
if ($useTty && Escape::isWindows($os)) {
$this->markTestSkipped('Windows doesn\'t have /dev/tty support');
}
// Symfony 7 won't let us use setTty when there isn't an actual tty,
// whereas Symfony 6 is more forgiving. (Our test doesn't actually need
// the tty.) Since I don't know a good way to detect Symfony 7+, I instead
// allow the test to run on PHP < 8.2.0, since Symfony 7 requires PHP 8.2+.
if ($useTty && getenv('CI') && (version_compare("8.2.0", PHP_VERSION) <= 0)) {
$this->markTestSkipped('CI doesn\'t provide /dev/tty support');
}
$processManager = ProcessManager::createDefault();
$siteAlias = new SiteAlias($siteAliasData, '@alias.dev');
$siteProcess = $processManager->siteProcess($siteAlias, $args, $options, $optionsPassedAsArgs);
$siteProcess->setTty($useTty);
// The transport handles the chdir during processArgs().
$fallback = $siteAlias->hasRoot() ? $siteAlias->root() : null;
$siteProcess->setWorkingDirectory($cd ?: $fallback);
$actual = $siteProcess->getCommandLine();
$this->assertEquals($expected, $actual);
}
/**
* Data provider for testSiteProcessJson.
*/
public function siteProcessJsonTestValues()
{
return [
[
'Output is empty.',
'',
'LINUX',
],
[
"Unable to decode output into JSON: Syntax error\n\nNo json data here",
'No json data here',
NULL,
],
[
'{"foo":"bar"}',
'{"foo":"bar"}',
NULL,
],
[
'{"foo":"b\'ar"}',
'{"foo":"b\'ar"}',
NULL,
],
[
'{"foo":"bar"}',
'Ignored leading data {"foo":"bar"} Ignored trailing data',
NULL,
],
[
'["a","b","c"]',
'["a", "b", "c"]',
NULL,
],
[
'"string"',
'"string"',
NULL,
],
[
'[]',
'[]',
NULL,
],
];
}
/**
* Test the SiteProcess class.
*
* @dataProvider siteProcessJsonTestValues
*/
public function testSiteProcessJson(
$expected,
$data,
$os)
{
if (Escape::isWindows()) {
$this->markTestSkipped("Windows is not working yet. PRs welcome.");
}
$args = ['echo', $data];
$processManager = ProcessManager::createDefault();
$siteAlias = new SiteAlias([], '@alias.dev');
$siteAlias->set('os', $os);
$siteProcess = $processManager->siteProcess($siteAlias, $args);
$siteProcess->mustRun();
try {
$actual = $siteProcess->getOutputAsJson();
$actual = json_encode($actual, true);
}
catch (\Exception $e) {
$actual = $e->getMessage();
}
$this->assertEquals($expected, $actual);
}
}

View File

@ -0,0 +1,96 @@
<?php
namespace Consolidation\SiteProcess;
use Consolidation\SiteProcess\Transport\DockerComposeTransport;
use PHPUnit\Framework\TestCase;
use Consolidation\SiteAlias\SiteAlias;
class DockerComposeTransportTest extends TestCase
{
/**
* Data provider for testWrap.
*/
public function wrapTestValues()
{
return [
[
'docker-compose --project project --project-directory projectDir --file myCompose.yml exec -T --user root drupal ls',
[
'docker' => [
'service' => 'drupal',
'compose' => [
'options' => '--project project --project-directory projectDir --file myCompose.yml'
],
'exec' => ['options' => '--user root']
]
],
],
[
'docker-compose exec -T drupal ls',
[
'docker' => [
'service' => 'drupal',
]
],
],
[
'docker compose exec -T drupal ls',
[
'docker' => [
'service' => 'drupal',
'compose' => [
'version' => '2',
],
]
],
],
[
'docker-compose exec -T drupal ls',
[
'docker' => [
'service' => 'drupal',
'compose' => [
'version' => '1',
]
]
],
],
[
'docker-compose --project project2 --file myCompose.yml exec -T drupal ls',
[
'docker' => [
'service' => 'drupal',
'project' => 'project1',
'compose' => [
'options' => '--project project2 --file myCompose.yml'
]
]
],
],
[
'docker-compose -p project1 --file myCompose.yml exec -T drupal ls',
[
'docker' => [
'service' => 'drupal',
'project' => 'project1',
'compose' => [
'options' => '--file myCompose.yml'
]
]
],
],
];
}
/**
* @dataProvider wrapTestValues
*/
public function testWrap($expected, $siteAliasData)
{
$siteAlias = new SiteAlias($siteAliasData, '@alias.dev');
$dockerTransport = new DockerComposeTransport($siteAlias);
$actual = $dockerTransport->wrap(['ls']);
$this->assertEquals($expected, implode(' ', $actual));
}
}

View File

@ -0,0 +1,116 @@
<?php
namespace Consolidation\SiteProcess;
use Consolidation\SiteProcess\Transport\KubectlTransport;
use PHPUnit\Framework\TestCase;
use Consolidation\SiteAlias\SiteAlias;
class KubectlTransportTest extends TestCase
{
/**
* Data provider for testWrap.
*/
public function wrapTestValues()
{
return [
// Everything explicit.
[
'kubectl --namespace=vv exec --tty=false --stdin=false deploy/drupal --container=drupal -- ls',
['ls'],
[
'kubectl' => [
'tty' => false,
'interactive' => false,
'namespace' => 'vv',
'resource' => 'deploy/drupal',
'container' => 'drupal',
]
],
],
// Minimal. Kubectl will pick a container.
[
'kubectl --namespace=vv exec --tty=false --stdin=false deploy/drupal -- ls',
['ls'],
[
'kubectl' => [
'namespace' => 'vv',
'resource' => 'deploy/drupal',
]
],
],
// Don't escape arguments after "--"
[
'kubectl --namespace=vv exec --tty=false --stdin=false deploy/drupal -- asdf "double" \'single\'',
['asdf', '"double"', "'single'"],
[
'kubectl' => [
'namespace' => 'vv',
'resource' => 'deploy/drupal',
]
],
],
// With kubeconfig.
[
'kubectl --namespace=vv exec --tty=false --stdin=false deploy/drupal --container=drupal --kubeconfig=/path/to/config.yaml -- ls',
['ls'],
[
'kubectl' => [
'tty' => false,
'interactive' => false,
'namespace' => 'vv',
'resource' => 'deploy/drupal',
'container' => 'drupal',
'kubeconfig' => '/path/to/config.yaml',
]
],
],
// With entrypoint as string.
[
'kubectl --namespace=vv exec --tty=false --stdin=false deploy/drupal --container=drupal -- /docker-entrypoint ls',
['ls'],
[
'kubectl' => [
'tty' => false,
'interactive' => false,
'namespace' => 'vv',
'resource' => 'deploy/drupal',
'container' => 'drupal',
'entrypoint' => '/docker-entrypoint',
]
],
],
// With entrypoint as array.
[
'kubectl --namespace=vv exec --tty=false --stdin=false deploy/drupal --container=drupal -- /docker-entrypoint --debug ls',
['ls'],
[
'kubectl' => [
'tty' => false,
'interactive' => false,
'namespace' => 'vv',
'resource' => 'deploy/drupal',
'container' => 'drupal',
'entrypoint' => ['/docker-entrypoint', '--debug'],
]
],
],
];
}
/**
* @dataProvider wrapTestValues
*/
public function testWrap($expected, $args, $siteAliasData)
{
$siteAlias = new SiteAlias($siteAliasData, '@alias.dev');
$dockerTransport = new KubectlTransport($siteAlias);
$actual = $dockerTransport->wrap($args);
$this->assertEquals($expected, implode(' ', $actual));
}
}

View File

@ -0,0 +1,51 @@
<?php
namespace Consolidation\SiteProcess;
use Consolidation\SiteProcess\Transport\SkprTransport;
use PHPUnit\Framework\TestCase;
use Consolidation\SiteAlias\SiteAlias;
class SkprTransportTest extends TestCase
{
/**
* Data provider for testWrap.
*/
public function wrapTestValues()
{
return [
// Everything explicit.
[
'skpr exec dev -- ls',
['ls'],
[
'skpr' => [
'env' => 'dev',
]
],
],
// Ensure we aren't escaping arguments after "--"
[
'skpr exec dev -- monday "tuesday" \'wednesday\'',
['monday', '"tuesday"', "'wednesday'"],
[
'skpr' => [
'env' => 'dev'
]
],
],
];
}
/**
* @dataProvider wrapTestValues
*/
public function testWrap($expected, $args, $siteAliasData)
{
$siteAlias = new SiteAlias($siteAliasData, '@alias.dev');
$dockerTransport = new SkprTransport($siteAlias);
$actual = $dockerTransport->wrap($args);
$this->assertEquals($expected, implode(' ', $actual));
}
}

View File

@ -0,0 +1,36 @@
<?php
namespace Consolidation\SiteProcess;
use Consolidation\SiteProcess\Transport\VagrantTransport;
use PHPUnit\Framework\TestCase;
use Consolidation\SiteAlias\SiteAlias;
class VagrantTransportTest extends TestCase
{
/**
* Data provider for testWrap.
*/
public function wrapTestValues()
{
return [
[
'vagrant ssh -c ls',
[
'vagrant' => []
],
]
];
}
/**
* @dataProvider wrapTestValues
*/
public function testWrap($expected, $siteAliasData)
{
$siteAlias = new SiteAlias($siteAliasData, '@alias.dev');
$dockerTransport = new VagrantTransport($siteAlias);
$actual = $dockerTransport->wrap(['ls']);
$this->assertEquals($expected, implode(' ', $actual));
}
}

View File

@ -0,0 +1,9 @@
<?php
namespace Consolidation\SiteProcess;
interface CommandTesterInterface
{
const STATUS_OK = 0;
const STATUS_ERROR = 1;
}

View File

@ -0,0 +1,64 @@
<?php
namespace Consolidation\SiteProcess;
use Symfony\Component\Console\Output\BufferedOutput;
trait CommandTesterTrait
{
/** @var string */
protected $appName;
/** @var string */
protected $appVersion;
/**
* Instantiate a new runner
*/
public function setupCommandTester($appName, $appVersion)
{
// Define our invariants for our test
$this->appName = $appName;
$this->appVersion = $appVersion;
}
/**
* Prepare our $argv array; put the app name in $argv[0] followed by
* the command name and all command arguments and options.
*
* @param array $functionParameters should usually be func_get_args()
* @param int $leadingParameterCount the number of function parameters
* that are NOT part of argv. Default is 2 (expected content and
* expected status code).
*/
protected function argv($functionParameters, $leadingParameterCount = 2)
{
$argv = $functionParameters;
$argv = array_slice($argv, $leadingParameterCount);
array_unshift($argv, $this->appName);
return $argv;
}
/**
* Simulated front controller
*/
protected function execute($argv, $commandClasses, $configurationFile = false)
{
// Define a global output object to capture the test results
$output = new BufferedOutput();
// We can only call `Runner::execute()` once; then we need to tear down.
$runner = new \Robo\Runner($commandClasses);
if ($configurationFile) {
$runner->setConfigurationFilename($configurationFile);
}
$statusCode = $runner->execute($argv, $this->appName, $this->appVersion, $output);
// Destroy our container so that we can call $runner->execute() again for the next test.
\Robo\Robo::unsetContainer();
// Return the output and status code.
return [trim($output->fetch()), $statusCode];
}
}