Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat/#94 resolve download url dynamically for chrome driver #95

4 changes: 3 additions & 1 deletion bdi
Original file line number Diff line number Diff line change
Expand Up @@ -49,8 +49,10 @@ $geckoDriverVersionResolver = new GeckoDriver\VersionResolver($httpClient);
$driverVersionResolverFactory->register($chromeDriverVersionResolver);
$driverVersionResolverFactory->register($geckoDriverVersionResolver);

$chromeDriverDownloadUrlResolver = new ChromeDriver\DownloadUrlResolver($httpClient);

$driverDownloaderFactory = new Driver\DownloaderFactory();
$driverDownloaderFactory->register(new ChromeDriver\Downloader($filesystem, $httpClient, $multiExtractor));
$driverDownloaderFactory->register(new ChromeDriver\Downloader($filesystem, $httpClient, $multiExtractor, $chromeDriverDownloadUrlResolver));
$driverDownloaderFactory->register(new GeckoDriver\Downloader($filesystem, $httpClient, $multiExtractor));

$browserFactory = new Browser\BrowserFactory($browserPathResolverFactory, $browserVersionResolverFactory);
Expand Down
106 changes: 106 additions & 0 deletions src/Driver/ChromeDriver/DownloadUrlResolver.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
<?php

declare(strict_types=1);

namespace DBrekelmans\BrowserDriverInstaller\Driver\ChromeDriver;

use DBrekelmans\BrowserDriverInstaller\Driver\DownloadUrlResolver as DownloadUrlResolverInterface;
use DBrekelmans\BrowserDriverInstaller\Driver\Driver;
use DBrekelmans\BrowserDriverInstaller\Exception\NotImplemented;
use DBrekelmans\BrowserDriverInstaller\OperatingSystem\OperatingSystem;
use Symfony\Contracts\HttpClient\HttpClientInterface;
use UnexpectedValueException;

use function is_string;
use function Safe\sprintf;

final class DownloadUrlResolver implements DownloadUrlResolverInterface
{
private const BINARY_LINUX = 'chromedriver_linux64';
private const BINARY_MAC = 'chromedriver_mac64';
private const BINARY_WINDOWS = 'chromedriver_win32';
private const LEGACY_DOWNLOAD_ENDPOINT = 'https://chromedriver.storage.googleapis.com';
private const LATEST_PATCH_WITH_DOWNLOAD_ENDPOINT = 'https://googlechromelabs.github.io/chrome-for-testing/latest-patch-versions-per-build-with-downloads.json';

private HttpClientInterface $httpClient;

public function __construct(HttpClientInterface $httpClient)
{
$this->httpClient = $httpClient;
}

public function byDriver(Driver $driver): string
{
if (! VersionResolver::isJsonVersion($driver->version())) {
return sprintf(
'%s/%s/%s.zip',
self::LEGACY_DOWNLOAD_ENDPOINT,
$driver->version()->toBuildString(),
$this->getBinaryName($driver),
);
}

$response = $this->httpClient->request('GET', self::LATEST_PATCH_WITH_DOWNLOAD_ENDPOINT);

$versions = $response->toArray();
if (! isset($versions['builds'][$driver->version()->toString()]['downloads']['chromedriver'])) {
throw new UnexpectedValueException(sprintf('Could not find the chromedriver downloads for version %s', $driver->version()->toString()));
}

$platformName = $this->getPlatformName($driver);
$downloads = $versions['builds'][$driver->version()->toString()]['downloads']['chromedriver'];
foreach ($downloads as $download) {
if ($download['platform'] === $platformName && isset($download['url']) && is_string($download['url'])) {
return $download['url'];
}
}

$operatingSystem = $driver->operatingSystem();

throw new UnexpectedValueException(sprintf(
'Could not resolve chromedriver download url for version %s with binary %s',
$driver->version()->toString(),
$operatingSystem->getValue()
));
}

private function getBinaryName(Driver $driver): string
{
$operatingSystem = $driver->operatingSystem();
if ($operatingSystem->equals(OperatingSystem::WINDOWS())) {
return self::BINARY_WINDOWS;
}

if ($operatingSystem->equals(OperatingSystem::MACOS())) {
return self::BINARY_MAC;
}

if ($operatingSystem->equals(OperatingSystem::LINUX())) {
return self::BINARY_LINUX;
}

throw NotImplemented::feature(
sprintf('Downloading %s for %s', $driver->name()->getValue(), $operatingSystem->getValue())
);
}

private function getPlatformName(Driver $driver): string
{
$operatingSystem = $driver->operatingSystem();
if ($operatingSystem->equals(OperatingSystem::WINDOWS())) {
return 'win32';
}

if ($operatingSystem->equals(OperatingSystem::MACOS())) {
return 'mac-x64';
}

if ($operatingSystem->equals(OperatingSystem::LINUX())) {
return 'linux64';
}

throw NotImplemented::feature(
sprintf('Downloading %s for %s', $driver->name()->getValue(), $operatingSystem->getValue())
);
}
}
108 changes: 19 additions & 89 deletions src/Driver/ChromeDriver/Downloader.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

