diff --git a/tests/GameCult.Caching.Tests/C2aGateWriter.cs b/tests/GameCult.Caching.Tests/C2aGateWriter.cs index a6e28283..9a018a3e 100644 --- a/tests/GameCult.Caching.Tests/C2aGateWriter.cs +++ b/tests/GameCult.Caching.Tests/C2aGateWriter.cs @@ -44,9 +44,14 @@ namespace GameCult.Caching.Tests .Concat(deck.Moves.Select(move => move.Id)) .ToArray(); + // The one instant every record is stamped with, so the bytes this writes are the same on every run and the committed vectors + // can be compared to them byte for byte. Regenerate the vectors with CULTLIB_GATE_OUT (see C2aStamps...) if C2a's format moves. + private static readonly DateTimeOffset StoredAt = new(2026, 10, 27, 0, 0, 0, TimeSpan.Zero); + private static void Write(string path, bool directory) { using var cache = CultCacheMessagePack.Create(path, new CultCacheOpenOptions { Registry = Registry, UseDirectoryStore = directory }); + cache.Clock = () => StoredAt; cache.Commit(batch => batch.Upsert(typeof(ElementIdTests.IdDeck), Deck(), new CultRecordKey("gate"))); } @@ -70,14 +75,13 @@ namespace GameCult.Caching.Tests throw new DirectoryNotFoundException($"tests/vectors/document-variants-c2a not found above {TestContext.CurrentContext.TestDirectory}."); } - private static void CopyTree(string from, string to) - { - Directory.CreateDirectory(to); - foreach (var file in Directory.GetFiles(from)) - File.Copy(file, Path.Combine(to, Path.GetFileName(file))); - foreach (var directory in Directory.GetDirectories(from)) - CopyTree(directory, Path.Combine(to, Path.GetFileName(directory))); - } + // Every file under root by relative path and bytes; a store's lock files hold no state and are not compared. + private static (string Path, byte[] Bytes)[] Tree(string root) => + Directory.GetFiles(root, "*", SearchOption.AllDirectories) + .Where(file => !file.EndsWith(".lock", StringComparison.Ordinal)) + .Select(file => (Path.GetRelativePath(root, file).Replace(Path.DirectorySeparatorChar, '/'), File.ReadAllBytes(file))) + .OrderBy(entry => entry.Item1, StringComparer.Ordinal) + .ToArray(); private string _temp = string.Empty; @@ -119,20 +123,18 @@ namespace GameCult.Caching.Tests public void TheCommittedVectorsAreWhatC2aWrites() { var live = Path.Combine(_temp, "live"); - Directory.CreateDirectory(live); + var liveDirectory = Path.Combine(live, DirectoryName); + Directory.CreateDirectory(liveDirectory); Write(Path.Combine(live, SingleName), directory: false); - Write(Path.Combine(live, ManifestName), directory: true); + Write(Path.Combine(liveDirectory, ManifestName), directory: true); - var committedSingle = Path.Combine(Vectors(), SingleName); - var committedManifest = Path.Combine(Vectors(), DirectoryName, ManifestName); - Assert.That(HeaderOf(committedSingle), Is.EqualTo(HeaderOf(Path.Combine(live, SingleName)))); - Assert.That(HeaderOf(committedManifest), Is.EqualTo(HeaderOf(Path.Combine(live, ManifestName)))); + Assert.That(File.ReadAllBytes(Path.Combine(Vectors(), SingleName)), Is.EqualTo(File.ReadAllBytes(Path.Combine(live, SingleName))), SingleName); - // The committed bytes are read through a copy: opening a store may take its lock beside it. - var copy = Path.Combine(_temp, "copy"); - CopyTree(Vectors(), copy); - Assert.That(ReadIds(Path.Combine(copy, SingleName)), Is.EqualTo(ReadIds(Path.Combine(live, SingleName)))); - Assert.That(ReadIds(Path.Combine(copy, DirectoryName, ManifestName)), Is.EqualTo(Ids)); + var committed = Tree(Path.Combine(Vectors(), DirectoryName)); + var written = Tree(liveDirectory); + Assert.That(committed.Select(entry => entry.Path), Is.EqualTo(written.Select(entry => entry.Path)), DirectoryName); + for (var i = 0; i < committed.Length; i++) + Assert.That(committed[i].Bytes, Is.EqualTo(written[i].Bytes), committed[i].Path); } } } diff --git a/tests/GameCult.Caching.Tests/RefListDriftTests.cs b/tests/GameCult.Caching.Tests/RefListDriftTests.cs index 8b863d41..9aa180ea 100644 --- a/tests/GameCult.Caching.Tests/RefListDriftTests.cs +++ b/tests/GameCult.Caching.Tests/RefListDriftTests.cs @@ -107,9 +107,10 @@ namespace GameCult.Caching.Tests } // The store the 1.0.60 writer wrote (tests/vectors/ref-list-drift/v1-0-60-written.cc, made by OldWriterRefListProbe), or the one - // old-reader-refusal.sh gate csharp names in CULTLIB_GATE_OLD_STORE. The line it prints is the one OldReaderRefListProbe prints - // at 2.0.1; the gate diffs them. A record is named by its schema name, not its id: a flush of an untouched record carries its - // persisted id forward at this tree and restamps it at 2.0.1, so the id after PushAll is not what "opens alike" is about. + // old-reader-refusal.sh gate csharp names in CULTLIB_GATE_OLD_STORE. It attaches the store, upserts alpha, beta and the list + // reversed, flushes, rereads the file and prints the REFLIST-SUMMARY line OldReaderRefListProbe prints at 2.0.1 after the same + // write; the gate diffs them, so the two must stay the same code. Ids are compared, not names: the write is what makes a record's + // schema id the one its type owns, and a record left under its persisted id is a type that cannot write it. [Test] public async Task A_1_0_60_Written_Store_Opens_Like_2_0_1() { @@ -118,20 +119,35 @@ namespace GameCult.Caching.Tests File.Copy(string.IsNullOrEmpty(source) ? VectorPath("v1-0-60-written.cc") : source, copy); try { - using var cache = new CultCache(); - var store = new SingleFileMessagePackBackingStore(copy); - cache.AddBackingStore(store); - await cache.PullAllBackingStoresAsync(); - var warnings = store.LastSchemaMigrationReports.SelectMany(report => report.Warnings).Select(warning => warning.Code).Distinct().OrderBy(code => code, StringComparer.Ordinal).ToArray(); - Assert.That(warnings, Does.Contain("gained_reference_semantics")); - var items = cache.GetAll().Single().Items.Select(item => item.Key.Value).ToArray(); - Assert.That(items, Is.EqualTo(new[] { "alpha", "beta" })); - Assert.That(cache.Get(cache.GetAll().Single().Items[0].Key)!.Name, Is.EqualTo("alpha")); + string[] warnings; + using (var cache = new CultCache()) + { + var store = new SingleFileMessagePackBackingStore(copy); + cache.AddBackingStore(store); + await cache.PullAllBackingStoresAsync(); + warnings = store.LastSchemaMigrationReports.SelectMany(report => report.Warnings).Select(warning => warning.Code).Distinct().OrderBy(code => code, StringComparer.Ordinal).ToArray(); + Assert.That(warnings, Does.Contain("gained_reference_semantics")); + var holder = cache.GetAll().Single(); + Assert.That(holder.Items.Select(item => item.Key.Value), Is.EqualTo(new[] { "alpha", "beta" })); + + // The write both runtimes make: the targets as they are, the holder with its list reversed. + foreach (var target in cache.GetAll().ToArray()) await cache.UpsertAsync(target, cache.TryGetHandle(target)); + holder.Items = holder.Items.AsEnumerable().Reverse().ToArray(); + await cache.UpsertAsync(holder, cache.TryGetHandle(holder)); + await cache.FlushAsync(); + } - store.PushAll(); var snapshot = CultDocumentMessagePackSerialization.DeserializeSnapshot(File.ReadAllBytes(copy)); - var records = snapshot.Records.OrderBy(record => record.Key, StringComparer.Ordinal).Select(record => record.Key + "=" + snapshot.SchemaCatalog.Where(entry => entry.SchemaId == record.SchemaId).Select(entry => entry.SchemaName).SingleOrDefault()).ToArray(); - TestContext.Out.WriteLine("REFLIST-SUMMARY " + System.Text.Json.JsonSerializer.Serialize(new { records, items, warnings, header = snapshot.FormatVersion })); + var records = snapshot.Records.OrderBy(record => record.Key, StringComparer.Ordinal).Select(record => record.Key + "=" + record.SchemaId).ToArray(); + var listId = snapshot.Records.Single(record => record.Key == "list").SchemaId; + var slot = snapshot.SchemaCatalog.Single(entry => entry.SchemaId == listId).Members.Single(member => member.MemberName == "Items"); + using var reopened = new CultCache(); + reopened.AddBackingStore(new SingleFileMessagePackBackingStore(copy)); + await reopened.PullAllBackingStoresAsync(); + var items = reopened.GetAll().Single().Items.Select(item => item.Key.Value).ToArray(); + Assert.That(items, Is.EqualTo(new[] { "beta", "alpha" })); + Assert.That(reopened.Get(reopened.GetAll().Single().Items[0].Key)!.Name, Is.EqualTo("beta")); + TestContext.Out.WriteLine("REFLIST-SUMMARY " + System.Text.Json.JsonSerializer.Serialize(new { records, items, itemsSlot = new { slot.IsReference, slot.TargetSchemaName }, warnings, header = snapshot.FormatVersion })); } finally { diff --git a/tests/vectors/document-variants-c2a/old-reader-probes/OldReaderRefListProbe.cs b/tests/vectors/document-variants-c2a/old-reader-probes/OldReaderRefListProbe.cs index f0a5d870..d544a377 100644 --- a/tests/vectors/document-variants-c2a/old-reader-probes/OldReaderRefListProbe.cs +++ b/tests/vectors/document-variants-c2a/old-reader-probes/OldReaderRefListProbe.cs @@ -7,6 +7,8 @@ using System.Threading.Tasks; using GameCult.Caching; using GameCult.Caching.MessagePack; using NUnit.Framework; +using RefListHolder = GameCult.Caching.Tests.RefListDriftTests.RefListHolder; +using RefListTarget = GameCult.Caching.Tests.RefListDriftTests.RefListTarget; namespace GameCult.Caching.Tests { @@ -16,6 +18,7 @@ namespace GameCult.Caching.Tests // types are 2.0.1's own RefListDriftTests.RefListTarget and RefListHolder. public class OldReaderRefListProbe { + // RefListDriftTests.A_1_0_60_Written_Store_Opens_Like_2_0_1 is this test's body at head; the two must stay the same code. [Test] public async Task A_1_0_60_Written_Store_Attaches_As_Compatible_Drift() { @@ -23,24 +26,39 @@ namespace GameCult.Caching.Tests File.Copy(Path.Combine(Environment.GetEnvironmentVariable("CULTLIB_GATE_OUT")!, "v1-0-60-ref-list.cc"), copy); try { - using var cache = new CultCache(); - var store = new SingleFileMessagePackBackingStore(copy); - cache.AddBackingStore(store); - await cache.PullAllBackingStoresAsync(); - var warnings = store.LastSchemaMigrationReports.SelectMany(report => report.Warnings).Select(warning => warning.Code).Distinct().OrderBy(code => code, StringComparer.Ordinal).ToArray(); - Assert.That(warnings, Does.Contain("gained_reference_semantics")); - var items = cache.GetAll().Single().Items.Select(item => item.Key.Value).ToArray(); - Assert.That(items, Is.EqualTo(new[] { "alpha", "beta" })); + string[] warnings; + using (var cache = new CultCache()) + { + var store = new SingleFileMessagePackBackingStore(copy); + cache.AddBackingStore(store); + await cache.PullAllBackingStoresAsync(); + warnings = store.LastSchemaMigrationReports.SelectMany(report => report.Warnings).Select(warning => warning.Code).Distinct().OrderBy(code => code, StringComparer.Ordinal).ToArray(); + Assert.That(warnings, Does.Contain("gained_reference_semantics")); + var holder = cache.GetAll().Single(); + Assert.That(holder.Items.Select(item => item.Key.Value), Is.EqualTo(new[] { "alpha", "beta" })); + + // The write both runtimes make: the targets as they are, the holder with its list reversed. + foreach (var target in cache.GetAll().ToArray()) await cache.UpsertAsync(target, cache.TryGetHandle(target)); + holder.Items = holder.Items.AsEnumerable().Reverse().ToArray(); + await cache.UpsertAsync(holder, cache.TryGetHandle(holder)); + await cache.FlushAsync(); + } - store.PushAll(); var snapshot = CultDocumentMessagePackSerialization.DeserializeSnapshot(File.ReadAllBytes(copy)); - var records = snapshot.Records.OrderBy(record => record.Key, StringComparer.Ordinal).Select(record => record.Key + "=" + snapshot.SchemaCatalog.Where(entry => entry.SchemaId == record.SchemaId).Select(entry => entry.SchemaName).SingleOrDefault()).ToArray(); - TestContext.Out.WriteLine("REFLIST-SUMMARY " + JsonSerializer.Serialize(new { records, items, warnings, header = snapshot.FormatVersion })); + var records = snapshot.Records.OrderBy(record => record.Key, StringComparer.Ordinal).Select(record => record.Key + "=" + record.SchemaId).ToArray(); + var listId = snapshot.Records.Single(record => record.Key == "list").SchemaId; + var slot = snapshot.SchemaCatalog.Single(entry => entry.SchemaId == listId).Members.Single(member => member.MemberName == "Items"); + using var reopened = new CultCache(); + reopened.AddBackingStore(new SingleFileMessagePackBackingStore(copy)); + await reopened.PullAllBackingStoresAsync(); + var items = reopened.GetAll().Single().Items.Select(item => item.Key.Value).ToArray(); + Assert.That(items, Is.EqualTo(new[] { "beta", "alpha" })); + Assert.That(reopened.Get(reopened.GetAll().Single().Items[0].Key)!.Name, Is.EqualTo("beta")); + TestContext.Out.WriteLine("REFLIST-SUMMARY " + System.Text.Json.JsonSerializer.Serialize(new { records, items, itemsSlot = new { slot.IsReference, slot.TargetSchemaName }, warnings, header = snapshot.FormatVersion })); } finally { File.Delete(copy); } - } } }