From 9f3e2105da38475088a4fe5b3d18b636d6bc3ee7 Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Wed, 7 Jun 2023 14:35:16 +0200 Subject: [PATCH] Add IOMock and get rid of all withConsecutive calls in tests (#11497) --- phpstan/baseline.neon | 10 + .../Test/Autoload/AutoloadGeneratorTest.php | 14 +- tests/Composer/Test/ConfigTest.php | 7 +- .../Test/Downloader/DownloadManagerTest.php | 12 +- .../Test/Downloader/FileDownloaderTest.php | 12 +- .../Test/Downloader/GitDownloaderTest.php | 20 +- .../EventDispatcher/EventDispatcherTest.php | 40 ++-- tests/Composer/Test/IO/ConsoleIOTest.php | 28 ++- .../SuggestedPackagesReporterTest.php | 80 +++---- tests/Composer/Test/Mock/IOMock.php | 196 ++++++++++++++++++ .../Test/Repository/Vcs/GitDriverTest.php | 10 +- tests/Composer/Test/TestCase.php | 20 +- tests/Composer/Test/Util/BitbucketTest.php | 112 ++++------ tests/Composer/Test/Util/GitHubTest.php | 38 +--- tests/Composer/Test/Util/GitLabTest.php | 60 ++---- 15 files changed, 394 insertions(+), 265 deletions(-) create mode 100644 tests/Composer/Test/Mock/IOMock.php diff --git a/phpstan/baseline.neon b/phpstan/baseline.neon index 3c445f08e..3597f9fee 100644 --- a/phpstan/baseline.neon +++ b/phpstan/baseline.neon @@ -5301,6 +5301,16 @@ parameters: count: 1 path: ../tests/Composer/Test/Json/JsonManipulatorTest.php + - + message: "#^Offset 'ask' might not exist on array\\{ask\\: string, reply\\?\\: string\\}\\|array\\{auth\\: array\\{string, string, string\\|null\\}\\}\\|array\\{text\\: string, verbosity\\?\\: 1\\|2\\|4\\|8\\|16\\}\\.$#" + count: 2 + path: ../tests/Composer/Test/Mock/IOMock.php + + - + message: "#^Offset 'text' might not exist on array\\{ask\\: string, reply\\?\\: string\\}\\|array\\{auth\\: array\\{string, string, string\\|null\\}\\}\\|array\\{text\\: string, verbosity\\?\\: 1\\|2\\|4\\|8\\|16\\}\\.$#" + count: 1 + path: ../tests/Composer/Test/Mock/IOMock.php + - message: "#^Composer\\\\Test\\\\Mock\\\\InstallationManagerMock\\:\\:__construct\\(\\) does not call parent constructor from Composer\\\\Installer\\\\InstallationManager\\.$#" count: 1 diff --git a/tests/Composer/Test/Autoload/AutoloadGeneratorTest.php b/tests/Composer/Test/Autoload/AutoloadGeneratorTest.php index cb98b38ca..fda3b06ed 100644 --- a/tests/Composer/Test/Autoload/AutoloadGeneratorTest.php +++ b/tests/Composer/Test/Autoload/AutoloadGeneratorTest.php @@ -1328,10 +1328,16 @@ EOF; $this->eventDispatcher ->expects($this->exactly(2)) ->method('dispatchScript') - ->withConsecutive( - [ScriptEvents::PRE_AUTOLOAD_DUMP, false], - [ScriptEvents::POST_AUTOLOAD_DUMP, false] - ); + ->willReturnCallback(function ($type, $dev) { + static $series = [ + [ScriptEvents::PRE_AUTOLOAD_DUMP, false], + [ScriptEvents::POST_AUTOLOAD_DUMP, false] + ]; + + $this->assertSame(array_shift($series), [$type, $dev]); + + return 0; + }); $package = new RootPackage('root/a', '1.0', '1.0'); $package->setAutoload(['psr-0' => ['Prefix' => 'foo/bar/non/existing/']]); diff --git a/tests/Composer/Test/ConfigTest.php b/tests/Composer/Test/ConfigTest.php index 2a2690765..428c4f265 100644 --- a/tests/Composer/Test/ConfigTest.php +++ b/tests/Composer/Test/ConfigTest.php @@ -307,12 +307,9 @@ class ConfigTest extends TestCase public function testProhibitedUrlsWarningVerifyPeer(): void { - $io = $this->getMockBuilder(IOInterface::class)->disableOriginalConstructor()->getMock(); + $io = $this->getIOMock(); - $io - ->expects($this->once()) - ->method('writeError') - ->with($this->equalTo('Warning: Accessing example.org with verify_peer and verify_peer_name disabled.')); + $io->expects([['text' => 'Warning: Accessing example.org with verify_peer and verify_peer_name disabled.']], true); $config = new Config(false); $config->prohibitUrlByConfig('https://example.org', $io, [ diff --git a/tests/Composer/Test/Downloader/DownloadManagerTest.php b/tests/Composer/Test/Downloader/DownloadManagerTest.php index 9717c3a7a..05d169cad 100644 --- a/tests/Composer/Test/Downloader/DownloadManagerTest.php +++ b/tests/Composer/Test/Downloader/DownloadManagerTest.php @@ -258,10 +258,14 @@ class DownloadManagerTest extends TestCase $package ->expects($this->exactly(2)) ->method('setInstallationSource') - ->withConsecutive( - ['dist'], - ['source'] - ); + ->willReturnCallback(function ($type) { + static $series = [ + 'dist', + 'source', + ]; + + $this->assertSame(array_shift($series), $type); + }); $downloaderFail = $this->createDownloaderMock(); $downloaderFail diff --git a/tests/Composer/Test/Downloader/FileDownloaderTest.php b/tests/Composer/Test/Downloader/FileDownloaderTest.php index 5a7cb667f..0978392f0 100644 --- a/tests/Composer/Test/Downloader/FileDownloaderTest.php +++ b/tests/Composer/Test/Downloader/FileDownloaderTest.php @@ -375,13 +375,11 @@ class FileDownloaderTest extends TestCase $newPackage = self::getPackage('dummy/pkg', '1.0.0'); $newPackage->setDistUrl($distUrl = 'http://example.com/script.js'); - $ioMock = $this->getMockBuilder('Composer\IO\IOInterface')->getMock(); - $ioMock->expects($this->atLeast(2)) - ->method('writeError') - ->withConsecutive( - [$this->stringContains('Downloading')], - [$this->stringContains('Downgrading')] - ); + $ioMock = $this->getIOMock(); + $ioMock->expects([ + ['text' => '{Downloading .*}', 'regex' => true], + ['text' => '{Downgrading .*}', 'regex' => true], + ]); $path = self::getUniqueTmpDirectory(); $config = $this->getConfig(['vendor-dir' => $path.'/vendor']); diff --git a/tests/Composer/Test/Downloader/GitDownloaderTest.php b/tests/Composer/Test/Downloader/GitDownloaderTest.php index 66a9c504d..5411767e5 100644 --- a/tests/Composer/Test/Downloader/GitDownloaderTest.php +++ b/tests/Composer/Test/Downloader/GitDownloaderTest.php @@ -547,12 +547,10 @@ composer https://github.com/old/url (push) $process = $this->getProcessExecutorMock(); - $ioMock = $this->getMockBuilder('Composer\IO\IOInterface')->getMock(); - $ioMock->expects($this->atLeastOnce()) - ->method('writeError') - ->withConsecutive( - [$this->stringContains('Downgrading')] - ); + $ioMock = $this->getIOMock(); + $ioMock->expects([ + ['text' => '{Downgrading .*}', 'regex' => true], + ]); $this->fs->ensureDirectoryExists($this->workingDir.'/.git'); $downloader = $this->getDownloaderMock($ioMock, null, $process); @@ -591,12 +589,10 @@ composer https://github.com/old/url (push) $process = $this->getProcessExecutorMock(); - $ioMock = $this->getMockBuilder('Composer\IO\IOInterface')->getMock(); - $ioMock->expects($this->atLeastOnce()) - ->method('writeError') - ->withConsecutive( - [$this->stringContains('Upgrading')] - ); + $ioMock = $this->getIOMock(); + $ioMock->expects([ + ['text' => '{Upgrading .*}', 'regex' => true], + ]); $this->fs->ensureDirectoryExists($this->workingDir.'/.git'); $downloader = $this->getDownloaderMock($ioMock, null, $process); diff --git a/tests/Composer/Test/EventDispatcher/EventDispatcherTest.php b/tests/Composer/Test/EventDispatcher/EventDispatcherTest.php index 7f4c6b73a..f0a7ecc5f 100644 --- a/tests/Composer/Test/EventDispatcher/EventDispatcherTest.php +++ b/tests/Composer/Test/EventDispatcher/EventDispatcherTest.php @@ -14,6 +14,7 @@ namespace Composer\Test\EventDispatcher; use Composer\EventDispatcher\Event; use Composer\EventDispatcher\EventDispatcher; +use Composer\EventDispatcher\ScriptExecutionException; use Composer\Installer\InstallerEvents; use Composer\Config; use Composer\Composer; @@ -32,21 +33,15 @@ class EventDispatcherTest extends TestCase { self::expectException('RuntimeException'); - $io = $this->getMockBuilder('Composer\IO\IOInterface')->getMock(); + $io = $this->getIOMock(IOInterface::NORMAL); $dispatcher = $this->getDispatcherStubForListenersTest([ 'Composer\Test\EventDispatcher\EventDispatcherTest::call', ], $io); - $io->expects($this->once()) - ->method('isVerbose') - ->willReturn(0); - - $io->expects($this->atLeast(2)) - ->method('writeError') - ->withConsecutive( - ['> Composer\Test\EventDispatcher\EventDispatcherTest::call'], - ['Script Composer\Test\EventDispatcher\EventDispatcherTest::call handling the post-install-cmd event terminated with an exception'] - ); + $io->expects([ + ['text' => '> Composer\Test\EventDispatcher\EventDispatcherTest::call'], + ['text' => 'Script Composer\Test\EventDispatcher\EventDispatcherTest::call handling the post-install-cmd event terminated with an exception'], + ], true); $dispatcher->dispatchScript(ScriptEvents::POST_INSTALL_CMD, false); } @@ -528,7 +523,7 @@ class EventDispatcherTest extends TestCase $dispatcher = $this->getMockBuilder('Composer\EventDispatcher\EventDispatcher') ->setConstructorArgs([ $this->createComposerInstance(), - $io = $this->getMockBuilder('Composer\IO\IOInterface')->getMock(), + $io = $this->getIOMock(IOInterface::NORMAL), new ProcessExecutor, ]) ->onlyMethods(['getListeners']) @@ -540,22 +535,13 @@ class EventDispatcherTest extends TestCase ->method('getListeners') ->will($this->returnValue($listener)); - $io->expects($this->once()) - ->method('isVerbose') - ->willReturn(0); + $io->expects([ + ['text' => '> exit 1'], + ['text' => 'Script '.$code.' handling the post-install-cmd event returned with error code 1'], + ], true); - $io->expects($this->atLeast(2)) - ->method('writeError') - ->withConsecutive( - ['> exit 1'], - ['Script '.$code.' handling the post-install-cmd event returned with error code 1'] - ); - - $io->expects($this->once()) - ->method('isInteractive') - ->willReturn(1); - - self::expectException('RuntimeException'); + self::expectException(ScriptExecutionException::class); + self::expectExceptionMessage('Error Output: '); $dispatcher->dispatchScript(ScriptEvents::POST_INSTALL_CMD, false); } diff --git a/tests/Composer/Test/IO/ConsoleIOTest.php b/tests/Composer/Test/IO/ConsoleIOTest.php index 04483e751..2d152eb61 100644 --- a/tests/Composer/Test/IO/ConsoleIOTest.php +++ b/tests/Composer/Test/IO/ConsoleIOTest.php @@ -111,15 +111,25 @@ class ConsoleIOTest extends TestCase ->willReturn(OutputInterface::VERBOSITY_NORMAL); $outputMock->expects($this->atLeast(7)) ->method('write') - ->withConsecutive( - [$this->equalTo('something (strlen = 23)')], - [$this->equalTo(str_repeat("\x08", 23)), $this->equalTo(false)], - [$this->equalTo('shorter (12)'), $this->equalTo(false)], - [$this->equalTo(str_repeat(' ', 11)), $this->equalTo(false)], - [$this->equalTo(str_repeat("\x08", 11)), $this->equalTo(false)], - [$this->equalTo(str_repeat("\x08", 12)), $this->equalTo(false)], - [$this->equalTo('something longer than initial (34)')] - ); + ->willReturnCallback(function (...$args) { + static $series = null; + + if ($series === null) { + $series = [ + ['something (strlen = 23)', true], + [str_repeat("\x08", 23), false], + ['shorter (12)', false], + [str_repeat(' ', 11), false], + [str_repeat("\x08", 11), false], + [str_repeat("\x08", 12), false], + ['something longer than initial (34)', false], + ]; + } + + if (count($series) > 0) { + $this->assertSame(array_shift($series), [$args[0], $args[1]]); + } + }); $helperMock = $this->getMockBuilder('Symfony\Component\Console\Helper\HelperSet')->getMock(); diff --git a/tests/Composer/Test/Installer/SuggestedPackagesReporterTest.php b/tests/Composer/Test/Installer/SuggestedPackagesReporterTest.php index 64a961f0d..a9969c4f4 100644 --- a/tests/Composer/Test/Installer/SuggestedPackagesReporterTest.php +++ b/tests/Composer/Test/Installer/SuggestedPackagesReporterTest.php @@ -15,6 +15,7 @@ namespace Composer\Test\Installer; use Composer\InstalledVersions; use Composer\Installer\SuggestedPackagesReporter; use Composer\Semver\VersionParser; +use Composer\Test\Mock\IOMock; use Composer\Test\TestCase; /** @@ -23,7 +24,7 @@ use Composer\Test\TestCase; class SuggestedPackagesReporterTest extends TestCase { /** - * @var \PHPUnit\Framework\MockObject\MockObject + * @var IOMock */ private $io; @@ -34,7 +35,7 @@ class SuggestedPackagesReporterTest extends TestCase protected function setUp(): void { - $this->io = $this->getMockBuilder('Composer\IO\IOInterface')->getMock(); + $this->io = $this->getIOMock(); $this->suggestedPackagesReporter = new SuggestedPackagesReporter($this->io); } @@ -44,8 +45,7 @@ class SuggestedPackagesReporterTest extends TestCase */ public function testConstructor(): void { - $this->io->expects($this->once()) - ->method('write'); + $this->io->expects([['text' => 'b']], true); $this->suggestedPackagesReporter->addPackage('a', 'b', 'c'); $this->suggestedPackagesReporter->output(SuggestedPackagesReporter::MODE_LIST); @@ -143,13 +143,11 @@ class SuggestedPackagesReporterTest extends TestCase { $this->suggestedPackagesReporter->addPackage('a', 'b', 'c'); - $this->io->expects($this->exactly(3)) - ->method('write') - ->withConsecutive( - ['a suggests:'], - [' - b: c'], - [''] - ); + $this->io->expects([ + ['text' => 'a suggests:'], + ['text' => ' - b: c'], + ['text' => ''], + ], true); $this->suggestedPackagesReporter->output(SuggestedPackagesReporter::MODE_BY_PACKAGE); } @@ -161,13 +159,11 @@ class SuggestedPackagesReporterTest extends TestCase { $this->suggestedPackagesReporter->addPackage('a', 'b', ''); - $this->io->expects($this->exactly(3)) - ->method('write') - ->withConsecutive( - ['a suggests:'], - [' - b'], - [''] - ); + $this->io->expects([ + ['text' => 'a suggests:'], + ['text' => ' - b'], + ['text' => ''], + ], true); $this->suggestedPackagesReporter->output(SuggestedPackagesReporter::MODE_BY_PACKAGE); } @@ -180,18 +176,12 @@ class SuggestedPackagesReporterTest extends TestCase $this->suggestedPackagesReporter->addPackage('source', 'target1', "\x1b[1;37;42m Like us\r\non Facebook \x1b[0m"); $this->suggestedPackagesReporter->addPackage('source', 'target2', "Like us on Facebook"); - $expectedWrite = InstalledVersions::satisfies(new VersionParser(), 'symfony/console', '^4.4.37 || ~5.3.14 || ^5.4.3 || ^6.0.3') - ? ' - target2: \\Like us on Facebook\\' - : ' - target2: \\Like us on Facebook\\'; - - $this->io->expects($this->exactly(4)) - ->method('write') - ->withConsecutive( - ['source suggests:'], - [' - target1: [1;37;42m Like us on Facebook [0m'], - [$expectedWrite], - [''] - ); + $this->io->expects([ + ['text' => 'source suggests:'], + ['text' => ' - target1: [1;37;42m Like us on Facebook [0m'], + ['text' => ' - target2: Like us on Facebook'], + ['text' => ''], + ], true); $this->suggestedPackagesReporter->output(SuggestedPackagesReporter::MODE_BY_PACKAGE); } @@ -204,16 +194,14 @@ class SuggestedPackagesReporterTest extends TestCase $this->suggestedPackagesReporter->addPackage('a', 'b', 'c'); $this->suggestedPackagesReporter->addPackage('source package', 'target', 'because reasons'); - $this->io->expects($this->exactly(6)) - ->method('write') - ->withConsecutive( - ['a suggests:'], - [' - b: c'], - [''], - ['source package suggests:'], - [' - target: because reasons'], - [''] - ); + $this->io->expects([ + ['text' => 'a suggests:'], + ['text' => ' - b: c'], + ['text' => ''], + ['text' => 'source package suggests:'], + ['text' => ' - target: because reasons'], + ['text' => ''], + ], true); $this->suggestedPackagesReporter->output(SuggestedPackagesReporter::MODE_BY_PACKAGE); } @@ -245,13 +233,11 @@ class SuggestedPackagesReporterTest extends TestCase $this->suggestedPackagesReporter->addPackage('a', 'b', 'c'); $this->suggestedPackagesReporter->addPackage('source package', 'target', 'because reasons'); - $this->io->expects($this->exactly(3)) - ->method('write') - ->withConsecutive( - ['source package suggests:'], - [' - target: because reasons'], - [''] - ); + $this->io->expects([ + ['text' => 'source package suggests:'], + ['text' => ' - target: because reasons'], + ['text' => ''], + ], true); $this->suggestedPackagesReporter->output(SuggestedPackagesReporter::MODE_BY_PACKAGE, $repository); } diff --git a/tests/Composer/Test/Mock/IOMock.php b/tests/Composer/Test/Mock/IOMock.php new file mode 100644 index 000000000..f3e9f79e7 --- /dev/null +++ b/tests/Composer/Test/Mock/IOMock.php @@ -0,0 +1,196 @@ + + * Jordi Boggiano + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Composer\Test\Mock; + +use Composer\Config; +use Composer\IO\BufferIO; +use Composer\IO\IOInterface; +use Composer\Pcre\PcreException; +use Composer\Pcre\Preg; +use Composer\Util\HttpDownloader; +use Composer\Util\Http\Response; +use Composer\Downloader\TransportException; +use Composer\Util\Platform; +use LogicException; +use PHPUnit\Framework\Assert; +use PHPUnit\Framework\AssertionFailedError; +use Symfony\Component\Console\Output\OutputInterface; + +class IOMock extends BufferIO +{ + /** + * @var list|null + */ + private $expectations = null; + /** + * @var bool + */ + private $strict = false; + /** + * @var list + */ + private $authLog = []; + + /** + * @param IOInterface::* $verbosity + */ + public function __construct(int $verbosity) + { + $sfVerbosity = [ + self::QUIET => OutputInterface::VERBOSITY_QUIET, + self::NORMAL => OutputInterface::VERBOSITY_NORMAL, + self::VERBOSE => OutputInterface::VERBOSITY_VERBOSE, + self::VERY_VERBOSE => OutputInterface::VERBOSITY_VERY_VERBOSE, + self::DEBUG => OutputInterface::VERBOSITY_DEBUG, + ][$verbosity]; + parent::__construct('', $sfVerbosity); + } + + /** + * @param list $expectations + * @param bool $strict set to true if you want to provide *all* expected messages, and not just a subset you are interested in testing + */ + public function expects(array $expectations, bool $strict = false): void + { + $this->expectations = $expectations; + $inputs = []; + foreach ($expectations as $expect) { + if (isset($expect['ask'], $expect['reply'])) { + if (!is_string($expect['reply'])) { + throw new \LogicException('A question\'s reply must be a string, use empty string for null replies'); + } + $inputs[] = $expect['reply']; + } + } + + if (count($inputs) > 0) { + $this->setUserInputs($inputs); + } + + $this->strict = $strict; + } + + public function assertComplete(): void + { + $output = $this->getOutput(); + + if (Platform::getEnv('DEBUG_OUTPUT') === '1') { + echo PHP_EOL.'Collected output: '.$output.PHP_EOL; + } + + // this was not configured to expect anything, so no need to react here + if (!is_array($this->expectations)) { + return; + } + + if (count($this->expectations) > 0) { + $lines = Preg::split("{\r?\n}", $output); + + foreach ($this->expectations as $expect) { + if (isset($expect['auth'])) { + while (count($this->authLog) > 0) { + $auth = array_shift($this->authLog); + if ($auth === $expect['auth']) { + continue 2; + } + + if ($this->strict) { + throw new AssertionFailedError('IO authentication mismatch. Expected:'.PHP_EOL.json_encode($expect['auth']).PHP_EOL.'Got:'.PHP_EOL.json_encode($auth)); + } + } + + throw new AssertionFailedError('Expected "'.json_encode($expect['auth']).'" auth to be set but there are no setAuthentication calls left to consume.'); + } + + if (isset($expect['ask'], $expect['reply'])) { + $pattern = '{^'.preg_quote($expect['ask']).'$}'; + } elseif (isset($expect['regex']) && $expect['regex']) { + $pattern = $expect['text']; + } else { + $pattern = '{^'.preg_quote($expect['text']).'$}'; + } + + while (count($lines) > 0) { + $line = array_shift($lines); + try { + if (Preg::isMatch($pattern, $line)) { + continue 2; + } + } catch (PcreException $e) { + throw new LogicException('Invalid regex pattern in IO expectation "'.$pattern.'": '.$e->getMessage()); + } + + if ($this->strict) { + throw new AssertionFailedError('IO output mismatch. Expected:'.PHP_EOL.($expect['text'] ?? $expect['ask']).PHP_EOL.'Got:'.PHP_EOL.$line); + } + } + + throw new AssertionFailedError('Expected "'.($expect['text'] ?? $expect['ask']).'" to be output still but there is no output left to consume. Complete output:'.PHP_EOL.$output); + } + } elseif ($output !== '' && $this->strict) { + throw new AssertionFailedError('There was strictly no output expected but some output occurred: '.$output); + } + + // dummy assertion to ensure the test is not marked as having no assertions + Assert::assertTrue(true); // @phpstan-ignore-line + } + + /** + * @inheritDoc + */ + public function ask($question, $default = null) + { + return parent::ask(rtrim($question, "\r\n").PHP_EOL, $default); + } + + /** + * @inheritDoc + */ + public function askConfirmation($question, $default = true) + { + return parent::askConfirmation(rtrim($question, "\r\n").PHP_EOL, $default); + } + + /** + * @inheritDoc + */ + public function askAndValidate($question, $validator, $attempts = null, $default = null) + { + return parent::askAndValidate(rtrim($question, "\r\n").PHP_EOL, $validator, $attempts, $default); + } + + /** + * @inheritDoc + */ + public function askAndHideAnswer($question) + { + // do not hide answer in tests because that blocks on windows with hiddeninput.exe + return parent::ask(rtrim($question, "\r\n").PHP_EOL); + } + + /** + * @inheritDoc + */ + public function select($question, $choices, $default, $attempts = false, $errorMessage = 'Value "%s" is invalid', $multiselect = false) + { + return parent::select(rtrim($question, "\r\n").PHP_EOL, $choices, $default, $attempts, $errorMessage, $multiselect); + } + + public function setAuthentication($repositoryName, $username, $password = null) + { + $this->authentications[$repositoryName] = ['username' => $username, 'password' => $password]; + $this->authLog[] = [$repositoryName, $username, $password]; + + parent::setAuthentication($repositoryName, $username, $password); + } +} diff --git a/tests/Composer/Test/Repository/Vcs/GitDriverTest.php b/tests/Composer/Test/Repository/Vcs/GitDriverTest.php index 5dd57dead..b54e29ad1 100644 --- a/tests/Composer/Test/Repository/Vcs/GitDriverTest.php +++ b/tests/Composer/Test/Repository/Vcs/GitDriverTest.php @@ -55,7 +55,7 @@ class GitDriverTest extends TestCase public function testGetRootIdentifierFromRemoteLocalRepository(): void { $process = $this->getProcessExecutorMock(); - $io = $this->getMockBuilder(IOInterface::class)->getMock(); + $io = $this->getIOMock(); $driver = new GitDriver(['url' => $this->home], $io, $this->config, $this->getHttpDownloaderMock(), $process); $this->setRepoDir($driver, $this->home); @@ -82,11 +82,9 @@ GIT; public function testGetRootIdentifierFromRemote(): void { $process = $this->getProcessExecutorMock(); - $io = $this->getMockBuilder(IOInterface::class)->getMock(); + $io = $this->getIOMock(); - $io - ->expects($this->never()) - ->method('writeError'); + $io->expects([], true); $driver = new GitDriver(['url' => 'https://example.org/acme.git'], $io, $this->config, $this->getHttpDownloaderMock(), $process); $this->setRepoDir($driver, $this->home); @@ -119,7 +117,7 @@ GIT; Platform::putEnv('COMPOSER_DISABLE_NETWORK', '1'); $process = $this->getProcessExecutorMock(); - $io = $this->getMockBuilder(IOInterface::class)->getMock(); + $io = $this->getIOMock(); $driver = new GitDriver(['url' => 'https://example.org/acme.git'], $io, $this->config, $this->getHttpDownloaderMock(), $process); $this->setRepoDir($driver, $this->home); diff --git a/tests/Composer/Test/TestCase.php b/tests/Composer/Test/TestCase.php index 6dcc04510..6a2d7ce86 100644 --- a/tests/Composer/Test/TestCase.php +++ b/tests/Composer/Test/TestCase.php @@ -24,6 +24,7 @@ use Composer\Package\PackageInterface; use Composer\Semver\Constraint\Constraint; use Composer\Test\Mock\FactoryMock; use Composer\Test\Mock\HttpDownloaderMock; +use Composer\Test\Mock\IOMock; use Composer\Test\Mock\ProcessExecutorMock; use Composer\Util\Filesystem; use Composer\Util\Platform; @@ -59,6 +60,10 @@ abstract class TestCase extends \PHPUnit\Framework\TestCase * @var list */ private $processExecutorMocks = []; + /** + * @var list + */ + private $ioMocks = []; /** * @var list */ @@ -75,6 +80,9 @@ abstract class TestCase extends \PHPUnit\Framework\TestCase foreach ($this->processExecutorMocks as $mock) { $mock->assertComplete(); } + foreach ($this->ioMocks as $mock) { + $mock->assertComplete(); + } if (null !== $this->prevCwd) { chdir($this->prevCwd); @@ -181,7 +189,7 @@ abstract class TestCase extends \PHPUnit\Framework\TestCase { $factory = new FactoryMock(); - $locker = new Locker($this->getMockBuilder(IOInterface::class)->getMock(), new JsonFile('./composer.lock'), $factory->createInstallationManager(), (string) file_get_contents('./composer.json')); + $locker = new Locker($this->getIOMock(), new JsonFile('./composer.lock'), $factory->createInstallationManager(), (string) file_get_contents('./composer.json')); $locker->setLockData($packages, $devPackages, [], [], [], 'dev', [], false, false, []); } @@ -356,6 +364,16 @@ abstract class TestCase extends \PHPUnit\Framework\TestCase return $mock; } + /** + * @param IOInterface::* $verbosity + */ + protected function getIOMock(int $verbosity = IOInterface::DEBUG): IOMock + { + $this->ioMocks[] = $mock = new IOMock($verbosity); + + return $mock; + } + protected function createTempFile(?string $dir = null): string { $dir = $dir ?? sys_get_temp_dir(); diff --git a/tests/Composer/Test/Util/BitbucketTest.php b/tests/Composer/Test/Util/BitbucketTest.php index bd380b87b..dfaac651d 100644 --- a/tests/Composer/Test/Util/BitbucketTest.php +++ b/tests/Composer/Test/Util/BitbucketTest.php @@ -12,6 +12,7 @@ namespace Composer\Test\Util; +use Composer\Test\Mock\IOMock; use Composer\Util\Bitbucket; use Composer\Util\Http\Response; use Composer\Test\TestCase; @@ -36,7 +37,7 @@ class BitbucketTest extends TestCase /** @var string */ private $token = 'bitbuckettoken'; - /** @var \Composer\IO\ConsoleIO&\PHPUnit\Framework\MockObject\MockObject */ + /** @var IOMock */ private $io; /** @var \Composer\Util\HttpDownloader&\PHPUnit\Framework\MockObject\MockObject */ private $httpDownloader; @@ -49,11 +50,7 @@ class BitbucketTest extends TestCase protected function setUp(): void { - $this->io = $this - ->getMockBuilder('Composer\IO\ConsoleIO') - ->disableOriginalConstructor() - ->getMock() - ; + $this->io = $this->getIOMock(); $this->httpDownloader = $this ->getMockBuilder('Composer\Util\HttpDownloader') @@ -70,9 +67,9 @@ class BitbucketTest extends TestCase public function testRequestAccessTokenWithValidOAuthConsumer(): void { - $this->io->expects($this->once()) - ->method('setAuthentication') - ->with($this->origin, $this->consumer_key, $this->consumer_secret); + $this->io->expects([ + ['auth' => [$this->origin, $this->consumer_key, $this->consumer_secret]], + ]); $this->httpDownloader->expects($this->once()) ->method('get') @@ -151,9 +148,9 @@ class BitbucketTest extends TestCase ] ); - $this->io->expects($this->once()) - ->method('setAuthentication') - ->with($this->origin, $this->consumer_key, $this->consumer_secret); + $this->io->expects([ + ['auth' => [$this->origin, $this->consumer_key, $this->consumer_secret]], + ]); $this->httpDownloader->expects($this->once()) ->method('get') @@ -189,19 +186,14 @@ class BitbucketTest extends TestCase public function testRequestAccessTokenWithUsernameAndPassword(): void { - $this->io->expects($this->once()) - ->method('setAuthentication') - ->with($this->origin, $this->username, $this->password); - - $this->io->expects($this->any()) - ->method('writeError') - ->withConsecutive( - ['Invalid OAuth consumer provided.'], - ['This can have three reasons:'], - ['1. You are authenticating with a bitbucket username/password combination'], - ['2. You are using an OAuth consumer, but didn\'t configure a (dummy) callback url'], - ['3. You are using an OAuth consumer, but didn\'t configure it as private consumer'] - ); + $this->io->expects([ + ['auth' => [$this->origin, $this->username, $this->password]], + ['text' => 'Invalid OAuth consumer provided.'], + ['text' => 'This can have three reasons:'], + ['text' => '1. You are authenticating with a bitbucket username/password combination'], + ['text' => '2. You are using an OAuth consumer, but didn\'t configure a (dummy) callback url'], + ['text' => '3. You are using an OAuth consumer, but didn\'t configure it as private consumer'], + ], true); $this->httpDownloader->expects($this->once()) ->method('get') @@ -240,16 +232,11 @@ class BitbucketTest extends TestCase ->with('bitbucket-oauth') ->willReturn(null); - $this->io->expects($this->once()) - ->method('setAuthentication') - ->with($this->origin, $this->username, $this->password); - - $this->io->expects($this->any()) - ->method('writeError') - ->withConsecutive( - ['Invalid OAuth consumer provided.'], - ['You can also add it manually later by using "composer config --global --auth bitbucket-oauth.bitbucket.org "'] - ); + $this->io->expects([ + ['auth' => [$this->origin, $this->username, $this->password]], + ['text' => 'Invalid OAuth consumer provided.'], + ['text' => 'You can also add it manually later by using "composer config --global --auth bitbucket-oauth.bitbucket.org "'], + ], true); $this->httpDownloader->expects($this->once()) ->method('get') @@ -276,9 +263,9 @@ class BitbucketTest extends TestCase ->with('bitbucket-oauth') ->willReturn(null); - $this->io->expects($this->once()) - ->method('setAuthentication') - ->with($this->origin, $this->username, $this->password); + $this->io->expects([ + ['auth' => [$this->origin, $this->username, $this->password]], + ]); $exception = new \Composer\Downloader\TransportException('HTTP/1.1 404 NOT FOUND', 404); $this->httpDownloader->expects($this->once()) @@ -300,19 +287,11 @@ class BitbucketTest extends TestCase public function testUsernamePasswordAuthenticationFlow(): void { - $this->io - ->expects($this->atLeastOnce()) - ->method('writeError') - ->withConsecutive([$this->message]) - ; - - $this->io->expects($this->exactly(2)) - ->method('askAndHideAnswer') - ->withConsecutive( - ['Consumer Key (hidden): '], - ['Consumer Secret (hidden): '] - ) - ->willReturnOnConsecutiveCalls($this->consumer_key, $this->consumer_secret); + $this->io->expects([ + ['text' => $this->message], + ['ask' => 'Consumer Key (hidden): ', 'reply' => $this->consumer_key], + ['ask' => 'Consumer Secret (hidden): ', 'reply' => $this->consumer_secret], + ]); $this->httpDownloader ->expects($this->once()) @@ -346,10 +325,9 @@ class BitbucketTest extends TestCase ->method('getAuthConfigSource') ->willReturn($authConfigSourceMock); - $this->io->expects($this->once()) - ->method('askAndHideAnswer') - ->with('Consumer Key (hidden): ') - ->willReturnOnConsecutiveCalls(null); + $this->io->expects([ + ['ask' => 'Consumer Key (hidden): ', 'reply' => ''], + ]); $this->assertFalse($this->bitbucket->authorizeOAuthInteractively($this->origin, $this->message)); } @@ -361,13 +339,11 @@ class BitbucketTest extends TestCase ->method('getAuthConfigSource') ->willReturn($authConfigSourceMock); - $this->io->expects($this->exactly(2)) - ->method('askAndHideAnswer') - ->withConsecutive( - ['Consumer Key (hidden): '], - ['Consumer Secret (hidden): '] - ) - ->willReturnOnConsecutiveCalls($this->consumer_key, null); + $this->io->expects([ + ['text' => $this->message], + ['ask' => 'Consumer Key (hidden): ', 'reply' => $this->consumer_key], + ['ask' => 'Consumer Secret (hidden): ', 'reply' => ''], + ]); $this->assertFalse($this->bitbucket->authorizeOAuthInteractively($this->origin, $this->message)); } @@ -379,13 +355,11 @@ class BitbucketTest extends TestCase ->method('getAuthConfigSource') ->willReturn($authConfigSourceMock); - $this->io->expects($this->exactly(2)) - ->method('askAndHideAnswer') - ->withConsecutive( - ['Consumer Key (hidden): '], - ['Consumer Secret (hidden): '] - ) - ->willReturnOnConsecutiveCalls($this->consumer_key, $this->consumer_secret); + $this->io->expects([ + ['text' => $this->message], + ['ask' => 'Consumer Key (hidden): ', 'reply' => $this->consumer_key], + ['ask' => 'Consumer Secret (hidden): ', 'reply' => $this->consumer_secret], + ]); $this->httpDownloader ->expects($this->once()) diff --git a/tests/Composer/Test/Util/GitHubTest.php b/tests/Composer/Test/Util/GitHubTest.php index 72b2e1c2c..d9e0e5412 100644 --- a/tests/Composer/Test/Util/GitHubTest.php +++ b/tests/Composer/Test/Util/GitHubTest.php @@ -30,17 +30,10 @@ class GitHubTest extends TestCase public function testUsernamePasswordAuthenticationFlow(): void { $io = $this->getIOMock(); - $io - ->expects($this->atLeastOnce()) - ->method('writeError') - ->withConsecutive([$this->message]) - ; - $io - ->expects($this->once()) - ->method('askAndHideAnswer') - ->with('Token (hidden): ') - ->willReturn($this->password) - ; + $io->expects([ + ['text' => $this->message], + ['ask' => 'Token (hidden): ', 'reply' => $this->password], + ]); $httpDownloader = $this->getHttpDownloaderMock(); $httpDownloader->expects( @@ -68,12 +61,9 @@ class GitHubTest extends TestCase public function testUsernamePasswordFailure(): void { $io = $this->getIOMock(); - $io - ->expects($this->exactly(1)) - ->method('askAndHideAnswer') - ->with('Token (hidden): ') - ->willReturn($this->password) - ; + $io->expects([ + ['ask' => 'Token (hidden): ', 'reply' => $this->password], + ]); $httpDownloader = $this->getHttpDownloaderMock(); $httpDownloader->expects( @@ -93,20 +83,6 @@ class GitHubTest extends TestCase $this->assertFalse($github->authorizeOAuthInteractively($this->origin)); } - /** - * @return \PHPUnit\Framework\MockObject\MockObject&\Composer\IO\ConsoleIO - */ - private function getIOMock() - { - $io = $this - ->getMockBuilder('Composer\IO\ConsoleIO') - ->disableOriginalConstructor() - ->getMock() - ; - - return $io; - } - /** * @return \PHPUnit\Framework\MockObject\MockObject&\Composer\Config */ diff --git a/tests/Composer/Test/Util/GitLabTest.php b/tests/Composer/Test/Util/GitLabTest.php index 1ce746176..5c8cc9443 100644 --- a/tests/Composer/Test/Util/GitLabTest.php +++ b/tests/Composer/Test/Util/GitLabTest.php @@ -36,23 +36,11 @@ class GitLabTest extends TestCase public function testUsernamePasswordAuthenticationFlow(): void { $io = $this->getIOMock(); - $io - ->expects($this->atLeastOnce()) - ->method('writeError') - ->withConsecutive([$this->message]) - ; - $io - ->expects($this->once()) - ->method('ask') - ->with('Username: ') - ->willReturn($this->username) - ; - $io - ->expects($this->once()) - ->method('askAndHideAnswer') - ->with('Password: ') - ->willReturn($this->password) - ; + $io->expects([ + ['text' => $this->message], + ['ask' => 'Username: ', 'reply' => $this->username], + ['ask' => 'Password: ', 'reply' => $this->password], + ]); $httpDownloader = $this->getHttpDownloaderMock(); $httpDownloader->expects( @@ -77,18 +65,18 @@ class GitLabTest extends TestCase self::expectException('RuntimeException'); self::expectExceptionMessage('Invalid GitLab credentials 5 times in a row, aborting.'); $io = $this->getIOMock(); - $io - ->expects($this->exactly(5)) - ->method('ask') - ->with('Username: ') - ->willReturn($this->username) - ; - $io - ->expects($this->exactly(5)) - ->method('askAndHideAnswer') - ->with('Password: ') - ->willReturn($this->password) - ; + $io->expects([ + ['ask' => 'Username: ', 'reply' => $this->username], + ['ask' => 'Password: ', 'reply' => $this->password], + ['ask' => 'Username: ', 'reply' => $this->username], + ['ask' => 'Password: ', 'reply' => $this->password], + ['ask' => 'Username: ', 'reply' => $this->username], + ['ask' => 'Password: ', 'reply' => $this->password], + ['ask' => 'Username: ', 'reply' => $this->username], + ['ask' => 'Password: ', 'reply' => $this->password], + ['ask' => 'Username: ', 'reply' => $this->username], + ['ask' => 'Password: ', 'reply' => $this->password], + ]); $httpDownloader = $this->getHttpDownloaderMock(); $httpDownloader->expects( @@ -114,20 +102,6 @@ class GitLabTest extends TestCase $gitLab->authorizeOAuthInteractively('https', $this->origin); } - /** - * @return \PHPUnit\Framework\MockObject\MockObject&\Composer\IO\ConsoleIO - */ - private function getIOMock() - { - $io = $this - ->getMockBuilder('Composer\IO\ConsoleIO') - ->disableOriginalConstructor() - ->getMock() - ; - - return $io; - } - /** * @return \PHPUnit\Framework\MockObject\MockObject&\Composer\Config */