Skip to content

Commit

Permalink
Split out the static cache into its own loader (#118)
Browse files Browse the repository at this point in the history
  • Loading branch information
phenaproxima authored Jul 12, 2024
1 parent 4afa9db commit e5eab1f
Show file tree
Hide file tree
Showing 6 changed files with 88 additions and 53 deletions.
18 changes: 1 addition & 17 deletions src/Loader.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
use Composer\Downloader\MaxFileSizeExceededException;
use Composer\Downloader\TransportException;
use Composer\InstalledVersions;
use Composer\IO\IOInterface;
use Composer\Util\HttpDownloader;
use GuzzleHttp\Promise\Create;
use GuzzleHttp\Promise\PromiseInterface;
Expand All @@ -19,15 +18,9 @@
*/
class Loader implements LoaderInterface
{
/**
* @var \Psr\Http\Message\StreamInterface[]
*/
private array $cache = [];

public function __construct(
private HttpDownloader $downloader,
private ComposerFileStorage $storage,
private IOInterface $io,
private string $baseUrl = ''
) {}

Expand All @@ -37,15 +30,6 @@ public function __construct(
public function load(string $locator, int $maxBytes): PromiseInterface
{
$url = $this->baseUrl . $locator;
if (array_key_exists($url, $this->cache)) {
$this->io->debug("[TUF] Loading $url from static cache.");

$cachedStream = $this->cache[$url];
// The underlying stream should always be seekable.
assert($cachedStream->isSeekable());
$cachedStream->rewind();
return Create::promiseFor($cachedStream);
}

$options = [
// Add 1 to $maxBytes to work around a bug in Composer.
Expand Down Expand Up @@ -90,7 +74,7 @@ public function load(string $locator, int $maxBytes): PromiseInterface
fwrite($content, $response->getBody());
}

$stream = $this->cache[$url] = Utils::streamFor($content);
$stream = Utils::streamFor($content);
$stream->rewind();
return Create::promiseFor($stream);
}
Expand Down
43 changes: 43 additions & 0 deletions src/StaticCache.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
<?php

namespace Tuf\ComposerIntegration;

use Composer\IO\IOInterface;
use GuzzleHttp\Promise\Create;
use GuzzleHttp\Promise\PromiseInterface;
use Psr\Http\Message\StreamInterface;
use Tuf\Loader\LoaderInterface;

class StaticCache implements LoaderInterface
{
/**
* @var \Psr\Http\Message\StreamInterface[]
*/
private array $cache = [];

public function __construct(
private readonly LoaderInterface $decorated,
private readonly IOInterface $io,
) {}

/**
* {@inheritDoc}
*/
public function load(string $locator, int $maxBytes): PromiseInterface
{
if (array_key_exists($locator, $this->cache)) {
$this->io->debug("[TUF] Loading '$locator' from static cache.");

$cachedStream = $this->cache[$locator];
// The underlying stream should always be seekable.
assert($cachedStream->isSeekable());
$cachedStream->rewind();
return Create::promiseFor($cachedStream);
}
return $this->decorated->load($locator, $maxBytes)
->then(function (StreamInterface $stream) use ($locator) {
$this->cache[$locator] = $stream;
return $stream;
});
}
}
3 changes: 2 additions & 1 deletion src/TufValidatedComposerRepository.php
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,8 @@ public function __construct(array $repoConfig, IOInterface $io, Config $config,

// @todo: Write a custom implementation of FileStorage that stores repo keys to user's global composer cache?
$storage = $this->initializeStorage($url, $config);
$loader = new Loader($httpDownloader, $storage, $io, $metadataUrl);
$loader = new Loader($httpDownloader, $storage, $metadataUrl);
$loader = new StaticCache($loader, $io);
$loader = new SizeCheckingLoader($loader);
$this->updater = new ComposerCompatibleUpdater($loader, $storage);

Expand Down
4 changes: 2 additions & 2 deletions tests/ComposerCommandsTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -75,8 +75,8 @@ public function testRequireAndRemove(): void

// Even though we are searching delegated roles for multiple targets, we should see the TUF metadata
// loaded from the static cache.
$this->assertStringContainsString('[TUF] Loading http://localhost:8080/metadata/1.package_metadata.json from static cache.', $debug);
$this->assertStringContainsString('[TUF] Loading http://localhost:8080/metadata/1.package.json from static cache.', $debug);
$this->assertStringContainsString("[TUF] Loading '1.package_metadata.json' from static cache.", $debug);
$this->assertStringContainsString("[TUF] Loading '1.package.json' from static cache.", $debug);
// The metadata should actually be *downloaded* no more than twice -- once while the
// dependency tree is being solved, and again when the solved dependencies are actually
// downloaded (which is done by Composer effectively re-invoking itself, resulting in
Expand Down
34 changes: 1 addition & 33 deletions tests/LoaderTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
use Composer\Config;
use Composer\Downloader\MaxFileSizeExceededException;
use Composer\Downloader\TransportException;
use Composer\IO\IOInterface;
use Composer\Util\Http\Response;
use Composer\Util\HttpDownloader;
use DMS\PHPUnitExtensions\ArraySubset\Constraint\ArraySubset;
Expand All @@ -27,7 +26,6 @@ public function testLoader(): void
return new Loader(
$downloader,
$this->createMock(ComposerFileStorage::class),
$this->createMock(IOInterface::class),
'/metadata/'
);
};
Expand Down Expand Up @@ -111,43 +109,13 @@ public function testNotModifiedResponse(): void
->with($url, $this->mockOptions(1025, $modifiedTime))
->willReturn($response);

$loader = new Loader($downloader, $storage, $this->createMock(IOInterface::class));
$loader = new Loader($downloader, $storage);
// Since the response has no actual body data, the fact that we get the contents
// of the file we wrote here is proof that it was ultimately read from persistent
// storage by the loader.
$this->assertSame('Some test data.', $loader->load('2.test.json', 1024)->wait()->getContents());
}

public function testStaticCache(): void
{
$response = $this->createMock(Response::class);
$response->expects($this->any())
->method('getStatusCode')
->willReturn(200);
$response->expects($this->any())
->method('getBody')
->willReturn('Truly, this is amazing stuff.');

$downloader = $this->createMock(HttpDownloader::class);
$downloader->expects($this->once())
->method('get')
->with('foo.txt', $this->mockOptions(1025))
->willReturn($response);

$loader = new Loader($downloader, $this->createMock(ComposerFileStorage::class), $this->createMock(IOInterface::class));
$stream = $loader->load('foo.txt', 1024)->wait();

// We should be at the beginning of the stream.
$this->assertSame(0, $stream->tell());
// Skip to the end of the stream, so we can confirm that it is rewound
// when loaded from the static cache.
$stream->seek(0, SEEK_END);
$this->assertGreaterThan(0, $stream->tell());

$this->assertSame($stream, $loader->load('foo.txt', 1024)->wait());
$this->assertSame(0, $stream->tell());
}

private function mockOptions(int $expectedSize, ?\DateTimeInterface $modifiedTime = null): object
{
$options = ['max_file_size' => $expectedSize];
Expand Down
39 changes: 39 additions & 0 deletions tests/StaticCacheTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
<?php

namespace Tuf\ComposerIntegration\Tests;

use Composer\IO\IOInterface;
use GuzzleHttp\Promise\Create;
use GuzzleHttp\Psr7\Utils;
use PHPUnit\Framework\TestCase;
use Tuf\ComposerIntegration\StaticCache;
use Tuf\Loader\LoaderInterface;

/**
* @covers \Tuf\ComposerIntegration\StaticCache
*/
class StaticCacheTest extends TestCase
{
public function testStaticCache(): void
{
$decorated = $this->createMock(LoaderInterface::class);

$mockedStream = Utils::streamFor('Across the universe');
$decorated->expects($this->once())
->method('load')
->with('foo.txt', 60)
->willReturn(Create::promiseFor($mockedStream));

$loader = new StaticCache($decorated, $this->createMock(IOInterface::class));
$stream = $loader->load('foo.txt', 60)->wait();

// We should be at the beginning of the stream.
$this->assertSame(0, $stream->tell());
// Skip to the end of the stream, so we can confirm that it is rewound when loaded from the static cache.
$stream->seek(0, SEEK_END);
$this->assertGreaterThan(0, $stream->tell());

$this->assertSame($stream, $loader->load('foo.txt', 60)->wait());
$this->assertSame(0, $stream->tell());
}
}

0 comments on commit e5eab1f

Please sign in to comment.