Skip to content

Commit

Permalink
Feat/#94 resolve download url dynamically for chrome driver (#95)
Browse files Browse the repository at this point in the history
* Feature: Introduced a new DownloadUrlResolver, which can be used to find the specific download url for a version

* Feature: Implement the new url resolver in the chromedriver downloader

* Feature: Implement the new url resolver in the chromedriver downloader

* Feature: Update the DownloaderTest

* Feature: Small changes to the url resolver, and added a unit test

* Feature: Change multiline call into one liner

* Feature: Moved the binary and platform resolving logic to the DownloadUrlResolver

* Feature: Remove the expected "with" argument

* Feature: Updated test, to remove the binary param, and test all OS version on the old and new download endpoint
  • Loading branch information
KevinVanSonsbeek authored Feb 20, 2024
1 parent 2628c95 commit 36bacb9
Show file tree
Hide file tree
Showing 8 changed files with 305 additions and 145 deletions.
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 @@ -29,6 +29,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

0 comments on commit 36bacb9

Please sign in to comment.