Merge branch 'main' into polish
# Conflicts: # CHANGELOG.md
This commit is contained in:
@@ -0,0 +1,57 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace OCA\NgBackup\Tests\Unit\Backend;
|
||||
|
||||
use OCA\NgBackup\Backend\BackendException;
|
||||
use OCA\NgBackup\Backend\StorageBackend;
|
||||
use OCA\NgBackup\Tests\Unit\TempDirTrait;
|
||||
use OCP\Files\Storage\IStorage;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
/** On a remote storage every is_dir() is a round trip: listing index/ must not cost one per pack. */
|
||||
class StorageBackendListTest extends TestCase {
|
||||
use TempDirTrait;
|
||||
|
||||
public function testListFilesChecksOnlyTheFolderItself(): void {
|
||||
$dir = $this->tempDir();
|
||||
foreach (['b1', 'a2', 'c3.part-0123abcd'] as $name) {
|
||||
touch("$dir/$name");
|
||||
}
|
||||
$isDirCalls = 0;
|
||||
$storage = $this->createMock(IStorage::class);
|
||||
$storage->method('instanceOfStorage')->willReturn(false);
|
||||
$storage->method('is_dir')->willReturnCallback(function (string $path) use (&$isDirCalls): bool {
|
||||
$isDirCalls++;
|
||||
return $path === 'repo' || $path === 'repo/index';
|
||||
});
|
||||
$storage->method('opendir')->willReturnCallback(fn (string $path) => $path === 'repo/index' ? opendir($dir) : false);
|
||||
|
||||
$backend = new StorageBackend($storage, 'repo');
|
||||
$isDirCalls = 0; // the constructor checks the base folder
|
||||
$this->assertSame(['index/a2', 'index/b1'], $backend->listFiles('index'));
|
||||
$this->assertSame(1, $isDirCalls);
|
||||
|
||||
$isDirCalls = 0;
|
||||
$this->assertSame(['index/a2', 'index/b1'], $backend->list('index'));
|
||||
$this->assertSame(4, $isDirCalls, 'list() checks every entry: the reason listFiles() exists');
|
||||
}
|
||||
|
||||
public function testListFilesOfAMissingFolderIsEmpty(): void {
|
||||
$storage = $this->createMock(IStorage::class);
|
||||
$storage->method('instanceOfStorage')->willReturn(false);
|
||||
$storage->method('is_dir')->willReturnCallback(fn (string $path): bool => $path === 'repo');
|
||||
$storage->expects($this->never())->method('opendir');
|
||||
$this->assertSame([], (new StorageBackend($storage, 'repo'))->listFiles('snapshots'));
|
||||
}
|
||||
|
||||
public function testAnUnreadableFolderIsAnErrorNotEmpty(): void {
|
||||
$storage = $this->createMock(IStorage::class);
|
||||
$storage->method('instanceOfStorage')->willReturn(false);
|
||||
$storage->method('is_dir')->willReturn(true);
|
||||
$storage->method('opendir')->willReturn(false);
|
||||
$this->expectException(BackendException::class);
|
||||
(new StorageBackend($storage, 'repo'))->listFiles('catalog');
|
||||
}
|
||||
}
|
||||
@@ -51,4 +51,41 @@ class IndexCacheTest extends TestCase {
|
||||
$fourth->restore($r1['snapshot'], $target);
|
||||
$this->assertCount(12, self::hashTree($target));
|
||||
}
|
||||
|
||||
/** The database dump reads in a read-only transaction: a pack that fills up then must not write the cache yet. */
|
||||
public function testHeldCacheWritesWaitForRelease(): void {
|
||||
$cache = new class implements IIndexCache {
|
||||
public array $data = [];
|
||||
public bool $readOnly = false;
|
||||
public function all(string $r): array { return $this->data[$r] ?? []; }
|
||||
public function put(string $r, string $p, array $e): void {
|
||||
if ($this->readOnly) {
|
||||
throw new \RuntimeException('cannot execute INSERT in a read-only transaction');
|
||||
}
|
||||
$this->data[$r][$p] = $e;
|
||||
}
|
||||
public function remove(string $r, array $ps): void { foreach ($ps as $p) { unset($this->data[$r][$p]); } }
|
||||
};
|
||||
$backend = new LocalBackend($this->tempDir());
|
||||
$keys = KeyRing::generate();
|
||||
Repository::initWithKey($backend, $keys, $keys->wrap('pw'));
|
||||
$repo = Repository::openWithKey($backend, $keys, $cache);
|
||||
|
||||
$stats = [];
|
||||
$repo->holdIndexCache();
|
||||
$cache->readOnly = true;
|
||||
$writer = $repo->blobWriter($stats);
|
||||
for ($i = 0; $i < 40; $i++) {
|
||||
$writer->write(random_bytes(1048576)); // more than one 32 MiB pack
|
||||
}
|
||||
$writer->finish();
|
||||
$this->assertGreaterThan(0, count($backend->list('index')), 'a full pack was uploaded during the hold');
|
||||
$this->assertSame([], $cache->data);
|
||||
|
||||
$cache->readOnly = false;
|
||||
$repo->releaseIndexCache();
|
||||
$repo->flushPacks();
|
||||
$this->assertCount(count($backend->list('index')), array_merge(...array_values($cache->data)));
|
||||
$this->assertSame(0, Repository::openWithKey($backend, $keys, $cache)->indexDownloads());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -166,6 +166,10 @@ final class FlakyRangeBackend implements IBackend {
|
||||
return $this->inner->list($prefix);
|
||||
}
|
||||
|
||||
public function listFiles(string $dir): array {
|
||||
return $this->inner->listFiles($dir);
|
||||
}
|
||||
|
||||
public function delete(string $path): void {
|
||||
$this->inner->delete($path);
|
||||
}
|
||||
|
||||
@@ -3,3 +3,13 @@
|
||||
declare(strict_types=1);
|
||||
|
||||
require __DIR__ . '/../vendor/autoload.php';
|
||||
|
||||
// nextcloud/ocp ships the public interfaces without an autoloader; tests that mock them need one.
|
||||
spl_autoload_register(static function (string $class): void {
|
||||
if (str_starts_with($class, 'OCP\\')) {
|
||||
$file = __DIR__ . '/../vendor/nextcloud/ocp/' . str_replace('\\', '/', $class) . '.php';
|
||||
if (is_file($file)) {
|
||||
require $file;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user