Skip to content

Commit

Permalink
chore: Finish initial functionality
Browse files Browse the repository at this point in the history
  • Loading branch information
nmfzone committed Nov 16, 2019
1 parent 95fe64f commit 38b0e8a
Show file tree
Hide file tree
Showing 8 changed files with 767 additions and 9 deletions.
14 changes: 13 additions & 1 deletion config/behat-dusk.php
Original file line number Diff line number Diff line change
@@ -1,5 +1,17 @@
<?php

return [
//

'base_path' => base_path('tests/Behat'),

'feature_path' => base_path('tests/Behat/Features'),

'context_path' => base_path('tests/Behat/Context'),

'screenshot_path' => base_path('tests/Behat/artifacts/screenshots'),

'console_log_path' => base_path('tests/Behat/artifacts/console'),

'file_path' => base_path('public'),

];
358 changes: 358 additions & 0 deletions src/Console/BehatCommand.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,358 @@
<?php

namespace Nmflabs\LaravelBehatDusk\Console;

use Closure;
use Dotenv\Dotenv;
use Illuminate\Support\Env;
use Illuminate\Support\Str;
use Illuminate\Console\Command;
use Symfony\Component\Finder\Finder;
use Symfony\Component\Process\Process;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Process\Exception\RuntimeException;
use Symfony\Component\Process\Exception\ProcessSignaledException;

