LibHac/tests/LibHac.Tests/Fs/DirectorySaveDataFileSystemTests.cs
2021-07-26 10:19:59 -07:00

436 lines
No EOL
16 KiB
C#

using System;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using LibHac.Common;
using LibHac.Fs;
using LibHac.Fs.Fsa;
using LibHac.FsSrv;
using LibHac.FsSystem;
using LibHac.Tests.Fs.IFileSystemTestBase;
using Xunit;
namespace LibHac.Tests.Fs
{
public class DirectorySaveDataFileSystemTests : CommittableIFileSystemTests
{
protected override IFileSystem CreateFileSystem()
{
return CreateFileSystemInternal().saveFs;
}
protected override IReopenableFileSystemCreator GetFileSystemCreator()
{
return new DirectorySaveDataFileSystemCreator();
}
private class DirectorySaveDataFileSystemCreator : IReopenableFileSystemCreator
{
private IFileSystem BaseFileSystem { get; }
public DirectorySaveDataFileSystemCreator()
{
BaseFileSystem = new InMemoryFileSystem();
}
public IFileSystem Create()
{
DirectorySaveDataFileSystem
.CreateNew(out DirectorySaveDataFileSystem saveFs, BaseFileSystem, true, true, true)
.ThrowIfFailure();
return saveFs;
}
}
private (IFileSystem baseFs, DirectorySaveDataFileSystem saveFs) CreateFileSystemInternal()
{
var baseFs = new InMemoryFileSystem();
DirectorySaveDataFileSystem.CreateNew(out DirectorySaveDataFileSystem saveFs, baseFs, true, true, true)
.ThrowIfFailure();
return (baseFs, saveFs);
}
[Fact]
public void CreateFile_CreatedInWorkingDirectory()
{
(IFileSystem baseFs, IFileSystem saveFs) = CreateFileSystemInternal();
saveFs.CreateFile("/file", 0, CreateFileOptions.None);
Assert.Success(baseFs.GetEntryType(out DirectoryEntryType type, "/1/file"));
Assert.Equal(DirectoryEntryType.File, type);
}
[Fact]
public void CreateFile_NotCreatedInCommittedDirectory()
{
(IFileSystem baseFs, IFileSystem saveFs) = CreateFileSystemInternal();
saveFs.CreateFile("/file", 0, CreateFileOptions.None);
Assert.Result(ResultFs.PathNotFound, baseFs.GetEntryType(out _, "/0/file"));
}
[Fact]
public void Commit_FileExistsInCommittedDirectory()
{
(IFileSystem baseFs, IFileSystem saveFs) = CreateFileSystemInternal();
saveFs.CreateFile("/file", 0, CreateFileOptions.None);
Assert.Success(saveFs.Commit());
Assert.Success(baseFs.GetEntryType(out DirectoryEntryType type, "/0/file"));
Assert.Equal(DirectoryEntryType.File, type);
}
[Fact]
public void Rollback_FileDoesNotExistInBaseAfterRollback()
{
(IFileSystem baseFs, IFileSystem saveFs) = CreateFileSystemInternal();
saveFs.CreateFile("/file", 0, CreateFileOptions.None);
// Rollback should succeed
Assert.Success(saveFs.Rollback());
// Make sure all the files are gone
Assert.Result(ResultFs.PathNotFound, saveFs.GetEntryType(out _, "/file"));
Assert.Result(ResultFs.PathNotFound, baseFs.GetEntryType(out _, "/0/file"));
Assert.Result(ResultFs.PathNotFound, baseFs.GetEntryType(out _, "/1/file"));
}
[Fact]
public void Rollback_DeletedFileIsRestoredInBaseAfterRollback()
{
(IFileSystem baseFs, IFileSystem saveFs) = CreateFileSystemInternal();
saveFs.CreateFile("/file", 0, CreateFileOptions.None);
saveFs.Commit();
saveFs.DeleteFile("/file");
// Rollback should succeed
Assert.Success(saveFs.Rollback());
// Make sure all the files are restored
Assert.Success(saveFs.GetEntryType(out _, "/file"));
Assert.Success(baseFs.GetEntryType(out _, "/0/file"));
Assert.Success(baseFs.GetEntryType(out _, "/1/file"));
}
[Fact]
public void Initialize_NormalState_UsesCommittedData()
{
var baseFs = new InMemoryFileSystem();
baseFs.CreateDirectory("/0").ThrowIfFailure();
baseFs.CreateDirectory("/1").ThrowIfFailure();
// Set the existing files before initializing the save FS
baseFs.CreateFile("/0/file1", 0, CreateFileOptions.None).ThrowIfFailure();
baseFs.CreateFile("/1/file2", 0, CreateFileOptions.None).ThrowIfFailure();
DirectorySaveDataFileSystem.CreateNew(out DirectorySaveDataFileSystem saveFs, baseFs, true, true, true)
.ThrowIfFailure();
Assert.Success(saveFs.GetEntryType(out _, "/file1"));
Assert.Result(ResultFs.PathNotFound, saveFs.GetEntryType(out _, "/file2"));
}
[Fact]
public void Initialize_InterruptedAfterCommitPart1_UsesWorkingData()
{
var baseFs = new InMemoryFileSystem();
baseFs.CreateDirectory("/_").ThrowIfFailure();
baseFs.CreateDirectory("/1").ThrowIfFailure();
// Set the existing files before initializing the save FS
baseFs.CreateFile("/_/file1", 0, CreateFileOptions.None).ThrowIfFailure();
baseFs.CreateFile("/1/file2", 0, CreateFileOptions.None).ThrowIfFailure();
DirectorySaveDataFileSystem.CreateNew(out DirectorySaveDataFileSystem saveFs, baseFs, true, true, true)
.ThrowIfFailure();
Assert.Result(ResultFs.PathNotFound, saveFs.GetEntryType(out _, "/file1"));
Assert.Success(saveFs.GetEntryType(out _, "/file2"));
}
[Fact]
public void Initialize_InterruptedDuringCommitPart2_UsesWorkingData()
{
var baseFs = new InMemoryFileSystem();
baseFs.CreateDirectory("/1").ThrowIfFailure();
// Set the existing files before initializing the save FS
baseFs.CreateFile("/1/file2", 0, CreateFileOptions.None).ThrowIfFailure();
DirectorySaveDataFileSystem.CreateNew(out DirectorySaveDataFileSystem saveFs, baseFs, true, true, true)
.ThrowIfFailure();
Assert.Result(ResultFs.PathNotFound, saveFs.GetEntryType(out _, "/file1"));
Assert.Success(saveFs.GetEntryType(out _, "/file2"));
}
[Fact]
public void Initialize_InitialExtraDataIsEmpty()
{
(IFileSystem _, DirectorySaveDataFileSystem saveFs) = CreateFileSystemInternal();
Assert.Success(saveFs.ReadExtraData(out SaveDataExtraData extraData));
Assert.True(SpanHelpers.AsByteSpan(ref extraData).IsZeros());
}
[Fact]
public void WriteExtraData_CanReadBackExtraData()
{
(IFileSystem _, DirectorySaveDataFileSystem saveFs) = CreateFileSystemInternal();
var originalExtraData = new SaveDataExtraData();
originalExtraData.DataSize = 0x12345;
Assert.Success(saveFs.WriteExtraData(in originalExtraData));
Assert.Success(saveFs.ReadExtraData(out SaveDataExtraData extraData));
Assert.Equal(originalExtraData, extraData);
}
[Fact]
public void Commit_AfterSuccessfulCommit_CanReadCommittedExtraData()
{
var baseFs = new InMemoryFileSystem();
DirectorySaveDataFileSystem.CreateNew(out DirectorySaveDataFileSystem saveFs, baseFs, true, true, true)
.ThrowIfFailure();
var originalExtraData = new SaveDataExtraData();
originalExtraData.DataSize = 0x12345;
saveFs.WriteExtraData(in originalExtraData).ThrowIfFailure();
Assert.Success(saveFs.CommitExtraData(false));
saveFs.Dispose();
DirectorySaveDataFileSystem.CreateNew(out saveFs, baseFs, true, true, true).ThrowIfFailure();
Assert.Success(saveFs.ReadExtraData(out SaveDataExtraData extraData));
Assert.Equal(originalExtraData, extraData);
}
[Fact]
public void Rollback_WriteExtraDataThenRollback_ExtraDataIsRolledBack()
{
var baseFs = new InMemoryFileSystem();
DirectorySaveDataFileSystem.CreateNew(out DirectorySaveDataFileSystem saveFs, baseFs, true, true, true)
.ThrowIfFailure();
var originalExtraData = new SaveDataExtraData();
originalExtraData.DataSize = 0x12345;
saveFs.WriteExtraData(in originalExtraData).ThrowIfFailure();
saveFs.CommitExtraData(false).ThrowIfFailure();
saveFs.Dispose();
DirectorySaveDataFileSystem.CreateNew(out saveFs, baseFs, true, true, true).ThrowIfFailure();
var newExtraData = new SaveDataExtraData();
newExtraData.DataSize = 0x67890;
saveFs.WriteExtraData(in newExtraData).ThrowIfFailure();
Assert.Success(saveFs.Rollback());
Assert.Success(saveFs.ReadExtraData(out SaveDataExtraData extraData));
Assert.Equal(originalExtraData, extraData);
}
[Fact]
public void Rollback_WriteExtraDataThenCloseFs_ExtraDataIsRolledBack()
{
var baseFs = new InMemoryFileSystem();
DirectorySaveDataFileSystem.CreateNew(out DirectorySaveDataFileSystem saveFs, baseFs, true, true, true)
.ThrowIfFailure();
// Write extra data and close with committing
var originalExtraData = new SaveDataExtraData();
originalExtraData.DataSize = 0x12345;
saveFs.WriteExtraData(in originalExtraData).ThrowIfFailure();
saveFs.CommitExtraData(false).ThrowIfFailure();
saveFs.Dispose();
DirectorySaveDataFileSystem.CreateNew(out saveFs, baseFs, true, true, true).ThrowIfFailure();
// Write a new extra data and close without committing
var newExtraData = new SaveDataExtraData();
newExtraData.DataSize = 0x67890;
saveFs.WriteExtraData(in newExtraData).ThrowIfFailure();
saveFs.Dispose();
// Read extra data should match the first one
DirectorySaveDataFileSystem.CreateNew(out saveFs, baseFs, true, true, true).ThrowIfFailure();
Assert.Success(saveFs.ReadExtraData(out SaveDataExtraData extraData));
Assert.Equal(originalExtraData, extraData);
}
[Fact]
public void Initialize_InterruptedAfterCommitPart1_UsesWorkingExtraData()
{
var baseFs = new InMemoryFileSystem();
CreateExtraDataForTest(baseFs, "/ExtraData_", 0x12345).ThrowIfFailure();
CreateExtraDataForTest(baseFs, "/ExtraData1", 0x67890).ThrowIfFailure();
DirectorySaveDataFileSystem.CreateNew(out DirectorySaveDataFileSystem saveFs, baseFs, true, true, true)
.ThrowIfFailure();
saveFs.ReadExtraData(out SaveDataExtraData extraData).ThrowIfFailure();
Assert.Equal(0x67890, extraData.DataSize);
}
[Fact]
public void CommitSaveData_MultipleCommits_CommitIdIsUpdatedSkippingInvalidIds()
{
var random = new RandomGenerator();
RandomDataGenerator randomGeneratorFunc = buffer => random.GenerateRandom(buffer);
var timeStampGetter = new TimeStampGetter();
var baseFs = new InMemoryFileSystem();
DirectorySaveDataFileSystem.CreateNew(out DirectorySaveDataFileSystem saveFs, baseFs, timeStampGetter,
randomGeneratorFunc, true, true, true, null).ThrowIfFailure();
saveFs.CommitExtraData(true).ThrowIfFailure();
saveFs.ReadExtraData(out SaveDataExtraData extraData).ThrowIfFailure();
Assert.Equal(2, extraData.CommitId);
saveFs.CommitExtraData(true).ThrowIfFailure();
saveFs.ReadExtraData(out extraData).ThrowIfFailure();
Assert.Equal(3, extraData.CommitId);
saveFs.CommitExtraData(true).ThrowIfFailure();
saveFs.ReadExtraData(out extraData).ThrowIfFailure();
Assert.Equal(6, extraData.CommitId);
saveFs.CommitExtraData(true).ThrowIfFailure();
saveFs.ReadExtraData(out extraData).ThrowIfFailure();
Assert.Equal(2, extraData.CommitId);
}
[Fact]
public void CommitSaveData_MultipleCommits_TimeStampUpdated()
{
var random = new RandomGenerator();
RandomDataGenerator randomGeneratorFunc = buffer => random.GenerateRandom(buffer);
var timeStampGetter = new TimeStampGetter();
var baseFs = new InMemoryFileSystem();
DirectorySaveDataFileSystem.CreateNew(out DirectorySaveDataFileSystem saveFs, baseFs, timeStampGetter,
randomGeneratorFunc, true, true, true, null).ThrowIfFailure();
saveFs.CommitExtraData(true).ThrowIfFailure();
saveFs.ReadExtraData(out SaveDataExtraData extraData).ThrowIfFailure();
Assert.Equal(1u, extraData.TimeStamp);
saveFs.CommitExtraData(true).ThrowIfFailure();
saveFs.ReadExtraData(out extraData).ThrowIfFailure();
Assert.Equal(2u, extraData.TimeStamp);
saveFs.CommitExtraData(true).ThrowIfFailure();
saveFs.ReadExtraData(out extraData).ThrowIfFailure();
Assert.Equal(3u, extraData.TimeStamp);
saveFs.CommitExtraData(true).ThrowIfFailure();
saveFs.ReadExtraData(out extraData).ThrowIfFailure();
Assert.Equal(4u, extraData.TimeStamp);
}
[Fact]
public void CommitSaveData_UpdateTimeStampIsFalse_TimeStampAndCommitIdAreNotUpdated()
{
var random = new RandomGenerator();
RandomDataGenerator randomGeneratorFunc = buffer => random.GenerateRandom(buffer);
var timeStampGetter = new TimeStampGetter();
var baseFs = new InMemoryFileSystem();
DirectorySaveDataFileSystem.CreateNew(out DirectorySaveDataFileSystem saveFs, baseFs, timeStampGetter,
randomGeneratorFunc, true, true, true, null).ThrowIfFailure();
saveFs.CommitExtraData(true).ThrowIfFailure();
saveFs.ReadExtraData(out SaveDataExtraData extraData).ThrowIfFailure();
Assert.Equal(1u, extraData.TimeStamp);
Assert.Equal(2, extraData.CommitId);
saveFs.CommitExtraData(false).ThrowIfFailure();
saveFs.ReadExtraData(out extraData).ThrowIfFailure();
Assert.Equal(1u, extraData.TimeStamp);
Assert.Equal(2, extraData.CommitId);
saveFs.CommitExtraData(true).ThrowIfFailure();
saveFs.ReadExtraData(out extraData).ThrowIfFailure();
Assert.Equal(2u, extraData.TimeStamp);
Assert.Equal(3, extraData.CommitId);
saveFs.CommitExtraData(false).ThrowIfFailure();
saveFs.ReadExtraData(out extraData).ThrowIfFailure();
Assert.Equal(2u, extraData.TimeStamp);
Assert.Equal(3, extraData.CommitId);
}
private class TimeStampGetter : ISaveDataCommitTimeStampGetter
{
private long _currentTimeStamp = 1;
public Result Get(out long timeStamp)
{
timeStamp = _currentTimeStamp++;
return Result.Success;
}
}
private class RandomGenerator
{
private static readonly int[] Values = { 2, 0, 3, 3, 6, 0 };
private int _index;
public Result GenerateRandom(Span<byte> output)
{
if (output.Length != 8)
throw new ArgumentException();
Unsafe.As<byte, long>(ref MemoryMarshal.GetReference(output)) = Values[_index];
_index = (_index + 1) % Values.Length;
return Result.Success;
}
}
private Result CreateExtraDataForTest(IFileSystem fileSystem, string path, int saveDataSize)
{
fileSystem.DeleteFile(path).IgnoreResult();
Result rc = fileSystem.CreateFile(path, Unsafe.SizeOf<SaveDataExtraData>());
if (rc.IsFailure()) return rc;
var extraData = new SaveDataExtraData();
extraData.DataSize = saveDataSize;
rc = fileSystem.OpenFile(out IFile file, path, OpenMode.ReadWrite);
if (rc.IsFailure()) return rc;
using (file)
{
rc = file.Write(0, SpanHelpers.AsByteSpan(ref extraData), WriteOption.Flush);
if (rc.IsFailure()) return rc;
}
return Result.Success;
}
}
}