welcome back to dyb-tech

This commit is contained in:
Daniel Guzman
2024-05-18 02:28:01 +02:00
parent 9513cdba09
commit 9f30bc98c7
6149 changed files with 668407 additions and 0 deletions
+146
View File
@@ -0,0 +1,146 @@
<?php
/*
* This file is part of the Symfony MakerBundle package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Bundle\MakerBundle\Test;
use Composer\Semver\Semver;
use PHPUnit\Framework\TestCase;
use Symfony\Bundle\MakerBundle\MakerInterface;
use Symfony\Bundle\MakerBundle\Str;
use Symfony\Component\HttpKernel\KernelInterface;
use Symfony\Component\Process\Process;
abstract class MakerTestCase extends TestCase
{
private ?KernelInterface $kernel = null;
/**
* @dataProvider getTestDetails
*
* @return void
*/
public function testExecute(MakerTestDetails $makerTestDetails)
{
$this->executeMakerCommand($makerTestDetails);
}
abstract public function getTestDetails();
abstract protected function getMakerClass(): string;
protected function createMakerTest(): MakerTestDetails
{
return new MakerTestDetails($this->getMakerInstance($this->getMakerClass()));
}
/**
* @return void
*/
protected function executeMakerCommand(MakerTestDetails $testDetails)
{
if (!class_exists(Process::class)) {
throw new \LogicException('The MakerTestCase cannot be run as the Process component is not installed. Try running "compose require --dev symfony/process".');
}
if (!$testDetails->isSupportedByCurrentPhpVersion()) {
$this->markTestSkipped();
}
$testEnv = MakerTestEnvironment::create($testDetails);
if ('7.0.x-dev' === $testEnv->getTargetSkeletonVersion() && $testDetails->getSkipOnSymfony7()) {
$this->markTestSkipped('This test is skipped on Symfony 7');
}
// prepare environment to test
$testEnv->prepareDirectory();
if (!$this->hasRequiredDependencyVersions($testDetails, $testEnv)) {
$this->markTestSkipped('Some dependencies versions are too low');
}
$makerRunner = new MakerTestRunner($testEnv);
foreach ($testDetails->getPreRunCallbacks() as $preRunCallback) {
$preRunCallback($makerRunner);
}
$callback = $testDetails->getRunCallback();
$callback($makerRunner);
// run tests
$files = $testEnv->getGeneratedFilesFromOutputText();
foreach ($files as $file) {
$this->assertTrue($testEnv->fileExists($file), sprintf('The file "%s" does not exist after generation', $file));
if (str_ends_with($file, '.twig')) {
$csProcess = $testEnv->runTwigCSLint($file);
$this->assertTrue($csProcess->isSuccessful(), sprintf('File "%s" has a twig-cs problem: %s', $file, $csProcess->getErrorOutput()."\n".$csProcess->getOutput()));
}
}
}
/**
* @return void
*/
protected function assertContainsCount(string $needle, string $haystack, int $count)
{
$this->assertEquals(1, substr_count($haystack, $needle), sprintf('Found more than %d occurrences of "%s" in "%s"', $count, $needle, $haystack));
}
private function getMakerInstance(string $makerClass): MakerInterface
{
if (null === $this->kernel) {
$this->kernel = $this->createKernel();
$this->kernel->boot();
}
// a cheap way to guess the service id
$serviceId ??= sprintf('maker.maker.%s', Str::asSnakeCase((new \ReflectionClass($makerClass))->getShortName()));
return $this->kernel->getContainer()->get($serviceId);
}
protected function createKernel(): KernelInterface
{
return new MakerTestKernel('dev', true);
}
private function hasRequiredDependencyVersions(MakerTestDetails $testDetails, MakerTestEnvironment $testEnv): bool
{
if (empty($testDetails->getRequiredPackageVersions())) {
return true;
}
$installedPackages = json_decode($testEnv->readFile('vendor/composer/installed.json'), true, 512, \JSON_THROW_ON_ERROR);
$packageVersions = [];
foreach ($installedPackages['packages'] ?? $installedPackages as $installedPackage) {
$packageVersions[$installedPackage['name']] = $installedPackage['version_normalized'];
}
foreach ($testDetails->getRequiredPackageVersions() as $requiredPackageData) {
$name = $requiredPackageData['name'];
$versionConstraint = $requiredPackageData['version_constraint'];
if (!isset($packageVersions[$name])) {
throw new \Exception(sprintf('Package "%s" is required in the test project at version "%s" but it is not installed?', $name, $versionConstraint));
}
if (!Semver::satisfies($packageVersions[$name], $versionConstraint)) {
return false;
}
}
return true;
}
}
@@ -0,0 +1,191 @@
<?php
/*
* This file is part of the Symfony MakerBundle package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Bundle\MakerBundle\Test;
use Symfony\Bundle\MakerBundle\DependencyBuilder;
use Symfony\Bundle\MakerBundle\MakerInterface;
final class MakerTestDetails
{
private ?\Closure $runCallback = null;
private array $preRunCallbacks = [];
private array $extraDependencies = [];
private string $rootNamespace = 'App';
private int $requiredPhpVersion = 80000;
private array $requiredPackageVersions = [];
private int $blockedPhpVersionUpper = 0;
private int $blockedPhpVersionLower = 0;
private bool $skipOnSymfony7 = false;
public function __construct(
private MakerInterface $maker,
) {
}
public function run(\Closure $callback): self
{
$this->runCallback = $callback;
return $this;
}
public function preRun(\Closure $callback): self
{
$this->preRunCallbacks[] = $callback;
return $this;
}
/**
* @return string
*/
public function getRootNamespace()
{
return $this->rootNamespace;
}
public function changeRootNamespace(string $rootNamespace): self
{
$this->rootNamespace = trim($rootNamespace, '\\');
return $this;
}
public function addExtraDependencies(string ...$packages): self
{
$this->extraDependencies += $packages;
return $this;
}
public function setRequiredPhpVersion(int $version): self
{
@trigger_deprecation('symfony/maker-bundle', 'v1.44.0', 'setRequiredPhpVersion() is no longer used and will be removed in a future version.');
$this->requiredPhpVersion = $version;
return $this;
}
/**
* Skip a test from running between a range of PHP Versions.
*
* @param int $lowerLimit Versions below this value will be allowed
* @param int $upperLimit Versions above this value will be allowed
*
* @internal
*/
public function setSkippedPhpVersions(int $lowerLimit, int $upperLimit): self
{
$this->blockedPhpVersionUpper = $upperLimit;
$this->blockedPhpVersionLower = $lowerLimit;
return $this;
}
public function addRequiredPackageVersion(string $packageName, string $versionConstraint): self
{
$this->requiredPackageVersions[] = ['name' => $packageName, 'version_constraint' => $versionConstraint];
return $this;
}
public function getUniqueCacheDirectoryName(): string
{
// for cache purposes, only the dependencies are important!
// You can change it ONLY if you don't have another way to implement it
return 'maker_'.strtolower($this->getRootNamespace()).'_'.md5(serialize($this->getDependencies()));
}
public function getMaker(): MakerInterface
{
return $this->maker;
}
public function getDependencies(): array
{
$depBuilder = $this->getDependencyBuilder();
return array_merge(
$depBuilder->getAllRequiredDependencies(),
$depBuilder->getAllRequiredDevDependencies(),
$this->extraDependencies
);
}
public function getExtraDependencies(): array
{
return $this->extraDependencies;
}
public function getDependencyBuilder(): DependencyBuilder
{
$depBuilder = new DependencyBuilder();
$this->maker->configureDependencies($depBuilder);
return $depBuilder;
}
public function isSupportedByCurrentPhpVersion(): bool
{
$hasPhpVersionConstraint = $this->blockedPhpVersionLower > 0 && $this->blockedPhpVersionUpper > 0;
$isSupported = false;
if (!$hasPhpVersionConstraint) {
$isSupported = true;
}
if (\PHP_VERSION_ID > $this->blockedPhpVersionUpper) {
$isSupported = true;
}
if (\PHP_VERSION_ID < $this->blockedPhpVersionLower) {
$isSupported = true;
}
return $isSupported && \PHP_VERSION_ID >= $this->requiredPhpVersion;
}
public function getRequiredPackageVersions(): array
{
return $this->requiredPackageVersions;
}
public function getRunCallback(): \Closure
{
if (!$this->runCallback) {
throw new \Exception('Don\'t forget to call ->run()');
}
return $this->runCallback;
}
/**
* @return \Closure[]
*/
public function getPreRunCallbacks(): array
{
return $this->preRunCallbacks;
}
public function skipOnSymfony7(): self
{
$this->skipOnSymfony7 = true;
return $this;
}
public function getSkipOnSymfony7(): bool
{
return $this->skipOnSymfony7;
}
}
@@ -0,0 +1,470 @@
<?php
/*
* This file is part of the Symfony MakerBundle package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Bundle\MakerBundle\Test;
use Symfony\Component\Filesystem\Filesystem;
use Symfony\Component\Process\InputStream;
/**
* @author Sadicov Vladimir <sadikoff@gmail.com>
* @author Nicolas Philippe <nikophil@gmail.com>
*
* @internal
*/
final class MakerTestEnvironment
{
private Filesystem $fs;
private bool|string $rootPath;
private string $cachePath;
private string $flexPath;
private string $path;
private MakerTestProcess $runnedMakerProcess;
private bool $isWindows;
private function __construct(
private MakerTestDetails $testDetails,
) {
$this->isWindows = str_contains(strtolower(\PHP_OS), 'win');
$this->fs = new Filesystem();
$this->rootPath = realpath(__DIR__.'/../../');
$cachePath = $this->rootPath.'/tests/tmp/cache';
if (!$this->fs->exists($cachePath)) {
$this->fs->mkdir($cachePath);
}
$this->cachePath = realpath($cachePath);
$targetVersion = $this->getTargetSkeletonVersion();
$this->flexPath = $this->cachePath.'/flex_project'.$targetVersion;
$directoryName = $targetVersion ?: 'current';
if (str_ends_with($directoryName, '.*')) {
$directoryName = substr($directoryName, 0, -2);
}
$this->path = $this->cachePath.\DIRECTORY_SEPARATOR.$testDetails->getUniqueCacheDirectoryName().'_'.$directoryName;
}
public static function create(MakerTestDetails $testDetails): self
{
return new self($testDetails);
}
public function getPath(): string
{
return $this->path;
}
public function readFile(string $path): string
{
if (!file_exists($this->path.'/'.$path)) {
throw new \InvalidArgumentException(sprintf('Cannot find file "%s"', $path));
}
return file_get_contents($this->path.'/'.$path);
}
private function changeRootNamespaceIfNeeded(): void
{
if ('App' === ($rootNamespace = $this->testDetails->getRootNamespace())) {
return;
}
$replacements = [
[
'filename' => 'composer.json',
'find' => '"App\\\\": "src/"',
'replace' => '"'.$rootNamespace.'\\\\": "src/"',
],
[
'filename' => 'src/Kernel.php',
'find' => 'namespace App',
'replace' => 'namespace '.$rootNamespace,
],
[
'filename' => 'bin/console',
'find' => 'use App\\Kernel',
'replace' => 'use '.$rootNamespace.'\\Kernel',
],
[
'filename' => 'public/index.php',
'find' => 'use App\\Kernel',
'replace' => 'use '.$rootNamespace.'\\Kernel',
],
[
'filename' => 'config/services.yaml',
'find' => 'App\\',
'replace' => $rootNamespace.'\\',
],
[
'filename' => '.env.test',
'find' => 'KERNEL_CLASS=\'App\Kernel\'',
'replace' => 'KERNEL_CLASS=\''.$rootNamespace.'\Kernel\'',
],
];
if ($this->fs->exists($this->path.'/config/packages/doctrine.yaml')) {
$replacements[] = [
'filename' => 'config/packages/doctrine.yaml',
'find' => 'App',
'replace' => $rootNamespace,
];
}
$this->processReplacements($replacements, $this->path);
$this->runCommand('composer dump-autoload');
}
public function prepareDirectory(): void
{
// Copy MakerBundle to a "repo" directory for tests
if (!file_exists($makerRepoPath = sprintf('%s/maker-repo', $this->cachePath))) {
MakerTestProcess::create(sprintf('git clone %s %s', $this->rootPath, $makerRepoPath), $this->cachePath)->run();
}
if (!$this->fs->exists($this->flexPath)) {
$this->buildFlexSkeleton();
}
if (!$this->fs->exists($this->path)) {
try {
// let's do some magic here git is faster than copy
MakerTestProcess::create(
'\\' === \DIRECTORY_SEPARATOR ? 'git clone %FLEX_PATH% %APP_PATH%' : 'git clone "$FLEX_PATH" "$APP_PATH"',
\dirname($this->flexPath),
[
'FLEX_PATH' => $this->flexPath,
'APP_PATH' => $this->path,
]
)
->run();
// In Window's we have to require MakerBundle in each project - git clone doesn't symlink well
if ($this->isWindows) {
$this->composerRequireMakerBundle($this->path);
}
// install any missing dependencies
$dependencies = $this->determineMissingDependencies();
if ($dependencies) {
// -v actually silences the "very" verbose output in case of an error
$composerProcess = MakerTestProcess::create(sprintf('composer require %s -v', implode(' ', $dependencies)), $this->path);
// @legacy Temporary code until doctrine/dbal 3.7 is out (which supports symfony/console 7.0
$composerProcess->run(true);
if (!$composerProcess->isSuccessful()) {
if (str_contains($composerProcess->getErrorOutput(), 'Declaration of Doctrine\DBAL\Tools\Console\Command\RunSqlCommand::execute')) {
$this->patchDoctrineDbalForSymfony7();
} else {
throw new \Exception(sprintf('Error running command: composer require %s -v. Output: "%s". Error: "%s"', implode(' ', $dependencies), $composerProcess->getOutput(), $composerProcess->getErrorOutput()));
}
}
}
$this->changeRootNamespaceIfNeeded();
file_put_contents($this->path.'/.gitignore', "var/cache/\nvendor/\n");
MakerTestProcess::create('git diff --quiet || ( git config user.name "symfony" && git config user.email "test@symfony.com" && git add . && git commit -a -m "second commit" )',
$this->path
)->run();
} catch (\Exception $e) {
$this->fs->remove($this->path);
throw $e;
}
} else {
MakerTestProcess::create('git reset --hard && git clean -fd', $this->path)->run();
$this->fs->remove($this->path.'/var/cache');
}
}
public function runCommand(string $command): MakerTestProcess
{
return MakerTestProcess::create($command, $this->path)->run();
}
public function runMaker(array $inputs, string $argumentsString = '', bool $allowedToFail = false, array $envVars = []): MakerTestProcess
{
// Let's remove cache
$this->fs->remove($this->path.'/var/cache');
$testProcess = $this->createInteractiveCommandProcess(
commandName: $this->testDetails->getMaker()::getCommandName(),
userInputs: $inputs,
argumentsString: $argumentsString,
envVars: $envVars,
);
$this->runnedMakerProcess = $testProcess->run($allowedToFail);
return $this->runnedMakerProcess;
}
public function getGeneratedFilesFromOutputText(): array
{
$output = $this->runnedMakerProcess->getOutput();
$matches = [];
preg_match_all('#(created|updated): (]8;;[^]*\\\)?(.*?)(]8;;\\\)?\n#iu', $output, $matches, \PREG_PATTERN_ORDER);
return array_map('trim', $matches[3]);
}
public function fileExists(string $file): bool
{
return $this->fs->exists($this->path.'/'.$file);
}
public function runTwigCSLint(string $file): MakerTestProcess
{
if (!file_exists(__DIR__.'/../../tools/twigcs/vendor/bin/twigcs')) {
throw new \Exception('twigcs not found: run: "composer install --working-dir=tools/twigcs".');
}
return MakerTestProcess::create(sprintf('php tools/twigcs/vendor/bin/twigcs --config ./tools/twigcs/.twig_cs.dist %s', $this->path.'/'.$file), $this->rootPath)
->run(true);
}
private function buildFlexSkeleton(): void
{
$targetVersion = $this->getTargetSkeletonVersion();
$versionString = $targetVersion ? sprintf(':%s', $targetVersion) : '';
$flexProjectDir = sprintf('flex_project%s', $targetVersion);
MakerTestProcess::create(
sprintf('composer create-project symfony/skeleton%s %s --prefer-dist --no-progress', $versionString, $flexProjectDir),
$this->cachePath
)->run();
$rootPath = str_replace('\\', '\\\\', realpath(__DIR__.'/../..'));
$this->addMakerBundleRepoToComposer(sprintf('%s/%s/composer.json', $this->cachePath, $flexProjectDir));
// In Linux, git plays well with symlinks - we can add maker to the flex skeleton.
if (!$this->isWindows) {
$this->composerRequireMakerBundle(sprintf('%s/%s', $this->cachePath, $flexProjectDir));
}
if ($_SERVER['MAKER_ALLOW_DEV_DEPS_IN_APP'] ?? false) {
MakerTestProcess::create('composer config minimum-stability dev', $this->flexPath)->run();
MakerTestProcess::create('composer config prefer-stable true', $this->flexPath)->run();
}
// fetch a few packages needed for testing
MakerTestProcess::create('composer require phpunit browser-kit symfony/css-selector --prefer-dist --no-progress --no-suggest', $this->flexPath)
->run();
if ('\\' !== \DIRECTORY_SEPARATOR) {
$this->fs->remove($this->flexPath.'/vendor/symfony/phpunit-bridge');
$this->fs->symlink($rootPath.'/vendor/symfony/phpunit-bridge', $this->flexPath.'/vendor/symfony/phpunit-bridge');
}
$replacements = [
// temporarily ignoring indirect deprecations - see #237
[
'filename' => '.env.test',
'find' => 'SYMFONY_DEPRECATIONS_HELPER=999999',
'replace' => 'SYMFONY_DEPRECATIONS_HELPER=max[self]=0',
],
// do not explicitly set the PHPUnit version
[
'filename' => 'phpunit.xml.dist',
'find' => '<server name="SYMFONY_PHPUNIT_VERSION" value="9.6" />',
'replace' => '',
],
];
$this->processReplacements($replacements, $this->flexPath);
// end of temp code
file_put_contents($this->flexPath.'/.gitignore', "var/cache/\n");
// Force adding vendor/ dir to Git repo in case users exclude it in global .gitignore
MakerTestProcess::create('git init && git config user.name "symfony" && git config user.email "test@symfony.com" && git add . && git add vendor/ -f && git commit -a -m "first commit"',
$this->flexPath
)->run();
}
private function processReplacements(array $replacements, string $rootDir): void
{
foreach ($replacements as $replacement) {
$this->processReplacement($rootDir, $replacement['filename'], $replacement['find'], $replacement['replace']);
}
}
public function processReplacement(string $rootDir, string $filename, string $find, string $replace, bool $allowNotFound = false): void
{
$path = realpath($rootDir.'/'.$filename);
if (!$this->fs->exists($path)) {
if ($allowNotFound) {
return;
}
throw new \Exception(sprintf('Could not find file "%s" to process replacements inside "%s"', $filename, $rootDir));
}
$contents = file_get_contents($path);
if (!str_contains($contents, $find)) {
if ($allowNotFound) {
return;
}
throw new \Exception(sprintf('Could not find "%s" inside "%s"', $find, $filename));
}
file_put_contents($path, str_replace($find, $replace, $contents));
}
public function createInteractiveCommandProcess(string $commandName, array $userInputs, string $argumentsString = '', array $envVars = []): MakerTestProcess
{
$envVars = array_merge(['SHELL_INTERACTIVE' => '1'], $envVars);
// We don't need ansi coloring in tests!
$process = MakerTestProcess::create(
commandLine: sprintf('php bin/console %s %s --no-ansi', $commandName, $argumentsString),
cwd: $this->path,
envVars: $envVars,
timeout: 10
);
if ($userInputs) {
$inputStream = new InputStream();
// start the command with some input
$inputStream->write(current($userInputs)."\n");
$inputStream->onEmpty(function () use ($inputStream, &$userInputs) {
$nextInput = next($userInputs);
if (false === $nextInput) {
$inputStream->close();
} else {
$inputStream->write($nextInput."\n");
}
});
$process->setInput($inputStream);
}
return $process;
}
public function getSymfonyVersionInApp(): int
{
$contents = file_get_contents($this->getPath().'/vendor/symfony/http-kernel/Kernel.php');
$position = strpos($contents, 'VERSION_ID = ');
return (int) substr($contents, $position + 13, 5);
}
public function doesClassExistInApp(string $class): bool
{
$classMap = require $this->getPath().'/vendor/composer/autoload_classmap.php';
return isset($classMap[$class]);
}
/**
* Executes the DependencyBuilder for the Maker command inside the
* actual project, so we know exactly what dependencies we need or
* don't need.
*/
private function determineMissingDependencies(): array
{
$depBuilder = $this->testDetails->getDependencyBuilder();
file_put_contents($this->path.'/dep_builder', serialize($depBuilder));
file_put_contents($this->path.'/dep_runner.php', '<?php
require __DIR__."/vendor/autoload.php";
$depBuilder = unserialize(file_get_contents("dep_builder"));
$missingDependencies = array_merge(
$depBuilder->getMissingDependencies(),
$depBuilder->getMissingDevDependencies()
);
echo json_encode($missingDependencies);
');
$process = MakerTestProcess::create('php dep_runner.php', $this->path)->run();
$data = json_decode($process->getOutput(), true, 512, \JSON_THROW_ON_ERROR);
unlink($this->path.'/dep_builder');
unlink($this->path.'/dep_runner.php');
return array_merge($data, $this->testDetails->getExtraDependencies());
}
public function getTargetSkeletonVersion(): ?string
{
return $_SERVER['SYMFONY_VERSION'] ?? '';
}
private function composerRequireMakerBundle(string $projectDirectory): void
{
MakerTestProcess::create('composer require --dev symfony/maker-bundle', $projectDirectory)
->run()
;
$makerRepoSrcPath = sprintf('%s/maker-repo/src', $this->cachePath);
// DX - So we can test local changes without having to commit them.
if (!is_link($makerRepoSrcPath)) {
$this->fs->remove($makerRepoSrcPath);
$this->fs->symlink(sprintf('%s/src', $this->rootPath), $makerRepoSrcPath);
}
}
/**
* Adds Symfony/MakerBundle as a "path" repository to composer.json.
*/
private function addMakerBundleRepoToComposer(string $composerJsonPath): void
{
$composerJson = json_decode(
file_get_contents($composerJsonPath), true, 512, \JSON_THROW_ON_ERROR);
// Require-dev is empty and composer complains about this being an array when we encode it again.
unset($composerJson['require-dev']);
$composerJson['repositories']['symfony/maker-bundle'] = [
'type' => 'path',
'url' => sprintf('%s%smaker-repo', $this->cachePath, \DIRECTORY_SEPARATOR),
'options' => [
'versions' => [
'symfony/maker-bundle' => '9999.99', // Arbitrary version to avoid stability conflicts
],
],
];
file_put_contents($composerJsonPath, json_encode($composerJson, \JSON_THROW_ON_ERROR | \JSON_PRETTY_PRINT | \JSON_UNESCAPED_SLASHES));
}
private function patchDoctrineDbalForSymfony7(): void
{
$commandPath = $this->path.'/vendor/doctrine/dbal/src/Tools/Console/Command/RunSqlCommand.php';
$contents = file_get_contents($commandPath);
$needle = 'protected function execute(InputInterface $input, OutputInterface $output)';
if (str_contains($contents, $needle.': int')) {
return;
}
$contents = str_replace(
$needle,
$needle.': int',
$contents
);
file_put_contents($commandPath, $contents);
}
}
@@ -0,0 +1,89 @@
<?php
/*
* This file is part of the Symfony MakerBundle package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Bundle\MakerBundle\Test;
use Symfony\Bundle\FrameworkBundle\FrameworkBundle;
use Symfony\Bundle\FrameworkBundle\Kernel\MicroKernelTrait;
use Symfony\Bundle\MakerBundle\DependencyInjection\CompilerPass\MakeCommandRegistrationPass;
use Symfony\Bundle\MakerBundle\MakerBundle;
use Symfony\Component\Config\Loader\LoaderInterface;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\HttpKernel\Kernel;
use Symfony\Component\Routing\Loader\Configurator\RoutingConfigurator;
class MakerTestKernel extends Kernel implements CompilerPassInterface
{
use MicroKernelTrait;
private string $testRootDir;
public function __construct(string $environment, bool $debug)
{
$this->testRootDir = sys_get_temp_dir().'/'.uniqid('sf_maker_', true);
parent::__construct($environment, $debug);
}
public function registerBundles(): iterable
{
return [
new FrameworkBundle(),
new MakerBundle(),
];
}
protected function configureRoutes(RoutingConfigurator $routes)
{
}
protected function configureRouting(RoutingConfigurator $routes)
{
}
protected function configureContainer(ContainerBuilder $c, LoaderInterface $loader)
{
$c->loadFromExtension('framework', [
'secret' => 123,
'router' => [
'utf8' => true,
],
'http_method_override' => false,
'handle_all_throwables' => true,
'php_errors' => [
'log' => true,
],
]);
}
public function getProjectDir(): string
{
return $this->getRootDir();
}
public function getRootDir(): string
{
return $this->testRootDir;
}
/**
* @return void
*/
public function process(ContainerBuilder $container)
{
// makes all makers public to help the tests
foreach ($container->findTaggedServiceIds(MakeCommandRegistrationPass::MAKER_TAG) as $id => $tags) {
$defn = $container->getDefinition($id);
$defn->setPublic(true);
}
}
}
@@ -0,0 +1,71 @@
<?php
/*
* This file is part of the Symfony MakerBundle package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Bundle\MakerBundle\Test;
use Symfony\Component\Process\Process;
/**
* @author Sadicov Vladimir <sadikoff@gmail.com>
*
* @internal
*/
final class MakerTestProcess
{
private Process $process;
private function __construct($commandLine, $cwd, array $envVars, $timeout)
{
$this->process = \is_string($commandLine)
? Process::fromShellCommandline($commandLine, $cwd, null, null, $timeout)
: new Process($commandLine, $cwd, null, null, $timeout);
$this->process->setEnv($envVars);
}
public static function create($commandLine, $cwd, array $envVars = [], $timeout = null): self
{
return new self($commandLine, $cwd, $envVars, $timeout);
}
public function setInput($input): self
{
$this->process->setInput($input);
return $this;
}
public function run($allowToFail = false, array $envVars = []): self
{
$this->process->run(null, $envVars);
if (!$allowToFail && !$this->process->isSuccessful()) {
throw new \Exception(sprintf('Error running command: "%s". Output: "%s". Error: "%s"', $this->process->getCommandLine(), $this->process->getOutput(), $this->process->getErrorOutput()));
}
return $this;
}
public function isSuccessful(): bool
{
return $this->process->isSuccessful();
}
public function getOutput(): string
{
return $this->process->getOutput();
}
public function getErrorOutput(): string
{
return $this->process->getErrorOutput();
}
}
+261
View File
@@ -0,0 +1,261 @@
<?php
/*
* This file is part of the Symfony MakerBundle package.
*
* (c) Fabien Potencier <fabien@symfony.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Symfony\Bundle\MakerBundle\Test;
use PHPUnit\Framework\ExpectationFailedException;
use Symfony\Bundle\MakerBundle\Util\ClassSourceManipulator;
use Symfony\Bundle\MakerBundle\Util\YamlSourceManipulator;
use Symfony\Component\Filesystem\Filesystem;
use Symfony\Component\Finder\Finder;
use Symfony\Component\Yaml\Yaml;
use Twig\Environment;
use Twig\Loader\FilesystemLoader;
class MakerTestRunner
{
private Filesystem $filesystem;
private ?MakerTestProcess $executedMakerProcess = null;
public function __construct(
private MakerTestEnvironment $environment,
) {
$this->filesystem = new Filesystem();
}
public function runMaker(array $inputs, string $argumentsString = '', bool $allowedToFail = false, array $envVars = []): string
{
$this->executedMakerProcess = $this->environment->runMaker($inputs, $argumentsString, $allowedToFail, $envVars);
return $this->executedMakerProcess->getOutput();
}
/**
* @return void
*/
public function copy(string $source, string $destination)
{
$path = __DIR__.'/../../tests/fixtures/'.$source;
if (!file_exists($path)) {
throw new \Exception(sprintf('Cannot find file "%s"', $path));
}
if (is_file($path)) {
$this->filesystem->copy($path, $this->getPath($destination), true);
return;
}
// handle a directory copy
$finder = new Finder();
$finder->in($path)->files();
foreach ($finder as $file) {
$this->filesystem->copy($file->getPathname(), $this->getPath($file->getRelativePathname()), true);
}
}
public function renderTemplateFile(string $source, string $destination, array $variables): void
{
$twig = new Environment(
new FilesystemLoader(__DIR__.'/../../tests/fixtures')
);
$rendered = $twig->render($source, $variables);
$this->filesystem->mkdir(\dirname($this->getPath($destination)));
file_put_contents($this->getPath($destination), $rendered);
}
public function getPath(string $filename): string
{
return $this->environment->getPath().'/'.$filename;
}
public function readYaml(string $filename): array
{
return Yaml::parse(file_get_contents($this->getPath($filename)));
}
public function getExecutedMakerProcess(): MakerTestProcess
{
if (!$this->executedMakerProcess) {
throw new \Exception('Maker process has not been executed yet.');
}
return $this->executedMakerProcess;
}
/**
* @return void
*/
public function modifyYamlFile(string $filename, \Closure $callback)
{
$path = $this->getPath($filename);
$manipulator = new YamlSourceManipulator(file_get_contents($path));
$newData = $callback($manipulator->getData());
if (!\is_array($newData)) {
throw new \Exception('The modifyYamlFile() callback must return the final array of data');
}
$manipulator->setData($newData);
file_put_contents($path, $manipulator->getContents());
}
/**
* @return void
*/
public function runConsole(string $command, array $inputs, string $arguments = '')
{
$process = $this->environment->createInteractiveCommandProcess(
$command,
$inputs,
$arguments
);
$process->run();
}
public function runProcess(string $command): void
{
MakerTestProcess::create($command, $this->environment->getPath())->run();
}
public function replaceInFile(string $filename, string $find, string $replace, bool $allowNotFound = false): void
{
$this->environment->processReplacement(
$this->environment->getPath(),
$filename,
$find,
$replace,
$allowNotFound
);
}
public function removeFromFile(string $filename, string $find, bool $allowNotFound = false): void
{
$this->environment->processReplacement(
$this->environment->getPath(),
$filename,
$find,
'',
$allowNotFound
);
}
public function configureDatabase(bool $createSchema = true): void
{
$this->replaceInFile(
'.env',
'postgresql://app:!ChangeMe!@127.0.0.1:5432/app?serverVersion=16&charset=utf8',
getenv('TEST_DATABASE_DSN')
);
// Flex includes a recipe to suffix the dbname w/ "_test" - lets keep
// things simple for these tests and not do that.
$this->modifyYamlFile('config/packages/doctrine.yaml', function (array $config) {
if (isset($config['when@test']['doctrine']['dbal']['dbname_suffix'])) {
unset($config['when@test']['doctrine']['dbal']['dbname_suffix']);
}
return $config;
});
// this looks silly, but it's the only way to drop the database *for sure*,
// as doctrine:database:drop will error if there is no database
// also, skip for SQLITE, as it does not support --if-not-exists
if (!str_starts_with(getenv('TEST_DATABASE_DSN'), 'sqlite://')) {
$this->runConsole('doctrine:database:create', [], '--env=test --if-not-exists');
}
$this->runConsole('doctrine:database:drop', [], '--env=test --force');
$this->runConsole('doctrine:database:create', [], '--env=test');
if ($createSchema) {
$this->runConsole('doctrine:schema:create', [], '--env=test');
}
}
public function updateSchema(): void
{
$this->runConsole('doctrine:schema:update', [], '--env=test --force');
}
public function runTests(): void
{
$internalTestProcess = MakerTestProcess::create(
sprintf('php %s', $this->getPath('/bin/phpunit')),
$this->environment->getPath())
->run(true)
;
if ($internalTestProcess->isSuccessful()) {
return;
}
throw new ExpectationFailedException(sprintf("Error while running the PHPUnit tests *in* the project: \n\n %s \n\n Command Output: %s", $internalTestProcess->getErrorOutput()."\n".$internalTestProcess->getOutput(), $this->getExecutedMakerProcess()->getErrorOutput()."\n".$this->getExecutedMakerProcess()->getOutput()));
}
public function writeFile(string $filename, string $contents): void
{
$this->filesystem->mkdir(\dirname($this->getPath($filename)));
file_put_contents($this->getPath($filename), $contents);
}
/**
* @return void
*/
public function addToAutoloader(string $namespace, string $path)
{
$composerJson = json_decode(
json: file_get_contents($this->getPath('composer.json')),
associative: true,
flags: \JSON_THROW_ON_ERROR | \JSON_UNESCAPED_SLASHES
);
$composerJson['autoload-dev']['psr-4'][$namespace] = $path;
$this->filesystem->dumpFile(
$this->getPath('composer.json'),
json_encode($composerJson, \JSON_UNESCAPED_SLASHES | \JSON_PRETTY_PRINT | \JSON_THROW_ON_ERROR)
);
$this->environment->runCommand('composer dump-autoload');
}
public function deleteFile(string $filename): void
{
$this->filesystem->remove($this->getPath($filename));
}
public function manipulateClass(string $filename, \Closure $callback): void
{
$contents = file_get_contents($this->getPath($filename));
$manipulator = new ClassSourceManipulator(
sourceCode: $contents,
overwrite: true,
);
$callback($manipulator);
file_put_contents($this->getPath($filename), $manipulator->getSourceCode());
}
public function getSymfonyVersion(): int
{
return $this->environment->getSymfonyVersionInApp();
}
public function doesClassExist(string $class): bool
{
return $this->environment->doesClassExistInApp($class);
}
}