1
0
mirror of https://github.com/danog/file.git synced 2025-01-22 13:21:13 +01:00

Rework stat cache to use a decorator

This commit is contained in:
Niklas Keller 2020-10-21 22:52:43 +02:00
parent cbfe211760
commit 2162e7dc48
15 changed files with 388 additions and 194 deletions

View File

@ -51,29 +51,16 @@ final class BlockingDriver implements Driver
public function getStatus(string $path): Promise
{
if ($stat = StatCache::get($path)) {
return new Success($stat);
}
\clearstatcache(true, $path);
if ($stat = @\stat($path)) {
StatCache::set($path, $stat);
\clearstatcache(true, $path);
} else {
$stat = null;
}
return new Success($stat);
return new Success(@\stat($path) ?: null);
}
public function getLinkStatus(string $path): Promise
{
if ($stat = @\lstat($path)) {
\clearstatcache(true, $path);
} else {
$stat = null;
}
\clearstatcache(true, $path);
return new Success($stat);
return new Success(@\lstat($path) ?: null);
}
public function createSymlink(string $target, string $link): Promise
@ -154,8 +141,6 @@ final class BlockingDriver implements Driver
public function deleteFile(string $path): Promise
{
StatCache::clear($path);
try {
\set_error_handler(static function ($type, $message) use ($path) {
throw new FilesystemException("Could not delete file '{$path}': {$message}");
@ -225,8 +210,6 @@ final class BlockingDriver implements Driver
public function deleteDirectory(string $path): Promise
{
StatCache::clear($path);
try {
\set_error_handler(static function ($type, $message) use ($path) {
throw new FilesystemException("Could not remove directory '{$path}': {$message}");
@ -282,8 +265,6 @@ final class BlockingDriver implements Driver
throw new FilesystemException("Failed to change permissions for '{$path}'");
}
StatCache::clear($path);
return new Success;
} catch (FilesystemException $e) {
return new Failure($e);
@ -307,8 +288,6 @@ final class BlockingDriver implements Driver
throw new FilesystemException("Failed to change owner for '{$path}'");
}
StatCache::clear($path);
return new Success;
} catch (FilesystemException $e) {
return new Failure($e);
@ -331,8 +310,6 @@ final class BlockingDriver implements Driver
throw new FilesystemException("Failed to touch '{$path}'");
}
StatCache::clear($path);
return new Success;
} catch (FilesystemException $e) {
return new Failure($e);

View File

@ -49,10 +49,6 @@ final class EioDriver implements Driver
public function getStatus(string $path): Promise
{
if ($stat = StatCache::get($path)) {
return new Success($stat);
}
$deferred = new Deferred;
$this->poll->listen($deferred->promise());
@ -219,7 +215,6 @@ final class EioDriver implements Driver
$priority = \EIO_PRI_DEFAULT;
\eio_chmod($path, $mode, $priority, [$this, "onGenericResult"], $deferred);
StatCache::clearOn($deferred->promise(), $path);
return $deferred->promise();
}
@ -231,7 +226,6 @@ final class EioDriver implements Driver
$priority = \EIO_PRI_DEFAULT;
\eio_chown($path, $uid ?? -1, $gid ?? -1, $priority, [$this, "onGenericResult"], $deferred);
StatCache::clearOn($deferred->promise(), $path);
return $deferred->promise();
}
@ -246,7 +240,6 @@ final class EioDriver implements Driver
$priority = \EIO_PRI_DEFAULT;
\eio_utime($path, $accessTime, $modificationTime, $priority, [$this, "onGenericResult"], $deferred);
StatCache::clearOn($deferred->promise(), $path);
return $deferred->promise();
}
@ -343,7 +336,6 @@ final class EioDriver implements Driver
if ($result === -1) {
$deferred->fail(new FilesystemException(\eio_get_last_error($req)));
} else {
StatCache::set($path, $result);
$handle = new EioFile($this->poll, $fh, $path, $mode, $result["size"]);
$deferred->resolve($handle);
}
@ -355,7 +347,6 @@ final class EioDriver implements Driver
if ($result === -1) {
$deferred->resolve(null);
} else {
StatCache::set($path, $result);
$deferred->resolve($result);
}
}
@ -394,7 +385,6 @@ final class EioDriver implements Driver
if ($result === -1) {
$deferred->fail(new FilesystemException(\eio_get_last_error($req)));
} else {
StatCache::clear($path);
$deferred->resolve();
}
}
@ -421,7 +411,6 @@ final class EioDriver implements Driver
if ($result === -1) {
$deferred->fail(new FilesystemException(\eio_get_last_error($req)));
} else {
StatCache::clear($path);
$deferred->resolve();
}
}

