using System; using System.Buffers.Binary; using LibHac.Common; using LibHac.Fs; using LibHac.FsSystem; using Xunit; namespace LibHac.Tests.FsSystem; public class ReadOnlyBlockCacheStorageTests { private class TestContext { private int _blockSize; private int _cacheBlockCount; public byte[] BaseData; public byte[] ModifiedBaseData; public byte[] CacheBuffer; public ReadOnlyBlockCacheStorage CacheStorage; public TestContext(int blockSize, int cacheBlockCount, int storageBlockCount, ulong rngSeed) { _blockSize = blockSize; _cacheBlockCount = cacheBlockCount; BaseData = new byte[_blockSize * storageBlockCount]; ModifiedBaseData = new byte[_blockSize * storageBlockCount]; CacheBuffer = new byte[_blockSize * _cacheBlockCount]; new Random(rngSeed).NextBytes(BaseData); BaseData.AsSpan().CopyTo(ModifiedBaseData); for (int i = 0; i < storageBlockCount; i++) { ModifyBlock(GetModifiedBaseDataBlock(i)); } using var baseStorage = new SharedRef(new MemoryStorage(BaseData)); CacheStorage = new ReadOnlyBlockCacheStorage(ref baseStorage.Ref(), _blockSize, CacheBuffer, _cacheBlockCount); } public Span GetBaseDataBlock(int index) => BaseData.AsSpan(_blockSize * index, _blockSize); public Span GetModifiedBaseDataBlock(int index) => ModifiedBaseData.AsSpan(_blockSize * index, _blockSize); public Span GetCacheDataBlock(int index) => CacheBuffer.AsSpan(_blockSize * index, _blockSize); private void ModifyBlock(Span block) { BinaryPrimitives.WriteUInt64LittleEndian(block, ulong.MaxValue); } public void ModifyAllCacheBlocks() { for (int i = 0; i < _cacheBlockCount; i++) { ModifyBlock(GetCacheDataBlock(i)); } } public Span ReadCachedStorage(int blockIndex) { byte[] buffer = new byte[_blockSize]; Assert.Success(CacheStorage.Read(_blockSize * blockIndex, buffer)); return buffer; } public Span ReadCachedStorage(long offset, int size) { byte[] buffer = new byte[size]; Assert.Success(CacheStorage.Read(offset, buffer)); return buffer; } public void InvalidateCache() { Assert.Success(CacheStorage.OperateRange(OperationId.InvalidateCache, 0, long.MaxValue)); } } private const int BlockSize = 0x4000; private const int CacheBlockCount = 4; private const int StorageBlockCount = 16; [Fact] public void Read_CompleteBlocks_ReadsCorrectData() { var context = new TestContext(BlockSize, CacheBlockCount, StorageBlockCount, 21341); for (int i = 0; i < StorageBlockCount; i++) { Assert.True(context.GetBaseDataBlock(i).SequenceEqual(context.ReadCachedStorage(i))); Assert.True(context.GetBaseDataBlock(i).SequenceEqual(context.ReadCachedStorage(i))); } } [Fact] public void Read_PreviouslyCachedBlock_ReturnsDataFromCache() { const int index = 4; var context = new TestContext(BlockSize, CacheBlockCount, StorageBlockCount, 21341); // Cache the block context.ReadCachedStorage(index); // Directly modify the cache buffer context.ModifyAllCacheBlocks(); // Next read should return the modified data from the cache buffer Assert.True(context.GetModifiedBaseDataBlock(index).SequenceEqual(context.ReadCachedStorage(index))); } [Fact] public void Read_BlockEvictedFromCache_ReturnsDataFromBaseStorage() { const int index = 4; var context = new TestContext(BlockSize, CacheBlockCount, StorageBlockCount, 21341); context.ReadCachedStorage(index); context.ModifyAllCacheBlocks(); // Read enough additional blocks to push the initial block out of the cache context.ReadCachedStorage(6); context.ReadCachedStorage(7); context.ReadCachedStorage(8); context.ReadCachedStorage(9); // Reading the initial block should now return the original data Assert.True(context.GetBaseDataBlock(index).SequenceEqual(context.ReadCachedStorage(index))); } [Fact] public void Read_ReadMultipleBlocks_BlocksAreEvictedAtTheRightTime() { const int index = 4; var context = new TestContext(BlockSize, CacheBlockCount, StorageBlockCount, 21341); context.ReadCachedStorage(index); context.ModifyAllCacheBlocks(); context.ReadCachedStorage(6); context.ReadCachedStorage(7); context.ReadCachedStorage(8); // Reading the initial block should return the cached data Assert.True(context.GetModifiedBaseDataBlock(index).SequenceEqual(context.ReadCachedStorage(index))); for (int i = 0; i < 3; i++) context.ReadCachedStorage(9 + i); context.ModifyAllCacheBlocks(); // The initial block should have been moved to the top of the cache when it was last accessed Assert.True(context.GetModifiedBaseDataBlock(index).SequenceEqual(context.ReadCachedStorage(index))); // Access all the other blocks in the cache so the initial block is the least recently accessed for (int i = 0; i < 3; i++) Assert.True(context.GetModifiedBaseDataBlock(9 + i).SequenceEqual(context.ReadCachedStorage(9 + i))); // Add a new block to the cache Assert.True(context.GetBaseDataBlock(2).SequenceEqual(context.ReadCachedStorage(2))); // The initial block should have been removed from the cache Assert.True(context.GetBaseDataBlock(index).SequenceEqual(context.ReadCachedStorage(index))); } [Fact] public void Read_UnalignedBlock_ReturnsOriginalData() { const int index = 4; var context = new TestContext(BlockSize, CacheBlockCount, StorageBlockCount, 21341); context.ReadCachedStorage(index); context.ModifyAllCacheBlocks(); // Read two blocks at once int offset = index * BlockSize; int size = BlockSize * 2; // The cache should be bypassed, returning the original data Assert.True(context.BaseData.AsSpan(offset, size).SequenceEqual(context.ReadCachedStorage(offset, size))); } [Fact] public void OperateRange_InvalidateCache_PreviouslyCachedBlockReturnsDataFromBaseStorage() { const int index = 4; var context = new TestContext(BlockSize, CacheBlockCount, StorageBlockCount, 21341); context.ReadCachedStorage(index); context.ModifyAllCacheBlocks(); // Next read should return the modified data from the cache buffer Assert.True(context.GetModifiedBaseDataBlock(index).SequenceEqual(context.ReadCachedStorage(index))); // Reading after invalidating the cache should return the original data context.InvalidateCache(); Assert.True(context.GetBaseDataBlock(index).SequenceEqual(context.ReadCachedStorage(index))); } }