1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220
|
<?php declare(strict_types=1);
/*
* This file is part of Composer.
*
* (c) Nils Adermann <naderman@naderman.de>
* Jordi Boggiano <j.boggiano@seld.be>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Composer\Test\Util;
use Composer\IO\ConsoleIO;
use Composer\Util\ProcessExecutor;
use Composer\Test\TestCase;
use Composer\IO\BufferIO;
use PHPUnit\Framework\Attributes\DataProvider;
use React\Promise\Promise;
use Symfony\Component\Console\Helper\HelperSet;
use Symfony\Component\Console\Input\ArrayInput;
use Symfony\Component\Console\Output\BufferedOutput;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Output\StreamOutput;
class ProcessExecutorTest extends TestCase
{
public function testExecuteCapturesOutput(): void
{
$process = new ProcessExecutor;
$process->execute('echo foo', $output);
self::assertEquals("foo".PHP_EOL, $output);
}
public function testExecuteOutputsIfNotCaptured(): void
{
$process = new ProcessExecutor;
ob_start();
$process->execute('echo foo');
$output = ob_get_clean();
self::assertEquals("foo".PHP_EOL, $output);
}
public function testUseIOIsNotNullAndIfNotCaptured(): void
{
$io = $this->getMockBuilder('Composer\IO\IOInterface')->getMock();
$io->expects($this->once())
->method('writeRaw')
->with($this->equalTo('foo'.PHP_EOL), false);
$process = new ProcessExecutor($io);
$process->execute('echo foo');
}
public function testExecuteCapturesStderr(): void
{
$process = new ProcessExecutor;
$process->execute('cat foo', $output);
self::assertStringContainsString('foo: No such file or directory', $process->getErrorOutput());
}
public function testTimeout(): void
{
ProcessExecutor::setTimeout(1);
$process = new ProcessExecutor;
self::assertEquals(1, $process->getTimeout());
ProcessExecutor::setTimeout(60);
}
#[DataProvider('hidePasswordProvider')]
public function testHidePasswords(string $command, string $expectedCommandOutput): void
{
$process = new ProcessExecutor($buffer = new BufferIO('', StreamOutput::VERBOSITY_DEBUG));
$process->execute($command, $output);
self::assertEquals('Executing command (CWD): ' . $expectedCommandOutput, trim($buffer->getOutput()));
}
public static function hidePasswordProvider(): array
{
return [
['echo https://foo:bar@example.org/', 'echo https://foo:***@example.org/'],
['echo http://foo@example.org', 'echo http://foo@example.org'],
['echo http://abcdef1234567890234578:x-oauth-token@github.com/', 'echo http://***:***@github.com/'],
["svn ls --verbose --non-interactive --username 'foo' --password 'bar' 'https://foo.example.org/svn/'", "svn ls --verbose --non-interactive --username 'foo' --password '***' 'https://foo.example.org/svn/'"],
["svn ls --verbose --non-interactive --username 'foo' --password 'bar \'bar' 'https://foo.example.org/svn/'", "svn ls --verbose --non-interactive --username 'foo' --password '***' 'https://foo.example.org/svn/'"],
];
}
public function testDoesntHidePorts(): void
{
$process = new ProcessExecutor($buffer = new BufferIO('', StreamOutput::VERBOSITY_DEBUG));
$process->execute('echo https://localhost:1234/', $output);
self::assertEquals('Executing command (CWD): echo https://localhost:1234/', trim($buffer->getOutput()));
}
public function testSplitLines(): void
{
$process = new ProcessExecutor;
self::assertEquals([], $process->splitLines(''));
self::assertEquals([], $process->splitLines(null));
self::assertEquals(['foo'], $process->splitLines('foo'));
self::assertEquals(['foo', 'bar'], $process->splitLines("foo\nbar"));
self::assertEquals(['foo', 'bar'], $process->splitLines("foo\r\nbar"));
self::assertEquals(['foo', 'bar'], $process->splitLines("foo\r\nbar\n"));
}
public function testConsoleIODoesNotFormatSymfonyConsoleStyle(): void
{
$output = new BufferedOutput(OutputInterface::VERBOSITY_NORMAL, true);
$process = new ProcessExecutor(new ConsoleIO(new ArrayInput([]), $output, new HelperSet([])));
$process->execute('php -ddisplay_errors=0 -derror_reporting=0 -r "echo \'<error>foo</error>\'.PHP_EOL;"');
self::assertSame('<error>foo</error>'.PHP_EOL, $output->fetch());
}
public function testExecuteAsyncCancel(): void
{
$process = new ProcessExecutor($buffer = new BufferIO('', StreamOutput::VERBOSITY_DEBUG));
$process->enableAsync();
$start = microtime(true);
$promise = $process->executeAsync('sleep 2');
self::assertEquals(1, $process->countActiveJobs());
$promise->cancel();
self::assertEquals(0, $process->countActiveJobs());
$process->wait();
$end = microtime(true);
self::assertTrue($end - $start < 2, 'Canceling took longer than it should, lasted '.($end - $start));
}
/**
* Test various arguments are escaped as expected
*
* @param string|false|null $argument
*/
#[DataProvider('dataEscapeArguments')]
public function testEscapeArgument($argument, string $win, string $unix): void
{
$expected = defined('PHP_WINDOWS_VERSION_BUILD') ? $win : $unix;
self::assertSame($expected, ProcessExecutor::escape($argument));
}
/**
* Each named test is an array of:
* argument, win-expected, unix-expected
*/
public static function dataEscapeArguments(): array
{
return [
// empty argument - must be quoted
'empty' => ['', '""', "''"],
// null argument - must be quoted
'empty null' => [null, '""', "''"],
// false argument - must be quoted
'empty false' => [false, '""', "''"],
// unix single-quote must be escaped
'unix-sq' => ["a'bc", "a'bc", "'a'\\''bc'"],
// new lines must be replaced
'new lines' => ["a\nb\nc", '"a b c"', "'a\nb\nc'"],
// whitespace <space> must be quoted
'ws space' => ['a b c', '"a b c"', "'a b c'"],
// whitespace <tab> must be quoted
'ws tab' => ["a\tb\tc", "\"a\tb\tc\"", "'a\tb\tc'"],
// no whitespace must not be quoted
'no-ws' => ['abc', 'abc', "'abc'"],
// commas must be quoted
'comma' => ['a,bc', '"a,bc"', "'a,bc'"],
// double-quotes must be backslash-escaped
'dq' => ['a"bc', 'a\^"bc', "'a\"bc'"],
// double-quotes must be backslash-escaped with preceding backslashes doubled
'dq-bslash' => ['a\\"bc', 'a\\\\\^"bc', "'a\\\"bc'"],
// backslashes not preceding a double-quote are treated as literal
'bslash' => ['ab\\\\c\\', 'ab\\\\c\\', "'ab\\\\c\\'"],
// trailing backslashes must be doubled up when the argument is quoted
'bslash dq' => ['a b c\\\\', '"a b c\\\\\\\\"', "'a b c\\\\'"],
// meta: outer double-quotes must be caret-escaped as well
'meta dq' => ['a "b" c', '^"a \^"b\^" c^"', "'a \"b\" c'"],
// meta: percent expansion must be caret-escaped
'meta-pc1' => ['%path%', '^%path^%', "'%path%'"],
// meta: expansion must have two percent characters
'meta-pc2' => ['%path', '%path', "'%path'"],
// meta: expansion must have have two surrounding percent characters
'meta-pc3' => ['%%path', '%%path', "'%%path'"],
// meta: bang expansion must be double caret-escaped
'meta-bang1' => ['!path!', '^^!path^^!', "'!path!'"],
// meta: bang expansion must have two bang characters
'meta-bang2' => ['!path', '!path', "'!path'"],
// meta: bang expansion must have two surrounding ang characters
'meta-bang3' => ['!!path', '!!path', "'!!path'"],
// meta: caret-escaping must escape all other meta chars (triggered by double-quote)
'meta-all-dq' => ['<>"&|()^', '^<^>\^"^&^|^(^)^^', "'<>\"&|()^'"],
// other meta: no caret-escaping when whitespace in argument
'other meta' => ['<> &| ()^', '"<> &| ()^"', "'<> &| ()^'"],
// other meta: quote escape chars when no whitespace in argument
'other-meta' => ['<>&|()^', '"<>&|()^"', "'<>&|()^'"],
];
}
}
|