View File

@ -45,24 +45,12 @@ final class ParallelDriver implements Driver
public function deleteFile(string $path): Promise
{
$promise = new Coroutine($this->runFileTask(new Internal\FileTask("deleteFile", [$path])));
StatCache::clearOn($promise, $path);
return $promise;
return new Coroutine($this->runFileTask(new Internal\FileTask("deleteFile", [$path])));
}
public function getStatus(string $path): Promise
{
if ($stat = StatCache::get($path)) {
return new Success($stat);
}
return call(function () use ($path) {
$stat = yield from $this->runFileTask(new Internal\FileTask("getStatus", [$path]));
if (!empty($stat)) {
StatCache::set($path, $stat);
}
return $stat;
});
return new Coroutine($this->runFileTask(new Internal\FileTask("getStatus", [$path])));
}
public function move(string $from, string $to): Promise
@ -102,23 +90,17 @@ final class ParallelDriver implements Driver
public function deleteDirectory(string $path): Promise
{
$promise = new Coroutine($this->runFileTask(new Internal\FileTask("deleteDirectory", [$path])));
StatCache::clearOn($promise, $path);
return $promise;
return new Coroutine($this->runFileTask(new Internal\FileTask("deleteDirectory", [$path])));
}
public function changePermissions(string $path, int $mode): Promise
{
$promise = new Coroutine($this->runFileTask(new Internal\FileTask("changePermissions", [$path, $mode])));
StatCache::clearOn($promise, $path);
return $promise;
return new Coroutine($this->runFileTask(new Internal\FileTask("changePermissions", [$path, $mode])));
}
public function changeOwner(string $path, ?int $uid, ?int $gid): Promise
{
$promise = new Coroutine($this->runFileTask(new Internal\FileTask("changeOwner", [$path, $uid, $gid])));
StatCache::clearOn($promise, $path);
return $promise;
return new Coroutine($this->runFileTask(new Internal\FileTask("changeOwner", [$path, $uid, $gid])));
}
public function getLinkStatus(string $path): Promise
@ -128,12 +110,10 @@ final class ParallelDriver implements Driver
public function touch(string $path, ?int $modificationTime, ?int $accessTime): Promise
{
$promise = new Coroutine($this->runFileTask(new Internal\FileTask(
return new Coroutine($this->runFileTask(new Internal\FileTask(
"touch",
[$path, $modificationTime, $accessTime]
)));
StatCache::clearOn($promise, $path);
return $promise;
}
public function read(string $path): Promise

View File

@ -0,0 +1,136 @@
<?php
namespace Amp\File\Driver;
use Amp\File\Driver;
use Amp\File\Internal\Cache;
use Amp\Promise;
use Amp\Success;
use function Amp\call;
final class StatusCachingDriver implements Driver
{
/** @var Driver */
private $driver;
/** @var Cache */
private $statusCache;
public function __construct(Driver $driver)
{
$this->driver = $driver;
$this->statusCache = new Cache(1000, 1024);
}
public function openFile(string $path, string $mode): Promise
{
return call(function () use ($path, $mode) {
$file = yield $this->driver->openFile($path, $mode);
return new StatusCachingFile($file, function () use ($path) {
$this->invalidate([$path], new Success);
});
});
}
public function getStatus(string $path): Promise
{
if ($cachedStat = $this->statusCache->get($path)) {
return new Success($cachedStat);
}
return $this->driver->getStatus($path);
}
public function getLinkStatus(string $path): Promise
{
return $this->driver->getLinkStatus($path);
}
public function createSymlink(string $target, string $link): Promise
{
return $this->invalidate([$target, $link], $this->driver->createSymlink($target, $link));
}
public function createHardlink(string $target, string $link): Promise
{
return $this->invalidate([$target, $link], $this->driver->createHardlink($target, $link));
}
public function resolveSymlink(string $target): Promise
{
return $this->driver->resolveSymlink($target);
}
public function move(string $from, string $to): Promise
{
return $this->invalidate([$from, $to], $this->driver->move($from, $to));
}
public function deleteFile(string $path): Promise
{
return $this->invalidate([$path], $this->driver->deleteFile($path));
}
public function createDirectory(string $path, int $mode = 0777): Promise
{
return $this->invalidate([$path], $this->driver->createDirectory($path, $mode));
}
public function createDirectoryRecursively(string $path, int $mode = 0777): Promise
{
return $this->invalidate([$path], $this->driver->createDirectoryRecursively($path, $mode));
}
public function deleteDirectory(string $path): Promise
{
return $this->invalidate([$path], $this->driver->deleteDirectory($path));
}
public function listFiles(string $path): Promise
{
return $this->driver->listFiles($path);
}
public function changePermissions(string $path, int $mode): Promise
{
return $this->invalidate([$path], $this->driver->changePermissions($path, $mode));
}
public function changeOwner(string $path, ?int $uid, ?int $gid): Promise
{
return $this->invalidate([$path], $this->driver->changeOwner($path, $uid, $gid));
}
public function touch(string $path, ?int $modificationTime, ?int $accessTime): Promise
{
return $this->invalidate([$path], $this->driver->touch($path, $modificationTime, $accessTime));
}
public function read(string $path): Promise
{
return $this->driver->read($path);
}
public function write(string $path, string $contents): Promise
{
return $this->invalidate([$path], $this->driver->write($path, $contents));
}
private function invalidate(array $paths, Promise $promise): Promise
{
foreach ($paths as $path) {
$this->statusCache->delete($path);
}
if (!$promise instanceof Success) {
$promise->onResolve(function () use ($paths) {
foreach ($paths as $path) {
$this->statusCache->delete($path);
}
});
}
return $promise;
}
}

View File

@ -0,0 +1,81 @@
<?php
namespace Amp\File\Driver;
use Amp\File\File;
use Amp\Promise;
final class StatusCachingFile implements File
{
/** @var File */
private $file;
/** @var callable */
private $invalidateCallback;
public function __construct(File $file, callable $invalidateCallback)
{
$this->file = $file;
$this->invalidateCallback = $invalidateCallback;
}
public function read(int $length = self::DEFAULT_READ_LENGTH): Promise
{
return $this->file->read($length);
}
public function write(string $data): Promise
{
return $this->invalidate($this->file->write($data));
}
public function end(string $data = ""): Promise
{
return $this->invalidate($this->file->end($data));
}
public function close(): Promise
{
return $this->file->close();
}
public function seek(int $position, int $whence = self::SEEK_SET): Promise
{
return $this->file->seek($position, $whence);
}
public function tell(): int
{
return $this->file->tell();
}
public function eof(): bool
{
return $this->file->eof();
}
public function getPath(): string
{
return $this->file->getPath();
}
public function getMode(): string
{
return $this->file->getMode();
}
public function truncate(int $size): Promise
{
return $this->invalidate($this->file->truncate($size));
}
private function invalidate(Promise $promise): Promise
{
$promise->onResolve(function () {
($this->invalidateCallback)();
});
return $promise;
}
}

View File

@ -65,10 +65,6 @@ final class UvDriver implements Driver
public function getStatus(string $path): Promise
{
if ($stat = StatCache::get($path)) {
return new Success($stat);
}
$deferred = new Deferred;
$this->poll->listen($deferred->promise());
@ -86,8 +82,6 @@ final class UvDriver implements Driver
unset($stat['link']);
}
StatCache::set($path, $stat);
$deferred->resolve($stat);
};
@ -183,7 +177,6 @@ final class UvDriver implements Driver
$this->poll->listen($deferred->promise());
\uv_fs_rename($this->loop, $from, $to, $this->createGenericCallback($deferred, "Could not rename file"));
StatCache::clearOn($deferred->promise(), $from);
return $deferred->promise();
}
@ -194,7 +187,6 @@ final class UvDriver implements Driver
$this->poll->listen($deferred->promise());
\uv_fs_unlink($this->loop, $path, $this->createGenericCallback($deferred, "Could not unlink file"));
StatCache::clearOn($deferred->promise(), $path);
return $deferred->promise();
}
@ -260,7 +252,6 @@ final class UvDriver implements Driver
$this->poll->listen($deferred->promise());
\uv_fs_rmdir($this->loop, $path, $this->createGenericCallback($deferred, "Could not remove directory"));
StatCache::clearOn($deferred->promise(), $path);
return $deferred->promise();
}
@ -303,7 +294,6 @@ final class UvDriver implements Driver
$callback = $this->createGenericCallback($deferred, "Could not change file permissions");
\uv_fs_chmod($this->loop, $path, $mode, $callback);
StatCache::clearOn($deferred->promise(), $path);
return $deferred->promise();
}
@ -316,7 +306,6 @@ final class UvDriver implements Driver
$callback = $this->createGenericCallback($deferred, "Could not change file owner");
\uv_fs_chown($this->loop, $path, $uid ?? -1, $gid ?? -1, $callback);
StatCache::clearOn($deferred->promise(), $path);
return $deferred->promise();
}
@ -331,7 +320,6 @@ final class UvDriver implements Driver
$callback = $this->createGenericCallback($deferred, "Could not touch file");
\uv_fs_utime($this->loop, $path, $modificationTime, $accessTime, $callback);
StatCache::clearOn($deferred->promise(), $path);
return $deferred->promise();
}
@ -398,7 +386,6 @@ final class UvDriver implements Driver
} else {
\uv_fs_fstat($this->loop, $fh, function ($fh, $stat) use ($openArr): void {
if (\is_resource($fh)) {
StatCache::set($openArr[1], $stat);
$this->finalizeHandle($fh, $stat["size"], $openArr);
} else {
[, $path, $deferred] = $openArr;

View File

@ -283,7 +283,6 @@ final class UvFile implements File
$deferred->fail(new StreamException("Writing to the file failed: " . $error));
}
} else {
StatCache::clear($this->path);
$this->position += $length;
if ($this->position > $this->size) {
$this->size = $this->position;
@ -312,7 +311,6 @@ final class UvFile implements File
$this->isActive = false;
}
StatCache::clear($this->path);
$this->size = $size;
$deferred->resolve();
};

123
src/Internal/Cache.php Normal file
View File

@ -0,0 +1,123 @@
<?php
namespace Amp\File\Internal;
use Amp\Loop;
use Amp\Struct;
/** @internal */
final class Cache
{
/** @var object */
private $sharedState;
/** @var string */
private $ttlWatcherId;
/** @var int|null */
private $maxSize;
/**
* @param int $gcInterval The frequency in milliseconds at which expired cache entries should be garbage
* collected.
* @param int|null $maxSize The maximum size of cache array (number of elements).
*/
public function __construct(int $gcInterval = 1000, int $maxSize = null)
{
// By using a shared state object we're able to use `__destruct()` for "normal" garbage collection of both this
// instance and the loop's watcher. Otherwise this object could only be GC'd when the TTL watcher was cancelled
// at the loop layer.
$this->sharedState = $sharedState = new class {
use Struct;
/** @var string[] */
public $cache = [];
/** @var int[] */
public $cacheTimeouts = [];
/** @var bool */
public $isSortNeeded = false;
public function collectGarbage(): void
{
$now = \time();
if ($this->isSortNeeded) {
\asort($this->cacheTimeouts);
$this->isSortNeeded = false;
}
foreach ($this->cacheTimeouts as $key => $expiry) {
if ($now <= $expiry) {
break;
}
unset(
$this->cache[$key],
$this->cacheTimeouts[$key]
);
}
}
};
$this->ttlWatcherId = Loop::repeat($gcInterval, [$sharedState, "collectGarbage"]);
$this->maxSize = $maxSize;
Loop::unreference($this->ttlWatcherId);
}
public function __destruct()
{
$this->sharedState->cache = [];
$this->sharedState->cacheTimeouts = [];
Loop::cancel($this->ttlWatcherId);
}
public function get(string $key)
{
if (!isset($this->sharedState->cache[$key])) {
return null;
}
if (isset($this->sharedState->cacheTimeouts[$key]) && \time() > $this->sharedState->cacheTimeouts[$key]) {
unset(
$this->sharedState->cache[$key],
$this->sharedState->cacheTimeouts[$key]
);
return null;
}
return $this->sharedState->cache[$key];
}
public function set(string $key, $value, int $ttl = null): void
{
if ($ttl === null) {
unset($this->sharedState->cacheTimeouts[$key]);
} elseif ($ttl >= 0) {
$expiry = \time() + $ttl;
$this->sharedState->cacheTimeouts[$key] = $expiry;
$this->sharedState->isSortNeeded = true;
} else {
throw new \Error("Invalid cache TTL ({$ttl}; integer >= 0 or null required");
}
unset($this->sharedState->cache[$key]);
if (\count($this->sharedState->cache) === $this->maxSize) {
\array_shift($this->sharedState->cache);
}
$this->sharedState->cache[$key] = $value;
}
public function delete(string $key): bool
{
$exists = isset($this->sharedState->cache[$key]);
unset(
$this->sharedState->cache[$key],
$this->sharedState->cacheTimeouts[$key]
);
return $exists;
}
}

View File

@ -125,8 +125,6 @@ final class FileTask implements Task
}
}
StatCache::clear();
switch ($this->operation) {
case "getStatus":
case "deleteFile":

View File

@ -1,100 +0,0 @@
<?php
namespace Amp\File;
use Amp\Loop;
use Amp\Promise;
use Throwable;
final class StatCache
{
private static $cache = [];
private static $timeouts = [];
private static $ttl = 3;
private static $now;
private static function init(): void
{
self::$now = \time();
$watcher = Loop::repeat(1000, function () {
self::$now = $now = \time();
foreach (self::$cache as $path => $expiry) {
if ($now > $expiry) {
unset(
self::$cache[$path],
self::$timeouts[$path]
);
} else {
break;
}
}
});
Loop::unreference($watcher);
Loop::setState(self::class, new class($watcher) {
private $watcher;
private $driver;
public function __construct(string $watcher)
{
$this->watcher = $watcher;
$this->driver = Loop::get();
}
public function __destruct()
{
$this->driver->cancel($this->watcher);
}
});
}
public static function get(string $path): ?array
{
return isset(self::$cache[$path]) ? self::$cache[$path] : null;
}
public static function set(string $path, array $stat): void
{
if (self::$ttl <= 0) {
return;
}
if (Loop::getState(self::class) === null) {
self::init();
}
self::$cache[$path] = $stat;
self::$timeouts[$path] = self::$now + self::$ttl;
}
public static function ttl(int $seconds): void
{
self::$ttl = $seconds;
}
public static function clear(?string $path = null): void
{
if (isset($path)) {
unset(
self::$cache[$path],
self::$timeouts[$path]
);
} else {
self::$cache = [];
self::$timeouts = [];
}
}
public static function clearOn(Promise $promise, ?string $path = null): void
{
$promise->onResolve(
function (?Throwable $exception) use ($path): void {
if ($exception === null) {
self::clear($path);
}
}
);
}
}

View File

@ -5,6 +5,7 @@ namespace Amp\File;
use Amp\File\Driver\BlockingDriver;
use Amp\File\Driver\EioDriver;
use Amp\File\Driver\ParallelDriver;
use Amp\File\Driver\StatusCachingDriver;
use Amp\File\Driver\UvDriver;
use Amp\Loop;
use Amp\Promise;
@ -25,7 +26,13 @@ function filesystem(?Driver $driver = null): Filesystem
return $filesystem;
}
$filesystem = new Filesystem(createDefaultDriver());
$defaultDriver = createDefaultDriver();
if (!\defined("AMP_WORKER")) { // Prevent caching in workers, cache in parent instead.
$defaultDriver = new StatusCachingDriver($defaultDriver);
}
$filesystem = new Filesystem($defaultDriver);
} else {
$filesystem = new Filesystem($driver);
}

