Skip to content

Commit 165ffa1

Browse files
authored
Implemented ConsoleApplicationTester for isolated E2E tests (#2513)
Added a new class ConsoleApplicationTester, which during E2E tests will create a separate isolated process for the Deployer to run in. This class exposes methods for interacting with stdin and to grab stdout, stderr and status code of finished process.
1 parent c23688a commit 165ffa1

12 files changed

+174
-79
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
- Added `contrib/php-fpm.php` recipe that provides a task to reload PHP-fpm. [#2487]
2727
- Added tasks `artisan:key:generate` and `artisan:passport:keys` to the Laravel recipe.
2828
- Added the following artisan tasks: `artisan:route:clear`, `artisan:route:list`, `artisan:horizon`, `artisan:horizon:clear`, `artisan:horizon:continue`, `artisan:horizon:list`, `artisan:horizon:pause`, `artisan:horizon:purge`, `artisan:horizon:status`, `artisan:event:list`, `artisan:queue:failed`, `artisan:queue:flushed`. [#2488]
29+
- Isolated console application runner for E2E tests.
2930

3031
### Changed
3132
- Refactored executor engine, up to 2x faster than before.

tests/e2e/AbstractE2ETest.php

Lines changed: 3 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,55 +1,17 @@
11
<?php declare(strict_types=1);
22
namespace e2e;
33

4-
use Deployer\Deployer;
54
use Deployer\Exception\Exception;
65
use PHPUnit\Framework\TestCase;
76
use Symfony\Component\Console\Application;
8-
use Symfony\Component\Console\Tester\ApplicationTester;
97

108
abstract class AbstractE2ETest extends TestCase
119
{
12-
/**
13-
* @var ApplicationTester
14-
*/
10+
/** @var ConsoleApplicationTester */
1511
protected $tester;
1612

17-
/**
18-
* @var Deployer
19-
*/
20-
protected $deployer;
21-
22-
public static function setUpBeforeClass(): void
23-
{
24-
self::cleanUp();
25-
mkdir(__TEMP_DIR__);
26-
}
27-
28-
public static function tearDownAfterClass(): void
13+
public function setUp(): void
2914
{
30-
self::cleanUp();
31-
}
32-
33-
protected static function cleanUp(): void
34-
{
35-
if (is_dir(__TEMP_DIR__)) {
36-
exec('rm -rf ' . __TEMP_DIR__);
37-
}
38-
}
39-
40-
/**
41-
* @param string $recipe path to recipe file
42-
* @throws Exception
43-
*/
44-
protected function init(string $recipe): void
45-
{
46-
$console = new Application();
47-
$console->setAutoExit(false);
48-
$this->tester = new ApplicationTester($console);
49-
50-
$this->deployer = new Deployer($console);
51-
$this->deployer->importer->import($recipe);
52-
$this->deployer->init();
53-
$this->deployer->config->set('deploy_path', __TEMP_DIR__ . '/{{hostname}}');
15+
$this->tester = new ConsoleApplicationTester(__DIR__ . '/../../bin/dep', __DIR__);
5416
}
5517
}
Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
<?php
2+
declare(strict_types=1);
3+
4+
namespace e2e;
5+
6+
use Symfony\Component\Process\Process;
7+
8+
class ConsoleApplicationTester
9+
{
10+
public const DEFAULT_TIMEOUT_IN_SECONDS = 10;
11+
12+
private $binaryPath;
13+
private $cwd;
14+
private $timeout = self::DEFAULT_TIMEOUT_IN_SECONDS;
15+
16+
private $inputs = [];
17+
18+
/** @var Process|null */
19+
private $process = null;
20+
21+
22+
private static function createInputsStream(array $inputs)
23+
{
24+
$stream = fopen('php://memory', 'r+', false);
25+
26+
foreach ($inputs as $input) {
27+
fwrite($stream, $input.\PHP_EOL);
28+
}
29+
30+
rewind($stream);
31+
32+
return $stream;
33+
}
34+
35+
private function generateCommand(array $arguments): array
36+
{
37+
$arguments = array_merge([ $this->binaryPath ], $arguments);
38+
39+
$outputArgs = [];
40+
foreach ($arguments as $key => $value) {
41+
if (!is_numeric($key)) {
42+
$outputArgs[] = $key;
43+
}
44+
45+
$outputArgs[] = $value;
46+
}
47+
48+
return $outputArgs;
49+
}
50+
51+
private function prepareProcess(array $arguments): Process
52+
{
53+
$commandLine = $this->generateCommand($arguments);
54+
55+
$process = new Process($commandLine);
56+
$process->setTimeout($this->timeout);
57+
58+
if (!empty($this->inputs)) {
59+
$inputs = self::createInputsStream($this->inputs);
60+
$process->setInput($inputs);
61+
}
62+
63+
if (!empty($this->cwd)) {
64+
$process->setWorkingDirectory($this->cwd);
65+
}
66+
67+
return $process;
68+
}
69+
70+
public function __construct(string $binaryPath, string $cwd = '')
71+
{
72+
$this->binaryPath = $binaryPath;
73+
$this->cwd = $cwd;
74+
}
75+
76+
public function __destruct()
77+
{
78+
if ($this->process && $this->process->isRunning()) {
79+
$this->process->stop(0);
80+
}
81+
}
82+
83+
/**
84+
* @param int $timeout timout in seconds after which process will be stopped
85+
* @return $this
86+
*/
87+
public function setTimeout(int $timeout): self
88+
{
89+
$this->timeout = $timeout;
90+
return $this;
91+
}
92+
93+
public function setInputs(array $inputs): self
94+
{
95+
$this->inputs = $inputs;
96+
return $this;
97+
}
98+
99+
public function run(array $arguments): self
100+
{
101+
if ($this->process && $this->process->isRunning()) {
102+
throw new \RuntimeException('Previous process did not end yet');
103+
}
104+
105+
$this->process = $this->prepareProcess($arguments);
106+
$this->process->run();
107+
108+
return $this;
109+
}
110+
111+
public function getDisplay(bool $normalize = false): string
112+
{
113+
if ($this->process === null) {
114+
throw new \RuntimeException('Output not initialized, did you execute the command before requesting the display?');
115+
}
116+
117+
$display = $this->process->getOutput();
118+
if ($normalize) {
119+
$display = str_replace(\PHP_EOL, "\n", $display);
120+
}
121+
122+
return $display;
123+
}
124+
125+
public function getErrors(bool $normalize = false): string
126+
{
127+
if ($this->process === null) {
128+
throw new \RuntimeException('Error output not initialized, did you execute the command before requesting the display?');
129+
}
130+
131+
$display = $this->process->getErrorOutput();
132+
if ($normalize) {
133+
$display = str_replace(\PHP_EOL, "\n", $display);
134+
}
135+
136+
return $display;
137+
}
138+
139+
public function getStatusCode()
140+
{
141+
if ($this->process === null) {
142+
throw new \RuntimeException('Status code not initialized, did you execute the command before requesting the display?');
143+
}
144+
145+
return $this->process->getExitCode();
146+
}
147+
}

tests/e2e/FunctionsE2ETest.php

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,13 +10,11 @@ class FunctionsE2ETest extends AbstractE2ETest
1010
*/
1111
public function testRunWithPlaceholders(): void
1212
{
13-
$this->init(self::RECIPE);
14-
1513
$this->tester->run([
16-
'test:functions:run-with-placeholders',
1714
'-f' => self::RECIPE,
18-
'selector' => 'all',
19-
], [ 'decorated' => false ]);
15+
'test:functions:run-with-placeholders',
16+
'all',
17+
]);
2018

2119
$display = trim($this->tester->getDisplay());
2220

tests/e2e/LaravelBoilerplateE2ETest.php

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,12 @@ class LaravelBoilerplateE2ETest extends AbstractE2ETest
77

88
public function testDeployLaravelBoilerplate(): void
99
{
10-
$this->init(self::RECIPE);
11-
12-
$this->tester->run([
13-
'deploy',
14-
'-f' => self::RECIPE,
15-
'selector' => 'all',
16-
]);
10+
$this->tester->setTimeout(180)
11+
->run([
12+
'-f' => self::RECIPE,
13+
'deploy',
14+
'all',
15+
]);
1716

1817
$display = trim($this->tester->getDisplay());
1918
self::assertEquals(0, $this->tester->getStatusCode(), $display);
@@ -29,9 +28,9 @@ protected function tearDown(): void
2928

3029
if ($this->tester) {
3130
$this->tester->run([
32-
'deploy:unlock',
3331
'-f' => self::RECIPE,
34-
'selector' => 'all',
32+
'deploy:unlock',
33+
'all',
3534
]);
3635
}
3736
}

tests/e2e/MiscE2ETest.php

Lines changed: 7 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -10,16 +10,14 @@ class MiscE2ETest extends AbstractE2ETest
1010
*/
1111
public function testSudoWithPasswordEnteredInteractively(): void
1212
{
13-
$this->init(self::RECIPE);
14-
1513
// We're adding this to inputs, to have it passed with via the STDIN
1614
$this->tester->setInputs(['deployer']);
1715

1816
$this->tester->run([
19-
'test:misc:sudo-write-user',
2017
'-f' => self::RECIPE,
21-
'selector' => 'all',
22-
], [ 'decorated' => false ]);
18+
'test:misc:sudo-write-user',
19+
'all',
20+
]);
2321

2422
$display = trim($this->tester->getDisplay());
2523

@@ -32,14 +30,12 @@ public function testSudoWithPasswordEnteredInteractively(): void
3230
*/
3331
public function testSudoWithPasswordProvidedViaArgument(): void
3432
{
35-
$this->init(self::RECIPE);
36-
3733
$this->tester->run([
38-
'test:misc:sudo-write-user',
3934
'-f' => self::RECIPE,
40-
'-o' => [ 'sudo_pass=deployer' ],
41-
'selector' => 'all',
42-
], [ 'decorated' => false ]);
35+
'test:misc:sudo-write-user',
36+
'-o' => 'sudo_pass=deployer',
37+
'all',
38+
]);
4339

