diff --git a/.travis.yml b/.travis.yml index 7a73aa6..28235e2 100644 --- a/.travis.yml +++ b/.travis.yml @@ -11,9 +11,10 @@ before_script: - curl -L https://codeclimate.com/downloads/test-reporter/test-reporter-latest-linux-amd64 > ./cc-test-reporter - chmod +x ./cc-test-reporter - ./cc-test-reporter before-build + - cat /dev/zero | ssh-keygen -q -N "" script: - vendor/bin/phpunit --disallow-test-output --strict-coverage -d error_reporting=-1 --coverage-clover=build/logs/clover.xml Tests after_script: - - ./cc-test-reporter after-build --exit-code $TRAVIS_TEST_RESULT \ No newline at end of file + - ./cc-test-reporter after-build --exit-code $TRAVIS_TEST_RESULT diff --git a/CLI/CliFactoryInterface.php b/CLI/CliFactoryInterface.php new file mode 100644 index 0000000..54013ec --- /dev/null +++ b/CLI/CliFactoryInterface.php @@ -0,0 +1,17 @@ +getEnvironment(); + /** @var AbstractApplication $application */ + $application = $appEnv->getApplication(); + + /** @var VirtualServer[] $servers */ + $servers = $environment->getVirtualServers(); + + foreach ($servers as $server) { + if (!$server->isTaskServer()) { + continue; + } + + $user = $application->getNameCanonical(); + $keyLocation = rtrim(Path::getHomeDirectory(), '/') . '/.ssh/id_rsa'; + + $host = $server->getHost(); + $port = $server->getPort() ?: 22; + $ssh = new SSH2($host, $port); + $key = new RSA(); + $key->loadKey(file_get_contents($keyLocation)); + + if (!$ssh->login($user, $key)) { + throw new \Exception(sprintf('SSH login for %s@%s:%s failed.', $user, $host, $port)); + } + return new RemoteCli($ssh); + } + return null; + } +} diff --git a/CLI/RemoteCli.php b/CLI/RemoteCli.php new file mode 100644 index 0000000..3fa6144 --- /dev/null +++ b/CLI/RemoteCli.php @@ -0,0 +1,78 @@ +connection = $connection; + $this->cwd = $cwd; + } + + /** + * Executes a command. + * + * @param CommandBuilder $command + * The command to execute. + * + * @return bool + * True on success, false on failure. + */ + public function execute(CommandBuilder $command) + { + if ($this->cwd) { + $command = CommandBuilder::create('cd') + ->addFlag('P') + ->addArgument($this->cwd) + ->onSuccess($command); + } + $this->lastOutput = 'Executing ' . $command . "\n"; + $result = $this->connection->exec($command->getCommand()); + $this->lastOutput .= $result ? $result : ''; + + return $this->connection->getExitStatus() === 0; + } + + /** + * Get the output of the last execution. + * + * @return string + */ + public function getLastOutput() + { + return $this->lastOutput; + } + + public function getConnection() { + return $this->connection; + } +} diff --git a/CacheClearer/CacheClearerInterface.php b/CacheClearer/CacheClearerInterface.php new file mode 100644 index 0000000..6a5b125 --- /dev/null +++ b/CacheClearer/CacheClearerInterface.php @@ -0,0 +1,19 @@ +getContainer() + ->get(TaskRunnerService::class) + ->runNext($type); + } +} diff --git a/Command/BuildCommand.php b/Command/BuildCommand.php index 25e5b4b..22daa4f 100644 --- a/Command/BuildCommand.php +++ b/Command/BuildCommand.php @@ -1,42 +1,37 @@ setName('domainator:build'); } /** + * Run the command. + * * @param InputInterface $input + * The input. * @param OutputInterface $output + * The output. */ public function execute(InputInterface $input, OutputInterface $output) { - $entityManager = $this->getContainer()->get('doctrine.orm.default_entity_manager'); - $eventDispatcher = $this->getContainer()->get('event_dispatcher'); - $task = $entityManager->getRepository(Task::class)->getNextTask(Task::TYPE_BUILD); - - if (!$task) { - return; - } - - $event = new BuildEvent($task); - $eventDispatcher->dispatch(BuildEvent::NAME, $event); + $this->runNextTask(Task::TYPE_BUILD); } } diff --git a/Command/DestroyCommand.php b/Command/DestroyCommand.php index 9721cfd..5bde9ac 100644 --- a/Command/DestroyCommand.php +++ b/Command/DestroyCommand.php @@ -1,42 +1,37 @@ setName('domainator:destroy'); } /** + * Run the command. + * * @param InputInterface $input + * The input. * @param OutputInterface $output + * The output. */ public function execute(InputInterface $input, OutputInterface $output) { - $entityManager = $this->getContainer()->get('doctrine.orm.default_entity_manager'); - $eventDispatcher = $this->getContainer()->get('event_dispatcher'); - $task = $entityManager->getRepository(Task::class)->getNextTask(Task::TYPE_DESTROY); - - if (!$task) { - return; - } - - $event = new DestroyEvent($task); - $eventDispatcher->dispatch(DestroyEvent::NAME, $event); + $this->runNextTask(Task::TYPE_DESTROY); } } diff --git a/DependencyInjection/Compiler/CacheClearProviderCompilerPass.php b/DependencyInjection/Compiler/CacheClearProviderCompilerPass.php new file mode 100644 index 0000000..1ade2ea --- /dev/null +++ b/DependencyInjection/Compiler/CacheClearProviderCompilerPass.php @@ -0,0 +1,31 @@ +has(CacheClearProvider::class)) { + return; + } + $definition = $container->getDefinition(CacheClearProvider::class); + + $taggedServices = $container->findTaggedServiceIds('domainator.cacheclearer'); + + foreach ($taggedServices as $id => $tags) { + foreach ($tags as $attributes) { + $definition->addMethodCall('registerCacheClearer', array(new Reference($id), $attributes['for'])); + } + } + } +} diff --git a/DependencyInjection/Compiler/CliFactoryProviderCompilerPass.php b/DependencyInjection/Compiler/CliFactoryProviderCompilerPass.php new file mode 100644 index 0000000..f95be46 --- /dev/null +++ b/DependencyInjection/Compiler/CliFactoryProviderCompilerPass.php @@ -0,0 +1,31 @@ +has(CliFactoryProvider::class)) { + return; + } + $definition = $container->getDefinition(CliFactoryProvider::class); + + $taggedServices = $container->findTaggedServiceIds('domainator.clifactory'); + + foreach ($taggedServices as $id => $tags) { + foreach ($tags as $attributes) { + $definition->addMethodCall('registerCliFactory', array(new Reference($id), $attributes['for'])); + } + } + } +} diff --git a/DigipolisGentDomainator9kCoreBundle.php b/DigipolisGentDomainator9kCoreBundle.php index dbe24a6..386323c 100644 --- a/DigipolisGentDomainator9kCoreBundle.php +++ b/DigipolisGentDomainator9kCoreBundle.php @@ -2,12 +2,17 @@ namespace DigipolisGent\Domainator9k\CoreBundle; -use DigipolisGent\Domainator9k\CoreBundle\DependencyInjection\Compiler\ApplicationTypePass; -use DigipolisGent\Domainator9k\CoreBundle\DependencyInjection\Compiler\CiTypePass; -use DigipolisGent\Domainator9k\CoreBundle\DependencyInjection\Compiler\TaskPass; +use DigipolisGent\Domainator9k\CoreBundle\DependencyInjection\Compiler\CacheClearProviderCompilerPass; +use DigipolisGent\Domainator9k\CoreBundle\DependencyInjection\Compiler\CliFactoryProviderCompilerPass; use Symfony\Component\DependencyInjection\ContainerBuilder; use Symfony\Component\HttpKernel\Bundle\Bundle; class DigipolisGentDomainator9kCoreBundle extends Bundle { + public function build(ContainerBuilder $container) + { + parent::build($container); + $container->addCompilerPass(new CacheClearProviderCompilerPass()); + $container->addCompilerPass(new CliFactoryProviderCompilerPass()); + } } diff --git a/Entity/AbstractApplication.php b/Entity/AbstractApplication.php index 3f48f8e..0f6b6e2 100644 --- a/Entity/AbstractApplication.php +++ b/Entity/AbstractApplication.php @@ -4,8 +4,10 @@ namespace DigipolisGent\Domainator9k\CoreBundle\Entity; use DigipolisGent\Domainator9k\CoreBundle\Entity\Traits\IdentifiableTrait; +use DigipolisGent\Domainator9k\CoreBundle\Entity\Traits\TemplateImplementationTrait; use DigipolisGent\SettingBundle\Entity\Traits\SettingImplementationTrait; use Doctrine\Common\Collections\ArrayCollection; +use Doctrine\Common\Collections\Collection; use Doctrine\ORM\Mapping as ORM; use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity; use Symfony\Component\Validator\Constraints as Assert; @@ -15,23 +17,29 @@ * @package DigipolisGent\Domainator9k\CoreBundle\Entity * * @ORM\Entity() - * @ORM\Table() + * @ORM\Table(name="abstract_application") * @ORM\InheritanceType("JOINED") * @ORM\DiscriminatorColumn(name="discr",type="string") - * @UniqueEntity(fields={"name"}) + * @UniqueEntity(fields={"name"})] + * @ORM\HasLifecycleCallbacks() */ abstract class AbstractApplication implements TemplateInterface { use SettingImplementationTrait; use IdentifiableTrait; + use TemplateImplementationTrait; /** * @var string * * @ORM\Column(name="name", type="string", nullable=false) * @Assert\NotBlank() - * @Assert\Length(min="3", max="255") + * @Assert\Length(min="2", max="255") + * @Assert\Regex( + * pattern="/^[a-z0-9\-]+$/", + * message="Name can only contain alphanumeric characters and dashes." + * ) */ protected $name; @@ -54,8 +62,6 @@ abstract class AbstractApplication implements TemplateInterface * @var ArrayCollection * * @ORM\OneToMany(targetEntity="ApplicationEnvironment", mappedBy="application", cascade={"all"},fetch="EAGER") - * @Assert\Valid() - * @Assert\NotNull() */ protected $applicationEnvironments; @@ -66,6 +72,13 @@ abstract class AbstractApplication implements TemplateInterface */ protected $deleted = false; + /** + * @var string + * + * @ORM\Column(name="application_type",type="string") + */ + protected $applicationType; + /** * @return string */ @@ -89,7 +102,7 @@ public function __construct() /** * @return string */ - public function getName() + public function getName(): ?string { return $this->name; } @@ -105,16 +118,16 @@ public function setName(string $name) /** * @return string */ - public function getNameCanonical() + public function getNameCanonical(): ?string { $name = strtolower(preg_replace("/[^a-zA-Z0-9]+/", "", $this->getName())); - return substr($name, 0, 12); + return substr($name, 0, 14); } /** * @return string */ - public function getGitRepo() + public function getGitRepo(): ?string { return $this->gitRepo; } @@ -130,7 +143,7 @@ public function setGitRepo(string $gitRepo) /** * @return bool */ - public function isHasDatabase(): bool + public function isHasDatabase(): ?bool { return $this->hasDatabase; } @@ -163,7 +176,7 @@ public function removeApplicationEnvironment(ApplicationEnvironment $application /** * @return ArrayCollection */ - public function getApplicationEnvironments() + public function getApplicationEnvironments(): Collection { return $this->applicationEnvironments; } @@ -172,7 +185,7 @@ public function getApplicationEnvironments() * @param $name * @return mixed|null */ - public function getApplicationEnvironmentByEnvironmentName(string $name) + public function getApplicationEnvironmentByEnvironmentName(string $name): ?ApplicationEnvironment { foreach ($this->applicationEnvironments as $applicationEnvironment) { if ($applicationEnvironment->getEnvironment()->getName() == $name) { @@ -180,16 +193,16 @@ public function getApplicationEnvironmentByEnvironmentName(string $name) } } - return ''; + return null; } /** * @return array */ - public static function getTemplateReplacements(): array + public static function additionalTemplateReplacements(): array { + // Backward compatibility. return [ - 'nameCanonical()' => 'getNameCanonical()', 'serverIps(dev_environment_name)' => 'getApplicationEnvironmentByEnvironmentName(dev_environment_name).getServerIps()', ]; } @@ -197,7 +210,7 @@ public static function getTemplateReplacements(): array /** * @return bool */ - public function isDeleted(): bool + public function isDeleted(): ?bool { return $this->deleted; } @@ -209,4 +222,11 @@ public function setDeleted(bool $deleted = false) { $this->deleted = $deleted; } + + /** + * @ORM\PrePersist() + */ + public function prePersist(){ + $this->applicationType = $this::getApplicationType(); + } } diff --git a/Entity/ApplicationEnvironment.php b/Entity/ApplicationEnvironment.php index 38bfe91..d596e93 100644 --- a/Entity/ApplicationEnvironment.php +++ b/Entity/ApplicationEnvironment.php @@ -3,8 +3,10 @@ namespace DigipolisGent\Domainator9k\CoreBundle\Entity; use DigipolisGent\Domainator9k\CoreBundle\Entity\Traits\IdentifiableTrait; +use DigipolisGent\Domainator9k\CoreBundle\Entity\Traits\TemplateImplementationTrait; use DigipolisGent\SettingBundle\Entity\Traits\SettingImplementationTrait; use Doctrine\Common\Collections\ArrayCollection; +use Doctrine\Common\Collections\Collection; use Doctrine\ORM\Mapping as ORM; use Symfony\Component\Validator\Constraints as Assert; @@ -17,6 +19,7 @@ class ApplicationEnvironment implements TemplateInterface use SettingImplementationTrait; use IdentifiableTrait; + use TemplateImplementationTrait; /** * @var ArrayCollection @@ -90,26 +93,10 @@ public static function getSettingImplementationName() return 'application_environment'; } - /** - * @return array - */ - public static function getTemplateReplacements(): array - { - return [ - 'serverIps()' => 'getServerIps()', - 'environmentName()' => 'getEnvironment().getName()', - 'config(key)' => 'getConfig(key)', - 'databaseName()' => 'getDatabaseName()', - 'databaseUser()' => 'getDatabaseUser()', - 'databasePassword()' => 'getDatabasePassword()', - 'gitRef()' => 'getGitRef()', - ]; - } - /** * @return AbstractApplication */ - public function getApplication() + public function getApplication(): ?AbstractApplication { return $this->application; } @@ -125,7 +112,7 @@ public function setApplication(AbstractApplication $application = null) /** * @return string */ - public function getDatabaseName() + public function getDatabaseName(): ?string { return $this->databaseName; } @@ -141,7 +128,7 @@ public function setDatabaseName(string $databaseName = null) /** * @return string */ - public function getEnvironmentName() + public function getEnvironmentName(): ?string { return $this->getEnvironment()->getName(); } @@ -149,7 +136,7 @@ public function getEnvironmentName() /** * @return Environment */ - public function getEnvironment() + public function getEnvironment(): ?Environment { return $this->environment; } @@ -165,7 +152,7 @@ public function setEnvironment(Environment $environment) /** * @return string */ - public function getDatabaseUser() + public function getDatabaseUser(): ?string { return $this->databaseUser; } @@ -181,7 +168,7 @@ public function setDatabaseUser(string $databaseUser = null) /** * @return string */ - public function getDatabasePassword() + public function getDatabasePassword(): ?string { return $this->databasePassword; } @@ -197,7 +184,7 @@ public function setDatabasePassword(string $databasePassword = null) /** * @return string */ - public function getGitRef() + public function getGitRef(): ?string { return $this->gitRef; } @@ -205,7 +192,7 @@ public function getGitRef() /** * @param string $gitRef */ - public function setGitRef(string $gitRef) + public function setGitRef(string $gitRef = null) { $this->gitRef = $gitRef; } @@ -213,7 +200,7 @@ public function setGitRef(string $gitRef) /** * @return ArrayCollection */ - public function getTasks() + public function getTasks(): Collection { return $this->tasks; } @@ -221,7 +208,7 @@ public function getTasks() /** * @return string */ - public function getDomain() + public function getDomain(): ?string { return $this->domain; } @@ -248,14 +235,19 @@ public function getServerIps(): string return implode(' ', $serverIps); } - public function getConfig($key) + /** + * @return string + */ + public function getWorkerServerIp(): string { - foreach ($this->getSettingDataValues() as $settingDataValue) { - if ($settingDataValue->getSettingDataType()->getKey() == $key) { - return $settingDataValue->getValue(); + /** @var VirtualServer $server */ + $servers = $this->getEnvironment()->getVirtualServers(); + foreach ($servers as $server) { + if ($server->isTaskServer()) { + return $server->getHost(); } } - return ''; + return end($servers)->getHost(); } } diff --git a/Entity/ApplicationType.php b/Entity/ApplicationType.php index a0e3a27..1255cf4 100644 --- a/Entity/ApplicationType.php +++ b/Entity/ApplicationType.php @@ -13,6 +13,7 @@ * @package DigipolisGent\Domainator9k\CoreBundle\Entity * * @ORM\Entity() + * @ORM\Table(name="application_type") */ class ApplicationType { diff --git a/Entity/ApplicationTypeEnvironment.php b/Entity/ApplicationTypeEnvironment.php index c76746e..f7319fc 100644 --- a/Entity/ApplicationTypeEnvironment.php +++ b/Entity/ApplicationTypeEnvironment.php @@ -11,7 +11,8 @@ * Class ApplicationTypeEnvironment * @package DigipolisGent\Domainator9k\CoreBundle\Entity * - * @ORM\Entity() + * @ORM\Entity(repositoryClass="DigipolisGent\Domainator9k\CoreBundle\Repository\ApplicationTypeEnvironmentRepository") + * @ORM\Table(name="application_type_environment") */ class ApplicationTypeEnvironment { @@ -79,4 +80,4 @@ public function getEnvironmentName() { return $this->getEnvironment()->getName(); } -} \ No newline at end of file +} diff --git a/Entity/Environment.php b/Entity/Environment.php index ac46971..53a77e1 100644 --- a/Entity/Environment.php +++ b/Entity/Environment.php @@ -3,8 +3,10 @@ namespace DigipolisGent\Domainator9k\CoreBundle\Entity; use DigipolisGent\Domainator9k\CoreBundle\Entity\Traits\IdentifiableTrait; +use DigipolisGent\Domainator9k\CoreBundle\Entity\Traits\TemplateImplementationTrait; use DigipolisGent\SettingBundle\Entity\Traits\SettingImplementationTrait; use Doctrine\Common\Collections\ArrayCollection; +use Doctrine\Common\Collections\Collection; use Doctrine\ORM\Mapping as ORM; use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity; use Symfony\Component\Validator\Constraints as Assert; @@ -14,11 +16,12 @@ * @ORM\Table(name="environment") * @UniqueEntity(fields={"name"}) */ -class Environment +class Environment implements TemplateInterface { use SettingImplementationTrait; use IdentifiableTrait; + use TemplateImplementationTrait; /** * @var string @@ -60,6 +63,21 @@ class Environment */ protected $virtualServers; + /** + * @var string + * + * @ORM\Column(name="git_ref",type="string",nullable=true) + * @Assert\NotBlank() + */ + protected $gitRef; + + /** + * @var integer + * + * @ORM\Column(name="priority",type="integer",nullable=true) + */ + protected $priority; + /** * Creates a new environment. */ @@ -83,7 +101,7 @@ public static function getSettingImplementationName() * * @return string */ - public function getName() + public function getName(): ?string { return $this->name; } @@ -141,7 +159,7 @@ public function addApplicationEnvironment(ApplicationEnvironment $applicationEnv /** * @return ArrayCollection */ - public function getApplicationEnvironments() + public function getApplicationEnvironments(): Collection { return $this->applicationEnvironments; } @@ -157,7 +175,7 @@ public function addApplicationTypeEnvironment(ApplicationTypeEnvironment $applic /** * @return ArrayCollection */ - public function getApplicationTypeEnvironments() + public function getApplicationTypeEnvironments(): Collection { return $this->applicationTypeEnvironments; } @@ -174,8 +192,40 @@ public function addVirtualServer(VirtualServer $virtualServer) /** * @return ArrayCollection */ - public function getVirtualServers() + public function getVirtualServers(): Collection { return $this->virtualServers; } + + /** + * @return string + */ + public function getGitRef(): ?string + { + return $this->gitRef; + } + + /** + * @param string $gitRef + */ + public function setGitRef(string $gitRef) + { + $this->gitRef = $gitRef; + } + + /** + * @return int + */ + public function getPriority(): ?int + { + return $this->priority; + } + + /** + * @param int $priority + */ + public function setPriority(int $priority = null) + { + $this->priority = $priority; + } } diff --git a/Entity/Repository/TaskRepository.php b/Entity/Repository/TaskRepository.php index d042f6b..ec7c483 100644 --- a/Entity/Repository/TaskRepository.php +++ b/Entity/Repository/TaskRepository.php @@ -28,6 +28,7 @@ public function getNextTask($type) public function getLastTaskId(ApplicationEnvironment $applicationEnvironment, string $type) { + $task = $this->_em->createQueryBuilder() ->select('t') ->from(Task::class, 't') @@ -36,7 +37,7 @@ public function getLastTaskId(ApplicationEnvironment $applicationEnvironment, st ->andWhere('ae.id=:id') ->setParameter('type', $type) ->setParameter('id', $applicationEnvironment->getId()) - ->orderBy('t.created') + ->orderBy('t.created','DESC') ->setMaxResults(1) ->getQuery() ->getOneOrNullResult(); diff --git a/Entity/Task.php b/Entity/Task.php index 3f79143..3f0d1c8 100644 --- a/Entity/Task.php +++ b/Entity/Task.php @@ -16,6 +16,8 @@ class Task const STATUS_NEW = 'new'; const STATUS_IN_PROGRESS = 'in_progress'; const STATUS_PROCESSED = 'processed'; + const STATUS_FAILED = 'failed'; + const STATUS_CANCEL= 'cancel'; const TYPE_BUILD = 'build'; const TYPE_DESTROY = 'destroy'; @@ -23,16 +25,19 @@ class Task use IdentifiableTrait; /** - * @var DateTime - * @ORM\Column(name="created", type="datetime", nullable=false) + * @var ApplicationEnvironment + * + * @ORM\ManyToOne(targetEntity="ApplicationEnvironment",inversedBy="tasks") + * @ORM\JoinColumn(referencedColumnName="id") */ - protected $created; + protected $applicationEnvironment; /** * @var string - * @ORM\Column(name="log", type="text", nullable=true) + * + * @ORM\Column(name="type",type="string") */ - protected $log; + protected $type; /** * @var string @@ -42,19 +47,22 @@ class Task protected $status; /** - * @var ApplicationEnvironment - * - * @ORM\ManyToOne(targetEntity="ApplicationEnvironment",inversedBy="tasks") - * @ORM\JoinColumn(referencedColumnName="id") + * @var DateTime + * @ORM\Column(name="created", type="datetime", nullable=false) */ - protected $applicationEnvironment; + protected $created; + + /** + * @var string[] + * @ORM\Column(name="provisioners", type="simple_array", nullable=true) + */ + protected $provisioners; /** * @var string - * - * @ORM\Column(name="type",type="string") + * @ORM\Column(name="log", type="text", nullable=true) */ - protected $type; + protected $log; /** * Build constructor. @@ -65,6 +73,54 @@ public function __construct() $this->status = self::STATUS_NEW; } + /** + * @return ApplicationEnvironment + */ + public function getApplicationEnvironment() + { + return $this->applicationEnvironment; + } + + /** + * @param ApplicationEnvironment $applicationEnvironment + */ + public function setApplicationEnvironment(ApplicationEnvironment $applicationEnvironment) + { + $this->applicationEnvironment = $applicationEnvironment; + } + + /** + * @return string + */ + public function getType(): string + { + return $this->type; + } + + /** + * @param string $type + */ + public function setType(string $type) + { + $this->type = $type; + } + + /** + * @return string + */ + public function getStatus(): string + { + return $this->status; + } + + /** + * @param string $status + */ + public function setStatus(string $status) + { + $this->status = $status; + } + /** * @return DateTime */ @@ -73,6 +129,22 @@ public function getCreated(): DateTime return $this->created; } + /** + * @return string[] + */ + public function getProvisioners() + { + return $this->provisioners; + } + + /** + * @param string[] $provisioners + */ + public function setProvisioners($provisioners) + { + $this->provisioners = $provisioners; + } + /** * Gets the log. * @@ -98,50 +170,84 @@ public function setLog($log) } /** - * @param ApplicationEnvironment $applicationEnvironment + * Mark this task as in progress. */ - public function setApplicationEnvironment(ApplicationEnvironment $applicationEnvironment) + public function setInProgress() { - $this->applicationEnvironment = $applicationEnvironment; + $this->setStatus(static::STATUS_IN_PROGRESS); } /** - * @return ApplicationEnvironment + * Mark this task as processed. */ - public function getApplicationEnvironment() + public function setProcessed() { - return $this->applicationEnvironment; + $this->setStatus(static::STATUS_PROCESSED); } /** - * @return string + * Mark this task as failed. */ - public function getStatus(): string + public function setFailed() { - return $this->status; + $this->setStatus(static::STATUS_FAILED); } /** - * @param string $status + * Mark this task as in cancelled. */ - public function setStatus(string $status) + public function setCancelled() { - $this->status = $status; + $this->setStatus(static::STATUS_CANCEL); } /** - * @return string + * Check if this task is new. + * + * @return boolean */ - public function getType(): string + public function isNew() { - return $this->type; + return $this->getStatus() === static::STATUS_NEW; } /** - * @param string $type + * Check if this task is in progress. + * + * @return boolean */ - public function setType(string $type) + public function isInProgress() { - $this->type = $type; + return $this->getStatus() === static::STATUS_IN_PROGRESS; + } + + /** + * Check if this task is processed. + * + * @return boolean + */ + public function isProcessed() + { + return $this->getStatus() === static::STATUS_PROCESSED; + } + + /** + * Check if this task is failed. + * + * @return boolean + */ + public function isFailed() + { + return $this->getStatus() === static::STATUS_FAILED; + } + + /** + * Check if this task is cancelled. + * + * @return boolean + */ + public function isCancelled() + { + return $this->getStatus() === static::STATUS_CANCEL; } } diff --git a/Entity/TemplateInterface.php b/Entity/TemplateInterface.php index 083b665..64347a3 100644 --- a/Entity/TemplateInterface.php +++ b/Entity/TemplateInterface.php @@ -9,5 +9,5 @@ */ interface TemplateInterface { - public static function getTemplateReplacements(): array; + public static function getTemplateReplacements(int $maxDepth = 3, array $skip = []): array; } diff --git a/Entity/Token.php b/Entity/Token.php new file mode 100644 index 0000000..77af678 --- /dev/null +++ b/Entity/Token.php @@ -0,0 +1,83 @@ +name; + } + + /** + * Sets the name. + * + * @param string $name + * + * @return $this + */ + public function setName($name) + { + $this->name = $name; + + return $this; + } + + /** + * Gets the value. + * + * @return string + */ + public function getValue(): ?string + { + return $this->value; + } + + /** + * Sets the value. + * + * @param string $value + * + * @return $this + */ + public function setValue($value) + { + $this->value = $value; + + return $this; + } +} diff --git a/Entity/Traits/TemplateImplementationTrait.php b/Entity/Traits/TemplateImplementationTrait.php new file mode 100644 index 0000000..f0301be --- /dev/null +++ b/Entity/Traits/TemplateImplementationTrait.php @@ -0,0 +1,199 @@ +hasMethod('additionalTemplateReplacements')) { + $replacements += static::additionalTemplateReplacements(); + } + return $replacements; + } + + /** + * Get relevant methods for template replacements. + * + * This function returns methods that are: + * - Not abstract or static + * - Start with -but are not equal to- 'get'. + * - Have a return type + * - Whose return type is scalar or implements TemplateInterface and is + * different from the current class (prevent loops). + * - Whose parameters are scalar (or non existant). + * + * @param \ReflectionClass $class + * The class to get the relevant methods of. + * + * @return \ReflectionMethod[] + * The relevant methods to build the templates. + */ + protected static function getRelevantMethods(ReflectionClass $class) + { + // Get all public methods. + $methods = $class->getMethods(\ReflectionMethod::IS_PUBLIC); + $relevantMethods = []; + foreach ($methods as $method) { + // That are not abstract or static. + if ($method->isAbstract() || $method->isStatic()) { + continue; + } + // That start with -but are not equal to- 'get'. + $name = $method->getName(); + if ($name === 'get' || substr($name, 0, 3) !== 'get') { + continue; + } + // That have a return type. + if (!$method->hasReturnType()) { + continue; + } + $returnType = $method->getReturnType(); + // Whose return type is scalar or implements TemplateInterface and + // is different from the current class (prevent loops). + if ( + !$returnType->isBuiltin() + && ( + !class_exists($returnType) + || !is_a((string)$returnType, TemplateInterface::class, true) + ) + ) { + continue; + } + // Whose parameters are scalar (or non existant). + foreach ($method->getParameters() as $parameter) { + $parameterType = $parameter->getType(); + if (!is_null($parameterType) && !$parameterType->isBuiltin()) { + continue 2; + } + } + $relevantMethods[$method->getName()] = $method; + } + return $relevantMethods; + } + + /** + * Get all template replacements for a method. + * + * If this method's return type is scalar, it'll have one template. If the + * return type implements TemplateInterface, it is chained (as a prefix) to + * the templates of that return type. + * + * @param ReflectionMethod $method + * The method to get the templates for. + * @param int $maxDepth + * The maximum depth to chain. + * @param array $skip + * An array of classes to skip chaining for. + * + * @return array + * The templates generated for this method. + */ + protected static function getTemplateReplacementsForMethod(ReflectionMethod $method, int $maxDepth, array $skip) + { + $returnType = $method->getReturnType(); + $parameters = []; + foreach ($method->getParameters() as $parameter) { + $parameters[] = $parameter->getName(); + } + $replacementParameters = implode(',', $parameters); + $replacementCallback = $method->getName() . '(' . $replacementParameters . ')'; + + // Scalar return type, do not chain. + if ($returnType->isBuiltin()) { + // Strip off 'get' from the keyword and lowercase the first letter. + $template = lcfirst(substr($method->getName(), 3)) . '(' . $replacementParameters . ')'; + return [$template => $replacementCallback]; + } + + // We've reached max depth or we should skip chaining for the return + // type. + if ($maxDepth <= 0 || in_array((string)$returnType, $skip)) { + return []; + } + + // Since method return type are usually more generic (interface, + // abstract class) we also check if the return type is a parent class of + // any of the classes to skip. + foreach ($skip as $skipClass) { + if (is_a($skipClass, (string) $returnType, true)) { + return []; + } + } + return static::getSubReplacementsForMethod($method, $maxDepth, $skip); + } + + /** + * Get all subtemplate replacements for a method. + * + * The methods return type should implement TemplateInterface. This return + * type is chained (as a prefix) to the templates of that return type. + * + * @param ReflectionMethod $method + * The method to get the templates for. + * @param int $maxDepth + * The maximum depth to chain. + * @param array $skip + * An array of classes to skip chaining for. + * + * @return array + * The templates generated for this method. + */ + protected static function getSubReplacementsForMethod(ReflectionMethod $method, int $maxDepth, array $skip) + { + $returnType = $method->getReturnType(); + $parameters = []; + foreach ($method->getParameters() as $parameter) { + $parameters[] = $parameter->getName(); + } + $replacementParameters = implode(',', $parameters); + $replacementCallback = $method->getName() . '(' . $replacementParameters . ')'; + // Build the templates + $replacements = []; + $maxDepth--; + $subs = call_user_func(array((string)$returnType, 'getTemplateReplacements'), $maxDepth, $skip); + foreach ($subs as $subTemplate => $replacementSubCallback) { + // Since we're chaining, we prepend the classname, with 'Abstract' or + // 'Interface' stripped off, to the method for uniqueness. + $template = lcfirst( + str_replace( + ['Abstract', 'Interface'], + ['', ''], + ReflectionClass::createFromName((string)$returnType)->getShortName() + ) + ); + // And we append the parameters of chained methods to the template. + $template .= ucfirst( + str_replace( + ['(,', ',)'], + ['(', ')'], + preg_replace( + '/\((.*)\)/', + '(' . $replacementParameters . ',$1)', + $subTemplate + ) + ) + ); + $replacements[$template] = $replacementCallback . '.' . $replacementSubCallback; + } + return $replacements; + } +} diff --git a/Entity/VirtualServer.php b/Entity/VirtualServer.php index 1f36177..71c8f87 100644 --- a/Entity/VirtualServer.php +++ b/Entity/VirtualServer.php @@ -9,7 +9,7 @@ /** * @ORM\Entity - * @ORM\Table(name="virtualserver") + * @ORM\Table(name="virtual_server") */ class VirtualServer { diff --git a/Event/AbstractEvent.php b/Event/AbstractEvent.php index 1462be1..4efe518 100644 --- a/Event/AbstractEvent.php +++ b/Event/AbstractEvent.php @@ -1,20 +1,37 @@ task = $task; } + /** + * Get the task object. + * + * @return Task + */ public function getTask() { return $this->task; diff --git a/EventListener/BuildEventListener.php b/EventListener/BuildEventListener.php deleted file mode 100644 index 552e3ee..0000000 --- a/EventListener/BuildEventListener.php +++ /dev/null @@ -1,50 +0,0 @@ -taskLoggerService = $taskLoggerService; - $this->entityManager = $entityManager; - } - - public function onStart(BuildEvent $event) - { - $task = $event->getTask(); - $task->setStatus(Task::STATUS_IN_PROGRESS); - $this->entityManager->persist($task); - $this->entityManager->flush(); - $this->taskLoggerService->setTask($event->getTask()); - } - - public function onEnd(BuildEvent $event) - { - $task = $event->getTask(); - $task->setStatus(Task::STATUS_PROCESSED); - $this->entityManager->persist($task); - $this->entityManager->flush(); - } -} diff --git a/EventListener/DestroyEventListener.php b/EventListener/DestroyEventListener.php deleted file mode 100644 index 4cc6ced..0000000 --- a/EventListener/DestroyEventListener.php +++ /dev/null @@ -1,57 +0,0 @@ -taskLoggerService = $taskLoggerService; - $this->entityManager = $entityManager; - } - - /** - * @param BuildEvent $event - */ - public function onStart(DestroyEvent $event) - { - $task = $event->getTask(); - $task->setStatus(Task::STATUS_IN_PROGRESS); - $this->entityManager->persist($task); - $this->entityManager->flush(); - $this->taskLoggerService->setTask($event->getTask()); - } - - /** - * @param BuildEvent $event - */ - public function onEnd(DestroyEvent $event) - { - $task = $event->getTask(); - $task->setStatus(Task::STATUS_PROCESSED); - $this->entityManager->persist($task); - $this->entityManager->flush(); - } -} diff --git a/EventListener/EnvironmentEventListener.php b/EventListener/EnvironmentEventListener.php index dc723e3..63f94a8 100644 --- a/EventListener/EnvironmentEventListener.php +++ b/EventListener/EnvironmentEventListener.php @@ -26,17 +26,6 @@ public function postPersist(LifecycleEventArgs $args) $entityManager = $args->getEntityManager(); if ($entity instanceof Environment) { - $applications = $entityManager->getRepository(AbstractApplication::class)->findAll(); - - foreach ($applications as $application) { - $applicationEnvironment = new ApplicationEnvironment(); - $applicationEnvironment->setApplication($application); - $applicationEnvironment->setEnvironment($entity); - - $entityManager->persist($applicationEnvironment); - $entityManager->flush(); - } - $applicationTypes = $entityManager->getRepository(ApplicationType::class)->findAll(); foreach ($applicationTypes as $applicationType) { diff --git a/Exception/LoggedException.php b/Exception/LoggedException.php new file mode 100644 index 0000000..ecc9af8 --- /dev/null +++ b/Exception/LoggedException.php @@ -0,0 +1,7 @@ +add('name'); $builder->add('prod'); + $builder->add('gitRef'); + $builder->add('priority'); $builder->addEventSubscriber(new SettingFormListener($this->formService)); } diff --git a/Form/Type/TaskFormType.php b/Form/Type/TaskFormType.php new file mode 100644 index 0000000..85e6bc7 --- /dev/null +++ b/Form/Type/TaskFormType.php @@ -0,0 +1,94 @@ +formService = $formService; + $this->taskRunnerService = $taskRunnerService; + } + + /** + * @param FormBuilderInterface $builder + * @param array $options + */ + public function buildForm(FormBuilderInterface $builder, array $options) + { + parent::buildForm($builder, $options); + + switch ($options['type']) { + case Task::TYPE_BUILD: + $provisioners = $this->taskRunnerService->getBuildProvisioners(); + break; + + case Task::TYPE_DESTROY: + $provisioners = $this->taskRunnerService->getDestroyProvisioners(); + break; + + default: + $provisioners = []; + break; + } + + $choices = []; + $defaults = []; + foreach ($provisioners as $provisioner) { + $class = get_class($provisioner); + $choices[$provisioner->getName()] = $class; + + if ($provisioner->isExecutedByDefault()) { + $defaults[] = $class; + } + } + + $builder->add('provisioners', ChoiceType::class, [ + 'expanded' => true, + 'multiple' => true, + 'required' => false, + 'choices' => $choices, + 'data' => $defaults, + 'empty_data' => $defaults, + 'label' => 'Limit to following provisioners (selecting none will run the default provisioners)', + ]); + $builder->addEventSubscriber(new SettingFormListener($this->formService)); + } + + /** + * @param OptionsResolver $resolver + */ + public function configureOptions(OptionsResolver $resolver) + { + parent::configureOptions($resolver); + $resolver->setDefault('data_class', Task::class); + $resolver->setDefault('type', Task::TYPE_BUILD); + } +} diff --git a/Form/Type/TokenFormType.php b/Form/Type/TokenFormType.php new file mode 100644 index 0000000..65a35ed --- /dev/null +++ b/Form/Type/TokenFormType.php @@ -0,0 +1,47 @@ +formService = $formService; + } + + /** + * @param FormBuilderInterface $builder + * @param array $options + */ + public function buildForm(FormBuilderInterface $builder, array $options) + { + parent::buildForm($builder, $options); + $builder->add('name'); + $builder->add('value'); + $builder->addEventSubscriber(new SettingFormListener($this->formService)); + } + + /** + * @param OptionsResolver $resolver + */ + public function configureOptions(OptionsResolver $resolver) + { + parent::configureOptions($resolver); + $resolver->setDefault('data_class', Token::class); + } +} diff --git a/Provider/CacheClearProvider.php b/Provider/CacheClearProvider.php new file mode 100644 index 0000000..51bfcdf --- /dev/null +++ b/Provider/CacheClearProvider.php @@ -0,0 +1,48 @@ +cacheClearers[$class] = $clearer; + } + + /** + * @param mixed $object + * + * @return CacheClearerInterface + * + * @throws \InvalidArgumentException + * @throws NoCacheClearerFoundException + */ + public function getCacheClearerFor($object) + { + if (!is_object($object)) { + throw new \InvalidArgumentException( + sprintf( + '%s::getCacheClearerFor() expects parameter 1 to be an object, %s given.', + get_called_class(), + gettype($object) + ) + ); + } + + $class = get_class($object); + if (!isset($this->cacheClearers[$class]) && $object instanceof Proxy) { + $class = get_parent_class($object); + } + if (isset($this->cacheClearers[$class])) { + return $this->cacheClearers[$class]; + } + + throw new NoCacheClearerFoundException('No cache clearer found for ' . $class); + } +} diff --git a/Provider/CliFactoryProvider.php b/Provider/CliFactoryProvider.php new file mode 100644 index 0000000..2eea257 --- /dev/null +++ b/Provider/CliFactoryProvider.php @@ -0,0 +1,69 @@ +defaultCliFactory = $defaultCliFactory; + } + + public function registerCliFactory(CliFactoryInterface $cliFactory, $class) + { + $this->cliFactories[$class] = $cliFactory; + } + + /** + * @param mixed $object + * + * @return CliInterface + * + * @throws \InvalidArgumentException + * @throws NoCliFactoryFoundException + */ + public function createCliFor($object) + { + if (!is_object($object)) { + throw new \InvalidArgumentException( + sprintf( + '%s::createCliFor() expects parameter 1 to be an object, %s given.', + get_called_class(), + gettype($object) + ) + ); + } + + $class = get_class($object); + + if (!isset($this->cliFactories[$class]) && $object instanceof Proxy) { + $class = get_parent_class($object); + } + if (isset($this->cliFactories[$class])) { + return $this->cliFactories[$class]->create($object); + } + if (!($this->defaultCliFactory instanceof CliFactoryInterface)) { + throw new NoCliFactoryFoundException( + sprintf('No cli factory found for %s and no default factory given.', $class) + ); + } + return $this->defaultCliFactory->create($object); + } +} diff --git a/Provisioner/AbstractProvisioner.php b/Provisioner/AbstractProvisioner.php new file mode 100644 index 0000000..9e5ce14 --- /dev/null +++ b/Provisioner/AbstractProvisioner.php @@ -0,0 +1,38 @@ +task = $task; + } + + public final function run() + { + if (!($this->task instanceof Task)) { + throw new \LogicException('A task must be set before running a provisioner.'); + } + $this->doRun(); + } + + abstract protected function doRun(); + + public function isExecutedByDefault() + { + return true; + } +} diff --git a/Provisioner/CacheClearBuildProvisioner.php b/Provisioner/CacheClearBuildProvisioner.php new file mode 100644 index 0000000..7d18255 --- /dev/null +++ b/Provisioner/CacheClearBuildProvisioner.php @@ -0,0 +1,84 @@ +cliFactoryProvider = $cliFactoryProvider; + $this->cacheClearProvider = $cacheClearProvider; + $this->taskLoggerService = $taskLoggerService; + } + + protected function doRun() + { + $appEnv = $this->task->getApplicationEnvironment(); + $application = $appEnv->getApplication(); + $environment = $appEnv->getEnvironment(); + + $this->taskLoggerService->addLogHeader( + $this->task, + sprintf( + 'Clearing cache for %s on %s.', + $application->getName(), + $environment->getName() + ) + ); + try { + $cli = $this->cliFactoryProvider->createCliFor($appEnv); + $result = $this->cacheClearProvider + ->getCacheClearerFor($application) + ->clearCache( + $appEnv, + $cli + ); + if (!$result) { + $this->taskLoggerService->addErrorLogMessage($this->task, 'Cache clear failed.', 2); + } + $output = $cli->getLastOutput(); + if ($output) { + $this->taskLoggerService->addInfoLogMessage($this->task, $output, 2); + } + } catch (NoCacheClearerFoundException $cacheEx) { + $this->taskLoggerService->addWarningLogMessage($this->task, $cacheEx->getMessage(), 2); + } catch (NoCliFactoryFoundException $cliEx) { + $this->taskLoggerService->addWarningLogMessage($this->task, $cliEx->getMessage(), 2); + } + } + + public function getName() + { + return 'Clear caches'; + } + + public function isExecutedByDefault() + { + return false; + } +} diff --git a/Provisioner/ProvisionerInterface.php b/Provisioner/ProvisionerInterface.php new file mode 100644 index 0000000..895df1d --- /dev/null +++ b/Provisioner/ProvisionerInterface.php @@ -0,0 +1,13 @@ +createQueryBuilder('ate') + ->innerJoin('ate.applicationType', 'at') + ->andWhere('at.name = :name') + ->setParameter('name', $type) + ->getQuery() + ->getResult(); + } +} diff --git a/Resources/config/services.yml b/Resources/config/services.yml index 7a7c241..a0f443f 100644 --- a/Resources/config/services.yml +++ b/Resources/config/services.yml @@ -6,20 +6,39 @@ services: - { name: doctrine.event_listener, event: postPersist, connection: default } - { name: doctrine.event_listener, event: postRemove, connection: default } DigipolisGent\Domainator9k\CoreBundle\Service\TaskLoggerService: - DigipolisGent\Domainator9k\CoreBundle\EventListener\BuildEventListener: - tags: - - { name: kernel.event_listener, event: domainator.build, method: onStart, priority: 99 } - - { name: kernel.event_listener, event: domainator.build, method: onEnd, priority: 1 } - DigipolisGent\Domainator9k\CoreBundle\EventListener\DestroyEventListener: - tags: - - { name: kernel.event_listener, event: domainator.destroy, method: onStart, priority: 99 } - - { name: kernel.event_listener, event: domainator.destroy, method: onEnd, priority: 1 } DigipolisGent\Domainator9k\CoreBundle\Service\TemplateService: + DigipolisGent\Domainator9k\CoreBundle\Service\TokenService: + DigipolisGent\Domainator9k\CoreBundle\Service\TaskRunnerService: + arguments: + - !tagged domainator.provisioner.build + - !tagged domainator.provisioner.destroy + - '@doctrine.orm.entity_manager' + - '@DigipolisGent\Domainator9k\CoreBundle\Service\TaskLoggerService' + DigipolisGent\Domainator9k\CoreBundle\Form\Type\ApplicationEnvironmentFormType: tags: [form.type] DigipolisGent\Domainator9k\CoreBundle\Form\Type\EnvironmentFormType: tags: [form.type] DigipolisGent\Domainator9k\CoreBundle\Form\Type\VirtualServerFormType: tags: [form.type] + DigipolisGent\Domainator9k\CoreBundle\Form\Type\TokenFormType: + tags: [form.type] + DigipolisGent\Domainator9k\CoreBundle\Form\Type\TaskFormType: + tags: [form.type] DigipolisGent\Domainator9k\CoreBundle\Form\Type\ApplicationTypeEnvironmentFormType: - tags: [form.type] \ No newline at end of file + tags: [form.type] + DigipolisGent\Domainator9k\CoreBundle\Twig\TemplateHelpExtension: + tags: [twig.extension] + DigipolisGent\Domainator9k\CoreBundle\Provisioner\CacheClearBuildProvisioner: + tags: + - {name: domainator.provisioner.build, priority: -90} + arguments: + - '@DigipolisGent\Domainator9k\CoreBundle\Provider\CliFactoryProvider' + - '@DigipolisGent\Domainator9k\CoreBundle\Provider\CacheClearProvider' + - '@DigipolisGent\Domainator9k\CoreBundle\Service\TaskLoggerService' + DigipolisGent\Domainator9k\CoreBundle\CLI\DefaultCliFactory: + DigipolisGent\Domainator9k\CoreBundle\Provider\CacheClearProvider: + DigipolisGent\Domainator9k\CoreBundle\Provider\CliFactoryProvider: + arguments: + - '@DigipolisGent\Domainator9k\CoreBundle\CLI\DefaultCliFactory' + diff --git a/Resources/public/js/templatehelper.js b/Resources/public/js/templatehelper.js new file mode 100644 index 0000000..ad20793 --- /dev/null +++ b/Resources/public/js/templatehelper.js @@ -0,0 +1,84 @@ +(function (document, window) { + window.templateHelpers = {}; + document.addEventListener("DOMContentLoaded", function () { + document.querySelectorAll('dialog[data-template-helper-textarea]').forEach(function (dialog) { + if (!dialog.dataset.templateHelperProcessed) { + if (typeof window.dialogPolyfill !== 'undefined') { + window.dialogPolyfill.registerDialog(dialog); + } + var selector = dialog.dataset.templateHelperTextarea; + window.templateHelpers[selector] = new TemplateHelper(dialog); + dialog.dataset.templateHelperProcessed = true; + } + }); + }); + + function TemplateHelper(dialog) { + var self = this; + self.dialog = dialog; + self.textareas = new Array(); + self.openDialogLinks = {} + document.querySelectorAll(dialog.dataset.templateHelperTextarea).forEach(function (textarea) { + self.registerTextarea(textarea); + }); + self.bindDialogClose(); + self.bindTemplateLinks(); + } + + TemplateHelper.prototype.registerTextarea = function (textarea) { + var self = this; + self.textareas.push(textarea); + self.activeTextarea = textarea; + self.createDialogLink(textarea); + }; + + TemplateHelper.prototype.bindDialogClose = function () { + var self = this; + self.dialog.querySelectorAll('.close-template-dialog')[0].addEventListener('click', function (e) { + e.preventDefault(); + self.closeDialog(); + }); + }; + + TemplateHelper.prototype.openDialog = function () { + var self = this; + self.dialog.showModal(); + }; + + TemplateHelper.prototype.closeDialog = function () { + var self = this; + self.dialog.close(); + }; + + TemplateHelper.prototype.createDialogLink = function (textarea) { + var self = this; + var link = document.createElement('a'); + link.href = '#'; + link.classList.add('template-helper-dialog-link'); + link.appendChild(document.createTextNode('Insert template value')); + link.addEventListener('click', function (e) { + e.preventDefault(); + self.activeTextarea = textarea; + self.openDialog(); + }); + textarea.parentNode.insertBefore(link, textarea.nextSibling); + self.openDialogLinks[textarea.name] = link; + }; + + TemplateHelper.prototype.bindTemplateLinks = function() { + var self = this; + self.dialog.querySelectorAll('a[data-template-value]').forEach(function (link) { + link.addEventListener('click', function(e) { + e.preventDefault(); + self.insertTemplate(link.dataset.templateValue); + }); + }); + }; + + TemplateHelper.prototype.insertTemplate = function (template) { + var self = this; + self.activeTextarea.value = self.activeTextarea.value + template; + var event = new Event('change'); + self.activeTextarea.dispatchEvent(event); + }; +})(document, window); diff --git a/Resources/views/Template/templatehelper.twig b/Resources/views/Template/templatehelper.twig new file mode 100644 index 0000000..3fa2f53 --- /dev/null +++ b/Resources/views/Template/templatehelper.twig @@ -0,0 +1,12 @@ + + + {% for key,template_items in templates %} +
+ {{ key }} + {% for template in template_items %} +
[[ {{ key }}:{{template}} ]]
+ {% endfor %} +
+ {% endfor %} + +
diff --git a/Service/TaskLoggerService.php b/Service/TaskLoggerService.php index 448ad75..f3da140 100644 --- a/Service/TaskLoggerService.php +++ b/Service/TaskLoggerService.php @@ -1,24 +1,35 @@ getLog()) { + $log .= PHP_EOL; + } + + $header = trim($header); + $header = preg_replace('/[\r\n]+/', ' ', $header); + $header = '### ' . $header . ' ###'; + + $log .= $this->indentText($header, $indent); + + $task->setLog($log); + + if ($persist) { + $this->entityManager->persist($task); + $this->entityManager->flush(); + } + + return $this; + } + + /** + * Add a log message. + * + * @param Task $task + * The task object. + * @param string $type + * The log type. + * @param string $message + * The log message. + * @param int $indent + * Number of levels to indent. + * @param bool $persist + * Persist the task to the database. + * + * @return self + */ + public function addLogMessage(Task $task, string $type, string $message, int $indent = 1, bool $persist = true): self + { + if ($log = $task->getLog()) { + $log .= PHP_EOL; + } + + $message = trim($message); + $message = str_replace(["\r\n", "\r", "\n"], PHP_EOL, $message); + + if ($type && $type !== self::LOG_TYPE_INFO) { + $message .= ' [' . $type . ']'; + } + + $log .= $this->indentText($message, $indent); + + $task->setLog($log); + + if ($persist) { + $this->entityManager->persist($task); + $this->entityManager->flush(); + } + + return $this; + } + + /** + * Add an "info" log message. + * + * @param Task $task + * The task object. + * @param string $message + * The log message. + * @param int $indent + * Number of levels to indent. + * @param bool $persist + * Persist the task to the database. + * + * @return self + */ + public function addInfoLogMessage(Task $task, string $message, int $indent = 1, bool $persist = true): self + { + return $this->addLogMessage($task, self::LOG_TYPE_INFO, $message, $indent, $persist); + } + + /** + * Add a "warning" log message. + * + * @param Task $task + * The task object. + * @param string $message + * The log message. + * @param int $indent + * Number of levels to indent. + * @param bool $persist + * Persist the task to the database. + * + * @return self + */ + public function addWarningLogMessage(Task $task, string $message, int $indent = 1, bool $persist = false): self + { + return $this->addLogMessage($task, self::LOG_TYPE_WARNING, $message, $indent, $persist); + } + + /** + * Add an "error" log message. + * + * @param Task $task + * The task object. + * @param string $message + * The log message. + * @param int $indent + * Number of levels to indent. + * @param bool $persist + * Persist the task to the database. + * + * @return self */ - public function setTask(Task $task) + public function addErrorLogMessage(Task $task, string $message, int $indent = 1, bool $persist = false): self { - $this->task = $task; + return $this->addLogMessage($task, self::LOG_TYPE_ERROR, $message, $indent, $persist); } /** - * @param string $line + * Add a "success" log message. + * + * @param Task $task + * The task object. + * @param string $message + * The log message. + * @param int $indent + * Number of levels to indent. + * @param bool $persist + * Persist the task to the database. + * + * @return self */ - public function addLine(string $line) + public function addSuccessLogMessage(Task $task, string $message, int $indent = 1, bool $persist = true): self { - $log = $this->task->getLog(); - $log .= $line . PHP_EOL; + return $this->addLogMessage($task, self::LOG_TYPE_SUCCESS, $message, $indent, $persist); + } + + /** + * Add a "failed" log message. + * + * @param Task $task + * The task object. + * @param string $message + * The log message. + * @param int $indent + * Number of levels to indent. + * @param bool $persist + * Persist the task to the database. + * + * @return self + */ + public function addFailedLogMessage(Task $task, string $message, int $indent = 1, bool $persist = true): self + { + return $this->addLogMessage($task, self::LOG_TYPE_FAILED, $message, $indent, $persist); + } + + /** + * Indent a text. + * + * @param string $text + * The text to indent. + * @param int $indent + * Number of levels to indent. + * + * @return string + * The indented text. + */ + protected function indentText(string $text, int $indent) + { + return preg_replace_callback('/(^|[\r\n]+)(\t+)?/', function($matches) use ($indent) { + $suffix = ''; + + if ($indent) { + $suffix .= str_repeat("\t", $indent); + } + + if (isset($matches[2])) { + $suffix .= str_repeat(' ', strlen($matches[2])); + } + + return $matches[1] . $suffix; + }, $text); + } + + /** + * Generate an HTML safe task log. + * + * @param string $log + * The task log. + * + * @return string + * The escaped log. + */ + public function escapeLog(string $log): string + { + // Default HTML escaping. + $log = htmlspecialchars($log, ENT_QUOTES, 'UTF-8', false); + $log = str_replace(["\r\n", "\r"], "\n", $log); + + // Make titles bold. + $log = preg_replace('/^(\t*)### (.+) ###$/m', '$1$2', $log); + + // Count the number of lines. + $lineCount = substr_count($log, "\n") + 1; + + // Get the line number width. + $lineNumberWidth = \strlen($lineCount); + + // Wrap all lines + $lineNumber = 0; + $prevIndents = []; + $log = (string) preg_replace_callback( + '/^(\t*)(?:(.+?)(?: \[(warning|error|success|failed)\])?)?$/m', + function ($matches) use (&$lineNumber, &$prevIndents, $lineCount, $lineNumberWidth) { + if (isset($matches[2])) { + $indent = \strlen($matches[1]); + $line = $matches[2]; + } else { + $indent = $prevIndents[0] ?? 0; + $line = ''; + } + + $status = $matches[3] ?? null; + + // Apply the message wrapper with indentation. + $line = sprintf( + '
%s
', + 'message message--indent-' . $indent, + $indent * 1.5, + $line + ); + + // Add the line number. + $lineNumber++; + $line = sprintf( + '
%' . $lineNumberWidth . 's
%s', + 'number number--' . $lineNumber, + $lineNumber, + $line + ); + + // Add the line status. + if ($status !== null) { + $line = sprintf( + '%s
[%s]
', + $line, + 'status status--' . $status, + $status + ); + } + + // Wrap the whole line. + $class = 'line line--' . $lineNumber; + + if ($lineNumber === 1) { + $class .= ' line--first'; + } elseif ($lineNumber === $lineCount) { + $class .= ' line--last'; + } + + if ($status !== null) { + $class .= ' line--status-' . $status; + } + + $line = sprintf( + '
%s
', + $class, + $line + ); + + if (!$prevIndents || $indent > $prevIndents[0]) { + // Start a new indentation group. + $line = sprintf( + '
%s', + 'group group--indent-' . $indent . ' group--number-' . $lineNumber, + $line + ); + array_unshift($prevIndents, $indent); + } elseif ($indent < $prevIndents[0]) { + // Close the previous groups. + do { + $line = '
' . $line; + array_shift($prevIndents); + } while ($indent < $prevIndents[0]); + } + + return $line; + }, + $log + ); + + if ($prevIndents) { + $log .= str_repeat('', \count($prevIndents)); + } - $this->task->setLog($log); - $this->entityManager->persist($this->task); - $this->entityManager->flush(); + return $log; } } diff --git a/Service/TaskRunnerService.php b/Service/TaskRunnerService.php new file mode 100644 index 0000000..60d3adf --- /dev/null +++ b/Service/TaskRunnerService.php @@ -0,0 +1,215 @@ +buildProvisioners = $buildProvisioners; + $this->destroyProvisioners = $destroyProvisioners; + $this->entityManager = $entityManager; + $this->logger = $logger; + } + + /** + * Run a task. + * + * @param Task $task + * The task to run. + * + * @return boolean + * True when the task has been processed succesfully, false for any other + * status. + */ + public function run(Task $task) + { + if (!$task->isNew()) { + throw new \InvalidArgumentException(sprintf('Task "%s" cannot be restarted.', $task->getId())); + } + + // Set the task in progress. + $task->setInProgress(); + $this->entityManager->persist($task); + $this->entityManager->flush(); + + $this->runProvisioners($task); + + // Update the status. + if ($task->isInProgress()) { + $task->setProcessed(); + } + + // Add a log message or simply persist any changes. + switch (true) { + case $task->isProcessed(): + $this->logger->addLogMessage($task, '', '', 0); + $this->logger->addSuccessLogMessage($task, 'Task run completed.', 0); + break; + + case $task->isFailed(): + $this->logger->addLogMessage($task, '', '', 0); + $this->logger->addFailedLogMessage($task, 'Task run failed.', 0); + break; + + default: + $this->entityManager->persist($task); + $this->entityManager->flush(); + break; + } + + return $task->isProcessed(); + } + + /** + * Run all provisioners for a task. + * + * @param Task $task + * The task to run the provisioners for. + * + * @throws \InvalidArgumentException + * If the task type is not supported. + */ + protected function runProvisioners(Task $task) + { + $provisioners = []; + switch ($task->getType()) { + case Task::TYPE_BUILD: + $provisioners = $this->buildProvisioners; + break; + + case Task::TYPE_DESTROY: + $provisioners = $this->destroyProvisioners; + break; + + default: + throw new \InvalidArgumentException(sprintf('Task type %s is not supported.', $task->getType())); + } + try { + foreach ($provisioners as $provisioner) { + if ($task->getProvisioners() && !in_array(get_class($provisioner), $task->getProvisioners())) { + continue; + } + $provisioner->setTask($task); + $provisioner->run(); + if ($task->isFailed()) { + break; + } + } + } catch (\Exception $ex) { + $task->setFailed(); + if (!($ex instanceof LoggedException)) { + $this->logger + ->addErrorLogMessage($task, $ex->getMessage(), 2) + ->addFailedLogMessage($task, sprintf('Provisioner %s failed.', $provisioner->getName())); + } + } + } + + /** + * Run the next task of the specified type. + * + * @param string $type + * The task type to run. + * + * @return boolean + * True on success, false on failure. + */ + public function runNext(string $type) + { + $task = $this->entityManager + ->getRepository(Task::class) + ->getNextTask($type); + + if ($task) { + return $this->run($task); + } + + return true; + } + + /** + * Cancel a task. + * + * @param Task $task + * The task to cancel. + */ + public function cancel(Task $task) + { + if ($task->getStatus() !== Task::STATUS_NEW) { + throw new \InvalidArgumentException(sprintf('Task %s cannot be cancelled.', $task->getId())); + } + + $task->setStatus(Task::STATUS_CANCEL); + $this->logger->addInfoLogMessage($task, 'Task run cancelled.'); + } + + /** + * @return ProvisionerInterface[] + */ + public function getBuildProvisioners() + { + return $this->buildProvisioners; + } + + /** + * @return ProvisionerInterface[] + */ + public function getDestroyProvisioners() + { + return $this->destroyProvisioners; + } +} diff --git a/Service/TemplateService.php b/Service/TemplateService.php index e3410d9..7454f63 100644 --- a/Service/TemplateService.php +++ b/Service/TemplateService.php @@ -8,91 +8,254 @@ /** * Class TemplateService + * * @package DigipolisGent\Domainator9k\CoreBundle\Service */ class TemplateService { /** + * The service for custom tokens. + * + * @var TokenService + */ + protected $tokenService; + + /** + * The replacements. + * + * @var array + */ + protected $replacements; + + /** + * Class constructor. + * + * @param TokenService $tokenService + * The token service. + */ + public function __construct(TokenService $tokenService) + { + $this->tokenService = $tokenService; + } + + /** + * Replace all keys. + * * @param string $text + * The text to replace in. * @param array $entities + * The template entities keyed by prefix. + * * @return string + * The processed text. */ public function replaceKeys($text, array $entities = array()): string { - $hasMatches = false; + // Register the replacements. + $this->resetReplacements(); + foreach ($entities as $type => $entity) { + $this->registerReplacements($type, $entity); + } + + // Replace the tokens. + do { + $result = preg_replace_callback('# + \[\[ + [ ]* + ([a-zA-Z][a-zA-Z0-9_]*) + : + ([a-zA-Z][a-zA-Z0-9_]*) + \( + [ ]* + ( + [^,\s]+ + (?:[ ]*,[ ]*[^,\s]+)* + )? + [ ]* + \) + [ ]* + \]\] + #x', [$this, 'doReplace'], $text); - // Loop over all entities - foreach ($entities as $entityPrefix => $entity) { - if (!$entity instanceof TemplateInterface) { - throw new TemplateException('This object doesn\'t implement the TemplateInterface'); + if ($result === $text) { + break; } - foreach ($entity::getTemplateReplacements() as $templateReplacementKey => $templateReplacementValue) { + $text = $result; + } while (true); - // Define the replacement arguments - $replacementArguments = []; + return $text; + } + + /** + * Replace a key match. + * + * @param array $matches + * The replacement matches. + * + * @return string + * The replacement text. + */ + protected function doReplace(array $matches): string + { + // Use readable variables names. + $matches[] = null; + list ($original, $type, $key, $params) = $matches; - preg_match('#\((.*?)\)#', $templateReplacementKey, $match); - if (isset($match[1]) && $match[1] != '') { - $replacementArguments = explode(',', $match[1]); + if (isset($this->replacements[$type][$key])) { + // Get the replacement. + $replacement = $this->replacements[$type][$key]; + + // Prepare the parameters. + if (!$replacement['params'] || $params === '') { + $params = []; + } else { + $params = explode(',', str_replace(' ', '', $params)); + $count1 = count($params); + $count2 = count($replacement['params']); + + // Ensure both arrays have the same number of parameters. + if ($count1 > $count2) { + $params = array_slice($params, 0, $count2); + } elseif ($count2 > $count1) { + $params = array_merge($params, array_fill(0, ($count2 - $count1), null)); } - // Complete the pattern and escape all existing special characters - $pattern = '[[ ' . $entityPrefix . ':' . $templateReplacementKey . ' ]]'; - $pattern = str_replace(['(', ')', '[', ']'], ['\(', '\)', '\[', '\]'], $pattern); + // Create an associative array. + $params = array_combine($replacement['params'], $params); + } + + $result = $replacement['object']; - // Get all the arguments out of the pattern so we can match them with the real arguments - foreach ($replacementArguments as $replacementArgument) { - $pattern = str_replace($replacementArgument, '([^)]*)', $pattern); + foreach ($replacement['callbacks'] as $callback => $callbackParams) { + // Get the parameters. + foreach ($callbackParams as $name => &$value) { + if (array_key_exists($name, $params)) { + $value = $params[$name]; + } } - // Check if the pattern exists in our text - $hasMatch = preg_match('/' . $pattern . '/', $text, $matches); + // Execute the callback. + $result = call_user_func_array([$result, $callback], $callbackParams); + } + + return $result; + } - // If we have a match for the pattern we substitute it - if ($hasMatch) { - $hasMatches = true; + return $original; + } - // The value can be called recursive - $passingValue = $entity; + /** + * Reset the replacements. + */ + protected function resetReplacements() + { + if ($this->replacements === null) { + $this->replacements = []; + $this->registerReplacements('token', $this->tokenService); + } else { + $this->replacements = [ + 'token' => $this->replacements['token'], + ]; + } + } - // Get a key value pair of all arguments - foreach ($replacementArguments as $key => $value) { - $replacementArguments[$value] = $matches[$key + 1]; - } + /** + * Register new replacements. + * + * @param string $type + * The replacement type. + * @param TemplateInterface|TokenService $object + * The object to use. + * @param array $replacements + * Array of replacements, leave null to get them from the object. + */ + protected function registerReplacements(string $type, $object, array $replacements = null) + { + // Initialize the replacements. + if ($this->replacements === null) { + $this->resetReplacements(); + } - // Get all functions that should be executed - $functions = explode('.', $templateReplacementValue); + // Get the default replacements. + if ($replacements === null) { + if ($object instanceof TemplateInterface) { + $replacements = $object::getTemplateReplacements(); + } elseif ($object instanceof TokenService) { + $replacements = $object->getTemplateReplacements(); + } else { + throw new TemplateException("The object doesn't specify default replacements."); + } + } + + foreach ($replacements as $replacementKey => $replacementValueCallback) { + // Extract the key and parameters. + if (!preg_match('#^ + ([a-zA-Z][a-zA-Z0-9_]*) + \( + [ ]* + ( + [a-zA-Z][a-zA-Z0-9_]* + (?:[ ]*,[ ]*[a-zA-Z][a-zA-Z0-9_]*)* + )? + [ ]* + \) + $#x', $replacementKey, $matches)) { + continue; + } - // Execute these functions on the defined entity with the discovered arguments - foreach ($functions as $function) { - preg_match('/^([a-zA-Z]*)(\((.*)\))?/', $function, $result); + $key = $matches[1]; - $functionArguments = []; - $methodName = $result[1]; - // Get the arguments and replace them by the real values if they are present - if (isset($result[3]) && $result[3] != '') { - $functionArguments = explode(',', $result[3]); - foreach ($functionArguments as $key => $value) { - $functionArguments[$key] = $replacementArguments[$value]; - } - } + // Prepare the parameters. + if (isset($matches[2])) { + $keyParams = explode(',', str_replace(' ', '', $matches[2])); + } else { + $keyParams = []; + } + + // Extract the callbacks. + $callbacks = []; + $replacementValueCallback = explode('.', $replacementValueCallback); + foreach ($replacementValueCallback as $callback) { + // Extract the method and parameters. + if (!preg_match('#^ + ([a-zA-Z][a-zA-Z0-9_]*) + \( + [ ]* + ( + [a-zA-Z][a-zA-Z0-9_]* + (?:[ ]*,[ ]*[a-zA-Z][a-zA-Z0-9_]*)* + )? + [ ]* + \) + $#x', $callback, $matches)) { + continue 2; + } - $passingValue = call_user_func_array(array($passingValue, $methodName), $functionArguments); + // Prepare the parameters. + if (isset($matches[2])) { + $params = explode(',', str_replace(' ', '', $matches[2])); + + if (array_diff($params, $keyParams)) { + throw new TemplateException('The replacement value callback uses unknown parameters.'); } - // Replace the pattern with the found value - $text = preg_replace('/' . $pattern . '/', $passingValue, $text); + $params = array_fill_keys($params, null); + } else { + $params = []; } + + $callbacks[$matches[1]] = $params; } - } - // Recursivly go trough this function until no matches are found - if ($hasMatches) { - $text = $this->replaceKeys($text, $entities); + // Add the replacement. + $this->replacements[$type][$key] = [ + 'params' => $keyParams, + 'callbacks' => $callbacks, + 'object' => $object, + ]; } - - return $text; } + } diff --git a/Service/TokenService.php b/Service/TokenService.php new file mode 100644 index 0000000..66e82e2 --- /dev/null +++ b/Service/TokenService.php @@ -0,0 +1,67 @@ +repository = $entityManager->getRepository(Token::class); + $this->caseTransformer = new CaseTransformer(new SnakeCase(), new StudlyCaps()); + } + + public function getTemplateReplacements(): array + { + $tokens = $this->repository->findAll(); + $replacements = []; + foreach ($tokens as $token) { + $replacements[$token->getName() . '()'] = 'get' . $this->caseTransformer->transform($token->getName()) . '()'; + } + + return $replacements; + } + + public function __call(string $name, array $arguments) + { + if (strpos($name, 'get') !== 0) { + throw new \BadMethodCallException('Call to undefined method ' . static::class . '::' . $name); + } + + $replacements = $this->getTemplateReplacements(); + $tokenName = array_search($name . '()', $replacements); + if ($tokenName === false) { + throw new \BadMethodCallException('Call to undefined method ' . static::class . '::' . $name); + } + $tokenName = substr($tokenName, 0, -2); + + $token = $this->repository->findOneBy(['name' => $tokenName]); + if (!$token) { + throw new \BadMethodCallException('Token ' . $tokenName . ' not found'); + } + + return $token->getValue(); + } +} diff --git a/Tests/CLI/DefaultCliFactoryTest.php b/Tests/CLI/DefaultCliFactoryTest.php new file mode 100644 index 0000000..efc5abe --- /dev/null +++ b/Tests/CLI/DefaultCliFactoryTest.php @@ -0,0 +1,68 @@ +keyCreated = true; + } + } + + protected function tearDown() + { + parent::tearDown(); + if ($this->keyCreated) { + unlink(rtrim(Path::getHomeDirectory(), '/') . '/.ssh/id_rsa'); + } + } + + public function testCreate() + { + // We have untestable code in DefaultCliFactory since the ssh client + // can't me mocked and it would be serious overkill to abstract it to + // yet another factory just so we can mock it here. + $factory = new DefaultCliFactory(); + $appEnv = $this->getMockBuilder(ApplicationEnvironment::class)->getMock(); + $env = $this->getMockBuilder(Environment::class)->getMock(); + $app = $this->getMockBuilder(AbstractApplication::class)->getMock(); + $appEnv->expects($this->once())->method('getEnvironment')->willReturn($env); + $appEnv->expects($this->once())->method('getApplication')->willReturn($app); + + $server = $this->getMockBuilder(VirtualServer::class)->getMock(); + $server->expects($this->once())->method('isTaskServer')->willreturn(false); + + $env->expects($this->once())->method('getVirtualServers')->willReturn(new ArrayCollection([$server])); + $this->assertNull($factory->create($appEnv)); + } + + /** + * @expectedException \InvalidArgumentException + */ + public function testCreateUnsuppoerted() + { + $factory = new DefaultCliFactory(); + $object = $this->getMockBuilder('\stdClass')->getMock(); + $factory->create($object); + } + +} diff --git a/Tests/CLI/RemoteCliTest.php b/Tests/CLI/RemoteCliTest.php new file mode 100644 index 0000000..8aff8d7 --- /dev/null +++ b/Tests/CLI/RemoteCliTest.php @@ -0,0 +1,33 @@ +getMockBuilder(SSH2::class)->disableOriginalConstructor()->getMock(); + $dir = '/some/dir'; + $execute = 'some command'; + $output = 'some output'; + $command = CommandBuilder::create('cd')->addFlag('P')->addArgument($dir)->onSuccess($execute)->getCommand(); + $connection + ->expects($this->at(0)) + ->method('exec') + ->with($command) + ->willReturn($output); + $connection + ->expects($this->at(1)) + ->method('getExitStatus') + ->willReturn(0); + $cli = new RemoteCli($connection, $dir); + $this->assertEquals(true, $cli->execute(CommandBuilder::create($execute))); + $this->assertEquals("Executing $command\n$output", $cli->getLastOutput()); + } +} diff --git a/Tests/Command/AbstractCommandTest.php b/Tests/Command/AbstractCommandTest.php deleted file mode 100644 index e62d0d1..0000000 --- a/Tests/Command/AbstractCommandTest.php +++ /dev/null @@ -1,102 +0,0 @@ -getMockBuilder(ContainerInterface::class) - ->disableOriginalConstructor() - ->getMock(); - - $mock - ->expects($this->at(0)) - ->method('get') - ->with($this->equalTo('doctrine.orm.default_entity_manager')) - ->willReturn($entityManager); - - $mock - ->expects($this->at(1)) - ->method('get') - ->with($this->equalTo('event_dispatcher')) - ->willReturn($eventDispatcher); - - return $mock; - } - - protected function getInputInterfaceMock() - { - $mock = $this - ->getMockBuilder(InputInterface::class) - ->disableOriginalConstructor() - ->getMock(); - - return $mock; - } - - protected function getOutputInterfaceMock() - { - $mock = $this - ->getMockBuilder(OutputInterface::class) - ->disableOriginalConstructor() - ->getMock(); - - return $mock; - } - - protected function getEntityManagerMock($taskRepository) - { - $mock = $this - ->getMockBuilder(EntityManagerInterface::class) - ->disableOriginalConstructor() - ->getMock(); - - $mock - ->expects($this->at(0)) - ->method('getRepository') - ->with($this->equalTo(Task::class)) - ->willReturn($taskRepository); - - return $mock; - } - - protected function getEventDispatcherMock() - { - $mock = $this - ->getMockBuilder(EventDispatcher::class) - ->disableOriginalConstructor() - ->getMock(); - - return $mock; - } - - protected function getTaskRepositoryMock($type, $task) - { - $mock = $this - ->getMockBuilder(TaskRepository::class) - ->disableOriginalConstructor() - ->getMock(); - - $mock - ->expects($this->at(0)) - ->method('getNextTask') - ->with($this->equalTo($type)) - ->willReturn($task); - - return $mock; - } -} - diff --git a/Tests/Command/BuildCommandTest.php b/Tests/Command/BuildCommandTest.php deleted file mode 100644 index 2e9a943..0000000 --- a/Tests/Command/BuildCommandTest.php +++ /dev/null @@ -1,43 +0,0 @@ -getTaskRepositoryMock(Task::TYPE_BUILD, null); - - $entityManager = $this->getEntityManagerMock($taskRepository); - $eventDispatcher = $this->getEventDispatcherMock(); - - $container = $this->getContainerMock($entityManager, $eventDispatcher); - $inputInterface = $this->getInputInterfaceMock(); - $outputInterface = $this->getOutputInterfaceMock(); - - $buildCommand = new BuildCommand(); - $buildCommand->setContainer($container); - $buildCommand->execute($inputInterface, $outputInterface); - } - - public function testExecuteWithTask() - { - $taskRepository = $this->getTaskRepositoryMock(Task::TYPE_BUILD, new Task()); - - $entityManager = $this->getEntityManagerMock($taskRepository); - $eventDispatcher = $this->getEventDispatcherMock(); - - $container = $this->getContainerMock($entityManager, $eventDispatcher); - $inputInterface = $this->getInputInterfaceMock(); - $outputInterface = $this->getOutputInterfaceMock(); - - $buildCommand = new BuildCommand(); - $buildCommand->setContainer($container); - $buildCommand->execute($inputInterface, $outputInterface); - } -} diff --git a/Tests/Command/DestroyCommandTest.php b/Tests/Command/DestroyCommandTest.php deleted file mode 100644 index e1f5f78..0000000 --- a/Tests/Command/DestroyCommandTest.php +++ /dev/null @@ -1,43 +0,0 @@ -getTaskRepositoryMock(Task::TYPE_DESTROY, null); - - $entityManager = $this->getEntityManagerMock($taskRepository); - $eventDispatcher = $this->getEventDispatcherMock(); - - $container = $this->getContainerMock($entityManager, $eventDispatcher); - $inputInterface = $this->getInputInterfaceMock(); - $outputInterface = $this->getOutputInterfaceMock(); - - $buildCommand = new DestroyCommand(); - $buildCommand->setContainer($container); - $buildCommand->execute($inputInterface, $outputInterface); - } - - public function testExecuteWithTask() - { - $taskRepository = $this->getTaskRepositoryMock(Task::TYPE_DESTROY, new Task()); - - $entityManager = $this->getEntityManagerMock($taskRepository); - $eventDispatcher = $this->getEventDispatcherMock(); - - $container = $this->getContainerMock($entityManager, $eventDispatcher); - $inputInterface = $this->getInputInterfaceMock(); - $outputInterface = $this->getOutputInterfaceMock(); - - $buildCommand = new DestroyCommand(); - $buildCommand->setContainer($container); - $buildCommand->execute($inputInterface, $outputInterface); - } -} diff --git a/Tests/DependencyInjection/Compiler/CacheClearProviderCompilerPassTest.php b/Tests/DependencyInjection/Compiler/CacheClearProviderCompilerPassTest.php new file mode 100644 index 0000000..92fcfd2 --- /dev/null +++ b/Tests/DependencyInjection/Compiler/CacheClearProviderCompilerPassTest.php @@ -0,0 +1,49 @@ +getMockBuilder(ContainerBuilder::class)->disableOriginalConstructor()->getMock(); + $container->expects($this->once())->method('has')->with(CacheClearProvider::class)->willReturn(false); + $pass = new CacheClearProviderCompilerPass(); + $this->assertNull($pass->process($container)); + } + + public function testProvider() + { + $container = $this->getMockBuilder(ContainerBuilder::class)->disableOriginalConstructor()->getMock(); + $container->expects($this->once())->method('has')->with(CacheClearProvider::class)->willReturn(true); + + $id = 'my_service_id'; + + $container->expects($this->once()) + ->method('findTaggedServiceIds') + ->with('domainator.cacheclearer') + ->willReturn([ + $id => [ + ['for' => '\stdClass'] + ] + ]); + + $definition = $this->getMockBuilder(Definition::class)->disableOriginalConstructor()->getMock(); + $definition->expects($this->once())->method('addMethodCall')->with( + 'registerCacheClearer', + $this->callback(function (array $args) use ($id) { + return ($args[0] instanceof Reference) && (string)$args[0] == $id && $args[1] == '\stdClass'; + }) + ); + + $container->expects($this->once())->method('getDefinition')->with(CacheClearProvider::class)->willReturn($definition); + $pass = new CacheClearProviderCompilerPass(); + $pass->process($container); + } +} diff --git a/Tests/DependencyInjection/Compiler/CliFactoryProviderCompilerPassTest.php b/Tests/DependencyInjection/Compiler/CliFactoryProviderCompilerPassTest.php new file mode 100644 index 0000000..cf40f4b --- /dev/null +++ b/Tests/DependencyInjection/Compiler/CliFactoryProviderCompilerPassTest.php @@ -0,0 +1,49 @@ +getMockBuilder(ContainerBuilder::class)->disableOriginalConstructor()->getMock(); + $container->expects($this->once())->method('has')->with(CliFactoryProvider::class)->willReturn(false); + $pass = new CliFactoryProviderCompilerPass(); + $this->assertNull($pass->process($container)); + } + + public function testProvider() + { + $container = $this->getMockBuilder(ContainerBuilder::class)->disableOriginalConstructor()->getMock(); + $container->expects($this->once())->method('has')->with(CliFactoryProvider::class)->willReturn(true); + + $id = 'my_service_id'; + + $container->expects($this->once()) + ->method('findTaggedServiceIds') + ->with('domainator.clifactory') + ->willReturn([ + $id => [ + ['for' => '\stdClass'] + ] + ]); + + $definition = $this->getMockBuilder(Definition::class)->disableOriginalConstructor()->getMock(); + $definition->expects($this->once())->method('addMethodCall')->with( + 'registerCliFactory', + $this->callback(function (array $args) use ($id) { + return ($args[0] instanceof Reference) && (string)$args[0] == $id && $args[1] == '\stdClass'; + }) + ); + + $container->expects($this->once())->method('getDefinition')->with(CliFactoryProvider::class)->willReturn($definition); + $pass = new CliFactoryProviderCompilerPass(); + $pass->process($container); + } +} diff --git a/Tests/Entity/AbstractApplicationTest.php b/Tests/Entity/AbstractApplicationTest.php index 4491da3..3ea35aa 100644 --- a/Tests/Entity/AbstractApplicationTest.php +++ b/Tests/Entity/AbstractApplicationTest.php @@ -15,7 +15,22 @@ class AbstractApplicationTest extends TestCase public function testGetTemplateReplacements() { $expected = [ + 'name()' => 'getName()', 'nameCanonical()' => 'getNameCanonical()', + 'gitRepo()' => 'getGitRepo()', + 'config(key)' => 'getConfig(key)', + 'applicationEnvironmentDatabaseName(name)' => 'getApplicationEnvironmentByEnvironmentName(name).getDatabaseName()', + 'applicationEnvironmentEnvironmentName(name)' => 'getApplicationEnvironmentByEnvironmentName(name).getEnvironmentName()', + 'applicationEnvironmentEnvironmentGitRef(name)' => 'getApplicationEnvironmentByEnvironmentName(name).getEnvironment().getGitRef()', + 'applicationEnvironmentEnvironmentConfig(name,key)' => 'getApplicationEnvironmentByEnvironmentName(name).getEnvironment().getConfig(key)', + 'applicationEnvironmentEnvironmentPriority(name)' => 'getApplicationEnvironmentByEnvironmentName(name).getEnvironment().getPriority()', + 'applicationEnvironmentDatabaseUser(name)' => 'getApplicationEnvironmentByEnvironmentName(name).getDatabaseUser()', + 'applicationEnvironmentDatabasePassword(name)' => 'getApplicationEnvironmentByEnvironmentName(name).getDatabasePassword()', + 'applicationEnvironmentGitRef(name)' => 'getApplicationEnvironmentByEnvironmentName(name).getGitRef()', + 'applicationEnvironmentDomain(name)' => 'getApplicationEnvironmentByEnvironmentName(name).getDomain()', + 'applicationEnvironmentServerIps(name)' => 'getApplicationEnvironmentByEnvironmentName(name).getServerIps()', + 'applicationEnvironmentWorkerServerIp(name)' => 'getApplicationEnvironmentByEnvironmentName(name).getWorkerServerIp()', + 'applicationEnvironmentConfig(name,key)' => 'getApplicationEnvironmentByEnvironmentName(name).getConfig(key)', 'serverIps(dev_environment_name)' => 'getApplicationEnvironmentByEnvironmentName(dev_environment_name).getServerIps()', ]; @@ -46,7 +61,7 @@ public function testGettersAndSetters() $application->setName('My application name'); $this->assertEquals('My application name', $application->getName()); - $this->assertEquals('myapplicatio', $application->getNameCanonical()); + $this->assertEquals('myapplicationn', $application->getNameCanonical()); $this->assertTrue($application->isHasDatabase()); $application->setHasDatabase(false); @@ -64,7 +79,7 @@ public function testGettersAndSetters() $this->assertCount(0,$application->getApplicationEnvironments()); $this->assertNull($application->getId()); - + $application->setDeleted(true); $this->assertTrue($application->isDeleted()); } diff --git a/Tests/Entity/ApplicationEnvironmentTest.php b/Tests/Entity/ApplicationEnvironmentTest.php index e1e0545..4d93f4f 100644 --- a/Tests/Entity/ApplicationEnvironmentTest.php +++ b/Tests/Entity/ApplicationEnvironmentTest.php @@ -23,12 +23,22 @@ public function testTemplateReplacements() { $expected = [ 'serverIps()' => 'getServerIps()', - 'environmentName()' => 'getEnvironment().getName()', + 'workerServerIp()' => 'getWorkerServerIp()', + 'environmentName()' => 'getEnvironmentName()', + 'applicationName()' => 'getApplication().getName()', + 'applicationNameCanonical()' => 'getApplication().getNameCanonical()', + 'applicationGitRepo()' => 'getApplication().getGitRepo()', + 'applicationServerIps(dev_environment_name)' => 'getApplication().getApplicationEnvironmentByEnvironmentName(dev_environment_name).getServerIps()', + 'environmentGitRef()' => 'getEnvironment().getGitRef()', + 'environmentConfig(key)' => 'getEnvironment().getConfig(key)', + 'environmentPriority()' => 'getEnvironment().getPriority()', 'config(key)' => 'getConfig(key)', 'databaseName()' => 'getDatabaseName()', 'databaseUser()' => 'getDatabaseUser()', 'databasePassword()' => 'getDatabasePassword()', 'gitRef()' => 'getGitRef()', + 'domain()' => 'getDomain()', + 'applicationConfig(key)' => 'getApplication().getConfig(key)' ]; $this->assertEquals($expected, ApplicationEnvironment::getTemplateReplacements()); diff --git a/Tests/Entity/TokenTest.php b/Tests/Entity/TokenTest.php new file mode 100644 index 0000000..a83d8b0 --- /dev/null +++ b/Tests/Entity/TokenTest.php @@ -0,0 +1,23 @@ +assertSame($token, $token->setName($name)); + $this->assertSame($token, $token->setValue($value)); + $this->assertEquals($token->getName(), $name); + $this->assertEquals($token->getValue(), $value); + } + +} diff --git a/Tests/EventListener/BuildEventListenerTest.php b/Tests/EventListener/BuildEventListenerTest.php deleted file mode 100644 index 5adc7b0..0000000 --- a/Tests/EventListener/BuildEventListenerTest.php +++ /dev/null @@ -1,62 +0,0 @@ -getTaskLoggerServiceMock(); - $taskLoggerService - ->expects($this->at(0)) - ->method('setTask'); - - $entityManager = $this->getEntityManagerMock(); - - $buildEventListener = new BuildEventListener($taskLoggerService,$entityManager); - $buildEventListener->onStart(new BuildEvent(new Task())); - } - - public function testOnEnd(){ - $taskLoggerService = $this->getTaskLoggerServiceMock(); - $entityManager = $this->getEntityManagerMock(); - - $buildEventListener = new BuildEventListener($taskLoggerService,$entityManager); - $buildEventListener->onEnd(new BuildEvent(new Task())); - } - - public function getTaskLoggerServiceMock(){ - $mock = $this - ->getMockBuilder(TaskLoggerService::class) - ->disableOriginalConstructor() - ->getMock(); - - return $mock; - } - - public function getEntityManagerMock(){ - $mock = $this - ->getMockBuilder(EntityManagerInterface::class) - ->disableOriginalConstructor() - ->getMock(); - - $mock - ->expects($this->at(0)) - ->method('persist'); - - $mock - ->expects($this->at(1)) - ->method('flush'); - - return $mock; - } - -} \ No newline at end of file diff --git a/Tests/EventListener/DestroyEventListenerTest.php b/Tests/EventListener/DestroyEventListenerTest.php deleted file mode 100644 index 5985b4e..0000000 --- a/Tests/EventListener/DestroyEventListenerTest.php +++ /dev/null @@ -1,64 +0,0 @@ -getTaskLoggerServiceMock(); - $taskLoggerService - ->expects($this->at(0)) - ->method('setTask'); - - $entityManager = $this->getEntityManagerMock(); - - $buildEventListener = new DestroyEventListener($taskLoggerService,$entityManager); - $buildEventListener->onStart(new DestroyEvent(new Task())); - } - - public function testOnEnd(){ - $taskLoggerService = $this->getTaskLoggerServiceMock(); - $entityManager = $this->getEntityManagerMock(); - - $buildEventListener = new DestroyEventListener($taskLoggerService,$entityManager); - $buildEventListener->onEnd(new DestroyEvent(new Task())); - } - - public function getTaskLoggerServiceMock(){ - $mock = $this - ->getMockBuilder(TaskLoggerService::class) - ->disableOriginalConstructor() - ->getMock(); - - return $mock; - } - - public function getEntityManagerMock(){ - $mock = $this - ->getMockBuilder(EntityManagerInterface::class) - ->disableOriginalConstructor() - ->getMock(); - - $mock - ->expects($this->at(0)) - ->method('persist'); - - $mock - ->expects($this->at(1)) - ->method('flush'); - - return $mock; - } - -} \ No newline at end of file diff --git a/Tests/EventListener/EnvironmentEventListenerTest.php b/Tests/EventListener/EnvironmentEventListenerTest.php index 85a3bce..5ab49f0 100644 --- a/Tests/EventListener/EnvironmentEventListenerTest.php +++ b/Tests/EventListener/EnvironmentEventListenerTest.php @@ -39,28 +39,11 @@ public function testPostPersist() $applications = new ArrayCollection(); $applications->add(new QuuxApplication()); - $entityManager - ->expects($this->at(0)) - ->method('getRepository') - ->with($this->equalTo(AbstractApplication::class)) - ->willReturn( - $this->getRepositoryMock($applications) - ); - - $entityManager - ->expects($this->at(1)) - ->method('persist'); - - - $entityManager - ->expects($this->at(2)) - ->method('flush'); - $applicationTypes = new ArrayCollection(); $applicationTypes->add(new ApplicationType()); $entityManager - ->expects($this->at(3)) + ->expects($this->at(0)) ->method('getRepository') ->with($this->equalTo(ApplicationType::class)) ->willReturn( @@ -68,12 +51,12 @@ public function testPostPersist() ); $entityManager - ->expects($this->at(4)) + ->expects($this->at(1)) ->method('persist'); $entityManager - ->expects($this->at(5)) + ->expects($this->at(2)) ->method('flush'); $args = $this->getLifecycleEventArgsMock($entity, $entityManager); diff --git a/Tests/Fixtures/Entity/Foo.php b/Tests/Fixtures/Entity/Foo.php index 83db261..1addd9e 100644 --- a/Tests/Fixtures/Entity/Foo.php +++ b/Tests/Fixtures/Entity/Foo.php @@ -5,11 +5,13 @@ use DigipolisGent\Domainator9k\CoreBundle\Entity\TemplateInterface; use DigipolisGent\Domainator9k\CoreBundle\Entity\Traits\IdentifiableTrait; +use DigipolisGent\Domainator9k\CoreBundle\Entity\Traits\TemplateImplementationTrait; class Foo implements TemplateInterface { use IdentifiableTrait; + use TemplateImplementationTrait; private $primaryTitle; @@ -17,13 +19,11 @@ class Foo implements TemplateInterface private $qux; - public static function getTemplateReplacements(): array + public static function additionalTemplateReplacements(): array { return [ 'primary()' => 'getPrimaryTitle()', 'second()' => 'getSecondTitle()', - 'quxTitle()' => 'getQux().getTitle()', - 'quxSubtitle()' => 'getQux().getSubtitle()', 'multiply(a,b)' => 'multiplyNumbers(a,b)', ]; } @@ -31,7 +31,7 @@ public static function getTemplateReplacements(): array /** * @return mixed */ - public function getPrimaryTitle() + public function getPrimaryTitle(): string { return $this->primaryTitle; } @@ -39,7 +39,7 @@ public function getPrimaryTitle() /** * @param mixed $primaryTitle */ - public function setPrimaryTitle($primaryTitle) + public function setPrimaryTitle(string $primaryTitle) { $this->primaryTitle = $primaryTitle; } @@ -47,7 +47,7 @@ public function setPrimaryTitle($primaryTitle) /** * @return mixed */ - public function getSecondTitle() + public function getSecondTitle(): string { return $this->secondTitle; } @@ -55,7 +55,7 @@ public function getSecondTitle() /** * @param mixed $secondTitle */ - public function setSecondTitle($secondTitle) + public function setSecondTitle(string $secondTitle) { $this->secondTitle = $secondTitle; } @@ -63,7 +63,7 @@ public function setSecondTitle($secondTitle) /** * @return mixed */ - public function getQux() + public function getQux(): Qux { return $this->qux; } @@ -76,7 +76,7 @@ public function setQux(Qux $qux) $this->qux = $qux; } - public function multiplyNumbers($a, $b) + public function multiplyNumbers(int $a, int $b): int { return $a * $b; } diff --git a/Tests/Fixtures/Entity/Qux.php b/Tests/Fixtures/Entity/Qux.php index ac93b3f..3a21150 100644 --- a/Tests/Fixtures/Entity/Qux.php +++ b/Tests/Fixtures/Entity/Qux.php @@ -5,28 +5,22 @@ use DigipolisGent\Domainator9k\CoreBundle\Entity\TemplateInterface; use DigipolisGent\Domainator9k\CoreBundle\Entity\Traits\IdentifiableTrait; +use DigipolisGent\Domainator9k\CoreBundle\Entity\Traits\TemplateImplementationTrait; class Qux implements TemplateInterface { use IdentifiableTrait; + use TemplateImplementationTrait; private $title; private $subtitle; - public static function getTemplateReplacements(): array - { - return [ - 'title()' => 'getTitle()', - 'subtitle()' => 'getSub()', - ]; - } - /** * @return mixed */ - public function getTitle() + public function getTitle(): string { return $this->title; } @@ -42,7 +36,7 @@ public function setTitle($title) /** * @return mixed */ - public function getSubtitle() + public function getSubtitle(): string { return $this->subtitle; } diff --git a/Tests/Form/Type/AbstractFormTypeTest.php b/Tests/Form/Type/AbstractFormTypeTest.php index 81e0be3..51556b8 100644 --- a/Tests/Form/Type/AbstractFormTypeTest.php +++ b/Tests/Form/Type/AbstractFormTypeTest.php @@ -3,6 +3,7 @@ namespace DigipolisGent\Domainator9k\CoreBundle\Tests\Form\Type; use DigipolisGent\SettingBundle\Service\FormService; +use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\OptionsResolver\OptionsResolver; @@ -10,6 +11,9 @@ abstract class AbstractFormTypeTest extends TestCase { + /** + * @return MockObject + */ protected function getFormBuilderMock() { $mock = $this @@ -20,6 +24,9 @@ protected function getFormBuilderMock() return $mock; } + /** + * @return MockObject + */ protected function getOptionsResolverMock() { $mock = $this @@ -30,6 +37,9 @@ protected function getOptionsResolverMock() return $mock; } + /** + * @return MockObject + */ protected function getFormServiceMock() { $mock = $this diff --git a/Tests/Form/Type/TaskFormTypeTest.php b/Tests/Form/Type/TaskFormTypeTest.php new file mode 100644 index 0000000..2172f34 --- /dev/null +++ b/Tests/Form/Type/TaskFormTypeTest.php @@ -0,0 +1,84 @@ +getOptionsResolverMock(); + + $optionsResolver + ->expects($this->at(0)) + ->method('setDefault') + ->with('data_class', Task::class); + $optionsResolver + ->expects($this->at(1)) + ->method('setDefault') + ->with('type', Task::TYPE_BUILD); + + $taskRunnerService = $this->getMockBuilder(TaskRunnerService::class) + ->disableOriginalConstructor() + ->getMock(); + + $formType = new TaskFormType($this->getFormServiceMock(), $taskRunnerService); + $formType->configureOptions($optionsResolver); + } + + public function testBuildForm() + { + $formBuilder = $this->getFormBuilderMock(); + + $taskRunnerService = $this->getMockBuilder(TaskRunnerService::class) + ->disableOriginalConstructor() + ->getMock(); + + $provisioners = []; + $choices = []; + foreach(range(0,5) as $index) { + $mock = $this->getMockBuilder(ProvisionerInterface::class)->getMock(); + $name = 'Provisioner' . $index; + $mock->expects($this->once())->method('getName')->willReturn($name); + $mock->expects($this->once())->method('isExecutedByDefault')->willReturn(false); + $provisioners[] = $mock; + $choices[$name] = get_class($mock); + } + + $taskRunnerService + ->expects($this->once()) + ->method('getBuildProvisioners') + ->willReturn($provisioners); + + $formBuilder + ->expects($this->at(0)) + ->method('add') + ->with( + 'provisioners', + ChoiceType::class, + [ + 'expanded' => true, + 'multiple' => true, + 'required' => false, + 'choices' => $choices, + 'data' => [], + 'empty_data' => [], + 'label' => 'Limit to following provisioners (selecting none will run the default provisioners)', + ] + ); + + $formBuilder + ->expects($this->at(1)) + ->method('addEventSubscriber'); + + $formType = new TaskFormType($this->getFormServiceMock(), $taskRunnerService); + $formType->buildForm($formBuilder, ['type' => Task::TYPE_BUILD]); + } +} diff --git a/Tests/Form/Type/TokenFormTypeTest.php b/Tests/Form/Type/TokenFormTypeTest.php new file mode 100644 index 0000000..78abec7 --- /dev/null +++ b/Tests/Form/Type/TokenFormTypeTest.php @@ -0,0 +1,52 @@ +getOptionsResolverMock(); + + $optionsResolver + ->expects($this->at(0)) + ->method('setDefault') + ->with('data_class', Token::class); + + $formType = new TokenFormType($this->getFormServiceMock()); + $formType->configureOptions($optionsResolver); + } + + public function testBuildForm() + { + $formBuilder = $this->getFormBuilderMock(); + + $arguments = [ + 'name', + 'value', + ]; + + $index = 0; + + foreach ($arguments as $argument) { + $formBuilder + ->expects($this->at($index)) + ->method('add') + ->with($argument); + + $index++; + } + + $formBuilder + ->expects($this->at($index)) + ->method('addEventSubscriber'); + + $formType = new TokenFormType($this->getFormServiceMock()); + $formType->buildForm($formBuilder, []); + } +} diff --git a/Tests/Provider/CacheClearProviderTest.php b/Tests/Provider/CacheClearProviderTest.php new file mode 100644 index 0000000..8e3b474 --- /dev/null +++ b/Tests/Provider/CacheClearProviderTest.php @@ -0,0 +1,35 @@ +getMockBuilder('\stdClass')->getMock(); + $class = get_class($object); + $clearer = $this->getMockBuilder(CacheClearerInterface::class)->getMock(); + $cacheClearProvider->registerCacheClearer($clearer, $class); + + $this->assertEquals($clearer, $cacheClearProvider->getCacheClearerFor($object)); + } + + /** + * @expectedException \DigipolisGent\Domainator9k\CoreBundle\Exception\NoCacheClearerFoundException + */ + public function testNoClearer() + { + $cacheClearProvider = new CacheClearProvider(); + $object = $this->getMockBuilder('\stdClass')->getMock(); + $class = get_class($object); + $cacheClearProvider->getCacheClearerFor($object); + } + +} diff --git a/Tests/Provider/CliFactoryProviderTest.php b/Tests/Provider/CliFactoryProviderTest.php new file mode 100644 index 0000000..92d0ee7 --- /dev/null +++ b/Tests/Provider/CliFactoryProviderTest.php @@ -0,0 +1,49 @@ +getMockBuilder('\stdClass')->getMock(); + $class = get_class($object); + $cli = $this->getMockBuilder(CliInterface::class)->getMock(); + $factory = $this->getMockBuilder(CliFactoryInterface::class)->getMock(); + $factory->expects($this->once())->method('create')->with($object)->willReturn($cli); + $cliFactoryProvider->registerCliFactory($factory, $class); + + $this->assertEquals($cli, $cliFactoryProvider->createCliFor($object)); + } + + /** + * @expectedException \DigipolisGent\Domainator9k\CoreBundle\Exception\NoCliFactoryFoundException + */ + public function testNoClearer() + { + $cacheClearProvider = new CliFactoryProvider(); + $object = $this->getMockBuilder('\stdClass')->getMock(); + $class = get_class($object); + $cacheClearProvider->createCliFor($object); + } + + public function testDefaultFactory() + { + $cli = $this->getMockBuilder(CliInterface::class)->getMock(); + $object = $this->getMockBuilder('\stdClass')->getMock(); + $factory = $this->getMockBuilder(CliFactoryInterface::class)->getMock(); + $factory->expects($this->once())->method('create')->with($object)->willReturn($cli); + $cliFactoryProvider = new CliFactoryProvider($factory); + + $this->assertEquals($cli, $cliFactoryProvider->createCliFor($object)); + } + +} diff --git a/Tests/Provisioner/CacheClearBuildProvisionerTest.php b/Tests/Provisioner/CacheClearBuildProvisionerTest.php new file mode 100644 index 0000000..649e9cd --- /dev/null +++ b/Tests/Provisioner/CacheClearBuildProvisionerTest.php @@ -0,0 +1,62 @@ +getMockBuilder(TaskLoggerService::class)->disableOriginalConstructor()->getMock(); + $provisioner = new CacheClearBuildProvisioner($cliFactoryProvider, $cacheClearProvider, $taskLoggerService); + $this->assertEquals('Clear caches', $provisioner->getName()); + } + + public function testRun() + { + $cliFactoryProvider = new CliFactoryProvider(); + $cacheClearProvider = new CacheClearProvider(); + + $application = $this->getMockBuilder(AbstractApplication::class)->getMock(); + $environment = $this->getMockBuilder(Environment::class)->getMock(); + + $appEnv = $this->getMockBuilder(ApplicationEnvironment::class)->getMock(); + $appEnv->expects($this->once())->method('getApplication')->willReturn($application); + $appEnv->expects($this->once())->method('getEnvironment')->willReturn($environment); + + $task = $this->getMockBuilder(Task::class)->getMock(); + $task->expects($this->once())->method('getApplicationEnvironment')->willReturn($appEnv); + + $cli = $this->getMockBuilder(CliInterface::class)->getMock(); + + $cliFactory = $this->getMockBuilder(CliFactoryInterface::class)->getMock(); + $cliFactory->expects($this->once())->method('create')->with($appEnv)->willReturn($cli); + + $cliFactoryProvider->registerCliFactory($cliFactory, get_class($appEnv)); + + $clearer = $this->getMockBuilder(CacheClearerInterface::class)->getMock(); + $clearer->expects($this->once())->method('clearCache')->with($appEnv, $cli)->willReturn(true); + + $cacheClearProvider->registerCacheClearer($clearer, get_class($application)); + + $taskLoggerService = $this->getMockBuilder(TaskLoggerService::class)->disableOriginalConstructor()->getMock(); + + $provisioner = new CacheClearBuildProvisioner($cliFactoryProvider, $cacheClearProvider, $taskLoggerService); + $provisioner->setTask($task); + $provisioner->run(); + } +} diff --git a/Tests/Service/TaskLoggerServiceTest.php b/Tests/Service/TaskLoggerServiceTest.php deleted file mode 100644 index 0d24d37..0000000 --- a/Tests/Service/TaskLoggerServiceTest.php +++ /dev/null @@ -1,39 +0,0 @@ -getEntityManagerMock(); - $loggerService = new TaskLoggerService($entityManager); - $task = new Task(); - $loggerService->setTask($task); - $loggerService->addLine('New log line'); - } - - private function getEntityManagerMock(){ - $mock = $this - ->getMockBuilder(EntityManagerInterface::class) - ->disableOriginalConstructor() - ->getMock(); - - $mock - ->expects($this->at(0)) - ->method('persist'); - - $mock - ->expects($this->at(0)) - ->method('flush'); - - return $mock; - } - -} diff --git a/Tests/Service/TaskRunnerServiceTest.php b/Tests/Service/TaskRunnerServiceTest.php new file mode 100644 index 0000000..6b3dcf0 --- /dev/null +++ b/Tests/Service/TaskRunnerServiceTest.php @@ -0,0 +1,388 @@ +task = new Task(); + $this->task->setType(Task::TYPE_BUILD); + $id = uniqid(); + $prop = new \ReflectionProperty($this->task, 'id'); + $prop->setAccessible(true); + $prop->setValue($this->task, $id); + + $this->entityManager = $this->getMockBuilder(EntityManagerInterface::class) + ->getMock(); + + $this->provisionService = $this->getMockBuilder(ProvisionService::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->logger = $this->getMockBuilder(TaskLoggerService::class) + ->disableOriginalConstructor() + ->getMock(); + + $this->entityManagerIndex = 0; + $this->provisionServiceIndex = 0; + } + + /** + * @expectedException \InvalidArgumentException + * @expectedExceptionMessageRegExp /Task "(.*)" cannot be restarted\./ + */ + public function testRunNotNew() + { + $this->task->setProcessed(); + $this->taskRunnerService = new TaskRunnerService([], [], $this->entityManager, $this->logger); + $this->taskRunnerService->run($this->task); + } + + public function testRunSuccessBuild() + { + $this->expectSuccessfulRun(); + $result = $this->taskRunnerService->run($this->task); + $this->assertTrue($result); + $this->assertTrue($this->task->isProcessed()); + } + + public function testRunSuccessDestroy() + { + $this->task->setType(Task::TYPE_DESTROY); + $this->expectSuccessfulRun(); + $result = $this->taskRunnerService->run($this->task); + $this->assertTrue($result); + $this->assertTrue($this->task->isProcessed()); + } + + public function testRunFailed() + { + $this->entityManager + ->expects($this->at($this->entityManagerIndex++)) + ->method('persist') + ->with($this->callback( + function (Task $task) + { + return $task->isInProgress(); + } + )); + + $this->entityManager + ->expects($this->at($this->entityManagerIndex++)) + ->method('flush'); + + $this->logger + ->expects($this->at(0)) + ->method('addLogMessage') + ->with($this->task, '', '', 0); + + $this->logger + ->expects($this->at(1)) + ->method('addFailedLogMessage') + ->with($this->task, 'Task run failed.', 0); + + $buildProvisioners = []; + foreach (range(0, 3) as $i) { + $mock = $this->getMockBuilder(ProvisionerInterface::class) + ->getMock(); + $mock->expects($this->once()) + ->method('setTask') + ->with($this->task) + ->willReturn(null); + $mock->expects($this->once()) + ->method('run') + ->willReturn(null); + $buildProvisioners[] = $mock; + } + $mock = $this->getMockBuilder(ProvisionerInterface::class) + ->getMock(); + $mock->expects($this->once()) + ->method('setTask') + ->with($this->task); + $mock->expects($this->once()) + ->method('run') + ->willReturnCallback(function () { + $this->task->setFailed(); + }); + $buildProvisioners[] = $mock; + + foreach (range(0, 2) as $i) { + $mock = $this->getMockBuilder(ProvisionerInterface::class) + ->getMock(); + $mock->expects($this->never()) + ->method('run'); + $buildProvisioners[] = $mock; + } + + $this->taskRunnerService = new TaskRunnerService( + $buildProvisioners, + [], + $this->entityManager, + $this->logger + ); + + $result = $this->taskRunnerService->run($this->task); + $this->assertFalse($result); + $this->assertTrue($this->task->isFailed()); + } + + public function testRunCancelled() + { + $this->entityManager + ->expects($this->at($this->entityManagerIndex++)) + ->method('persist') + ->with($this->callback( + function (Task $task) + { + return $task->isInProgress(); + } + )); + + $this->entityManager + ->expects($this->at($this->entityManagerIndex++)) + ->method('flush'); + + $this->entityManager + ->expects($this->at($this->entityManagerIndex++)) + ->method('persist') + ->with($this->callback( + function (Task $task) + { + return $task->isCancelled(); + } + )); + + $this->entityManager + ->expects($this->at($this->entityManagerIndex++)) + ->method('flush'); + + $buildProvisioners = []; + foreach (range(0, 5) as $i) { + $mock = $this->getMockBuilder(ProvisionerInterface::class) + ->getMock(); + $mock->expects($this->once()) + ->method('setTask') + ->with($this->task) + ->willReturn(null); + $mock->expects($this->once()) + ->method('run') + ->willReturn(null); + $buildProvisioners[] = $mock; + } + + $mock = $this->getMockBuilder(ProvisionerInterface::class) + ->getMock(); + $mock->expects($this->once()) + ->method('setTask') + ->with($this->task) + ->willReturn(null); + $mock->expects($this->once()) + ->method('run') + ->willReturnCallback( + function () + { + $this->task->setCancelled(); + } + ); + $buildProvisioners[] = $mock; + + $this->taskRunnerService = new TaskRunnerService( + $buildProvisioners, + [], + $this->entityManager, + $this->logger + ); + $result = $this->taskRunnerService->run($this->task); + $this->assertFalse($result); + $this->assertTrue($this->task->isCancelled()); + } + + public function testRunNext() + { + $repository = $this->getMockBuilder(TaskRepository::class) + ->disableOriginalConstructor() + ->getMock(); + $repository->expects($this->at(0)) + ->method('getNextTask') + ->with(Task::TYPE_BUILD) + ->willReturn($this->task); + $this->entityManager + ->expects($this->at($this->entityManagerIndex++)) + ->method('getRepository') + ->with(Task::class) + ->willReturn($repository); + $this->expectSuccessfulRun(); + $result = $this->taskRunnerService->runNext(Task::TYPE_BUILD); + $this->assertTrue($result); + $this->assertTrue($this->task->isProcessed()); + } + + public function testCancel() + { + $this->logger + ->expects($this->at(0)) + ->method('addInfoLogMessage') + ->with($this->task, 'Task run cancelled.'); + $this->taskRunnerService = new TaskRunnerService( + [], + [], + $this->entityManager, + $this->logger + ); + $this->taskRunnerService->cancel($this->task); + + $this->assertTrue($this->task->isCancelled()); + } + + /** + * @expectedException \InvalidArgumentException + * @expectedExceptionMessageRegExp /Task (.*) cannot be cancelled\./ + */ + public function testCancelRunning() + { + $this->task->setInProgress(); + $this->taskRunnerService = new TaskRunnerService( + [], + [], + $this->entityManager, + $this->logger + ); + $this->taskRunnerService->cancel($this->task); + } + + protected function expectSuccessfulRun() + { + $this->entityManager + ->expects($this->at($this->entityManagerIndex++)) + ->method('persist') + ->with($this->callback( + function (Task $task) + { + return $task->isInProgress(); + } + )); + + $this->entityManager + ->expects($this->at($this->entityManagerIndex++)) + ->method('flush'); + + $this->logger + ->expects($this->at(0)) + ->method('addLogMessage') + ->with($this->task, '', '', 0); + + $this->logger + ->expects($this->at(1)) + ->method('addSuccessLogMessage') + ->with($this->task, 'Task run completed.', 0); + + $buildProvisioners = []; + $destroyProvisioners = []; + switch ($this->task->getType()) { + case Task::TYPE_BUILD: + foreach (range(0, 5) as $i) { + $mock = $this->getMockBuilder(ProvisionerInterface::class) + ->getMock(); + $mock->expects($this->once()) + ->method('setTask') + ->with($this->task) + ->willReturn(null); + $mock->expects($this->once()) + ->method('run') + ->willReturn(null); + $buildProvisioners[] = $mock; + } + + foreach (range(0, 5) as $i) { + $mock = $this->getMockBuilder(ProvisionerInterface::class) + ->getMock(); + $mock->expects($this->never()) + ->method('run'); + $destroyProvisioners[] = $mock; + } + break; + case Task::TYPE_DESTROY: + + $buildProvisioners = []; + foreach (range(0, 5) as $i) { + $mock = $this->getMockBuilder(ProvisionerInterface::class) + ->getMock(); + $mock->expects($this->never()) + ->method('run'); + $buildProvisioners[] = $mock; + } + + $destroyProvisioners = []; + foreach (range(0, 5) as $i) { + $mock = $this->getMockBuilder(ProvisionerInterface::class) + ->getMock(); + $mock->expects($this->once()) + ->method('setTask') + ->with($this->task) + ->willReturn(null); + $mock->expects($this->once()) + ->method('run') + ->with() + ->willReturn(null); + $destroyProvisioners[] = $mock; + } + break; + } + + $this->taskRunnerService = new TaskRunnerService( + $buildProvisioners, + $destroyProvisioners, + $this->entityManager, + $this->logger + ); + } +} diff --git a/Tests/Service/TestTemplateServiceTest.php b/Tests/Service/TemplateServiceTest.php similarity index 52% rename from Tests/Service/TestTemplateServiceTest.php rename to Tests/Service/TemplateServiceTest.php index 3130563..2550b81 100644 --- a/Tests/Service/TestTemplateServiceTest.php +++ b/Tests/Service/TemplateServiceTest.php @@ -3,22 +3,54 @@ namespace DigipolisGent\Domainator9k\CoreBundle\Tests\Service; - +use DigipolisGent\Domainator9k\CoreBundle\Entity\Token; use DigipolisGent\Domainator9k\CoreBundle\Service\TemplateService; +use DigipolisGent\Domainator9k\CoreBundle\Service\TokenService; use DigipolisGent\Domainator9k\CoreBundle\Tests\Fixtures\Entity\Bar; use DigipolisGent\Domainator9k\CoreBundle\Tests\Fixtures\Entity\Foo; use DigipolisGent\Domainator9k\CoreBundle\Tests\Fixtures\Entity\Qux; +use Doctrine\ORM\EntityManager; +use Doctrine\ORM\EntityRepository; use PHPUnit\Framework\TestCase; class TemplateServiceTest extends TestCase { + protected $token; + protected $tokenService; + protected $repository; + + protected function setUp() + { + parent::setUp(); + $token = new Token(); + $token->setName(substr(str_shuffle("abcdefghijklmnopqrstuvwxyz"), 0, 10)); + $token->setValue(substr(str_shuffle("abcdefghijklmnopqrstuvwxyz"), 0, 10)); + $this->token = $token; + $this->repository = $this + ->getMockBuilder(EntityRepository::class) + ->disableOriginalConstructor() + ->getMock(); + $this->repository->expects($this->any())->method('findAll')->willReturn([$token]); + $this->repository->expects($this->any())->method('findOneBy')->with(['name' => $token->getName()])->willReturn($token); + $this->entityManager = $this + ->getMockBuilder(EntityManager::class) + ->disableOriginalConstructor() + ->getMock(); + $this->entityManager + ->expects($this->once()) + ->method('getRepository') + ->with(Token::class) + ->willReturn($this->repository); + $this->tokenService = new TokenService($this->entityManager); + } + /** * @expectedException \DigipolisGent\Domainator9k\CoreBundle\Exception\TemplateException */ public function testReplaceKeysWithInvalidEntity() { - $templateService = new TemplateService(); + $templateService = new TemplateService($this->tokenService); $text = <<tokenService); + $name = $this->token->getName(); + $value = $this->token->getValue(); $text = <<assertEquals($expected, $actual); @@ -62,7 +97,9 @@ public function testReplaceKeysWithValidEntity() public function testReplaceKeysRecursively() { - $templateService = new TemplateService(); + $templateService = new TemplateService($this->tokenService); + $name = $this->token->getName(); + $value = $this->token->getValue(); $text = <<setTitle('Qux title example'); + $qux->setTitle("[[ token:{$name}() ]]"); $qux->setSubTitle('[[ foo:primary() ]]'); $foo = new Foo(); @@ -85,7 +122,7 @@ public function testReplaceKeysRecursively() $actual = $templateService->replaceKeys($text, $entities); $expected = <<repository = $this + ->getMockBuilder(EntityRepository::class) + ->disableOriginalConstructor() + ->getMock(); + $this->entityManager = $this + ->getMockBuilder(EntityManager::class) + ->disableOriginalConstructor() + ->getMock(); + $this->entityManager + ->expects($this->once()) + ->method('getRepository') + ->with(Token::class) + ->willReturn($this->repository); + $this->tokenService = new TokenService($this->entityManager); + } + + public function testGetTemplateReplacements() + { + $name = substr(str_shuffle("abcdefghijklmnopqrstuvwxyz"), 0, 10); + $value = substr(str_shuffle("abcdefghijklmnopqrstuvwxyz"), 0, 10); + $token = new Token(); + $token->setName($name); + $token->setValue($value); + $this->repository->expects($this->once())->method('findAll')->willReturn([$token]); + $this->assertEquals([$name . '()' => 'get' . ucfirst($name) . '()'], $this->tokenService->getTemplateReplacements()); + } + + public function testMagicCallMethod() + { + $name = substr(str_shuffle("abcdefghijklmnopqrstuvwxyz"), 0, 10); + $value = substr(str_shuffle("abcdefghijklmnopqrstuvwxyz"), 0, 10); + $token = new Token(); + $token->setName($name); + $token->setValue($value); + $this->repository->expects($this->once())->method('findAll')->willReturn([$token]); + $this->repository->expects($this->once())->method('findOneBy')->with(['name' => $name])->willReturn($token); + $method = 'get' . ucfirst($name); + $this->assertEquals($value, $this->tokenService->{$method}()); + } +} diff --git a/Twig/TemplateHelpExtension.php b/Twig/TemplateHelpExtension.php new file mode 100644 index 0000000..00fb1b5 --- /dev/null +++ b/Twig/TemplateHelpExtension.php @@ -0,0 +1,62 @@ +tokenService = $tokenService; + } + + public function getFunctions() + { + return [ + new TwigFunction( + 'template_help', + [ + $this, + 'templateHelp', + ], + [ + 'needs_environment' => true, + 'is_safe' => [ + 'html', + ], + ] + ), + ]; + } + + public function templateHelp(Environment $environment, array $classes, $textarea) + { + + $templates = [ + 'token' => array_keys($this->tokenService->getTemplateReplacements()), + ]; + foreach ($classes as $key => $class) { + if (!is_a($class, TemplateInterface::class, true)) { + new RuntimeError(sprintf('Class %s does not implement %s.', $class, TemplateInterface::class)); + } + $templates[$key] = array_keys(call_user_func([$class, 'getTemplateReplacements'])); + } + + return $environment->render( + '@DigipolisGentDomainator9kCore/Template/templatehelper.twig', + [ + 'templates' => $templates, + 'textarea' => $textarea, + ] + ); + } +} diff --git a/composer.json b/composer.json index dcc2ed3..0a93813 100644 --- a/composer.json +++ b/composer.json @@ -9,16 +9,19 @@ "require": { "php": ">=7.1", "symfony/symfony": ">=3.4", - "digipolisgent/setting-bundle": "^1.0.0@alpha", + "digipolisgent/setting-bundle": "^1.0", + "digipolisgent/command-builder": "^1.0", "doctrine/doctrine-bundle": "^1.6", "doctrine/orm": "^2.5", "symfony/swiftmailer-bundle": "^2.3", "phpseclib/phpseclib": "^2.0", "webmozart/path-util": "^2.3", - "doctrine/doctrine-fixtures-bundle": "^2.3" + "doctrine/doctrine-fixtures-bundle": "^2.3", + "mattketmo/camel": "^1.1", + "roave/better-reflection": "^3" }, "require-dev": { - "phpunit/phpunit": "6.5" + "phpunit/phpunit": "^6.5" }, "autoload": { "psr-4": { diff --git a/phpunit.xml b/phpunit.xml index d84bd06..e6dbf79 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -1,4 +1,11 @@ - + ./Tests