diff --git a/tests/GameCult.Caching.Tests/CachelessSingleFileTests.cs b/tests/GameCult.Caching.Tests/CachelessSingleFileTests.cs index bc30b718..a1d0125e 100644 --- a/tests/GameCult.Caching.Tests/CachelessSingleFileTests.cs +++ b/tests/GameCult.Caching.Tests/CachelessSingleFileTests.cs @@ -1,8 +1,10 @@ #nullable enable using System; using System.IO; +using System.Threading.Tasks; using GameCult.Caching.MessagePack; using NUnit.Framework; +using static GameCult.Caching.Tests.ElementIdTests; namespace GameCult.Caching.Tests { @@ -92,5 +94,59 @@ namespace GameCult.Caching.Tests Assert.That(new SingleFileMessagePackBackingStore(_path).ReadStoreSnapshot()!.FormatVersion, Is.EqualTo(CultPersistedStoreSnapshot.FormatV3)); } + + [Test] + public void WriteRecordRefusesNullArguments() + { + var store = new SingleFileMessagePackBackingStore(_path); + + Assert.Throws(() => store.WriteRecord(null!, One)); + Assert.Throws(() => store.WriteRecord(Record("a", One, 1), null!)); + Assert.That(File.Exists(_path), Is.False); + } + + // The write is under the store's lock and waits for it: a write that finds the lock held lands after it is released, and + // never without it. + [Test] + public async Task WriteRecordWaitsForTheStoreLock() + { + var write = default(Task); + using (new FileStream(_path + ".lock", FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.None)) + { + write = Task.Run(() => new SingleFileMessagePackBackingStore(_path).WriteRecord(Record("a", One, 1), One)); + await Task.Delay(500); + Assert.That(write.IsCompleted, Is.False, "the write waits while another holder has the lock"); + Assert.That(File.Exists(_path), Is.False); + } + + await write; + + Assert.That(new SingleFileMessagePackBackingStore(_path).ReadStoreSnapshot()!.Records, Has.Length.EqualTo(1)); + } + + // The registry the payload was encoded with lets the write decide the header for the records it copies: another runtime may + // have written ids under an unmarked header, and a write that can read them marks the store. Without a registry it decides + // nothing for them. + [TestCase(true, CultPersistedStoreSnapshot.FormatV3)] + [TestCase(false, CultPersistedStoreSnapshot.FormatV1)] + public async Task WriteRecordGivenARegistryMarksAStoreWhoseCopiedRecordHoldsIds(bool withRegistry, string expected) + { + var registry = CultDocumentRegistry.ForTypes(new[] { typeof(IdDeck) }); + using (var cache = new CultCache(registry)) + { + cache.AddBackingStore(new SingleFileMessagePackBackingStore(_path)); + await cache.AddAsync(new IdDeck { Name = "deck", Reels = { new IdReel { Label = "reel" } } }); + cache.FlushAllBackingStores(); + } + + var written = CultDocumentMessagePackSerialization.DeserializeSnapshot(File.ReadAllBytes(_path)); + Assert.That(written.FormatVersion, Is.EqualTo(CultPersistedStoreSnapshot.FormatV3), "the cache marked the store it wrote ids into"); + written.FormatVersion = CultPersistedStoreSnapshot.FormatV1; + File.WriteAllBytes(_path, CultDocumentMessagePackSerialization.SerializeSnapshot(written)); + + new SingleFileMessagePackBackingStore(_path).WriteRecord(Record("other", One, 1), One, holdsIds: false, registry: withRegistry ? registry : null); + + Assert.That(new SingleFileMessagePackBackingStore(_path).ReadStoreSnapshot()!.FormatVersion, Is.EqualTo(expected)); + } } }