From b014c771605b331a5b059846c451b8c08dd16d0e Mon Sep 17 00:00:00 2001 From: Luca Tumedei Date: Tue, 22 Aug 2023 09:25:41 +0200 Subject: [PATCH] fix(Command) add command to download Chromedriver --- codeception.dist.yml | 1 + composer.json | 1 + src/Command/ChromedriverUpdate.php | 73 ++++ src/Project/ContentProject.php | 9 +- src/Project/SetupTemplateTrait.php | 30 -- src/Project/SiteProject.php | 9 +- src/Utils/ChromedriverInstaller.php | 319 ++++++++++++++++++ tests/_data/bins/chrome-mock | 4 + tests/_data/bins/chrome-version-not-string | 3 + tests/_data/bins/chrome-version-wrong-format | 4 + .../Command/ChromedriverUpdateTest.php | 40 +++ .../Utils/ChromedriverInstallerTest.php | 304 +++++++++++++++++ 12 files changed, 761 insertions(+), 36 deletions(-) create mode 100644 src/Command/ChromedriverUpdate.php delete mode 100644 src/Project/SetupTemplateTrait.php create mode 100644 src/Utils/ChromedriverInstaller.php create mode 100755 tests/_data/bins/chrome-mock create mode 100755 tests/_data/bins/chrome-version-not-string create mode 100755 tests/_data/bins/chrome-version-wrong-format create mode 100644 tests/unit/lucatume/WPBrowser/Command/ChromedriverUpdateTest.php create mode 100644 tests/unit/lucatume/WPBrowser/Utils/ChromedriverInstallerTest.php diff --git a/codeception.dist.yml b/codeception.dist.yml index 2b6e96a12..300f80631 100644 --- a/codeception.dist.yml +++ b/codeception.dist.yml @@ -23,3 +23,4 @@ extensions: - "lucatume\\WPBrowser\\Command\\RunOriginal" - "lucatume\\WPBrowser\\Command\\RunAll" - "lucatume\\WPBrowser\\Command\\GenerateWPUnit" + - "lucatume\\WPBrowser\\Command\\ChromedriverUpdate" diff --git a/composer.json b/composer.json index 66c9ba16e..2a4029410 100644 --- a/composer.json +++ b/composer.json @@ -24,6 +24,7 @@ "ext-fileinfo": "*", "ext-json": "*", "ext-curl": "*", + "ext-zip": "*", "composer-runtime-api": "^2.2", "codeception/codeception": "^5.0", "codeception/module-asserts": "^2.0 || ^3.0", diff --git a/src/Command/ChromedriverUpdate.php b/src/Command/ChromedriverUpdate.php new file mode 100644 index 000000000..f2cd92891 --- /dev/null +++ b/src/Command/ChromedriverUpdate.php @@ -0,0 +1,73 @@ +setDescription('Updates the Chromedriver binary.') + ->addArgument( + 'version', + InputArgument::OPTIONAL, + 'The version of Chrome to install chromedriver for.', + '' + )->addOption( + 'platform', + '', + InputOption::VALUE_REQUIRED, + 'The platform to install Chromedriver for.', + '' + )->addOption( + 'binary', + '', + InputOption::VALUE_REQUIRED, + 'The path to the Chrome binary to download Chromedriver for.', + false + ); + } + + /** + * @throws JsonException + */ + public function execute(InputInterface $input, OutputInterface $output) + { + $platform = $input->getOption('platform') ?: null; + + if ($platform && !is_string($platform)) { + throw new InvalidArgumentException('The platform option must be a string.'); + } + + $version = $input->getArgument('version') ?: null; + + if ($version && !is_string($version)) { + throw new InvalidArgumentException('The version argument must be a string.'); + } + + $binary = $input->getOption('binary') ?: null; + + if ($binary && !is_string($binary)) { + throw new InvalidArgumentException('The binary option must be a string.'); + } + + $chromedriverInstaller = new ChromedriverInstaller($version, $platform, $binary, $output); + $chromedriverInstaller->install(); + + return 0; + } +} diff --git a/src/Project/ContentProject.php b/src/Project/ContentProject.php index f5c80ade3..b87c84925 100644 --- a/src/Project/ContentProject.php +++ b/src/Project/ContentProject.php @@ -4,6 +4,7 @@ use Closure; use Codeception\InitTemplate; +use lucatume\WPBrowser\Command\ChromedriverUpdate; use lucatume\WPBrowser\Command\DevInfo; use lucatume\WPBrowser\Command\DevRestart; use lucatume\WPBrowser\Command\DevStart; @@ -11,6 +12,7 @@ use lucatume\WPBrowser\Exceptions\RuntimeException; use lucatume\WPBrowser\Extension\BuiltInServerController; use lucatume\WPBrowser\Extension\ChromeDriverController; +use lucatume\WPBrowser\Utils\ChromedriverInstaller; use lucatume\WPBrowser\Utils\Filesystem as FS; use lucatume\WPBrowser\Utils\Random; use lucatume\WPBrowser\WordPress\Database\SQLiteDatabase; @@ -20,8 +22,6 @@ abstract class ContentProject extends InitTemplate implements ProjectInterface { - use SetupTemplateTrait; - protected TestEnvironment $testEnvironment; abstract protected function getProjectType(): string; @@ -123,7 +123,9 @@ public function setup(): void } $this->sayInfo('Created database dump in tests/Support/Data/dump.sql.'); - $this->addChromedriverDevDependency(); + $this->sayInfo('Installing Chromedriver ...'); + $chromedriverPath = (new ChromedriverInstaller())->install(); + $this->sayInfo("Chromedriver installed in $chromedriverPath"); $chromedriverPort = Random::openLocalhostPort(); $this->testEnvironment->testTablePrefix = 'test_'; $this->testEnvironment->wpTablePrefix = 'wp_'; @@ -157,6 +159,7 @@ public function setup(): void $this->testEnvironment->customCommands[] = DevStop::class; $this->testEnvironment->customCommands[] = DevInfo::class; $this->testEnvironment->customCommands[] = DevRestart::class; + $this->testEnvironment->customCommands[] = ChromedriverUpdate::class; $this->testEnvironment->wpRootDir = FS::relativePath($this->workDir, $wpRootDir); $this->testEnvironment->dbUrl = 'sqlite://%codecept_root_dir%/tests/_wordpress/data/db.sqlite'; diff --git a/src/Project/SetupTemplateTrait.php b/src/Project/SetupTemplateTrait.php deleted file mode 100644 index a8ddee4fc..000000000 --- a/src/Project/SetupTemplateTrait.php +++ /dev/null @@ -1,30 +0,0 @@ -sayInfo('Adding Chromedriver binary as a development dependency ...'); - $composer = new Composer($this->workDir . '/composer.json'); - $composer->requireDev(['webdriver-binary/binary-chromedriver' => '*']); - $composer->allowPluginsFromPackage('webdriver-binary/binary-chromedriver'); - $composer->update('webdriver-binary/binary-chromedriver'); - } -} diff --git a/src/Project/SiteProject.php b/src/Project/SiteProject.php index 29929da41..2cca7985d 100644 --- a/src/Project/SiteProject.php +++ b/src/Project/SiteProject.php @@ -3,6 +3,7 @@ namespace lucatume\WPBrowser\Project; use Codeception\InitTemplate; +use lucatume\WPBrowser\Command\ChromedriverUpdate; use lucatume\WPBrowser\Command\DevInfo; use lucatume\WPBrowser\Command\DevRestart; use lucatume\WPBrowser\Command\DevStart; @@ -10,6 +11,7 @@ use lucatume\WPBrowser\Exceptions\RuntimeException; use lucatume\WPBrowser\Extension\BuiltInServerController; use lucatume\WPBrowser\Extension\ChromeDriverController; +use lucatume\WPBrowser\Utils\ChromedriverInstaller; use lucatume\WPBrowser\Utils\Filesystem as FS; use lucatume\WPBrowser\Utils\Random; use lucatume\WPBrowser\WordPress\Database\SQLiteDatabase; @@ -22,8 +24,6 @@ class SiteProject extends InitTemplate implements ProjectInterface { - use SetupTemplateTrait; - private Installation $installation; private TestEnvironment $testEnvironment; @@ -117,7 +117,9 @@ public function setup(): void } $this->sayInfo('Created database dump in tests/Support/Data/dump.sql.'); - $this->addChromedriverDevDependency(); + $this->sayInfo('Installing Chromedriver ...'); + $chromedriverPath = (new ChromedriverInstaller())->install(); + $this->sayInfo("Chromedriver installed in $chromedriverPath"); $chromedriverPort = Random::openLocalhostPort(); $this->testEnvironment->testTablePrefix = 'test_'; $this->testEnvironment->wpTablePrefix = 'wp_'; @@ -152,6 +154,7 @@ public function setup(): void $this->testEnvironment->customCommands[] = DevStop::class; $this->testEnvironment->customCommands[] = DevInfo::class; $this->testEnvironment->customCommands[] = DevRestart::class; + $this->testEnvironment->customCommands[] = ChromedriverUpdate::class; $this->testEnvironment->wpRootDir = '.'; $this->testEnvironment->dbUrl = 'sqlite://%codecept_root_dir%/tests/Support/Data/db.sqlite'; diff --git a/src/Utils/ChromedriverInstaller.php b/src/Utils/ChromedriverInstaller.php new file mode 100644 index 000000000..03448b750 --- /dev/null +++ b/src/Utils/ChromedriverInstaller.php @@ -0,0 +1,319 @@ +output = $output ?? new NullOutput(); + + $platform = $platform ?? $this->detectPlatform(); + $this->platform = $this->checkPlatform($platform); + + $this->output->writeln("Platform: $platform"); + + $binary = $binary ?? $this->detectBinary(); + $this->binary = $this->checkBinary($binary); + + $this->output->writeln("Binary: $binary"); + + $version = $version ?? $this->detectVersion(); + $this->version = $this->checkVersion($version); + + $this->output->writeln("Version: $version"); + } + + /** + * @throws JsonException + */ + public function install(string $dir = null): string + { + if ($dir === null) { + global $_composer_bin_dir; + $dir = $_composer_bin_dir; + } + + if (!is_dir($dir)) { + throw new InvalidArgumentException( + "The directory $dir does not exist.", + self::ERR_DESTINATION_NOT_DIR + ); + } + + $this->output->writeln("Fetching Chromedriver version URL ..."); + + $downloadUrl = $this->fetchChromedriverVersionUrl(); + $zipFilePathname = rtrim(sys_get_temp_dir(), '\\/') . '/' . basename($downloadUrl); + + if (is_file($zipFilePathname) && !unlink($zipFilePathname)) { + throw new RuntimeException( + "Could not remove existing zip file $zipFilePathname", + self::ERR_REMOVE_EXISTING_ZIP_FILE + ); + } + + $zipFilePathname = Download::fileFromUrl($downloadUrl, $zipFilePathname); + $this->output->writeln('Downloaded Chromedriver to ' . $zipFilePathname); + + $executableFileName = $dir . '/' . $this->getExecutableFileName(); + + if (is_file($executableFileName) && !unlink($executableFileName)) { + throw new RuntimeException( + "Could not remove existing executable file $executableFileName", + self::ERR_REMOVE_EXISTING_BINARY + ); + } + + $extractedPath = Zip::extractTo($zipFilePathname, sys_get_temp_dir()); + + if (!rename( + "$extractedPath/chromedriver-$this->platform/" . $this->getExecutableFileName(), + $executableFileName + )) { + throw new RuntimeException( + "Could not move Chromedriver to $executableFileName", + self::ERR_MOVE_BINARY + ); + } + + if (chmod($executableFileName, 0755) === false) { + throw new RuntimeException( + "Could not make Chromedriver executable", + self::ERR_BINARY_CHMOD + ); + } + + $this->output->writeln("Installed Chromedriver to $executableFileName"); + + return $executableFileName; + } + + /** + * @throws RuntimeException + */ + private function detectVersion(): string + { + $chromeVersion = exec($this->binary . ' --version'); + if (!($chromeVersion && is_string($chromeVersion))) { + throw new RuntimeException( + "Could not detect Chrome version from $this->binary", + self::ERR_VERSION_NOT_STRING + ); + } + + return $chromeVersion; + } + + /** + * @throws RuntimeException + */ + private function detectPlatform(): string + { + // Return one of `linux64`, `mac-arm64`,`mac-x64`, `win32`, `win64`. + $system = php_uname('s'); + $arch = php_uname('m'); + + if ($system === 'Darwin') { + if ($arch === 'arm64') { + return 'mac-arm64'; + } + + return 'mac-x64'; + } + + if ($system === 'Linux') { + return 'linux64'; + } + + if ($system === 'Windows NT') { + if ($arch === 'x86_64') { + return 'win64'; + } + + return 'win32'; + } + + throw new RuntimeException('Failed to detect platform.', self::ERR_DETECT_PLATFORM); + } + + /** + * @return 'linux64'|'mac-x64'|'mac-arm64'|'win32'|'win64' + * + * @throws RuntimeException + */ + private function checkPlatform(mixed $platform): string + { + if (!(is_string($platform) && in_array($platform, [ + 'linux64', + 'mac-arm64', + 'mac-x64', + 'win32', + 'win64' + ]))) { + throw new RuntimeException( + 'Invalid platform, supported platforms are: linux64, mac-arm64, mac-x64, win32, win64.', + self::ERR_UNSUPPORTED_PLATFORM + ); + } + + /** @var 'linux64'|'mac-x64'|'mac-arm64'|'win32'|'win64' $platform */ + return $platform; + } + + /** + * @throws RuntimeException + */ + private function detectBinary(): string + { + return match ($this->platform) { + 'linux64' => '/usr/bin/google-chrome', + 'mac-x64', 'mac-arm64' => '/Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome', + 'win32', 'win64' => 'C:\\\\Program Files (x86)\\\\Google\\\\Chrome\\\\Application\\\\chrome.exe' + }; + } + + private function checkBinary(mixed $binary): string + { + // Replace escaped spaces with spaces to check the binary. + if (!(is_string($binary) && is_executable(str_replace('\ ', ' ', $binary)))) { + throw new RuntimeException( + "Invalid Chrome binary: not executable or not existing.", + self::ERR_INVALID_BINARY + ); + } + + return $binary; + } + + private function checkVersion(mixed $version): string + { + $matches = []; + if (!(is_string($version) && preg_match('/^.*?(?\d+\.\d+\.\d+\.\d+)$/', $version, $matches))) { + throw new RuntimeException( + "Invalid Chrome version: must be in the form X.Y.Z.W.", + self::ERR_INVALID_VERSION_FORMAT + ); + } + + return $matches['version']; + } + + /** + * @throws JsonException + */ + private function fetchChromedriverVersionUrl(): string + { + $knownGoodVersionsWithDownloads = file_get_contents( + 'https://googlechromelabs.github.io/chrome-for-testing/known-good-versions-with-downloads.json' + ); + + if ($knownGoodVersionsWithDownloads === false) { + throw new RuntimeException( + 'Failed to fetch known good Chrome and Chromedriver versions with downloads.', + self::ERR_FETCH_KNOWN_GOOD_VERSIONS_WITH_DOWNLOADS + ); + } + + $decoded = json_decode($knownGoodVersionsWithDownloads, true, 512, JSON_THROW_ON_ERROR); + + if (!( + is_array($decoded) + && isset($decoded['versions']) + && is_array($decoded['versions']) + )) { + throw new RuntimeException( + 'Failed to decode known good Chrome and Chromedriver versions with downloads.', + self::ERR_DECODE_KNOWN_GOOD_VERSIONS_WITH_DOWNLOADS + ); + } + + foreach ($decoded['versions'] as $versionData) { + if (!( + is_array($versionData) + && isset($versionData['version'], $versionData['downloads']) + && $versionData['version'] === $this->version + && is_array($versionData['downloads']) + && isset($versionData['downloads']['chromedriver']) + && is_array($versionData['downloads']['chromedriver']) + )) { + continue; + } + + foreach ($versionData['downloads']['chromedriver'] as $chromedriverDownload) { + if (!( + is_array($chromedriverDownload) + && isset($chromedriverDownload['platform'], $chromedriverDownload['url']) + && $chromedriverDownload['platform'] === $this->platform + && is_string($chromedriverDownload['url']) + ) + ) { + continue; + } + + return $chromedriverDownload['url']; + } + + break; + } + + throw new RuntimeException( + 'Failed to find a download URL for Chromedriver version ' . $this->version, + self::ERR_DOWNLOAD_URL_NOT_FOUND + ); + } + + private function getExecutableFileName(): string + { + return match ($this->platform) { + 'linux64', 'mac-x64', 'mac-arm64' => 'chromedriver', + 'win32', 'win64' => 'chromedriver.exe' + }; + } + + public function getVersion(): string + { + return $this->version; + } + + public function getBinary(): string + { + return $this->binary; + } + + public function getPlatform(): string + { + return $this->platform; + } +} diff --git a/tests/_data/bins/chrome-mock b/tests/_data/bins/chrome-mock new file mode 100755 index 000000000..7c7d413c9 --- /dev/null +++ b/tests/_data/bins/chrome-mock @@ -0,0 +1,4 @@ +#! /usr/bin/env sh + +echo 'Google Chrome 116.0.5845.96' +exit 0 diff --git a/tests/_data/bins/chrome-version-not-string b/tests/_data/bins/chrome-version-not-string new file mode 100755 index 000000000..10af03b01 --- /dev/null +++ b/tests/_data/bins/chrome-version-not-string @@ -0,0 +1,3 @@ +#! /usr/bin/env sh + +exit 0 diff --git a/tests/_data/bins/chrome-version-wrong-format b/tests/_data/bins/chrome-version-wrong-format new file mode 100755 index 000000000..2d5741aa7 --- /dev/null +++ b/tests/_data/bins/chrome-version-wrong-format @@ -0,0 +1,4 @@ +#! /usr/bin/env sh + +echo 'Google Chrome 116.lorem.5845.96' +exit 0 diff --git a/tests/unit/lucatume/WPBrowser/Command/ChromedriverUpdateTest.php b/tests/unit/lucatume/WPBrowser/Command/ChromedriverUpdateTest.php new file mode 100644 index 000000000..cda774cd0 --- /dev/null +++ b/tests/unit/lucatume/WPBrowser/Command/ChromedriverUpdateTest.php @@ -0,0 +1,40 @@ +expectException(InvalidArgumentException::class); + $this->expectExceptionMessage('The platform option must be a string.'); + + $command = new ChromedriverUpdate(); + $command->execute(new ArrayInput(['--platform' => 23], $command->getDefinition()), new NullOutput()); + } + + /** + * It should throw if specified platform is not supported + * + * @test + */ + public function should_throw_if_specified_platform_is_not_supported(): void + { + $this->expectException(RuntimeException::class); + $this->expectExceptionMessage('Invalid platform, supported platforms are: linux64, mac-arm64, mac-x64, win32, win64.'); + + $command = new ChromedriverUpdate(); + $command->execute(new ArrayInput(['--platform' => 'loremx86'], $command->getDefinition()), new NullOutput()); + } +} diff --git a/tests/unit/lucatume/WPBrowser/Utils/ChromedriverInstallerTest.php b/tests/unit/lucatume/WPBrowser/Utils/ChromedriverInstallerTest.php new file mode 100644 index 000000000..4f12f4ee6 --- /dev/null +++ b/tests/unit/lucatume/WPBrowser/Utils/ChromedriverInstallerTest.php @@ -0,0 +1,304 @@ +uopzSetFunctionReturn('php_uname', 'Lorem'); + + $this->expectException(RuntimeException::class); + $this->expectExceptionCode(ChromedriverInstaller::ERR_DETECT_PLATFORM); + + new ChromedriverInstaller(); + } + + /** + * It should throw if specified platform is not supported + * + * @test + */ + public function should_throw_if_specified_platform_is_not_supported(): void + { + $this->expectException(RuntimeException::class); + $this->expectExceptionCode(ChromedriverInstaller::ERR_UNSUPPORTED_PLATFORM); + + new ChromedriverInstaller('1.2.3.4', 'Lorem86'); + } + + /** + * It should throw if binary cannot be found + * + * @test + */ + public function should_throw_if_binary_cannot_be_found(): void + { + $this->uopzSetFunctionReturn('is_executable', function (string $file): bool { + return !str_contains($file, 'chrome') && is_executable($file); + }, true); + + $this->expectException(RuntimeException::class); + $this->expectExceptionCode(ChromedriverInstaller::ERR_INVALID_BINARY); + + new ChromedriverInstaller('1.2.3.4', 'win32'); + } + + /** + * It should throw if specified binary is not valid + * + * @test + */ + public function should_throw_if_specified_binary_is_not_valid(): void + { + $this->uopzSetFunctionReturn('is_executable', function (string $file): bool { + return !str_contains($file, 'Chromium') && is_executable($file); + }, true); + + $this->expectException(RuntimeException::class); + $this->expectExceptionCode(ChromedriverInstaller::ERR_INVALID_BINARY); + + new ChromedriverInstaller('1.2.3.4', + 'mac-arm64', + '/Applications/Chromium.app/Contents/MacOS/Chromium'); + } + + /** + * It should throw if version from binary is not a string + * + * @test + */ + public function should_throw_if_version_from_binary_is_not_a_string(): void + { + $this->expectException(RuntimeException::class); + $this->expectExceptionCode(ChromedriverInstaller::ERR_VERSION_NOT_STRING); + + new ChromedriverInstaller(null, + null, + codecept_data_dir('bins/chrome-version-not-string')); + } + + /** + * It should throw if version from binary has not correct format + * + * @test + */ + public function should_throw_if_version_from_binary_has_not_correct_format(): void + { + $this->expectException(RuntimeException::class); + $this->expectExceptionCode(ChromedriverInstaller::ERR_INVALID_VERSION_FORMAT); + + new ChromedriverInstaller(null, null, codecept_data_dir('bins/chrome-version-wrong-format')); + } + + /** + * It should throw if specified version is not valid + * + * @test + */ + public function should_throw_if_specified_version_is_not_valid(): void + { + $this->expectException(RuntimeException::class); + $this->expectExceptionCode(ChromedriverInstaller::ERR_INVALID_VERSION_FORMAT); + + new ChromedriverInstaller('lorem.dolor.sit.amet', null, codecept_data_dir('bins/chrome-mock')); + } + + /** + * It should pick up version and platform correctly + * + * @test + */ + public function should_pick_up_version_and_platform_correctly(): void + { + $ci = new ChromedriverInstaller(null, 'linux64', codecept_data_dir('bins/chrome-mock')); + + $this->assertEquals('116.0.5845.96', $ci->getVersion()); + $this->assertEquals(codecept_data_dir('bins/chrome-mock'), $ci->getBinary()); + $this->assertEquals('linux64', $ci->getPlatform()); + } + + /** + * It should throw if trying to install to non-existing directory + * + * @test + */ + public function should_throw_if_trying_to_install_to_non_existing_directory(): void + { + $ci = new ChromedriverInstaller(null, 'linux64', codecept_data_dir('bins/chrome-mock')); + + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionCode(ChromedriverInstaller::ERR_DESTINATION_NOT_DIR); + + $ci->install(__DIR__ . '/non-existing-dir'); + } + + /** + * It should throw if it cannot get known good version + * + * @test + */ + public function should_throw_if_it_cannot_get_known_good_version(): void + { + $this->uopzSetFunctionReturn('file_get_contents', function (string $file): string|false { + return str_contains($file, 'chrome-for-testing') ? false : file_get_contents($file); + }, true); + + $ci = new ChromedriverInstaller(null, 'linux64', codecept_data_dir('bins/chrome-mock')); + + $this->expectException(RuntimeException::class); + $this->expectExceptionCode(ChromedriverInstaller::ERR_FETCH_KNOWN_GOOD_VERSIONS_WITH_DOWNLOADS); + + $ci->install(__DIR__); + } + + /** + * It should throw if response is not valid JSON + * + * @test + */ + public function should_throw_if_response_is_not_valid_json(): void + { + $this->uopzSetFunctionReturn('file_get_contents', function (string $file): string|false { + return str_contains($file, 'chrome-for-testing') ? '{}' : file_get_contents($file); + }, true); + + $ci = new ChromedriverInstaller(null, 'linux64', codecept_data_dir('bins/chrome-mock')); + + $this->expectException(RuntimeException::class); + $this->expectExceptionCode(ChromedriverInstaller::ERR_DECODE_KNOWN_GOOD_VERSIONS_WITH_DOWNLOADS); + + $ci->install(__DIR__); + } + + /** + * It should throw if download URL for Chrome version cannot be found in known good versions + * + * @test + */ + public function should_throw_if_download_url_for_chrome_version_cannot_be_found_in_known_good_versions(): void + { + $this->uopzSetFunctionReturn('file_get_contents', function (string $file): string|false { + return str_contains($file, 'chrome-for-testing') ? '{"versions":[]}' : file_get_contents($file); + }, true); + + $ci = new ChromedriverInstaller(null, 'linux64', codecept_data_dir('bins/chrome-mock')); + + $this->expectException(RuntimeException::class); + $this->expectExceptionCode(ChromedriverInstaller::ERR_DOWNLOAD_URL_NOT_FOUND); + + $ci->install(__DIR__); + } + + /** + * It should throw if existing zip file cannot be removed + * + * @test + */ + public function should_throw_if_existing_zip_file_cannot_be_removed(): void + { + $this->uopzSetFunctionReturn('sys_get_temp_dir', codecept_output_dir()); + touch(codecept_output_dir('chromedriver-linux64.zip')); + $this->uopzSetFunctionReturn('unlink', function (string $file): bool { + return $file === codecept_output_dir('chromedriver-linux64.zip') ? false : unlink($file); + }, true); + + $ci = new ChromedriverInstaller(null, 'linux64', codecept_data_dir('bins/chrome-mock')); + + $this->expectException(RuntimeException::class); + $this->expectExceptionCode(ChromedriverInstaller::ERR_REMOVE_EXISTING_ZIP_FILE); + + $ci->install(__DIR__); + } + + /** + * It should throw if existing binary cannot be removed + * + * @test + */ + public function should_throw_if_existing_binary_cannot_be_removed(): void + { + $dir = Filesystem::tmpDir('chromedriver_installer_', ['chromedriver' => '']); + $this->uopzSetFunctionReturn('sys_get_temp_dir', codecept_output_dir()); + $this->uopzSetFunctionReturn('unlink', function (string $file) use ($dir): bool { + return $file === $dir . '/chromedriver' ? false : unlink($file); + }, true); + + $ci = new ChromedriverInstaller(null, 'linux64', codecept_data_dir('bins/chrome-mock')); + + $this->expectException(RuntimeException::class); + $this->expectExceptionCode(ChromedriverInstaller::ERR_REMOVE_EXISTING_BINARY); + + $ci->install($dir); + } + + /** + * It should throw if new binary cannot be moved in place + * + * @test + */ + public function should_throw_if_new_binary_cannot_be_moved_in_place(): void + { + $dir = Filesystem::tmpDir('chromedriver_installer_'); + $this->uopzSetFunctionReturn('sys_get_temp_dir', codecept_output_dir()); + $this->uopzSetFunctionReturn('rename', false); + + $ci = new ChromedriverInstaller(null, 'linux64', codecept_data_dir('bins/chrome-mock')); + + $this->expectException(RuntimeException::class); + $this->expectExceptionCode(ChromedriverInstaller::ERR_MOVE_BINARY); + + $ci->install($dir); + } + + /** + * It should throw if new binary cannot be made executable + * + * @test + */ + public function should_throw_if_new_binary_cannot_be_made_executable(): void + { + $dir = Filesystem::tmpDir('chromedriver_installer_'); + $this->uopzSetFunctionReturn('sys_get_temp_dir', codecept_output_dir()); + $this->uopzSetFunctionReturn('chmod', false); + + $ci = new ChromedriverInstaller(null, 'linux64', codecept_data_dir('bins/chrome-mock')); + + $this->expectException(RuntimeException::class); + $this->expectExceptionCode(ChromedriverInstaller::ERR_BINARY_CHMOD); + + $ci->install($dir); + } + + /** + * It should correctly install chromedriver + * + * @test + */ + public function should_correctly_install_chromedriver(): void + { + $tmpDir = Filesystem::tmpDir('chromedriver_installer_tmp_'); + $dir = Filesystem::tmpDir('chromedriver_installer_'); + $this->uopzSetFunctionReturn('sys_get_temp_dir', $tmpDir); + + $ci = new ChromedriverInstaller(null, 'linux64', codecept_data_dir('bins/chrome-mock')); + + $executablePath = $ci->install($dir); + + $this->assertEquals($dir . '/chromedriver', $executablePath); + $this->assertFileExists($executablePath); + $this->assertFileExists($tmpDir . '/chromedriver-linux64.zip'); + } +}