use DBrekelmans\BrowserDriverInstaller\Archive\Extractor;
use DBrekelmans\BrowserDriverInstaller\Driver\Downloader as DownloaderInterface;
use DBrekelmans\BrowserDriverInstaller\Driver\DownloadUrlResolver;
use DBrekelmans\BrowserDriverInstaller\Driver\Driver;
use DBrekelmans\BrowserDriverInstaller\Driver\DriverName;
use DBrekelmans\BrowserDriverInstaller\Exception\NotImplemented;
Expand All @@ -31,17 +32,9 @@

final class Downloader implements DownloaderInterface
{
private const DOWNLOAD_ENDPOINT = 'https://chromedriver.storage.googleapis.com';
private const BINARY_LINUX = 'chromedriver_linux64';
private const BINARY_MAC = 'chromedriver_mac64';
private const BINARY_WINDOWS = 'chromedriver_win32';
private const DOWNLOAD_ENDPOINT_JSON = 'https://edgedl.me.gvt1.com/edgedl/chrome/chrome-for-testing';
private const DOWNLOAD_ENDPOINT_JSON_NEW = 'https://storage.googleapis.com/chrome-for-testing-public';
private const BINARY_LINUX_JSON = 'chromedriver-linux64';
private const BINARY_MAC_JSON = 'chromedriver-mac-x64';
private const BINARY_WINDOWS_JSON = 'chromedriver-win32';
private const NEW_JSON_API_ENDPOINT_MAJOR = 121;
private const NEW_JSON_API_ENDPOINT_PATCH = 6167;
private const BINARY_LINUX_JSON = 'chromedriver-linux64';
private const BINARY_MAC_JSON = 'chromedriver-mac-x64';
private const BINARY_WINDOWS_JSON = 'chromedriver-win32';


private Filesystem $filesystem;
Expand All @@ -52,12 +45,19 @@ final class Downloader implements DownloaderInterface

private string $tempDir;

public function __construct(Filesystem $filesystem, HttpClientInterface $httpClient, Extractor $archiveExtractor)
{
$this->filesystem = $filesystem;
$this->httpClient = $httpClient;
$this->archiveExtractor = $archiveExtractor;
$this->tempDir = sys_get_temp_dir();
private DownloadUrlResolver $downloadUrlResolver;

public function __construct(
Filesystem $filesystem,
HttpClientInterface $httpClient,
Extractor $archiveExtractor,
DownloadUrlResolver $downloadUrlResolver
) {
$this->filesystem = $filesystem;
$this->httpClient = $httpClient;
$this->archiveExtractor = $archiveExtractor;
$this->downloadUrlResolver = $downloadUrlResolver;
$this->tempDir = sys_get_temp_dir();
}

public function supports(Driver $driver): bool
Expand Down Expand Up @@ -124,12 +124,7 @@ private function downloadArchive(Driver $driver): string

$response = $this->httpClient->request(
'GET',
sprintf(
'%s/%s/%s.zip',
$this->getDownloadEndpoint($driver),
$driver->version()->toBuildString(),
$this->getBinaryName($driver)
)
$this->downloadUrlResolver->byDriver($driver),
);

$fileHandler = fopen($temporaryFile, 'wb');
Expand All @@ -147,43 +142,6 @@ private function downloadArchive(Driver $driver): string
return $temporaryFile;
}

/**
* @throws NotImplemented
*/
private function getBinaryName(Driver $driver): string
{
$operatingSystem = $driver->operatingSystem();
if ($this->isJsonVersion($driver)) {
if ($operatingSystem->equals(OperatingSystem::WINDOWS())) {
return 'win32/' . self::BINARY_WINDOWS_JSON;
}

if ($operatingSystem->equals(OperatingSystem::MACOS())) {
return 'mac-x64/' . self::BINARY_MAC_JSON;
}

if ($operatingSystem->equals(OperatingSystem::LINUX())) {
return 'linux64/' . self::BINARY_LINUX_JSON;
}
} else {
if ($operatingSystem->equals(OperatingSystem::WINDOWS())) {
return self::BINARY_WINDOWS;
}

if ($operatingSystem->equals(OperatingSystem::MACOS())) {
return self::BINARY_MAC;
}

if ($operatingSystem->equals(OperatingSystem::LINUX())) {
return self::BINARY_LINUX;
}
}

throw NotImplemented::feature(
sprintf('Downloading %s for %s', $driver->name()->getValue(), $operatingSystem->getValue())
);
}

/**
* @throws RuntimeException
* @throws IOException
Expand All @@ -192,7 +150,7 @@ private function extractArchive(string $archive, Driver $driver): string
{
$unzipLocation = $this->tempDir . DIRECTORY_SEPARATOR . 'chromedriver';
$extractedFiles = $this->archiveExtractor->extract($archive, $unzipLocation);
if ($this->isJsonVersion($driver)) {
if (VersionResolver::isJsonVersion($driver->version())) {
$extractedFiles = $this->cleanArchiveStructure($driver, $unzipLocation, $extractedFiles);
}

Expand Down Expand Up @@ -234,34 +192,6 @@ private function getFileName(OperatingSystem $operatingSystem): string
return $fileName;
}

private function isJsonVersion(Driver $driver): bool
{
return $driver->version()->major() >= VersionResolver::MAJOR_VERSION_ENDPOINT_BREAKPOINT;
}

private function getDownloadEndpoint(Driver $driver): string
{
if ($this->isJsonVersion($driver)) {
return $this->resolveJsonVersionEndpoint($driver);
}

return self::DOWNLOAD_ENDPOINT;
}

private function resolveJsonVersionEndpoint(Driver $driver): string
{
$version = $driver->version();
if ((int) $version->major() < self::NEW_JSON_API_ENDPOINT_MAJOR) {
return self::DOWNLOAD_ENDPOINT_JSON;
}

if ((int) $version->major() === self::NEW_JSON_API_ENDPOINT_MAJOR && (int) $version->patch() < self::NEW_JSON_API_ENDPOINT_PATCH) {
return self::DOWNLOAD_ENDPOINT_JSON;
}

return self::DOWNLOAD_ENDPOINT_JSON_NEW;
}

/**
* @param string[] $extractedFiles
*
Expand Down
5 changes: 5 additions & 0 deletions src/Driver/ChromeDriver/VersionResolver.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,11 @@ final class VersionResolver implements VersionResolverInterface

private HttpClientInterface $httpClient;

public static function isJsonVersion(Version $version): bool
{
return $version->major() >= self::MAJOR_VERSION_ENDPOINT_BREAKPOINT;
}

public function __construct(HttpClientInterface $httpClient)
{
$this->httpClient = $httpClient;
Expand Down
10 changes: 10 additions & 0 deletions src/Driver/DownloadUrlResolver.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<?php

declare(strict_types=1);

namespace DBrekelmans\BrowserDriverInstaller\Driver;

interface DownloadUrlResolver
{
public function byDriver(Driver $driver): string;
}
99 changes: 99 additions & 0 deletions tests/Driver/ChromeDriver/DownloadUrlResolverTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
<?php

declare(strict_types=1);

namespace DBrekelmans\BrowserDriverInstaller\Tests\Driver\ChromeDriver;

use DBrekelmans\BrowserDriverInstaller\Driver\ChromeDriver\DownloadUrlResolver;
use DBrekelmans\BrowserDriverInstaller\Driver\Driver;
use DBrekelmans\BrowserDriverInstaller\Driver\DriverName;
use DBrekelmans\BrowserDriverInstaller\OperatingSystem\OperatingSystem;
use DBrekelmans\BrowserDriverInstaller\Version;
use PHPUnit\Framework\TestCase;
use Symfony\Component\HttpClient\MockHttpClient;
use Symfony\Component\HttpClient\Response\MockResponse;

use function Safe\json_encode;

final class DownloadUrlResolverTest extends TestCase
{
private DownloadUrlResolver $urlResolver;

/**
* @return iterable<string, array{0: Driver, 1: string}>
*/
public static function byDriverDataProvider(): iterable
{
yield 'legacy version linux' => [
new Driver(DriverName::CHROME(), Version::fromString('88.0.4299.0'), OperatingSystem::LINUX()),
'https://chromedriver.storage.googleapis.com/88.0.4299.0/chromedriver_linux64.zip',
];

yield 'legacy version macos' => [
new Driver(DriverName::CHROME(), Version::fromString('88.0.4299.0'), OperatingSystem::MACOS()),
'https://chromedriver.storage.googleapis.com/88.0.4299.0/chromedriver_mac64.zip',
];

yield 'legacy version windows' => [
new Driver(DriverName::CHROME(), Version::fromString('88.0.4299.0'), OperatingSystem::WINDOWS()),
'https://chromedriver.storage.googleapis.com/88.0.4299.0/chromedriver_win32.zip',
];

yield 'new version linux' => [
new Driver(DriverName::CHROME(), Version::fromString('115.0.5790.170'), OperatingSystem::LINUX()),
'https://dynamic-url-2/',
];

yield 'new version macos' => [
new Driver(DriverName::CHROME(), Version::fromString('115.0.5790.170'), OperatingSystem::MACOS()),
'https://dynamic-url-3/',
];

yield 'new version windows' => [
new Driver(DriverName::CHROME(), Version::fromString('115.0.5790.170'), OperatingSystem::WINDOWS()),
'https://dynamic-url-1/',
];
}

/**
* @dataProvider byDriverDataProvider
*/
public function testByDriver(Driver $driver, string $expectedUrl): void
{
self::assertSame($expectedUrl, $this->urlResolver->byDriver($driver));
}

protected function setUp(): void
{
$httpClientMock = new MockHttpClient(
static function (string $method, string $url): MockResponse {
if ($method === 'GET') {
if ($url === 'https://googlechromelabs.github.io/chrome-for-testing/latest-patch-versions-per-build-with-downloads.json') {
return new MockResponse(
json_encode([
'builds' => [
'115.0.5790' => [
'downloads' => [
'chromedriver' => [
['platform' => 'win32', 'url' => 'https://dynamic-url-1/'],
['platform' => 'linux64', 'url' => 'https://dynamic-url-2/'],
['platform' => 'mac-x64', 'url' => 'https://dynamic-url-3/'],
],
],
],
],
])
);
}
}

return new MockResponse(
'<?xml version=\'1.0\' encoding=\'UTF-8\'?><Error><Code>NoSuchKey</Code><Message>The specified key does not exist.</Message><Details>No such object: chromedriver/LATEST_RELEASE_xxx</Details></Error>',
['http_code' => 404]
);
}
);

$this->urlResolver = new DownloadUrlResolver($httpClientMock);
}
}
Loading
Loading