Skip to content

Commit

Permalink
UHF-10713: Add getOutdated() to VersionChecker service
Browse files Browse the repository at this point in the history
  • Loading branch information
hyrsky committed Dec 16, 2024
1 parent 096a75d commit e7230d8
Show file tree
Hide file tree
Showing 7 changed files with 208 additions and 5 deletions.
3 changes: 2 additions & 1 deletion helfi_api_base.services.yml
Original file line number Diff line number Diff line change
Expand Up @@ -77,10 +77,11 @@ services:
tags:
- { name: service_collector, call: add, tag: helfi_api_base.version_checker }

Drupal\helfi_api_base\Package\ComposerOutdatedProcess: ~

Drupal\helfi_api_base\Package\HelfiPackage: '@helfi_api_base.helfi_package_version_checker'
helfi_api_base.helfi_package_version_checker:
class: Drupal\helfi_api_base\Package\HelfiPackage
arguments: ['@http_client']
tags:
- { name: helfi_api_base.version_checker }

Expand Down
12 changes: 12 additions & 0 deletions src/Exception/VersionCheckException.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<?php

declare(strict_types=1);

namespace Drupal\helfi_api_base\Exception;

/**
* Indicated failure to check versions.
*/
class VersionCheckException extends \RuntimeException {

}
30 changes: 30 additions & 0 deletions src/Package/ComposerOutdatedProcess.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
<?php

declare(strict_types=1);

namespace Drupal\helfi_api_base\Package;

use Symfony\Component\Process\Process;

/**
* Process for `composer outdated` command.
*/
class ComposerOutdatedProcess {

/**
* Runs `composer outdated`.
*
* @return array
* Decoded JSON from `composer outdated`.
*
* @throws \Symfony\Component\Process\Exception\ProcessFailedException
*/
public function run($workingDir): array {
$process = new Process([
'composer', 'outdated', '--direct', '--format=json', '--working-dir=' . $workingDir,
]);
$process->mustRun();
return json_decode($process->getOutput(), TRUE);
}

}
5 changes: 2 additions & 3 deletions src/Package/HelfiPackage.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,15 +22,14 @@ final class HelfiPackage implements VersionCheckerInterface {
* The HTTP client.
*/
public function __construct(
private ClientInterface $client,
private readonly ClientInterface $client,
) {
}

/**
* {@inheritdoc}
*/
public function applies(string $packageName): bool {
// @todo Allow other city-of-helsinki packages too.
return str_starts_with($packageName, 'drupal/helfi_')
|| str_starts_with($packageName, 'drupal/hdbt');
}
Expand Down Expand Up @@ -83,7 +82,7 @@ public function get(string $packageName, string $version): ? Version {
if (empty($latest['version']) || !is_string($latest['version'])) {
throw new InvalidPackageException('No version data found.');
}
return new Version($packageName, $latest['version'], version_compare($version, $latest['version'], '>='));
return new Version($packageName, $latest['version'], version_compare($version, $latest['version'], '>='), $version);
}

}
6 changes: 5 additions & 1 deletion src/Package/Version.php
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,17 @@ final class Version {
* @param string $name
* The package name.
* @param string $latestVersion
* The version.
* The latest version.
* @param bool $isLatest
* Whether the given version is latest or not.
* @param string $version
* The current version.
*/
public function __construct(
public string $name,
public string $latestVersion,
public bool $isLatest,
public string $version,
) {
}

Expand All @@ -37,6 +40,7 @@ public function toArray() : array {
'name' => $this->name,
'latestVersion' => $this->latestVersion,
'isLatest' => $this->isLatest,
'version' => $this->version,
];
}

Expand Down
72 changes: 72 additions & 0 deletions src/Package/VersionChecker.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@

namespace Drupal\helfi_api_base\Package;

use Drupal\helfi_api_base\Exception\VersionCheckException;
use Symfony\Component\DependencyInjection\Attribute\Autowire;
use Symfony\Component\Process\Exception\ProcessFailedException;