class BehatCommand extends Command
{
/**
* The console command name.
*
* @var string
*/
protected $name = 'behat';

/**
* The console command description.
*
* @var string
*/
protected $description = 'Serve the application and run Behat tests';

/**
* Create a new command instance.
*
* @return void
*/
public function __construct()
{
parent::__construct();

$this->ignoreValidationErrors();
}

/**
* Execute the console command.
*
* @return mixed
*/
public function handle()
{
$this->purgeScreenshots();

$this->purgeConsoleLogs();

$this->setupBehatEnvironment();

$this->startHttpServer(function () {
$behatProcess = (new Process(array_merge(
$this->binary(), $this->behatArguments($_SERVER['argv'])
)))->setTimeout(null)->setTty(true);

$behatProcess->start();
$behatProcess->wait();
$this->killChildsProcess($behatProcess->getPid(), 15, false);
$behatProcess->stop();

$this->teardownBehatEnviroment();
});

return true;
}

/**
* Build a process to run Laravel Http Server.
*
* @param \Closure $callback
* @return void
*/
protected function startHttpServer(Closure $callback)
{
$arguments = [
PHP_BINARY,
'artisan',
'serve',
$this->input->getOption('host') ? '--host=' . $this->input->getOption('host') : null,
$this->input->getOption('port') ? '--port=' . $this->input->getOption('port') : null,
'--tries=0',
];

$process = (new Process(array_filter($arguments)))
->setTimeout(null);

try {
$process->start();

$process->waitUntil(function () use ($callback) {
$this->output->writeln(sprintf(
"Laravel HTTP Server started: http://%s:%s",
$this->input->getOption('host') ?? '127.0.0.1',
$this->input->getOption('port') ?? Env::get('SERVER_PORT', 8000)
));
$this->output->newLine();

$callback();

return true;
});
} catch (ProcessSignaledException $e) {
$this->killChildsProcess($process->getPid(), 15, false);
$process->stop();

if (extension_loaded('pcntl') && $e->getSignal() !== SIGINT) {
throw $e;
}
}

$this->killChildsProcess($process->getPid(), 15, false);
$process->stop();

$this->output->writeln("\nShutdown Laravel HTTP Server.");
}

/**
* Get the console command options.
*
* @return array
*/
protected function getOptions()
{
return [
['host', null, InputOption::VALUE_OPTIONAL, 'The host address to serve the application on'],

['port', null, InputOption::VALUE_OPTIONAL, 'The port to serve the application on'],
];
}

/**
* Get the Behat binary to execute.
*
* @return array
*/
protected function binary()
{
if ('phpdbg' === PHP_SAPI) {
return [PHP_BINARY, '-qrr', 'vendor/bin/behat'];
}

return [PHP_BINARY, 'vendor/bin/behat'];
}

/**
* Get the array of arguments for running Behat.
*
* @param array $options
* @return array
*/
protected function behatArguments($options)
{
$options = array_slice($options, 2);

$options = array_values(array_filter($options, function ($option) {
return ! Str::startsWith($option, [
'--env=',
'--host',
'--port',
]);
}));

return $options;
}

/**
* Purge the failure screenshots.
*
* @return void
*/
protected function purgeScreenshots()
{
$path = config('behat-dusk.screenshot_path');

if (! is_dir($path)) {
return;
}

$files = Finder::create()->files()
->in($path)
->name('failure-*');

foreach ($files as $file) {
@unlink($file->getRealPath());
}
}

/**
* Purge the console logs.
*
* @return void
*/
protected function purgeConsoleLogs()
{
$path = config('behat-dusk.console_log_path');

if (! is_dir($path)) {
return;
}

$files = Finder::create()->files()
->in($path)
->name('*.log');

foreach ($files as $file) {
@unlink($file->getRealPath());
}
}

/**
* Setup the Behat environment.
*
* @return void
*/
protected function setupBehatEnvironment()
{
if (file_exists(base_path($this->behatEnvironmentFile()))) {
if (file_get_contents(base_path('.env')) !== file_get_contents(base_path($this->behatEnvironmentFile()))) {
$this->backupEnvironment();
}

$this->refreshEnvironment();
}

$this->setupSignalHandler();
}

/**
* Backup the current environment file.
*
* @return void
*/
protected function backupEnvironment()
{
copy(base_path('.env'), base_path('.env-behat.backup'));

copy(base_path($this->behatEnvironmentFile()), base_path('.env'));
}

/**
* Refresh the current environment variables.
*
* @return void
*/
protected function refreshEnvironment()
{
if (! method_exists(Dotenv::class, 'create')) {
(new Dotenv(base_path()))->overload();

return;
}

Dotenv::create(base_path())->overload();
}

/**
* Setup the SIGINT signal handler for CTRL+C exits.
*
* @return void
*/
protected function setupSignalHandler()
{
if (extension_loaded('pcntl')) {
pcntl_async_signals(true);

pcntl_signal(SIGINT, function () {
$this->teardownBehatEnviroment();
});
}
}

/**
* Restore the original environment.
*
* @return void
*/
protected function teardownBehatEnviroment()
{
if (file_exists(base_path($this->behatEnvironmentFile())) && file_exists(base_path('.env-behat.backup'))) {
$this->restoreEnvironment();
}
}

/**
* Restore the backed-up environment file.
*
* @return void
*/
protected function restoreEnvironment()
{
copy(base_path('.env-behat.backup'), base_path('.env'));

unlink(base_path('.env-behat.backup'));
}

/**
* Get the name of the Behat file for the environment.
*
* @return string
*/
protected function behatEnvironmentFile()
{
return '.env.behat';
}

/**
* Kill childs process created by Symfony Process.
*
* @see https://github.com/symfony/symfony/issues/34406
*
* @param int $pid
* @param int $signal
* @param bool $throwException
* @return bool
*/
protected function killChildsProcess($pid, int $signal, bool $throwException): bool
{
$ret = proc_open(sprintf('pgrep -P %d', $pid), [1 => ['pipe', 'w']], $pipes);

if ($ret && $output = fgets($pipes[1])) {
proc_close($ret);
$pids = explode("\n", $output);

foreach ($pids as $childPid) {
if (empty($childPid)) {
continue;
}

$childPid = (int) $childPid;
if (\function_exists('posix_kill')) {
$ok = @posix_kill($childPid, $signal);
} elseif ($ok = proc_open(sprintf('kill -%d %d', $signal, $childPid), [2 => ['pipe', 'w']], $pipes)) {
$resource = $ok;
$ok = false === fgets($pipes[2]);
proc_close($resource);
}

if (!$ok) {
if ($throwException) {
throw new RuntimeException(sprintf('Error while sending signal "%s" to child.', $signal));
}

return false;
}
}

return true;
}

return false;
}
}
Loading

0 comments on commit 38b0e8a

Please sign in to comment.