View File

@ -0,0 +1,15 @@
<?php
namespace Amp\File\Test\Driver;
use Amp\File;
use Amp\File\Driver\BlockingDriver;
use Amp\File\Test\DriverTest;
class StatusCachingDriverTest extends DriverTest
{
protected function createDriver(): File\Driver
{
return new File\Driver\StatusCachingDriver(new BlockingDriver);
}
}

View File

@ -0,0 +1,15 @@
<?php
namespace Amp\File\Test\Driver;
use Amp\File;
use Amp\File\Driver\BlockingDriver;
use Amp\File\Test\FileTest;
class StatusCachingFileTest extends FileTest
{
protected function createDriver(): File\Driver
{
return new File\Driver\StatusCachingDriver(new BlockingDriver);
}
}

View File

@ -133,7 +133,6 @@ abstract class DriverTest extends FilesystemTest
$fixtureDir = Fixture::path();
$path = "{$fixtureDir}/file";
$stat = yield $this->driver->getStatus($path);
$this->assertNotNull(File\StatCache::get($path));
$this->assertIsArray($stat);
$this->assertSameStatus(\stat($path), $stat);
}
@ -172,7 +171,6 @@ abstract class DriverTest extends FilesystemTest
$path = "{$fixtureDir}/file";
$stat = (yield $this->driver->getStatus($path));
$size = $stat["size"];
File\StatCache::clear($path);
$this->assertSame($size, yield $this->driver->getSize($path));
}
@ -192,7 +190,6 @@ abstract class DriverTest extends FilesystemTest
$fixtureDir = Fixture::path();
$path = "{$fixtureDir}/dir";
$this->assertTrue(yield $this->driver->isDirectory($path));
File\StatCache::clear($path);
yield $this->driver->getSize($path);
}
@ -300,7 +297,6 @@ abstract class DriverTest extends FilesystemTest
$toUnlink = "{$fixtureDir}/unlink";
yield $this->driver->getStatus($toUnlink);
yield $this->driver->write($toUnlink, "unlink me");
$this->assertNull(File\StatCache::get($toUnlink));
$this->assertNull(yield $this->driver->deleteFile($toUnlink));
$this->assertNull(yield $this->driver->getStatus($toUnlink));
}
@ -338,7 +334,6 @@ abstract class DriverTest extends FilesystemTest
$stat = yield $this->driver->getStatus($dir);
$this->assertSame('0755', $this->getPermissionsFromStatus($stat));
$this->assertNull(yield $this->driver->deleteDirectory($dir));
$this->assertNull(File\StatCache::get($dir));
$this->assertNull(yield $this->driver->getStatus($dir));
// test for 0, because previous array_filter made that not work
@ -385,7 +380,6 @@ abstract class DriverTest extends FilesystemTest
$path = "{$fixtureDir}/file";
$stat = yield $this->driver->getStatus($path);
$statMtime = $stat["mtime"];
File\StatCache::clear($path);
$this->assertSame($statMtime, yield $this->driver->getModificationTime($path));
}
@ -405,7 +399,6 @@ abstract class DriverTest extends FilesystemTest
$path = "{$fixtureDir}/file";
$stat = yield $this->driver->getStatus($path);
$statAtime = $stat["atime"];
File\StatCache::clear($path);
$this->assertSame($statAtime, yield $this->driver->getAccessTime($path));
}
@ -425,7 +418,6 @@ abstract class DriverTest extends FilesystemTest
$path = "{$fixtureDir}/file";
$stat = yield $this->driver->getStatus($path);
$statCtime = $stat["ctime"];
File\StatCache::clear($path);
$this->assertSame($statCtime, yield $this->driver->getCreationTime($path));
}
@ -451,7 +443,6 @@ abstract class DriverTest extends FilesystemTest
$oldStat = yield $this->driver->getStatus($touch);
$this->assertNull(yield $this->driver->touch($touch, \time() + 10, \time() + 20));
$this->assertNull(File\StatCache::get($touch));
$newStat = yield $this->driver->getStatus($touch);
yield $this->driver->deleteFile($touch);
@ -477,7 +468,6 @@ abstract class DriverTest extends FilesystemTest
$stat = yield $this->driver->getStatus($path);
$this->assertNotSame('0777', \substr(\decoct($stat['mode']), -4));
$this->assertNull(yield $this->driver->changePermissions($path, 0777));
$this->assertNull(File\StatCache::get($path));
$stat = yield $this->driver->getStatus($path);
$this->assertSame('0777', \substr(\decoct($stat['mode']), -4));
}
@ -500,7 +490,6 @@ abstract class DriverTest extends FilesystemTest
yield $this->driver->getStatus($path);
$user = \fileowner($path);
$this->assertNull(yield $this->driver->changeOwner($path, $user, null));
$this->assertNull(File\StatCache::get($path));
}
public function testChangeOwnerFailsOnNonexistentPath(): \Generator

View File

@ -11,7 +11,6 @@ abstract class FilesystemTest extends AsyncTestCase
{
parent::setUp();
Fixture::init();
File\StatCache::clear();
}
protected function tearDown(): void