4440
$display = trim($this->tester->getDisplay());
4541

tests/e2e/SymfonyBoilerplateE2ETest.php

Whitespace-only changes.

tests/e2e/bootstrap.php

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,14 @@
11
<?php
2-
$loaded = false;
32
$file = __DIR__ . '/../../vendor/autoload.php';
43

54
if (file_exists($file)) {
6-
require $file;
5+
require_once $file;
76
} else {
87
die(
98
'You need to set up the project dependencies using the following commands:' . PHP_EOL .
109
'composer install' . PHP_EOL
1110
);
1211
}
1312

14-
// For loading recipes
15-
set_include_path(__DIR__ . '/../..' . PATH_SEPARATOR . get_include_path());
16-
17-
define('DEPLOYER_BIN', __DIR__ . '/../../bin/dep');
18-
define('__FIXTURES__', __DIR__ . '/../fixtures');
19-
define('__REPOSITORY__', __DIR__ . '/../fixtures/repository');
20-
define('__TEMP_DIR__', sys_get_temp_dir() . '/deployer');
21-
13+
require_once __DIR__ . '/ConsoleApplicationTester.php';
2214
require_once __DIR__ . '/AbstractE2ETest.php';

tests/e2e/recipe/functions.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
namespace Deployer;
33

44
// we need to user require instead of require_once, as the hosts HAVE to be loaded multiple times
5-
require __DIR__ . '/deploy.php';
5+
require_once __DIR__ . '/hosts.php';
66

77
task('test:functions:run-with-placeholders', function (): void {
88
$cmd = "echo 'placeholder %foo% %baz%'";

0 commit comments

Comments
 (0)