using Amphora; using System; using System.Collections.Generic; using System.IO; using UnitTestSharp; namespace Glass.UnitTests { public class KeyValueFileSystemTests : TestFixture { public class FullStore : InMemoryKeyValueStore, IKeyValueStore { public bool Full; public int SetsBeforeFull = int.MaxValue; public int RemovesBeforeFull = int.MaxValue; void IKeyValueStore.Set(string key, string value) { if (Full || SetsBeforeFull-- <= 0) { throw new IOException("The store is full."); } Set(key, value); } void IKeyValueStore.Remove(string key) { if (Full || RemovesBeforeFull-- <= 0) { throw new IOException("The store is full."); } Remove(key); } } public static byte[] Bytes(KeyValueFileSystem fileSystem, string path) { return InMemoryFileSystemTests.ReadAll(fileSystem.OpenRead(path).Value); } public static void Write(IFileSystem fileSystem, string path, params byte[] contents) { InMemoryFileSystemTests.WriteAll(fileSystem.OpenWrite(path).Value, contents); } public class CtorTests : TestFixture { public void EmptyStore_GivesAnEmptyFileSystem() { CheckEqual(0, new KeyValueFileSystem(new InMemoryKeyValueStore()).List("/").Value.Count); } public void NullStore_Throws() { CheckThrow(typeof(ArgumentNullException)); new KeyValueFileSystem(null); } public void ReadsBackFilesWithTheirBytesAndTimes() { var store = new InMemoryKeyValueStore(); var first = new KeyValueFileSystem(store); Write(first, "/a.bin", 1, 2, 3); var second = new KeyValueFileSystem(store); CheckEqual(new byte[] { 1, 2, 3 }, Bytes(second, "/a.bin")); CheckEqual(first.List("/").Value[0].Modified, second.List("/").Value[0].Modified); } public void ReadsBackDirectoriesIncludingEmptyOnes() { var store = new InMemoryKeyValueStore(); var first = new KeyValueFileSystem(store); first.CreateDirectory("/empty"); first.CreateDirectory("/full"); Write(first, "/full/a.bin", 1); var second = new KeyValueFileSystem(store); CheckEqual(FileKind.Directory, second.GetKind("/empty")); CheckEqual(FileKind.File, second.GetKind("/full/a.bin")); } public void ReadsBackAnEmptyFile() { var store = new InMemoryKeyValueStore(); Write(new KeyValueFileSystem(store), "/a.bin"); CheckEqual(0, Bytes(new KeyValueFileSystem(store), "/a.bin").Length); } public void FileWithNoTime_ReadsBackWithNone() { var store = new InMemoryKeyValueStore(); store.Set("file:/a.bin", ";AQID"); var fileSystem = new KeyValueFileSystem(store); CheckNull(fileSystem.List("/").Value[0].Modified); CheckEqual(new byte[] { 1, 2, 3 }, Bytes(fileSystem, "/a.bin")); } public void KeysOfOtherKinds_AreIgnored() { var store = new InMemoryKeyValueStore(); store.Set("theme", "dark"); var fileSystem = new KeyValueFileSystem(store); CheckEqual(0, fileSystem.List("/").Value.Count); CheckEqual(0, fileSystem.UnreadableKeys.Count); } public void DamagedEntries_AreSkippedAndReported() { var store = new InMemoryKeyValueStore(); store.Set("file:/noseparator", "AQID"); store.Set("file:/badtime", "x;AQID"); store.Set("file:/badbase64", ";***"); store.Set("file:relative", ";AQID"); store.Set("dir:/a//b", ""); store.Set("file:/good.bin", ";AQID"); var fileSystem = new KeyValueFileSystem(store); CheckEqual(1, fileSystem.List("/").Value.Count); CheckEqual(5, fileSystem.UnreadableKeys.Count); CheckEqual(6, store.GetKeys().Count); } public void FileAndDirectoryWithTheSameName_KeepsOneAndReportsTheOther() { var store = new InMemoryKeyValueStore(); store.Set("dir:/a", ""); store.Set("file:/a", ";AQID"); var fileSystem = new KeyValueFileSystem(store); CheckEqual(FileKind.Directory, fileSystem.GetKind("/a")); CheckEqual(1, fileSystem.UnreadableKeys.Count); CheckEqual("file:/a", fileSystem.UnreadableKeys[0]); } public void LoadingWritesNothingBack() { var store = new InMemoryKeyValueStore(); store.Set("file:/a.bin", ";AQID"); new KeyValueFileSystem(store); CheckEqual(";AQID", store.Get("file:/a.bin")); } } public class OpenWriteTests : TestFixture { public void DisposingStoresTheFile() { var store = new InMemoryKeyValueStore(); Write(new KeyValueFileSystem(store), "/a.bin", 1, 2, 3); CheckNotNull(store.Get("file:/a.bin")); } public void BeforeDisposing_StoresNothing() { var store = new InMemoryKeyValueStore(); var fileSystem = new KeyValueFileSystem(store); using (Stream stream = fileSystem.OpenWrite("/a.bin").Value) { stream.WriteByte(1); CheckNull(store.Get("file:/a.bin")); } } public void RewritingAFile_ReplacesItsStoredBytes() { var store = new InMemoryKeyValueStore(); var fileSystem = new KeyValueFileSystem(store); Write(fileSystem, "/a.bin", 1, 2, 3); Write(fileSystem, "/a.bin", 9); CheckEqual(new byte[] { 9 }, Bytes(new KeyValueFileSystem(store), "/a.bin")); } public void RewritingBeforeDisposing_KeepsTheOldBytesInTheStore() { var store = new InMemoryKeyValueStore(); var fileSystem = new KeyValueFileSystem(store); Write(fileSystem, "/a.bin", 1, 2, 3); using (fileSystem.OpenWrite("/a.bin").Value) { CheckEqual(new byte[] { 1, 2, 3 }, Bytes(new KeyValueFileSystem(store), "/a.bin")); } } public void FileDeletedWhileOpen_IsNotStored() { var store = new InMemoryKeyValueStore(); var fileSystem = new KeyValueFileSystem(store); Stream stream = fileSystem.OpenWrite("/a.bin").Value; fileSystem.Delete("/a.bin"); stream.Dispose(); CheckNull(store.Get("file:/a.bin")); } public void StoreFull_ThrowsFromDisposeAndKeepsTheFileInMemory() { var store = new FullStore(); var fileSystem = new KeyValueFileSystem(store); Stream stream = fileSystem.OpenWrite("/a.bin").Value; stream.WriteByte(7); store.Full = true; CheckThrow(typeof(IOException)); try { stream.Dispose(); } finally { CheckEqual(new byte[] { 7 }, Bytes(fileSystem, "/a.bin")); } } } public class CreateDirectoryTests : TestFixture { public void StoresTheDirectory() { var store = new InMemoryKeyValueStore(); new KeyValueFileSystem(store).CreateDirectory("/new"); CheckEqual("", store.Get("dir:/new")); } public void Failure_StoresNothing() { var store = new InMemoryKeyValueStore(); new KeyValueFileSystem(store).CreateDirectory("/missing/new"); CheckEqual(0, store.GetKeys().Count); } public void StoreFull_FailsAsIoError() { var store = new FullStore { Full = true }; var fileSystem = new KeyValueFileSystem(store); CheckEqual(FileSystemFailures.IoError.Fail("/new"), fileSystem.CreateDirectory("/new")); } } public class DeleteTests : TestFixture { public void File_ForgetsIt() { var store = new InMemoryKeyValueStore(); var fileSystem = new KeyValueFileSystem(store); Write(fileSystem, "/a.bin", 1); CheckEqual(Status.Ok, fileSystem.Delete("/a.bin")); CheckEqual(0, store.GetKeys().Count); } public void Directory_ForgetsIt() { var store = new InMemoryKeyValueStore(); var fileSystem = new KeyValueFileSystem(store); fileSystem.CreateDirectory("/new"); CheckEqual(Status.Ok, fileSystem.Delete("/new")); CheckEqual(0, store.GetKeys().Count); } public void Failure_KeepsTheStore() { var store = new InMemoryKeyValueStore(); var fileSystem = new KeyValueFileSystem(store); fileSystem.CreateDirectory("/dir"); Write(fileSystem, "/dir/a.bin", 1); CheckEqual(FileSystemFailures.DirectoryNotEmpty.Fail("/dir"), fileSystem.Delete("/dir")); CheckEqual(2, store.GetKeys().Count); } public void StoreFull_FailsAsIoError() { var store = new FullStore(); var fileSystem = new KeyValueFileSystem(store); fileSystem.CreateDirectory("/new"); store.Full = true; CheckEqual(FileSystemFailures.IoError.Fail("/new"), fileSystem.Delete("/new")); } } public class RenameTests : TestFixture { [IgnoreTest] private static KeyValueFileSystem Tree(IKeyValueStore store) { var fileSystem = new KeyValueFileSystem(store); fileSystem.CreateDirectory("/dir"); fileSystem.CreateDirectory("/dir/sub"); Write(fileSystem, "/dir/a.bin", 1); Write(fileSystem, "/dir/sub/b.bin", 2); Write(fileSystem, "/dir2.bin", 3); return fileSystem; } [IgnoreTest] private static List SortedKeys(IKeyValueStore store) { var returnMe = new List(store.GetKeys()); returnMe.Sort(string.CompareOrdinal); return returnMe; } public void File_MovesItsKey() { var store = new InMemoryKeyValueStore(); KeyValueFileSystem fileSystem = Tree(store); CheckEqual(Status.Ok, fileSystem.Rename("/dir2.bin", "other.bin")); CheckEqual(new byte[] { 3 }, Bytes(new KeyValueFileSystem(store), "/other.bin")); CheckNull(store.Get("file:/dir2.bin")); } public void Directory_MovesEveryKeyBeneathIt() { var store = new InMemoryKeyValueStore(); KeyValueFileSystem fileSystem = Tree(store); CheckEqual(Status.Ok, fileSystem.Rename("/dir", "folder")); CheckEqual( new List { "dir:/folder", "dir:/folder/sub", "file:/dir2.bin", "file:/folder/a.bin", "file:/folder/sub/b.bin", }, SortedKeys(store)); } public void Directory_LeavesSiblingsThatShareItsPrefixAlone() { var store = new InMemoryKeyValueStore(); KeyValueFileSystem fileSystem = Tree(store); fileSystem.CreateDirectory("/dir3"); fileSystem.Rename("/dir", "folder"); CheckEqual(FileKind.Directory, new KeyValueFileSystem(store).GetKind("/dir3")); CheckEqual(FileKind.File, new KeyValueFileSystem(store).GetKind("/dir2.bin")); } public void Directory_ComesBackRenamedAfterReloading() { var store = new InMemoryKeyValueStore(); Tree(store).Rename("/dir", "folder"); var reloaded = new KeyValueFileSystem(store); CheckEqual(new byte[] { 2 }, Bytes(reloaded, "/folder/sub/b.bin")); CheckEqual(FileKind.None, reloaded.GetKind("/dir")); } public void Failure_LeavesTheStoreAndTheTreeAsTheyWere() { var store = new FullStore(); KeyValueFileSystem fileSystem = Tree(store); List before = SortedKeys(store); store.Full = true; CheckEqual(FileSystemFailures.IoError.Fail("/dir"), fileSystem.Rename("/dir", "folder")); CheckEqual(before, SortedKeys(store)); CheckEqual(FileKind.Directory, fileSystem.GetKind("/dir")); CheckEqual(FileKind.None, fileSystem.GetKind("/folder")); } public void FailureHalfwayThrough_TakesBackTheKeysAlreadyMoved() { var store = new FullStore(); KeyValueFileSystem fileSystem = Tree(store); List before = SortedKeys(store); store.SetsBeforeFull = 2; CheckEqual(FileSystemFailures.IoError.Fail("/dir"), fileSystem.Rename("/dir", "folder")); CheckEqual(before, SortedKeys(store)); } } public class TrashTests : TestFixture { [IgnoreTest] private static KeyValueFileSystem Tree(IKeyValueStore store) { var fileSystem = new KeyValueFileSystem(store); fileSystem.CreateDirectory("/dir"); fileSystem.CreateDirectory("/dir/sub"); Write(fileSystem, "/dir/a.bin", 1); Write(fileSystem, "/dir/sub/b.bin", 2); Write(fileSystem, "/top.bin", 3); return fileSystem; } [IgnoreTest] private static List SortedKeys(IKeyValueStore store) { var returnMe = new List(store.GetKeys()); returnMe.Sort(string.CompareOrdinal); return returnMe; } public void File_LeavesTheStore() { var store = new InMemoryKeyValueStore(); KeyValueFileSystem fileSystem = Tree(store); fileSystem.Trash("/top.bin"); CheckNull(store.Get("file:/top.bin")); } public void Directory_LeavesTheStoreWithEverythingInside() { var store = new InMemoryKeyValueStore(); KeyValueFileSystem fileSystem = Tree(store); fileSystem.Trash("/dir"); CheckEqual(new List { "file:/top.bin" }, SortedKeys(store)); } public void Directory_LeavesSiblingsThatShareItsPrefixAlone() { var store = new InMemoryKeyValueStore(); KeyValueFileSystem fileSystem = Tree(store); Write(fileSystem, "/dir2.bin", 4); fileSystem.Trash("/dir"); CheckEqual(new List { "file:/dir2.bin", "file:/top.bin" }, SortedKeys(store)); } public void Restoring_PutsEveryKeyBack() { var store = new InMemoryKeyValueStore(); KeyValueFileSystem fileSystem = Tree(store); List before = SortedKeys(store); TrashedItem item = fileSystem.Trash("/dir").Value; CheckEqual(Status.Ok, fileSystem.Restore(item)); CheckEqual(before, SortedKeys(store)); CheckEqual(new byte[] { 2 }, Bytes(new KeyValueFileSystem(store), "/dir/sub/b.bin")); } public void Purging_KeepsTheKeysGone() { var store = new InMemoryKeyValueStore(); KeyValueFileSystem fileSystem = Tree(store); TrashedItem item = fileSystem.Trash("/dir").Value; CheckEqual(Status.Ok, fileSystem.Purge(item)); CheckEqual(new List { "file:/top.bin" }, SortedKeys(store)); CheckEqual(FileSystemFailures.NotFound.Fail("/dir"), fileSystem.Restore(item)); } public void StoreFull_FailsAndLeavesTheStoreAndTreeAsTheyWere() { var store = new FullStore(); KeyValueFileSystem fileSystem = Tree(store); List before = SortedKeys(store); store.Full = true; CheckEqual(FileSystemFailures.IoError.Fail("/dir"), fileSystem.Trash("/dir").Status); CheckEqual(before, SortedKeys(store)); CheckEqual(FileKind.Directory, fileSystem.GetKind("/dir")); } public void FailureHalfwayThrough_PutsBackTheKeysAlreadyRemoved() { var store = new FullStore(); KeyValueFileSystem fileSystem = Tree(store); List before = SortedKeys(store); store.RemovesBeforeFull = 2; CheckEqual(FileSystemFailures.IoError.Fail("/dir"), fileSystem.Trash("/dir").Status); CheckEqual(before, SortedKeys(store)); CheckEqual(FileKind.File, fileSystem.GetKind("/dir/sub/b.bin")); } public void RestoringIntoAFullStore_FailsAndCanBeRetried() { var store = new FullStore(); KeyValueFileSystem fileSystem = Tree(store); List before = SortedKeys(store); TrashedItem item = fileSystem.Trash("/dir").Value; store.SetsBeforeFull = 2; CheckEqual(FileSystemFailures.IoError.Fail("/dir"), fileSystem.Restore(item)); CheckEqual(FileKind.None, fileSystem.GetKind("/dir")); CheckEqual(new List { "file:/top.bin" }, SortedKeys(store)); store.SetsBeforeFull = int.MaxValue; CheckEqual(Status.Ok, fileSystem.Restore(item)); CheckEqual(before, SortedKeys(store)); } } public class AddFileTests : TestFixture { public void ChangesMemoryOnly() { var store = new InMemoryKeyValueStore(); var fileSystem = new KeyValueFileSystem(store); fileSystem.AddFile("/a.bin", 3); fileSystem.AddDirectory("/dir"); CheckEqual(2, fileSystem.List("/").Value.Count); CheckEqual(0, store.GetKeys().Count); } } } }