/**
* Provides a package version checker.
*/
Expand All @@ -16,6 +20,21 @@ final class VersionChecker {
*/
private array $collectors = [];

/**
* Cache result of `composer outdated`.
*/
private array $packages = [];

/**
* Constructs a new instance.
*/
public function __construct(
#[Autowire(param: 'helfi_api_base.default_composer_lock')]
private readonly string $defaultComposerLockFile,
private readonly ComposerOutdatedProcess $process,
) {
}

/**
* Adds a version checker.
*
Expand Down Expand Up @@ -53,4 +72,57 @@ public function get(string $packageName, string $version) : ? Version {
return NULL;
}

/**
* Gets outdated package versions.
*
* @param string|null $composerLockFile
* Path to composer lock file. Defaults to project lock file.
*
* @return Version[]
* Outdated packages.
*
* @throws \Drupal\helfi_api_base\Exception\VersionCheckException
*/
public function getOutdated(?string $composerLockFile = NULL) : array {
$packages = $this->getPackages($composerLockFile ?? $this->defaultComposerLockFile);
$versions = [];

foreach ($packages as $packageName => $package) {
$versions[] = new Version($packageName, $package['latest'], FALSE, $package['version']);
}

return $versions;
}

/**
* Get outdated packages.
*
* Uses variable cache since running the composer process is expensive.
*
* @throws \Drupal\helfi_api_base\Exception\VersionCheckException
*/
private function getPackages(string $composerLockFile): array {
if (!$composerLockFile = realpath($composerLockFile)) {
throw new VersionCheckException('Composer lock file not found');
}

$workingDir = dirname($composerLockFile);
if (empty($this->packages[$workingDir])) {
try {
$packages = $this->process->run($workingDir);
$packages = $packages['installed'] ?? [];
}
catch (ProcessFailedException) {
throw new VersionCheckException("Composer process failed");
}

// Key with package name.
foreach ($packages as $package) {
$this->packages[$workingDir][$package['name']] = $package;
}
}

return $this->packages[$workingDir] ?? [];
}

}
85 changes: 85 additions & 0 deletions tests/src/Unit/Package/VersionCheckerTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
<?php

declare(strict_types=1);

namespace Drupal\Tests\helfi_api_base\Unit\Package;

use Drupal\helfi_api_base\Exception\VersionCheckException;
use Drupal\helfi_api_base\Package\ComposerOutdatedProcess;
use Drupal\helfi_api_base\Package\Version;
use Drupal\helfi_api_base\Package\VersionChecker;
use Drupal\Tests\UnitTestCase;
use Prophecy\Argument;
use Prophecy\PhpUnit\ProphecyTrait;
use Symfony\Component\Process\Exception\ProcessFailedException;

/**
* Tests Version checker.
*
* @group helfi_api_base
*/
class VersionCheckerTest extends UnitTestCase {

use ProphecyTrait;

/**
* Tests missing composer file.
*/
public function testMissingComposerFile(): void {
$process = $this->prophesize(ComposerOutdatedProcess::class);
$process->run(Argument::any())->willReturn([]);
$sut = new VersionChecker('nonexistent.lock', $process->reveal());

$this->expectException(VersionCheckException::class);
$sut->getOutdated();
}

/**
* Tests composer command failure.
*/
public function testProcessFailure(): void {
$process = $this->prophesize(ComposerOutdatedProcess::class);
$process
->run(Argument::any())
->shouldBeCalled()
->willThrow($this->prophesize(ProcessFailedException::class)->reveal());

$sut = new VersionChecker(__DIR__ . '/../../../fixtures/composer.lock', $process->reveal());

$this->expectException(VersionCheckException::class);
$sut->getOutdated();
}

/**
* Tests getOutdated().
*/
public function testGetOutdated(): void {
$process = $this->prophesize(ComposerOutdatedProcess::class);
$process
->run(Argument::any())
->shouldBeCalled()
->willReturn([
'installed' => [
[
'name' => 'drupal/helfi_api_base',
'version' => '1.0.18',
'latest' => '1.1.0',
],
],
]);

$sut = new VersionChecker(__DIR__ . '/../../../fixtures/composer.lock', $process->reveal());

$outdated = $sut->getOutdated();

$this->assertNotEmpty($outdated);
$outdated = reset($outdated);
$this->assertInstanceOf(Version::class, $outdated);
$this->assertEquals('drupal/helfi_api_base', $outdated->name);
$this->assertEquals('1.0.18', $outdated->version);
$this->assertEquals('1.1.0', $outdated->latestVersion);
$this->assertFalse($outdated->isLatest);

}

}

0 comments on commit e7230d8

Please sign in to comment.