2022-02-23 15:58:18 +00:00
< ? php declare ( strict_types = 1 );
2021-08-18 15:12:41 +00:00
/*
* 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\Mock ;
2022-03-17 13:52:14 +00:00
use Composer\Test\TestCase ;
use PHPUnit\Framework\MockObject\MockBuilder ;
2022-02-18 10:22:01 +00:00
use React\Promise\PromiseInterface ;
2021-08-18 15:12:41 +00:00
use Composer\Util\ProcessExecutor ;
use Composer\Util\Platform ;
2021-12-09 16:09:07 +00:00
use PHPUnit\Framework\Assert ;
2021-08-18 21:35:27 +00:00
use PHPUnit\Framework\AssertionFailedError ;
2021-08-18 15:12:41 +00:00
use Symfony\Component\Process\Process ;
use React\Promise\Promise ;
/**
* @ author Jordi Boggiano < j . boggiano @ seld . be >
*/
class ProcessExecutorMock extends ProcessExecutor
{
2021-10-16 08:16:06 +00:00
/**
2022-01-06 12:56:12 +00:00
* @ var array < array { cmd : string | list < string > , return : int , stdout : string , stderr : string , callback : callable | null } >| null
2021-10-16 08:16:06 +00:00
*/
2021-12-09 16:09:07 +00:00
private $expectations = null ;
2021-10-16 08:16:06 +00:00
/**
* @ var bool
*/
2021-08-18 15:12:41 +00:00
private $strict = false ;
2021-10-16 08:16:06 +00:00
/**
* @ var array { return : int , stdout : string , stderr : string }
*/
2021-08-18 15:12:41 +00:00
private $defaultHandler = array ( 'return' => 0 , 'stdout' => '' , 'stderr' => '' );
2021-10-16 08:16:06 +00:00
/**
* @ var string []
*/
2021-08-18 15:12:41 +00:00
private $log = array ();
2022-03-17 13:52:14 +00:00
/**
* @ var MockBuilder < Process >
*/
private $processMockBuilder ;
/**
* @ param MockBuilder < Process > $processMockBuilder
*/
public function __construct ( MockBuilder $processMockBuilder )
{
parent :: __construct ();
$this -> processMockBuilder = $processMockBuilder -> disableOriginalConstructor ();
}
2021-08-18 15:12:41 +00:00
/**
2022-01-06 12:56:12 +00:00
* @ param array < string | array { cmd : string | list < string > , return ? : int , stdout ? : string , stderr ? : string , callback ? : callable } > $expectations
* @ param bool $strict set to true if you want to provide * all * expected commands , and not just a subset you are interested in testing
* @ param array { return : int , stdout ? : string , stderr ? : string } $defaultHandler default command handler for undefined commands if not in strict mode
2021-10-27 14:18:24 +00:00
*
2021-10-27 12:56:39 +00:00
* @ return void
2021-08-18 15:12:41 +00:00
*/
2022-02-22 15:47:09 +00:00
public function expects ( array $expectations , bool $strict = false , array $defaultHandler = array ( 'return' => 0 , 'stdout' => '' , 'stderr' => '' )) : void
2021-08-18 15:12:41 +00:00
{
2022-01-06 12:56:12 +00:00
/** @var array{cmd: string|list<string>, return?: int, stdout?: string, stderr?: string, callback?: callable} $default */
2021-08-18 21:35:27 +00:00
$default = array ( 'cmd' => '' , 'return' => 0 , 'stdout' => '' , 'stderr' => '' , 'callback' => null );
2022-02-21 12:42:28 +00:00
$this -> expectations = array_map ( function ( $expect ) use ( $default ) : array {
2021-08-18 15:12:41 +00:00
if ( is_string ( $expect )) {
2022-02-16 12:24:57 +00:00
$command = $expect ;
$expect = $default ;
$expect [ 'cmd' ] = $command ;
2022-01-06 12:56:12 +00:00
} elseif ( count ( $diff = array_diff_key ( array_merge ( $default , $expect ), $default )) > 0 ) {
2021-08-18 21:35:27 +00:00
throw new \UnexpectedValueException ( 'Unexpected keys in process execution step: ' . implode ( ', ' , array_keys ( $diff )));
2021-08-18 15:12:41 +00:00
}
2022-02-16 12:24:57 +00:00
// set defaults in a PHPStan-happy way (array_merge is not well supported)
$expect [ 'cmd' ] = $expect [ 'cmd' ] ? ? $default [ 'cmd' ];
$expect [ 'return' ] = $expect [ 'return' ] ? ? $default [ 'return' ];
$expect [ 'stdout' ] = $expect [ 'stdout' ] ? ? $default [ 'stdout' ];
$expect [ 'stderr' ] = $expect [ 'stderr' ] ? ? $default [ 'stderr' ];
$expect [ 'callback' ] = $expect [ 'callback' ] ? ? $default [ 'callback' ];
return $expect ;
2021-08-18 15:12:41 +00:00
}, $expectations );
$this -> strict = $strict ;
2022-02-16 12:24:57 +00:00
// set defaults in a PHPStan-happy way (array_merge is not well supported)
$defaultHandler [ 'return' ] = $defaultHandler [ 'return' ] ? ? $this -> defaultHandler [ 'return' ];
$defaultHandler [ 'stdout' ] = $defaultHandler [ 'stdout' ] ? ? $this -> defaultHandler [ 'stdout' ];
$defaultHandler [ 'stderr' ] = $defaultHandler [ 'stderr' ] ? ? $this -> defaultHandler [ 'stderr' ];
$this -> defaultHandler = $defaultHandler ;
2021-08-18 15:12:41 +00:00
}
2021-12-09 16:09:07 +00:00
public function assertComplete () : void
2021-08-18 15:12:41 +00:00
{
2021-12-09 16:09:07 +00:00
// this was not configured to expect anything, so no need to react here
if ( ! is_array ( $this -> expectations )) {
return ;
}
if ( count ( $this -> expectations ) > 0 ) {
2022-02-21 12:42:28 +00:00
$expectations = array_map ( function ( $expect ) : string {
2022-01-06 12:56:12 +00:00
return is_array ( $expect [ 'cmd' ]) ? implode ( ' ' , $expect [ 'cmd' ]) : $expect [ 'cmd' ];
2021-08-18 15:12:41 +00:00
}, $this -> expectations );
2021-08-18 21:35:27 +00:00
throw new AssertionFailedError (
2021-08-18 15:12:41 +00:00
'There are still ' . count ( $this -> expectations ) . ' expected process calls which have not been consumed:' . PHP_EOL .
implode ( PHP_EOL , $expectations ) . PHP_EOL . PHP_EOL .
'Received calls:' . PHP_EOL . implode ( PHP_EOL , $this -> log )
);
}
2021-12-09 16:09:07 +00:00
// dummy assertion to ensure the test is not marked as having no assertions
2021-12-09 21:14:04 +00:00
Assert :: assertTrue ( true ); // @phpstan-ignore-line
2021-08-18 15:12:41 +00:00
}
2022-02-23 15:57:47 +00:00
public function execute ( $command , & $output = null , ? string $cwd = null ) : int
2021-08-18 15:12:41 +00:00
{
2022-02-22 15:47:09 +00:00
$cwd = $cwd ? ? Platform :: getCwd ();
2021-08-18 15:12:41 +00:00
if ( func_num_args () > 1 ) {
return $this -> doExecute ( $command , $cwd , false , $output );
}
return $this -> doExecute ( $command , $cwd , false );
}
2022-02-23 15:57:47 +00:00
public function executeTty ( $command , ? string $cwd = null ) : int
2021-08-18 15:12:41 +00:00
{
2022-02-22 15:47:09 +00:00
$cwd = $cwd ? ? Platform :: getCwd ();
2021-08-18 15:12:41 +00:00
if ( Platform :: isTty ()) {
return $this -> doExecute ( $command , $cwd , true );
}
return $this -> doExecute ( $command , $cwd , false );
}
2021-10-27 12:56:39 +00:00
/**
2022-01-06 12:56:12 +00:00
* @ param string | list < string > $command
2021-10-27 12:56:39 +00:00
* @ param string $cwd
* @ param bool $tty
2022-02-22 15:47:09 +00:00
* @ param callable | string | null $output
2021-10-27 12:56:39 +00:00
* @ return mixed
*/
2022-02-22 15:47:09 +00:00
private function doExecute ( $command , string $cwd , bool $tty , & $output = null )
2021-08-18 15:12:41 +00:00
{
$this -> captureOutput = func_num_args () > 3 ;
$this -> errorOutput = '' ;
2022-02-22 21:10:52 +00:00
$callback = is_callable ( $output ) ? $output : function ( string $type , string $buffer ) : void {
$this -> outputHandler ( $type , $buffer );
};
2021-08-18 15:12:41 +00:00
2022-01-06 12:56:12 +00:00
$commandString = is_array ( $command ) ? implode ( ' ' , $command ) : $command ;
$this -> log [] = $commandString ;
2021-08-18 21:35:27 +00:00
2021-12-09 16:09:07 +00:00
if ( is_array ( $this -> expectations ) && count ( $this -> expectations ) > 0 && $command === $this -> expectations [ 0 ][ 'cmd' ]) {
2021-08-18 15:12:41 +00:00
$expect = array_shift ( $this -> expectations );
$stdout = $expect [ 'stdout' ];
$stderr = $expect [ 'stderr' ];
$return = $expect [ 'return' ];
2021-08-18 21:35:27 +00:00
if ( isset ( $expect [ 'callback' ])) {
call_user_func ( $expect [ 'callback' ]);
}
2021-08-18 15:12:41 +00:00
} elseif ( ! $this -> strict ) {
$stdout = $this -> defaultHandler [ 'stdout' ];
$stderr = $this -> defaultHandler [ 'stderr' ];
$return = $this -> defaultHandler [ 'return' ];
} else {
2021-08-18 21:35:27 +00:00
throw new AssertionFailedError (
2022-01-06 12:56:12 +00:00
'Received unexpected command ' . var_export ( $command , true ) . ' in "' . $cwd . '"' . PHP_EOL .
( is_array ( $this -> expectations ) && count ( $this -> expectations ) > 0 ? 'Expected ' . var_export ( $this -> expectations [ 0 ][ 'cmd' ], true ) . ' at this point.' : 'Expected no more calls at this point.' ) . PHP_EOL .
2021-08-18 21:35:27 +00:00
'Received calls:' . PHP_EOL . implode ( PHP_EOL , array_slice ( $this -> log , 0 , - 1 ))
2021-08-18 15:12:41 +00:00
);
}
if ( $stdout ) {
call_user_func ( $callback , Process :: STDOUT , $stdout );
}
if ( $stderr ) {
call_user_func ( $callback , Process :: ERR , $stderr );
}
if ( $this -> captureOutput && ! is_callable ( $output )) {
$output = $stdout ;
}
$this -> errorOutput = $stderr ;
return $return ;
}
2022-02-22 15:47:09 +00:00
public function executeAsync ( $command , ? string $cwd = null ) : PromiseInterface
2021-08-18 15:12:41 +00:00
{
2022-03-17 13:52:14 +00:00
$cwd = $cwd ? ? Platform :: getCwd ();
$resolver = function ( $resolve , $reject ) use ( $command , $cwd ) : void {
$result = $this -> doExecute ( $command , $cwd , false , $output );
$procMock = $this -> processMockBuilder -> getMock ();
$procMock -> method ( 'getOutput' ) -> willReturn ( $output );
$procMock -> method ( 'isSuccessful' ) -> willReturn ( $result === 0 );
$procMock -> method ( 'getExitCode' ) -> willReturn ( $result );
$resolve ( $procMock );
2021-08-18 15:12:41 +00:00
};
2022-02-18 09:38:54 +00:00
$canceler = function () : void {
2021-08-18 15:12:41 +00:00
throw new \RuntimeException ( 'Aborted process' );
};
return new Promise ( $resolver , $canceler );
}
2022-02-18 10:22:01 +00:00
public function getErrorOutput () : string
2021-08-18 15:12:41 +00:00
{
return $this -> errorOutput ;
}
}